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>
86 lines
2.8 KiB
GDScript
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
|