Add PlayerJr entity and associated rendering and command features
This commit is contained in:
parent
77d7e7412e
commit
1b9f7d85c0
@ -9,6 +9,7 @@ import net.Chipperfluff.chipi.effect.ModEffects;
|
|||||||
import net.Chipperfluff.chipi.entity.ModEntities;
|
import net.Chipperfluff.chipi.entity.ModEntities;
|
||||||
import net.Chipperfluff.chipi.entity.SpawnLogic;
|
import net.Chipperfluff.chipi.entity.SpawnLogic;
|
||||||
import net.Chipperfluff.chipi.entity.MepEntity;
|
import net.Chipperfluff.chipi.entity.MepEntity;
|
||||||
|
import net.Chipperfluff.chipi.entity.PlayerJrEntity;
|
||||||
import net.Chipperfluff.chipi.item.ModItemGroups;
|
import net.Chipperfluff.chipi.item.ModItemGroups;
|
||||||
import net.Chipperfluff.chipi.item.ModItems;
|
import net.Chipperfluff.chipi.item.ModItems;
|
||||||
import net.Chipperfluff.chipi.server.ChipiServerEvents;
|
import net.Chipperfluff.chipi.server.ChipiServerEvents;
|
||||||
@ -41,6 +42,8 @@ public class ChipiMod implements ModInitializer {
|
|||||||
|
|
||||||
FabricDefaultAttributeRegistry.register(ModEntities.MEP, MepEntity.createMepAttributes());
|
FabricDefaultAttributeRegistry.register(ModEntities.MEP, MepEntity.createMepAttributes());
|
||||||
|
|
||||||
|
FabricDefaultAttributeRegistry.register(ModEntities.PLAYER_JR, PlayerJrEntity.createAttributes());
|
||||||
|
|
||||||
ChipiServerEvents.register();
|
ChipiServerEvents.register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,5 +7,6 @@ public class ModEntityRenderers {
|
|||||||
|
|
||||||
public static void register() {
|
public static void register() {
|
||||||
EntityRendererRegistry.register(ModEntities.MEP, MepRenderer::new);
|
EntityRendererRegistry.register(ModEntities.MEP, MepRenderer::new);
|
||||||
|
EntityRendererRegistry.register(ModEntities.PLAYER_JR, PlayerJrRenderer::new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<PlayerJrEntity, BipedEntityModel<PlayerJrEntity>> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ public final class CommandHandler {
|
|||||||
(dispatcher, registryAccess, environment) -> {
|
(dispatcher, registryAccess, environment) -> {
|
||||||
ChpCommand.register(dispatcher);
|
ChpCommand.register(dispatcher);
|
||||||
CspCommand.register(dispatcher);
|
CspCommand.register(dispatcher);
|
||||||
|
SpawnJrCommand.register(dispatcher);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<ServerCommandSource> 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<ServerCommandSource> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,16 @@ public final class ModEntities {
|
|||||||
.build()
|
.build()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static final EntityType<PlayerJrEntity> 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() {
|
private ModEntities() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
552
src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java
Normal file
552
src/main/java/net/Chipperfluff/chipi/entity/PlayerJrEntity.java
Normal file
@ -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<String> 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<? extends PathAwareEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user