Guard gear fit with headless and Blender regression checks

The Python fit test now caps the glasses at 0.70x head width and requires a
positive bag clearance on both the coat and dress. The Godot character suite
loads head/back gear on six roles and checks attach parents, head width and
brow bounds, and that back gear reaches beyond the 0.26 m body silhouette.
Rebuilt and re-baked all 18 gear assets; no-op GLB export churn was reverted.
Headless 229/229, Python 87/87.
This commit is contained in:
zwitschi committed 2026-11-01 18:04:51 +01:00
1 parent a478bfbf35
commit 1b319c48b7
5 files changed
+82 -8

No files matched your search

+62
View File
@@ -191,6 +191,68 @@ func test_library_gear_attaches_to_the_contract_points() -> bool:
responder.free()
return a and b and c and d
## Horizontal bounds of a gear GLB in its attach-node space. The head/back
## contract is measured against the Blender fit report (head width 0.315 m,
## widest body silhouette 0.26 m); this catches a gear shrinking back inside
## the body or a cap sinking below the brow.
func _gear_bounds(node: Node3D) -> AABB:
var box := AABB()
var first := true
for child in node.find_children("*", "MeshInstance3D", true, false):
var mi := child as MeshInstance3D
var b: AABB = mi.get_aabb()
# Gear meshes have no extra transform below the attach node.
if first:
box = b
first = false
else:
box = box.merge(b)
return box
func test_head_and_back_gear_clear_the_body_silhouette() -> bool:
const HEAD_WIDTH := 0.315
const WIDEST_BODY_RADIUS := 0.26
var problems: Array[String] = []
var seen := {}
for npc_type in [Rules.NpcType.TOURIST, Rules.NpcType.INFLUENCER,
Rules.NpcType.RESPONDER, Rules.NpcType.PERFORMER,
Rules.NpcType.DELIVERY, Rules.NpcType.TRESPASSER]:
var avatar := _avatar(npc_type, 5)
for child in avatar.find_children("Gear_*", "Node3D", true, false):
var gear := child as Node3D
var name := gear.name.trim_prefix("Gear_")
var attach := str(NpcAvatar.GEAR_ATTACH.get(name, ""))
if attach != "head" and attach != "back":
continue
seen[attach + ":" + name] = true
var box := _gear_bounds(gear)
if attach == "head":
var parent_name := gear.get_parent().name
if parent_name != "Head":
problems.append("%s is not under Head" % name)
if box.size.x > HEAD_WIDTH * 1.70:
problems.append("%s is %.3f m wide (head %.3f)" % [
name, box.size.x, HEAD_WIDTH])
# The hood wraps the neck; all other headgear must start on the skull.
if name != "hood" and box.position.y < 0.0:
problems.append("%s starts %.3f m below the head origin" % [
name, box.position.y])
else:
if gear.get_parent().name != "Rig":
problems.append("%s is not under Rig" % name)
var outer_x := maxf(absf(box.position.x), absf(box.end.x))
var outer_z := maxf(absf(box.position.z), absf(box.end.z))
if maxf(outer_x, outer_z) < WIDEST_BODY_RADIUS:
problems.append("%s only reaches %.3f m (body %.3f)" % [
name, maxf(outer_x, outer_z), WIDEST_BODY_RADIUS])
avatar.free()
for required in ["head:sunglasses", "head:firefighter_helmet", "head:cap",
"back:shoulder_bag", "back:backpack", "back:scba_tank"]:
if not seen.has(required):
problems.append("missing %s" % required)
return TestUtils.check(problems.is_empty(),
"head/back gear clears the body silhouette (%s)" % str(problems))
func test_fixed_detail_materials_are_not_tinted() -> bool:
var avatar := _avatar(Rules.NpcType.RESPONDER, 3)
var mi := avatar.find_child("npc_base", true, false) as MeshInstance3D