NO HEALTH BAR. Nothing else in this game does damage, so hit points would be a
survival-horror UI bolted onto a demolition game. Instead: one meter, DREAD, fed by
every way this place gets at you — being stared at, something close behind you, each
loop, and your own coughing. It decays when nothing is happening to you, and every loop
makes it grip harder (DREAD_LOOP_GRIP), so the place gets better at this the longer you
stay.
At 100% you die, and WHICH source pushed it over decides the death, which is what keeps
the three memes legible at the one moment it matters:
STARE "YOU LOOKED TOO LONG / it was still. it was always still. that was never the
safe part." (Slenderman)
REACH "IT WAS ALREADY IN THE ROOM / you were making so much noise."
Sprays. This is the gory one. (the xenomorph)
LOOP "YOU ARE STILL IN THE ROOM / you walked it eleven times. you will walk it
again." Eleven loops and the room simply keeps you. (Blair Witch)
Gore.gd is procedural — seeded blobs that swell on the lens and then bleed downward in
streaks, a wet burst at the camera (physical deaths only; being taken by the static
shouldn't spray), the camera going down to land on its side on the carpet, then the
card. The sequence is deliberately SLOW: a jumpscare is a spike, but a death should take
its time, because the seconds spent watching the lens settle are where it lands. R wakes
you up.
Player.set_frozen() so you can't stroll around during your own death.
Renamed Backrooms' VHS layer to `tape` — `dread` is the meter now, and it was clearer
anyway.
dev/probe_death.gd drives all three deaths and the revive.
Note: the "Parameter material is null" warning only appears under the headless dummy
renderer; windowed runs are clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# Destroyulator — the game
|
||
|
||
Mac-first (Apple Silicon / Metal), **Godot 4.7**, **Jolt** physics.
|
||
|
||
## Run it
|
||
|
||
**Editor:** open `/Applications/Godot.app`, import this folder (`game/`), press **F5** / ▶.
|
||
|
||
**CLI:**
|
||
```sh
|
||
/Applications/Godot.app/Contents/MacOS/Godot --path /Users/m3ultra/Documents/Destroyulater/game
|
||
```
|
||
|
||
## Controls
|
||
|
||
| Input | Does |
|
||
|---|---|
|
||
| **WASD / Space** | move / jump |
|
||
| **mouse** | look (Esc releases the cursor, click recaptures) |
|
||
| **LMB** | swing the equipped weapon |
|
||
| **1–6 · wheel · Q** | pick a weapon · cycle · swap to previous |
|
||
| **H** | cycle HUD style (Arcade / Minimal / Work Order / Dev) |
|
||
| **M** | cycle game mode (Cubicle Hell / Gauntlet / Total Destruction / Against the Clock) |
|
||
| **L** | cycle site (Scranton / Pawnee / The Incubator / Sub-level 4) |
|
||
| **G** | restart a Gauntlet run |
|
||
| **F** | report the disc in hand (Find-the-Misfiled-Disc) |
|
||
| **E** | work at the station you're standing at (locks you in place); otherwise grab a record |
|
||
| **drag ←→ · LMB · G** | with a record held: slide the disc out · throw it · drop |
|
||
| **digits · arrows · ENTER** | while working: type · move cell · commit |
|
||
| **B · R** | rain 500 bodies (stress gate) · reset the office |
|
||
|
||
## The four sites
|
||
|
||
`scripts/Floorplan.gd` builds a workplace from a **data spec**; `scripts/Levels.gd` holds
|
||
four of them. A site is a Dictionary — bounds, palette, walls with doors and glazing,
|
||
window runs, ceiling style, light grid and style, slabs, props, desk clusters, chairs,
|
||
smashables, gauntlet records-area and spawn — so a new one is authorable in minutes and a
|
||
generator can later emit the same structure.
|
||
|
||
| | palette / light | what makes it different |
|
||
|---|---|---|
|
||
| **SCRANTON BRANCH** | magnolia, grey carpet, troffers | the baseline office |
|
||
| **PAWNEE CITY HALL** | civic beige, blue-grey | a public counter, pinboards, over-partitioned |
|
||
| **THE INCUBATOR** | timber, white, 3 m, pendants | nobody has a desk; they work at a dining table |
|
||
| **SUB-LEVEL 4** | concrete, green strip lights | **no windows**, exposed services, a wall of servers |
|
||
| **LEVEL 0** | one colour, everywhere | the Backrooms. Walls are **generated**, and everything you're here to destroy is **invisible** |
|
||
|
||
`dev/probe_levels.gd` builds every one and reports residual motion after a full second.
|
||
All five must read `0.00 m/s`.
|
||
|
||
### LEVEL 0 — the Backrooms, and the dust
|
||
|
||
The only site whose walls are **generated** rather than authored, which is both the
|
||
honest test of the spec and the right call for the fiction: the place is meant to feel
|
||
endless and samey, and a seeded RNG does that better than a human placing slabs. It
|
||
emits partial wall runs on a coarse lattice plus freestanding pillars — the Backrooms
|
||
aren't corridors, they're big offset slabs in an open floor, and a proper maze would be
|
||
legible.
|
||
|
||
`scripts/Dust.gd` is its mechanic. **Everything smashable here is invisible but solid** —
|
||
you find things by walking into them. Right-click with the fire extinguisher throws a
|
||
cloud of powder; anything it touches is coated, glows faintly out of all that yellow for
|
||
seven seconds, and then settles back into nothing.
|
||
|
||
And you breathe it. Every cloud puts a little in your lungs, and the haze it builds is on
|
||
**your** side of the glass. So the mechanic argues with itself: spray more to see
|
||
individual objects, and see the room less. Past 72% you start coughing — the camera
|
||
jolts, the screen blooms, and it feeds the rage meter, because of course it does.
|
||
|
||
The intended rhythm is spray → switch to something heavy → break it before the coating
|
||
settles → spray again, coughing.
|
||
|
||
### …and there is something in there with you
|
||
|
||
`scripts/Entity.gd` borrows from three places, and each borrowing does a different job:
|
||
|
||
- **Slenderman** — it does not move while you are looking at it, and it has closed the
|
||
distance every time you look back. Looking at it is *also* what builds the **static**,
|
||
so the safe option (keep it in view) is the one that kills you slowly and the fast
|
||
option (run) is the one that lets it catch up. That's the trap, and it's why Slender
|
||
worked.
|
||
- **Xenomorph** — it hunts by **sound**, and the thing you came here to do is smash
|
||
furniture. Every break and every puff of powder drops a marker it walks toward.
|
||
Playing well is what feeds it; playing quietly means not playing. A loud enough noise
|
||
overrides the freeze entirely.
|
||
- **Blair Witch** — walk far enough in any direction and you come out of the opposite
|
||
wall into the same room, because every room here *is* the same room. And a wrap is
|
||
when the entity gets to reposition, so "I've been here before" and "it's already here"
|
||
land in the same second.
|
||
|
||
It can't be fought — weapons pass through. The outs are distance, silence, and not
|
||
looking, three things the rest of the game actively discourages.
|
||
|
||
### DREAD, and dying
|
||
|
||
**No health bar.** Nothing else in this game does damage, so hit points would be a
|
||
survival-horror UI bolted onto a demolition game. Instead there is one meter — **DREAD** —
|
||
fed by every way this place gets at you: being stared at, something close behind you,
|
||
each loop, and your own coughing. It falls when nothing is happening to you, and every
|
||
loop makes it harder to shake.
|
||
|
||
At 100% you die, and **which** death you get is decided by whichever source pushed it
|
||
over — so the three memes stay legible in the moment it matters:
|
||
|
||
| | how | the card |
|
||
|---|---|---|
|
||
| **Slenderman** | you looked too long | *YOU LOOKED TOO LONG — it was still. it was always still. that was never the safe part.* |
|
||
| **the xenomorph** | it reached you, and it sprays | *IT WAS ALREADY IN THE ROOM — you were making so much noise.* |
|
||
| **Blair Witch** | eleven loops of the same room | *YOU ARE STILL IN THE ROOM — you walked it eleven times. you will walk it again.* |
|
||
|
||
`scripts/Gore.gd` does the rest, procedurally: seeded blobs that swell on the lens and
|
||
then bleed downward in streaks, a wet burst at the camera (physical deaths only — being
|
||
taken by the static shouldn't spray), and the camera going down to land on its side on
|
||
the carpet. The sequence is deliberately **slow** — a jumpscare is a spike, but a death
|
||
should take its time, because the seconds you spend watching the lens settle are where it
|
||
actually lands. `R` to wake up.
|
||
|
||
`scripts/Dread.gd` is the tape: grain, scanlines, chroma split, a wandering tracking
|
||
tear, and Slender's interference driven straight off the entity's stare — the HUD element
|
||
that isn't a HUD element.
|
||
|
||
## The four modes
|
||
|
||
`scripts/Modes.gd`, cycled with M.
|
||
|
||
- **CUBICLE HELL** — the main one, below.
|
||
- **THE GAUNTLET** — `scripts/Gauntlet.gd` + `scripts/Cabinet.gd`. Five identical
|
||
cabinets, each labelled with an alphabetical range. The brief names a **client**, never
|
||
a cabinet, so working out that HENDERSON lives in F–J is the puzzle — nothing glows.
|
||
Destruction is granular: pull a drawer, it slides out and goes live, and the folders
|
||
inside become individual smashables. Doing that by hand inside the clock isn't really
|
||
possible — **every site hides a shredder that eats a whole open drawer at once, and the
|
||
game never says so.** Noticing it is the level.
|
||
- **TOTAL DESTRUCTION** — no job, no pay. Clear the whole site, scored on time.
|
||
- **AGAINST THE CLOCK** — fixed timer, maximise damage *value*. The exact inverse of
|
||
Cubicle Hell: here the expensive things are the goal rather than the mistake.
|
||
|
||
## CUBICLE HELL
|
||
|
||
The mode the game is about. See [`../LANES/LANE7-cubicle-hell.md`](../LANES/LANE7-cubicle-hell.md)
|
||
for the full design.
|
||
|
||
`Rage.gd` is the tilt meter: work provokes it, clean work drains it slightly, and at 100%
|
||
you SNAP into a forced destruction window that **stalls if you stop destroying**.
|
||
`RageOverlay.gd` is the blood vessel — a vignette that closes in, procedural veins that
|
||
creep further into frame, and a heartbeat that goes 62 → 172 BPM. All of it generated in
|
||
code; no textures, no shader files, no audio assets.
|
||
|
||
`Tasks.gd` is the job — four stations, each with one designed way of wasting your time:
|
||
|
||
- **SPREADSHEET** — the selected cell **silently drifts one cell** partway through entry.
|
||
No sound, no animation. Commit without noticing and the figure is in the wrong cell.
|
||
- **PHOTOCOPIER** — jams, and wants the right tray out of `1, 2, 2A, 3, 3B`.
|
||
- **STAPLES** — SPACE lifts a staple, then it must be pulled the way it's bent. Dither
|
||
and the sheet tears and the bundle reprints.
|
||
- **GUILLOTINE** — align to a mark and cut. **The guide has a permanent offset** and
|
||
nothing on screen admits it; you have to learn to distrust the equipment.
|
||
|
||
`Ambience.gd` is the attrition you can't play around: the tube above your desk flickers
|
||
(and actually flickers, on the real lights), someone puts fish in the microwave, a phone
|
||
rings four desks away, reply-all to forty-one recipients. No interaction, just a periodic
|
||
tick on the meter — it's what makes the room hostile rather than merely beige.
|
||
|
||
`CubicleHell.gd` scores it as a payslip: **wages − damages**. Rage drains a flat amount
|
||
per object but each object bills at its own value, so the meltdown decision is *smash a
|
||
lot of cheap things fast* versus *one expensive swing*.
|
||
|
||
## The weapon-vs-material matrix
|
||
|
||
This is the decision the game is built on. A swing deals `Weapon.power × Weapon.vs[material]`
|
||
against that material's `hp`, and there are three outcomes: it **breaks**, it **dents**
|
||
(knockback + a chip of damage), or it's **futile** — a dead clank, no score, no combo,
|
||
and a HUD nudge to switch tools.
|
||
|
||
| | cardboard | paper | vinyl | glass | wood | steel |
|
||
|---|---|---|---|---|---|---|
|
||
| **Bare Hands** | ok | good | ok | ok | poor | useless |
|
||
| **Box Cutter** | **shreds** | **shreds** | good | poor | useless | useless |
|
||
| **Cricket Bat** | good | poor | **great** | **great** | ok | poor |
|
||
| **Crowbar** | ok | poor | ok | good | good | **great** |
|
||
| **Sledgehammer** | ok | poor | ok | **great** | **great** | **great** |
|
||
| **Fire Extinguisher** | ok | poor | ok | **great** | good | good |
|
||
|
||
Tuned in one place: `scripts/Weapon.gd` (`vs` tables) and `scripts/Smashable.gd`
|
||
(`PROFILES[kind].hp`). The office printer is the level boss — steel at `toughness_scale
|
||
= 5.0`, so it's a real fight with anything but the sledge.
|
||
|
||
Weapons also differ in **when** they connect, not just how hard: the hit lands at
|
||
`Weapon.contact` through the swing animation, so the sledgehammer commits late and heavy
|
||
while the box cutter is near-instant.
|
||
|
||
## The viewmodel
|
||
|
||
`scripts/ViewModel.gd`. Real rigged hands (mixamorig, all 20 finger bones per hand)
|
||
holding real weapon meshes.
|
||
|
||
The arms rig is **instanced once per hand** and each instance is *placed* so its own hand
|
||
bone lands on the grip — no IK solver. That works because in first person you only ever
|
||
see forearm and hand. Placement solves two aims at once: the fist's bore lines up with
|
||
the weapon shaft, and the forearm runs back toward where that shoulder would be. Both
|
||
directions are **measured off the rig** at load (pinky-knuckle → index-knuckle for the
|
||
bore, elbow → hand for the forearm), so the grip survives re-tuning `grip_rest_rot`
|
||
instead of needing a fresh set of hand-authored euler angles.
|
||
|
||
Motion is layered on one node: look-sway with spring-back, walk bob scaled by speed and
|
||
weapon heft, idle breathing, landing dip, and a keyframed swing arc per archetype
|
||
(`overhead` / `horizontal` / `jab` / `thrust`).
|
||
|
||
## Assets
|
||
|
||
| Path | Source |
|
||
|---|---|
|
||
| `assets/viewmodel/fps_arms.glb` | `tools/gen_fps_arms.py` — cut from the GODVERSE modular character kit |
|
||
| `assets/viewmodel/{bat,sledge,crowbar,cutter,extinguisher}.glb` | `tools/gen_viewmodel.py` — procedural, grip at origin, shaft +Y |
|
||
| `assets/store/*.glb` + `*.fractured.glb` | Lane 2 prop batch; the fractured sibling drives real chunk destruction |
|
||
| `assets/art/sleeves_*.jpg` | 3×3 sheets of generated cover art, windowed per record via uv1 offset |
|
||
|
||
Regenerate either set with:
|
||
```sh
|
||
/Applications/Blender.app/Contents/MacOS/Blender --background --python tools/gen_viewmodel.py -- --render
|
||
```
|
||
|
||
## Dev harness
|
||
|
||
`dev/` is not shipped. Because macOS screen-recording permission isn't available to the
|
||
CLI, the game records **itself**:
|
||
|
||
```sh
|
||
Godot --path game --resolution 1280x720 --write-movie /tmp/cap.avi --quit-after 2250 dev/demo.tscn
|
||
```
|
||
|
||
`dev/DemoDriver.gd` drives the player through a scripted tour (every weapon, the material
|
||
matrix, the boss, all four HUDs) so a change can be eyeballed frame by frame without a
|
||
human at the keyboard. `dev/probe_*.gd` print rig/scale/placement numbers — renders are
|
||
too coarse to tell a 5 cm error from a 50 cm one.
|
||
|
||
## Smoke test before committing
|
||
|
||
```sh
|
||
/Applications/Godot.app/Contents/MacOS/Godot --headless --path game --quit-after 300
|
||
```
|
||
|
||
Zero script errors required. After adding any `class_name` script, run
|
||
`Godot --path game --editor --quit` first — headless Godot won't rescan a stale class cache.
|
||
|
||
## The level
|
||
|
||
`scripts/Office.gd` builds LEVEL 01: an open-plan office after the Dunder Mifflin floor
|
||
plan — bullpen of facing desk pairs behind cubicle partitions, reception, a glass-walled
|
||
manager's office, conference room, break room with vending machines, a copier alcove for
|
||
the printer boss, and a warehouse roller door. Drop ceiling, fluorescent troffers (which
|
||
are the actual light sources), magnolia walls, grey carpet, a window wall with blinds.
|
||
|
||
Office owns the **shell and static fittings**; `Main._populate()` owns everything
|
||
**smashable**, and asks Office where things go (`desk_spots()`, `chair_spots()`,
|
||
`copier_spot()`). Walls, desks and counters are boxes built in code because they are
|
||
boxes; chairs, vending machines, the microwave and the plant come from
|
||
`tools/gen_office_props.py`, because a box reads as obviously wrong for those.
|
||
|
||
### The spawn guard
|
||
|
||
Placing ~40 props by formula means something will eventually spawn a few centimetres
|
||
inside something else, and Jolt's depenetration is not gentle — a chair overlapping a
|
||
desk left the building at 500 m/s on frame one, knocking over the furniture on its way.
|
||
|
||
`Main._physics_process()` speed-limits dynamic bodies for the first 0.75 s: enough for a
|
||
real overlap to push apart calmly, far too little to launch anything. Every offender is
|
||
named once in a warning with **the position it was placed at**, so the underlying overlap
|
||
stays visible and fixable rather than silently papered over.
|
||
|
||
Verify a level is genuinely at rest with:
|
||
```bash
|
||
/Applications/Godot.app/Contents/MacOS/Godot --headless --path game --quit-after 300 dev/demo.tscn
|
||
```
|
||
The demo's first act touches nothing and prints total body speed each second. It must
|
||
read `0.00 m/s`, and no `[spawn]` warnings should appear.
|