Improve AI: bed memory (30-block search), relationship-based follow goal, crouch fixes

- EllieSleepGoal: check stored bedPos first, search 30 blocks on miss
- releaseBed() no longer clears memory; forgetBed() for destruction
- New FollowPlayerGoal: 50-75% distant follow, 75-99% closer, 100% approach
- fix: crouch animation thenLoop, idle doesn't override crouching
- fix: sleep Y rotation = facing.toYRot() (head toward headboard)
- fix: checkLowCeiling scan range reduced (2-4 blocks, 3 for path)
This commit is contained in:
2026-06-09 21:27:11 +03:00
parent f5d318f02e
commit 31a21f2f45
4 changed files with 119 additions and 4 deletions
@@ -78,7 +78,10 @@ public class Main {
EllieEntity.class, EllieEntity.class,
new net.minecraft.world.phys.AABB(pos).inflate(2), new net.minecraft.world.phys.AABB(pos).inflate(2),
e -> e.isSleeping() && pos.equals(e.getBedPos()) e -> e.isSleeping() && pos.equals(e.getBedPos())
).forEach(EllieEntity::wakeUp); ).forEach(e -> {
e.wakeUp();
e.forgetBed();
});
} }
} }
} }
@@ -29,10 +29,19 @@ public class EllieSleepGoal extends Goal {
if (!ellie.level().isNight() && !ellie.isTired()) return false; if (!ellie.level().isNight() && !ellie.isTired()) return false;
if (ellie.isSleeping()) return false; if (ellie.isSleeping()) return false;
if (ellie.hurtTime > 0) return false; if (ellie.hurtTime > 0) return false;
bedPos = findNearestBed();
bedPos = findBed();
return bedPos != null; return bedPos != null;
} }
private BlockPos findBed() {
BlockPos stored = ellie.getBedPos();
if (stored != null && isFreeBed(stored) && ellie.distanceToSqr(Vec3.atCenterOf(stored)) < 50 * 50) {
return stored;
}
return findNearestBed();
}
@Override @Override
public void start() { public void start() {
claimed = false; claimed = false;
@@ -0,0 +1,98 @@
package me.sashegdev.fabled_hearts.ai;
import me.sashegdev.fabled_hearts.entity.ellie.EllieEntity;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec3;
import java.util.EnumSet;
public class FollowPlayerGoal extends Goal {
private final EllieEntity ellie;
private Player target;
private int cooldown;
private static final double FOLLOW_RANGE = 16.0;
private static final double FOLLOW_RANGE_SQR = FOLLOW_RANGE * FOLLOW_RANGE;
public FollowPlayerGoal(EllieEntity ellie) {
this.ellie = ellie;
this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK));
}
@Override
public boolean canUse() {
if (ellie.isSleeping()) return false;
if (ellie.hurtTime > 0) return false;
float rel = ellie.getRelationshipPoints();
if (rel < 50) return false;
Player nearest = ellie.level().getNearestPlayer(ellie, FOLLOW_RANGE);
if (nearest == null) return false;
target = nearest;
return true;
}
@Override
public void start() {
cooldown = 0;
}
@Override
public void tick() {
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;
} else {
minDist = 6.0; maxDist = 10.0;
}
if (distSqr > maxDist * maxDist) {
if (--cooldown <= 0) {
cooldown = 10;
ellie.getNavigation().moveTo(target, 0.6);
}
} else if (distSqr < minDist * minDist) {
ellie.getNavigation().stop();
if (--cooldown <= 0) {
cooldown = 10;
Vec3 away = new Vec3(
ellie.getX() - target.getX(),
0,
ellie.getZ() - target.getZ()
).normalize();
ellie.getNavigation().moveTo(
target.getX() + away.x * maxDist,
target.getY(),
target.getZ() + away.z * maxDist,
0.4
);
}
} else {
ellie.getNavigation().stop();
cooldown = 0;
ellie.getLookControl().setLookAt(target, 30, 30);
}
}
@Override
public boolean canContinueToUse() {
if (target == null || !target.isAlive()) return false;
if (ellie.isSleeping()) return false;
if (ellie.hurtTime > 0) return false;
double distSqr = ellie.distanceToSqr(target);
return distSqr <= FOLLOW_RANGE_SQR;
}
@Override
public void stop() {
target = null;
ellie.getNavigation().stop();
}
}
@@ -92,8 +92,9 @@ public class EllieEntity extends Animal implements GeoEntity {
@Override @Override
protected void registerGoals() { protected void registerGoals() {
BasicAI.addBasicGoals(this, this.goalSelector, 2); BasicAI.addBasicGoals(this, this.goalSelector, 2);
this.goalSelector.addGoal(1, new EllieSleepGoal(this, 20)); this.goalSelector.addGoal(1, new EllieSleepGoal(this, 30));
this.goalSelector.addGoal(1, new IdleAnimationGoal(this)); this.goalSelector.addGoal(1, new IdleAnimationGoal(this));
this.goalSelector.addGoal(3, new me.sashegdev.fabled_hearts.ai.FollowPlayerGoal(this));
} }
@Override @Override
@@ -265,10 +266,13 @@ public class EllieEntity extends Animal implements GeoEntity {
} }
} }
} }
bedPos = null;
} }
} }
public void forgetBed() {
bedPos = null;
}
public boolean isBedAt(BlockPos pos) { public boolean isBedAt(BlockPos pos) {
return level().getBlockState(pos).isBed(level(), pos, null); return level().getBlockState(pos).isBed(level(), pos, null);
} }
@@ -421,6 +425,7 @@ 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 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; }
public void addRelationshipPoints(float amount) { public void addRelationshipPoints(float amount) {