Add chaotic flight when carried flying animals are struck (beta.114)
Build Sanctuary / build (push) Canceled after 0s

This commit is contained in:
koka
2026-09-17 06:57:34 +02:00
parent 4a6f63c52d
commit 21845bd736
18 changed files with 293 additions and 18 deletions
+10 -5
View File
@@ -20,11 +20,10 @@ La [vision complète](docs/vision.md) conserve les intentions ; le
L’économie, les claims et les dimensions décrits dans la vision restent L’économie, les claims et les dimensions décrits dans la vision restent
à implémenter. à implémenter.
La version **beta.113** nomme les miniatures « sculptures dargile » et les La version **beta.114** ajoute une fuite chaotique aux animaux volants portés :
cale contre le bord visé du bloc, sur la grille exacte de 1/16 de bloc. un coup les fait décoller avec leur porteur, dans une direction aléatoire,
Le [ticket](docs/clay-sculpture-grid-beta113.md) décrit laccroche, sa sauvegarde pendant deux à trois secondes. Le [ticket](docs/carried-flight-panic-beta114.md)
et les vérifications. Cette version est installée dans **Sanctuary Beta**. décrit le comportement et ses vérifications.
Latelier conserve son menu limité à la hotbar.
La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà La [livraison précédente](docs/clay-workshop-integration-beta110.md) réunit déjà
latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**. latelier, les œufs et la neige saisonnière dans linstance **Sanctuary Beta**.
Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées. Les [synchronisations antérieures](docs/git-sync-beta092.md) restent documentées.
@@ -35,6 +34,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après
l’étude de Xaero et des alternatives libres. l’étude de Xaero et des alternatives libres.
## beta.114 — Animaux portés et fuite en vol
Un animal volant porté entraîne son porteur après un coup. La trajectoire
aléatoire prend brièvement le dessus sur les commandes ; déposer lanimal
interrompt le vol. [Contrat et vérifications](docs/carried-flight-panic-beta114.md).
## beta.113 — Sculptures dargile et alignement ## beta.113 — Sculptures dargile et alignement
Pose contre le bord visé, orientation selon le regard et coordonnées entières Pose contre le bord visé, orientation selon le regard et coordonnées entières
+79
View File
@@ -0,0 +1,79 @@
# CARRY-08 — Fuite des animaux volants portés — beta.114
Contrat du 17 septembre 2026, branche `codex/carried-flight-panic-beta114`.
Un coup reçu par un animal volant porté sur la tête déclenche une fuite de
40 à 60 ticks : il entraîne son porteur vers le haut, dans une direction
aléatoire, avec des embardées. Le serveur choisit le départ et la trajectoire,
transmis au client pour la physique native. Les touches de déplacement et le
regard ne pilotent pas cette brève fuite. La caméra reste libre. Un nouveau coup peut relancer la fuite avec un autre
cap après dix ticks, sans cumuler les vitesses.
Le porteur peut viser et frapper son animal volant. Les animaux ordinaires
reçoivent les dégâts habituels ; le familier conserve son coup amical sans
perte de santé. Les attaques extérieures acceptées déclenchent aussi la fuite.
Les autres protections entre passagers et porteurs restent en place.
Les espèces reprennent le groupe déjà utilisé pour planer, poule comprise.
La vitesse reste bornée, les collisions sont natives et aucun bloc nest
traversé ni déplacé. Déposer lanimal (Maj + clic droit), le perdre, mourir,
se déconnecter ou changer de dimension arrête la propulsion. Immersion,
mode spectateur, vol créatif, élytres et porteur lui-même passager linhibent.
La fin de fuite rend le contrôle au joueur et conserve le planeur existant
si lanimal reste porté. Les montures colossales gardent leur pilotage actuel.
L’état de fuite est temporaire, synchronisé et non sauvegardé. Aucun format
persistant, identifiant existant ou monde personnel nest modifié. Les nouveaux
retours dinterface sont traduits FR/EN. Client et serveur utilisent beta.114.
## Vérifications natives
`CarryPanic114ClientChecks` passe en **56 secondes** avec un serveur intégré
Minecraft 26.3 et un monde plat jetable de graine 114 :
- Véritable ciblage du perroquet sur la tête et frappe avec la touche native,
perte de santé de lanimal ordinaire, décollage et déplacement du joueur.
- Trajectoire synchronisée et priorité sur les touches avant/droite/saut et
le regard ; comparaison exacte avec la vitesse du tick de physique.
- Expiration, suppression de l’état temporaire et retour au planeur normal.
- 32 trajectoires tirées côté serveur couvrent les quatre quadrants, toutes
dans les bornes de durée et de vitesse ; Maj + clic droit natif interrompt.
- Sept espèces ordinaires : poule, chauve-souris, abeille, allay, perroquet,
blaze et breeze. Une vache portée reste protégée et ne déclenche aucun vol.
- Attaque extérieure, mort, activation du vol créatif et détachement arrêtent
ou déclenchent la réaction selon le contrat.
- Plafond bas : aucune traversée ; le passager bloqué est déposé par les
contrôles de place existants et la propulsion sinterrompt.
- Véritable coup amical sur un familier perroquet : lancement sans perte de
santé, puis arrêt immédiat lors du détachement.
```sh
./gradlew :sanctuary:runClientGameTest \
-PsanctuaryClientTests=true -PsanctuaryCarryPanic114ClientTests=true \
-PsanctuaryClientNoVsync=true -PsanctuaryQuickTests=true
```
Marqueurs `CARRY114_NATIVE_FLIGHT_PASS` et `CARRY114_PASS` dans
`build/carry-panic114-client.log`. Capture en jeu relue dans
`build/carry-panic114-evidence/`. Le GameTest dédié reste exclu conformément
au refus antérieur de son EULA. Pas de test Windows, de réseau distant ou de
latence simulée ; aucun monde personnel na été ouvert.
## Livraison
`./gradlew check build assemblePack assembleTestPack -x :sanctuary:runGameTest`
passe en **2 min 22 s**, 125 tâches. Les sources JAR correspondent exactement
aux sources du dépôt. Les classes et ressources hors de ce ticket restent
identiques à beta.113, notamment les sculptures dargile. Les deux packs
contiennent le même JAR, sans monde, GLB ni classes de test.
- Sanctuary-beta.114.mrpack : 10179428 octets, SHA-256
`92aac61c2c00a2206f3f5ac3d170ab058f6765526cf5a2a64c5336261db3f5bf`.
- Sanctuary-Test-beta.114.mrpack : 10198350 octets, SHA-256
`5e8ec1f18e34ca51e09e2738f2b04f9a1bbac5d14c7a7c2d766218a4cff6021f`.
- JAR Sanctuary : SHA-256
`18105b76f63008210c62d5728f999c7abc083a861ecc1de2c7ee87262dde5a40`.
Reçu local : `build/carry-panic114-artifact.json`.
Publication et synchronisation de linstance à effectuer après le commit source.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3 fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md). # Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.113 mod_version=beta.114
pack_version=beta.113 pack_version=beta.114
resource_pack_version=beta.090 resource_pack_version=beta.090
maven_group=fr.koka.sanctuary maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.3 jei_version=30.32.0-sanctuary.3
+4
View File
@@ -93,6 +93,10 @@ tasks.named('runGameTest') {
} }
tasks.named('processGametestResources') { tasks.named('processGametestResources') {
def carryPanic114Client = providers.gradleProperty('sanctuaryCarryPanic114ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryCarryPanic114ClientTests', carryPanic114Client)
if (carryPanic114Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CarryPanic114ClientChecks') }
def statuary106Client = providers.gradleProperty('sanctuaryStatuary106ClientTests').map { it.toBoolean() }.getOrElse(false) def statuary106Client = providers.gradleProperty('sanctuaryStatuary106ClientTests').map { it.toBoolean() }.getOrElse(false)
inputs.property('sanctuaryStatuary106ClientTests', statuary106Client) inputs.property('sanctuaryStatuary106ClientTests', statuary106Client)
if (statuary106Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Statuary106ClientChecks') } if (statuary106Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Statuary106ClientChecks') }
@@ -0,0 +1,115 @@
package fr.koka.sanctuary.client;
import com.mojang.blaze3d.platform.InputConstants;
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.CameraType;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.*;
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 cursor/attack packets, synchronized player travel, interruption and collision regression. */
public final class CarryPanic114ClientChecks implements FabricClientGameTest {
public void runTest(ClientGameTestContext c){
var intro=new AtomicBoolean(true);
c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);});
ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){
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());intro.set(false);
}});
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("CarryPanic114 "+UUID.randomUUID());s.setSeed("114");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()){
c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer();
server.runOnServer(s->{var p=player(s);p.getInventory().clearContent();p.getAbilities().invulnerable=true;p.onUpdateAbilities();reset(p);
for(int x=-24;x<=24;x++)for(int z=-24;z<=24;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState());
s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"sanctuary time vanilla");s.getCommands().performPrefixedCommand(s.createCommandSourceStack(),"time set noon");});
int parrot=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
c.waitFor(m->m.level.getEntity(parrot)!=null&&CarryService.carried(m.level.getEntity(parrot)),150);
float health=server.computeOnServer(s->((LivingEntity)player(s).level().getEntity(parrot)).getHealth());
attack(c,parrot);c.waitFor(m->CarryPanic.active(m.player),100);
server.runOnServer(s->{var p=player(s);check(CarryPanic.active(p),"Real native attack triggers server panic");check(((LivingEntity)p.level().getEntity(parrot)).getHealth()<health,"Ordinary carried animal takes native damage");});
c.getInput().holdKey(o->o.keyUp);c.getInput().holdKey(o->o.keyRight);c.getInput().holdKey(o->o.keyJump);
c.runOnClient(m->{m.player.setYRot(170);m.player.setXRot(15);});c.waitTicks(12);
c.runOnClient(m->{check(m.player.getY()>2.5&&m.player.position().subtract(new Vec3(.5,1,.5)).horizontalDistance()>1,"Actual client moves upward and sideways");
check(CarryPanic.active(m.player),"Panic remains active through steering keys");
var wanted=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime());
// Level time can advance immediately after the player physics tick. Match either exact tick.
var previous=m.player.getAttached(CarryPanic.FLIGHT).velocity(m.level.getGameTime()-1);
check(Math.min(m.player.getDeltaMovement().distanceTo(wanted),m.player.getDeltaMovement().distanceTo(previous))<.00001,"Movement follows animal, not camera or held keys: actual="+m.player.getDeltaMovement()+", expected="+wanted+", flight="+m.player.getAttached(CarryPanic.FLIGHT)+", time="+m.level.getGameTime());check(m.player.fallDistance<.2,"Flight clears falling distance");
m.options.setCameraType(CameraType.THIRD_PERSON_BACK);});c.waitTicks(2);c.takeScreenshot("carry114-chaotic-flight");
c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);
c.waitTicks(65);c.waitFor(m->!CarryPanic.active(m.player),100);
server.runOnServer(s->{var p=player(s);check(!p.hasAttached(CarryPanic.FLIGHT)&&CarryGlide.supported(p),"Timeout removes panic and retains ordinary glider");check(p.connection.isAcceptingMessages(),"Flight is accepted by server");});
c.runOnClient(m->{check(m.player.getDeltaMovement().y<=0&&m.player.getDeltaMovement().y>=-CarryGlide.DESCENT-.02,"Normal gentle descent resumes");m.options.setCameraType(CameraType.FIRST_PERSON);});
System.out.println("CARRY114_NATIVE_FLIGHT_PASS cursor, real attack, damage, movement, input override, timeout, glide");
server.runOnServer(s->{var p=player(s);p.getRandom().setSeed(114);var bird=p.level().getEntity(parrot);var quadrants=new HashSet<Integer>();
for(int i=0;i<32;i++){
CarryPanic.clear(p);CarryPanic.start(bird);var flight=p.getAttached(CarryPanic.FLIGHT);check(flight.duration()>=40&&flight.duration()<=60,"Duration bounded at 23 seconds");quadrants.add((int)(flight.yaw()/(Math.PI/2)));
for(int t=0;t<=flight.duration();t++){var v=flight.velocity(flight.start()+t);check(v.horizontalDistance()<=.331&&v.y>=0&&v.y<=.241,"Velocity bounded without cumulative impulses");}
}
check(quadrants.size()==4,"Server random headings cover every quadrant");
});c.waitFor(m->CarryPanic.active(m.player),100);
c.getInput().holdKey(o->o.keyShift);c.waitTicks(2);c.getInput().holdKey(o->o.keyUse);c.waitTicks(1);c.getInput().releaseKey(o->o.keyUse);c.getInput().releaseKey(o->o.keyShift);
c.waitFor(m->!CarryService.carrying(m.player)&&!CarryPanic.active(m.player),100);
server.runOnServer(s->{check(!player(s).hasAttached(CarryPanic.FLIGHT),"Native drop clears flight immediately");player(s).level().getEntity(parrot).discard();reset(player(s));});
int cow=server.computeOnServer(s->carry(player(s),EntityTypes.COW).getId());
server.runOnServer(s->{var p=player(s);var animal=(LivingEntity)p.level().getEntity(cow);float before=animal.getHealth();p.attack(animal);
check(CarryService.blocksAttack(p,animal)&&animal.getHealth()==before&&!animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&!CarryPanic.active(p),"Terrestrial carry pair stays protected");CarryPanic.start(animal);check(!CarryPanic.active(p),"Ground animal cannot launch player");CarryService.release(animal,true);animal.discard();reset(p);});
// Every supported appearance can trigger the shared flight, including the gliding chicken.
server.runOnServer(s->{var p=player(s);for(var type:List.of(EntityTypes.CHICKEN,EntityTypes.BAT,EntityTypes.BEE,EntityTypes.ALLAY,EntityTypes.PARROT,EntityTypes.BLAZE,EntityTypes.BREEZE)){
var animal=carry(p,type);check(CarryGlide.flyer(animal)&&CarryPanic.canStrike(p,animal),"Carried flying species can be struck: "+type);
check(animal.hurtServer(p.level(),p.damageSources().playerAttack(p),1)&&CarryPanic.active(p),"Accepted damage launches carrier: "+type);CarryService.release(animal,true);animal.discard();check(!p.hasAttached(CarryPanic.FLIGHT),"Release clears each flight");
}});
int external=server.computeOnServer(s->carry(player(s),EntityTypes.PARROT).getId());
server.runOnServer(s->{var p=player(s);var bird=(LivingEntity)p.level().getEntity(external);var zombie=EntityTypes.ZOMBIE.create(p.level(),EntitySpawnReason.COMMAND);
check(bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),1)&&CarryPanic.active(p),"External accepted hit also triggers flight");
p.getAbilities().flying=true;CarryGlide.tick(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Creative flying invalidates propulsion");p.getAbilities().flying=false;
CarryPanic.start(bird);bird.hurtServer(p.level(),p.damageSources().mobAttack(zombie),100);CarryGlide.tick(p);check(!CarryPanic.active(p),"Death stops propulsion");CarryService.detach(p);bird.discard();reset(p);});
int ceiling=server.computeOnServer(s->{var p=player(s);var bird=carry(p,EntityTypes.PARROT);for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().setBlockAndUpdate(new BlockPos(x,4,z),Blocks.STONE.defaultBlockState());return bird.getId();});
c.waitFor(m->m.level.getEntity(ceiling)!=null&&CarryService.carried(m.level.getEntity(ceiling)),100);
server.runOnServer(s->{var p=player(s);((LivingEntity)p.level().getEntity(ceiling)).hurtServer(p.level(),p.damageSources().playerAttack(p),1);});c.waitTicks(12);
server.runOnServer(s->{var p=player(s);check(p.getY()<2&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Player cannot fly through low ceiling");check(!CarryService.carrying(p)&&!CarryPanic.active(p),"Blocked carried animal is safely released");
p.level().getEntity(ceiling).discard();for(int x=-3;x<=3;x++)for(int z=-3;z<=3;z++)p.level().removeBlock(new BlockPos(x,4,z),false);reset(p);
var egg=new ItemStack(Items.PARROT_SPAWN_EGG);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",1000);CustomData.update(DataComponents.CUSTOM_DATA,egg,t->t.put(FamiliarSize.KEY,size));AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg);p.inventoryMenu.broadcastChanges();});
c.waitTicks(25);
int familiar=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Familiar spawned");pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Flying familiar picked up");return pet.getId();});
c.waitFor(m->m.level.getEntity(familiar)!=null&&CarryService.carried(m.level.getEntity(familiar)),100);
float petHealth=server.computeOnServer(s->FamiliarService.active(player(s)).getHealth());attack(c,familiar);c.waitFor(m->CarryPanic.active(m.player),100);
server.runOnServer(s->{var p=player(s);check(FamiliarService.active(p).getHealth()==petHealth&&CarryPanic.active(p),"Friendly familiar tap launches without hurting it");CarryService.detach(p);check(!p.hasAttached(CarryPanic.FLIGHT),"Lifecycle detach clears synchronized flight");});
System.out.println("CARRY114_PASS random flight, native ordinary/familiar strikes, seven species, outside hits, ground protection, drop/death/creative cancellation, ceiling collisions, timeout and normal glide");
}finally{intro.set(false);c.getInput().releaseKey(o->o.keyUp);c.getInput().releaseKey(o->o.keyRight);c.getInput().releaseKey(o->o.keyJump);c.getInput().releaseKey(o->o.keyShift);c.getInput().releaseKey(o->o.keyAttack);c.getInput().releaseKey(o->o.keyUse);}
}
private static void attack(ClientGameTestContext c,int id){
c.waitFor(m->m.level.getEntity(id).position().distanceTo(CarryService.head(m.player,m.level.getEntity(id)))<.01,100);
c.runOnClient(m->{m.options.setCameraType(CameraType.FIRST_PERSON);m.player.lookAt(net.minecraft.commands.arguments.EntityAnchorArgument.Anchor.EYES,m.level.getEntity(id).getBoundingBox().getCenter());});c.waitTicks(3);
c.runOnClient(m->check(m.hitResult instanceof EntityHitResult hit&&hit.getEntity().getId()==id,"Native cursor can pick own carried flyer: hit="+m.hitResult+", target="+m.level.getEntity(id).getBoundingBox()+", eye="+m.player.getEyePosition()+", view="+m.player.getLookAngle()+", eligible="+CarryPanic.canStrike(m.player,m.level.getEntity(id))));
c.getInput().holdKey(o->o.keyAttack);c.waitTicks(1);c.getInput().releaseKey(o->o.keyAttack);
}
private static Mob carry(ServerPlayer p,EntityType<? extends Mob> type){var mob=type.create(p.level(),EntitySpawnReason.COMMAND);mob.setNoAi(true);mob.setPos(p.position().add(0,0,2));p.level().addFreshEntity(mob);check(CarryService.interact(p,mob),"Animal picked up: "+type);return mob;}
private static void reset(ServerPlayer p){CarryService.detach(p);p.teleportTo(.5,1,.5);p.setDeltaMovement(Vec3.ZERO);p.resetFallDistance();p.setShiftKeyDown(false);}
private static ServerPlayer player(MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);}
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
}
@@ -31,6 +31,7 @@ public final class CarryGlide {
} }
public static boolean active(Player p){return supported(p)&&!p.onGround()&&(p.getDeltaMovement().y<=0||lifting(p));} public static boolean active(Player p){return supported(p)&&!p.onGround()&&(p.getDeltaMovement().y<=0||lifting(p));}
public static void tick(Player p){ public static void tick(Player p){
if(CarryPanic.apply(p))return;
boolean lift=lifting(p); boolean lift=lifting(p);
if(!lift&&!active(p))return; if(!lift&&!active(p))return;
var velocity=p.getDeltaMovement(); var velocity=p.getDeltaMovement();
@@ -0,0 +1,62 @@
package fr.koka.sanctuary.cosmetics;
import fr.koka.sanctuary.SanctuaryMod;
import net.fabricmc.fabric.api.attachment.v1.*;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
/** Server-chosen, temporary flight; both sides use the same bounded native movement. */
public final class CarryPanic {
public record Flight(int animal,long start,int duration,float yaw,float phase){
static final StreamCodec<RegistryFriendlyByteBuf,Flight> CODEC=StreamCodec.of(
(b,f)->{b.writeVarInt(f.animal);b.writeLong(f.start);b.writeVarInt(f.duration);b.writeFloat(f.yaw);b.writeFloat(f.phase);},
b->new Flight(b.readVarInt(),b.readLong(),b.readVarInt(),b.readFloat(),b.readFloat()));
public Vec3 velocity(long now){
double t=Math.clamp(now-start,0,duration),angle=yaw+Math.sin(t*.27+phase)*.65+Math.sin(t*.61)*.18;
double speed=.28+.05*Math.sin(t*.23+phase);
double rise=t<5?.24:.12+.045*Math.sin(t*.32+phase);
double fade=Math.min(1,(duration-t)/8.0);
return new Vec3(-Math.sin(angle)*speed*fade,rise*fade,Math.cos(angle)*speed*fade);
}
}
public static final AttachmentType<Flight> FLIGHT=AttachmentRegistry.create(SanctuaryMod.id("carry_panic"),
b->b.syncWith(Flight.CODEC,AttachmentSyncPredicate.all()));
private CarryPanic(){}
public static void register(){
ServerLivingEntityEvents.AFTER_DAMAGE.register((entity,source,base,taken,blocked)->{
if(taken>0&&!blocked)start(entity);
});
}
public static boolean canStrike(Entity actor,Entity target){
return actor instanceof Player p&&!p.isSpectator()&&p.isAlive()&&target instanceof Mob
&&target.getVehicle()==p&&CarryService.carried(target)&&CarryGlide.flyer(target);
}
public static void start(Entity animal){
if(!(animal.getVehicle() instanceof ServerPlayer p)||!CarryService.carried(animal)||!CarryGlide.flyer(animal)||!CarryGlide.supported(p))return;
long now=p.level().getGameTime();var previous=p.getAttached(FLIGHT);
if(previous!=null&&now-previous.start()<10)return;
var random=p.getRandom();
var flight=new Flight(animal.getId(),now,40+random.nextInt(21),random.nextFloat()*(float)(Math.PI*2),random.nextFloat()*(float)(Math.PI*2));
p.setAttached(FLIGHT,flight);apply(p);p.syncVelocity=true;
p.sendOverlayMessage(Component.translatable("sanctuary.carry.panic"));
}
public static boolean active(Player p){
var flight=p.getAttached(FLIGHT);if(flight==null||!CarryGlide.supported(p))return false;
long elapsed=p.level().getGameTime()-flight.start();
return elapsed>=0&&elapsed<flight.duration()&&p.getPassengers().stream().anyMatch(e->e.getId()==flight.animal()&&CarryService.carried(e)&&CarryGlide.flyer(e));
}
public static boolean apply(Player p){
if(!active(p)){
if(!p.level().isClientSide()&&p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);
return false;
}
p.setDeltaMovement(p.getAttached(FLIGHT).velocity(p.level().getGameTime()));p.resetFallDistance();return true;
}
public static void clear(Player p){if(p.hasAttached(FLIGHT))p.removeAttached(FLIGHT);}
}
@@ -46,6 +46,7 @@ public final class CarryService {
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(a,b) &&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(a,b)
&&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(b,a));} &&!fr.koka.sanctuary.boats.BoatPassengerInteractions.aboardTogether(b,a));}
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;} private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
public static boolean blocksAttack(Entity attacker,Entity target){return pair(attacker,target)&&!CarryPanic.canStrike(attacker,target);}
/** One seat per head; the chain includes babies even though they add no carrying penalty. */ /** One seat per head; the chain includes babies even though they add no carrying penalty. */
private static Entity passenger(Entity e){ private static Entity passenger(Entity e){
return e instanceof Player&&e.getPassengers().size()==1&&carried(e.getFirstPassenger())?e.getFirstPassenger():null; return e instanceof Player&&e.getPassengers().size()==1&&carried(e.getFirstPassenger())?e.getFirstPassenger():null;
@@ -60,6 +61,7 @@ public final class CarryService {
}; };
} }
public static void register(){ public static void register(){
CarryPanic.register();
net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(Drop.TYPE,Drop.CODEC); net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry.serverboundPlay().register(Drop.TYPE,Drop.CODEC);
net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Drop.TYPE,(request,context)->context.server().execute(()->{ net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.registerGlobalReceiver(Drop.TYPE,(request,context)->context.server().execute(()->{
if(active(context.player()))for(var passenger:List.copyOf(context.player().getPassengers()))if(carried(passenger))release(passenger,true); if(active(context.player()))for(var passenger:List.copyOf(context.player().getPassengers()))if(carried(passenger))release(passenger,true);
@@ -72,7 +74,7 @@ public final class CarryService {
if(!success)player.sendOverlayMessage(Component.translatable("sanctuary.carry.unavailable")); if(!success)player.sendOverlayMessage(Component.translatable("sanctuary.carry.unavailable"));
return InteractionResult.SUCCESS; return InteractionResult.SUCCESS;
}); });
AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->pair(p,target)?InteractionResult.FAIL:InteractionResult.PASS); AttackEntityCallback.EVENT.register((p,level,hand,target,hit)->blocksAttack(p,target)?InteractionResult.FAIL:InteractionResult.PASS);
ServerTickEvents.END_SERVER_TICK.register(s->{ ServerTickEvents.END_SERVER_TICK.register(s->{
for(var rider:new ArrayList<>(RIDES.keySet())){ for(var rider:new ArrayList<>(RIDES.keySet())){
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue; var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
@@ -187,7 +189,7 @@ public final class CarryService {
public static boolean mayDismount(Player p){var ride=RIDES.get(p);return ride==null||ride.releasedShift;} public static boolean mayDismount(Player p){var ride=RIDES.get(p);return ride==null||ride.releasedShift;}
public static void removed(Entity rider){ public static void removed(Entity rider){
var ride=RIDES.remove(rider);rider.removeAttached(CARRIED); var ride=RIDES.remove(rider);rider.removeAttached(CARRIED);
if(ride!=null){refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));} if(ride!=null){CarryPanic.clear(ride.carrier);refreshBelow(ride.carrier);ride.carrier.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(ride.carrier));}
} }
public static boolean release(Entity rider,boolean place){ public static boolean release(Entity rider,boolean place){
var ride=RIDES.get(rider);if(ride==null){rider.removeAttached(CARRIED);return false;} var ride=RIDES.get(rider);if(ride==null){rider.removeAttached(CARRIED);return false;}
@@ -209,6 +211,7 @@ public final class CarryService {
return true; return true;
} }
public static void detach(ServerPlayer p){ public static void detach(ServerPlayer p){
CarryPanic.clear(p);
// Put down the upper group before removing its support, without orphaning its riders. // Put down the upper group before removing its support, without orphaning its riders.
for(var e:List.copyOf(p.getPassengers()))if(carried(e))release(e,true); for(var e:List.copyOf(p.getPassengers()))if(carried(e))release(e,true);
if(RIDES.containsKey(p))release(p,true); if(RIDES.containsKey(p))release(p,true);
@@ -245,7 +245,7 @@ public final class FamiliarEntity extends PathfinderMob {
return super.causeFallDamage(distance,multiplier,source); return super.causeFallDamage(distance,multiplier,source);
} }
@Override public boolean hurtServer(ServerLevel level,DamageSource source,float amount) { @Override public boolean hurtServer(ServerLevel level,DamageSource source,float amount) {
boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt)playTapSound(source);return hurt; boolean hurt=fr.koka.sanctuary.familiar.FamiliarBattle.hurt(this,source,amount);if(hurt){playTapSound(source);CarryPanic.start(this);}return hurt;
} }
@Override public boolean isIgnoringBlockTriggers() {return true;} @Override public boolean isIgnoringBlockTriggers() {return true;}
@Override public boolean canBeHitByProjectile() {return true;} @Override public boolean canBeHitByProjectile() {return true;}
@@ -316,11 +316,12 @@ public final class FamiliarEntity extends PathfinderMob {
} }
/** Native feedback without damage callbacks, enchantments, aggro or durability. */ /** Native feedback without damage callbacks, enchantments, aggro or durability. */
public void tap(ServerPlayer player) { public void tap(ServerPlayer player) {
if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.pair(player,this))return; if(hurtTime>0||!isAlive()||player.level()!=level()||player.isSpectator()||CarryService.blocksAttack(player,this))return;
var source=damageSources().playerAttack(player); var source=damageSources().playerAttack(player);
if(!HeadEquipment.get(this).isEmpty()){MobHeadActions.activate(this,source,true);} if(!HeadEquipment.get(this).isEmpty()){MobHeadActions.activate(this,source,true);}
animateHurt(player.getYRot()); animateHurt(player.getYRot());
((ServerLevel)level()).broadcastDamageEvent(this,source); ((ServerLevel)level()).broadcastDamageEvent(this,source);
if(CarryPanic.canStrike(player,this)){CarryPanic.start(this);playTapSound(source);return;}
double dx=player.getX()-getX(),dz=player.getZ()-getZ(); double dx=player.getX()-getX(),dz=player.getZ()-getZ();
if(dx*dx+dz*dz<1.0E-5){var look=player.getLookAngle();dx=-look.x;dz=-look.z;} if(dx*dx+dz*dz<1.0E-5){var look=player.getLookAngle();dx=-look.x;dz=-look.z;}
knockback(.4,dx,dz,source,0); knockback(.4,dx,dz,source,0);
@@ -29,7 +29,7 @@ public final class FamiliarService {
net.fabricmc.fabric.api.event.player.AttackEntityCallback.EVENT.register((player,level,hand,target,hit)->{ net.fabricmc.fabric.api.event.player.AttackEntityCallback.EVENT.register((player,level,hand,target,hit)->{
if(!(target instanceof FamiliarEntity pet)||player.isSpectator())return net.minecraft.world.InteractionResult.PASS; if(!(target instanceof FamiliarEntity pet)||player.isSpectator())return net.minecraft.world.InteractionResult.PASS;
if(pet.owner()!=null&&fr.koka.sanctuary.familiar.FamiliarDuels.fighting(pet.owner()))return net.minecraft.world.InteractionResult.FAIL; if(pet.owner()!=null&&fr.koka.sanctuary.familiar.FamiliarDuels.fighting(pet.owner()))return net.minecraft.world.InteractionResult.FAIL;
if(CarryService.pair(player,pet))return net.minecraft.world.InteractionResult.FAIL; if(CarryService.blocksAttack(player,pet))return net.minecraft.world.InteractionResult.FAIL;
if(player instanceof ServerPlayer attacker&&fr.koka.sanctuary.familiar.FamiliarBattle.enemy(attacker,pet))return net.minecraft.world.InteractionResult.PASS; if(player instanceof ServerPlayer attacker&&fr.koka.sanctuary.familiar.FamiliarBattle.enemy(attacker,pet))return net.minecraft.world.InteractionResult.PASS;
if(player instanceof ServerPlayer p&&p.isWithinEntityInteractionRange(pet,0)&&p.hasLineOfSight(pet))pet.tap(p); if(player instanceof ServerPlayer p&&p.isWithinEntityInteractionRange(pet,0)&&p.hasLineOfSight(pet))pet.tap(p);
return net.minecraft.world.InteractionResult.SUCCESS; return net.minecraft.world.InteractionResult.SUCCESS;
@@ -7,13 +7,13 @@ import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*; import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Sustained lift is legitimate only while the server sees the equipped, carried large dragon. */ /** Flight exemption is limited to server-validated dragon lift or a carried flyer's brief panic. */
@Mixin(ServerGamePacketListenerImpl.class) @Mixin(ServerGamePacketListenerImpl.class)
public abstract class CarryFlightConnectionMixin { public abstract class CarryFlightConnectionMixin {
@Shadow public ServerPlayer player; @Shadow public ServerPlayer player;
@Shadow private boolean clientIsFloating; @Shadow private boolean clientIsFloating;
@Inject(method="tick",at=@At("HEAD")) @Inject(method="tick",at=@At("HEAD"))
private void sanctuary$carriedFlight(CallbackInfo ci){ private void sanctuary$carriedFlight(CallbackInfo ci){
if(CarryGlide.lifting(player))clientIsFloating=false; if(CarryGlide.lifting(player)||fr.koka.sanctuary.cosmetics.CarryPanic.active(player))clientIsFloating=false;
} }
} }
@@ -8,6 +8,8 @@ import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.*; import org.spongepowered.asm.mixin.injection.callback.*;
@Mixin(LivingEntity.class) @Mixin(LivingEntity.class)
public abstract class CarryGlideMixin { public abstract class CarryGlideMixin {
@ModifyVariable(method="travelInAir",at=@At("HEAD"),argsOnly=true)
private Vec3 sanctuary$panicSteering(Vec3 input){return (Object)this instanceof Player p&&fr.koka.sanctuary.cosmetics.CarryPanic.active(p)?Vec3.ZERO:input;}
@Inject(method="travelInAir",at={@At("HEAD"),@At("TAIL")}) @Inject(method="travelInAir",at={@At("HEAD"),@At("TAIL")})
private void sanctuary$glide(Vec3 input,CallbackInfo ci){if((Object)this instanceof Player p)CarryGlide.tick(p);} private void sanctuary$glide(Vec3 input,CallbackInfo ci){if((Object)this instanceof Player p)CarryGlide.tick(p);}
@Inject(method="getEffectiveGravity",at=@At("RETURN"),cancellable=true) @Inject(method="getEffectiveGravity",at=@At("RETURN"),cancellable=true)
@@ -16,6 +16,6 @@ public abstract class CarryLivingMixin {
} }
@Inject(method="hurtServer",at=@At("HEAD"),cancellable=true) @Inject(method="hurtServer",at=@At("HEAD"),cancellable=true)
private void sanctuary$protectPair(ServerLevel level,DamageSource source,float damage,CallbackInfoReturnable<Boolean> ci){ private void sanctuary$protectPair(ServerLevel level,DamageSource source,float damage,CallbackInfoReturnable<Boolean> ci){
if(CarryService.pair((Entity)(Object)this,source.getEntity()))ci.setReturnValue(false); if(CarryService.blocksAttack(source.getEntity(),(Entity)(Object)this))ci.setReturnValue(false);
} }
} }
@@ -16,5 +16,5 @@ public abstract class CarryPlayerMixin {
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())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) @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();} private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.blocksAttack((Entity)(Object)this,target))ci.cancel();}
} }
@@ -15,7 +15,8 @@ public abstract class BoatPassengerPickMixin {
at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/Entity;getRootVehicle()Lnet/minecraft/world/entity/Entity;")) at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/Entity;getRootVehicle()Lnet/minecraft/world/entity/Entity;"))
private static Entity sanctuary$pickSeatedMob(Entity candidate, Operation<Entity> original, Entity source) { private static Entity sanctuary$pickSeatedMob(Entity candidate, Operation<Entity> original, Entity source) {
// Only the player's cursor: projectile collisions and native range/occlusion stay unchanged. // Only the player's cursor: projectile collisions and native range/occlusion stay unchanged.
return source instanceof LocalPlayer && BoatPassengerInteractions.aboardTogether(source,candidate) return source instanceof LocalPlayer && (BoatPassengerInteractions.aboardTogether(source,candidate)
||fr.koka.sanctuary.cosmetics.CarryPanic.canStrike(source,candidate))
? candidate : original.call(candidate); ? candidate : original.call(candidate);
} }
} }
@@ -1013,6 +1013,7 @@
"sanctuary.companion.rename_hint": "Rename its egg at an anvil, or use a name tag on your familiar.", "sanctuary.companion.rename_hint": "Rename its egg at an anvil, or use a name tag on your familiar.",
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 is required on both client and server.", "sanctuary.companion.interactions_version_required": "Sanctuary beta.020 is required on both client and server.",
"sanctuary.carry.unavailable": "No free space to carry or put down here.", "sanctuary.carry.unavailable": "No free space to carry or put down here.",
"sanctuary.carry.panic": "The animal panics and carries you away!",
"sanctuary.companion.tooltip_size": "Size: %s", "sanctuary.companion.tooltip_size": "Size: %s",
"sanctuary.companion.size.tiny": "Tiny", "sanctuary.companion.size.tiny": "Tiny",
"sanctuary.companion.size.small": "Small", "sanctuary.companion.size.small": "Small",
@@ -1013,6 +1013,7 @@
"sanctuary.companion.rename_hint": "Renomme son œuf à lenclume, ou utilise une étiquette sur lui.", "sanctuary.companion.rename_hint": "Renomme son œuf à lenclume, ou utilise une étiquette sur lui.",
"sanctuary.companion.interactions_version_required": "Sanctuary beta.020 est requis côté client et serveur.", "sanctuary.companion.interactions_version_required": "Sanctuary beta.020 est requis côté client et serveur.",
"sanctuary.carry.unavailable": "Pas de place libre pour porter ou déposer ici.", "sanctuary.carry.unavailable": "Pas de place libre pour porter ou déposer ici.",
"sanctuary.carry.panic": "Lanimal saffole et vous entraîne !",
"sanctuary.companion.tooltip_size": "Taille : %s", "sanctuary.companion.tooltip_size": "Taille : %s",
"sanctuary.companion.size.tiny": "Minuscule", "sanctuary.companion.size.tiny": "Minuscule",
"sanctuary.companion.size.small": "Petit", "sanctuary.companion.size.small": "Petit",
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary" name = "Sanctuary"
author = "KOKA99CAB" author = "KOKA99CAB"
version = "beta.113" version = "beta.114"
pack-format = "packwiz:1.1.0" pack-format = "packwiz:1.1.0"
[index] [index]