package com.tiedup.remake.util; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.Set; /** * Phonetic transformation system for gagged speech. * Maps original phonemes to muffled equivalents based on gag material. */ public class PhoneticMapper { private static final Random RANDOM = new Random(); // Phonetic categories private static final Set VOWELS = Set.of( 'a', 'e', 'i', 'o', 'u', 'y' ); private static final Set PLOSIVES = Set.of( 'b', 'd', 'g', 'k', 'p', 't' ); private static final Set NASALS = Set.of('m', 'n'); private static final Set FRICATIVES = Set.of( 'f', 'h', 's', 'v', 'z' ); private static final Set LIQUIDS = Set.of('l', 'r'); // Material-specific phoneme mappings private static final Map< GagMaterial, Map > CONSONANT_MAPS = new EnumMap<>(GagMaterial.class); private static final Map> VOWEL_MAPS = new EnumMap<>(GagMaterial.class); static { initializeClothMappings(); initializeBallMappings(); initializeTapeMappings(); initializeStuffedMappings(); initializePanelMappings(); initializeLatexMappings(); initializeRingMappings(); initializeBiteMappings(); initializeSpongeMappings(); initializeBaguetteMappings(); } private static void initializeClothMappings() { Map consonants = new HashMap<>(); consonants.put('b', new String[] { "m", "mph" }); consonants.put('c', new String[] { "h", "kh" }); consonants.put('d', new String[] { "n", "nd" }); consonants.put('f', new String[] { "f", "ph" }); consonants.put('g', new String[] { "ng", "gh" }); consonants.put('h', new String[] { "h", "hh" }); consonants.put('j', new String[] { "zh", "jh" }); consonants.put('k', new String[] { "kh", "gh" }); consonants.put('l', new String[] { "l", "hl" }); consonants.put('m', new String[] { "m", "mm" }); consonants.put('n', new String[] { "n", "nn" }); consonants.put('p', new String[] { "mph", "m" }); consonants.put('q', new String[] { "kh", "gh" }); consonants.put('r', new String[] { "r", "hr" }); consonants.put('s', new String[] { "s", "sh" }); consonants.put('t', new String[] { "th", "n" }); consonants.put('v', new String[] { "f", "vh" }); consonants.put('w', new String[] { "wh", "u" }); consonants.put('x', new String[] { "ks", "kh" }); consonants.put('z', new String[] { "z", "s" }); CONSONANT_MAPS.put(GagMaterial.CLOTH, consonants); Map vowels = new HashMap<>(); vowels.put('a', new String[] { "ah", "a" }); vowels.put('e', new String[] { "eh", "e" }); vowels.put('i', new String[] { "ih", "e" }); vowels.put('o', new String[] { "oh", "o" }); vowels.put('u', new String[] { "uh", "u" }); vowels.put('y', new String[] { "ih", "e" }); VOWEL_MAPS.put(GagMaterial.CLOTH, vowels); } private static void initializeBallMappings() { Map consonants = new HashMap<>(); // Ball gag forces mouth open around ball - tongue and lips blocked consonants.put('b', new String[] { "m", "mm" }); consonants.put('c', new String[] { "g", "kh" }); consonants.put('d', new String[] { "n", "nn" }); consonants.put('f', new String[] { "h", "hh" }); consonants.put('g', new String[] { "ng", "g" }); consonants.put('h', new String[] { "h", "hh" }); consonants.put('j', new String[] { "g", "ng" }); consonants.put('k', new String[] { "g", "gh" }); consonants.put('l', new String[] { "u", "l" }); consonants.put('m', new String[] { "m", "mm" }); consonants.put('n', new String[] { "n", "nn" }); consonants.put('p', new String[] { "m", "mm" }); consonants.put('q', new String[] { "g", "gh" }); consonants.put('r', new String[] { "u", "r" }); consonants.put('s', new String[] { "h", "s" }); consonants.put('t', new String[] { "n", "nn" }); consonants.put('v', new String[] { "h", "f" }); consonants.put('w', new String[] { "u", "w" }); consonants.put('x', new String[] { "g", "ks" }); consonants.put('z', new String[] { "s", "z" }); CONSONANT_MAPS.put(GagMaterial.BALL, consonants); Map vowels = new HashMap<>(); // Ball forces all vowels toward "oo/uu" (rounded) vowels.put('a', new String[] { "a", "o" }); vowels.put('e', new String[] { "u", "o" }); vowels.put('i', new String[] { "u", "i" }); vowels.put('o', new String[] { "o", "oo" }); vowels.put('u', new String[] { "u", "uu" }); vowels.put('y', new String[] { "u", "i" }); VOWEL_MAPS.put(GagMaterial.BALL, vowels); } private static void initializeTapeMappings() { Map consonants = new HashMap<>(); // Tape seals mouth almost completely - only nasal sounds consonants.put('b', new String[] { "m", "mm" }); consonants.put('c', new String[] { "n", "m" }); consonants.put('d', new String[] { "n", "nn" }); consonants.put('f', new String[] { "m", "hm" }); consonants.put('g', new String[] { "n", "ng" }); consonants.put('h', new String[] { "m", "hm" }); consonants.put('j', new String[] { "n", "m" }); consonants.put('k', new String[] { "n", "m" }); consonants.put('l', new String[] { "n", "m" }); consonants.put('m', new String[] { "m", "mm" }); consonants.put('n', new String[] { "n", "nn" }); consonants.put('p', new String[] { "m", "mm" }); consonants.put('q', new String[] { "n", "m" }); consonants.put('r', new String[] { "n", "m" }); consonants.put('s', new String[] { "m", "s" }); consonants.put('t', new String[] { "n", "nn" }); consonants.put('v', new String[] { "m", "f" }); consonants.put('w', new String[] { "m", "u" }); consonants.put('x', new String[] { "n", "m" }); consonants.put('z', new String[] { "n", "m" }); CONSONANT_MAPS.put(GagMaterial.TAPE, consonants); Map vowels = new HashMap<>(); // Tape muffles all vowels to near silence vowels.put('a', new String[] { "m", "mm" }); vowels.put('e', new String[] { "n", "m" }); vowels.put('i', new String[] { "n", "m" }); vowels.put('o', new String[] { "m", "mm" }); vowels.put('u', new String[] { "m", "u" }); vowels.put('y', new String[] { "n", "m" }); VOWEL_MAPS.put(GagMaterial.TAPE, vowels); } private static void initializeStuffedMappings() { Map consonants = new HashMap<>(); // Stuffed gag - nearly silent for (char c = 'a'; c <= 'z'; c++) { if (!VOWELS.contains(c)) { consonants.put(c, new String[] { "mm", "m", "" }); } } CONSONANT_MAPS.put(GagMaterial.STUFFED, consonants); Map vowels = new HashMap<>(); for (char c : VOWELS) { vowels.put(c, new String[] { "mm", "m", "" }); } VOWEL_MAPS.put(GagMaterial.STUFFED, vowels); } private static void initializePanelMappings() { // Panel gag - similar to tape but slightly more sound Map consonants = new HashMap<>(); consonants.put('b', new String[] { "m", "mph" }); consonants.put('c', new String[] { "n", "m" }); consonants.put('d', new String[] { "n", "nd" }); consonants.put('f', new String[] { "m", "f" }); consonants.put('g', new String[] { "n", "ng" }); consonants.put('h', new String[] { "hm", "m" }); consonants.put('j', new String[] { "n", "m" }); consonants.put('k', new String[] { "n", "m" }); consonants.put('l', new String[] { "n", "m" }); consonants.put('m', new String[] { "m", "mm" }); consonants.put('n', new String[] { "n", "nn" }); consonants.put('p', new String[] { "m", "mph" }); consonants.put('q', new String[] { "n", "m" }); consonants.put('r', new String[] { "n", "m" }); consonants.put('s', new String[] { "s", "m" }); consonants.put('t', new String[] { "n", "m" }); consonants.put('v', new String[] { "m", "f" }); consonants.put('w', new String[] { "m", "u" }); consonants.put('x', new String[] { "n", "m" }); consonants.put('z', new String[] { "n", "m" }); CONSONANT_MAPS.put(GagMaterial.PANEL, consonants); Map vowels = new HashMap<>(); vowels.put('a', new String[] { "m", "ah" }); vowels.put('e', new String[] { "n", "m" }); vowels.put('i', new String[] { "n", "m" }); vowels.put('o', new String[] { "m", "oh" }); vowels.put('u', new String[] { "m", "u" }); vowels.put('y', new String[] { "n", "m" }); VOWEL_MAPS.put(GagMaterial.PANEL, vowels); } private static void initializeLatexMappings() { Map consonants = new HashMap<>(); // Latex - tight seal, rubber sounds consonants.put('b', new String[] { "m", "mm" }); consonants.put('c', new String[] { "n", "m" }); consonants.put('d', new String[] { "n", "nn" }); consonants.put('f', new String[] { "h", "f" }); consonants.put('g', new String[] { "ng", "m" }); consonants.put('h', new String[] { "h", "hh" }); consonants.put('j', new String[] { "n", "m" }); consonants.put('k', new String[] { "n", "m" }); consonants.put('l', new String[] { "n", "m" }); consonants.put('m', new String[] { "m", "mm" }); consonants.put('n', new String[] { "n", "nn" }); consonants.put('p', new String[] { "m", "mm" }); consonants.put('q', new String[] { "n", "m" }); consonants.put('r', new String[] { "n", "m" }); consonants.put('s', new String[] { "s", "h" }); consonants.put('t', new String[] { "n", "nn" }); consonants.put('v', new String[] { "f", "m" }); consonants.put('w', new String[] { "m", "u" }); consonants.put('x', new String[] { "n", "m" }); consonants.put('z', new String[] { "s", "m" }); CONSONANT_MAPS.put(GagMaterial.LATEX, consonants); Map vowels = new HashMap<>(); vowels.put('a', new String[] { "u", "a" }); vowels.put('e', new String[] { "u", "e" }); vowels.put('i', new String[] { "u", "i" }); vowels.put('o', new String[] { "u", "o" }); vowels.put('u', new String[] { "u", "uu" }); vowels.put('y', new String[] { "u", "i" }); VOWEL_MAPS.put(GagMaterial.LATEX, vowels); } private static void initializeRingMappings() { Map consonants = new HashMap<>(); // Ring gag - mouth forced open, tongue partially free consonants.put('b', new String[] { "b", "bh" }); consonants.put('c', new String[] { "k", "kh" }); consonants.put('d', new String[] { "d", "dh" }); consonants.put('f', new String[] { "f", "fh" }); consonants.put('g', new String[] { "g", "gh" }); consonants.put('h', new String[] { "h", "ah" }); consonants.put('j', new String[] { "j", "zh" }); consonants.put('k', new String[] { "k", "kh" }); consonants.put('l', new String[] { "l", "lh" }); consonants.put('m', new String[] { "m", "mh" }); consonants.put('n', new String[] { "n", "nh" }); consonants.put('p', new String[] { "p", "ph" }); consonants.put('q', new String[] { "k", "kh" }); consonants.put('r', new String[] { "r", "rh" }); consonants.put('s', new String[] { "s", "sh" }); consonants.put('t', new String[] { "t", "th" }); consonants.put('v', new String[] { "v", "vh" }); consonants.put('w', new String[] { "w", "wh" }); consonants.put('x', new String[] { "ks", "kh" }); consonants.put('z', new String[] { "z", "zh" }); CONSONANT_MAPS.put(GagMaterial.RING, consonants); Map vowels = new HashMap<>(); // Ring forces mouth open - vowels become "a/ah" vowels.put('a', new String[] { "a", "ah" }); vowels.put('e', new String[] { "eh", "a" }); vowels.put('i', new String[] { "ih", "a" }); vowels.put('o', new String[] { "oh", "a" }); vowels.put('u', new String[] { "uh", "a" }); vowels.put('y', new String[] { "ih", "a" }); VOWEL_MAPS.put(GagMaterial.RING, vowels); } private static void initializeBiteMappings() { Map consonants = new HashMap<>(); // Bite gag - teeth clenched on bar consonants.put('b', new String[] { "bh", "ph" }); consonants.put('c', new String[] { "kh", "gh" }); consonants.put('d', new String[] { "dh", "th" }); consonants.put('f', new String[] { "fh", "f" }); consonants.put('g', new String[] { "gh", "ng" }); consonants.put('h', new String[] { "h", "hh" }); consonants.put('j', new String[] { "jh", "zh" }); consonants.put('k', new String[] { "kh", "gh" }); consonants.put('l', new String[] { "lh", "hl" }); consonants.put('m', new String[] { "m", "mh" }); consonants.put('n', new String[] { "n", "nh" }); consonants.put('p', new String[] { "ph", "bh" }); consonants.put('q', new String[] { "kh", "gh" }); consonants.put('r', new String[] { "rh", "hr" }); consonants.put('s', new String[] { "sh", "s" }); consonants.put('t', new String[] { "th", "dh" }); consonants.put('v', new String[] { "vh", "fh" }); consonants.put('w', new String[] { "wh", "uh" }); consonants.put('x', new String[] { "ksh", "kh" }); consonants.put('z', new String[] { "zh", "sh" }); CONSONANT_MAPS.put(GagMaterial.BITE, consonants); Map vowels = new HashMap<>(); vowels.put('a', new String[] { "eh", "ah" }); vowels.put('e', new String[] { "eh", "e" }); vowels.put('i', new String[] { "ih", "eh" }); vowels.put('o', new String[] { "oh", "eh" }); vowels.put('u', new String[] { "uh", "eh" }); vowels.put('y', new String[] { "ih", "eh" }); VOWEL_MAPS.put(GagMaterial.BITE, vowels); } private static void initializeSpongeMappings() { // Sponge - absorbs almost all sound Map consonants = new HashMap<>(); for (char c = 'a'; c <= 'z'; c++) { if (!VOWELS.contains(c)) { consonants.put(c, new String[] { "mm", "" }); } } CONSONANT_MAPS.put(GagMaterial.SPONGE, consonants); Map vowels = new HashMap<>(); for (char c : VOWELS) { vowels.put(c, new String[] { "mm", "" }); } VOWEL_MAPS.put(GagMaterial.SPONGE, vowels); } private static void initializeBaguetteMappings() { Map consonants = new HashMap<>(); // Baguette - comedic, food-blocked consonants.put('b', new String[] { "bm", "mm" }); consonants.put('c', new String[] { "km", "gm" }); consonants.put('d', new String[] { "dm", "nm" }); consonants.put('f', new String[] { "fm", "hm" }); consonants.put('g', new String[] { "gm", "ngm" }); consonants.put('h', new String[] { "hm", "h" }); consonants.put('j', new String[] { "jm", "zhm" }); consonants.put('k', new String[] { "km", "gm" }); consonants.put('l', new String[] { "lm", "mm" }); consonants.put('m', new String[] { "mm", "m" }); consonants.put('n', new String[] { "nm", "n" }); consonants.put('p', new String[] { "pm", "mm" }); consonants.put('q', new String[] { "km", "gm" }); consonants.put('r', new String[] { "rm", "mm" }); consonants.put('s', new String[] { "sm", "shm" }); consonants.put('t', new String[] { "tm", "nm" }); consonants.put('v', new String[] { "vm", "fm" }); consonants.put('w', new String[] { "wm", "um" }); consonants.put('x', new String[] { "ksm", "km" }); consonants.put('z', new String[] { "zm", "sm" }); CONSONANT_MAPS.put(GagMaterial.BAGUETTE, consonants); Map vowels = new HashMap<>(); vowels.put('a', new String[] { "am", "om" }); vowels.put('e', new String[] { "em", "um" }); vowels.put('i', new String[] { "im", "um" }); vowels.put('o', new String[] { "om", "o" }); vowels.put('u', new String[] { "um", "u" }); vowels.put('y', new String[] { "im", "um" }); VOWEL_MAPS.put(GagMaterial.BAGUETTE, vowels); } /** * Map a single phoneme to its muffled equivalent. * * @param c The original character * @param material The gag material * @param bleedChance Chance (0-1) that the original sound passes through * @return The muffled phoneme */ public static String mapPhoneme( char c, GagMaterial material, float bleedChance ) { char lower = Character.toLowerCase(c); // Non-alphabetic characters pass through if (!Character.isLetter(c)) { return String.valueOf(c); } // Bleed-through check: original sound passes if (RANDOM.nextFloat() < bleedChance) { return String.valueOf(c); } // Get appropriate map Map map = isVowel(lower) ? VOWEL_MAPS.get(material) : CONSONANT_MAPS.get(material); if (map == null) { return String.valueOf(c); } String[] options = map.get(lower); if (options == null || options.length == 0) { // Default fallback return isVowel(lower) ? "mm" : "nn"; } // Pick a random option String result = options[RANDOM.nextInt(options.length)]; // Preserve case for first character if (Character.isUpperCase(c) && !result.isEmpty()) { return ( Character.toUpperCase(result.charAt(0)) + result.substring(1) ); } return result; } /** * Check if a character is a vowel. */ public static boolean isVowel(char c) { return VOWELS.contains(Character.toLowerCase(c)); } /** * Check if a character is a plosive consonant. */ public static boolean isPlosive(char c) { return PLOSIVES.contains(Character.toLowerCase(c)); } /** * Check if a character is a nasal consonant. */ public static boolean isNasal(char c) { return NASALS.contains(Character.toLowerCase(c)); } /** * Check if a character is a fricative consonant. */ public static boolean isFricative(char c) { return FRICATIVES.contains(Character.toLowerCase(c)); } /** * Check if a character is a liquid consonant. */ public static boolean isLiquid(char c) { return LIQUIDS.contains(Character.toLowerCase(c)); } /** * Check if a character can potentially bleed through for a given material. * Nasals have higher bleed-through, plosives have lower. */ public static float getBleedModifier(char c, GagMaterial material) { char lower = Character.toLowerCase(c); // Nasals almost always pass through if (isNasal(lower)) { return 2.0f; } // Plosives are harder to pronounce with most gags if (isPlosive(lower)) { return material == GagMaterial.RING ? 1.5f : 0.3f; } // Fricatives depend on whether air can escape if (isFricative(lower)) { return ( material == GagMaterial.TAPE || material == GagMaterial.STUFFED ) ? 0.1f : 0.8f; } // Liquids depend on tongue freedom if (isLiquid(lower)) { return ( material == GagMaterial.RING || material == GagMaterial.BITE ) ? 1.2f : 0.2f; } return 1.0f; } }