saints-mile 1.0.2

A frontier JRPG for the adults who loved those games first
Documentation
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
//! Chapter 8 — The Burned Mission.
//!
//! Emotional law: Revelation. History beneath record.
//! The party is the investigative instrument.

use crate::types::*;
use crate::scene::types::*;
use crate::combat::types::*;
use crate::content::builders::*;

// ─── Scenes ────────────────────────────────────────────────────────

/// Into the mission valley.
pub fn valley_entry() -> Scene {
    scene(
        "bm_valley_entry", "mission_valley", "8_1",
        PacingTag::Exploration,
        vec![
            narrate_with(
                "The road to the mission is older than the rail, older than the \
                 territory, older than the law that claims to own it. Stone-lined \
                 drainage, prayer markers turned into fence posts, a path designed \
                 for carts, not wagons.",
                EmotionTag::Quiet,
            ),
            narrate(
                "You are moving backward in time, not just space. The frontier \
                 has layers. This is the oldest one.",
            ),
        ],
        vec![
            choice("Approach the ruins", vec![], to_scene("bm_ruins")),
        ],
        vec![
            set_flag("ch8_started", true),
        ],
    )
}

/// The mission ruins — contested ground.
pub fn ruins_scene() -> Scene {
    scene(
        "bm_ruins", "burned_mission", "8_2",
        PacingTag::Pressure,
        vec![
            narrate_with(
                "Adobe walls reduced to shoulder height. A collapsed bell tower. \
                 A well with a newer wooden frame — the one reliable water source \
                 in this part of the basin. A small cemetery with markers worn \
                 to near-illegibility.",
                EmotionTag::Quiet,
            ),
            narrate(
                "A territorial surveyor crew camps nearby, mapping the water \
                 source. An older woman lives in a small house built from \
                 mission stone. She maintains the cemetery.",
            ),
            say_with("cordelia",
                "Cordelia Vane. I've been waiting for someone to come asking \
                 the right questions. I've decided in advance to be disappointed \
                 by whoever does.",
                EmotionTag::Dry,
            ),
            // Memory ref: trestle_blast_scar echo from ch6
            say_if_with("narrator",
                "The blast scars on the mission walls look familiar. Same \
                 directed ignition pattern as the Millburn Trestle — charges \
                 placed to destroy specific rooms while leaving the structure \
                 standing. A tradition of precision demolition.",
                vec![Condition::HasMemoryObject(MemoryObjectId::new("trestle_blast_scar"))],
                EmotionTag::Tense,
            ),
        ],
        vec![
            choice("Enter the basement", vec![], to_scene("bm_basement")),
        ],
        vec![],
    )
}

/// The basement — the descent into truth.
pub fn basement_scene() -> Scene {
    scene(
        "bm_basement", "mission_basement", "8_3",
        PacingTag::Intimate,
        vec![
            narrate_with(
                "Below grade. Stone walls, partial ceiling, water damage. The air \
                 is cold in a way that doesn't match the surface temperature. \
                 Characters who spend time here become quieter afterward.",
                EmotionTag::Quiet,
            ),
            narrate(
                "What remains of the mission's paper is here: land grants, water \
                 certificates, transfer deeds, baptismal records, and a death \
                 register.",
            ),
        ],
        vec![
            choice("Read the records — each with different eyes", vec![],
                to_scene("bm_party_reads")),
        ],
        vec![],
    )
}

/// Party reads — the collective investigation.
pub fn party_reads_scene() -> Scene {
    scene(
        "bm_party_reads", "mission_basement", "8_3",
        PacingTag::Intimate,
        vec![
            // Ada reads medical
            say_with("ada",
                "Treatment patterns that predate the current fever by decades. \
                 The same water source, the same symptoms, the same communities. \
                 This place has been making people sick — or healing them — \
                 for longer than anyone alive remembers.",
                EmotionTag::Neutral,
            ),
            // Eli reads financial
            say_with("eli",
                "Financial chain shows the mission's land grant was transferred, \
                 amended, and 'lost' through territorial re-filings. The money \
                 trail predates the railroad by forty years. This isn't one \
                 crime. It's a tradition.",
                EmotionTag::Dry,
            ),
            // Galen reads land
            narrate_with(
                "The land grants prove the original claim covered everything the \
                 rail is now taking. These grants were never legally voided. \
                 They were burned.",
                EmotionTag::Tense,
            ),
            // Miriam reads death
            say_with("miriam",
                "More names in the death register than markers in the ground. \
                 Where are they? The people who are missing from the cemetery \
                 are missing from the official record too.",
                EmotionTag::Grief,
            ),
            // Cordelia fills the gaps
            say_with("cordelia",
                "They counted the dead for the books. They didn't count the \
                 dead for the ground.",
                EmotionTag::Quiet,
            ),
            say_with("cordelia",
                "I was a child when it burned. It was not a cook fire. The men \
                 who came — they knew which rooms had the paper. They burned \
                 those rooms first.",
                EmotionTag::Grief,
            ),
            // Lucien reads the fire
            say_if_with("lucien",
                "This was a job. Better than mine, but the same language. \
                 Directed ignition. Controlled enough to destroy the record \
                 rooms while leaving walls standing.",
                vec![flag_is("lucien_captured", true)],
                EmotionTag::Quiet,
            ),
            say_if_with("narrator",
                "He says it flat. The man who demolishes things recognized \
                 the work of another man who demolished things. He is \
                 confronting a lineage he did not ask to join.",
                vec![flag_is("lucien_captured", true)],
                EmotionTag::Quiet,
            ),
            // Rosa reads the land
            say_with("rosa",
                "The well sits on the valley's anchor. My family has drawn \
                 from water like this for decades. If this grant is real — \
                 and it is — then we were never trespassing. We were inheriting.",
                EmotionTag::Warm,
            ),
        ],
        vec![
            choice("Absorb what this means", vec![], to_scene("bm_bell_moment")),
        ],
        vec![
            set_flag("mission_records_read", true),
            set_flag("historical_fraud_discovered", true),
            set_flag("lucien_reads_fire_pattern", true),
        ],
    )
}

/// The bell moment — the uncanny, unresolved.
pub fn bell_moment() -> Scene {
    scene(
        "bm_bell_moment", "burned_mission", "8_4",
        PacingTag::Intimate,
        vec![
            narrate_with(
                "The bell in the collapsed tower should not ring. It is fallen, \
                 half-buried, cracked.",
                EmotionTag::Quiet,
            ),
            narrate("It rings."),
            narrate(
                "Not loud. Not dramatic. A single tone that carries through \
                 the valley like it was always there and you only just stopped \
                 talking long enough to hear it.",
            ),
            // Each character reacts differently
            say_with("ada",
                "Acoustic resonance from the crack. The valley's shape amplifies \
                 certain frequencies.",
                EmotionTag::Neutral,
            ),
            say_with("eli", "Irrelevant. Focus on the records.", EmotionTag::Dry),
            say_with("rosa",
                "Don't stand where old things make noise.",
                EmotionTag::Tense,
            ),
            say_with("miriam",
                "...",
                EmotionTag::Quiet,
            ),
            narrate_with(
                "Miriam does not explain. She does not dismiss. She listens. \
                 The bell is speaking. What it says depends on what you bring \
                 to it.",
                EmotionTag::Quiet,
            ),
            say_if_with("lucien",
                "I've heard metal talk before. Usually means the structure's \
                 about to go.",
                vec![flag_is("lucien_captured", true)],
                EmotionTag::Neutral,
            ),
        ],
        vec![
            choice("Continue", vec![
                set_flag("bell_heard", true),
            ], to_combat("mission_defense")),
        ],
        vec![],
    )
}

/// Post-mission defense — the fight is over, the records survive or don't.
///
/// Reached via the combat resolution system after `mission_defense` encounter.
pub fn mission_defense_aftermath() -> Scene {
    scene(
        "bm_defense_aftermath", "burned_mission", "8_5",
        PacingTag::Pressure,
        vec![
            narrate_with(
                "The enforcement team withdraws. Cordelia watches them go from \
                 behind the well housing, hands steady, face unreadable.",
                EmotionTag::Quiet,
            ),
            say_with("cordelia",
                "They'll come back. Men like that always come back. But now \
                 you know what's in the ground.",
                EmotionTag::Quiet,
            ),
        ],
        vec![
            choice("Leave the mission", vec![], to_scene("bm_chapter_close")),
        ],
        vec![],
    )
}

/// Chapter close — the ground has memory.
pub fn chapter_close() -> Scene {
    scene_with_memory(
        "bm_chapter_close", "mission_valley", "8_6",
        PacingTag::Intimate,
        vec![
            // Combat outcome: defend_records objective result
            say_if_with("narrator",
                "The mission records survived the fight. Every page intact — \
                 land grants, water certificates, the death register. The \
                 evidence leaves with the party.",
                vec![flag_is("mission_records_defended", true)],
                EmotionTag::Quiet,
            ),
            say_if_with("narrator",
                "Some records burned in the fight. Not all. Enough survived \
                 to prove the pattern — but the gaps will haunt every filing \
                 and every testimony.",
                vec![flag_is("mission_records_destroyed", true)],
                EmotionTag::Grief,
            ),
            narrate_with(
                "The party leaves the mission with the evidence. Behind them, \
                 the ruins remain. The bell may or may not ring as they go.",
                EmotionTag::Quiet,
            ),
            narrate(
                "The machine did not create the lie. The machine inherited it. \
                 And the people who built the machine knew exactly what they \
                 were building on top of.",
            ),
            // Relay branch callback — the evidence trail connects back
            say_if_with("narrator",
                "Tom's structural proof of the relay connects here. The \
                 same engineering language — build to specification, then \
                 specification becomes the weapon. The mission burned the \
                 same way the relay did: precisely, deliberately, with \
                 the records targeted first.",
                vec![flag_eq("relay_branch", "tom")],
                EmotionTag::Quiet,
            ),
            say_if_with("narrator",
                "Nella witnessed the relay fire. Cordelia witnessed the \
                 mission fire. Two women, two generations, the same \
                 story told to people who weren't ready to hear it. \
                 The evidence trail is made of witnesses, not paper.",
                vec![flag_eq("relay_branch", "nella")],
                EmotionTag::Quiet,
            ),
            say_if_with("narrator",
                "The relay papers and the mission papers tell the same \
                 story in different handwriting. Transfer orders become \
                 land grants become re-filings become ash. The \
                 administrative tradition of erasure predates the \
                 railroad by decades.",
                vec![flag_eq("relay_branch", "papers")],
                EmotionTag::Quiet,
            ),
            narrate_with(
                "The lie didn't start with the railroad. The railroad just \
                 learned the language of something older.",
                EmotionTag::Grief,
            ),
        ],
        vec![],
        vec![
            set_flag("ch8_complete", true),
            set_flag("mission_truth_recovered", true),
            set_flag("fire_was_deliberate", true),
            set_flag("regrant_was_fraud", true),
            memory("mission_land_grants"),
            memory("death_register_discrepancy"),
            memory("bell_phenomenon"),
        ],
        vec![
            MemoryRef {
                object: MemoryObjectId::new("bell_phenomenon"),
                callback_type: MemoryCallbackType::Echo,
                target_chapter: Some(ChapterId::new("ch15")),
            },
        ],
    )
}

// ─── Encounters ────────────────────────────────────────────────────

/// Mission defense — enforcement team arrives.
pub fn mission_defense_encounter() -> Encounter {
    Encounter {
        id: EncounterId::new("mission_defense"),
        phases: vec![CombatPhase {
            id: "ruins_fight".to_string(),
            description: "Fight in and around the mission ruins. Partial walls, \
                          cemetery, well area.".to_string(),
            enemies: vec![
                enemy_full("enforcer_a", "Enforcement Agent", 30, 24, 10, 58, 7, 12, 7),
                enemy_full("enforcer_b", "Enforcement Agent", 28, 22, 9, 55, 8, 12, 6),
                enemy("enforcer_c", "Enforcement Agent", 26, 20, 8, 52, 7),
            ],
            npc_allies: vec![],
            entry_conditions: vec![],
            phase_effects: vec![],
        }],
        standoff: Some(Standoff {
            postures: vec![StandoffPosture::EarlyDraw, StandoffPosture::SteadyHand, StandoffPosture::Bait],
            allow_focus: true,
            eli_influence: true,
        }),
        party_slots: 4,
        terrain: Terrain {
            name: "Burned Mission Ruins".to_string(),
            cover: vec![
                CoverElement { name: "Adobe wall section".to_string(), durability: 45, destructible: true },
                CoverElement { name: "Bell tower base".to_string(), durability: 70, destructible: false },
                CoverElement { name: "Well housing".to_string(), durability: 60, destructible: false },
            ],
            hazards: vec![],
        },
        objectives: vec![
            Objective {
                id: "defend_records".to_string(),
                label: "Defend the mission records".to_string(),
                objective_type: ObjectiveType::Primary,
                fail_consequence: vec![set_flag("mission_records_destroyed", true)],
                success_consequence: vec![set_flag("mission_records_defended", true)],
            },
        ],
        outcome_effects: vec![],
        escapable: true,
    }
}

// ─── Scene Registry ────────────────────────────────────────────────

pub fn get_scene(id: &str) -> Option<Scene> {
    match id {
        "bm_valley_entry" => Some(valley_entry()),
        "bm_ruins" => Some(ruins_scene()),
        "bm_basement" => Some(basement_scene()),
        "bm_party_reads" => Some(party_reads_scene()),
        "bm_bell_moment" => Some(bell_moment()),
        "bm_defense_aftermath" => Some(mission_defense_aftermath()),
        "bm_chapter_close" => Some(chapter_close()),
        _ => None,
    }
}

pub fn get_encounter(id: &str) -> Option<Encounter> {
    match id {
        "mission_defense" => Some(mission_defense_encounter()),
        _ => None,
    }
}