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>
88 lines
5.4 KiB
Markdown
88 lines
5.4 KiB
Markdown
# 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`.
|