Commit ac6d193a by Jonathan Thomas

Massive refactor to Goals and Behaviors:

- New PROTECT behavior
- Native ATTACK abilities
- Improved FOLLOW (with teleportation)
- Improved FLEE (more random, more reliable)
parent 05c312a8
Pipeline #12561 passed with stages
in 2 minutes 17 seconds
......@@ -12,8 +12,8 @@ import com.owlmaddie.message.MessageParser;
import com.owlmaddie.message.ParsedMessage;
import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.utils.LivingEntityInterface;
import com.owlmaddie.utils.ServerEntityFinder;
import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
......@@ -276,7 +276,7 @@ public class ChatDataManager {
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FLEE")) {
float fleeDistance = 400F; // 20 blocks squared
float fleeDistance = 40F;
FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
......@@ -290,6 +290,13 @@ public class ChatDataManager {
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.addGoal(entity, attackGoal, GoalPriority.ATTACK_PLAYER);
} else if (behavior.getName().equals("PROTECT")) {
ProtectPlayerGoal protectGoal = new ProtectPlayerGoal(player, entity, 1.0);
EntityBehaviorManager.addGoal(entity, protectGoal, GoalPriority.PROTECT_PLAYER);
} else if (behavior.getName().equals("UNPROTECT")) {
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
} else if (behavior.getName().equals("FRIENDSHIP")) {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
if (new_friendship > 0) {
......
package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.RangedAttackMob;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.mob.Angerable;
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.Vec3d;
......@@ -15,73 +14,91 @@ import net.minecraft.util.math.Vec3d;
import java.util.EnumSet;
/**
* The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards the current player.
* For passive entities like chickens, damage is simulated with particles. But all MobEntity instances can damage
* the player.
* The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards a target Entity.
* For passive entities like chickens (or hostile entities in creative mode), damage is simulated with particles.
*/
public class AttackPlayerGoal extends Goal {
private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double speed;
enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING }
private EntityState currentState = EntityState.IDLE;
private int cooldownTimer = 0;
private final int CHARGE_TIME = 15; // Time before leaping / attacking
private final double MOVE_DISTANCE = 200D; // 20 blocks away
private final double CHARGE_DISTANCE = 25D; // 5 blocks away
private final double ATTACK_DISTANCE = 4D; // 2 blocks away
public AttackPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) {
this.targetPlayer = player;
this.entity = entity;
protected final MobEntity attackerEntity;
protected LivingEntity targetEntity;
protected final double speed;
protected enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING }
protected EntityState currentState = EntityState.IDLE;
protected int cooldownTimer = 0;
protected final int CHARGE_TIME = 15; // Time before leaping / attacking
protected final double MOVE_DISTANCE = 200D; // 20 blocks away
protected final double CHARGE_DISTANCE = 25D; // 5 blocks away
protected final double ATTACK_DISTANCE = 4D; // 2 blocks away
public AttackPlayerGoal(LivingEntity targetEntity, MobEntity attackerEntity, double speed) {
this.targetEntity = targetEntity;
this.attackerEntity = attackerEntity;
this.speed = speed;
this.navigation = entity.getNavigation();
this.setControls(EnumSet.of(Control.MOVE, Control.LOOK));
this.setControls(EnumSet.of(Control.MOVE, Control.LOOK, Control.TARGET));
// Set the target
if (this.targetEntity != null) {
this.attackerEntity.setTarget(this.targetEntity);
}
}
@Override
public boolean canStart() {
// Can start showing aggression if the player is within a certain range.
return this.entity.squaredDistanceTo(this.targetPlayer) < MOVE_DISTANCE;
return isGoalActive();
}
@Override
public boolean shouldContinue() {
// Continue showing aggression as long as the player is alive and within range.
return this.targetPlayer.isAlive() && this.entity.squaredDistanceTo(this.targetPlayer) < MOVE_DISTANCE;
return isGoalActive();
}
@Override
public void stop() {
}
private void performAttack() {
// Check if the entity is a type that is capable of attacking
if (this.entity instanceof HostileEntity || this.entity instanceof Angerable || this.entity instanceof RangedAttackMob) {
// Entity attacks the player
this.entity.tryAttack(this.targetPlayer);
} else {
// For passive entities, apply minimal damage to simulate a 'leap' attack
this.targetPlayer.damage(this.entity.getDamageSources().generic(), 1.0F);
// Play damage sound
this.targetPlayer.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);
// Spawn red particles to simulate 'injury'
((ServerWorld) this.entity.getWorld()).spawnParticles(ParticleTypes.DAMAGE_INDICATOR,
this.targetPlayer.getX(),
this.targetPlayer.getBodyY(0.5D),
this.targetPlayer.getZ(),
10, // number of particles
0.1, 0.1, 0.1, 0.2); // speed and randomness
private boolean isGoalActive() {
if (this.targetEntity == null || (this.targetEntity != null && !this.targetEntity.isAlive())) {
return false;
}
// Is nearby to target
boolean isNearby = this.attackerEntity.squaredDistanceTo(this.targetEntity) < MOVE_DISTANCE;
// Check if the attacker is nearby and no native attacks
boolean isNearbyAndNoNativeAttacks = isNearby && !hasNativeAttacks();
// Check if it has native attacks but can't target (e.g., creative mode)
LivingEntity livingAttackerEntity = this.attackerEntity;
boolean hasNativeAttacksButCannotTarget = isNearby && hasNativeAttacks() && !livingAttackerEntity.canTarget(this.targetEntity);
// Return true if either condition is met
return isNearbyAndNoNativeAttacks || hasNativeAttacksButCannotTarget;
}
private boolean hasNativeAttacks() {
// Does this entity have native attacks
return this.attackerEntity instanceof HostileEntity || this.attackerEntity instanceof Angerable || this.attackerEntity instanceof RangedAttackMob;
}
private void performAttack() {
// For passive entities (or hostile in creative mode), apply minimal damage to simulate a 'leap' / 'melee' attack
this.targetEntity.damage(this.attackerEntity.getDamageSources().generic(), 1.0F);
// Play damage sound
this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);
// Spawn red particles to simulate 'injury'
((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ParticleTypes.DAMAGE_INDICATOR,
this.targetEntity.getX(),
this.targetEntity.getBodyY(0.5D),
this.targetEntity.getZ(),
10, // number of particles
0.1, 0.1, 0.1, 0.2); // speed and randomness
}
@Override
public void tick() {
double squaredDistanceToPlayer = this.entity.squaredDistanceTo(this.targetPlayer);
this.entity.getLookControl().lookAt(this.targetPlayer, 30.0F, 30.0F); // Entity faces the player
double squaredDistanceToPlayer = this.attackerEntity.squaredDistanceTo(this.targetEntity);
this.attackerEntity.getLookControl().lookAt(this.targetEntity, 30.0F, 30.0F);
// State transitions and actions
switch (currentState) {
......@@ -97,7 +114,7 @@ public class AttackPlayerGoal extends Goal {
break;
case MOVING_TOWARDS_PLAYER:
this.entity.getNavigation().startMovingTo(this.targetPlayer, this.speed);
this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed);
if (squaredDistanceToPlayer < CHARGE_DISTANCE) {
currentState = EntityState.CHARGING;
} else {
......@@ -106,7 +123,7 @@ public class AttackPlayerGoal extends Goal {
break;
case CHARGING:
this.entity.getNavigation().startMovingTo(this.targetPlayer, this.speed / 2.5D);
this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D);
if (cooldownTimer <= 0) {
currentState = EntityState.LEAPING;
}
......@@ -114,16 +131,16 @@ public class AttackPlayerGoal extends Goal {
case LEAPING:
// Leap towards the player
Vec3d leapDirection = new Vec3d(this.targetPlayer.getX() - this.entity.getX(), 0.1D, this.targetPlayer.getZ() - this.entity.getZ()).normalize().multiply(1.0);
this.entity.setVelocity(leapDirection);
this.entity.velocityModified = true;
Vec3d leapDirection = new Vec3d(this.targetEntity.getX() - this.attackerEntity.getX(), 0.1D, this.targetEntity.getZ() - this.attackerEntity.getZ()).normalize().multiply(1.0);
this.attackerEntity.setVelocity(leapDirection);
this.attackerEntity.velocityModified = true;
currentState = EntityState.ATTACKING;
break;
case ATTACKING:
// Attack player
this.entity.getNavigation().startMovingTo(this.targetPlayer, this.speed / 2.5D);
this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed / 2.5D);
if (squaredDistanceToPlayer < ATTACK_DISTANCE && cooldownTimer <= 0) {
this.performAttack();
currentState = EntityState.IDLE;
......
package com.owlmaddie.goals;
import net.minecraft.entity.ai.FuzzyTargeting;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.ai.pathing.Path;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.Vec3d;
......@@ -10,12 +12,11 @@ import java.util.EnumSet;
/**
* The {@code FleePlayerGoal} class instructs a Mob Entity to flee from the current player
* and only recalculates the flee path when it has reached its destination and the player is close again.
* and only recalculates path when it has reached its destination and the player is close again.
*/
public class FleePlayerGoal extends Goal {
private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double speed;
private final float fleeDistance;
......@@ -24,7 +25,6 @@ public class FleePlayerGoal extends Goal {
this.entity = entity;
this.speed = speed;
this.fleeDistance = fleeDistance;
this.navigation = entity.getNavigation();
this.setControls(EnumSet.of(Control.MOVE));
}
......@@ -35,22 +35,25 @@ public class FleePlayerGoal extends Goal {
@Override
public boolean shouldContinue() {
return this.navigation.isFollowingPath();
return this.targetPlayer != null && this.entity.squaredDistanceTo(this.targetPlayer) < fleeDistance * fleeDistance;
}
@Override
public void stop() {
this.navigation.stop();
this.entity.getNavigation().stop();
}
private void fleeFromPlayer() {
Vec3d fleeDirection = new Vec3d(
this.entity.getX() - this.targetPlayer.getX(),
this.entity.getY() - this.targetPlayer.getY(),
this.entity.getZ() - this.targetPlayer.getZ()
).normalize();
Vec3d fleeTarget = fleeDirection.multiply(fleeDistance).add(this.entity.getPos());
this.navigation.startMovingTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, this.speed);
int roundedFleeDistance = Math.round(fleeDistance);
Vec3d fleeTarget = FuzzyTargeting.findFrom((PathAwareEntity)this.entity, roundedFleeDistance,
roundedFleeDistance, this.entity.getPos());
if (fleeTarget != null) {
Path path = this.entity.getNavigation().findPathTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, 0);
if (path != null) {
this.entity.getNavigation().startMovingAlong(path, this.speed);
}
}
}
@Override
......@@ -60,9 +63,7 @@ public class FleePlayerGoal extends Goal {
@Override
public void tick() {
// Only recalculate the flee path if the entity has reached its destination or doesn't have an active path,
// and the player is within the flee distance again.
if (!this.navigation.isFollowingPath() && this.entity.squaredDistanceTo(this.targetPlayer) < fleeDistance * fleeDistance) {
if (!this.entity.getNavigation().isFollowingPath()) {
fleeFromPlayer();
}
}
......
package com.owlmaddie.goals;
import com.owlmaddie.controls.LookControls;
import net.minecraft.entity.ai.FuzzyTargeting;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.*;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.Vec3d;
import java.util.EnumSet;
......@@ -33,8 +35,8 @@ public class FollowPlayerGoal extends Goal {
@Override
public boolean shouldContinue() {
// Continue unless the entity gets within 3.x blocks of the player
return this.targetPlayer != null && this.targetPlayer.isAlive() && this.entity.squaredDistanceTo(this.targetPlayer) > 12;
// Continue unless the entity gets within 3 blocks of the player
return this.targetPlayer != null && this.targetPlayer.isAlive() && this.entity.squaredDistanceTo(this.targetPlayer) > 9;
}
@Override
......@@ -45,8 +47,22 @@ public class FollowPlayerGoal extends Goal {
@Override
public void tick() {
// Look at the player and start moving towards them
LookControls.lookAtPlayer(this.targetPlayer, this.entity);
this.navigation.startMovingTo(this.targetPlayer, this.speed);
if (this.entity.squaredDistanceTo(this.targetPlayer) > 256) {
// If the entity is too far away (more than 16 blocks), teleport it within 8 blocks of the player
if (this.entity instanceof EndermanEntity || this.entity instanceof EndermiteEntity || this.entity instanceof ShulkerEntity) {
Vec3d targetPos = findTeleportPosition(8);
if (targetPos != null) {
this.entity.teleport(targetPos.x, targetPos.y, targetPos.z);
}
}
} else {
// Look at the player and start moving towards them
LookControls.lookAtPlayer(this.targetPlayer, this.entity);
this.navigation.startMovingTo(this.targetPlayer, this.speed);
}
}
private Vec3d findTeleportPosition(int distance) {
return FuzzyTargeting.findTo((PathAwareEntity)this.entity, distance, distance, this.targetPlayer.getPos());
}
}
......@@ -7,6 +7,7 @@ package com.owlmaddie.goals;
public enum GoalPriority {
// Enum constants (Goal Types) with their corresponding priority values
TALK_PLAYER(2),
PROTECT_PLAYER(2),
FOLLOW_PLAYER(3),
FLEE_PLAYER(3),
ATTACK_PLAYER(3);
......
package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.MobEntity;
/**
* The {@code ProtectPlayerGoal} class instructs a Mob Entity to show aggression towards any attacker
* of the current player.
*/
public class ProtectPlayerGoal extends AttackPlayerGoal {
protected final LivingEntity protectedEntity;
protected int lastAttackedTime;
public ProtectPlayerGoal(LivingEntity protectEntity, MobEntity attackerEntity, double speed) {
super(null, attackerEntity, speed);
this.protectedEntity = protectEntity;
this.lastAttackedTime = 0;
}
@Override
public boolean canStart() {
MobEntity lastAttackedByEntity = (MobEntity)this.protectedEntity.getLastAttacker();
int i = this.protectedEntity.getLastAttackedTime();
if (i != this.lastAttackedTime && lastAttackedByEntity != null) {
// Set target to attack
this.lastAttackedTime = i;
this.targetEntity = lastAttackedByEntity;
this.attackerEntity.setTarget(this.targetEntity);
}
if (this.targetEntity != null && !this.targetEntity.isAlive()) {
// clear target
this.targetEntity = null;
}
return super.canStart();
}
}
......@@ -19,7 +19,7 @@ public class MessageParser {
LOGGER.info("Parsing message: {}", input);
StringBuilder cleanedMessage = new StringBuilder();
List<Behavior> behaviors = new ArrayList<>();
Pattern pattern = Pattern.compile("[<*](FOLLOW|FLEE|ATTACK|FRIENDSHIP|UNFOLLOW)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
Pattern pattern = Pattern.compile("[<*](FOLLOW|FLEE|ATTACK|FRIENDSHIP|UNFOLLOW|PROTECT|UNPROTECT)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
......
......@@ -47,6 +47,8 @@ Include as many behaviors as needed at the end of the message. These are the ONL
<UNFOLLOW> Stop following the player location. If the player asks you to stay, wait, or stop following them, please output this behavior.
<FLEE> Flee from the player (if you are weak or timid). If the player threatens or scares you, please output this behavior to stay away from the player.
<ATTACK> Attack the player (if you are strong and brave). If the player threatens or scares you, please output this behavior to attack the player and defend yourself.
<PROTECT> Protect the player when they are attacked (if you are strong and brave).
<UNPROTECT> Stop protecting the player
Output Syntax:
......@@ -83,4 +85,16 @@ USER: Prepare to die!
ASSISTANT: Ahhh!!! <FLEE> <FRIENDSHIP -3>
USER: Prepare to die!
ASSISTANT: Ahhh!!! <ATTACK> <FRIENDSHIP -3>
\ No newline at end of file
ASSISTANT: Ahhh!!! <ATTACK> <FRIENDSHIP -3>
USER: Please keep me safe.
ASSISTANT: No problem, I'll keep you safe from danger! <PROTECT>
USER: Can you come with me and protect me?
ASSISTANT: No problem, I'll keep you safe from danger. Let's go! <PROTECT> <FOLLOW>
USER: Don't protect me anymore please
ASSISTANT: Okay! Be safe out there on your own. <UNPROTECT>
USER: I don't need anyone protecting me
ASSISTANT: Okay! Be safe out there on your own. <UNPROTECT>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment