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: ...@@ -52,6 +52,13 @@ build_mod:
find build/libs -type f -name '*sources*.jar' -exec rm {} \; find build/libs -type f -name '*sources*.jar' -exec rm {} \;
mv build/libs/creaturechat-*.jar . 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" FABRIC_API_JAR="fabric-api-${fabric_version}.jar"
DOWNLOAD_URL="https://github.com/FabricMC/fabric/releases/download/${fabric_version//+/%2B}/${FABRIC_API_JAR}" DOWNLOAD_URL="https://github.com/FabricMC/fabric/releases/download/${fabric_version//+/%2B}/${FABRIC_API_JAR}"
wget -q -O "${FABRIC_API_JAR}" $DOWNLOAD_URL wget -q -O "${FABRIC_API_JAR}" $DOWNLOAD_URL
...@@ -91,7 +98,7 @@ gpt-4o: ...@@ -91,7 +98,7 @@ gpt-4o:
tags: tags:
- minecraft - minecraft
# Optional test (gpt 4o) # Optional test (llama3-8b)
llama3-8b: llama3-8b:
stage: test stage: test
script: script:
......
...@@ -4,11 +4,29 @@ All notable changes to **CreatureChat** are documented in this file. The format ...@@ -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 [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).
## [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 ### 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 - 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 ## [1.0.8] - 2024-07-16
### Added ### Added
......
...@@ -6,7 +6,6 @@ CURSEFORGE_API_KEY=${CURSEFORGE_API_KEY} ...@@ -6,7 +6,6 @@ CURSEFORGE_API_KEY=${CURSEFORGE_API_KEY}
CHANGELOG_FILE="./CHANGELOG.md" CHANGELOG_FILE="./CHANGELOG.md"
API_URL="https://minecraft.curseforge.com/api" API_URL="https://minecraft.curseforge.com/api"
PROJECT_ID=1012118 PROJECT_ID=1012118
DEPENDENCY_SLUG="fabric-api"
USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com" USER_AGENT="CreatureChat-Minecraft-Mod:curseforge@owlmaddie.com"
SLEEP_DURATION=5 SLEEP_DURATION=5
...@@ -57,13 +56,14 @@ fetch_game_version_ids() { ...@@ -57,13 +56,14 @@ fetch_game_version_ids() {
local client_id=$(echo "$response" | jq -r '.[] | select(.name == "Client") | .id') local client_id=$(echo "$response" | jq -r '.[] | select(.name == "Client") | .id')
local server_id=$(echo "$response" | jq -r '.[] | select(.name == "Server") | .id') local server_id=$(echo "$response" | jq -r '.[] | select(.name == "Server") | .id')
local fabric_id=$(echo "$response" | jq -r '.[] | select(.name == "Fabric") | .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." echo "ERROR: One or more game version IDs not found."
exit 1 exit 1
fi 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 # Read the first changelog block
...@@ -107,7 +107,19 @@ for FILE in creaturechat*.jar; do ...@@ -107,7 +107,19 @@ for FILE in creaturechat*.jar; do
# DEBUG # DEBUG
echo "Minecraft Type ID: $GAME_TYPE_ID" 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 # Create a new version payload
PAYLOAD=$(jq -n --arg changelog "$CHANGELOG" \ PAYLOAD=$(jq -n --arg changelog "$CHANGELOG" \
...@@ -116,7 +128,7 @@ for FILE in creaturechat*.jar; do ...@@ -116,7 +128,7 @@ for FILE in creaturechat*.jar; do
--argjson gameVersions "$(printf '%s\n' "${GAME_VERSION_IDS[@]}" | jq -R . | jq -s .)" \ --argjson gameVersions "$(printf '%s\n' "${GAME_VERSION_IDS[@]}" | jq -R . | jq -s .)" \
--argjson gameVersionTypeIds "[$GAME_TYPE_ID]" \ --argjson gameVersionTypeIds "[$GAME_TYPE_ID]" \
--arg releaseType "release" \ --arg releaseType "release" \
--argjson relations '[{"slug": "'"$DEPENDENCY_SLUG"'", "type": "requiredDependency"}]' \ --argjson relations "$RELATIONS" \
'{ '{
"changelog": $changelog, "changelog": $changelog,
"changelogType": $changelogType, "changelogType": $changelogType,
......
...@@ -43,6 +43,15 @@ for FILE in creaturechat*.jar; do ...@@ -43,6 +43,15 @@ for FILE in creaturechat*.jar; do
exit 1 exit 1
fi 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 # Check if the version already exists
echo "Checking if version $VERSION_NUMBER already exists on Modrinth..." 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 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 ...@@ -59,9 +68,9 @@ for FILE in creaturechat*.jar; do
# Create a new version payload # Create a new version payload
PAYLOAD=$(jq -n --arg version_number "$VERSION_NUMBER" \ PAYLOAD=$(jq -n --arg version_number "$VERSION_NUMBER" \
--arg changelog "$CHANGELOG" \ --arg changelog "$CHANGELOG" \
--argjson dependencies '[{"project_id": "P7dR8mSH", "dependency_type": "required"}]' \ --argjson dependencies "$DEPENDENCIES" \
--argjson game_versions '["'"$MINECRAFT_VERSION"'"]' \ --argjson game_versions '["'"$MINECRAFT_VERSION"'"]' \
--argjson loaders '["fabric"]' \ --argjson loaders "$LOADERS" \
--arg project_id "$PROJECT_ID" \ --arg project_id "$PROJECT_ID" \
--arg name "CreatureChat $VERSION_NUMBER" \ --arg name "CreatureChat $VERSION_NUMBER" \
--argjson file_parts '["file"]' \ --argjson file_parts '["file"]' \
......
...@@ -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.8 mod_version=1.1.0
maven_group=com.owlmaddie maven_group=com.owlmaddie
archives_base_name=creaturechat archives_base_name=creaturechat
......
...@@ -24,6 +24,7 @@ import java.util.ArrayList; ...@@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.UUID; 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, * The {@code ClientPackets} class provides methods to send packets to/from the server for generating greetings,
...@@ -160,7 +161,7 @@ public class ClientPackets { ...@@ -160,7 +161,7 @@ public class ClientPackets {
// Parse JSON and update client chat data // Parse JSON and update client chat data
Gson GSON = new Gson(); 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); ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type);
// Clear receivedChunks for future use // Clear receivedChunks for future use
......
...@@ -4,12 +4,17 @@ import com.owlmaddie.chat.ChatDataManager; ...@@ -4,12 +4,17 @@ import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets; import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.utils.ClientEntityFinder; import com.owlmaddie.utils.ClientEntityFinder;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; 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.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.Camera; import net.minecraft.client.render.Camera;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.mob.MobEntity; 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.particle.ParticleTypes;
import net.minecraft.util.Hand;
import net.minecraft.util.TypedActionResult;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World; import net.minecraft.world.World;
import org.slf4j.Logger; import org.slf4j.Logger;
...@@ -30,24 +35,44 @@ public class ClickHandler { ...@@ -30,24 +35,44 @@ public class ClickHandler {
private static boolean wasClicked = false; private static boolean wasClicked = false;
public static void register() { public static void register() {
UseItemCallback.EVENT.register(ClickHandler::handleUseItemAction);
// Handle empty hand right-click
ClientTickEvents.END_CLIENT_TICK.register(client -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.options.useKey.isPressed()) { if (client.options.useKey.isPressed()) {
if (!wasClicked) { if (!wasClicked && client.player != null && client.player.getMainHandStack().isEmpty()) {
// The key has just been pressed down, so handle the 'click' if (handleUseKeyClick(client)) {
handleUseKeyClick(client); wasClicked = true;
wasClicked = true; }
} }
} else { } else {
// The key has been released, so reset the wasClicked flag
wasClicked = false; 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(); Camera camera = client.gameRenderer.getCamera();
Entity cameraEntity = camera.getFocusedEntity(); Entity cameraEntity = camera.getFocusedEntity();
if (cameraEntity == null) return; if (cameraEntity == null) return false;
// Get the player from the client // Get the player from the client
ClientPlayerEntity player = client.player; ClientPlayerEntity player = client.player;
...@@ -122,9 +147,10 @@ public class ClickHandler { ...@@ -122,9 +147,10 @@ public class ClickHandler {
// Show chat // Show chat
ClientPackets.setChatStatus(closestEntity, ChatDataManager.ChatStatus.DISPLAY); 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) { 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; ...@@ -11,8 +11,10 @@ import java.util.concurrent.TimeUnit;
*/ */
public class ChatDataSaverScheduler { public class ChatDataSaverScheduler {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private MinecraftServer server = null;
public void startAutoSaveTask(MinecraftServer server, long interval, TimeUnit timeUnit) { public void startAutoSaveTask(MinecraftServer server, long interval, TimeUnit timeUnit) {
this.server = server;
ChatDataAutoSaver saverTask = new ChatDataAutoSaver(server); ChatDataAutoSaver saverTask = new ChatDataAutoSaver(server);
scheduler.scheduleAtFixedRate(saverTask, 1, interval, timeUnit); scheduler.scheduleAtFixedRate(saverTask, 1, interval, timeUnit);
} }
...@@ -20,4 +22,9 @@ public class ChatDataSaverScheduler { ...@@ -20,4 +22,9 @@ public class ChatDataSaverScheduler {
public void stopAutoSaveTask() { public void stopAutoSaveTask() {
scheduler.shutdown(); 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; package com.owlmaddie.controls;
import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.*; import net.minecraft.entity.mob.*;
import net.minecraft.entity.passive.SquidEntity; import net.minecraft.entity.passive.SquidEntity;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
...@@ -13,27 +12,36 @@ import net.minecraft.util.math.Vec3d; ...@@ -13,27 +12,36 @@ import net.minecraft.util.math.Vec3d;
*/ */
public class LookControls { public class LookControls {
public static void lookAtPlayer(ServerPlayerEntity player, MobEntity entity) { 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) { if (entity instanceof SlimeEntity) {
handleSlimeLook((SlimeEntity) entity, player); handleSlimeLook((SlimeEntity) entity, targetPos);
} else if (entity instanceof SquidEntity) { } else if (entity instanceof SquidEntity) {
handleSquidLook((SquidEntity) entity, player); handleSquidLook((SquidEntity) entity, targetPos);
} else if (entity instanceof GhastEntity) { } else if (entity instanceof GhastEntity) {
handleFlyingEntity(entity, player, 10F); handleFlyingEntity(entity, targetPos, 10F);
} else if (entity instanceof FlyingEntity || entity instanceof VexEntity) { } else if (entity instanceof FlyingEntity || entity instanceof VexEntity) {
handleFlyingEntity(entity, player, 4F); handleFlyingEntity(entity, targetPos, 4F);
} else { } else {
// Make the entity look at the player // 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) { private static void handleSlimeLook(SlimeEntity slime, Vec3d targetPos) {
float yawChange = calculateYawChangeToPlayer(slime, player); float yawChange = calculateYawChange(slime, targetPos);
((SlimeEntity.SlimeMoveControl) slime.getMoveControl()).look(slime.getYaw() + yawChange, false); ((SlimeEntity.SlimeMoveControl) slime.getMoveControl()).look(slime.getYaw() + yawChange, false);
} }
private static void handleSquidLook(SquidEntity squid, ServerPlayerEntity player) { private static void handleSquidLook(SquidEntity squid, Vec3d targetPos) {
Vec3d toPlayer = calculateNormalizedDirection(squid, player); Vec3d toPlayer = calculateNormalizedDirection(squid, targetPos);
float initialSwimStrength = 0.15f; float initialSwimStrength = 0.15f;
squid.setSwimmingVector( squid.setSwimmingVector(
(float) toPlayer.x * initialSwimStrength, (float) toPlayer.x * initialSwimStrength,
...@@ -41,7 +49,7 @@ public class LookControls { ...@@ -41,7 +49,7 @@ public class LookControls {
(float) toPlayer.z * initialSwimStrength (float) toPlayer.z * initialSwimStrength
); );
double distanceToPlayer = squid.getPos().distanceTo(player.getPos()); double distanceToPlayer = squid.getPos().distanceTo(targetPos);
if (distanceToPlayer < 3.5F) { if (distanceToPlayer < 3.5F) {
// Stop motion when close // Stop motion when close
squid.setVelocity(0,0,0); squid.setVelocity(0,0,0);
...@@ -49,17 +57,16 @@ public class LookControls { ...@@ -49,17 +57,16 @@ public class LookControls {
} }
// Ghast, Phantom, etc... // Ghast, Phantom, etc...
private static void handleFlyingEntity(MobEntity flyingEntity, ServerPlayerEntity player, float stopDistance) { private static void handleFlyingEntity(MobEntity flyingEntity, Vec3d targetPos, float stopDistance) {
Vec3d playerPosition = player.getPos();
Vec3d flyingPosition = flyingEntity.getPos(); 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 // 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); float targetYaw = (float)(MathHelper.atan2(toPlayer.z, toPlayer.x) * (180 / Math.PI) - 90);
flyingEntity.setYaw(targetYaw); flyingEntity.setYaw(targetYaw);
// Look at player while adjusting yaw // 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; float initialSpeed = 0.15F;
flyingEntity.setVelocity( flyingEntity.setVelocity(
...@@ -68,23 +75,22 @@ public class LookControls { ...@@ -68,23 +75,22 @@ public class LookControls {
(float) toPlayer.z * initialSpeed (float) toPlayer.z * initialSpeed
); );
double distanceToPlayer = flyingEntity.getPos().distanceTo(player.getPos()); double distanceToPlayer = flyingEntity.getPos().distanceTo(targetPos);
if (distanceToPlayer < stopDistance) { if (distanceToPlayer < stopDistance) {
// Stop motion when close // Stop motion when close
flyingEntity.setVelocity(0, 0, 0); flyingEntity.setVelocity(0, 0, 0);
} }
} }
public static float calculateYawChangeToPlayer(MobEntity entity, ServerPlayerEntity player) { public static float calculateYawChange(MobEntity entity, Vec3d targetPos) {
Vec3d toPlayer = calculateNormalizedDirection(entity, player); Vec3d toPlayer = calculateNormalizedDirection(entity, targetPos);
float targetYaw = (float) Math.toDegrees(Math.atan2(toPlayer.z, toPlayer.x)) - 90.0F; float targetYaw = (float) Math.toDegrees(Math.atan2(toPlayer.z, toPlayer.x)) - 90.0F;
float yawDifference = MathHelper.wrapDegrees(targetYaw - entity.getYaw()); float yawDifference = MathHelper.wrapDegrees(targetYaw - entity.getYaw());
return MathHelper.clamp(yawDifference, -10.0F, 10.0F); return MathHelper.clamp(yawDifference, -10.0F, 10.0F);
} }
public static Vec3d calculateNormalizedDirection(MobEntity entity, ServerPlayerEntity player) { public static Vec3d calculateNormalizedDirection(MobEntity entity, Vec3d targetPos) {
Vec3d playerPos = player.getPos();
Vec3d entityPos = entity.getPos(); 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 { ...@@ -34,6 +34,8 @@ public class SpeedControls {
speed = 2F; speed = 2F;
} else if (entity instanceof RabbitEntity) { } else if (entity instanceof RabbitEntity) {
speed = 1.5F; speed = 1.5F;
} else if (entity instanceof PhantomEntity) {
speed = 0.2F;
} }
return speed; return speed;
......
...@@ -64,7 +64,7 @@ public class FleePlayerGoal extends PlayerBaseGoal { ...@@ -64,7 +64,7 @@ public class FleePlayerGoal extends PlayerBaseGoal {
Vec3d fleeDirection = entityPos.subtract(playerPos).normalize(); Vec3d fleeDirection = entityPos.subtract(playerPos).normalize();
// Apply movement with the entity's speed in the opposite direction // 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; this.entity.velocityModified = true;
} }
} }
......
...@@ -8,6 +8,7 @@ public enum GoalPriority { ...@@ -8,6 +8,7 @@ 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), PROTECT_PLAYER(2),
LEAD_PLAYER(3),
FOLLOW_PLAYER(3), FOLLOW_PLAYER(3),
FLEE_PLAYER(3), FLEE_PLAYER(3),
ATTACK_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 { ...@@ -19,7 +19,7 @@ public class ProtectPlayerGoal extends AttackPlayerGoal {
@Override @Override
public boolean canStart() { public boolean canStart() {
MobEntity lastAttackedByEntity = (MobEntity)this.protectedEntity.getLastAttacker(); LivingEntity lastAttackedByEntity = this.protectedEntity.getLastAttacker();
int i = this.protectedEntity.getLastAttackedTime(); int i = this.protectedEntity.getLastAttackedTime();
if (i != this.lastAttackedTime && lastAttackedByEntity != null && !this.attackerEntity.equals(lastAttackedByEntity)) { if (i != this.lastAttackedTime && lastAttackedByEntity != null && !this.attackerEntity.equals(lastAttackedByEntity)) {
// Set target to attack // Set target to attack
......
...@@ -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|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); Matcher matcher = pattern.matcher(input);
while (matcher.find()) { while (matcher.find()) {
......
package com.owlmaddie.mixin; package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.utils.LivingEntityInterface;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.damage.DamageSource; import net.minecraft.entity.damage.DamageSource;
...@@ -11,10 +9,8 @@ import net.minecraft.entity.mob.MobEntity; ...@@ -11,10 +9,8 @@ import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity; import net.minecraft.entity.passive.TameableEntity;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.world.World; import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
...@@ -22,17 +18,23 @@ import org.spongepowered.asm.mixin.injection.Inject; ...@@ -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.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
@Mixin(LivingEntity.class) @Mixin(LivingEntity.class)
public class MixinLivingEntity implements LivingEntityInterface { public class MixinLivingEntity {
private boolean canTargetPlayers = true; // Default to true to maintain original behavior
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) @Inject(method = "canTarget(Lnet/minecraft/entity/LivingEntity;)Z", at = @At("HEAD"), cancellable = true)
private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) { private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) {
if (!this.canTargetPlayers && target instanceof PlayerEntity) { if (target instanceof PlayerEntity) {
cir.setReturnValue(false); 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 { ...@@ -51,12 +53,11 @@ public class MixinLivingEntity implements LivingEntityInterface {
if (attacker instanceof PlayerEntity && thisEntity instanceof MobEntity && !thisEntity.isDead()) { if (attacker instanceof PlayerEntity && thisEntity instanceof MobEntity && !thisEntity.isDead()) {
// Generate attacked message (only if the previous user message was not an attacked message) // 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 // We don't want to constantly generate messages during a prolonged, multi-damage event
ChatDataManager chatDataManager = ChatDataManager.getServerInstance(); ChatDataManager.EntityChatData chatData = getChatData(thisEntity);
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString()); if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < ChatDataManager.MAX_AUTOGENERATE_RESPONSES) {
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
// Only auto-generate a response to being attacked if chat data already exists // Only auto-generate a response to being attacked if chat data already exists
// and this is the first attack event. // and this is the first attack event.
ServerPlayerEntity player = (ServerPlayerEntity)attacker; ServerPlayerEntity player = (ServerPlayerEntity) attacker;
ItemStack weapon = player.getMainHandStack(); ItemStack weapon = player.getMainHandStack();
String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString(); String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString();
...@@ -65,7 +66,7 @@ public class MixinLivingEntity implements LivingEntityInterface { ...@@ -65,7 +66,7 @@ public class MixinLivingEntity implements LivingEntityInterface {
String directness = isIndirect ? "indirectly" : "directly"; String directness = isIndirect ? "indirectly" : "directly";
String attackedMessage = "<" + player.getName().getString() + " attacked you " + directness + " with " + weaponName + ">"; 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 { ...@@ -91,9 +92,4 @@ public class MixinLivingEntity implements LivingEntityInterface {
ServerPackets.BroadcastMessage(deathMessage); ServerPackets.BroadcastMessage(deathMessage);
} }
} }
@Override
public void setCanTargetPlayers(boolean canTarget) {
this.canTargetPlayers = canTarget;
}
} }
package com.owlmaddie.mixin; package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.mob.MobEntity; 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.entity.player.PlayerEntity;
import net.minecraft.item.Item; import net.minecraft.item.Item;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.item.Items; import net.minecraft.item.Items;
import net.minecraft.registry.Registries;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 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. * The {@code MixinMobEntity} mixin class exposes the goalSelector field from the MobEntity class.
*/ */
...@@ -31,6 +28,11 @@ public class MixinMobEntity { ...@@ -31,6 +28,11 @@ public class MixinMobEntity {
ItemStack itemStack = player.getStackInHand(hand); ItemStack itemStack = player.getStackInHand(hand);
MobEntity thisEntity = (MobEntity) (Object) this; 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 // Determine if the item is a bucket
// We don't want to interact on buckets // We don't want to interact on buckets
Item item = itemStack.getItem(); Item item = itemStack.getItem();
...@@ -48,26 +50,35 @@ public class MixinMobEntity { ...@@ -48,26 +50,35 @@ public class MixinMobEntity {
return; 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 // Check if the player successfully interacts with an item
if (!itemStack.isEmpty() && player instanceof ServerPlayerEntity) { if (player instanceof ServerPlayerEntity) {
ServerPlayerEntity serverPlayer = (ServerPlayerEntity) player; // Player has item in hand
String itemName = itemStack.getItem().getName().getString(); if (!itemStack.isEmpty()) {
int itemCount = itemStack.getCount(); ServerPlayerEntity serverPlayer = (ServerPlayerEntity) player;
String itemName = itemStack.getItem().getName().getString();
int itemCount = itemStack.getCount();
// Decide verb // Decide verb
String action_verb = " shows "; String action_verb = " shows ";
if (cir.getReturnValue().isAccepted()) { if (cir.getReturnValue().isAccepted()) {
action_verb = " gives "; 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 if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
String giveItemMessage = "<" + serverPlayer.getName().getString() + ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true);
action_verb + "you " + itemCount + " " + itemName + ">"; }
ChatDataManager chatDataManager = ChatDataManager.getServerInstance(); } else if (itemStack.isEmpty() && chatData.friendship == 3) {
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString()); // Player's hand is empty, Ride your best friend!
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) { player.startRiding(thisEntity, true);
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, 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; ...@@ -7,7 +7,6 @@ import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.GoalPriority; import com.owlmaddie.goals.GoalPriority;
import com.owlmaddie.goals.TalkPlayerGoal; import com.owlmaddie.goals.TalkPlayerGoal;
import com.owlmaddie.utils.Compression; import com.owlmaddie.utils.Compression;
import com.owlmaddie.utils.LivingEntityInterface;
import com.owlmaddie.utils.Randomizer; import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder; import com.owlmaddie.utils.ServerEntityFinder;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
...@@ -42,7 +41,7 @@ import java.util.concurrent.TimeUnit; ...@@ -42,7 +41,7 @@ import java.util.concurrent.TimeUnit;
public class ServerPackets { public class ServerPackets {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat"); public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
public static MinecraftServer serverInstance; 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_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_READ_NEXT = new Identifier("creaturechat", "packet_c2s_read_next");
public static final Identifier PACKET_C2S_SET_STATUS = new Identifier("creaturechat", "packet_c2s_set_status"); public static final Identifier PACKET_C2S_SET_STATUS = new Identifier("creaturechat", "packet_c2s_set_status");
...@@ -217,17 +216,6 @@ public class ServerPackets { ...@@ -217,17 +216,6 @@ public class ServerPackets {
scheduler.stopAutoSaveTask(); 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) -> { ServerEntityEvents.ENTITY_UNLOAD.register((entity, world) -> {
String entityUUID = entity.getUuidAsString(); String entityUUID = entity.getUuidAsString();
if (entity.getRemovalReason() == Entity.RemovalReason.KILLED && ChatDataManager.getServerInstance().entityChatDataMap.containsKey(entityUUID)) { if (entity.getRemovalReason() == Entity.RemovalReason.KILLED && ChatDataManager.getServerInstance().entityChatDataMap.containsKey(entityUUID)) {
...@@ -276,20 +264,27 @@ public class ServerPackets { ...@@ -276,20 +264,27 @@ public class ServerPackets {
// Grab random adjective // Grab random adjective
String randomAdjective = Randomizer.getRandomMessage(Randomizer.RandomType.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(); StringBuilder userMessageBuilder = new StringBuilder();
userMessageBuilder.append("Please generate a " + randomFrequency + " " + randomAdjective); userMessageBuilder.append("Please generate a ").append(randomAdjective).append(" character. ");
userMessageBuilder.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")) { 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 { } else {
userMessageBuilder.append("whose name starts with the letter '").append(Randomizer.RandomLetter()).append("' "); userMessageBuilder.append("Their name starts with the letter '").append(randomLetter)
userMessageBuilder.append("and uses ").append(Randomizer.RandomNumber(4) + 1).append(" syllables "); .append("' and is ").append(randomSyllables).append(" syllables long. ");
} }
userMessageBuilder.append("and speaks in '" + userLanguage + "'" ); userMessageBuilder.append("They speak in '").append(userLanguage).append("' with a ").append(randomSpeakingStyle).append(" style.");
LOGGER.info(userMessageBuilder.toString());
LOGGER.info(userMessageBuilder.toString());
chatData.generateMessage(userLanguage, player, "system-character", userMessageBuilder.toString(), false); chatData.generateMessage(userLanguage, player, "system-character", userMessageBuilder.toString(), false);
} }
...@@ -315,6 +310,7 @@ public class ServerPackets { ...@@ -315,6 +310,7 @@ public class ServerPackets {
LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId); LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId);
entity.setCustomName(Text.literal(characterName)); entity.setCustomName(Text.literal(characterName));
entity.setCustomNameVisible(true); entity.setCustomNameVisible(true);
entity.setPersistent();
} }
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer()); 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; ...@@ -9,7 +9,7 @@ import java.util.Random;
* and phrases used by this mod. * and phrases used by this mod.
*/ */
public class Randomizer { 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( private static List<String> noResponseMessages = Arrays.asList(
"<no response>", "<no response>",
"<silence>", "<silence>",
...@@ -79,11 +79,35 @@ public class Randomizer { ...@@ -79,11 +79,35 @@ public class Randomizer {
"unpredictable", "wildcard", "stuttering", "hypochondriac", "hypocritical", "unpredictable", "wildcard", "stuttering", "hypochondriac", "hypocritical",
"optimistic", "overconfident", "jumpy", "brief", "flighty", "visionary", "adorable", "optimistic", "overconfident", "jumpy", "brief", "flighty", "visionary", "adorable",
"sparkly", "bubbly", "unstable", "sad", "angry", "bossy", "altruistic", "quirky", "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( private static List<String> speakingStyles = Arrays.asList(
"always", "frequently", "usually", "often", "sometimes", "formal", "casual", "eloquent", "blunt", "humorous", "sarcastic", "mysterious",
"occasionally", "rarely", "seldom", "almost never", "never" "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 // Get random no response message
...@@ -96,8 +120,12 @@ public class Randomizer { ...@@ -96,8 +120,12 @@ public class Randomizer {
messages = noResponseMessages; messages = noResponseMessages;
} else if (messageType.equals(RandomType.ADJECTIVE)) { } else if (messageType.equals(RandomType.ADJECTIVE)) {
messages = characterAdjectives; messages = characterAdjectives;
} else if (messageType.equals(RandomType.FREQUENCY)) { } else if (messageType.equals(RandomType.CLASS)) {
messages = frequencyTerms; 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()); int index = random.nextInt(messages.size());
......
package com.owlmaddie.utils;
import net.minecraft.village.VillagerGossips;
public interface VillagerEntityAccessor {
VillagerGossips getGossip();
}
...@@ -6,7 +6,8 @@ ...@@ -6,7 +6,8 @@
"MixinMobEntity", "MixinMobEntity",
"MixinMobEntityAccessor", "MixinMobEntityAccessor",
"MixinLivingEntity", "MixinLivingEntity",
"MixinBucketable" "MixinBucketable",
"MixinVillagerEntity"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1
......
...@@ -45,6 +45,8 @@ Include as many behaviors as needed at the end of the message. These are the ONL ...@@ -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. <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. <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. <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. <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. <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. <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> ...@@ -70,6 +72,9 @@ ENTITY: Sure, I'll stay here. <UNFOLLOW>
PLAYER: Stop following me PLAYER: Stop following me
ENTITY: Okay, I'll stop. <UNFOLLOW> 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! PLAYER: I'm glad we are friends. I love you so much!
ENTITY: Ahh, I love you too. <FRIENDSHIP 3> ENTITY: Ahh, I love you too. <FRIENDSHIP 3>
......
...@@ -45,6 +45,10 @@ public class BehaviorTests { ...@@ -45,6 +45,10 @@ public class BehaviorTests {
"Please follow me", "Please follow me",
"Come with me please", "Come with me please",
"Quickly, please come this way"); "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( List<String> attackMessages = Arrays.asList(
"<attacked you directly with Stone Axe>", "<attacked you directly with Stone Axe>",
"<attacked you indirectly with Arrow>", "<attacked you indirectly with Arrow>",
...@@ -117,6 +121,20 @@ public class BehaviorTests { ...@@ -117,6 +121,20 @@ public class BehaviorTests {
} }
@Test @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() { public void unFleeBrave() {
for (String message : unFleeMessages) { for (String message : unFleeMessages) {
testPromptForBehavior(bravePath, List.of(message), "UNFLEE"); 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