destroyulator/game/dev/DemoDriver.gd
Monster Robot Party 61757d24bc LANE6: rigged FPS viewmodel, weapon loadout + material matrix, 4 HUD styles
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>
2026-07-31 16:34:00 +10:00

157 lines
5.8 KiB
GDScript

extends Node3D
## Dev-only capture harness. Loads main.tscn and drives the player through a scripted
## tour so a headless/CI run can record video of the actual game with Godot's
## --write-movie, without a human at the keyboard (and without screen-recording perms).
##
## /Applications/Godot.app/Contents/MacOS/Godot --path game \
## --resolution 1280x720 --write-movie /tmp/cap.avi --quit-after 900 \
## dev/demo.tscn
##
## Not shipped: excluded from export presets. Drives the player directly (position /
## yaw / pitch / _queue_attack) rather than faking InputEvents, so it stays immune to
## input-map changes.
const MAIN := "res://main.tscn"
var _main: Node3D
var _player: Player
var _t := 0.0
var _step := 0
var _step_t := 0.0
# Each step: {dur, move_to (Vector3 or null), look_at (Vector3 or null), act (String)}
# act: "" | "punch" | "kick" | "rain" | "reset"
var _script: Array = []
func _ready() -> void:
var packed := load(MAIN) as PackedScene
_main = packed.instantiate()
add_child(_main)
await get_tree().process_frame
_player = _find_player(_main)
if _player == null:
push_error("[demo] no Player found")
return
# the demo drives the camera; don't let the OS grab the pointer
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_build_script()
func _find_player(n: Node) -> Player:
if n is Player:
return n
for c in n.get_children():
var r := _find_player(c)
if r != null:
return r
return null
func _build_script() -> void:
# world layout (from Main._build_rack / _build_office, origin 0,0):
# racks (-0.95,-0.7) (0.95,-0.7)
# crates y=0.5 and y=1.2 rows, x -0.8..0.8
# desk+printer (2.7,-0.3) cabinet (-2.7,-0.3) cooler (3.3,1.6)
# CRT (-2.7,1.3) box (1.9,2.4) turntable (-1.7,-1.3)
_script = []
# ---- ACT 1: the loadout. Stand still and hold each weapon so the viewmodel,
# ---- the grip and one swing per archetype can all be inspected frame by frame.
for slot in range(6):
_script.append({"dur": 0.5, "move_to": Vector3(0, 0, 3.2),
"look_at": Vector3(0, 1.0, 0), "act": "weapon:%d" % slot})
_script.append({"dur": 1.0, "move_to": null, "look_at": null, "act": ""})
_script.append({"dur": 1.1, "move_to": null, "look_at": null, "act": "swing"})
# ---- ACT 2: the matrix. Box cutter shreds the carton, then clanks off the
# ---- filing cabinet; the sledge then removes the cabinet in one go.
_script += [
{"dur": 0.6, "move_to": null, "look_at": null, "act": "weapon:1"},
{"dur": 1.2, "move_to": Vector3(1.9, 0, 3.4), "look_at": Vector3(1.9, 0.3, 2.4), "act": ""},
{"dur": 0.45, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 0.45, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
# cutter vs steel — expect the clank + the HUD nudge, and no damage
{"dur": 1.3, "move_to": Vector3(-2.7, 0, 1.1), "look_at": Vector3(-2.7, 0.6, -0.3), "act": ""},
{"dur": 0.4, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 0.9, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 0.5, "move_to": null, "look_at": null, "act": "weapon:4"},
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 1.2, "move_to": null, "look_at": null, "act": "swing"},
]
# ---- ACT 3: the boss + the HUD styles
_script += [
{"dur": 1.4, "move_to": Vector3(2.7, 0, 1.3), "look_at": Vector3(2.7, 0.95, -0.3), "act": ""},
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 1.0, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 1.4, "move_to": null, "look_at": null, "act": "swing"},
{"dur": 1.6, "move_to": Vector3(0.6, 0, 5.2), "look_at": Vector3(0, 0.6, 0), "act": "hud"},
{"dur": 1.6, "move_to": null, "look_at": null, "act": "hud"},
{"dur": 1.6, "move_to": null, "look_at": null, "act": "hud"},
{"dur": 1.6, "move_to": null, "look_at": null, "act": "hud"},
]
func _process(dt: float) -> void:
if _player == null or _step >= _script.size():
return
_t += dt
var s: Dictionary = _script[_step]
var dur: float = s["dur"]
# fire the action when the step BEGINS, so a capture at step_start + n frames
# actually shows the swing rather than the second of stillness before it
if not s.get("_fired", false):
s["_fired"] = true
_do(String(s["act"]))
_step_t += dt
var mt = s.get("move_to")
if mt != null:
var to: Vector3 = mt
var k: float = clampf(_step_t / maxf(dur, 0.001), 0.0, 1.0)
k = k * k * (3.0 - 2.0 * k) # smoothstep so the dolly eases
var from: Vector3 = s.get("_from", _player.global_position)
if not s.has("_from"):
s["_from"] = _player.global_position
from = _player.global_position
_player.global_position = from.lerp(to, k)
var la = s.get("look_at")
if la != null:
_aim(la, clampf(_step_t / maxf(dur, 0.001), 0.0, 1.0))
if _step_t >= dur:
_step += 1
_step_t = 0.0
## Turn the body (yaw) + head (pitch) toward a world point, eased.
func _aim(target: Vector3, k: float) -> void:
var eye: Vector3 = _player.global_position + Vector3(0, _player.eye_height, 0)
var d: Vector3 = target - eye
if d.length() < 0.001:
return
var want_yaw := atan2(-d.x, -d.z)
var want_pitch := atan2(d.y, Vector2(d.x, d.z).length())
var e := clampf(k * 0.14, 0.0, 1.0) + 0.06
_player.rotation.y = lerp_angle(_player.rotation.y, want_yaw, e)
var head: Node3D = _player.get_node_or_null("Head")
if head != null:
head.rotation.x = lerp_angle(head.rotation.x, want_pitch, e)
func _do(act: String) -> void:
if act.begins_with("weapon:"):
_player.select(int(act.substr(7)))
return
match act:
"swing":
_player._queued = true # same path a left-click takes
"hud":
var hud = _main.get("_hud")
if hud != null:
hud.cycle()
"rain":
if _main.has_method("_rain"):
_main._rain(500)
"reset":
if _main.has_method("_reset"):
_main._reset()