The game stopped being a smash sandbox. You work a shift, the work provokes you, a
meter fills, you snap, and you cannot go back to your desk until you have broken
enough things. Then you sit back down and try to earn the rest of your day.
The score is your payslip: WAGES (tasks completed) - DAMAGES (everything you broke).
The load-bearing detail is that rage drains a FLAT amount per object destroyed while
each object bills at its OWN value — so during a meltdown the play is to wreck a lot
of cheap rubbish fast rather than put the sledgehammer through a filing cabinet. You
don't choose whether you lose your temper, you choose how much it costs. FURY (objects
broken while melting down) is tracked separately and deliberately isn't money, so
"I ended the shift owing them $2,400" stays a brag.
Rage.gd — WORKING -> MELTDOWN -> COOLDOWN. During a meltdown the clock STALLS if you
stop destroying; you must actually be breaking things.
RageOverlay.gd — the blood vessel. A vignette that closes in with rage and jumps
inward on every heartbeat, procedural veins that creep further into frame the angrier
you get, and a synthesized lub-dub going 62 -> 172 BPM. Zero assets: the shader is
built in code, the veins are draw_polyline from a seeded RNG, the heartbeat is a
generated WAV. Tops out as a heavy frame rather than a red screen — you have to be
able to see what you're about to destroy.
Tasks.gd — stations you walk to and are LOCKED IN PLACE at, because you're at your
desk and that's the joke. Two so far:
SPREADSHEET the selected cell silently drifts one cell partway through entry, with
no sound and no animation. Commit without noticing and the figure lands
in the wrong cell and you start again. This is the whole thesis of the
game in about forty lines.
PHOTOCOPIER jams, and wants the right tray out of 1, 2, 2A, 3, 3B.
Snapping ejects you from the station at no penalty — you didn't choose to walk away.
CubicleHell.gd — 5-minute shift, wages vs damages, meltdown count, end-of-shift
payslip with a verdict line.
Also: HUD gets a monospace face (Godot's fallback is proportional, so the spreadsheet
grid and the work-order docket never lined up), and the arcade HUD's headline becomes
net pay, red when negative.
LANES/LANE7-cubicle-hell.md captures the rest of the design: more provocations, the
GAUNTLET mode (find which of many cabinets, pull drawers, destroy files individually,
and work out which tool the level wants without being told), the other three
workplaces, and why doratracyer/floor_plan can't help — it's OpenSCAD SVG->STL for
3D printing, no generation, no room polygons, GPL-3.0. Office.gd is already most of
a parametric floor-plan builder; lifting its numbers into a data spec is the real path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
7.4 KiB
Markdown
158 lines
7.4 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** | toggle game mode (FreePlay ⟷ Find-the-Misfiled-Disc) |
|
||
| **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 |
|
||
|
||
## 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. The spreadsheet's selected cell **silently drifts one cell**
|
||
partway through entry, so the figure lands in the wrong place and you start again. The
|
||
photocopier jams and wants the right tray out of `1, 2, 2A, 3, 3B`.
|
||
|
||
`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.
|