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

88 lines
2.6 KiB
GDScript

extends SceneTree
## Dev probe: dump how Godot imported fps_arms.glb (node tree, skeleton scale, bone
## names) so ViewModel.gd can be authored against the real numbers instead of guesses.
##
## Godot --headless --path game --script dev/probe_arms.gd
func _initialize() -> void:
_probe("res://assets/viewmodel/fps_arms.glb")
for w in ["bat", "sledge", "crowbar", "cutter", "extinguisher"]:
_probe("res://assets/viewmodel/%s.glb" % w, false)
quit()
func _probe(path: String, deep := true) -> void:
print("\n=========== ", path)
if not ResourceLoader.exists(path):
print(" MISSING")
return
var ps := load(path) as PackedScene
if ps == null:
print(" not a PackedScene")
return
var root := ps.instantiate()
_dump(root, 0, deep)
var skel := _find_skel(root)
if skel != null:
print(" -- skeleton: ", skel.name, " bones=", skel.get_bone_count())
print(" -- skeleton global xform: ", skel.transform)
# Godot sanitises glTF bone names on import, so print what actually landed
# rather than assuming the mixamorig: spelling survived.
for i in skel.get_bone_count():
var nm := skel.get_bone_name(i)
if nm.to_lower().contains("right") or nm.to_lower().contains("spine"):
print(" %-34s idx=%-3d parent=%-3d rest.origin=%s" % [
nm, i, skel.get_bone_parent(i), skel.get_bone_rest(i).origin])
# overall size
var aabb := _aabb(root)
print(" -- combined AABB pos=", aabb.position, " size=", aabb.size)
root.free()
func _dump(n: Node, d: int, deep: bool) -> void:
var pad := ""
for i in d:
pad += " "
var extra := ""
if n is MeshInstance3D:
var mi := n as MeshInstance3D
extra = " surfaces=%d skin=%s" % [
mi.mesh.get_surface_count() if mi.mesh else 0, str(mi.skin != null)]
if mi.mesh:
for s in mi.mesh.get_surface_count():
var m := mi.mesh.surface_get_material(s)
extra += " [mat%d=%s]" % [s, m.resource_name if m else "null"]
print(pad, n.name, " <", n.get_class(), ">", extra)
if not deep and d >= 1:
return
for c in n.get_children():
_dump(c, d + 1, deep)
func _find_skel(n: Node) -> Skeleton3D:
if n is Skeleton3D:
return n
for c in n.get_children():
var r := _find_skel(c)
if r != null:
return r
return null
func _aabb(n: Node) -> AABB:
var acc := AABB()
var started := false
for mi in _meshes(n):
var a: AABB = (mi as MeshInstance3D).get_aabb()
a = (mi as Node3D).transform * a
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