port A LOT

This commit is contained in:
2026-01-10 23:36:28 -05:00
parent dfde4b2315
commit 9a954a0af1
446 changed files with 57881 additions and 71 deletions
@@ -0,0 +1,776 @@
package net.cmr.jurassicrevived.entity.ai;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.util.DefaultRandomPos;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.FlyingAnimal;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.pathfinder.Node;
import net.minecraft.world.level.pathfinder.Path;
import net.minecraft.world.phys.Vec3;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class DinoAIController {
private final DinoEntityBase dino;
private State currentState = State.IDLE;
private LivingEntity attackTarget;
private Animal mateTarget; // Target for mating
private BlockPos waterTarget;
private BlockPos homePos; // Center of territory
private Vec3 roamTarget;
private int stateTimer = 0;
private int pathRecalcTimer = 0;
private int failedPathfindingAttempts = 0;
private boolean isSelfBreeding = false; // For parthenogenesis
// Attack Cooldown Tracker
private int attackCooldown = 0;
public State getCurrentState() { return currentState; }
public LivingEntity getAttackTarget() { return attackTarget; }
public BlockPos getWaterTarget() { return waterTarget; }
public enum State {
IDLE,
ROAMING,
TERRITORIAL_ROAMING,
CHASING,
ATTACKING,
FLEEING,
SLEEPING,
MATING, // New state
DEFACATING, DROWNING, FLOCKING, HIBERNATING, HIDING, HUNTING, NESTING, RAMPAGING, SOCIALIZING
}
public DinoAIController(DinoEntityBase dino) {
this.dino = dino;
}
public void tick() {
if (homePos == null) homePos = dino.blockPosition();
updateSensors();
switch (currentState) {
case IDLE -> tickIdle();
case ROAMING -> tickRoaming();
case TERRITORIAL_ROAMING -> tickTerritorialRoaming();
case CHASING -> tickChasing();
case ATTACKING -> tickAttacking();
case FLEEING -> tickFleeing();
case SLEEPING -> tickSleeping();
case MATING -> tickMating();
}
stateTimer++;
if (attackCooldown > 0) attackCooldown--;
}
public void onHurtBy(LivingEntity attacker) {
// Retaliate if we are capable of attacking (have damage attribute > 0)
// Carnivores always attack back. Herbivores/others attack back if they have strength.
// We SKIP the generic canAttack check here because if something hurt us,
// we should try to fight back even if it's "too big" or technically invalid by roaming standards.
boolean canFightBack = dino.getAttributeValue(Attributes.ATTACK_DAMAGE) > 0 && !dino.isBaby();
if (canFightBack) {
this.attackTarget = attacker;
transitionTo(State.CHASING);
} else {
this.attackTarget = attacker;
transitionTo(State.FLEEING);
}
}
private void transitionTo(State newState) {
// Handle Condition updates
if (dino.dinoData != null) {
if (newState == State.SLEEPING) dino.dinoData.addCondition(IDinoData.Condition.SLEEPING);
else dino.dinoData.removeCondition(IDinoData.Condition.SLEEPING);
}
this.currentState = newState;
this.stateTimer = 0;
this.pathRecalcTimer = 0;
this.failedPathfindingAttempts = 0;
// Reset sprinting if we aren't in a high-speed state
if (newState != State.CHASING && newState != State.ATTACKING && newState != State.FLEEING) {
dino.setSprinting(false);
}
// Do NOT stop navigation here if switching Chasing <-> Attacking to maintain momentum
if (newState == State.IDLE || newState == State.ROAMING || newState == State.TERRITORIAL_ROAMING || newState == State.SLEEPING || newState == State.MATING) {
this.dino.getNavigation().stop();
}
}
// --- SENSORS ---
private void updateSensors() {
DinoEntityBase.DinoAIConfig config = dino.getAIConfig();
// 1. Check for Mating (High priority)
// If we are in love but not currently fighting or already mating, switch to mating.
if (dino.isInLove() && currentState != State.CHASING && currentState != State.ATTACKING && currentState != State.FLEEING && currentState != State.MATING) {
transitionTo(State.MATING);
}
// 2. Vitals Update
if (dino.dinoData != null) {
float hungerDecay = config.hungerDecay();
float thirstDecay = config.thirstDecay();
if (currentState == State.SLEEPING) {
hungerDecay *= 0.5f;
thirstDecay *= 0.5f;
if (dino.tickCount % 40 == 0 && dino.getHealth() < dino.getMaxHealth()) {
dino.heal(1.0f);
}
}
dino.dinoData.modifyHunger(-hungerDecay);
float currentThirst = dino.dinoData.getThirst();
dino.dinoData.setThirst(Math.max(0, currentThirst - thirstDecay));
float hunger = dino.dinoData.getHunger();
float thirst = dino.dinoData.getThirst();
if (hunger <= 0 || thirst <= 0) {
if (currentState == State.SLEEPING) {
transitionTo(State.IDLE);
}
if (stateTimer % 20 == 0) {
dino.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 40, 1));
if (hunger <= 0) {
dino.hurt(dino.damageSources().starve(), 1.0f);
dino.dinoData.addCondition(IDinoData.Condition.STARVING);
}
if (thirst <= 0) {
dino.hurt(dino.damageSources().dryOut(), 1.0f);
dino.dinoData.addCondition(IDinoData.Condition.DEHYDRATED);
}
}
} else {
dino.dinoData.removeCondition(IDinoData.Condition.STARVING);
dino.dinoData.removeCondition(IDinoData.Condition.DEHYDRATED);
}
if (hunger < 30) dino.dinoData.addCondition(IDinoData.Condition.HUNGRY);
else dino.dinoData.removeCondition(IDinoData.Condition.HUNGRY);
if (thirst < 30) dino.dinoData.addCondition(IDinoData.Condition.THIRSTY);
else dino.dinoData.removeCondition(IDinoData.Condition.THIRSTY);
if (dino.getHealth() < dino.getMaxHealth() * 0.25) dino.dinoData.addCondition(IDinoData.Condition.LOW_HEALTH);
else dino.dinoData.removeCondition(IDinoData.Condition.LOW_HEALTH);
}
// 3. Sleep check
if (currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING || currentState == State.SLEEPING) {
if (dino.dinoData != null) {
IDinoData.ActivityPattern activity = dino.dinoData.getActivityPattern();
boolean isDay = dino.level().isDay();
boolean shouldSleep = false;
if (activity == IDinoData.ActivityPattern.DIURNAL && !isDay) shouldSleep = true;
if (activity == IDinoData.ActivityPattern.NOCTURNAL && isDay) shouldSleep = true;
if (dino.getLastHurtByMob() != null && dino.tickCount - dino.getLastHurtByMobTimestamp() < 200) {
shouldSleep = false;
}
if (shouldSleep && currentState != State.SLEEPING) {
transitionTo(State.SLEEPING);
} else if (!shouldSleep && currentState == State.SLEEPING) {
transitionTo(State.IDLE);
}
}
}
// 4. Target Validation (Attack)
if (attackTarget != null) {
boolean shouldStop = false;
// Basic checks
if (!attackTarget.isAlive() || dino.distanceToSqr(attackTarget) > 64 * 64) {
shouldStop = true;
}
// Player specific checks (Creative/Spectator/Peaceful)
if (attackTarget instanceof Player player) {
if (player.isCreative() || player.isSpectator()) shouldStop = true;
}
// Note: We deliberately do NOT call dino.canAttack(attackTarget) here.
// canAttack() includes "preferences" (like size limits or whitelist) which should
// be ignored if we are actively retaliating or hunting a valid target we already selected.
if (shouldStop) {
attackTarget = null;
if (currentState == State.CHASING || currentState == State.ATTACKING) {
transitionTo(State.IDLE);
}
}
}
// 5. Hunt check
if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING) && dino.isCarnivore()) {
boolean hungry = dino.dinoData != null && dino.dinoData.getHunger() < 70;
boolean territorial = dino.dinoData != null && dino.dinoData.getAggression() == IDinoData.Aggression.TERRITORIAL;
if (hungry || (territorial && dino.dinoData.getHunger() < 90)) {
if (stateTimer % 10 == 0) {
findTarget();
}
}
}
// 6. Water check
if ((currentState == State.IDLE || currentState == State.ROAMING || currentState == State.TERRITORIAL_ROAMING)) {
if (dino.dinoData != null && dino.dinoData.getThirst() < 50 && waterTarget == null) {
if (stateTimer % 10 == 0) findWater();
}
}
}
private void findWater() {
// Increase vertical search range to find water even if we are high up
BlockPos pos = BlockPos.findClosestMatch(dino.blockPosition(), 32, 16, p -> dino.level().getFluidState(p).is(FluidTags.WATER)).orElse(null);
if (pos != null) {
this.waterTarget = pos;
dino.getNavigation().moveTo(pos.getX(), pos.getY(), pos.getZ(), dino.getAIConfig().walkSpeed());
if (currentState == State.IDLE) {
float territoriality = dino.dinoData != null ? dino.dinoData.getTerritoriality() : 0.0f;
if (dino.getRandom().nextFloat() < territoriality) {
transitionTo(State.TERRITORIAL_ROAMING);
} else {
transitionTo(State.ROAMING);
}
}
}
}
private void findTarget() {
double range = dino.getAttributeValue(Attributes.FOLLOW_RANGE);
if (range <= 0) range = 32.0;
List<LivingEntity> nearby = dino.level().getEntitiesOfClass(LivingEntity.class,
dino.getBoundingBox().inflate(range));
List<LivingEntity> candidates = new ArrayList<>();
for (LivingEntity e : nearby) {
if (e == dino) continue;
if (!dino.canAttack(e)) continue;
if (e instanceof Player player) {
// Apply motivation logic for Players
boolean isTerritorial = dino.dinoData != null && dino.dinoData.getAggression() == IDinoData.Aggression.TERRITORIAL;
boolean hungry = dino.dinoData != null && dino.dinoData.getHunger() < 70;
boolean validPlayerTarget = false;
if (isTerritorial) {
if (hungry || dino.distanceToSqr(player) < 225) {
validPlayerTarget = true;
}
} else {
if (hungry) {
validPlayerTarget = true;
}
}
if (validPlayerTarget) {
candidates.add(player);
}
} else {
// Animals are valid if canAttack passes (which checks size etc)
candidates.add(e);
}
}
// Sort candidates by distance to prefer closest valid target
candidates.sort(Comparator.comparingDouble(dino::distanceToSqr));
// Check pathfinding for the closest targets to ensure we can reach them
int checks = 0;
for (LivingEntity candidate : candidates) {
if (checks >= 5) break; // Limit pathfinding checks to avoid lag
checks++;
Path path = dino.getNavigation().createPath(candidate, 0);
if (path != null) {
// VERIFY PATH REACHES TARGET
// The pathfinder may return a partial path that ends at a wall.
// We check if the end point of the path is reasonably close to the target.
Node endNode = path.getEndNode();
if (endNode != null) {
double distToTargetSqr = candidate.distanceToSqr(endNode.x + 0.5, endNode.y + 0.5, endNode.z + 0.5);
// 25.0 = 5 blocks tolerance. If the path ends > 5 blocks away from target, it's likely obstructed.
if (distToTargetSqr < 25.0) {
this.attackTarget = candidate;
transitionTo(State.CHASING);
return;
}
}
}
}
}
// --- STATE LOGIC ---
private void tickIdle() {
dino.getNavigation().stop();
// Check for Natural Breeding (approx once every 2 in-game days = 48000 ticks)
if (!dino.level().isClientSide && stateTimer % 100 == 0) {
// 1. Trigger Ready State
if (dino.getAge() == 0 && !dino.isInLove() && dino.canBreed()) {
if (dino.dinoData != null && !dino.dinoData.hasCondition(IDinoData.Condition.READY_TO_MATE)) {
// Chance: 1 in 480 checks (~ once per 48000 ticks / 2 days)
if (dino.getRandom().nextInt(480) == 0) {
// Parthenogenesis check (1% chance)
if (dino.getRandom().nextInt(100) == 0) {
dino.setInLoveTime(600); // 30 Seconds of hearts
this.isSelfBreeding = true;
} else {
// Standard: Set condition, wait for partner
dino.dinoData.addCondition(IDinoData.Condition.READY_TO_MATE);
}
}
}
}
// 2. Scan for Partner if Ready
if (dino.dinoData != null && dino.dinoData.hasCondition(IDinoData.Condition.READY_TO_MATE)) {
// Add a chance to "lose interest" so they aren't ready forever (1 in 50 chance every 5 seconds = approx 4 minutes duration)
if (dino.getRandom().nextInt(50) == 0) {
dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE);
} else {
List<DinoEntityBase> nearby = dino.level().getEntitiesOfClass(DinoEntityBase.class,
dino.getBoundingBox().inflate(8.0),
e -> e.getType() == dino.getType() && e != dino && !e.isBaby());
for (DinoEntityBase potentialPartner : nearby) {
if (dino.canMate(potentialPartner)) {
// Initiate mating for both
dino.setInLoveTime(600); // 30 seconds
potentialPartner.setInLoveTime(600); // 30 seconds
dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE);
if (potentialPartner.dinoData != null) {
potentialPartner.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE);
}
break;
}
}
}
}
}
float territoriality = 0.0f;
if (dino.dinoData != null) {
territoriality = dino.dinoData.getTerritoriality();
}
int idleTime = 60; // Default 3 seconds
// If we are a flying animal and on the ground, stay down longer (e.g. 15-30 seconds) to walk around
if (dino instanceof FlyingAnimal && dino.onGround()) {
idleTime = 300 + dino.getRandom().nextInt(300);
}
if (stateTimer > idleTime) {
if (dino.getRandom().nextFloat() < 0.05f) {
if (dino.getRandom().nextFloat() < territoriality) {
transitionTo(State.TERRITORIAL_ROAMING);
} else {
transitionTo(State.ROAMING);
}
}
}
}
private void tickMating() {
// If love ran out, stop
if (!dino.isInLove()) {
this.mateTarget = null;
this.isSelfBreeding = false;
// Also ensure we don't have the condition anymore if we just failed/finished
if (dino.dinoData != null) dino.dinoData.removeCondition(IDinoData.Condition.READY_TO_MATE);
transitionTo(State.IDLE);
return;
}
// Parthenogenesis Logic
if (this.isSelfBreeding) {
dino.spawnChildFromBreeding((net.minecraft.server.level.ServerLevel)dino.level(), dino);
dino.setInLoveTime(0); // Reset
this.isSelfBreeding = false;
transitionTo(State.IDLE);
return;
}
// Find Partner
if (this.mateTarget == null || !this.mateTarget.isAlive() || !this.mateTarget.isInLove()) {
List<Animal> nearby = dino.level().getEntitiesOfClass(Animal.class, dino.getBoundingBox().inflate(16.0),
e -> e.getType() == dino.getType() && e != dino && e.isInLove());
this.mateTarget = nearby.stream()
.min(Comparator.comparingDouble(dino::distanceToSqr))
.orElse(null);
}
if (this.mateTarget != null) {
dino.getNavigation().moveTo(this.mateTarget, dino.getAIConfig().walkSpeed());
if (dino.distanceToSqr(this.mateTarget) < 4.0) { // < 2 blocks
dino.spawnChildFromBreeding((net.minecraft.server.level.ServerLevel)dino.level(), this.mateTarget);
// Breeding consumes love in spawnChildFromBreeding
this.mateTarget = null;
transitionTo(State.IDLE);
}
} else {
// No partner found yet, wander slowly?
if (dino.getNavigation().isDone()) {
Vec3 pos = DefaultRandomPos.getPos(dino, 10, 3);
if (pos != null) dino.getNavigation().moveTo(pos.x, pos.y, pos.z, dino.getAIConfig().walkSpeed());
}
}
}
private void tickSleeping() {
dino.getNavigation().stop();
}
private boolean handleWaterPathing() {
if (waterTarget != null) {
double dist = dino.distanceToSqr(waterTarget.getCenter());
double reach = dino.getAIConfig().attackReach() * dino.getBbWidth();
if (dist < (reach * reach) + 9.0) {
dino.dinoData.setThirst(dino.getAIConfig().maxThirst());
waterTarget = null;
dino.getNavigation().stop();
transitionTo(State.IDLE);
return true;
}
if (dino.getNavigation().isDone()) {
if (dist < 1024) {
dino.getNavigation().moveTo(waterTarget.getX(), waterTarget.getY(), waterTarget.getZ(), dino.getAIConfig().walkSpeed());
} else {
waterTarget = null;
}
}
return true;
}
return false;
}
private void tickRoaming() {
if (handleWaterPathing()) return;
if (stateTimer == 0) {
this.roamTarget = null;
findAndSetRoamTarget();
if (this.roamTarget == null) {
transitionTo(State.IDLE);
return;
}
}
if (stateTimer > 400) {
transitionTo(State.IDLE);
return;
}
if (dino.getNavigation().isDone()) {
if (roamTarget != null && dino.distanceToSqr(roamTarget) < 9.0) {
// If we are a flyer and in the air, don't stop! Keep flying!
if (dino instanceof FlyingAnimal && !dino.onGround()) {
findAndSetRoamTarget(); // Immediately pick next waypoint
return;
}
transitionTo(State.IDLE);
return;
}
boolean resumed = false;
if (roamTarget != null) {
resumed = dino.getNavigation().moveTo(roamTarget.x, roamTarget.y, roamTarget.z, dino.getAIConfig().walkSpeed());
}
if (!resumed) {
findAndSetRoamTarget();
if (this.roamTarget == null) {
transitionTo(State.IDLE);
}
}
}
}
private void findAndSetRoamTarget() {
this.roamTarget = null;
// Flying Logic
if (dino instanceof FlyingAnimal) {
Vec3 airPos = getAirRoamPos();
if (airPos != null) {
// Use walkSpeed as cruising speed
if (dino.getNavigation().moveTo(airPos.x, airPos.y, airPos.z, dino.getAIConfig().walkSpeed())) {
this.roamTarget = airPos;
return;
}
}
}
// Ground Logic
for (int i = 0; i < 3; i++) {
Vec3 pos = DefaultRandomPos.getPos(dino, 20, 7);
if (pos != null && dino.distanceToSqr(pos) > 49.0) {
if (dino.getNavigation().moveTo(pos.x, pos.y, pos.z, dino.getAIConfig().walkSpeed())) {
this.roamTarget = pos;
return;
}
}
}
}
private Vec3 getAirRoamPos() {
net.minecraft.util.RandomSource random = dino.getRandom();
Vec3 pos = dino.position();
// Increased radius for better flight
double x = pos.x + (random.nextFloat() * 2 - 1) * 64;
double z = pos.z + (random.nextFloat() * 2 - 1) * 64;
// Height check
int groundY = dino.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z);
double y;
if (dino.onGround()) {
// Takeoff: 10-20 blocks up
y = pos.y + 10 + random.nextInt(10);
} else {
// 20% chance to land if already flying
if (random.nextFloat() < 0.20f) {
y = groundY;
} else {
// Wander vertically
if (random.nextFloat() < 0.30f) {
y = groundY + 10 + random.nextInt(30);
} else {
y = pos.y + (random.nextFloat() * 2 - 1) * 20;
}
if (y < groundY + 5) y = groundY + 5;
if (y > groundY + 50) y = groundY + 50;
}
}
// --- Water Avoidance ---
// If the target is over water, try to pull it back towards us/land
BlockPos targetPos = new BlockPos((int)x, (int)groundY, (int)z);
if (dino.level().getFluidState(targetPos).is(FluidTags.WATER)) {
// Check if it's "deep" water (just checking surface isn't enough, but usually good proxy)
// Move target 50% closer to current position (which presumably was safe)
x = (x + pos.x) / 2.0;
z = (z + pos.z) / 2.0;
// Re-calculate ground Y for new X/Z
groundY = dino.level().getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, (int)x, (int)z);
if (y < groundY) y = groundY;
}
// --- Landing Logic ---
// If we are aiming for a spot very close to the ground (<= 4 blocks), just land.
// This prevents awkward low-altitude hovering.
if (y <= groundY + 4) {
y = groundY;
}
return new Vec3(x, y, z);
}
private void tickTerritorialRoaming() {
if (handleWaterPathing()) return;
if (stateTimer == 0) {
findAndSetTerritorialTarget();
if (this.roamTarget == null) {
transitionTo(State.IDLE);
return;
}
}
if (stateTimer > 400) {
transitionTo(State.IDLE);
return;
}
if (dino.getNavigation().isDone()) {
if (roamTarget != null && dino.distanceToSqr(roamTarget) < 9.0) {
transitionTo(State.IDLE);
return;
}
boolean resumed = false;
if (roamTarget != null) {
resumed = dino.getNavigation().moveTo(roamTarget.x, roamTarget.y, roamTarget.z, dino.getAIConfig().walkSpeed());
}
if (!resumed) {
findAndSetTerritorialTarget();
if (this.roamTarget == null) {
transitionTo(State.IDLE);
}
}
}
}
private void findAndSetTerritorialTarget() {
this.roamTarget = null;
Vec3 target = null;
for (int i = 0; i < 5; i++) {
Vec3 candidate;
if (homePos != null && dino.distanceToSqr(homePos.getCenter()) > 40 * 40) {
Vec3 toHome = Vec3.atCenterOf(homePos).subtract(dino.position()).normalize().scale(10);
Vec3 biasTarget = dino.position().add(toHome);
candidate = DefaultRandomPos.getPosTowards(dino, 15, 7, biasTarget, 1.57);
} else {
candidate = DefaultRandomPos.getPos(dino, 15, 7);
}
if (candidate != null && dino.distanceToSqr(candidate) > 25.0) {
if (dino.getNavigation().moveTo(candidate.x, candidate.y, candidate.z, dino.getAIConfig().walkSpeed())) {
this.roamTarget = candidate;
return;
}
}
}
Vec3 fallback = DefaultRandomPos.getPos(dino, 10, 5);
if (fallback != null) {
if (dino.getNavigation().moveTo(fallback.x, fallback.y, fallback.z, dino.getAIConfig().walkSpeed())) {
this.roamTarget = fallback;
}
}
}
private void tickChasing() {
if (attackTarget == null) {
transitionTo(State.IDLE);
return;
}
dino.setSprinting(true);
waterTarget = null;
dino.getLookControl().setLookAt(attackTarget, 30.0F, 30.0F);
double distSqr = dino.distanceToSqr(attackTarget);
double reachMult = dino.getAIConfig().attackReach();
double reach = (double)(dino.getBbWidth() * reachMult * dino.getBbWidth() * reachMult) + attackTarget.getBbWidth();
if (distSqr <= reach * 1.1) {
transitionTo(State.ATTACKING);
return;
}
if (pathRecalcTimer-- <= 0 || dino.getNavigation().isDone()) {
if (!dino.getNavigation().moveTo(attackTarget, dino.getAIConfig().runSpeed())) {
pathRecalcTimer = 10; // Wait before retrying to prevent rapid failure loops
failedPathfindingAttempts++;
// Tolerance allows for temporary pathfinding failures (e.g., target inside hitbox)
if (failedPathfindingAttempts > 5) {
attackTarget = null;
transitionTo(State.IDLE);
}
} else {
pathRecalcTimer = 10;
failedPathfindingAttempts = 0;
}
}
}
private void tickAttacking() {
if (attackTarget == null) {
transitionTo(State.IDLE);
return;
}
dino.setSprinting(true);
dino.getLookControl().setLookAt(attackTarget, 30.0F, 30.0F);
double distSqr = dino.distanceToSqr(attackTarget);
double reachMult = dino.getAIConfig().attackReach();
double reach = (double)(dino.getBbWidth() * reachMult * dino.getBbWidth() * reachMult) + attackTarget.getBbWidth();
if (distSqr > reach * 2.5) {
transitionTo(State.CHASING);
return;
}
double stopDist = (dino.getBbWidth()/2.0 + attackTarget.getBbWidth()/2.0) + 0.5;
double stopDistSqr = stopDist * stopDist;
if (distSqr > stopDistSqr) {
dino.getNavigation().moveTo(attackTarget, dino.getAIConfig().runSpeed());
} else {
dino.getNavigation().stop();
}
if (attackCooldown <= 0) {
dino.swing(InteractionHand.MAIN_HAND);
boolean success = false;
if (dino.isWithinMeleeAttackRange(attackTarget)) {
success = dino.doHurtTarget(attackTarget);
}
if (!success && attackTarget.isAlive()) {
if (distSqr <= reach) {
success = attackTarget.hurt(dino.damageSources().mobAttack(dino), (float)dino.getAttributeValue(Attributes.ATTACK_DAMAGE));
}
}
if (success) {
attackCooldown = 20;
} else {
attackCooldown = 5;
}
}
}
private void tickFleeing() {
if (attackTarget == null) {
transitionTo(State.IDLE);
return;
}
if (dino.getNavigation().isDone() || stateTimer % 10 == 0) {
Vec3 awayDir = DefaultRandomPos.getPosAway(dino, 16, 7, attackTarget.position());
if (awayDir != null) {
dino.getNavigation().moveTo(awayDir.x, awayDir.y, awayDir.z, dino.getAIConfig().runSpeed() * 1.2);
}
}
if (dino.distanceToSqr(attackTarget) > 48 * 48) {
transitionTo(State.IDLE);
}
}
}
@@ -0,0 +1,232 @@
package net.cmr.jurassicrevived.entity.ai;
import net.cmr.jurassicrevived.entity.ai.navigation.CustomDinoNavigation;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.ItemTags;
import net.minecraft.world.Difficulty;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
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.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import java.util.HashMap;
import java.util.Map;
public abstract class DinoEntityBase extends Animal {
protected IDinoData dinoData;
protected final DinoAIController aiController;
public DinoEntityBase(EntityType<? extends Animal> type, Level level) {
super(type, level);
this.aiController = new DinoAIController(this);
}
// Disable Vanilla Goals
@Override
protected void registerGoals() {}
// Ensure our AI ticks regardless of what vanilla method is called
@Override
public void tick() {
super.tick(); // Vanilla Mob tick
if (!this.level().isClientSide) {
// FORCE TICK AI
this.aiController.tick();
// Sync
if (dinoData != null) {
dinoData.tickSync(this.entityData);
}
}
}
// --- Navigation ---
@Override
protected PathNavigation createNavigation(Level level) {
return new CustomDinoNavigation(this, level);
}
// --- Hooks ---
public abstract boolean isCarnivore();
public abstract boolean isMarine();
public abstract boolean isAmphibious();
public abstract Block getEggBlock(); // New hook for breeding
public abstract DinoAIConfig getAIConfig();
public record DinoAIConfig(double walkSpeed, double runSpeed, double attackReach,
float maxHunger, float maxThirst,
float hungerDecay, float thirstDecay,
float defaultHungerReplenishment) {}
private static final Map<EntityType<?>, Float> ENTITY_HUNGER_VALUES = new HashMap<>();
public static void registerHungerValue(EntityType<?> type, float value) {
ENTITY_HUNGER_VALUES.put(type, value);
}
public float getHungerReplenishment(LivingEntity target) {
return ENTITY_HUNGER_VALUES.getOrDefault(target.getType(), getAIConfig().defaultHungerReplenishment());
}
public IDinoData getDinoData() { return this.dinoData; }
@Override
public InteractionResult mobInteract(Player player, InteractionHand hand) {
if (!this.level().isClientSide && hand == InteractionHand.MAIN_HAND && player.isShiftKeyDown() && player.getMainHandItem().isEmpty()) {
if (this.dinoData != null) {
// Format details nicely
StringBuilder sb = new StringBuilder();
sb.append("§6--- Dino Status ---§r\n");
sb.append("§eState:§r ").append(this.aiController.getCurrentState()).append("\n");
// Line 1: Biological Classification
sb.append("§eType:§r ").append(dinoData.getType())
.append(" §eGroup:§r ").append(dinoData.getGroup())
.append(" §eDiet:§r ").append(dinoData.getDiet()).append("\n");
// Line 2: Behavioral/Lifecycle
sb.append("§eBehavior:§r ").append(dinoData.getAggression())
.append(" §eBirth:§r ").append(dinoData.getBirthType())
.append(" §eActivity:§r ").append(dinoData.getActivityPattern()).append("\n");
// Line 3: Status
sb.append("§eMood:§r ").append(dinoData.getMood())
.append(" §eConditions:§r ").append(dinoData.getConditions()).append("\n");
// Line 4: Vitals
sb.append("§aHunger:§r ").append(String.format("%.1f", dinoData.getHunger()))
.append(" §bThirst:§r ").append(String.format("%.1f", dinoData.getThirst())).append("\n");
if (this.aiController.getAttackTarget() != null) {
sb.append("§cTarget:§r ").append(this.aiController.getAttackTarget().getName().getString()).append("\n");
}
if (this.aiController.getWaterTarget() != null) {
sb.append("§9Water Target:§r ").append(this.aiController.getWaterTarget().toShortString()).append("\n");
}
player.sendSystemMessage(Component.literal(sb.toString()));
return InteractionResult.SUCCESS;
}
}
return super.mobInteract(player, hand);
}
@Override
public boolean isFood(ItemStack stack) {
if (dinoData == null) return false;
IDinoData.DietaryClassification diet = dinoData.getDiet();
// 1. Carnivores: All meat
if (diet == IDinoData.DietaryClassification.CARNIVORE || diet == IDinoData.DietaryClassification.PISCIVORE) {
// WOLF_FOOD includes beef, pork, chicken, rabbit, mutton, rotten flesh
//if (stack.is(ItemTags.WOLF_FOOD)) return true;
// Fallback for fish items
if (stack.is(ItemTags.FISHES)) return true;
}
// 3. Herbivores: Leaves, Fruits, Vegetables
if (diet == IDinoData.DietaryClassification.HERBIVORE) {
if (stack.is(ItemTags.LEAVES)) return true;
if (stack.is(ItemTags.FLOWERS)) return true;
if (stack.is(Items.APPLE) || stack.is(Items.MELON_SLICE) || stack.is(Items.SWEET_BERRIES) || stack.is(Items.GLOW_BERRIES)) return true;
if (stack.is(Items.SEAGRASS) || stack.is(Items.KELP)) return true;
}
// 4. Omnivores: Both
if (diet == IDinoData.DietaryClassification.OMNIVORE) {
//if (stack.is(ItemTags.WOLF_FOOD)) return true;
if (stack.is(ItemTags.LEAVES)) return true;
}
return false;
}
@Override
public void spawnChildFromBreeding(ServerLevel level, Animal partner) {
// Custom breeding logic
if (this.dinoData != null && this.dinoData.getBirthType() == IDinoData.BirthType.EGG_LAYING) {
// Place Egg Block
Block eggBlock = getEggBlock();
if (eggBlock != null) {
// Consume breeding status
this.setAge(6000);
partner.setAge(6000);
this.resetLove();
partner.resetLove();
// Place egg at parent location
level.setBlock(this.blockPosition(), eggBlock.defaultBlockState(), 3);
level.levelEvent(2001, this.blockPosition(), Block.getId(eggBlock.defaultBlockState())); // Particles?
}
} else {
// Live Birth (Default Vanilla)
super.spawnChildFromBreeding(level, partner);
}
}
// --- Attack Logic ---
public boolean canAttack(LivingEntity target) {
if (target == null || target == this) return false;
if (!target.isAlive()) return false;
if (this.getClass().equals(target.getClass())) return false;
// Prevent targeting if the entity is invisible (unless we have special senses, but basic AI implies sight)
if (target.hasEffect(MobEffects.INVISIBILITY)) return false;
if (target instanceof Player player) {
if (player.isCreative() || player.isSpectator()) return false;
// Peaceful mode check: Do not target players if difficulty is Peaceful
if (this.level().getDifficulty() == Difficulty.PEACEFUL) return false;
if (dinoData != null && dinoData.isWhitelisted(player.getUUID())) return false;
return true; // Always attack valid players
}
double myVolume = this.getBbWidth() * this.getBbWidth() * this.getBbHeight();
double targetVolume = target.getBbWidth() * target.getBbWidth() * target.getBbHeight();
// Prevent attacking entities significantly larger than self
// Check Volume (2.5x)
if (myVolume > 0 && targetVolume > myVolume * 2.5) return false;
// Check Height (1.2x) - Prevents attacking tall things like Brachiosaurus even if volume calc is close
if (this.getBbHeight() > 0 && target.getBbHeight() > this.getBbHeight() * 1.2) return false;
return true;
}
@Override
public boolean hurt(DamageSource source, float amount) {
boolean result = super.hurt(source, amount);
if (result && !this.level().isClientSide && source.getEntity() instanceof LivingEntity attacker) {
this.aiController.onHurtBy(attacker);
}
return result;
}
@Override
public void addAdditionalSaveData(CompoundTag tag) {
super.addAdditionalSaveData(tag);
if(dinoData != null) dinoData.saveNBT(tag);
}
@Override
public void readAdditionalSaveData(CompoundTag tag) {
super.readAdditionalSaveData(tag);
if(dinoData != null) dinoData.loadNBT(tag);
}
}
@@ -0,0 +1,74 @@
package net.cmr.jurassicrevived.entity.ai;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.SynchedEntityData;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public interface IDinoData {
// Core Vitals
float getHunger();
void setHunger(float value);
void modifyHunger(float change);
float getThirst();
void setThirst(float value);
// Mood & Personality
enum Mood { ANGRY, NEUTRAL, CONTENT, HAPPY, SCARED, SLEEPY }
Mood getMood();
void setMood(Mood mood);
// Aggression / Territoriality
// Replaces old float getTerritoriality? Or keeps it?
// "I want to add... Aggression, and a Condition." and "And this list for Aggression: Free-Roaming, Territorial, and Neutral"
// We will keep getTerritoriality() as a float for logic if needed, but add the Enum.
enum Aggression { FREE_ROAMING, TERRITORIAL, NEUTRAL, SCAVENGER }
Aggression getAggression();
void setAggression(Aggression aggression);
float getTerritoriality();
void setTerritoriality(float value);
// New Stats
enum DietaryClassification { CRUSTACIVORE, DETRITIVORE, HERBIVORE, INSECTIVORE, OMNIVORE, PISCIVORE, PLANKTIVORE, CARNIVORE }
DietaryClassification getDiet();
void setDiet(DietaryClassification diet);
enum Type { TERRESTRIAL, AVIAN, AMPHIBIOUS, MARINE }
Type getType();
void setType(Type type);
enum Group { THEROPOD, THYREOPHORAN, CERAPOD, SAUROPOD, ORNITHOPOD, AMPHIBIAN, ARCHOSAUROMORPH, PLEURODIRE, PTEROSAUR, REPTILIOMORPH, SQUAMATE }
Group getGroup();
void setGroup(Group group);
enum BirthType { EGG_LAYING, LIVE_BIRTH }
BirthType getBirthType();
void setBirthType(BirthType birthType);
enum ActivityPattern { CATHEMERAL, CREPUSCULAR, DIURNAL, NOCTURNAL }
ActivityPattern getActivityPattern();
void setActivityPattern(ActivityPattern pattern);
// Conditions
enum Condition { COMATOSE, INFECTED, LOW_HEALTH, HUNGRY, OVERHEATING, POISONED, SEDATED, SUFFOCATING, TAMED, WITHER, READY_TO_MATE, THIRSTY, STARVING, DEHYDRATED, FREEZING, SLEEPING }
Set<Condition> getConditions();
void addCondition(Condition condition);
void removeCondition(Condition condition);
boolean hasCondition(Condition condition);
// Taming / Whitelist
void addWhitelistedPlayer(UUID playerUUID);
boolean isWhitelisted(UUID playerUUID);
List<UUID> getWhitelistedPlayers();
// Serialization & Sync
void saveNBT(CompoundTag tag);
void loadNBT(CompoundTag tag);
// Called on entity tick to sync specific data (Hunger/Mood) to client via SynchedEntityData if needed
void tickSync(SynchedEntityData entityData);
}
@@ -0,0 +1,61 @@
package net.cmr.jurassicrevived.entity.ai;
import net.minecraft.world.Difficulty;
import net.minecraft.world.entity.AgeableMob;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.MeleeAttackGoal;
import net.minecraft.world.entity.player.Player;
public class SprintingMeleeAttackGoal extends MeleeAttackGoal {
private final LivingEntity entity;
public SprintingMeleeAttackGoal(PathfinderMob pMob, double pSpeedModifier, boolean pCanUnseenMemory) {
super(pMob, pSpeedModifier, pCanUnseenMemory);
this.entity = pMob;
}
// Override to prevent attacking players in peaceful difficulty and prevent babies from attacking
@Override
public boolean canUse() {
if (!super.canUse()) {
return false;
}
// Prevent baby mobs from attacking
if (this.mob instanceof AgeableMob ageableMob && ageableMob.isBaby()) {
return false;
}
// Check if target is a player and difficulty is peaceful
LivingEntity target = this.mob.getTarget();
if (target instanceof Player && this.mob.level().getDifficulty() == Difficulty.PEACEFUL) {
return false;
}
return true;
}
// This method is called to start the goal
@Override
public void start() {
super.start();
this.entity.setSprinting(true); // Force the entity to sprint
}
// This method is called to tick the goal
@Override
public void tick() {
super.tick();
// The movement speed is handled by the default goal behavior
// The sprint state is maintained from the start() method
}
// This method is called to end the goal
@Override
public void stop() {
super.stop();
this.entity.setSprinting(false); // Stop sprinting when the goal is finished
}
}
@@ -0,0 +1,26 @@
package net.cmr.jurassicrevived.entity.ai;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.PathfinderMob;
import net.minecraft.world.entity.ai.goal.PanicGoal;
public class SprintingPanicGoal extends PanicGoal {
private final LivingEntity entity;
public SprintingPanicGoal(PathfinderMob mob, double speedModifier) {
super(mob, speedModifier);
this.entity = mob;
}
@Override
public void start() {
super.start();
this.entity.setSprinting(true);
}
@Override
public void stop() {
super.stop();
this.entity.setSprinting(false);
}
}
@@ -0,0 +1,27 @@
package net.cmr.jurassicrevived.entity.ai.navigation;
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;
import net.minecraft.world.phys.Vec3;
public class CustomDinoNavigation extends GroundPathNavigation {
public CustomDinoNavigation(Mob mob, Level level) {
super(mob, level);
}
@Override
protected PathFinder createPathFinder(int maxVisitedNodes) {
this.nodeEvaluator = new LargeEntityNodeEvaluator();
this.nodeEvaluator.setCanPassDoors(true);
return new PathFinder(this.nodeEvaluator, maxVisitedNodes);
}
// Fix for large entities getting stuck: Ensure we don't try to path to the exact center of a block if our hitbox is huge
@Override
protected Vec3 getTempMobPos() {
return this.mob.position();
}
}
@@ -0,0 +1,46 @@
package net.cmr.jurassicrevived.entity.ai.navigation;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.level.block.state.BlockState;
/*? if <=1.20.1 {*/
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.pathfinder.BlockPathTypes;
/*?} else {*/
/*import net.minecraft.world.level.pathfinder.PathType;
import net.minecraft.world.level.pathfinder.PathfindingContext;
*//*?}*/
import net.minecraft.world.level.pathfinder.WalkNodeEvaluator;
public class LargeEntityNodeEvaluator extends WalkNodeEvaluator {
/*? if <=1.20.1 {*/
public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) {
Mob entity = this.mob;
BlockPos pos = new BlockPos(x, y, z);
BlockState state = level.getBlockState(pos);
if (entity != null && entity.getBbWidth() > 1.5f && state.is(BlockTags.LEAVES)) {
return BlockPathTypes.OPEN;
}
/*?} else {*/
/*@Override
public PathType getPathType(PathfindingContext context, int x, int y, int z) {
Mob entity = this.mob;
BlockPos pos = new BlockPos(x, y, z);
BlockState state = context.level().getBlockState(pos);
if (entity != null && entity.getBbWidth() > 1.5f && state.is(BlockTags.LEAVES)) {
return PathType.OPEN;
}
*//*?}*/
if (entity != null && entity.getBbWidth() >= 2.0f) {
}
/*? if <=1.20.1 {*/
return super.getBlockPathType(level, x, y, z);
/*?} else {*/
/*return super.getPathType(context, x, y, z);
*//*?}*/
}
}