Office.gd is gone. Floorplan.gd consumes a spec Dictionary — bounds, palette, walls
with doors and glazing styles, window runs with mullions and blinds, ceiling style,
light grid and style, slabs, static props, desk clusters, chairs, level-specific
smashables and spawn — and Levels.gd holds four of them.
Making this data rather than four subclasses means a site is authorable in minutes,
levels can be diffed, and a real generator can later emit the same structure (a BSP
split of the footprint -> rooms -> doors on shared walls -> fittings by room type).
That was the actual answer to "can we use this git to generate plans": the repo John
found is OpenSCAD SVG->STL for 3D printing, with no generation, no plan parsing, no
room polygons and GPL-3.0, so it can't help — but Office.gd was already most of a
parametric plan builder, and lifting its numbers out is the real path.
The four sites are deliberately not reskins; they differ in the three things a player
actually reads — palette, light, and what the walls are made of:
SCRANTON magnolia, grey carpet, drop ceiling, fluorescent troffers, daylight
down one glazed wall. The baseline.
PAWNEE civic beige and blue-grey, low partitions everywhere, a public
counter, pinboards. Municipal and over-partitioned.
THE INCUBATOR timber floor, white walls, 3 m ceiling, PENDANT lights, glass wall
onto a pool. Nobody has an office; they work at a dining table.
SUB-LEVEL 4 concrete, NO windows at all, exposed services, bare strip lights, a
wall of server racks. The light is green and everything is junk.
L cycles sites in-game; _load_level tears down the shell, rebuilds, re-registers task
stations and re-arms the shift.
tools/gen_level_props.py adds ten more procedural props. The filing cabinet is the
important one: it exports as a CARCASS plus a separate DRAWER, each with its own
floor-centre origin, so the Gauntlet can pull a drawer out as its own rigid body and
spill the files. Also file folder, desk phone, guillotine, shredder, server rack,
sofa, wastebin, stapler.
dev/probe_levels.gd builds every level and reports residual motion after a full
second. All four read 0.00 m/s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|---|---|---|
| .. | ||
| assets | ||
| dev | ||
| scripts | ||
| .gitignore | ||
| main.tscn | ||
| project.godot | ||
| README.md | ||
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:
/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
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:
/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:
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
/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:
/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.