Consolidate project documentation and attribution
Add public-alias credits and a tested in-game credits page, centralize project guidance and asset provenance, import the complete authored fur channel maps, and keep starter-island collision synchronized with imported props.
This commit is contained in:
parent
1940ae13e5
commit
db6074ddfa
59 changed files with 1638 additions and 1032 deletions
|
|
@ -67,12 +67,26 @@ func _run() -> void:
|
|||
assert(fox_arm_pattern_texture != null)
|
||||
assert(fox_arm_pattern_texture.get_width() == 1024)
|
||||
assert(fox_arm_pattern_texture.get_height() == 1024)
|
||||
assert(
|
||||
var bengal_head_texture := (
|
||||
CharacterCustomizationCatalog.fur_pattern_texture(
|
||||
"spots_bengal",
|
||||
"head_round",
|
||||
) == null
|
||||
"spots_bengal", "head_round"
|
||||
)
|
||||
)
|
||||
assert(bengal_head_texture != null)
|
||||
assert(bengal_head_texture.get_width() == 1024)
|
||||
assert(bengal_head_texture.get_height() == 1024)
|
||||
var fox_head_texture := CharacterCustomizationCatalog.fur_pattern_texture(
|
||||
"fox", "head_pointy"
|
||||
)
|
||||
assert(fox_head_texture != null)
|
||||
assert(fox_head_texture.get_width() == 1024)
|
||||
assert(fox_head_texture.get_height() == 1024)
|
||||
var fox_tail_texture := CharacterCustomizationCatalog.fur_pattern_texture(
|
||||
"fox", "tails_fox"
|
||||
)
|
||||
assert(fox_tail_texture != null)
|
||||
assert(fox_tail_texture.get_width() == 1024)
|
||||
assert(fox_tail_texture.get_height() == 1024)
|
||||
|
||||
var patterned := migrated.duplicate(true)
|
||||
patterned["fur_pattern"] = "orange"
|
||||
|
|
@ -117,7 +131,36 @@ func _run() -> void:
|
|||
)
|
||||
var round_head := skeleton.get_node_or_null("head_round") as MeshInstance3D
|
||||
assert(round_head != null)
|
||||
assert(round_head.material_override is StandardMaterial3D)
|
||||
var round_head_material := round_head.material_override as ShaderMaterial
|
||||
assert(round_head_material != null)
|
||||
assert(
|
||||
round_head_material.get_shader_parameter("pattern_texture")
|
||||
== bengal_head_texture
|
||||
)
|
||||
|
||||
var fox_snapshot := patterned.duplicate(true)
|
||||
fox_snapshot["species"] = "pointy"
|
||||
fox_snapshot["fur_style"] = "fox"
|
||||
fox_snapshot["tail"] = "fox"
|
||||
PlayerVisualPresenter.apply_appearance(visuals, fox_snapshot)
|
||||
var pointy_head := (
|
||||
skeleton.get_node_or_null("head_pointy") as MeshInstance3D
|
||||
)
|
||||
assert(pointy_head != null)
|
||||
var pointy_head_material := pointy_head.material_override as ShaderMaterial
|
||||
assert(pointy_head_material != null)
|
||||
assert(
|
||||
pointy_head_material.get_shader_parameter("pattern_texture")
|
||||
== fox_head_texture
|
||||
)
|
||||
var fox_tail := skeleton.get_node_or_null("tails_fox") as MeshInstance3D
|
||||
assert(fox_tail != null)
|
||||
var fox_tail_material := fox_tail.material_override as ShaderMaterial
|
||||
assert(fox_tail_material != null)
|
||||
assert(
|
||||
fox_tail_material.get_shader_parameter("pattern_texture")
|
||||
== fox_tail_texture
|
||||
)
|
||||
|
||||
var recolored := patterned.duplicate(true)
|
||||
recolored["fur_color_3"] = "teal"
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func _run() -> void:
|
|||
runtime_terrain.get_surface_override_material(surface_index) == null,
|
||||
"Region surface %d must use its Blender material." % surface_index,
|
||||
)
|
||||
_validate_region_collision(region)
|
||||
region.free()
|
||||
_finish()
|
||||
|
||||
|
|
@ -118,6 +119,62 @@ func _validate_terrain_materials(
|
|||
)
|
||||
|
||||
|
||||
func _validate_region_collision(region: StarterIslandRegion) -> void:
|
||||
_check(
|
||||
region.rebuild_terrain_collision_on_ready,
|
||||
"Starter-island collision must rebuild from the imported GLB.",
|
||||
)
|
||||
var visual_root := region.get_node_or_null("Terrain/Visual") as Node3D
|
||||
var collision_shape := region.get_node_or_null(
|
||||
"Terrain/Collision/Shape"
|
||||
) as CollisionShape3D
|
||||
_check(visual_root != null, "Region must contain its visual hierarchy.")
|
||||
_check(collision_shape != null, "Region must contain its collision owner.")
|
||||
if visual_root == null or collision_shape == null:
|
||||
return
|
||||
var concave_shape := collision_shape.shape as ConcavePolygonShape3D
|
||||
_check(
|
||||
concave_shape != null,
|
||||
"Region collision must be a generated concave polygon shape.",
|
||||
)
|
||||
if concave_shape == null:
|
||||
return
|
||||
var expected_faces := PackedVector3Array()
|
||||
var mesh_count := _append_collision_faces(
|
||||
region,
|
||||
visual_root,
|
||||
expected_faces,
|
||||
)
|
||||
var actual_faces := concave_shape.get_faces()
|
||||
_check(mesh_count > 1, "Collision must include the imported prop meshes.")
|
||||
_check(not expected_faces.is_empty(), "Imported meshes must provide collision.")
|
||||
_check(
|
||||
actual_faces == expected_faces,
|
||||
"Generated collision must exactly match every imported mesh transform.",
|
||||
)
|
||||
|
||||
|
||||
func _append_collision_faces(
|
||||
region: StarterIslandRegion,
|
||||
node: Node,
|
||||
faces: PackedVector3Array,
|
||||
) -> int:
|
||||
var mesh_count := 0
|
||||
if node is MeshInstance3D:
|
||||
var mesh_instance := node as MeshInstance3D
|
||||
if mesh_instance.mesh != null:
|
||||
var mesh_shape := mesh_instance.mesh.create_trimesh_shape()
|
||||
if mesh_shape != null:
|
||||
mesh_count += 1
|
||||
for face_vertex: Vector3 in mesh_shape.get_faces():
|
||||
faces.append(
|
||||
region.to_local(mesh_instance.to_global(face_vertex))
|
||||
)
|
||||
for child: Node in node.get_children():
|
||||
mesh_count += _append_collision_faces(region, child, faces)
|
||||
return mesh_count
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
|
|
@ -125,9 +182,9 @@ func _check(condition: bool, message: String) -> void:
|
|||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("Blender terrain material validation: PASS")
|
||||
print("Blender terrain and collision validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in failures:
|
||||
printerr("Blender terrain material validation: ", failure)
|
||||
printerr("Blender terrain and collision validation: ", failure)
|
||||
quit(1)
|
||||
|
|
|
|||
207
tests/title_credits_validation.gd
Normal file
207
tests/title_credits_validation.gd
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
extends SceneTree
|
||||
|
||||
const TitleScreenScene := preload("res://ui/title_screen.tscn")
|
||||
const TitleCreditsPageType = preload("res://ui/title_credits_page.gd")
|
||||
|
||||
var _failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var original_size: Vector2i = root.size
|
||||
root.size = Vector2i(1280, 720)
|
||||
var title_screen := TitleScreenScene.instantiate() as TitleScreen
|
||||
root.add_child(title_screen)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var credits_button := title_screen.get_node(
|
||||
"%CreditsButton"
|
||||
) as BubbleButton
|
||||
var credits_page := title_screen.get_node(
|
||||
"%CreditsPage"
|
||||
) as TitleCreditsPageType
|
||||
var back_button: Button = credits_page.get_back_button()
|
||||
var presentation := title_screen.get_node("%Center") as Control
|
||||
var bubble_field := title_screen.get_node("%BubbleField") as Control
|
||||
title_screen.call("_update_title_layout")
|
||||
(title_screen.get_node("%ButtonCenter") as Control).show()
|
||||
bubble_field.show()
|
||||
title_screen.call("_set_title_bubbles_interactive", true)
|
||||
await process_frame
|
||||
|
||||
_expect(credits_button != null, "title menu exposes a credits button")
|
||||
_expect(credits_page != null, "title screen contains the credits page")
|
||||
_expect(not credits_page.visible, "credits page starts closed")
|
||||
_expect(
|
||||
Rect2(Vector2.ZERO, bubble_field.size).encloses(
|
||||
Rect2(credits_button.position, credits_button.size)
|
||||
),
|
||||
"credits bubble stays inside the authored title field",
|
||||
)
|
||||
_expect(
|
||||
credits_button.mouse_filter == Control.MOUSE_FILTER_STOP,
|
||||
"credits bubble accepts pointer input",
|
||||
)
|
||||
if OS.has_environment("NETFISHING_TITLE_MENU_CAPTURE"):
|
||||
(title_screen.get_node("%StartPromptCenter") as Control).hide()
|
||||
title_screen.set_process(true)
|
||||
await create_timer(0.5).timeout
|
||||
_expect(
|
||||
_save_capture(
|
||||
OS.get_environment("NETFISHING_TITLE_MENU_CAPTURE")
|
||||
) == OK,
|
||||
"title menu validation capture saves successfully",
|
||||
)
|
||||
title_screen.set_process(false)
|
||||
|
||||
title_screen.set("_navigation_focus_active", false)
|
||||
credits_button.pressed.emit()
|
||||
await process_frame
|
||||
_expect(credits_page.visible, "pointer activation opens credits")
|
||||
_expect(not presentation.visible, "credits replace the primary title menu")
|
||||
_expect(
|
||||
root.gui_get_focus_owner() != back_button,
|
||||
"pointer activation does not pseudo-select back",
|
||||
)
|
||||
await create_timer(0.25).timeout
|
||||
_expect(
|
||||
credits_page.mouse_filter == Control.MOUSE_FILTER_PASS,
|
||||
"credits page becomes pointer-interactive after entry motion",
|
||||
)
|
||||
_expect(
|
||||
back_button.mouse_filter == Control.MOUSE_FILTER_STOP,
|
||||
"credits back button accepts pointer clicks",
|
||||
)
|
||||
if OS.has_environment("NETFISHING_CREDITS_CAPTURE"):
|
||||
await process_frame
|
||||
_expect(
|
||||
_save_capture(
|
||||
OS.get_environment("NETFISHING_CREDITS_CAPTURE")
|
||||
) == OK,
|
||||
"credits validation capture saves successfully",
|
||||
)
|
||||
back_button.pressed.emit()
|
||||
await process_frame
|
||||
_expect(not credits_page.visible, "pointer back closes credits")
|
||||
_expect(presentation.visible, "closing credits restores the title menu")
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == null,
|
||||
"pointer return leaves the title menu unfocused",
|
||||
)
|
||||
|
||||
title_screen.set("_navigation_focus_active", true)
|
||||
credits_button.grab_focus()
|
||||
credits_button.pressed.emit()
|
||||
await process_frame
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == back_button,
|
||||
"keyboard or controller activation focuses the credits back button",
|
||||
)
|
||||
|
||||
var pointer_motion := InputEventMouseMotion.new()
|
||||
pointer_motion.position = Vector2(640.0, 360.0)
|
||||
title_screen._input(pointer_motion)
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == null,
|
||||
"pointer motion clears credits navigation focus",
|
||||
)
|
||||
|
||||
var navigate := InputEventAction.new()
|
||||
navigate.action = &"ui_down"
|
||||
navigate.pressed = true
|
||||
title_screen._input(navigate)
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == back_button,
|
||||
"navigation input restores credits focus",
|
||||
)
|
||||
|
||||
var cancel := InputEventAction.new()
|
||||
cancel.action = &"ui_cancel"
|
||||
cancel.pressed = true
|
||||
title_screen._input(cancel)
|
||||
await process_frame
|
||||
_expect(not credits_page.visible, "ui cancel closes credits")
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == credits_button,
|
||||
"navigation return restores focus to the credits bubble",
|
||||
)
|
||||
|
||||
var chillnfill_name := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/ChillnfillName"
|
||||
) as Label
|
||||
var voyager_name := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/VoyagerName"
|
||||
) as Label
|
||||
var endeavour_name := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/EndeavourName"
|
||||
) as Label
|
||||
var chillnfill_credit := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/ChillnfillCredit"
|
||||
) as Label
|
||||
var tekgator_name := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/TekgatorName"
|
||||
) as Label
|
||||
var tekgator_credit := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits/TekgatorCredit"
|
||||
) as Label
|
||||
var creative_credits := credits_page.get_node(
|
||||
"Paper/Margin/Layout/Columns/CreativeCredits"
|
||||
) as VBoxContainer
|
||||
_expect(voyager_name.text == "Voyager", "in-game credits use Voyager")
|
||||
_expect(
|
||||
endeavour_name.text == "Endeavour",
|
||||
"in-game credits use Endeavour",
|
||||
)
|
||||
_expect(
|
||||
chillnfill_name.text == "chillnfill",
|
||||
"in-game credits name chillnfill",
|
||||
)
|
||||
_expect(
|
||||
"trees and bridges" in chillnfill_credit.text.to_lower(),
|
||||
"in-game credits describe the supplied model families",
|
||||
)
|
||||
_expect(
|
||||
tekgator_name.text == "Tekgator",
|
||||
"in-game credits name Tekgator",
|
||||
)
|
||||
_expect(
|
||||
"shop icon" in tekgator_credit.text.to_lower(),
|
||||
"in-game credits describe Tekgator's shop icon artwork",
|
||||
)
|
||||
_expect(
|
||||
Rect2(Vector2.ZERO, creative_credits.size).encloses(
|
||||
Rect2(tekgator_credit.position, tekgator_credit.size)
|
||||
),
|
||||
"Tekgator's credit stays inside the authored credits column",
|
||||
)
|
||||
|
||||
title_screen.queue_free()
|
||||
root.size = original_size
|
||||
await process_frame
|
||||
await process_frame
|
||||
if _failures.is_empty():
|
||||
print("Title credits validation: PASS")
|
||||
quit(0)
|
||||
return
|
||||
for failure: String in _failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
_failures.append(message)
|
||||
|
||||
|
||||
func _save_capture(path: String) -> Error:
|
||||
var viewport_texture: Texture2D = root.get_texture()
|
||||
if viewport_texture == null:
|
||||
return ERR_UNAVAILABLE
|
||||
return viewport_texture.get_image().save_png(path)
|
||||
1
tests/title_credits_validation.gd.uid
Normal file
1
tests/title_credits_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dimq4ehtlw64n
|
||||
Loading…
Add table
Add a link
Reference in a new issue