Case Eight, a case picker, and a linter for the authoring traps
Case Eight — The Smell of Fear (1991). Act II, full colour. Five rooms, 100 points, verified at 100/100 with two verified deaths. The puzzle is identification: Meinheimer has an eidetic memory, so the double cannot be caught out on a fact — he can be caught out on a manner. Ask him two questions and he answers "about four hundred", "roughly", "give or take", and a man who remembers everything never says roughly. The catch is gated on having heard the approximations, so the player has to earn it. Accusing him without that is a death. Also in: the Blue Note, the SWAT tank through the wall into the zoo (which arms the lion that kills Hapsburg two rooms later), the mariachi infiltration, and a nuclear device disarmed by tripping over its power cable. tools/lint.py: three of the four bugs that cost real time here were the same class, and I walked into sets_flag-on-a-takeable twice — once in Case Ten, then again in Case Eight after documenting it. So the rules are a linter now, not a paragraph. It caught two live instances of that trap (case-08 blueprint, case-10 wig) and a scoring mismatch on the first run. All five cases lint clean and all five still win. play.sh is now a case picker: bare invocation lists the cases and asks, or pass a number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38
DESIGN.md
@ -156,7 +156,7 @@ in the title card** — a real object in the room whose only purpose is to die.
|
||||
| # | Case | From | Hook |
|
||||
|---|------|------|------|
|
||||
| 07 | **From the Files** | *Naked Gun* (1988) | **BUILT — see §7.** Nordberg shot on Ludwig's freighter; the Queen's reception; then Anaheim Stadium, where you **umpire a baseball game in order to legally frisk the players**. |
|
||||
| 08 | **The Smell of Fear** | *Naked Gun 2½* (1991) | Spot the fake Dr. Meinheimer by testing a photographic memory the double doesn't have. Includes the zoo, the SWAT tank, and disarming a nuclear device — solution: unplug it. |
|
||||
| 08 | **The Smell of Fear** | *Naked Gun 2½* (1991) | **BUILT.** The double gives himself away by being *approximately* right — a man with an eidetic memory never says "roughly". Blue Note, SWAT tank into the zoo, mariachi infiltration, and a nuclear device disarmed by tripping over its power cable. |
|
||||
| 09 | **The Final Insult** | *33⅓* (1994) | Retired Frank goes undercover in prison as "Nick the Slasher McGurk". Bomb in the Best Picture envelope. Marriage counselling is a live stat you must not tank. |
|
||||
|
||||
### Act III — Frank Drebin Jr, 2025 (full colour, cold palette)
|
||||
@ -291,14 +291,44 @@ never once asked about; the animated paperclip assistant; the trousers, which
|
||||
he retrieves with enormous dignity because standards are not a thing you keep
|
||||
only when they are being counted.
|
||||
|
||||
**Third authoring trap (add to the §7 list):** `sets_flag` fires on **use**,
|
||||
**Third authoring trap:** `sets_flag` fires on **use**,
|
||||
never on **take** — `points` does both. A takeable item that was supposed to
|
||||
set a flag silently didn't, and the gate it fed never opened. Gate on `needs`
|
||||
(which checks inventory) instead of inventing a flag for "is carrying it".
|
||||
|
||||
---
|
||||
|
||||
## 9. The glitches are canon
|
||||
## 9. `tools/lint.py` — the traps, encoded
|
||||
|
||||
Three of the four bugs that cost real debugging time in this project were the
|
||||
same *class* of mistake, and I walked into the `sets_flag`-on-a-takeable one
|
||||
twice — once in Case Ten, then again in Case Eight after writing it down. So the
|
||||
rules are now a linter rather than a paragraph:
|
||||
|
||||
```bash
|
||||
python3 tools/lint.py # every case
|
||||
python3 tools/lint.py cases/... # one
|
||||
```
|
||||
|
||||
It checks, per case:
|
||||
|
||||
| Rule | Why it exists |
|
||||
|---|---|
|
||||
| `takeable` + `sets_flag` | `sets_flag` fires on **use**, never on **take** (`points` does both). The flag silently never sets and the gate it feeds never opens. |
|
||||
| One object name a prefix of another | The parser matches loosely — `ludwigdesk` shadowed `ludwig`, so `talk ludwig` addressed the furniture. |
|
||||
| Flag required but never set | Catches a typo'd `requires_flag` / `exit_flags` before it becomes an unwinnable case. |
|
||||
| Exit points at a room that doesn't exist | |
|
||||
| `exit_flags` set on an edge with no exit | |
|
||||
| Dialogue `goto` out of range | |
|
||||
| Sprite referenced but missing from `sprites/` | Otherwise it's a silent magenta box. |
|
||||
| Authored points **below** `max_score` | The target would be unreachable. (Above is fine — alternative dialogue branches awarding the same latched flag inflate the naive sum.) |
|
||||
|
||||
It does **not** check that every exit is walkable — that one still needs a real
|
||||
playthrough, because it depends on painted `Wall` bands (§7). Walk your exits.
|
||||
|
||||
---
|
||||
|
||||
## 10. The glitches are canon
|
||||
|
||||
*Police Quest II* shipped with bugs that are, without alteration, Naked Gun
|
||||
jokes. A player who has never heard of Sierra reads them as Drebin being Drebin;
|
||||
@ -323,7 +353,7 @@ the game is behaving exactly as documented.
|
||||
|
||||
---
|
||||
|
||||
## 10. Art pipeline
|
||||
## 11. Art pipeline
|
||||
|
||||
`tools/sprites.py <case>` reads that case's `sprites.json` and generates every
|
||||
missing sprite on the **MODELBEAST** farm (free, local, m3ultra):
|
||||
|
||||
@ -18,6 +18,8 @@ itself a reimplementation of AGI, the engine Sierra shipped *Police Quest* on.
|
||||
./play.sh
|
||||
```
|
||||
|
||||
That lists the cases and asks which one. `./play.sh 07` jumps straight to one.
|
||||
|
||||
Or directly:
|
||||
|
||||
```bash
|
||||
@ -34,7 +36,9 @@ cargo run -p mrpgi --manifest-path ~/Documents/MRPGI/Cargo.toml -- --game cases/
|
||||
| `cases/case-07-from-the-files/` | **Case Seven — playable, 100 points.** Drebin Sr, 1988, full colour. Six rooms, the Anaheim Stadium finale. |
|
||||
| `cases/case-02-ring-of-fear/` | **Case Two — playable, 100 points.** Drebin Sr, 1982, monochrome. Five rooms, undercover, three deaths. |
|
||||
| `cases/case-10-plot-device/` | **Case Ten — playable, 100 points.** Drebin Jr (Neeson), 2025, cold palette. Six rooms, the arena finale and the owl. |
|
||||
| `cases/case-08-smell-of-fear/` | **Case Eight — playable, 100 points.** Drebin Sr, 1991, full colour. Five rooms, the memory test. |
|
||||
| `tools/sprites.py` | Generates every sprite on the MODELBEAST farm (free, local). |
|
||||
| `tools/lint.py` | Checks every case for the authoring traps that have bitten us. Run it before playtesting. |
|
||||
| `RESEARCH.md` / `DESIGN.md` | Source research and the campaign bible. |
|
||||
|
||||
## Case One — A Substantial Gift
|
||||
|
||||
22
cases/case-08-smell-of-fear/game.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "Police Squad Quest — Case Eight: The Smell of Fear",
|
||||
"start_room": 0,
|
||||
"max_score": 100,
|
||||
"intro_text": "It was the kind of night that makes a man wonder where it all went wrong. Somebody blew up a research institute this morning, and on Thursday Dr. Albert Meinheimer tells the President where the country gets its power for the next fifty years. My name is Drebin. Detective Lieutenant Frank Drebin, Police Squad. Type 'help'.",
|
||||
"death_text": "† OFFICER DOWN. The recommendation goes ahead without you. The day restarts. †",
|
||||
"verbs": [
|
||||
{ "name": "use", "words": ["frisk", "search", "inspect", "cuff", "arrest", "wear", "radio", "halt", "unplug", "disarm", "drive", "free", "play"] },
|
||||
{ "name": "look", "words": ["investigate", "observe", "detect"] },
|
||||
{ "name": "talk", "words": ["interrogate", "question", "brief", "test"] }
|
||||
],
|
||||
"flags": {
|
||||
"jane_warned": false,
|
||||
"has_blueprint": false,
|
||||
"heard_approx": false,
|
||||
"knows_fake": false,
|
||||
"lion_loose": false,
|
||||
"meinheimer_free": false,
|
||||
"disguised": false,
|
||||
"confessed": false
|
||||
}
|
||||
}
|
||||
95
cases/case-08-smell-of-fear/rooms/room0.json
Normal file
@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "The Blue Note",
|
||||
"enter_text": "A bar. A piano. A man at the end of it who has been there since four in the afternoon and who is you.",
|
||||
"music": 2,
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 60, "color": 4, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 22, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 58, "w": 160, "h": 3, "color": 6, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 61, "w": 160, "h": 107, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 12, "y": 26, "w": 34, "h": 26, "color": 6, "meaning": "None" } },
|
||||
{ "Rect": { "x": 16, "y": 30, "w": 26, "h": 18, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 22, "y": 36, "w": 14, "h": 6, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 108, "y": 26, "w": 40, "h": 26, "color": 6, "meaning": "None" } },
|
||||
{ "Rect": { "x": 112, "y": 30, "w": 32, "h": 18, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 116, "y": 34, "w": 6, "h": 10, "color": 10, "meaning": "None" } },
|
||||
{ "Rect": { "x": 126, "y": 34, "w": 6, "h": 10, "color": 12, "meaning": "None" } },
|
||||
{ "Rect": { "x": 136, "y": 34, "w": 6, "h": 10, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 96, "w": 160, "h": 1, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||
],
|
||||
"spawn": [24, 146],
|
||||
"exits": [null, 1, null, null],
|
||||
"exit_flags": ["", "jane_warned", "", ""],
|
||||
"exit_blocked": ["", "She is standing right there and she is going to marry an oil man on Saturday. You can leave after you have said something.", "", ""],
|
||||
"objects": [
|
||||
{
|
||||
"name": "bartop",
|
||||
"sprite": "bartop",
|
||||
"x": 118,
|
||||
"y": 108,
|
||||
"look": "The bar. Nine hours of it. The bartender has stopped asking and started anticipating, which is the point at which a man ought to go home.",
|
||||
"synonyms": "bar counter bottles stools drink",
|
||||
"use_text": "You order another. It arrives before you finish ordering it."
|
||||
},
|
||||
{
|
||||
"name": "piano",
|
||||
"sprite": "piano",
|
||||
"x": 40,
|
||||
"y": 116,
|
||||
"look": "An upright piano nobody is playing. On top of it: a candle, and a glass somebody left in 1987.",
|
||||
"synonyms": "keys upright instrument candle",
|
||||
"points": 5,
|
||||
"use_text": "You sit down and play. You have never had a lesson in your life and you play beautifully, for four minutes, and nobody in the bar looks up, because this is the Blue Note and a man playing beautifully at two in the morning is not news here. When you stop, the bartender says: 'That's the one she liked.' You had not mentioned her."
|
||||
},
|
||||
{
|
||||
"name": "jane",
|
||||
"sprite": "jane",
|
||||
"x": 74,
|
||||
"y": 128,
|
||||
"look": "Jane Spencer. Six years, and she is marrying Quentin Hapsburg on Saturday, and she has come into a bar at two in the morning to tell you herself rather than let you read it, which is the single kindest thing anybody has done for you this decade.",
|
||||
"synonyms": "spencer woman blonde",
|
||||
"use_text": "You do not take her hand. You have thought about it for the entire conversation and you do not do it, because she is engaged on Saturday and because there is a way to behave.",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "Jane: Frank. I heard about the institute. Six people. Jane: ...And I wanted you to hear the other thing from me.",
|
||||
"choices": [
|
||||
{ "text": "I heard. Saturday.", "goto": 1 },
|
||||
{ "text": "Tell me about the institute, Jane.", "goto": 2 },
|
||||
{ "text": "Congratulations. He seems like a fine man.", "goto": 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "Jane: Saturday. Neither of you says anything for a while. Somewhere behind you a man playing beautifully at two in the morning is not news.",
|
||||
"choices": [{ "text": "Tell me about the institute, Jane.", "goto": 2 }]
|
||||
},
|
||||
{
|
||||
"says": "Jane: I was there this morning. I do the press for Dr. Meinheimer's institute — I've done it for four years. Jane: Frank, a man came out of the building nine minutes before it went up. Thin. Leather jacket. He looked right at me and he did not hurry.",
|
||||
"choices": [
|
||||
{ "text": "A man who doesn't hurry has somewhere to be.", "goto": 3 },
|
||||
{ "text": "Did you tell anyone?", "goto": 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "Jane: That's what I thought. That's exactly what I thought, and then I thought: I've been thinking like Frank Drebin for six years and it is not a transferable skill.",
|
||||
"choices": [{ "text": "Jane. Who benefits?", "goto": 5 }]
|
||||
},
|
||||
{
|
||||
"says": "Jane: I told Quentin. She says it, and then she hears it, the way people in this game always hear it about half a second too late.",
|
||||
"choices": [{ "text": "Jane. Who benefits?", "goto": 5 }]
|
||||
},
|
||||
{
|
||||
"says": "Jane: ...Nobody benefits, Frank. It's a research institute. Albert makes a recommendation to the President on Thursday and then he goes back to his garden. Jane: Coal benefits. Oil benefits. Everybody who isn't the sun benefits.",
|
||||
"choices": [
|
||||
{ "text": "What does Quentin do again, Jane?", "goto": 6, "sets_flag": "jane_warned", "points": 10 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "Jane: ...He's in energy. She puts down the glass. She does not pick it up again. Jane: Frank. Be careful. And — the institute's still open. Nobody's been through Albert's office since the fire crew.",
|
||||
"choices": [{ "text": "Then I'll go through it.", "goto": -1 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
234
cases/case-08-smell-of-fear/rooms/room1.json
Normal file
@ -0,0 +1,234 @@
|
||||
{
|
||||
"name": "Meinheimer Institute — What's Left of the East Wing",
|
||||
"enter_text": "Six people this morning. The fire crew have gone. Nobody has been through here since, because nobody has been asked to.",
|
||||
"music": 2,
|
||||
"strokes": [
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 168,
|
||||
"color": 8,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 30,
|
||||
"color": 1,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 26,
|
||||
"w": 160,
|
||||
"h": 36,
|
||||
"color": 7,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 40,
|
||||
"y": 26,
|
||||
"w": 46,
|
||||
"h": 36,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 52,
|
||||
"y": 30,
|
||||
"w": 8,
|
||||
"h": 24,
|
||||
"color": 8,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 68,
|
||||
"y": 34,
|
||||
"w": 6,
|
||||
"h": 20,
|
||||
"color": 8,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 60,
|
||||
"w": 160,
|
||||
"h": 3,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 63,
|
||||
"w": 160,
|
||||
"h": 105,
|
||||
"color": 8,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 8,
|
||||
"y": 32,
|
||||
"w": 24,
|
||||
"h": 24,
|
||||
"color": 15,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 12,
|
||||
"y": 36,
|
||||
"w": 16,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 12,
|
||||
"y": 44,
|
||||
"w": 12,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 96,
|
||||
"y": 32,
|
||||
"w": 52,
|
||||
"h": 24,
|
||||
"color": 15,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 36,
|
||||
"w": 44,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 44,
|
||||
"w": 30,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 100,
|
||||
"w": 160,
|
||||
"h": 1,
|
||||
"color": 7,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 164,
|
||||
"w": 160,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
}
|
||||
],
|
||||
"spawn": [
|
||||
16,
|
||||
140
|
||||
],
|
||||
"exits": [
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
0
|
||||
],
|
||||
"exit_flags": [
|
||||
"",
|
||||
"has_blueprint",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"exit_blocked": [
|
||||
"",
|
||||
"There is something in this room that somebody came back for and did not find. Find it.",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"name": "rubble",
|
||||
"sprite": "rubble",
|
||||
"x": 62,
|
||||
"y": 132,
|
||||
"look": "Masonry and splintered timber where the east wall was. The blast came from inside the room and went out through the wall, which is the wrong way round for a gas main and the right way round for a person.",
|
||||
"synonyms": "debris masonry wreckage mess timber",
|
||||
"use_text": "You go through it by hand for forty minutes. Under the third slab is a maintenance man's boot with a maintenance man still in it, and you sit back on your heels for a moment, because that is what you do, and then you carry on."
|
||||
},
|
||||
{
|
||||
"name": "clockbomb",
|
||||
"sprite": "clockbomb",
|
||||
"x": 108,
|
||||
"y": 108,
|
||||
"look": "The wall clock from the corridor, thrown clear, face down. It stopped at 9:04. The institute went up at 9:11.",
|
||||
"synonyms": "clock timer face wires device",
|
||||
"points": 10,
|
||||
"sets_flag": "saw_timer",
|
||||
"use_text": "You turn the clock over. There is a second mechanism taped to the back of it — a timer, wired, and a residue where a small charge sat. The clock did not stop because the building blew up. The building blew up because the clock stopped. It was set at 9:04 by somebody standing on a chair in a corridor, in daylight, in a working building, which means he was wearing something that made him invisible in a working building. A jacket. A clipboard. A leather jacket and no hurry."
|
||||
},
|
||||
{
|
||||
"name": "blueprint",
|
||||
"sprite": "blueprint",
|
||||
"x": 30,
|
||||
"y": 124,
|
||||
"look": "A blueprint tube in the corner of what used to be Dr. Meinheimer's office, under a filing cabinet that fell on it and saved it.",
|
||||
"synonyms": "plans drawing tube roll paper report",
|
||||
"takeable": false,
|
||||
"requires_flag": "saw_timer",
|
||||
"points": 5,
|
||||
"sets_flag": "has_blueprint",
|
||||
"use_text": "It is not a blueprint. It is the recommendation — the actual one, four hundred and twelve pages, in Albert Meinheimer's hand, rolled in a tube because a man who remembers everything still likes to hold a thing. Page one, first line, underlined twice: THE SUN ARRIVES EVERY DAY AND SENDS NO INVOICE. Somebody came back into a burning building for this and did not find it, because a filing cabinet fell on it."
|
||||
},
|
||||
{
|
||||
"name": "savage",
|
||||
"sprite": "savage",
|
||||
"x": 138,
|
||||
"y": 148,
|
||||
"look": "Across the road, in a parked car that has been there since you arrived, is a thin man in a leather jacket who is not reading the newspaper he is holding.",
|
||||
"synonyms": "man thin leather jacket car watcher hitman",
|
||||
"kills": true,
|
||||
"use_text": "You cross the road alone, at night, to a parked car containing a man who has already killed six people this morning, without calling it in, because Jane is not answering her telephone and you are not thinking clearly. Regulations, page 9, cover approaching an occupied vehicle. You have read them. You wrote part of them."
|
||||
}
|
||||
]
|
||||
}
|
||||
113
cases/case-08-smell-of-fear/rooms/room2.json
Normal file
@ -0,0 +1,113 @@
|
||||
{
|
||||
"name": "Hapsburg Energy — Reception for Dr. Meinheimer",
|
||||
"enter_text": "Canapés, a string quartet, and the guest of honour in a wheelchair by the window being extremely charming to everybody.",
|
||||
"music": 5,
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 6, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 58, "color": 15, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 12, "w": 160, "h": 4, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 56, "w": 160, "h": 4, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 60, "w": 160, "h": 108, "color": 6, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 14, "y": 60, "w": 132, "h": 96, "color": 4, "meaning": "None" } },
|
||||
{ "Rect": { "x": 18, "y": 64, "w": 124, "h": 2, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 104, "y": 18, "w": 48, "h": 34, "color": 1, "meaning": "None" } },
|
||||
{ "Rect": { "x": 108, "y": 22, "w": 40, "h": 26, "color": 9, "meaning": "None" } },
|
||||
{ "Rect": { "x": 126, "y": 18, "w": 2, "h": 34, "color": 15, "meaning": "None" } },
|
||||
{ "Rect": { "x": 10, "y": 20, "w": 34, "h": 30, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 14, "y": 24, "w": 26, "h": 22, "color": 2, "meaning": "None" } },
|
||||
{ "Rect": { "x": 20, "y": 30, "w": 14, "h": 8, "color": 14, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||
],
|
||||
"spawn": [20, 148],
|
||||
"exits": [null, 3, null, 1],
|
||||
"exit_flags": ["", "knows_fake", "", ""],
|
||||
"exit_blocked": ["", "Everything about that man is right. That is what is wrong with him. Find out what he can't do.", "", ""],
|
||||
"objects": [
|
||||
{
|
||||
"name": "champagne",
|
||||
"sprite": "champagne",
|
||||
"x": 30,
|
||||
"y": 106,
|
||||
"look": "A tray of champagne going past at shoulder height, permanently, in a loop, all evening.",
|
||||
"synonyms": "tray drinks flutes glasses waiter",
|
||||
"use_text": "You are on duty. You take one anyway, because you are at a party for the oil industry and a man at a party for the oil industry holding nothing at all is the most conspicuous object in the room."
|
||||
},
|
||||
{
|
||||
"name": "hapsburg",
|
||||
"sprite": "hapsburg",
|
||||
"x": 44,
|
||||
"y": 130,
|
||||
"look": "Quentin Hapsburg. Oil, gas, coal, and on Saturday, Jane. He has the handshake of a man who has never once been on the wrong end of anything.",
|
||||
"synonyms": "quentin oil man executive host",
|
||||
"use_text": "You shake his hand. He tells you he has heard a great deal about you. He has not.",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "Hapsburg: Detective Drebin! Jane's told me all about you. All about you. Hapsburg: Terrible business at the institute. Six people. I've offered them my own building until they rebuild — whatever they need, for as long as they need it.",
|
||||
"choices": [
|
||||
{ "text": "That's very generous.", "goto": 1 },
|
||||
{ "text": "Where is Dr. Meinheimer staying?", "goto": 2 },
|
||||
{ "text": "You blew up that institute, Hapsburg.", "goto": 3 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "Hapsburg: It's the least we can do. We're all on the same side, Detective — everybody wants the lights to come on.",
|
||||
"choices": [{ "text": "Everybody does.", "goto": 0 }]
|
||||
},
|
||||
{
|
||||
"says": "Hapsburg: With me. Since Tuesday. Poor Albert lost his home and his files in one morning — he's been under my roof ever since, and he will be until Thursday. Under his roof since Tuesday. The institute went up on Wednesday.",
|
||||
"choices": [{ "text": "Since Tuesday.", "goto": 0 }]
|
||||
},
|
||||
{
|
||||
"says": "Hapsburg: ...I did. He smiles, and leans in, and says it quietly and pleasantly, three feet from a string quartet. Hapsburg: And there is not one thing in this room that says so, Detective, and you have just told me exactly how much you have got. Enjoy the champagne.",
|
||||
"choices": [{ "text": "I intend to.", "goto": 0 }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hacker",
|
||||
"sprite": "hacker",
|
||||
"x": 104,
|
||||
"y": 124,
|
||||
"look": "Dr. Albert Meinheimer, in his wheelchair by the window, delightful to everybody, correct about everything. He is the right height, the right age, the right beard, and the right bow tie, and you have been watching him for eleven minutes and there is something the matter with him that you cannot name.",
|
||||
"synonyms": "meinheimer albert doctor scientist wheelchair professor",
|
||||
"dialogue": [
|
||||
{
|
||||
"says": "\"Meinheimer\": Detective! Jane has spoken of you often. Sit, sit — ask me anything. I am told I am a difficult man to interview.",
|
||||
"choices": [
|
||||
{ "text": "How long is the recommendation, Doctor?", "goto": 1 },
|
||||
{ "text": "You're not Albert Meinheimer.", "goto": 5, "kills": true },
|
||||
{ "text": "Enjoy your evening, Doctor.", "goto": -1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "\"Meinheimer\": The recommendation? Oh — about four hundred pages. Give or take. Four hundred and twelve. You have it in a tube in the car. But that is not the thing. The thing is smaller than that and you have not got it yet.",
|
||||
"choices": [
|
||||
{ "text": "Give or take. And the first line, Doctor?", "goto": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "\"Meinheimer\": The first line! Ha! Something about the sun, I believe — roughly, roughly. You must understand, Detective, I have written a great many things. He is warm, and modest, and he is doing it beautifully, and he has now said 'about', 'give or take', and 'roughly' in ninety seconds. Albert Meinheimer has an eidetic memory. It is the only interesting thing about him. It is in every profile ever written of him. It is why the President asked him and not somebody cheaper.",
|
||||
"choices": [
|
||||
{ "text": "A man who remembers everything never says 'roughly'.", "goto": 3, "sets_flag": "knows_fake", "points": 20 },
|
||||
{ "text": "Of course, Doctor. Forgive me.", "goto": 4 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"says": "The warmth goes out of his face in stages, like a building being switched off floor by floor. \"Meinheimer\": ...I've had a very difficult week, Detective. Earl Hacker. Twenty years in regional theatre and four days in a wheelchair, and he has been word-perfect all evening on everything except the one thing that cannot be learned. Somewhere in this city the real Albert Meinheimer is in a room, because you do not keep a double and a corpse — a corpse is a murder and a murder is an investigation, and a man in a room is nothing at all until Thursday.",
|
||||
"choices": [{ "text": "Where is he?", "goto": 6 }]
|
||||
},
|
||||
{
|
||||
"says": "\"Meinheimer\": Not at all, not at all! He is enormously relieved, and he shows it, which is the second mistake, and you make a note of it and let him have the evening.",
|
||||
"choices": [{ "text": "The first line, Doctor?", "goto": 2 }]
|
||||
},
|
||||
{
|
||||
"says": "You accuse the guest of honour of impersonation, out loud, at his own reception, on the strength of a feeling. He is charming about it. Hapsburg is charming about it. Everybody is extremely charming about it, and Captain Hocken is charming about it on the telephone at seven the next morning, and by Thursday you are charming about it to a man who runs the property room, who has been expecting you."
|
||||
},
|
||||
{
|
||||
"says": "Hacker: ...There's a bonded warehouse. Alameda and Sixth. I've never been. I only get the pages to learn. Hacker: Detective — he's a nice old man. He asked them for a chair.",
|
||||
"choices": [{ "text": "Alameda and Sixth.", "goto": -1 }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
70
cases/case-08-smell-of-fear/rooms/room3.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "Bonded Warehouse — Alameda & Sixth",
|
||||
"enter_text": "A bonded warehouse backing onto the city zoo, which is the kind of detail you notice and file and do not yet know why.",
|
||||
"music": 3,
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 64, "color": 6, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 62, "w": 160, "h": 3, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 65, "w": 160, "h": 103, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 8, "y": 8, "w": 40, "h": 24, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 12, "y": 12, "w": 32, "h": 16, "color": 1, "meaning": "None" } },
|
||||
{ "Rect": { "x": 56, "y": 6, "w": 48, "h": 52, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 60, "y": 10, "w": 40, "h": 44, "color": 8, "meaning": "None" } },
|
||||
{ "Rect": { "x": 66, "y": 10, "w": 3, "h": 44, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 76, "y": 10, "w": 3, "h": 44, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 86, "y": 10, "w": 3, "h": 44, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 118, "y": 10, "w": 34, "h": 46, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 122, "y": 14, "w": 26, "h": 38, "color": 6, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 104, "w": 160, "h": 1, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||
],
|
||||
"spawn": [16, 146],
|
||||
"exits": [null, 4, null, 2],
|
||||
"exit_flags": ["", "meinheimer_free", "", ""],
|
||||
"exit_blocked": ["", "There is a nice old man in this building who asked them for a chair. You are not leaving without him.", "", ""],
|
||||
"objects": [
|
||||
{
|
||||
"name": "crate",
|
||||
"sprite": "crate",
|
||||
"x": 40,
|
||||
"y": 120,
|
||||
"look": "Bonded crates, stencilled HAPSBURG ENERGY — SAMPLES, stacked to the roof. Every one of them is empty. Eleven thousand square feet of warehouse rented for four days to hold nothing at all.",
|
||||
"synonyms": "crates boxes samples cargo stack",
|
||||
"use_text": "You put your shoulder to one and it goes over like a cereal packet. Whatever this building is for, it is not for storage, and it has not been for storage since Tuesday."
|
||||
},
|
||||
{
|
||||
"name": "tank",
|
||||
"sprite": "tank",
|
||||
"x": 116,
|
||||
"y": 132,
|
||||
"look": "Police Squad's armoured entry vehicle, on the ramp, with Nordberg in the driver's seat because Nordberg was closest.",
|
||||
"synonyms": "swat armoured vehicle ram truck",
|
||||
"points": 10,
|
||||
"sets_flag": "lion_loose",
|
||||
"use_text": "You call the entry. Nordberg drives. The vehicle enters the building through the loading door, continues through the internal partition, continues through the north wall of the building, crosses the service alley, and enters the city zoo through the perimeter fence and then through the big cat enclosure, coming to rest against a drinking fountain. Nobody is hurt. Nordberg is delighted. The gate of the big cat enclosure is, as of eleven forty at night, standing open, and it is nobody's job to close it, and nobody does."
|
||||
},
|
||||
{
|
||||
"name": "zoocage",
|
||||
"sprite": "zoocage",
|
||||
"x": 76,
|
||||
"y": 96,
|
||||
"look": "Through the hole in the north wall, forty feet away across a service alley, is the city zoo. The big cat enclosure gate is standing open.",
|
||||
"synonyms": "zoo enclosure gate bars cage",
|
||||
"visible_flag": "lion_loose",
|
||||
"use_text": "You radio it in. The night desk takes the details and reads them back to you, correctly, and files them, correctly, and the form goes to Animal Control, who open at nine."
|
||||
},
|
||||
{
|
||||
"name": "meinheimer",
|
||||
"sprite": "meinheimer",
|
||||
"x": 78,
|
||||
"y": 130,
|
||||
"look": "Dr. Albert Meinheimer, in a store room, in a chair. Four days. They gave him the chair when he asked for it, and paper when he asked for it, and he has spent four days writing the recommendation out again from memory because he assumed the first copy burned.",
|
||||
"synonyms": "albert doctor scientist professor old man",
|
||||
"requires_flag": "lion_loose",
|
||||
"points": 15,
|
||||
"sets_flag": "meinheimer_free",
|
||||
"use_text": "You get him out. Meinheimer: You're the detective. Jane's detective. He has never met you. Jane described you once, four years ago, at a dinner, and he has an eidetic memory, and he has been carrying you around in it ever since without ever expecting to need you. Meinheimer: They have a man doing me at a party. Is he any good? You tell him the man is very good indeed but says 'roughly'. Meinheimer, delighted: Ha! Then he isn't any good at all."
|
||||
}
|
||||
]
|
||||
}
|
||||
196
cases/case-08-smell-of-fear/rooms/room4.json
Normal file
@ -0,0 +1,196 @@
|
||||
{
|
||||
"name": "The Press Club — Thursday, 8:00 PM",
|
||||
"enter_text": "Four hundred journalists, one lectern, and a man in a wheelchair about to tell the country where its power comes from for the next fifty years.",
|
||||
"music": 5,
|
||||
"strokes": [
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 168,
|
||||
"color": 4,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 56,
|
||||
"color": 1,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 8,
|
||||
"w": 160,
|
||||
"h": 4,
|
||||
"color": 14,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 54,
|
||||
"w": 160,
|
||||
"h": 4,
|
||||
"color": 14,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 58,
|
||||
"w": 160,
|
||||
"h": 110,
|
||||
"color": 4,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 20,
|
||||
"y": 16,
|
||||
"w": 120,
|
||||
"h": 32,
|
||||
"color": 15,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 26,
|
||||
"y": 22,
|
||||
"w": 108,
|
||||
"h": 5,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 26,
|
||||
"y": 32,
|
||||
"w": 76,
|
||||
"h": 5,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 26,
|
||||
"y": 41,
|
||||
"w": 92,
|
||||
"h": 3,
|
||||
"color": 8,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 10,
|
||||
"y": 58,
|
||||
"w": 140,
|
||||
"h": 100,
|
||||
"color": 12,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 14,
|
||||
"y": 62,
|
||||
"w": 132,
|
||||
"h": 2,
|
||||
"color": 14,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 164,
|
||||
"w": 160,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
}
|
||||
],
|
||||
"spawn": [
|
||||
18,
|
||||
150
|
||||
],
|
||||
"exits": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
3
|
||||
],
|
||||
"objects": [
|
||||
{
|
||||
"name": "mariachi",
|
||||
"sprite": "mariachi",
|
||||
"x": 32,
|
||||
"y": 106,
|
||||
"look": "The house band's spare kit, in the service corridor: three charro suits and three sombreros, which is precisely three more disguises than Police Squad has ever needed at one time.",
|
||||
"synonyms": "band suit sombrero costume disguise musicians guitar",
|
||||
"points": 0,
|
||||
"sets_flag": "disguised",
|
||||
"use_text": "You, Captain Hocken and Officer Nordberg enter the Press Club dinner of the National Association of Energy Correspondents dressed as a mariachi band. Nobody stops you. Nobody stops a mariachi band. A mariachi band is the only thing in the world that can walk into any room, at any time, at any volume, and be treated as weather. You are asked to play. You play."
|
||||
},
|
||||
{
|
||||
"name": "podium",
|
||||
"sprite": "podium",
|
||||
"x": 84,
|
||||
"y": 100,
|
||||
"look": "The lectern. Earl Hacker is nine minutes from standing up at it and recommending forty years of coal to four hundred journalists in the voice of a man everybody trusts.",
|
||||
"synonyms": "lectern stand microphone stage",
|
||||
"requires_flag": "disguised",
|
||||
"points": 10,
|
||||
"sets_flag": "confessed",
|
||||
"use_text": "Captain Hocken takes Hacker in the wings, still in the sombrero, and Hacker gives it up in under a minute — the fee, the four days of pages, the wheelchair, Hapsburg's name, all of it — because Earl Hacker has been waiting twenty years for somebody to ask him a question about his work. Then you wheel the real Albert Meinheimer out in front of four hundred journalists, and he does not need a lectern, and he does not need notes, and he recites four hundred and twelve pages from memory, beginning: THE SUN ARRIVES EVERY DAY AND SENDS NO INVOICE."
|
||||
},
|
||||
{
|
||||
"name": "hapsburg",
|
||||
"sprite": "hapsburg",
|
||||
"x": 116,
|
||||
"y": 118,
|
||||
"look": "Quentin Hapsburg, on the roof stair with Jane in front of him, going up.",
|
||||
"synonyms": "quentin oil man executive",
|
||||
"requires_flag": "confessed",
|
||||
"sets_flag": "on_roof",
|
||||
"use_text": "You follow him to the roof. He has Jane, and he has a device the size of a filing cabinet wired to the building, and he explains it to you at some length, because he has spent four days being the cleverest man in every room he has been in and he has nine minutes left of it."
|
||||
},
|
||||
{
|
||||
"name": "nuke",
|
||||
"sprite": "nuke",
|
||||
"x": 68,
|
||||
"y": 138,
|
||||
"look": "A tactical device, on the roof, wired into the building's mains. Six minutes on the timer. There are eleven wires and none of them are colour-coded, because the man who built it was not expecting an audience.",
|
||||
"synonyms": "bomb device timer wires cylinder",
|
||||
"requires_flag": "on_roof",
|
||||
"wins": true,
|
||||
"points": 15,
|
||||
"use_text": "Regulations, page 41, are unambiguous: an officer does not attempt to render safe an improvised device. An officer establishes a cordon and waits for the bomb squad. The bomb squad is nineteen minutes out. The timer says six. So you kneel down in front of eleven uncoded wires with four hundred journalists downstairs, and you look at it for a long moment, and you stand up to get a better angle, and your foot catches the power cable and pulls it out of the wall. The timer stops at 0:41. It was mains-powered. Nobody had thought to ask what it was plugged into, least of all the man who plugged it in. Hapsburg goes over the parapet, off the awning of the Press Club, and into Sixth Street, where he is met — at eleven forty on a Thursday night, forty feet from a hole in a zoo fence that Animal Control were not opening until nine — by a full-grown male African lion. Jane looks at you the way she looked at you six years ago. Saturday is off. Captain Hocken says something to Officer Nordberg, and everybody laughs, and everybody freezes, smiling, holding it — except the lion, who is still going."
|
||||
},
|
||||
{
|
||||
"name": "lion",
|
||||
"sprite": "lion",
|
||||
"x": 140,
|
||||
"y": 152,
|
||||
"look": "There is a lion on Sixth Street. It has been on Sixth Street since eleven forty. Animal Control open at nine.",
|
||||
"synonyms": "cat animal beast",
|
||||
"visible_flag": "lion_loose",
|
||||
"kills": true,
|
||||
"use_text": "You approach the lion. Regulations do not cover the lion. Regulations have never once covered the lion."
|
||||
}
|
||||
]
|
||||
}
|
||||
71
cases/case-08-smell-of-fear/sprites.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"_comment": "Shared Act II sprites (ego, hocken, nordberg, jane, podium, champagne) are copied from case-07. The tool skips existing files, so only the new ones below are generated.",
|
||||
"meinheimer": {
|
||||
"h": 20,
|
||||
"prompt": "A small round elderly scientist with a white beard and spectacles sitting in a wheelchair, tweed jacket and bow tie, facing the viewer, kindly expression, full colour"
|
||||
},
|
||||
"hacker": {
|
||||
"h": 20,
|
||||
"prompt": "A small round elderly man with a white beard and spectacles sitting in a wheelchair, tweed jacket and bow tie, facing the viewer, shifty sideways glance and a smirk, full colour"
|
||||
},
|
||||
"hapsburg": {
|
||||
"h": 26,
|
||||
"prompt": "A tall silver haired 1990s oil executive facing the viewer, expensive navy double breasted suit, gold tie, tanned, smug confident smile, full colour"
|
||||
},
|
||||
"savage": {
|
||||
"h": 26,
|
||||
"prompt": "A gaunt hollow cheeked hitman facing the viewer, black leather jacket, lank hair, dead eyes, hands at his sides, full colour"
|
||||
},
|
||||
"piano": {
|
||||
"h": 22,
|
||||
"prompt": "An upright bar piano with a lit candle and a glass on top, seen from the side, dim warm lighting, full colour"
|
||||
},
|
||||
"bartop": {
|
||||
"h": 20,
|
||||
"prompt": "A dim wooden cocktail bar counter with bottles and a row of stools, seen from the front, warm moody lighting, full colour"
|
||||
},
|
||||
"tank": {
|
||||
"h": 26,
|
||||
"prompt": "A black armoured police SWAT assault vehicle with a battering ram on the front, seen from the side, full colour"
|
||||
},
|
||||
"lion": {
|
||||
"h": 18,
|
||||
"prompt": "A full grown male African lion standing in profile facing left, big mane, full body, full colour"
|
||||
},
|
||||
"mariachi": {
|
||||
"h": 26,
|
||||
"prompt": "A mariachi musician facing the viewer, black charro suit with silver trim, enormous wide brimmed sombrero, holding a guitar, full colour"
|
||||
},
|
||||
"nuke": {
|
||||
"h": 20,
|
||||
"prompt": "A sinister metal cylindrical nuclear device with a digital countdown timer and a thick black power cable trailing from it, seen from the side, full colour"
|
||||
},
|
||||
"clockbomb": {
|
||||
"h": 14,
|
||||
"prompt": "A wall mounted institutional clock with wires and a small block of explosive taped to the back of it, seen from the front, full colour"
|
||||
},
|
||||
"wheelchair": {
|
||||
"h": 16,
|
||||
"prompt": "An empty old fashioned wooden and chrome wheelchair, seen from the side, full colour"
|
||||
},
|
||||
"zoocage": {
|
||||
"h": 28,
|
||||
"prompt": "A zoo enclosure cage of heavy iron bars with the barred gate hanging wide open, seen from the front, full colour"
|
||||
},
|
||||
"firehose": {
|
||||
"h": 14,
|
||||
"prompt": "A coiled red fire hose on a wall mounted reel with a brass nozzle, seen from the front, full colour"
|
||||
},
|
||||
"blueprint": {
|
||||
"h": 12,
|
||||
"prompt": "A rolled and partly unrolled blue technical blueprint drawing, lying flat, single object, full colour"
|
||||
},
|
||||
"rubble": {
|
||||
"h": 13,
|
||||
"prompt": "A heap of smoking broken masonry rubble and splintered timber, seen from the side, full colour"
|
||||
},
|
||||
"crate": {
|
||||
"h": 16,
|
||||
"prompt": "A large wooden shipping crate with stencilled lettering, seen at a slight angle, dim warehouse lighting, full colour"
|
||||
}
|
||||
}
|
||||
BIN
cases/case-08-smell-of-fear/sprites/bartop.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
cases/case-08-smell-of-fear/sprites/blueprint.png
Normal file
|
After Width: | Height: | Size: 232 B |
BIN
cases/case-08-smell-of-fear/sprites/champagne.png
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
cases/case-08-smell-of-fear/sprites/clockbomb.png
Normal file
|
After Width: | Height: | Size: 349 B |
BIN
cases/case-08-smell-of-fear/sprites/crate.png
Normal file
|
After Width: | Height: | Size: 897 B |
BIN
cases/case-08-smell-of-fear/sprites/ego.png
Normal file
|
After Width: | Height: | Size: 813 B |
BIN
cases/case-08-smell-of-fear/sprites/firehose.png
Normal file
|
After Width: | Height: | Size: 446 B |
BIN
cases/case-08-smell-of-fear/sprites/hacker.png
Normal file
|
After Width: | Height: | Size: 776 B |
BIN
cases/case-08-smell-of-fear/sprites/hapsburg.png
Normal file
|
After Width: | Height: | Size: 720 B |
BIN
cases/case-08-smell-of-fear/sprites/hocken.png
Normal file
|
After Width: | Height: | Size: 659 B |
BIN
cases/case-08-smell-of-fear/sprites/jane.png
Normal file
|
After Width: | Height: | Size: 630 B |
BIN
cases/case-08-smell-of-fear/sprites/lion.png
Normal file
|
After Width: | Height: | Size: 779 B |
BIN
cases/case-08-smell-of-fear/sprites/mariachi.png
Normal file
|
After Width: | Height: | Size: 875 B |
BIN
cases/case-08-smell-of-fear/sprites/meinheimer.png
Normal file
|
After Width: | Height: | Size: 836 B |
BIN
cases/case-08-smell-of-fear/sprites/nordberg.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
cases/case-08-smell-of-fear/sprites/nuke.png
Normal file
|
After Width: | Height: | Size: 738 B |
BIN
cases/case-08-smell-of-fear/sprites/piano.png
Normal file
|
After Width: | Height: | Size: 702 B |
BIN
cases/case-08-smell-of-fear/sprites/podium.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
cases/case-08-smell-of-fear/sprites/rubble.png
Normal file
|
After Width: | Height: | Size: 594 B |
BIN
cases/case-08-smell-of-fear/sprites/savage.png
Normal file
|
After Width: | Height: | Size: 834 B |
BIN
cases/case-08-smell-of-fear/sprites/tank.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
cases/case-08-smell-of-fear/sprites/wheelchair.png
Normal file
|
After Width: | Height: | Size: 679 B |
BIN
cases/case-08-smell-of-fear/sprites/zoocage.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@ -3,27 +3,189 @@
|
||||
"enter_text": "Four armed men. Thirty hostages. One detective in a pinafore.",
|
||||
"music": 3,
|
||||
"strokes": [
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 60, "color": 1, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 58, "w": 160, "h": 3, "color": 11, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 61, "w": 160, "h": 107, "color": 8, "meaning": "Floor" } },
|
||||
{ "Rect": { "x": 0, "y": 96, "w": 160, "h": 1, "color": 7, "meaning": "None" } },
|
||||
{ "Rect": { "x": 96, "y": 8, "w": 56, "h": 44, "color": 0, "meaning": "None" } },
|
||||
{ "Rect": { "x": 100, "y": 12, "w": 48, "h": 3, "color": 11, "meaning": "None" } },
|
||||
{ "Rect": { "x": 100, "y": 20, "w": 32, "h": 3, "color": 3, "meaning": "None" } },
|
||||
{ "Rect": { "x": 100, "y": 28, "w": 40, "h": 3, "color": 3, "meaning": "None" } },
|
||||
{ "Rect": { "x": 100, "y": 36, "w": 24, "h": 3, "color": 3, "meaning": "None" } },
|
||||
{ "Rect": { "x": 10, "y": 10, "w": 46, "h": 34, "color": 9, "meaning": "None" } },
|
||||
{ "Rect": { "x": 14, "y": 14, "w": 38, "h": 26, "color": 1, "meaning": "None" } },
|
||||
{ "Rect": { "x": 20, "y": 20, "w": 26, "h": 6, "color": 15, "meaning": "None" } },
|
||||
{ "Rect": { "x": 20, "y": 30, "w": 18, "h": 4, "color": 11, "meaning": "None" } },
|
||||
{ "Rect": { "x": 0, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 168,
|
||||
"color": 8,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 160,
|
||||
"h": 60,
|
||||
"color": 1,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 58,
|
||||
"w": 160,
|
||||
"h": 3,
|
||||
"color": 11,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 61,
|
||||
"w": 160,
|
||||
"h": 107,
|
||||
"color": 8,
|
||||
"meaning": "Floor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 96,
|
||||
"w": 160,
|
||||
"h": 1,
|
||||
"color": 7,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 96,
|
||||
"y": 8,
|
||||
"w": 56,
|
||||
"h": 44,
|
||||
"color": 0,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 12,
|
||||
"w": 48,
|
||||
"h": 3,
|
||||
"color": 11,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 20,
|
||||
"w": 32,
|
||||
"h": 3,
|
||||
"color": 3,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 28,
|
||||
"w": 40,
|
||||
"h": 3,
|
||||
"color": 3,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 100,
|
||||
"y": 36,
|
||||
"w": 24,
|
||||
"h": 3,
|
||||
"color": 3,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
"w": 46,
|
||||
"h": 34,
|
||||
"color": 9,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 14,
|
||||
"y": 14,
|
||||
"w": 38,
|
||||
"h": 26,
|
||||
"color": 1,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 20,
|
||||
"y": 20,
|
||||
"w": 26,
|
||||
"h": 6,
|
||||
"color": 15,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 20,
|
||||
"y": 30,
|
||||
"w": 18,
|
||||
"h": 4,
|
||||
"color": 11,
|
||||
"meaning": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 4,
|
||||
"h": 168,
|
||||
"color": 0,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Rect": {
|
||||
"x": 0,
|
||||
"y": 164,
|
||||
"w": 160,
|
||||
"h": 4,
|
||||
"color": 0,
|
||||
"meaning": "Wall"
|
||||
}
|
||||
}
|
||||
],
|
||||
"spawn": [
|
||||
24,
|
||||
140
|
||||
],
|
||||
"exits": [
|
||||
null,
|
||||
1,
|
||||
null,
|
||||
null
|
||||
],
|
||||
"exit_flags": [
|
||||
"",
|
||||
"knows_theft",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"exit_blocked": [
|
||||
"",
|
||||
"Something happened in this bank that was not the robbery. Find out what.",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"spawn": [24, 140],
|
||||
"exits": [null, 1, null, null],
|
||||
"exit_flags": ["", "knows_theft", "", ""],
|
||||
"exit_blocked": ["", "Something happened in this bank that was not the robbery. Find out what.", "", ""],
|
||||
"objects": [
|
||||
{
|
||||
"name": "wig",
|
||||
@ -32,7 +194,7 @@
|
||||
"y": 122,
|
||||
"look": "A blonde wig with two pigtails and red ribbons. Requisition 4471-B: ONE (1) SCHOOLGIRL, COMPLETE. The requisition was approved. Somebody in this department approved this.",
|
||||
"synonyms": "disguise pigtails hair costume",
|
||||
"takeable": true,
|
||||
"takeable": false,
|
||||
"points": 5,
|
||||
"sets_flag": "disguised",
|
||||
"use_text": "You put on the wig."
|
||||
@ -70,4 +232,4 @@
|
||||
"use_text": "You buy a hot dog. It is, and you say this as a man who has eaten in forty countries, the finest hot dog you have ever had in your life. You will think about it on your deathbed. The vendor will never know."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
35
play.sh
@ -1,9 +1,38 @@
|
||||
#!/bin/sh
|
||||
# Play a case. No arg = the first one.
|
||||
# Play a case: ./play.sh list them and pick
|
||||
# ./play.sh 07 by number
|
||||
# ./play.sh cases/case-07-from-the-files
|
||||
# ponytail: MRPGI lives next door; no vendoring, no submodule.
|
||||
set -e
|
||||
ENGINE="${MRPGI:-$HOME/Documents/MRPGI}"
|
||||
CASE="${1:-$(ls -d cases/*/ | head -1)}"
|
||||
[ -f "$CASE/game.json" ] || { echo "no game.json in $CASE"; ls -d cases/*/; exit 1; }
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Menu goes to stderr so the caller only captures the reply.
|
||||
pick() {
|
||||
{
|
||||
echo "POLICE SQUAD QUEST — cases:"
|
||||
echo
|
||||
for d in cases/*/; do
|
||||
n=$(basename "$d" | sed 's/^case-//; s/-.*//')
|
||||
t=$(sed -n 's/.*"name"[^"]*"\([^"]*\)".*/\1/p' "$d/game.json" 2>/dev/null | head -1)
|
||||
printf ' %s %s\n' "$n" "${t:-$(basename "$d")}"
|
||||
done
|
||||
echo
|
||||
printf 'case number: '
|
||||
} >&2
|
||||
read -r reply
|
||||
echo "$reply"
|
||||
}
|
||||
|
||||
arg="$1"
|
||||
[ -n "$arg" ] || arg=$(pick)
|
||||
|
||||
# A bare number selects by case prefix; anything else is treated as a path.
|
||||
case "$arg" in
|
||||
[0-9]*) CASE=$(ls -d cases/case-"$arg"-*/ 2>/dev/null | head -1) ;;
|
||||
*) CASE="$arg" ;;
|
||||
esac
|
||||
|
||||
[ -n "$CASE" ] && [ -f "$CASE/game.json" ] || { echo "no such case: $arg"; exit 1; }
|
||||
exec cargo run --release -p mrpgi --manifest-path "$ENGINE/Cargo.toml" -- \
|
||||
--game "$(cd "$CASE" && pwd)"
|
||||
|
||||
100
tools/lint.py
Normal file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check a case for the authoring traps this project keeps hitting.
|
||||
|
||||
python3 tools/lint.py # every case
|
||||
python3 tools/lint.py cases/case-08-... # one
|
||||
|
||||
Each rule here exists because it cost a debugging session at least once.
|
||||
Exit status is 1 if anything is reported.
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
ARROW = ["N", "E", "S", "W"]
|
||||
|
||||
|
||||
def check(case):
|
||||
bad = []
|
||||
say = lambda m: bad.append(f"{os.path.basename(case)}: {m}")
|
||||
man = json.load(open(os.path.join(case, "game.json")))
|
||||
sprites = {os.path.basename(p)[:-4]
|
||||
for p in glob.glob(os.path.join(case, "sprites", "*.png"))}
|
||||
|
||||
rooms = {}
|
||||
for p in sorted(glob.glob(os.path.join(case, "rooms", "room*.json"))):
|
||||
rooms[int(os.path.basename(p)[4:-5])] = json.load(open(p))
|
||||
|
||||
set_flags, need_flags, total = set(man.get("flags", {})), set(), 0
|
||||
# A manifest flag is only "set" if something later flips it true.
|
||||
set_flags = set()
|
||||
|
||||
for n, doc in rooms.items():
|
||||
names = [o["name"] for o in doc.get("objects", [])]
|
||||
for o in doc.get("objects", []):
|
||||
where = f"room{n}.{o['name']}"
|
||||
if o.get("sprite") not in sprites:
|
||||
say(f"{where}: sprite '{o.get('sprite')}' missing from sprites/")
|
||||
# THE recurring one: sets_flag fires on `use`, never on `take`.
|
||||
if o.get("takeable") and o.get("sets_flag"):
|
||||
say(f"{where}: takeable AND sets_flag '{o['sets_flag']}' — "
|
||||
f"taking it will NOT set the flag. Make it non-takeable, "
|
||||
f"or gate on `needs` (which checks inventory).")
|
||||
if o.get("sets_flag"):
|
||||
set_flags.add(o["sets_flag"])
|
||||
for f in ("requires_flag", "visible_flag", "hidden_by_flag"):
|
||||
if o.get(f):
|
||||
need_flags.add(o[f])
|
||||
total += o.get("points", 0)
|
||||
# The parser matches loosely, so one name must not shadow another.
|
||||
for other in names:
|
||||
if other != o["name"] and other.startswith(o["name"]):
|
||||
say(f"room{n}: '{o['name']}' is a prefix of '{other}' — "
|
||||
f"the parser will match the wrong object.")
|
||||
for i, node in enumerate(o.get("dialogue", [])):
|
||||
for c in node.get("choices", []):
|
||||
if c.get("sets_flag"):
|
||||
set_flags.add(c["sets_flag"])
|
||||
if c.get("requires_flag"):
|
||||
need_flags.add(c["requires_flag"])
|
||||
total += c.get("points", 0)
|
||||
if not (-1 <= c["goto"] < len(o["dialogue"])):
|
||||
say(f"{where}: dialogue node {i} goto={c['goto']} "
|
||||
f"out of range (0..{len(o['dialogue']) - 1} or -1)")
|
||||
|
||||
for d, tgt in enumerate(doc.get("exits", [None] * 4)):
|
||||
if tgt is not None and tgt not in rooms:
|
||||
say(f"room{n}: exit {ARROW[d]} -> room{tgt}, which does not exist")
|
||||
for d, f in enumerate(doc.get("exit_flags", [""] * 4)):
|
||||
if f:
|
||||
need_flags.add(f)
|
||||
if doc.get("exits", [None] * 4)[d] is None:
|
||||
say(f"room{n}: exit_flags[{ARROW[d]}]='{f}' but no exit there")
|
||||
|
||||
for f in sorted(need_flags - set_flags):
|
||||
say(f"flag '{f}' is required somewhere but nothing ever sets it")
|
||||
# Alternative dialogue branches that award the same flag inflate this sum
|
||||
# (the flag latch makes each award once-only), so only a SHORTFALL is a bug:
|
||||
# it means max_score is unreachable.
|
||||
if man.get("max_score") and total < man["max_score"]:
|
||||
say(f"only {total} points authored but max_score is "
|
||||
f"{man['max_score']} — the target is unreachable")
|
||||
return bad
|
||||
|
||||
|
||||
def main():
|
||||
cases = sys.argv[1:] or sorted(glob.glob("cases/*/"))
|
||||
bad = []
|
||||
for c in cases:
|
||||
c = c.rstrip("/")
|
||||
if os.path.exists(os.path.join(c, "game.json")):
|
||||
bad += check(c)
|
||||
for b in bad:
|
||||
print(" " + b)
|
||||
print(f"{len(bad)} issue(s) across {len(cases)} case(s)")
|
||||
sys.exit(1 if bad else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||