Fix 354 gdlint warnings across 65 files: 194 class-definitions-order (reorder declarations), 138 max-line-length (split long lines), 22 code issues (unused args, no-else-return, naming). Update .gdlintrc to exclude addons/ and raise max-public-methods for test files. No logic changes — declaration order, whitespace, and naming only. Ticket: #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
557 lines
21 KiB
GDScript
557 lines
21 KiB
GDScript
## Sprint 28 — CharacterVisual compositor tests (#704)
|
||
##
|
||
## Validates the character visual assembler (CharacterVisual.gd) against the
|
||
## architectural spec at docs/architecture/character-asset-organization.md.
|
||
##
|
||
## Most tests are test-first stubs: they push_warning and return when the
|
||
## compositor class or required asset files are not yet present. Once Stig's
|
||
## implementation lands, the guards are removed automatically.
|
||
##
|
||
## Test areas (per team lead focus list):
|
||
## 1. Descriptor-to-node-tree construction
|
||
## 2. Segment visibility toggling for clothing coverage
|
||
## 3. Torso variant swap (seg_torso vs seg_torso_upper)
|
||
## 4. Tint application (skin tone index → texture)
|
||
## 5. Facing rotation (set_facing / rotation change)
|
||
## 6. Edge cases: empty hair_id, no clothing, no accessories
|
||
##
|
||
## Spec: D-159 (body types), D-160 (17 bone-group regions), D-161 (head on Head bone),
|
||
## D-162 (clothing pre-baked per body type), D-164 (fork skeleton)
|
||
## Ticket: #704
|
||
class_name TestCharacterVisualSprint28
|
||
extends GdUnitTestSuite
|
||
|
||
const COMPOSITOR_PATH := "res://scripts/rendering/character_visual.gd"
|
||
const SKIN_TONES_DIR := "res://assets/characters/skin_tones/"
|
||
const BODIES_DIR := "res://assets/characters/bodies/"
|
||
|
||
const SKELETON_PATH := "res://assets/characters/skeleton/armature.glb"
|
||
|
||
## Returns a minimal valid descriptor with no optional layers.
|
||
func _make_minimal_descriptor() -> CharacterVisualDescriptor:
|
||
var d := CharacterVisualDescriptor.new()
|
||
d.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M
|
||
d.head_id = "head_001"
|
||
d.hair_id = "" # empty = no hair
|
||
d.facial_hair_id = "" # empty = none
|
||
d.eyebrow_id = "" # empty = no eyebrows
|
||
d.skin_tone = 0
|
||
return d
|
||
|
||
## Returns true if the compositor script is present and loadable.
|
||
func _compositor_available() -> bool:
|
||
if not ResourceLoader.exists(COMPOSITOR_PATH):
|
||
push_warning("TestCharacterVisualSprint28: character_visual.gd not found — test-first stub (awaiting #704)")
|
||
return false
|
||
return true
|
||
|
||
## Returns true if the skeleton GLB is present (required for segment/clothing tests).
|
||
func _skeleton_available() -> bool:
|
||
return ResourceLoader.exists(SKELETON_PATH)
|
||
|
||
## Loads and instantiates a CharacterVisual node. Returns null with warning if unavailable.
|
||
func _make_compositor() -> Node:
|
||
if not _compositor_available():
|
||
return null
|
||
var script: GDScript = load(COMPOSITOR_PATH)
|
||
if script == null:
|
||
push_warning("TestCharacterVisualSprint28: failed to load character_visual.gd")
|
||
return null
|
||
var node := Node3D.new()
|
||
node.set_script(script)
|
||
add_child(node)
|
||
return node
|
||
|
||
func after_test() -> void:
|
||
# Clean up any nodes added during testing
|
||
for child in get_children():
|
||
if child != self:
|
||
child.queue_free()
|
||
|
||
|
||
# =============================================================================
|
||
# 1. API presence — compositor class exists and has expected public interface
|
||
# =============================================================================
|
||
|
||
func test_compositor_script_file_exists() -> void:
|
||
# Gate: is the file present at all?
|
||
if not ResourceLoader.exists(COMPOSITOR_PATH):
|
||
push_warning("TestCharacterVisualSprint28: character_visual.gd missing — test-first stub")
|
||
return
|
||
assert_bool(ResourceLoader.exists(COMPOSITOR_PATH)).is_true()
|
||
|
||
|
||
func test_compositor_has_load_descriptor_method() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
assert_bool(node.has_method("load_descriptor")).override_failure_message(
|
||
"CharacterVisual must expose load_descriptor(desc: CharacterVisualDescriptor)"
|
||
).is_true()
|
||
|
||
|
||
func test_compositor_has_set_facing_method() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
assert_bool(node.has_method("set_facing")).override_failure_message(
|
||
"CharacterVisual must expose set_facing(direction: Vector2 or int)"
|
||
).is_true()
|
||
|
||
|
||
func test_compositor_is_node3d() -> void:
|
||
# Compositor must be a Node3D (3D scene tree, not 2D)
|
||
if not _compositor_available():
|
||
return
|
||
var script: GDScript = load(COMPOSITOR_PATH)
|
||
var node := Node3D.new()
|
||
node.set_script(script)
|
||
add_child(node)
|
||
assert_bool(node is Node3D).override_failure_message(
|
||
"CharacterVisual must extend Node3D"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# 2. Descriptor-to-node-tree construction
|
||
# =============================================================================
|
||
|
||
func test_load_descriptor_does_not_crash_with_minimal_desc() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
var desc := _make_minimal_descriptor()
|
||
# Must not crash — assets may be missing but the call must not throw
|
||
node.load_descriptor(desc)
|
||
assert_bool(true).is_true() # If we got here, no crash
|
||
|
||
|
||
func test_load_descriptor_produces_child_nodes() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
var desc := _make_minimal_descriptor()
|
||
node.load_descriptor(desc)
|
||
# A fully loaded compositor must have at least one child (body skeleton or armature)
|
||
# Guard: skip if assets genuinely not present yet (real test once assets land)
|
||
if node.get_child_count() == 0:
|
||
push_warning("TestCharacterVisualSprint28: no child nodes after load_descriptor — assets may not be imported yet")
|
||
return
|
||
assert_bool(node.get_child_count() > 0).is_true()
|
||
|
||
|
||
func test_reload_descriptor_clears_previous_nodes() -> void:
|
||
# Loading a second descriptor must replace the first, not accumulate nodes
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
var desc1 := _make_minimal_descriptor()
|
||
desc1.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_M
|
||
var desc2 := _make_minimal_descriptor()
|
||
desc2.body_type = CharacterVisualDescriptor.BodyType.AVERAGE_F
|
||
|
||
node.load_descriptor(desc1)
|
||
var count_after_first := node.get_child_count()
|
||
node.load_descriptor(desc2)
|
||
var count_after_second := node.get_child_count()
|
||
|
||
if count_after_first == 0:
|
||
push_warning("TestCharacterVisualSprint28: node count 0 — assets may not be present yet")
|
||
return
|
||
# Second load must produce exactly the same child count — no more, no less.
|
||
# _clear() frees and rebuilds; any deviation indicates stale nodes accumulating.
|
||
assert_int(count_after_second).override_failure_message(
|
||
"load_descriptor must produce identical child count on reload" +
|
||
" — stale nodes detected if higher, missing cleanup if lower"
|
||
).is_equal(count_after_first)
|
||
|
||
|
||
# =============================================================================
|
||
# 3. Segment visibility toggling for clothing coverage
|
||
# =============================================================================
|
||
|
||
func test_no_clothing_all_body_segments_visible() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not _skeleton_available():
|
||
push_warning("TestCharacterVisualSprint28: skeleton not found — segment visibility tests require #706 assets")
|
||
return
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {} # no clothing
|
||
node.load_descriptor(desc)
|
||
# All non-swapped segments should be visible with no clothing.
|
||
# is_segment_visible returns false for missing segments — only meaningful when skeleton loaded.
|
||
assert_bool(node.is_segment_visible("torso")).override_failure_message(
|
||
"seg_torso must be visible when no clothing covers it"
|
||
).is_true()
|
||
assert_bool(node.is_segment_visible("arm_upper_l")).is_true()
|
||
assert_bool(node.is_segment_visible("leg_upper_l")).is_true()
|
||
|
||
|
||
func test_clothing_hides_declared_segments() -> void:
|
||
# A full coverall with hides = [torso, arm_upper_l, arm_upper_r, ...] must
|
||
# hide those segments
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("is_segment_visible"):
|
||
push_warning("TestCharacterVisualSprint28: is_segment_visible not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {"torso": "coveralls_basic"}
|
||
node.load_descriptor(desc)
|
||
|
||
# Edge case: coverage.json may not exist yet — guard
|
||
if not node.has_method("get_active_coverage"):
|
||
push_warning("TestCharacterVisualSprint28: coverage integration not yet wired — skipping segment hide check")
|
||
return
|
||
|
||
# Coveralls should hide the torso segment (among others)
|
||
# The exact hidden set is declared in coverage.json — we test the contract, not the file content
|
||
var coverage: Dictionary = node.get_active_coverage("coveralls_basic")
|
||
if coverage.is_empty():
|
||
push_warning("TestCharacterVisualSprint28: coveralls_basic/coverage.json not found — stub")
|
||
return
|
||
|
||
for hidden_seg: String in coverage.get("hides", []):
|
||
assert_bool(node.is_segment_visible(hidden_seg)).override_failure_message(
|
||
"Segment '%s' declared in coverage.json hides must not be visible" % hidden_seg
|
||
).is_false()
|
||
|
||
|
||
func test_segments_not_in_hides_remain_visible() -> void:
|
||
# A jacket (upper body only) must NOT hide leg segments
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not _skeleton_available():
|
||
push_warning("TestCharacterVisualSprint28: skeleton not found — segment visibility tests require #706 assets")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {"torso": "jacket_utility"} # upper body only
|
||
node.load_descriptor(desc)
|
||
|
||
var coverage: Dictionary = node.get_active_coverage("jacket_utility") if node.has_method("get_active_coverage") else {}
|
||
if coverage.is_empty():
|
||
push_warning("TestCharacterVisualSprint28: jacket_utility/coverage.json not found — stub")
|
||
return
|
||
|
||
var hides: Array = coverage.get("hides", [])
|
||
if not "leg_upper_l" in hides:
|
||
# Jacket doesn't cover legs — legs must still be visible
|
||
assert_bool(node.is_segment_visible("leg_upper_l")).override_failure_message(
|
||
"Leg segments must remain visible when only a jacket is worn"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# 4. Torso variant swap
|
||
# =============================================================================
|
||
|
||
func test_torso_variant_full_uses_seg_torso() -> void:
|
||
# coverage.json torso_variant: "full" → seg_torso is loaded (default)
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("get_active_torso_variant"):
|
||
push_warning("TestCharacterVisualSprint28: get_active_torso_variant not found — stub (awaiting #704)")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {} # no clothing → default torso
|
||
node.load_descriptor(desc)
|
||
|
||
assert_str(node.get_active_torso_variant()).override_failure_message(
|
||
"With no clothing, torso variant must be 'full' (uses seg_torso)"
|
||
).is_equal("full")
|
||
|
||
|
||
func test_torso_variant_upper_uses_seg_torso_upper() -> void:
|
||
# coverage.json torso_variant: "upper" → seg_torso_upper is loaded
|
||
# (e.g. tank top: exposes lower abdomen)
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("get_active_torso_variant"):
|
||
push_warning("TestCharacterVisualSprint28: get_active_torso_variant not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {"torso": "shirt_tank"} # a tank top with torso_variant: "upper"
|
||
node.load_descriptor(desc)
|
||
|
||
var coverage: Dictionary = node.get_active_coverage("shirt_tank") if node.has_method("get_active_coverage") else {}
|
||
if coverage.is_empty() or coverage.get("torso_variant") != "upper":
|
||
push_warning("TestCharacterVisualSprint28: shirt_tank/coverage.json not found or no upper variant — stub")
|
||
return
|
||
|
||
assert_str(node.get_active_torso_variant()).override_failure_message(
|
||
"Tank top with torso_variant:'upper' must load seg_torso_upper"
|
||
).is_equal("upper")
|
||
|
||
|
||
func test_torso_hidden_when_full_coverage_clothing_worn() -> void:
|
||
# A full coverall hides the torso segment entirely (not swapped to upper)
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not _skeleton_available():
|
||
push_warning("TestCharacterVisualSprint28: skeleton not found — torso hide test requires #706 assets")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {"torso": "coveralls_basic"}
|
||
node.load_descriptor(desc)
|
||
|
||
var coverage: Dictionary = node.get_active_coverage("coveralls_basic") \
|
||
if node.has_method("get_active_coverage") else {}
|
||
if coverage.is_empty():
|
||
push_warning("TestCharacterVisualSprint28: coveralls_basic/coverage.json not found — stub")
|
||
return
|
||
|
||
if "torso" in coverage.get("hides", []):
|
||
# Full coverage: torso segment is hidden, not swapped
|
||
assert_bool(node.is_segment_visible("torso")).override_failure_message(
|
||
"Full-coverage clothing with 'torso' in hides must hide seg_torso entirely"
|
||
).is_false()
|
||
|
||
|
||
# =============================================================================
|
||
# 5. Facing rotation
|
||
# =============================================================================
|
||
|
||
func test_set_facing_changes_node_rotation() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
# set_facing only writes rotation.y — no asset dependency, no load_descriptor needed
|
||
var rot_before: float = node.rotation.y
|
||
node.set_facing(Vector2(1, 0)) # east / right
|
||
var rot_after: float = node.rotation.y
|
||
# Rotation must change when facing changes (exact value is implementation-defined)
|
||
# We only check that it differs — not the exact angle
|
||
# (Different implementations may use 4-dir or 8-dir mappings)
|
||
# Guard: allow no-op only if facing east == default (unlikely)
|
||
if rot_before == rot_after:
|
||
# Try a definitely-different facing
|
||
node.set_facing(Vector2(-1, 0)) # west
|
||
var rot_west: float = node.rotation.y
|
||
assert_bool(rot_west != rot_before).override_failure_message(
|
||
"set_facing must rotate the node — east and west must produce different rotations"
|
||
).is_true()
|
||
|
||
|
||
func test_set_facing_opposite_directions_differ() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
# set_facing only writes rotation.y — no asset dependency
|
||
node.set_facing(Vector2(0, -1)) # north
|
||
var rot_north: float = node.rotation.y
|
||
node.set_facing(Vector2(0, 1)) # south
|
||
var rot_south: float = node.rotation.y
|
||
|
||
assert_bool(rot_north != rot_south).override_failure_message(
|
||
"North and south facing must produce different rotation values"
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# 6. Edge cases
|
||
# =============================================================================
|
||
|
||
func test_empty_hair_id_adds_no_hair_node() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("has_slot_node"):
|
||
push_warning("TestCharacterVisualSprint28: has_slot_node not found — stub (awaiting #704)")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.hair_id = "" # explicit empty = no hair
|
||
node.load_descriptor(desc)
|
||
|
||
assert_bool(node.has_slot_node("hair")).override_failure_message(
|
||
"Empty hair_id must not add a hair node to the character tree"
|
||
).is_false()
|
||
|
||
|
||
func test_bald_hair_id_adds_no_hair_node() -> void:
|
||
# "bald" is the explicit key for no-hair — treated identically to empty string.
|
||
# Stig: `if desc.hair_id.is_empty() or desc.hair_id == "bald": return`
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("has_slot_node"):
|
||
push_warning("TestCharacterVisualSprint28: has_slot_node not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.hair_id = "bald"
|
||
node.load_descriptor(desc)
|
||
|
||
assert_bool(node.has_slot_node("hair")).override_failure_message(
|
||
"hair_id='bald' must not add a hair node (bald is the explicit no-hair sentinel)"
|
||
).is_false()
|
||
|
||
|
||
func test_empty_facial_hair_id_adds_no_facial_hair_node() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("has_slot_node"):
|
||
push_warning("TestCharacterVisualSprint28: has_slot_node not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.facial_hair_id = ""
|
||
node.load_descriptor(desc)
|
||
|
||
assert_bool(node.has_slot_node("facial_hair")).override_failure_message(
|
||
"Empty facial_hair_id must not add a facial hair node"
|
||
).is_false()
|
||
|
||
|
||
func test_no_clothing_slots_leaves_no_clothing_nodes() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("get_clothing_node_count"):
|
||
push_warning("TestCharacterVisualSprint28: get_clothing_node_count not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.clothing_slots = {}
|
||
node.load_descriptor(desc)
|
||
|
||
assert_int(node.get_clothing_node_count()).override_failure_message(
|
||
"No clothing slots in descriptor must produce zero clothing nodes in the scene tree"
|
||
).is_equal(0)
|
||
|
||
|
||
func test_no_accessories_adds_no_accessory_nodes() -> void:
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("get_accessory_node_count"):
|
||
push_warning("TestCharacterVisualSprint28: get_accessory_node_count not found — stub")
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.accessory_slots = {}
|
||
node.load_descriptor(desc)
|
||
|
||
assert_int(node.get_accessory_node_count()).override_failure_message(
|
||
"No accessory slots in descriptor must produce zero accessory nodes"
|
||
).is_equal(0)
|
||
|
||
|
||
func test_missing_asset_file_does_not_crash() -> void:
|
||
# If an asset file referenced by the descriptor is missing from disk,
|
||
# the compositor must degrade gracefully (skip the missing slot, not crash).
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
|
||
var desc := _make_minimal_descriptor()
|
||
desc.hair_id = "hair_does_not_exist_zzz" # guaranteed non-existent
|
||
# Must not crash — should either skip the slot or substitute a placeholder
|
||
node.load_descriptor(desc)
|
||
assert_bool(true).is_true() # survived = pass
|
||
|
||
|
||
func test_all_11_body_types_load_without_crash() -> void:
|
||
# Each of the 11 body types must not cause load_descriptor to crash.
|
||
# Assets may be missing — but the compositor must handle it gracefully.
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
|
||
var bt := CharacterVisualDescriptor.BodyType
|
||
for variant in [bt.THIN_M, bt.THIN_F, bt.AVERAGE_M, bt.AVERAGE_F,
|
||
bt.MUSCULAR_M, bt.MUSCULAR_F, bt.TEEN_M, bt.TEEN_F,
|
||
bt.HEAVY_M, bt.HEAVY_F, bt.CHILD]:
|
||
var desc := _make_minimal_descriptor()
|
||
desc.body_type = variant
|
||
node.load_descriptor(desc)
|
||
# If we get here without a crash, all 11 body types were handled
|
||
assert_bool(true).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# 7. Skin tone application
|
||
# =============================================================================
|
||
|
||
func test_skin_tone_index_resolves_to_correct_texture_name() -> void:
|
||
# The 9 skin tone textures are indexed 0–8 in this order (per architecture doc):
|
||
# 0:pale_cool, 1:pale_warm, 2:light_olive, 3:medium_golden,
|
||
# 4:olive_warm, 5:medium_brown, 6:deep_brown, 7:very_deep_warm, 8:very_deep_cool
|
||
var node := _make_compositor()
|
||
if node == null:
|
||
return
|
||
if not node.has_method("get_skin_tone_texture_name"):
|
||
push_warning("TestCharacterVisualSprint28: get_skin_tone_texture_name not found — stub (awaiting #704)")
|
||
return
|
||
|
||
# Spot-check a few indices
|
||
assert_str(node.get_skin_tone_texture_name(0)).override_failure_message(
|
||
"Skin tone index 0 must resolve to pale_cool.png"
|
||
).is_equal("pale_cool")
|
||
assert_str(node.get_skin_tone_texture_name(8)).override_failure_message(
|
||
"Skin tone index 8 must resolve to very_deep_cool.png"
|
||
).is_equal("very_deep_cool")
|
||
|
||
|
||
func test_skin_tone_textures_exist_on_disk() -> void:
|
||
# Verify all 9 expected skin tone PNGs are present (copied from spike in #702)
|
||
var expected := [
|
||
"pale_cool.png", "pale_warm.png", "light_olive.png", "medium_golden.png",
|
||
"olive_warm.png", "medium_brown.png", "deep_brown.png",
|
||
"very_deep_warm.png", "very_deep_cool.png",
|
||
]
|
||
for filename: String in expected:
|
||
var path := SKIN_TONES_DIR + filename
|
||
assert_bool(ResourceLoader.exists(path)).override_failure_message(
|
||
"Skin tone texture missing: %s (should have been copied in #702)" % path
|
||
).is_true()
|
||
|
||
|
||
# =============================================================================
|
||
# 8. Path construction invariants (pure logic, no assets required)
|
||
# =============================================================================
|
||
|
||
func test_body_type_key_produces_expected_path_fragment() -> void:
|
||
# Confirm that CharacterVisualDescriptor.body_type_key() returns the right
|
||
# string for use in path construction (this is the bridge between descriptor and compositor)
|
||
var d := CharacterVisualDescriptor.new()
|
||
d.body_type = CharacterVisualDescriptor.BodyType.MUSCULAR_F
|
||
assert_str(d.body_type_key()).is_equal("muscular_f")
|
||
|
||
|
||
func test_all_body_type_keys_match_expected_directory_names() -> void:
|
||
# The compositor derives paths as bodies/{body_type_key}/seg_{segment}.glb
|
||
# Every key must be lowercase and match the expected directory name convention
|
||
var expected := {
|
||
CharacterVisualDescriptor.BodyType.THIN_M: "thin_m",
|
||
CharacterVisualDescriptor.BodyType.THIN_F: "thin_f",
|
||
CharacterVisualDescriptor.BodyType.AVERAGE_M: "average_m",
|
||
CharacterVisualDescriptor.BodyType.AVERAGE_F: "average_f",
|
||
CharacterVisualDescriptor.BodyType.MUSCULAR_M: "muscular_m",
|
||
CharacterVisualDescriptor.BodyType.MUSCULAR_F: "muscular_f",
|
||
CharacterVisualDescriptor.BodyType.TEEN_M: "teen_m",
|
||
CharacterVisualDescriptor.BodyType.TEEN_F: "teen_f",
|
||
CharacterVisualDescriptor.BodyType.HEAVY_M: "heavy_m",
|
||
CharacterVisualDescriptor.BodyType.HEAVY_F: "heavy_f",
|
||
CharacterVisualDescriptor.BodyType.CHILD: "child",
|
||
}
|
||
var d := CharacterVisualDescriptor.new()
|
||
for body_type: int in expected:
|
||
d.body_type = body_type
|
||
assert_str(d.body_type_key()).override_failure_message(
|
||
"BodyType %d must produce key '%s'" % [body_type, expected[body_type]]
|
||
).is_equal(expected[body_type])
|