Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8b0684460 |
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## beta.061 — continuité musicale et montures colossales
|
||||
|
||||
- La musique Minecraft commence au dévoilement du portail, après les lettres. Son flux est conservé
|
||||
lors du passage sous le flash blanc au monde ; les autres sons sont nettoyés.
|
||||
- Le poids des familiers dépend de la taille stable de leur œuf, même avec un
|
||||
modèle bébé. Les minuscules ne ralentissent pas, les petits légèrement,
|
||||
puis le ralentissement augmente avec la taille.
|
||||
- Les colossaux deviennent des montures personnelles : Maj + clic droit à
|
||||
main vide, déplacements et saut/vol/nage selon l’espèce, relâcher Maj puis
|
||||
réappuyer pour descendre. La simulation et les collisions restent serveur.
|
||||
- Infobulles FR/EN pour le poids et le geste de monte. Aucun format de
|
||||
sauvegarde ou paramètre de génération modifié.
|
||||
|
||||
## beta.060 — objets et blocs sur les mobs
|
||||
|
||||
- Ajoute un emplacement de tête indépendant de l’armure native des mobs et
|
||||
|
||||
@@ -20,8 +20,8 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
|
||||
L’économie, les claims et les dimensions décrits dans la vision restent
|
||||
à implémenter.
|
||||
|
||||
Les sources Git sont synchronisées jusqu’à **beta.060**. Le récapitulatif de
|
||||
[synchronisation](docs/git-sync-beta060.md) précise les vérifications et les
|
||||
Les sources Git sont synchronisées jusqu’à **beta.061**. Le récapitulatif de
|
||||
[synchronisation](docs/git-sync-beta061.md) précise les vérifications et les
|
||||
limites. Les archives bêta mentionnées dans ce document sont des fichiers
|
||||
locaux ; la dernière release du canal packwiz reste **alpha.30.7**.
|
||||
|
||||
@@ -31,6 +31,17 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
|
||||
l’étude de Xaero et des alternatives libres.
|
||||
|
||||
|
||||
## beta.061 — musique d’arrivée et montures familières
|
||||
|
||||
La musique commence au dévoilement du portail et accompagne le flash blanc
|
||||
puis l’entrée dans le monde sans coupure. Le poids d’un familier porté suit la taille de son
|
||||
œuf : minuscule sans ralentissement, petit léger, grand plus lourd. Un colossal
|
||||
se monte avec Maj + clic droit à main vide et se dirige avec les touches de
|
||||
déplacement. [Contrat et vérifications](docs/arrival-mount-beta061.md).
|
||||
Build, parcours natif et archives vérifiés.
|
||||
[Pack normal](build/Sanctuary-beta.061.mrpack) ·
|
||||
[Monde plat rapide](build/Sanctuary-Test-beta.061.mrpack).
|
||||
|
||||
## beta.060 — objets et blocs sur les mobs
|
||||
|
||||
Maj + clic droit avec un objet équipe la tête d'un mob. Clic droit utilise
|
||||
@@ -935,7 +946,7 @@ Le relief de l’île initiale reste celui de l’alpha.30.7. Les anciennes
|
||||
sauvegardes gardent leur contrat et ne reçoivent pas ces îles automatiquement.
|
||||
Voir [le contrat et les vérifications](docs/expeditions-beta001.md).
|
||||
|
||||
Les sources bêta sont disponibles dans Git jusqu’à beta.060 ; la dernière
|
||||
Les sources bêta sont disponibles dans Git jusqu’à beta.061 ; la dernière
|
||||
release du canal packwiz reste l’alpha.30.7 ci-dessous. Les sections alpha
|
||||
constituent l’historique.
|
||||
|
||||
@@ -1586,7 +1597,7 @@ sera une mise à jour explicite, avec vérification des API et des sauvegardes.
|
||||
| Fabric API | 0.160.0+26.3 |
|
||||
| Fabric Loom | 1.17.20 |
|
||||
| Gradle Wrapper | 9.5.1, distribution vérifiée par SHA-256 |
|
||||
| Sanctuary / pack | beta.060 |
|
||||
| Sanctuary / pack | beta.061 |
|
||||
|
||||
Java 25 et Python 3.11 ou plus récent sont nécessaires. Le script pack utilise
|
||||
uniquement la bibliothèque standard et repère aussi une installation Python
|
||||
@@ -1615,7 +1626,7 @@ décrits dans [Validation](docs/testing.md).
|
||||
|
||||
Résultats :
|
||||
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.060.jar` : mod à installer avec
|
||||
- `mods/sanctuary/build/libs/sanctuary-beta.061.jar` : mod à installer avec
|
||||
Fabric API sur la version Minecraft indiquée.
|
||||
- `build/packwiz/` : pack de développement complet, avec le mod construit et
|
||||
l'index vérifié. Voir [Installation packwiz](packwiz/README.md).
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# beta.061 — musique d'arrivée et taille des familiers portés
|
||||
|
||||
Branche `codex/intro-familiar-mount-beta061`.
|
||||
|
||||
Contrat : lancer la musique Minecraft au dévoilement du portail (23,5 s),
|
||||
puis conserver ce même morceau pendant le flash blanc et le passage au monde.
|
||||
Les lettres gardent leurs ambiances sans musique. Le menu est arrêté une fois
|
||||
à l'ouverture de l'introduction ; le gestionnaire attend le portail. Le lancement
|
||||
se fait une seule fois, y compris après un redimensionnement. Passer avant le
|
||||
portail ne force pas de morceau ; passer après conserve le morceau commencé.
|
||||
Les sons de la cinématique gardent leur propre nettoyage ; les volumes choisis par le joueur restent respectés.
|
||||
|
||||
Le portage des familiers utilise la taille déjà enregistrée dans l'œuf :
|
||||
minuscule sans pénalité, petit avec une pénalité légère, ordinaire à 75 %
|
||||
de vitesse à la taille 1, puis de plus en plus lourd jusqu'à 25 % à la limite.
|
||||
Un colossal (taille ≥ 2,5) ne peut plus être porté. Maj + clic droit à main
|
||||
vide sur son familier colossal permet de monter dessus ; relâcher Maj puis
|
||||
appuyer à nouveau fait descendre. Les touches de déplacement le dirigent,
|
||||
Espace saute au sol ou monte en vol/nage ; regarder vers le bas en avançant
|
||||
permet de descendre en vol/nage. Les aquatiques restent lents hors de l'eau.
|
||||
|
||||
Le propriétaire dirige sa monture côté serveur. Les commandes de combat
|
||||
autonomes ne détournent pas le déplacement tant qu'il est dessus. L'équipement
|
||||
sur la tête du mob conserve la priorité du geste (récupérer son chapeau avant
|
||||
de monter). Les animaux natifs bébés restent sans poids, les piles de joueurs
|
||||
et leur interaction avec Force gardent leurs règles.
|
||||
|
||||
La monte occupe une place pour le propriétaire, sans autre passager porté.
|
||||
Les duels conservent leur combat autonome et font descendre le cavalier.
|
||||
|
||||
Les relations de monture sont temporaires. Retrait de l'œuf, K.-O., déconnexion
|
||||
ou changement de dimension interrompent la monte. Aucun nouveau format de
|
||||
sauvegarde : `sanctuary:familiar_size` schéma 1 et tous les autres composants
|
||||
de l'œuf restent inchangés. Les œufs sans taille valide gardent le poids
|
||||
ordinaire et ne deviennent pas des montures. Aucun monde existant n'est modifié.
|
||||
|
||||
## Vérifications
|
||||
|
||||
`ArrivalMount061ClientChecks` passe sur le client natif Minecraft 26.3-pre-2,
|
||||
avec un monde plat de développement neuf et une vraie introduction complète :
|
||||
|
||||
- absence de musique Minecraft pendant les lettres, lancement au portail,
|
||||
même instance sonore active jusque deux secondes après la fin du flash ;
|
||||
- portage vache bébé / dragon miniature sur sept tailles, suppression du
|
||||
ralentissement à la dépose, exemption conservée pour un bébé natif ;
|
||||
- Maj + clic droit réellement envoyé au serveur, monte d'une vache colossale,
|
||||
déplacement par les touches du client et orientation du cavalier ;
|
||||
- maintien pendant le premier appui sur Maj, démontage après relâchement,
|
||||
nouvelle monte puis retrait de l'œuf sans cavalier orphelin ;
|
||||
- allay colossal : montée, descente et collision du cavalier sous un plafond ;
|
||||
- morue colossale : mouvement lent à terre, puis vraie nage verticale dans
|
||||
un bassin fermé ; arrêt sonore explicite encore fonctionnel hors arrivée.
|
||||
|
||||
Journal : `build/arrival-mount061-client.log`, **1 min 32 s**.
|
||||
Marqueurs `ARRIVAL061_MUSIC_PASS` et `ARRIVAL_MOUNT061_PASS`.
|
||||
La capture de la vache montée est inspectée dans
|
||||
`build/arrival-mount061-evidence/`. Les mêmes règles et modèles natifs couvrent
|
||||
les autres espèces, mais leur placement visuel individuel n'a pas été vérifié
|
||||
pour chacune des 88 espèces. Les grands familiers ont besoin d'un espace libre
|
||||
adapté à leur volume. Aucun essai avec deux clients humains n'est revendiqué.
|
||||
|
||||
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest
|
||||
-PsanctuaryClientTests=true -PsanctuaryArrivalMount061ClientTests=true
|
||||
-PsanctuaryQuickTests=true` réussit : **122 tâches**, **3 min 14 s**.
|
||||
Le parcours client est exécuté séparément par `:sanctuary:runClientGameTest`
|
||||
avec les mêmes propriétés. Aucun EULA de serveur dédié n'a été accepté.
|
||||
|
||||
## Archives locales
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.061.mrpack), 5676845 octets.
|
||||
SHA-256 : `c97424d94b2e2960c071795670cbc1300cf5a805c4dadbfa5dad33fccb79edbb`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.061.mrpack), 5695778 octets.
|
||||
SHA-256 : `2c3a9c3cfa849a3f24c38ceaf112ef4a96f6b182faa99d1eb78f00f8f2293c63`.
|
||||
|
||||
Les **1623 classes** du mod embarqué sont conformes au build. Le reçu
|
||||
`build/arrival-mount061-artifact.json` vérifie les ressources, les dépendances
|
||||
imbriquées, les différences ciblées avec beta.060 et la conservation des
|
||||
archives précédentes. JEI et les ressources de génération restent identiques.
|
||||
Aucun canal publié, instance personnelle ou monde existant n'est modifié.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Synchronisation Git jusqu’à beta.061
|
||||
|
||||
Ticket du 15 septembre 2026, branche `codex/git-sync-beta061`.
|
||||
|
||||
La livraison beta.061 s’est terminée pendant le
|
||||
[rattrapage Git beta.060](git-sync-beta060.md). Ses sources finales sont
|
||||
enregistrées dans un second commit, au-dessus du point de sauvegarde beta.060.
|
||||
Les tags `beta.060` et `beta.061` désignent ces deux états ; `main` rejoint
|
||||
beta.061 par avance directe, sans réécriture de l’historique.
|
||||
|
||||
Le numéro reste celui de la livraison existante. Aucun changement de gameplay
|
||||
n’est ajouté par cette synchronisation. Les références courantes du README
|
||||
et de l’exemple d’export packwiz sont actualisées.
|
||||
|
||||
## Validation des sources livrées
|
||||
|
||||
Les sources et tests sont copiés après la fin de la tâche
|
||||
`codex/intro-familiar-mount-beta061`. Son
|
||||
[compte rendu de livraison](arrival-mount-beta061.md#vérifications), vérifié
|
||||
dans les journaux locaux, donne :
|
||||
|
||||
- `check build assemblePack assembleTestPack` avec la sélection beta.061
|
||||
et `-x :sanctuary:runGameTest` : réussite en 3 min 14 s, 122 tâches.
|
||||
- Parcours client natif `ArrivalMount061ClientChecks` : réussite en
|
||||
1 min 32 s, marqueurs `ARRIVAL061_MUSIC_PASS` et `ARRIVAL_MOUNT061_PASS`.
|
||||
- Reçu `build/arrival-mount061-artifact.json` : 1 623 classes Sanctuary,
|
||||
ressources, dépendances et conservation des archives beta.060 vérifiées.
|
||||
|
||||
La copie destinée à Git est ensuite reconstruite dans le worktree isolé avec
|
||||
`assemblePack assembleTestPack`, en excluant les tâches `check` déjà exécutées
|
||||
sur les sources livrées. La comparaison des JAR contrôle que cette copie
|
||||
produit le même code que la livraison testée. La reconstruction réussit en
|
||||
**15 s**, avec **21 tâches** ; tous les contenus des entrées du JAR sont
|
||||
strictement identiques, y compris les 1 623 classes, les ressources, les
|
||||
métadonnées et les dépendances imbriquées.
|
||||
|
||||
Reçus locaux : `build/git-sync-beta061-verification.json` et
|
||||
`build/git-sync-beta061-assemble.log`. Les empreintes des deux archives
|
||||
MRpack originales sont également conformes au reçu de livraison.
|
||||
|
||||
La suite serveur générale a été exécutée sur beta.060 pendant ce rattrapage :
|
||||
**235 réussites sur 246**, avec [11 échecs consignés](git-sync-beta060.md#vérifications).
|
||||
Ces scénarios restent à trier ; le parcours ciblé beta.061 ne constitue pas
|
||||
une nouvelle exécution ni une réussite de cette suite générale.
|
||||
|
||||
## Distribution
|
||||
|
||||
Cette opération publie les sources et leurs tags sur le Git du projet.
|
||||
Les archives bêta restent locales, le canal packwiz reste sur alpha.30.7,
|
||||
et aucune instance Prism ou sauvegarde personnelle n’est modifiée.
|
||||
@@ -39,8 +39,9 @@ Le fond sonore combine l'ambiance de la vallée des âmes, le portail assourdi,
|
||||
une écoute lointaine et huit battements de Warden de plus en plus rapprochés,
|
||||
puis l’ouverture du portail de l’End. Les sons proviennent de Minecraft **26.3-pre-2**, déjà requis ; aucune
|
||||
nouvelle dépendance ou copie de fichier audio vanilla n'est ajoutée. Le mixage
|
||||
respecte les volumes **Général / Ambiance**. La musique est interrompue pendant
|
||||
la séquence, sans modifier les réglages. Les sons sont arrêtés à la sortie.
|
||||
respecte les volumes **Général / Ambiance**. Dans beta.031, la musique était interrompue pendant
|
||||
la séquence. [Beta.061](arrival-mount-beta061.md) déclenche désormais la musique
|
||||
à la révélation du portail, puis la conserve jusque dans le monde, sans modifier les réglages. Les ambiances de la cinématique sont arrêtées à la sortie.
|
||||
Le voile blanc est abandonné à une déconnexion ou à un écran d’erreur ;
|
||||
les menus ne sont pas masqués.
|
||||
|
||||
|
||||
@@ -13,6 +13,22 @@ JAR Sanctuary sont des pièces jointes de releases Gitea. Aucun JAR de mod ou
|
||||
dépendance téléchargée n'entre dans l'historique Git. Le canal Beta est distinct
|
||||
de l'ancien pack 26.2.
|
||||
|
||||
## beta.061 — musique au portail et montures colossales
|
||||
|
||||
[Contrat et vérifications](arrival-mount-beta061.md). Musique démarrant au
|
||||
portail et conservée jusqu'au monde ; poids des familiers selon leur taille,
|
||||
monte des colossaux avec commandes et collisions serveur. Build et parcours
|
||||
client natif réussis.
|
||||
|
||||
- [Pack normal](../build/Sanctuary-beta.061.mrpack), 5676845 octets.
|
||||
SHA-256 : `c97424d94b2e2960c071795670cbc1300cf5a805c4dadbfa5dad33fccb79edbb`.
|
||||
- [Monde plat rapide](../build/Sanctuary-Test-beta.061.mrpack), 5695778 octets.
|
||||
SHA-256 : `2c3a9c3cfa849a3f24c38ceaf112ef4a96f6b182faa99d1eb78f00f8f2293c63`.
|
||||
|
||||
Reçu `build/arrival-mount061-artifact.json` : 1623 classes conformes au
|
||||
build, dépendances et archives beta.060 préservées. Aucun canal ni instance
|
||||
personnelle modifié.
|
||||
|
||||
## beta.060 — objets et blocs sur les mobs
|
||||
|
||||
[Contrat et vérifications](mob-head-blocks-beta060.md). Équipement par
|
||||
|
||||
+2
-2
@@ -9,6 +9,6 @@ loom_version=1.17.20
|
||||
fabric_api_version=0.160.0+26.3
|
||||
|
||||
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
|
||||
mod_version=beta.060
|
||||
pack_version=beta.060
|
||||
mod_version=beta.061
|
||||
pack_version=beta.061
|
||||
maven_group=fr.koka.sanctuary
|
||||
|
||||
@@ -114,6 +114,9 @@ tasks.named('processGametestResources') {
|
||||
if (skillsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Skills025ClientChecks') }
|
||||
def fogClient = providers.gradleProperty('sanctuaryFogClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def sky056Client = providers.gradleProperty('sanctuarySky056ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def arrivalMount061Client = providers.gradleProperty('sanctuaryArrivalMount061ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('arrivalMount061Client', arrivalMount061Client)
|
||||
if (arrivalMount061Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ArrivalMount061ClientChecks') }
|
||||
def mobHead060Client = providers.gradleProperty('sanctuaryMobHead060ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('mobHead060Client', mobHead060Client)
|
||||
if (mobHead060Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.MobHead060ClientChecks') }
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real audio stream continuity and native player input across integrated-server mount physics. */
|
||||
public final class ArrivalMount061ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var watching=new AtomicBoolean(true);var started=new AtomicBoolean();var submitted=new AtomicBoolean();
|
||||
var sound=new AtomicReference<SoundInstance>();var white=new AtomicBoolean();var worldSeen=new AtomicBoolean();
|
||||
c.runOnClient(m->{m.options.getSoundSourceOptionInstance(SoundSource.MASTER).set(1.0);m.options.getSoundSourceOptionInstance(SoundSource.MUSIC).set(.3);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(!watching.get())return;
|
||||
if(m.gui.screen() instanceof HelloWorldScreen screen&&submitted.compareAndSet(false,true)){
|
||||
IntroClient.setQuickTestIntro(false);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}
|
||||
if(m.gui.screen() instanceof IntroScreen intro){
|
||||
if(intro.waitingForMusic())check(music(m)==null,"No Minecraft music while letters reveal");
|
||||
else if(!intro.isFinished()&&started.compareAndSet(false,true)){
|
||||
sound.set(music(m));check(sound.get()!=null,"Portal reveal starts native music");
|
||||
}
|
||||
}
|
||||
if(started.get()){
|
||||
check(sound.get()==music(m),"The same music instance survives intro and world handoff");
|
||||
check(m.getSoundManager().isActive(sound.get()),"Music channel remains active through handoff");
|
||||
if(IntroSpawnFlash.active())white.set(true);
|
||||
if(m.level!=null&&m.player!=null)worldSeen.set(true);
|
||||
}
|
||||
});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ArrivalMount061 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),1600);
|
||||
c.waitTicks(40);watching.set(false);check(started.get()&&white.get()&&worldSeen.get(),"Actual cinematic, white flash and world were all exercised");
|
||||
System.out.println("ARRIVAL061_MUSIC_PASS: same active streamed SoundInstance before, during and after first world");
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
p.teleportTo(0.5,1,0.5);var origin=p.blockPosition();for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)for(int y=-1;y<=10;y++)p.level().setBlockAndUpdate(origin.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
double base=p.getAttributeValue(Attributes.MOVEMENT_SPEED);var pet=FamiliarService.TYPE.create(p.level(),EntitySpawnReason.COMMAND);
|
||||
for(var item:List.of(Items.COW_SPAWN_EGG,Items.ENDER_DRAGON_SPAWN_EGG))for(int size:new int[]{120,350,650,849,1000,1500,2499}){
|
||||
pet.bind(p,egg(item,size));pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Carry familiar size "+size);
|
||||
double expected=size<=350?1:size==1000?.75:FamiliarSize.carrySpeed(pet.egg());check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-expected)<.00001,"Size changes real movement attribute even on baby models");
|
||||
CarryService.release(pet,true);check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Release restores unladen speed");
|
||||
}pet.discard();
|
||||
var baby=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);baby.setBaby(true);baby.setPos(p.position().add(0,0,2));check(CarryService.interact(p,baby),"Native baby still carries");check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Native baby remains weightless");CarryService.release(baby,true);baby.discard();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COW_SPAWN_EGG,3500));p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitTicks(40);
|
||||
int id=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Real equipped egg spawns colossal familiar");pet.setPos(p.position().add(0,0,2));pet.setNoAi(true);return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().isEmpty(),200);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet&&!CarryService.carried(pet),"Actual sneak interaction mounts, never carries, a colossal");check(pet.getBbHeight()>.7&&pet.getControllingPassenger()==null,"Mount uses rendered scale and server authority");pet.setNoAi(false);});
|
||||
c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);c.getInput().lookAt(0,0);
|
||||
var before=server.computeOnServer(s->FamiliarService.active(player(s)).position());
|
||||
c.getInput().holdKey(o->o.keyUp);c.waitTicks(25);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Mount survives released sneak");check(pet.getZ()>before.z+1,"Forward input moves the actual server mount");check(Math.abs(pet.getX()-before.x)<1,"Movement follows rider yaw");});
|
||||
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);});c.waitTicks(3);c.takeScreenshot("sanctuary-beta061-colossal-mount");
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(8);c.getInput().releaseKey(o->o.keyShift);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.isPassenger(),"Release then sneak dismounts normally");var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));p.setShiftKeyDown(false);check(CarryService.interact(p,pet),"Remount: actor="+p.position()+" pet="+pet.position()+" asleep="+p.isSleeping()+" lying="+fr.koka.sanctuary.progression.LyingService.active(p)+" ground="+p.onGround()+" removed="+pet.isRemoved()+" alive="+pet.isAlive()+" rider="+pet.getFirstPassenger()+" line="+p.hasLineOfSight(pet));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);check(!p.isPassenger()&&pet.isRemoved(),"Removing egg safely detaches player and despawns mount");
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.ALLAY_SPAWN_EGG,6000));});
|
||||
c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=player(s);p.setShiftKeyDown(false);var pet=FamiliarService.active(p);check(pet!=null,"Flying mount spawned");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Colossal allay is rideable");});c.waitTicks(5);
|
||||
double ground=server.computeOnServer(s->FamiliarService.active(player(s)).getY());c.getInput().holdKey(o->o.keyJump);c.waitTicks(20);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>ground+2,"Space lifts native flying familiar");check(p.getVehicle()==pet,"Flying pilot stays attached");});
|
||||
double peak=server.computeOnServer(s->FamiliarService.active(player(s)).getY());
|
||||
c.getInput().lookAt(0,65);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()<peak-1,"Looking down and moving descends");});
|
||||
int ceiling=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);int y=(int)Math.ceil(p.getBoundingBox().maxY)+1;var at=pet.blockPosition();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(at.getX()+x,y,at.getZ()+z),Blocks.STONE.defaultBlockState());return y;});
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(40);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Ceiling stops mount without ejecting its rider");check(p.getBoundingBox().maxY<=ceiling+.03&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Rider cannot be driven through a low ceiling");
|
||||
pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);p.teleportTo(.5,1,.5);
|
||||
for(int x=4;x<=12;x++)for(int z=-1;z<=7;z++)for(int y=1;y<=8;y++)if(x==4||x==12||z==-1||z==7)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=5;x<=11;x++)for(int z=0;z<=6;z++)for(int y=1;y<=7;y++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COD_SPAWN_EGG,6000));});c.waitTicks(30);
|
||||
var land=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Aquatic colossal spawns on dry land");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Aquatic colossal is rideable");return pet.position();});
|
||||
c.getInput().lookAt(0,0);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(!pet.isInWater(),"Dry-land measurement is outside the contained pool");check(pet.position().distanceTo(land)<2,"Aquatic mount moves slowly on land: "+pet.position().distanceTo(land));pet.teleportTo(8.5,2,2.5);pet.setDeltaMovement(Vec3.ZERO);pet.positionRider(p);});c.waitTicks(10);
|
||||
double wet=server.computeOnServer(s->{var pet=FamiliarService.active(player(s));check(pet.isInWater(),"Aquatic mount actually enters water");return pet.getY();});
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(15);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>wet+1,"Aquatic mount swims upward under native input");pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);});
|
||||
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON);m.getSoundManager().stop();});c.waitTicks(3);
|
||||
c.runOnClient(m->check(!m.getSoundManager().isActive(sound.get()),"Explicit sound stop remains effective outside arrival"));
|
||||
}finally{watching.set(false);}
|
||||
System.out.println("ARRIVAL_MOUNT061_PASS: audio continuity, size weights, native babies, real mount input, dismount, removal and flight");
|
||||
}
|
||||
private static SoundInstance music(net.minecraft.client.Minecraft m){try{var f=m.getMusicManager().getClass().getDeclaredField("currentMusic");f.setAccessible(true);return (SoundInstance)f.get(m.getMusicManager());}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static ItemStack egg(Item item,int value){var stack=new ItemStack(item);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);CustomData.update(DataComponents.CUSTOM_DATA,stack,t->t.put(FamiliarSize.KEY,size));return stack;}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var e=m.level.getEntity(id);m.gameMode.interact(m.player,e,new EntityHitResult(e,e.getEyePosition()),InteractionHand.MAIN_HAND);});}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public final class Carry027GameTests {
|
||||
}finally{online.removeAll(added);cleanup(p);cow.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void familiarBabyAppearanceAddsNoWeightButMiniatureAdultsDo(GameTestHelper h){
|
||||
@GameTest public void familiarWithoutSizeUsesOrdinaryWeightRegardlessOfBabyAppearance(GameTestHelper h){
|
||||
var p=players(h,2);double base=speed(p[0]);
|
||||
var pet=fr.koka.sanctuary.cosmetics.FamiliarService.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{
|
||||
@@ -59,7 +59,7 @@ public final class Carry027GameTests {
|
||||
boolean baby=egg!=net.minecraft.world.item.Items.ENDER_DRAGON_SPAWN_EGG;
|
||||
h.assertTrue(pet.babyAppearance()==baby,"Familiar uses the same baby support as its native model");
|
||||
h.assertTrue(CarryService.interact(p[1],pet),"The familiar can be carried");
|
||||
ratio(h,p[0],base,baby?.75:.5625);ratio(h,p[1],base,baby?1:.75);
|
||||
ratio(h,p[0],base,.5625);ratio(h,p[1],base,.75);
|
||||
h.assertTrue(CarryService.release(pet,true),"The same familiar is put down");
|
||||
ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ public final class CompanionTooltips {
|
||||
if(xp>0)extra.add(fr.koka.sanctuary.familiar.FamiliarBattle.label("xp.tooltip",fr.koka.sanctuary.familiar.FamiliarLevels.level(xp),xp).copy().withStyle(ChatFormatting.GREEN));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(CompanionClient.label("tooltip_size",
|
||||
CompanionClient.label("size."+fr.koka.sanctuary.cosmetics.FamiliarSize.category(stack))).copy().withStyle(ChatFormatting.GRAY));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(Component.translatable(
|
||||
fr.koka.sanctuary.cosmetics.FamiliarSize.colossal(stack)?"sanctuary.familiar.mount.hint":"sanctuary.familiar.carry.hint",
|
||||
Math.round(fr.koka.sanctuary.cosmetics.FamiliarSize.carrySpeed(stack)*100)).withStyle(ChatFormatting.GRAY));
|
||||
var profile=fr.koka.sanctuary.familiar.FamiliarCatalog.bundled().forEgg(stack);
|
||||
if(profile!=null){
|
||||
if(FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals(profile.id()))profile=FamiliarClient.profile;
|
||||
|
||||
@@ -90,8 +90,7 @@ public final class FamiliarRenderer extends EntityRenderer<FamiliarEntity,Famili
|
||||
}
|
||||
state.renderer=renderer;state.appearance=appearance;
|
||||
if(fr.koka.sanctuary.cosmetics.CarryService.carried(pet))CarryPose.apply(appearance);
|
||||
state.size=Math.min(0.65F,Math.min(0.85F/Math.max(0.1F,mob.getBbHeight()),0.85F/Math.max(0.1F,mob.getBbWidth())));
|
||||
state.size*=FamiliarSize.multiplier(pet.egg());
|
||||
state.size=FamiliarSize.renderScale(mob,pet.egg());
|
||||
float height=mob.getBbHeight()*state.size;
|
||||
state.visualRadius=Math.max(mob.getBbWidth()*state.size,height)*.5F;
|
||||
if(state.nameTagAttachment!=null)state.nameTagAttachment=new net.minecraft.world.phys.Vec3(0,height+.2,0);
|
||||
|
||||
@@ -6,13 +6,16 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworkin
|
||||
|
||||
public final class IntroClient {
|
||||
private static boolean previewRequested;
|
||||
private static boolean arrivalMusic;
|
||||
static void keepArrivalMusic(){arrivalMusic=true;}
|
||||
public static boolean consumeArrivalMusic(){boolean keep=arrivalMusic;arrivalMusic=false;return keep;}
|
||||
private static boolean quickTestIntro;
|
||||
public static void setQuickTestIntro(boolean quick) { quickTestIntro=quick; }
|
||||
private IntroClient() { }
|
||||
public static void register() {
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (!previewRequested) return;
|
||||
previewRequested=false;
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class IntroScreen extends Screen {
|
||||
private long started=-1;
|
||||
private int nextBeat;
|
||||
private net.minecraft.client.renderer.texture.TextureAtlasSprite portalSprite;
|
||||
private boolean rumble, whisper, portal, finished, skipped;
|
||||
private boolean rumble, whisper, portal, finished, skipped, musicStarted;
|
||||
|
||||
public IntroScreen(UUID uuid,java.util.function.Consumer<Boolean> complete) {
|
||||
this(uuid,complete,System::nanoTime);
|
||||
@@ -41,14 +41,18 @@ public final class IntroScreen extends Screen {
|
||||
}
|
||||
public UUID uuid() { return uuid; }
|
||||
boolean isFinished() { return finished; }
|
||||
public boolean waitingForMusic() { return !finished&&!musicStarted; }
|
||||
@Override public net.minecraft.sounds.Music getBackgroundMusic() { return musicStarted?net.minecraft.sounds.Musics.GAME:null; }
|
||||
protected void init() {
|
||||
if (started < 0) started=clock.getAsLong();
|
||||
if (started < 0) {started=clock.getAsLong();minecraft.getMusicManager().stopPlaying();}
|
||||
}
|
||||
private double seconds() { return started < 0 ? 0 : Math.max(0,(clock.getAsLong()-started)/1_000_000_000.0); }
|
||||
public void tick() {
|
||||
if (finished) return;
|
||||
double time=seconds();
|
||||
minecraft.getMusicManager().stopPlaying();
|
||||
if(!musicStarted&&time>=IntroSequence.PORTAL_REVEAL){
|
||||
musicStarted=true;minecraft.getMusicManager().startPlaying(net.minecraft.sounds.Musics.GAME);
|
||||
}
|
||||
if (!rumble && time>=0.7) {
|
||||
rumble=true;
|
||||
play(SoundEvents.AMBIENT_SOUL_SAND_VALLEY_LOOP.value(),0.32f,0.65f,true);
|
||||
@@ -126,7 +130,7 @@ public final class IntroScreen extends Screen {
|
||||
double opacity=IntroSequence.smooth(progress/0.06)*(1-IntroSequence.smooth((progress-0.93)/0.07));
|
||||
glyph(graphics,i,x,y,unit*(float)(0.65+point.radius()*5.5),opacity,0xD1B9F1);
|
||||
}
|
||||
double aperture=IntroSequence.smooth((time-23.5)/5.5);
|
||||
double aperture=IntroSequence.smooth((time-IntroSequence.PORTAL_REVEAL)/5.5);
|
||||
// The destination moves toward the viewer until the doorway exceeds the entire viewport.
|
||||
int extent=(int)(3*unit+Math.max(width,height)*0.68*Math.pow(aperture,2.2));
|
||||
for(int glow=8;glow>=1;glow--) {
|
||||
@@ -164,7 +168,9 @@ public final class IntroScreen extends Screen {
|
||||
}
|
||||
private void finish(boolean skipped) {
|
||||
if (finished) return;
|
||||
finished=true;this.skipped=skipped;stopSounds();complete.accept(skipped);
|
||||
finished=true;this.skipped=skipped;stopSounds();
|
||||
if(musicStarted&&minecraft.level==null)IntroClient.keepArrivalMusic();
|
||||
complete.accept(skipped);
|
||||
}
|
||||
private void stopSounds() {
|
||||
for (var sound:sounds) { sound.close();minecraft.getSoundManager().stop(sound); }
|
||||
|
||||
@@ -40,7 +40,9 @@ public final class CarryService {
|
||||
public static boolean marked(Entity e){return e.getAttachedOrElse(CARRIED,false);}
|
||||
public static boolean carried(Entity e){return marked(e)&&e.getVehicle() instanceof Player;}
|
||||
public static boolean carrying(Entity e){return e instanceof Player&&e.getPassengers().stream().anyMatch(CarryService::carried);}
|
||||
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(carried(a)||carried(b))&&root(a)==root(b);}
|
||||
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(
|
||||
(carried(a)||carried(b))&&root(a)==root(b)
|
||||
||(a instanceof FamiliarEntity||b instanceof FamiliarEntity)&&a.getRootVehicle()==b.getRootVehicle());}
|
||||
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
|
||||
/** One seat per head; the chain includes babies even though they add no carrying penalty. */
|
||||
private static Entity passenger(Entity e){
|
||||
@@ -73,6 +75,7 @@ public final class CarryService {
|
||||
for(var rider:new ArrayList<>(RIDES.keySet())){
|
||||
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
|
||||
var carrier=ride.carrier;
|
||||
if(rider instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg())){release(rider,true);continue;}
|
||||
if(!carried(rider)||rider.getVehicle()!=carrier||!rider.isAlive()||!active(carrier)||rider.level()!=carrier.level()
|
||||
||!validCarrierChain(carrier)){
|
||||
release(rider,true);continue;
|
||||
@@ -102,6 +105,7 @@ public final class CarryService {
|
||||
public static boolean interact(ServerPlayer actor,Entity target){
|
||||
if(!active(actor)||target.level()!=actor.level()||!target.isAlive()||!actor.isWithinEntityInteractionRange(target,0)||!actor.hasLineOfSight(target))return false;
|
||||
if(carried(target)&&target.getVehicle()==actor)return release(target,true);
|
||||
if(target instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg()))return pet.mount(actor);
|
||||
ServerPlayer carrier;Entity rider;
|
||||
if(target instanceof ServerPlayer player){carrier=player;rider=actor;}
|
||||
else if(animal(target)){carrier=actor;rider=target;}
|
||||
@@ -213,9 +217,10 @@ public final class CarryService {
|
||||
}
|
||||
private static void speed(ServerPlayer p){
|
||||
var attribute=p.getAttribute(Attributes.MOVEMENT_SPEED);if(attribute==null)return;
|
||||
int players=0,animals=0;
|
||||
int players=0;double animals=0;
|
||||
for(Entity e=passenger(p);e!=null;e=passenger(e)){
|
||||
if(e instanceof Player)players++;
|
||||
else if(e instanceof FamiliarEntity pet)animals+=Math.log(FamiliarSize.carrySpeed(pet.egg()))/Math.log(1-SLOWDOWN);
|
||||
else if(!baby(e))animals++;
|
||||
}
|
||||
var strength=p.getEffect(net.minecraft.world.effect.MobEffects.STRENGTH);
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
private int motionMode=-1;
|
||||
private int recoilUntil;
|
||||
private Mob voice;
|
||||
private boolean mountShiftReleased;
|
||||
public FamiliarEntity(EntityType<? extends PathfinderMob> type,Level level) {
|
||||
super(type,level);setPermanentlyInvulnerable(true);setPersistenceRequired();setCanPickUpLoot(false);
|
||||
getNavigation().setCanFloat(true);
|
||||
@@ -41,7 +42,96 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
public ServerPlayer owner() {
|
||||
return owner;
|
||||
}
|
||||
public boolean mount(ServerPlayer player){
|
||||
if(player!=owner()||!FamiliarSize.colossal(egg())||isPassenger()||isVehicle()
|
||||
||player.isPassenger()||player.isVehicle()||player.isSleeping()||player.isFallFlying()
|
||||
||fr.koka.sanctuary.progression.LyingService.active(player)
|
||||
||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))return false;
|
||||
var at=position().add(0,mountHeight(),0).subtract(player.getVehicleAttachmentPoint(this));
|
||||
if(!mountRoom(player,at)||!player.startRiding(this,true,true))return false;
|
||||
mountShiftReleased=!player.isShiftKeyDown();getNavigation().stop();setTarget(null);setDeltaMovement(Vec3.ZERO);
|
||||
positionRider(player);
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(this));
|
||||
player.sendOverlayMessage(net.minecraft.network.chat.Component.translatable("sanctuary.familiar.mount.controls"));return true;
|
||||
}
|
||||
public boolean mayDismount(){return level().isClientSide()||mountShiftReleased;}
|
||||
private ServerPlayer pilot(){return getFirstPassenger() instanceof ServerPlayer p&&p==owner()?p:null;}
|
||||
@Override protected boolean canAddPassenger(Entity rider){return !isVehicle()&&FamiliarSize.colossal(egg())&&rider==owner();}
|
||||
// Like crew boats, native input packets drive the server simulation, never client vehicle positions.
|
||||
@Override public LivingEntity getControllingPassenger(){return null;}
|
||||
private double mountHeight(){return visualHeight()*.85;}
|
||||
public float visualHeight(){return prepareVoice()?voice.getBbHeight()*FamiliarSize.renderScale(voice,egg()):getBbHeight();}
|
||||
@Override protected Vec3 getPassengerAttachmentPoint(Entity rider,EntityDimensions dimensions,float scale){return new Vec3(0,mountHeight(),0);}
|
||||
@Override protected EntityDimensions getDefaultDimensions(Pose pose){
|
||||
// The colossal mount's collision shape follows the same miniature model as its renderer.
|
||||
if(FamiliarSize.colossal(egg())&&prepareVoice()){
|
||||
float scale=FamiliarSize.renderScale(voice,egg());
|
||||
return EntityDimensions.scalable(Math.max(.25F,voice.getBbWidth()*scale),Math.max(.3F,voice.getBbHeight()*scale));
|
||||
}
|
||||
return super.getDefaultDimensions(pose);
|
||||
}
|
||||
@Override public void onSyncedDataUpdated(EntityDataAccessor<?> key){
|
||||
super.onSyncedDataUpdated(key);if(EGG.equals(key))refreshDimensions();
|
||||
}
|
||||
private boolean mountRoom(LivingEntity rider,Vec3 at){
|
||||
var box=rider.getDimensions(Pose.STANDING).makeBoundingBox(at).deflate(.01);
|
||||
if(!level().getWorldBorder().isWithinBounds(box)||box.minY<level().getMinY()||box.maxY>level().getMaxY())return false;
|
||||
for(int x=((int)Math.floor(box.minX))>>4;x<=((int)Math.floor(box.maxX))>>4;x++)
|
||||
for(int z=((int)Math.floor(box.minZ))>>4;z<=((int)Math.floor(box.maxZ))>>4;z++)if(!level().hasChunk(x,z))return false;
|
||||
return level().noBlockCollision(rider,box);
|
||||
}
|
||||
@Override public Vec3 getDismountLocationForPassenger(LivingEntity rider){
|
||||
int edge=(int)Math.ceil(getBbWidth()/2+rider.getBbWidth()/2);
|
||||
for(int radius=Math.max(1,edge);radius<=edge+3;radius++)for(int dy:new int[]{0,1,-1,2,-2})
|
||||
for(int x=-radius;x<=radius;x++)for(int z=-radius;z<=radius;z++){
|
||||
if(Math.max(Math.abs(x),Math.abs(z))!=radius)continue;
|
||||
var at=position().add(x,dy+.05,z);var below=BlockPos.containing(at).below();
|
||||
if(mountRoom(rider,at)&&level().getFluidState(BlockPos.containing(at)).isEmpty()
|
||||
&&!level().getBlockState(below).getCollisionShape(level(),below).isEmpty())return at;
|
||||
}
|
||||
return rider.position();
|
||||
}
|
||||
@Override public void travel(Vec3 input){
|
||||
var player=pilot();
|
||||
if(player==null){super.travel(input);return;}
|
||||
var keys=player.getLastClientInput();
|
||||
setYRot(player.getYRot());setXRot(player.getXRot()*.5F);yBodyRot=getYRot();yHeadRot=getYRot();
|
||||
double forward=(keys.forward()?1:0)-(keys.backward()?1:0),side=(keys.left()?1:0)-(keys.right()?1:0);
|
||||
if(forward<0)forward*=.5;
|
||||
boolean spatial=motion().flies()||motion().swims()&&isInWater();
|
||||
if(spatial){
|
||||
double pitch=Math.toRadians(player.getXRot());
|
||||
input=new Vec3(side,keys.jump()?1:-Math.sin(pitch)*forward,Math.cos(pitch)*forward);
|
||||
if(input.lengthSqr()>1)input=input.normalize();
|
||||
// Bounded acceleration/inertia, native swept block collisions and a full stop at rest.
|
||||
var desired=getInputVector(input,.32F,getYRot());
|
||||
setDeltaMovement(getDeltaMovement().lerp(desired,.3));
|
||||
move(MoverType.SELF,getDeltaMovement());setDeltaMovement(getDeltaMovement().scale(.9));resetFallDistance();player.resetFallDistance();
|
||||
}else{
|
||||
setSpeed(motion()==FamiliarMotion.AQUATIC?.05F:.28F);
|
||||
if(onGround()&&(keys.jump()||motion().hops()&&tickCount%14==0&&(side!=0||forward!=0)))jumpFromGround();
|
||||
input=new Vec3(side,0,forward);if(input.lengthSqr()>1)input=input.normalize();
|
||||
super.travel(input);
|
||||
}
|
||||
}
|
||||
/** A rider's head also collides; tall mounts cannot carry their player through a ceiling. */
|
||||
@Override public void move(MoverType type,Vec3 delta){
|
||||
var player=pilot();
|
||||
if(player!=null&&!mountRoom(player,player.position().add(delta))){
|
||||
double x=mountRoom(player,player.position().add(delta.x,0,0))?delta.x:0;
|
||||
double y=mountRoom(player,player.position().add(x,delta.y,0))?delta.y:0;
|
||||
double z=mountRoom(player,player.position().add(x,y,delta.z))?delta.z:0;
|
||||
delta=new Vec3(x,y,z);setDeltaMovement(delta);
|
||||
}
|
||||
super.move(type,delta);
|
||||
}
|
||||
@Override protected void registerGoals() {
|
||||
goalSelector.addGoal(-3,new Goal(){
|
||||
{setFlags(EnumSet.of(Flag.MOVE,Flag.LOOK,Flag.JUMP));}
|
||||
@Override public boolean canUse(){return isVehicle();}
|
||||
@Override public boolean requiresUpdateEveryTick(){return true;}
|
||||
@Override public void tick(){getNavigation().stop();setTarget(null);setSpeed(0);setXxa(0);setYya(0);setZza(0);}
|
||||
});
|
||||
goalSelector.addGoal(-2,new Goal(){
|
||||
{setFlags(EnumSet.of(Flag.MOVE,Flag.JUMP));}
|
||||
@Override public boolean canUse(){return tickCount<recoilUntil;}
|
||||
@@ -81,6 +171,14 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
}
|
||||
var player=owner();
|
||||
if(player==null || player.level()!=level || !player.isAlive() || player.isSpectator()) {discard();return;}
|
||||
if(isVehicle()){
|
||||
if(!FamiliarSize.colossal(egg())||pilot()==null||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))ejectPassengers();
|
||||
else {
|
||||
if(!player.isShiftKeyDown())mountShiftReleased=true;getNavigation().stop();
|
||||
// Manual techniques retain their cast cadence while movement is driven by the rider.
|
||||
fr.koka.sanctuary.familiar.FamiliarTechniques.advance(this);return;
|
||||
}
|
||||
}
|
||||
if(CarryService.carried(this)){getNavigation().stop();return;}
|
||||
boolean obstructed=!level.noBlockCollision(this,getBoundingBox().deflate(0.02));
|
||||
if(distanceToSqr(player)>(fr.koka.sanctuary.familiar.FamiliarBattle.fighting(player)?24*24:144) || getY()<level.getMinY() || obstructed) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
|
||||
/** A permanent visual trait of one equipped egg, using native item components. */
|
||||
/** A permanent trait of one equipped egg, shared by its appearance, weight and mount. */
|
||||
public final class FamiliarSize {
|
||||
public static final String KEY="sanctuary:familiar_size";
|
||||
private FamiliarSize(){}
|
||||
@@ -31,6 +31,17 @@ public final class FamiliarSize {
|
||||
int value=number.intValue();return value>=120&&value<=6000?value:0;
|
||||
}
|
||||
public static float multiplier(ItemStack egg){int value=value(egg);return value==0?1:value/1000F;}
|
||||
public static boolean colossal(ItemStack egg){return value(egg)>=2500;}
|
||||
public static double carrySpeed(ItemStack egg){
|
||||
int size=value(egg);if(size==0)size=1000;
|
||||
if(size<=350)return 1;
|
||||
if(size<850)return 1-(size-350)*.1/500;
|
||||
if(size<=1000)return .9-(size-850)*.15/150;
|
||||
return Math.max(.25,.75-(size-1000)*.5/1500);
|
||||
}
|
||||
public static float renderScale(net.minecraft.world.entity.Mob model,ItemStack egg){
|
||||
return Math.min(.65F,Math.min(.85F/Math.max(.1F,model.getBbHeight()),.85F/Math.max(.1F,model.getBbWidth())))*multiplier(egg);
|
||||
}
|
||||
public static String category(ItemStack egg){
|
||||
int value=value(egg);
|
||||
return value==0?"unknown":value<=350?"tiny":value<850?"small":value<=1200?"ordinary":value<2500?"large":"colossal";
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.UUID;
|
||||
public final class IntroSequence {
|
||||
public static final int GLYPHS = 32;
|
||||
public static final double SPIRAL = 22.2;
|
||||
public static final double PORTAL_REVEAL = 23.5;
|
||||
public static final double PORTAL = 26.8;
|
||||
public static final double DURATION = 29.5;
|
||||
private IntroSequence() { }
|
||||
|
||||
@@ -14,7 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
public abstract class CarryEntityMixin {
|
||||
@ModifyExpressionValue(method="startRiding(Lnet/minecraft/world/entity/Entity;ZZ)Z",at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/EntityType;canSerialize()Z"))
|
||||
private boolean sanctuary$playerCanCarry(boolean original,Entity vehicle,boolean force,boolean events){
|
||||
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this);
|
||||
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this)
|
||||
||vehicle instanceof FamiliarEntity pet&&pet.owner()==(Object)this&&FamiliarSize.colossal(pet.egg());
|
||||
}
|
||||
@Inject(method="removeVehicle",at=@At("TAIL"))
|
||||
private void sanctuary$finishCarry(CallbackInfo ci){
|
||||
|
||||
@@ -13,6 +13,7 @@ public abstract class CarryPlayerMixin {
|
||||
private void sanctuary$releaseThenSneak(CallbackInfoReturnable<Boolean> ci){
|
||||
var p=(Player)(Object)this;
|
||||
if(CarryService.carried(p)&&!CarryService.mayDismount(p))ci.setReturnValue(false);
|
||||
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())ci.setReturnValue(false);
|
||||
}
|
||||
@Inject(method="attack",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.pair((Entity)(Object)this,target))ci.cancel();}
|
||||
|
||||
@@ -10,7 +10,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
@Mixin(ServerPlayer.class)
|
||||
public abstract class CarryServerPlayerMixin {
|
||||
@Inject(method="saveParentVehicle",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){if(CarryService.carried((ServerPlayer)(Object)this))ci.cancel();}
|
||||
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){
|
||||
var p=(ServerPlayer)(Object)this;
|
||||
if(CarryService.carried(p)||p.getRootVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity)ci.cancel();
|
||||
}
|
||||
@Inject(method="die",at=@At("HEAD"))
|
||||
private void sanctuary$detachOnDeath(CallbackInfo ci){CarryService.detach((ServerPlayer)(Object)this);}
|
||||
@Inject(method="teleport(Lnet/minecraft/world/level/portal/TeleportTransition;)Lnet/minecraft/server/level/ServerPlayer;",at=@At("HEAD"))
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.IntroScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.sounds.MusicManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(MusicManager.class)
|
||||
abstract class IntroMusicManagerMixin {
|
||||
@Inject(method="tick",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$waitForPortal(CallbackInfo ci){
|
||||
if(Minecraft.getInstance().gui.screen() instanceof IntroScreen intro&&intro.waitingForMusic())ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import fr.koka.sanctuary.client.IntroScreen;
|
||||
import fr.koka.sanctuary.client.IntroSpawnFlash;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.sounds.SoundManager;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Keep the same music stream when the apparition hands off to its first world. */
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class IntroMusicMixin {
|
||||
@WrapOperation(method="updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;Z)V",
|
||||
at=@At(value="INVOKE",target="Lnet/minecraft/client/sounds/SoundManager;stop()V"))
|
||||
private void sanctuary$continueArrivalMusic(SoundManager sounds,Operation<Void> original,ClientLevel next,boolean stopSounds) {
|
||||
var client=(Minecraft)(Object)this;
|
||||
boolean handoff=next!=null&&fr.koka.sanctuary.client.IntroClient.consumeArrivalMusic();
|
||||
if(next!=null && (handoff || IntroSpawnFlash.active() || client.gui.screen() instanceof IntroScreen)) {
|
||||
for(var source:SoundSource.values())if(source!=SoundSource.MUSIC)sounds.stop(null,source);
|
||||
} else original.call(sounds);
|
||||
}
|
||||
}
|
||||
@@ -1724,5 +1724,8 @@
|
||||
"sanctuary.mob_head.removed": "Mob head item retrieved.",
|
||||
"sanctuary.mob_head.occupied": "This mob already wears an item. Retrieve it with an empty hand first.",
|
||||
"sanctuary.mob_head.unreadable": "This mob’s head equipment data is unreadable and has been preserved.",
|
||||
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it."
|
||||
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it.",
|
||||
"sanctuary.familiar.mount.controls": "Mount: movement keys · Space: jump/rise · Sneak: dismount after releasing it.",
|
||||
"sanctuary.familiar.mount.hint": "Colossal: Sneak + empty-hand right-click to ride your familiar.",
|
||||
"sanctuary.familiar.carry.hint": "Carried movement speed: %s%%"
|
||||
}
|
||||
|
||||
@@ -1724,5 +1724,8 @@
|
||||
"sanctuary.mob_head.removed": "Objet récupéré sur le mob.",
|
||||
"sanctuary.mob_head.occupied": "Ce mob porte déjà un objet. Récupérez-le d’abord à main vide.",
|
||||
"sanctuary.mob_head.unreadable": "Les données de l’objet porté sont illisibles et ont été conservées.",
|
||||
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer."
|
||||
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer.",
|
||||
"sanctuary.familiar.mount.controls": "Monture : déplacement · Espace : saut/montée · Maj : descendre après relâchement.",
|
||||
"sanctuary.familiar.mount.hint": "Colossal : Maj + clic droit à main vide pour monter sur ton familier.",
|
||||
"sanctuary.familiar.carry.hint": "Vitesse en le portant : %s %%"
|
||||
}
|
||||
|
||||
@@ -219,7 +219,9 @@
|
||||
"client.ShaderSaturationMixin",
|
||||
"client.WeatherEnvironmentMixin",
|
||||
"client.VanillaLightMixin",
|
||||
"client.ButtonHoverTextMixin"
|
||||
"client.ButtonHoverTextMixin",
|
||||
"client.IntroMusicMixin",
|
||||
"client.IntroMusicManagerMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ Pour produire un fichier importable par un lanceur compatible Modrinth :
|
||||
|
||||
```sh
|
||||
cd build/packwiz # depuis la racine du dépôt
|
||||
packwiz modrinth export --output ../Sanctuary-beta.060.mrpack
|
||||
packwiz modrinth export --output ../Sanctuary-beta.061.mrpack
|
||||
```
|
||||
|
||||
L’exemple vise `beta.060` et nécessite son build local vérifié.
|
||||
L’exemple vise `beta.061` et nécessite son build local vérifié.
|
||||
Les versions suivent la [convention commune au mod et au pack](../docs/versioning.md).
|
||||
L'export contient le JAR Sanctuary local et la référence vérifiée de Fabric API.
|
||||
Il ne publie rien sur Modrinth.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.060"
|
||||
version = "beta.061"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
Reference in New Issue
Block a user