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>
88 lines
2.6 KiB
GDScript
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
|