/* global React */ /* ============================================================ KEYWORDS — taxonomia central de termos do jogo ============================================================ Esta é a única fonte de verdade para classificar termos do sistema. Toda nova aba (Classes, Raças, Itens, Bestiário) apenas adiciona entradas aqui — sem refatorar nada mais. Como funciona: 1) KEYWORDS é um dicionário { termo: { kind, tooltip? } } 2) renderiza um span colorido (via --kw-{kind}) 3) parseText(str) varre a string e marca automaticamente todo termo conhecido, devolvendo um array de React nodes 4) Adicione termos novos a KEYWORDS e tudo já fica colorido ============================================================ */ // Tipos semânticos (cada um tem cor própria em styles.css) const KW_KINDS = { school: { label: "Escola de magia", color: "var(--kw-school)" }, circle: { label: "Círculo de magia", color: "var(--kw-circle)" }, "power-type":{ label: "Tipo de poder", color: "var(--kw-power-type)" }, condition: { label: "Condição", color: "var(--kw-condition)" }, damage: { label: "Tipo de dano", color: "var(--kw-damage)" }, attribute: { label: "Atributo", color: "var(--kw-attribute)" }, class: { label: "Classe", color: "var(--kw-class)" }, race: { label: "Raça", color: "var(--kw-race)" }, action: { label: "Ação", color: "var(--kw-action)" }, area: { label: "Área/Alcance", color: "var(--kw-area)" }, item: { label: "Item/Equipamento", color: "var(--kw-item)" }, dice: { label: "Dado", color: "var(--kw-dice)" }, }; // dicionário canônico. Match case-insensitive, whole-word. // "alias" permite que vários textos resolvam para o mesmo termo. const KEYWORDS = { // — escolas — "Abjuração": { kind: "school" }, "Adivinhação": { kind: "school" }, "Convocação": { kind: "school" }, "Encantamento": { kind: "school" }, "Evocação": { kind: "school" }, "Ilusão": { kind: "school" }, "Necromancia": { kind: "school" }, "Transmutação": { kind: "school" }, // — tipos de poder — "Combate": { kind: "power-type" }, "Concedido": { kind: "power-type" }, "Destino": { kind: "power-type" }, "Mágico": { kind: "power-type" }, "Tormenta": { kind: "power-type" }, // — condições — "Atordoado": { kind: "condition" }, "Caído": { kind: "condition" }, "Enfraquecido": { kind: "condition" }, "Esmorecido": { kind: "condition" }, "Frustrado": { kind: "condition" }, "Imóvel": { kind: "condition" }, "Paralisado": { kind: "condition" }, "Pasmo": { kind: "condition" }, "Surpreendido": { kind: "condition" }, "Vulnerável": { kind: "condition" }, "Indefeso": { kind: "condition" }, // — danos — "fogo": { kind: "damage" }, "frio": { kind: "damage" }, "elétrico": { kind: "damage" }, "ácido": { kind: "damage" }, "trovão": { kind: "damage" }, "necrótico": { kind: "damage" }, "psíquico": { kind: "damage" }, "radiante": { kind: "damage" }, "perfurante": { kind: "damage" }, "cortante": { kind: "damage" }, "contundente": { kind: "damage" }, // — atributos — "Força": { kind: "attribute" }, "Destreza": { kind: "attribute" }, "Constituição": { kind: "attribute" }, "Inteligência": { kind: "attribute" }, "Sabedoria": { kind: "attribute" }, "Carisma": { kind: "attribute" }, "For": { kind: "attribute" }, "Des": { kind: "attribute" }, "Con": { kind: "attribute" }, "Int": { kind: "attribute" }, "Sab": { kind: "attribute" }, "Car": { kind: "attribute" }, // — classes (preparadas para futuro) — "Bárbaro": { kind: "class" }, "Bardo": { kind: "class" }, "Bucaneiro": { kind: "class" }, "Cavaleiro": { kind: "class" }, "Caçador": { kind: "class" }, "Clérigo": { kind: "class" }, "Druida": { kind: "class" }, "Guerreiro": { kind: "class" }, "Inventor": { kind: "class" }, "Ladino": { kind: "class" }, "Lutador": { kind: "class" }, "Mago": { kind: "class" }, "Nobre": { kind: "class" }, "Paladino": { kind: "class" }, // — raças — "Humano": { kind: "race" }, "Anão": { kind: "race" }, "Elfo": { kind: "race" }, "Goblin": { kind: "race" }, "Hynne": { kind: "race" }, "Lefou": { kind: "race" }, "Minotauro": { kind: "race" }, "Qareen": { kind: "race" }, "Golem": { kind: "race" }, "Osteon": { kind: "race" }, "Sereia": { kind: "race" }, "Silfide": { kind: "race" }, "Trog": { kind: "race" }, // — ações — "ação padrão": { kind: "action" }, "ação completa": { kind: "action" }, "ação livre": { kind: "action" }, "ação de movimento": { kind: "action" }, "reação": { kind: "action" }, "1 PM": { kind: "action", tooltip: "Custa 1 ponto de mana" }, "2 PM": { kind: "action", tooltip: "Custa 2 pontos de mana" }, "3 PM": { kind: "action", tooltip: "Custa 3 pontos de mana" }, "4 PM": { kind: "action", tooltip: "Custa 4 pontos de mana" }, "5 PM": { kind: "action", tooltip: "Custa 5 pontos de mana" }, // — itens — "componente material": { kind: "item" }, "foco arcano": { kind: "item" }, "símbolo sagrado": { kind: "item" }, }; // regex de dados (1d6, 2d8+3, etc) — captura inline, sem precisar listar const DICE_RE = /\b(\d+d\d+(?:\s*[+-]\s*\d+)?|\d+d\d+)\b/g; // base da wiki para links internos [[Página|Texto]] const WIKI_BASE = "https://tormenta-collab.fandom.com/pt-br/wiki/"; // hiperlink externo: [https://url.com Texto do link] const LINK_RE = /\[(https?:\/\/\S+)\s+([^\]]+)\]/g; // link interno wiki: [[Página|Texto]] ou [[Página]] const WIKI_LINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g; // monta regex único para todas as chaves (ordenado por tamanho desc // pra "ação completa" ganhar de "ação") const ALL_TERMS = Object.keys(KEYWORDS).sort((a, b) => b.length - a.length); const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const KW_RE = new RegExp( "\\b(" + ALL_TERMS.map(escape).join("|") + ")\\b", "gi" ); // — Kw: renderiza um termo — function Kw({ kind, children, title }) { const meta = KW_KINDS[kind] || {}; return ( {children} ); } // — parseText: string → array de nodes com Kw onde necessário — function parseText(str) { if (!str) return []; const out = []; let cursor = 0; // 1) achar todos os matches de termos + dados, ordenar por índice const matches = []; for (const m of str.matchAll(KW_RE)) { const original = m[0]; // achar a entrada canônica (case-insensitive) const canonical = ALL_TERMS.find((t) => t.toLowerCase() === original.toLowerCase()); if (canonical) { matches.push({ idx: m.index, len: original.length, text: original, kind: KEYWORDS[canonical].kind, tooltip: KEYWORDS[canonical].tooltip, }); } } for (const m of str.matchAll(DICE_RE)) { matches.push({ idx: m.index, len: m[0].length, text: m[0], kind: "dice", }); } for (const m of str.matchAll(LINK_RE)) { matches.push({ idx: m.index, len: m[0].length, text: m[2], href: m[1], kind: "link", }); } for (const m of str.matchAll(WIKI_LINK_RE)) { const page = m[1].trim(); matches.push({ idx: m.index, len: m[0].length, text: (m[2] || page).trim(), href: WIKI_BASE + encodeURIComponent(page), kind: "link", }); } // ordenar e remover overlap matches.sort((a, b) => a.idx - b.idx); const clean = []; let lastEnd = -1; for (const m of matches) { if (m.idx >= lastEnd) { clean.push(m); lastEnd = m.idx + m.len; } } // 2) montar nodes for (const m of clean) { if (cursor < m.idx) out.push(str.slice(cursor, m.idx)); if (m.kind === "link") { out.push( {m.text} ); } else { out.push( {m.text} ); } cursor = m.idx + m.len; } if (cursor < str.length) out.push(str.slice(cursor)); return out; } // componente conveniente: parsea uma string solta function Parsed({ children }) { if (typeof children !== "string") return children; return <>{parseText(children).map((n, i) => ( typeof n === "string" ? {n} : n ))}; } // expõe globais Object.assign(window, { KEYWORDS, KW_KINDS, Kw, parseText, Parsed });