MRPCI/games/neon-precinct/rooms/room1.json
type-two ce42d69a3a v0.5 — playtest-driven fixes + engine/UX upgrades
Bugs found by playing the game over the bus:
- hotspots now have positions: 'walk to precinct door' pathfinds there,
  and touch verbs walk over to hotspots like they do to props/NPCs
- GameState::settle(): the MCP surface resolves walk-then-act in one call
  instead of returning zero events on a distant verb
- typed 'use X on Y' walks over like a clicked use (was unlimited range)
- taking/deleting a solid prop rebakes — no more lingering invisible walls
- sergeant dialogue no longer loops back to its greeting
- serde-facing maps are BTreeMap: room JSON and --sample are byte-stable

Engine:
- sprite-sheet actor views: <name>-sheet<C>x<R>.png slices into C frames x
  R direction loops (vend-bot now rolls on animated treads)
- pixel-accurate click targeting with 1px slop; typed commands click
  mid-sprite (a feet click lands between an actor's legs)
- parser: compass walking (go east / n), 'bye' ends conversations,
  'look' lists exits

GUI:
- hover hot-text: cursor reads the sentence a click would say
- death banner + '+N' score toast

Tests: full-playthrough regression suite (win path, death rewind,
walk-over dialogue, solid-prop footprint) + sheet slicing unit test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:38:33 +10:00

846 lines
15 KiB
JSON

{
"name": "41st Precinct Lobby",
"enter_text": "Fluorescents flicker over the duty desk. The evidence locker waits along the east wall.",
"background": "",
"ops": [
{
"VGradient": {
"x": 0,
"y": 0,
"w": 320,
"h": 96,
"ramp": [
119,
76
],
"ink": {
"color": 0,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"Rect": {
"x": 0,
"y": 96,
"w": 320,
"h": 4,
"ink": {
"color": 153,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"VGradient": {
"x": 0,
"y": 100,
"w": 320,
"h": 90,
"ramp": [
23,
21
],
"ink": {
"color": 0,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 0,
"y": 100,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 64,
"y": 100,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 128,
"y": 100,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 192,
"y": 100,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 256,
"y": 100,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 32,
"y": 115,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 96,
"y": 115,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 160,
"y": 115,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 224,
"y": 115,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 288,
"y": 115,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 0,
"y": 130,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 64,
"y": 130,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 128,
"y": 130,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 192,
"y": 130,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 256,
"y": 130,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 32,
"y": 145,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 96,
"y": 145,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 160,
"y": 145,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 224,
"y": 145,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 288,
"y": 145,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 0,
"y": 160,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 64,
"y": 160,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 128,
"y": 160,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 192,
"y": 160,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 256,
"y": 160,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 32,
"y": 175,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 96,
"y": 175,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 160,
"y": 175,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 224,
"y": 175,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 288,
"y": 175,
"w": 32,
"h": 15,
"ink": {
"color": 118,
"pri": "Band",
"ctl": {
"Set": 0
},
"hot": null
}
}
},
{
"Rect": {
"x": 118,
"y": 96,
"w": 90,
"h": 26,
"ink": {
"color": 110,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"Rect": {
"x": 118,
"y": 96,
"w": 90,
"h": 4,
"ink": {
"color": 153,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"Rect": {
"x": 30,
"y": 30,
"w": 20,
"h": 26,
"ink": {
"color": 196,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"Rect": {
"x": 56,
"y": 30,
"w": 20,
"h": 26,
"ink": {
"color": 167,
"pri": "Keep",
"ctl": {
"Set": 1
},
"hot": null
}
}
},
{
"Rect": {
"x": 140,
"y": 182,
"w": 40,
"h": 8,
"ink": {
"color": 147,
"pri": "Keep",
"ctl": "Keep",
"hot": null
}
}
}
],
"palette": null,
"cycles": [],
"scale": {
"horizon_y": 88,
"min_scale": 0.7,
"full_y": 185
},
"spawn": [
160,
176
],
"exits": [
null,
null,
null,
null
],
"exit_flags": [
"",
"",
"",
""
],
"exit_blocked": [
"",
"",
"",
""
],
"hotspots": [
{
"name": "street door",
"rect": [
136,
184,
48,
6
],
"poly": [],
"msgs": {
"look": "Back out to Neon Row."
},
"exit_to": 0,
"arrive": [
226,
120
],
"requires_flag": "",
"blocked_text": "",
"trigger": false,
"blocks": false
},
{
"name": "duty desk",
"rect": [
118,
96,
90,
30
],
"poly": [],
"msgs": {
"do": "You straighten a stack of incident forms. Order is restored, briefly.",
"look": "The duty desk. Coffee rings dating back three administrations."
},
"exit_to": null,
"arrive": null,
"requires_flag": "",
"blocked_text": "",
"trigger": false,
"blocks": false
},
{
"name": "wanted posters",
"rect": [
28,
28,
52,
30
],
"poly": [],
"msgs": {
"look": "WANTED: the Compost Bandit. Last seen fleeing Sickbay with a datacard."
},
"exit_to": null,
"arrive": null,
"requires_flag": "",
"blocked_text": "",
"trigger": false,
"blocks": false
}
],
"props": [
{
"name": "locker",
"sprite": "locker",
"x": 286,
"y": 138,
"msgs": {
"do": "Locked tight. The reader-slot blinks at you, unimpressed.",
"look": "Evidence locker 47. The reader-slot blinks, wanting a keycard."
},
"takeable": false,
"synonyms": "",
"use_text": "The keycard chirps. Locker 47 clunks open: one data-slate, sealed in an evidence bag.",
"needs": "keycard",
"wins": false,
"requires_flag": "",
"sets_flag": "locker_open",
"dialogue": [],
"portrait": "",
"points": 10,
"consumes": false,
"kills": false,
"visible_flag": "",
"hidden_by_flag": "",
"solid": true,
"fixed_priority": null,
"fixed_scale": null
},
{
"name": "terminal",
"sprite": "terminal",
"x": 36,
"y": 130,
"msgs": {
"do": "It wants the evidence, not your fingerprints.",
"look": "The case terminal. Slot: DATA-SLATE. It has been hungry for weeks."
},
"takeable": false,
"synonyms": "",
"use_text": "The slate slides home. Records unspool. Warrants bloom. Somewhere, the Compost Bandit sneezes.",
"needs": "evidence",
"wins": true,
"requires_flag": "",
"sets_flag": "",
"dialogue": [],
"portrait": "",
"points": 10,
"consumes": true,
"kills": false,
"visible_flag": "",
"hidden_by_flag": "",
"solid": true,
"fixed_priority": null,
"fixed_scale": null
}
],
"npcs": [
{
"name": "sergeant",
"sprite": "sergeant",
"x": 162,
"y": 132,
"msgs": {
"look": "Sergeant Brasso. Chrome polished, patience not."
},
"dialogue": [
{
"says": "Morp-9. The data-slate case won't close itself.",
"choices": [
{
"text": "Where do I start?",
"goto": 1,
"requires_flag": "",
"sets_flag": "",
"points": 0,
"kills": false
},
{
"text": "What do I do with evidence?",
"goto": 2,
"requires_flag": "locker_open",
"sets_flag": "",
"points": 0,
"kills": false
},
{
"text": "On it, Sarge.",
"goto": -1,
"requires_flag": "",
"sets_flag": "",
"points": 0,
"kills": false
}
]
},
{
"says": "Perp dumped a keycard in Rain Alley, east off Neon Row. It opens evidence locker 47.",
"choices": [
{
"text": "Got it.",
"goto": -1,
"requires_flag": "",
"sets_flag": "",
"points": 0,
"kills": false
}
]
},
{
"says": "Slot the slate into the case terminal, west wall. Then we both go home.",
"choices": [
{
"text": "Copy.",
"goto": -1,
"requires_flag": "",
"sets_flag": "",
"points": 0,
"kills": false
}
]
}
],
"portrait": "sergeant-face",
"wander": 0,
"visible_flag": "",
"hidden_by_flag": "",
"fixed_scale": null
}
],
"music": 1,
"weather": 0,
"defaults": {},
"script": "room1.rhai"
}