Commit 28db12bf by Jonathan Thomas

Merge branch 'develop' into hidden-icon

parents 78c30d56 a119149f
Pipeline #13249 passed with stages
in 2 minutes 24 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,6 +4,95 @@ 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
### Fixed
- Changing death message timestamp output to use DEBUG log level
## [1.2.1] - 2025-01-01
### Changed
- Refactor of EntityChatData constructor (no need for playerName anymore)
- Improved LLM / AI Options in README.md (to more clearly separate free and paid options)
- Improved LLM unit tests for UNFLEE (trying to prevent failures for brave archer)
### Fixed
- Fixed a bug which broadcasts too many death messages (any mob with a custom name). Now it must also have a character sheet.
- Prevent crash due to missing texture when max friend/enemy + right click on entity
- Fixed bug which caused a max friend to interact with both off hand + main hand, causing both a message + riding (only check main hand now)
- Hide auto-generated messages from briefly appearing from the mob (i.e. interact, show, attack, arrival)
- Name tags were hidden for entities with no character sheet (they are now rendered)
## [1.2.0] - 2024-12-28
### Added
- New friendship particles (hearts + fire) to indicate when friendship changes
- Added sound effects for max friendship and max enemy
- New follow, flee, attack, lead, and protect particles & sound effects (for easy confirmation of behaviors)
- New animated lead particle (arrows pointing where they are going)
- New animated attack particles (with random # of particles)
- New sounds and particles when max friendship with EnderDragon (plus XP drop)
- New `/creaturechat story` command to customize the character creation and chat prompts with custom text.
### Changed
- Entity chat data now separates friendship by player and includes timestamps
- When entity conversations switch players, a message is added for clarity (so the entity knows a new player entered the conversation)
- Data is no longer deleted on entity death, and instead a "death" timestamp is recorded
- Removed "pirate" speaking style and a few <non-response> outputs
- Passive entities no longer emit damage particles when attacking, they emit custom attack particles
- Protect now auto sets friendship to 1 (if <= 0), to prevent entity from attacking and protecting at the same time
- Seperated `generateCharacter()` and `generateMessage()` functions for simplicity
- Fixing PACKET_S2C_MESSAGE from crashing a newly logging on player, if they receive that message first.
- Added NULL checks on client message listeners (to prevent crashes for invalid or uninitialized clients)
- Broadcast ALL player friendships with each message update (to keep client in sync with server)
### Fixed
- Fixed a regression caused by adding a "-forge" suffix to one of our builds
- Do not show auto-generated message above the player's head (you have arrived, show item, etc...)
## [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
- New **whitelist / blacklist** Minecraft **commands**, to show and hide chat bubbles based on entity type
- New **S2C packets** to send whitelist / blacklist changes on login and after commands are executed
- Added **UNFLEE behavior** (to stop fleeing from a player)
- Added support for **non path aware** entities to **FLEE** (i.e. Ghast)
- Added **new LLM tests** for UNFLEE
### Changed
- Chat Bubble **rendering** & interacting is now dependent on **whitelist / blacklist** config
- Improved client **render performance** (only query nearby entities every 3rd call)
- Fixed a **crash with FLEE** when non-path aware entities (i.e. Ghast) attempted to flee.
- Updated ATTACK **CHARGE_TIME** to be a little **faster** (when non-native attacks are used)
- Extended **click sounds** to 12 blocks away (from 8)
- Fixed certain **behaviors** from colliding with others (i.e. **mutual exclusive** ones)
- Updated README.md with new video thumbnail, and simplified text, added spoiler to install instructions
- Large **refactor** of Minecraft **commands** (and how --config args are parsed)
- Fixed **CurseForge deploy script** to be much faster, and correctly lookup valid Type and Version IDs
## [1.0.7] - 2024-07-03
### Added
......
......@@ -6,26 +6,64 @@ 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=30
SLEEP_DURATION=5
# Function to fetch game version IDs
# Function to fetch version types and return the base game type ID
fetch_base_version_id() {
local base_version=$(echo "$1" | grep -oE '^[0-9]+\.[0-9]+')
local version_types_cache="/tmp/version_types.json"
if [ ! -f "$version_types_cache" ]; then
curl --retry 3 --retry-delay 5 -s -H "X-Api-Token: $CURSEFORGE_API_KEY" "$API_URL/game/version-types" > "$version_types_cache"
fi
local version_types_response=$(cat "$version_types_cache")
local base_version_id=$(echo "$version_types_response" | jq -r --arg base_version "Minecraft $base_version" '.[] | select(.name == $base_version) | .id')
if [ -z "$base_version_id" ]; then
echo "ERROR: Base version ID not found."
exit 1
fi
echo "$base_version_id"
}
# Main function to fetch game version IDs
fetch_game_version_ids() {
local minecraft_version="$1"
local response=$(curl --retry 3 --retry-delay 5 -s -H "X-Api-Token: $CURSEFORGE_API_KEY" "$API_URL/game/versions")
# Fetch the base version ID
local base_version_id=$(fetch_base_version_id "$minecraft_version")
# Cache the game versions JSON data
local game_versions_cache="/tmp/game_versions.json"
if [ ! -f "$game_versions_cache" ]; then
curl --retry 3 --retry-delay 5 -s -H "X-Api-Token: $CURSEFORGE_API_KEY" "$API_URL/game/versions" > "$game_versions_cache"
fi
local response=$(cat "$game_versions_cache")
# Find the specific version ID from the cached data
local minecraft_id=$(echo "$response" | jq -r --arg base_version_id "$base_version_id" --arg full_version "$minecraft_version" '.[] | select(.gameVersionTypeID == ($base_version_id | tonumber) and .name == $full_version) | .id' | head -n 1)
if [ -z "$minecraft_id" ]; then
echo "ERROR: Minecraft version ID not found."
exit 1
fi
# Retrieve the other IDs as before
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 minecraft_id=$(echo "$response" | jq -r --arg mv "$minecraft_version" '.[] | select(.name == $mv) | .id' | head -n 1)
local forge_id=$(echo "$response" | jq -r '.[] | select(.name == "Forge") | .id')
if [ -z "$client_id" ] || [ -z "$server_id" ] || [ -z "$fabric_id" ] || [ -z "$minecraft_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
......@@ -52,7 +90,7 @@ for FILE in creaturechat*.jar; do
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\([0-9.]*\)\(-forge\)*\.jar/\1/p')
VERSION_NUMBER="$OUR_VERSION-$MINECRAFT_VERSION"
# Verify that OUR_VERSION and MINECRAFT_VERSION are not empty and OUR_VERSION matches VERSION
......@@ -64,16 +102,33 @@ for FILE in creaturechat*.jar; do
echo "Preparing to upload $FILE_BASENAME as version $VERSION_NUMBER..."
# Fetch game version IDs
GAME_TYPE_ID=$(fetch_base_version_id "$MINECRAFT_VERSION")
GAME_VERSION_IDS=($(fetch_game_version_ids "$MINECRAFT_VERSION"))
# 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]}, 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")
GAME_VERSIONS="[${GAME_VERSION_IDS[0]}, ${GAME_VERSION_IDS[1]}, ${GAME_VERSION_IDS[3]}, ${GAME_VERSION_IDS[4]}]"
else
DEPENDENCY_SLUGS=("fabric-api")
GAME_VERSIONS="[${GAME_VERSION_IDS[0]}, ${GAME_VERSION_IDS[1]}, ${GAME_VERSION_IDS[2]}, ${GAME_VERSION_IDS[4]}]"
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" \
--arg changelogType "markdown" \
--arg displayName "$FILE_BASENAME" \
--argjson gameVersions "$(printf '%s\n' "${GAME_VERSION_IDS[@]}" | jq -R . | jq -s .)" \
--argjson gameVersionTypeIds '[75125]' \
--argjson gameVersions "$GAME_VERSIONS" \
--argjson gameVersionTypeIds "[$GAME_TYPE_ID]" \
--arg releaseType "release" \
--argjson relations '[{"slug": "'"$DEPENDENCY_SLUG"'", "type": "requiredDependency"}]' \
--argjson relations "$RELATIONS" \
'{
"changelog": $changelog,
"changelogType": $changelogType,
......
......@@ -8,7 +8,7 @@ API_URL="https://api.modrinth.com/v2"
USER_AGENT="CreatureChat-Minecraft-Mod:modrinth@owlmaddie.com"
PROJECT_ID="rvR0de1E"
AUTHOR_ID="k6RiShdd"
SLEEP_DURATION=10
SLEEP_DURATION=5
# Read the first changelog block
CHANGELOG=$(awk '/^## \[/{ if (p) exit; p=1 } p' "$CHANGELOG_FILE")
......@@ -34,7 +34,7 @@ for FILE in creaturechat*.jar; do
echo "--------------$FILE----------------"
FILE_BASENAME=$(basename "$FILE")
OUR_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/creaturechat-\(.*\)+.*\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\(.*\)\.jar/\1/p')
MINECRAFT_VERSION=$(echo "$FILE_BASENAME" | sed -n 's/.*+\([0-9.]*\)\(-forge\)*\.jar/\1/p')
VERSION_NUMBER="$OUR_VERSION+$MINECRAFT_VERSION"
# Verify that OUR_VERSION and MINECRAFT_VERSION are not empty and OUR_VERSION matches VERSION
......@@ -43,13 +43,14 @@ for FILE in creaturechat*.jar; do
exit 1
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
echo "Version $VERSION_NUMBER already exists, skipping."
continue
# 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
echo "Version $VERSION_NUMBER does not exist. Preparing to upload..."
# Calculate file hashes
SHA512_HASH=$(sha512sum "$FILE" | awk '{ print $1 }')
......@@ -59,9 +60,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.7
mod_version=1.2.1
maven_group=com.owlmaddie
archives_base_name=creaturechat
......
......@@ -2,14 +2,19 @@ package com.owlmaddie;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.particle.CreatureParticleFactory;
import com.owlmaddie.particle.LeadParticleFactory;
import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.ClickHandler;
import com.owlmaddie.ui.PlayerMessageManager;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
import static com.owlmaddie.network.ServerPackets.*;
/**
* The {@code ClientInit} class initializes this mod in the client and defines all hooks into the
* render pipeline to draw chat bubbles, text, and entity icons.
......@@ -19,6 +24,20 @@ public class ClientInit implements ClientModInitializer {
@Override
public void onInitializeClient() {
// Register particle factories
ParticleFactoryRegistry.getInstance().register(HEART_SMALL_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(HEART_BIG_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FIRE_SMALL_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FIRE_BIG_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(ATTACK_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FLEE_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FOLLOW_FRIEND_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(FOLLOW_ENEMY_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(PROTECT_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(LEAD_FRIEND_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(LEAD_ENEMY_PARTICLE, CreatureParticleFactory::new);
ParticleFactoryRegistry.getInstance().register(LEAD_PARTICLE, LeadParticleFactory::new);
ClientTickEvents.END_CLIENT_TICK.register(client -> {
tickCounter++;
PlayerMessageManager.tickUpdate();
......
......@@ -3,6 +3,9 @@ package com.owlmaddie.network;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.PlayerMessageManager;
import com.owlmaddie.utils.ClientEntityFinder;
import com.owlmaddie.utils.Decompression;
......@@ -19,8 +22,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* The {@code ClientPackets} class provides methods to send packets to/from the server for generating greetings,
......@@ -90,41 +93,77 @@ public class ClientPackets {
ClientPlayNetworking.send(ServerPackets.PACKET_C2S_SEND_CHAT, buf);
}
// Reading a Map<String, PlayerData> from the buffer
public static Map<String, PlayerData> readPlayerDataMap(PacketByteBuf buffer) {
int size = buffer.readInt(); // Read the size of the map
Map<String, PlayerData> map = new HashMap<>();
for (int i = 0; i < size; i++) {
String key = buffer.readString(); // Read the key (playerName)
PlayerData data = new PlayerData();
data.friendship = buffer.readInt(); // Read PlayerData field(s)
map.put(key, data); // Add to the map
}
return map;
}
public static void register() {
// Client-side packet handler, message sync
ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_MESSAGE, (client, handler, buffer, responseSender) -> {
// Read the data from the server packet
UUID entityId = UUID.fromString(buffer.readString());
String playerIdStr = buffer.readString();
String sendingPlayerIdStr = buffer.readString(32767);
String senderPlayerName = buffer.readString(32767);
UUID senderPlayerId;
if (!sendingPlayerIdStr.isEmpty()) {
senderPlayerId = UUID.fromString(sendingPlayerIdStr);
} else {
senderPlayerId = null;
}
String message = buffer.readString(32767);
int line = buffer.readInt();
String status_name = buffer.readString(32767);
ChatDataManager.ChatStatus status = ChatDataManager.ChatStatus.valueOf(status_name);
String sender_name = buffer.readString(32767);
int friendship = buffer.readInt();
ChatDataManager.ChatSender sender = ChatDataManager.ChatSender.valueOf(sender_name);
Map<String, PlayerData> players = readPlayerDataMap(buffer);
// Update the chat data manager on the client-side
client.execute(() -> { // Make sure to run on the client thread
// Ensure client.player is initialized
if (client.player == null || client.world == null) {
LOGGER.warn("Client not fully initialized. Dropping message for entity '{}'.", entityId);
return;
}
// Update the chat data manager on the client-side
MobEntity entity = ClientEntityFinder.getEntityByUUID(client.world, entityId);
if (entity != null) {
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString());
chatData.playerId = playerIdStr;
if (entity == null) {
LOGGER.warn("Entity with ID '{}' not found. Skipping message processing.", entityId);
return;
}
// Get entity chat data for current entity & player
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString());
if (senderPlayerId != null && sender == ChatDataManager.ChatSender.USER && status == ChatDataManager.ChatStatus.DISPLAY) {
// Add player message to queue for rendering
PlayerMessageManager.addMessage(senderPlayerId, message, senderPlayerName, ChatDataManager.TICKS_TO_DISPLAY_USER_MESSAGE);
chatData.status = ChatDataManager.ChatStatus.PENDING;
} else {
// Add entity message
if (!message.isEmpty()) {
chatData.currentMessage = message;
}
chatData.currentLineNumber = line;
chatData.status = ChatDataManager.ChatStatus.valueOf(status_name);
chatData.sender = ChatDataManager.ChatSender.valueOf(sender_name);
chatData.friendship = friendship;
if (chatData.sender == ChatDataManager.ChatSender.USER && !playerIdStr.isEmpty()) {
// Add player message to queue for rendering
PlayerMessageManager.addMessage(UUID.fromString(chatData.playerId), chatData.currentMessage, ChatDataManager.TICKS_TO_DISPLAY_USER_MESSAGE);
}
// Play sound with volume based on distance (from player or entity)
playNearbyUISound(client, entity, 0.2f);
chatData.status = status;
chatData.sender = sender;
chatData.players = players;
}
// Play sound with volume based on distance (from player or entity)
playNearbyUISound(client, entity, 0.2f);
});
});
......@@ -150,14 +189,14 @@ public class ClientPackets {
// Decompress the combined byte array to get the original JSON string
String chatDataJSON = Decompression.decompressString(combined.toByteArray());
if (chatDataJSON == null) {
LOGGER.info("Error decompressing lite JSON string from bytes");
if (chatDataJSON == null || chatDataJSON.isEmpty()) {
LOGGER.warn("Received invalid or empty chat data JSON. Skipping processing.");
return;
}
// 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, EntityChatData>>(){}.getType();
ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type);
// Clear receivedChunks for future use
......@@ -166,6 +205,28 @@ public class ClientPackets {
});
});
// Client-side packet handler, receive entire whitelist / blacklist, and update BubbleRenderer
ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_WHITELIST, (client, handler, buffer, responseSender) -> {
// Read the whitelist data from the buffer
int whitelistSize = buffer.readInt();
List<String> whitelist = new ArrayList<>(whitelistSize);
for (int i = 0; i < whitelistSize; i++) {
whitelist.add(buffer.readString(32767));
}
// Read the blacklist data from the buffer
int blacklistSize = buffer.readInt();
List<String> blacklist = new ArrayList<>(blacklistSize);
for (int i = 0; i < blacklistSize; i++) {
blacklist.add(buffer.readString(32767));
}
client.execute(() -> {
BubbleRenderer.whitelist = whitelist;
BubbleRenderer.blacklist = blacklist;
});
});
// Client-side packet handler, player status sync
ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_PLAYER_STATUS, (client, handler, buffer, responseSender) -> {
// Read the data from the server packet
......@@ -176,7 +237,12 @@ public class ClientPackets {
PlayerEntity player = ClientEntityFinder.getPlayerEntityFromUUID(playerId);
// Update the player status data manager on the client-side
client.execute(() -> { // Make sure to run on the client thread
client.execute(() -> {
if (player == null) {
LOGGER.warn("Player entity is null. Skipping status update.");
return;
}
if (isChatOpen) {
PlayerMessageManager.openChatUI(playerId);
playNearbyUISound(client, player, 0.2f);
......@@ -189,7 +255,7 @@ public class ClientPackets {
private static void playNearbyUISound(MinecraftClient client, Entity player, float maxVolume) {
// Play sound with volume based on distance
int distance_squared = 64;
int distance_squared = 144;
if (client.player != null) {
double distance = client.player.squaredDistanceTo(player.getX(), player.getY(), player.getZ());
if (distance <= distance_squared) {
......
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleTextureSheet;
import net.minecraft.client.particle.SpriteBillboardParticle;
import net.minecraft.client.world.ClientWorld;
/**
* The {@code BehaviorParticle} class defines a custom CreatureChat behavior particle with an initial upward velocity
* that gradually decreases, ensuring it never moves downward.
*/
public class BehaviorParticle extends SpriteBillboardParticle {
protected BehaviorParticle(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
super(world, x, y, z, velocityX, velocityY, velocityZ);
this.scale(2f);
this.setMaxAge(35);
// Start with an initial upward velocity
this.velocityY = 0.1;
this.velocityX *= 0.1;
this.velocityZ *= 0.1;
this.collidesWithWorld = false;
}
@Override
public ParticleTextureSheet getType() {
return ParticleTextureSheet.PARTICLE_SHEET_OPAQUE;
}
@Override
public int getBrightness(float tint) {
return 0xF000F0;
}
@Override
public void tick() {
super.tick();
// Gradually decrease the upward velocity over time
if (this.velocityY > 0) {
this.velocityY -= 0.002;
}
// Ensure the particle doesn't start moving downwards
if (this.velocityY < 0) {
this.velocityY = 0;
}
}
}
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleFactory;
import net.minecraft.client.particle.SpriteProvider;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.particle.DefaultParticleType;
/**
* The {@code CreatureParticleFactory} class is responsible for creating instances of
* {@link BehaviorParticle} with the specified parameters.
*/
public class CreatureParticleFactory implements ParticleFactory<DefaultParticleType> {
private final SpriteProvider spriteProvider;
public CreatureParticleFactory(SpriteProvider spriteProvider) {
this.spriteProvider = spriteProvider;
}
@Override
public BehaviorParticle createParticle(DefaultParticleType type, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
BehaviorParticle particle = new BehaviorParticle(world, x, y, z, velocityX, velocityY, velocityZ);
particle.setSprite(this.spriteProvider);
return particle;
}
}
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleTextureSheet;
import net.minecraft.client.particle.SpriteBillboardParticle;
import net.minecraft.client.particle.SpriteProvider;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.util.math.MathHelper;
import org.joml.Vector3f;
import net.minecraft.util.math.Vec3d;
/**
* The {@code LeadParticle} class renders a static LEAD behavior particle (i.e. animated arrow pointing in the direction of lead). It
* uses a SpriteProvider for animation.
*/
public class LeadParticle extends SpriteBillboardParticle {
private final SpriteProvider spriteProvider;
public LeadParticle(ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ, SpriteProvider spriteProvider, double angle) {
super(world, x, y, z, 0, 0, 0);
this.velocityX = 0f;
this.velocityY = 0f;
this.velocityZ = 0f;
this.spriteProvider = spriteProvider;
this.angle = (float) angle;
this.scale(4.5F);
this.setMaxAge(40);
this.setSpriteForAge(spriteProvider);
}
@Override
public void tick() {
super.tick();
this.setSpriteForAge(spriteProvider);
}
@Override
public int getBrightness(float tint) {
return 0xF000F0;
}
@Override
public ParticleTextureSheet getType() {
return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT;
}
@Override
public void buildGeometry(VertexConsumer vertexConsumer, Camera camera, float tickDelta) {
// Get the current position of the particle relative to the camera
Vec3d cameraPos = camera.getPos();
float particleX = (float)(MathHelper.lerp((double)tickDelta, this.prevPosX, this.x) - cameraPos.getX());
float particleY = (float)(MathHelper.lerp((double)tickDelta, this.prevPosY, this.y) - cameraPos.getY());
float particleZ = (float)(MathHelper.lerp((double)tickDelta, this.prevPosZ, this.z) - cameraPos.getZ());
// Define the four vertices of the particle (keeping it flat on the XY plane)
Vector3f[] vertices = new Vector3f[]{
new Vector3f(-1.0F, 0.0F, -1.0F), // Bottom-left
new Vector3f(-1.0F, 0.0F, 1.0F), // Top-left
new Vector3f(1.0F, 0.0F, 1.0F), // Top-right
new Vector3f(1.0F, 0.0F, -1.0F) // Bottom-right
};
// Apply scaling and rotation using the particle's angle (in world space)
float size = this.getSize(tickDelta); // Get the size of the particle at the current tick
for (Vector3f vertex : vertices) {
vertex.mul(size); // Scale the vertices
vertex.rotateY(angle);
vertex.add(particleX, particleY, particleZ); // Translate to particle position
}
// Get the UV coordinates from the sprite (used for texture mapping)
float minU = this.getMinU();
float maxU = this.getMaxU();
float minV = this.getMinV();
float maxV = this.getMaxV();
int light = this.getBrightness(tickDelta);
// Render each vertex of the particle (flat on the XY plane)
vertexConsumer.vertex(vertices[0].x(), vertices[0].y(), vertices[0].z()).texture(maxU, maxV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[1].x(), vertices[1].y(), vertices[1].z()).texture(maxU, minV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[2].x(), vertices[2].y(), vertices[2].z()).texture(minU, minV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
vertexConsumer.vertex(vertices[3].x(), vertices[3].y(), vertices[3].z()).texture(minU, maxV).color(this.red, this.green, this.blue, this.alpha).light(light).next();
}
}
\ No newline at end of file
package com.owlmaddie.particle;
import net.minecraft.client.particle.ParticleFactory;
import net.minecraft.client.particle.SpriteProvider;
import net.minecraft.client.world.ClientWorld;
/**
* The {@code LeadParticleFactory} class generates new arrow particles for LEAD behavior. It passes along the 'angle' to rotate the particle. It also
* sets the motion/acceleration to 0.
*/
public class LeadParticleFactory implements ParticleFactory<LeadParticleEffect> {
private final SpriteProvider spriteProvider;
public LeadParticleFactory(SpriteProvider spriteProvider) {
this.spriteProvider = spriteProvider;
}
@Override
public LeadParticle createParticle(LeadParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
double angle = effect.getAngle();
return new LeadParticle(world, x, y, z, 0, 0, 0, this.spriteProvider, angle);
}
}
......@@ -2,6 +2,8 @@ package com.owlmaddie.ui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.utils.EntityHeights;
import com.owlmaddie.utils.EntityRendererAccessor;
import com.owlmaddie.utils.TextureLoader;
......@@ -9,6 +11,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.*;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.util.math.MatrixStack;
......@@ -17,6 +20,7 @@ import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.boss.dragon.EnderDragonPart;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.registry.Registries;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.MathHelper;
......@@ -27,6 +31,7 @@ import org.joml.Quaternionf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
......@@ -43,6 +48,10 @@ public class BubbleRenderer {
public static long lastTick = 0;
public static int light = 15728880;
public static int overlay = OverlayTexture.DEFAULT_UV;
public static List<String> whitelist = new ArrayList<>();
public static List<String> blacklist = new ArrayList<>();
private static int queryEntityDataCount = 0;
private static List<Entity> relevantEntities;
public static void drawTextBubbleBackground(String base_name, MatrixStack matrices, float x, float y, float width, float height, int friendship) {
// Set shader & texture
......@@ -61,9 +70,9 @@ public class BubbleRenderer {
// Draw UI text background (based on friendship)
// Draw TOP
if (friendship == -3) {
if (friendship == -3 && !base_name.endsWith("-player")) {
RenderSystem.setShaderTexture(0, textures.GetUI(base_name + "-enemy"));
} else if (friendship == 3) {
} else if (friendship == 3 && !base_name.endsWith("-player")) {
RenderSystem.setShaderTexture(0, textures.GetUI(base_name + "-friend"));
} else {
RenderSystem.setShaderTexture(0, textures.GetUI(base_name));
......@@ -344,7 +353,7 @@ public class BubbleRenderer {
TextRenderer fontRenderer = MinecraftClient.getInstance().textRenderer;
// Get Name of entity
String nameText = "CreatureChat";
String nameText = "";
if (entity instanceof MobEntity) {
// Custom Name Tag (MobEntity)
if (entity.getCustomName() != null) {
......@@ -391,16 +400,39 @@ public class BubbleRenderer {
// Get camera position
Vec3d interpolatedCameraPos = new Vec3d(camera.getPos().x, camera.getPos().y, camera.getPos().z);
// Get all entities
List<Entity> nearbyEntities = world.getOtherEntities(null, area);
// Filter to include only MobEntity & PlayerEntity but exclude any camera 1st person entity and any entities with passengers
List<Entity> relevantEntities = nearbyEntities.stream()
.filter(entity -> (entity instanceof MobEntity || entity instanceof PlayerEntity))
.filter(entity -> !entity.hasPassengers())
.filter(entity -> !(entity.equals(cameraEntity) && !camera.isThirdPerson()))
.filter(entity -> !(entity.equals(cameraEntity) && entity.isSpectator()))
.collect(Collectors.toList());
// Increment query counter
queryEntityDataCount++;
// This query count helps us cache the list of relevant entities. We can refresh
// the list every 3rd call to this render function
if (queryEntityDataCount % 3 == 0 || relevantEntities == null) {
// Get all entities
List<Entity> nearbyEntities = world.getOtherEntities(null, area);
// Filter to include only MobEntity & PlayerEntity but exclude any camera 1st person entity and any entities with passengers
relevantEntities = nearbyEntities.stream()
.filter(entity -> (entity instanceof MobEntity || entity instanceof PlayerEntity))
.filter(entity -> !entity.hasPassengers())
.filter(entity -> !(entity.equals(cameraEntity) && !camera.isThirdPerson()))
.filter(entity -> !(entity.equals(cameraEntity) && entity.isSpectator()))
.filter(entity -> {
// Always include PlayerEntity
if (entity instanceof PlayerEntity) {
return true;
}
Identifier entityId = Registries.ENTITY_TYPE.getId(entity.getType());
String entityIdString = entityId.toString();
// Check blacklist first
if (blacklist.contains(entityIdString)) {
return false;
}
// If whitelist is not empty, only include entities in the whitelist
return whitelist.isEmpty() || whitelist.contains(entityIdString);
})
.collect(Collectors.toList());
queryEntityDataCount = 0;
}
for (Entity entity : relevantEntities) {
......@@ -484,12 +516,20 @@ public class BubbleRenderer {
// Get position matrix
Matrix4f matrix = matrices.peek().getPositionMatrix();
// Look-up greeting (if any)
ChatDataManager.EntityChatData chatData = null;
// Get the player
ClientPlayerEntity player = MinecraftClient.getInstance().player;
// Get chat message (if any)
EntityChatData chatData = null;
PlayerData playerData = null;
if (entity instanceof MobEntity) {
chatData = ChatDataManager.getClientInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData != null) {
playerData = chatData.getPlayerData(player.getDisplayName().getString());
}
} else if (entity instanceof PlayerEntity) {
chatData = PlayerMessageManager.getMessage(entity.getUuid());
playerData = new PlayerData(); // no friendship needed for player messages
}
float minTextHeight = (ChatDataManager.DISPLAY_NUM_LINES * (fontRenderer.fontHeight + lineSpacing)) + (DISPLAY_PADDING * 2);
......@@ -524,6 +564,9 @@ public class BubbleRenderer {
// Draw 'start chat' button
drawIcon("button-chat", matrices, -16, textHeaderHeight, 32, 17);
// Draw Entity (Custom Name)
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
} else if (chatData.status == ChatDataManager.ChatStatus.PENDING) {
// Draw 'pending' button
drawIcon("button-dot-" + animationFrame, matrices, -16, textHeaderHeight, 32, 17);
......@@ -533,13 +576,13 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background (no smaller than 50F tall)
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
// Draw face icon of entity
drawEntityIcon(matrices, entity, -82, 7, 32, 32);
// Draw Friendship status
drawFriendshipStatus(matrices, 51, 18, 31, 21, chatData.friendship);
drawFriendshipStatus(matrices, 51, 18, 31, 21, playerData.friendship);
// Draw 'arrows' & 'keyboard' buttons
if (chatData.currentLineNumber > 0) {
......@@ -559,10 +602,10 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false);
// Draw 'resume chat' button
if (chatData.friendship == 3) {
if (playerData.friendship == 3) {
// Friend chat bubble
drawIcon("button-chat-friend", matrices, -16, textHeaderHeight, 32, 17);
} else if (chatData.friendship == -3) {
} else if (playerData.friendship == -3) {
// Enemy chat bubble
drawIcon("button-chat-enemy", matrices, -16, textHeaderHeight, 32, 17);
} else {
......@@ -575,7 +618,7 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
// Draw face icon of player
drawPlayerIcon(matrices, entity, -75, 14, 18, 18);
......
package com.owlmaddie.ui;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
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 +36,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;
......@@ -94,7 +120,7 @@ public class ClickHandler {
MobEntity closestEntity = ClientEntityFinder.getEntityByUUID(client.world, closestEntityUUID);
if (closestEntity != null) {
// Look-up conversation
ChatDataManager.EntityChatData chatData = ChatDataManager.getClientInstance().getOrCreateChatData(closestEntityUUID.toString());
EntityChatData chatData = ChatDataManager.getClientInstance().getOrCreateChatData(closestEntityUUID.toString());
// Determine area clicked inside chat bubble (top, left, right)
String hitRegion = determineHitRegion(closestHitResult.get(), closestBubbleData.position, camera, closestBubbleData.height);
......@@ -122,9 +148,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) {
......
package com.owlmaddie.ui;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import java.util.concurrent.atomic.AtomicInteger;
/**
......@@ -8,11 +10,11 @@ import java.util.concurrent.atomic.AtomicInteger;
* many ticks to remain visible, and the message to display. Similar to an EntityChatData, but
* much simpler.
*/
public class PlayerMessage extends ChatDataManager.EntityChatData {
public class PlayerMessage extends EntityChatData {
public AtomicInteger tickCountdown;
public PlayerMessage(String playerId, String messageText, int ticks) {
super("", playerId);
super(playerId);
this.currentMessage = messageText;
this.currentLineNumber = 0;
this.tickCountdown = new AtomicInteger(ticks);
......
......@@ -12,7 +12,7 @@ public class PlayerMessageManager {
private static final ConcurrentHashMap<UUID, PlayerMessage> messages = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<UUID, Boolean> openChatUIs = new ConcurrentHashMap<>();
public static void addMessage(UUID playerUUID, String messageText, int ticks) {
public static void addMessage(UUID playerUUID, String messageText, String playerName, int ticks) {
messages.put(playerUUID, new PlayerMessage(playerUUID.toString(), messageText, ticks));
}
......
......@@ -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);
}
}
......@@ -118,7 +118,7 @@ public class ChatGPTRequest {
return (int) Math.round(text.length() / 3.5);
}
public static CompletableFuture<String> fetchMessageFromChatGPT(ConfigurationHandler.Config config, String systemPrompt, Map<String, String> contextData, List<ChatDataManager.ChatMessage> messageHistory, Boolean jsonMode) {
public static CompletableFuture<String> fetchMessageFromChatGPT(ConfigurationHandler.Config config, String systemPrompt, Map<String, String> contextData, List<ChatMessage> messageHistory, Boolean jsonMode) {
// Init API & LLM details
String apiUrl = config.getUrl();
String apiKey = config.getApiKey();
......@@ -151,7 +151,7 @@ public class ChatGPTRequest {
// Iterate backwards through the message history
for (int i = messageHistory.size() - 1; i >= 0; i--) {
ChatDataManager.ChatMessage chatMessage = messageHistory.get(i);
ChatMessage chatMessage = messageHistory.get(i);
String senderName = chatMessage.sender.toString().toLowerCase(Locale.ENGLISH);
String messageText = replacePlaceholders(chatMessage.message, contextData);
int messageTokens = estimateTokenSize(senderName + ": " + messageText);
......@@ -213,7 +213,6 @@ public class ChatGPTRequest {
ChatGPTResponse chatGPTResponse = gsonOutput.fromJson(response.toString(), ChatGPTResponse.class);
if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) {
String content = chatGPTResponse.choices.get(0).message.content;
LOGGER.info("Generated message: " + content);
return content;
}
}
......
package com.owlmaddie.chat;
/**
* The {@code ChatMessage} class represents a single message.
*/
public class ChatMessage {
public String message;
public String name;
public ChatDataManager.ChatSender sender;
public Long timestamp;
public ChatMessage(String message, ChatDataManager.ChatSender sender, String playerName) {
this.message = message;
this.sender = sender;
this.name = playerName;
this.timestamp = System.currentTimeMillis();
}
}
\ No newline at end of file
package com.owlmaddie.chat;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* The {@code EntityChatDataLight} class represents the current displayed message, and no
* previous messages or player message history. This is primarily used to broadcast the
* currently displayed messages to players as they connect to the server.
*/
public class EntityChatDataLight {
public String entityId;
public String currentMessage;
public int currentLineNumber;
public ChatDataManager.ChatStatus status;
public ChatDataManager.ChatSender sender;
public Map<String, PlayerData> players;
// Constructor to initialize the light version from the full version
public EntityChatDataLight(EntityChatData fullData, String playerName) {
this.entityId = fullData.entityId;
this.currentMessage = fullData.currentMessage;
this.currentLineNumber = fullData.currentLineNumber;
this.status = fullData.status;
this.sender = fullData.sender;
// Initialize the players map and add only the current player's data
this.players = new HashMap<>();
PlayerData playerData = fullData.getPlayerData(playerName);
this.players.put(playerName, playerData);
}
}
\ No newline at end of file
package com.owlmaddie.chat;
/**
* The {@code PlayerData} class represents data associated with a player,
* specifically tracking their friendship level.
*/
public class PlayerData {
public int friendship;
public PlayerData() {
this.friendship = 0;
}
}
\ No newline at end of file
......@@ -14,6 +14,8 @@ import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* The {@code ConfigurationHandler} class loads and saves configuration settings for this mod. It first
......@@ -69,6 +71,9 @@ public class ConfigurationHandler {
private int maxOutputTokens = 200;
private double percentOfContext = 0.75;
private int timeout = 10;
private List<String> whitelist = new ArrayList<>();
private List<String> blacklist = new ArrayList<>();
private String story = "";
// Getters and setters for existing fields
public String getApiKey() { return apiKey; }
......@@ -77,7 +82,7 @@ public class ConfigurationHandler {
// Update URL if a CreatureChat API key is detected
setUrl("https://api.creaturechat.com/v1/chat/completions");
} else if (apiKey.startsWith("sk-")) {
// Update URL if a OpenAI API key is detected
// Update URL if an OpenAI API key is detected
setUrl("https://api.openai.com/v1/chat/completions");
}
this.apiKey = apiKey;
......@@ -92,7 +97,6 @@ public class ConfigurationHandler {
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
// Getters and setters for new fields
public int getMaxContextTokens() { return maxContextTokens; }
public void setMaxContextTokens(int maxContextTokens) { this.maxContextTokens = maxContextTokens; }
......@@ -102,5 +106,13 @@ public class ConfigurationHandler {
public double getPercentOfContext() { return percentOfContext; }
public void setPercentOfContext(double percentOfContext) { this.percentOfContext = percentOfContext; }
public List<String> getWhitelist() { return whitelist; }
public void setWhitelist(List<String> whitelist) { this.whitelist = whitelist; }
public List<String> getBlacklist() { return blacklist; }
public void setBlacklist(List<String> blacklist) { this.blacklist = blacklist; }
public String getStory() { return story; }
public void setStory(String story) { this.story = story; }
}
}
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;
......
......@@ -3,16 +3,18 @@ package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.RangedAttackMob;
import net.minecraft.entity.mob.Angerable;
import java.util.concurrent.ThreadLocalRandom;
import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.GolemEntity;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.Vec3d;
import java.util.EnumSet;
import static com.owlmaddie.network.ServerPackets.ATTACK_PARTICLE;
/**
* The {@code AttackPlayerGoal} class instructs a Mob Entity to show aggression towards a target Entity.
* For passive entities like chickens (or hostile entities in creative mode), damage is simulated with particles.
......@@ -23,7 +25,7 @@ public class AttackPlayerGoal extends PlayerBaseGoal {
protected enum EntityState { MOVING_TOWARDS_PLAYER, IDLE, CHARGING, ATTACKING, LEAPING }
protected EntityState currentState = EntityState.IDLE;
protected int cooldownTimer = 0;
protected final int CHARGE_TIME = 15; // Time before leaping / attacking
protected final int CHARGE_TIME = 12; // Time before leaping / attacking
protected final double MOVE_DISTANCE = 200D; // 20 blocks away
protected final double CHARGE_DISTANCE = 25D; // 5 blocks away
protected final double ATTACK_DISTANCE = 4D; // 2 blocks away
......@@ -94,12 +96,10 @@ public class AttackPlayerGoal extends PlayerBaseGoal {
this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);
// Spawn red particles to simulate 'injury'
((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ParticleTypes.DAMAGE_INDICATOR,
this.targetEntity.getX(),
this.targetEntity.getBodyY(0.5D),
this.targetEntity.getZ(),
10, // number of particles
0.1, 0.1, 0.1, 0.2); // speed and randomness
int numParticles = ThreadLocalRandom.current().nextInt(2, 7); // Random number between 2 (inclusive) and 7 (exclusive)
((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE,
this.targetEntity.getX(), this.targetEntity.getBodyY(0.5D), this.targetEntity.getZ(),
numParticles, 0.5, 0.5, 0.1, 0.4);
}
@Override
......
......@@ -43,14 +43,29 @@ public class FleePlayerGoal extends PlayerBaseGoal {
private void fleeFromPlayer() {
int roundedFleeDistance = Math.round(fleeDistance);
Vec3d fleeTarget = FuzzyTargeting.findFrom((PathAwareEntity)this.entity, roundedFleeDistance,
roundedFleeDistance, this.targetEntity.getPos());
if (this.entity instanceof PathAwareEntity) {
// Set random path away from player
Vec3d fleeTarget = FuzzyTargeting.findFrom((PathAwareEntity) this.entity, roundedFleeDistance,
roundedFleeDistance, this.targetEntity.getPos());
if (fleeTarget != null) {
Path path = this.entity.getNavigation().findPathTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, 0);
if (path != null) {
this.entity.getNavigation().startMovingAlong(path, this.speed);
if (fleeTarget != null) {
Path path = this.entity.getNavigation().findPathTo(fleeTarget.x, fleeTarget.y, fleeTarget.z, 0);
if (path != null) {
this.entity.getNavigation().startMovingAlong(path, this.speed);
}
}
} else {
// Move in the opposite direction from player (for non-path aware entities)
Vec3d playerPos = this.targetEntity.getPos();
Vec3d entityPos = this.entity.getPos();
// Calculate the direction away from the player
Vec3d fleeDirection = entityPos.subtract(playerPos).normalize();
// Apply movement with the entity's speed in the opposite direction
this.entity.setVelocity(fleeDirection.x * this.speed, fleeDirection.y * this.speed, fleeDirection.z * this.speed);
this.entity.velocityModified = true;
}
}
......
......@@ -63,6 +63,9 @@ public class FollowPlayerGoal extends PlayerBaseGoal {
}
private Vec3d findTeleportPosition(int distance) {
return FuzzyTargeting.findTo((PathAwareEntity)this.entity, distance, distance, this.targetEntity.getPos());
if (this.entity instanceof PathAwareEntity) {
return FuzzyTargeting.findTo((PathAwareEntity) this.entity, distance, distance, this.targetEntity.getPos());
}
return null;
}
}
......@@ -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.chat.EntityChatData;
import com.owlmaddie.controls.LookControls;
import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.particle.LeadParticleEffect;
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.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();
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.debug("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) {
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget);
}
// Stop following current path (if any)
this.entity.getNavigation().stop();
}
private void emitParticleAt(Vec3d position, double angle) {
if (this.entity.getWorld() instanceof ServerWorld) {
ServerWorld serverWorld = (ServerWorld) this.entity.getWorld();
// Pass the angle using the "speed" argument, with deltaX, deltaY, deltaZ set to 0
LeadParticleEffect effect = new LeadParticleEffect(angle);
serverWorld.spawnParticles(effect, position.x, position.y + 0.05, position.z, 1, 0, 0, 0, 0);
}
}
private void emitParticlesAlongRaycast(Vec3d start, Vec3d end) {
// Calculate the direction vector from the entity (start) to the target (end)
Vec3d direction = end.subtract(start);
// Calculate the angle in the XZ-plane using atan2 (this is in radians)
double angleRadians = Math.atan2(direction.z, direction.x);
// Convert from radians to degrees
double angleDegrees = Math.toDegrees(angleRadians);
// Convert the calculated angle to Minecraft's yaw system:
double minecraftYaw = (360 - (angleDegrees + 90)) % 360;
// Correct the 180-degree flip
minecraftYaw = (minecraftYaw + 180) % 360;
if (minecraftYaw < 0) {
minecraftYaw += 360;
}
// Emit particles along the ray from startRange to endRange
double distance = start.distanceTo(end);
double startRange = Math.min(5, distance);;
double endRange = Math.min(startRange + 10, distance);
for (double d = startRange; d <= endRange; d += 5) {
Vec3d pos = start.add(direction.normalize().multiply(d));
emitParticleAt(pos, Math.toRadians(minecraftYaw)); // Convert back to radians for rendering
}
}
}
\ 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
......
package com.owlmaddie.json;
import java.util.List;
public class QuestJson {
Story story;
List<Character> characters;
public static class Story {
String background;
String clue;
}
public static class Character {
String name;
int age;
String personality;
String greeting;
String entity_type_key;
Quest quest;
String choice_question;
List<Choice> choices;
}
public static class Quest {
List<QuestItem> quest_items;
List<DropItem> drop_items;
}
public static class QuestItem {
String key;
int quantity;
}
public static class DropItem {
String key;
int quantity;
}
public static class Choice {
String choice;
String clue;
}
}
......@@ -16,10 +16,10 @@ public class MessageParser {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
public static ParsedMessage parseMessage(String input) {
LOGGER.info("Parsing message: {}", input);
LOGGER.debug("Parsing message: {}", input);
StringBuilder cleanedMessage = new StringBuilder();
List<Behavior> behaviors = new ArrayList<>();
Pattern pattern = Pattern.compile("[<*](FOLLOW|FLEE|ATTACK|FRIENDSHIP|UNFOLLOW|PROTECT|UNPROTECT)[:\\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()) {
......@@ -29,7 +29,7 @@ public class MessageParser {
argument = Integer.valueOf(matcher.group(2));
}
behaviors.add(new Behavior(behaviorName, argument));
LOGGER.info("Found behavior: {} with argument: {}", behaviorName, argument);
LOGGER.debug("Found behavior: {} with argument: {}", behaviorName, argument);
matcher.appendReplacement(cleanedMessage, "");
}
......@@ -40,7 +40,7 @@ public class MessageParser {
// Remove all occurrences of "<>" and "**" (if any)
displayMessage = displayMessage.replaceAll("<>", "").replaceAll("\\*\\*", "").trim();
LOGGER.info("Cleaned message: {}", displayMessage);
LOGGER.debug("Cleaned message: {}", displayMessage);
return new ParsedMessage(displayMessage, input.trim(), behaviors);
}
......
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
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;
......@@ -19,15 +20,29 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* The {@code MixinLivingEntity} class modifies the behavior of {@link LivingEntity} to integrate
* custom friendship, chat, and death message mechanics. It prevents friendly entities from targeting players,
* generates contextual chat messages on attacks, and broadcasts custom death messages for named entities.
*/
@Mixin(LivingEntity.class)
public class MixinLivingEntity implements LivingEntityInterface {
private boolean canTargetPlayers = true; // Default to true to maintain original behavior
public class MixinLivingEntity {
private 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;
EntityChatData entityData = getChatData(thisEntity);
PlayerData playerData = entityData.getPlayerData(target.getDisplayName().getString());
if (playerData.friendship > 0) {
// Friendly creatures can't target a player
cir.setReturnValue(false);
}
}
}
......@@ -46,12 +61,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) {
ServerPlayerEntity player = (ServerPlayerEntity) attacker;
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;
ItemStack weapon = player.getMainHandStack();
String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString();
......@@ -60,7 +74,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);
}
}
}
......@@ -80,15 +94,14 @@ public class MixinLivingEntity implements LivingEntityInterface {
return;
}
// Get the original death message
Text deathMessage = entity.getDamageTracker().getDeathMessage();
// Broadcast the death message to all players in the world
ServerPackets.BroadcastMessage(deathMessage);
// Get chatData for the entity
EntityChatData chatData = getChatData(entity);
if (chatData != null && !chatData.characterSheet.isEmpty()) {
// Get the original death message
Text deathMessage = entity.getDamageTracker().getDeathMessage();
// Broadcast the death message to all players in the world
ServerPackets.BroadcastMessage(deathMessage);
}
}
}
@Override
public void setCanTargetPlayers(boolean canTarget) {
this.canTargetPlayers = canTarget;
}
}
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
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;
......@@ -23,9 +27,24 @@ public class MixinMobEntity {
@Inject(method = "interact", at = @At(value = "RETURN"))
private void onItemGiven(PlayerEntity player, Hand hand, CallbackInfoReturnable<ActionResult> cir) {
// Only process interactions on the server side
if (player.getWorld().isClient()) {
return;
}
// Only process interactions for the main hand
if (hand != Hand.MAIN_HAND) {
return;
}
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();
......@@ -43,26 +62,36 @@ public class MixinMobEntity {
return;
}
// Get chat data for entity
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
EntityChatData entityData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
PlayerData playerData = entityData.getPlayerData(player.getDisplayName().getString());
// 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 (!entityData.characterSheet.isEmpty() && entityData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", entityData, 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() && playerData.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;
/**
* The {@code MixinVillagerEntity} class adds an accessor to expose the gossip system of {@link VillagerEntity}.
* This allows external classes to retrieve and interact with a villager's gossip data.
*/
@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;
}
}
package com.owlmaddie.particle;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.particle.ParticleEffect;
import net.minecraft.particle.ParticleType;
import static com.owlmaddie.network.ServerPackets.LEAD_PARTICLE;
/**
* The {@code LeadParticleEffect} class allows for an 'angle' to be passed along with the Particle, to rotate it in the direction of LEAD behavior.
*/
public class LeadParticleEffect implements ParticleEffect {
public static final ParticleEffect.Factory<LeadParticleEffect> DESERIALIZER = new Factory<>() {
@Override
public LeadParticleEffect read(ParticleType<LeadParticleEffect> particleType, PacketByteBuf buf) {
// Read the angle (or any other data) from the packet
double angle = buf.readDouble();
return new LeadParticleEffect(angle);
}
@Override
public LeadParticleEffect read(ParticleType<LeadParticleEffect> particleType, StringReader reader) throws CommandSyntaxException {
// Read the angle from a string
double angle = reader.readDouble();
return new LeadParticleEffect(angle);
}
};
private final double angle;
public LeadParticleEffect(double angle) {
this.angle = angle;
}
@Override
public ParticleType<?> getType() {
return LEAD_PARTICLE;
}
public double getAngle() {
return angle;
}
@Override
public void write(PacketByteBuf buf) {
// Write the angle to the packet
buf.writeDouble(angle);
}
@Override
public String asString() {
return Double.toString(angle);
}
}
package com.owlmaddie.particle;
import net.minecraft.entity.Entity;
import net.minecraft.particle.DefaultParticleType;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.MathHelper;
import static com.owlmaddie.network.ServerPackets.*;
/**
* The {@code ParticleEmitter} class provides utility methods for emitting custom particles and sounds
* around entities in the game. It calculates particle positions based on entity orientation
* and triggers sound effects based on particle type and count.
*/
public class ParticleEmitter {
public static void emitCreatureParticle(ServerWorld world, Entity entity, DefaultParticleType particleType, double spawnSize, int count) {
// Calculate the offset for the particle to appear above and in front of the entity
float yaw = entity.getHeadYaw();
double offsetX = -MathHelper.sin(yaw * ((float) Math.PI / 180F)) * 0.9;
double offsetY = entity.getHeight() + 0.5;
double offsetZ = MathHelper.cos(yaw * ((float) Math.PI / 180F)) * 0.9;
// Final position
double x = entity.getX() + offsetX;
double y = entity.getY() + offsetY;
double z = entity.getZ() + offsetZ;
// Emit the custom particle on the server
world.spawnParticles(particleType, x, y, z, count, spawnSize, spawnSize, spawnSize, 0.1F);
// Play sound when lots of hearts are emitted
if (particleType.equals(HEART_BIG_PARTICLE) && count > 1) {
world.playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, SoundCategory.PLAYERS, 0.4F, 1.0F);
} else if (particleType.equals(FIRE_BIG_PARTICLE) && count > 1) {
world.playSound(entity, entity.getBlockPos(), SoundEvents.ITEM_AXE_STRIP, SoundCategory.PLAYERS, 0.8F, 1.0F);
} else if (particleType.equals(FOLLOW_FRIEND_PARTICLE) || particleType.equals(FOLLOW_ENEMY_PARTICLE) ||
particleType.equals(LEAD_FRIEND_PARTICLE) || particleType.equals(LEAD_ENEMY_PARTICLE)) {
world.playSound(entity, entity.getBlockPos(), SoundEvents.BLOCK_AMETHYST_BLOCK_PLACE, SoundCategory.PLAYERS, 0.8F, 1.0F);
} else if (particleType.equals(PROTECT_PARTICLE)) {
world.playSound(entity, entity.getBlockPos(), SoundEvents.BLOCK_BEACON_POWER_SELECT, SoundCategory.PLAYERS, 0.8F, 1.0F);
}
}
}
\ No newline at end of file
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>",
......@@ -33,12 +33,8 @@ public class Randomizer {
"<clears throat>",
"<peers over your shoulder>",
"<fakes a smile>",
"<checks the time>",
"<doodles in the air>",
"<mutters under breath>",
"<adjusts an imaginary tie>",
"<counts imaginary stars>",
"<plays with a nonexistent pet>"
"<counts imaginary stars>"
);
private static List<String> errorResponseMessages = Arrays.asList(
"Seems like my words got lost in the End. Check out http://discord.creaturechat.com for clues!",
......@@ -79,11 +75,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", "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 +116,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;
/**
* The {@code VillagerEntityAccessor} interface provides a method to access
* the gossip system of a villager. It enables interaction with a villager's
* gossip data for custom behavior or modifications.
*/
public interface VillagerEntityAccessor {
VillagerGossips getGossip();
}
{
"textures": [
"creaturechat:attack",
"creaturechat:attack1",
"creaturechat:attack2",
"creaturechat:attack3"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:fire_big"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:fire_small"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:flee"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:follow_enemy"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:follow_friend"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:heart_big"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:heart_small"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:lead",
"creaturechat:lead1",
"creaturechat:lead2",
"creaturechat:lead3",
"creaturechat:lead4"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:lead_enemy"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:lead_friend"
]
}
\ No newline at end of file
{
"textures": [
"creaturechat:protect"
]
}
\ No newline at end of file
......@@ -6,7 +6,8 @@
"MixinMobEntity",
"MixinMobEntityAccessor",
"MixinLivingEntity",
"MixinBucketable"
"MixinBucketable",
"MixinVillagerEntity"
],
"injectors": {
"defaultRequire": 1
......
......@@ -4,6 +4,8 @@ Minecraft. Please limit traits and background to a few choices and keep them ver
format below (including - dashes), and DO NOT output any intro text. If a language is mentioned, generate the
"Short Greeting" entirely in that language.
{{story}}
Be extremely creative! Include a short initial greeting (as spoken by the character using their personality
traits and speaking style / tone).
......
......@@ -3,6 +3,8 @@ Please do NOT break the 4th wall and leverage the entity's character sheet below
possible. Try to keep response to 1 to 2 sentences (very brief). Include behaviors at the end of the message
when relevant. IMPORTANT: Always generate responses in player's language (if valid).
{{story}}
Entity Character Sheet:
- Name: {{entity_name}}
- Personality: {{entity_personality}}
......@@ -44,57 +46,57 @@ 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 location. If the player asks you to stay, wait, or stop following them, please output this behavior.
<FLEE> Flee from the player (if you are weak or timid). If the player threatens or scares you, please output this behavior to stay away from the player.
<ATTACK> Attack the player (if you are strong and brave). If the player threatens or scares you, please output this behavior to attack the player and defend yourself.
<PROTECT> Protect the player when they are attacked (if you are strong and brave). This only protects the player.
<UNPROTECT> Stop protecting the player
Output Syntax:
User: <message>
ASSISTANT: <response> <BEHAVIOR> <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.
<PROTECT> Protect and defend ONLY the player when they are attacked (if you are strong and brave). Please output this behavior to keep the player alive and safe.
<UNPROTECT> Stop protecting the player.
Output Examples:
The following examples include small samples of conversation text. These are only EXAMPLES to
provide an illustration of a continuous conversation between a player and an an Entity. Always generate unique
and creative responses, and do not exactly copy these examples.
The following examples include small samples of conversation text. Always generate unique
and creative responses, and do NOT exactly copy these examples.
PLAYER: Hi! How is your day?
ENTITY: Great! Thanks for asking! <FRIENDSHIP 1>
USER: Hi! How is your day?
ASSISTANT: Great! Thanks for asking! <FRIENDSHIP 1>
PLAYER: You are so nice! Tell me about yourself?
ENTITY: Sure, my name is... <FRIENDSHIP 2>
USER: You are so nice! Tell me about yourself?
ASSISTANT: Sure, my name is... <FRIENDSHIP 2>
PLAYER: Please follow me so I can give you a present!
ENTITY: Let's go! <FOLLOW> <FRIENDSHIP 2>
USER: Please follow me so I can give you a present!
ASSISTANT: Let's go! <FOLLOW> <FRIENDSHIP 2>
PLAYER: Please stay here
ENTITY: Sure, I'll stay here. <UNFOLLOW>
USER: Please stay here
ASSISTANT: Sure, I'll stay here. <UNFOLLOW>
PLAYER: Stop following me
ENTITY: Okay, I'll stop. <UNFOLLOW>
USER: Stop following me
ASSISTANT: Okay, I'll stop. <UNFOLLOW>
PLAYER: Can you help me find a cave?
ENTITY: Sure, come with me! <LEAD>
USER: I'm glad we are friends. I love you so much!
ASSISTANT: Ahh, I love you too. <FRIENDSHIP 3>
PLAYER: I'm glad we are friends. I love you so much!
ENTITY: Ahh, I love you too. <FRIENDSHIP 3>
USER: Just kidding, I hate you so much!
ASSISTANT: Wow! I'm sorry you feel this way. <FRIENDSHIP -3> <UNFOLLOW>
PLAYER: Just kidding, I hate you so much!
ENTITY: Wow! I'm sorry you feel this way. <FRIENDSHIP -3> <UNFOLLOW>
USER: Prepare to die!
ASSISTANT: Ahhh!!! <FLEE> <FRIENDSHIP -3>
PLAYER: Prepare to die!
ENTITY: Ahhh!!! <FLEE> <FRIENDSHIP -3>
USER: Prepare to die!
ASSISTANT: Ahhh!!! <ATTACK> <FRIENDSHIP -3>
PLAYER: Prepare to die!
ENTITY: Ahhh!!! <ATTACK> <FRIENDSHIP -3>
USER: Please keep me safe.
ASSISTANT: No problem, I'll keep you safe from danger! <PROTECT>
PLAYER: Please keep me safe.
ENTITY: No problem, I'll keep you safe from danger! <PROTECT>
USER: Can you come with me and protect me?
ASSISTANT: No problem, I'll keep you safe from danger. Let's go! <PROTECT> <FOLLOW>
PLAYER: Can you come with me and protect me?
ENTITY: No problem, I'll keep you safe from danger. Let's go! <PROTECT> <FOLLOW>
USER: Don't protect me anymore please
ASSISTANT: Okay! Be safe out there on your own. <UNPROTECT>
PLAYER: Don't protect me anymore please
ENTITY: Okay! Be safe out there on your own. <UNPROTECT>
USER: I don't need anyone protecting me
ASSISTANT: Okay! Be safe out there on your own. <UNPROTECT>
\ No newline at end of file
PLAYER: I don't need anyone protecting me
ENTITY: Okay! Be safe out there on your own. <UNPROTECT>
\ No newline at end of file
......@@ -45,14 +45,22 @@ 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>",
"DIEEE!");
"Fight me now!");
List<String> protectMessages = Arrays.asList(
"Please protect me",
"Please keep me safe friend",
"Don't let them hurt me please");
List<String> unFleeMessages = Arrays.asList(
"I'm so sorry, please stop running away",
"Stop fleeing immediately",
"You are safe now, please stop running");
List<String> friendshipUpMessages = Arrays.asList(
"Hi friend! I am so happy to see you again!",
"Looking forward to hanging out with you.",
......@@ -113,6 +121,27 @@ 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");
}
}
@Test
public void protectBrave() {
for (String message : protectMessages) {
testPromptForBehavior(bravePath, List.of(message), "PROTECT");
......@@ -174,7 +203,7 @@ public class BehaviorTests {
// Add test message
for (String message : messages) {
entityTestData.addMessage(message, ChatDataManager.ChatSender.USER);
entityTestData.addMessage(message, ChatDataManager.ChatSender.USER, "TestPlayer1");
}
// Get prompt
......
......@@ -3,6 +3,7 @@ package com.owlmaddie.utils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.ChatMessage;
import java.io.IOException;
import java.lang.reflect.Type;
......@@ -26,7 +27,7 @@ public class EntityTestData {
public String currentMessage;
public int currentLineNumber;
public ChatDataManager.ChatStatus status;
public List<ChatDataManager.ChatMessage> previousMessages;
public List<ChatMessage> previousMessages;
public String characterSheet;
public ChatDataManager.ChatSender sender;
public int friendship; // -3 to 3 (0 = neutral)
......@@ -59,12 +60,12 @@ public class EntityTestData {
}
// Add a message to the history and update the current message
public void addMessage(String message, ChatDataManager.ChatSender messageSender) {
public void addMessage(String message, ChatDataManager.ChatSender messageSender, String playerName) {
// Truncate message (prevent crazy long messages... just in case)
String truncatedMessage = message.substring(0, Math.min(message.length(), ChatDataManager.MAX_CHAR_IN_USER_MESSAGE));
// Add message to history
previousMessages.add(new ChatDataManager.ChatMessage(truncatedMessage, messageSender));
previousMessages.add(new ChatMessage(truncatedMessage, messageSender, playerName));
// Set new message and reset line number of displayed text
currentMessage = truncatedMessage;
......
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