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 ...@@ -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 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [1.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
- Fixed a regression caused by adding a "-forge" suffix to one of our builds - Fixed a regression caused by adding a "-forge" suffix to one of our builds
......
...@@ -2,14 +2,19 @@ package com.owlmaddie; ...@@ -2,14 +2,19 @@ package com.owlmaddie;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.network.ClientPackets; import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.particle.CreatureParticleFactory;
import com.owlmaddie.particle.LeadParticleFactory;
import com.owlmaddie.ui.BubbleRenderer; import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.ClickHandler; import com.owlmaddie.ui.ClickHandler;
import com.owlmaddie.ui.PlayerMessageManager; import com.owlmaddie.ui.PlayerMessageManager;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; 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 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 * 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. * render pipeline to draw chat bubbles, text, and entity icons.
...@@ -19,6 +24,20 @@ public class ClientInit implements ClientModInitializer { ...@@ -19,6 +24,20 @@ public class ClientInit implements ClientModInitializer {
@Override @Override
public void onInitializeClient() { 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 -> { ClientTickEvents.END_CLIENT_TICK.register(client -> {
tickCounter++; tickCounter++;
PlayerMessageManager.tickUpdate(); PlayerMessageManager.tickUpdate();
......
...@@ -3,6 +3,8 @@ package com.owlmaddie.network; ...@@ -3,6 +3,8 @@ package com.owlmaddie.network;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.ui.BubbleRenderer; import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.PlayerMessageManager; import com.owlmaddie.ui.PlayerMessageManager;
import com.owlmaddie.utils.ClientEntityFinder; import com.owlmaddie.utils.ClientEntityFinder;
...@@ -20,10 +22,7 @@ import org.slf4j.LoggerFactory; ...@@ -20,10 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
/** /**
...@@ -94,41 +93,78 @@ public class ClientPackets { ...@@ -94,41 +93,78 @@ public class ClientPackets {
ClientPlayNetworking.send(ServerPackets.PACKET_C2S_SEND_CHAT, buf); 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() { public static void register() {
// Client-side packet handler, message sync // Client-side packet handler, message sync
ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_MESSAGE, (client, handler, buffer, responseSender) -> { ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_MESSAGE, (client, handler, buffer, responseSender) -> {
// Read the data from the server packet // Read the data from the server packet
UUID entityId = UUID.fromString(buffer.readString()); 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); String message = buffer.readString(32767);
int line = buffer.readInt(); int line = buffer.readInt();
String status_name = buffer.readString(32767); String status_name = buffer.readString(32767);
ChatDataManager.ChatStatus status = ChatDataManager.ChatStatus.valueOf(status_name);
String sender_name = buffer.readString(32767); 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 // Update the chat data manager on the client-side
client.execute(() -> { // Make sure to run on the client thread 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); MobEntity entity = ClientEntityFinder.getEntityByUUID(client.world, entityId);
if (entity != null) { if (entity == null) {
ChatDataManager chatDataManager = ChatDataManager.getClientInstance(); LOGGER.warn("Entity with ID '{}' not found. Skipping message processing.", entityId);
ChatDataManager.EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString()); return;
chatData.playerId = playerIdStr; }
// Get entity chat data for current entity & player
String currentPlayerName = client.player.getDisplayName().getString();
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
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()) { if (!message.isEmpty()) {
chatData.currentMessage = message; chatData.currentMessage = message;
} }
chatData.currentLineNumber = line; chatData.currentLineNumber = line;
chatData.status = ChatDataManager.ChatStatus.valueOf(status_name); chatData.status = status;
chatData.sender = ChatDataManager.ChatSender.valueOf(sender_name); chatData.sender = sender;
chatData.friendship = friendship; chatData.players = players; // friendships
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);
} }
// Play sound with volume based on distance (from player or entity)
playNearbyUISound(client, entity, 0.2f);
}); });
}); });
...@@ -154,14 +190,14 @@ public class ClientPackets { ...@@ -154,14 +190,14 @@ public class ClientPackets {
// Decompress the combined byte array to get the original JSON string // Decompress the combined byte array to get the original JSON string
String chatDataJSON = Decompression.decompressString(combined.toByteArray()); String chatDataJSON = Decompression.decompressString(combined.toByteArray());
if (chatDataJSON == null) { if (chatDataJSON == null || chatDataJSON.isEmpty()) {
LOGGER.info("Error decompressing lite JSON string from bytes"); LOGGER.warn("Received invalid or empty chat data JSON. Skipping processing.");
return; return;
} }
// Parse JSON and update client chat data // Parse JSON and update client chat data
Gson GSON = new Gson(); Gson GSON = new Gson();
Type type = new TypeToken<ConcurrentHashMap<String, ChatDataManager.EntityChatData>>(){}.getType(); Type type = new TypeToken<ConcurrentHashMap<String, EntityChatData>>(){}.getType();
ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type); ChatDataManager.getClientInstance().entityChatDataMap = GSON.fromJson(chatDataJSON, type);
// Clear receivedChunks for future use // Clear receivedChunks for future use
...@@ -202,7 +238,12 @@ public class ClientPackets { ...@@ -202,7 +238,12 @@ public class ClientPackets {
PlayerEntity player = ClientEntityFinder.getPlayerEntityFromUUID(playerId); PlayerEntity player = ClientEntityFinder.getPlayerEntityFromUUID(playerId);
// Update the player status data manager on the client-side // 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) { if (isChatOpen) {
PlayerMessageManager.openChatUI(playerId); PlayerMessageManager.openChatUI(playerId);
playNearbyUISound(client, player, 0.2f); 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; ...@@ -2,6 +2,8 @@ package com.owlmaddie.ui;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.utils.EntityHeights; import com.owlmaddie.utils.EntityHeights;
import com.owlmaddie.utils.EntityRendererAccessor; import com.owlmaddie.utils.EntityRendererAccessor;
import com.owlmaddie.utils.TextureLoader; import com.owlmaddie.utils.TextureLoader;
...@@ -9,6 +11,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; ...@@ -9,6 +11,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer; import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType; import net.minecraft.client.font.TextRenderer.TextLayerType;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.*; import net.minecraft.client.render.*;
import net.minecraft.client.render.entity.EntityRenderer; import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
...@@ -469,12 +472,20 @@ public class BubbleRenderer { ...@@ -469,12 +472,20 @@ public class BubbleRenderer {
// Get position matrix // Get position matrix
Matrix4f matrix = matrices.peek().getPositionMatrix(); Matrix4f matrix = matrices.peek().getPositionMatrix();
// Look-up greeting (if any) // Get the player
ChatDataManager.EntityChatData chatData = null; ClientPlayerEntity player = MinecraftClient.getInstance().player;
// Get chat message (if any)
EntityChatData chatData = null;
PlayerData playerData = null;
if (entity instanceof MobEntity) { 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) { } else if (entity instanceof PlayerEntity) {
chatData = PlayerMessageManager.getMessage(entity.getUuid()); 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); float minTextHeight = (ChatDataManager.DISPLAY_NUM_LINES * (fontRenderer.fontHeight + lineSpacing)) + (DISPLAY_PADDING * 2);
...@@ -518,13 +529,13 @@ public class BubbleRenderer { ...@@ -518,13 +529,13 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background (no smaller than 50F tall) // 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 // Draw face icon of entity
drawEntityIcon(matrices, entity, -82, 7, 32, 32); drawEntityIcon(matrices, entity, -82, 7, 32, 32);
// Draw Friendship status // Draw Friendship status
drawFriendshipStatus(matrices, 51, 18, 31, 21, chatData.friendship); drawFriendshipStatus(matrices, 51, 18, 31, 21, playerData.friendship);
// Draw 'arrows' & 'keyboard' buttons // Draw 'arrows' & 'keyboard' buttons
if (chatData.currentLineNumber > 0) { if (chatData.currentLineNumber > 0) {
...@@ -544,10 +555,10 @@ public class BubbleRenderer { ...@@ -544,10 +555,10 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false);
// Draw 'resume chat' button // Draw 'resume chat' button
if (chatData.friendship == 3) { if (playerData.friendship == 3) {
// Friend chat bubble // Friend chat bubble
drawIcon("button-chat-friend", matrices, -16, textHeaderHeight, 32, 17); drawIcon("button-chat-friend", matrices, -16, textHeaderHeight, 32, 17);
} else if (chatData.friendship == -3) { } else if (playerData.friendship == -3) {
// Enemy chat bubble // Enemy chat bubble
drawIcon("button-chat-enemy", matrices, -16, textHeaderHeight, 32, 17); drawIcon("button-chat-enemy", matrices, -16, textHeaderHeight, 32, 17);
} else { } else {
...@@ -560,7 +571,7 @@ public class BubbleRenderer { ...@@ -560,7 +571,7 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true); drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background // 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 // Draw face icon of player
drawPlayerIcon(matrices, entity, -75, 14, 18, 18); drawPlayerIcon(matrices, entity, -75, 14, 18, 18);
......
package com.owlmaddie.ui; package com.owlmaddie.ui;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.network.ClientPackets; import com.owlmaddie.network.ClientPackets;
import com.owlmaddie.utils.ClientEntityFinder; import com.owlmaddie.utils.ClientEntityFinder;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
...@@ -119,7 +120,7 @@ public class ClickHandler { ...@@ -119,7 +120,7 @@ public class ClickHandler {
MobEntity closestEntity = ClientEntityFinder.getEntityByUUID(client.world, closestEntityUUID); MobEntity closestEntity = ClientEntityFinder.getEntityByUUID(client.world, closestEntityUUID);
if (closestEntity != null) { if (closestEntity != null) {
// Look-up conversation // 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) // Determine area clicked inside chat bubble (top, left, right)
String hitRegion = determineHitRegion(closestHitResult.get(), closestBubbleData.position, camera, closestBubbleData.height); String hitRegion = determineHitRegion(closestHitResult.get(), closestBubbleData.position, camera, closestBubbleData.height);
......
package com.owlmaddie.ui; package com.owlmaddie.ui;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
/** /**
...@@ -8,11 +10,11 @@ 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 * many ticks to remain visible, and the message to display. Similar to an EntityChatData, but
* much simpler. * much simpler.
*/ */
public class PlayerMessage extends ChatDataManager.EntityChatData { public class PlayerMessage extends EntityChatData {
public AtomicInteger tickCountdown; public AtomicInteger tickCountdown;
public PlayerMessage(String playerId, String messageText, int ticks) { public PlayerMessage(String playerId, String playerName, String messageText, int ticks) {
super("", playerId); super(playerId, playerName);
this.currentMessage = messageText; this.currentMessage = messageText;
this.currentLineNumber = 0; this.currentLineNumber = 0;
this.tickCountdown = new AtomicInteger(ticks); this.tickCountdown = new AtomicInteger(ticks);
......
...@@ -12,8 +12,8 @@ public class PlayerMessageManager { ...@@ -12,8 +12,8 @@ public class PlayerMessageManager {
private static final ConcurrentHashMap<UUID, PlayerMessage> messages = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<UUID, PlayerMessage> messages = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<UUID, Boolean> openChatUIs = 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)); messages.put(playerUUID, new PlayerMessage(playerUUID.toString(), playerName, messageText, ticks));
} }
public static PlayerMessage getMessage(UUID playerId) { public static PlayerMessage getMessage(UUID playerId) {
......
...@@ -118,7 +118,7 @@ public class ChatGPTRequest { ...@@ -118,7 +118,7 @@ public class ChatGPTRequest {
return (int) Math.round(text.length() / 3.5); 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 // Init API & LLM details
String apiUrl = config.getUrl(); String apiUrl = config.getUrl();
String apiKey = config.getApiKey(); String apiKey = config.getApiKey();
...@@ -151,7 +151,7 @@ public class ChatGPTRequest { ...@@ -151,7 +151,7 @@ public class ChatGPTRequest {
// Iterate backwards through the message history // Iterate backwards through the message history
for (int i = messageHistory.size() - 1; i >= 0; i--) { 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 senderName = chatMessage.sender.toString().toLowerCase(Locale.ENGLISH);
String messageText = replacePlaceholders(chatMessage.message, contextData); String messageText = replacePlaceholders(chatMessage.message, contextData);
int messageTokens = estimateTokenSize(senderName + ": " + messageText); int messageTokens = estimateTokenSize(senderName + ": " + messageText);
...@@ -213,7 +213,6 @@ public class ChatGPTRequest { ...@@ -213,7 +213,6 @@ public class ChatGPTRequest {
ChatGPTResponse chatGPTResponse = gsonOutput.fromJson(response.toString(), ChatGPTResponse.class); ChatGPTResponse chatGPTResponse = gsonOutput.fromJson(response.toString(), ChatGPTResponse.class);
if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) { if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) {
String content = chatGPTResponse.choices.get(0).message.content; String content = chatGPTResponse.choices.get(0).message.content;
LOGGER.info("Generated message: " + content);
return 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 { ...@@ -73,6 +73,7 @@ public class ConfigurationHandler {
private int timeout = 10; private int timeout = 10;
private List<String> whitelist = new ArrayList<>(); private List<String> whitelist = new ArrayList<>();
private List<String> blacklist = new ArrayList<>(); private List<String> blacklist = new ArrayList<>();
private String story = "";
// Getters and setters for existing fields // Getters and setters for existing fields
public String getApiKey() { return apiKey; } public String getApiKey() { return apiKey; }
...@@ -110,5 +111,8 @@ public class ConfigurationHandler { ...@@ -110,5 +111,8 @@ public class ConfigurationHandler {
public List<String> getBlacklist() { return blacklist; } public List<String> getBlacklist() { return blacklist; }
public void setBlacklist(List<String> blacklist) { this.blacklist = 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 { ...@@ -46,6 +46,7 @@ public class CreatureChatCommands {
.then(registerSetCommand("url", "URL", StringArgumentType.string())) .then(registerSetCommand("url", "URL", StringArgumentType.string()))
.then(registerSetCommand("model", "Model", StringArgumentType.string())) .then(registerSetCommand("model", "Model", StringArgumentType.string()))
.then(registerSetCommand("timeout", "Timeout (seconds)", IntegerArgumentType.integer())) .then(registerSetCommand("timeout", "Timeout (seconds)", IntegerArgumentType.integer()))
.then(registerStoryCommand())
.then(registerWhitelistCommand()) .then(registerWhitelistCommand())
.then(registerBlacklistCommand()) .then(registerBlacklistCommand())
.then(registerHelpCommand())); .then(registerHelpCommand()));
...@@ -133,6 +134,7 @@ public class CreatureChatCommands { ...@@ -133,6 +134,7 @@ public class CreatureChatCommands {
+ "/creaturechat url set \"<url>\" - Sets the URL\n" + "/creaturechat url set \"<url>\" - Sets the URL\n"
+ "/creaturechat model set <model> - Sets the model\n" + "/creaturechat model set <model> - Sets the model\n"
+ "/creaturechat timeout set <seconds> - Sets the API timeout\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 whitelist <entityType | all | clear> - Show chat bubbles\n"
+ "/creaturechat blacklist <entityType | all | clear> - Hide chat bubbles\n" + "/creaturechat blacklist <entityType | all | clear> - Hide chat bubbles\n"
+ "\n" + "\n"
...@@ -144,6 +146,50 @@ public class CreatureChatCommands { ...@@ -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) { private static <T> int setConfig(ServerCommandSource source, String settingName, T value, boolean useServerConfig, String settingDescription) {
ConfigurationHandler configHandler = new ConfigurationHandler(source.getServer()); ConfigurationHandler configHandler = new ConfigurationHandler(source.getServer());
ConfigurationHandler.Config config = configHandler.loadConfig(); ConfigurationHandler.Config config = configHandler.loadConfig();
......
...@@ -3,16 +3,18 @@ package com.owlmaddie.goals; ...@@ -3,16 +3,18 @@ package com.owlmaddie.goals;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.ai.RangedAttackMob; import net.minecraft.entity.ai.RangedAttackMob;
import net.minecraft.entity.mob.Angerable; import net.minecraft.entity.mob.Angerable;
import java.util.concurrent.ThreadLocalRandom;
import net.minecraft.entity.mob.HostileEntity; import net.minecraft.entity.mob.HostileEntity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.GolemEntity; import net.minecraft.entity.passive.GolemEntity;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundEvents; import net.minecraft.sound.SoundEvents;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
import java.util.EnumSet; 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. * 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. * 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 { ...@@ -94,12 +96,10 @@ public class AttackPlayerGoal extends PlayerBaseGoal {
this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F); this.attackerEntity.playSound(SoundEvents.ENTITY_PLAYER_HURT, 1F, 1F);
// Spawn red particles to simulate 'injury' // Spawn red particles to simulate 'injury'
((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ParticleTypes.DAMAGE_INDICATOR, int numParticles = ThreadLocalRandom.current().nextInt(2, 7); // Random number between 2 (inclusive) and 7 (exclusive)
this.targetEntity.getX(), ((ServerWorld) this.attackerEntity.getWorld()).spawnParticles(ATTACK_PARTICLE,
this.targetEntity.getBodyY(0.5D), this.targetEntity.getX(), this.targetEntity.getBodyY(0.5D), this.targetEntity.getZ(),
this.targetEntity.getZ(), numParticles, 0.5, 0.5, 0.1, 0.4);
10, // number of particles
0.1, 0.1, 0.1, 0.2); // speed and randomness
} }
@Override @Override
......
package com.owlmaddie.goals; package com.owlmaddie.goals;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.controls.LookControls; import com.owlmaddie.controls.LookControls;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import com.owlmaddie.particle.LeadParticleEffect;
import com.owlmaddie.utils.RandomTargetFinder; import com.owlmaddie.utils.RandomTargetFinder;
import net.minecraft.entity.ai.pathing.Path; import net.minecraft.entity.ai.pathing.Path;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.mob.PathAwareEntity; 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.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
...@@ -71,7 +71,7 @@ public class LeadPlayerGoal extends PlayerBaseGoal { ...@@ -71,7 +71,7 @@ public class LeadPlayerGoal extends PlayerBaseGoal {
String arrivedMessage = "<You have arrived at your destination>"; String arrivedMessage = "<You have arrived at your destination>";
ChatDataManager chatDataManager = ChatDataManager.getServerInstance(); 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) { if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, (ServerPlayerEntity) this.targetEntity, this.entity, arrivedMessage, true); ServerPackets.generate_chat("N/A", chatData, (ServerPlayerEntity) this.targetEntity, this.entity, arrivedMessage, true);
} }
...@@ -130,27 +130,49 @@ public class LeadPlayerGoal extends PlayerBaseGoal { ...@@ -130,27 +130,49 @@ public class LeadPlayerGoal extends PlayerBaseGoal {
LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints); LOGGER.info("Waypoint " + currentWaypoint + " / " + this.totalWaypoints);
this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36); this.currentTarget = RandomTargetFinder.findRandomTarget(this.entity, 30, 24, 36);
if (this.currentTarget != null) { if (this.currentTarget != null) {
emitParticleAt(this.currentTarget, ParticleTypes.FLAME); emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget);
emitParticlesAlongRaycast(this.entity.getPos(), this.currentTarget, ParticleTypes.CLOUD, 0.5);
} }
// Stop following current path (if any) // Stop following current path (if any)
this.entity.getNavigation().stop(); this.entity.getNavigation().stop();
} }
private void emitParticleAt(Vec3d position, ParticleEffect particleType) { private void emitParticleAt(Vec3d position, double angle) {
if (this.entity.getWorld() instanceof ServerWorld) { if (this.entity.getWorld() instanceof ServerWorld) {
ServerWorld serverWorld = (ServerWorld) this.entity.getWorld(); 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, ParticleEffect particleType, double step) { private void emitParticlesAlongRaycast(Vec3d start, Vec3d end) {
Vec3d direction = end.subtract(start).normalize(); // 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 distance = start.distanceTo(end);
for (double d = 0; d <= distance; d += step) { double startRange = Math.min(5, distance);;
Vec3d pos = start.add(direction.multiply(d)); double endRange = Math.min(startRange + 10, distance);
emitParticleAt(pos, particleType); 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 { ...@@ -16,7 +16,7 @@ public class MessageParser {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat"); public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
public static ParsedMessage parseMessage(String input) { public static ParsedMessage parseMessage(String input) {
LOGGER.info("Parsing message: {}", input); LOGGER.debug("Parsing message: {}", input);
StringBuilder cleanedMessage = new StringBuilder(); StringBuilder cleanedMessage = new StringBuilder();
List<Behavior> behaviors = new ArrayList<>(); List<Behavior> behaviors = new ArrayList<>();
Pattern pattern = Pattern.compile("[<*](FOLLOW|LEAD|FLEE|ATTACK|PROTECT|FRIENDSHIP|UNFOLLOW|UNLEAD|UNPROTECT|UNFLEE)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE); Pattern pattern = Pattern.compile("[<*](FOLLOW|LEAD|FLEE|ATTACK|PROTECT|FRIENDSHIP|UNFOLLOW|UNLEAD|UNPROTECT|UNFLEE)[:\\s]*(\\s*[+-]?\\d+)?[>*]", Pattern.CASE_INSENSITIVE);
...@@ -29,7 +29,7 @@ public class MessageParser { ...@@ -29,7 +29,7 @@ public class MessageParser {
argument = Integer.valueOf(matcher.group(2)); argument = Integer.valueOf(matcher.group(2));
} }
behaviors.add(new Behavior(behaviorName, argument)); 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, ""); matcher.appendReplacement(cleanedMessage, "");
} }
...@@ -40,7 +40,7 @@ public class MessageParser { ...@@ -40,7 +40,7 @@ public class MessageParser {
// Remove all occurrences of "<>" and "**" (if any) // Remove all occurrences of "<>" and "**" (if any)
displayMessage = displayMessage.replaceAll("<>", "").replaceAll("\\*\\*", "").trim(); displayMessage = displayMessage.replaceAll("<>", "").replaceAll("\\*\\*", "").trim();
LOGGER.info("Cleaned message: {}", displayMessage); LOGGER.debug("Cleaned message: {}", displayMessage);
return new ParsedMessage(displayMessage, input.trim(), behaviors); return new ParsedMessage(displayMessage, input.trim(), behaviors);
} }
......
package com.owlmaddie.mixin; package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity; import net.minecraft.entity.LivingEntity;
...@@ -18,20 +20,26 @@ import org.spongepowered.asm.mixin.injection.Inject; ...@@ -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.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 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) @Mixin(LivingEntity.class)
public class MixinLivingEntity { public class MixinLivingEntity {
private ChatDataManager.EntityChatData getChatData(LivingEntity entity) { private EntityChatData getChatData(LivingEntity entity, PlayerEntity player) {
ChatDataManager chatDataManager = ChatDataManager.getServerInstance(); 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) @Inject(method = "canTarget(Lnet/minecraft/entity/LivingEntity;)Z", at = @At("HEAD"), cancellable = true)
private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) { private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) {
if (target instanceof PlayerEntity) { if (target instanceof PlayerEntity) {
LivingEntity thisEntity = (LivingEntity) (Object) this; LivingEntity thisEntity = (LivingEntity) (Object) this;
ChatDataManager.EntityChatData chatData = getChatData(thisEntity); EntityChatData entityData = getChatData(thisEntity, (PlayerEntity) target);
if (chatData.friendship > 0) { PlayerData playerData = entityData.getPlayerData(target.getDisplayName().getString());
if (playerData.friendship > 0) {
// Friendly creatures can't target a player // Friendly creatures can't target a player
cir.setReturnValue(false); cir.setReturnValue(false);
} }
...@@ -53,11 +61,11 @@ public class MixinLivingEntity { ...@@ -53,11 +61,11 @@ public class MixinLivingEntity {
if (attacker instanceof PlayerEntity && thisEntity instanceof MobEntity && !thisEntity.isDead()) { if (attacker instanceof PlayerEntity && thisEntity instanceof MobEntity && !thisEntity.isDead()) {
// Generate attacked message (only if the previous user message was not an attacked message) // Generate attacked message (only if the previous user message was not an attacked message)
// We don't want to constantly generate messages during a prolonged, multi-damage event // We don't want to constantly generate messages during a prolonged, multi-damage event
ChatDataManager.EntityChatData chatData = getChatData(thisEntity); ServerPlayerEntity player = (ServerPlayerEntity) attacker;
EntityChatData chatData = getChatData(thisEntity, player);
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < ChatDataManager.MAX_AUTOGENERATE_RESPONSES) { if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < ChatDataManager.MAX_AUTOGENERATE_RESPONSES) {
// Only auto-generate a response to being attacked if chat data already exists // Only auto-generate a response to being attacked if chat data already exists
// and this is the first attack event. // and this is the first attack event.
ServerPlayerEntity player = (ServerPlayerEntity) attacker;
ItemStack weapon = player.getMainHandStack(); ItemStack weapon = player.getMainHandStack();
String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString(); String weaponName = weapon.isEmpty() ? "with fists" : "with " + weapon.getItem().toString();
......
package com.owlmaddie.mixin; package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.network.ServerPackets; import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity; import net.minecraft.entity.passive.TameableEntity;
...@@ -52,7 +54,8 @@ public class MixinMobEntity { ...@@ -52,7 +54,8 @@ public class MixinMobEntity {
// Get chat data for entity // Get chat data for entity
ChatDataManager chatDataManager = ChatDataManager.getServerInstance(); 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 // Check if the player successfully interacts with an item
if (player instanceof ServerPlayerEntity) { if (player instanceof ServerPlayerEntity) {
...@@ -72,11 +75,11 @@ public class MixinMobEntity { ...@@ -72,11 +75,11 @@ public class MixinMobEntity {
String giveItemMessage = "<" + serverPlayer.getName().getString() + String giveItemMessage = "<" + serverPlayer.getName().getString() +
action_verb + "you " + itemCount + " " + itemName + ">"; action_verb + "you " + itemCount + " " + itemName + ">";
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) { if (!entityData.characterSheet.isEmpty() && entityData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true); 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's hand is empty, Ride your best friend!
player.startRiding(thisEntity, true); player.startRiding(thisEntity, true);
} }
......
...@@ -6,6 +6,10 @@ import net.minecraft.village.VillagerGossips; ...@@ -6,6 +6,10 @@ import net.minecraft.village.VillagerGossips;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; 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) @Mixin(VillagerEntity.class)
public abstract class MixinVillagerEntity implements VillagerEntityAccessor { 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 { ...@@ -33,12 +33,8 @@ public class Randomizer {
"<clears throat>", "<clears throat>",
"<peers over your shoulder>", "<peers over your shoulder>",
"<fakes a smile>", "<fakes a smile>",
"<checks the time>",
"<doodles in the air>",
"<mutters under breath>", "<mutters under breath>",
"<adjusts an imaginary tie>", "<counts imaginary stars>"
"<counts imaginary stars>",
"<plays with a nonexistent pet>"
); );
private static List<String> errorResponseMessages = Arrays.asList( private static List<String> errorResponseMessages = Arrays.asList(
"Seems like my words got lost in the End. Check out http://discord.creaturechat.com for clues!", "Seems like my words got lost in the End. Check out http://discord.creaturechat.com for clues!",
...@@ -88,7 +84,7 @@ public class Randomizer { ...@@ -88,7 +84,7 @@ public class Randomizer {
"inquisitive", "cynical", "empathetic", "boisterous", "monotone", "laconic", "poetic", "inquisitive", "cynical", "empathetic", "boisterous", "monotone", "laconic", "poetic",
"archaic", "childlike", "erudite", "streetwise", "flirtatious", "stoic", "rhetorical", "archaic", "childlike", "erudite", "streetwise", "flirtatious", "stoic", "rhetorical",
"inspirational", "goofy", "overly dramatic", "deadpan", "sing-song", "pompous", "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( private static List<String> classes = Arrays.asList(
"warrior", "mage", "archer", "rogue", "paladin", "necromancer", "bard", "lorekeeper", "warrior", "mage", "archer", "rogue", "paladin", "necromancer", "bard", "lorekeeper",
......
...@@ -2,6 +2,11 @@ package com.owlmaddie.utils; ...@@ -2,6 +2,11 @@ package com.owlmaddie.utils;
import net.minecraft.village.VillagerGossips; 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 { public interface VillagerEntityAccessor {
VillagerGossips getGossip(); 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 ...@@ -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 format below (including - dashes), and DO NOT output any intro text. If a language is mentioned, generate the
"Short Greeting" entirely in that language. "Short Greeting" entirely in that language.
{{story}}
Be extremely creative! Include a short initial greeting (as spoken by the character using their personality Be extremely creative! Include a short initial greeting (as spoken by the character using their personality
traits and speaking style / tone). traits and speaking style / tone).
......
...@@ -3,6 +3,8 @@ Please do NOT break the 4th wall and leverage the entity's character sheet below ...@@ -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 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). when relevant. IMPORTANT: Always generate responses in player's language (if valid).
{{story}}
Entity Character Sheet: Entity Character Sheet:
- Name: {{entity_name}} - Name: {{entity_name}}
- Personality: {{entity_personality}} - Personality: {{entity_personality}}
......
...@@ -203,7 +203,7 @@ public class BehaviorTests { ...@@ -203,7 +203,7 @@ public class BehaviorTests {
// Add test message // Add test message
for (String message : messages) { for (String message : messages) {
entityTestData.addMessage(message, ChatDataManager.ChatSender.USER); entityTestData.addMessage(message, ChatDataManager.ChatSender.USER, "TestPlayer1");
} }
// Get prompt // Get prompt
......
...@@ -3,6 +3,7 @@ package com.owlmaddie.utils; ...@@ -3,6 +3,7 @@ package com.owlmaddie.utils;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager; import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.ChatMessage;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type; import java.lang.reflect.Type;
...@@ -26,7 +27,7 @@ public class EntityTestData { ...@@ -26,7 +27,7 @@ public class EntityTestData {
public String currentMessage; public String currentMessage;
public int currentLineNumber; public int currentLineNumber;
public ChatDataManager.ChatStatus status; public ChatDataManager.ChatStatus status;
public List<ChatDataManager.ChatMessage> previousMessages; public List<ChatMessage> previousMessages;
public String characterSheet; public String characterSheet;
public ChatDataManager.ChatSender sender; public ChatDataManager.ChatSender sender;
public int friendship; // -3 to 3 (0 = neutral) public int friendship; // -3 to 3 (0 = neutral)
...@@ -59,12 +60,12 @@ public class EntityTestData { ...@@ -59,12 +60,12 @@ public class EntityTestData {
} }
// Add a message to the history and update the current message // 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) // Truncate message (prevent crazy long messages... just in case)
String truncatedMessage = message.substring(0, Math.min(message.length(), ChatDataManager.MAX_CHAR_IN_USER_MESSAGE)); String truncatedMessage = message.substring(0, Math.min(message.length(), ChatDataManager.MAX_CHAR_IN_USER_MESSAGE));
// Add message to history // 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 // Set new message and reset line number of displayed text
currentMessage = truncatedMessage; 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