Police Squad Quest: research, design bible, and Case One playable
A Sierra-style adventure in the Police Squad!/Naked Gun universe, on MRPGI — which reimplements AGI, the engine Sierra shipped Police Quest on. The premise: Police Quest's pedantic procedure x Police Squad!'s farce. The game grades you on correct procedure with a straight face, and the reward for perfect procedure is catastrophe anyway. Per John, the famous Police Quest II bugs are shipped deliberately as canon gags. - RESEARCH.md — all six Police Squad! episodes, all four films (incl. Neeson's Frank Drebin Jr, 2025), the recurring cast, and the ZAZ comedy grammar. - DESIGN.md — the joke engine, the two Drebins across three eras (Drebin Sr's 1982 cases render in a monochrome EGA subset for the 1950s M Squad look), an eleven-case campaign, and the PQ2 glitch-to-feature table. - cases/case-01-substantial-gift — playable. Five rooms, 100 points, full win path verified headlessly at 95/100 plus four authored Sierra deaths. Procedure gates, dusting for latents, and Miranda rights in the correct order are all pure JSON; no engine features were invented for it. - tools/sprites.py — all 24 sprites generated locally on MODELBEAST (flux_local -> bg_remove_local -> trim/downscale), reproducible by seed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
255
DESIGN.md
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
# Police Squad Quest — design bible
|
||||||
|
|
||||||
|
A Sierra-style adventure game in the *Police Squad!* / *Naked Gun* universe,
|
||||||
|
built on [MRPGI](../MRPGI). Source research: [RESEARCH.md](RESEARCH.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The joke engine
|
||||||
|
|
||||||
|
The premise isn't "a funny adventure game". It's a specific collision:
|
||||||
|
|
||||||
|
> **Sierra's *Police Quest* (1987) × ZAZ's *Police Squad!* (1982).**
|
||||||
|
|
||||||
|
*Police Quest* was designed by Jim Walls, a real ex-cop, and was famously
|
||||||
|
**pedantic**: you had to walk around your car and check the tyres, radio in
|
||||||
|
correctly, draw your weapon *before* firing (or you shot yourself in the leg),
|
||||||
|
and read the Miranda rights in the right order. Get the procedure wrong → you
|
||||||
|
die. It is the single most self-serious game Sierra ever shipped.
|
||||||
|
|
||||||
|
*Police Squad!* is the same subject matter played as total farce.
|
||||||
|
|
||||||
|
**So the game grades you on procedure with a completely straight face, and the
|
||||||
|
reward for perfect procedure is catastrophe anyway.** That's the machine. It's
|
||||||
|
mechanical, not just written — the player *performs* the joke rather than
|
||||||
|
watching it.
|
||||||
|
|
||||||
|
Three rules fall out of that:
|
||||||
|
|
||||||
|
1. **The game never winks.** All system text is flat police-procedural prose.
|
||||||
|
Score readouts, death cards, refusals — deadpan. The comedy is what the flat
|
||||||
|
text is describing. (See RESEARCH.md §4.)
|
||||||
|
2. **Correct procedure is mandatory and useless.** You *must* do it. It never
|
||||||
|
helps.
|
||||||
|
3. **The parser is Frank's brain.** A text parser is a literal-minded machine,
|
||||||
|
and literalism is ZAZ's core joke. `look` answers take things at face value.
|
||||||
|
|
||||||
|
### Worked example — the opening of Case One
|
||||||
|
|
||||||
|
```
|
||||||
|
> take badge
|
||||||
|
Taken. (+2)
|
||||||
|
> east
|
||||||
|
You have not signed for a sidearm. Regulations are regulations.
|
||||||
|
> take revolver
|
||||||
|
Taken. (+2)
|
||||||
|
> east
|
||||||
|
```
|
||||||
|
Two points for procedure. Then you get to the crime scene and the body is gone
|
||||||
|
because Nordberg filed it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The two Drebins, and the three eras
|
||||||
|
|
||||||
|
John's instinct that Frank Drebin Sr should be "a 1950s style cop" is exactly
|
||||||
|
right, and it's supported by the source: *Police Squad!* was a straight parody of
|
||||||
|
***M Squad*** (1957–60) and *Felony Squad*. The 2025 film then makes Neeson's
|
||||||
|
Frank Drebin **Jr.** canon — and gives Sr. a supernatural cameo as an owl. A
|
||||||
|
two-Drebin game is the franchise's own structure, not a liberty.
|
||||||
|
|
||||||
|
| Act | Who | Look | Palette |
|
||||||
|
|-----|-----|------|---------|
|
||||||
|
| **I** | Frank Drebin **Sr** — the TV series | 1950s hardboiled cop show, shot on videotape | EGA **0 / 7 / 8 / 15** + brown ego. Monochrome. |
|
||||||
|
| **II** | Frank Drebin **Sr** — the films | 1988–94 blockbuster | Full 16-colour EGA |
|
||||||
|
| **III** | Frank Drebin **Jr** (Neeson) — 2025 | modern LA, tech-money | Full EGA, cold blues/cyans |
|
||||||
|
|
||||||
|
**The palette restriction is free and does all the work.** MRPGI is EGA anyway;
|
||||||
|
Act I simply doesn't use the other twelve colours, so the era switch is visible
|
||||||
|
the instant a case loads. Act III's owl is the transition device: Sr. shows up
|
||||||
|
to help Jr., which is canon and also lets both Drebins share a screen.
|
||||||
|
|
||||||
|
**Drebin Jr's homage** (John's specific ask): Neeson's register is *sincerity*
|
||||||
|
where Nielsen's was obliviousness. Jr. isn't stupid — he's earnest, gravel-voiced,
|
||||||
|
and completely committed to a plan that makes no sense. Write Jr's `look` text as
|
||||||
|
genuinely moving. Then have it be about a vending machine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Structure — one folder per case
|
||||||
|
|
||||||
|
MRPGI loads any directory with `--game DIR`, so **each mission is a standalone
|
||||||
|
game folder** under `cases/`. No global room numbering, no hub-state plumbing,
|
||||||
|
cases can be built in any order and in parallel.
|
||||||
|
|
||||||
|
```
|
||||||
|
cases/case-01-substantial-gift/
|
||||||
|
game.json manifest: name, start_room, intro, extra verbs, flags, max_score
|
||||||
|
sprites.json MODELBEAST prompts (input to tools/sprites.py — not read by the engine)
|
||||||
|
sprites/*.png generated art; ego.png is the player
|
||||||
|
rooms/room*.json the case
|
||||||
|
```
|
||||||
|
|
||||||
|
`./play.sh` lists cases and runs one. Skipped: a hub world and cross-case save —
|
||||||
|
add when there are enough cases to want them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Mechanics — all of it is pure JSON
|
||||||
|
|
||||||
|
MRPGI already has every field this design needs. **No engine work.**
|
||||||
|
|
||||||
|
| Want | Field | Use |
|
||||||
|
|------|-------|-----|
|
||||||
|
| Procedure gate on a doorway | `exit_flags` + `exit_blocked` | "You have not signed for a sidearm." |
|
||||||
|
| Sierra death | `kills` on object or dialogue choice | The `use_text` is the death card |
|
||||||
|
| Police Quest scoring | `points` + manifest `max_score` | Points for *procedure*, not progress |
|
||||||
|
| Evidence that appears once found | `visible_flag` | Dusting reveals the print |
|
||||||
|
| Things that vanish | `hidden_by_flag` | The body Nordberg files |
|
||||||
|
| Item locks | `needs`, `consumes` | Warrant, evidence bag |
|
||||||
|
| Interrogation | `dialogue` + `requires_flag` / `sets_flag` | Miranda rights in order |
|
||||||
|
|
||||||
|
The one engine change made for this project is general-purpose and lives
|
||||||
|
upstream in MRPGI: **a game folder can now supply `sprites/ego.png`** (plus
|
||||||
|
optional `ego1.png` for the walk bob) to replace the default party robot. Ten
|
||||||
|
lines in `world.rs` + `state.rs`. Any MRPGI game benefits.
|
||||||
|
|
||||||
|
### Custom verbs
|
||||||
|
|
||||||
|
`game.json` merges extra synonyms into the verb table, and the AI-parser
|
||||||
|
whitelist is generated from the same table so they can't drift:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"verbs": [
|
||||||
|
{ "name": "use", "words": ["dust", "cuff", "book", "frisk", "radio", "mirandize", "arrest"] },
|
||||||
|
{ "name": "look", "words": ["investigate", "detect"] },
|
||||||
|
{ "name": "talk", "words": ["interrogate", "question"] }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The campaign
|
||||||
|
|
||||||
|
### Act I — Frank Drebin Sr, *Police Squad!* (monochrome)
|
||||||
|
|
||||||
|
Six cases, one per episode. Each opens with a **special guest star being murdered
|
||||||
|
in the title card** — a real object in the room whose only purpose is to die.
|
||||||
|
|
||||||
|
| # | Case | From | Hook | Procedure gag |
|
||||||
|
|---|------|------|------|---------------|
|
||||||
|
| 01 | **A Substantial Gift** | Ep 1 | Credit union manager shot; teller Sally Decker did it for orthodontist money and framed a customer | Sign out badge + sidearm; dust for prints; Mirandize in order — **built, see §6** |
|
||||||
|
| 02 | **Ring of Fear** | Ep 2 | Undercover as a boxing manager; a fighter's wife kidnapped to force a dive | You must maintain your cover identity *and* file correct overtime paperwork |
|
||||||
|
| 03 | **Rendezvous at Big Gulch** | Ep 3 | Protection racket; Frank and Norberg run a **locksmith shop** as a front | The front business is graded on customer service. Real locks. Real customers. |
|
||||||
|
| 04 | **Revenge and Remorse** | Ep 4 | Courthouse bombing, Eddie Casales framed | Bomb disposal by the manual, in order. The manual is wrong. |
|
||||||
|
| 05 | **The Butler Did It** | Ep 5 | Kidnapping at a rich man's birthday party | Chain of custody on a birthday cake |
|
||||||
|
| 06 | **Testimony of Evil** | Ep 6 | Comedian dies; nightclub drug ring | Undercover as a stand-up. You must actually pick punchlines. Dying on stage = a Sierra death card. |
|
||||||
|
|
||||||
|
### Act II — Frank Drebin Sr, the films (full colour)
|
||||||
|
|
||||||
|
| # | Case | From | Hook |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 07 | **From the Files** | *Naked Gun* (1988) | Nordberg shot on Ludwig's heroin boat; 24 hours to clear him before the Queen's visit. Ends at Anaheim Stadium where you **umpire a baseball game in order to legally frisk the players**, while Reggie Jackson is hypnotically activated. The umpire's strike-zone rules *are* the puzzle. |
|
||||||
|
| 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. |
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
| # | Case | From | Hook |
|
||||||
|
|---|------|------|------|
|
||||||
|
| 10 | **The P.L.O.T. Device** | *Naked Gun* (2025) | Open in the schoolgirl disguise during the bank robbery — you're the distraction and don't know it. Investigate Simon Davenport's "suicide" with Beth. Richard Cane's bunker plan. Police Squad is decommissioned mid-game and your badge stops opening doors. |
|
||||||
|
| 11 | **Ponzi-scheme.com Arena** | *Naked Gun* (2025) finale | New Year's ball drop. You lose your trousers retrieving the device — and the game keeps scoring you on uniform regulations. **Frank Drebin Sr arrives as an owl**: Act I's monochrome palette flies into Act III's colour one. |
|
||||||
|
|
||||||
|
### The prologue — "1958"
|
||||||
|
|
||||||
|
A short unlockable Act 0: **Drebin Sr as a rookie**, played completely straight
|
||||||
|
as *M Squad*. Same monochrome palette, no jokes in the system text at all — until
|
||||||
|
the last screen. It's the origin of the owl.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Case One — as built
|
||||||
|
|
||||||
|
**`cases/case-01-substantial-gift/`** — the vertical slice, playable now.
|
||||||
|
|
||||||
|
Five rooms, 100 points.
|
||||||
|
|
||||||
|
| Room | Place | Contains |
|
||||||
|
|------|-------|----------|
|
||||||
|
| 0 | Police Squad HQ, squad room | Hocken (briefing), Nordberg (injured), **Al** (74px — his head leaves the top of the screen and no one comments), your desk, badge, sidearm, case file |
|
||||||
|
| 1 | The street | Johnny the Snitch's shoeshine stand, the squad car |
|
||||||
|
| 2 | The credit union | The body, the vault, the teller counter, **Sally Decker** |
|
||||||
|
| 3 | The crime lab | Ted Olson and the microscope |
|
||||||
|
| 4 | The orthodontist's | The dental chair, the braces cast, the arrest |
|
||||||
|
|
||||||
|
**The chain:** sign out badge + revolver (procedure gate on the HQ exit) → read
|
||||||
|
the case file → crime scene → dust the counter for prints → take the evidence bag
|
||||||
|
→ lab: Olson matches the print → the orthodontist's bill names Sally → return and
|
||||||
|
Mirandize her **in the correct order** (wrong order = she walks and you get a
|
||||||
|
Sierra death card from Hocken, which is not a real death, which is the joke) →
|
||||||
|
arrest.
|
||||||
|
|
||||||
|
**Running gags wired in as mechanics:**
|
||||||
|
- **Al** — a 74px sprite in a 168px room. Placed so his head is above the frame.
|
||||||
|
His `look` text describes only his shoes.
|
||||||
|
- **Johnny** — `talk johnny` costs a dollar and will answer *anything*. His
|
||||||
|
dialogue tree has more nodes about orthodontics and macroeconomics than the case.
|
||||||
|
- **Nordberg** — appears in every room in worse condition. Never mentioned.
|
||||||
|
- **The guest star** — dies in the title card. `look guest` in room 0 before he
|
||||||
|
dies, and he's already dead.
|
||||||
|
- **The freeze-frame ending** — the win text describes the cast holding still
|
||||||
|
while things audibly keep going wrong.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 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;
|
||||||
|
a player who has reads them as a love letter. **So we ship them deliberately.**
|
||||||
|
|
||||||
|
| Real PQ2 bug | In Police Squad Quest |
|
||||||
|
|---|---|
|
||||||
|
| Keys locked in the car, station doors lock behind you → **softlocked in the parking lot on screen one** | The squad car in Case One: *"The keys are locked inside, on the seat, in plain view, behind your own reflection."* Non-fatal. Nobody mentions it. |
|
||||||
|
| **Keith**, your partner, slacks off and smokes; driving back to the station makes him go do "paperwork" | **Nordberg.** Casts, traction, cigarette, assigned as your partner, has not stood up. His paperwork is a magazine. |
|
||||||
|
| Firing without raising the weapon → **you shoot yourself in the foot** | Hocken dialogue: *"Permission to test-fire my sidearm indoors, Captain."* → *"Granted."* → Sierra death. An optional trap, exactly as Sierra used it. |
|
||||||
|
| Holding a direction while drawing → **hands and gun detach and float above your head** | Act II gag: a `raise` verb that reports the weapon is now above the ceiling, and stays there for the rest of the case. |
|
||||||
|
| **"Headless chicken" pathfinding** — NPCs spiral endlessly around obstacles | Al, in Case One, *"continues walking in a tight, rapid circle around the water cooler, as he has all morning."* |
|
||||||
|
| **Metal detector only checks if you're facing up** — walk diagonally past it | Act III airport screen. The detector is correct and the regulation is correct and you go through diagonally. |
|
||||||
|
| **Waving a drawn gun at airport security** with no reaction until you cross a specific vector | Same screen. The guard reacts to geometry, not to guns. |
|
||||||
|
| **Air-walking** — keep the swimming animation on dry land | Act II, after the zoo. Frank swims across the car park. |
|
||||||
|
| **Levitating into the sky** off a transition | Act III finale — but it's the owl. |
|
||||||
|
| Typing **`halt`** resets the enemy's shot counter → infinite standoff | `halt` is already a verb in Case One's manifest. In the Act II standoff it works, forever, and the game never acknowledges why. |
|
||||||
|
| **Infinite score** by re-dusting the ashtray / re-questioning the amnesiac witness | Case One, dusting the counter: *"The department pays out on a lifted print once. You may dust this counter as many times as you like. It has been looked into."* |
|
||||||
|
|
||||||
|
The rule: **play the bug straight.** Never nudge the player. The comedy is that
|
||||||
|
the game is behaving exactly as documented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Art pipeline
|
||||||
|
|
||||||
|
`tools/sprites.py <case>` reads that case's `sprites.json` and generates every
|
||||||
|
missing sprite on the **MODELBEAST** farm (free, local, m3ultra):
|
||||||
|
|
||||||
|
```
|
||||||
|
flux_local (text→image, 512²) → bg_remove_local (RMBG-2.0 cutout) → Pillow
|
||||||
|
nearest-neighbour trim+downscale to the sprite's target height
|
||||||
|
```
|
||||||
|
|
||||||
|
Trimming to the alpha bounding box before scaling is what puts the subject's feet
|
||||||
|
on the bottom edge, which is how MRPGI positions and depth-sorts sprites. The
|
||||||
|
engine quantizes to EGA itself on load, so the script only owes it a small
|
||||||
|
transparent PNG.
|
||||||
|
|
||||||
|
A shared `STYLE` constant in the script enforces the Sierra look; per-sprite
|
||||||
|
prompts stay short. Act I prompts end in *"black and white monochrome grayscale
|
||||||
|
only"* — that one phrase is the whole 1950s era treatment.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift # fill gaps
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift --force # redo all
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift --only ego,al
|
||||||
|
```
|
||||||
|
|
||||||
|
Seeds are derived from the sprite name, so a rerun reproduces the same art.
|
||||||
68
README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# Police Squad Quest
|
||||||
|
|
||||||
|
A Sierra-style adventure game set in the *Police Squad!* / *Naked Gun* universe,
|
||||||
|
built on the [MRPGI](https://github.com/) engine (`~/Documents/MRPGI`) — which is
|
||||||
|
itself a reimplementation of AGI, the engine Sierra shipped *Police Quest* on.
|
||||||
|
|
||||||
|
> The game grades you on correct police procedure with a completely straight
|
||||||
|
> face. The reward for perfect procedure is catastrophe anyway.
|
||||||
|
|
||||||
|
- **[RESEARCH.md](RESEARCH.md)** — the source material: all six *Police Squad!*
|
||||||
|
episodes, all four films, the recurring cast, and the ZAZ comedy grammar.
|
||||||
|
- **[DESIGN.md](DESIGN.md)** — the game bible: the joke engine, the two Drebins,
|
||||||
|
the eleven-case campaign, and how it all maps onto MRPGI data.
|
||||||
|
|
||||||
|
## Play
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./play.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run -p mrpgi --manifest-path ~/Documents/MRPGI/Cargo.toml -- --game cases/case-01-substantial-gift
|
||||||
|
```
|
||||||
|
|
||||||
|
`Tab` switches between play and the room editor. Type `help` in play.
|
||||||
|
|
||||||
|
## What's here
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `cases/case-01-substantial-gift/` | **Case One — playable, 100 points.** Five rooms, full win path, four Sierra deaths. |
|
||||||
|
| `tools/sprites.py` | Generates every sprite on the MODELBEAST farm (free, local). |
|
||||||
|
| `RESEARCH.md` / `DESIGN.md` | Source research and the campaign bible. |
|
||||||
|
|
||||||
|
## Case One — A Substantial Gift
|
||||||
|
|
||||||
|
*Police Squad!* episode 1. A credit union manager is dead, the wrong man is in a
|
||||||
|
cell, and the teller has new braces.
|
||||||
|
|
||||||
|
Sign out your badge and sidearm (the door will not let you leave without them),
|
||||||
|
get briefed, tip Johnny, dust the counter, match the print, find the bill, and
|
||||||
|
Mirandize the suspect **in the correct order**. Get the order wrong and the
|
||||||
|
statement is suppressed, which is a Sierra death card, which is the joke.
|
||||||
|
|
||||||
|
## Regenerating art
|
||||||
|
|
||||||
|
Sprites are generated locally on MODELBEAST (`flux_local` → `bg_remove_local` →
|
||||||
|
downscale). Prompts live in each case's `sprites.json`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift # fill gaps
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift --force # redo all
|
||||||
|
MB_POOL=1 python3 tools/sprites.py cases/... # if the farm 401s
|
||||||
|
```
|
||||||
|
|
||||||
|
Seeds derive from the sprite name, so reruns reproduce the same art.
|
||||||
|
|
||||||
|
## Engine changes
|
||||||
|
|
||||||
|
Two small, general-purpose additions were made upstream in MRPGI for this
|
||||||
|
project — both benefit any game:
|
||||||
|
|
||||||
|
- `sprites/ego.png` in a game folder replaces the default party robot as the
|
||||||
|
player avatar (`ego1.png` optional second walk cel).
|
||||||
|
- `death_text` in `game.json` overrides the death banner, which was previously
|
||||||
|
hardcoded to another game's flavour text.
|
||||||
148
RESEARCH.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Source material — *Police Squad!* and *The Naked Gun*
|
||||||
|
|
||||||
|
Research notes behind the game design. Everything here is canon from the six TV
|
||||||
|
episodes and four films. Design decisions built on it live in [DESIGN.md](DESIGN.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The canon, in order
|
||||||
|
|
||||||
|
| # | Work | Year | Drebin | Villain | Notes |
|
||||||
|
|---|------|------|--------|---------|-------|
|
||||||
|
| — | *Police Squad!* (6 eps) | 1982 | Leslie Nielsen | one-off | ABC. Cancelled after 4 aired. |
|
||||||
|
| 1 | *The Naked Gun: From the Files of Police Squad!* | 1988 | Leslie Nielsen | Vincent Ludwig | Plot skeleton lifted from *Telefon* |
|
||||||
|
| 2 | *The Naked Gun 2½: The Smell of Fear* | 1991 | Leslie Nielsen | Quentin Hapsburg | Energy policy |
|
||||||
|
| 3 | *Naked Gun 33⅓: The Final Insult* | 1994 | Leslie Nielsen | Rocco Dillon | Drebin retired |
|
||||||
|
| 4 | *The Naked Gun* | 2025 | **Liam Neeson as Frank Drebin Jr.** | Richard Cane | 88% RT, Critics' Choice Best Comedy |
|
||||||
|
|
||||||
|
The 1982 series was created by Zucker–Abrahams–Zucker straight off *Airplane!*,
|
||||||
|
and was a direct parody of ***M Squad*** (1957–60, Lee Marvin) and *Felony Squad*
|
||||||
|
— 1950s hardboiled cop TV. **This matters:** the "1950s Frank Drebin Sr" idea
|
||||||
|
isn't an invention, it's the form the joke was originally cut from.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. *Police Squad!* — all six episodes
|
||||||
|
|
||||||
|
Each is a self-contained case. Perfect mission-sized units.
|
||||||
|
|
||||||
|
| Ep | Title | Guest star murdered in the titles | Case |
|
||||||
|
|----|-------|-----------------------------------|------|
|
||||||
|
| 1 | A Substantial Gift (The Broken Promise) | Lorne Greene | Credit union manager shot. Teller **Sally Decker** did it — stealing to pay orthodontist bills — and framed a customer. |
|
||||||
|
| 2 | Ring of Fear (A Dangerous Assignment) | Georg Stanford Brown | Boxing corruption. Frank goes undercover as a fight manager; a boxer's wife is kidnapped to force a dive. |
|
||||||
|
| 3 | Rendezvous at Big Gulch (Terror in the Neighborhood) | Florence Henderson | Protection racket. Frank and Norberg open a **locksmith shop** as a front. |
|
||||||
|
| 4 | Revenge and Remorse (The Guilty Alibi) | William Shatner, Dr. Joyce Brothers | Courthouse bombing; **Eddie Casales** framed. |
|
||||||
|
| 5 | The Butler Did It (A Bird in the Hand) | Robert Goulet, Tommy Lasorda | Kidnapping of a businessman's daughter during a birthday party. |
|
||||||
|
| 6 | Testimony of Evil (Dead Men Don't Laugh) | William Conrad, Dick Clark | A comedian dies; Frank infiltrates a nightclub drug ring. |
|
||||||
|
|
||||||
|
*(Ep 5's guest was originally John Belushi, replaced after his death.)*
|
||||||
|
|
||||||
|
### The recurring cast
|
||||||
|
|
||||||
|
- **Det. Lt. Frank Drebin** (Leslie Nielsen) — narrates in hardboiled voiceover
|
||||||
|
while doing something idiotic on screen.
|
||||||
|
- **Capt. Ed Hocken** (Alan North; George Kennedy in the films) — the briefing.
|
||||||
|
- **Officer Nordberg** (Peter Lupus in the series, "Norberg"; O. J. Simpson in
|
||||||
|
the films) — exists to be injured.
|
||||||
|
- **Ted Olson** (Ed Williams) — lab scientist. Explains forensics while doing
|
||||||
|
something wildly unsuitable involving children present in the lab.
|
||||||
|
- **Johnny the Snitch** (William Duell) — shoeshine man. Knows *everything*.
|
||||||
|
- **Al** (Tiny Ron Taylor) — an officer so tall his head is permanently above
|
||||||
|
the top of frame. Never commented on.
|
||||||
|
- **Rex Hamilton as Abraham Lincoln** — credited in the titles, never in an episode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The films — plot beats worth stealing
|
||||||
|
|
||||||
|
### *The Naked Gun* (1988)
|
||||||
|
Beirut cold open: Drebin single-handedly breaks up a summit of America's enemies
|
||||||
|
(Gorbachev, Gaddafi, Khomeini, Arafat, Castro, Idi Amin). Home in LA, **Nordberg**
|
||||||
|
is shot on a heroin boat run by shipping magnate **Vincent Ludwig**
|
||||||
|
(Ricardo Montalbán). Hocken gives Frank 24 hours to clear Nordberg before
|
||||||
|
**Queen Elizabeth II** visits. Frank visits Ludwig's office and accidentally
|
||||||
|
reveals Nordberg survived. Ludwig sets his assistant **Jane Spencer**
|
||||||
|
(Priscilla Presley) on Frank; they fall in love. Ludwig is being paid $20m by
|
||||||
|
**Pahpshmir** to kill the Queen using a **beeper that triggers post-hypnotic
|
||||||
|
suggestion** — anyone becomes an unwitting assassin. At the Queen's reception
|
||||||
|
Drebin mistakes a ceremonial musket for an attack, causes chaos, gets fired.
|
||||||
|
Climax at **Anaheim Stadium**: Frank sneaks in, becomes the **home plate umpire**
|
||||||
|
so he can frisk players, and Ludwig activates **Reggie Jackson**. Ludwig falls
|
||||||
|
from the stadium, is flattened by a steamroller, then trampled by a marching band.
|
||||||
|
|
||||||
|
### *The Naked Gun 2½* (1991)
|
||||||
|
Energy policy. **Dr. Albert Meinheimer** will recommend renewables; oil man
|
||||||
|
**Quentin Hapsburg** (Robert Goulet) swaps him for a lookalike, **Earl Hacker**.
|
||||||
|
Frank spots the fake because the double lacks Meinheimer's photographic memory.
|
||||||
|
Set pieces: the **Blue Note bar** where Frank drowns his sorrows; a SWAT tank
|
||||||
|
driven through a house and into the **zoo**, releasing animals; assassin
|
||||||
|
**Hector Savage** killed with a fire hose; the squad infiltrating the Press Club
|
||||||
|
dinner **dressed as a mariachi band**; a nuclear device disarmed by tripping over
|
||||||
|
its power cord. The escaped **lion** kills Hapsburg. Frank proposes to Jane.
|
||||||
|
|
||||||
|
### *Naked Gun 33⅓* (1994)
|
||||||
|
Frank is retired, in marriage counselling, and bored. Bomber **Rocco Dillon**
|
||||||
|
(Fred Ward) is in prison; Frank goes undercover as **"Nick the Slasher McGurk"**
|
||||||
|
to get into his cell, and escapes with him through a tunnel to Rocco's mother
|
||||||
|
**Muriel**. Rocco's plan: a bomb in the **Best Picture envelope** at the Academy
|
||||||
|
Awards. Famous sequence: Frank repeatedly giving samples at a **sperm bank** to
|
||||||
|
avoid being recognised by **Tanya Peters**.
|
||||||
|
|
||||||
|
### *The Naked Gun* (2025)
|
||||||
|
**Lt. Frank Drebin Jr.** (Liam Neeson), LAPD Police Squad. Opens with Frank
|
||||||
|
foiling bank robbers **while dressed as a schoolgirl** — unaware he's the
|
||||||
|
distraction for the real theft: the **P.L.O.T. Device** (Primordial Law of
|
||||||
|
Toughness). Tech billionaire **Richard Cane** (Danny Huston) intends to use it to
|
||||||
|
revert humanity to a primitive state, cull the population, and ride it out in a
|
||||||
|
bunker. Frank investigates the "suicide" of engineer Simon Davenport and teams up
|
||||||
|
with his sister, novelist **Beth Davenport** (Pamela Anderson). **Capt. Ed Hocken
|
||||||
|
Jr.** (Paul Walter Hauser) and **Chief Davis** (CCH Pounder) round out the squad;
|
||||||
|
henchman **Sig Gustafson** (Kevin Durand). Police Squad is decommissioned. The
|
||||||
|
climax is a New Year's ball drop at the **"Ponzi-scheme.com Arena"**; Frank loses
|
||||||
|
his trousers retrieving the device. **His dead father's spirit arrives as an owl**,
|
||||||
|
airlifts him, and blinds Cane with excrement. Ending: "Internal Affairs" turns out
|
||||||
|
to be a tropical resort.
|
||||||
|
|
||||||
|
**The father is canon.** The 2025 film explicitly makes Leslie Nielsen's Drebin
|
||||||
|
the Sr. to Neeson's Jr., and gives Sr. a literal supernatural cameo. A two-Drebin
|
||||||
|
game is not a liberty — it's the franchise's own structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The comedy grammar (the part that actually matters)
|
||||||
|
|
||||||
|
Plot is scaffolding; ZAZ jokes are built from a small number of repeatable
|
||||||
|
machines. These are what the game has to reproduce.
|
||||||
|
|
||||||
|
1. **Deadpan.** Nobody acknowledges anything is funny. Nielsen played it exactly
|
||||||
|
as Lee Marvin would have. **Never wink.**
|
||||||
|
2. **Literalism.** The joke is a word taken at face value.
|
||||||
|
*"Cigarette?" — "Yes, it is."* / *"Doctor" — "Yes?" — "Nurse" — "Yes?"*
|
||||||
|
3. **Background gags.** Foreground dialogue plays straight while something
|
||||||
|
catastrophic happens behind it, uncommented.
|
||||||
|
4. **Escalating property damage.** Frank parks, and something enormous is
|
||||||
|
destroyed off-screen with a crash.
|
||||||
|
5. **Literalised metaphor.** Someone says "he was in over his head" and there is
|
||||||
|
a man in a swimming pool.
|
||||||
|
6. **The narration/action gap.** Hardboiled voiceover over visibly pathetic action.
|
||||||
|
7. **Nordberg suffers.** Continuously, escalating, uncommented.
|
||||||
|
8. **Al's head is out of frame.** Nobody has ever mentioned it.
|
||||||
|
9. **Johnny knows everything.** Asked about a case, he also fluently explains
|
||||||
|
astrophysics, ballet, or macroeconomics — and everyone queues to ask him.
|
||||||
|
10. **The freeze-frame ending.** The cast laughs and holds still — while things
|
||||||
|
keep visibly moving and going wrong inside the "frozen" shot.
|
||||||
|
|
||||||
|
**Rule for writing this game:** the *game* must never think it's funny. Text stays
|
||||||
|
police-procedural and flat. The comedy is in what the flat text is describing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [Police Squad! — Wikipedia](https://en.wikipedia.org/wiki/Police_Squad!)
|
||||||
|
- [The Naked Gun (1988) — Wikipedia](https://en.wikipedia.org/wiki/The_Naked_Gun:_From_the_Files_of_Police_Squad!)
|
||||||
|
- [The Naked Gun 2½ — Wikipedia](https://en.wikipedia.org/wiki/The_Naked_Gun_2%C2%BD:_The_Smell_of_Fear)
|
||||||
|
- [Naked Gun 33⅓ — Wikipedia](https://en.wikipedia.org/wiki/Naked_Gun_33%E2%85%93:_The_Final_Insult)
|
||||||
|
- [The Naked Gun (2025) — Wikipedia](https://en.wikipedia.org/wiki/The_Naked_Gun_(2025_film))
|
||||||
|
- [Police Quest: In Pursuit of the Death Angel — Wikipedia](https://en.wikipedia.org/wiki/Police_Quest:_In_Pursuit_of_the_Death_Angel)
|
||||||
|
- [The Naked Gun Franchise — Fandom](https://thenakedgun.fandom.com/wiki/The_Naked_Gun_Franchise) *(402 on fetch; used for cross-checking names only)*
|
||||||
53
cases/case-01-substantial-gift/game.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "Police Squad Quest — Case One: A Substantial Gift",
|
||||||
|
"start_room": 0,
|
||||||
|
"max_score": 100,
|
||||||
|
"intro_text": "POLICE SQUAD. 8:04 AM. A credit union manager is dead and a customer is in a cell for it. My name is Drebin. Detective Lieutenant Frank Drebin, Police Squad. Sign out your badge and your sidearm before you leave this room. Regulations are regulations. Type 'help'.",
|
||||||
|
"verbs": [
|
||||||
|
{
|
||||||
|
"name": "use",
|
||||||
|
"words": [
|
||||||
|
"dust",
|
||||||
|
"cuff",
|
||||||
|
"book",
|
||||||
|
"frisk",
|
||||||
|
"radio",
|
||||||
|
"mirandize",
|
||||||
|
"arrest",
|
||||||
|
"raise",
|
||||||
|
"halt",
|
||||||
|
"sign",
|
||||||
|
"search",
|
||||||
|
"print"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "look",
|
||||||
|
"words": [
|
||||||
|
"investigate",
|
||||||
|
"detect",
|
||||||
|
"observe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "talk",
|
||||||
|
"words": [
|
||||||
|
"interrogate",
|
||||||
|
"question",
|
||||||
|
"brief"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"flags": {
|
||||||
|
"armed": false,
|
||||||
|
"briefed": false,
|
||||||
|
"print_found": false,
|
||||||
|
"print_matched": false,
|
||||||
|
"motive_found": false,
|
||||||
|
"miranda1": false,
|
||||||
|
"miranda2": false,
|
||||||
|
"miranda3": false,
|
||||||
|
"mirandized": false
|
||||||
|
},
|
||||||
|
"death_text": "† OFFICER DOWN. The day restarts at 8:04 AM. Regulations are regulations. †"
|
||||||
|
}
|
||||||
392
cases/case-01-substantial-gift/rooms/room0.json
Normal file
@ -0,0 +1,392 @@
|
|||||||
|
{
|
||||||
|
"name": "Police Squad HQ \u2014 Squad Room",
|
||||||
|
"enter_text": "The squad room. It smells of carbon paper and old coffee.",
|
||||||
|
"music": 3,
|
||||||
|
"strokes": [
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 160,
|
||||||
|
"h": 168,
|
||||||
|
"color": 7,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 160,
|
||||||
|
"h": 58,
|
||||||
|
"color": 8,
|
||||||
|
"meaning": "Wall"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 58,
|
||||||
|
"w": 160,
|
||||||
|
"h": 110,
|
||||||
|
"color": 7,
|
||||||
|
"meaning": "Floor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 55,
|
||||||
|
"w": 160,
|
||||||
|
"h": 3,
|
||||||
|
"color": 0,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 100,
|
||||||
|
"y": 12,
|
||||||
|
"w": 46,
|
||||||
|
"h": 32,
|
||||||
|
"color": 15,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 122,
|
||||||
|
"y": 12,
|
||||||
|
"w": 2,
|
||||||
|
"h": 32,
|
||||||
|
"color": 0,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 100,
|
||||||
|
"y": 26,
|
||||||
|
"w": 46,
|
||||||
|
"h": 2,
|
||||||
|
"color": 0,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 14,
|
||||||
|
"w": 42,
|
||||||
|
"h": 30,
|
||||||
|
"color": 15,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 18,
|
||||||
|
"w": 16,
|
||||||
|
"h": 3,
|
||||||
|
"color": 8,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 24,
|
||||||
|
"w": 24,
|
||||||
|
"h": 3,
|
||||||
|
"color": 8,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 30,
|
||||||
|
"w": 20,
|
||||||
|
"h": 3,
|
||||||
|
"color": 8,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 18,
|
||||||
|
"y": 36,
|
||||||
|
"w": 28,
|
||||||
|
"h": 3,
|
||||||
|
"color": 8,
|
||||||
|
"meaning": "None"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 5,
|
||||||
|
"h": 168,
|
||||||
|
"color": 0,
|
||||||
|
"meaning": "Wall"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Rect": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 163,
|
||||||
|
"w": 160,
|
||||||
|
"h": 5,
|
||||||
|
"color": 0,
|
||||||
|
"meaning": "Wall"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spawn": [
|
||||||
|
86,
|
||||||
|
148
|
||||||
|
],
|
||||||
|
"exits": [
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
],
|
||||||
|
"exit_flags": [
|
||||||
|
"",
|
||||||
|
"briefed",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"exit_blocked": [
|
||||||
|
"",
|
||||||
|
"You have not been briefed. Captain Hocken is standing four feet away, waiting to brief you.",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"name": "locker",
|
||||||
|
"sprite": "filecabinet",
|
||||||
|
"x": 18,
|
||||||
|
"y": 106,
|
||||||
|
"look": "The arms locker. A steel cabinet with a sign-out book chained to it. The chain is longer than the book.",
|
||||||
|
"synonyms": "cabinet armory arms sheet book signout",
|
||||||
|
"needs": "badge",
|
||||||
|
"use_text": "You sign for a sidearm. The book is countersigned by Officer Nordberg, in crayon.",
|
||||||
|
"sets_flag": "armed",
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "revolver",
|
||||||
|
"sprite": "revolver",
|
||||||
|
"x": 22,
|
||||||
|
"y": 120,
|
||||||
|
"look": "A .38 service revolver. Standard issue. Six rounds, of which regulations permit you to fire zero.",
|
||||||
|
"synonyms": "gun sidearm weapon pistol",
|
||||||
|
"takeable": true,
|
||||||
|
"visible_flag": "armed",
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "desk",
|
||||||
|
"sprite": "desk",
|
||||||
|
"x": 50,
|
||||||
|
"y": 142,
|
||||||
|
"look": "Your desk. Eleven years at this desk. The drawer still sticks.",
|
||||||
|
"synonyms": "table",
|
||||||
|
"use_text": "The drawer sticks. You have filed a requisition about the drawer. The requisition is in the drawer."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "badge",
|
||||||
|
"sprite": "badge",
|
||||||
|
"x": 43,
|
||||||
|
"y": 128,
|
||||||
|
"look": "Detective's shield. Police Squad. The number has worn off, which has never once been a problem.",
|
||||||
|
"synonyms": "shield tin",
|
||||||
|
"takeable": true,
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "casefile",
|
||||||
|
"sprite": "casefile",
|
||||||
|
"x": 57,
|
||||||
|
"y": 128,
|
||||||
|
"look": "SIXTH STREET CREDIT UNION \u2014 HOMICIDE. Manager shot twice. $12,000 missing. One customer held: SKALANY, D. One teller interviewed: DECKER, S. The teller's statement is four pages. Nobody asked her four pages' worth of questions.",
|
||||||
|
"synonyms": "file folder report papers case",
|
||||||
|
"takeable": true,
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "phone",
|
||||||
|
"sprite": "phone",
|
||||||
|
"x": 30,
|
||||||
|
"y": 140,
|
||||||
|
"look": "A black rotary telephone. It has been ringing since you came in. Nobody has mentioned it.",
|
||||||
|
"synonyms": "telephone",
|
||||||
|
"use_text": "You answer it. A man asks whether you are satisfied with your current carpeting. You say you are. He hangs up. The phone keeps ringing."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "coffeepot",
|
||||||
|
"sprite": "coffeepot",
|
||||||
|
"x": 146,
|
||||||
|
"y": 110,
|
||||||
|
"look": "The squad room coffee. It has been on the hotplate since Tuesday. It is now technically a solid.",
|
||||||
|
"synonyms": "coffee pot percolator",
|
||||||
|
"use_text": "You pour a cup. It comes out in one piece. You put it back."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "al",
|
||||||
|
"sprite": "al",
|
||||||
|
"x": 80,
|
||||||
|
"y": 66,
|
||||||
|
"look": "Officer Al. You can see his shoes, his trousers to about the knee, and no more of him than that. He is describing something with his hands, somewhere above the ceiling. Everyone is nodding.",
|
||||||
|
"synonyms": "tall officer legs",
|
||||||
|
"use_text": "You shake his hand. You have to jump. He does not appear to notice, and continues walking in a tight, rapid circle around the water cooler, as he has all morning."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hocken",
|
||||||
|
"sprite": "hocken",
|
||||||
|
"x": 106,
|
||||||
|
"y": 126,
|
||||||
|
"look": "Captain Ed Hocken. Twenty-two years on the job. He has the look of a man about to say something about racquetball.",
|
||||||
|
"synonyms": "captain ed boss",
|
||||||
|
"dialogue": [
|
||||||
|
{
|
||||||
|
"says": "Hocken: Frank. Credit union on Sixth Street. Manager took two in the chest at eight this morning. We've got a customer in a cell for it and I don't like him for it.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "What have we got, Captain?",
|
||||||
|
"goto": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Who's the customer?",
|
||||||
|
"goto": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Captain, can I go?",
|
||||||
|
"goto": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Permission to test-fire my sidearm indoors, Captain.",
|
||||||
|
"goto": 5,
|
||||||
|
"kills": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "I'm signed out and ready, Captain.",
|
||||||
|
"goto": 3,
|
||||||
|
"requires_flag": "armed",
|
||||||
|
"sets_flag": "briefed",
|
||||||
|
"points": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Twelve thousand dollars gone out of the drawer. The teller says the customer did it. The customer says he was at the dentist. The dentist says he was at the dentist. Everybody was at the dentist, Frank.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "Somebody wasn't.",
|
||||||
|
"goto": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Name of Skalany. Clean record, pays his parking tickets, cried through the whole interview. I've seen guilty men cry, Frank. They cry differently. They cry like they're getting away with it.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "And Skalany?",
|
||||||
|
"goto": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Good. Stop and see Johnny on the way \u2014 he's been out on that corner since six. And Frank: the manager was a personal friend of mine. We played racquetball every Thursday for eleven years. I never beat him once. Not once in eleven years. Get me something.",
|
||||||
|
"choices": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Not without a badge and a sidearm, you can't. Sign for them. The locker's right there. It has been right there for eleven years, Frank.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "Right.",
|
||||||
|
"goto": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Granted. You draw and fire in a single motion, without first raising the weapon. Regulations, page 14, covers this exact circumstance in considerable detail. Officer Nordberg does not look up.",
|
||||||
|
"choices": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Hocken: Skalany cried like a man who'd been arrested. That's the wrong crying, Frank.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "Understood.",
|
||||||
|
"goto": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nordberg",
|
||||||
|
"sprite": "nordberg",
|
||||||
|
"x": 140,
|
||||||
|
"y": 154,
|
||||||
|
"look": "Officer Nordberg. Both arms in casts, one leg in traction, and a cigarette going. He is assigned to this case as your partner. He has not stood up yet.",
|
||||||
|
"synonyms": "norberg partner officer",
|
||||||
|
"use_text": "You ask Nordberg to come with you. He explains that he has paperwork. The paperwork is a magazine.",
|
||||||
|
"dialogue": [
|
||||||
|
{
|
||||||
|
"says": "Nordberg: Frank! You want me to come along? Because I could come along. I'd have to finish this first.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "What happened to you, Nordberg?",
|
||||||
|
"goto": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Finish what?",
|
||||||
|
"goto": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Stay here, Nordberg.",
|
||||||
|
"goto": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Nordberg: This? Filing. I was filing and the drawer was open. Then I was filing and the window was open.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "Both arms?",
|
||||||
|
"goto": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Nordberg: Paperwork. Real backlog. I'll catch you up.",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"text": "Sure, Nordberg.",
|
||||||
|
"goto": -1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Nordberg: You got it, Frank. I'll hold the fort. He lights a second cigarette off the first.",
|
||||||
|
"choices": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guest",
|
||||||
|
"sprite": "guest",
|
||||||
|
"x": 118,
|
||||||
|
"y": 150,
|
||||||
|
"look": "A distinguished silver-haired television actor in a tuxedo, arms spread in greeting, smiling broadly. He has been dead since the opening titles. Nobody has moved him and nobody is going to.",
|
||||||
|
"synonyms": "star actor tuxedo body man",
|
||||||
|
"use_text": "You check him for a pulse, as procedure requires. There isn't one, as the opening titles established. You note the time."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
108
cases/case-01-substantial-gift/rooms/room1.json
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"name": "Sixth Street",
|
||||||
|
"enter_text": "Sixth Street. The city is going about its business, which today includes a homicide.",
|
||||||
|
"music": 1,
|
||||||
|
"strokes": [
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 34, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 6, "y": 8, "w": 46, "h": 44, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 12, "y": 14, "w": 10, "h": 8, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 28, "y": 14, "w": 10, "h": 8, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 12, "y": 28, "w": 10, "h": 8, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 28, "y": 28, "w": 10, "h": 8, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 108, "y": 4, "w": 48, "h": 48, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 114, "y": 12, "w": 12, "h": 9, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 134, "y": 12, "w": 12, "h": 9, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 114, "y": 28, "w": 12, "h": 9, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 134, "y": 28, "w": 12, "h": 9, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 52, "w": 160, "h": 2, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 128, "w": 160, "h": 2, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 130, "w": 160, "h": 38, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 147, "w": 18, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 46, "y": 147, "w": 18, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 78, "y": 147, "w": 18, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 110, "y": 147, "w": 18, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 62, "y": 40, "w": 36, "h": 14, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 66, "y": 44, "w": 28, "h": 2, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 66, "y": 48, "w": 20, "h": 2, "color": 15, "meaning": "None" } }
|
||||||
|
],
|
||||||
|
"spawn": [30, 112],
|
||||||
|
"exits": [3, 2, 4, 0],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"name": "shoeshine",
|
||||||
|
"sprite": "shoeshine",
|
||||||
|
"x": 30,
|
||||||
|
"y": 100,
|
||||||
|
"look": "A shoeshine stand, bolted to the pavement outside the credit union. It has been here longer than the credit union.",
|
||||||
|
"synonyms": "stand chair box",
|
||||||
|
"use_text": "You put a foot up. Johnny goes to work without being asked. Your shoes have never once been dirty."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "johnny",
|
||||||
|
"sprite": "johnny",
|
||||||
|
"x": 50,
|
||||||
|
"y": 104,
|
||||||
|
"look": "Johnny. He works this corner. He knows things. There is no upper bound on the things Johnny knows.",
|
||||||
|
"synonyms": "snitch shoeshine informant boy",
|
||||||
|
"points": 5,
|
||||||
|
"sets_flag": "asked_johnny",
|
||||||
|
"use_text": "You put a dollar in Johnny's hand. It disappears so completely that you briefly doubt you had a dollar.",
|
||||||
|
"dialogue": [
|
||||||
|
{
|
||||||
|
"says": "Johnny: Detective. Sixth Street Credit Union. Manager's dead and you've got the wrong man in a cell.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "What do you know about the teller?", "goto": 1 },
|
||||||
|
{ "text": "What do you know about the customer?", "goto": 2 },
|
||||||
|
{ "text": "What do you know about orthodontics?", "goto": 3 },
|
||||||
|
{ "text": "What do you know about the money supply?", "goto": 4 },
|
||||||
|
{ "text": "Thanks, Johnny.", "goto": -1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: Decker. Sally Decker. Eleven years behind that window. Asked the manager for an advance three weeks ago. Got told no. Been smiling with her mouth closed ever since.",
|
||||||
|
"choices": [{ "text": "Closed?", "goto": 5 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: Skalany. Sells industrial flooring. Cries at commercials. Couldn't shoot a man if the man asked him to.",
|
||||||
|
"choices": [{ "text": "Anything else?", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: Fixed appliances run three to five thousand dollars, Detective, and insurance won't touch an adult. Eighteen to thirty months in the wire. The archwire's what costs — it's a nickel-titanium alloy, memory metal, developed for the space programme. Folks pay for the space programme twice: once in taxes, once in the mouth.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "Three to five thousand.", "goto": 6 },
|
||||||
|
{ "text": "How do you know that, Johnny?", "goto": 7 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: M1's been growing five and a quarter percent annualised, which the Board will tell you is disinflationary and which I will tell you is a man with his thumb on a scale describing the scale. Watch the velocity, Detective. Velocity never lies.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "Velocity never lies.", "goto": 0 },
|
||||||
|
{ "text": "How do you know that, Johnny?", "goto": 7 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: Closed. Eleven years of open, then three weeks of closed. A woman doesn't change her whole face for nothing.",
|
||||||
|
"choices": [{ "text": "No. She doesn't.", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: Three to five. And a fella's got to get it somewhere. He goes back to the shoe. A queue has formed behind you. The man at the front is holding a set of blueprints and looks like he needs them explained.",
|
||||||
|
"choices": [{ "text": "Somewhere.", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Johnny: I shine shoes, Detective.",
|
||||||
|
"choices": [{ "text": "Right.", "goto": 0 }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "squadcar",
|
||||||
|
"sprite": "squadcar",
|
||||||
|
"x": 112,
|
||||||
|
"y": 158,
|
||||||
|
"look": "Your unit. Regulations require a walk-around inspection before operating a department vehicle. You walk around it. It is still there.",
|
||||||
|
"synonyms": "car unit cruiser vehicle patrol",
|
||||||
|
"use_text": "The doors are locked. The keys are locked inside, on the seat, in plain view, behind your own reflection. Regulations require that you secure the vehicle. The vehicle is extremely secure."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
136
cases/case-01-substantial-gift/rooms/room2.json
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"name": "Sixth Street Credit Union",
|
||||||
|
"enter_text": "The credit union. Twelve thousand dollars left this room this morning and so did the manager.",
|
||||||
|
"music": 2,
|
||||||
|
"strokes": [
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 64, "color": 8, "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": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 8, "y": 10, "w": 40, "h": 30, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 16, "w": 28, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 24, "w": 20, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 32, "w": 24, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 60, "y": 8, "w": 40, "h": 16, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 64, "y": 12, "w": 32, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 64, "y": 18, "w": 22, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 88, "w": 160, "h": 1, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 156, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||||
|
],
|
||||||
|
"spawn": [20, 120],
|
||||||
|
"exits": [null, null, null, 1],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"name": "vault",
|
||||||
|
"sprite": "vault",
|
||||||
|
"x": 128,
|
||||||
|
"y": 100,
|
||||||
|
"look": "The vault. Eighteen inches of hardened steel, a time lock, and a combination known to two people. It was not touched. The money left through the window at the front, in a handbag.",
|
||||||
|
"synonyms": "safe door steel",
|
||||||
|
"use_text": "The vault is secure. The vault has always been secure. The vault is not the problem and never was."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "counter",
|
||||||
|
"sprite": "counter",
|
||||||
|
"x": 58,
|
||||||
|
"y": 112,
|
||||||
|
"look": "The teller counter. Brass bars, a small arched opening, and a laminate surface that a great many hands touch every day. Exactly one set of them matters.",
|
||||||
|
"synonyms": "window teller desk brass laminate",
|
||||||
|
"use_text": "You dust the counter for latents. A clean thumb comes up off the inside ledge — the customer's side is smeared to uselessness, but the inside ledge is where somebody leaned who had no reason to lean there. You lift it and bag it. (The department pays out on a lifted print once. You may dust this counter as many times as you like. It has been looked into.)",
|
||||||
|
"sets_flag": "print_found",
|
||||||
|
"points": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "evidence",
|
||||||
|
"sprite": "evidence",
|
||||||
|
"x": 74,
|
||||||
|
"y": 116,
|
||||||
|
"look": "A sealed evidence bag. One latent thumbprint, lifted from the inside ledge. Chain of custody begins with you and, regulations are clear, ends with you.",
|
||||||
|
"synonyms": "bag print thumbprint latent",
|
||||||
|
"takeable": true,
|
||||||
|
"visible_flag": "print_found",
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"sprite": "body",
|
||||||
|
"x": 92,
|
||||||
|
"y": 150,
|
||||||
|
"look": "The manager. Two entry wounds, close group, chest. Whoever fired this was steady, and standing near enough to be handed a withdrawal slip. Captain Hocken played racquetball with this man every Thursday for eleven years.",
|
||||||
|
"synonyms": "manager victim corpse deceased sheet",
|
||||||
|
"use_text": "You examine the body as procedure requires. Powder stippling on the shirt: the shot came from inside arm's reach. Nobody reaches through brass bars. Whoever shot him was already behind the counter.",
|
||||||
|
"sets_flag": "examined_body",
|
||||||
|
"points": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sally",
|
||||||
|
"sprite": "sally",
|
||||||
|
"x": 32,
|
||||||
|
"y": 128,
|
||||||
|
"look": "Sally Decker. Eleven years at that window. She is holding her handbag with both hands, the way you hold a thing you are thinking about. She smiles at you without opening her mouth.",
|
||||||
|
"synonyms": "decker teller woman miss",
|
||||||
|
"needs": "",
|
||||||
|
"requires_flag": "mirandized",
|
||||||
|
"use_text": "You place Sally Decker under arrest for the murder of the manager of the Sixth Street Credit Union. She goes quietly, and she goes with her mouth shut, which by now you understand is not the same as going quietly. Captain Hocken will not have to play racquetball on Thursday. He will not have to lose, either. Somewhere behind you, Officer Al says something that everybody laughs at, and everybody freezes, smiling, holding it — except the phone, which is still ringing, and Officer Nordberg, who is still on fire.",
|
||||||
|
"wins": true,
|
||||||
|
"points": 10,
|
||||||
|
"dialogue": [
|
||||||
|
{
|
||||||
|
"says": "Sally Decker: Oh — Detective. Have you found him? The man who did it? He was a big man. Bigger than you.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "Tell me about the customer.", "goto": 1 },
|
||||||
|
{ "text": "Tell me about your teeth, Miss Decker.", "goto": 2, "requires_flag": "motive_found" },
|
||||||
|
{ "text": "You have the right to remain silent.", "goto": 3, "requires_flag": "motive_found", "sets_flag": "miranda1", "points": 5 },
|
||||||
|
{ "text": "You're under arrest, Decker.", "goto": 9, "kills": true },
|
||||||
|
{ "text": "Nothing yet, Miss Decker.", "goto": -1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: He was at the window. He asked for a withdrawal slip. Then there was shouting, and I hid, and when I came up he was gone and Mr. Bowman was on the floor.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "Which window?", "goto": 8 },
|
||||||
|
{ "text": "You hid behind the counter.", "goto": 0 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: My teeth? She puts a hand flat over her mouth. The wire behind it is three weeks old and cost more than the car outside.",
|
||||||
|
"choices": [{ "text": "Four thousand dollars of new, Miss Decker.", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: ...I know my rights, Detective.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "Anything you say can and will be used against you in a court of law.", "goto": 4, "requires_flag": "miranda1", "sets_flag": "miranda2", "points": 5 },
|
||||||
|
{ "text": "Then you'll know the rest. Let's save some time.", "goto": 9, "kills": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: This is ridiculous. I have worked here eleven years.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "You have the right to an attorney.", "goto": 5, "requires_flag": "miranda2", "sets_flag": "miranda3", "points": 5 },
|
||||||
|
{ "text": "You have the right to an orthodontist.", "goto": 9, "kills": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: ...and if I can't afford one?",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "If you cannot afford an attorney, one will be appointed for you.", "goto": 6, "requires_flag": "miranda3", "sets_flag": "mirandized", "points": 5 },
|
||||||
|
{ "text": "You should have thought about that before the braces.", "goto": 9, "kills": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: Do you know what they cost, Detective? Four thousand dollars. He kept twelve in that drawer and he wouldn't advance me four. Eleven years I stood at that window and smiled at people. She stops holding the handbag with both hands.",
|
||||||
|
"choices": []
|
||||||
|
},
|
||||||
|
{ "says": "", "choices": [] },
|
||||||
|
{
|
||||||
|
"says": "Sally: The... the far one. Or the near one. It was very confusing, with all the shouting.",
|
||||||
|
"choices": [{ "text": "It usually is.", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Sally: I'd like to speak to my attorney. You have arrested a suspect without advising her of her rights, in the correct order, in full. The statement is suppressed. Skalany goes to trial for a murder he cried through. Captain Hocken never beats his friend at racquetball, and now he never will. You finish your career in the property room, where the coffee is better."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
72
cases/case-01-substantial-gift/rooms/room3.json
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "Police Squad Crime Lab",
|
||||||
|
"enter_text": "The crime lab. Ted Olson is mid-demonstration. There are, as always, several children present.",
|
||||||
|
"music": 4,
|
||||||
|
"strokes": [
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 60, "color": 8, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 58, "w": 160, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 61, "w": 160, "h": 107, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 10, "y": 10, "w": 30, "h": 38, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 14, "w": 22, "h": 4, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 22, "w": 22, "h": 4, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 30, "w": 22, "h": 4, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 14, "y": 38, "w": 22, "h": 4, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 52, "y": 14, "w": 96, "h": 30, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 56, "y": 18, "w": 40, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 56, "y": 26, "w": 60, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 56, "y": 34, "w": 34, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 84, "w": 160, "h": 5, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 89, "w": 160, "h": 2, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 156, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } }
|
||||||
|
],
|
||||||
|
"spawn": [80, 150],
|
||||||
|
"exits": [null, null, 1, null],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"name": "microscope",
|
||||||
|
"sprite": "microscope",
|
||||||
|
"x": 116,
|
||||||
|
"y": 84,
|
||||||
|
"look": "A comparison microscope. Two stages, one eyepiece: you put the known print on the left and the unknown print on the right, and the machine does not care whose feelings are involved.",
|
||||||
|
"synonyms": "scope comparison lens bench",
|
||||||
|
"needs": "evidence",
|
||||||
|
"requires_flag": "print_found",
|
||||||
|
"use_text": "Olson runs your latent against the staff cards. It takes four minutes. Olson: Left stage, staff print. Right stage, your latent off the inside ledge. Same thumb, Frank. Sixteen points of comparison — I stopped counting at sixteen because I got hungry. It's Decker, S. Teller. Eleven years. He adds, without looking up: there's alloy residue in the whorls. Nickel-titanium. You get that off orthodontic wire, if you've been putting your fingers in your mouth. New wire does that. New wire hurts.",
|
||||||
|
"sets_flag": "print_matched",
|
||||||
|
"points": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "olson",
|
||||||
|
"sprite": "olson",
|
||||||
|
"x": 62,
|
||||||
|
"y": 96,
|
||||||
|
"look": "Ted Olson, Police Squad crime lab. He is explaining muzzle velocity to a boy of about nine, using a live weapon and the boy.",
|
||||||
|
"synonyms": "ted scientist lab technician",
|
||||||
|
"use_text": "You ask Olson to be careful. He assures you that he is a professional, and hands the flamethrower to a smaller child.",
|
||||||
|
"dialogue": [
|
||||||
|
{
|
||||||
|
"says": "Olson: Frank! Perfect timing — I'm just showing the children what a shotgun does to a ballistics block. Kevin, hold this. No, closer. Closer than that, Kevin.",
|
||||||
|
"choices": [
|
||||||
|
{ "text": "I've got a latent for you, Ted.", "goto": 1 },
|
||||||
|
{ "text": "Should Kevin be holding that?", "goto": 2 },
|
||||||
|
{ "text": "Whose children are these, Ted?", "goto": 3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Olson: Then bag it on the comparison stage and I'll run it against the staff cards. Everybody who works a counter is printed, Frank. It's the one useful thing about banking.",
|
||||||
|
"choices": [{ "text": "Right.", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Olson: Absolutely not. Kevin is nine. He takes it off Kevin and gives it to a smaller child. Olson: Deborah is eleven.",
|
||||||
|
"choices": [{ "text": "That's not—", "goto": 0 }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"says": "Olson: The public's, Frank. It's a public lab.",
|
||||||
|
"choices": [{ "text": "Of course it is.", "goto": 0 }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
48
cases/case-01-substantial-gift/rooms/room4.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "Offices of Dr. Elias Frame, Orthodontist",
|
||||||
|
"enter_text": "An orthodontist's surgery. Somewhere behind a partition, a drill is running, and someone is being very brave about it.",
|
||||||
|
"music": 2,
|
||||||
|
"strokes": [
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 168, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 160, "h": 56, "color": 15, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 54, "w": 160, "h": 3, "color": 0, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 57, "w": 160, "h": 111, "color": 7, "meaning": "Floor" } },
|
||||||
|
{ "Rect": { "x": 12, "y": 10, "w": 44, "h": 32, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 16, "y": 14, "w": 36, "h": 4, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 16, "y": 22, "w": 26, "h": 4, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 16, "y": 30, "w": 32, "h": 4, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 96, "y": 8, "w": 52, "h": 38, "color": 8, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 102, "y": 14, "w": 40, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 102, "y": 22, "w": 28, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 102, "y": 30, "w": 34, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 102, "y": 38, "w": 20, "h": 3, "color": 15, "meaning": "None" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 156, "y": 0, "w": 4, "h": 168, "color": 0, "meaning": "Wall" } },
|
||||||
|
{ "Rect": { "x": 0, "y": 164, "w": 160, "h": 4, "color": 0, "meaning": "Wall" } }
|
||||||
|
],
|
||||||
|
"spawn": [80, 70],
|
||||||
|
"exits": [1, null, null, null],
|
||||||
|
"objects": [
|
||||||
|
{
|
||||||
|
"name": "dentalchair",
|
||||||
|
"sprite": "dentalchair",
|
||||||
|
"x": 46,
|
||||||
|
"y": 130,
|
||||||
|
"look": "The chair. Reclined, lamp swung aside, a paper bib still on the headrest. Whoever was in it this morning left in a hurry and left their bib.",
|
||||||
|
"synonyms": "chair seat lamp bib",
|
||||||
|
"use_text": "You sit in the chair. It is, against all expectation, the most comfortable thing you have sat in for eleven years. You get up before anybody sees."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "braces",
|
||||||
|
"sprite": "braces",
|
||||||
|
"x": 116,
|
||||||
|
"y": 118,
|
||||||
|
"look": "A plaster cast of an adult upper and lower jaw, wired for a full fixed appliance, sitting on the counter with a card taped to the stand. The card is a billing card.",
|
||||||
|
"synonyms": "cast mould teeth wire card bill ledger appliance",
|
||||||
|
"requires_flag": "print_matched",
|
||||||
|
"use_text": "You read the billing card. DECKER, S. — full upper and lower fixed appliance. Fitted three weeks ago. Balance outstanding: $4,000.00. Payment terms: THIRTY DAYS. Stamped twice in red: FINAL NOTICE. Twenty-eight days ago she asked her manager for an advance of four thousand dollars, and he kept twelve thousand in a drawer, and he said no.",
|
||||||
|
"sets_flag": "motive_found",
|
||||||
|
"points": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
98
cases/case-01-substantial-gift/sprites.json
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"ego": {
|
||||||
|
"h": 26,
|
||||||
|
"prompt": "A 1950s hardboiled police detective standing facing the viewer, tan trench coat, dark suit and skinny tie, square jaw, deadpan blank expression, arms at his sides, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"hocken": {
|
||||||
|
"h": 26,
|
||||||
|
"prompt": "A stout 1950s police captain facing the viewer, dark uniform, peaked cap, bushy moustache, arms folded, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"nordberg": {
|
||||||
|
"h": 24,
|
||||||
|
"prompt": "A police officer completely wrapped in white bandages and plaster casts, both arms in slings, leaning on a crutch, standing facing viewer, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"al": {
|
||||||
|
"h": 74,
|
||||||
|
"prompt": "An absurdly enormously tall thin police officer in uniform, extremely elongated stretched body and legs, standing straight facing viewer, full body, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"johnny": {
|
||||||
|
"h": 22,
|
||||||
|
"prompt": "A small wiry shoeshine man kneeling beside a wooden shoeshine box holding a brush, flat cap, waistcoat, grinning knowingly, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"olson": {
|
||||||
|
"h": 25,
|
||||||
|
"prompt": "A bespectacled 1950s laboratory scientist facing the viewer, white lab coat, bow tie, holding a test tube, cheerful expression, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"sally": {
|
||||||
|
"h": 24,
|
||||||
|
"prompt": "A nervous 1950s bank teller woman facing the viewer, neat blouse and pencil skirt, hair in a bun, clutching a handbag tightly, anxious expression, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"guest": {
|
||||||
|
"h": 25,
|
||||||
|
"prompt": "A distinguished silver haired 1950s television actor in a tuxedo facing the viewer, arms spread in a theatrical greeting, wide showbiz smile, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"h": 12,
|
||||||
|
"prompt": "A body covered by a white sheet lying flat on the floor seen from the side, with a chalk outline on the ground around it, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"desk": {
|
||||||
|
"h": 18,
|
||||||
|
"prompt": "A battered 1950s wooden office desk seen from the front, papers and a desk lamp on top, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"badge": {
|
||||||
|
"h": 10,
|
||||||
|
"prompt": "A police detective's metal shield badge lying flat, star shaped, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"revolver": {
|
||||||
|
"h": 9,
|
||||||
|
"prompt": "A snub nosed police service revolver seen from the side, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"casefile": {
|
||||||
|
"h": 11,
|
||||||
|
"prompt": "A manila cardboard case folder stuffed with papers, closed, seen at a slight angle, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"vault": {
|
||||||
|
"h": 30,
|
||||||
|
"prompt": "A heavy round steel bank vault door set in a wall, big spoked handle wheel, seen straight on, closed, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"counter": {
|
||||||
|
"h": 22,
|
||||||
|
"prompt": "A 1950s bank teller counter window with brass bars and a small arched opening, seen from the front, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"h": 12,
|
||||||
|
"prompt": "A small paper evidence bag sealed with tape and a label, standing upright, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"squadcar": {
|
||||||
|
"h": 26,
|
||||||
|
"prompt": "A 1950s American police patrol car seen from the side, roof light, rounded fenders, whitewall tyres, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"microscope": {
|
||||||
|
"h": 20,
|
||||||
|
"prompt": "A 1950s laboratory microscope on a bench, seen from the side, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"shoeshine": {
|
||||||
|
"h": 20,
|
||||||
|
"prompt": "A wooden shoeshine stand chair with a raised footrest and brass fittings, empty, seen from the side, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"coffeepot": {
|
||||||
|
"h": 13,
|
||||||
|
"prompt": "A percolator coffee pot on a hotplate with a mug beside it, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"filecabinet": {
|
||||||
|
"h": 24,
|
||||||
|
"prompt": "A tall grey steel four drawer office filing cabinet seen from the front, top drawer slightly open, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"h": 11,
|
||||||
|
"prompt": "A 1950s black rotary dial desk telephone with the handset resting on the cradle, seen from the side, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"braces": {
|
||||||
|
"h": 13,
|
||||||
|
"prompt": "A plaster dental cast of a set of teeth fitted with metal orthodontic braces, sitting on a small stand, single object, black and white monochrome grayscale only"
|
||||||
|
},
|
||||||
|
"dentalchair": {
|
||||||
|
"h": 26,
|
||||||
|
"prompt": "A 1950s dentist examination chair with an articulated lamp arm beside it, seen from the side, empty, black and white monochrome grayscale only"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
cases/case-01-substantial-gift/sprites/al.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
cases/case-01-substantial-gift/sprites/badge.png
Normal file
|
After Width: | Height: | Size: 245 B |
BIN
cases/case-01-substantial-gift/sprites/body.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
cases/case-01-substantial-gift/sprites/braces.png
Normal file
|
After Width: | Height: | Size: 386 B |
BIN
cases/case-01-substantial-gift/sprites/casefile.png
Normal file
|
After Width: | Height: | Size: 310 B |
BIN
cases/case-01-substantial-gift/sprites/coffeepot.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
cases/case-01-substantial-gift/sprites/counter.png
Normal file
|
After Width: | Height: | Size: 741 B |
BIN
cases/case-01-substantial-gift/sprites/dentalchair.png
Normal file
|
After Width: | Height: | Size: 949 B |
BIN
cases/case-01-substantial-gift/sprites/desk.png
Normal file
|
After Width: | Height: | Size: 686 B |
BIN
cases/case-01-substantial-gift/sprites/ego.png
Normal file
|
After Width: | Height: | Size: 713 B |
BIN
cases/case-01-substantial-gift/sprites/evidence.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
cases/case-01-substantial-gift/sprites/filecabinet.png
Normal file
|
After Width: | Height: | Size: 581 B |
BIN
cases/case-01-substantial-gift/sprites/guest.png
Normal file
|
After Width: | Height: | Size: 748 B |
BIN
cases/case-01-substantial-gift/sprites/hocken.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
cases/case-01-substantial-gift/sprites/johnny.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
cases/case-01-substantial-gift/sprites/microscope.png
Normal file
|
After Width: | Height: | Size: 523 B |
BIN
cases/case-01-substantial-gift/sprites/nordberg.png
Normal file
|
After Width: | Height: | Size: 841 B |
BIN
cases/case-01-substantial-gift/sprites/olson.png
Normal file
|
After Width: | Height: | Size: 698 B |
BIN
cases/case-01-substantial-gift/sprites/phone.png
Normal file
|
After Width: | Height: | Size: 332 B |
BIN
cases/case-01-substantial-gift/sprites/revolver.png
Normal file
|
After Width: | Height: | Size: 281 B |
BIN
cases/case-01-substantial-gift/sprites/sally.png
Normal file
|
After Width: | Height: | Size: 546 B |
BIN
cases/case-01-substantial-gift/sprites/shoeshine.png
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
cases/case-01-substantial-gift/sprites/squadcar.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
cases/case-01-substantial-gift/sprites/vault.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
9
play.sh
Executable file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Play a case. No arg = the first one.
|
||||||
|
# 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; }
|
||||||
|
exec cargo run --release -p mrpgi --manifest-path "$ENGINE/Cargo.toml" -- \
|
||||||
|
--game "$(cd "$CASE" && pwd)"
|
||||||
168
tools/sprites.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate MRPGI sprites on the MODELBEAST farm.
|
||||||
|
|
||||||
|
python3 tools/sprites.py cases/case-01-substantial-gift # all missing
|
||||||
|
python3 tools/sprites.py cases/... --only ego,johnny # named
|
||||||
|
python3 tools/sprites.py cases/... --force # redo existing
|
||||||
|
|
||||||
|
Chain per sprite: flux_local (text->image) -> bg_remove_local (RMBG-2.0 cutout)
|
||||||
|
-> Pillow nearest-neighbour downscale to the sprite's target height. MRPGI
|
||||||
|
quantizes to EGA itself on load (MANUAL SS14), so we only owe it a small
|
||||||
|
transparent PNG with the subject's feet on the bottom edge.
|
||||||
|
|
||||||
|
Sprite prompts live in sprites.json next to the game's game.json.
|
||||||
|
Token: MB_TOKEN env, else ~/Documents/backnforth/.env.
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import uuid
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
HOST = os.environ.get("MB_HOST", "http://100.89.131.57:8777")
|
||||||
|
# ponytail: guest token allows 4 active jobs; 3 leaves headroom for other users.
|
||||||
|
# Drop to MB_POOL=1 if the farm starts returning 401s (that's the cap, not auth).
|
||||||
|
POOL = int(os.environ.get("MB_POOL", "3"))
|
||||||
|
|
||||||
|
STYLE = (
|
||||||
|
"1980s Sierra AGI adventure game pixel art sprite, EGA 16-colour palette, "
|
||||||
|
"chunky low-resolution pixels, flat solid colour blocks, hard black outline, "
|
||||||
|
"single subject centred and complete, full body head to feet, no cropping, "
|
||||||
|
"plain flat white background, no text, no logo, no shadow, no border"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def token():
|
||||||
|
t = os.environ.get("MB_TOKEN")
|
||||||
|
if t:
|
||||||
|
return t
|
||||||
|
for line in open(os.path.expanduser("~/Documents/backnforth/.env")):
|
||||||
|
if line.startswith("MB_TOKEN="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
sys.exit("no MB_TOKEN")
|
||||||
|
|
||||||
|
|
||||||
|
def req(path, data=None, headers=None, raw=False):
|
||||||
|
h = {"Authorization": f"Bearer {token()}"}
|
||||||
|
h.update(headers or {})
|
||||||
|
body = urllib.request.urlopen(
|
||||||
|
urllib.request.Request(HOST + path, data=data, headers=h), timeout=300
|
||||||
|
).read()
|
||||||
|
if raw:
|
||||||
|
return body
|
||||||
|
# Job logs carry raw control chars that break json.loads (known farm quirk).
|
||||||
|
s = body.decode("utf-8", "replace")
|
||||||
|
return json.loads("".join(c if c >= " " or c == "\t" else " " for c in s))
|
||||||
|
|
||||||
|
|
||||||
|
def upload(name, blob):
|
||||||
|
b = uuid.uuid4().hex
|
||||||
|
body = (
|
||||||
|
f'--{b}\r\nContent-Disposition: form-data; name="file"; filename="{name}"\r\n'
|
||||||
|
f"Content-Type: image/png\r\n\r\n".encode()
|
||||||
|
+ blob
|
||||||
|
+ f"\r\n--{b}--\r\n".encode()
|
||||||
|
)
|
||||||
|
a = req("/api/assets", data=body,
|
||||||
|
headers={"Content-Type": f"multipart/form-data; boundary={b}"})
|
||||||
|
return a.get("id") or (a.get("items") or [a])[0].get("id")
|
||||||
|
|
||||||
|
|
||||||
|
def run_job(payload):
|
||||||
|
"""Submit, poll to completion, return the output asset's bytes."""
|
||||||
|
j = req("/api/jobs", data=json.dumps(payload).encode(),
|
||||||
|
headers={"Content-Type": "application/json"})
|
||||||
|
jid = j["id"]
|
||||||
|
for _ in range(150): # 150 * 4s = 10 min ceiling
|
||||||
|
time.sleep(4)
|
||||||
|
st = req(f"/api/jobs/{jid}").get("status")
|
||||||
|
if st in ("done", "error", "cancelled"):
|
||||||
|
break
|
||||||
|
if st != "done":
|
||||||
|
raise RuntimeError(f"job {jid} ended {st}")
|
||||||
|
items = req("/api/assets?limit=100")
|
||||||
|
items = items if isinstance(items, list) else items.get("items", [])
|
||||||
|
# parent_job is the only reliable link back — newest-asset races other users.
|
||||||
|
mine = [x for x in items if x.get("parent_job") == jid]
|
||||||
|
if not mine:
|
||||||
|
raise RuntimeError(f"job {jid} produced no linked asset")
|
||||||
|
return req(f"/api/assets/{mine[0]['id']}/file", raw=True)
|
||||||
|
|
||||||
|
|
||||||
|
def trim_and_scale(blob, height):
|
||||||
|
"""Crop to the subject's alpha bounds, then nearest-neighbour to `height`.
|
||||||
|
|
||||||
|
Trimming first is what puts the feet on the bottom edge, which is how MRPGI
|
||||||
|
positions and depth-sorts a sprite (MANUAL SS14).
|
||||||
|
"""
|
||||||
|
img = Image.open(io.BytesIO(blob)).convert("RGBA")
|
||||||
|
box = img.getbbox()
|
||||||
|
if box:
|
||||||
|
img = img.crop(box)
|
||||||
|
w = max(1, round(img.width * height / img.height))
|
||||||
|
img = img.resize((w, height), Image.NEAREST)
|
||||||
|
out = io.BytesIO()
|
||||||
|
img.save(out, "PNG")
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def make(name, spec, outdir):
|
||||||
|
prompt = f"{spec['prompt']}. {STYLE}"
|
||||||
|
png = run_job({
|
||||||
|
"operator": "flux_local",
|
||||||
|
"params": {"prompt": prompt, "model": "flux2-klein-4b",
|
||||||
|
"steps": 4, "width": 512, "height": 512,
|
||||||
|
# Stable seed per sprite name => reruns are reproducible.
|
||||||
|
"seed": abs(hash(name)) % 100000},
|
||||||
|
})
|
||||||
|
aid = upload(f"{name}_raw.png", png)
|
||||||
|
cut = run_job({"operator": "bg_remove_local", "asset_id": aid,
|
||||||
|
"params": {"resolution": 1024, "background": "transparent"}})
|
||||||
|
path = os.path.join(outdir, f"{name}.png")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(trim_and_scale(cut, spec.get("h", 22)))
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
game = sys.argv[1].rstrip("/")
|
||||||
|
only = None
|
||||||
|
if "--only" in sys.argv:
|
||||||
|
only = set(sys.argv[sys.argv.index("--only") + 1].split(","))
|
||||||
|
force = "--force" in sys.argv
|
||||||
|
|
||||||
|
specs = json.load(open(os.path.join(game, "sprites.json")))
|
||||||
|
outdir = os.path.join(game, "sprites")
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
todo = {
|
||||||
|
n: s for n, s in specs.items()
|
||||||
|
if (only is None or n in only)
|
||||||
|
and (force or not os.path.exists(os.path.join(outdir, f"{n}.png")))
|
||||||
|
}
|
||||||
|
if not todo:
|
||||||
|
print("nothing to do")
|
||||||
|
return
|
||||||
|
print(f"[mb] {len(todo)} sprites -> {outdir}", flush=True)
|
||||||
|
|
||||||
|
def one(item):
|
||||||
|
name, spec = item
|
||||||
|
try:
|
||||||
|
make(name, spec, outdir)
|
||||||
|
print(f"[mb] ok {name}", flush=True)
|
||||||
|
except Exception as e: # keep the batch alive; report at the end
|
||||||
|
print(f"[mb] FAIL {name}: {e}", flush=True)
|
||||||
|
return name
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(POOL) as ex:
|
||||||
|
failed = [f for f in ex.map(one, todo.items()) if f]
|
||||||
|
print(f"[mb] done. failed: {failed or 'none'}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||