Commit 32293b1e by Jonathan Thomas

- Added new Goal to Talk to Player (i.e. look at them and stop moving temporarily)

- Refactored Goal Adding/Removing to make it more generic
- Added enum for Goal Priorities, since an entity can have both Talk and Follow goals at the same time.
- Updated Follow goal to no longer look at the player once they reach the player.
- Removed slowness status effect, replaced with Talk Goal
parent b6047d5a
Pipeline #11933 passed with stage
in 26 seconds
......@@ -23,7 +23,11 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* The {@code ClickHandler} class is used for the client to interact with the Entity chat UI. This class helps
* to receive messages from the server, cast rays to see what the user clicked on, and communicate these events
* back to the server.
*/
public class ClickHandler {
private static final Logger LOGGER = LoggerFactory.getLogger("mobgpt");
private static boolean wasClicked = false;
......@@ -160,7 +164,7 @@ public class ClickHandler {
ModPackets.sendUpdateLineNumber(closestEntity, chatData.currentLineNumber + ClientInit.DISPLAY_NUM_LINES);
} else if (chatData.status == ChatDataManager.ChatStatus.END) {
// End of chat (open player chat screen)
ModPackets.sendStartChat(closestEntity); // Slow down entity while chat screen is open
ModPackets.sendStartChat(closestEntity);
client.setScreen(new ChatScreen(closestEntity));
}
}
......
......@@ -3,6 +3,8 @@ package com.owlmaddie;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.FollowPlayerGoal;
import com.owlmaddie.goals.GoalPriority;
import com.owlmaddie.json.QuestJson;
import com.owlmaddie.message.Behavior;
import com.owlmaddie.message.MessageParser;
......@@ -25,6 +27,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* The {@code ChatDataManager} class manages chat data for all entities. This class also helps
* generate new messages, set entity goals, and other useful chat-related functions.
*/
public class ChatDataManager {
// Use a static instance to manage our data globally
private static final ChatDataManager SERVER_INSTANCE = new ChatDataManager(true);
......@@ -216,9 +222,10 @@ public class ChatDataManager {
// Apply behaviors to entity
if (behavior.getName().equals("FOLLOW")) {
EntityBehaviorManager.addFollowPlayerGoal(player, entity, 1.0);
FollowPlayerGoal followGoal = new FollowPlayerGoal(player, entity, 1.0);
EntityBehaviorManager.addGoal(entity, followGoal, GoalPriority.FOLLOW_PLAYER);
} else if (behavior.getName().equals("UNFOLLOW")) {
EntityBehaviorManager.removeFollowPlayerGoal(entity);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FRIENDSHIP")) {
friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
}
......
package com.owlmaddie;
import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.GoalPriority;
import com.owlmaddie.goals.TalkPlayerGoal;
import io.netty.buffer.Unpooled;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.effect.StatusEffectInstance;
import net.minecraft.entity.effect.StatusEffects;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.Identifier;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import java.util.UUID;
/**
* The {@code ModInit} class initializes this mod and defines all the server message
* identifiers. It also listens for messages from the client, and has code to send
* messages to the client.
*/
public class ModInit implements ModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("mobgpt");
public static MinecraftServer serverInstance;
......@@ -46,8 +51,9 @@ public class ModInit implements ModInitializer {
server.execute(() -> {
Entity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Slow entity
SlowEntity((LivingEntity) entity, 3.5F);
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, (MobEntity)entity, 3.5F);
EntityBehaviorManager.addGoal((MobEntity)entity, talkGoal, GoalPriority.TALK_PLAYER);
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.status == ChatDataManager.ChatStatus.NONE ||
......@@ -79,8 +85,9 @@ public class ModInit implements ModInitializer {
server.execute(() -> {
Entity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Slow entity
SlowEntity((LivingEntity) entity, 3.5F);
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, (MobEntity)entity, 3.5F);
EntityBehaviorManager.addGoal((MobEntity)entity, talkGoal, GoalPriority.TALK_PLAYER);
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.status == ChatDataManager.ChatStatus.DISPLAY) {
......@@ -100,8 +107,9 @@ public class ModInit implements ModInitializer {
server.execute(() -> {
Entity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Slow entity, so it does NOT walk away during player typing
SlowEntity((LivingEntity) entity, 7F);
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, (MobEntity)entity, 7F);
EntityBehaviorManager.addGoal((MobEntity)entity, talkGoal, GoalPriority.TALK_PLAYER);
}
});
});
......@@ -115,8 +123,9 @@ public class ModInit implements ModInitializer {
server.execute(() -> {
Entity entity = ServerEntityFinder.getEntityByUUID(player.getServerWorld(), entityId);
if (entity != null) {
// Slow entity
SlowEntity((LivingEntity) entity, 3.5F);
// Set talk to player goal (prevent entity from walking off)
TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, (MobEntity)entity, 3.5F);
EntityBehaviorManager.addGoal((MobEntity)entity, talkGoal, GoalPriority.TALK_PLAYER);
ChatDataManager.EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(entity.getUuidAsString());
if (chatData.status == ChatDataManager.ChatStatus.END) {
......@@ -193,15 +202,4 @@ public class ModInit implements ModInitializer {
}
}
}
public void SlowEntity(LivingEntity entity, float numSeconds) {
// Slow the entity temporarily (so they don't run away)
// Apply a slowness effect with a high amplifier for a short duration
// (Amplifier value must be between 0 and 127)
LOGGER.info("Apply SLOWNESS status effect to: " + entity.getType().toString());
float TPS = 20F; // ticks per second
StatusEffectInstance slowness = new StatusEffectInstance(StatusEffects.SLOWNESS, Math.round(numSeconds * TPS),
127, false, false);
entity.addStatusEffect(slowness);
}
}
\ No newline at end of file
package com.owlmaddie.goals;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.goal.GoalSelector;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.*;
/**
* The {@code EntityBehaviorManager} class keeps track of all Mob Entities which have
......@@ -18,35 +16,42 @@ import java.util.UUID;
*/
public class EntityBehaviorManager {
public static final Logger LOGGER = LoggerFactory.getLogger("mobgpt");
private static final Map<UUID, FollowPlayerGoal> followGoals = new HashMap<>();
private static final Map<UUID, List<Goal>> entityGoals = new HashMap<>();
public static void addFollowPlayerGoal(ServerPlayerEntity player, MobEntity entity, double speed) {
public static void addGoal(MobEntity entity, Goal goal, GoalPriority priority) {
if (!(entity.getWorld() instanceof ServerWorld)) {
LOGGER.debug("Attempted to add FollowPlayerGoal in a non-server world. Aborting.");
LOGGER.debug("Attempted to add a goal in a non-server world. Aborting.");
return;
}
UUID entityId = entity.getUuid();
if (!followGoals.containsKey(entityId)) {
FollowPlayerGoal goal = new FollowPlayerGoal(player, entity, speed);
GoalSelector goalSelector = GoalUtils.getGoalSelector(entity);
goalSelector.add(1, goal);
followGoals.put(entityId, goal);
LOGGER.info("FollowPlayerGoal added for entity UUID: {} with speed: {}", entityId, speed);
} else {
LOGGER.debug("FollowPlayerGoal already exists for entity UUID: {}", entityId);
}
// Use removeGoal to remove any existing goal of the same type
removeGoal(entity, goal.getClass());
// Now that any existing goal of the same type has been removed, we can add the new goal
List<Goal> goals = entityGoals.computeIfAbsent(entityId, k -> new ArrayList<>());
goals.add(goal);
GoalSelector goalSelector = GoalUtils.getGoalSelector(entity);
goalSelector.add(priority.getPriority(), goal);
LOGGER.info("Goal of type {} added to entity UUID: {}", goal.getClass().getSimpleName(), entityId);
}
public static void removeFollowPlayerGoal(MobEntity entity) {
public static void removeGoal(MobEntity entity, Class<? extends Goal> goalClass) {
UUID entityId = entity.getUuid();
if (followGoals.containsKey(entityId)) {
FollowPlayerGoal goal = followGoals.remove(entityId);
List<Goal> goals = entityGoals.get(entityId);
if (goals != null) {
GoalSelector goalSelector = GoalUtils.getGoalSelector(entity);
goalSelector.remove(goal);
LOGGER.info("FollowPlayerGoal removed for entity UUID: {}", entityId);
} else {
LOGGER.debug("No FollowPlayerGoal found for entity UUID: {} to remove", entityId);
goals.removeIf(goal -> {
if (goalClass.isInstance(goal)) {
goalSelector.remove(goal);
LOGGER.info("Goal of type {} removed for entity UUID: {}", goalClass.getSimpleName(), entityId);
return true;
}
return false;
});
}
}
}
......@@ -6,14 +6,11 @@ import net.minecraft.entity.mob.MobEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import java.util.EnumSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@code FollowPlayerGoal} class instructs a Mob Entity to follow the current player.
*/
public class FollowPlayerGoal extends Goal {
public static final Logger LOGGER = LoggerFactory.getLogger("mobgpt");
private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
......@@ -47,14 +44,13 @@ public class FollowPlayerGoal extends Goal {
@Override
public void tick() {
this.entity.getLookControl().lookAt(this.targetPlayer, 10.0F, (float) this.entity.getMaxLookPitchChange());
// Calculate the squared distance between the entity and the player
double squaredDistanceToPlayer = this.entity.squaredDistanceTo(this.targetPlayer);
// Check if the entity is further away than 4 blocks (16 when squared)
if (squaredDistanceToPlayer > 16) {
// Entity is more than 4 blocks away, start moving towards the player
// Entity is more than 4 blocks away, look at the player and start moving towards them
this.entity.getLookControl().lookAt(this.targetPlayer, 10.0F, (float) this.entity.getMaxLookPitchChange());
this.navigation.startMovingTo(this.targetPlayer, this.speed);
} else if (squaredDistanceToPlayer < 9) {
// Entity is closer than 3 blocks, stop moving to maintain distance
......
package com.owlmaddie.goals;
/**
* The {@code GoalPriority} enum sets the priorities of each type of custom Goal used in this mod.
* For example, talking to a player is higher priority than following a player.
*/
public enum GoalPriority {
// Enum constants (Goal Types) with their corresponding priority values
TALK_PLAYER(1),
FOLLOW_PLAYER(2);
private final int priority;
// Constructor for the enum to set the priority value
GoalPriority(int priority) {
this.priority = priority;
}
// Getter method to access the priority value
public int getPriority() {
return this.priority;
}
}
package com.owlmaddie.goals;
import net.minecraft.entity.ai.goal.Goal;
import net.minecraft.entity.ai.pathing.EntityNavigation;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.server.network.ServerPlayerEntity;
import java.util.EnumSet;
/**
* The {@code TalkPlayerGoal} class instructs a Mob Entity to look at a player and not move for X seconds.
*/
public class TalkPlayerGoal extends Goal {
private final MobEntity entity;
private ServerPlayerEntity targetPlayer;
private final EntityNavigation navigation;
private final double seconds;
private long startTime;
public TalkPlayerGoal(ServerPlayerEntity player, MobEntity entity, double seconds) {
this.targetPlayer = player;
this.entity = entity;
this.seconds = seconds;
this.navigation = entity.getNavigation();
this.setControls(EnumSet.of(Control.MOVE));
}
@Override
public boolean canStart() {
if (this.targetPlayer != null) {
this.startTime = System.currentTimeMillis(); // Record the start time
this.entity.getNavigation().stop(); // Stop the entity's current navigation/movement
return true;
}
return false;
}
@Override
public boolean shouldContinue() {
// Check if the target player is still valid and if the specified duration has not yet passed
return this.targetPlayer != null && this.targetPlayer.isAlive() &&
(System.currentTimeMillis() - this.startTime) < (this.seconds * 1000);
}
@Override
public void stop() {
this.targetPlayer = null;
}
@Override
public void tick() {
// Make the entity look at the player without moving towards them
this.entity.getLookControl().lookAt(this.targetPlayer, 10.0F, (float)this.entity.getMaxLookPitchChange());
// Continuously stop the entity's navigation to ensure it remains stationary
this.navigation.stop();
}
}
\ No newline at end of file
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