)
}
function FlagSection({ flags, onChange }) {
const entries = Object.entries(flags || {})
const rename = (o, n) => { const x = withRenamedKey(flags, o, n); if (x) onChange(x) }
const setFlag = (k, val) => onChange({ ...flags, [k]: val })
const remove = (k) => { const x = { ...flags }; delete x[k]; onChange(x) }
const add = () => {
let i = 1, key = 'flag'
while (flags[key]) key = `flag${i++}`
onChange({ ...flags, [key]: { initial: false, desc: '' } })
}
return (
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 })} />
))}
)
}
function MilestoneSection({ milestones, onChange }) {
const entries = Object.entries(milestones || {})
const rename = (o, n) => { const x = withRenamedKey(milestones, o, n); if (x) onChange(x) }
const setM = (k, val) => onChange({ ...milestones, [k]: val })
const remove = (k) => { const x = { ...milestones }; delete x[k]; onChange(x) }
const add = () => {
let i = 1, key = 'goal'
while (milestones[key]) key = `goal${i++}`
onChange({ ...milestones, [key]: { desc: '' } })
}
return (
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 })} />
))}
)
}
// ---- 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 (
)
}
// The NPC roster. Takes/returns just the `npcs` slice of a stat_schema.
export function NpcEditor({ npcs, onChange }) {
const entries = Object.entries(npcs || {})
const rename = (o, n) => { 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) }
if (entries.length === 0) {
return (
No NPCs yet. Each one gets its own stats (trust, health, ferocity — whatever suits them),
and a story card is created automatically so they show up in context when mentioned.
)
}
// Form-based editor for a stat_schema, minus the NPCs (see `NpcEditor`, which
// the scenario editor renders as its own page section). `schema` is the parsed
// object (or null); `onChange(nextSchema)` fires on every edit. Empty sections
// are dropped.
export default function SchemaEditor({ schema, onChange }) {
const s = schema && typeof schema === 'object' ? schema : {}
const setSection = (key, val) => {
const next = { ...s }
if (val && Object.keys(val).length) next[key] = val
else delete next[key]
onChange(next)
}
return (
setSection('world', v)}
hint="Things about the situation, not any one character — time of day, the camp’s alert level." />
setSection('player', v)}
hint="The player’s own numbers — hp, gold, reputation — plus free-text ones like an outfit." />
setSection('flags', v)} />
setSection('milestones', v)} />