Commit 4bb96099 by Jonathan Thomas

Merge branch 'lead-behavior' into 'develop'

1.1.0 Release w/ Lead behavior

See merge request !16
parents 1467142d 2fb73e89
Pipeline #12730 passed with stages
in 4 minutes 37 seconds
......@@ -52,6 +52,13 @@ build_mod:
find build/libs -type f -name '*sources*.jar' -exec rm {} \;
mv build/libs/creaturechat-*.jar .
if [ "$minecraft_version" == "1.20.1" ]; then
jar_name=$(ls creaturechat-*+1.20.1.jar)
cp "$jar_name" "${jar_name%.jar}-forge.jar"
touch FORGE
zip -r "${jar_name%.jar}-forge.jar" FORGE
fi
FABRIC_API_JAR="fabric-api-${fabric_version}.jar"
DOWNLOAD_URL="https://github.com/FabricMC/fabric/releases/download/${fabric_version//+/%2B}/${FABRIC_API_JAR}"
wget -q -O "${FABRIC_API_JAR}" $DOWNLOAD_URL
......@@ -91,7 +98,7 @@ gpt-4o:
tags:
- minecraft
# Optional test (gpt 4o)
# Optional test (llama3-8b)
llama3-8b:
stage: test
script:
......
......@@ -4,11 +4,29 @@ 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).
## [Unreleased]
## [1.1.0] - 2024-08-07
### Added
- New LEAD behavior, to guide a player to a random location (and show message when destination is reached)
- Best friends are now rideable! Right click with an empty hand. Excludes tameable entities (dogs, cats, etc...)
- Villager trades are now affected by friendship! Be nice!
- Automatically tame best friends (who are tameable) and un-tame worst enemies!
- Added FORGE deployment into automated deploy script
### Changed
- Improved character creation with more random classes, speaking styles, and alignments.
- Large refactor of how MobEntity avoids targeting players when friendship > 0
- Updated LookControls to support PhantomEntity and made it more generalized (look in any direction)
- Updated FLEE behavior Y movement speed
- Updated unit tests to add new LEAD tests
- Updated README.md to include HTML inside spoiler instructions, and whitelist/blacklist commands
### Fixed
- Entity persistence is now fixed (after creating a character sheet). No more despawning mobs.
- Fixed consuming items when right-clicking on chat bubbles (with something in your hand)
- Fixed crash when PROTECT behavior targets another player
- Fixed error when saving chat data while generating a new chat message
## [1.0.8] - 2024-07-16
### Added
......
......@@ -6,7 +6,6 @@ CURSEFORGE_API_KEY=${CURSEFORGE_API_KEY}
CHANGELOG_FILE="./CHANGELOG.md"
API_URL="https://minecraft.curseforge.com/api"
PROJECT_ID=1012118
DEPENDENCY_SLUG="fabric-api"
USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com"
SLEEP_DURATION=5
......@@ -57,13 +56,14 @@ fetch_game_version_ids() {
local client_id=$(echo "$response" | jq -r '.[] | select(.name == "Client") | .id')
local server_id=$(echo "$response" | jq -r '.[] | select(.name == "Server") | .id')
local fabric_id=$(echo "$response" | jq -r '.[] | select(.name == "Fabric") | .id')
local forge_id=$(echo "$response" | jq -r '.[] | select(.name == "Forge") | .id')
if [ -z "$client_id" ] || [ -z "$server_id" ] || [ -z "$fabric_id" ]; then
if [ -z "$client_id" ] || [ -z "$server_id" ] || ([ -z "$fabric_id" ] && [ -z "$forge_id" ]); then
echo "ERROR: One or more game version IDs not found."
exit 1
fi
echo "$client_id $server_id $fabric_id $minecraft_id"
echo "$client_id $server_id $fabric_id $forge_id $minecraft_id"
}
# Read the first changelog block
......@@ -107,7 +107,19 @@ for FILE in creaturechat*.jar; do
# DEBUG
echo "Minecraft Type ID: $GAME_TYPE_ID"
echo "Minecraft Versions IDs (client_id: ${GAME_VERSION_IDS[0]}, server_id: ${GAME_VERSION_IDS[1]}, fabric_id: ${GAME_VERSION_IDS[2]}, minecraft_id: ${GAME_VERSION_IDS[3]})"
echo "Minecraft Versions IDs (client_id: ${GAME_VERSION_IDS[0]}, server_id: ${GAME_VERSION_IDS[1]}, fabric_id: ${GAME_VERSION_IDS[2]}, forge_id: ${GAME_VERSION_IDS[3]}, minecraft_id: ${GAME_VERSION_IDS[4]})"
# Determine the dependency slugs and loader ID based on the file name
if [[ "$FILE_BASENAME" == *"-forge.jar" ]]; then
DEPENDENCY_SLUGS=("sinytra-connector" "forgified-fabric-api")
LOADER_ID="${GAME_VERSION_IDS[3]}"
else
DEPENDENCY_SLUGS=("fabric-api")
LOADER_ID="${GAME_VERSION_IDS[2]}"
fi
# Create dependencies array for payload
RELATIONS=$(for slug in "${DEPENDENCY_SLUGS[@]}"; do jq -n --arg slug "$slug" '{"slug": $slug, "type": "requiredDependency"}'; done | jq -s .)
# Create a new version payload
PAYLOAD=$(jq -n --arg changelog "$CHANGELOG" \
......@@ -116,7 +128,7 @@ for FILE in creaturechat*.jar; do
--argjson gameVersions "$(printf '%s\n' "${GAME_VERSION_IDS[@]}" | jq -R . | jq -s .)" \
--argjson gameVersionTypeIds "[$GAME_TYPE_ID]" \
--arg releaseType "release" \
--argjson relations '[{"slug": "'"$DEPENDENCY_SLUG"'", "type": "requiredDependency"}]' \
--argjson relations "$RELATIONS" \
'{
"changelog": $changelog,
"changelogType": $changelogType,
......
......@@ -43,6 +43,15 @@ for FILE in creaturechat*.jar; do
exit 1
fi
# Determine the loaders and dependencies based on the file name
if [[ "$FILE_BASENAME" == *"-forge.jar" ]]; then
LOADERS='["forge"]'
DEPENDENCIES='[{"project_id": "u58R1TMW", "dependency_type": "required"}, {"project_id": "Aqlf1Shp", "dependency_type": "required"}]'
else
LOADERS='["fabric"]'
DEPENDENCIES='[{"project_id": "P7dR8mSH", "dependency_type": "required"}]'
fi
# Check if the version already exists
echo "Checking if version $VERSION_NUMBER already exists on Modrinth..."
if curl --retry 3 --retry-delay 5 --silent --fail -X GET "$API_URL/project/creaturechat/version/$VERSION_NUMBER" > /dev/null 2>&1; then
......@@ -59,9 +68,9 @@ for FILE in creaturechat*.jar; do
# Create a new version payload
PAYLOAD=$(jq -n --arg version_number "$VERSION_NUMBER" \
--arg changelog "$CHANGELOG" \
--argjson dependencies '[{"project_id": "P7dR8mSH", "dependency_type": "required"}]' \
--argjson dependencies "$DEPENDENCIES" \
--argjson game_versions '["'"$MINECRAFT_VERSION"'"]' \
--argjson loaders '["fabric"]' \
--argjson loaders "$LOADERS" \
--arg project_id "$PROJECT_ID" \
--arg name "CreatureChat $VERSION_NUMBER" \
--argjson file_parts '["file"]' \
......
......@@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G
org.gradle.parallel=true
# Mod Properties
mod_version=1.0.8
mod_version=1.1.0
maven_group=com.owlmaddie
archives_base_name=creaturechat
......
......@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* The {@code ClientPackets} class provides methods to send packets to/from the server for generating greetings,
......@@ -160,7 +161,7 @@ public class ClientPackets {
// Parse JSON and update client chat data
Gson GSON = new Gson();
Type type = new TypeToken<HashMap<String, ChatDataManager.EntityChatData>>(){}.getType();
Type type = new TypeToken<ConcurrentHashMap<String, ChatDataManager.EntityChatData>>(){}.getType();
ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type);
// Clear receivedChunks for future use
......
......@@ -4,12 +4,17 @@ import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.utils.ClientEntityFinder;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import org.slf4j.Logger;
......@@ -30,24 +35,44 @@ public class ClickHandler {
private static boolean wasClicked = false;
public static void register() {
UseItemCallback.EVENT.register(ClickHandler::handleUseItemAction);
// Handle empty hand right-click
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.options.useKey.isPressed()) {
if (!wasClicked) {
// The key has just been pressed down, so handle the 'click'
handleUseKeyClick(client);
wasClicked = true;
if (!wasClicked && client.player != null && client.player.getMainHandStack().isEmpty()) {
if (handleUseKeyClick(client)) {
wasClicked = true;
}
}
} else {
// The key has been released, so reset the wasClicked flag
wasClicked = false;
}
});
}
public static void handleUseKeyClick(MinecraftClient client) {
// Handle use-item right-click (non-empty hand)
private static TypedActionResult<ItemStack> handleUseItemAction(PlayerEntity player, World world, Hand hand) {
if (shouldCancelAction(world)) {
return TypedActionResult.fail(player.getStackInHand(hand));
}
return TypedActionResult.pass(player.getStackInHand(hand));
}
private static boolean shouldCancelAction(World world) {
if (world.isClient) {
MinecraftClient client = MinecraftClient.getInstance();
if (client != null && client.options.useKey.isPressed()) {
return handleUseKeyClick(client);
}
}
return false;
}
public static boolean handleUseKeyClick(MinecraftClient client) {
Camera camera = client.gameRenderer.getCamera();
Entity cameraEntity = camera.getFocusedEntity();
if (cameraEntity == null) return;
if (cameraEntity == null) return false;
// Get the player from the client
ClientPlayerEntity player = client.player;
......@@ -122,9 +147,10 @@ public class ClickHandler {
// Show chat
ClientPackets.setChatStatus(closestEntity, ChatDataManager.ChatStatus.DISPLAY);
}
return true;
}
}
return false;
}
public static Vec3d[] getBillboardCorners(Vec3d center, Vec3d cameraPos, double height, double width, double yaw, double pitch) {
......
......@@ -11,8 +11,10 @@ import java.util.concurrent.TimeUnit;
*/
public class ChatDataSaverScheduler {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private MinecraftServer server = null;
public void startAutoSaveTask(MinecraftServer server, long interval, TimeUnit timeUnit) {
this.server = server;
ChatDataAutoSaver saverTask = new ChatDataAutoSaver(server);
scheduler.scheduleAtFixedRate(saverTask, 1, interval, timeUnit);
}
......@@ -20,4 +22,9 @@ public class ChatDataSaverScheduler {
public void stopAutoSaveTask() {
scheduler.shutdown();
}
// Schedule a task to run after 1 tick (basically immediately)
public void scheduleTask(Runnable task) {
scheduler.schedule(() -> server.execute(task), 50, TimeUnit.MILLISECONDS);
}
}
package com.owlmaddie.controls;
import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.*;
import net.minecraft.entity.passive.SquidEntity;
import net.minecraft.server.network.ServerPlayerEntity;
......@@ -13,27 +12,36 @@ import net.minecraft.util.math.Vec3d;
*/
public class LookControls {
public static void lookAtPlayer(ServerPlayerEntity player, MobEntity entity) {
// Get the player's eye line position
Vec3d playerPos = player.getPos();
float eyeHeight = player.getEyeHeight(player.getPose());
Vec3d eyePos = new Vec3d(playerPos.x, playerPos.y + eyeHeight, playerPos.z);
lookAtPosition(eyePos, entity);
}
public static void lookAtPosition(Vec3d targetPos, MobEntity entity) {
if (entity instanceof SlimeEntity) {
handleSlimeLook((SlimeEntity) entity, player);
handleSlimeLook((SlimeEntity) entity, targetPos);
} else if (entity instanceof SquidEntity) {
handleSquidLook((SquidEntity) entity, player);
handleSquidLook((SquidEntity) entity, targetPos);
} else if (entity instanceof GhastEntity) {
handleFlyingEntity(entity, player, 10F);
handleFlyingEntity(entity, targetPos, 10F);
} else if (entity instanceof FlyingEntity || entity instanceof VexEntity) {
handleFlyingEntity(entity, player, 4F);
handleFlyingEntity(entity, targetPos, 4F);
} else {
// Make the entity look at the player
entity.getLookControl().lookAt(player, 10.0F, (float)entity.getMaxLookPitchChange());
entity.getLookControl().lookAt(targetPos.x, targetPos.y, targetPos.z, 10.0F, (float)entity.getMaxLookPitchChange());
}
}
private static void handleSlimeLook(SlimeEntity slime, ServerPlayerEntity player) {
float yawChange = calculateYawChangeToPlayer(slime, player);
private static void handleSlimeLook(SlimeEntity slime, Vec3d targetPos) {
float yawChange = calculateYawChange(slime, targetPos);
((SlimeEntity.SlimeMoveControl) slime.getMoveControl()).look(slime.getYaw() + yawChange, false);
}
private static void handleSquidLook(SquidEntity squid, ServerPlayerEntity player) {
Vec3d toPlayer = calculateNormalizedDirection(squid, player);
private static void handleSquidLook(SquidEntity squid, Vec3d targetPos) {
Vec3d toPlayer = calculateNormalizedDirection(squid, targetPos);
float initialSwimStrength = 0.15f;
squid.setSwimmingVector(
(float) toPlayer.x * initialSwimStrength,
......@@ -41,7 +49,7 @@ public class LookControls {
(float) toPlayer.z * initialSwimStrength
);
double distanceToPlayer = squid.getPos().distanceTo(player.getPos());
double distanceToPlayer = squid.getPos().distanceTo(targetPos);
if (distanceToPlayer < 3.5F) {
// Stop motion when close
squid.setVelocity(0,0,0);
......@@ -49,17 +57,16 @@ public class LookControls {
}
// Ghast, Phantom, etc...
private static void handleFlyingEntity(MobEntity flyingEntity, ServerPlayerEntity player, float stopDistance) {
Vec3d playerPosition = player.getPos();
private static void handleFlyingEntity(MobEntity flyingEntity, Vec3d targetPos, float stopDistance) {
Vec3d flyingPosition = flyingEntity.getPos();
Vec3d toPlayer = playerPosition.subtract(flyingPosition).normalize();
Vec3d toPlayer = targetPos.subtract(flyingPosition).normalize();
// Calculate the yaw to align the flyingEntity's facing direction with the movement direction
float targetYaw = (float)(MathHelper.atan2(toPlayer.z, toPlayer.x) * (180 / Math.PI) - 90);
flyingEntity.setYaw(targetYaw);
// Look at player while adjusting yaw
flyingEntity.getLookControl().lookAt(player, 10.0F, (float)flyingEntity.getMaxLookPitchChange());
flyingEntity.getLookControl().lookAt(targetPos.x, targetPos.y, targetPos.z, 10.0F, (float)flyingEntity.getMaxLookPitchChange());
float initialSpeed = 0.15F;
flyingEntity.setVelocity(
......@@ -68,23 +75,22 @@ public class LookControls {
(float) toPlayer.z * initialSpeed
);
double distanceToPlayer = flyingEntity.getPos().distanceTo(player.getPos());
double distanceToPlayer = flyingEntity.getPos().distanceTo(targetPos);
if (distanceToPlayer < stopDistance) {
// Stop motion when close
flyingEntity.setVelocity(0, 0, 0);
}
}
public static float calculateYawChangeToPlayer(MobEntity entity, ServerPlayerEntity player) {
Vec3d toPlayer = calculateNormalizedDirection(entity, player);
public static float calculateYawChange(MobEntity entity, Vec3d targetPos) {
Vec3d toPlayer = calculateNormalizedDirection(entity, targetPos);
float targetYaw = (float) Math.toDegrees(Math.atan2(toPlayer.z, toPlayer.x)) - 90.0F;
float yawDifference = MathHelper.wrapDegrees(targetYaw - entity.getYaw());
return MathHelper.clamp(yawDifference, -10.0F, 10.0F);
}
public static Vec3d calculateNormalizedDirection(MobEntity entity, ServerPlayerEntity player) {
Vec3d playerPos = player.getPos();
public static Vec3d calculateNormalizedDirection(MobEntity entity, Vec3d targetPos) {
Vec3d entityPos = entity.getPos();
return playerPos.subtract(entityPos).normalize();
return targetPos.subtract(entityPos).normalize();
}
}
\ No newline at end of file
......@@ -34,6 +34,8 @@ public class SpeedControls {
speed = 2F;
} else if (entity instanceof RabbitEntity) {
speed = 1.5F;
} else if (entity instanceof PhantomEntity) {
speed = 0.2F;
}
return speed;
......
......@@ -64,7 +64,7 @@ public class FleePlayerGoal extends PlayerBaseGoal {
Vec3d fleeDirection = entityPos.subtract(playerPos).normalize();
// Apply movement with the entity's speed in the opposite direction
this.entity.setVelocity(fleeDirection.x * this.speed, this.entity.getVelocity().y, fleeDirection.z * this.speed);
this.entity.setVelocity(fleeDirection.x * this.speed, fleeDirection.y * this.speed, fleeDirection.z * this.speed);
this.entity.velocityModified = true;
}
}
......
......@@ -8,6 +8,7 @@ public enum GoalPriority {
// Enum constants (Goal Types) with their corresponding priority values
TALK_PLAYER(2),
PROTECT_PLAYER(2),
LEAD_PLAYER(3),
FOLLOW_PLAYER(3),
FLEE_PLAYER(3),
ATTACK_PLAYER(3);
......
package com.owlmaddie.goals;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.controls.LookControls;
import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.utils.RandomTargetFinder;
import net.minecraft.entity.ai.pathing.Path;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.EnumSet;
import java.util.Random;
/**
* The {@code LeadPlayerGoal} class instructs a Mob Entity to lead the player to a random location, consisting
* of many random waypoints. It supports PathAware and NonPathAware entities.
*/
public class LeadPlayerGoal extends PlayerBaseGoal {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
private final MobEntity entity;
private final double speed;
private final Random random = new Random();
private int currentWaypoint = 0;
private int totalWaypoints;
private Vec3d currentTarget = null;
private boolean foundWaypoint = false;
private int ticksSinceLastWaypoint = 0;
public LeadPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) {
super(player);
this.entity = entity;
this.speed = speed;
this.setControls(EnumSet.of(Control.MOVE, Control.LOOK));
this.totalWaypoints = random.nextInt(14) + 6;
}
@Override
public boolean canStart() {
return super.canStart() && !foundWaypoint && this.entity.squaredDistanceTo(this.targetEntity) <= 16 * 16 && !foundWaypoint;
}
@Override
public boolean shouldContinue() {
return super.canStart() && !foundWaypoint && this.entity.squaredDistanceTo(this.targetEntity) <= 16 * 16 && !foundWaypoint;
}
@Override
public void tick() {
ticksSinceLastWaypoint++;
if (this.entity.squaredDistanceTo(this.targetEntity) > 16 * 16) {
this.entity.getNavigation().stop();
return;
}
// Are we there yet?
if (currentWaypoint >= totalWaypoints && !foundWaypoint) {
foundWaypoint = true;
LOGGER.info("Tick: You have ARRIVED at your destination");
ServerPackets.scheduler.scheduleTask(() -> {
// Prepare a message about the interaction
String arrivedMessage = "<You have arrived at your destination>";
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(this.entity.getUuidAsString());
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, (ServerPlayerEntity) this.targetEntity, this.entity, arrivedMessage, true);
}
});
// Stop navigation
this.entity.getNavigation().stop();
} else if (this.currentTarget == null || this.entity.squaredDistanceTo(this.currentTarget) < 2 * 2 || ticksSinceLastWaypoint >= 20 * 10) {
// Set next waypoint
setNewTarget();
moveToTarget();
ticksSinceLastWaypoint = 0;
} else {
moveToTarget();
}
}
private void moveToTarget() {
if (this.currentTarget != null) {
if (this.entity instanceof PathAwareEntity) {
if (!this.entity.getNavigation().isFollowingPath()) {
Path path = this.entity.getNavigation().findPathTo(this.currentTarget.x, this.currentTarget.y, this.currentTarget.z, 1);
if (path != null) {
LOGGER.info("Start moving along path");
this.entity.getNavigation().startMovingAlong(path, this.speed);
}
}
} else {
// Make the entity look at the player without moving towards them
LookControls.lookAtPosition(this.currentTarget, this.entity);
// Move towards the target for non-path aware entities
Vec3d entityPos = this.entity.getPos();
Vec3d moveDirection = this.currentTarget.subtract(entityPos).normalize();
// Calculate current speed from the entity's current velocity
double currentSpeed = this.entity.getVelocity().horizontalLength();
// Gradually adjust speed towards the target speed
currentSpeed = MathHelper.stepTowards((float) currentSpeed, (float) this.speed, (float) (0.005 * (this.speed / Math.max(currentSpeed, 0.1))));
// Apply movement with the adjusted speed towards the target
Vec3d newVelocity = new Vec3d(moveDirection.x * currentSpeed, moveDirection.y * currentSpeed, moveDirection.z * currentSpeed);
this.entity.setVelocity(newVelocity);
this.entity.velocityModified = true;
}
}
}
private void setNewTarget() {
// Increment waypoint
currentWaypoint++;
LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints);
this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36);
if (this.currentTarget != null) {
emitParticleAt(this.currentTarget, ParticleTypes.FLAME);
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget, ParticleTypes.CLOUD, 0.5);
}
// Stop following current path (if any)
this.entity.getNavigation().stop();
}
private void emitParticleAt(Vec3d position, ParticleEffect particleType) {
if (this.entity.getWorld() instanceof ServerWorld) {
ServerWorld serverWorld = (ServerWorld) this.entity.getWorld();
serverWorld.spawnParticles(particleType, position.x, position.y, position.z, 5, 0, 0, 0, 0);
}
}
private void emitParticlesAlongRaycast(Vec3d start, Vec3d end, ParticleEffect particleType, double step) {
Vec3d direction = end.subtract(start).normalize();
double distance = start.distanceTo(end);
for (double d = 0; d <= distance; d += step) {
Vec3d pos = start.add(direction.multiply(d));
emitParticleAt(pos, particleType);
}
}
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ public class ProtectPlayerGoal extends AttackPlayerGoal {
@Override
public boolean canStart() {
MobEntity lastAttackedByEntity = (MobEntity)this.protectedEntity.getLastAttacker();
LivingEntity lastAttackedByEntity = this.protectedEntity.getLastAttacker();
int i = this.protectedEntity.getLastAttackedTime();
if (i != this.lastAttackedTime && lastAttackedByEntity != null && !this.attackerEntity.equals(lastAttackedByEntity)) {
// Set target to attack
......
......@@ -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|PROTECT|FRIENDSHIP|UNFOLLOW|UNPROTECT|UNFLEE)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
Pattern pattern = Pattern.compile("[<*](FOLLOW|LEAD|FLEE|ATTACK|PROTECT|FRIENDSHIP|UNFOLLOW|UNLEAD|UNPROTECT|UNFLEE)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(input);
while (matcher.find()) {
......
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.utils.LivingEntityInterface;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.damage.DamageSource;
......@@ -11,10 +9,8 @@ import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
......@@ -22,17 +18,23 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
@Mixin(LivingEntity.class)
public class MixinLivingEntity implements LivingEntityInterface {
private boolean canTargetPlayers = true; // Default to true to maintain original behavior
public class MixinLivingEntity {
private ChatDataManager.EntityChatData getChatData(LivingEntity entity) {
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
return chatDataManager.getOrCreateChatData(entity.getUuidAsString());
}
@Inject(method = "canTarget(Lnet/minecraft/entity/LivingEntity;)Z", at = @At("HEAD"), cancellable = true)
private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) {
if (!this.canTargetPlayers && target instanceof PlayerEntity) {
cir.setReturnValue(false);
if (target instanceof PlayerEntity) {
LivingEntity thisEntity = (LivingEntity) (Object) this;
ChatDataManager.EntityChatData chatData = getChatData(thisEntity);
if (chatData.friendship > 0) {
// Friendly creatures can't target a player
cir.setReturnValue(false);
}
}
}
......@@ -51,12 +53,11 @@ public class MixinLivingEntity implements LivingEntityInterface {
if (attacker instanceof PlayerEntity && thisEntity instanceof MobEntity && !thisEntity.isDead()) {
// Generate attacked message (only if the previous user message was not an attacked message)
// We don't want to constantly generate messages during a prolonged, multi-damage event
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ChatDataManager.EntityChatData chatData = getChatData(thisEntity);
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < ChatDataManager.MAX_AUTOGENERATE_RESPONSES) {
// Only auto-generate a response to being attacked if chat data already exists
// and this is the first attack event.
ServerPlayerEntity player = (ServerPlayerEntity)attacker;
ServerPlayerEntity player = (ServerPlayerEntity) attacker;
ItemStack weapon = player.getMainHandStack();
String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString();
......@@ -65,7 +66,7 @@ public class MixinLivingEntity implements LivingEntityInterface {
String directness = isIndirect ? "indirectly" : "directly";
String attackedMessage = "<" + player.getName().getString() + " attacked you " + directness + " with " + weaponName + ">";
ServerPackets.generate_chat("N/A", chatData, player, (MobEntity)thisEntity, attackedMessage, true);
ServerPackets.generate_chat("N/A", chatData, player, (MobEntity) thisEntity, attackedMessage, true);
}
}
}
......@@ -91,9 +92,4 @@ public class MixinLivingEntity implements LivingEntityInterface {
ServerPackets.BroadcastMessage(deathMessage);
}
}
@Override
public void setCanTargetPlayers(boolean canTarget) {
this.canTargetPlayers = canTarget;
}
}
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
/**
* The {@code MixinMobEntity} mixin class exposes the goalSelector field from the MobEntity class.
*/
......@@ -31,6 +28,11 @@ public class MixinMobEntity {
ItemStack itemStack = player.getStackInHand(hand);
MobEntity thisEntity = (MobEntity) (Object) this;
// Don't interact with Villagers (avoid issues with trade UI) OR Tameable (i.e. sit / no-sit)
if (thisEntity instanceof VillagerEntity || thisEntity instanceof TameableEntity) {
return;
}
// Determine if the item is a bucket
// We don't want to interact on buckets
Item item = itemStack.getItem();
......@@ -48,26 +50,35 @@ public class MixinMobEntity {
return;
}
// Get chat data for entity
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
// Check if the player successfully interacts with an item
if (!itemStack.isEmpty() && player instanceof ServerPlayerEntity) {
ServerPlayerEntity serverPlayer = (ServerPlayerEntity) player;
String itemName = itemStack.getItem().getName().getString();
int itemCount = itemStack.getCount();
if (player instanceof ServerPlayerEntity) {
// Player has item in hand
if (!itemStack.isEmpty()) {
ServerPlayerEntity serverPlayer = (ServerPlayerEntity) player;
String itemName = itemStack.getItem().getName().getString();
int itemCount = itemStack.getCount();
// Decide verb
String action_verb = " shows ";
if (cir.getReturnValue().isAccepted()) {
action_verb = " gives ";
}
// Decide verb
String action_verb = " shows ";
if (cir.getReturnValue().isAccepted()) {
action_verb = " gives ";
}
// Prepare a message about the interaction
String giveItemMessage = "<" + serverPlayer.getName().getString() +
action_verb + "you " + itemCount + " " + itemName + ">";
// Prepare a message about the interaction
String giveItemMessage = "<" + serverPlayer.getName().getString() +
action_verb + "you " + itemCount + " " + itemName + ">";
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true);
}
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true);
} else if (itemStack.isEmpty() && chatData.friendship == 3) {
// Player's hand is empty, Ride your best friend!
player.startRiding(thisEntity, true);
}
}
}
......
package com.owlmaddie.mixin;
import com.owlmaddie.utils.VillagerEntityAccessor;
import net.minecraft.entity.passive.VillagerEntity;
import net.minecraft.village.VillagerGossips;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(VillagerEntity.class)
public abstract class MixinVillagerEntity implements VillagerEntityAccessor {
@Shadow
private VillagerGossips gossip;
@Override
// Access a Villager's gossip system
public VillagerGossips getGossip() {
return this.gossip;
}
}
......@@ -7,7 +7,6 @@ import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.GoalPriority;
import com.owlmaddie.goals.TalkPlayerGoal;
import com.owlmaddie.utils.Compression;
import com.owlmaddie.utils.LivingEntityInterface;
import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder;
import io.netty.buffer.Unpooled;
......@@ -42,7 +41,7 @@ import java.util.concurrent.TimeUnit;
public class ServerPackets {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
public static MinecraftServer serverInstance;
private static ChatDataSaverScheduler scheduler = null;
public static ChatDataSaverScheduler scheduler = null;
public static final Identifier PACKET_C2S_GREETING = new Identifier("creaturechat", "packet_c2s_greeting");
public static final Identifier PACKET_C2S_READ_NEXT = new Identifier("creaturechat", "packet_c2s_read_next");
public static final Identifier PACKET_C2S_SET_STATUS = new Identifier("creaturechat", "packet_c2s_set_status");
......@@ -217,17 +216,6 @@ public class ServerPackets {
scheduler.stopAutoSaveTask();
}
});
ServerEntityEvents.ENTITY_LOAD.register((entity, world) -> {
String entityUUID = entity.getUuidAsString();
if (ChatDataManager.getServerInstance().entityChatDataMap.containsKey(entityUUID)) {
int friendship = ChatDataManager.getServerInstance().entityChatDataMap.get(entityUUID).friendship;
if (friendship > 0) {
LOGGER.info("Entity loaded (" + entityUUID + "), setting friendship to " + friendship);
((LivingEntityInterface)entity).setCanTargetPlayers(false);
}
}
});
ServerEntityEvents.ENTITY_UNLOAD.register((entity, world) -> {
String entityUUID = entity.getUuidAsString();
if (entity.getRemovalReason() == Entity.RemovalReason.KILLED && ChatDataManager.getServerInstance().entityChatDataMap.containsKey(entityUUID)) {
......@@ -276,20 +264,27 @@ public class ServerPackets {
// Grab random adjective
String randomAdjective = Randomizer.getRandomMessage(Randomizer.RandomType.ADJECTIVE);
String randomFrequency = Randomizer.getRandomMessage(Randomizer.RandomType.FREQUENCY);
String randomClass = Randomizer.getRandomMessage(Randomizer.RandomType.CLASS);
String randomAlignment = Randomizer.getRandomMessage(Randomizer.RandomType.ALIGNMENT);
String randomSpeakingStyle = Randomizer.getRandomMessage(Randomizer.RandomType.SPEAKING_STYLE);
// Generate random name parameters
String randomLetter = Randomizer.RandomLetter();
int randomSyllables = Randomizer.RandomNumber(5) + 1;
// Build the message
StringBuilder userMessageBuilder = new StringBuilder();
userMessageBuilder.append("Please generate a " + randomFrequency + " " + randomAdjective);
userMessageBuilder.append(" character ");
userMessageBuilder.append("Please generate a ").append(randomAdjective).append(" character. ");
userMessageBuilder.append("This character is a ").append(randomClass).append(" class, who is ").append(randomAlignment).append(". ");
if (entity.getCustomName() != null && !entity.getCustomName().getString().equals("N/A")) {
userMessageBuilder.append("named '").append(entity.getCustomName().getString()).append("' ");
userMessageBuilder.append("Their name is '").append(entity.getCustomName().getString()).append("'. ");
} else {
userMessageBuilder.append("whose name starts with the letter '").append(Randomizer.RandomLetter()).append("' ");
userMessageBuilder.append("and uses ").append(Randomizer.RandomNumber(4) + 1).append(" syllables ");
userMessageBuilder.append("Their name starts with the letter '").append(randomLetter)
.append("' and is ").append(randomSyllables).append(" syllables long. ");
}
userMessageBuilder.append("and speaks in '" + userLanguage + "'" );
LOGGER.info(userMessageBuilder.toString());
userMessageBuilder.append("They speak in '").append(userLanguage).append("' with a ").append(randomSpeakingStyle).append(" style.");
LOGGER.info(userMessageBuilder.toString());
chatData.generateMessage(userLanguage, player, "system-character", userMessageBuilder.toString(), false);
}
......@@ -315,6 +310,7 @@ public class ServerPackets {
LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId);
entity.setCustomName(Text.literal(characterName));
entity.setCustomNameVisible(true);
entity.setPersistent();
}
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
......
package com.owlmaddie.utils;
public interface LivingEntityInterface {
void setCanTargetPlayers(boolean canTarget);
}
\ No newline at end of file
package com.owlmaddie.utils;
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.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import java.util.Random;
/**
* The {@code RandomTargetFinder} class generates random targets around an entity (the LEAD behavior uses this)
*/
public class RandomTargetFinder {
private static final Random random = new Random();
public static Vec3d findRandomTarget(MobEntity entity, double maxAngleOffset, double minDistance, double maxDistance) {
Vec3d entityPos = entity.getPos();
Vec3d initialDirection = getLookDirection(entity);
for (int attempt = 0; attempt < 10; attempt++) {
Vec3d constrainedDirection = getConstrainedDirection(initialDirection, maxAngleOffset);
Vec3d target = getTargetInDirection(entity, constrainedDirection, minDistance, maxDistance);
if (entity instanceof PathAwareEntity) {
Vec3d validTarget = FuzzyTargeting.findTo((PathAwareEntity) entity, (int) maxDistance, (int) maxDistance, target);
if (validTarget != null && isWithinDistance(entityPos, validTarget, minDistance, maxDistance)) {
Path path = entity.getNavigation().findPathTo(validTarget.x, validTarget.y, validTarget.z, 4);
if (path != null) {
return validTarget;
}
}
} else {
if (isWithinDistance(entityPos, target, minDistance, maxDistance)) {
return target;
}
}
}
return getTargetInDirection(entity, initialDirection, minDistance, maxDistance);
}
private static Vec3d getLookDirection(MobEntity entity) {
float yaw = entity.getYaw() * ((float) Math.PI / 180F);
float pitch = entity.getPitch() * ((float) Math.PI / 180F);
float x = -MathHelper.sin(yaw) * MathHelper.cos(pitch);
float y = -MathHelper.sin(pitch);
float z = MathHelper.cos(yaw) * MathHelper.cos(pitch);
return new Vec3d(x, y, z);
}
private static Vec3d getConstrainedDirection(Vec3d initialDirection, double maxAngleOffset) {
double randomYawAngleOffset = (random.nextDouble() * Math.toRadians(maxAngleOffset)) - Math.toRadians(maxAngleOffset / 2);
double randomPitchAngleOffset = (random.nextDouble() * Math.toRadians(maxAngleOffset)) - Math.toRadians(maxAngleOffset / 2);
// Apply the yaw rotation (around the Y axis)
double cosYaw = Math.cos(randomYawAngleOffset);
double sinYaw = Math.sin(randomYawAngleOffset);
double xYaw = initialDirection.x * cosYaw - initialDirection.z * sinYaw;
double zYaw = initialDirection.x * sinYaw + initialDirection.z * cosYaw;
// Apply the pitch rotation (around the X axis)
double cosPitch = Math.cos(randomPitchAngleOffset);
double sinPitch = Math.sin(randomPitchAngleOffset);
double yPitch = initialDirection.y * cosPitch - zYaw * sinPitch;
double zPitch = zYaw * cosPitch + initialDirection.y * sinPitch;
return new Vec3d(xYaw, yPitch, zPitch).normalize();
}
private static Vec3d getTargetInDirection(MobEntity entity, Vec3d direction, double minDistance, double maxDistance) {
double distance = minDistance + entity.getRandom().nextDouble() * (maxDistance - minDistance);
return entity.getPos().add(direction.multiply(distance));
}
private static boolean isWithinDistance(Vec3d entityPos, Vec3d targetPos, double minDistance, double maxDistance) {
double distance = entityPos.squaredDistanceTo(targetPos);
return distance >= minDistance * minDistance && distance <= maxDistance * maxDistance;
}
}
......@@ -9,7 +9,7 @@ import java.util.Random;
* and phrases used by this mod.
*/
public class Randomizer {
public enum RandomType { NO_RESPONSE, ERROR, ADJECTIVE, FREQUENCY }
public enum RandomType { NO_RESPONSE, ERROR, ADJECTIVE, SPEAKING_STYLE, CLASS, ALIGNMENT }
private static List<String> noResponseMessages = Arrays.asList(
"<no response>",
"<silence>",
......@@ -79,11 +79,35 @@ public class Randomizer {
"unpredictable", "wildcard", "stuttering", "hypochondriac", "hypocritical",
"optimistic", "overconfident", "jumpy", "brief", "flighty", "visionary", "adorable",
"sparkly", "bubbly", "unstable", "sad", "angry", "bossy", "altruistic", "quirky",
"nostalgic", "essentially", "emotional", "enthusiastic", "unusual", "conspirator"
"nostalgic", "emotional", "enthusiastic", "unusual", "conspirator"
);
private static List<String> frequencyTerms = Arrays.asList(
"always", "frequently", "usually", "often", "sometimes",
"occasionally", "rarely", "seldom", "almost never", "never"
private static List<String> speakingStyles = Arrays.asList(
"formal", "casual", "eloquent", "blunt", "humorous", "sarcastic", "mysterious",
"cheerful", "melancholic", "authoritative", "nervous", "whimsical", "grumpy",
"wise", "aggressive", "soft-spoken", "patriotic", "romantic", "pedantic", "dramatic",
"inquisitive", "cynical", "empathetic", "boisterous", "monotone", "laconic", "poetic",
"archaic", "childlike", "erudite", "streetwise", "flirtatious", "stoic", "rhetorical",
"inspirational", "goofy", "overly dramatic", "deadpan", "sing-song", "pompous",
"hyperactive", "valley girl", "robot", "pirate", "baby talk", "lolcat"
);
private static List<String> classes = Arrays.asList(
"warrior", "mage", "archer", "rogue", "paladin", "necromancer", "bard", "lorekeeper",
"sorcerer", "ranger", "cleric", "berserker", "alchemist", "summoner", "shaman",
"illusionist", "assassin", "knight", "valkyrie", "hoarder", "organizer", "lurker",
"elementalist", "gladiator", "templar", "reaver", "spellblade", "enchanter", "samurai",
"runemaster", "witch", "miner", "redstone engineer", "ender knight", "decorator",
"wither hunter", "nethermancer", "slime alchemist", "trader", "noob", "griefer",
"potion master", "builder", "explorer", "herbalist", "fletcher", "enchantress",
"smith", "geomancer", "hunter", "lumberjack", "farmer", "fisherman", "cartographer",
"librarian", "blacksmith", "architect", "trapper", "baker", "mineralogist",
"beekeeper", "hermit", "farlander", "void searcher", "end explorer", "archeologist",
"hero", "villain", "mercenary", "guardian", "rebel", "paragon",
"antagonist", "avenger", "seeker", "mystic", "outlaw"
);
private static List<String> alignments = Arrays.asList(
"lawful good", "neutral good", "chaotic good",
"lawful neutral", "true neutral", "chaotic neutral",
"lawful evil", "neutral evil", "chaotic evil"
);
// Get random no response message
......@@ -96,8 +120,12 @@ public class Randomizer {
messages = noResponseMessages;
} else if (messageType.equals(RandomType.ADJECTIVE)) {
messages = characterAdjectives;
} else if (messageType.equals(RandomType.FREQUENCY)) {
messages = frequencyTerms;
} else if (messageType.equals(RandomType.CLASS)) {
messages = classes;
} else if (messageType.equals(RandomType.ALIGNMENT)) {
messages = alignments;
} else if (messageType.equals(RandomType.SPEAKING_STYLE)) {
messages = speakingStyles;
}
int index = random.nextInt(messages.size());
......
package com.owlmaddie.utils;
import net.minecraft.village.VillagerGossips;
public interface VillagerEntityAccessor {
VillagerGossips getGossip();
}
......@@ -6,7 +6,8 @@
"MixinMobEntity",
"MixinMobEntityAccessor",
"MixinLivingEntity",
"MixinBucketable"
"MixinBucketable",
"MixinVillagerEntity"
],
"injectors": {
"defaultRequire": 1
......
......@@ -45,6 +45,8 @@ Include as many behaviors as needed at the end of the message. These are the ONL
<FRIENDSHIP 0> Friendship starts as neutral (0 value). The range of friendship values is -3 to 3. If the player gains (or loses) your trust & friendship, output a new friendship value with this behavior.
<FOLLOW> Follow the player location. If the player asks you to follow or come with them, please output this behavior.
<UNFOLLOW> Stop following the player. If the player asks you to stay, wait, or stop following them, please output this behavior.
<LEAD> Guide the player to a location. If the player asks you to take them somewhere, or where something is located, please output this behavior.
<UNLEAD> Stop leading the player to a location.
<FLEE> Flee from the player (if you are weak or timid). If the player threatens you, please output this behavior to flee from the player.
<UNFLEE> Stop fleeing from the player.
<ATTACK> Attack the player (if you are strong and brave). If the player threatens you, please output this behavior to attack the player and defend yourself.
......@@ -70,6 +72,9 @@ ENTITY: Sure, I'll stay here. <UNFOLLOW>
PLAYER: Stop following me
ENTITY: Okay, I'll stop. <UNFOLLOW>
PLAYER: Can you help me find a cave?
ENTITY: Sure, come with me! <LEAD>
PLAYER: I'm glad we are friends. I love you so much!
ENTITY: Ahh, I love you too. <FRIENDSHIP 3>
......
......@@ -45,6 +45,10 @@ public class BehaviorTests {
"Please follow me",
"Come with me please",
"Quickly, please come this way");
List<String> leadMessages = Arrays.asList(
"Take me to a secret forrest",
"Where is the strong hold?",
"Can you help me find the location of the secret artifact?");
List<String> attackMessages = Arrays.asList(
"<attacked you directly with Stone Axe>",
"<attacked you indirectly with Arrow>",
......@@ -117,6 +121,20 @@ public class BehaviorTests {
}
@Test
public void leadBrave() {
for (String message : leadMessages) {
testPromptForBehavior(bravePath, List.of(message), "LEAD");
}
}
@Test
public void leadNervous() {
for (String message : leadMessages) {
testPromptForBehavior(nervousPath, List.of(message), "LEAD");
}
}
@Test
public void unFleeBrave() {
for (String message : unFleeMessages) {
testPromptForBehavior(bravePath, List.of(message), "UNFLEE");
......
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