port A LOT
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
@@ -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();
|
||||
}
|
||||
}
|
||||
+46
@@ -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);
|
||||
*//*?}*/
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user