138 lines
4.9 KiB
GDScript
138 lines
4.9 KiB
GDScript
extends SceneTree
|
|
|
|
## Do the hand anchors move where they are told, in the frame they are told in?
|
|
##
|
|
## `grip_shift` and `fore_shift` exist because the two hand anchors could only
|
|
## ever slide along the barrel: `gun_stock` and `gun_fore` are distances along
|
|
## the weapon's own axis, so the trigger and support hands travelled up and down
|
|
## the gun and nowhere else. What could move freely was the GUN, under anchors
|
|
## that stayed put.
|
|
##
|
|
## Two things have to hold, and only the first is obvious:
|
|
##
|
|
## 1. the anchor moves by the amount asked for;
|
|
## 2. it moves in the GUN's frame, not the skeleton's. A sideways nudge has to
|
|
## stay sideways relative to the weapon whether the muzzle is pitched down
|
|
## at low ready or level at ADS — otherwise the same number means two
|
|
## different places in the two poses, and a skeleton-space implementation
|
|
## passes check 1 happily.
|
|
##
|
|
## MEASURED IN THE GUN'S FRAME, and it has to be. Taking each anchor relative to
|
|
## the one it hangs off and rotating into the current gun basis cancels the ADS
|
|
## blend and recoil kick exactly, because both move the basis and anchor
|
|
## together.
|
|
##
|
|
## godot --headless --path . -s res://debug/anchor_shift_check.gd
|
|
|
|
## All three axes, deliberately asymmetric, so an axis swap or a sign flip
|
|
## cannot pass.
|
|
const SHIFT := Vector3(0.05, -0.03, 0.02)
|
|
const TOLERANCE := 0.0015
|
|
## Long enough for the ADS blend and the hold's take-up to settle. The gun-frame
|
|
## measurement is invariant to both, but a half-blended pose is a bad place to
|
|
## be reading anything.
|
|
const SETTLE := 40
|
|
|
|
var _fails := 0
|
|
|
|
|
|
func _init() -> void:
|
|
await process_frame
|
|
await process_frame
|
|
var weapon := _first_weapon()
|
|
var data = JSON.parse_string(FileAccess.get_file_as_string(
|
|
"res://assets/characters/skins/skins.json"))
|
|
for entry in data["skins"]:
|
|
await _check(entry["id"], entry.get("model", ""), weapon)
|
|
print("\n=== ANCHOR SHIFTS ===\nFailures: %d" % _fails)
|
|
quit(1 if _fails > 0 else 0)
|
|
|
|
|
|
func _first_weapon() -> String:
|
|
var db = root.get_node("LoadoutManager").weapon_db
|
|
var ids: Array = db.keys()
|
|
ids.sort()
|
|
for id in ids:
|
|
var s: String = db[id].get("script", "")
|
|
if s != "" and ResourceLoader.exists(s):
|
|
return s
|
|
return ""
|
|
|
|
|
|
## The two anchors in the gun's own across/up/along frame:
|
|
## grip, relative to the buttstock == (0, 0, gun_stock) + grip_shift
|
|
## fore, relative to the grip == (0, 0, fore_dist) + fore_shift
|
|
func _local(pm) -> Array:
|
|
var inv: Basis = pm.dbg_gun_basis.inverse()
|
|
var grip: Vector3 = pm.dbg_grip
|
|
var fore: Vector3 = pm.dbg_fore
|
|
var stock: Vector3 = pm.dbg_stock
|
|
return [inv * (grip - stock), inv * (fore - grip)]
|
|
|
|
|
|
func _check(id: String, path: String, weapon: String) -> void:
|
|
if path == "" or not ResourceLoader.exists(path):
|
|
return
|
|
var model := SkinnedPlayerModel.new()
|
|
model.model_path = path
|
|
model.skin_id = id
|
|
root.add_child(model)
|
|
for _i in 4:
|
|
await process_frame
|
|
model.set_weapon(weapon)
|
|
for _i in 6:
|
|
await process_frame
|
|
|
|
# Both poses, because the gun's pitch differs between them and that is the
|
|
# whole point of expressing the shift in the gun's frame.
|
|
for pose in [["low ready", 0.0], ["ADS", 1.0]]:
|
|
var pm = model._pose_mod
|
|
if pm == null:
|
|
_expect(false, "'%s' has a pose layer" % id)
|
|
break
|
|
model.set_hold_tuning({})
|
|
model.update_state("ground", 0.0, false)
|
|
model.set_locomotion(0.0, 0.0, pose[1])
|
|
for _i in SETTLE:
|
|
await process_frame
|
|
var base: Array = _local(pm)
|
|
|
|
# Each anchor on its own. `fore` hangs off `grip`, so shifting the grip
|
|
# legitimately carries the support hand with it — moving where the
|
|
# trigger hand holds a rifle moves the whole rifle, handguard included.
|
|
# Testing them together would just measure that, and the first version
|
|
# of this check did, and reported the sum as a 2x error.
|
|
for which in [["grip_shift", 0, "trigger"], ["fore_shift", 1, "support"]]:
|
|
model.set_hold_tuning({which[0]: SHIFT})
|
|
for _i in 6:
|
|
await process_frame
|
|
var now: Array = _local(pm)
|
|
var moved: Vector3 = now[which[1]] - base[which[1]]
|
|
# x and y are across the barrel — the freedom that did not exist
|
|
# before. z is along it, and for the support hand the reach solver
|
|
# owns that, so it is not ours to predict.
|
|
var across := Vector2(moved.x, moved.y)
|
|
var want := Vector2(SHIFT.x, SHIFT.y)
|
|
_expect(across.distance_to(want) <= TOLERANCE,
|
|
"'%s' %s: %s anchor moved %.0f, %.0f mm across the barrel (wanted %.0f, %.0f)"
|
|
% [id, pose[0], which[2], across.x * 1000.0, across.y * 1000.0,
|
|
want.x * 1000.0, want.y * 1000.0])
|
|
|
|
model.set_hold_tuning({})
|
|
for _i in 6:
|
|
await process_frame
|
|
var back: Array = _local(pm)
|
|
var residue: Vector3 = back[which[1]] - base[which[1]]
|
|
_expect(Vector2(residue.x, residue.y).length() <= TOLERANCE,
|
|
"'%s' %s: clearing %s restores the derived anchor"
|
|
% [id, pose[0], which[0]])
|
|
model.queue_free()
|
|
|
|
|
|
func _expect(ok: bool, what: String) -> void:
|
|
if ok:
|
|
print(" OK: %s" % what)
|
|
else:
|
|
print(" FAIL: %s" % what)
|
|
_fails += 1
|