/* global React, window, Kw, Parsed */ /* ============================================================ DEITY CREATOR — aba "Divindade" do livro. Lista agrupada por panteão à esquerda. Detalhes completos (com poderes concedidos) à direita. Seleção persiste em localStorage. Regra de poderes concedidos: - Paladino e Clérigo → 2 poderes - demais classes → 1 poder ============================================================ */ const { useState, useEffect, useMemo } = React; const LS_DEITY = "arton_char_deity"; const LS_CLASS = "arton_char_class"; function dcLsGet(key) { try { return JSON.parse(localStorage.getItem(key)); } catch { return null; } } function dcLsSet(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} } /** Lê as classes salvas do personagem (multiclasse). */ function getSavedClassNames() { try { const data = JSON.parse(localStorage.getItem(LS_CLASS)); if (data && Array.isArray(data.classes)) { return data.classes.map((c) => c.className).filter(Boolean); } } catch {} return []; } /** Quais das classes salvas têm is_devoted=true no backend. */ function getDevotedSavedClass(classNames) { const allClasses = window.CLASSES || []; for (const n of classNames || []) { const cls = allClasses.find((c) => c.name === n); if (cls?.is_devoted) return n; } return null; } /** Quantos poderes concedidos o personagem pode escolher. Se ele tem ALGUMA classe devota, ganha o bônus. */ function getMaxPowers(classNames) { return getDevotedSavedClass(classNames) ? 2 : 1; } /** Intersecta allowed_deities de TODAS as classes que têm restrição. Classes sem restrição não contribuem. Retorna null = sem restrição. */ function getAllowedDeities(classNames) { const allClasses = window.CLASSES || []; let allowed = null; for (const n of classNames || []) { const cls = allClasses.find((c) => c.name === n); const list = cls?.allowed_deities; if (!Array.isArray(list) || list.length === 0) continue; if (allowed === null) { allowed = new Set(list); } else { allowed = new Set([...allowed].filter((d) => list.includes(d))); } } return allowed ? [...allowed] : null; } const CHANNEL_COLORS = { "Positiva": "var(--gilt-0)", "Negativa": "var(--kw-school)", "Ambas": "var(--kw-damage)", }; function DCCorners() { return <> ; } function ChannelDot({ energy }) { return ( ); } /* ============================================================ DEITY LIST (página esquerda) ============================================================ */ function DeityList({ deities, total, selected, saved, onSelect, query, setQuery, groups, activeGroup, onGroupToggle, activeChannel, onChannelToggle, restrictionHint, }) { const folioLabel = deities.length < total ? `${deities.length} de ${total}` : String(total); const grouped = useMemo(() => { const map = new Map(); for (const d of deities) { if (!map.has(d.deity_group)) map.set(d.deity_group, []); map.get(d.deity_group).push(d); } return map; }, [deities]); return (
Divindades · Arton {folioLabel} divindades

Panteão de Arton

Escolha uma divindade patrona. Ela concede poderes e obrigações ao devoto.

{restrictionHint && (
{restrictionHint}
)}
setQuery(e.target.value)} />
{groups.length > 1 && (
{groups.map(g => { const short = g .replace(/^Panteão\s+(d[oa]s?|d[ae])\s+/i, "") .replace(/^Panteão\s+/i, "") .trim(); return ( ); })}
)}
{["Positiva", "Negativa"].map(ch => ( ))}
{[...grouped.entries()].map(([groupName, members]) => (
{groupName}
{members.map(d => { const isSaved = saved?.id === d.id; const isSelected = selected?.id === d.id; return ( ); })}
))} {deities.length === 0 && (
Nenhuma divindade encontrada.
)}
); } /* ============================================================ POWER ROW (expansível + seletor checkbox/rádio) ============================================================ */ function GrantedPowerRow({ power, selected, onSelect, disabled }) { const [open, setOpen] = useState(false); return (
setOpen(o => !o)}> {power.name} setOpen(o => !o)}> {power.power_type} {power.power_category ? <> · {power.power_category} : null} setOpen(o => !o)} aria-label={open ? "Recolher" : "Expandir"} >{open ? "−" : "+"}
{open && power.description && (
{power.prerequisites && (

Pré-requisito:{" "} {power.prerequisites}

)} {power.description}
)}
); } /* ============================================================ DEITY DETAIL (página direita) ============================================================ */ function DeityDetail({ deity, fullDeity, detailLoading, saved, onSave, chosenIds, togglePower, maxPowers, devotedClassName, }) { if (!deity) { return (
Detalhes
✦ ✦ ✦
Selecione uma divindade à esquerda.
); } const display = fullDeity || deity; const powers = display.granted_powers || []; /* objetos dos poderes escolhidos (precisam estar carregados) */ const chosenPowers = powers.filter(p => chosenIds.has(p.id)); /* está salvo exatamente este combo? */ const isSavedDeity = saved?.id === deity.id; const savedIds = new Set((saved?.chosen_powers || []).map(p => p.id)); const isExactSave = isSavedDeity && savedIds.size === chosenIds.size && [...chosenIds].every(id => savedIds.has(id)); const canSave = chosenIds.size === maxPowers && !isExactSave; /* label do botão */ let saveLabel; if (isExactSave) { saveLabel = `✦ Devotado a ${deity.name}`; } else if (chosenIds.size === 0) { saveLabel = maxPowers === 1 ? "Escolha um poder concedido" : `Escolha ${maxPowers} poderes concedidos`; } else if (chosenIds.size < maxPowers) { const faltam = maxPowers - chosenIds.size; saveLabel = `Escolha mais ${faltam} poder${faltam > 1 ? "es" : ""}`; } else if (isSavedDeity) { saveLabel = "Atualizar poderes"; } else { saveLabel = `Devotar-se a ${deity.name}`; } /* hint da seção de poderes */ let powerHint; if (chosenIds.size === maxPowers) { const names = chosenPowers.map(p => p.name).join(" · "); powerHint = names ? { text: names, color: "var(--gilt-1)" } : { text: "restaurando escolha…", color: "var(--ink-2)" }; } else { const faltam = maxPowers - chosenIds.size; powerHint = { text: `escolha ${faltam} poder${faltam > 1 ? "es" : ""}`, color: "var(--kw-condition)", }; } /* badge de classe devota */ const isDevotedClass = !!devotedClassName; return (
{deity.deity_group} {" "}{deity.channel_energy}

{deity.name}

{deity.motto && (

"{deity.motto}"

)} {deity.other_names && (

Também conhecida como: {deity.other_names}

)}
Símbolo sagrado
{deity.sacred_symbol || "—"}
Arma preferida
{deity.preferred_weapon || "—"}
Plano divino
{deity.divine_world || "—"}
Energia canalizada
{deity.channel_energy}
{deity.significant_colors && (
Cores significativas
{deity.significant_colors}
)}
{deity.description && (
{deity.description.split("\n\n").map((p, i) => (

{p}

))}
)} {deity.areas_of_influence && (
Áreas de Influência
{deity.areas_of_influence .split(",") .map(a => a.trim()) .filter(Boolean) .map((area, i) => ( {area} ))}
)} {deity.beliefs_and_goals && (
Crenças e Objetivos

{deity.beliefs_and_goals}

)} {deity.obligations && (
Obrigações dos Devotos

{deity.obligations}

)} {((deity.devoted_races && deity.devoted_races.length > 0) || (deity.devoted_classes && deity.devoted_classes.length > 0)) && (
Devotos Típicos
{(deity.devoted_races || []).map((r, i) => ( {r} ))} {(deity.devoted_classes || []).map((c, i) => ( {c} ))}
)} {/* poderes concedidos */} {(deity.granted_power_names && deity.granted_power_names.length > 0) && (
{maxPowers === 1 ? "Poder Concedido" : "Poderes Concedidos"} {/* badge de classe devota */} {isDevotedClass && ( {devotedClassName} {" · "}+1 poder )} {powerHint.text} {!detailLoading && powers.length > 0 && ( {powers.length} disponíve{powers.length === 1 ? "l" : "is"} )}
{/* slots visuais quando maxPowers > 1 */} {maxPowers > 1 && (
{Array.from({ length: maxPowers }).map((_, i) => { const p = chosenPowers[i]; return (
{p ? p.name : vazio}
); })}
)} {detailLoading ? (
{deity.granted_power_names.map((name, i) => (
{name}
))}
) : powers.length > 0 ? (
{powers.map(p => { const isSelected = chosenIds.has(p.id); const isDisabled = !isSelected && chosenIds.size >= maxPowers; return ( togglePower(p.id)} /> ); })}
) : (
{deity.granted_power_names.join(" · ")}
)}
)}
); } /* ============================================================ DEITY CREATOR (raiz da aba "divindade") ============================================================ */ function DeityCreator() { const [allDeities, setAllDeities] = useState([]); const [loading, setLoading] = useState(true); const [query, setQuery] = useState(""); const [activeGroup, setActiveGroup] = useState(null); const [activeChannel, setActiveChannel] = useState(null); const [selected, setSelected] = useState(null); const [fullDeity, setFullDeity] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [saved, setSaved] = useState(() => dcLsGet(LS_DEITY)); /* conjunto de IDs de poderes selecionados */ const [chosenIds, setChosenIds] = useState(() => new Set()); /* classes do personagem (multiclasse) — lidas no mount e sincronizadas em foco */ const [savedClassNames, setSavedClassNames] = useState(() => getSavedClassNames()); const devotedClass = getDevotedSavedClass(savedClassNames); const maxPowers = getMaxPowers(savedClassNames); const allowedNames = useMemo( () => getAllowedDeities(savedClassNames), [savedClassNames] ); /* atualiza classes se o jogador as trocou em outra aba */ useEffect(() => { const sync = () => setSavedClassNames(getSavedClassNames()); window.addEventListener("focus", sync); return () => window.removeEventListener("focus", sync); }, []); /* quando maxPowers diminui (ex: Paladino → Guerreiro), descarta excedentes */ useEffect(() => { setChosenIds(prev => { if (prev.size <= maxPowers) return prev; return new Set([...prev].slice(0, maxPowers)); }); }, [maxPowers]); /* carga inicial: busca todas as divindades e restaura seleção */ useEffect(() => { window.fetchDeities().then(data => { setAllDeities(data); setLoading(false); const s = dcLsGet(LS_DEITY); if (s) setSelected(data.find(d => d.id === s.id) || null); }); }, []); /* carrega detalhe ao selecionar divindade; restaura poderes se for a salva */ useEffect(() => { if (!selected) { setFullDeity(null); setChosenIds(new Set()); return; } setDetailLoading(true); setFullDeity(null); /* restaura IDs salvos respeitando o maxPowers atual. Suporta formato antigo (chosen_power) e novo (chosen_powers). */ const s = dcLsGet(LS_DEITY); if (s?.id === selected.id) { const ids = s.chosen_powers ? s.chosen_powers.map(p => p.id) : s.chosen_power?.id ? [s.chosen_power.id] : []; setChosenIds(new Set(ids.slice(0, maxPowers))); } else { setChosenIds(new Set()); } window.fetchDeity(selected.id).then(data => { setFullDeity(data); setDetailLoading(false); }); }, [selected && selected.id]); /* toggle de um poder: adiciona/remove respeitando maxPowers */ const togglePower = (powerId) => { setChosenIds(prev => { const next = new Set(prev); if (next.has(powerId)) { next.delete(powerId); } else if (next.size < maxPowers) { next.add(powerId); } /* se maxPowers === 1 e já há um selecionado, substituir */ else if (maxPowers === 1) { next.clear(); next.add(powerId); } return next; }); }; const groups = useMemo( () => [...new Set(allDeities.map(d => d.deity_group))], [allDeities] ); const filtered = useMemo(() => { let out = allDeities; /* restrição por classe (Druida só pode Allihanna, Megalokk, Oceano, Aharadak, Tenebra) */ if (Array.isArray(allowedNames)) { const set = new Set(allowedNames); out = out.filter(d => set.has(d.name)); } if (activeGroup) out = out.filter(d => d.deity_group === activeGroup); if (activeChannel) out = out.filter(d => d.channel_energy === activeChannel); const q = query.toLowerCase().trim(); if (q) out = out.filter(d => d.name.toLowerCase().includes(q) || (d.areas_of_influence || "").toLowerCase().includes(q) || (d.other_names || "").toLowerCase().includes(q) || (d.deity_group || "").toLowerCase().includes(q) ); return out; }, [allDeities, query, activeGroup, activeChannel, allowedNames]); const handleSave = (deity, chosenPowers) => { const val = { id: deity.id, name: deity.name, deity_group: deity.deity_group, chosen_powers: chosenPowers.map(p => ({ id: p.id, name: p.name })), }; dcLsSet(LS_DEITY, val); setSaved(val); }; if (loading) { return (
·
Consultando os panteões…
); } return (
setActiveGroup(prev => prev === g ? null : g)} activeChannel={activeChannel} onChannelToggle={ch => setActiveChannel(prev => prev === ch ? null : ch)} restrictionHint={ allowedNames ? `Sua classe permite apenas: ${allowedNames.join(" · ")}` : null } />
); } window.DeityCreator = DeityCreator;