destroyulator/game/scripts/GameMode.gd
Monster Robot Party ad0ca62f3e LANE1: record-destruction ritual + Find-the-Misfiled-Disc mode
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>
2026-07-14 19:40:52 +10:00

130 lines
4.5 KiB
GDScript

extends Node
class_name GameMode
## The rules layer. Owned by Main, fed the live records + the player's GrabController.
## Two modes:
## FreePlay ..... just smash the store, no timer (default).
## FindMisfiled . exactly ONE disc is in the wrong sleeve. Extract it and press F
## before the 60s timer runs out. The frantic dig leaves a mess — and
## the mess is scored, because the mess is the point.
##
## Main drives transitions (it owns the world reset + the input keys); GameMode owns
## the planting, the clock, the win/lose call, and the scoreboard line.
##
## Godot 4.7 GDScript 2.0.
enum Mode { FREE_PLAY, FIND_MISFILED }
const GENRES: Array[String] = ["jazz", "techno", "punk", "soul", "funk", "dub"]
const ROUND_TIME := 60.0
var mode: int = Mode.FREE_PLAY
var _records: Array[Record] = []
var _grab: GrabController = null
var _active := false # a FindMisfiled round is running
var _time_left := 0.0
var _mess := 0 # bodies smashed during the round (the score)
var _result := "" # win/lose banner, "" while playing
var _misfiled: Record = null
func setup(grab: GrabController) -> void:
_grab = grab
# ---------------------------------------------------------------- transitions
## FreePlay: no timer, every disc back in its right sleeve.
func begin_free_play(records: Array[Record]) -> void:
mode = Mode.FREE_PLAY
_records = records
_active = false
_result = ""
_misfiled = null
for r in _records:
if is_instance_valid(r):
r.set_genres(r.sleeve_genre, r.sleeve_genre)
## FindMisfiled: plant one mismatch, start the clock.
func begin_find_misfiled(records: Array[Record]) -> void:
mode = Mode.FIND_MISFILED
_records = records
_active = true
_time_left = ROUND_TIME
_mess = 0
_result = ""
_plant()
## Put exactly one disc in the wrong sleeve. Prints the plant (acceptance requires it).
func _plant() -> void:
var valid: Array[Record] = []
for r in _records:
if is_instance_valid(r):
r.set_genres(r.sleeve_genre, r.sleeve_genre) # reset any prior mismatch
valid.append(r)
if valid.is_empty():
print("[FindMisfiled] no records to plant into")
return
_misfiled = valid[randi() % valid.size()]
var wrong := _other_genre(_misfiled.sleeve_genre)
_misfiled.set_genres(_misfiled.sleeve_genre, wrong)
print("[FindMisfiled] planted: a %s disc hidden inside a %s sleeve (of %d records)" % [
wrong, _misfiled.sleeve_genre, valid.size()])
func _other_genre(g: String) -> String:
var pool := GENRES.filter(func(x): return x != g)
if pool.is_empty():
return g
return pool[randi() % pool.size()]
# ---------------------------------------------------------------- events
## Main forwards Smashable.smashed here so the mess is scored during a round.
func on_smashed() -> void:
if _active:
_mess += 1
## F (report). Win only if the disc in hand is genuinely misfiled (disc != sleeve genre).
func report() -> void:
if mode != Mode.FIND_MISFILED or not _active:
return
var held: Record = _grab.held_record() if _grab != null else null
if held != null and held.has_disc_in_hand() and held.disc_genre != held.sleeve_genre:
_win(held)
else:
# a wrong call just nudges the banner; no hard penalty, keep it playful
_result = "NOT THAT ONE — keep digging"
func _win(held: Record) -> void:
_active = false
_result = "FOUND IT! the %s disc was in a %s sleeve · mess %d · %d records disturbed" % [
held.disc_genre, held.sleeve_genre, _mess, disturbed_count()]
func _lose(_reason: String) -> void:
_active = false
var where := "somewhere in the crates"
if is_instance_valid(_misfiled):
where = "a %s sleeve" % _misfiled.sleeve_genre
_result = "TIME'S UP — the misfiled disc was in %s · mess %d" % [where, _mess]
# ---------------------------------------------------------------- bookkeeping
func disturbed_count() -> int:
var n := 0
for r in _records:
if is_instance_valid(r) and r.was_disturbed():
n += 1
return n
func _process(delta: float) -> void:
if _active and mode == Mode.FIND_MISFILED:
_time_left -= delta
if _time_left <= 0.0:
_time_left = 0.0
_lose("timeout")
## The scoreboard line Main appends to the HUD.
func hud_line() -> String:
if mode == Mode.FREE_PLAY:
return "MODE: FreePlay (1 FreePlay · 2 Find-the-Misfiled-Disc)"
if _active:
return "MODE: Find Misfiled %02d s mess %d · pull discs, F to report the wrong one" % [
int(ceil(_time_left)), _mess]
return "MODE: Find Misfiled %s (2 to replay · 1 FreePlay)" % _result