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:
1 parent
a478bfbf35
commit
1b319c48b7
5 files changed
+82
-8
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user