Files
TiedUp-/src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
NotEvil 099cd0d984 feat(D-01/D): V1 cleanup — delete 28 files, ~5400 lines removed
D1: ThreadLocal alert suppression moved from ItemCollar to CollarHelper.
    onCollarRemoved() logic (kidnapper alert) moved to CollarHelper.

D2+D3: Deleted 17 V1 item classes + 4 V1-only interfaces:
  ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, ItemMittens,
  ItemColor, ItemClassicCollar, ItemShockCollar, ItemShockCollarAuto,
  ItemGpsCollar, ItemChokeCollar, ItemHood, ItemMedicalGag,
  IBondageItem, IHasGaggingEffect, IHasBlindingEffect, IAdjustable

D4: KidnapperTheme/KidnapperItemSelector/DispenserBehaviors migrated
    from variant enums to string-based DataDrivenItemRegistry IDs.

D5: Deleted 11 variant enums + Generic* factories + ItemBallGag3D:
  BindVariant, GagVariant, BlindfoldVariant, EarplugsVariant, MittensVariant,
  GenericBind, GenericGag, GenericBlindfold, GenericEarplugs, GenericMittens

D6: ModItems cleaned — all V1 bondage registrations removed.
D7: ModCreativeTabs rewritten — iterates DataDrivenItemRegistry.
D8+D9: All V2 helpers cleaned (V1 fallbacks removed), orphan imports removed.

Zero V1 bondage code references remain (only Javadoc comments).
All bondage items are now data-driven via 47 JSON definitions.
2026-04-15 01:55:16 +02:00

568 lines
18 KiB
Java

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;
/**
*
* <p>Features:
* <ul>
* <li>Phonetic-based transformation preserving word structure</li>
* <li>Syllable-aware processing for natural rhythm</li>
* <li>Emotional context detection and expression</li>
* <li>Material-specific bleed-through rates</li>
* <li>Progressive comprehension (partial understanding)</li>
* </ul>
*/
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<String> 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.
*
* <p>Unlike {@link #processGagMessage}, this method:
* <ul>
* <li>Does not apply suffocation effects</li>
* <li>Does not show internal thoughts</li>
* <li>Does not have critical fail chance</li>
* <li>Simply returns the muffled text</li>
* </ul>
*
* @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);
}
}