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>
157 lines
5.8 KiB
GDScript
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()
|