/* 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 (
✦ ✦ ✦
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;