# 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.gd` — `class_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_smashed`→`Juice`. - `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.gd` — `class_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.gd` — `class_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) ```sh # 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`.