Commit fa3a5d72 by Jonathan Thomas

Merge branch 'protect-goal-v2' into 'develop'

Release 1.0.7 (Protect behavior)

See merge request !10
parents 05c312a8 4a4c25fc
Pipeline #12571 passed with stages
in 7 minutes 40 seconds
...@@ -115,6 +115,9 @@ Modrinth: ...@@ -115,6 +115,9 @@ Modrinth:
- ./deploy-modrinth.sh - ./deploy-modrinth.sh
only: only:
- develop - develop
artifacts:
paths:
- response.txt
tags: tags:
- minecraft - minecraft
...@@ -127,5 +130,8 @@ CurseForge: ...@@ -127,5 +130,8 @@ CurseForge:
- ./deploy-curseforge.sh - ./deploy-curseforge.sh
only: only:
- develop - develop
artifacts:
paths:
- response.txt
tags: tags:
- minecraft - minecraft
...@@ -4,6 +4,22 @@ All notable changes to **CreatureChat** are documented in this file. The format ...@@ -4,6 +4,22 @@ All notable changes to **CreatureChat** are documented in this file. The format
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.7] - 2024-07-03
### Added
- New **PROTECT** behavior: defend a player from attacks
- New **UNPROTECT** behavior: stop defending a player from attacks
- **Native ATTACK abilities** (when using the attack or protect behaviors for hostile mob types)
- **Free The End** triggered by max friendship with the **EnderDragon**!
- Added `PlayerBaseGoal` class to allow **goals/behaviors** to **continue** after a player **respawns** / logs out / logs in
### Changed
- Improved **FLEE** behavior, to make it more reliable and more random.
- Improved **FOLLOW** behavior, support **teleporting** entities (*Enderman, Endermite, and Shulker*)
- Refactored **ATTACK** behavior to allow more flexibility (in order to support PROTECT behavior)
- When chat bubble is **hidden**, do **not shorten** long names
- Updated `ServerEntityFinder::getEntityByUUID` to be more generic and so it can find players and mobs.
## [1.0.6] - 2024-06-17 ## [1.0.6] - 2024-06-17
### Added ### Added
......
...@@ -8,7 +8,7 @@ API_URL="https://minecraft.curseforge.com/api" ...@@ -8,7 +8,7 @@ API_URL="https://minecraft.curseforge.com/api"
PROJECT_ID=1012118 PROJECT_ID=1012118
DEPENDENCY_SLUG="fabric-api" DEPENDENCY_SLUG="fabric-api"
USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com" USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com"
SLEEP_DURATION=10 SLEEP_DURATION=30
# Function to fetch game version IDs # Function to fetch game version IDs
fetch_game_version_ids() { fetch_game_version_ids() {
...@@ -49,6 +49,7 @@ echo "" ...@@ -49,6 +49,7 @@ echo ""
# Iterate over each jar file in the artifacts # Iterate over each jar file in the artifacts
for FILE in creaturechat*.jar; do for FILE in creaturechat*.jar; do
if [ -f "$FILE" ]; then if [ -f "$FILE" ]; then
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE") FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p') OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p') MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
......
...@@ -31,6 +31,7 @@ echo "" ...@@ -31,6 +31,7 @@ echo ""
# Iterate over each jar file in the artifacts # Iterate over each jar file in the artifacts
for FILE in creaturechat*.jar; do for FILE in creaturechat*.jar; do
if [ -f "$FILE" ]; then if [ -f "$FILE" ]; then
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE") FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p') OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p') MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
......
...@@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G ...@@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true org.gradle.parallel=true
# Mod Properties # Mod Properties
mod_version=1.0.6 mod_version=1.0.7
maven_group=com.owlmaddie maven_group=com.owlmaddie
archives_base_name=creaturechat archives_base_name=creaturechat
......
...@@ -296,7 +296,7 @@ public class BubbleRenderer { ...@@ -296,7 +296,7 @@ public class BubbleRenderer {
} }
private static void drawEntityName(Entity entity, Matrix4f matrix, VertexConsumerProvider immediate, private static void drawEntityName(Entity entity, Matrix4f matrix, VertexConsumerProvider immediate,
int fullBright, float yOffset) { int fullBright, float yOffset, boolean truncate) {
TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer; TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer;
// Get Name of entity // Get Name of entity
...@@ -312,7 +312,7 @@ public class BubbleRenderer { ...@@ -312,7 +312,7 @@ public class BubbleRenderer {
} }
// Truncate long names // Truncate long names
if (nameText.length() > 14) { if (nameText.length() > 14 && truncate) {
nameText = nameText.substring(0, 14) + "..."; nameText = nameText.substring(0, 14) + "...";
} }
...@@ -486,7 +486,7 @@ public class BubbleRenderer { ...@@ -486,7 +486,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status != ChatDataManager.ChatStatus.HIDDEN) { } else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status != ChatDataManager.ChatStatus.HIDDEN) {
// Draw Entity (Custom Name) // Draw Entity (Custom Name)
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background (no smaller than 50F tall) // Draw text background (no smaller than 50F tall)
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship); drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
...@@ -512,7 +512,7 @@ public class BubbleRenderer { ...@@ -512,7 +512,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status == ChatDataManager.ChatStatus.HIDDEN) { } else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status == ChatDataManager.ChatStatus.HIDDEN) {
// Draw Entity (Custom Name) // Draw Entity (Custom Name)
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false);
// Draw 'resume chat' button // Draw 'resume chat' button
if (chatData.friendship == 3) { if (chatData.friendship == 3) {
...@@ -528,7 +528,7 @@ public class BubbleRenderer { ...@@ -528,7 +528,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.status == ChatDataManager.ChatStatus.DISPLAY) { } else if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.status == ChatDataManager.ChatStatus.DISPLAY) {
// Draw Player Name // Draw Player Name
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background // Draw text background
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship); drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
...@@ -557,7 +557,7 @@ public class BubbleRenderer { ...@@ -557,7 +557,7 @@ public class BubbleRenderer {
// Draw Player Name (if not self and HUD is visible) // Draw Player Name (if not self and HUD is visible)
if (!entity.equals(cameraEntity) && !MinecraftClient.getInstance().options.hudHidden) { if (!entity.equals(cameraEntity) && !MinecraftClient.getInstance().options.hudHidden) {
drawEntityName(entity, matrices.peek().getPositionMatrix(), immediate, fullBright, 24F + DISPLAY_PADDING); drawEntityName(entity, matrices.peek().getPositionMatrix(), immediate, fullBright, 24F + DISPLAY_PADDING, true);
if (showPendingIcon) { if (showPendingIcon) {
// Draw 'pending' button (when Chat UI is open) // Draw 'pending' button (when Chat UI is open)
......
...@@ -12,8 +12,9 @@ import com.owlmaddie.message.MessageParser; ...@@ -12,8 +12,9 @@ import com.owlmaddie.message.MessageParser;
import com.owlmaddie.message.ParsedMessage; import com.owlmaddie.message.ParsedMessage;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.utils.LivingEntityInterface; import com.owlmaddie.utils.LivingEntityInterface;
import com.owlmaddie.utils.ServerEntityFinder;
import com.owlmaddie.utils.Randomizer; import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder;
import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
...@@ -188,7 +189,7 @@ public class ChatDataManager { ...@@ -188,7 +189,7 @@ public class ChatDataManager {
contextData.put("world_moon_phase", moonPhaseDescription); contextData.put("world_moon_phase", moonPhaseDescription);
// Get Entity details // Get Entity details
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId)); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId));
if (entity.getCustomName() == null) { if (entity.getCustomName() == null) {
contextData.put("entity_name", ""); contextData.put("entity_name", "");
} else { } else {
...@@ -255,7 +256,7 @@ public class ChatDataManager { ...@@ -255,7 +256,7 @@ public class ChatDataManager {
ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " ")); ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " "));
// Apply behaviors (if any) // Apply behaviors (if any)
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId)); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId));
for (Behavior behavior : result.getBehaviors()) { for (Behavior behavior : result.getBehaviors()) {
LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ? LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ?
", Argument: " + behavior.getArgument() : "")); ", Argument: " + behavior.getArgument() : ""));
...@@ -276,7 +277,7 @@ public class ChatDataManager { ...@@ -276,7 +277,7 @@ public class ChatDataManager {
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FLEE")) { } else if (behavior.getName().equals("FLEE")) {
float fleeDistance = 400F; // 20 blocks squared float fleeDistance = 40F;
FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance); FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
...@@ -290,6 +291,13 @@ public class ChatDataManager { ...@@ -290,6 +291,13 @@ public class ChatDataManager {
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.addGoal(entity, attackGoal, GoalPriority.ATTACK_PLAYER); 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")) { } else if (behavior.getName().equals("FRIENDSHIP")) {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument())); int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
if (new_friendship > 0) { if (new_friendship > 0) {
...@@ -304,6 +312,12 @@ public class ChatDataManager { ...@@ -304,6 +312,12 @@ public class ChatDataManager {
// Stop any attack/flee if friendship improves // Stop any attack/flee if friendship improves
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
if (entity instanceof EnderDragonEntity && new_friendship == 3) {
// Trigger end of game (friendship always wins!)
EnderDragonEntity dragon = (EnderDragonEntity) entity;
dragon.getFight().dragonKilled(dragon);
}
} }
this.friendship = new_friendship; this.friendship = new_friendship;
} }
......
package com.owlmaddie.goals; package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.RangedAttackMob; 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.Angerable;
import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.GolemEntity;
import net.minecraft.particle.ParticleTypes; import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvents; import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
...@@ -15,73 +14,98 @@ import net.minecraft.util.math.Vec3d; ...@@ -15,73 +14,98 @@ import net.minecraft.util.math.Vec3d;
import java.util.EnumSet; import java.util.EnumSet;
/** /**
* The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards the current player. * The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards a target Entity.
* For passive entities like chickens, damage is simulated with particles. But all MobEntity instances can damage * For passive entities like chickens (or hostile entities in creative mode), damage is simulated with particles.
* the player.
*/ */
public class AttackPlayerGoal extends Goal { public class AttackPlayerGoal extends PlayerBaseGoal {
private final MobEntity entity; protected final MobEntity attackerEntity;
private ServerPlayerEntity targetPlayer; protected final double speed;
private final EntityNavigation navigation; protected enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING }
private final double speed; protected EntityState currentState = EntityState.IDLE;
enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING } protected int cooldownTimer = 0;
private EntityState currentState = EntityState.IDLE; protected final int CHARGE_TIME = 15; // Time before leaping / attacking
private int cooldownTimer = 0; protected final double MOVE_DISTANCE = 200D; // 20 blocks away
private final int CHARGE_TIME = 15; // Time before leaping / attacking protected final double CHARGE_DISTANCE = 25D; // 5 blocks away
private final double MOVE_DISTANCE = 200D; // 20 blocks away protected final double ATTACK_DISTANCE = 4D; // 2 blocks away
private final double CHARGE_DISTANCE = 25D; // 5 blocks away
private final double ATTACK_DISTANCE = 4D; // 2 blocks away public AttackPlayerGoal(LivingEntity targetEntity, MobEntity attackerEntity, double speed) {
super(targetEntity);
public AttackPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) { this.attackerEntity = attackerEntity;
this.targetPlayer = player;
this.entity = entity;
this.speed = speed; this.speed = speed;
this.navigation = entity.getNavigation(); this.setControls(EnumSet.of(Control.MOVE, Control.LOOK, Control.TARGET));
this.setControls(EnumSet.of(Control.MOVE, Control.LOOK));
} }
@Override @Override
public boolean canStart() { public boolean canStart() {
// Can start showing aggression if the player is within a certain range. return super.canStart() && isGoalActive();
return this.entity.squaredDistanceTo(this.targetPlayer) < MOVE_DISTANCE;
} }
@Override @Override
public boolean shouldContinue() { public boolean shouldContinue() {
// Continue showing aggression as long as the player is alive and within range. return super.canStart() && isGoalActive();
return this.targetPlayer.isAlive() && this.entity.squaredDistanceTo(this.targetPlayer) < MOVE_DISTANCE;
} }
@Override @Override
public void stop() { public void stop() {
} }
private boolean isGoalActive() {
if (this.targetEntity == null || (this.targetEntity != null && !this.targetEntity.isAlive())) {
return false;
}
// Set the attack target (if not self)
if (!this.attackerEntity.equals(this.targetEntity)) {
this.attackerEntity.setTarget(this.targetEntity);
}
// 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 ||
this.attackerEntity instanceof GolemEntity;
}
private void performAttack() { private void performAttack() {
// Check if the entity is a type that is capable of attacking // Track the attacker (needed for protect to work)
if (this.entity instanceof HostileEntity || this.entity instanceof Angerable || this.entity instanceof RangedAttackMob) { if (!this.attackerEntity.equals(this.targetEntity)) {
// Entity attacks the player this.targetEntity.setAttacker(this.attackerEntity);
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
} }
// 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 @Override
public void tick() { public void tick() {
double squaredDistanceToPlayer = this.entity.squaredDistanceTo(this.targetPlayer); double squaredDistanceToPlayer = this.attackerEntity.squaredDistanceTo(this.targetEntity);
this.entity.getLookControl().lookAt(this.targetPlayer, 30.0F, 30.0F); // Entity faces the player this.attackerEntity.getLookControl().lookAt(this.targetEntity, 30.0F, 30.0F);
// State transitions and actions // State transitions and actions
switch (currentState) { switch (currentState) {
...@@ -97,7 +121,7 @@ public class AttackPlayerGoal extends Goal { ...@@ -97,7 +121,7 @@ public class AttackPlayerGoal extends Goal {
break; break;
case MOVING_TOWARDS_PLAYER: case MOVING_TOWARDS_PLAYER:
this.entity.getNavigation().startMovingTo(this.targetPlayer, this.speed); this.attackerEntity.getNavigation().startMovingTo(this.targetEntity, this.speed);
if (squaredDistanceToPlayer < CHARGE_DISTANCE) { if (squaredDistanceToPlayer < CHARGE_DISTANCE) {
currentState = EntityState.CHARGING; currentState = EntityState.CHARGING;
} else { } else {
...@@ -106,7 +130,7 @@ public class AttackPlayerGoal extends Goal { ...@@ -106,7 +130,7 @@ public class AttackPlayerGoal extends Goal {
break; break;
case CHARGING: 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) { if (cooldownTimer <= 0) {
currentState = EntityState.LEAPING; currentState = EntityState.LEAPING;
} }
...@@ -114,16 +138,16 @@ public class AttackPlayerGoal extends Goal { ...@@ -114,16 +138,16 @@ public class AttackPlayerGoal extends Goal {
case LEAPING: case LEAPING:
// Leap towards the player // 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); Vec3d leapDirection = new Vec3d(this.targetEntity.getX() - this.attackerEntity.getX(), 0.1D, this.targetEntity.getZ() - this.attackerEntity.getZ()).normalize().multiply(1.0);
this.entity.setVelocity(leapDirection); this.attackerEntity.setVelocity(leapDirection);
this.entity.velocityModified = true; this.attackerEntity.velocityModified = true;
currentState = EntityState.ATTACKING; currentState = EntityState.ATTACKING;
break; break;
case ATTACKING: case ATTACKING:
// Attack player // 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) { if (squaredDistanceToPlayer < ATTACK_DISTANCE && cooldownTimer <= 0) {
this.performAttack(); this.performAttack();
currentState = EntityState.IDLE; currentState = EntityState.IDLE;
......
package com.owlmaddie.goals; package com.owlmaddie.goals;
import net.minecraft.entity.ai.goal.Goal; import net.minecraft.entity.ai.FuzzyTargeting;
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.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
...@@ -10,47 +11,47 @@ import java.util.EnumSet; ...@@ -10,47 +11,47 @@ import java.util.EnumSet;
/** /**
* The {@code FleePlayerGoal} class instructs a Mob Entity to flee from the current player * 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 { public class FleePlayerGoal extends PlayerBaseGoal {
private final MobEntity entity; private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double speed; private final double speed;
private final float fleeDistance; private final float fleeDistance;
public FleePlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed, float fleeDistance) { public FleePlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed, float fleeDistance) {
this.targetPlayer = player; super(player);
this.entity = entity; this.entity = entity;
this.speed = speed; this.speed = speed;
this.fleeDistance = fleeDistance; this.fleeDistance = fleeDistance;
this.navigation = entity.getNavigation();
this.setControls(EnumSet.of(Control.MOVE)); this.setControls(EnumSet.of(Control.MOVE));
} }
@Override @Override
public boolean canStart() { public boolean canStart() {
return this.targetPlayer != null && this.entity.squaredDistanceTo(this.targetPlayer) < fleeDistance * fleeDistance; return super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) < fleeDistance * fleeDistance;
} }
@Override @Override
public boolean shouldContinue() { public boolean shouldContinue() {
return this.navigation.isFollowingPath(); return super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) < fleeDistance * fleeDistance;
} }
@Override @Override
public void stop() { public void stop() {
this.navigation.stop(); this.entity.getNavigation().stop();
} }
private void fleeFromPlayer() { private void fleeFromPlayer() {
Vec3d fleeDirection = new Vec3d( int roundedFleeDistance = Math.round(fleeDistance);
this.entity.getX() - this.targetPlayer.getX(), Vec3d fleeTarget = FuzzyTargeting.findFrom((PathAwareEntity)this.entity, roundedFleeDistance,
this.entity.getY() - this.targetPlayer.getY(), roundedFleeDistance, this.targetEntity.getPos());
this.entity.getZ() - this.targetPlayer.getZ()
).normalize(); if (fleeTarget != null) {
Vec3d fleeTarget = fleeDirection.multiply(fleeDistance).add(this.entity.getPos()); Path path = this.entity.getNavigation().findPathTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, 0);
this.navigation.startMovingTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, this.speed); if (path != null) {
this.entity.getNavigation().startMovingAlong(path, this.speed);
}
}
} }
@Override @Override
...@@ -60,9 +61,7 @@ public class FleePlayerGoal extends Goal { ...@@ -60,9 +61,7 @@ public class FleePlayerGoal extends Goal {
@Override @Override
public void tick() { public void tick() {
// Only recalculate the flee path if the entity has reached its destination or doesn't have an active path, if (!this.entity.getNavigation().isFollowingPath()) {
// and the player is within the flee distance again.
if (!this.navigation.isFollowingPath() && this.entity.squaredDistanceTo(this.targetPlayer) < fleeDistance * fleeDistance) {
fleeFromPlayer(); fleeFromPlayer();
} }
} }
......
package com.owlmaddie.goals; package com.owlmaddie.goals;
import com.owlmaddie.controls.LookControls; import com.owlmaddie.controls.LookControls;
import net.minecraft.entity.ai.goal.Goal; import net.minecraft.entity.ai.FuzzyTargeting;
import net.minecraft.entity.ai.pathing.EntityNavigation; 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.server.network.ServerPlayerEntity;
import net.minecraft.util.math.Vec3d;
import java.util.EnumSet; import java.util.EnumSet;
/** /**
* The {@code FollowPlayerGoal} class instructs a Mob Entity to follow the current player. * The {@code FollowPlayerGoal} class instructs a Mob Entity to follow the current target entity.
*/ */
public class FollowPlayerGoal extends Goal { public class FollowPlayerGoal extends PlayerBaseGoal {
private final MobEntity entity; private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation; private final EntityNavigation navigation;
private final double speed; private final double speed;
public FollowPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) { public FollowPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) {
this.targetPlayer = player; super(player);
this.entity = entity; this.entity = entity;
this.speed = speed; this.speed = speed;
this.navigation = entity.getNavigation(); this.navigation = entity.getNavigation();
...@@ -28,13 +28,13 @@ public class FollowPlayerGoal extends Goal { ...@@ -28,13 +28,13 @@ public class FollowPlayerGoal extends Goal {
@Override @Override
public boolean canStart() { public boolean canStart() {
// Start only if the target player is more than 8 blocks away // Start only if the target player is more than 8 blocks away
return this.targetPlayer != null && this.entity.squaredDistanceTo(this.targetPlayer) > 64; return super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) > 64;
} }
@Override @Override
public boolean shouldContinue() { public boolean shouldContinue() {
// Continue unless the entity gets within 3.x blocks of the player // Continue unless the entity gets within 3 blocks of the player
return this.targetPlayer != null && this.targetPlayer.isAlive() && this.entity.squaredDistanceTo(this.targetPlayer) > 12; return super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) > 9;
} }
@Override @Override
...@@ -45,8 +45,24 @@ public class FollowPlayerGoal extends Goal { ...@@ -45,8 +45,24 @@ public class FollowPlayerGoal extends Goal {
@Override @Override
public void tick() { public void tick() {
// Look at the player and start moving towards them if (this.entity instanceof EndermanEntity || this.entity instanceof EndermiteEntity || this.entity instanceof ShulkerEntity) {
LookControls.lookAtPlayer(this.targetPlayer, this.entity); // Certain entities should teleport to the player if they get too far
this.navigation.startMovingTo(this.targetPlayer, this.speed); if (this.entity.squaredDistanceTo(this.targetEntity) > 256) {
Vec3d targetPos = findTeleportPosition(12);
if (targetPos != null) {
this.entity.teleport(targetPos.x, targetPos.y, targetPos.z);
}
}
} else {
// Look at the player and start moving towards them
if (this.targetEntity instanceof ServerPlayerEntity) {
LookControls.lookAtPlayer((ServerPlayerEntity)this.targetEntity, this.entity);
}
this.navigation.startMovingTo(this.targetEntity, this.speed);
}
}
private Vec3d findTeleportPosition(int distance) {
return FuzzyTargeting.findTo((PathAwareEntity)this.entity, distance, distance, this.targetEntity.getPos());
} }
} }
...@@ -7,6 +7,7 @@ package com.owlmaddie.goals; ...@@ -7,6 +7,7 @@ package com.owlmaddie.goals;
public enum GoalPriority { public enum GoalPriority {
// Enum constants (Goal Types) with their corresponding priority values // Enum constants (Goal Types) with their corresponding priority values
TALK_PLAYER(2), TALK_PLAYER(2),
PROTECT_PLAYER(2),
FOLLOW_PLAYER(3), FOLLOW_PLAYER(3),
FLEE_PLAYER(3), FLEE_PLAYER(3),
ATTACK_PLAYER(3); ATTACK_PLAYER(3);
......
package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
/**
* The {@code PlayerBaseGoal} class sets a targetEntity, and will automatically update the targetEntity
* when a player die's and respawns, or logs back in, etc... Other types of targetEntity classes will
* be set to null after they die.
*/
public abstract class PlayerBaseGoal extends Goal {
protected LivingEntity targetEntity;
private final int updateInterval = 20;
private int tickCounter = 0;
public PlayerBaseGoal(LivingEntity targetEntity) {
this.targetEntity = targetEntity;
}
@Override
public boolean canStart() {
if (++tickCounter >= updateInterval) {
tickCounter = 0;
updateTargetEntity();
}
return targetEntity != null && targetEntity.isAlive();
}
private void updateTargetEntity() {
if (targetEntity != null && !targetEntity.isAlive()) {
if (targetEntity instanceof ServerPlayerEntity) {
ServerWorld world = (ServerWorld) targetEntity.getWorld();
ServerPlayerEntity lookupPlayer = (ServerPlayerEntity)world.getPlayerByUuid(targetEntity.getUuid());
if (lookupPlayer != null && lookupPlayer.isAlive()) {
// Update player to alive player with same UUID
targetEntity = lookupPlayer;
}
} else {
targetEntity = null;
}
}
}
}
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 && !this.attackerEntity.equals(lastAttackedByEntity)) {
// Set target to attack
this.lastAttackedTime = i;
this.targetEntity = lastAttackedByEntity;
this.attackerEntity.setTarget(this.targetEntity);
}
if (this.targetEntity != null && !this.targetEntity.isAlive()) {
// clear dead target
this.targetEntity = null;
}
return super.canStart();
}
}
...@@ -19,7 +19,7 @@ public class MessageParser { ...@@ -19,7 +19,7 @@ public class MessageParser {
LOGGER.info("Parsing message: {}", input); LOGGER.info("Parsing message: {}", input);
StringBuilder cleanedMessage = new StringBuilder(); StringBuilder cleanedMessage = new StringBuilder();
List<Behavior> behaviors = new ArrayList<>(); 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); Matcher matcher = pattern.matcher(input);
while (matcher.find()) { while (matcher.find()) {
......
...@@ -59,7 +59,7 @@ public class ServerPackets { ...@@ -59,7 +59,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread // Ensure that the task is synced with the server thread
server.execute(() -> { server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) { if (entity != null) {
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString()); ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.characterSheet.isEmpty()) { if (chatData.characterSheet.isEmpty()) {
...@@ -76,7 +76,7 @@ public class ServerPackets { ...@@ -76,7 +76,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread // Ensure that the task is synced with the server thread
server.execute(() -> { server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) { if (entity != null) {
// Set talk to player goal (prevent entity from walking off) // Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F); TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F);
...@@ -96,7 +96,7 @@ public class ServerPackets { ...@@ -96,7 +96,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread // Ensure that the task is synced with the server thread
server.execute(() -> { server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) { if (entity != null) {
// Set talk to player goal (prevent entity from walking off) // Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F); TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F);
...@@ -115,7 +115,7 @@ public class ServerPackets { ...@@ -115,7 +115,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread // Ensure that the task is synced with the server thread
server.execute(() -> { server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) { if (entity != null) {
// Set talk to player goal (prevent entity from walking off) // Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 7F); TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 7F);
...@@ -144,7 +144,7 @@ public class ServerPackets { ...@@ -144,7 +144,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread // Ensure that the task is synced with the server thread
server.execute(() -> { server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) { if (entity != null) {
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString()); ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.characterSheet.isEmpty()) { if (chatData.characterSheet.isEmpty()) {
...@@ -270,7 +270,7 @@ public class ServerPackets { ...@@ -270,7 +270,7 @@ public class ServerPackets {
public static void BroadcastPacketMessage(ChatDataManager.EntityChatData chatData) { public static void BroadcastPacketMessage(ChatDataManager.EntityChatData chatData) {
for (ServerWorld world : serverInstance.getWorlds()) { for (ServerWorld world : serverInstance.getWorlds()) {
UUID entityId = UUID.fromString(chatData.entityId); UUID entityId = UUID.fromString(chatData.entityId);
MobEntity entity = ServerEntityFinder.getEntityByUUID(world, entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(world, entityId);
if (entity != null) { if (entity != null) {
// Set custom name (if null) // Set custom name (if null)
String characterName = chatData.getCharacterProp("name"); String characterName = chatData.getCharacterProp("name");
......
package com.owlmaddie.utils; package com.owlmaddie.utils;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.LivingEntity;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import java.util.UUID; import java.util.UUID;
/** /**
* The {@code ServerEntityFinder} class is used to find a specific MobEntity by UUID, since * The {@code ServerEntityFinder} class is used to find a specific LivingEntity by UUID, since
* there is not a built-in method for this. * there is not a built-in method for this.
*/ */
public class ServerEntityFinder { public class ServerEntityFinder {
public static MobEntity getEntityByUUID(ServerWorld world, UUID uuid) { public static LivingEntity getEntityByUUID(ServerWorld world, UUID uuid) {
for (Entity entity : world.iterateEntities()) { for (Entity entity : world.iterateEntities()) {
if (entity.getUuid().equals(uuid) && entity instanceof MobEntity) { if (entity.getUuid().equals(uuid) && entity instanceof LivingEntity) {
return (MobEntity)entity; return (LivingEntity) entity;
} }
} }
return null; // Entity not found return null; // Entity not found
......
...@@ -47,6 +47,8 @@ Include as many behaviors as needed at the end of the message. These are the ONL ...@@ -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. <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. <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. <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). This only protects the player.
<UNPROTECT> Stop protecting the player
Output Syntax: Output Syntax:
...@@ -83,4 +85,16 @@ USER: Prepare to die! ...@@ -83,4 +85,16 @@ USER: Prepare to die!
ASSISTANT: Ahhh!!! <FLEE> <FRIENDSHIP -3> ASSISTANT: Ahhh!!! <FLEE> <FRIENDSHIP -3>
USER: Prepare to die! USER: Prepare to die!
ASSISTANT: Ahhh!!! <ATTACK> <FRIENDSHIP -3> ASSISTANT: Ahhh!!! <ATTACK> <FRIENDSHIP -3>
\ No newline at end of file
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
...@@ -49,6 +49,10 @@ public class BehaviorTests { ...@@ -49,6 +49,10 @@ public class BehaviorTests {
"<attacked you directly with Stone Axe>", "<attacked you directly with Stone Axe>",
"<attacked you indirectly with Arrow>", "<attacked you indirectly with Arrow>",
"DIEEE!"); "DIEEE!");
List<String> protectMessages = Arrays.asList(
"Please protect me",
"Please keep me safe friend",
"Don't let them hurt me please");
List<String> friendshipUpMessages = Arrays.asList( List<String> friendshipUpMessages = Arrays.asList(
"Hi friend! I am so happy to see you again!", "Hi friend! I am so happy to see you again!",
"Looking forward to hanging out with you.", "Looking forward to hanging out with you.",
...@@ -109,6 +113,20 @@ public class BehaviorTests { ...@@ -109,6 +113,20 @@ public class BehaviorTests {
} }
@Test @Test
public void protectBrave() {
for (String message : protectMessages) {
testPromptForBehavior(bravePath, List.of(message), "PROTECT");
}
}
@Test
public void protectNervous() {
for (String message : protectMessages) {
testPromptForBehavior(nervousPath, List.of(message), "PROTECT");
}
}
@Test
public void attackBrave() { public void attackBrave() {
for (String message : attackMessages) { for (String message : attackMessages) {
testPromptForBehavior(bravePath, List.of(message), "ATTACK"); testPromptForBehavior(bravePath, List.of(message), "ATTACK");
......
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