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:
Alexander Sellite 2026-08-12 22:12:11 -04:00
parent 1940ae13e5
commit db6074ddfa
59 changed files with 1638 additions and 1032 deletions

View file

@ -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"

View file

@ -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)

View 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)

View file

@ -0,0 +1 @@
uid://dimq4ehtlw64n