// The right drawer: the RPG world state, and the form that edits it.
//
// Editing builds a flat draft keyed by path, so a nested value can be edited
// without rebuilding the tree on every keystroke. `buildWorldStateDraft` makes
// it and `sliceDraft` reads one section back out.
import { useCallback, useEffect, useState } from 'react'
import { api } from '../../../api'
import { npcInitials } from '../../../components'
// Returns the word label for a value, read from the stat def's bands. This
// mirrors `worldstate.band_label` on the server.
function bandLabel(def, value) {
const bands = def?.bands
if (!Array.isArray(bands) || typeof value !== 'number') return null
for (const b of bands) {
if (Array.isArray(b) && b.length === 3 && value >= b[0] && value < b[1]) return b[2]
}
const last = bands[bands.length - 1]
if (last && value === last[1]) return last[2]
return null
}
function StatRow({ name, def, value, editing, onChange }) {
const isText = def?.type === 'text'
if (editing) {
return (
)
}
// `values`/`draft` are both plain {statName: value} maps — `draft` (edit mode)
// is a slice of the drawer's flat path->value map for this group's prefix.
// `nested` = the caller already drew a heading (an NPC card), so this drops the
// group's own spacing and says "nothing here" rather than vanishing and leaving
// that heading dangling over empty space.
function StatGroup({ title, defs, values, desc, editing, draft, onEdit, nested }) {
const entries = Object.entries(defs || {}).filter(([, d]) => d && typeof d === 'object')
if (entries.length === 0) {
return nested ?