diff --git a/frontend/src/SchemaEditor.jsx b/frontend/src/SchemaEditor.jsx index 0f99773..ea42d45 100644 --- a/frontend/src/SchemaEditor.jsx +++ b/frontend/src/SchemaEditor.jsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { npcInitials } from './components' // Rebuild an object with one key renamed, preserving order. Returns null on a // no-op or a collision (so the caller keeps the old object). @@ -27,13 +28,24 @@ function KeyInput({ value, onCommit, placeholder }) { ) } +// Every control in this editor wears the same tiny caption — that consistency +// is what keeps the mixed row types (stats, flags, milestones, NPCs) reading +// as one form rather than five. +function Field({ label, className = '', children }) { + return ( + + ) +} + function Num({ label, value, onChange }) { return ( - + ) } @@ -65,18 +77,17 @@ function StatEditor({ statKey, def, onRenameKey, onChange, onRemove }) { onChange(next) } return ( -
+
{isText ? ( - + ) : ( <> set('min', v)} /> @@ -86,18 +97,19 @@ function StatEditor({ statKey, def, onRenameKey, onChange, onRemove }) { )} set('cooldown', v)} /> - +
- set('desc', e.target.value || undefined)} /> + + set('desc', e.target.value || undefined)} /> + {!isText && (
Bands — low, high, label
@@ -121,7 +133,7 @@ function StatEditor({ statKey, def, onRenameKey, onChange, onRemove }) { ) } -function StatSection({ title, defs, onChange, addLabel = '+ stat', nested }) { +function StatSection({ title, hint, defs, onChange, addLabel = '+ stat', nested }) { const entries = Object.entries(defs || {}) const rename = (o, n) => { const x = withRenamedKey(defs, o, n); if (x) onChange(x) } const setDef = (k, val) => onChange({ ...defs, [k]: val }) @@ -137,6 +149,7 @@ function StatSection({ title, defs, onChange, addLabel = '+ stat', nested }) { {title}
+ {hint &&

{hint}

} {entries.length === 0 &&
None yet.
} {entries.map(([k, d]) => ( { const x = withRenamedKey(npcs, o, n); if (x) onChange(x) } - const setNpc = (k, val) => onChange({ ...npcs, [k]: val }) - const remove = (k) => { const x = { ...npcs }; delete x[k]; onChange(x) } - const add = () => { - let i = 1, key = 'npc' - while (npcs[key]) key = `npc${i++}` - onChange({ ...npcs, [key]: { name: '', keys: '', desc: '', stats: {} } }) - } - return ( -
-
- NPCs - -
- {entries.length === 0 &&
No NPCs.
} - {entries.map(([k, npc]) => ( -
-
- rename(k, nk)} placeholder="npc_id" /> - npc.{k} - -
- setNpc(k, { ...npc, name: e.target.value })} /> - setNpc(k, { ...npc, keys: e.target.value })} /> - setNpc(k, { ...npc, desc: e.target.value })} /> - setNpc(k, { ...npc, stats: nd })} /> -
- ))} -
- ) -} - function FlagSection({ flags, onChange }) { const entries = Object.entries(flags || {}) const rename = (o, n) => { const x = withRenamedKey(flags, o, n); if (x) onChange(x) } @@ -199,12 +174,13 @@ function FlagSection({ flags, onChange }) { return (
- Flags (on/off) + Flags
+

On/off switches the AI can flip either way — a door unlocked, an alarm raised.

{entries.length === 0 &&
No flags.
} {entries.map(([k, f]) => ( -
+
rename(k, nk)} placeholder="flag_name" />
- setFlag(k, { ...f, desc: e.target.value })} /> + + setFlag(k, { ...f, desc: e.target.value })} /> +
))}
@@ -235,26 +213,104 @@ function MilestoneSection({ milestones, onChange }) { return (
- Milestones (sticky objectives) + Milestones
+

Objectives that stick once reached — they never un-tick on their own.

{entries.length === 0 &&
No milestones.
} {entries.map(([k, m]) => ( -
+
rename(k, nk)} placeholder="milestone_id" />
- setM(k, { ...m, desc: e.target.value })} /> + + setM(k, { ...m, desc: e.target.value })} /> +
))}
) } -// Form-based editor for a stat_schema. `schema` is the parsed object (or null); -// `onChange(nextSchema)` fires on every edit. Empty sections are dropped. +// ---- NPCs ---------------------------------------------------------------- +// NPCs live inside the same stat_schema (`schema.npcs`) but get their own +// top-level section in the editor: each one is a small character sheet, which +// doesn't fit the flat stat rows the other sections use. + +// Next `npcs` object with a fresh entry appended, and the id it used. +export function addNpc(npcs) { + let i = 1, key = 'npc' + while (npcs?.[key]) key = `npc${i++}` + return { ...(npcs || {}), [key]: { name: '', keys: '', desc: '', stats: {} } } +} + +function NpcCard({ npcId, npc, onRenameKey, onChange, onRemove }) { + const set = (field, val) => onChange({ ...npc, [field]: val }) + const statCount = Object.keys(npc.stats || {}).length + return ( +
+
+ +
+ set('name', e.target.value)} /> + npc.{npcId} +
+ {statCount} {statCount === 1 ? 'stat' : 'stats'} + +
+
+ + + + + set('keys', e.target.value)} /> + + +