diff --git a/patches/net/minecraft/client/renderer/entity/LivingEntityRenderer.java.patch b/patches/net/minecraft/client/renderer/entity/LivingEntityRenderer.java.patch new file mode 100644 index 0000000..8729499 --- /dev/null +++ b/patches/net/minecraft/client/renderer/entity/LivingEntityRenderer.java.patch @@ -0,0 +1,39 @@ +--- a/net/minecraft/client/renderer/entity/LivingEntityRenderer.java ++++ b/net/minecraft/client/renderer/entity/LivingEntityRenderer.java +@@ -49,14 +_,17 @@ + } + + public void m_7392_(T p_115308_, float p_115309_, float p_115310_, PoseStack p_115311_, MultiBufferSource p_115312_, int p_115313_) { ++ if (net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Pre(p_115308_, this, p_115310_, p_115311_, p_115312_, p_115313_))) return; + p_115311_.m_85836_(); + this.f_115290_.f_102608_ = this.m_115342_(p_115308_, p_115310_); +- this.f_115290_.f_102609_ = p_115308_.m_20159_(); ++ ++ boolean shouldSit = p_115308_.m_20159_() && (p_115308_.m_20202_() != null && p_115308_.m_20202_().shouldRiderSit()); ++ this.f_115290_.f_102609_ = shouldSit; + this.f_115290_.f_102610_ = p_115308_.m_6162_(); + float f = Mth.m_14189_(p_115310_, p_115308_.f_20884_, p_115308_.f_20883_); + float f1 = Mth.m_14189_(p_115310_, p_115308_.f_20886_, p_115308_.f_20885_); + float f2 = f1 - f; +- if (p_115308_.m_20159_() && p_115308_.m_20202_() instanceof LivingEntity) { ++ if (shouldSit && p_115308_.m_20202_() instanceof LivingEntity) { + LivingEntity livingentity = (LivingEntity)p_115308_.m_20202_(); + f = Mth.m_14189_(p_115310_, livingentity.f_20884_, livingentity.f_20883_); + f2 = f1 - f; +@@ -98,7 +_,7 @@ + p_115311_.m_252880_(0.0F, -1.501F, 0.0F); + float f8 = 0.0F; + float f5 = 0.0F; +- if (!p_115308_.m_20159_() && p_115308_.m_6084_()) { ++ if (!shouldSit && p_115308_.m_6084_()) { + f8 = p_115308_.f_267362_.m_267711_(p_115310_); + f5 = p_115308_.f_267362_.m_267590_(p_115310_); + if (p_115308_.m_6162_()) { +@@ -131,6 +_,7 @@ + + p_115311_.m_85849_(); + super.m_7392_(p_115308_, p_115309_, p_115310_, p_115311_, p_115312_, p_115313_); ++ net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.client.event.RenderLivingEvent.Post(p_115308_, this, p_115310_, p_115311_, p_115312_, p_115313_)); + } + + @Nullable diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/EllieDoorInteractGoal.java b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieDoorInteractGoal.java new file mode 100644 index 0000000..df87281 --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieDoorInteractGoal.java @@ -0,0 +1,177 @@ +package me.sashegdev.fabled_hearts.ai; + +import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Path; +import net.minecraft.world.phys.Vec3; + +import java.util.EnumSet; + +public class EllieDoorInteractGoal extends Goal { + private static final int CLOSE_DELAY = 25; + private static final double CLOSE_DIST_SQ = 2.0 * 2.0; + private static final double WAYPOINT_REACH_DIST = 0.4; + + private final EllieEntity ellie; + private BlockPos doorPos; + private BlockPos pairedDoorPos; + private Vec3 passageCenter; + private boolean hasPassed; + private int closeTimer; + + public EllieDoorInteractGoal(EllieEntity ellie) { + this.ellie = ellie; + this.setFlags(EnumSet.noneOf(Flag.class)); + } + + @Override + public boolean canUse() { + if (ellie.tickCount < 20) return false; + return !ellie.isSleeping(); + } + + @Override + public boolean requiresUpdateEveryTick() { + return true; + } + + @Override + public void tick() { + if (doorPos != null) { + BlockState state = ellie.level().getBlockState(doorPos); + if (!(state.getBlock() instanceof DoorBlock)) { + clear(); + return; + } + + if (passageCenter != null) { + boolean reached = ellie.distanceToSqr(passageCenter) < WAYPOINT_REACH_DIST * WAYPOINT_REACH_DIST + || ellie.getNavigation().isDone(); + if (reached) { + passageCenter = null; + } else { + ellie.getNavigation().moveTo(passageCenter.x, passageCenter.y, passageCenter.z, 0.6); + } + } + + if (!hasPassed) { + double distSq = ellie.distanceToSqr(Vec3.atCenterOf(doorPos)); + if (distSq > CLOSE_DIST_SQ) { + hasPassed = true; + closeTimer = CLOSE_DELAY; + } + } else if (--closeTimer <= 0) { + closeDoor(doorPos); + if (pairedDoorPos != null) { + closeDoor(pairedDoorPos); + } + clear(); + } + return; + } + + BlockPos door = findDoor(); + if (door != null) { + BlockState state = ellie.level().getBlockState(door); + openDoor(door, state); + + doorPos = door; + pairedDoorPos = findPairedDoor(door, state); + if (pairedDoorPos != null) { + BlockState pairedState = ellie.level().getBlockState(pairedDoorPos); + if (pairedState.getBlock() instanceof DoorBlock) { + if (!pairedState.getValue(DoorBlock.OPEN)) { + openDoor(pairedDoorPos, pairedState); + } + } + } + + hasPassed = false; + closeTimer = 0; + passageCenter = calcPassageCenter(); + ellie.getNavigation().moveTo(passageCenter.x, passageCenter.y, passageCenter.z, 0.6); + } + } + + private void openDoor(BlockPos pos, BlockState state) { + if (state.getBlock() instanceof DoorBlock door && !state.getValue(DoorBlock.OPEN)) { + door.setOpen(ellie, ellie.level(), state, pos, true); + } + } + + private void closeDoor(BlockPos pos) { + BlockState state = ellie.level().getBlockState(pos); + if (state.getBlock() instanceof DoorBlock door && state.getValue(DoorBlock.OPEN)) { + door.setOpen(null, ellie.level(), state, pos, false); + } + } + + private BlockPos findDoor() { + Path path = ellie.getNavigation().getPath(); + if (path != null && !path.isDone()) { + BlockPos entityPos = ellie.blockPosition(); + for (int i = 0; i < Math.min(8, path.getNodeCount()); i++) { + Node node = path.getNode(i); + if (node == null) continue; + BlockPos checkPos = new BlockPos(node.x, node.y, node.z); + if (checkPos.distSqr(entityPos) > 64) continue; + if (checkForDoor(checkPos)) return checkPos; + if (checkForDoor(checkPos.above())) return checkPos.above(); + } + } + BlockPos center = ellie.blockPosition(); + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + BlockPos checkPos = center.offset(dx, 0, dz); + if (checkForDoor(checkPos)) return checkPos; + if (checkForDoor(checkPos.above())) return checkPos.above(); + } + } + return null; + } + + private boolean checkForDoor(BlockPos pos) { + BlockState state = ellie.level().getBlockState(pos); + return state.getBlock() instanceof DoorBlock && !state.getValue(DoorBlock.OPEN); + } + + private BlockPos findPairedDoor(BlockPos pos, BlockState state) { + Direction facing = state.getValue(DoorBlock.FACING); + for (Direction adj : Direction.Plane.HORIZONTAL) { + BlockPos adjPos = pos.relative(adj); + BlockState adjState = ellie.level().getBlockState(adjPos); + if (adjState.getBlock() instanceof DoorBlock) { + Direction adjFacing = adjState.getValue(DoorBlock.FACING); + if (adjFacing == facing.getOpposite()) { + return adjPos; + } + } + } + return null; + } + + private Vec3 calcPassageCenter() { + if (pairedDoorPos != null) { + return Vec3.atCenterOf(doorPos).add(Vec3.atCenterOf(pairedDoorPos)).scale(0.5); + } + return Vec3.atCenterOf(doorPos); + } + + private void clear() { + doorPos = null; + pairedDoorPos = null; + passageCenter = null; + hasPassed = false; + closeTimer = 0; + } + + @Override + public void stop() { + clear(); + } +} diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java b/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java index fc974c6..42d1fdf 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/ElliePathNavigation.java @@ -1,20 +1,33 @@ package me.sashegdev.fabled_hearts.ai; +import net.minecraft.core.BlockPos; 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.BlockPathTypes; import net.minecraft.world.level.pathfinder.PathFinder; +import net.minecraft.world.phys.Vec3; public class ElliePathNavigation extends GroundPathNavigation { + private Vec3 lastDest; + private double lastSpeed; + private BlockPos lastStuckCheck; + private int stuckTicks; + private int repathTimer; + + private static final int REPATH_INTERVAL = 60; + private static final int STUCK_THRESHOLD = 40; + private static final int CROUCH_STUCK_THRESHOLD = 120; + private static final double PROGRESS_MIN_SQ = 0.2 * 0.2; + public ElliePathNavigation(Mob mob, Level level) { super(mob, level); } @Override protected PathFinder createPathFinder(int maxVisitedNodes) { - mob.setPathfindingMalus(BlockPathTypes.DOOR_WOOD_CLOSED, 2.0F); + mob.setPathfindingMalus(BlockPathTypes.DOOR_WOOD_CLOSED, 0.0F); mob.setPathfindingMalus(BlockPathTypes.DOOR_IRON_CLOSED, 5.0F); mob.setPathfindingMalus(BlockPathTypes.DANGER_FIRE, -1.0F); mob.setPathfindingMalus(BlockPathTypes.DAMAGE_FIRE, -1.0F); @@ -29,4 +42,65 @@ public class ElliePathNavigation extends GroundPathNavigation { this.nodeEvaluator = evaluator; return new PathFinder(evaluator, maxVisitedNodes); } + + @Override + public void tick() { + if (isInProgress() && lastDest != null) { + BlockPos currentPos = mob.blockPosition(); + + boolean crouching = mob.getPose() == net.minecraft.world.entity.Pose.CROUCHING; + + if (lastStuckCheck != null) { + double movedSq = currentPos.distSqr(lastStuckCheck); + if (movedSq < PROGRESS_MIN_SQ) { + stuckTicks++; + } else { + stuckTicks = 0; + } + } + lastStuckCheck = currentPos; + + int threshold = crouching ? CROUCH_STUCK_THRESHOLD : STUCK_THRESHOLD; + if (stuckTicks > threshold) { + moveTo(lastDest.x, lastDest.y, lastDest.z, lastSpeed); + stuckTicks = 0; + repathTimer = 0; + } + + if (!crouching && repathTimer++ > REPATH_INTERVAL) { + moveTo(lastDest.x, lastDest.y, lastDest.z, lastSpeed); + repathTimer = 0; + } + } else if (isDone() && lastDest != null) { + if (mob.distanceToSqr(lastDest) > 4.0) { + if (repathTimer++ > REPATH_INTERVAL) { + moveTo(lastDest.x, lastDest.y, lastDest.z, lastSpeed); + repathTimer = 0; + } + } else { + lastDest = null; + } + } + + super.tick(); + } + + @Override + public boolean moveTo(double x, double y, double z, double speed) { + lastDest = new Vec3(x, y, z); + lastSpeed = speed; + stuckTicks = 0; + repathTimer = 0; + lastStuckCheck = mob.blockPosition(); + return super.moveTo(x, y, z, speed); + } + + @Override + public void stop() { + super.stop(); + lastDest = null; + stuckTicks = 0; + repathTimer = 0; + lastStuckCheck = null; + } } 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 3f1008e..c89b64a 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/EllieSleepGoal.java @@ -4,10 +4,7 @@ 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.level.block.BedBlock; -import net.minecraft.world.level.block.DoorBlock; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.pathfinder.Node; -import net.minecraft.world.level.pathfinder.Path; import net.minecraft.world.phys.Vec3; import java.util.EnumSet; @@ -62,8 +59,6 @@ public class EllieSleepGoal extends Goal { return; } - tryOpenDoor(); - double distSqr = ellie.distanceToSqr(Vec3.atCenterOf(bedPos)); if (distSqr < REACH_DIST_SQR) { @@ -87,35 +82,6 @@ public class EllieSleepGoal extends Goal { } } - private void tryOpenDoor() { - Path path = ellie.getNavigation().getPath(); - BlockPos entityPos = ellie.blockPosition(); - if (path != null) { - for (int i = 0; i < Math.min(4, path.getNodeCount()); i++) { - Node node = path.getNode(i); - BlockPos checkPos = new BlockPos(node.x, node.y, node.z); - if (checkPos.distSqr(entityPos) > 9) break; - BlockState state = ellie.level().getBlockState(checkPos); - if (state.getBlock() instanceof DoorBlock) { - if (!state.getValue(DoorBlock.OPEN)) { - ellie.level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3); - } - return; - } - } - } - for (int i = 0; i < 3; i++) { - BlockPos checkPos = entityPos.relative(ellie.getDirection(), i); - BlockState state = ellie.level().getBlockState(checkPos); - if (state.getBlock() instanceof DoorBlock) { - if (!state.getValue(DoorBlock.OPEN)) { - ellie.level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3); - } - return; - } - } - } - @Override public void stop() { if (ellie.isSleeping()) { 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 3b01382..a53be95 100644 --- a/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/FollowPlayerGoal.java @@ -2,6 +2,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.Entity; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; @@ -27,8 +28,20 @@ public class FollowPlayerGoal extends Goal { @Override public boolean canUse() { + if (ellie.tickCount < 20) return false; if (ellie.isSleeping()) return false; if (ellie.hurtTime > 0) return false; + + int followId = ellie.getFollowTargetId(); + if (followId > 0) { + Entity e = ellie.level().getEntity(followId); + if (e instanceof Player p && p.isAlive()) { + target = p; + return true; + } + return false; + } + float rel = ellie.getRelationshipPoints(); if (rel < 50) return false; @@ -51,15 +64,19 @@ public class FollowPlayerGoal extends Goal { if (target == null) return; double distSqr = ellie.distanceToSqr(target); - float rel = ellie.getRelationshipPoints(); double minDist, maxDist; - if (rel >= 100) { - minDist = 1.5; maxDist = 3.0; - } else if (rel >= 75) { - minDist = 3.0; maxDist = 6.0; + if (ellie.getFollowTargetId() == target.getId()) { + minDist = 2.0; maxDist = 4.0; } else { - minDist = 6.0; maxDist = 10.0; + float rel = ellie.getRelationshipPoints(); + if (rel >= 100) { + minDist = 1.5; maxDist = 3.0; + } else if (rel >= 75) { + minDist = 3.0; maxDist = 6.0; + } else { + minDist = 6.0; maxDist = 10.0; + } } if (distSqr > maxDist * maxDist) { @@ -124,6 +141,9 @@ public class FollowPlayerGoal extends Goal { if (target == null || !target.isAlive()) return false; if (ellie.isSleeping()) return false; if (ellie.hurtTime > 0) return false; + if (ellie.getFollowTargetId() == target.getId()) { + return ellie.isFollowingPlayer(); + } double distSqr = ellie.distanceToSqr(target); return distSqr <= FOLLOW_RANGE_SQR; } diff --git a/src/main/java/me/sashegdev/fabled_hearts/ai/ShelterGoal.java b/src/main/java/me/sashegdev/fabled_hearts/ai/ShelterGoal.java new file mode 100644 index 0000000..4b5f36b --- /dev/null +++ b/src/main/java/me/sashegdev/fabled_hearts/ai/ShelterGoal.java @@ -0,0 +1,94 @@ +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.level.Level; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; + +import java.util.EnumSet; + +public class ShelterGoal extends Goal { + private final EllieEntity ellie; + private BlockPos shelterPos; + private static final int SEARCH_RADIUS = 12; + private static final double REACH_DIST_SQR = 1.5 * 1.5; + + public ShelterGoal(EllieEntity ellie) { + this.ellie = ellie; + this.setFlags(EnumSet.of(Flag.MOVE)); + } + + @Override + public boolean canUse() { + if (ellie.tickCount < 20) return false; + if (ellie.isSleeping()) return false; + if (!ellie.level().isRaining() && !ellie.level().isThundering()) return false; + if (!ellie.isInRain()) return false; + return true; + } + + @Override + public void start() { + shelterPos = findShelter(); + if (shelterPos != null) { + ellie.getNavigation().moveTo(shelterPos.getX() + 0.5, shelterPos.getY(), shelterPos.getZ() + 0.5, 0.6); + } + } + + @Override + public void tick() { + if (shelterPos == null) return; + if (ellie.distanceToSqr(Vec3.atCenterOf(shelterPos)) < REACH_DIST_SQR) { + ellie.getNavigation().stop(); + } else if (ellie.getNavigation().isDone()) { + ellie.getNavigation().moveTo(shelterPos.getX() + 0.5, shelterPos.getY(), shelterPos.getZ() + 0.5, 0.6); + } + } + + @Override + public boolean canContinueToUse() { + if (shelterPos == null) return false; + if (ellie.isSleeping()) return false; + return ellie.level().isRaining() || ellie.level().isThundering(); + } + + @Override + public void stop() { + shelterPos = null; + } + + private boolean hasShelter(BlockPos pos) { + Level level = ellie.level(); + for (int y = 1; y <= 3; y++) { + if (level.getBlockState(pos.above(y)).blocksMotion()) { + return true; + } + } + return false; + } + + private BlockPos findShelter() { + BlockPos entityPos = ellie.blockPosition(); + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + BlockPos nearest = null; + double nearestDist = Double.MAX_VALUE; + + for (int x = -SEARCH_RADIUS; x <= SEARCH_RADIUS; x++) { + for (int z = -SEARCH_RADIUS; z <= SEARCH_RADIUS; z++) { + mutable.set(entityPos.getX() + x, entityPos.getY(), entityPos.getZ() + z); + int topY = ellie.level().getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, mutable).getY(); + mutable.setY(topY); + if (hasShelter(mutable)) { + double dist = entityPos.distSqr(mutable); + if (dist < nearestDist) { + nearestDist = dist; + nearest = mutable.immutable(); + } + } + } + } + return nearest; + } +} 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 0ee3635..478d850 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,9 +1,11 @@ package me.sashegdev.fabled_hearts.entity.ellie; import me.sashegdev.fabled_hearts.ai.BasicAI; +import me.sashegdev.fabled_hearts.ai.EllieDoorInteractGoal; 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.ai.ShelterGoal; import me.sashegdev.fabled_hearts.network.ModNetworking; import me.sashegdev.fabled_hearts.network.OpenDialogPacket; import net.minecraft.core.BlockPos; @@ -16,7 +18,10 @@ import java.util.ArrayList; import java.util.List; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.*; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.server.level.ServerPlayer; @@ -26,13 +31,11 @@ import net.minecraft.world.entity.animal.Animal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BedBlock; -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.minecraft.world.level.pathfinder.BlockPathTypes; import net.minecraftforge.network.PacketDistributor; import me.sashegdev.fabled_hearts.network.DebugSyncPacket; import me.sashegdev.fabled_hearts.network.ModNetworking; @@ -66,16 +69,27 @@ public class EllieEntity extends Animal implements GeoEntity { SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.BOOLEAN); private static final EntityDataAccessor DATA_DEBUG_BRAIN = SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_TURNING_TIMER = + SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_DEBUG_ANIM = + SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_FOLLOW_TARGET = + SynchedEntityData.defineId(EllieEntity.class, EntityDataSerializers.INT); + + private static final List DEBUG_ANIMS = List.of( + "idle1", "idle2", "idle3", "turn", "eat1", "fall" + ); private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this); private int ticksWithoutSleep; private float relationshipPoints; + private float prevYRot; + private float yawDeltaAccumulator; private BlockPos bedPos; private double lastX, lastZ; - private BlockPos lastOpenedDoor; - private int doorCloseTimer; private int crouchTimer; + private int debugAnimTimer; private boolean needsSleepRestore; public EllieEntity(EntityType type, Level level) { @@ -84,6 +98,7 @@ public class EllieEntity extends Animal implements GeoEntity { this.setPersistenceRequired(); lastX = getX(); lastZ = getZ(); + prevYRot = getYRot(); } @Override @@ -111,20 +126,33 @@ public class EllieEntity extends Animal implements GeoEntity { this.entityData.define(DATA_MOVING, false); this.entityData.define(DATA_DEBUG, false); this.entityData.define(DATA_DEBUG_BRAIN, ""); + this.entityData.define(DATA_TURNING_TIMER, 0); + this.entityData.define(DATA_DEBUG_ANIM, 0); + this.entityData.define(DATA_FOLLOW_TARGET, 0); } @Override protected void registerGoals() { - BasicAI.addBasicGoals(this, this.goalSelector, 2); + BasicAI.addBasicGoals(this, this.goalSelector, 4); this.goalSelector.addGoal(1, new EllieSleepGoal(this, 30)); + this.goalSelector.addGoal(1, new EllieDoorInteractGoal(this)); this.goalSelector.addGoal(1, new IdleAnimationGoal(this)); + this.goalSelector.addGoal(2, new ShelterGoal(this)); this.goalSelector.addGoal(3, new me.sashegdev.fabled_hearts.ai.FollowPlayerGoal(this)); } @Override public void aiStep() { + if (!level().isClientSide && !entityData.get(DATA_SLEEPING)) { + boolean aheadLow = checkLowCeiling(); + if (aheadLow) { + crouchTimer = Math.max(crouchTimer, 20); + if (onGround()) { + setPose(Pose.CROUCHING); + } + } + } super.aiStep(); - tickDoors(); if (!level().isClientSide) { tickServer(); lastX = getX(); @@ -132,28 +160,6 @@ public class EllieEntity extends Animal implements GeoEntity { } } - private void tickDoors() { - if (lastOpenedDoor != null) { - BlockState state = level().getBlockState(lastOpenedDoor); - if (state.getBlock() instanceof DoorBlock && state.getValue(DoorBlock.OPEN)) { - double dist = distanceToSqr(lastOpenedDoor.getX() + 0.5, lastOpenedDoor.getY(), lastOpenedDoor.getZ() + 0.5); - if (dist > 0.5 * 0.5) { - doorCloseTimer++; - if (doorCloseTimer > 10) { - level().setBlock(lastOpenedDoor, state.setValue(DoorBlock.OPEN, false), 3); - lastOpenedDoor = null; - doorCloseTimer = 0; - } - } else { - doorCloseTimer = 0; - } - } else { - lastOpenedDoor = null; - doorCloseTimer = 0; - } - } - } - public boolean isMoving() { boolean navBusy = navigation.isInProgress() && !navigation.isDone(); boolean hasVelocity = getDeltaMovement().horizontalDistanceSqr() > 0.001; @@ -166,6 +172,13 @@ public class EllieEntity extends Animal implements GeoEntity { needsSleepRestore = false; } + if (entityData.get(DATA_SLEEPING) && bedPos != null) { + BlockState bedState = level().getBlockState(bedPos); + if (!(bedState.getBlock() instanceof BedBlock)) { + wakeUp(); + } + } + boolean isNight = level().isNight(); boolean raining = level().isRainingAt(blockPosition()); boolean lowCeiling = checkLowCeiling(); @@ -185,10 +198,6 @@ 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 (lowCeiling) { @@ -234,6 +243,38 @@ public class EllieEntity extends Animal implements GeoEntity { } else { ticksWithoutSleep = Math.max(0, ticksWithoutSleep - 10); } + + int turnTimer = entityData.get(DATA_TURNING_TIMER); + if (turnTimer > 0) { + entityData.set(DATA_TURNING_TIMER, turnTimer - 1); + if (turnTimer == 1) yawDeltaAccumulator = 0; + } else if (!sleeping && !falling) { + float yawDelta = Math.abs(getYRot() - prevYRot); + yawDeltaAccumulator += yawDelta; + if (yawDeltaAccumulator > 30 && !moving) { + entityData.set(DATA_TURNING_TIMER, 17); + yawDeltaAccumulator = 0; + } + } + prevYRot = getYRot(); + + if (tickCount > 20) { + int debugAnim = entityData.get(DATA_DEBUG_ANIM); + if (debugAnim > 0) { + debugAnimTimer--; + if (debugAnimTimer <= 0) { + entityData.set(DATA_DEBUG_ANIM, 0); + } + } + + int followTarget = entityData.get(DATA_FOLLOW_TARGET); + if (followTarget > 0) { + Entity target = level().getEntity(followTarget); + if (!(target instanceof Player) || !target.isAlive() || target.distanceToSqr(this) > 64 * 64) { + entityData.set(DATA_FOLLOW_TARGET, 0); + } + } + } } private void updateDebugBrainString() { @@ -251,6 +292,7 @@ public class EllieEntity extends Animal implements GeoEntity { 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("|"); + sb.append("TurnT:").append(entityData.get(DATA_TURNING_TIMER)).append("|"); if (navigation.isInProgress() && !navigation.isDone()) { sb.append("NavTo:").append(navigation.getTargetPos()); } else { @@ -283,9 +325,7 @@ public class EllieEntity extends Animal implements GeoEntity { if (isSpaceLow(pos)) return true; if (isMoving() && navigation.isInProgress()) { Direction dir = getDirection(); - for (int i = 1; i <= 2; i++) { - if (isSpaceLow(pos.relative(dir, i))) return true; - } + if (isSpaceLow(pos.relative(dir, 1))) return true; } return false; } @@ -301,20 +341,30 @@ public class EllieEntity extends Animal implements GeoEntity { @Override public EntityDimensions getDimensions(Pose pose) { - if (pose == Pose.SLEEPING) return EntityDimensions.fixed(0.6f, 0.6f); + if (pose == Pose.SLEEPING) return EntityDimensions.fixed(0.15f, 0.15f); if (pose == Pose.CROUCHING) return EntityDimensions.fixed(0.6f, 1.0f); return super.getDimensions(pose); } + @Override + public float getEyeHeight(Pose pose) { + if (pose == Pose.STANDING && getPose() == Pose.SLEEPING) return 0.1f; + return super.getEyeHeight(pose); + } + public boolean occupyBed(BlockPos pos) { BlockState state = level().getBlockState(pos); if (state.getBlock() instanceof BedBlock) { if (state.getValue(BedBlock.OCCUPIED)) return false; + Direction facing = state.getValue(BedBlock.FACING); BlockPos foot = state.getValue(BedBlock.PART) == BedPart.HEAD - ? pos.relative(state.getValue(BedBlock.FACING).getOpposite()) + ? pos.relative(facing.getOpposite()) : pos; - level().setBlock(foot, state.setValue(BedBlock.OCCUPIED, true), 3); - BlockPos head = foot.relative(state.getValue(BedBlock.FACING)); + BlockState footState = level().getBlockState(foot); + if (footState.getBlock() instanceof BedBlock) { + level().setBlock(foot, footState.setValue(BedBlock.OCCUPIED, true), 3); + } + BlockPos head = foot.relative(facing); BlockState headState = level().getBlockState(head); if (headState.getBlock() instanceof BedBlock) { level().setBlock(head, headState.setValue(BedBlock.OCCUPIED, true), 3); @@ -370,7 +420,11 @@ public class EllieEntity extends Animal implements GeoEntity { : pos.relative(facing); setPose(Pose.SLEEPING); - setPos(headPos.getX() + 0.5, headPos.getY() + 0.5625, headPos.getZ() + 0.5); + BlockPos footPos = headPos.relative(facing.getOpposite()); + double footX = footPos.getX() + 0.5 - facing.getStepX() * 0.5; + double footZ = footPos.getZ() + 0.5 - facing.getStepZ() * 0.5; + setPos(footX, footPos.getY() + 0.6875, footZ); + setSleepingPos(headPos); float yRot = facing.getOpposite().toYRot(); setYRot(yRot); yRotO = yRot; @@ -387,45 +441,17 @@ public class EllieEntity extends Animal implements GeoEntity { setDeltaMovement(getDeltaMovement().add(0, 0.15, 0)); } - public void openDoorInFront() { - Path path = navigation.getPath(); - BlockPos entityPos = blockPosition(); - if (path != null) { - for (int i = 0; i < Math.min(4, path.getNodeCount()); i++) { - Node node = path.getNode(i); - BlockPos checkPos = new BlockPos(node.x, node.y, node.z); - if (checkPos.distSqr(entityPos) > 9) break; - BlockState state = level().getBlockState(checkPos); - if (state.getBlock() instanceof DoorBlock) { - if (!state.getValue(DoorBlock.OPEN)) { - level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3); - lastOpenedDoor = checkPos; - doorCloseTimer = 0; - } - return; - } - } - } - for (int i = 0; i < 3; i++) { - BlockPos checkPos = entityPos.relative(getDirection(), 2 + i); - BlockState state = level().getBlockState(checkPos); - if (state.getBlock() instanceof DoorBlock) { - if (!state.getValue(DoorBlock.OPEN)) { - level().setBlock(checkPos, state.setValue(DoorBlock.OPEN, true), 3); - lastOpenedDoor = checkPos; - doorCloseTimer = 0; - } - return; - } - } - } - @Override public void registerControllers(AnimatableManager.ControllerRegistrar controllers) { controllers.add(new AnimationController<>(this, "main", 4, this::animationHandler)); } private PlayState animationHandler(AnimationState state) { + int debugAnim = entityData.get(DATA_DEBUG_ANIM); + if (debugAnim > 0 && debugAnim <= DEBUG_ANIMS.size()) { + return state.setAndContinue(RawAnimation.begin().thenPlay(DEBUG_ANIMS.get(debugAnim - 1))); + } + int idleAnim = entityData.get(DATA_IDLE_ANIM); if (idleAnim > 0) { String animName = switch (idleAnim) { @@ -466,6 +492,10 @@ public class EllieEntity extends Animal implements GeoEntity { if (fallTicks > 5) return RawAnimation.begin().thenPlay("fall"); if (eating) return RawAnimation.begin().thenPlay("eat1"); + if (entityData.get(DATA_TURNING_TIMER) > 0) { + return RawAnimation.begin().thenPlay("turn").thenLoop(resolveTurnLoopName()); + } + if (tired) return moving ? RawAnimation.begin().thenLoop("walkingsleepy") : RawAnimation.begin().thenLoop("idlesleepy"); @@ -479,6 +509,19 @@ public class EllieEntity extends Animal implements GeoEntity { return RawAnimation.begin().thenLoop("idleins"); } + private String resolveTurnLoopName() { + boolean tired = entityData.get(DATA_TIRED); + boolean inRain = entityData.get(DATA_IN_RAIN); + boolean lowCeiling = entityData.get(DATA_UNDER_LOW_CEILING); + boolean moving = entityData.get(DATA_MOVING); + + if (tired) return moving ? "walkingsleepy" : "idlesleepy"; + if (inRain) return moving ? "walkingrain" : "idleconrain"; + if (lowCeiling) return moving ? "shiftwalking" : "shiftidle"; + if (moving) return "walkingsimple"; + return "idleins"; + } + @Override public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob other) { return null; @@ -486,6 +529,31 @@ public class EllieEntity extends Animal implements GeoEntity { @Override public InteractionResult mobInteract(Player player, InteractionHand hand) { + ItemStack stack = player.getItemInHand(hand); + boolean holdingStick = stack.is(Items.STICK); + + if (holdingStick && player.isShiftKeyDown()) { + if (!level().isClientSide) { + int current = entityData.get(DATA_FOLLOW_TARGET); + if (current == player.getId()) { + entityData.set(DATA_FOLLOW_TARGET, 0); + } else { + entityData.set(DATA_FOLLOW_TARGET, player.getId()); + } + } + return InteractionResult.SUCCESS; + } + + if (holdingStick) { + if (!level().isClientSide) { + int current = entityData.get(DATA_DEBUG_ANIM); + int next = (current % DEBUG_ANIMS.size()) + 1; + entityData.set(DATA_DEBUG_ANIM, next); + debugAnimTimer = 200; + } + return InteractionResult.SUCCESS; + } + if (player.isShiftKeyDown()) return InteractionResult.PASS; if (entityData.get(DATA_SLEEPING)) { @@ -503,6 +571,24 @@ public class EllieEntity extends Animal implements GeoEntity { return InteractionResult.SUCCESS; } + @Override + public boolean hurt(DamageSource source, float amount) { + if (source.getEntity() instanceof Player player) { + ItemStack weapon = player.getMainHandItem(); + if (weapon.is(Items.STICK)) { + if (!level().isClientSide) { + int current = entityData.get(DATA_DEBUG_ANIM); + if (current > 0) { + entityData.set(DATA_DEBUG_ANIM, current); + debugAnimTimer = 200; + } + } + return false; + } + } + return super.hurt(source, amount); + } + @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); @@ -541,4 +627,7 @@ public class EllieEntity extends Animal implements GeoEntity { this.relationshipPoints = Math.min(100, Math.max(0, this.relationshipPoints + amount)); } public int getTicksWithoutSleep() { return ticksWithoutSleep; } + public int getFollowTargetId() { return entityData.get(DATA_FOLLOW_TARGET); } + public boolean isFollowingPlayer() { return entityData.get(DATA_FOLLOW_TARGET) > 0; } + public int getDebugAnim() { return entityData.get(DATA_DEBUG_ANIM); } } 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 939dc2a..7de663e 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 @@ -2,13 +2,18 @@ package me.sashegdev.fabled_hearts.entity.ellie; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; 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.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.Pose; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; import software.bernie.geckolib.renderer.GeoEntityRenderer; @@ -19,6 +24,31 @@ public class EllieRenderer extends GeoEntityRenderer { this.shadowRadius = 0.5f; } + @Override + protected void applyRotations(EllieEntity entity, PoseStack poseStack, float tick, float yRot, float partialTick) { + if (entity.getPose() == Pose.SLEEPING) { + BlockPos bedPos = entity.getBedPos(); + if (bedPos != null) { + BlockState state = entity.level().getBlockState(bedPos); + if (state.getBlock() instanceof BedBlock) { + Direction facing = state.getValue(BedBlock.FACING); + float angle = switch (facing) { + case SOUTH -> 90; + case WEST -> 0; + case NORTH -> 270; + case EAST -> 180; + default -> 180; + }; + poseStack.mulPose(Axis.YP.rotationDegrees(angle)); + poseStack.mulPose(Axis.ZP.rotationDegrees(90)); + poseStack.mulPose(Axis.YP.rotationDegrees(270)); + return; + } + } + } + super.applyRotations(entity, poseStack, tick, yRot, partialTick); + } + @Override public RenderType getRenderType(EllieEntity animatable, ResourceLocation texture, @Nullable MultiBufferSource bufferSource, float partialTick) { @@ -64,6 +94,7 @@ public class EllieRenderer extends GeoEntityRenderer { else if (line.startsWith("Rel:")) color = 0xFF69B4; else if (line.startsWith("CrouchT:")) color = 0xAAAAAA; else if (line.startsWith("NavTo:")) color = 0x55FFAA; + else if (line.startsWith("TurnT:")) color = 0xFFAAFF; int width = font.width(line); int x = -width / 2; diff --git a/src/main/resources/assets/fabled_hearts/animations/ellie.animation.json b/src/main/resources/assets/fabled_hearts/animations/ellie.animation.json index 04a3e96..e039913 100644 --- a/src/main/resources/assets/fabled_hearts/animations/ellie.animation.json +++ b/src/main/resources/assets/fabled_hearts/animations/ellie.animation.json @@ -13899,97 +13899,53 @@ "bones": { "root": { "rotation": { - "vector": [90, 0, -180] + "vector": [0, 0, 0] }, "position": { - "vector": [0, 9.1, 9] + "vector": [0, 0, 0] } }, "torso": { "rotation": { - "0.0833": { - "vector": [0, 0, -5] - }, - "1.0833": { - "vector": [2, 0, -5] - }, - "1.3333": { - "vector": [2.25, 0, -5] - }, - "2.0833": { - "vector": [0, 0, -5] - } + "0.0833": { "vector": [0, 0, -5] }, + "1.0833": { "vector": [2, 0, -5] }, + "1.3333": { "vector": [2.25, 0, -5] }, + "2.0833": { "vector": [0, 0, -5] } }, "position": { - "0.0": { - "vector": [0, -0.4, 0] - }, - "1.0": { - "vector": [0, -0.3, 0] - }, - "1.25": { - "vector": [0, -0.3, 0] - }, - "2.0": { - "vector": [0, -0.4, 0] - } + "0.0": { "vector": [0, -0.4, 0] }, + "1.0": { "vector": [0, -0.3, 0] }, + "1.25": { "vector": [0, -0.3, 0] }, + "2.0": { "vector": [0, -0.4, 0] } } }, "head": { "rotation": { - "0.0": { - "vector": [0, 0, -5] - }, - "0.1667": { - "vector": [0, 0, -5] - }, - "1.1667": { - "vector": [-3, 0, -5] - }, - "1.4167": { - "vector": [-3, 0, -5] - }, - "2.1667": { - "vector": [0, 0, -5] - } + "0.0": { "vector": [0, 0, -5] }, + "0.1667": { "vector": [0, 0, -5] }, + "1.1667": { "vector": [-3, 0, -5] }, + "1.4167": { "vector": [-3, 0, -5] }, + "2.1667": { "vector": [0, 0, -5] } } }, "righteye": { - "position": { - "vector": [0, 0, 1] - } + "position": { "vector": [0, 0, 1] } }, "frown": { - "position": { - "vector": [0, 0, 1] - } + "position": { "vector": [0, 0, 1] } }, "rightarm": { "rotation": { - "0.0": { - "vector": [0, 0, -10] - }, - "0.2083": { - "vector": [0, 0, -10] - }, - "1.2083": { - "vector": [2, 0, -10] - }, - "1.4583": { - "vector": [3, 0, -10] - }, - "2.2083": { - "vector": [0, 0, -10] - } + "0.0": { "vector": [0, 0, -10] }, + "0.2083": { "vector": [0, 0, -10] }, + "1.2083": { "vector": [2, 0, -10] }, + "1.4583": { "vector": [3, 0, -10] }, + "2.2083": { "vector": [0, 0, -10] } } }, "rightleg": { - "rotation": { - "vector": [0, 0, 25] - }, - "position": { - "vector": [0, 0.7, 0] - } + "rotation": { "vector": [0, 0, 25] }, + "position": { "vector": [0, 0.7, 0] } }, "openmouth]2": { "position": {