Commit 1ee5fbc1 by Jonathan Thomas

Merge branch 'player-data-refactor' into 'develop'

Player Data Refactor: Particles, Sounds, and Data Enhancements

See merge request !18
parents c3112c6b 35d3af57
Pipeline #13216 passed with stages
in 2 minutes 58 seconds
......@@ -4,7 +4,28 @@ All notable changes to **CreatureChat** are documented in this file. The format
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.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
......
......@@ -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,8 @@ 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;
......@@ -20,10 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
......@@ -94,41 +93,78 @@ 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) {
if (entity == null) {
LOGGER.warn("Entity with ID '{}' not found. Skipping message processing.", entityId);
return;
}
// Get entity chat data for current entity & player
String currentPlayerName = client.player.getDisplayName().getString();
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString());
chatData.playerId = playerIdStr;
EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString(), currentPlayerName);
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);
chatData.status = status;
chatData.sender = sender;
chatData.players = players; // friendships
}
// Play sound with volume based on distance (from player or entity)
playNearbyUISound(client, entity, 0.2f);
}
});
});
......@@ -154,14 +190,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<ConcurrentHashMap<String, ChatDataManager.EntityChatData>>(){}.getType();
Type type = new TypeToken<ConcurrentHashMap<String, EntityChatData>>(){}.getType();
ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type);
// Clear receivedChunks for future use
......@@ -202,7 +238,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);
......
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;
......@@ -469,12 +472,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());
chatData = ChatDataManager.getClientInstance().getOrCreateChatData(entity.getUuidAsString(), player.getDisplayName().getString());
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);
......@@ -518,13 +529,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) {
......@@ -544,10 +555,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 {
......@@ -560,7 +571,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;
......@@ -119,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(), player.getDisplayName().getString());
// Determine area clicked inside chat bubble (top, left, right)
String hitRegion = determineHitRegion(closestHitResult.get(), closestBubbleData.position, camera, closestBubbleData.height);
......
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);
public PlayerMessage(String playerId, String playerName, String messageText, int ticks) {
super(playerId, playerName);
this.currentMessage = messageText;
this.currentLineNumber = 0;
this.tickCountdown = new AtomicInteger(ticks);
......
......@@ -12,8 +12,8 @@ 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) {
messages.put(playerUUID, new PlayerMessage(playerUUID.toString(), messageText, ticks));
public static void addMessage(UUID playerUUID, String messageText, String playerName, int ticks) {
messages.put(playerUUID, new PlayerMessage(playerUUID.toString(), playerName, messageText, ticks));
}
public static PlayerMessage getMessage(UUID playerId) {
......
......@@ -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
......@@ -73,6 +73,7 @@ public class ConfigurationHandler {
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; }
......@@ -110,5 +111,8 @@ public class ConfigurationHandler {
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; }
}
}
......@@ -46,6 +46,7 @@ public class CreatureChatCommands {
.then(registerSetCommand("url", "URL", StringArgumentType.string()))
.then(registerSetCommand("model", "Model", StringArgumentType.string()))
.then(registerSetCommand("timeout", "Timeout (seconds)", IntegerArgumentType.integer()))
.then(registerStoryCommand())
.then(registerWhitelistCommand())
.then(registerBlacklistCommand())
.then(registerHelpCommand()));
......@@ -133,6 +134,7 @@ public class CreatureChatCommands {
+ "/creaturechat url set \"<url>\" - Sets the URL\n"
+ "/creaturechat model set <model> - Sets the model\n"
+ "/creaturechat timeout set <seconds> - Sets the API timeout\n"
+ "/creaturechat story set \"<story>\" - Sets a custom story\n"
+ "/creaturechat whitelist <entityType | all | clear> - Show chat bubbles\n"
+ "/creaturechat blacklist <entityType | all | clear> - Hide chat bubbles\n"
+ "\n"
......@@ -144,6 +146,50 @@ public class CreatureChatCommands {
});
}
private static LiteralArgumentBuilder<ServerCommandSource> registerStoryCommand() {
return CommandManager.literal("story")
.requires(source -> source.hasPermissionLevel(4))
.then(CommandManager.literal("set")
.then(CommandManager.argument("value", StringArgumentType.string())
.executes(context -> {
String story = StringArgumentType.getString(context, "value");
ConfigurationHandler.Config config = new ConfigurationHandler(context.getSource().getServer()).loadConfig();
config.setStory(story); // Assuming Config has a `setStory` method
if (new ConfigurationHandler(context.getSource().getServer()).saveConfig(config, true)) {
context.getSource().sendFeedback(() -> Text.literal("Story set successfully: " + story).formatted(Formatting.GREEN), true);
return 1;
} else {
context.getSource().sendFeedback(() -> Text.literal("Failed to set story!").formatted(Formatting.RED), false);
return 0;
}
})
))
.then(CommandManager.literal("clear")
.executes(context -> {
ConfigurationHandler.Config config = new ConfigurationHandler(context.getSource().getServer()).loadConfig();
config.setStory(""); // Clear the story
if (new ConfigurationHandler(context.getSource().getServer()).saveConfig(config, true)) {
context.getSource().sendFeedback(() -> Text.literal("Story cleared successfully!").formatted(Formatting.GREEN), true);
return 1;
} else {
context.getSource().sendFeedback(() -> Text.literal("Failed to clear story!").formatted(Formatting.RED), false);
return 0;
}
}))
.then(CommandManager.literal("display")
.executes(context -> {
ConfigurationHandler.Config config = new ConfigurationHandler(context.getSource().getServer()).loadConfig();
String story = config.getStory(); // Assuming Config has a `getStory` method
if (story == null || story.isEmpty()) {
context.getSource().sendFeedback(() -> Text.literal("No story is currently set.").formatted(Formatting.RED), false);
return 0;
} else {
context.getSource().sendFeedback(() -> Text.literal("Current story: " + story).formatted(Formatting.AQUA), false);
return 1;
}
}));
}
private static <T> int setConfig(ServerCommandSource source, String settingName, T value, boolean useServerConfig, String settingDescription) {
ConfigurationHandler configHandler = new ConfigurationHandler(source.getServer());
ConfigurationHandler.Config config = configHandler.loadConfig();
......
......@@ -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.
......@@ -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
......
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.particle.ParticleEffect;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.MathHelper;
......@@ -71,7 +71,7 @@ public class LeadPlayerGoal extends PlayerBaseGoal {
String arrivedMessage = "<You have arrived at your destination>";
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(this.entity.getUuidAsString());
EntityChatData chatData = chatDataManager.getOrCreateChatData(this.entity.getUuidAsString(), this.targetEntity.getDisplayName().getString());
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, (ServerPlayerEntity) this.targetEntity, this.entity, arrivedMessage, true);
}
......@@ -130,27 +130,49 @@ public class LeadPlayerGoal extends PlayerBaseGoal {
LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints);
this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36);
if (this.currentTarget != null) {
emitParticleAt(this.currentTarget, ParticleTypes.FLAME);
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget, ParticleTypes.CLOUD, 0.5);
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget);
}
// Stop following current path (if any)
this.entity.getNavigation().stop();
}
private void emitParticleAt(Vec3d position, ParticleEffect particleType) {
private void emitParticleAt(Vec3d position, double angle) {
if (this.entity.getWorld() instanceof ServerWorld) {
ServerWorld serverWorld = (ServerWorld) this.entity.getWorld();
serverWorld.spawnParticles(particleType, position.x, position.y, position.z, 5, 0, 0, 0, 0);
// 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;
}
private void emitParticlesAlongRaycast(Vec3d start, Vec3d end, ParticleEffect particleType, double step) {
Vec3d direction = end.subtract(start).normalize();
// Emit particles along the ray from startRange to endRange
double distance = start.distanceTo(end);
for (double d = 0; d <= distance; d += step) {
Vec3d pos = start.add(direction.multiply(d));
emitParticleAt(pos, particleType);
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
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,7 +16,7 @@ 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|LEAD|FLEE|ATTACK|PROTECT|FRIENDSHIP|UNFOLLOW|UNLEAD|UNPROTECT|UNFLEE)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
......@@ -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 net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
......@@ -18,20 +20,26 @@ 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 {
private ChatDataManager.EntityChatData getChatData(LivingEntity entity) {
private EntityChatData getChatData(LivingEntity entity, PlayerEntity player) {
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
return chatDataManager.getOrCreateChatData(entity.getUuidAsString());
return chatDataManager.getOrCreateChatData(entity.getUuidAsString(), player.getDisplayName().getString());
}
@Inject(method = "canTarget(Lnet/minecraft/entity/LivingEntity;)Z", at = @At("HEAD"), cancellable = true)
private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) {
if (target instanceof PlayerEntity) {
LivingEntity thisEntity = (LivingEntity) (Object) this;
ChatDataManager.EntityChatData chatData = getChatData(thisEntity);
if (chatData.friendship > 0) {
EntityChatData entityData = getChatData(thisEntity, (PlayerEntity) target);
PlayerData playerData = entityData.getPlayerData(target.getDisplayName().getString());
if (playerData.friendship > 0) {
// Friendly creatures can't target a player
cir.setReturnValue(false);
}
......@@ -53,11 +61,11 @@ public class MixinLivingEntity {
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.EntityChatData chatData = getChatData(thisEntity);
ServerPlayerEntity player = (ServerPlayerEntity) attacker;
EntityChatData chatData = getChatData(thisEntity, player);
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();
......
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;
......@@ -52,7 +54,8 @@ public class MixinMobEntity {
// Get chat data for entity
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
EntityChatData entityData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString(), player.getDisplayName().getString());
PlayerData playerData = entityData.getPlayerData(player.getDisplayName().getString());
// Check if the player successfully interacts with an item
if (player instanceof ServerPlayerEntity) {
......@@ -72,11 +75,11 @@ public class MixinMobEntity {
String giveItemMessage = "<" + serverPlayer.getName().getString() +
action_verb + "you " + itemCount + " " + itemName + ">";
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true);
if (!entityData.characterSheet.isEmpty() && entityData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true);
}
} else if (itemStack.isEmpty() && chatData.friendship == 3) {
} else if (itemStack.isEmpty() && playerData.friendship == 3) {
// Player's hand is empty, Ride your best friend!
player.startRiding(thisEntity, true);
}
......
......@@ -6,6 +6,10 @@ 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 {
......
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
......@@ -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!",
......@@ -88,7 +84,7 @@ public class Randomizer {
"inquisitive", "cynical", "empathetic", "boisterous", "monotone", "laconic", "poetic",
"archaic", "childlike", "erudite", "streetwise", "flirtatious", "stoic", "rhetorical",
"inspirational", "goofy", "overly dramatic", "deadpan", "sing-song", "pompous",
"hyperactive", "valley girl", "robot", "pirate", "baby talk", "lolcat"
"hyperactive", "valley girl", "robot", "baby talk", "lolcat"
);
private static List<String> classes = Arrays.asList(
"warrior", "mage", "archer", "rogue", "paladin", "necromancer", "bard", "lorekeeper",
......
......@@ -2,6 +2,11 @@ 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
......@@ -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}}
......
......@@ -203,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