Polish world, shop, and artwork presentation

This commit is contained in:
Alexander Sellite 2026-08-13 14:53:39 -04:00
parent 7d38bc962b
commit 117fdbfdaa
94 changed files with 1771 additions and 579 deletions

View file

@ -325,6 +325,25 @@ func _run() -> void:
and finalize_button != null
and close_button != null
)
assert(eraser_button.text.is_empty())
assert(eraser_button.icon != null)
assert(
eraser_button.icon.resource_path.ends_with(
"/art/art_kit_eraser.png"
)
)
for icon_button: Button in [eraser_button, undo_button]:
assert(icon_button.custom_minimum_size == Vector2(48, 44))
assert(icon_button.get_theme_constant("icon_max_width") == 40)
var icon_style := (
icon_button.get_theme_stylebox("normal") as StyleBoxFlat
)
assert(is_equal_approx(icon_style.content_margin_left, 4.0))
assert(is_equal_approx(icon_style.content_margin_top, 2.0))
assert(hide_button.button_group != null)
assert(hide_button.button_group == restore_button.button_group)
assert(hide_button.button_group == finalize_button.button_group)
assert(hide_button.button_group.allow_unpress)
assert(brush_option.item_count == 4)
assert(grid_option.item_count == 4)
assert(not brush_option.get_popup().is_item_disabled(0))
@ -367,30 +386,42 @@ func _run() -> void:
world_click.button_index = MOUSE_BUTTON_LEFT
world_click.pressed = true
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.HIDE
)
assert(service.is_placement_mode())
assert(not service.is_eraser_mode())
assert(hide_button.button_pressed)
hide_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode())
assert(service.is_eraser_mode())
assert(not service.is_placement_mode() and service.is_eraser_mode())
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.RESTORE
)
restore_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
assert(not restore_button.button_pressed)
assert(finalize_button.button_pressed)
assert(service.handle_input(world_click, true))
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
)
finalize_button.pressed.emit()
assert(
service.get_armed_guide_action()
== NetworkSurfaceDrawingService.GuideAction.NONE
)
assert(not service.is_placement_mode() and service.is_eraser_mode())
(color_buttons[&"ocean_teal"] as Button).pressed.emit()
assert(not service.is_eraser_mode())
@ -410,6 +441,32 @@ func _run() -> void:
player.hotbar.select_slot(0)
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
await process_frame
var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
var pocket_animation_player := player.get(
"_character_animation_player"
) as AnimationPlayer
var pocket_animation_name: StringName = player.get(
"_pocket_visual_animation"
)
if (
not held_art_kit_display.visible
and pocket_animation_player != null
and not pocket_animation_name.is_empty()
and pocket_animation_player.has_animation(pocket_animation_name)
):
var pocket_animation: Animation = (
pocket_animation_player.get_animation(pocket_animation_name)
)
await create_timer(pocket_animation.length * 0.55).timeout
assert(held_art_kit_display.visible)
assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
assert(is_zero_approx(angle_difference(
held_art_kit_sprite.rotation.z,
-PI * 0.5,
)))
assert(held_art_kit_sprite.flip_h)
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
assert(service.is_active() and toolbar.visible)
assert(service.get_grid_size() == 128)

View file

@ -47,6 +47,10 @@ func _run() -> void:
assert(body != null)
var body_material := body.material_override as StandardMaterial3D
assert(body_material != null)
assert(
body_material.texture_filter
== BaseMaterial3D.TEXTURE_FILTER_NEAREST
)
assert(body_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
assert(is_zero_approx(body_material.metallic))
assert(is_zero_approx(body_material.metallic_specular))

View file

@ -511,6 +511,8 @@ func _test_host_art_shop_purchase(
ArtShopStock.ART_KIT_ITEM_ID
)
assert(art_item != null and art_item.hotbar_allowed and art_item.equippable)
assert(art_item.icon != null)
assert(art_item.icon.resource_path.ends_with("/art/art_kit.png"))
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
for product_id: StringName in [
&"marker_ocean_teal", &"brush_2x", &"grid_32x",
@ -568,6 +570,41 @@ func _test_fishing_shop_sale_ui(
var shop_panel_style := shop_panel.get_theme_stylebox("panel") as StyleBoxFlat
assert(shop_panel_style != null)
assert(shop_panel_style.corner_radius_top_left == 24)
var upgrade_grid := shop.get_node("%UpgradeGrid") as GridContainer
assert(upgrade_grid != null and upgrade_grid.columns == 3)
assert((shop.get_node("%Upgrades") as Control).visible)
assert(not (shop.get_node("%Supplies") as Control).visible)
for upgrade_name: String in [
"ReelPurchase", "BarrierPurchase", "CoolerPurchase"
]:
var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
assert(upgrade_button != null)
assert(upgrade_button.size == Vector2(144.0, 144.0))
assert(upgrade_button.text.is_empty())
assert(upgrade_button.icon != null)
assert(upgrade_button.tooltip_text.contains("level"))
assert(not shop.has_node("%ReelLevel"))
assert(not shop.has_node("%BarrierEffect"))
assert(not shop.has_node("%CoolerLevel"))
var balance_coin := shop.get_node(
"ShopPanel/Margin/Layout/Header/BalanceDisplay/CoinIcon"
) as TextureRect
assert(balance_coin != null and balance_coin.texture != null)
assert(
balance_coin.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
for cost_name: String in ["ReelCost", "BarrierCost", "CoolerCost"]:
var cost_display := shop.get_node("%%%s" % cost_name) as CurrencyAmount
assert(cost_display != null)
var cost_icon := cost_display.get_node("Icon") as TextureRect
assert(cost_icon != null and cost_icon.texture != null)
assert(
cost_icon.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
assert(not shop.has_node("ShopPanel/Margin/Layout/ModeTabs"))
var shop_tabs: Array = shop.get("_shop_tabs") as Array
assert(shop_tabs.size() == 6)
@ -582,6 +619,37 @@ func _test_fishing_shop_sale_ui(
if child is Label:
stock_sections.append((child as Label).text)
assert(stock_sections == ["art kit", "markers", "brushes", "grids"])
var marker_icons := shop.find_children(
"MarkerIcon", "TextureRect", true, false
)
assert(marker_icons.size() == ArtShopStock.MARKER_PRODUCTS.size())
for index: int in marker_icons.size():
var marker_icon := marker_icons[index] as TextureRect
var marker_material := marker_icon.material as ShaderMaterial
var product_id: StringName = ArtShopStock.MARKER_PRODUCTS[index]
var color_id: StringName = PlayerArtUnlocks.color_id_for_product(
product_id
)
assert(marker_icon.texture.resource_path.ends_with("art_kit_marker.png"))
assert(marker_material != null)
assert(
marker_material.get_shader_parameter("marker_color")
== SurfaceDrawingPalette.get_color(color_id)
)
var price_bubbles := shop.find_children(
"PriceBubble", "PanelContainer", true, false
)
assert(not price_bubbles.is_empty())
for price_bubble: Node in price_bubbles:
var currency_icon := price_bubble.find_child(
"CurrencyIcon", true, false
) as TextureRect
assert(currency_icon != null and currency_icon.texture != null)
assert(
currency_icon.texture.resource_path.ends_with(
"/shop/32_currency.png"
)
)
await _activate_pointer_control(sell_mode, ui_viewport)
await process_frame
assert(shop.visible and not player_menu.visible)

View file

@ -189,6 +189,27 @@ func _validate_starter_region_surfaces() -> void:
assert(not covered_ocean.is_fishable())
assert(covered_ocean.position.y > -0.45)
# The ocean fishing layer covers the island's complete editable footprint.
# Terrain at or above the waterline remains authoritative, while shoreline
# terrain lowered below the surface becomes fishable without reshaping a
# manually authored exclusion ring.
var editable_shallows_found := 0
for x_position: int in range(-24, 24, 2):
for z_position: int in range(-21, 22, 2):
var candidate: FishingSurfaceSampleType = (
fishing_spot.resolve_fishing_surface(
Vector3(x_position, -0.45, z_position),
4.0,
)
)
if (
candidate.is_fishable()
and candidate.water_region.water_type
== WaterType.Type.SALT_WATER
):
editable_shallows_found += 1
assert(editable_shallows_found > 0)
fishing_spot.queue_free()
region.queue_free()
await process_frame

View file

@ -0,0 +1,97 @@
extends SceneTree
const ShorelineMesh: ArrayMesh = preload(
"res://world/generated/shorelines/starter_ocean_shoreline.tres"
)
const GEOMETRY_EPSILON := 0.0001
const CROSSING_NEIGHBORHOOD := 32
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
assert(ShorelineMesh.get_surface_count() == 1)
var arrays := ShorelineMesh.surface_get_arrays(0)
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
assert(not vertices.is_empty())
assert(vertices.size() % 2 == 0)
assert(indices.size() == vertices.size() * 3)
var point_count := vertices.size() / 2
var land_edge := PackedVector2Array()
var water_edge := PackedVector2Array()
for index: int in point_count:
var land := vertices[index * 2]
var water := vertices[index * 2 + 1]
land_edge.append(Vector2(land.x, land.z))
water_edge.append(Vector2(water.x, water.z))
var ribbon_width := land.distance_to(water)
assert(ribbon_width >= 0.34)
assert(ribbon_width <= 1.5)
var crossing_count := (
_count_local_crossings(water_edge)
+ _count_local_crossings(land_edge)
)
if crossing_count > 0:
push_error(
"Shoreline ribbon contains %d local edge crossings."
% crossing_count
)
quit(1)
return
print("Shoreline ribbon validation: PASS")
quit()
func _count_local_crossings(points: PackedVector2Array) -> int:
var crossing_count := 0
for first: int in points.size():
var first_next := (first + 1) % points.size()
var checked_neighbors := mini(
CROSSING_NEIGHBORHOOD,
points.size() - 2,
)
for offset: int in range(2, checked_neighbors + 1):
var second := (first + offset) % points.size()
var second_next := (second + 1) % points.size()
if second_next == first:
continue
if _segments_cross(
points[first],
points[first_next],
points[second],
points[second_next],
):
crossing_count += 1
return crossing_count
func _segments_cross(
a: Vector2,
b: Vector2,
c: Vector2,
d: Vector2,
) -> bool:
if (
maxf(a.x, b.x) < minf(c.x, d.x) - GEOMETRY_EPSILON
or maxf(c.x, d.x) < minf(a.x, b.x) - GEOMETRY_EPSILON
or maxf(a.y, b.y) < minf(c.y, d.y) - GEOMETRY_EPSILON
or maxf(c.y, d.y) < minf(a.y, b.y) - GEOMETRY_EPSILON
):
return false
var first_direction := b - a
var second_direction := d - c
var denominator := first_direction.cross(second_direction)
if absf(denominator) <= GEOMETRY_EPSILON:
return false
var offset := c - a
var first_amount := offset.cross(second_direction) / denominator
var second_amount := offset.cross(first_direction) / denominator
return (
first_amount > GEOMETRY_EPSILON
and first_amount < 1.0 - GEOMETRY_EPSILON
and second_amount > GEOMETRY_EPSILON
and second_amount < 1.0 - GEOMETRY_EPSILON
)

View file

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

View file

@ -0,0 +1,148 @@
extends SceneTree
const POLICY := preload("res://main/texture_sampling_policy.gd")
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_runtime_policy()
_validate_repository_files("res://")
if failures.is_empty():
print("Texture sampling validation passed.")
quit(0)
return
for failure: String in failures:
printerr("Texture sampling validation: ", failure)
quit(1)
func _validate_runtime_policy() -> void:
var texture_rect := TextureRect.new()
texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
POLICY.enforce_node(texture_rect)
_check(
texture_rect.texture_filter == CanvasItem.TEXTURE_FILTER_NEAREST,
"CanvasItem policy must replace linear sampling with nearest sampling.",
)
texture_rect.free()
var sprite := Sprite3D.new()
sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
POLICY.enforce_node(sprite)
_check(
sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"Sprite3D policy must replace linear sampling with nearest sampling.",
)
sprite.free()
var material := StandardMaterial3D.new()
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
var quad := QuadMesh.new()
quad.material = material
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = quad
POLICY.enforce_node(mesh_instance)
_check(
material.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"3D material policy must replace linear sampling with nearest sampling.",
)
mesh_instance.free()
func _validate_repository_files(path: String) -> void:
var directory := DirAccess.open(path)
if directory == null:
failures.append("Could not scan %s." % path)
return
directory.list_dir_begin()
while true:
var name := directory.get_next()
if name.is_empty():
break
if name == ".godot" or name == ".git":
continue
var child_path := path.path_join(name)
if directory.current_is_dir():
_validate_repository_files(child_path)
continue
if name.ends_with(".import"):
_validate_import_file(child_path)
elif name.ends_with(".gdshader"):
_validate_shader_file(child_path)
elif name.ends_with(".tscn") or name.ends_with(".tres"):
_validate_serialized_resource(child_path)
elif name.ends_with(".gd") and child_path != get_script().resource_path:
_validate_script_file(child_path)
directory.list_dir_end()
func _validate_import_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
_check(
"mipmaps/generate=true" not in source,
"Texture import generates mipmaps: %s" % path,
)
func _validate_shader_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
var declaration_start := source.find("uniform sampler")
while declaration_start >= 0:
var declaration_end := source.find(";", declaration_start)
if declaration_end < 0:
_check(false, "Incomplete sampler declaration in %s." % path)
return
var declaration := source.substr(
declaration_start,
declaration_end - declaration_start,
)
_check(
"filter_nearest" in declaration,
"Shader sampler must declare filter_nearest: %s" % path,
)
declaration_start = source.find("uniform sampler", declaration_end + 1)
func _validate_serialized_resource(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
var uses_3d_filter_enum := false
for line: String in source.split("\n"):
if line.begins_with("["):
uses_3d_filter_enum = (
("type=\"Sprite3D\"" in line)
or ("type=\"AnimatedSprite3D\"" in line)
or ("type=\"StandardMaterial3D\"" in line)
or ("type=\"ORMMaterial3D\"" in line)
)
if not line.begins_with("texture_filter = "):
continue
var value := int(line.trim_prefix("texture_filter = "))
if uses_3d_filter_enum:
_check(
value == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
"3D resource declares non-nearest filtering: %s" % path,
)
else:
_check(
value == CanvasItem.TEXTURE_FILTER_PARENT_NODE
or value == CanvasItem.TEXTURE_FILTER_NEAREST,
"Canvas resource declares non-nearest filtering: %s" % path,
)
func _validate_script_file(path: String) -> void:
var source := FileAccess.get_file_as_string(path)
_check(
"TEXTURE_FILTER_LINEAR" not in source,
"Script explicitly requests linear texture filtering: %s" % path,
)
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)

View file

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