From 1b9f7d85c04880a549d2c055540c5d53885c37b7 Mon Sep 17 00:00:00 2001 From: lordlogo2002 Date: Mon, 22 Dec 2025 20:01:56 +0100 Subject: [PATCH] Add PlayerJr entity and associated rendering and command features --- .../java/net/Chipperfluff/chipi/ChipiMod.java | 3 + .../client/entity/ModEntityRenderers.java | 1 + .../chipi/client/entity/PlayerJrRenderer.java | 53 ++ .../chipi/command/CommandHandler.java | 1 + .../chipi/command/SpawnJrCommand.java | 89 +++ .../chipi/entity/ModEntities.java | 10 + .../chipi/entity/PlayerJrEntity.java | 552 ++++++++++++++++++ 7 files changed, 709 insertions(+) create mode 100644 src/main/java/net/Chipperfluff/chipi/client/entity/PlayerJrRenderer.java create mode 100644 src/main/java/net/Chipperfluff/chipi/command/SpawnJrCommand.java create mode 100644 src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java diff --git a/src/main/java/net/Chipperfluff/chipi/ChipiMod.java b/src/main/java/net/Chipperfluff/chipi/ChipiMod.java index af6d378..627e5b7 100644 --- a/src/main/java/net/Chipperfluff/chipi/ChipiMod.java +++ b/src/main/java/net/Chipperfluff/chipi/ChipiMod.java @@ -9,6 +9,7 @@ import net.Chipperfluff.chipi.effect.ModEffects; import net.Chipperfluff.chipi.entity.ModEntities; import net.Chipperfluff.chipi.entity.SpawnLogic; import net.Chipperfluff.chipi.entity.MepEntity; +import net.Chipperfluff.chipi.entity.PlayerJrEntity; import net.Chipperfluff.chipi.item.ModItemGroups; import net.Chipperfluff.chipi.item.ModItems; import net.Chipperfluff.chipi.server.ChipiServerEvents; @@ -41,6 +42,8 @@ public class ChipiMod implements ModInitializer { FabricDefaultAttributeRegistry.register(ModEntities.MEP, MepEntity.createMepAttributes()); + FabricDefaultAttributeRegistry.register(ModEntities.PLAYER_JR, PlayerJrEntity.createAttributes()); + ChipiServerEvents.register(); } } diff --git a/src/main/java/net/Chipperfluff/chipi/client/entity/ModEntityRenderers.java b/src/main/java/net/Chipperfluff/chipi/client/entity/ModEntityRenderers.java index d388b34..4be8e84 100644 --- a/src/main/java/net/Chipperfluff/chipi/client/entity/ModEntityRenderers.java +++ b/src/main/java/net/Chipperfluff/chipi/client/entity/ModEntityRenderers.java @@ -7,5 +7,6 @@ public class ModEntityRenderers { public static void register() { EntityRendererRegistry.register(ModEntities.MEP, MepRenderer::new); + EntityRendererRegistry.register(ModEntities.PLAYER_JR, PlayerJrRenderer::new); } } diff --git a/src/main/java/net/Chipperfluff/chipi/client/entity/PlayerJrRenderer.java b/src/main/java/net/Chipperfluff/chipi/client/entity/PlayerJrRenderer.java new file mode 100644 index 0000000..29381ae --- /dev/null +++ b/src/main/java/net/Chipperfluff/chipi/client/entity/PlayerJrRenderer.java @@ -0,0 +1,53 @@ +package net.Chipperfluff.chipi.client.entity; + +import net.Chipperfluff.chipi.entity.PlayerJrEntity; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.BipedEntityRenderer; +import net.minecraft.client.render.entity.EntityRendererFactory; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.client.render.entity.model.EntityModelLayers; +import net.minecraft.client.util.DefaultSkinHelper; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; + +public class PlayerJrRenderer + extends BipedEntityRenderer> { + + public PlayerJrRenderer(EntityRendererFactory.Context ctx) { + super(ctx, new BipedEntityModel<>(ctx.getPart(EntityModelLayers.PLAYER)), 0.3f); + } + + @Override + public void render( + PlayerJrEntity entity, + float yaw, + float tickDelta, + MatrixStack matrices, + VertexConsumerProvider vertices, + int light + ) { + matrices.scale(0.5f, 0.5f, 0.5f); + super.render(entity, yaw, tickDelta, matrices, vertices, light); + } + + @Override + public Identifier getTexture(PlayerJrEntity entity) { + String dadName = entity.getDadName(); + + if (dadName == null || dadName.isBlank()) { + return DefaultSkinHelper.getTexture(); + } + + /* + * THIS is how vanilla + mods do it: + * - stable Identifier + * - async skin download + * - cached by SkinProvider + */ + Identifier skin = AbstractClientPlayerEntity.getSkinId(dadName); + AbstractClientPlayerEntity.loadSkin(skin, dadName); + + return skin; + } +} diff --git a/src/main/java/net/Chipperfluff/chipi/command/CommandHandler.java b/src/main/java/net/Chipperfluff/chipi/command/CommandHandler.java index a059710..1cb7076 100644 --- a/src/main/java/net/Chipperfluff/chipi/command/CommandHandler.java +++ b/src/main/java/net/Chipperfluff/chipi/command/CommandHandler.java @@ -12,6 +12,7 @@ public final class CommandHandler { (dispatcher, registryAccess, environment) -> { ChpCommand.register(dispatcher); CspCommand.register(dispatcher); + SpawnJrCommand.register(dispatcher); } ); } diff --git a/src/main/java/net/Chipperfluff/chipi/command/SpawnJrCommand.java b/src/main/java/net/Chipperfluff/chipi/command/SpawnJrCommand.java new file mode 100644 index 0000000..b9ceeb8 --- /dev/null +++ b/src/main/java/net/Chipperfluff/chipi/command/SpawnJrCommand.java @@ -0,0 +1,89 @@ +package net.Chipperfluff.chipi.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; + +import net.Chipperfluff.chipi.entity.ModEntities; +import net.Chipperfluff.chipi.entity.PlayerJrEntity; + +import net.minecraft.command.argument.EntityArgumentType; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; + +public final class SpawnJrCommand { + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + CommandManager.literal("spawnjr") + .requires(src -> src.hasPermissionLevel(2)) + .then( + CommandManager.argument("dad", EntityArgumentType.player()) + .executes(SpawnJrCommand::execute) + ) + ); + } + + private static int execute(CommandContext ctx) { + ServerCommandSource source = ctx.getSource(); + + System.out.println("[spawnjr] ================================"); + System.out.println("[spawnjr] Command invoked"); + + try { + ServerWorld world = source.getWorld(); + ServerPlayerEntity dad = EntityArgumentType.getPlayer(ctx, "dad"); + ServerPlayerEntity spawner = source.getPlayerOrThrow(); + + BlockPos pos = spawner.getBlockPos(); + + System.out.println("[spawnjr] Dad = " + dad.getName().getString()); + System.out.println("[spawnjr] Dad UUID = " + dad.getUuidAsString()); + System.out.println("[spawnjr] World = " + world.getRegistryKey().getValue()); + System.out.println("[spawnjr] Spawn pos = " + pos); + + PlayerJrEntity jr = new PlayerJrEntity(ModEntities.PLAYER_JR, world); + System.out.println("[spawnjr] Entity constructed"); + + jr.refreshPositionAndAngles( + pos.getX() + 0.5, + pos.getY(), + pos.getZ() + 0.5, + world.random.nextFloat() * 360f, + 0f + ); + + // single source of truth + jr.setDad(dad); + System.out.println("[spawnjr] Dad relationship set"); + + boolean spawned = world.spawnEntity(jr); + System.out.println("[spawnjr] spawnEntity() returned = " + spawned); + + if (!spawned) { + source.sendError(Text.literal("[spawnjr] Spawn failed (entity rejected by world)")); + return 0; + } + + System.out.println("[spawnjr] SUCCESS"); + System.out.println("[spawnjr] ================================"); + return 1; + + } catch (Exception e) { + System.out.println("[spawnjr] ================================"); + System.out.println("[spawnjr] CRASH"); + System.out.println("[spawnjr] hey message Chipperfluff"); + System.out.println("[spawnjr] Exception: " + e.getClass().getName()); + System.out.println("[spawnjr] Message: " + e.getMessage()); + e.printStackTrace(); + + source.sendError(Text.literal( + "[spawnjr] Internal error. Check logs. (hey message Chipperfluff)" + )); + return 0; + } + } +} diff --git a/src/main/java/net/Chipperfluff/chipi/entity/ModEntities.java b/src/main/java/net/Chipperfluff/chipi/entity/ModEntities.java index 43c1371..96fc9e3 100644 --- a/src/main/java/net/Chipperfluff/chipi/entity/ModEntities.java +++ b/src/main/java/net/Chipperfluff/chipi/entity/ModEntities.java @@ -23,6 +23,16 @@ public final class ModEntities { .build() ); + public static final EntityType PLAYER_JR = + Registry.register( + Registries.ENTITY_TYPE, + new Identifier("chipi", "player_jr"), + FabricEntityTypeBuilder + .create(SpawnGroup.CREATURE, PlayerJrEntity::new) + .dimensions(EntityDimensions.fixed(0.45f, 0.9f)) // small + .build() + ); + private ModEntities() { } } diff --git a/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java b/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java new file mode 100644 index 0000000..8387c70 --- /dev/null +++ b/src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java @@ -0,0 +1,552 @@ +package net.Chipperfluff.chipi.entity; + +import net.minecraft.entity.EntityType; +import net.minecraft.entity.ai.goal.*; +import net.minecraft.entity.ai.pathing.Path; +import net.minecraft.entity.ai.pathing.PathNode; +import net.minecraft.entity.attribute.DefaultAttributeContainer; +import net.minecraft.entity.attribute.EntityAttributes; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.mob.PathAwareEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import java.util.List; +import java.util.UUID; + +public class PlayerJrEntity extends PathAwareEntity { + + /* ---------------- CONFIG ---------------- */ + + private static final int CHAT_INTERVAL = 20 * 6; + + // When to consider cheating + private static final double FAR_DISTANCE = 30.0; // if far and path sucks -> cheat + private static final int MAX_PATH_LENGTH = 45; // path too long -> cheat + private static final int MAX_VERTICAL_GAP = 6; // dad much higher than path end -> cheat + private static final int STUCK_CHECK_EVERY = 20; // check progress every 1s + private static final double STUCK_MIN_PROGRESS = 1.2; // blocks per second expected + private static final int STUCK_STRIKES_TO_FLY = 3; // needs 3 bad checks (avoids spam) + + // Cheat (flight) controls + private static final int FLY_COOLDOWN_TICKS = 100; // max once per 5 seconds + private static final int MIN_FLY_TICKS = 30; // must stay in "creative" at least 1.5s + private static final int LAND_STABLE_TICKS = 6; // must be stable on a standable spot for this long + private static final double LAND_DISTANCE = 2.2; // near landing target + private static final double LANDING_SEARCH_RADIUS = 6.0; + + // Flight feel + private static final double FLY_SPEED = 0.55; // horizontal + private static final double FLY_ACCEL = 0.20; // smoothing (0..1) + private static final double LAND_DESCEND_SPEED = 0.15; // gentle descent + private static final double HOVER_HEIGHT_ABOVE_DAD = 2.6; // where it hovers when no landing spot + + /* ---------------- STATE ---------------- */ + + private UUID dadUuid; + private String dadName = ""; + private int chatCooldown = 40; + + private enum TravelMode { + GROUND, + FLYING, + LANDING + } + + private TravelMode travelMode = TravelMode.GROUND; + + // Anti-spam + stability + private int flyCooldown = 0; // counts down + private int flyTicks = 0; // how long we've been flying + private int landingStableTicks = 0; // stable-on-ground counter + + // "Is it actually stuck?" tracking + private int stuckCheckTimer = STUCK_CHECK_EVERY; + private Vec3d lastStuckPos = null; + private int stuckStrikes = 0; + + // landing target caching (reduces jitter) + private BlockPos landingTarget = null; + private int landingTargetRefresh = 0; + + /* ---------------- CHAT ---------------- */ + + private static final List MESSAGES = List.of( + "hold on…", + "yeah no this path is cooked", + "creative moment, don’t look", + "brb disabling gravity", + "pathfinding said no", + "minecraft moment detected" + ); + + /* ---------------- CONSTRUCTOR ---------------- */ + + public PlayerJrEntity(EntityType type, World world) { + super(type, world); + } + + /* ---------------- ATTRIBUTES ---------------- */ + + public static DefaultAttributeContainer.Builder createAttributes() { + return MobEntity.createMobAttributes() + .add(EntityAttributes.GENERIC_MAX_HEALTH, 10.0) + .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.25) + .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 64.0); + } + + /* ---------------- DAD ---------------- */ + + public void setDad(ServerPlayerEntity dad) { + this.dadUuid = dad.getUuid(); + this.dadName = dad.getGameProfile().getName(); + this.setCustomName(Text.literal(this.dadName + " jr.")); + this.setCustomNameVisible(true); + } + + private ServerPlayerEntity getDad() { + if (dadUuid == null || getWorld().isClient) return null; + return getWorld().getServer().getPlayerManager().getPlayer(dadUuid); + } + + public String getDadName() { + return dadName; + } + + public UUID getDadUuid() { + return dadUuid; + } + + /* ---------------- GOALS ---------------- */ + + @Override + protected void initGoals() { + this.goalSelector.add(1, new SwimGoal(this)); + this.goalSelector.add(2, new FollowDadGoal(this)); + this.goalSelector.add(3, new WanderAroundGoal(this, 0.8)); + this.goalSelector.add(4, new LookAtEntityGoal(this, PlayerEntity.class, 6f)); + this.goalSelector.add(5, new LookAroundGoal(this)); + } + + /* ---------------- TICK ---------------- */ + + @Override + public void tick() { + super.tick(); + if (getWorld().isClient) return; + + ServerPlayerEntity dad = getDad(); + if (dad == null) return; + + if (flyCooldown > 0) flyCooldown--; + + // movement state machine + switch (travelMode) { + case GROUND -> { + evaluateCheatNeed(dad); + } + case FLYING -> { + flyTicks++; + doFlying(dad); + } + case LANDING -> { + flyTicks++; + doLanding(dad); + } + } + + // chat + chatCooldown--; + if (chatCooldown <= 0 && this.squaredDistanceTo(dad) < 36 && !MESSAGES.isEmpty()) { + dad.sendMessage(Text.literal(MESSAGES.get(this.random.nextInt(MESSAGES.size()))), true); + chatCooldown = CHAT_INTERVAL; + } + } + + /* ---------------- CHEAT DECISION ---------------- */ + + private void evaluateCheatNeed(ServerPlayerEntity dad) { + // Too close? don't cheat. + double distSq = this.squaredDistanceTo(dad); + if (distSq < (12.0 * 12.0)) { + resetStuckTracking(); // near dad, don't keep strikes + return; + } + + // If on cooldown, we still allow normal pathing but don't fly. + if (flyCooldown > 0) return; + + Path path = this.getNavigation().getCurrentPath(); + + boolean far = distSq > (FAR_DISTANCE * FAR_DISTANCE); + boolean noPath = path == null; + boolean tooLong = path != null && path.getLength() > MAX_PATH_LENGTH; + boolean finishedButStillFar = path != null && path.isFinished() && far; + boolean pathWater = path != null && pathTouchesWater(path); + boolean dadHighButPathLow = path != null && dadIsHighButPathStaysLow(dad, path); + + // "stuck" detection (only counts if far-ish) + if (distSq > (18.0 * 18.0)) { + boolean stuck = checkStuck(); + if (stuck) stuckStrikes++; + else stuckStrikes = Math.max(0, stuckStrikes - 1); + } else { + stuckStrikes = 0; + } + + boolean pathIsCooked = noPath || tooLong || finishedButStillFar || pathWater || dadHighButPathLow; + boolean trulyStuck = stuckStrikes >= STUCK_STRIKES_TO_FLY; + + // Only cheat if far AND (path is cooked OR truly stuck) + if (far && (pathIsCooked || trulyStuck)) { + startFlying(dad); + } + } + + private boolean checkStuck() { + stuckCheckTimer--; + if (stuckCheckTimer > 0) return false; + + stuckCheckTimer = STUCK_CHECK_EVERY; + + Vec3d now = this.getPos(); + if (lastStuckPos == null) { + lastStuckPos = now; + return false; + } + + double moved = now.distanceTo(lastStuckPos); + lastStuckPos = now; + + // expected progress in 1s ~ STUCK_MIN_PROGRESS blocks + return moved < STUCK_MIN_PROGRESS; + } + + private void resetStuckTracking() { + stuckCheckTimer = STUCK_CHECK_EVERY; + lastStuckPos = null; + stuckStrikes = 0; + } + + private boolean pathTouchesWater(Path path) { + for (int i = 0; i < path.getLength(); i++) { + PathNode node = path.getNode(i); + BlockPos pos = new BlockPos(node.x, node.y, node.z); + if (!getWorld().getFluidState(pos).isEmpty()) { + return true; + } + } + return false; + } + + private boolean dadIsHighButPathStaysLow(ServerPlayerEntity dad, Path path) { + if (path.getLength() == 0) return false; + PathNode end = path.getNode(path.getLength() - 1); + int endY = end.y; + int dadY = dad.getBlockY(); + return dadY - endY >= MAX_VERTICAL_GAP; + } + + /* ---------------- FLYING ---------------- */ + + private void startFlying(ServerPlayerEntity dad) { + if (travelMode != TravelMode.GROUND) return; + + travelMode = TravelMode.FLYING; + flyTicks = 0; + landingStableTicks = 0; + + landingTarget = null; + landingTargetRefresh = 0; + + this.setNoGravity(true); + this.getNavigation().stop(); + this.setVelocity(Vec3d.ZERO); + this.velocityDirty = true; + + // cooldown starts now (prevents spam toggles) + flyCooldown = FLY_COOLDOWN_TICKS; + + dad.sendMessage(Text.literal("* " + getName().getString() + " switched to Creative Mode"), false); + } + + private void doFlying(ServerPlayerEntity dad) { + // refresh landing target occasionally (not every tick to avoid jitter) + if (landingTargetRefresh-- <= 0) { + landingTargetRefresh = 6; // update ~3 times/sec + landingTarget = findLandingSpotBlock(dad); + } + + Vec3d target; + + if (landingTarget != null) { + target = Vec3d.ofCenter(landingTarget); + } else { + // no landing spot => hover above dad and keep searching + target = dad.getPos().add(0, HOVER_HEIGHT_ABOVE_DAD, 0); + } + + // If we have a landing block and we are close enough, go LANDING + if (landingTarget != null) { + double dist = this.getPos().distanceTo(Vec3d.ofCenter(landingTarget)); + if (dist < LAND_DISTANCE) { + travelMode = TravelMode.LANDING; + landingStableTicks = 0; + // stop forward movement before landing phase + this.setVelocity(Vec3d.ZERO); + this.velocityDirty = true; + return; + } + } + + // smooth creative-like movement + flyTowardSmooth(target); + this.lookAtEntity(dad, 30f, 30f); + } + + private void doLanding(ServerPlayerEntity dad) { + // MUST stay in "creative" for at least MIN_FLY_TICKS before returning to ground + if (flyTicks < MIN_FLY_TICKS) { + // keep holding position / gentle settle + this.setVelocity(Vec3d.ZERO); + this.velocityDirty = true; + return; + } + + // If we lost landing target, go back to flying (but keep creative) + if (landingTarget == null) { + travelMode = TravelMode.FLYING; + return; + } + + // If landing target became invalid, go back to flying (don’t drop) + if (!canStandAt(landingTarget)) { + travelMode = TravelMode.FLYING; + landingTarget = null; + return; + } + + // We want to be exactly centered above the landing block + Vec3d center = Vec3d.ofCenter(landingTarget); + Vec3d pos = this.getPos(); + + // horizontal adjust (gentle) + Vec3d horiz = new Vec3d(center.x - pos.x, 0, center.z - pos.z); + double horizLen = horiz.length(); + + double vx = 0, vz = 0; + if (horizLen > 0.08) { + Vec3d hn = horiz.normalize().multiply(Math.min(FLY_SPEED * 0.6, horizLen)); + vx = hn.x; + vz = hn.z; + } + + // descend gently until feet are on standable position + double vy = -LAND_DESCEND_SPEED; + + // If we are basically at block level, stop vertical movement + double dy = center.y - pos.y; + if (dy > -0.15 && dy < 0.65) { + vy = 0; + } + + this.setVelocity(vx, vy, vz); + this.velocityDirty = true; + + // "stable landing" means: our current feet pos is standable and we are almost not moving + BlockPos feet = this.getBlockPos(); + boolean stableFeet = canStandAt(feet) || feet.equals(landingTarget); + boolean slow = this.getVelocity().lengthSquared() < 0.01; + + if (stableFeet && slow) { + landingStableTicks++; + } else { + landingStableTicks = 0; + } + + if (landingStableTicks >= LAND_STABLE_TICKS) { + stopFlying(dad); + } + } + + private void stopFlying(ServerPlayerEntity dad) { + travelMode = TravelMode.GROUND; + + this.setVelocity(Vec3d.ZERO); + this.velocityDirty = true; + this.setNoGravity(false); + + // after returning, clear stuck tracking so it doesn't instantly cheat again + resetStuckTracking(); + + // don't instantly re-cheat; cooldown already running, but this avoids instant flip due to path recalculation + landingTarget = null; + landingTargetRefresh = 0; + + dad.sendMessage(Text.literal("* " + getName().getString() + " returned to Survival Mode"), false); + } + + private void flyTowardSmooth(Vec3d target) { + Vec3d dir = target.subtract(this.getPos()); + if (dir.lengthSquared() < 0.0005) { + // gently stop + Vec3d v = this.getVelocity().multiply(0.5); + if (v.lengthSquared() < 0.0002) v = Vec3d.ZERO; + this.setVelocity(v); + this.velocityDirty = true; + return; + } + + Vec3d desired = dir.normalize().multiply(FLY_SPEED); + + // smoothing: v = v*(1-a) + desired*a + Vec3d current = this.getVelocity(); + Vec3d blended = current.multiply(1.0 - FLY_ACCEL).add(desired.multiply(FLY_ACCEL)); + + this.setVelocity(blended); + this.velocityDirty = true; + } + + /* ---------------- LANDING TARGET SEARCH ---------------- */ + + private BlockPos findLandingSpotBlock(ServerPlayerEntity dad) { + BlockPos center = dad.getBlockPos(); + double bestDist = Double.MAX_VALUE; + BlockPos best = null; + + int radius = (int) Math.ceil(LANDING_SEARCH_RADIUS); + + // prefer roughly same Y as dad feet-ish (+0..+2) + int minY = center.getY() - 2; + int maxY = center.getY() + 3; + + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + // quick circle check + if ((dx * dx + dz * dz) > (LANDING_SEARCH_RADIUS * LANDING_SEARCH_RADIUS)) continue; + + for (int y = minY; y <= maxY; y++) { + BlockPos pos = new BlockPos(center.getX() + dx, y, center.getZ() + dz); + if (!canStandAt(pos)) continue; + + // also avoid landing inside water or on water edge + if (!getWorld().getFluidState(pos).isEmpty()) continue; + if (!getWorld().getFluidState(pos.down()).isEmpty()) continue; + + double d = this.getPos().squaredDistanceTo(Vec3d.ofCenter(pos)); + if (d < bestDist) { + bestDist = d; + best = pos; + } + } + } + } + + return best; + } + + private boolean canStandAt(BlockPos pos) { + // feet + head must be air + if (!getWorld().getBlockState(pos).isAir()) return false; + if (!getWorld().getBlockState(pos.up()).isAir()) return false; + + // block below must be solid + BlockPos below = pos.down(); + return getWorld().getBlockState(below).isSolidBlock(getWorld(), below); + } + + /* ---------------- DEATH ---------------- */ + + @Override + public void onDeath(DamageSource source) { + super.onDeath(source); + if (getWorld().isClient) return; + + getWorld().createExplosion( + this, + getX(), + getY(), + getZ(), + 3.0f, + World.ExplosionSourceType.MOB + ); + + ServerPlayerEntity dad = getDad(); + if (dad != null) { + dad.sendMessage(Text.literal("oh dad im gonna go"), false); + } + } + + /* ---------------- NAME ---------------- */ + + @Override + public float getNameLabelHeight() { + return this.getHeight() + 1.5f; // YES. STILL CORRECT. + } + + /* ---------------- SAVE ---------------- */ + + @Override + public void writeCustomDataToNbt(NbtCompound nbt) { + if (dadUuid != null) nbt.putUuid("Dad", dadUuid); + nbt.putString("DadName", dadName); + nbt.putString("TravelMode", travelMode.name()); + nbt.putInt("FlyCooldown", flyCooldown); + nbt.putInt("FlyTicks", flyTicks); + nbt.putInt("LandingStable", landingStableTicks); + nbt.putInt("StuckStrikes", stuckStrikes); + } + + @Override + public void readCustomDataFromNbt(NbtCompound nbt) { + if (nbt.containsUuid("Dad")) dadUuid = nbt.getUuid("Dad"); + if (nbt.contains("DadName")) dadName = nbt.getString("DadName"); + + if (nbt.contains("TravelMode")) { + travelMode = TravelMode.valueOf(nbt.getString("TravelMode")); + this.setNoGravity(travelMode != TravelMode.GROUND); + } + + if (nbt.contains("FlyCooldown")) flyCooldown = nbt.getInt("FlyCooldown"); + if (nbt.contains("FlyTicks")) flyTicks = nbt.getInt("FlyTicks"); + if (nbt.contains("LandingStable")) landingStableTicks = nbt.getInt("LandingStable"); + if (nbt.contains("StuckStrikes")) stuckStrikes = nbt.getInt("StuckStrikes"); + + // reset volatile runtime fields + stuckCheckTimer = STUCK_CHECK_EVERY; + lastStuckPos = null; + landingTarget = null; + landingTargetRefresh = 0; + } + + /* ---------------- FOLLOW GOAL ---------------- */ + + static class FollowDadGoal extends Goal { + private final PlayerJrEntity jr; + + FollowDadGoal(PlayerJrEntity jr) { + this.jr = jr; + } + + @Override + public boolean canStart() { + return jr.travelMode == TravelMode.GROUND && jr.getDad() != null; + } + + @Override + public void tick() { + ServerPlayerEntity dad = jr.getDad(); + if (dad == null) return; + + if (jr.squaredDistanceTo(dad) > 4) { + jr.getNavigation().startMovingTo(dad, 1.0); + } + } + } +}