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:
@@ -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 -> "...";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user