From c8b0684460e9bdba15d5658a6987b266fcbe7139 Mon Sep 17 00:00:00 2001 From: koka Date: Tue, 15 Sep 2026 09:31:32 +0200 Subject: [PATCH] Record completed beta.061 release and finish Git synchronization --- CHANGELOG.md | 13 ++ README.md | 21 +++- docs/arrival-mount-beta061.md | 79 ++++++++++++ docs/git-sync-beta061.md | 50 ++++++++ docs/introduction-beta031.md | 5 +- docs/packwiz.md | 16 +++ gradle.properties | 4 +- mods/sanctuary/build.gradle | 3 + .../client/ArrivalMount061ClientChecks.java | 115 ++++++++++++++++++ .../sanctuary/gametest/Carry027GameTests.java | 4 +- .../sanctuary/client/CompanionTooltips.java | 3 + .../sanctuary/client/FamiliarRenderer.java | 3 +- .../fr/koka/sanctuary/client/IntroClient.java | 9 +- .../fr/koka/sanctuary/client/IntroScreen.java | 16 ++- .../sanctuary/cosmetics/CarryService.java | 9 +- .../sanctuary/cosmetics/FamiliarEntity.java | 98 +++++++++++++++ .../sanctuary/cosmetics/FamiliarSize.java | 13 +- .../koka/sanctuary/intro/IntroSequence.java | 1 + .../sanctuary/mixin/CarryEntityMixin.java | 3 +- .../sanctuary/mixin/CarryPlayerMixin.java | 1 + .../mixin/CarryServerPlayerMixin.java | 5 +- .../mixin/client/IntroMusicManagerMixin.java | 17 +++ .../mixin/client/IntroMusicMixin.java | 26 ++++ .../assets/sanctuary/lang/en_us.json | 5 +- .../assets/sanctuary/lang/fr_fr.json | 5 +- .../src/main/resources/sanctuary.mixins.json | 4 +- packwiz/README.md | 4 +- packwiz/pack.toml | 2 +- 28 files changed, 502 insertions(+), 32 deletions(-) create mode 100644 docs/arrival-mount-beta061.md create mode 100644 docs/git-sync-beta061.md create mode 100644 mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/ArrivalMount061ClientChecks.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicManagerMixin.java create mode 100644 mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicMixin.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d2317a..404ba3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7d05ca2..ffff5d4 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/docs/arrival-mount-beta061.md b/docs/arrival-mount-beta061.md new file mode 100644 index 0000000..8c97c97 --- /dev/null +++ b/docs/arrival-mount-beta061.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é. diff --git a/docs/git-sync-beta061.md b/docs/git-sync-beta061.md new file mode 100644 index 0000000..2d76b47 --- /dev/null +++ b/docs/git-sync-beta061.md @@ -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. diff --git a/docs/introduction-beta031.md b/docs/introduction-beta031.md index 96cc76c..be0ce82 100644 --- a/docs/introduction-beta031.md +++ b/docs/introduction-beta031.md @@ -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. diff --git a/docs/packwiz.md b/docs/packwiz.md index e4aab6c..4b3c83d 100644 --- a/docs/packwiz.md +++ b/docs/packwiz.md @@ -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 diff --git a/gradle.properties b/gradle.properties index 5efe676..bdb1bb8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index 1b78468..42defbe 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -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') } diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/ArrivalMount061ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/ArrivalMount061ClientChecks.java new file mode 100644 index 0000000..915c7c1 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/ArrivalMount061ClientChecks.java @@ -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();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(){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);} +} diff --git a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Carry027GameTests.java b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Carry027GameTests.java index 9c7c9dc..813c91b 100644 --- a/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Carry027GameTests.java +++ b/mods/sanctuary/src/gametest/java/fr/koka/sanctuary/gametest/Carry027GameTests.java @@ -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); } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/CompanionTooltips.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/CompanionTooltips.java index ed6443a..c660250 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/CompanionTooltips.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/CompanionTooltips.java @@ -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; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/FamiliarRenderer.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/FamiliarRenderer.java index a7618a5..5ee437d 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/FamiliarRenderer.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/FamiliarRenderer.java @@ -90,8 +90,7 @@ public final class FamiliarRenderer extends EntityRenderer { 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; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/IntroScreen.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/IntroScreen.java index 79fdc96..0335b60 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/IntroScreen.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/IntroScreen.java @@ -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 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); } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/CarryService.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/CarryService.java index 7216632..14896c0 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/CarryService.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/CarryService.java @@ -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); diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/FamiliarEntity.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/FamiliarEntity.java index 5961c64..a3fee8e 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/FamiliarEntity.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/cosmetics/FamiliarEntity.java @@ -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 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.minYlevel().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(fr.koka.sanctuary.familiar.FamiliarBattle.fighting(player)?24*24:144) || getY()=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"; diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/intro/IntroSequence.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/intro/IntroSequence.java index 657eaa2..350f183 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/intro/IntroSequence.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/intro/IntroSequence.java @@ -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() { } diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryEntityMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryEntityMixin.java index 167f2b7..7574b59 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryEntityMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryEntityMixin.java @@ -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){ diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryPlayerMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryPlayerMixin.java index d4a21cf..8282a45 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryPlayerMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryPlayerMixin.java @@ -13,6 +13,7 @@ public abstract class CarryPlayerMixin { private void sanctuary$releaseThenSneak(CallbackInfoReturnable 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();} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryServerPlayerMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryServerPlayerMixin.java index caa527c..52a05b9 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryServerPlayerMixin.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/CarryServerPlayerMixin.java @@ -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")) diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicManagerMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicManagerMixin.java new file mode 100644 index 0000000..6a0795e --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicManagerMixin.java @@ -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(); + } +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicMixin.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicMixin.java new file mode 100644 index 0000000..18edd19 --- /dev/null +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/mixin/client/IntroMusicMixin.java @@ -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 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); + } +} diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json index 0edde01..e7f0df4 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/en_us.json @@ -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%%" } diff --git a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json index eb071ba..196ef83 100644 --- a/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json +++ b/mods/sanctuary/src/main/resources/assets/sanctuary/lang/fr_fr.json @@ -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 %%" } diff --git a/mods/sanctuary/src/main/resources/sanctuary.mixins.json b/mods/sanctuary/src/main/resources/sanctuary.mixins.json index fb4e522..3362baa 100644 --- a/mods/sanctuary/src/main/resources/sanctuary.mixins.json +++ b/mods/sanctuary/src/main/resources/sanctuary.mixins.json @@ -219,7 +219,9 @@ "client.ShaderSaturationMixin", "client.WeatherEnvironmentMixin", "client.VanillaLightMixin", - "client.ButtonHoverTextMixin" + "client.ButtonHoverTextMixin", + "client.IntroMusicMixin", + "client.IntroMusicManagerMixin" ], "injectors": { "defaultRequire": 1 diff --git a/packwiz/README.md b/packwiz/README.md index 376df33..0caa3b2 100644 --- a/packwiz/README.md +++ b/packwiz/README.md @@ -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. diff --git a/packwiz/pack.toml b/packwiz/pack.toml index 23eb730..bd0fc28 100644 --- a/packwiz/pack.toml +++ b/packwiz/pack.toml @@ -1,6 +1,6 @@ name = "Sanctuary" author = "KOKA99CAB" -version = "beta.060" +version = "beta.061" pack-format = "packwiz:1.1.0" [index]