diff --git a/patches/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch b/patches/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch new file mode 100644 index 0000000..0a5dca8 --- /dev/null +++ b/patches/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch @@ -0,0 +1,59 @@ +--- a/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java ++++ b/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java +@@ -112,7 +_,7 @@ + BlockPathTypes blockpathtypes = this.m_77567_(this.f_77313_, p_77641_.f_77271_, p_77641_.f_77272_ + 1, p_77641_.f_77273_); + BlockPathTypes blockpathtypes1 = this.m_77567_(this.f_77313_, p_77641_.f_77271_, p_77641_.f_77272_, p_77641_.f_77273_); + if (this.f_77313_.m_21439_(blockpathtypes) >= 0.0F && blockpathtypes1 != BlockPathTypes.STICKY_HONEY) { +- j = Mth.m_14143_(Math.max(1.0F, this.f_77313_.m_274421_())); ++ j = Mth.m_14143_(Math.max(1.0F, this.f_77313_.getStepHeight())); + } + + double d0 = this.m_142213_(new BlockPos(p_77641_.f_77271_, p_77641_.f_77272_, p_77641_.f_77273_)); +@@ -306,7 +_,7 @@ + } + + private double m_255203_() { +- return Math.max(1.125D, (double)this.f_77313_.m_274421_()); ++ return Math.max(1.125D, (double)this.f_77313_.getStepHeight()); + } + + private Node m_230619_(int p_230620_, int p_230621_, int p_230622_, BlockPathTypes p_230623_, float p_230624_) { +@@ -453,6 +_,11 @@ + if (l != 0 || j1 != 0) { + p_77609_.m_122178_(i + l, j + i1, k + j1); + BlockState blockstate = p_77608_.m_8055_(p_77609_); ++ BlockPathTypes blockPathType = blockstate.getAdjacentBlockPathType(p_77608_, p_77609_, null, p_77610_); ++ if (blockPathType != null) return blockPathType; ++ FluidState fluidState = blockstate.m_60819_(); ++ BlockPathTypes fluidPathType = fluidState.getAdjacentBlockPathType(p_77608_, p_77609_, null, p_77610_); ++ if (fluidPathType != null) return fluidPathType; + if (blockstate.m_60713_(Blocks.f_50128_) || blockstate.m_60713_(Blocks.f_50685_)) { + return BlockPathTypes.DANGER_OTHER; + } +@@ -478,6 +_,8 @@ + + protected static BlockPathTypes m_77643_(BlockGetter p_77644_, BlockPos p_77645_) { + BlockState blockstate = p_77644_.m_8055_(p_77645_); ++ BlockPathTypes type = blockstate.getBlockPathType(p_77644_, p_77645_, null); ++ if (type != null) return type; + Block block = blockstate.m_60734_(); + if (blockstate.m_60795_()) { + return BlockPathTypes.OPEN; +@@ -491,6 +_,8 @@ + return BlockPathTypes.COCOA; + } else if (!blockstate.m_60713_(Blocks.f_50070_) && !blockstate.m_60713_(Blocks.f_152588_)) { + FluidState fluidstate = p_77644_.m_6425_(p_77645_); ++ BlockPathTypes nonLoggableFluidPathType = fluidstate.getBlockPathType(p_77644_, p_77645_, null, false); ++ if (nonLoggableFluidPathType != null) return nonLoggableFluidPathType; + if (fluidstate.m_205070_(FluidTags.f_13132_)) { + return BlockPathTypes.LAVA; + } else if (m_77622_(blockstate)) { +@@ -510,6 +_,8 @@ + if (!blockstate.m_60647_(p_77644_, p_77645_, PathComputationType.LAND)) { + return BlockPathTypes.BLOCKED; + } else { ++ BlockPathTypes loggableFluidPathType = fluidstate.getBlockPathType(p_77644_, p_77645_, null, true); ++ if (loggableFluidPathType != null) return loggableFluidPathType; + return fluidstate.m_205070_(FluidTags.f_13131_) ? BlockPathTypes.WATER : BlockPathTypes.OPEN; + } + } else { diff --git a/src/main/java/me/sashegdev/fabled_hearts/Main.java b/src/main/java/me/sashegdev/fabled_hearts/Main.java index 86960d2..65e00d9 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/Main.java +++ b/src/main/java/me/sashegdev/fabled_hearts/Main.java @@ -1,6 +1,7 @@ package me.sashegdev.fabled_hearts; import com.mojang.logging.LogUtils; +import me.sashegdev.fabled_hearts.client.DebugClientHandler; import me.sashegdev.fabled_hearts.dialog.DialogLoader; import me.sashegdev.fabled_hearts.dialog.DialogManager; import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; @@ -10,6 +11,7 @@ import me.sashegdev.fabled_hearts.registry.ModEntities; import me.sashegdev.fabled_hearts.registry.ModItems; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.EntityRenderersEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.AddReloadListenerEvent; import net.minecraftforge.event.entity.EntityAttributeCreationEvent; @@ -17,6 +19,7 @@ import net.minecraftforge.event.level.BlockEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.slf4j.Logger; @@ -62,6 +65,16 @@ public class Main { public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { event.registerEntityRenderer(ModEntities.ELLIE.get(), EllieRenderer::new); } + + @SubscribeEvent + public static void onClientSetup(FMLClientSetupEvent event) { + DebugClientHandler.init(); + } + + @SubscribeEvent + public static void registerKeyMappings(RegisterKeyMappingsEvent event) { + DebugClientHandler.registerKeyMapping(event); + } } public static class ForgeHandler { @@ -76,10 +89,10 @@ public class Main { var pos = event.getPos(); event.getLevel().getEntitiesOfClass( EllieEntity.class, - new net.minecraft.world.phys.AABB(pos).inflate(2), - e -> e.isSleeping() && pos.equals(e.getBedPos()) + new net.minecraft.world.phys.AABB(pos).inflate(3), + e -> e.isBedAtOrAdjacent(pos) || (e.getBedPos() != null && e.getBedPos().equals(pos)) ).forEach(e -> { - e.wakeUp(); + if (e.isSleeping()) e.wakeUp(); e.forgetBed(); }); } diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/EllieNodeEvaluator.java b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieNodeEvaluator.java new file mode 100644 index 0000000..34fb86b --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieNodeEvaluator.java @@ -0,0 +1,36 @@ +package me.sashegdev.fabled_hearts.ai; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.WalkNodeEvaluator; + +public class EllieNodeEvaluator extends WalkNodeEvaluator { + + @Override + protected Node findAcceptedNode(int x, int y, int z, int depth, double floorY, Direction direction, BlockPathTypes expectedType) { + Node node = super.findAcceptedNode(x, y, z, depth, floorY, direction, expectedType); + if (node != null) return node; + + BlockPathTypes head1 = this.getCachedBlockType(this.mob, x, y + 1, z); + if (head1 == BlockPathTypes.BLOCKED) return null; + BlockPathTypes head2 = this.getCachedBlockType(this.mob, x, y + 2, z); + if (head2 != BlockPathTypes.BLOCKED) return null; + + BlockPathTypes footType = this.getCachedBlockType(this.mob, x, y, z); + float costMalus = this.mob.getPathfindingMalus(footType); + if (costMalus < 0.0f) return null; + + double floorLevel = getFloorLevel(new BlockPos(x, y, z)); + double maxJump = Math.max(1.125, this.mob.getStepHeight()); + if (floorLevel - floorY > maxJump) return null; + + node = this.getNode(x, y, z); + if (node != null) { + node.type = footType; + node.costMalus = costMalus; + } + return node; + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java b/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java new file mode 100644 index 0000000..3fcbcc2 --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java @@ -0,0 +1,21 @@ +package me.sashegdev.fabled_hearts.ai; + +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.pathfinder.PathFinder; + +public class ElliePathNavigation extends GroundPathNavigation { + + public ElliePathNavigation(Mob mob, Level level) { + super(mob, level); + } + + @Override + protected PathFinder createPathFinder(int maxVisitedNodes) { + EllieNodeEvaluator evaluator = new EllieNodeEvaluator(); + evaluator.setCanPassDoors(true); + this.nodeEvaluator = evaluator; + return new PathFinder(evaluator, maxVisitedNodes); + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java index 8845cb4..65ee3b8 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java @@ -106,14 +106,18 @@ public class EllieSleepGoal extends Goal { public boolean canContinueToUse() { if (ellie.isSleeping()) { if (sleepTimer >= MAX_SLEEP_TICKS) return false; + if (ellie.level().isDay()) return false; + if (ellie.hurtTime > 10) return false; if (bedPos != null) { BlockState state = ellie.level().getBlockState(bedPos); if (!state.isBed(ellie.level(), bedPos, null)) return false; + if (ellie.distanceToSqr(Vec3.atCenterOf(bedPos)) > 16.0) return false; } return true; } if (bedPos == null) return false; if (ellie.hurtTime > 10) return false; + if (ellie.level().isDay() && !ellie.isTired()) return false; if (!ellie.level().isNight() && !ellie.isTired()) return false; return true; } diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java b/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java index ae97e28..3b01382 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java @@ -1,6 +1,7 @@ package me.sashegdev.fabled_hearts.ai; import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; +import net.minecraft.core.BlockPos; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; @@ -13,6 +14,11 @@ public class FollowPlayerGoal extends Goal { private int cooldown; private static final double FOLLOW_RANGE = 16.0; private static final double FOLLOW_RANGE_SQR = FOLLOW_RANGE * FOLLOW_RANGE; + private static final int RECALC_COOLDOWN = 5; + private static final int STUCK_THRESHOLD = 40; + private BlockPos lastStuckPos; + private int stuckTicks; + private int stuckCooldown; public FollowPlayerGoal(EllieEntity ellie) { this.ellie = ellie; @@ -35,6 +41,9 @@ public class FollowPlayerGoal extends Goal { @Override public void start() { cooldown = 0; + stuckTicks = 0; + stuckCooldown = 0; + lastStuckPos = null; } @Override @@ -55,13 +64,13 @@ public class FollowPlayerGoal extends Goal { if (distSqr > maxDist * maxDist) { if (--cooldown <= 0) { - cooldown = 10; - ellie.getNavigation().moveTo(target, 0.6); + cooldown = RECALC_COOLDOWN; + checkStuck(); } } else if (distSqr < minDist * minDist) { ellie.getNavigation().stop(); if (--cooldown <= 0) { - cooldown = 10; + cooldown = RECALC_COOLDOWN; Vec3 away = new Vec3( ellie.getX() - target.getX(), 0, @@ -77,10 +86,39 @@ public class FollowPlayerGoal extends Goal { } else { ellie.getNavigation().stop(); cooldown = 0; + stuckTicks = 0; + lastStuckPos = null; ellie.getLookControl().setLookAt(target, 30, 30); } } + private void checkStuck() { + if (stuckCooldown > 0) { + stuckCooldown--; + return; + } + + boolean found = ellie.getNavigation().moveTo(target, 0.6); + if (!found) { + stuckCooldown = 10; + } + + if (ellie.getNavigation().isInProgress()) { + BlockPos currentPos = ellie.blockPosition(); + if (currentPos.equals(lastStuckPos)) { + stuckTicks++; + if (stuckTicks > STUCK_THRESHOLD) { + ellie.getNavigation().stop(); + stuckTicks = 0; + stuckCooldown = 20; + } + } else { + stuckTicks = 0; + } + lastStuckPos = currentPos; + } + } + @Override public boolean canContinueToUse() { if (target == null || !target.isAlive()) return false; @@ -94,5 +132,8 @@ public class FollowPlayerGoal extends Goal { public void stop() { target = null; ellie.getNavigation().stop(); + stuckTicks = 0; + stuckCooldown = 0; + lastStuckPos = null; } } diff --git a/src/main/java/me/sashegdev/fabled_hearts/client/DebugClientHandler.java b/src/main/java/me/sashegdev/fabled_hearts/client/DebugClientHandler.java new file mode 100644 index 0000000..928715d --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/client/DebugClientHandler.java @@ -0,0 +1,154 @@ +package me.sashegdev.fabled_hearts.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import me.sashegdev.fabled_hearts.Main; +import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; +import me.sashegdev.fabled_hearts.network.DebugTogglePacket; +import me.sashegdev.fabled_hearts.network.ModNetworking; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.InputEvent; +import net.minecraftforge.client.event.RenderLevelStageEvent; +import net.minecraftforge.client.event.RegisterKeyMappingsEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import org.lwjgl.glfw.GLFW; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class DebugClientHandler { + public static final DebugClientHandler INSTANCE = new DebugClientHandler(); + + private static net.minecraft.client.KeyMapping debugKey; + + private final Map debugDataMap = new ConcurrentHashMap<>(); + + public static class DebugData { + public final List waypoints; + public final long lastUpdate; + + public DebugData(List waypoints) { + this.waypoints = waypoints; + this.lastUpdate = System.currentTimeMillis(); + } + } + + public static void init() { + MinecraftForge.EVENT_BUS.register(INSTANCE); + } + + @SubscribeEvent + public void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.END) return; + if (debugKey == null) return; + + while (debugKey.consumeClick()) { + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; + if (player == null) return; + + EllieEntity target = findTargetEntity(player); + if (target != null) { + ModNetworking.CHANNEL.sendToServer(new DebugTogglePacket(target.getId())); + } + } + } + + private EllieEntity findTargetEntity(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); + if (mc.hitResult != null && mc.hitResult.getType() == HitResult.Type.ENTITY) { + EntityHitResult entityHit = (EntityHitResult) mc.hitResult; + if (entityHit.getEntity() instanceof EllieEntity ellie) { + return ellie; + } + } + + double nearestDist = 25.0; + EllieEntity nearest = null; + Vec3 eyePos = player.getEyePosition(); + AABB searchBox = player.getBoundingBox().inflate(8); + for (EllieEntity ellie : player.level().getEntitiesOfClass(EllieEntity.class, searchBox)) { + double dist = ellie.distanceToSqr(eyePos); + if (dist < nearestDist) { + nearestDist = dist; + nearest = ellie; + } + } + return nearest; + } + + @SubscribeEvent + public void onRenderLevelStage(RenderLevelStageEvent event) { + if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_LEVEL) return; + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return; + + PoseStack poseStack = event.getPoseStack(); + var pose = poseStack.last(); + VertexConsumer consumer = mc.renderBuffers().bufferSource().getBuffer( + net.minecraft.client.renderer.RenderType.LINES); + + for (Map.Entry entry : debugDataMap.entrySet()) { + Entity entity = mc.player.level().getEntity(entry.getKey()); + if (!(entity instanceof EllieEntity ellie) || !ellie.isDebugMode()) continue; + + DebugData data = entry.getValue(); + if (data.waypoints.size() < 2) continue; + + Vec3 prev = null; + for (BlockPos wp : data.waypoints) { + Vec3 current = new Vec3(wp.getX() + 0.5, wp.getY() + 0.2, wp.getZ() + 0.5); + if (prev != null) { + drawLine(consumer, pose, prev, current, 0x00FF00); + } + prev = current; + } + } + } + + private void drawLine(VertexConsumer consumer, PoseStack.Pose pose, + Vec3 from, Vec3 to, int color) { + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = (color & 0xFF) / 255f; + + consumer.vertex(pose.pose(), (float) from.x, (float) from.y, (float) from.z) + .color(r, g, b, 1.0f) + .normal(pose.normal(), 0, 1, 0) + .endVertex(); + consumer.vertex(pose.pose(), (float) to.x, (float) to.y, (float) to.z) + .color(r, g, b, 1.0f) + .normal(pose.normal(), 0, 1, 0) + .endVertex(); + } + + public void onDebugSync(int entityId, List waypoints, String brainState) { + debugDataMap.put(entityId, new DebugData(waypoints)); + } + + public void removeDebug(int entityId) { + debugDataMap.remove(entityId); + } + + public static void registerKeyMapping(RegisterKeyMappingsEvent event) { + debugKey = new net.minecraft.client.KeyMapping( + "key.fabled_hearts.debug", + GLFW.GLFW_KEY_F6, + "key.categories.fabled_hearts" + ); + event.register(debugKey); + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieEntity.java b/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieEntity.java index d4747e9..2d97c89 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieEntity.java +++ b/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieEntity.java @@ -1,6 +1,7 @@ package me.sashegdev.fabled_hearts.entity.ellie; import me.sashegdev.fabled_hearts.ai.BasicAI; +import me.sashegdev.fabled_hearts.ai.ElliePathNavigation; import me.sashegdev.fabled_hearts.ai.EllieSleepGoal; import me.sashegdev.fabled_hearts.ai.IdleAnimationGoal; import me.sashegdev.fabled_hearts.network.ModNetworking; @@ -11,12 +12,16 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; +import java.util.ArrayList; +import java.util.List; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.*; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.ai.navigation.GroundPathNavigation; +import net.minecraft.world.entity.ai.navigation.PathNavigation; import net.minecraft.world.entity.animal.Animal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; @@ -25,6 +30,11 @@ import net.minecraft.world.level.block.DoorBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.properties.BedPart; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraftforge.network.PacketDistributor; +import me.sashegdev.fabled_hearts.network.DebugSyncPacket; +import me.sashegdev.fabled_hearts.network.ModNetworking; import software.bernie.geckolib.animatable.GeoEntity; import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; import software.bernie.geckolib.core.animation.AnimatableManager; @@ -51,6 +61,10 @@ public class EllieEntity extends Animal implements GeoEntity { SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_MOVING = SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_DEBUG = + SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN); + private static final EntityDataAccessor DATA_DEBUG_BRAIN = + SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.STRING); private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); @@ -58,15 +72,22 @@ public class EllieEntity extends Animal implements GeoEntity { private float relationshipPoints; private BlockPos bedPos; private double lastX, lastZ; - private int moveDetectCooldown; private BlockPos lastOpenedDoor; private int doorCloseTimer; private int crouchTimer; + private boolean needsSleepRestore; public EllieEntity(EntityType type, Level level) { super(type, level); ((GroundPathNavigation) this.getNavigation()).setCanOpenDoors(true); this.setPersistenceRequired(); + lastX = getX(); + lastZ = getZ(); + } + + @Override + public PathNavigation createNavigation(Level level) { + return new ElliePathNavigation(this, level); } public static AttributeSupplier.Builder createAttributes() { @@ -87,6 +108,8 @@ public class EllieEntity extends Animal implements GeoEntity { this.entityData.define(DATA_EATING, false); this.entityData.define(DATA_FALL_TICKS, 0); this.entityData.define(DATA_MOVING, false); + this.entityData.define(DATA_DEBUG, false); + this.entityData.define(DATA_DEBUG_BRAIN, ""); } @Override @@ -100,18 +123,9 @@ public class EllieEntity extends Animal implements GeoEntity { @Override public void aiStep() { super.aiStep(); - tickMovementDetection(); tickDoors(); if (!level().isClientSide) { tickServer(); - } - } - - private void tickMovementDetection() { - if (moveDetectCooldown-- <= 0) { - moveDetectCooldown = 2; - double dx = getX() - lastX; - double dz = getZ() - lastZ; lastX = getX(); lastZ = getZ(); } @@ -140,15 +154,17 @@ public class EllieEntity extends Animal implements GeoEntity { } public boolean isMoving() { - double dx = getX() - lastX; - double dz = getZ() - lastZ; - boolean posChanged = dx * dx + dz * dz > 1.0e-8; boolean navBusy = navigation.isInProgress() && !navigation.isDone(); boolean hasVelocity = getDeltaMovement().horizontalDistanceSqr() > 0.001; - return posChanged || navBusy || hasVelocity; + return navBusy || hasVelocity; } private void tickServer() { + if (needsSleepRestore && bedPos != null && entityData.get(DATA_SLEEPING)) { + setBedSleepPos(bedPos); + needsSleepRestore = false; + } + boolean isNight = level().isNight(); boolean raining = level().isRainingAt(blockPosition()); boolean lowCeiling = checkLowCeiling(); @@ -168,6 +184,10 @@ public class EllieEntity extends Animal implements GeoEntity { entityData.set(DATA_FALL_TICKS, 0); } + if (!sleeping && moving && tickCount % 10 == 0) { + openDoorInFront(); + } + boolean wasCrouching = getPose() == Pose.CROUCHING; if (wasCrouching) { crouchTimer = Math.max(0, crouchTimer - 1); @@ -189,6 +209,21 @@ public class EllieEntity extends Animal implements GeoEntity { } } + if (entityData.get(DATA_DEBUG) && tickCount % 10 == 0) { + updateDebugBrainString(); + if (!level().isClientSide) { + List wps = getPathWaypoints(); + String brain = entityData.get(DATA_DEBUG_BRAIN); + for (Player player : level().players()) { + if (player.distanceToSqr(this) < 64 * 64) { + ModNetworking.CHANNEL.send( + PacketDistributor.PLAYER.with(() -> (ServerPlayer) player), + new DebugSyncPacket(this.getId(), wps, brain)); + } + } + } + } + if (!sleeping) { if (isNight) { ticksWithoutSleep++; @@ -198,17 +233,77 @@ public class EllieEntity extends Animal implements GeoEntity { } } + private void updateDebugBrainString() { + StringBuilder sb = new StringBuilder(); + String goalName = goalSelector.getRunningGoals() + .findFirst() + .map(g -> g.getGoal().getClass().getSimpleName()) + .orElse("none"); + sb.append("Goal:").append(goalName).append("|"); + sb.append("Bed:").append(bedPos != null ? bedPos : "-").append("|"); + sb.append("Sleep:").append(entityData.get(DATA_SLEEPING)).append("|"); + sb.append("Tired:").append(ticksWithoutSleep).append("|"); + sb.append("Move:").append(entityData.get(DATA_MOVING)).append("|"); + sb.append("Pose:").append(getPose()).append("|"); + sb.append("LowCeil:").append(entityData.get(DATA_UNDER_LOW_CEILING)).append("|"); + sb.append("Rel:").append(String.format("%.0f", relationshipPoints)).append("|"); + sb.append("CrouchT:").append(crouchTimer).append("|"); + if (navigation.isInProgress() && !navigation.isDone()) { + sb.append("NavTo:").append(navigation.getTargetPos()); + } else { + sb.append("NavTo:-"); + } + entityData.set(DATA_DEBUG_BRAIN, sb.toString()); + } + + public void toggleDebug() { + entityData.set(DATA_DEBUG, !entityData.get(DATA_DEBUG)); + if (!entityData.get(DATA_DEBUG)) { + entityData.set(DATA_DEBUG_BRAIN, ""); + } + } + + private List getPathWaypoints() { + List result = new ArrayList<>(); + Path path = navigation.getPath(); + if (path != null) { + for (int i = 0; i < path.getNodeCount(); i++) { + Node node = path.getNode(i); + result.add(new BlockPos(node.x, node.y, node.z)); + } + } + return result; + } + private boolean checkLowCeiling() { - return isSpaceLow(blockPosition()); + BlockPos pos = blockPosition(); + if (isSpaceLow(pos)) return true; + if (isMoving() && navigation.isInProgress()) { + Direction dir = getDirection(); + Direction left = dir.getCounterClockWise(); + Direction right = dir.getClockWise(); + for (int i = 1; i <= 3; i++) { + BlockPos ahead = pos.relative(dir, i); + if (isSpaceLow(ahead)) return true; + if (isSpaceLow(ahead.relative(left, 1))) return true; + if (isSpaceLow(ahead.relative(right, 1))) return true; + } + } + return false; } public boolean isSpaceLow(BlockPos pos) { - return level().getBlockState(pos.above(1)).blocksMotion(); + for (int dy = 1; dy < 3; dy++) { + if (level().getBlockState(pos.above(dy)).blocksMotion()) { + return true; + } + } + return false; } @Override public EntityDimensions getDimensions(Pose pose) { - if (pose == Pose.SLEEPING) return EntityDimensions.fixed(0.4f, 0.3f); + if (pose == Pose.SLEEPING) return EntityDimensions.fixed(0.6f, 0.6f); if (pose == Pose.CROUCHING) return EntityDimensions.fixed(0.6f, 1.0f); return super.getDimensions(pose); } @@ -255,17 +350,29 @@ public class EllieEntity extends Animal implements GeoEntity { return level().getBlockState(pos).isBed(level(), pos, null); } + public boolean isBedAtOrAdjacent(BlockPos pos) { + if (pos.equals(bedPos)) return true; + if (bedPos != null) { + BlockState state = level().getBlockState(bedPos); + if (state.getBlock() instanceof BedBlock) { + BlockPos head = bedPos.relative(state.getValue(BedBlock.FACING)); + if (pos.equals(head)) return true; + } + } + return false; + } + public void setBedSleepPos(BlockPos pos) { BlockState state = level().getBlockState(pos); if (!(state.getBlock() instanceof BedBlock)) return; Direction facing = state.getValue(BedBlock.FACING); - BlockPos footPos = state.getValue(BedBlock.PART) == BedPart.HEAD - ? pos.relative(facing.getOpposite()) - : pos; + BlockPos headPos = state.getValue(BedBlock.PART) == BedPart.HEAD + ? pos + : pos.relative(facing); - float yRot = facing.toYRot(); - setPos(footPos.getX() + 0.5, footPos.getY() + 0.5625, footPos.getZ() + 0.5); + float yRot = facing.getOpposite().toYRot(); + setPos(headPos.getX() + 0.5, headPos.getY() + 0.5625, headPos.getZ() + 0.5); setYRot(yRot); yRotO = yRot; setYHeadRot(yRot); @@ -275,6 +382,7 @@ public class EllieEntity extends Animal implements GeoEntity { public void wakeUp() { releaseBed(); + forgetBed(); setSleeping(false); setPose(Pose.STANDING); navigation.stop(); @@ -392,7 +500,12 @@ public class EllieEntity extends Animal implements GeoEntity { super.readAdditionalSaveData(tag); ticksWithoutSleep = tag.getInt("TicksWithoutSleep"); relationshipPoints = tag.getFloat("Relationship"); - if (tag.contains("BedPos")) bedPos = BlockPos.of(tag.getLong("BedPos")); + if (tag.contains("BedPos")) { + bedPos = BlockPos.of(tag.getLong("BedPos")); + if (entityData.get(DATA_SLEEPING)) { + needsSleepRestore = true; + } + } } @Override @@ -403,6 +516,8 @@ public class EllieEntity extends Animal implements GeoEntity { public boolean isTired() { return entityData.get(DATA_TIRED); } public boolean isInRain() { return entityData.get(DATA_IN_RAIN); } public boolean isUnderLowCeiling() { return entityData.get(DATA_UNDER_LOW_CEILING); } + public boolean isDebugMode() { return entityData.get(DATA_DEBUG); } + public String getDebugBrainString() { return entityData.get(DATA_DEBUG_BRAIN); } public void setBedPos(BlockPos pos) { this.bedPos = pos; } public BlockPos getBedPos() { return bedPos; } public float getRelationshipPoints() { return relationshipPoints; } diff --git a/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieRenderer.java b/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieRenderer.java index b671a25..939dc2a 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieRenderer.java +++ b/src/main/java/me/sashegdev/fabled_hearts/entity/ellie/EllieRenderer.java @@ -1,13 +1,16 @@ package me.sashegdev.fabled_hearts.entity.ellie; import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.math.Axis; +import com.mojang.blaze3d.vertex.VertexConsumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Pose; import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; import software.bernie.geckolib.renderer.GeoEntityRenderer; public class EllieRenderer extends GeoEntityRenderer { @@ -25,14 +28,53 @@ public class EllieRenderer extends GeoEntityRenderer { @Override public void render(EllieEntity entity, float entityYaw, float partialTick, PoseStack poseStack, MultiBufferSource bufferSource, int packedLight) { - if (entity.getPose() == Pose.SLEEPING) { - poseStack.pushPose(); - poseStack.translate(0, 0.5625f, 0); - poseStack.mulPose(Axis.XP.rotationDegrees(-90)); - super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight); - poseStack.popPose(); - } else { - super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight); + super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight); + + if (entity.isDebugMode()) { + renderBrainText(entity, poseStack, bufferSource, packedLight); } } + + private void renderBrainText(EllieEntity entity, PoseStack poseStack, + MultiBufferSource bufferSource, int packedLight) { + String brain = entity.getDebugBrainString(); + if (brain == null || brain.isEmpty()) return; + + Font font = Minecraft.getInstance().font; + String[] lines = brain.split("\\|"); + + poseStack.pushPose(); + float heightOffset = entity.getPose() == Pose.SLEEPING ? 1.8f : entity.getBbHeight() + 0.7f; + poseStack.translate(0, heightOffset, 0); + poseStack.mulPose(Minecraft.getInstance().getEntityRenderDispatcher().cameraOrientation()); + poseStack.scale(-0.025f, -0.025f, 0.025f); + + int totalHeight = lines.length * (font.lineHeight + 2); + int startY = -totalHeight / 2; + + for (String line : lines) { + int color = 0xFFFFFF; + if (line.startsWith("Goal:")) color = 0x55FF55; + else if (line.startsWith("Bed:")) color = 0xAAAAAA; + else if (line.startsWith("Sleep:")) color = 0xFFFF55; + else if (line.startsWith("Tired:")) color = 0xFFAA00; + else if (line.startsWith("Move:")) color = 0x55FFFF; + else if (line.startsWith("Pose:")) color = 0xFF55FF; + else if (line.startsWith("LowCeil:")) color = 0xFF5555; + else if (line.startsWith("Rel:")) color = 0xFF69B4; + else if (line.startsWith("CrouchT:")) color = 0xAAAAAA; + else if (line.startsWith("NavTo:")) color = 0x55FFAA; + + int width = font.width(line); + int x = -width / 2; + + Matrix4f matrix = poseStack.last().pose(); + font.drawInBatch(line, x, startY, color, false, + matrix, bufferSource, Font.DisplayMode.SEE_THROUGH, + 0x40000000, packedLight); + startY += font.lineHeight + 2; + } + + poseStack.popPose(); + } } diff --git a/src/main/java/me/sashegdev/fabled_hearts/network/DebugSyncPacket.java b/src/main/java/me/sashegdev/fabled_hearts/network/DebugSyncPacket.java new file mode 100644 index 0000000..964feb6 --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/network/DebugSyncPacket.java @@ -0,0 +1,51 @@ +package me.sashegdev.fabled_hearts.network; + +import me.sashegdev.fabled_hearts.client.DebugClientHandler; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class DebugSyncPacket { + private final int entityId; + private final List waypoints; + private final String brainState; + + public DebugSyncPacket(int entityId, List waypoints, String brainState) { + this.entityId = entityId; + this.waypoints = waypoints; + this.brainState = brainState; + } + + public void encode(FriendlyByteBuf buf) { + buf.writeInt(entityId); + buf.writeInt(waypoints.size()); + for (BlockPos wp : waypoints) { + buf.writeInt(wp.getX()); + buf.writeInt(wp.getY()); + buf.writeInt(wp.getZ()); + } + buf.writeUtf(brainState); + } + + public static DebugSyncPacket decode(FriendlyByteBuf buf) { + int entityId = buf.readInt(); + int size = buf.readInt(); + List waypoints = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + waypoints.add(new BlockPos(buf.readInt(), buf.readInt(), buf.readInt())); + } + String brainState = buf.readUtf(); + return new DebugSyncPacket(entityId, waypoints, brainState); + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + DebugClientHandler.INSTANCE.onDebugSync(entityId, waypoints, brainState); + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/network/DebugTogglePacket.java b/src/main/java/me/sashegdev/fabled_hearts/network/DebugTogglePacket.java new file mode 100644 index 0000000..e163326 --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/network/DebugTogglePacket.java @@ -0,0 +1,34 @@ +package me.sashegdev.fabled_hearts.network; + +import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class DebugTogglePacket { + private final int entityId; + + public DebugTogglePacket(int entityId) { + this.entityId = entityId; + } + + public void encode(FriendlyByteBuf buf) { + buf.writeInt(entityId); + } + + public static DebugTogglePacket decode(FriendlyByteBuf buf) { + return new DebugTogglePacket(buf.readInt()); + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + Player player = ctx.get().getSender(); + if (player != null && player.level().getEntity(entityId) instanceof EllieEntity ellie) { + ellie.toggleDebug(); + } + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/network/ModNetworking.java b/src/main/java/me/sashegdev/fabled_hearts/network/ModNetworking.java index 8ad0242..d8b1153 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/network/ModNetworking.java +++ b/src/main/java/me/sashegdev/fabled_hearts/network/ModNetworking.java @@ -25,5 +25,9 @@ public class ModNetworking { DialogNodePacket::encode, DialogNodePacket::decode, DialogNodePacket::handle); CHANNEL.registerMessage(id++, RelationshipSyncPacket.class, RelationshipSyncPacket::encode, RelationshipSyncPacket::decode, RelationshipSyncPacket::handle); + CHANNEL.registerMessage(id++, DebugTogglePacket.class, + DebugTogglePacket::encode, DebugTogglePacket::decode, DebugTogglePacket::handle); + CHANNEL.registerMessage(id++, DebugSyncPacket.class, + DebugSyncPacket::encode, DebugSyncPacket::decode, DebugSyncPacket::handle); } } diff --git a/src/main/java/me/sashegdev/fabled_hearts/registry/ModEntities.java b/src/main/java/me/sashegdev/fabled_hearts/registry/ModEntities.java index 4090c70..d52cf8e 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/registry/ModEntities.java +++ b/src/main/java/me/sashegdev/fabled_hearts/registry/ModEntities.java @@ -15,7 +15,7 @@ public class ModEntities { public static final RegistryObject> ELLIE = ENTITIES.register("ellie", () -> EntityType.Builder.of(EllieEntity::new, MobCategory.CREATURE) - .sized(0.9f, 2.5f) + .sized(0.6f, 2.5f) .clientTrackingRange(64) .build("ellie"));