/** * CommandLinkManager — mounts as a child of
and intercepts * clicks on :cmd[] directive spans to send them as journal stream * messages. * * Uses capture-phase event delegation on [data-cmd] spans. */ import { Component, onCleanup } from "solid-js"; import { useArticleDom } from "./Article"; import { useJournalStream, sendMessage } from "./stores/journalStream"; import { useJournalCompletions } from "./journal/completions"; import { parseInput } from "./journal/command-parser"; import { resolveRollPayload } from "./journal/types/roll"; import { resolveSparkPayload } from "./journal/types/spark"; import { resolveStatRoll, resolveTemplateSet, canModifyStat, fullKey, findStatDef, } from "./journal/stat-helpers"; function unwrap( r: { success: true; msg: R } | { success: false; error: string }, ) { return r.success ? ({ ok: true, err: undefined } as const) : ({ ok: false, err: r.error } as const); } export const CommandLinkManager: Component = () => { const contentDom = useArticleDom(); const stream = useJournalStream(); const comp = useJournalCompletions(); // ---- Click interceptor (capture phase) ---- const onClick = (e: MouseEvent) => { const cmdSpan = (e.target as HTMLElement).closest("[data-cmd]"); if (!cmdSpan) return; const command = cmdSpan.getAttribute("data-cmd"); if (!command) return; e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); dispatchCommand(command); }; const dom = contentDom(); if (dom) { dom.addEventListener("click", onClick, true); } onCleanup(() => { const d = contentDom(); if (d) d.removeEventListener("click", onClick, true); }); // ---- Command dispatch ---- function dispatchCommand(command: string) { // parseInput expects /roll, /spark, etc. — prepend / if missing const prefixed = command.startsWith("/") ? command : "/" + command; const parsed = parseInput(prefixed); if (parsed.error) return; if (parsed.type === "roll") { const p = resolveRollPayload( parsed.payload as { notation: string; label?: string }, ); sendMessage("roll", p); } else if (parsed.type === "spark") { void (async () => { try { const key = (parsed.payload as { key: string }).key; const match = comp.data.sparkTables.find((s) => s.slug === key); const csvPath = match?.csvPath ?? ""; const remix = match?.remix ?? false; const p = await resolveSparkPayload({ key, csvPath, remix }); sendMessage("spark", p); } catch { // silently ignore spark table errors from directive clicks } })(); } else if (parsed.type === "stat") { dispatchStat(parsed.payload as Record); } else { sendMessage(parsed.type, parsed.payload); } } /** Resolve bare key -> full key. */ function resolveKey( inputKey: string, statDefs: typeof comp.data.stats, playerName: string, ): string { if (statDefs.some((d) => fullKey(d, playerName) === inputKey)) return inputKey; const def = statDefs.find((d) => d.key === inputKey); if (def) return fullKey(def, playerName); return inputKey; } function dispatchStat(payload: Record) { const p = payload as { action?: string; key?: string; value?: string }; if (p.action === "roll" && p.key) { const resolved = resolveStatRoll( p.key, comp.data.stats, stream.stats, stream.myName, comp.data.statTemplates, ); if (resolved.error) return; const result = sendMessage("stat", { action: "set", key: resolved.fullKey, value: resolved.value, }); if (!unwrap(result).ok) return; if (resolved.modifiers) { for (const [mk, mv] of Object.entries(resolved.modifiers)) { sendMessage("stat", { action: "set", key: mk, value: mv }); } } return; } if (!p.action || !p.key) return; const fk = resolveKey(p.key, comp.data.stats, stream.myName); if (!canModifyStat(stream.myRole, stream.myName, fk, comp.data.stats)) { return; } sendMessage("stat", { action: p.action, key: fk, value: p.value }); if (p.action === "set" && p.value) { const def = findStatDef(fk, comp.data.stats, stream.myName); if (def) { const modifiers = resolveTemplateSet( def, p.value, comp.data.stats, stream.stats, stream.myName, comp.data.statTemplates, ); if (modifiers) { for (const [mk, mv] of Object.entries(modifiers)) { sendMessage("stat", { action: "set", key: mk, value: mv }); } } } } } return null; }; export default CommandLinkManager;