Files
settled-reach/client/tests/test_garment_engine_t1089.gd
T
jpmschweitzerandClaude Fable 5 c765efd54e feat(assets): wardrobe wave 1 — per-body shells, 8 garments, try-on UI (T-1089)
Infra: offset-shell gains --per-body mode (each body's own segments, cut/mask
thresholds derived from that body's bone landmarks — reproduces the
hand-calibrated reference constants exactly on average_m); compositor prefers
<body>_mask.png with reference_mask.png fallback; tshirt re-authored per-body
on all 11 (the Q-060 torso poke-through class is GONE — residual flags are a
sleeve-hem epsilon artifact on thick arms, offset-insensitive, documented).

Garments (all per-body x 11, chromakey-gated <=150px worst, previewed):
hoodie (hood-down roll, kangaroo pocket, logo), button-down (collar/placket),
shorts, jeans (analytic denim field driving albedo+mask together; boundary
weld + open-rim flattening — real segment-splitter findings), formal pants,
jacket (over-shirt standoff, zip), suit_jacket_black (lapel region, tintable
shirt triangle — the hand-author proof), uniform_utility (11-segment
coverall, gap-free waist join by construction, 4-zone showcase, logo patch).

Try-on UI: creation screen shows per-region tint pickers (multi_region
garments) + logo picker (logo_capable + logos/*.png scan), data-driven off
manifest+coverage. Manifest merged by the lead: 12 clothing entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 00:35:20 +02:00

181 lines
7.3 KiB
GDScript

## Tests for the T-1089 wardrobe engine: the toon_garment multi-region tint +
## logo pipeline and the proof garment (tshirt_modern).
##
## Covers what is verifiable headless:
## 1. The new toon_garment.gdshader loads/compiles.
## 2. The region-mask CHANNEL MATH — pure GDScript replication of the shader's
## RGBA→4-tint channel blend, asserting channel-blended (not indexed) routing.
## 3. The manifest schema extension (style tag + logo_capable flag).
## 4. coverage.json shape.
## 5. Every fitted body variant loads as a skinned garment mesh.
## 6. The logo + region-mask textures load.
##
## The shader itself runs on the GPU, so its arithmetic is re-implemented here in
## GDScript and asserted against the same convention the .gdshader uses:
## region_tint = m.r*t0 + m.g*t1 + m.b*t2 + m.a*t3
## Ref: T-1089 gaps G3 (multi-region tint) + G4 (logo decal), D-251.
class_name TestGarmentEngineT1089
extends GdUnitTestSuite
const SHADER_PATH := "res://assets/characters/shaders/toon_garment.gdshader"
const CLOTHING_DIR := "res://assets/characters/clothing/tshirt_modern/"
const LOGO_PATH := "res://assets/characters/logos/thrds.png"
const MANIFEST_PATH := "res://assets/characters/manifest.json"
## All 11 body types — tshirt_modern is authored PER BODY (offset-shell mode,
## T-1089): each body gets its own <body>.glb + <body>_mask.png.
const FITTED_BODIES := [
"average_m", "average_f", "muscular_m", "muscular_f", "thin_m", "thin_f",
"heavy_m", "heavy_f", "teen_m", "teen_f", "child",
]
# --- 1. Shader loads -------------------------------------------------------
func test_toon_garment_shader_loads() -> void:
assert_bool(ResourceLoader.exists(SHADER_PATH)).override_failure_message(
"toon_garment.gdshader missing at %s" % SHADER_PATH
).is_true()
var shader := load(SHADER_PATH) as Shader
assert_object(shader).is_not_null()
assert_str(shader.code).contains("region_mask")
assert_str(shader.code).contains("logo_tex")
assert_str(shader.code).contains("UV2")
func test_toon_garment_binds_to_material() -> void:
# A ShaderMaterial with all four tints + logo params must accept them.
var shader := load(SHADER_PATH) as Shader
var mat := ShaderMaterial.new()
mat.shader = shader
mat.set_shader_parameter("tint_0", Color.RED)
mat.set_shader_parameter("tint_1", Color.GREEN)
mat.set_shader_parameter("tint_2", Color.BLUE)
mat.set_shader_parameter("tint_3", Color.WHITE)
mat.set_shader_parameter("logo_enabled", 1.0)
assert_object(mat.get_shader_parameter("tint_2")).is_equal(Color.BLUE)
# --- 2. Region-mask channel math (the testable shader logic) ---------------
## Pure-GDScript replication of the shader's per-texel region tint selection.
func _region_tint(mask: Color, t0: Color, t1: Color, t2: Color, t3: Color) -> Color:
var r := t0 * mask.r + t1 * mask.g + t2 * mask.b + t3 * mask.a
return Color(r.r, r.g, r.b, 1.0)
func test_pure_channel_routes_to_its_tint() -> void:
var t0 := Color(0.1, 0.2, 0.3)
var t1 := Color(0.4, 0.5, 0.6)
var t2 := Color(0.7, 0.8, 0.9)
var t3 := Color(0.0, 0.0, 0.0)
# Pure collar (R) -> tint_0, body (G) -> tint_1, sleeve (B) -> tint_2
assert_bool(_region_tint(Color(1, 0, 0, 0), t0, t1, t2, t3).is_equal_approx(t0)).is_true()
assert_bool(_region_tint(Color(0, 1, 0, 0), t0, t1, t2, t3).is_equal_approx(t1)).is_true()
assert_bool(_region_tint(Color(0, 0, 1, 0), t0, t1, t2, t3).is_equal_approx(t2)).is_true()
func test_boundary_channel_blends_not_indexes() -> void:
# The whole point of G3: a bilinear boundary texel (R=0.5,G=0.5) must produce
# a MIDPOINT of the two tints — a crossfade — not snap to one index.
var blended := _region_tint(
Color(0.5, 0.5, 0.0, 0.0), Color(1, 0, 0), Color(0, 0, 1), Color.BLACK, Color.BLACK
)
assert_bool(blended.is_equal_approx(Color(0.5, 0.0, 0.5))).override_failure_message(
"boundary texel must crossfade tints, got %s" % blended
).is_true()
func test_zero_mask_contributes_no_tint() -> void:
var out := _region_tint(Color(0, 0, 0, 0), Color.RED, Color.GREEN, Color.BLUE, Color.WHITE)
assert_bool(out.is_equal_approx(Color.BLACK)).is_true()
# --- 3. Manifest schema extension ------------------------------------------
func test_manifest_has_tshirt_with_style_and_logo_flag() -> void:
var f := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
assert_object(f).is_not_null()
var data: Variant = JSON.parse_string(f.get_as_text())
f.close()
assert_bool(data is Dictionary).is_true()
var clothing: Dictionary = data["clothing"]
assert_bool(clothing.has("tshirt_modern")).override_failure_message(
"manifest.clothing missing tshirt_modern entry"
).is_true()
var entry: Dictionary = clothing["tshirt_modern"]
assert_str(entry.get("slot", "")).is_equal("torso")
assert_str(entry.get("style", "")).is_equal("modern")
assert_bool(entry.get("logo_capable", false)).is_true()
# --- 4. coverage.json ------------------------------------------------------
func test_coverage_json_shape() -> void:
var path := CLOTHING_DIR + "coverage.json"
var f := FileAccess.open(path, FileAccess.READ)
assert_object(f).is_not_null()
var cov: Variant = JSON.parse_string(f.get_as_text())
f.close()
assert_bool(cov is Dictionary).is_true()
assert_bool((cov as Dictionary).has("hides")).is_true()
assert_array((cov as Dictionary)["hides"]).contains(["torso"])
assert_str((cov as Dictionary).get("torso_variant", "")).is_equal("full")
# --- 5. Fitted variants load as skinned garment meshes ---------------------
func test_all_fitted_bodies_load_skinned() -> void:
for body in FITTED_BODIES:
var glb := CLOTHING_DIR + "%s.glb" % body
assert_bool(ResourceLoader.exists(glb)).override_failure_message(
"fitted variant missing: %s" % glb
).is_true()
var scene := load(glb) as PackedScene
assert_object(scene).override_failure_message(
"variant %s failed to load as PackedScene" % body
).is_not_null()
var inst := scene.instantiate()
var skinned := _has_skinned_mesh(inst)
inst.queue_free()
assert_bool(skinned).override_failure_message(
"variant %s has no skinned MeshInstance3D (cannot animate)" % body
).is_true()
func _has_skinned_mesh(root: Node) -> bool:
var stack: Array[Node] = [root]
while not stack.is_empty():
var n: Node = stack.pop_back()
for c in n.get_children():
stack.append(c)
if n is MeshInstance3D and (n as MeshInstance3D).skin != null:
return true
return false
# --- 6. Decal + mask textures load -----------------------------------------
func test_logo_and_mask_textures_load() -> void:
assert_bool(ResourceLoader.exists(LOGO_PATH)).is_true()
# Runtime fallback mask (SD-fit garments + safety net) must still exist.
assert_bool(ResourceLoader.exists(CLOTHING_DIR + "reference_mask.png")).is_true()
var logo := load(LOGO_PATH) as Texture2D
assert_object(logo).is_not_null()
var mask := load(CLOTHING_DIR + "reference_mask.png") as Texture2D
assert_object(mask).is_not_null()
func test_per_body_masks_load() -> void:
# Per-body offset-shell authoring: the compositor prefers
# <body>_mask.png over reference_mask.png (character_visual.gd).
for body in FITTED_BODIES:
var mask_path := CLOTHING_DIR + "%s_mask.png" % body
assert_bool(ResourceLoader.exists(mask_path)).override_failure_message(
"per-body region mask missing: %s" % mask_path
).is_true()
var mask := load(mask_path) as Texture2D
assert_object(mask).override_failure_message(
"per-body region mask failed to load: %s" % mask_path
).is_not_null()