Give NPCs their own editor section and unify the world-state boxes

NPCs are still part of stat_schema, but editing them inside the World State
panel meant a nested dashed sub-box among four flat stat lists. They now get
a top-level "Cast & NPCs" section: one card per NPC (avatar, name, npc.<id>
address, triggers, description, its own stats) in a responsive grid, saved
through the same schema path via the new NpcEditor/addNpc exports.

The panel's real inconsistency was CSS, not layout: the global field rule
keys off input[type="..."], which never matches the editor's typeless key and
description inputs, so those rendered with browser defaults beside properly
styled number boxes. One scoped base rule now covers the whole editor and
every entry is the same tile with a caption over each control.

Two bugs fell out of that: .se-band-n (0,1,0) always lost to
input[type="number"] (0,1,1), so band bounds rendered full-width; and the
exclusion has to be :not(:where([type="checkbox"])) because a plain :not()
inherits its argument's specificity and swallows every override.

In Play, NPCs group under a single "Cast" heading as compact plates instead
of each becoming another top-level group in the rail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UeQVy5bEjLhfgWNc27Efet
This commit is contained in:
parththakkar106
2026-08-02 19:46:32 +05:30
co-authored by Claude Opus 5
parent d66fd1d6a2
commit be6dbb0f24
5 changed files with 361 additions and 109 deletions
+35 -5
View File
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'
import { api } from '../api'
import { Field, StoryCardRow, downloadJSON, pickJSONFile, useToast } from '../components'
import ArtPicker from '../ArtPicker'
import SchemaEditor from '../SchemaEditor'
import SchemaEditor, { NpcEditor, addNpc } from '../SchemaEditor'
export default function ScenarioEditor() {
const { id } = useParams()
@@ -106,6 +106,15 @@ export default function ScenarioEditor() {
debounceSave('stat_schema', () => saveSchema(cleaned))
}
// NPCs live in the same stat_schema, but are edited in their own page
// section — so this is just the `npcs` slice of the same save path.
const setNpcs = (npcs) => {
const next = { ...(parsedSchema || {}) }
if (npcs && Object.keys(npcs).length) next.npcs = npcs
else delete next.npcs
applySchema(next)
}
const addCard = async () => {
const card = await api.createStoryCard({ scenario_id: Number(id) })
setScenario({ ...scenario, story_cards: [...scenario.story_cards, card] })
@@ -243,10 +252,10 @@ export default function ScenarioEditor() {
</div>
</div>
<p className="dim" style={{ margin: '0 0 10px', fontSize: '0.85rem' }}>
Optional. Define stats (with bands and rules), NPCs, flags, and milestones, and
the AI will track them each turn — HP, mana, an NPC’s trust, quest objectives.
Each NPC has its own name, trigger keys, description, and its own stats; a story
card is created for it automatically. Leave blank for a plain narrative scenario.
Optional. Define stats (with bands and rules), flags, and milestones, and the AI
will track them each turn — HP, mana, a raised alarm, quest objectives. NPCs are
part of this same schema but have their own section below. Leave blank for a plain
narrative scenario.
</p>
{schemaView === 'form' ? (
<SchemaEditor schema={parsedSchema} onChange={applySchema} />
@@ -272,6 +281,27 @@ export default function ScenarioEditor() {
</>
)}
<div className="page-header" style={{ marginTop: 28 }}>
<h2 style={{ margin: 0, fontFamily: 'Georgia, serif', fontSize: '1.2rem' }}>Cast &amp; NPCs</h2>
{schemaView === 'form' && (
<button onClick={() => setNpcs(addNpc(parsedSchema?.npcs))}>+ Add NPC</button>
)}
</div>
<p className="dim" style={{ margin: '0 0 10px', fontSize: '0.85rem' }}>
Characters the AI tracks by name. Each has its own trigger words, description and
stats — a dragon’s <em>ferocity</em>, a ranger’s <em>trust</em> — addressed as
<code> npc.&lt;id&gt;.&lt;stat&gt;</code>. Adding one here also creates a story card
for it when an adventure starts.
</p>
{schemaView === 'form' ? (
<NpcEditor npcs={parsedSchema?.npcs || {}} onChange={setNpcs} />
) : (
<div className="empty" style={{ padding: '20px 0' }}>
NPCs are part of the schema JSON above — switch to <strong>Editor</strong> to edit
them as cards.
</div>
)}
<div className="page-header" style={{ marginTop: 28 }}>
<h2 style={{ margin: 0, fontFamily: 'Georgia, serif', fontSize: '1.2rem' }}>Attached Scripts</h2>
</div>