destroyulator/game/dev/probe_vm.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

86 lines
2.8 KiB
GDScript

extends SceneTree
## Where do the hand bones actually end up relative to the grip? Renders are too coarse
## to tell a 5 cm error from a 50 cm one, so print the numbers.
##
## Godot --headless --path game --script dev/probe_vm.gd
func _initialize() -> void:
var main: Node = (load("res://main.tscn") as PackedScene).instantiate()
get_root().add_child(main)
await process_frame
await process_frame
var p := _find(main, "Player") as Player
if p == null:
print("no player"); quit(); return
var vm := p.viewmodel
var cam := p.camera
for slot in [0, 2, 4]:
p.select(slot)
# select() defers the actual equip to the swap animation; force it for the probe
vm.equip(p.loadout()[slot])
await process_frame
var w: Weapon = p.loadout()[slot]
print("\n===== slot %d %s" % [slot, w.display_name])
var grip: Node3D = vm.get_node("Rig/Grip")
print(" grip (cam space) = ", cam.global_transform.affine_inverse() * grip.global_position)
for side in ["ArmR", "ArmL"]:
var arm := vm.get_node_or_null("Rig/" + side) as Node3D
if arm == null:
print(" %s: MISSING" % side); continue
var skel := _skel(arm)
var bone := "mixamorig_RightHand" if side == "ArmR" else "mixamorig_LeftHand"
var bi := skel.find_bone(bone)
var hand_world := skel.global_transform * skel.get_bone_global_pose(bi)
var in_cam := cam.global_transform.affine_inverse() * hand_world
print(" %s hand (cam space) = %s" % [side, in_cam.origin])
# where does the forearm run? (elbow -> hand direction, in camera space)
var fb := skel.find_bone("mixamorig_RightForeArm" if side == "ArmR" else "mixamorig_LeftForeArm")
var fore_world := skel.global_transform * skel.get_bone_global_pose(fb)
var fore_cam := cam.global_transform.affine_inverse() * fore_world
print(" elbow (cam space) = %s forearm dir = %s" % [
fore_cam.origin, (in_cam.origin - fore_cam.origin).normalized()])
if vm.get_node_or_null("Rig/Grip").get_child_count() > 0:
var wn := grip.get_child(0) as Node3D
var ab := _aabb(wn)
print(" weapon aabb (local) pos=%s size=%s" % [ab.position, ab.size])
quit()
func _find(n: Node, cls: String) -> Node:
if n.get_class() == cls or (cls == "Player" and n is Player):
return n
for c in n.get_children():
var r := _find(c, cls)
if r != null:
return r
return null
func _skel(n: Node) -> Skeleton3D:
if n is Skeleton3D:
return n
for c in n.get_children():
var r := _skel(c)
if r != null:
return r
return null
func _aabb(n: Node) -> AABB:
var acc := AABB()
var started := false
for m in _meshes(n):
var a: AABB = (m as MeshInstance3D).get_aabb()
if not started:
acc = a; started = true
else:
acc = acc.merge(a)
return acc
func _meshes(n: Node, acc: Array = []) -> Array:
if n is MeshInstance3D:
acc.append(n)
for c in n.get_children():
_meshes(c, acc)
return acc