Fix AI and sleep positioning: stuck detection, cooldown, movement tracking, bed break handling, double rotation, sleep restore on load

This commit is contained in:
2026-06-14 01:23:20 +03:00
parent 15a31b1aec
commit dcd402eebf
13 changed files with 613 additions and 39 deletions
@@ -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 {
@@ -1,6 +1,7 @@
package me.sashegdev.fabled_hearts; package me.sashegdev.fabled_hearts;
import com.mojang.logging.LogUtils; 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.DialogLoader;
import me.sashegdev.fabled_hearts.dialog.DialogManager; import me.sashegdev.fabled_hearts.dialog.DialogManager;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; 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 me.sashegdev.fabled_hearts.registry.ModItems;
import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.EntityRenderersEvent; import net.minecraftforge.client.event.EntityRenderersEvent;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.AddReloadListenerEvent; import net.minecraftforge.event.AddReloadListenerEvent;
import net.minecraftforge.event.entity.EntityAttributeCreationEvent; 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.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -62,6 +65,16 @@ public class Main {
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) { public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
event.registerEntityRenderer(ModEntities.ELLIE.get(), EllieRenderer::new); 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 { public static class ForgeHandler {
@@ -76,10 +89,10 @@ public class Main {
var pos = event.getPos(); var pos = event.getPos();
event.getLevel().getEntitiesOfClass( event.getLevel().getEntitiesOfClass(
EllieEntity.class, EllieEntity.class,
new net.minecraft.world.phys.AABB(pos).inflate(2), new net.minecraft.world.phys.AABB(pos).inflate(3),
e -> e.isSleeping() && pos.equals(e.getBedPos()) e -> e.isBedAtOrAdjacent(pos) || (e.getBedPos() != null && e.getBedPos().equals(pos))
).forEach(e -> { ).forEach(e -> {
e.wakeUp(); if (e.isSleeping()) e.wakeUp();
e.forgetBed(); e.forgetBed();
}); });
} }
@@ -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;
}
}
@@ -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);
}
}
@@ -106,14 +106,18 @@ public class EllieSleepGoal extends Goal {
public boolean canContinueToUse() { public boolean canContinueToUse() {
if (ellie.isSleeping()) { if (ellie.isSleeping()) {
if (sleepTimer >= MAX_SLEEP_TICKS) return false; if (sleepTimer >= MAX_SLEEP_TICKS) return false;
if (ellie.level().isDay()) return false;
if (ellie.hurtTime > 10) return false;
if (bedPos != null) { if (bedPos != null) {
BlockState state = ellie.level().getBlockState(bedPos); BlockState state = ellie.level().getBlockState(bedPos);
if (!state.isBed(ellie.level(), bedPos, null)) return false; if (!state.isBed(ellie.level(), bedPos, null)) return false;
if (ellie.distanceToSqr(Vec3.atCenterOf(bedPos)) > 16.0) return false;
} }
return true; return true;
} }
if (bedPos == null) return false; if (bedPos == null) return false;
if (ellie.hurtTime > 10) return false; if (ellie.hurtTime > 10) return false;
if (ellie.level().isDay() && !ellie.isTired()) return false;
if (!ellie.level().isNight() && !ellie.isTired()) return false; if (!ellie.level().isNight() && !ellie.isTired()) return false;
return true; return true;
} }
@@ -1,6 +1,7 @@
package me.sashegdev.fabled_hearts.ai; package me.sashegdev.fabled_hearts.ai;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; 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.ai.goal.Goal;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
@@ -13,6 +14,11 @@ public class FollowPlayerGoal extends Goal {
private int cooldown; private int cooldown;
private static final double FOLLOW_RANGE = 16.0; private static final double FOLLOW_RANGE = 16.0;
private static final double FOLLOW_RANGE_SQR = FOLLOW_RANGE * FOLLOW_RANGE; 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) { public FollowPlayerGoal(EllieEntity ellie) {
this.ellie = ellie; this.ellie = ellie;
@@ -35,6 +41,9 @@ public class FollowPlayerGoal extends Goal {
@Override @Override
public void start() { public void start() {
cooldown = 0; cooldown = 0;
stuckTicks = 0;
stuckCooldown = 0;
lastStuckPos = null;
} }
@Override @Override
@@ -55,13 +64,13 @@ public class FollowPlayerGoal extends Goal {
if (distSqr > maxDist * maxDist) { if (distSqr > maxDist * maxDist) {
if (--cooldown <= 0) { if (--cooldown <= 0) {
cooldown = 10; cooldown = RECALC_COOLDOWN;
ellie.getNavigation().moveTo(target, 0.6); checkStuck();
} }
} else if (distSqr < minDist * minDist) { } else if (distSqr < minDist * minDist) {
ellie.getNavigation().stop(); ellie.getNavigation().stop();
if (--cooldown <= 0) { if (--cooldown <= 0) {
cooldown = 10; cooldown = RECALC_COOLDOWN;
Vec3 away = new Vec3( Vec3 away = new Vec3(
ellie.getX() - target.getX(), ellie.getX() - target.getX(),
0, 0,
@@ -77,10 +86,39 @@ public class FollowPlayerGoal extends Goal {
} else { } else {
ellie.getNavigation().stop(); ellie.getNavigation().stop();
cooldown = 0; cooldown = 0;
stuckTicks = 0;
lastStuckPos = null;
ellie.getLookControl().setLookAt(target, 30, 30); 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 @Override
public boolean canContinueToUse() { public boolean canContinueToUse() {
if (target == null || !target.isAlive()) return false; if (target == null || !target.isAlive()) return false;
@@ -94,5 +132,8 @@ public class FollowPlayerGoal extends Goal {
public void stop() { public void stop() {
target = null; target = null;
ellie.getNavigation().stop(); ellie.getNavigation().stop();
stuckTicks = 0;
stuckCooldown = 0;
lastStuckPos = null;
} }
} }
@@ -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<Integer, DebugData> debugDataMap = new ConcurrentHashMap<>();
public static class DebugData {
public final List<BlockPos> waypoints;
public final long lastUpdate;
public DebugData(List<BlockPos> 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<Integer, DebugData> 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<BlockPos> 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);
}
}
@@ -1,6 +1,7 @@
package me.sashegdev.fabled_hearts.entity.ellie; package me.sashegdev.fabled_hearts.entity.ellie;
import me.sashegdev.fabled_hearts.ai.BasicAI; 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.EllieSleepGoal;
import me.sashegdev.fabled_hearts.ai.IdleAnimationGoal; import me.sashegdev.fabled_hearts.ai.IdleAnimationGoal;
import me.sashegdev.fabled_hearts.network.ModNetworking; 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.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.network.syncher.SynchedEntityData;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.*; import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes; 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.GroundPathNavigation;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.animal.Animal; import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level; 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.BlockState;
import net.minecraft.world.level.block.state.properties.BedPart; import net.minecraft.world.level.block.state.properties.BedPart;
import net.minecraft.server.level.ServerLevel; 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.animatable.GeoEntity;
import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache; import software.bernie.geckolib.core.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.core.animation.AnimatableManager; import software.bernie.geckolib.core.animation.AnimatableManager;
@@ -51,6 +61,10 @@ public class EllieEntity extends Animal implements GeoEntity {
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT); SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT);
private static final EntityDataAccessor<Boolean> DATA_MOVING = private static final EntityDataAccessor<Boolean> DATA_MOVING =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN); SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<Boolean> DATA_DEBUG =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN);
private static final EntityDataAccessor<String> DATA_DEBUG_BRAIN =
SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.STRING);
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
@@ -58,15 +72,22 @@ public class EllieEntity extends Animal implements GeoEntity {
private float relationshipPoints; private float relationshipPoints;
private BlockPos bedPos; private BlockPos bedPos;
private double lastX, lastZ; private double lastX, lastZ;
private int moveDetectCooldown;
private BlockPos lastOpenedDoor; private BlockPos lastOpenedDoor;
private int doorCloseTimer; private int doorCloseTimer;
private int crouchTimer; private int crouchTimer;
private boolean needsSleepRestore;
public EllieEntity(EntityType<? extends Animal> type, Level level) { public EllieEntity(EntityType<? extends Animal> type, Level level) {
super(type, level); super(type, level);
((GroundPathNavigation) this.getNavigation()).setCanOpenDoors(true); ((GroundPathNavigation) this.getNavigation()).setCanOpenDoors(true);
this.setPersistenceRequired(); this.setPersistenceRequired();
lastX = getX();
lastZ = getZ();
}
@Override
public PathNavigation createNavigation(Level level) {
return new ElliePathNavigation(this, level);
} }
public static AttributeSupplier.Builder createAttributes() { 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_EATING, false);
this.entityData.define(DATA_FALL_TICKS, 0); this.entityData.define(DATA_FALL_TICKS, 0);
this.entityData.define(DATA_MOVING, false); this.entityData.define(DATA_MOVING, false);
this.entityData.define(DATA_DEBUG, false);
this.entityData.define(DATA_DEBUG_BRAIN, "");
} }
@Override @Override
@@ -100,18 +123,9 @@ public class EllieEntity extends Animal implements GeoEntity {
@Override @Override
public void aiStep() { public void aiStep() {
super.aiStep(); super.aiStep();
tickMovementDetection();
tickDoors(); tickDoors();
if (!level().isClientSide) { if (!level().isClientSide) {
tickServer(); tickServer();
}
}
private void tickMovementDetection() {
if (moveDetectCooldown-- <= 0) {
moveDetectCooldown = 2;
double dx = getX() - lastX;
double dz = getZ() - lastZ;
lastX = getX(); lastX = getX();
lastZ = getZ(); lastZ = getZ();
} }
@@ -140,15 +154,17 @@ public class EllieEntity extends Animal implements GeoEntity {
} }
public boolean isMoving() { 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 navBusy = navigation.isInProgress() && !navigation.isDone();
boolean hasVelocity = getDeltaMovement().horizontalDistanceSqr() > 0.001; boolean hasVelocity = getDeltaMovement().horizontalDistanceSqr() > 0.001;
return posChanged || navBusy || hasVelocity; return navBusy || hasVelocity;
} }
private void tickServer() { private void tickServer() {
if (needsSleepRestore && bedPos != null && entityData.get(DATA_SLEEPING)) {
setBedSleepPos(bedPos);
needsSleepRestore = false;
}
boolean isNight = level().isNight(); boolean isNight = level().isNight();
boolean raining = level().isRainingAt(blockPosition()); boolean raining = level().isRainingAt(blockPosition());
boolean lowCeiling = checkLowCeiling(); boolean lowCeiling = checkLowCeiling();
@@ -168,6 +184,10 @@ public class EllieEntity extends Animal implements GeoEntity {
entityData.set(DATA_FALL_TICKS, 0); entityData.set(DATA_FALL_TICKS, 0);
} }
if (!sleeping && moving && tickCount % 10 == 0) {
openDoorInFront();
}
boolean wasCrouching = getPose() == Pose.CROUCHING; boolean wasCrouching = getPose() == Pose.CROUCHING;
if (wasCrouching) { if (wasCrouching) {
crouchTimer = Math.max(0, crouchTimer - 1); 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<BlockPos> 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 (!sleeping) {
if (isNight) { if (isNight) {
ticksWithoutSleep++; 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<BlockPos> getPathWaypoints() {
List<BlockPos> 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() { 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) { 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 @Override
public EntityDimensions getDimensions(Pose pose) { 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); if (pose == Pose.CROUCHING) return EntityDimensions.fixed(0.6f, 1.0f);
return super.getDimensions(pose); return super.getDimensions(pose);
} }
@@ -255,17 +350,29 @@ public class EllieEntity extends Animal implements GeoEntity {
return level().getBlockState(pos).isBed(level(), pos, null); 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) { public void setBedSleepPos(BlockPos pos) {
BlockState state = level().getBlockState(pos); BlockState state = level().getBlockState(pos);
if (!(state.getBlock() instanceof BedBlock)) return; if (!(state.getBlock() instanceof BedBlock)) return;
Direction facing = state.getValue(BedBlock.FACING); Direction facing = state.getValue(BedBlock.FACING);
BlockPos footPos = state.getValue(BedBlock.PART) == BedPart.HEAD BlockPos headPos = state.getValue(BedBlock.PART) == BedPart.HEAD
? pos.relative(facing.getOpposite()) ? pos
: pos; : pos.relative(facing);
float yRot = facing.toYRot(); float yRot = facing.getOpposite().toYRot();
setPos(footPos.getX() + 0.5, footPos.getY() + 0.5625, footPos.getZ() + 0.5); setPos(headPos.getX() + 0.5, headPos.getY() + 0.5625, headPos.getZ() + 0.5);
setYRot(yRot); setYRot(yRot);
yRotO = yRot; yRotO = yRot;
setYHeadRot(yRot); setYHeadRot(yRot);
@@ -275,6 +382,7 @@ public class EllieEntity extends Animal implements GeoEntity {
public void wakeUp() { public void wakeUp() {
releaseBed(); releaseBed();
forgetBed();
setSleeping(false); setSleeping(false);
setPose(Pose.STANDING); setPose(Pose.STANDING);
navigation.stop(); navigation.stop();
@@ -392,7 +500,12 @@ public class EllieEntity extends Animal implements GeoEntity {
super.readAdditionalSaveData(tag); super.readAdditionalSaveData(tag);
ticksWithoutSleep = tag.getInt("TicksWithoutSleep"); ticksWithoutSleep = tag.getInt("TicksWithoutSleep");
relationshipPoints = tag.getFloat("Relationship"); 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 @Override
@@ -403,6 +516,8 @@ public class EllieEntity extends Animal implements GeoEntity {
public boolean isTired() { return entityData.get(DATA_TIRED); } public boolean isTired() { return entityData.get(DATA_TIRED); }
public boolean isInRain() { return entityData.get(DATA_IN_RAIN); } public boolean isInRain() { return entityData.get(DATA_IN_RAIN); }
public boolean isUnderLowCeiling() { return entityData.get(DATA_UNDER_LOW_CEILING); } 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 void setBedPos(BlockPos pos) { this.bedPos = pos; }
public BlockPos getBedPos() { return bedPos; } public BlockPos getBedPos() { return bedPos; }
public float getRelationshipPoints() { return relationshipPoints; } public float getRelationshipPoints() { return relationshipPoints; }
@@ -1,13 +1,16 @@
package me.sashegdev.fabled_hearts.entity.ellie; package me.sashegdev.fabled_hearts.entity.ellie;
import com.mojang.blaze3d.vertex.PoseStack; 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.MultiBufferSource;
import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.Pose;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.joml.Matrix4f;
import software.bernie.geckolib.renderer.GeoEntityRenderer; import software.bernie.geckolib.renderer.GeoEntityRenderer;
public class EllieRenderer extends GeoEntityRenderer<EllieEntity> { public class EllieRenderer extends GeoEntityRenderer<EllieEntity> {
@@ -25,14 +28,53 @@ public class EllieRenderer extends GeoEntityRenderer<EllieEntity> {
@Override @Override
public void render(EllieEntity entity, float entityYaw, float partialTick, PoseStack poseStack, public void render(EllieEntity entity, float entityYaw, float partialTick, PoseStack poseStack,
MultiBufferSource bufferSource, int packedLight) { MultiBufferSource bufferSource, int packedLight) {
if (entity.getPose() == Pose.SLEEPING) { 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(); poseStack.pushPose();
poseStack.translate(0, 0.5625f, 0); float heightOffset = entity.getPose() == Pose.SLEEPING ? 1.8f : entity.getBbHeight() + 0.7f;
poseStack.mulPose(Axis.XP.rotationDegrees(-90)); poseStack.translate(0, heightOffset, 0);
super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight); 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(); poseStack.popPose();
} else {
super.render(entity, entityYaw, partialTick, poseStack, bufferSource, packedLight);
}
} }
} }
@@ -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<BlockPos> waypoints;
private final String brainState;
public DebugSyncPacket(int entityId, List<BlockPos> 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<BlockPos> 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<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
DebugClientHandler.INSTANCE.onDebugSync(entityId, waypoints, brainState);
});
ctx.get().setPacketHandled(true);
}
}
@@ -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<NetworkEvent.Context> 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);
}
}
@@ -25,5 +25,9 @@ public class ModNetworking {
DialogNodePacket::encode, DialogNodePacket::decode, DialogNodePacket::handle); DialogNodePacket::encode, DialogNodePacket::decode, DialogNodePacket::handle);
CHANNEL.registerMessage(id++, RelationshipSyncPacket.class, CHANNEL.registerMessage(id++, RelationshipSyncPacket.class,
RelationshipSyncPacket::encode, RelationshipSyncPacket::decode, RelationshipSyncPacket::handle); 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);
} }
} }
@@ -15,7 +15,7 @@ public class ModEntities {
public static final RegistryObject<EntityType<EllieEntity>> ELLIE = public static final RegistryObject<EntityType<EllieEntity>> ELLIE =
ENTITIES.register("ellie", () -> EntityType.Builder.of(EllieEntity::new, MobCategory.CREATURE) ENTITIES.register("ellie", () -> EntityType.Builder.of(EllieEntity::new, MobCategory.CREATURE)
.sized(0.9f, 2.5f) .sized(0.6f, 2.5f)
.clientTrackingRange(64) .clientTrackingRange(64)
.build("ellie")); .build("ellie"));