diff --git a/backend/app/routers/adventures.py b/backend/app/routers/adventures.py index 6a58761..b10b434 100644 --- a/backend/app/routers/adventures.py +++ b/backend/app/routers/adventures.py @@ -141,6 +141,17 @@ def get_adventure( return get_adventure_or_404(adventure_id, db, user) +@router.get("/{adventure_id}/script-state") +def get_script_state( + adventure_id: int, db: Session = Depends(get_db), user: models.User = CurrentUser +): + """The scripting `state` object — every variable scripts read/write via + `state.x`, persisted after each hook. Empty {} until a script sets one.""" + adventure = get_adventure_or_404(adventure_id, db, user) + state = adventure.script_state if isinstance(adventure.script_state, dict) else {} + return {"state": state} + + @router.patch("/{adventure_id}", response_model=schemas.AdventureOut) def update_adventure( adventure_id: int, diff --git a/frontend/src/api.js b/frontend/src/api.js index 3205bdb..dcc2400 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -79,6 +79,7 @@ export const api = { // Adventures listAdventures: () => request('/adventures'), getAdventure: (id) => request(`/adventures/${id}`), + getScriptState: (id) => request(`/adventures/${id}/script-state`), createAdventure: (data) => request('/adventures', { method: 'POST', body: JSON.stringify(data) }), updateAdventure: (id, data) => request(`/adventures/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), deleteAdventure: (id) => request(`/adventures/${id}`, { method: 'DELETE' }), diff --git a/frontend/src/index.css b/frontend/src/index.css index baf8fff..39832f0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -439,6 +439,72 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n top: 56px; animation: panel-in 0.25s ease both; } + +/* ---------- Left status drawer (script state) ---------- */ + +.status-drawer { + flex-shrink: 0; + position: sticky; + top: 56px; + height: calc(100vh - 56px); + display: flex; + align-items: flex-start; + border-right: 1px solid var(--border); + background: var(--bg-panel); +} +.status-drawer.open { width: 300px; } +.status-toggle { + flex-shrink: 0; + height: 100%; + width: 30px; + padding: 12px 0; + border: none; + border-radius: 0; + background: transparent; + color: var(--text-dim); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} +.status-toggle:hover { color: var(--text); background: var(--bg-input); } +.status-toggle-label { + writing-mode: vertical-rl; + text-orientation: mixed; + letter-spacing: 0.12em; + font-size: 0.72rem; + text-transform: uppercase; +} +.status-body { + flex: 1; + min-width: 0; + height: 100%; + overflow-y: auto; + padding: 20px 16px; + animation: panel-in 0.2s ease both; +} +.status-vars { list-style: none; margin: 0; padding: 0; } +.status-vars li { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 0; + border-bottom: 1px solid var(--border); +} +.status-key { + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + color: var(--accent-bright); + word-break: break-word; +} +.status-val { + font-family: var(--font-mono, monospace); + font-size: 0.82rem; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; +} @keyframes panel-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } diff --git a/frontend/src/pages/Play.jsx b/frontend/src/pages/Play.jsx index b53c46d..87fe660 100644 --- a/frontend/src/pages/Play.jsx +++ b/frontend/src/pages/Play.jsx @@ -317,6 +317,60 @@ function ScriptsPanel({ advId }) { ) } +// Collapsible left rail showing the scripting `state` object — every variable +// scripts read/write via state.x, refreshed after each turn. +function StatusDrawer({ advId, refreshKey }) { + const [open, setOpen] = useState(false) + const [state, setState] = useState(null) + const [failed, setFailed] = useState(false) + + const load = useCallback(() => { + api.getScriptState(advId) + .then((r) => { setState(r.state || {}); setFailed(false) }) + .catch(() => setFailed(true)) + }, [advId]) + + // Only fetch while open; re-fetch after each turn so values stay live. + useEffect(() => { if (open) load() }, [open, refreshKey, load]) + + const entries = state ? Object.entries(state) : [] + + return ( +
state will appear here after a turn.
+