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:
- ./deploy-modrinth.sh
only:
- develop
artifacts:
paths:
- response.txt
tags:
- minecraft
......@@ -127,5 +130,8 @@ CurseForge:
- ./deploy-curseforge.sh
only:
- develop
artifacts:
paths:
- response.txt
tags:
- minecraft
......@@ -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
[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
### Added
......
......@@ -8,7 +8,7 @@ API_URL="https://minecraft.curseforge.com/api"
PROJECT_ID=1012118
DEPENDENCY_SLUG="fabric-api"
USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com"
SLEEP_DURATION=10
SLEEP_DURATION=30
# Function to fetch game version IDs
fetch_game_version_ids() {
......@@ -49,6 +49,7 @@ echo ""
# Iterate over each jar file in the artifacts
for FILE in creaturechat*.jar; do
if [ -f "$FILE" ]; then
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
......
......@@ -31,6 +31,7 @@ echo ""
# Iterate over each jar file in the artifacts
for FILE in creaturechat*.jar; do
if [ -f "$FILE" ]; then
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
......
......@@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# Mod Properties
mod_version=1.0.6
mod_version=1.0.7
maven_group=com.owlmaddie
archives_base_name=creaturechat
......
......@@ -296,7 +296,7 @@ public class BubbleRenderer {
}
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;
// Get Name of entity
......@@ -312,7 +312,7 @@ public class BubbleRenderer {
}
// Truncate long names
if (nameText.length() > 14) {
if (nameText.length() > 14 && truncate) {
nameText = nameText.substring(0, 14) + "...";
}
......@@ -486,7 +486,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status != ChatDataManager.ChatStatus.HIDDEN) {
// 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)
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
......@@ -512,7 +512,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.ASSISTANT && chatData.status == ChatDataManager.ChatStatus.HIDDEN) {
// 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
if (chatData.friendship == 3) {
......@@ -528,7 +528,7 @@ public class BubbleRenderer {
} else if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.status == ChatDataManager.ChatStatus.DISPLAY) {
// Draw Player Name
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING);
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
......@@ -557,7 +557,7 @@ public class BubbleRenderer {
// Draw Player Name (if not self and HUD is visible)
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) {
// Draw 'pending' button (when Chat UI is open)
......
......@@ -12,8 +12,9 @@ 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.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
......@@ -188,7 +189,7 @@ public class ChatDataManager {
contextData.put("world_moon_phase", moonPhaseDescription);
// 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) {
contextData.put("entity_name", "");
} else {
......@@ -255,7 +256,7 @@ public class ChatDataManager {
ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " "));
// 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()) {
LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ?
", Argument: " + behavior.getArgument() : ""));
......@@ -276,7 +277,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 +291,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) {
......@@ -304,6 +312,12 @@ public class ChatDataManager {
// Stop any attack/flee if friendship improves
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.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;
}
......
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.entity.passive.GolemEntity;
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,98 @@ 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;
public class AttackPlayerGoal extends PlayerBaseGoal {
protected final MobEntity attackerEntity;
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) {
super(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));
}
@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 super.canStart() && 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 super.canStart() && isGoalActive();
}
@Override
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() {
// 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
// Track the attacker (needed for protect to work)
if (!this.attackerEntity.equals(this.targetEntity)) {
this.targetEntity.setAttacker(this.attackerEntity);
}
// 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 +121,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 +130,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 +138,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.goal.Goal;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.ai.FuzzyTargeting;
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,47 +11,47 @@ 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 {
public class FleePlayerGoal extends PlayerBaseGoal {
private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double speed;
private final float fleeDistance;
public FleePlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed, float fleeDistance) {
this.targetPlayer = player;
super(player);
this.entity = entity;
this.speed = speed;
this.fleeDistance = fleeDistance;
this.navigation = entity.getNavigation();
this.setControls(EnumSet.of(Control.MOVE));
}
@Override
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
public boolean shouldContinue() {
return this.navigation.isFollowingPath();
return super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) < 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.targetEntity.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 +61,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.goal.Goal;
import net.minecraft.entity.ai.FuzzyTargeting;
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;
/**
* 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 ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double speed;
public FollowPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) {
this.targetPlayer = player;
super(player);
this.entity = entity;
this.speed = speed;
this.navigation = entity.getNavigation();
......@@ -28,13 +28,13 @@ public class FollowPlayerGoal extends Goal {
@Override
public boolean canStart() {
// 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
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 super.canStart() && this.entity.squaredDistanceTo(this.targetEntity) > 9;
}
@Override
......@@ -45,8 +45,24 @@ 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 instanceof EndermanEntity || this.entity instanceof EndermiteEntity || this.entity instanceof ShulkerEntity) {
// Certain entities should teleport to the player if they get too far
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;
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.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 {
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()) {
......
......@@ -59,7 +59,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread
server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.characterSheet.isEmpty()) {
......@@ -76,7 +76,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread
server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F);
......@@ -96,7 +96,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread
server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F);
......@@ -115,7 +115,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread
server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 7F);
......@@ -144,7 +144,7 @@ public class ServerPackets {
// Ensure that the task is synced with the server thread
server.execute(() -> {
MobEntity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.characterSheet.isEmpty()) {
......@@ -270,7 +270,7 @@ public class ServerPackets {
public static void BroadcastPacketMessage(ChatDataManager.EntityChatData chatData) {
for (ServerWorld world : serverInstance.getWorlds()) {
UUID entityId = UUID.fromString(chatData.entityId);
MobEntity entity = ServerEntityFinder.getEntityByUUID(world, entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(world, entityId);
if (entity != null) {
// Set custom name (if null)
String characterName = chatData.getCharacterProp("name");
......
package com.owlmaddie.utils;
import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.server.world.ServerWorld;
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.
*/
public class ServerEntityFinder {
public static MobEntity getEntityByUUID(ServerWorld world, UUID uuid) {
public static LivingEntity getEntityByUUID(ServerWorld world, UUID uuid) {
for (Entity entity : world.iterateEntities()) {
if (entity.getUuid().equals(uuid) && entity instanceof MobEntity) {
return (MobEntity)entity;
if (entity.getUuid().equals(uuid) && entity instanceof LivingEntity) {
return (LivingEntity) entity;
}
}
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
<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). This only protects the player.
<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
......@@ -49,6 +49,10 @@ public class BehaviorTests {
"<attacked you directly with Stone Axe>",
"<attacked you indirectly with Arrow>",
"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(
"Hi friend! I am so happy to see you again!",
"Looking forward to hanging out with you.",
......@@ -109,6 +113,20 @@ public class BehaviorTests {
}
@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() {
for (String message : attackMessages) {
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