Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,562 @@
package com.tiedup.remake.dialogue.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.dialogue.DialogueContext;
import com.tiedup.remake.dialogue.DialogueManager;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.dialogue.IDialogueSpeaker;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.NpcTypeHelper;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.personality.PersonalityType;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
/**
* Manages interactive conversations between players and NPCs.
* Handles topic availability, dialogue retrieval, conversation state,
* cooldowns, refusals, and topic effects.
*
* Phase 5: Enhanced Conversation System
*/
public class ConversationManager {
/**
* Active conversations: player UUID -> conversation state
*/
private static final Map<UUID, ConversationState> activeConversations =
new HashMap<>();
/**
* Maximum distance for conversation
*/
private static final double MAX_CONVERSATION_DISTANCE = 5.0;
/**
* Conversation state holder.
*/
public static class ConversationState {
private final UUID speakerEntityId;
private final ResourceKey<Level> speakerDimension;
private final long startTime;
private ConversationTopic lastTopic;
private int messageCount;
public ConversationState(
UUID speakerEntityId,
ResourceKey<Level> speakerDimension
) {
this.speakerEntityId = speakerEntityId;
this.speakerDimension = speakerDimension;
this.startTime = System.currentTimeMillis();
this.messageCount = 0;
}
public UUID getSpeakerEntityId() {
return speakerEntityId;
}
public ResourceKey<Level> getSpeakerDimension() {
return speakerDimension;
}
public long getStartTime() {
return startTime;
}
public ConversationTopic getLastTopic() {
return lastTopic;
}
public void setLastTopic(ConversationTopic topic) {
this.lastTopic = topic;
this.messageCount++;
}
public int getMessageCount() {
return messageCount;
}
}
/**
* Check if a player can start a conversation with a speaker.
* Does not check refusal reasons - use checkRefusal for that.
*
* @param speaker The NPC to converse with
* @param player The player
* @return true if conversation can start (basic checks only)
*/
public static boolean canConverse(IDialogueSpeaker speaker, Player player) {
if (speaker == null || player == null) {
return false;
}
// Check distance
if (speaker.asEntity().distanceTo(player) > MAX_CONVERSATION_DISTANCE) {
return false;
}
// Check if speaker is alive
if (!speaker.asEntity().isAlive()) {
return false;
}
// Check if player is already in a conversation
ConversationState current = activeConversations.get(player.getUUID());
if (current != null) {
// Allow if it's with the same speaker
if (
!current
.getSpeakerEntityId()
.equals(speaker.asEntity().getUUID())
) {
return false;
}
}
return true;
}
/**
* Check if the NPC will refuse to talk and why.
*
* @param speaker The NPC
* @param player The player
* @return Refusal reason (NONE if willing to talk)
*/
public static ConversationRefusalReason checkRefusal(
IDialogueSpeaker speaker,
Player player
) {
// No refusal system — NPCs always talk
return ConversationRefusalReason.NONE;
}
/**
* Start a conversation between a player and a speaker.
*
* @param speaker The NPC to converse with
* @param player The player
* @return true if conversation started successfully
*/
public static boolean startConversation(
IDialogueSpeaker speaker,
Player player
) {
if (!canConverse(speaker, player)) {
return false;
}
UUID playerId = player.getUUID();
UUID speakerId = speaker.asEntity().getUUID();
ResourceKey<Level> dimension = speaker.asEntity().level().dimension();
// Create new conversation state
ConversationState state = new ConversationState(speakerId, dimension);
activeConversations.put(playerId, state);
TiedUpMod.LOGGER.info(
"[ConversationManager] Started conversation: player={}, speaker={}",
player.getName().getString(),
speaker.getDialogueName()
);
return true;
}
/**
* End the current conversation for a player.
* Starts cooldown on the NPC's conversation memory.
*
* @param player The player
* @param damsel The damsel (can be null)
*/
public static void endConversation(
Player player,
@Nullable EntityDamsel damsel
) {
ConversationState state = activeConversations.remove(player.getUUID());
if (state != null) {
TiedUpMod.LOGGER.info(
"[ConversationManager] Ended conversation: player={}, messages={}",
player.getName().getString(),
state.getMessageCount()
);
}
// No conversation memory/cooldown system
}
/**
* End the current conversation for a player (legacy overload).
*
* @param player The player
*/
public static void endConversation(Player player) {
endConversation(player, null);
}
/**
* Check if a player is in an active conversation.
*
* @param player The player
* @return true if in conversation
*/
public static boolean isInConversation(Player player) {
return activeConversations.containsKey(player.getUUID());
}
/**
* Get the current conversation state for a player.
*
* @param player The player
* @return Conversation state, or null if not in conversation
*/
@Nullable
public static ConversationState getConversationState(Player player) {
return activeConversations.get(player.getUUID());
}
/**
* Get all available conversation topics for a speaker.
*
* @param speaker The NPC
* @param player The player
* @return List of available topics
*/
public static List<ConversationTopic> getAvailableTopics(
IDialogueSpeaker speaker,
Player player
) {
DialogueContext context = DialogueBridge.buildContext(speaker, player);
Set<ConversationTopic> topics = ConversationTopic.getAvailableTopics(
context
);
// Sort by category, then by name within category
return topics
.stream()
.sorted((a, b) -> {
int catCompare = a.getCategory().compareTo(b.getCategory());
if (catCompare != 0) return catCompare;
return a.getDisplayText().compareTo(b.getDisplayText());
})
.collect(Collectors.toList());
}
/**
* Get available topics by category.
*
* @param speaker The NPC
* @param player The player
* @return Map of category -> topics
*/
public static Map<
ConversationTopic.Category,
List<ConversationTopic>
> getTopicsByCategory(IDialogueSpeaker speaker, Player player) {
DialogueContext context = DialogueBridge.buildContext(speaker, player);
return ConversationTopic.getAvailableTopics(context)
.stream()
.collect(Collectors.groupingBy(ConversationTopic::getCategory));
}
/**
* Get topic effectiveness for UI display.
*
* @param speaker The NPC
* @param player The player
* @param topic The topic
* @return Effectiveness multiplier (0.2 to 1.0)
*/
public static float getTopicEffectiveness(
IDialogueSpeaker speaker,
Player player,
ConversationTopic topic
) {
return 1.0f;
}
/**
* Send a conversation topic to a speaker and get the response.
*
* @param speaker The NPC
* @param player The player
* @param topic The selected topic
* @return The response text, or null if failed
*/
@Nullable
public static String sendTopic(
IDialogueSpeaker speaker,
Player player,
ConversationTopic topic
) {
if (!canConverse(speaker, player)) {
TiedUpMod.LOGGER.warn(
"[ConversationManager] Cannot send topic: conversation not valid"
);
return null;
}
DialogueContext context = DialogueBridge.buildContext(speaker, player);
// Check if topic is available
if (!topic.isAvailableFor(context)) {
TiedUpMod.LOGGER.warn(
"[ConversationManager] Topic {} not available for context",
topic.name()
);
return null;
}
// Get response from DialogueManager
String response = DialogueManager.getDialogue(
topic.getDialogueId(),
context
);
if (response == null) {
TiedUpMod.LOGGER.warn(
"[ConversationManager] No dialogue found for topic: {}",
topic.getDialogueId()
);
return null;
}
// Update conversation state
ConversationState state = activeConversations.get(player.getUUID());
if (state != null) {
state.setLastTopic(topic);
}
// Apply effects for action topics
if (speaker.asEntity() instanceof EntityDamsel damsel && NpcTypeHelper.isDamselOnly(speaker.asEntity())) {
applyTopicEffects(damsel, player, topic);
}
return response;
}
/**
* Apply comprehensive topic effects based on the topic selected.
* Uses TopicEffect system with personality modifiers and fatigue.
*
* @param damsel The damsel entity
* @param player The player
* @param topic The topic that was selected
*/
private static void applyTopicEffects(
EntityDamsel damsel,
Player player,
ConversationTopic topic
) {
PersonalityState state = damsel.getPersonalityState();
if (state == null) {
return;
}
// Get base effect for topic
TopicEffect baseEffect = TopicEffect.forTopic(topic);
// Apply personality modifier
PersonalityType personality = state.getPersonality();
TopicEffect finalEffect = baseEffect.withPersonalityModifier(
personality,
topic
);
// Apply mood effect only (no relationship/resentment/memory)
if (finalEffect.hasEffect() && finalEffect.moodChange() != 0) {
state.modifyMood(finalEffect.moodChange());
}
}
/**
* Send a topic and display the response to the player.
* This is the main method to use from network packets.
*
* @param speaker The NPC
* @param player The server player
* @param topic The selected topic
*/
public static void handleTopicSelection(
IDialogueSpeaker speaker,
ServerPlayer player,
ConversationTopic topic
) {
String response = sendTopic(speaker, player, topic);
if (
response != null &&
speaker.asEntity() instanceof EntityDamsel damsel &&
NpcTypeHelper.isDamselOnly(speaker.asEntity())
) {
// Send the response as a dialogue message
EntityDialogueManager.talkTo(damsel, player, response);
}
}
/**
* Clean up conversation state for a disconnecting player.
* Called from {@link com.tiedup.remake.events.lifecycle.PlayerDisconnectHandler}.
*
* @param playerId UUID of the disconnecting player
*/
public static void cleanupPlayer(java.util.UUID playerId) {
ConversationState removed = activeConversations.remove(playerId);
if (removed != null) {
TiedUpMod.LOGGER.debug(
"[ConversationManager] Cleaned up conversation for disconnecting player {}",
playerId
);
}
}
/**
* Clean up stale conversations (called periodically).
*/
public static void cleanupStaleConversations() {
long now = System.currentTimeMillis();
long maxAge = 5 * 60 * 1000; // 5 minutes
activeConversations
.entrySet()
.removeIf(entry -> {
if (now - entry.getValue().getStartTime() > maxAge) {
TiedUpMod.LOGGER.debug(
"[ConversationManager] Cleaned up stale conversation for player {}",
entry.getKey()
);
return true;
}
return false;
});
}
/**
* Open a conversation GUI for a player with an NPC.
* Called from server side - sends packet to open client GUI.
* Checks for refusal reasons before opening.
*
* @param speaker The NPC to converse with
* @param player The server player
* @return true if conversation was opened
*/
public static boolean openConversation(
IDialogueSpeaker speaker,
ServerPlayer player
) {
if (!canConverse(speaker, player)) {
TiedUpMod.LOGGER.warn(
"[ConversationManager] Cannot open conversation: canConverse returned false"
);
return false;
}
// Check for refusal
ConversationRefusalReason refusal = checkRefusal(speaker, player);
if (refusal != ConversationRefusalReason.NONE) {
// Send refusal dialogue to player
if (
refusal.hasDialogue() &&
speaker.asEntity() instanceof EntityDamsel damsel &&
NpcTypeHelper.isDamselOnly(speaker.asEntity())
) {
DialogueContext context = DialogueBridge.buildContext(
speaker,
player
);
String refusalMsg = DialogueManager.getDialogue(
refusal.getDialogueId(),
context
);
if (refusalMsg != null) {
EntityDialogueManager.talkTo(damsel, player, refusalMsg);
} else {
// Fallback message
EntityDialogueManager.talkTo(
damsel,
player,
getDefaultRefusalMessage(refusal)
);
}
}
TiedUpMod.LOGGER.info(
"[ConversationManager] Conversation refused: player={}, reason={}",
player.getName().getString(),
refusal.name()
);
return false;
}
// Start the conversation state
if (!startConversation(speaker, player)) {
return false;
}
// Get available topics
List<ConversationTopic> topics = getAvailableTopics(speaker, player);
if (topics.isEmpty()) {
TiedUpMod.LOGGER.warn(
"[ConversationManager] No available topics for conversation"
);
endConversation(player);
return false;
}
// Send packet to open conversation GUI
com.tiedup.remake.network.ModNetwork.sendToPlayer(
new com.tiedup.remake.network.conversation.PacketOpenConversation(
speaker.asEntity().getId(),
speaker.getDialogueName(),
topics
),
player
);
TiedUpMod.LOGGER.info(
"[ConversationManager] Opened conversation GUI for {} with {} ({} topics)",
player.getName().getString(),
speaker.getDialogueName(),
topics.size()
);
return true;
}
/**
* Get default refusal message when dialogue is not available.
*
* @param reason The refusal reason
* @return Default message
*/
private static String getDefaultRefusalMessage(
ConversationRefusalReason reason
) {
return switch (reason) {
case COOLDOWN -> "*looks away* We just talked...";
case LOW_MOOD -> "*stares at the ground* I don't feel like talking...";
case HIGH_RESENTMENT -> "*glares silently*";
case FEAR_OF_PLAYER -> "*looks away nervously*";
case EXHAUSTED -> "*yawns* Too tired...";
case TOPIC_LIMIT -> "*sighs* I'm tired of talking...";
default -> "...";
};
}
}

View File

@@ -0,0 +1,100 @@
package com.tiedup.remake.dialogue.conversation;
/**
* Reasons why an NPC might refuse to engage in conversation.
* Each reason has a corresponding dialogue ID for personality-specific responses.
*
* Phase 5: Enhanced Conversation System
*/
public enum ConversationRefusalReason {
/** No refusal - NPC will talk */
NONE("", false),
/** Cooldown active - recently finished talking */
COOLDOWN("conversation.refusal.cooldown", false),
/** Mood too low (< 20) - not feeling talkative */
LOW_MOOD("conversation.refusal.low_mood", true),
/** Resentment too high (> 70) - silent treatment */
HIGH_RESENTMENT("conversation.refusal.resentment", true),
/** Fear too high (> 60) - too scared to talk */
FEAR_OF_PLAYER("conversation.refusal.fear", true),
/** Rest too low (< 20) - too exhausted to talk */
EXHAUSTED("conversation.refusal.exhausted", true),
/** Topic limit reached - tired of talking this session */
TOPIC_LIMIT("conversation.refusal.tired", true);
/** Dialogue ID for the refusal message (personality-specific) */
private final String dialogueId;
/** Whether this refusal can be shown to the player as a message */
private final boolean hasDialogue;
ConversationRefusalReason(String dialogueId, boolean hasDialogue) {
this.dialogueId = dialogueId;
this.hasDialogue = hasDialogue;
}
/**
* Get the dialogue ID for this refusal reason.
* Used to fetch personality-specific refusal messages.
*
* @return Dialogue ID string
*/
public String getDialogueId() {
return dialogueId;
}
/**
* Whether this refusal has associated dialogue.
*
* @return true if there's a dialogue response
*/
public boolean hasDialogue() {
return hasDialogue;
}
/**
* Get the translation key for the reason (for UI display).
*
* @return Translation key
*/
public String getTranslationKey() {
return "conversation.refusal." + this.name().toLowerCase();
}
/**
* Get how long (in ticks) before the player can try again.
* Returns 0 for reasons that aren't time-based.
*
* @return Retry delay in ticks, or 0
*/
public int getRetryDelay() {
return switch (this) {
case COOLDOWN -> 1200; // 1 minute cooldown in ticks
case TOPIC_LIMIT -> 200; // 10 seconds
default -> 0; // State-based, no fixed delay
};
}
/**
* Get a suggestion for the player on how to resolve this refusal.
*
* @return Translation key for the suggestion
*/
public String getSuggestionKey() {
return switch (this) {
case COOLDOWN -> "conversation.suggestion.wait";
case LOW_MOOD -> "conversation.suggestion.improve_mood";
case HIGH_RESENTMENT -> "conversation.suggestion.reduce_resentment";
case FEAR_OF_PLAYER -> "conversation.suggestion.be_gentle";
case EXHAUSTED -> "conversation.suggestion.let_rest";
case TOPIC_LIMIT -> "conversation.suggestion.end_conversation";
default -> "";
};
}
}

View File

@@ -0,0 +1,292 @@
package com.tiedup.remake.dialogue.conversation;
import com.tiedup.remake.dialogue.DialogueContext;
import com.tiedup.remake.dialogue.SpeakerType;
import java.util.EnumSet;
import java.util.Set;
/**
* Simplified conversation topics for interactive dialogue.
* 8 core topics with significant effects, organized in 2 categories.
*
* Phase 5: Enhanced Conversation System
*/
public enum ConversationTopic {
// === ACTIONS (Always visible) ===
COMPLIMENT(
"conversation.compliment",
"Compliment",
Category.ACTION,
null,
null,
"You look nice today.",
"Give a compliment"
),
COMFORT(
"conversation.comfort",
"Comfort",
Category.ACTION,
null,
50,
"It's going to be okay.",
"Comfort them"
),
PRAISE(
"conversation.praise",
"Praise",
Category.ACTION,
null,
null,
"You did well.",
"Praise their behavior"
),
SCOLD(
"conversation.scold",
"Scold",
Category.ACTION,
null,
null,
"That was wrong.",
"Scold them"
),
THREATEN(
"conversation.threaten",
"Threaten",
Category.ACTION,
null,
null,
"Don't make me...",
"Make a threat"
),
TEASE(
"conversation.tease",
"Tease",
Category.ACTION,
null,
null,
"Having fun?",
"Tease them playfully"
),
// === QUESTIONS (Basic inquiries) ===
HOW_ARE_YOU(
"conversation.how_are_you",
"How are you?",
Category.QUESTION,
null,
null,
"How are you feeling?",
"Ask about their state"
),
WHATS_WRONG(
"conversation.whats_wrong",
"What's wrong?",
Category.QUESTION,
null,
40,
"Something seems off...",
"Ask what's bothering them"
);
/**
* Topic categories for UI organization.
*/
public enum Category {
ACTION("Actions"),
QUESTION("Questions");
private final String displayName;
Category(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
private final String dialogueId;
private final String displayText;
private final Category category;
private final Integer minMood;
private final Integer maxMood;
private final String playerText;
private final String description;
ConversationTopic(
String dialogueId,
String displayText,
Category category,
Integer minMood,
Integer maxMood,
String playerText,
String description
) {
this.dialogueId = dialogueId;
this.displayText = displayText;
this.category = category;
this.minMood = minMood;
this.maxMood = maxMood;
this.playerText = playerText;
this.description = description;
}
/**
* Get the dialogue ID for this topic (used to fetch NPC response).
*/
public String getDialogueId() {
return dialogueId;
}
/**
* Get the display text shown in the UI button.
*/
public String getDisplayText() {
return displayText;
}
/**
* Get the topic category.
*/
public Category getCategory() {
return category;
}
/**
* Get the text the player says when selecting this topic.
*/
public String getPlayerText() {
return playerText;
}
/**
* Get a description of what this topic does.
*/
public String getDescription() {
return description;
}
/**
* Check if this topic is available for the given context.
*
* @param context The dialogue context
* @return true if the topic can be used
*/
public boolean isAvailableFor(DialogueContext context) {
// Check speaker type
if (!isValidForSpeaker(context.getSpeakerType())) {
return false;
}
// Check mood bounds
int mood = context.getMood();
if (minMood != null && mood < minMood) {
return false;
}
if (maxMood != null && mood > maxMood) {
return false;
}
return true;
}
/**
* Check if this topic is valid for a speaker type.
*
* @param speakerType The speaker type
* @return true if valid
*/
public boolean isValidForSpeaker(SpeakerType speakerType) {
// All 8 topics work with DAMSEL
return speakerType == SpeakerType.DAMSEL;
}
/**
* Check if this is an action topic (as opposed to a question).
*
* @return true if this is an action
*/
public boolean isAction() {
return category == Category.ACTION;
}
/**
* Check if this is a positive action (compliment, comfort, praise).
*
* @return true if positive
*/
public boolean isPositive() {
return this == COMPLIMENT || this == COMFORT || this == PRAISE;
}
/**
* Check if this is a negative action (scold, threaten).
*
* @return true if negative
*/
public boolean isNegative() {
return this == SCOLD || this == THREATEN;
}
/**
* Get all topics available for a given context.
*
* @param context The dialogue context
* @return Set of available topics
*/
public static Set<ConversationTopic> getAvailableTopics(
DialogueContext context
) {
Set<ConversationTopic> available = EnumSet.noneOf(
ConversationTopic.class
);
for (ConversationTopic topic : values()) {
if (topic.isAvailableFor(context)) {
available.add(topic);
}
}
return available;
}
/**
* Get topics by category that are available for a context.
*
* @param context The dialogue context
* @param category The category to filter by
* @return Set of available topics in that category
*/
public static Set<ConversationTopic> getAvailableByCategory(
DialogueContext context,
Category category
) {
Set<ConversationTopic> available = EnumSet.noneOf(
ConversationTopic.class
);
for (ConversationTopic topic : values()) {
if (topic.category == category && topic.isAvailableFor(context)) {
available.add(topic);
}
}
return available;
}
/**
* Get the translation key for this topic's display text.
*
* @return Translation key
*/
public String getTranslationKey() {
return "conversation.topic." + this.name().toLowerCase();
}
}

View File

@@ -0,0 +1,144 @@
package com.tiedup.remake.dialogue.conversation;
/**
* Enum defining requests that a pet (player) can make to their Master NPC.
* This is the inverse of ConversationTopic - these are actions initiated by the player.
*
* Each request has:
* - An ID for network serialization
* - Display text shown in the menu
* - Player speech text (what the player "says" when selecting)
* - Response dialogue ID for the Master's response
*/
public enum PetRequest {
/**
* Ask Master for food. Triggers feeding sequence with bowl placement.
*/
REQUEST_FOOD(
"request.food",
"Ask for food",
"Please, may I have something to eat?",
"petplay.feeding"
),
/**
* Ask Master to rest. Triggers resting sequence with pet bed placement.
*/
REQUEST_SLEEP(
"request.sleep",
"Ask to rest",
"I'm tired... may I rest?",
"petplay.resting"
),
/**
* Request a walk where the pet leads (Master follows).
*/
REQUEST_WALK_PASSIVE(
"request.walk_passive",
"Request a walk (you lead)",
"Can we go for a walk? I'll lead the way.",
"petplay.walk_passive"
),
/**
* Request a walk where the Master leads (Master walks, pulls pet).
*/
REQUEST_WALK_ACTIVE(
"request.walk_active",
"Request a walk (Master leads)",
"Can we go for a walk? You lead.",
"petplay.walk_active"
),
/**
* Ask Master to tie you up.
*/
REQUEST_TIE(
"request.tie",
"Ask to be tied",
"Would you tie me up, please?",
"petplay.tie_request"
),
/**
* Ask Master to untie you.
*/
REQUEST_UNTIE(
"request.untie",
"Ask to be untied",
"May I be untied, please?",
"petplay.untie_request"
),
/**
* End the conversation gracefully.
*/
END_CONVERSATION(
"request.end",
"End conversation",
"Thank you, Master.",
"petplay.dismiss"
);
private final String id;
private final String displayText;
private final String playerText;
private final String responseDialogueId;
PetRequest(
String id,
String displayText,
String playerText,
String responseDialogueId
) {
this.id = id;
this.displayText = displayText;
this.playerText = playerText;
this.responseDialogueId = responseDialogueId;
}
/**
* Get the unique ID for this request (used in network packets).
*/
public String getId() {
return id;
}
/**
* Get the display text shown in the request menu button.
*/
public String getDisplayText() {
return displayText;
}
/**
* Get the text that represents what the player "says" when making this request.
* This is displayed as player speech before the Master responds.
*/
public String getPlayerText() {
return playerText;
}
/**
* Get the dialogue ID that the Master should respond with.
*/
public String getResponseDialogueId() {
return responseDialogueId;
}
/**
* Find a PetRequest by its ID.
*
* @param id The request ID
* @return The matching PetRequest, or null if not found
*/
public static PetRequest fromId(String id) {
for (PetRequest request : values()) {
if (request.id.equals(id)) {
return request;
}
}
return null;
}
}

View File

@@ -0,0 +1,290 @@
package com.tiedup.remake.dialogue.conversation;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.dialogue.DialogueBridge;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ai.master.MasterPlaceBlockGoal;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.master.PacketOpenPetRequestMenu;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Manager for handling pet requests to their Master NPC.
*
* This is the server-side handler for player-initiated requests in pet play mode.
* Utilizes existing systems:
* - PlayerEquipment.equip(BodyRegionV2.ARMS, bind) / takeBindOff() for tie/untie
* - MasterState.DOGWALK for walk mode
* - MasterPlaceBlockGoal for feeding/resting
* - Leash physics via mixin for pulling the player
*/
public class PetRequestManager {
/** Maximum distance for pet request interaction */
private static final double MAX_DISTANCE = 6.0;
/**
* Open the pet request menu for a pet player.
* Sends a packet to the client to display the GUI.
*
* @param master The master entity
* @param pet The pet player
*/
public static void openRequestMenu(EntityMaster master, ServerPlayer pet) {
if (!master.isPetPlayer(pet)) {
TiedUpMod.LOGGER.warn(
"[PetRequestManager] {} is not a pet of {}",
pet.getName().getString(),
master.getNpcName()
);
return;
}
// Check distance
double dist = master.distanceTo(pet);
if (dist > MAX_DISTANCE) {
pet.sendSystemMessage(
Component.literal("You are too far from your Master to talk.")
);
return;
}
// Send packet to open GUI on client
ModNetwork.sendToPlayer(
new PacketOpenPetRequestMenu(
master.getId(),
master.getNpcName()
),
pet
);
TiedUpMod.LOGGER.debug(
"[PetRequestManager] Opened request menu for {} with {}",
pet.getName().getString(),
master.getNpcName()
);
}
/**
* Handle a request from the pet player.
* Called when the player selects an option from the pet request menu.
*
* @param master The master entity
* @param pet The pet player making the request
* @param request The request type
*/
public static void handleRequest(
EntityMaster master,
ServerPlayer pet,
PetRequest request
) {
if (!master.isPetPlayer(pet)) {
TiedUpMod.LOGGER.warn(
"[PetRequestManager] Rejected request from non-pet {} to {}",
pet.getName().getString(),
master.getNpcName()
);
return;
}
// Check distance
double dist = master.distanceTo(pet);
if (dist > MAX_DISTANCE) {
pet.sendSystemMessage(
Component.literal("You are too far from your Master.")
);
return;
}
TiedUpMod.LOGGER.info(
"[PetRequestManager] {} requested {} from {}",
pet.getName().getString(),
request.name(),
master.getNpcName()
);
// Display what the player "says"
pet.sendSystemMessage(
Component.literal("You: " + request.getPlayerText())
);
// Handle specific request
switch (request) {
case REQUEST_FOOD -> triggerFeeding(master, pet);
case REQUEST_SLEEP -> triggerResting(master, pet);
case REQUEST_WALK_PASSIVE -> triggerDogwalk(master, pet, false);
case REQUEST_WALK_ACTIVE -> triggerDogwalk(master, pet, true);
case REQUEST_TIE -> triggerTie(master, pet);
case REQUEST_UNTIE -> triggerUntie(master, pet);
case END_CONVERSATION -> endConversation(master, pet);
}
}
/**
* Trigger feeding action - Master places bowl for pet.
*/
private static void triggerFeeding(EntityMaster master, ServerPlayer pet) {
DialogueBridge.talkTo(master, pet, "petplay.feeding");
MasterPlaceBlockGoal goal = master.getPlaceBlockGoal();
if (goal != null) {
goal.triggerFeeding();
}
}
/**
* Trigger resting action - Master places pet bed for pet.
*/
private static void triggerResting(EntityMaster master, ServerPlayer pet) {
DialogueBridge.talkTo(master, pet, "petplay.resting");
MasterPlaceBlockGoal goal = master.getPlaceBlockGoal();
if (goal != null) {
goal.triggerResting();
}
}
/**
* Trigger dogwalk mode.
* Puts dogbind on player and attaches leash.
*
* @param masterLeads If true, Master walks and pulls pet. If false, Master follows pet.
*/
private static void triggerDogwalk(
EntityMaster master,
ServerPlayer pet,
boolean masterLeads
) {
// Get player bind state
PlayerBindState state = PlayerBindState.getInstance(pet);
if (state == null) {
TiedUpMod.LOGGER.warn(
"[PetRequestManager] Could not get PlayerBindState for {}",
pet.getName().getString()
);
return;
}
// Put dogbind on player (if not already tied)
if (!state.isTiedUp()) {
ItemStack dogbind = new ItemStack(
ModItems.getBind(BindVariant.DOGBINDER)
);
state.equip(BodyRegionV2.ARMS, dogbind);
TiedUpMod.LOGGER.debug(
"[PetRequestManager] Equipped dogbind on {} for walk",
pet.getName().getString()
);
}
// Attach leash
master.attachLeashToPet();
// Set dogwalk mode
master.setDogwalkMode(masterLeads);
master.setMasterState(MasterState.DOGWALK);
String dialogueId = masterLeads
? "petplay.walk_active"
: "petplay.walk_passive";
DialogueBridge.talkTo(master, pet, dialogueId);
TiedUpMod.LOGGER.info(
"[PetRequestManager] {} entered DOGWALK mode with {} (masterLeads={})",
master.getNpcName(),
pet.getName().getString(),
masterLeads
);
}
/**
* Trigger tie request - Master ties up the pet.
*/
private static void triggerTie(EntityMaster master, ServerPlayer pet) {
// Don't allow tie requests during dogwalk
if (master.getStateManager().getCurrentState() == MasterState.DOGWALK) {
DialogueBridge.talkTo(master, pet, "petplay.busy");
return;
}
// Get player bind state
PlayerBindState state = PlayerBindState.getInstance(pet);
if (state == null) {
TiedUpMod.LOGGER.warn(
"[PetRequestManager] Could not get PlayerBindState for {}",
pet.getName().getString()
);
return;
}
// Check if already tied
if (state.isTiedUp()) {
DialogueBridge.talkTo(master, pet, "petplay.already_tied");
return;
}
// Master equips armbinder on pet (classic pet play restraint)
ItemStack bind = new ItemStack(ModItems.getBind(BindVariant.ARMBINDER));
state.equip(BodyRegionV2.ARMS, bind);
DialogueBridge.talkTo(master, pet, "petplay.tie_accept");
TiedUpMod.LOGGER.info(
"[PetRequestManager] {} tied up {} with armbinder",
master.getNpcName(),
pet.getName().getString()
);
}
/**
* Trigger untie request - Master unties the pet.
*/
private static void triggerUntie(EntityMaster master, ServerPlayer pet) {
// Don't allow untie requests during dogwalk
if (master.getStateManager().getCurrentState() == MasterState.DOGWALK) {
DialogueBridge.talkTo(master, pet, "petplay.busy");
return;
}
// Get player bind state
PlayerBindState state = PlayerBindState.getInstance(pet);
if (state == null) {
TiedUpMod.LOGGER.warn(
"[PetRequestManager] Could not get PlayerBindState for {}",
pet.getName().getString()
);
return;
}
// Check if actually tied
if (!state.isTiedUp()) {
DialogueBridge.talkTo(master, pet, "petplay.not_tied");
return;
}
// Master removes bind from pet
state.unequip(BodyRegionV2.ARMS);
DialogueBridge.talkTo(master, pet, "petplay.untie_accept");
TiedUpMod.LOGGER.info(
"[PetRequestManager] {} untied {}",
master.getNpcName(),
pet.getName().getString()
);
}
/**
* End the conversation gracefully.
*/
private static void endConversation(EntityMaster master, ServerPlayer pet) {
DialogueBridge.talkTo(master, pet, "petplay.dismiss");
}
}

View File

@@ -0,0 +1,167 @@
package com.tiedup.remake.dialogue.conversation;
import com.tiedup.remake.personality.PersonalityType;
/**
* Represents the effects of a conversation topic on an NPC's state.
* Effects are applied with personality modifiers.
*
* Phase 5: Enhanced Conversation System
*/
public record TopicEffect(
/** Mood change (-20 to +20) */
float moodChange
) {
// --- Static Factory Methods for Standard Topics ---
public static TopicEffect compliment() {
return new TopicEffect(5f);
}
public static TopicEffect comfort() {
return new TopicEffect(10f);
}
public static TopicEffect praise() {
return new TopicEffect(3f);
}
public static TopicEffect scold() {
return new TopicEffect(-5f);
}
public static TopicEffect threaten() {
return new TopicEffect(-8f);
}
public static TopicEffect tease() {
return new TopicEffect(0f);
}
public static TopicEffect howAreYou() {
return new TopicEffect(1f);
}
public static TopicEffect whatsWrong() {
return new TopicEffect(2f);
}
// --- Effect Application Methods ---
/**
* Apply personality modifier to this effect.
* Different personalities react differently to topics.
*
* @param personality The NPC's personality
* @param topic The topic being used
* @return Modified TopicEffect
*/
public TopicEffect withPersonalityModifier(
PersonalityType personality,
ConversationTopic topic
) {
float moodMult = 1.0f;
switch (personality) {
case MASOCHIST -> {
if (
topic == ConversationTopic.SCOLD ||
topic == ConversationTopic.THREATEN
) {
moodMult = -0.5f;
}
}
case SUBMISSIVE -> {
if (topic == ConversationTopic.PRAISE) {
moodMult = 1.5f;
}
}
case PLAYFUL -> {
if (topic == ConversationTopic.TEASE) {
moodMult = 3.0f;
}
}
case TIMID -> {
if (topic == ConversationTopic.COMFORT) {
moodMult = 1.5f;
}
}
case GENTLE -> {
if (
topic == ConversationTopic.COMPLIMENT ||
topic == ConversationTopic.COMFORT
) {
moodMult = 1.3f;
}
}
case PROUD -> {
if (topic == ConversationTopic.COMPLIMENT) {
moodMult = 0.5f;
}
}
default -> {
// No special handling
}
}
return new TopicEffect(this.moodChange * moodMult);
}
/**
* Apply effectiveness multiplier (from topic fatigue).
*
* @param effectiveness Multiplier (0.2 to 1.0)
* @return Scaled TopicEffect
*/
public TopicEffect withEffectiveness(float effectiveness) {
return new TopicEffect(this.moodChange * effectiveness);
}
/**
* Get the base effect for a conversation topic.
*
* @param topic The topic
* @return Base TopicEffect
*/
public static TopicEffect forTopic(ConversationTopic topic) {
return switch (topic) {
case COMPLIMENT -> compliment();
case COMFORT -> comfort();
case PRAISE -> praise();
case SCOLD -> scold();
case THREATEN -> threaten();
case TEASE -> tease();
case HOW_ARE_YOU -> howAreYou();
case WHATS_WRONG -> whatsWrong();
default -> neutral();
};
}
/**
* Create a neutral effect (no changes).
*/
public static TopicEffect neutral() {
return new TopicEffect(0f);
}
/**
* Check if this effect has any significant changes.
*
* @return true if any value is non-zero
*/
public boolean hasEffect() {
return moodChange != 0;
}
/**
* Get a descriptive string of the effect for debugging.
*
* @return Human-readable effect description
*/
public String toDebugString() {
if (moodChange != 0) {
return "Mood:" + (moodChange > 0 ? "+" : "") + moodChange;
}
return "";
}
}