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:
562
src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
Normal file
562
src/main/java/com/tiedup/remake/dialogue/GagTalkManager.java
Normal file
@@ -0,0 +1,562 @@
|
||||
package com.tiedup.remake.dialogue;
|
||||
|
||||
import static com.tiedup.remake.util.GameConstants.*;
|
||||
|
||||
import com.tiedup.remake.dialogue.EmotionalContext.EmotionType;
|
||||
import com.tiedup.remake.items.base.ItemGag;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Phase 16: GagTalk System V4 - Realistic phonetic transformation
|
||||
*
|
||||
* <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;
|
||||
if (gagStack.getItem() instanceof ItemGag gag) {
|
||||
material = gag.getGagMaterial();
|
||||
}
|
||||
|
||||
// 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.getItem() instanceof ItemGag gag) {
|
||||
material = gag.getGagMaterial();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user