Polish world, shop, and artwork presentation
This commit is contained in:
parent
7d38bc962b
commit
117fdbfdaa
94 changed files with 1771 additions and 579 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
97
tests/shoreline_ribbon_validation.gd
Normal file
97
tests/shoreline_ribbon_validation.gd
Normal 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
|
||||
)
|
||||
1
tests/shoreline_ribbon_validation.gd.uid
Normal file
1
tests/shoreline_ribbon_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c73frygpfhred
|
||||
148
tests/texture_sampling_validation.gd
Normal file
148
tests/texture_sampling_validation.gd
Normal 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)
|
||||
1
tests/texture_sampling_validation.gd.uid
Normal file
1
tests/texture_sampling_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dqm0j1xnkeox1
|
||||
Loading…
Add table
Add a link
Reference in a new issue