package com.tiedup.remake.dialogue; import static com.tiedup.remake.util.GameConstants.*; import com.tiedup.remake.dialogue.EmotionalContext.EmotionType; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.v2.bondage.component.ComponentType; import com.tiedup.remake.v2.bondage.component.GaggingComponent; import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem; import com.tiedup.remake.util.GagMaterial; import com.tiedup.remake.util.PhoneticMapper; import com.tiedup.remake.util.SyllableAnalyzer; import java.util.List; import java.util.Random; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; /** * *

Features: *

*/ public class GagTalkManager { private static final Random RANDOM = new Random(); private static final String[] CRIT_FAIL_SOUNDS = { "Mmph!!", "Mmmph...", "Hmpf!", "Mmm...", "Mph?!", "Nnnnh!", "Hffff!", "P-pph!", }; /** * Process a gagged message for any IBondageState entity. * * @param kidnapped The kidnapped entity (Player, EntityDamsel, etc.) * @param gagStack The gag item stack * @param originalMessage The original message before gagging * @return The muffled message component */ public static Component processGagMessage( IBondageState kidnapped, ItemStack gagStack, String originalMessage ) { LivingEntity entity = kidnapped.asLivingEntity(); GagMaterial material = GagMaterial.CLOTH; // V2: check data-driven GaggingComponent first GaggingComponent gaggingComp = DataDrivenBondageItem.getComponent( gagStack, ComponentType.GAGGING, GaggingComponent.class); if (gaggingComp != null && gaggingComp.getMaterial() != null) { material = gaggingComp.getMaterial(); } // 1. EFFET DE SUFFOCATION (Si message trop long) applySuffocationEffects(entity, originalMessage.length(), material); // 2. CHANCE D'ECHEC CRITIQUE Component critFailResult = checkCriticalFailure( kidnapped, gagStack, originalMessage.length(), material ); if (critFailResult != null) { return critFailResult; } // 3. DETECT OVERALL MESSAGE EMOTION EmotionType messageEmotion = EmotionalContext.detectEmotion( originalMessage ); // 4. CONSTRUCTION DU MESSAGE V4 StringBuilder muffled = new StringBuilder(); String[] words = originalMessage.split("\\s+"); for (int i = 0; i < words.length; i++) { String word = words[i]; // Progressive comprehension: longer messages get harder to understand float positionPenalty = (i > 5) ? 0.05f * (i - 5) : 0.0f; float baseComp = material.getComprehension(); float effectiveComprehension = Math.max( 0, baseComp - positionPenalty ); // Apply emotional intensity modifier effectiveComprehension /= EmotionalContext.getIntensityMultiplier( messageEmotion ); // Whispering is clearer if (EmotionalContext.shouldPreserveMore(messageEmotion)) { effectiveComprehension *= 1.5f; } // Material-specific interjections (rare, natural placement) if (RANDOM.nextFloat() < 0.03f && i > 0 && i < words.length - 1) { muffled.append(getMaterialInterjection(material)).append(" "); } // Movement affects clarity if (entity.isSprinting() || !entity.onGround()) { effectiveComprehension *= 0.7f; } // Three-tier comprehension: // - Full pass: word understood completely // - Partial: first letter(s) + muffled rest // - None: fully muffled float roll = RANDOM.nextFloat(); if (roll < effectiveComprehension * 0.4f) { // Full word passes through muffled.append(word); } else if (roll < effectiveComprehension) { // Partial: first letter(s) visible + muffled rest muffled.append(generatePartiallyMuffledWord(word, material)); } else { // Fully muffled muffled.append(generateMuffledWord(word, material)); } // Word separator if (i < words.length - 1) { if (RANDOM.nextFloat() < 0.1f) { muffled.append("-"); } else { muffled.append(" "); } } } // 5. PENSEE INTERNE (Visible seulement pour l'entite) kidnapped.sendMessage( Component.literal("(") .append( Component.literal(originalMessage).withStyle( ChatFormatting.ITALIC ) ) .append(")") .withStyle(ChatFormatting.GRAY), false ); return Component.literal(muffled.toString()); } /** * Generate a partially muffled word - first part recognizable, rest muffled. */ private static String generatePartiallyMuffledWord( String original, GagMaterial material ) { if (original.isEmpty()) return ""; if (original.length() <= 2) return original; // Find a good split point (after first consonant cluster + vowel) int splitPoint = findNaturalSplitPoint(original); String visible = original.substring(0, splitPoint); String toMuffle = original.substring(splitPoint); if (toMuffle.isEmpty()) { return visible; } return visible + generateMuffledWord(toMuffle, material); } /** * Find a natural split point in a word for partial muffling. */ private static int findNaturalSplitPoint(String word) { if (word.length() <= 2) return word.length(); // Try to split after first syllable-ish structure boolean foundVowel = false; for (int i = 0; i < word.length() && i < 4; i++) { char c = Character.toLowerCase(word.charAt(i)); boolean isVowel = "aeiouy".indexOf(c) >= 0; if (isVowel) { foundVowel = true; } else if (foundVowel) { // Found consonant after vowel - good split point return i; } } // Default: first 1-2 characters return Math.min(2, word.length()); } /** * Generate a fully muffled word using phonetic transformation. */ private static String generateMuffledWord( String original, GagMaterial material ) { if (original == null || original.isEmpty()) return ""; // Extract and preserve punctuation String punctuation = extractTrailingPunctuation(original); String cleanWord = original.replaceAll("[^a-zA-Z]", ""); if (cleanWord.isEmpty()) { return original; // Pure punctuation, keep as-is } // Preserve original case pattern boolean wasAllCaps = cleanWord.length() >= 2 && cleanWord.equals(cleanWord.toUpperCase()); // Split into syllables for rhythm preservation List syllables = SyllableAnalyzer.splitIntoSyllables(cleanWord); StringBuilder result = new StringBuilder(); float baseBleed = material.getComprehension(); for ( int syllableIdx = 0; syllableIdx < syllables.size(); syllableIdx++ ) { String syllable = syllables.get(syllableIdx); // First syllable gets bonus bleed (more recognizable) float syllableBleed = (syllableIdx == 0) ? baseBleed * 1.3f : baseBleed; // Stressed syllables are clearer if ( SyllableAnalyzer.isStressedSyllable( syllable, syllableIdx, syllables.size() ) ) { syllableBleed *= 1.2f; } StringBuilder muffledSyllable = new StringBuilder(); char prevOutput = 0; for (int i = 0; i < syllable.length(); i++) { char c = syllable.charAt(i); // Calculate bleed for this specific phoneme float phonemeBleed = syllableBleed * material.getBleedRateFor(c); // Apply phonetic mapping String mapped = PhoneticMapper.mapPhoneme( c, material, phonemeBleed ); // Avoid excessive repetition (mmmmm -> mm) if (shouldSkipRepetition(mapped, prevOutput, muffledSyllable)) { continue; } muffledSyllable.append(mapped); if (!mapped.isEmpty()) { prevOutput = mapped.charAt(mapped.length() - 1); } } result.append(muffledSyllable); // Occasional syllable separator for multi-syllable words if ( syllableIdx < syllables.size() - 1 && RANDOM.nextFloat() < 0.2f ) { result.append("-"); } } String muffled = result.toString(); // Collapse excessive repetitions (max 2 of same letter) muffled = collapseRepetitions(muffled, 2); // Ensure we have something if (muffled.isEmpty()) { muffled = material.getDominantConsonant() + material.getDominantVowel(); } // Preserve ALL CAPS if original was all caps if (wasAllCaps) { muffled = muffled.toUpperCase(); } return muffled + punctuation; } /** * Check if we should skip adding a mapped sound to avoid excessive repetition. */ private static boolean shouldSkipRepetition( String mapped, char prevOutput, StringBuilder current ) { if (mapped.isEmpty()) return true; char firstMapped = mapped.charAt(0); // Skip if same character repeated more than twice if (firstMapped == prevOutput) { int repeatCount = 0; for ( int i = current.length() - 1; i >= 0 && i >= current.length() - 3; i-- ) { if (current.charAt(i) == firstMapped) { repeatCount++; } else { break; } } if (repeatCount >= 2) { return true; } } return false; } /** * Collapse runs of repeated characters to a maximum count. * "uuuuu" with maxRepeat=2 becomes "uu" */ private static String collapseRepetitions(String text, int maxRepeat) { if (text == null || text.length() <= maxRepeat) { return text; } StringBuilder result = new StringBuilder(); char prev = 0; int count = 0; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); char lower = Character.toLowerCase(c); char prevLower = Character.toLowerCase(prev); if (lower == prevLower && Character.isLetter(c)) { count++; if (count <= maxRepeat) { result.append(c); } } else { result.append(c); count = 1; } prev = c; } return result.toString(); } /** * Extract trailing punctuation from a word. */ private static String extractTrailingPunctuation(String word) { StringBuilder punct = new StringBuilder(); for (int i = word.length() - 1; i >= 0; i--) { char c = word.charAt(i); if (!Character.isLetter(c)) { punct.insert(0, c); } else { break; } } return punct.toString(); } /** * Get a material-specific interjection sound. */ private static String getMaterialInterjection(GagMaterial material) { return switch (material) { case BALL -> "*hhuup*"; case TAPE -> "*mmn*"; case STUFFED, SPONGE -> "*mm*"; case RING -> "*aah*"; case BITE -> "*ngh*"; case LATEX -> "*uuh*"; case BAGUETTE -> "*nom*"; case PANEL -> "*mmph*"; default -> "*mph*"; }; } // SUFFOCATION & CRITICAL FAILURE HELPERS /** * Apply suffocation effects if message is too long. * Long messages through restrictive gags cause slowness and potential blindness. * * @param entity The entity speaking * @param messageLength The length of the message * @param material The gag material type */ private static void applySuffocationEffects( LivingEntity entity, int messageLength, GagMaterial material ) { if ( messageLength > GAG_MAX_MESSAGE_LENGTH_BEFORE_SUFFOCATION && material != GagMaterial.CLOTH ) { entity.addEffect( new MobEffectInstance( MobEffects.MOVEMENT_SLOWDOWN, GAG_SUFFOCATION_SLOWNESS_DURATION, 1 ) ); if (RANDOM.nextFloat() < GAG_SUFFOCATION_BLINDNESS_CHANCE) { entity.addEffect( new MobEffectInstance( MobEffects.BLINDNESS, GAG_SUFFOCATION_BLINDNESS_DURATION, 0 ) ); } } } /** * Check for critical failure when speaking through gag. * Longer messages have higher chance of complete muffling. * * @param kidnapped The kidnapped entity * @param gagStack The gag item stack * @param messageLength The length of the message * @param material The gag material type * @return Critical fail Component if failed, null otherwise */ private static Component checkCriticalFailure( IBondageState kidnapped, ItemStack gagStack, int messageLength, GagMaterial material ) { float critChance = GAG_BASE_CRITICAL_FAIL_CHANCE + (messageLength * GAG_LENGTH_CRITICAL_FACTOR); if (RANDOM.nextFloat() < critChance && material != GagMaterial.CLOTH) { kidnapped.sendMessage( Component.translatable( "chat.tiedup.gag.crit_fail", gagStack.getHoverName() ).withStyle(ChatFormatting.RED), true ); return Component.literal( CRIT_FAIL_SOUNDS[RANDOM.nextInt(CRIT_FAIL_SOUNDS.length)] ); } return null; } // MCA INTEGRATION METHODS /** * Transform a message to gagged speech without side effects. * For use with MCA villagers and AI chat. * *

Unlike {@link #processGagMessage}, this method: *

* * @param originalMessage The original message to transform * @param gagStack The gag item stack (determines material) * @return The muffled message string */ public static String transformToGaggedSpeech( String originalMessage, ItemStack gagStack ) { if (originalMessage == null || originalMessage.isEmpty()) { return ""; } GagMaterial material = GagMaterial.CLOTH; if (gagStack != null && !gagStack.isEmpty()) { // V2: check data-driven GaggingComponent first GaggingComponent comp = DataDrivenBondageItem.getComponent( gagStack, ComponentType.GAGGING, GaggingComponent.class); if (comp != null && comp.getMaterial() != null) { material = comp.getMaterial(); } } StringBuilder muffled = new StringBuilder(); String[] words = originalMessage.split("\\s+"); for (int i = 0; i < words.length; i++) { String word = words[i]; // Three-tier comprehension for MCA as well float comp = material.getComprehension(); float roll = RANDOM.nextFloat(); if (roll < comp * 0.4f) { muffled.append(word); } else if (roll < comp) { muffled.append(generatePartiallyMuffledWord(word, material)); } else { muffled.append(generateMuffledWord(word, material)); } if (i < words.length - 1) { muffled.append(RANDOM.nextFloat() < 0.1f ? "-" : " "); } } return muffled.toString(); } /** * Transform a message to gagged speech using default cloth gag. * Convenience method for when gag item is not available. * * @param originalMessage The original message to transform * @return The muffled message string */ public static String transformToGaggedSpeech(String originalMessage) { return transformToGaggedSpeech(originalMessage, ItemStack.EMPTY); } }