File size: 28,964 Bytes
c782fbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
from __future__ import annotations

import json
import random
from dataclasses import dataclass
from pathlib import Path
from typing import Any


@dataclass(frozen=True)
class WorldTheme:
    title: str
    answer: str
    foyer_label: str
    foyer_description: str
    shrine_label: str
    shrine_description: str
    workshop_label: str
    workshop_description: str
    courtyard_label: str
    courtyard_description: str
    gallery_label: str
    gallery_description: str
    entry_chest_label: str
    entry_chest_description: str
    iron_door_label: str
    iron_door_description: str
    ash_mural_label: str
    ash_mural_description: str
    ash_mural_text: str
    iron_chest_label: str
    iron_chest_description: str
    stone_well_label: str
    stone_well_description: str
    water_plaque_label: str
    water_plaque_description: str
    water_plaque_text: str
    cartographer_label: str
    cartographer_description: str
    faded_letter_label: str
    faded_letter_description: str
    faded_letter_text: str
    stone_guardian_label: str
    stone_guardian_description: str
    brass_key_label: str
    brass_key_description: str
    torch_label: str
    torch_description: str
    torn_map_left_label: str
    torn_map_left_description: str
    torn_map_right_label: str
    torn_map_right_description: str
    full_map_label: str
    full_map_description: str
    lens_label: str
    lens_description: str
    initial_clue_text: str
    river_clue_text: str
    waterwarden_clue_text: str


_WORLD_THEMES: tuple[WorldTheme, ...] = (
    WorldTheme(
        title="The River Ward",
        answer="mira",
        foyer_label="Foyer",
        foyer_description="A drafty entry hall with passages north, south, east, and west.",
        shrine_label="Shrine",
        shrine_description="An open shrine watched by a silent stone guardian.",
        workshop_label="Workshop",
        workshop_description="An ash-streaked workshop lit by a guttering lamp.",
        courtyard_label="Courtyard",
        courtyard_description="Rainwater gathers around a cracked stone well.",
        gallery_label="Gallery",
        gallery_description="Portraits of the wardens hang above a long dust-covered table.",
        entry_chest_label="Entry Chest",
        entry_chest_description="A squat travel chest sits beside the door.",
        iron_door_label="Iron Door",
        iron_door_description="A blackened iron door seals the workshop.",
        ash_mural_label="Ash Mural",
        ash_mural_description="An ash-dark mural is impossible to make out with the naked eye.",
        ash_mural_text="The mural preserves one line: the betrayer's name begins with M.",
        iron_chest_label="Iron Chest",
        iron_chest_description="A soot-stained iron chest is tucked under a bench.",
        stone_well_label="Stone Well",
        stone_well_description="Etchings circle the well's rim, but they only align from the proper vantage.",
        water_plaque_label="Water Plaque",
        water_plaque_description="A bronze plaque slides out from the well masonry.",
        water_plaque_text="The betrayer lived closest to the river gate.",
        cartographer_label="Cartographer",
        cartographer_description="The cartographer studies the walls and waits for a completed survey.",
        faded_letter_label="Faded Letter",
        faded_letter_description="A faded letter is still too blurred to decipher.",
        faded_letter_text="Of the wardens, only Mira kept quarters beside the water.",
        stone_guardian_label="Stone Guardian",
        stone_guardian_description="The guardian asks for the betrayer's name once you are ready.",
        brass_key_label="Brass Key",
        brass_key_description="A brass key with soot in its teeth.",
        torch_label="Torch",
        torch_description="A pitch torch with a steady flame.",
        torn_map_left_label="Torn Map Left",
        torn_map_left_description="The left half of a survey map.",
        torn_map_right_label="Torn Map Right",
        torn_map_right_description="The right half of a survey map.",
        full_map_label="Full Map",
        full_map_description="A restored map of the ward.",
        lens_label="Lens",
        lens_description="A polished lens in a brass frame.",
        initial_clue_text="The betrayer's name begins with M.",
        river_clue_text="The betrayer lived closest to the river gate.",
        waterwarden_clue_text="Of the wardens, only Mira kept quarters beside the water.",
    ),
    WorldTheme(
        title="The Ember Vault",
        answer="vesna",
        foyer_label="Receiving Hall",
        foyer_description="A warm stone hall lined with soot and copper hooks.",
        shrine_label="Crucible Shrine",
        shrine_description="A brass sentinel stands before a furnace-bright altar.",
        workshop_label="Forge Annex",
        workshop_description="Bellows creak above benches powdered with black ash.",
        courtyard_label="Quench Yard",
        courtyard_description="A cracked basin gathers rain beside the old quench line.",
        gallery_label="Ledger Hall",
        gallery_description="Burned account books rest beneath portraits of furnace wardens.",
        entry_chest_label="Courier Trunk",
        entry_chest_description="A courier trunk waits under a soot-marked peg rail.",
        iron_door_label="Furnace Door",
        iron_door_description="A scorched iron door blocks the annex.",
        ash_mural_label="Cinder Frieze",
        ash_mural_description="A smoke-dark frieze only sharpens under moving flame.",
        ash_mural_text="A surviving line says the betrayer's name begins with V.",
        iron_chest_label="Coal Locker",
        iron_chest_description="A riveted locker is wedged beneath a slagged bench.",
        stone_well_label="Quench Basin",
        stone_well_description="Marks on the basin align only when seen with the full survey.",
        water_plaque_label="Cooling Plaque",
        water_plaque_description="A brass plate rises from a seam in the basin stone.",
        water_plaque_text="The betrayer worked closest to the quench trench.",
        cartographer_label="Quartermaster",
        cartographer_description="The quartermaster trades only for a complete furnace survey.",
        faded_letter_label="Scorched Ledger",
        faded_letter_description="Heat has blurred the ink into copper-colored streaks.",
        faded_letter_text="Only Vesna kept the cooling ledgers beside the trench.",
        stone_guardian_label="Brass Sentinel",
        stone_guardian_description="The sentinel requests the betrayer's name when the case is ready.",
        brass_key_label="Copper Key",
        brass_key_description="A copper key with furnace grit packed in the cuts.",
        torch_label="Coal Torch",
        torch_description="A coal torch that burns with a steady orange core.",
        torn_map_left_label="Smelter Map Left",
        torn_map_left_description="The left half of a furnace survey.",
        torn_map_right_label="Smelter Map Right",
        torn_map_right_description="The right half of a furnace survey.",
        full_map_label="Furnace Survey",
        full_map_description="A restored survey of the ember vault.",
        lens_label="Gauge Lens",
        lens_description="A thick gauge lens set in a brass ring.",
        initial_clue_text="The betrayer's name begins with V.",
        river_clue_text="The betrayer worked closest to the quench trench.",
        waterwarden_clue_text="Only Vesna kept the cooling ledgers beside the trench.",
    ),
    WorldTheme(
        title="The Astral Archive",
        answer="selene",
        foyer_label="Entry Rotunda",
        foyer_description="A quiet rotunda opens toward stacked corridors and a dim observatory stair.",
        shrine_label="Moon Chapel",
        shrine_description="A silver warden stands beneath a ceiling of cold stars.",
        workshop_label="Chart Room",
        workshop_description="Tables of brass instruments glint in powdery moon dust.",
        courtyard_label="Star Court",
        courtyard_description="A dry fountain mirrors the constellations in chipped stone.",
        gallery_label="Catalog Hall",
        gallery_description="Glass cases hold the names of long-dead archivists.",
        entry_chest_label="Porter's Case",
        entry_chest_description="A leather case rests under the chart hooks.",
        iron_door_label="Star Door",
        iron_door_description="A ribbed iron door seals the chart room.",
        ash_mural_label="Night Chart",
        ash_mural_description="The chart is unreadable until lit from the proper angle.",
        ash_mural_text="One surviving note says the betrayer's name begins with S.",
        iron_chest_label="Index Chest",
        iron_chest_description="A narrow chest sits below a shelf of cracked lenses.",
        stone_well_label="Dry Fountain",
        stone_well_description="Its star marks align only when the full survey is restored.",
        water_plaque_label="Star Plaque",
        water_plaque_description="A silver plaque slides free from the fountain rim.",
        water_plaque_text="The betrayer slept nearest the eastern telescope.",
        cartographer_label="Archivist",
        cartographer_description="The archivist will trade for a complete celestial survey.",
        faded_letter_label="Blurred Index",
        faded_letter_description="The index script is too faint without magnification.",
        faded_letter_text="Among the archivists, only Selene kept quarters by the east telescope.",
        stone_guardian_label="Silver Warden",
        stone_guardian_description="The warden will hear the accusation once you have evidence.",
        brass_key_label="Star Key",
        brass_key_description="A slim key engraved with a crescent notch.",
        torch_label="Lamp Wand",
        torch_description="A narrow lamp wand with a clean blue flame.",
        torn_map_left_label="Celestial Map Left",
        torn_map_left_description="The left half of a star survey.",
        torn_map_right_label="Celestial Map Right",
        torn_map_right_description="The right half of a star survey.",
        full_map_label="Celestial Survey",
        full_map_description="A restored survey of the astral archive.",
        lens_label="Astrolabe Lens",
        lens_description="A polished lens mounted in silver wire.",
        initial_clue_text="The betrayer's name begins with S.",
        river_clue_text="The betrayer slept nearest the eastern telescope.",
        waterwarden_clue_text="Among the archivists, only Selene kept quarters by the east telescope.",
    ),
    WorldTheme(
        title="The Glass Conservatory",
        answer="liora",
        foyer_label="Gate House",
        foyer_description="A humid gate house opens onto vine-choked passages.",
        shrine_label="Bloom Shrine",
        shrine_description="A mossy guardian waits among chipped planters.",
        workshop_label="Potting Room",
        workshop_description="Clay dust and root knives cover the worktables.",
        courtyard_label="Glass Court",
        courtyard_description="A cracked basin sits beneath panes webbed with ivy.",
        gallery_label="Seed Gallery",
        gallery_description="Pressed flowers hang beside records of vanished caretakers.",
        entry_chest_label="Garden Chest",
        entry_chest_description="A cedar chest is tucked beside the rain cloaks.",
        iron_door_label="Greenhouse Door",
        iron_door_description="A warped iron door blocks the potting room.",
        ash_mural_label="Vine Panel",
        ash_mural_description="The panel's scratches only read clearly under a steady flame.",
        ash_mural_text="A scratched line says the betrayer's name begins with L.",
        iron_chest_label="Tool Locker",
        iron_chest_description="A damp locker crouches under a potting bench.",
        stone_well_label="Ivy Basin",
        stone_well_description="The etched rings align only when the full garden survey is in hand.",
        water_plaque_label="Root Plaque",
        water_plaque_description="A greened plaque slides from the basin wall.",
        water_plaque_text="The betrayer tended the beds nearest the rain cistern.",
        cartographer_label="Head Gardener",
        cartographer_description="The gardener will barter only for a complete bed map.",
        faded_letter_label="Watered Note",
        faded_letter_description="The note is blurred by old rain and fertilizer.",
        faded_letter_text="Only Liora kept the cistern ledgers beside the rain beds.",
        stone_guardian_label="Moss Guardian",
        stone_guardian_description="The guardian listens when you are ready to name the betrayer.",
        brass_key_label="Trellis Key",
        brass_key_description="A greened key shaped like a curling vine.",
        torch_label="Glass Lantern",
        torch_description="A glass-sided lantern with a bright white flame.",
        torn_map_left_label="Bed Map Left",
        torn_map_left_description="The left half of a conservatory plan.",
        torn_map_right_label="Bed Map Right",
        torn_map_right_description="The right half of a conservatory plan.",
        full_map_label="Bed Survey",
        full_map_description="A restored survey of the conservatory beds.",
        lens_label="Prism Lens",
        lens_description="A prism lens wrapped in tarnished copper.",
        initial_clue_text="The betrayer's name begins with L.",
        river_clue_text="The betrayer tended the beds nearest the rain cistern.",
        waterwarden_clue_text="Only Liora kept the cistern ledgers beside the rain beds.",
    ),
    WorldTheme(
        title="The Salt Bastion",
        answer="corin",
        foyer_label="Watch Hall",
        foyer_description="A salt-stung hall opens toward barracks, chapel, and the sea court.",
        shrine_label="Tide Chapel",
        shrine_description="A stone warden keeps watch over a shrine of ropes and shells.",
        workshop_label="Signal Room",
        workshop_description="Lantern hooks sway above benches dusted with salt ash.",
        courtyard_label="Sea Court",
        courtyard_description="A dry cistern sits beneath walls pitted by ocean wind.",
        gallery_label="Roll Hall",
        gallery_description="Roster boards hang beneath portraits of old coast captains.",
        entry_chest_label="Harbor Chest",
        entry_chest_description="A travel chest sits beside a rack of oilskins.",
        iron_door_label="Beacon Door",
        iron_door_description="A rusted iron door bars the signal room.",
        ash_mural_label="Signal Board",
        ash_mural_description="Salt haze hides the markings until a lamp is raised close.",
        ash_mural_text="A surviving mark says the betrayer's name begins with C.",
        iron_chest_label="Tar Locker",
        iron_chest_description="A tar-black locker hides below a signal bench.",
        stone_well_label="Dry Cistern",
        stone_well_description="Its carved rings make sense only with the restored coast survey.",
        water_plaque_label="Harbor Plaque",
        water_plaque_description="A plaque rises from a crack in the cistern lip.",
        water_plaque_text="The betrayer bunked nearest the harbor chain.",
        cartographer_label="Harbor Clerk",
        cartographer_description="The clerk trades only for a complete bastion survey.",
        faded_letter_label="Salted Roll",
        faded_letter_description="Salt has crusted over the roster names.",
        faded_letter_text="Only Corin kept the harbor ledgers beside the chain gate.",
        stone_guardian_label="Stone Warden",
        stone_guardian_description="The warden asks for the betrayer's name when the proof is ready.",
        brass_key_label="Anchor Key",
        brass_key_description="A heavy key stamped with a worn anchor.",
        torch_label="Signal Lamp",
        torch_description="A shuttered lamp with a disciplined yellow flame.",
        torn_map_left_label="Coast Map Left",
        torn_map_left_description="The left half of a bastion survey.",
        torn_map_right_label="Coast Map Right",
        torn_map_right_description="The right half of a bastion survey.",
        full_map_label="Coast Survey",
        full_map_description="A restored survey of the salt bastion.",
        lens_label="Captain's Lens",
        lens_description="A salt-clear lens held in a bronze ring.",
        initial_clue_text="The betrayer's name begins with C.",
        river_clue_text="The betrayer bunked nearest the harbor chain.",
        waterwarden_clue_text="Only Corin kept the harbor ledgers beside the chain gate.",
    ),
)


def sample_world_definition(seed: int | None = None, difficulty_target: float = 1.5) -> dict[str, Any]:
    theme = _select_theme(seed)
    return _build_world(theme, difficulty_target=difficulty_target)


def load_world(path: str) -> dict[str, Any]:
    return json.loads(Path(path).read_text(encoding="utf-8"))


def _select_theme(seed: int | None) -> WorldTheme:
    if seed is None:
        return _WORLD_THEMES[0]
    rng = random.Random(seed)
    return _WORLD_THEMES[rng.randrange(len(_WORLD_THEMES))]


def _build_world(theme: WorldTheme, *, difficulty_target: float) -> dict[str, Any]:
    return {
        "meta": {
            "title": theme.title,
            "difficulty_target": difficulty_target,
            "start_node_id": "foyer",
            "win_condition": {
                "type": "deduce",
                "target_npc_id": "stone_guardian",
                "answer_string": theme.answer,
            },
        },
        "nodes": [
            {"id": "foyer", "type": "location", "label": theme.foyer_label, "description": theme.foyer_description},
            {"id": "shrine", "type": "location", "label": theme.shrine_label, "description": theme.shrine_description},
            {"id": "workshop", "type": "location", "label": theme.workshop_label, "description": theme.workshop_description},
            {"id": "courtyard", "type": "location", "label": theme.courtyard_label, "description": theme.courtyard_description},
            {"id": "gallery", "type": "location", "label": theme.gallery_label, "description": theme.gallery_description},
            {
                "id": "entry_chest",
                "type": "container",
                "label": theme.entry_chest_label,
                "description": theme.entry_chest_description,
                "parent_id": "foyer",
                "open": False,
                "locked": False,
                "lock_key_id": None,
            },
            {
                "id": "iron_door",
                "type": "door",
                "label": theme.iron_door_label,
                "description": theme.iron_door_description,
                "open": False,
                "locked": True,
                "lock_key_id": "brass_key",
            },
            {
                "id": "ash_mural",
                "type": "readable",
                "label": theme.ash_mural_label,
                "description": theme.ash_mural_description,
                "parent_id": "workshop",
                "clue_id": "initial_clue",
                "requires_item_id": "torch",
                "consumes_item": False,
                "text_content": theme.ash_mural_text,
            },
            {
                "id": "iron_chest",
                "type": "container",
                "label": theme.iron_chest_label,
                "description": theme.iron_chest_description,
                "parent_id": "workshop",
                "open": False,
                "locked": False,
                "lock_key_id": None,
            },
            {
                "id": "stone_well",
                "type": "fixture",
                "label": theme.stone_well_label,
                "description": theme.stone_well_description,
                "parent_id": "courtyard",
                "requires_item_id": "full_map",
                "reveals_item_id": None,
                "reveals_readable_id": "water_plaque",
                "consumes_item": False,
            },
            {
                "id": "water_plaque",
                "type": "readable",
                "label": theme.water_plaque_label,
                "description": theme.water_plaque_description,
                "parent_id": "courtyard",
                "clue_id": "river_clue",
                "requires_item_id": None,
                "consumes_item": False,
                "text_content": theme.water_plaque_text,
            },
            {
                "id": "cartographer",
                "type": "npc",
                "label": theme.cartographer_label,
                "description": theme.cartographer_description,
                "parent_id": "gallery",
                "requires_item_id": "full_map",
                "gives_item_id": "lens",
                "gives_clue_id": None,
            },
            {
                "id": "faded_letter",
                "type": "readable",
                "label": theme.faded_letter_label,
                "description": theme.faded_letter_description,
                "parent_id": "gallery",
                "clue_id": "waterwarden_clue",
                "requires_item_id": "lens",
                "consumes_item": False,
                "text_content": theme.faded_letter_text,
            },
            {
                "id": "stone_guardian",
                "type": "npc",
                "label": theme.stone_guardian_label,
                "description": theme.stone_guardian_description,
                "parent_id": "shrine",
                "requires_item_id": None,
                "gives_item_id": None,
                "gives_clue_id": None,
            },
        ],
        "edges": [
            {"id": "foyer_north", "from_node_id": "foyer", "to_node_id": "shrine", "direction": "north", "type": "passage", "required_item_id": None, "door_node_id": None},
            {"id": "shrine_south", "from_node_id": "shrine", "to_node_id": "foyer", "direction": "south", "type": "passage", "required_item_id": None, "door_node_id": None},
            {"id": "foyer_east", "from_node_id": "foyer", "to_node_id": "workshop", "direction": "east", "type": "locked_passage", "required_item_id": "brass_key", "door_node_id": "iron_door"},
            {"id": "workshop_west", "from_node_id": "workshop", "to_node_id": "foyer", "direction": "west", "type": "locked_passage", "required_item_id": "brass_key", "door_node_id": "iron_door"},
            {"id": "foyer_west", "from_node_id": "foyer", "to_node_id": "courtyard", "direction": "west", "type": "passage", "required_item_id": None, "door_node_id": None},
            {"id": "courtyard_east", "from_node_id": "courtyard", "to_node_id": "foyer", "direction": "east", "type": "passage", "required_item_id": None, "door_node_id": None},
            {"id": "foyer_south", "from_node_id": "foyer", "to_node_id": "gallery", "direction": "south", "type": "passage", "required_item_id": None, "door_node_id": None},
            {"id": "gallery_north", "from_node_id": "gallery", "to_node_id": "foyer", "direction": "north", "type": "passage", "required_item_id": None, "door_node_id": None},
        ],
        "items": [
            {"id": "brass_key", "label": theme.brass_key_label, "description": theme.brass_key_description, "subtype": "key", "start_node_id": "entry_chest"},
            {"id": "torch", "label": theme.torch_label, "description": theme.torch_description, "subtype": "puzzle", "start_node_id": "workshop"},
            {"id": "torn_map_left", "label": theme.torn_map_left_label, "description": theme.torn_map_left_description, "subtype": "puzzle", "start_node_id": "iron_chest"},
            {"id": "torn_map_right", "label": theme.torn_map_right_label, "description": theme.torn_map_right_description, "subtype": "puzzle", "start_node_id": "courtyard"},
            {"id": "full_map", "label": theme.full_map_label, "description": theme.full_map_description, "subtype": "puzzle", "start_node_id": None},
            {"id": "lens", "label": theme.lens_label, "description": theme.lens_description, "subtype": "puzzle", "start_node_id": None},
        ],
        "clues": [
            {"id": "initial_clue", "text": theme.initial_clue_text},
            {"id": "river_clue", "text": theme.river_clue_text},
            {"id": "waterwarden_clue", "text": theme.waterwarden_clue_text},
        ],
        "recipes": [
            {
                "id": "restore_map",
                "input_item_ids": ["torn_map_left", "torn_map_right"],
                "output_item_id": "full_map",
            }
        ],
        "quest_chain": [
            {"step_id": "open_entry_chest", "description": f"Open the {theme.entry_chest_label.lower()}.", "requires_step_ids": [], "action": "open(entry_chest)"},
            {"step_id": "take_brass_key", "description": f"Take the {theme.brass_key_label.lower()}.", "requires_step_ids": ["open_entry_chest"], "action": "take(brass_key,entry_chest)"},
            {"step_id": "unlock_workshop", "description": f"Unlock the {theme.iron_door_label.lower()}.", "requires_step_ids": ["take_brass_key"], "action": "unlock(iron_door,brass_key)"},
            {"step_id": "open_workshop", "description": f"Open the {theme.iron_door_label.lower()}.", "requires_step_ids": ["unlock_workshop"], "action": "open(iron_door)"},
            {"step_id": "go_workshop", "description": f"Enter the {theme.workshop_label.lower()}.", "requires_step_ids": ["open_workshop"], "action": "go(workshop)"},
            {"step_id": "take_torch", "description": f"Take the {theme.torch_label.lower()}.", "requires_step_ids": ["go_workshop"], "action": "take(torch,workshop)"},
            {"step_id": "use_torch_on_mural", "description": f"Use the {theme.torch_label.lower()} on the {theme.ash_mural_label.lower()}.", "requires_step_ids": ["take_torch"], "action": "use(torch,ash_mural)"},
            {"step_id": "open_iron_chest", "description": f"Open the {theme.iron_chest_label.lower()}.", "requires_step_ids": ["go_workshop"], "action": "open(iron_chest)"},
            {"step_id": "take_left_map", "description": f"Take the {theme.torn_map_left_label.lower()}.", "requires_step_ids": ["open_iron_chest"], "action": "take(torn_map_left,iron_chest)"},
            {"step_id": "return_foyer", "description": f"Return to the {theme.foyer_label.lower()}.", "requires_step_ids": ["take_left_map"], "action": "go(foyer)"},
            {"step_id": "go_courtyard", "description": f"Head to the {theme.courtyard_label.lower()}.", "requires_step_ids": ["return_foyer"], "action": "go(courtyard)"},
            {"step_id": "take_right_map", "description": f"Take the {theme.torn_map_right_label.lower()}.", "requires_step_ids": ["go_courtyard"], "action": "take(torn_map_right,courtyard)"},
            {"step_id": "combine_map", "description": f"Restore the {theme.full_map_label.lower()}.", "requires_step_ids": ["take_right_map"], "action": "combine(torn_map_left,torn_map_right)"},
            {"step_id": "use_map_on_well", "description": f"Use the {theme.full_map_label.lower()} on the {theme.stone_well_label.lower()}.", "requires_step_ids": ["combine_map"], "action": "use(full_map,stone_well)"},
            {"step_id": "read_plaque", "description": f"Read the {theme.water_plaque_label.lower()}.", "requires_step_ids": ["use_map_on_well"], "action": "read(water_plaque)"},
            {"step_id": "go_foyer_again", "description": f"Go back to the {theme.foyer_label.lower()}.", "requires_step_ids": ["read_plaque"], "action": "go(foyer)"},
            {"step_id": "go_gallery", "description": f"Head to the {theme.gallery_label.lower()}.", "requires_step_ids": ["go_foyer_again"], "action": "go(gallery)"},
            {"step_id": "give_map", "description": f"Give the map to the {theme.cartographer_label.lower()}.", "requires_step_ids": ["go_gallery"], "action": "give(full_map,cartographer)"},
            {"step_id": "use_lens_on_letter", "description": f"Use the {theme.lens_label.lower()} on the {theme.faded_letter_label.lower()}.", "requires_step_ids": ["give_map"], "action": "use(lens,faded_letter)"},
            {"step_id": "return_foyer_final", "description": f"Return to the {theme.foyer_label.lower()} again.", "requires_step_ids": ["use_lens_on_letter"], "action": "go(foyer)"},
            {"step_id": "go_shrine", "description": f"Go to the {theme.shrine_label.lower()}.", "requires_step_ids": ["return_foyer_final"], "action": "go(shrine)"},
            {"step_id": "talk_guardian", "description": f"Speak to the {theme.stone_guardian_label.lower()}.", "requires_step_ids": ["go_shrine"], "action": "talk(stone_guardian)"},
            {"step_id": "submit_answer", "description": "Submit the betrayer's name.", "requires_step_ids": ["talk_guardian"], "action": f'submit("{theme.answer}")'},
        ],
    }