destroyulator/LANES/LANE1-godot-record-ritual.md
Monster Robot Party c03bb85c9f Add parallel-lane execution specs for Opus 4.8
LANES/PLAN.md         — verdict, GODVERSE pipeline architecture, shared contracts, 3GOD security note
LANES/LANE1-*.md      — Godot: grab/slide/smash record ritual + find-the-misfiled-disc mode (owns game/)
LANES/LANE2-*.md      — meshgod cell-fracture stage + Destroyulator prop batch -> 3GOD (owns ~/meshgod)
LANES/LANE3-*.md      — thriftgod-based web smash toy, Rapier physics, 3GOD props (owns web/)
LANES/KICKOFF.md      — per-lane opening prompts + collision-safety rules

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:47:47 +10:00

5.4 KiB

LANE 1 — Godot: the record-destruction ritual + "find the misfiled disc" mode

Owns: game/ only. Engine: Godot 4.7 (Jolt). For: an Opus 4.8 session. Runs on: M3 Ultra, /Applications/Godot.app.

Objective

Turn records from one-hit props into the game's signature dexterity ritual:

look at a record → Grab (E) to lift the sleeve out of the crate → drag mouse left↔right to slide the black disc out of the sleeve → the freed disc is a throwable brittle object (LMB throw / smash), and the empty sleeve is separately trashable.

Then a first game mode on top of it: "Find the Misfiled Disc" — one disc is in the wrong sleeve; find it before the timer, and the frantic search leaves a mess (the point).

What already exists (read these first)

  • game/scripts/Player.gd — FP CharacterBody3D: mouse-look, WASD, camera, punch/kick via MeleeAttack. Add the grab controller here (or a child node it owns).
  • game/scripts/Smashable.gdclass_name Smashable extends RigidBody3D; kind, PROFILES, smash(impulse), shatter(), supports[]/release() cascade, smashed signal.
  • game/scripts/Main.gd — builds the store (_glb_piece() auto-fits colliders, drops to floor), _skin_record() UV-tiles sleeve art, HUD, B=rain, R=reset. _on_smashedJuice.
  • game/scripts/Juice.gd — hitstop/shake/particles/procedural audio, impact(kind,pos,shards).
  • Assets: game/assets/store/record.glb (sleeve, carries M_Sleeve_Front/Back mats). Disc-only + sleeve-only source GLBs exist at ~/Documents/Destroyulater/3D-STORE/clean_glbs/ (VinylDisc_v4.glb, VinylSleeve_v4.glb) — copy into game/assets/store/ if you split them.

Deliverables

1. game/scripts/Record.gdclass_name Record extends RigidBody3D

A record = a cardboard sleeve (this body, kind="cardboard" Smashable-like) that contains a hidden vinyl disc. State machine: IN_CRATE → HELD → EXTRACTING → DISC_FREE.

  • Exports/fields: sleeve_genre: String, disc_genre: String (usually equal; the mode sets one mismatch), _extract: float (0→1 pull progress), _disc: RigidBody3D (the disc, starts frozen inside the sleeve at local offset).
  • grab(hold_point: Node3D): freeze, reparent-follow the player hold point (kinematic).
  • set_extract(amount: float): clamp 0..1, lerp _disc local position along the sleeve's +X (opening) axis by amount * sleeve_length; peek the disc label as it emerges.
  • At _extract >= 1.0: release_disc() → unfreeze _disc, detach as free brittle vinyl Smashable, keep the now-empty sleeve as a trashable cardboard body.
  • The disc + sleeve both reuse Smashable's shatter/juice path (emit smashed).

2. game/scripts/GrabController.gd — attach under Player

  • A hold point (Node3D child of camera, ~0.5m forward, ~-0.1 down).
  • E = grab the Record under the crosshair (reuse the sphere/ray query pattern from MeleeAttack.strike, but filter for Record). Only grabbable within reach (~2m).
  • While held: accumulate InputEventMouseMotion.relative.x into the held record's set_extract() (tune: ~600px of drag = full pull). Show an on-screen extract meter.
  • LMB while holding a freed disc = throw (impulse along camera forward, disc is brittle so it cracks on impact). G or LMB on the empty sleeve = drop/trash it.
  • Esc still releases mouse (don't break existing Player input).

3. game/scripts/GameMode.gdclass_name GameMode extends Node, owned by Main

  • FreePlay (default) and FindMisfiled.
  • FindMisfiled: on start, pick N records, set exactly ONE to disc_genre != sleeve_genre (e.g. a techno disc in a jazz sleeve). Start a 60s timer (HUD). Win = the player extracts the misfiled disc and presses F (report) while holding it. Track "records disturbed" and "mess score" (bodies knocked / smashed via _on_smashed). Show result on win/lose.
  • Key 1 = FreePlay, 2 = start FindMisfiled, on the existing Main._unhandled_input.

4. Wire into Main._build_rack

  • Replace the _glb_piece(RECORD_GLB, "vinyl", …) record placement with Record instances (sleeve+disc). Keep them stacked on crates; keep crate.supports.append(record).
  • Keep _skin_record() for the sleeve art; add a small disc-label tint by disc_genre.

Acceptance (must pass before each commit)

# zero script errors across 180 frames (2 "material is null" lines are benign headless noise)
/Applications/Godot.app/Contents/MacOS/Godot --headless --path game --quit-after 180 2>&1 \
  | grep -E 'SCRIPT ERROR:|Parse Error' | grep -v 'material.*null'    # must be empty

Plus a print() on mode start confirming the misfiled record was planted (genre A in genre B). Then eyeball in-editor: grab a record, drag to pull the disc, throw it, trash the sleeve.

Gotchas

  • Held bodies: set freeze=true + move via global_transform each _physics_process, OR use a Generic6DOFJoint3D/PinJoint3D to the hold point — simpler is freeze+follow.
  • Don't let the disc's brittle body_entered fire while it's still inside the sleeve (guard: only arm brittleness after release_disc()).
  • Keep everything in groups smashable/debris so the HUD body count + Juice keep working.

Definition of done

Grab/lift/slide/smash works end-to-end; FindMisfiled mode is playable start→win/lose with a timer and mess score; headless smoke test clean; committed + pushed to origin main.