// ── Character identity rules ────────────────────────────────────────────────── // Which alias/name tokens are evidence that two character sheets describe the // SAME person. A token made only of common nouns, roles, ranks, species or an // article-led description ("Zwerg", "der Zwerg", "Mann", "Fremder", // "Alter Soldat", "Ork-General", "der Tunneltreiber") labels many people and // is never such evidence. On the real library a shared "Zwerg"/"Mann" token // merged or copied unrelated sheets (Arthag/Kolon, Mann/Fremder in book 03). // // Pure functions, no DOM: loaded in the browser before characters-library.js // and character-sheets.js, and importable from Node for unit tests. The word // list mirrors core/character_records.py (a Python test keeps them in sync). (function (root) { 'use strict'; const ARTICLES = new Set(('a an das dem den der des die ein eine einem einen einer eines the').split(' ')); const GENERIC_WORDS = new Set(( 'adept adepten admiral alte alten alter altes amazone anführer anführerin auelfe baron ' + 'baronin bauer befehlshaber begleiter begleiterin blacksmith blonde blonder bote botin boy brother ' + 'bruder bub bäckerstochter bäuerin bürger bürgerin bürgersohn captain chief child colonel commander ' + 'companion creature crowd dame daughter de del demon diener dienerin dritte dritter ' + 'druid druide dunkle dunkler dwarf dämon elf elfe elfen erste erster erzähler ' + 'executioner father feldherr figure fischer frau frauen fremde fremden fremder führer gast ' + 'gefährte gefährtin general gestalt geweihter girl graf greis greisin groß großadmiral große ' + 'großer gräfin guard hauptfrau hauptmann heerführer henker herr herrin herzog herzogin hexe ' + 'human händler häuptling inquisitor irre jung junge jungen junger junges junker jäger ' + 'kaiser kaiserin kapitän kaufmann kid kind king kleine kleiner knabe knecht knight ' + 'kommandant kommandantin kreatur krieger kriegerin kriegshäuptling kutscher könig königin lady leader lehrer ' + 'leute lord magd mage magier magierin man mann marschall marshal master meister ' + 'menge mensch menschen merchant messenger militärführer monster mother mutter mädchen männer mönch ' + 'nachbar nachbarin narrator navigator nonne oberst obrist officer offizier offizierin old onkel ' + 'orc ork orkin orks person priest priester priesterin prince princess prinz prinzessin ' + 'queen reiter ritter ritterin rote roter schamane schmied schreiber schwarzhaarige schwester schüler ' + 'scribe servant shaman sir sister sklave sklaven sklavin smith sohn soldat soldaten ' + 'soldatin soldier son stimme stimmen stranger tante tochter torwächter tote toter unbekannt ' + 'unbekannte unbekannter unknown unsicher vampir vampire vampirin van vater vermummt vermummte vermummten ' + 'vermummter veteran voice vom von wache wachen warrior weber wesen wirt wirtin ' + 'witch witwe witwer wizard woman wächter young zauberer zauberin zu zum zur ' + 'zweite zweiter zwerg zwerge zwergin' ).split(' ')); const GENERIC_SUFFIXES = [...GENERIC_WORDS].filter(w => w.length >= 5).sort((a, b) => b.length - a.length); function normalizeToken(token) { return String(token == null ? '' : token) .replace(/\([^)]*\)/g, ' ') .replace(/\s+/g, ' ') .trim() .replace(/^[\s.,;:!?"'»«„“”‚‘’]+|[\s.,;:!?"'»«„“”‚‘’]+$/g, '') .toLowerCase(); } function isGenericIdentityToken(token) { const norm = normalizeToken(token); if (!norm) return true; const words = norm.split(/[\s-]+/).filter(Boolean); if (!words.length) return true; if (ARTICLES.has(words[0])) return true; // "der Henker": a description return words.every(word => { const bare = word.replace(/^['’.]+|['’.]+$/g, ''); if (bare.length <= 1 || ARTICLES.has(bare) || GENERIC_WORDS.has(bare)) return true; return GENERIC_SUFFIXES.some(sfx => bare.length > sfx.length && bare.endsWith(sfx)); }); } // Shared tokens of two lower-cased token collections that prove identity. function sharedIdentityEvidence(a, b) { const left = new Set([...(a || [])].map(normalizeToken)); const out = []; for (const t of (b || [])) { const n = normalizeToken(t); if (n && left.has(n) && !isGenericIdentityToken(n) && !out.includes(n)) out.push(n); } return out; } const characterIdentityApi = { isGenericIdentityToken, sharedIdentityEvidence, normalizeToken, GENERIC_WORDS, ARTICLES }; root.TTVC = root.TTVC || {}; root.TTVC.characterIdentity = characterIdentityApi; root.ttvcIsGenericIdentityToken = isGenericIdentityToken; root.ttvcSharedIdentityEvidence = sharedIdentityEvidence; if (typeof module !== 'undefined' && module.exports) module.exports = characterIdentityApi; })(typeof window !== 'undefined' ? window : globalThis);