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>
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>
The game had no room. Props sat on a grey disc in a black void, which quietly broke
the premise: a game about wrecking your workplace needs a workplace.
Office.gd — LEVEL 01
- Open-plan office laid out after The Office (US) floor plan: bullpen of facing desk
pairs behind cubicle partitions, reception, glass-walled manager's office, conference
room, break room with vending machines, copier alcove, warehouse roller door.
- Drop ceiling on a T-bar grid with fluorescent troffers that ARE the light sources
(the old scene lit an interior with one outdoor directional lamp, which is exactly
why it read as "props on a plane"), window wall with blinds, magnolia and carpet.
- Office owns the shell + static fittings; Main._populate() owns everything smashable
and asks Office where things go. Walls/desks/counters are boxes because they ARE
boxes; chairs, vending machines, microwave and plant come from a new Blender
generator (tools/gen_office_props.py) because a box reads as wrong for those.
The level no longer falls over on its own
- Records were stacked 3 cm apart vertically while being 30 cm TALL, so Jolt resolved
27 cm of interpenetration explosively on frame one and shoved the furniture over
before the player touched anything. They now stand side by side.
- Per-material mass (MASSES): everything was 1 kg, so a thrown record could tip a
filing cabinet.
- Spawn guard: placing ~40 props by formula guarantees an occasional overlap, and
depenetration is violent (a chair left the building at 500 m/s). Dynamic bodies are
speed-limited for 0.75 s, and each offender is named once with the position it was
PLACED at, so the cause stays visible instead of being papered over. It then found
the real bug: bullpen rows 3.6 m apart left the two rows' chairs meeting
back-to-back with 3 cm to spare. Rows are now 4.8 m apart.
- dev/DemoDriver.gd act 0 touches nothing for 5 s and prints total body speed. Now
reads 0.00 m/s with zero spawn warnings.
Two real melee bugs found while testing the level
- The hit test was a SPHERE parked at `reach`, so anything CLOSER than the weapon's
reach fell in front of it and was missed — you could stand against the printer with
a sledgehammer and swing straight through it. Now a capsule swept from the camera.
- Swings started at eye height (1.6 m), so a carton on the floor was ~1.5 m away even
standing over it and short-reach weapons could never touch anything on the ground.
Swings now originate at hand height.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hands/POV
- Cut a real first-person arms rig out of the GODVERSE modular character kit
(tools/gen_fps_arms.py): ch01 hands + per-side sleeves on the full 65-bone
mixamorig skeleton, so all 20 finger bones per hand are poseable at runtime.
Textures shrunk to 1k; 4.2 MB.
- ViewModel.gd instances that rig ONCE PER HAND and places each instance so its
own hand bone lands on the grip — no IK. Grip orientation is measured off the
rig at load (pinky->index knuckle = bore axis, elbow->hand = forearm dir), so
it survives re-tuning grip_rest_rot instead of needing new euler angles.
- Motion layers: look-sway with spring-back, walk bob scaled by speed and weapon
heft, idle breathe, landing dip, weapon lower/raise on swap.
- Sleeve material overridden (donor asset is a fantasy leather bracer); hand
material forced non-metallic (its spec/gloss maps rendered skin as bronze).
Weapons
- Weapon.gd replaces MeleeAttack: 6 weapons, 4 swing archetypes, and the
weapon-vs-material matrix from the founding chat.
- Smashable moves from binary hits_to_break to hp/toughness, giving three
outcomes: break, dent, or futile (dead clank, no score, HUD nudge). The box
cutter genuinely shreds cardboard and genuinely cannot hurt a filing cabinet.
- The hit lands at Weapon.contact THROUGH the swing, not on the click — that
delay is most of why the sledge feels different from the cutter.
- Slots on 1-6 / wheel / Q; game modes moved to M, HUD cycle to H.
HUD
- Hud.gd: Arcade, Minimal, Work Order (a corporate destruction docket that fills
in line items) and Dev, over one shared data feed. Scoring + combo multipliers.
Dev harness (not shipped)
- macOS screen-recording perms aren't available to the CLI, so the game records
itself: dev/demo.tscn + DemoDriver.gd drive a scripted tour for --write-movie,
and dev/probe_*.gd print rig/scale/placement numbers.
Fix: tools/gen_viewmodel.py box() scaled by size/2 on top of primitive_cube_add's
already-unit side length, halving every box — which detached the bat's blade.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ship the browser smash toy publicly and document it.
Deploy: web/ is live at https://digalot.fyi/destroy/ — served on the dealgod VPS like
thriftgod (an nginx:alpine static container on dealgod_default + a /destroy/ location in
the shared reverse proxy). README.md now carries the exact deploy/update steps + a full
rollback runbook. No app code changed.
3GOD hardening (LANE5 deliverable B) was found ALREADY DONE on the live instance and
verified — not re-changed:
- GOD3_OPEN is off: unauthenticated write via Cloudflare → 403; GET /api/list → authed:false.
- SSRF plugged: POST /api/fetch with file:///etc/hostname and http://169.254.169.254/ →
rejected ("only public http(s) URLs allowed"), nothing written.
- Reads stay public (CF GET 200 + CORS *); trusted tailnet peers still push passwordless
via the TS_ALLOW IP allowlist (this session's meshgod pushes used that path).
Verified public: /destroy/ 200 over CF, JS as application/javascript, props (intact +
fractured) load from /3god/a/* with CORS *, printer boss shatters into 16 GLB chunks,
no console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LANE4 spot-checked clean (smoke test green, 20 fractured GLBs in game assets, full
web catalog, boss mechanics present). Next blocker between here and a shareable
link is ops: 3GOD writes are still unauthenticated + /api/fetch SSRF, and web/ only
exists on localhost. LANE5 is recon-first and reversible-steps on production boxes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consume Lane 2's props + close the review gaps in the browser toy.
props.js: all 16 depot slugs added to CATALOG (material/kind per prop; printer is the
boss). loadFractured() now (a) caches its result per id — including the null miss — so a
prop with no fractured sibling stops re-fetching + 404-ing on EVERY spawn (the record.
fractured.glb spam), and (b) returns the real chunk container (the fractured_root wrapper
Blender exports), so physics.js's direct-children chunk swap actually fires.
main.js: loadHeroProps() places the printer BOSS + office set dressing (desk, crt-monitor,
filing-cabinet, water-cooler) with their fractured templates. The boss soaks 4 hits then
bursts into its 16 GLB chunks (kept non-shatterable so a topple can't break it early).
Pointer-lock fallback (shareability fix): if the lock request is refused — iframes/embedded
hosts, where the toy used to dead-end on the title screen — we fall back to DRAG-TO-LOOK
(hold-drag rotates, a tap still smashes) so the shareable link always plays.
Assets: office-printer / crt-monitor / filing-cabinet mirrored into web/assets/ (offline
fallback); record/crate/rack.fractured.glb generated + mirrored so the record ritual smashes
into real chunks. The three store fractured GLBs were also pushed to the 3GOD shelf.
Verified in-browser via ?debug: printer aimSmash→"printer" then 16 chunks; thrown record →
8 chunks; debris drains 16→0; record.fractured.glb serves 200 (no 404); drag-look rotates
the camera when pointer lock is unavailable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consume Lane 2's pre-fractured props in the Godot game. Smashable.shatter() now prefers
a <name>.fractured.glb sibling: it instances the fractured template and turns each
chunk_00…chunk_NN mesh into its own debris RigidBody3D that inherits the intact body's
velocity + the smash impulse (box collider per chunk, debris group so the HUD count +
Juice keep working), falling back to primitive shards when no fractured sibling exists.
_glb_piece auto-wires the sibling, so every GLB prop upgrades for free.
Smashable gains hits_to_break + is_boss: the office PRINTER is the level boss — it sits on
a desk, soaks 5 hits (rocking with each), then bursts in a bigger juice cloud into its 16
GLB chunks. Six more Lane 2 props (crt-monitor, filing-cabinet, water-cooler, cardboard-box,
turntable, office-desk) are scattered as smashable furniture; the store crate/rack also pick
up real 8-chunk shatter from their mirrored fractured GLBs.
Assets pulled into game/assets/store/ (intact + .fractured.glb): office-printer, office-desk,
crt-monitor, filing-cabinet, water-cooler, cardboard-box, turntable + record/crate/rack
fractured siblings (generated via meshgod's Voronoi fracture CLI).
Verified headless: smoke test clean (0 script errors), printer boss soaks 4 hits then
spawns 16 chunk bodies, a crate spawns 8 — both real GLB chunks, not primitive shards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review of the three lanes (2026-07-14): all shipped and verified — Lane 1 smoke test
clean, Lane 2's 16 props (intact+fractured) live on 3GOD with the chunk_* contract,
Lane 3 play-tested via its ?debug handle (smash, rack cascade, record ritual, debris
pool all work). LANE4 closes the integration gap the parallel run left open, plus two
review findings: record.fractured.glb 404 spam and no pointer-lock fallback in
embedded browsers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shareable one-click web toy (not a port of the Godot game): walk a record store
first-person, pull a record, and wreck the place in the browser. Built on
thriftgod's pointer-lock FP-interior patterns, zero build step (three r0.175 +
@dimforge/rapier3d-compat over an importmap).
- physics.js: Rapier WASM world, one body per prop, support-graph cascade
(smash a rack → its crates pancake), pre-fractured swap (chunk_* bodies) with
generative-shard fallback until Lane 2 ships fractured GLBs, capped debris pool.
- props.js: GLTFLoader/DRACOLoader; loads props live from the 3GOD depot
(/a/<file>, CORS-open) with a local web/assets/ mirror fallback so it never
hard-fails offline. Read-only against the depot.
- juice.js: camera-trauma screenshake, hitstop, per-material particle bursts and
procedural Web-Audio smash voices (wood/cardboard/vinyl/glass/steel), combo HUD.
- main.js: FP controller, record-store room, grab/smash raycasting, record ritual
(pull → held → throw → shatter), "share your mess" canvas screenshot.
Assets mirrored offline: record/crate/rack (from the Godot lane) + cashbot/
council-bin (from 3GOD). Verified in-browser: props load, smashing spawns shards +
particles + combo, rack cascade pancakes its crates, record ritual and share work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn records from one-hit props into the store's signature dexterity ritual,
and add the first game mode on top of it.
Record.gd (class_name Record extends Smashable): a cardboard SLEEVE that hides a
vinyl DISC. Splits record.glb's Sleeve_Mesh / Vinyl_Disc nodes at runtime — the
sleeve stays on the body, the disc becomes its own frozen-kinematic vinyl Smashable
that follows the sleeve until freed. State machine IN_CRATE -> HELD -> EXTRACTING
-> DISC_FREE. set_extract() slides the disc up and out of the mouth; release_disc()
arms it as a live brittle throwable; shatter() spills the disc first so brute-force
smashing still spills it. Genre tints on disc label / sleeve spine make a misfiled
disc read at a glance.
GrabController.gd (under Player): hold point on the camera, E grabs the Record under
the crosshair (fat-sphere query, Record-filtered), horizontal mouse drag pulls the
disc (~600px = full), LMB throws the freed disc then trashes the empty sleeve, G
drops. On-screen pull meter.
GameMode.gd (class_name GameMode, owned by Main): FreePlay (default) + FindMisfiled —
plants exactly one disc_genre != sleeve_genre, 60s timer, win by extracting it and
pressing F while holding it, scores the mess (bodies smashed) + records disturbed.
Prints the plant on start.
Wiring: Player creates the GrabController and routes E/G/LMB/drag (yaw suspended
mid-pull, Esc untouched); Main._build_rack spawns Record instances on the crates
(keeps supports cascade + sleeve-art skinning + per-record genres), forwards smashes
to the mode, and adds keys 1/2 (modes) and F (report).
Headless smoke test clean (0 script errors / 0 warnings, 15 records); a runtime
harness exercised grab/extract/throw/drop/shatter-spill/mode-switch/reset — all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repo root moves up from game/ to the project folder. Lanes live as top-level
dirs (game/ now, web/ planned); LANES/ will hold parallel-session specs.
3D-STORE asset vault stays out of git. History preserved as renames.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- assets/art/sleeves_{a,b}.jpg: two 3x3 sheets of ORIGINAL fictional album
covers (18 total — dub, techno, metal, jazz, punk, synthwave, hip-hop,
psych, ambient, northern soul, krautrock, disco, white-label, bossa,
UKG, country, noise) generated with Gemini gemini-3-pro-image. No real
artists/albums — copyright-clean destructible content.
- assets/art/keyart.jpg: DESTROYULATOR title key art ("CLOCK OUT. SMASH
EVERYTHING.") for a future title screen.
- Main.gd _skin_record(): records duplicate their M_Sleeve_Front/Back
materials and window one random atlas tile via uv1_scale/uv1_offset —
one texture in VRAM serves nine covers, every record looks different.
- Verified headless: boots clean, "[art] records skinned: 15" (all of
them). The two dummy-renderer "material is null" prints are headless-
only (override materials have no RID in the dummy backend). Eyeball a
sleeve in the editor — if the art renders rotated, flip uv1 here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- game/assets/store/{rack,crate,record}.glb from the 3D-STORE library
- _glb_piece(): instances a GLB as a Smashable's visual, auto-fits a box
collider to the mesh AABB, and drops it flush to the floor (no hardcoded dims)
- _build_rack now assembles two smashable racks + crates of records on the floor
- cascade preserved: smashing a crate spills the records stacked on it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Juice.gd — the feel system, driven off Smashable.smashed (purely additive):
- Hitstop freeze-frame on every impact (time_scale dip, released in real time)
- Screenshake with quadratic trauma falloff, scaled by shard count
- Per-material GPU particle bursts, colored from the PROFILES table
- Procedural impact audio synthesized in code per material — no sound files,
the dig.js trick: wood crunch, cardboard rip, vinyl crack+warble, glass tinkle, steel ring
- Combo counter on the HUD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>