package com.tiedup.remake.dialogue; import com.tiedup.remake.entities.EntityDamsel; import com.tiedup.remake.util.MessageDispatcher; import java.util.List; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; /** * Complete dialogue system for EntityDamsel and EntityKidnapper. * * Phase 14.3: Centralized NPC dialogue management * * Features: * - Multiple dialogue variants per action category * - Integration with GagTalkManager for gagged NPCs * - Formatted messages with entity name * - Action messages vs speech messages * - Radius-based broadcasting */ public class EntityDialogueManager { // ======================================== // DIALOGUE CATEGORIES // ======================================== /** * Dialogue categories for different NPC actions. */ public enum DialogueCategory { // === KIDNAPPER CAPTURE SEQUENCE === CAPTURE_START, // When kidnapper starts chasing CAPTURE_APPROACHING, // While approaching target CAPTURE_CHASE, // When pursuing an escaped captive CAPTURE_TYING, // While tying up target CAPTURE_TIED, // After target is tied CAPTURE_GAGGING, // While gagging target CAPTURE_GAGGED, // After target is gagged CAPTURE_ENSLAVED, // After enslaving target CAPTURE_ESCAPE, // When captive escapes // === SLAVE MANAGEMENT === SLAVE_TALK_RESPONSE, // When slave tries to talk SLAVE_STRUGGLE, // When slave struggles SLAVE_TRANSPORT, // When transporting slave SLAVE_ARRIVE_PRISON, // When arriving at prison SLAVE_TIED_TO_POLE, // When tying slave to pole PUNISH, // When punishing a recaptured or misbehaving captive // === SALE SYSTEM === SALE_WAITING, // Waiting for buyer SALE_ANNOUNCE, // Announcing sale SALE_OFFER, // Offering to sell to a passing player (needs %s for player name and %s for price) SALE_COMPLETE, // Sale completed SALE_ABANDONED, // When kidnapper abandons captive (solo mode) SALE_KEPT, // When kidnapper decides to keep captive (solo mode) // === JOB SYSTEM === JOB_ASSIGNED, // Giving job to slave JOB_HURRY, // Urging slave to hurry JOB_COMPLETE, // Job completed successfully JOB_FAILED, // Job failed JOB_LAST_CHANCE, // Warning before killing JOB_KILL, // Killing the slave for failure // === COMBAT/ATTACK === ATTACKED_RESPONSE, // When attacked by someone ATTACK_SLAVE, // When slave attacks // === DAMSEL SPECIFIC === DAMSEL_PANIC, // When damsel is scared DAMSEL_FLEE, // When damsel flees DAMSEL_CAPTURED, // When damsel is captured DAMSEL_FREED, // When damsel is freed DAMSEL_GREETING, // Greeting nearby players DAMSEL_IDLE, // Random idle chatter DAMSEL_CALL_FOR_HELP, // When tied damsel sees a player nearby (needs %s for player name) // === GENERAL === FREED, // When freeing someone GOODBYE, // Saying goodbye GET_OUT, // Telling someone to leave GENERIC_THREAT, // Generic threatening line GENERIC_TAUNT, // Generic taunting line // === PERSONALITY SYSTEM (Phase E) === COMMAND_ACCEPT, // When NPC accepts a command COMMAND_REFUSE, // When NPC refuses a command COMMAND_HESITATE, // When NPC hesitates before command // === DISCIPLINE SYSTEM (Training V2) === PRAISE_RESPONSE, // When NPC is praised SCOLD_RESPONSE, // When NPC is scolded THREATEN_RESPONSE, // When NPC is threatened NEED_HUNGRY, // When NPC is hungry NEED_TIRED, // When NPC is tired NEED_UNCOMFORTABLE, // When NPC is uncomfortable NEED_DIGNITY_LOW, // When NPC has low dignity PERSONALITY_HINT_TIMID, // Hint for TIMID personality PERSONALITY_HINT_GENTLE, // Hint for GENTLE personality PERSONALITY_HINT_SUBMISSIVE, // Hint for SUBMISSIVE personality PERSONALITY_HINT_CALM, // Hint for CALM personality PERSONALITY_HINT_CURIOUS, // Hint for CURIOUS personality PERSONALITY_HINT_PROUD, // Hint for PROUD personality PERSONALITY_HINT_FIERCE, // Hint for FIERCE personality PERSONALITY_HINT_DEFIANT, // Hint for DEFIANT personality PERSONALITY_HINT_PLAYFUL, // Hint for PLAYFUL personality PERSONALITY_HINT_MASOCHIST, // Hint for MASOCHIST personality PERSONALITY_HINT_SADIST, // Hint for SADIST personality (kidnappers) } // ======================================== // DIALOGUE RETRIEVAL (Data-Driven Only) // ======================================== /** * Get random dialogue for a category using the data-driven system. * All dialogues are now loaded from JSON files. * * @param category The dialogue category * @return Dialogue text, or a fallback message if not found */ public static String getDialogue(DialogueCategory category) { // Use data-driven system with default context if (com.tiedup.remake.dialogue.DialogueManager.isInitialized()) { String dialogueId = com.tiedup.remake.dialogue.DialogueBridge.categoryToDialogueId( category ); com.tiedup.remake.dialogue.DialogueContext defaultContext = com.tiedup.remake.dialogue.DialogueContext.builder() .personality( com.tiedup.remake.personality.PersonalityType.CALM ) .mood(50) .build(); String text = com.tiedup.remake.dialogue.DialogueManager.getDialogue( dialogueId, defaultContext ); if (text != null) { return text; } } // Fallback for uninitialized system com.tiedup.remake.core.TiedUpMod.LOGGER.warn( "[EntityDialogueManager] Data-driven dialogue not found for category: {}", category.name() ); return "[" + category.name() + "]"; } /** * Get dialogue for a category using the data-driven system. * Uses entity context for personality-aware dialogue selection. * * @param entity The entity speaking * @param player The player (can be null) * @param category The dialogue category * @return Dialogue text */ public static String getDialogue( EntityDamsel entity, Player player, DialogueCategory category ) { // Always use data-driven system String dataDriven = com.tiedup.remake.dialogue.DialogueBridge.getDataDrivenDialogue( entity, player, category ); if (dataDriven != null) { return dataDriven; } // Fallback to category-only lookup (no entity context) return getDialogue(category); } // ======================================== // MESSAGE SENDING METHODS // ======================================== /** * Send a dialogue message to a specific player. * Format: *EntityName* : message * Uses data-driven dialogue system if available. * * @param entity The entity speaking * @param player The player receiving the message * @param category The dialogue category */ public static void talkTo( EntityDamsel entity, Player player, DialogueCategory category ) { // Use data-driven dialogue when available talkTo(entity, player, getDialogue(entity, player, category)); } /** * Send a custom message to a specific player. * Delegates formatting, gag talk, and earplug handling to MessageDispatcher. * * @param entity The entity speaking * @param player The player receiving the message * @param message The message to send */ public static void talkTo( EntityDamsel entity, Player player, String message ) { if (entity == null || player == null || message == null) return; if (entity.level().isClientSide()) return; if (!(player instanceof ServerPlayer)) return; MessageDispatcher.talkTo(entity, player, message); } /** * Send a dialogue message using a dialogue ID. * Resolves the ID to actual text via DialogueManager. * * @param entity The entity speaking * @param player The player receiving the message * @param dialogueId The dialogue ID (e.g., "action.whip") */ public static void talkByDialogueId( EntityDamsel entity, Player player, String dialogueId ) { if (entity == null || player == null || dialogueId == null) return; if (entity.level().isClientSide()) return; if (!com.tiedup.remake.dialogue.DialogueManager.isInitialized()) { talkTo(entity, player, "[Dialogue system not ready]"); return; } // Build context and resolve dialogue text com.tiedup.remake.dialogue.DialogueContext context = com.tiedup.remake.dialogue.DialogueBridge.buildContext( entity, player ); String text = com.tiedup.remake.dialogue.DialogueManager.getDialogue( dialogueId, context ); if (text == null) { text = "[Missing: " + dialogueId + "]"; com.tiedup.remake.core.TiedUpMod.LOGGER.warn( "[EntityDialogueManager] Missing dialogue for ID: {} with personality: {}", dialogueId, context.getPersonality() ); } talkTo(entity, player, text); } /** * Send an action message to a specific player. * Format: EntityName action * Uses data-driven dialogue system if available. * * @param entity The entity performing the action * @param player The player receiving the message * @param category The dialogue category */ public static void actionTo( EntityDamsel entity, Player player, DialogueCategory category ) { // Use data-driven dialogue when available actionTo(entity, player, getDialogue(entity, player, category)); } /** * Send a custom action message to a specific player. * Delegates formatting and earplug handling to MessageDispatcher. * * @param entity The entity performing the action * @param player The player receiving the message * @param action The action description */ public static void actionTo( EntityDamsel entity, Player player, String action ) { if (entity == null || player == null || action == null) return; if (entity.level().isClientSide()) return; if (!(player instanceof ServerPlayer)) return; MessageDispatcher.actionTo(entity, player, action); } /** * Talk to all players within a radius. * Builds proper context per player for variable substitution. * * @param entity The entity speaking * @param category The dialogue category * @param radius The radius in blocks */ public static void talkToNearby( EntityDamsel entity, DialogueCategory category, int radius ) { if (entity == null) return; List players = entity .level() .getEntitiesOfClass( Player.class, entity.getBoundingBox().inflate(radius) ); for (Player player : players) { // Use the context-aware dialogue lookup for proper {player} substitution String dialogue = getDialogue(entity, player, category); talkTo(entity, player, dialogue); } } /** * Talk to all players within a radius with a custom message. * * @param entity The entity speaking * @param message The message to send * @param radius The radius in blocks */ public static void talkToNearby( EntityDamsel entity, String message, int radius ) { if (entity == null || message == null) return; List players = entity .level() .getEntitiesOfClass( Player.class, entity.getBoundingBox().inflate(radius) ); for (Player player : players) { talkTo(entity, player, message); } } /** * Send an action message to all players within a radius. * Builds proper context per player for variable substitution. * * @param entity The entity performing the action * @param category The dialogue category * @param radius The radius in blocks */ public static void actionToNearby( EntityDamsel entity, DialogueCategory category, int radius ) { if (entity == null) return; List players = entity .level() .getEntitiesOfClass( Player.class, entity.getBoundingBox().inflate(radius) ); for (Player player : players) { // Use the context-aware dialogue lookup for proper {player} substitution String action = getDialogue(entity, player, category); actionTo(entity, player, action); } } /** * Send a custom action message to all players within a radius. * * @param entity The entity performing the action * @param action The action description * @param radius The radius in blocks */ public static void actionToNearby( EntityDamsel entity, String action, int radius ) { if (entity == null || action == null) return; List players = entity .level() .getEntitiesOfClass( Player.class, entity.getBoundingBox().inflate(radius) ); for (Player player : players) { actionTo(entity, player, action); } } // ======================================== // CONVENIENCE METHODS // ======================================== /** * Make entity say something to their target (if player). */ public static void talkToTarget( EntityDamsel entity, LivingEntity target, DialogueCategory category ) { if (target instanceof Player player) { talkTo(entity, player, category); } } /** * Make entity say a custom message to their target (if player). */ public static void talkToTarget( EntityDamsel entity, LivingEntity target, String message ) { if (target instanceof Player player) { talkTo(entity, player, message); } } /** * Make entity perform action to their target (if player). */ public static void actionToTarget( EntityDamsel entity, LivingEntity target, DialogueCategory category ) { if (target instanceof Player player) { actionTo(entity, player, category); } } /** * Make entity perform custom action to their target (if player). */ public static void actionToTarget( EntityDamsel entity, LivingEntity target, String action ) { if (target instanceof Player player) { actionTo(entity, player, action); } } // ======================================== // COMPOUND MESSAGES // ======================================== /** * Get a dialogue for job assignment with item name. */ public static String getJobAssignmentDialogue(String itemName) { return getDialogue(DialogueCategory.JOB_ASSIGNED) + itemName; } /** * Get a call for help dialogue with player name. * * @deprecated Use {@link #callForHelp(EntityDamsel, Player)} which builds proper context. */ @Deprecated public static String getCallForHelpDialogue(String playerName) { // Legacy fallback - manually substitute {player} String text = getDialogue(DialogueCategory.DAMSEL_CALL_FOR_HELP); return text.replace("{player}", playerName); } /** * Get a sale offer dialogue with player name and price. * The {player} placeholder is the buyer, {target} is the price. * * @param playerName The buyer's name * @param price The price as a display string (e.g., "50 gold") * @return Formatted dialogue text */ public static String getSaleOfferDialogue(String playerName, String price) { // Substitute placeholders manually since we don't have full context String text = getDialogue(DialogueCategory.SALE_OFFER); text = text.replace("{player}", playerName); text = text.replace("{target}", price); // {target} is used for price in sale dialogues return text; } /** * Make a tied damsel call for help to a nearby player. * Pre-conditions (tied, slave, not gagged) should be checked by caller. * * @param damsel The damsel calling for help * @param targetPlayer The player to call for help */ public static void callForHelp(EntityDamsel damsel, Player targetPlayer) { if (damsel == null || targetPlayer == null) return; // Use context-aware dialogue for proper {player} substitution String message = getDialogue( damsel, targetPlayer, DialogueCategory.DAMSEL_CALL_FOR_HELP ); talkTo(damsel, targetPlayer, message); } /** * Get a sale announcement with coordinates. */ public static String getSaleAnnouncement( EntityDamsel entity, String slaveName ) { return String.format( "%s is selling %s at coordinates: %d, %d, %d", entity.getNpcName(), slaveName, (int) entity.getX(), (int) entity.getY(), (int) entity.getZ() ); } // ======================================== // PERSONALITY SYSTEM HELPERS // ======================================== /** * Get personality hint dialogue based on personality type name. * * @param personalityTypeName The name of the personality type (from PersonalityType enum) * @return The hint dialogue category, or null if unknown */ public static DialogueCategory getPersonalityHintCategory( String personalityTypeName ) { return switch (personalityTypeName.toUpperCase()) { case "TIMID" -> DialogueCategory.PERSONALITY_HINT_TIMID; case "GENTLE" -> DialogueCategory.PERSONALITY_HINT_GENTLE; case "SUBMISSIVE" -> DialogueCategory.PERSONALITY_HINT_SUBMISSIVE; case "CALM" -> DialogueCategory.PERSONALITY_HINT_CALM; case "CURIOUS" -> DialogueCategory.PERSONALITY_HINT_CURIOUS; case "PROUD" -> DialogueCategory.PERSONALITY_HINT_PROUD; case "FIERCE" -> DialogueCategory.PERSONALITY_HINT_FIERCE; case "DEFIANT" -> DialogueCategory.PERSONALITY_HINT_DEFIANT; case "PLAYFUL" -> DialogueCategory.PERSONALITY_HINT_PLAYFUL; case "MASOCHIST" -> DialogueCategory.PERSONALITY_HINT_MASOCHIST; case "SADIST" -> DialogueCategory.PERSONALITY_HINT_SADIST; default -> null; }; } /** * Show a personality hint action to a player. * Used when discovery level is GLIMPSE to give behavioral cues. * * @param entity The entity with the personality * @param player The player to show the hint to * @param personalityTypeName The personality type name */ public static void showPersonalityHint( EntityDamsel entity, Player player, String personalityTypeName ) { DialogueCategory hintCategory = getPersonalityHintCategory( personalityTypeName ); if (hintCategory != null) { actionTo(entity, player, hintCategory); } } /** * Get dialogue for command response based on acceptance. * * @param accepted true if command was accepted, false if refused * @param hesitated true if the NPC hesitated before responding * @return The appropriate dialogue */ public static String getCommandResponseDialogue( boolean accepted, boolean hesitated ) { if (!accepted) { return getDialogue(DialogueCategory.COMMAND_REFUSE); } if (hesitated) { return getDialogue(DialogueCategory.COMMAND_HESITATE); } return getDialogue(DialogueCategory.COMMAND_ACCEPT); } /** * Get dialogue for a specific need when it's critically low. * * @param needType The type of need ("hunger", "comfort", "rest", "dignity") * @return The appropriate dialogue category, or null if unknown */ public static DialogueCategory getNeedDialogueCategory(String needType) { return switch (needType.toLowerCase()) { case "hunger" -> DialogueCategory.NEED_HUNGRY; case "rest" -> DialogueCategory.NEED_TIRED; case "comfort" -> DialogueCategory.NEED_UNCOMFORTABLE; case "dignity" -> DialogueCategory.NEED_DIGNITY_LOW; default -> null; }; } }