Record completed beta.061 release and finish Git synchronization
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-15 09:31:32 +02:00
parent e13f3cc1b8
commit c8b0684460
28 changed files with 502 additions and 32 deletions
+13
View File
@@ -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 lespè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 larmure native des mobs et
+16 -5
View File
@@ -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 darrivée et montures familières
La musique commence au dévoilement du portail et accompagne le flash blanc
puis lentrée dans le monde sans coupure. Le poids dun 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 lalpha.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 lalpha.30.7 ci-dessous. Les sections alpha
constituent lhistorique.
@@ -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).
+79
View File
@@ -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é.
+50
View File
@@ -0,0 +1,50 @@
# Synchronisation Git jusqu’à beta.061
Ticket du 15 septembre 2026, branche `codex/git-sync-beta061`.
La livraison beta.061 sest 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 lhistorique.
Le numéro reste celui de la livraison existante. Aucun changement de gameplay
nest ajouté par cette synchronisation. Les références courantes du README
et de lexemple dexport 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 nest modifiée.
+3 -2
View File
@@ -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 louverture du portail de lEnd. 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 derreur ;
les menus ne sont pas masqués.
+16
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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') }
@@ -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"))
@@ -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 mobs 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 dabord à main vide.",
"sanctuary.mob_head.unreadable": "Les données de lobjet 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
View File
@@ -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
```
Lexemple vise `beta.060` et nécessite son build local vérifié.
Lexemple 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
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.060"
version = "beta.061"
pack-format = "packwiz:1.1.0"
[index]