Record completed beta.061 release and finish Git synchronization
Build Sanctuary / build (push) Canceled after 0s
Build Sanctuary / build (push) Canceled after 0s
This commit is contained in:
@@ -114,6 +114,9 @@ tasks.named('processGametestResources') {
|
||||
if (skillsClient) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.Skills025ClientChecks') }
|
||||
def fogClient = providers.gradleProperty('sanctuaryFogClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def sky056Client = providers.gradleProperty('sanctuarySky056ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
def arrivalMount061Client = providers.gradleProperty('sanctuaryArrivalMount061ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('arrivalMount061Client', arrivalMount061Client)
|
||||
if (arrivalMount061Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.ArrivalMount061ClientChecks') }
|
||||
def mobHead060Client = providers.gradleProperty('sanctuaryMobHead060ClientTests').map { it.toBoolean() }.getOrElse(false)
|
||||
inputs.property('mobHead060Client', mobHead060Client)
|
||||
if (mobHead060Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.gametest.MobHead060ClientChecks') }
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka.sanctuary.client;
|
||||
|
||||
import fr.koka.sanctuary.cosmetics.*;
|
||||
import fr.koka.sanctuary.gametest.Starter039TestSupport;
|
||||
import fr.koka.sanctuary.inventory.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.resources.sounds.SoundInstance;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.contents.TranslatableContents;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.*;
|
||||
import net.minecraft.world.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.item.*;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/** Real audio stream continuity and native player input across integrated-server mount physics. */
|
||||
public final class ArrivalMount061ClientChecks implements FabricClientGameTest {
|
||||
public void runTest(ClientGameTestContext c){
|
||||
var watching=new AtomicBoolean(true);var started=new AtomicBoolean();var submitted=new AtomicBoolean();
|
||||
var sound=new AtomicReference<SoundInstance>();var white=new AtomicBoolean();var worldSeen=new AtomicBoolean();
|
||||
c.runOnClient(m->{m.options.getSoundSourceOptionInstance(SoundSource.MASTER).set(1.0);m.options.getSoundSourceOptionInstance(SoundSource.MUSIC).set(.3);});
|
||||
ClientTickEvents.END_CLIENT_TICK.register(m->{
|
||||
if(!watching.get())return;
|
||||
if(m.gui.screen() instanceof HelloWorldScreen screen&&submitted.compareAndSet(false,true)){
|
||||
IntroClient.setQuickTestIntro(false);Starter039TestSupport.selectFirst(screen);
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter());
|
||||
screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());
|
||||
}
|
||||
if(m.gui.screen() instanceof IntroScreen intro){
|
||||
if(intro.waitingForMusic())check(music(m)==null,"No Minecraft music while letters reveal");
|
||||
else if(!intro.isFinished()&&started.compareAndSet(false,true)){
|
||||
sound.set(music(m));check(sound.get()!=null,"Portal reveal starts native music");
|
||||
}
|
||||
}
|
||||
if(started.get()){
|
||||
check(sound.get()==music(m),"The same music instance survives intro and world handoff");
|
||||
check(m.getSoundManager().isActive(sound.get()),"Music channel remains active through handoff");
|
||||
if(IntroSpawnFlash.active())white.set(true);
|
||||
if(m.level!=null&&m.player!=null)worldSeen.set(true);
|
||||
}
|
||||
});
|
||||
try(var world=c.worldBuilder().adjustSettings(s->{s.setName("ArrivalMount061 "+UUID.randomUUID());s.setSeed("42");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){
|
||||
world.getConnection().waitForChunksDownload(2400);c.waitFor(m->m.player!=null&&m.gui.screen()==null&&ProgressionClient.active()&&!IntroSpawnFlash.active(),1600);
|
||||
c.waitTicks(40);watching.set(false);check(started.get()&&white.get()&&worldSeen.get(),"Actual cinematic, white flash and world were all exercised");
|
||||
System.out.println("ARRIVAL061_MUSIC_PASS: same active streamed SoundInstance before, during and after first world");
|
||||
var server=world.getServer();
|
||||
server.runOnServer(s->{var p=player(s);p.getAbilities().invulnerable=true;p.getInventory().clearContent();p.setItemInHand(InteractionHand.MAIN_HAND,ItemStack.EMPTY);AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);
|
||||
p.teleportTo(0.5,1,0.5);var origin=p.blockPosition();for(int x=-12;x<=12;x++)for(int z=-12;z<=12;z++)for(int y=-1;y<=10;y++)p.level().setBlockAndUpdate(origin.offset(x,y,z),(y==-1?Blocks.STONE:Blocks.AIR).defaultBlockState());
|
||||
double base=p.getAttributeValue(Attributes.MOVEMENT_SPEED);var pet=FamiliarService.TYPE.create(p.level(),EntitySpawnReason.COMMAND);
|
||||
for(var item:List.of(Items.COW_SPAWN_EGG,Items.ENDER_DRAGON_SPAWN_EGG))for(int size:new int[]{120,350,650,849,1000,1500,2499}){
|
||||
pet.bind(p,egg(item,size));pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Carry familiar size "+size);
|
||||
double expected=size<=350?1:size==1000?.75:FamiliarSize.carrySpeed(pet.egg());check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-expected)<.00001,"Size changes real movement attribute even on baby models");
|
||||
CarryService.release(pet,true);check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Release restores unladen speed");
|
||||
}pet.discard();
|
||||
var baby=EntityTypes.COW.create(p.level(),EntitySpawnReason.COMMAND);baby.setBaby(true);baby.setPos(p.position().add(0,0,2));check(CarryService.interact(p,baby),"Native baby still carries");check(Math.abs(p.getAttributeValue(Attributes.MOVEMENT_SPEED)/base-1)<.00001,"Native baby remains weightless");CarryService.release(baby,true);baby.discard();
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COW_SPAWN_EGG,3500));p.inventoryMenu.broadcastChanges();
|
||||
});
|
||||
c.waitTicks(40);
|
||||
int id=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Real equipped egg spawns colossal familiar");pet.setPos(p.position().add(0,0,2));pet.setNoAi(true);return pet.getId();});
|
||||
c.waitFor(m->m.level.getEntity(id)!=null&&m.player.getMainHandItem().isEmpty(),200);
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(5);click(c,id);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet&&!CarryService.carried(pet),"Actual sneak interaction mounts, never carries, a colossal");check(pet.getBbHeight()>.7&&pet.getControllingPassenger()==null,"Mount uses rendered scale and server authority");pet.setNoAi(false);});
|
||||
c.getInput().releaseKey(o->o.keyShift);c.waitTicks(5);c.getInput().lookAt(0,0);
|
||||
var before=server.computeOnServer(s->FamiliarService.active(player(s)).position());
|
||||
c.getInput().holdKey(o->o.keyUp);c.waitTicks(25);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Mount survives released sneak");check(pet.getZ()>before.z+1,"Forward input moves the actual server mount");check(Math.abs(pet.getX()-before.x)<1,"Movement follows rider yaw");});
|
||||
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);});c.waitTicks(3);c.takeScreenshot("sanctuary-beta061-colossal-mount");
|
||||
c.getInput().holdKey(o->o.keyShift);c.waitTicks(8);c.getInput().releaseKey(o->o.keyShift);
|
||||
server.runOnServer(s->{var p=player(s);check(!p.isPassenger(),"Release then sneak dismounts normally");var pet=FamiliarService.active(p);pet.setNoAi(true);pet.setPos(p.position().add(0,0,2));p.setShiftKeyDown(false);check(CarryService.interact(p,pet),"Remount: actor="+p.position()+" pet="+pet.position()+" asleep="+p.isSleeping()+" lying="+fr.koka.sanctuary.progression.LyingService.active(p)+" ground="+p.onGround()+" removed="+pet.isRemoved()+" alive="+pet.isAlive()+" rider="+pet.getFirstPassenger()+" line="+p.hasLineOfSight(pet));
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);check(!p.isPassenger()&&pet.isRemoved(),"Removing egg safely detaches player and despawns mount");
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.ALLAY_SPAWN_EGG,6000));});
|
||||
c.waitTicks(30);
|
||||
server.runOnServer(s->{var p=player(s);p.setShiftKeyDown(false);var pet=FamiliarService.active(p);check(pet!=null,"Flying mount spawned");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Colossal allay is rideable");});c.waitTicks(5);
|
||||
double ground=server.computeOnServer(s->FamiliarService.active(player(s)).getY());c.getInput().holdKey(o->o.keyJump);c.waitTicks(20);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(8);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>ground+2,"Space lifts native flying familiar");check(p.getVehicle()==pet,"Flying pilot stays attached");});
|
||||
double peak=server.computeOnServer(s->FamiliarService.active(player(s)).getY());
|
||||
c.getInput().lookAt(0,65);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()<peak-1,"Looking down and moving descends");});
|
||||
int ceiling=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);int y=(int)Math.ceil(p.getBoundingBox().maxY)+1;var at=pet.blockPosition();for(int x=-4;x<=4;x++)for(int z=-4;z<=4;z++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(at.getX()+x,y,at.getZ()+z),Blocks.STONE.defaultBlockState());return y;});
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(40);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(5);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(p.getVehicle()==pet,"Ceiling stops mount without ejecting its rider");check(p.getBoundingBox().maxY<=ceiling+.03&&p.level().noBlockCollision(p,p.getBoundingBox().deflate(.02)),"Rider cannot be driven through a low ceiling");
|
||||
pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);p.teleportTo(.5,1,.5);
|
||||
for(int x=4;x<=12;x++)for(int z=-1;z<=7;z++)for(int y=1;y<=8;y++)if(x==4||x==12||z==-1||z==7)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.STONE.defaultBlockState());
|
||||
for(int x=5;x<=11;x++)for(int z=0;z<=6;z++)for(int y=1;y<=7;y++)p.level().setBlockAndUpdate(new net.minecraft.core.BlockPos(x,y,z),Blocks.WATER.defaultBlockState());
|
||||
AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,egg(Items.COD_SPAWN_EGG,6000));});c.waitTicks(30);
|
||||
var land=server.computeOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet!=null,"Aquatic colossal spawns on dry land");pet.setPos(p.position().add(0,0,2));check(CarryService.interact(p,pet),"Aquatic colossal is rideable");return pet.position();});
|
||||
c.getInput().lookAt(0,0);c.getInput().holdKey(o->o.keyUp);c.waitTicks(15);c.getInput().releaseKey(o->o.keyUp);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(!pet.isInWater(),"Dry-land measurement is outside the contained pool");check(pet.position().distanceTo(land)<2,"Aquatic mount moves slowly on land: "+pet.position().distanceTo(land));pet.teleportTo(8.5,2,2.5);pet.setDeltaMovement(Vec3.ZERO);pet.positionRider(p);});c.waitTicks(10);
|
||||
double wet=server.computeOnServer(s->{var pet=FamiliarService.active(player(s));check(pet.isInWater(),"Aquatic mount actually enters water");return pet.getY();});
|
||||
c.getInput().holdKey(o->o.keyJump);c.waitTicks(15);c.getInput().releaseKey(o->o.keyJump);c.waitTicks(3);
|
||||
server.runOnServer(s->{var p=player(s);var pet=FamiliarService.active(p);check(pet.getY()>wet+1,"Aquatic mount swims upward under native input");pet.ejectPassengers();AccessoryAccess.of(p).setItem(AccessoryContainer.FAMILIAR,ItemStack.EMPTY);FamiliarService.tick(p);});
|
||||
c.runOnClient(m->{m.options.setCameraType(net.minecraft.client.CameraType.FIRST_PERSON);m.getSoundManager().stop();});c.waitTicks(3);
|
||||
c.runOnClient(m->check(!m.getSoundManager().isActive(sound.get()),"Explicit sound stop remains effective outside arrival"));
|
||||
}finally{watching.set(false);}
|
||||
System.out.println("ARRIVAL_MOUNT061_PASS: audio continuity, size weights, native babies, real mount input, dismount, removal and flight");
|
||||
}
|
||||
private static SoundInstance music(net.minecraft.client.Minecraft m){try{var f=m.getMusicManager().getClass().getDeclaredField("currentMusic");f.setAccessible(true);return (SoundInstance)f.get(m.getMusicManager());}catch(ReflectiveOperationException e){throw new AssertionError(e);}}
|
||||
private static ServerPlayer player(net.minecraft.server.MinecraftServer s){return s.getPlayerList().getPlayers().getFirst();}
|
||||
private static ItemStack egg(Item item,int value){var stack=new ItemStack(item);var size=new CompoundTag();size.putInt("schema",1);size.putInt("per_mille",value);CustomData.update(DataComponents.CUSTOM_DATA,stack,t->t.put(FamiliarSize.KEY,size));return stack;}
|
||||
private static KeyEvent enter(){return new KeyEvent(com.mojang.blaze3d.platform.InputConstants.KEY_RETURN,0,0);}
|
||||
private static void click(ClientGameTestContext c,int id){c.runOnClient(m->{var e=m.level.getEntity(id);m.gameMode.interact(m.player,e,new EntityHitResult(e,e.getEyePosition()),InteractionHand.MAIN_HAND);});}
|
||||
private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public final class Carry027GameTests {
|
||||
}finally{online.removeAll(added);cleanup(p);cow.discard();}h.succeed();
|
||||
}
|
||||
|
||||
@GameTest public void familiarBabyAppearanceAddsNoWeightButMiniatureAdultsDo(GameTestHelper h){
|
||||
@GameTest public void familiarWithoutSizeUsesOrdinaryWeightRegardlessOfBabyAppearance(GameTestHelper h){
|
||||
var p=players(h,2);double base=speed(p[0]);
|
||||
var pet=fr.koka.sanctuary.cosmetics.FamiliarService.TYPE.create(h.getLevel(),EntitySpawnReason.COMMAND);
|
||||
try{
|
||||
@@ -59,7 +59,7 @@ public final class Carry027GameTests {
|
||||
boolean baby=egg!=net.minecraft.world.item.Items.ENDER_DRAGON_SPAWN_EGG;
|
||||
h.assertTrue(pet.babyAppearance()==baby,"Familiar uses the same baby support as its native model");
|
||||
h.assertTrue(CarryService.interact(p[1],pet),"The familiar can be carried");
|
||||
ratio(h,p[0],base,baby?.75:.5625);ratio(h,p[1],base,baby?1:.75);
|
||||
ratio(h,p[0],base,.5625);ratio(h,p[1],base,.75);
|
||||
h.assertTrue(CarryService.release(pet,true),"The same familiar is put down");
|
||||
ratio(h,p[0],base,.75);ratio(h,p[1],base,1);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ public final class CompanionTooltips {
|
||||
if(xp>0)extra.add(fr.koka.sanctuary.familiar.FamiliarBattle.label("xp.tooltip",fr.koka.sanctuary.familiar.FamiliarLevels.level(xp),xp).copy().withStyle(ChatFormatting.GREEN));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(CompanionClient.label("tooltip_size",
|
||||
CompanionClient.label("size."+fr.koka.sanctuary.cosmetics.FamiliarSize.category(stack))).copy().withStyle(ChatFormatting.GRAY));
|
||||
if(fr.koka.sanctuary.cosmetics.FamiliarSize.value(stack)>0)extra.add(Component.translatable(
|
||||
fr.koka.sanctuary.cosmetics.FamiliarSize.colossal(stack)?"sanctuary.familiar.mount.hint":"sanctuary.familiar.carry.hint",
|
||||
Math.round(fr.koka.sanctuary.cosmetics.FamiliarSize.carrySpeed(stack)*100)).withStyle(ChatFormatting.GRAY));
|
||||
var profile=fr.koka.sanctuary.familiar.FamiliarCatalog.bundled().forEgg(stack);
|
||||
if(profile!=null){
|
||||
if(FamiliarClient.profile!=null&&FamiliarClient.profile.id().equals(profile.id()))profile=FamiliarClient.profile;
|
||||
|
||||
@@ -90,8 +90,7 @@ public final class FamiliarRenderer extends EntityRenderer<FamiliarEntity,Famili
|
||||
}
|
||||
state.renderer=renderer;state.appearance=appearance;
|
||||
if(fr.koka.sanctuary.cosmetics.CarryService.carried(pet))CarryPose.apply(appearance);
|
||||
state.size=Math.min(0.65F,Math.min(0.85F/Math.max(0.1F,mob.getBbHeight()),0.85F/Math.max(0.1F,mob.getBbWidth())));
|
||||
state.size*=FamiliarSize.multiplier(pet.egg());
|
||||
state.size=FamiliarSize.renderScale(mob,pet.egg());
|
||||
float height=mob.getBbHeight()*state.size;
|
||||
state.visualRadius=Math.max(mob.getBbWidth()*state.size,height)*.5F;
|
||||
if(state.nameTagAttachment!=null)state.nameTagAttachment=new net.minecraft.world.phys.Vec3(0,height+.2,0);
|
||||
|
||||
@@ -6,13 +6,16 @@ import net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationNetworkin
|
||||
|
||||
public final class IntroClient {
|
||||
private static boolean previewRequested;
|
||||
private static boolean arrivalMusic;
|
||||
static void keepArrivalMusic(){arrivalMusic=true;}
|
||||
public static boolean consumeArrivalMusic(){boolean keep=arrivalMusic;arrivalMusic=false;return keep;}
|
||||
private static boolean quickTestIntro;
|
||||
public static void setQuickTestIntro(boolean quick) { quickTestIntro=quick; }
|
||||
private IntroClient() { }
|
||||
public static void register() {
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.INIT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientConfigurationConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents.DISCONNECT.register((handler,client) -> { quickTestIntro=false;arrivalMusic=false;IntroSpawnFlash.clear(); });
|
||||
net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (!previewRequested) return;
|
||||
previewRequested=false;
|
||||
|
||||
@@ -29,7 +29,7 @@ public final class IntroScreen extends Screen {
|
||||
private long started=-1;
|
||||
private int nextBeat;
|
||||
private net.minecraft.client.renderer.texture.TextureAtlasSprite portalSprite;
|
||||
private boolean rumble, whisper, portal, finished, skipped;
|
||||
private boolean rumble, whisper, portal, finished, skipped, musicStarted;
|
||||
|
||||
public IntroScreen(UUID uuid,java.util.function.Consumer<Boolean> complete) {
|
||||
this(uuid,complete,System::nanoTime);
|
||||
@@ -41,14 +41,18 @@ public final class IntroScreen extends Screen {
|
||||
}
|
||||
public UUID uuid() { return uuid; }
|
||||
boolean isFinished() { return finished; }
|
||||
public boolean waitingForMusic() { return !finished&&!musicStarted; }
|
||||
@Override public net.minecraft.sounds.Music getBackgroundMusic() { return musicStarted?net.minecraft.sounds.Musics.GAME:null; }
|
||||
protected void init() {
|
||||
if (started < 0) started=clock.getAsLong();
|
||||
if (started < 0) {started=clock.getAsLong();minecraft.getMusicManager().stopPlaying();}
|
||||
}
|
||||
private double seconds() { return started < 0 ? 0 : Math.max(0,(clock.getAsLong()-started)/1_000_000_000.0); }
|
||||
public void tick() {
|
||||
if (finished) return;
|
||||
double time=seconds();
|
||||
minecraft.getMusicManager().stopPlaying();
|
||||
if(!musicStarted&&time>=IntroSequence.PORTAL_REVEAL){
|
||||
musicStarted=true;minecraft.getMusicManager().startPlaying(net.minecraft.sounds.Musics.GAME);
|
||||
}
|
||||
if (!rumble && time>=0.7) {
|
||||
rumble=true;
|
||||
play(SoundEvents.AMBIENT_SOUL_SAND_VALLEY_LOOP.value(),0.32f,0.65f,true);
|
||||
@@ -126,7 +130,7 @@ public final class IntroScreen extends Screen {
|
||||
double opacity=IntroSequence.smooth(progress/0.06)*(1-IntroSequence.smooth((progress-0.93)/0.07));
|
||||
glyph(graphics,i,x,y,unit*(float)(0.65+point.radius()*5.5),opacity,0xD1B9F1);
|
||||
}
|
||||
double aperture=IntroSequence.smooth((time-23.5)/5.5);
|
||||
double aperture=IntroSequence.smooth((time-IntroSequence.PORTAL_REVEAL)/5.5);
|
||||
// The destination moves toward the viewer until the doorway exceeds the entire viewport.
|
||||
int extent=(int)(3*unit+Math.max(width,height)*0.68*Math.pow(aperture,2.2));
|
||||
for(int glow=8;glow>=1;glow--) {
|
||||
@@ -164,7 +168,9 @@ public final class IntroScreen extends Screen {
|
||||
}
|
||||
private void finish(boolean skipped) {
|
||||
if (finished) return;
|
||||
finished=true;this.skipped=skipped;stopSounds();complete.accept(skipped);
|
||||
finished=true;this.skipped=skipped;stopSounds();
|
||||
if(musicStarted&&minecraft.level==null)IntroClient.keepArrivalMusic();
|
||||
complete.accept(skipped);
|
||||
}
|
||||
private void stopSounds() {
|
||||
for (var sound:sounds) { sound.close();minecraft.getSoundManager().stop(sound); }
|
||||
|
||||
@@ -40,7 +40,9 @@ public final class CarryService {
|
||||
public static boolean marked(Entity e){return e.getAttachedOrElse(CARRIED,false);}
|
||||
public static boolean carried(Entity e){return marked(e)&&e.getVehicle() instanceof Player;}
|
||||
public static boolean carrying(Entity e){return e instanceof Player&&e.getPassengers().stream().anyMatch(CarryService::carried);}
|
||||
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(carried(a)||carried(b))&&root(a)==root(b);}
|
||||
public static boolean pair(Entity a,Entity b){return a!=null&&b!=null&&a!=b&&(
|
||||
(carried(a)||carried(b))&&root(a)==root(b)
|
||||
||(a instanceof FamiliarEntity||b instanceof FamiliarEntity)&&a.getRootVehicle()==b.getRootVehicle());}
|
||||
private static Entity root(Entity e){while(carried(e))e=e.getVehicle();return e;}
|
||||
/** One seat per head; the chain includes babies even though they add no carrying penalty. */
|
||||
private static Entity passenger(Entity e){
|
||||
@@ -73,6 +75,7 @@ public final class CarryService {
|
||||
for(var rider:new ArrayList<>(RIDES.keySet())){
|
||||
var ride=RIDES.get(rider);if(ride==null||ride.carrier.level().getServer()!=s)continue;
|
||||
var carrier=ride.carrier;
|
||||
if(rider instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg())){release(rider,true);continue;}
|
||||
if(!carried(rider)||rider.getVehicle()!=carrier||!rider.isAlive()||!active(carrier)||rider.level()!=carrier.level()
|
||||
||!validCarrierChain(carrier)){
|
||||
release(rider,true);continue;
|
||||
@@ -102,6 +105,7 @@ public final class CarryService {
|
||||
public static boolean interact(ServerPlayer actor,Entity target){
|
||||
if(!active(actor)||target.level()!=actor.level()||!target.isAlive()||!actor.isWithinEntityInteractionRange(target,0)||!actor.hasLineOfSight(target))return false;
|
||||
if(carried(target)&&target.getVehicle()==actor)return release(target,true);
|
||||
if(target instanceof FamiliarEntity pet&&FamiliarSize.colossal(pet.egg()))return pet.mount(actor);
|
||||
ServerPlayer carrier;Entity rider;
|
||||
if(target instanceof ServerPlayer player){carrier=player;rider=actor;}
|
||||
else if(animal(target)){carrier=actor;rider=target;}
|
||||
@@ -213,9 +217,10 @@ public final class CarryService {
|
||||
}
|
||||
private static void speed(ServerPlayer p){
|
||||
var attribute=p.getAttribute(Attributes.MOVEMENT_SPEED);if(attribute==null)return;
|
||||
int players=0,animals=0;
|
||||
int players=0;double animals=0;
|
||||
for(Entity e=passenger(p);e!=null;e=passenger(e)){
|
||||
if(e instanceof Player)players++;
|
||||
else if(e instanceof FamiliarEntity pet)animals+=Math.log(FamiliarSize.carrySpeed(pet.egg()))/Math.log(1-SLOWDOWN);
|
||||
else if(!baby(e))animals++;
|
||||
}
|
||||
var strength=p.getEffect(net.minecraft.world.effect.MobEffects.STRENGTH);
|
||||
|
||||
@@ -27,6 +27,7 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
private int motionMode=-1;
|
||||
private int recoilUntil;
|
||||
private Mob voice;
|
||||
private boolean mountShiftReleased;
|
||||
public FamiliarEntity(EntityType<? extends PathfinderMob> type,Level level) {
|
||||
super(type,level);setPermanentlyInvulnerable(true);setPersistenceRequired();setCanPickUpLoot(false);
|
||||
getNavigation().setCanFloat(true);
|
||||
@@ -41,7 +42,96 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
public ServerPlayer owner() {
|
||||
return owner;
|
||||
}
|
||||
public boolean mount(ServerPlayer player){
|
||||
if(player!=owner()||!FamiliarSize.colossal(egg())||isPassenger()||isVehicle()
|
||||
||player.isPassenger()||player.isVehicle()||player.isSleeping()||player.isFallFlying()
|
||||
||fr.koka.sanctuary.progression.LyingService.active(player)
|
||||
||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))return false;
|
||||
var at=position().add(0,mountHeight(),0).subtract(player.getVehicleAttachmentPoint(this));
|
||||
if(!mountRoom(player,at)||!player.startRiding(this,true,true))return false;
|
||||
mountShiftReleased=!player.isShiftKeyDown();getNavigation().stop();setTarget(null);setDeltaMovement(Vec3.ZERO);
|
||||
positionRider(player);
|
||||
player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetPassengersPacket(this));
|
||||
player.sendOverlayMessage(net.minecraft.network.chat.Component.translatable("sanctuary.familiar.mount.controls"));return true;
|
||||
}
|
||||
public boolean mayDismount(){return level().isClientSide()||mountShiftReleased;}
|
||||
private ServerPlayer pilot(){return getFirstPassenger() instanceof ServerPlayer p&&p==owner()?p:null;}
|
||||
@Override protected boolean canAddPassenger(Entity rider){return !isVehicle()&&FamiliarSize.colossal(egg())&&rider==owner();}
|
||||
// Like crew boats, native input packets drive the server simulation, never client vehicle positions.
|
||||
@Override public LivingEntity getControllingPassenger(){return null;}
|
||||
private double mountHeight(){return visualHeight()*.85;}
|
||||
public float visualHeight(){return prepareVoice()?voice.getBbHeight()*FamiliarSize.renderScale(voice,egg()):getBbHeight();}
|
||||
@Override protected Vec3 getPassengerAttachmentPoint(Entity rider,EntityDimensions dimensions,float scale){return new Vec3(0,mountHeight(),0);}
|
||||
@Override protected EntityDimensions getDefaultDimensions(Pose pose){
|
||||
// The colossal mount's collision shape follows the same miniature model as its renderer.
|
||||
if(FamiliarSize.colossal(egg())&&prepareVoice()){
|
||||
float scale=FamiliarSize.renderScale(voice,egg());
|
||||
return EntityDimensions.scalable(Math.max(.25F,voice.getBbWidth()*scale),Math.max(.3F,voice.getBbHeight()*scale));
|
||||
}
|
||||
return super.getDefaultDimensions(pose);
|
||||
}
|
||||
@Override public void onSyncedDataUpdated(EntityDataAccessor<?> key){
|
||||
super.onSyncedDataUpdated(key);if(EGG.equals(key))refreshDimensions();
|
||||
}
|
||||
private boolean mountRoom(LivingEntity rider,Vec3 at){
|
||||
var box=rider.getDimensions(Pose.STANDING).makeBoundingBox(at).deflate(.01);
|
||||
if(!level().getWorldBorder().isWithinBounds(box)||box.minY<level().getMinY()||box.maxY>level().getMaxY())return false;
|
||||
for(int x=((int)Math.floor(box.minX))>>4;x<=((int)Math.floor(box.maxX))>>4;x++)
|
||||
for(int z=((int)Math.floor(box.minZ))>>4;z<=((int)Math.floor(box.maxZ))>>4;z++)if(!level().hasChunk(x,z))return false;
|
||||
return level().noBlockCollision(rider,box);
|
||||
}
|
||||
@Override public Vec3 getDismountLocationForPassenger(LivingEntity rider){
|
||||
int edge=(int)Math.ceil(getBbWidth()/2+rider.getBbWidth()/2);
|
||||
for(int radius=Math.max(1,edge);radius<=edge+3;radius++)for(int dy:new int[]{0,1,-1,2,-2})
|
||||
for(int x=-radius;x<=radius;x++)for(int z=-radius;z<=radius;z++){
|
||||
if(Math.max(Math.abs(x),Math.abs(z))!=radius)continue;
|
||||
var at=position().add(x,dy+.05,z);var below=BlockPos.containing(at).below();
|
||||
if(mountRoom(rider,at)&&level().getFluidState(BlockPos.containing(at)).isEmpty()
|
||||
&&!level().getBlockState(below).getCollisionShape(level(),below).isEmpty())return at;
|
||||
}
|
||||
return rider.position();
|
||||
}
|
||||
@Override public void travel(Vec3 input){
|
||||
var player=pilot();
|
||||
if(player==null){super.travel(input);return;}
|
||||
var keys=player.getLastClientInput();
|
||||
setYRot(player.getYRot());setXRot(player.getXRot()*.5F);yBodyRot=getYRot();yHeadRot=getYRot();
|
||||
double forward=(keys.forward()?1:0)-(keys.backward()?1:0),side=(keys.left()?1:0)-(keys.right()?1:0);
|
||||
if(forward<0)forward*=.5;
|
||||
boolean spatial=motion().flies()||motion().swims()&&isInWater();
|
||||
if(spatial){
|
||||
double pitch=Math.toRadians(player.getXRot());
|
||||
input=new Vec3(side,keys.jump()?1:-Math.sin(pitch)*forward,Math.cos(pitch)*forward);
|
||||
if(input.lengthSqr()>1)input=input.normalize();
|
||||
// Bounded acceleration/inertia, native swept block collisions and a full stop at rest.
|
||||
var desired=getInputVector(input,.32F,getYRot());
|
||||
setDeltaMovement(getDeltaMovement().lerp(desired,.3));
|
||||
move(MoverType.SELF,getDeltaMovement());setDeltaMovement(getDeltaMovement().scale(.9));resetFallDistance();player.resetFallDistance();
|
||||
}else{
|
||||
setSpeed(motion()==FamiliarMotion.AQUATIC?.05F:.28F);
|
||||
if(onGround()&&(keys.jump()||motion().hops()&&tickCount%14==0&&(side!=0||forward!=0)))jumpFromGround();
|
||||
input=new Vec3(side,0,forward);if(input.lengthSqr()>1)input=input.normalize();
|
||||
super.travel(input);
|
||||
}
|
||||
}
|
||||
/** A rider's head also collides; tall mounts cannot carry their player through a ceiling. */
|
||||
@Override public void move(MoverType type,Vec3 delta){
|
||||
var player=pilot();
|
||||
if(player!=null&&!mountRoom(player,player.position().add(delta))){
|
||||
double x=mountRoom(player,player.position().add(delta.x,0,0))?delta.x:0;
|
||||
double y=mountRoom(player,player.position().add(x,delta.y,0))?delta.y:0;
|
||||
double z=mountRoom(player,player.position().add(x,y,delta.z))?delta.z:0;
|
||||
delta=new Vec3(x,y,z);setDeltaMovement(delta);
|
||||
}
|
||||
super.move(type,delta);
|
||||
}
|
||||
@Override protected void registerGoals() {
|
||||
goalSelector.addGoal(-3,new Goal(){
|
||||
{setFlags(EnumSet.of(Flag.MOVE,Flag.LOOK,Flag.JUMP));}
|
||||
@Override public boolean canUse(){return isVehicle();}
|
||||
@Override public boolean requiresUpdateEveryTick(){return true;}
|
||||
@Override public void tick(){getNavigation().stop();setTarget(null);setSpeed(0);setXxa(0);setYya(0);setZza(0);}
|
||||
});
|
||||
goalSelector.addGoal(-2,new Goal(){
|
||||
{setFlags(EnumSet.of(Flag.MOVE,Flag.JUMP));}
|
||||
@Override public boolean canUse(){return tickCount<recoilUntil;}
|
||||
@@ -81,6 +171,14 @@ public final class FamiliarEntity extends PathfinderMob {
|
||||
}
|
||||
var player=owner();
|
||||
if(player==null || player.level()!=level || !player.isAlive() || player.isSpectator()) {discard();return;}
|
||||
if(isVehicle()){
|
||||
if(!FamiliarSize.colossal(egg())||pilot()==null||fr.koka.sanctuary.familiar.FamiliarDuels.fighting(player))ejectPassengers();
|
||||
else {
|
||||
if(!player.isShiftKeyDown())mountShiftReleased=true;getNavigation().stop();
|
||||
// Manual techniques retain their cast cadence while movement is driven by the rider.
|
||||
fr.koka.sanctuary.familiar.FamiliarTechniques.advance(this);return;
|
||||
}
|
||||
}
|
||||
if(CarryService.carried(this)){getNavigation().stop();return;}
|
||||
boolean obstructed=!level.noBlockCollision(this,getBoundingBox().deflate(0.02));
|
||||
if(distanceToSqr(player)>(fr.koka.sanctuary.familiar.FamiliarBattle.fighting(player)?24*24:144) || getY()<level.getMinY() || obstructed) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
|
||||
/** A permanent visual trait of one equipped egg, using native item components. */
|
||||
/** A permanent trait of one equipped egg, shared by its appearance, weight and mount. */
|
||||
public final class FamiliarSize {
|
||||
public static final String KEY="sanctuary:familiar_size";
|
||||
private FamiliarSize(){}
|
||||
@@ -31,6 +31,17 @@ public final class FamiliarSize {
|
||||
int value=number.intValue();return value>=120&&value<=6000?value:0;
|
||||
}
|
||||
public static float multiplier(ItemStack egg){int value=value(egg);return value==0?1:value/1000F;}
|
||||
public static boolean colossal(ItemStack egg){return value(egg)>=2500;}
|
||||
public static double carrySpeed(ItemStack egg){
|
||||
int size=value(egg);if(size==0)size=1000;
|
||||
if(size<=350)return 1;
|
||||
if(size<850)return 1-(size-350)*.1/500;
|
||||
if(size<=1000)return .9-(size-850)*.15/150;
|
||||
return Math.max(.25,.75-(size-1000)*.5/1500);
|
||||
}
|
||||
public static float renderScale(net.minecraft.world.entity.Mob model,ItemStack egg){
|
||||
return Math.min(.65F,Math.min(.85F/Math.max(.1F,model.getBbHeight()),.85F/Math.max(.1F,model.getBbWidth())))*multiplier(egg);
|
||||
}
|
||||
public static String category(ItemStack egg){
|
||||
int value=value(egg);
|
||||
return value==0?"unknown":value<=350?"tiny":value<850?"small":value<=1200?"ordinary":value<2500?"large":"colossal";
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.UUID;
|
||||
public final class IntroSequence {
|
||||
public static final int GLYPHS = 32;
|
||||
public static final double SPIRAL = 22.2;
|
||||
public static final double PORTAL_REVEAL = 23.5;
|
||||
public static final double PORTAL = 26.8;
|
||||
public static final double DURATION = 29.5;
|
||||
private IntroSequence() { }
|
||||
|
||||
@@ -14,7 +14,8 @@ import org.spongepowered.asm.mixin.injection.callback.*;
|
||||
public abstract class CarryEntityMixin {
|
||||
@ModifyExpressionValue(method="startRiding(Lnet/minecraft/world/entity/Entity;ZZ)Z",at=@At(value="INVOKE",target="Lnet/minecraft/world/entity/EntityType;canSerialize()Z"))
|
||||
private boolean sanctuary$playerCanCarry(boolean original,Entity vehicle,boolean force,boolean events){
|
||||
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this);
|
||||
return original||vehicle instanceof Player&&CarryService.marked((Entity)(Object)this)
|
||||
||vehicle instanceof FamiliarEntity pet&&pet.owner()==(Object)this&&FamiliarSize.colossal(pet.egg());
|
||||
}
|
||||
@Inject(method="removeVehicle",at=@At("TAIL"))
|
||||
private void sanctuary$finishCarry(CallbackInfo ci){
|
||||
|
||||
@@ -13,6 +13,7 @@ public abstract class CarryPlayerMixin {
|
||||
private void sanctuary$releaseThenSneak(CallbackInfoReturnable<Boolean> ci){
|
||||
var p=(Player)(Object)this;
|
||||
if(CarryService.carried(p)&&!CarryService.mayDismount(p))ci.setReturnValue(false);
|
||||
if(p.getVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity pet&&!pet.mayDismount())ci.setReturnValue(false);
|
||||
}
|
||||
@Inject(method="attack",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$noPassengerAttack(Entity target,CallbackInfo ci){if(CarryService.pair((Entity)(Object)this,target))ci.cancel();}
|
||||
|
||||
@@ -10,7 +10,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
@Mixin(ServerPlayer.class)
|
||||
public abstract class CarryServerPlayerMixin {
|
||||
@Inject(method="saveParentVehicle",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){if(CarryService.carried((ServerPlayer)(Object)this))ci.cancel();}
|
||||
private void sanctuary$omitTemporaryMount(ValueOutput output,CallbackInfo ci){
|
||||
var p=(ServerPlayer)(Object)this;
|
||||
if(CarryService.carried(p)||p.getRootVehicle() instanceof fr.koka.sanctuary.cosmetics.FamiliarEntity)ci.cancel();
|
||||
}
|
||||
@Inject(method="die",at=@At("HEAD"))
|
||||
private void sanctuary$detachOnDeath(CallbackInfo ci){CarryService.detach((ServerPlayer)(Object)this);}
|
||||
@Inject(method="teleport(Lnet/minecraft/world/level/portal/TeleportTransition;)Lnet/minecraft/server/level/ServerPlayer;",at=@At("HEAD"))
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka.sanctuary.client.IntroScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.sounds.MusicManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(MusicManager.class)
|
||||
abstract class IntroMusicManagerMixin {
|
||||
@Inject(method="tick",at=@At("HEAD"),cancellable=true)
|
||||
private void sanctuary$waitForPortal(CallbackInfo ci){
|
||||
if(Minecraft.getInstance().gui.screen() instanceof IntroScreen intro&&intro.waitingForMusic())ci.cancel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package fr.koka.sanctuary.mixin.client;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
import fr.koka.sanctuary.client.IntroScreen;
|
||||
import fr.koka.sanctuary.client.IntroSpawnFlash;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.sounds.SoundManager;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Keep the same music stream when the apparition hands off to its first world. */
|
||||
@Mixin(Minecraft.class)
|
||||
abstract class IntroMusicMixin {
|
||||
@WrapOperation(method="updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;Z)V",
|
||||
at=@At(value="INVOKE",target="Lnet/minecraft/client/sounds/SoundManager;stop()V"))
|
||||
private void sanctuary$continueArrivalMusic(SoundManager sounds,Operation<Void> original,ClientLevel next,boolean stopSounds) {
|
||||
var client=(Minecraft)(Object)this;
|
||||
boolean handoff=next!=null&&fr.koka.sanctuary.client.IntroClient.consumeArrivalMusic();
|
||||
if(next!=null && (handoff || IntroSpawnFlash.active() || client.gui.screen() instanceof IntroScreen)) {
|
||||
for(var source:SoundSource.values())if(source!=SoundSource.MUSIC)sounds.stop(null,source);
|
||||
} else original.call(sounds);
|
||||
}
|
||||
}
|
||||
@@ -1724,5 +1724,8 @@
|
||||
"sanctuary.mob_head.removed": "Mob head item retrieved.",
|
||||
"sanctuary.mob_head.occupied": "This mob already wears an item. Retrieve it with an empty hand first.",
|
||||
"sanctuary.mob_head.unreadable": "This mob’s head equipment data is unreadable and has been preserved.",
|
||||
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it."
|
||||
"sanctuary.mob_head.hint": "Sneak + right-click a mob: equip one item. Empty hand: retrieve it.",
|
||||
"sanctuary.familiar.mount.controls": "Mount: movement keys · Space: jump/rise · Sneak: dismount after releasing it.",
|
||||
"sanctuary.familiar.mount.hint": "Colossal: Sneak + empty-hand right-click to ride your familiar.",
|
||||
"sanctuary.familiar.carry.hint": "Carried movement speed: %s%%"
|
||||
}
|
||||
|
||||
@@ -1724,5 +1724,8 @@
|
||||
"sanctuary.mob_head.removed": "Objet récupéré sur le mob.",
|
||||
"sanctuary.mob_head.occupied": "Ce mob porte déjà un objet. Récupérez-le d’abord à main vide.",
|
||||
"sanctuary.mob_head.unreadable": "Les données de l’objet porté sont illisibles et ont été conservées.",
|
||||
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer."
|
||||
"sanctuary.mob_head.hint": "Maj + clic droit sur un mob : poser un objet. Main vide : le récupérer.",
|
||||
"sanctuary.familiar.mount.controls": "Monture : déplacement · Espace : saut/montée · Maj : descendre après relâchement.",
|
||||
"sanctuary.familiar.mount.hint": "Colossal : Maj + clic droit à main vide pour monter sur ton familier.",
|
||||
"sanctuary.familiar.carry.hint": "Vitesse en le portant : %s %%"
|
||||
}
|
||||
|
||||
@@ -219,7 +219,9 @@
|
||||
"client.ShaderSaturationMixin",
|
||||
"client.WeatherEnvironmentMixin",
|
||||
"client.VanillaLightMixin",
|
||||
"client.ButtonHoverTextMixin"
|
||||
"client.ButtonHoverTextMixin",
|
||||
"client.IntroMusicMixin",
|
||||
"client.IntroMusicManagerMixin"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
|
||||
Reference in New Issue
Block a user