feat(world): add the Lost Twins optional escort side-quest

Two separated brothers, Marlon (conservatory) and Malte (boiler room), can be
reunited as optional content. Carry each twin's marble to the other as proof
he's alive and that twin follows you; lead both to the foyer and they escape
together, granting a Brass Compass keepsake and unlocking a new top-priority
"No One Left Behind" ending. Purely optional — no main win condition depends
on it.

The new EscortEngine drives following and the foyer reunion from flags alone,
so save/load needs no format change (it reconciles follower placement on the
first tick after loading). Adds Room.removeNpc, one tick call in the game loop,
and a follower note appended by LookCommand. descriptionStates swap the twins'
home-room prose once they leave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:31:25 +02:00
parent f8f7eb90f0
commit 41c79c3b96
10 changed files with 379 additions and 5 deletions

View File

@@ -2,6 +2,17 @@
# specific (best) ending is listed first. ghost_banished implies power_on
# implies the upper floor was reached, so #1 needs only left_manor + ghost_banished.
# Best possible end: spirit laid to rest AND the lost twins led out (optional).
- id: no_one_left_behind
title: "No One Left Behind"
victory: true
when: [{ flag: left_manor }, { flag: ghost_banished }, { flag: twins_reunited }]
text: |
You step into the night, and you are not alone. Marlon and Malte wait at
the gate, together, breathing the cold free air. Behind you the manor is
utterly still. You laid a spirit to rest and brought two brothers home —
there is no better way out of that house than this one.
- id: spirit_at_rest
title: "Spirit at Rest"
victory: true

View File

@@ -122,3 +122,27 @@
id: shovel
name: Shovel
description: A rusty shovel. Sturdy enough to dig, though nothing here needs digging.
# ── The lost twins (optional escort side-quest) ──────────────────────────────
- type: plain
id: marlon_keepsake
name: Blue Marble
description: |
A child's blue glass marble, worn smooth from years in a pocket. Marlon's.
Somewhere its red twin is being clutched just as tightly.
- type: plain
id: malte_keepsake
name: Red Marble
description: |
A child's red glass marble, the mate to a blue one. Malte's — the only
thing he kept hold of when the dark took him.
- type: readable
id: brass_compass
name: Brass Compass
description: A small brass compass, pressed on you by two very grateful brothers.
readText: |
Engraved on the back, in a child's scratchy hand: "For the one who led us
out. — M & M." The needle, you notice, always settles toward the door.

View File

@@ -27,3 +27,54 @@
consumes: letter
effects:
- { setFlag: heard_tale }
# ── The lost twins (optional escort side-quest) ──────────────────────────────
# Marlon (conservatory) and Malte (boiler room) were separated in the manor.
# Give each the other's marble as proof his brother lives, and he follows you;
# lead both to the foyer and they escape together. See EscortEngine.
- id: marlon
name: Marlon
description: |
A frightened young man pressed into the corner, knuckles white around a
blue glass marble. He has his brother's face — minus the brother.
greeting: |
"Please — have you seen Malte? My twin. We were exploring and the dark
just... took him. I can't leave this house without him. I can't."
dialogue:
- when: [{ hasItem: malte_keepsake }]
text: |
"That red marble — that's MALTE'S! He's alive? Here, quick: give it to
me so I know it's real. Then I'll follow you to him."
reactions:
- onReceive: malte_keepsake
response: |
"His marble. His actual marble. Then he's alive — and you've seen him.
Lead the way. I'm right behind you, I won't lose him again."
consumes: malte_keepsake
effects:
- { setFlag: marlon_following }
- { startQuest: reunite_twins }
- id: malte
name: Malte
description: |
A shivering young man clutching a red glass marble in the furnace-dark.
The image of his brother, alone.
greeting: |
"Who's there? Marlon? No... Please, have you seen him — my twin, Marlon?
We got split up and I can't find my way back in this dark."
dialogue:
- when: [{ hasItem: marlon_keepsake }]
text: |
"A blue marble — Marlon's! You've found him! Give it here so I know
it's real, then take me to him, please, before I lose my nerve."
reactions:
- onReceive: marlon_keepsake
response: |
"Marlon's marble. He's really here. Don't let go of me — wherever
you're going, I'm following. Take me to my brother."
consumes: marlon_keepsake
effects:
- { setFlag: malte_following }
- { startQuest: reunite_twins }

View File

@@ -16,3 +16,13 @@
stages:
- objective: "Lay the restless spirit to rest in the chapel."
completion: [{ flag: ghost_banished }]
# Optional side-quest: started when you first recruit a twin (see npcs.yaml).
- id: reunite_twins
title: "The Lost Twins"
autoStart: false
stages:
- objective: "Reunite Marlon and Malte — lead them both to the entrance."
completion: [{ flag: twins_reunited }]
onComplete:
- { say: "The brothers are together again, and gone into the night. You didn't have to help them. You're glad you did." }

View File

@@ -74,11 +74,18 @@
description: |
A glass-roofed room choked with dead ferns. Rusted plumbing lines the
walls, and a heavy iron valve wheel has been left on a potting bench.
A frightened young man cowers among the planters, a blue marble in his fist.
music: manor-theme.ogg
exits:
south: dining_room
items: [valve_wheel]
npcs: []
items: [valve_wheel, marlon_keepsake]
npcs: [marlon]
descriptionStates:
- when: [{ flag: marlon_following }]
text: |
A glass-roofed room choked with dead ferns. Rusted plumbing lines the
walls, and a heavy iron valve wheel has been left on a potting bench.
The corner where the frightened young man crouched is empty now.
- id: grand_staircase
name: Grand Staircase
@@ -135,13 +142,20 @@
name: Boiler Room
description: |
A cramped stone room behind the furnace. A rusty generator squats in the
corner, an empty fuse socket gaping in its housing.
corner, an empty fuse socket gaping in its housing. A shivering young man
huddles against the cold iron, a red marble clenched in his hand.
music: cellar-drone.ogg
dark: true
exits:
east: cellar_stairs
items: [generator]
npcs: []
items: [generator, malte_keepsake]
npcs: [malte]
descriptionStates:
- when: [{ flag: malte_following }]
text: |
A cramped stone room behind the furnace. A rusty generator squats in
the corner, an empty fuse socket gaping in its housing. The cold spot
by the iron where the shivering man huddled is empty now.
# ── Upper floor (reachable only once the power is on) ───────────────────────