Improve UI scaling and fishing commerce
This commit is contained in:
parent
2de96dd7bb
commit
6b8c3eb2f7
29 changed files with 1515 additions and 817 deletions
|
|
@ -47,26 +47,22 @@ func _run() -> void:
|
|||
main.get_node("%PlayerAssetReservationService")
|
||||
as PlayerAssetReservationService
|
||||
)
|
||||
var pelican := (
|
||||
main.get("_test_world") as TestWorld
|
||||
).get_pelican_convenience_landmark()
|
||||
assert(player != null)
|
||||
assert(catalog != null and catalog.candidates.size() == 8)
|
||||
assert(sale_service != null)
|
||||
assert(shop_service != null)
|
||||
assert(session != null and session.is_host())
|
||||
assert(reservations != null)
|
||||
assert(pelican != null)
|
||||
sale_service.local_sale_finished.connect(_on_sale_finished)
|
||||
shop_service.local_purchase_finished.connect(_on_shop_finished)
|
||||
player.global_position = pelican.global_position
|
||||
await process_frame
|
||||
|
||||
_test_each_species(player, catalog, sale_service)
|
||||
_test_multi_sale(player, catalog, sale_service)
|
||||
_test_reservations(player, catalog, sale_service, reservations)
|
||||
_test_rejection_cleanup(player, catalog, sale_service, pelican)
|
||||
await _test_player_menu_sale(main, player, catalog, sale_service, pelican)
|
||||
_test_anywhere_sale(player, catalog, sale_service)
|
||||
await _test_player_menu_sale(main, player, catalog, sale_service)
|
||||
await _test_host_shop_sale(main, player, catalog, sale_service)
|
||||
|
||||
assert(session.set_host_open(true))
|
||||
assert(session.state == NetworkSession.State.OPEN_HOST)
|
||||
|
|
@ -76,6 +72,9 @@ func _run() -> void:
|
|||
assert(session.set_host_open(false))
|
||||
|
||||
await _test_host_shop_purchase(main, player, shop_service)
|
||||
await _test_fishing_shop_sale_ui(
|
||||
main, player, catalog, sale_service, reservations
|
||||
)
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
assert(not shop_service.is_local_purchase_pending())
|
||||
|
||||
|
|
@ -111,14 +110,9 @@ func _run_multiplayer_host() -> void:
|
|||
assert(remote_peer_id > 1)
|
||||
var spawn_service := main.get("_player_spawn_service") as PlayerSpawnService
|
||||
var remote_avatar := spawn_service.get_avatar(remote_peer_id)
|
||||
var pelican := (
|
||||
main.get("_test_world") as TestWorld
|
||||
).get_pelican_convenience_landmark()
|
||||
assert(remote_avatar != null and pelican != null)
|
||||
# The client cannot teleport its authoritative avatar. Place the host-owned
|
||||
# test avatar at each interaction so this fixture validates the remote
|
||||
# transaction path without conflating it with the movement suite.
|
||||
remote_avatar.global_position = pelican.global_position
|
||||
assert(remote_avatar != null)
|
||||
# Pelican sales are available from the Cooler anywhere. The host-owned
|
||||
# avatar only moves when the later Fishing Shop purchase needs proximity.
|
||||
await create_timer(5.0).timeout
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
assert(interaction != null)
|
||||
|
|
@ -156,10 +150,6 @@ func _run_multiplayer_client() -> void:
|
|||
var shop_service := main.get_node("%NetworkShopService") as NetworkShopService
|
||||
sale_service.local_sale_finished.connect(_on_sale_finished)
|
||||
shop_service.local_purchase_finished.connect(_on_shop_finished)
|
||||
var pelican := (
|
||||
main.get("_test_world") as TestWorld
|
||||
).get_pelican_convenience_landmark()
|
||||
player.global_position = pelican.global_position
|
||||
var catch_ids: Array[StringName] = []
|
||||
for fish: FishData in catalog.candidates:
|
||||
var fish_catch := _make_catch(fish)
|
||||
|
|
@ -182,6 +172,28 @@ func _run_multiplayer_client() -> void:
|
|||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
player.global_position = interaction.global_position
|
||||
await create_timer(5.5).timeout
|
||||
var shop_catch := _make_catch(catalog.candidates.front())
|
||||
player.inventory.add_catch(shop_catch)
|
||||
var shop_sale_balance_before: int = player.wallet.get_balance()
|
||||
_sale_result.clear()
|
||||
assert(not sale_service.request_local_sale(
|
||||
[shop_catch.catch_id],
|
||||
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||
).is_empty())
|
||||
var shop_sale_deadline: int = Time.get_ticks_msec() + 10000
|
||||
while Time.get_ticks_msec() < shop_sale_deadline:
|
||||
await process_frame
|
||||
if not _sale_result.is_empty():
|
||||
break
|
||||
print("Client shop sale result: ", _sale_result)
|
||||
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
||||
assert(not player.inventory.contains_catch_id(shop_catch.catch_id))
|
||||
assert(
|
||||
player.wallet.get_balance()
|
||||
== shop_sale_balance_before + shop_catch.sale_value
|
||||
)
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
|
||||
assert(player.wallet.credit(100))
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_supply(&"coffee").is_empty())
|
||||
|
|
@ -277,34 +289,90 @@ func _test_reservations(
|
|||
)
|
||||
|
||||
|
||||
func _test_rejection_cleanup(
|
||||
func _test_anywhere_sale(
|
||||
player: Player,
|
||||
catalog: FishPool,
|
||||
sale_service: NetworkSaleService,
|
||||
pelican: Node3D,
|
||||
) -> void:
|
||||
var fish_catch := _make_catch(catalog.candidates[5])
|
||||
player.inventory.add_catch(fish_catch)
|
||||
player.global_position = pelican.global_position + Vector3(20.0, 0.0, 0.0)
|
||||
_assert_sale(player, sale_service, [fish_catch.catch_id], false)
|
||||
assert(player.inventory.contains_catch_id(fish_catch.catch_id))
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
player.global_position = pelican.global_position
|
||||
# The spawn point is intentionally far from the Pelican landmark. Discounted
|
||||
# Pelican sales are a convenience action and do not require proximity.
|
||||
player.global_position = Vector3(0.0, 3.95, 13.0)
|
||||
_assert_sale(player, sale_service, [fish_catch.catch_id], true)
|
||||
|
||||
|
||||
func _test_host_shop_sale(
|
||||
main: Node,
|
||||
player: Player,
|
||||
catalog: FishPool,
|
||||
sale_service: NetworkSaleService,
|
||||
) -> void:
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
assert(interaction != null)
|
||||
var out_of_range_catch := _make_catch(catalog.candidates[6])
|
||||
player.inventory.add_catch(out_of_range_catch)
|
||||
player.global_position = Vector3(0.0, 3.95, 13.0)
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
_assert_sale(
|
||||
player,
|
||||
sale_service,
|
||||
[out_of_range_catch.catch_id],
|
||||
false,
|
||||
true,
|
||||
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||
)
|
||||
player.global_position = interaction.global_position
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
assert(interaction.is_avatar_in_range(player))
|
||||
var balance_before: int = player.wallet.get_balance()
|
||||
_assert_sale(
|
||||
player,
|
||||
sale_service,
|
||||
[out_of_range_catch.catch_id],
|
||||
true,
|
||||
true,
|
||||
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||
)
|
||||
assert(
|
||||
player.wallet.get_balance()
|
||||
== balance_before + out_of_range_catch.sale_value
|
||||
)
|
||||
var species_ids: Array[StringName] = []
|
||||
var expected_payout: int = 0
|
||||
for fish: FishData in catalog.candidates:
|
||||
var fish_catch := _make_catch(fish)
|
||||
player.inventory.add_catch(fish_catch)
|
||||
species_ids.append(fish_catch.catch_id)
|
||||
expected_payout += fish_catch.sale_value
|
||||
var species_balance_before: int = player.wallet.get_balance()
|
||||
_assert_sale(
|
||||
player,
|
||||
sale_service,
|
||||
species_ids,
|
||||
true,
|
||||
true,
|
||||
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||
)
|
||||
assert(
|
||||
player.wallet.get_balance()
|
||||
== species_balance_before + expected_payout
|
||||
)
|
||||
|
||||
|
||||
func _test_player_menu_sale(
|
||||
main: Node,
|
||||
player: Player,
|
||||
catalog: FishPool,
|
||||
sale_service: NetworkSaleService,
|
||||
pelican: Node3D,
|
||||
) -> void:
|
||||
player.global_position = pelican.global_position
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||
var fish_catch := _make_catch(catalog.candidates[6])
|
||||
player.inventory.add_catch(fish_catch)
|
||||
player.global_position = Vector3(0.0, 3.95, 13.0)
|
||||
player_menu.open_menu()
|
||||
await create_timer(2.2).timeout
|
||||
player_menu.call("_on_catch_card_pressed", fish_catch.catch_id)
|
||||
|
|
@ -312,15 +380,23 @@ func _test_player_menu_sale(
|
|||
var sell_action := player_menu.get_node("%SellBubble") as Button
|
||||
var confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
||||
var confirm_button := player_menu.get_node("%ConfirmSaleButton") as Button
|
||||
var ui_viewport := main.get_node(
|
||||
"UIPresentation/UIViewport"
|
||||
) as SubViewport
|
||||
assert(sell_action.visible and not sell_action.disabled)
|
||||
assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP)
|
||||
assert(sell_action.focus_mode == Control.FOCUS_ALL)
|
||||
sell_action.pressed.emit()
|
||||
assert(ui_viewport != null)
|
||||
await _activate_focused_button(sell_action, ui_viewport)
|
||||
await process_frame
|
||||
assert(confirmation.visible)
|
||||
assert(confirm_button.visible and not confirm_button.disabled)
|
||||
assert(
|
||||
confirmation.z_index
|
||||
> (player_menu.get_node("%CoolerOuterWall") as Control).z_index
|
||||
)
|
||||
_sale_result.clear()
|
||||
confirm_button.pressed.emit()
|
||||
await _activate_focused_button(confirm_button, ui_viewport)
|
||||
await process_frame
|
||||
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
||||
assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
|
||||
|
|
@ -339,6 +415,21 @@ func _test_player_menu_sale(
|
|||
await create_timer(2.2).timeout
|
||||
|
||||
|
||||
func _activate_focused_button(
|
||||
button: Button,
|
||||
ui_viewport: SubViewport,
|
||||
) -> void:
|
||||
button.grab_focus()
|
||||
await process_frame
|
||||
assert(button.has_focus())
|
||||
for is_pressed: bool in [true, false]:
|
||||
var accept := InputEventAction.new()
|
||||
accept.action = &"ui_accept"
|
||||
accept.pressed = is_pressed
|
||||
ui_viewport.push_input(accept, false)
|
||||
await process_frame
|
||||
|
||||
|
||||
func _test_host_shop_purchase(
|
||||
main: Node,
|
||||
player: Player,
|
||||
|
|
@ -359,16 +450,123 @@ func _test_host_shop_purchase(
|
|||
assert(not shop_service.is_local_purchase_pending())
|
||||
|
||||
|
||||
func _test_fishing_shop_sale_ui(
|
||||
main: Node,
|
||||
player: Player,
|
||||
catalog: FishPool,
|
||||
sale_service: NetworkSaleService,
|
||||
reservations: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var shop := game_ui.get_node("%FishingShop") as FishingShop
|
||||
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||
var shop_backdrop := main.get_node("%ShopBackdrop") as ColorRect
|
||||
var ui_viewport := main.get_node(
|
||||
"UIPresentation/UIViewport"
|
||||
) as SubViewport
|
||||
assert(
|
||||
interaction != null
|
||||
and shop != null
|
||||
and player_menu != null
|
||||
and shop_backdrop != null
|
||||
and ui_viewport != null
|
||||
)
|
||||
player.global_position = interaction.global_position
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
var fish_catch := _make_catch(catalog.candidates[7])
|
||||
var reserved_catch := _make_catch(catalog.candidates[6])
|
||||
player.inventory.add_catch(fish_catch)
|
||||
player.inventory.add_catch(reserved_catch)
|
||||
var reservation_id := "shop-ui-reservation"
|
||||
assert(reservations.reserve(reservation_id, {
|
||||
"type": PlayerAssetReservationService.AttachmentType.FISH,
|
||||
"catch_id": str(reserved_catch.catch_id),
|
||||
"catch": reserved_catch.to_network_dict(),
|
||||
}))
|
||||
var balance_before: int = player.wallet.get_balance()
|
||||
assert(shop.open_shop())
|
||||
await process_frame
|
||||
assert(shop_backdrop.visible)
|
||||
assert(shop_backdrop.material is ShaderMaterial)
|
||||
assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer"))
|
||||
var shop_panel := shop.get_node("%ShopPanel") as PanelContainer
|
||||
assert(shop_panel != null and shop_panel.size == Vector2(980.0, 600.0))
|
||||
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 buy_mode := shop.get_node("%BuyModeButton") as Button
|
||||
var sell_mode := shop.get_node("%SellModeButton") as Button
|
||||
assert(buy_mode.visible and sell_mode.visible)
|
||||
await _activate_focused_button(sell_mode, ui_viewport)
|
||||
await process_frame
|
||||
assert(shop.visible and not player_menu.visible)
|
||||
assert(not shop.has_node("ShopPanel/Margin/Layout/Body/FishSales"))
|
||||
assert((shop.get_node("%ShopCoolerPage") as Control).visible)
|
||||
assert(not (shop.get_node("%ShopPanel") as Control).visible)
|
||||
var mounted_cooler := player_menu.get("_cooler_page") as Control
|
||||
assert(mounted_cooler != null and mounted_cooler.visible)
|
||||
assert(
|
||||
mounted_cooler.get_parent() == shop.get_node("%ShopCoolerMount")
|
||||
)
|
||||
var cooler_outer_wall := player_menu.get("_cooler_outer_wall") as Control
|
||||
var water_surface := player_menu.get("_cooler_water_surface") as ColorRect
|
||||
assert(cooler_outer_wall != null and cooler_outer_wall.visible)
|
||||
assert(water_surface.visible and water_surface.material is ShaderMaterial)
|
||||
assert(
|
||||
StringName(
|
||||
(player_menu.get("_sale_buyer_override") as FishBuyerProfile).id
|
||||
) == NetworkSaleService.MAIN_SHOP_BUYER_ID
|
||||
)
|
||||
var fish_nodes: Dictionary = player_menu.get("_fish_nodes")
|
||||
var fish_button := fish_nodes.get(fish_catch.catch_id) as Button
|
||||
var reserved_button := fish_nodes.get(reserved_catch.catch_id) as Button
|
||||
assert(fish_button != null and fish_button.visible)
|
||||
assert(reserved_button != null and reserved_button.visible)
|
||||
await _activate_focused_button(fish_button, ui_viewport)
|
||||
var sell_button := player_menu.get("_sell_bubble") as Button
|
||||
assert(sell_button.visible and not sell_button.disabled)
|
||||
await _activate_focused_button(sell_button, ui_viewport)
|
||||
await process_frame
|
||||
var confirmation := player_menu.get("_sale_confirmation") as Control
|
||||
var confirm_button := player_menu.get("_confirm_sale_button") as Button
|
||||
assert(confirmation.visible)
|
||||
assert(
|
||||
confirmation.z_index
|
||||
> cooler_outer_wall.z_index
|
||||
)
|
||||
_sale_result.clear()
|
||||
await _activate_focused_button(confirm_button, ui_viewport)
|
||||
await process_frame
|
||||
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
||||
assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
|
||||
assert(player.wallet.get_balance() == balance_before + fish_catch.sale_value)
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
assert(reservations.release(reservation_id))
|
||||
var back_button := shop.get_node("%BackToShopButton") as Button
|
||||
await _activate_focused_button(back_button, ui_viewport)
|
||||
await process_frame
|
||||
assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible)
|
||||
assert(not (shop.get_node("%ShopCoolerPage") as Control).visible)
|
||||
assert(not player_menu.is_shop_cooler_mounted())
|
||||
shop.close_shop()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _assert_sale(
|
||||
player: Player,
|
||||
sale_service: NetworkSaleService,
|
||||
catch_ids: Array[StringName],
|
||||
expected_success: bool,
|
||||
expect_request_id: bool = true,
|
||||
buyer_id: StringName = NetworkSaleService.PELICAN_BUYER_ID,
|
||||
) -> void:
|
||||
var balance_before: int = player.wallet.get_balance()
|
||||
_sale_result.clear()
|
||||
var request_id: String = sale_service.request_local_sale(catch_ids)
|
||||
var request_id: String = sale_service.request_local_sale(
|
||||
catch_ids, buyer_id
|
||||
)
|
||||
assert((not request_id.is_empty()) == expect_request_id)
|
||||
assert(not _sale_result.is_empty())
|
||||
assert(bool(_sale_result[1]) == expected_success)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
|
|||
const FishDataType = preload("res://fish/fish_data.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||
const FishSelectorType = preload("res://fish/fish_selector.gd")
|
||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||
const NetworkSaleServiceType = preload(
|
||||
|
|
@ -227,7 +228,10 @@ func _validate_catches_and_authoritative_sale() -> void:
|
|||
root.add_child(sale_service)
|
||||
sale_service.set("_session", session)
|
||||
sale_service.set("_fish_catalog", Catalog)
|
||||
sale_service.set("_buyer", PelicanBuyer)
|
||||
var sale_buyers: Dictionary[StringName, FishBuyerProfileType] = {
|
||||
PelicanBuyer.id: PelicanBuyer,
|
||||
}
|
||||
sale_service.set("_buyers", sale_buyers)
|
||||
|
||||
for index: int in Catalog.candidates.size():
|
||||
var fish: FishDataType = Catalog.candidates[index]
|
||||
|
|
@ -257,6 +261,7 @@ func _validate_catches_and_authoritative_sale() -> void:
|
|||
1,
|
||||
"catalog_sale_%d" % index,
|
||||
[loaded.to_network_dict()],
|
||||
PelicanBuyer,
|
||||
)
|
||||
assert(bool(sale_result.get("accepted", false)))
|
||||
assert(int(sale_result.get("base_value", -1)) == loaded.sale_value)
|
||||
|
|
|
|||
273
tests/inventory_notepad_art_validation.gd
Normal file
273
tests/inventory_notepad_art_validation.gd
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
extends SceneTree
|
||||
|
||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||
const UIReferencePresentationType = preload(
|
||||
"res://ui/ui_reference_presentation.gd"
|
||||
)
|
||||
const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0)
|
||||
const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8)
|
||||
const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4)
|
||||
const INVENTORY_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
|
||||
const NOTEPAD_HOST_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
|
||||
const NOTEPAD_ART_RECT := Rect2(
|
||||
NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
|
||||
EXPECTED_ART_SIZE,
|
||||
)
|
||||
const HOTBAR_BUBBLE_FIELD_RECT := Rect2(245.0, 608.0, 790.0, 104.0)
|
||||
const TARGET_SIZES: Array[Vector2] = [
|
||||
Vector2(1280.0, 720.0),
|
||||
Vector2(1920.0, 1080.0),
|
||||
Vector2(2560.0, 1440.0),
|
||||
Vector2(3840.0, 2160.0),
|
||||
Vector2(640.0, 480.0),
|
||||
Vector2(1024.0, 768.0),
|
||||
Vector2(1729.0, 973.0),
|
||||
Vector2(2560.0, 1080.0),
|
||||
Vector2(3440.0, 1440.0),
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_print_runtime_parameters()
|
||||
var presentation_stage := Control.new()
|
||||
presentation_stage.name = "CanonicalUIStage"
|
||||
_apply_stage_layout(presentation_stage, Vector2(root.size))
|
||||
root.add_child(presentation_stage)
|
||||
var player_menu := PlayerMenuScene.instantiate() as PlayerMenu
|
||||
presentation_stage.add_child(player_menu)
|
||||
await process_frame
|
||||
var notepads: Array[Node] = player_menu.find_children(
|
||||
"*", "InventoryNotepad", true, false
|
||||
)
|
||||
assert(notepads.size() == 3)
|
||||
for node: Node in notepads:
|
||||
var notepad := node as InventoryNotepad
|
||||
assert(notepad != null)
|
||||
assert(notepad.size.is_equal_approx(EXPECTED_HOST_SIZE))
|
||||
assert(notepad.mouse_filter == Control.MOUSE_FILTER_PASS)
|
||||
var art_rect: Rect2 = notepad.get_art_rect()
|
||||
assert(art_rect.size.is_equal_approx(EXPECTED_ART_SIZE))
|
||||
assert(art_rect.position.is_equal_approx(EXPECTED_ART_POSITION))
|
||||
assert(art_rect.get_center().is_equal_approx(
|
||||
EXPECTED_HOST_SIZE * 0.5
|
||||
+ InventoryNotepad.NOTEPAD_ART_OFFSET
|
||||
))
|
||||
_validate_shared_inventory_geometry(player_menu)
|
||||
_validate_inventory_layering(player_menu)
|
||||
_validate_resolution_matrix()
|
||||
await _capture_inventory_pages(player_menu)
|
||||
print("Inventory notepad artwork validation: PASS")
|
||||
presentation_stage.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _print_runtime_parameters() -> void:
|
||||
var window: Window = root
|
||||
var screen_index: int = window.current_screen
|
||||
print(
|
||||
(
|
||||
"Runtime UI: window=%s screen=%s screen_scale=%.3f dpi=%d "
|
||||
+ "content_size=%s content_mode=%d content_aspect=%d "
|
||||
+ "content_factor=%.3f"
|
||||
)
|
||||
% [
|
||||
window.size,
|
||||
DisplayServer.screen_get_size(screen_index),
|
||||
DisplayServer.screen_get_scale(screen_index),
|
||||
DisplayServer.screen_get_dpi(screen_index),
|
||||
window.content_scale_size,
|
||||
window.content_scale_mode,
|
||||
window.content_scale_aspect,
|
||||
window.content_scale_factor,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
|
||||
stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
stage.position = UIReferencePresentationType.get_offset(display_size)
|
||||
stage.size = UIReferencePresentationType.REFERENCE_SIZE
|
||||
stage.scale = Vector2.ONE * UIReferencePresentationType.get_scale(
|
||||
display_size
|
||||
)
|
||||
|
||||
|
||||
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
|
||||
for node_name: StringName in [
|
||||
&"DetailConstellation",
|
||||
&"BagDetailConstellation",
|
||||
&"TackleDetailPanel",
|
||||
]:
|
||||
var host := player_menu.get_node("%%%s" % node_name) as Control
|
||||
assert(host != null)
|
||||
assert(host.position.is_equal_approx(Vector2(952.0, 166.0)))
|
||||
assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
|
||||
|
||||
|
||||
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
|
||||
var bag_filters := player_menu.get_node("%BagFilterTabs") as Control
|
||||
var bag_panel := player_menu.get_node("%BagOuterWall") as Control
|
||||
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
||||
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
|
||||
var tackle_panel := player_menu.get_node("%TackleMainPanel") as Control
|
||||
assert(bag_filters != null)
|
||||
assert(bag_panel != null)
|
||||
assert(sale_confirmation != null)
|
||||
assert(cooler_panel != null)
|
||||
assert(tackle_panel != null)
|
||||
assert(bag_filters.z_index > bag_panel.z_index)
|
||||
assert(sale_confirmation.z_index > cooler_panel.z_index)
|
||||
assert(sale_confirmation.z_index > tackle_panel.z_index)
|
||||
# The contextual Hotbar uses z=90 while the Player Menu is open.
|
||||
assert(sale_confirmation.z_index > 90)
|
||||
assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0)))
|
||||
assert(sale_confirmation.size.is_equal_approx(Vector2(520.0, 190.0)))
|
||||
var confirmation_style := sale_confirmation.get_theme_stylebox(
|
||||
"panel"
|
||||
) as StyleBoxFlat
|
||||
assert(confirmation_style != null)
|
||||
assert(
|
||||
confirmation_style.bg_color.is_equal_approx(
|
||||
UtilityPageStyle.OCEAN_PANEL_DEEP
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _validate_resolution_matrix() -> void:
|
||||
for target_size: Vector2 in TARGET_SIZES:
|
||||
var stage_rect: Rect2 = UIReferencePresentationType.get_rect(
|
||||
target_size
|
||||
)
|
||||
var stage_scale: float = UIReferencePresentationType.get_scale(
|
||||
target_size
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
stage_rect.size.x / stage_rect.size.y,
|
||||
1280.0 / 720.0,
|
||||
))
|
||||
assert(stage_rect.get_center().is_equal_approx(target_size * 0.5))
|
||||
_validate_transformed_rect(
|
||||
"Inventory panel", INVENTORY_PANEL_RECT, stage_rect, stage_scale
|
||||
)
|
||||
_validate_transformed_rect(
|
||||
"Notepad host", NOTEPAD_HOST_RECT, stage_rect, stage_scale
|
||||
)
|
||||
_validate_transformed_rect(
|
||||
"Notepad art", NOTEPAD_ART_RECT, stage_rect, stage_scale
|
||||
)
|
||||
_validate_transformed_rect(
|
||||
"Hotbar", HOTBAR_BUBBLE_FIELD_RECT, stage_rect, stage_scale
|
||||
)
|
||||
var screen_host: Rect2 = _to_screen_rect(
|
||||
NOTEPAD_HOST_RECT, stage_rect.position, stage_scale
|
||||
)
|
||||
var screen_art: Rect2 = _to_screen_rect(
|
||||
NOTEPAD_ART_RECT, stage_rect.position, stage_scale
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
screen_art.size.y,
|
||||
screen_host.size.y * InventoryNotepad.NOTEPAD_CANONICAL_OVERSCAN,
|
||||
))
|
||||
assert(screen_art.get_center().is_equal_approx(
|
||||
screen_host.get_center()
|
||||
+ InventoryNotepad.NOTEPAD_ART_OFFSET * stage_scale
|
||||
))
|
||||
print(
|
||||
(
|
||||
"UI geometry: window=%s stage=%s scale=%.6f "
|
||||
+ "inventory=%s notepad=%s art=%s hotbar=%s"
|
||||
)
|
||||
% [
|
||||
target_size,
|
||||
stage_rect,
|
||||
stage_scale,
|
||||
_to_screen_rect(
|
||||
INVENTORY_PANEL_RECT,
|
||||
stage_rect.position,
|
||||
stage_scale,
|
||||
),
|
||||
screen_host,
|
||||
screen_art,
|
||||
_to_screen_rect(
|
||||
HOTBAR_BUBBLE_FIELD_RECT,
|
||||
stage_rect.position,
|
||||
stage_scale,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
func _validate_transformed_rect(
|
||||
label: String,
|
||||
canonical_rect: Rect2,
|
||||
stage_rect: Rect2,
|
||||
stage_scale: float,
|
||||
) -> void:
|
||||
var screen_rect: Rect2 = _to_screen_rect(
|
||||
canonical_rect, stage_rect.position, stage_scale
|
||||
)
|
||||
assert(
|
||||
screen_rect.position.is_equal_approx(
|
||||
stage_rect.position + canonical_rect.position * stage_scale
|
||||
),
|
||||
"%s position does not use the single stage transform" % label,
|
||||
)
|
||||
assert(
|
||||
screen_rect.size.is_equal_approx(canonical_rect.size * stage_scale),
|
||||
"%s size does not use the single stage transform" % label,
|
||||
)
|
||||
|
||||
|
||||
func _to_screen_rect(
|
||||
canonical_rect: Rect2,
|
||||
stage_offset: Vector2,
|
||||
stage_scale: float,
|
||||
) -> Rect2:
|
||||
return Rect2(
|
||||
stage_offset + canonical_rect.position * stage_scale,
|
||||
canonical_rect.size * stage_scale,
|
||||
)
|
||||
|
||||
|
||||
func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
|
||||
if not OS.has_environment("NETFISHING_NOTEPAD_CAPTURE"):
|
||||
return
|
||||
player_menu.visible = true
|
||||
var sections: Array[PlayerMenu.Section] = [
|
||||
PlayerMenu.Section.COOLER,
|
||||
PlayerMenu.Section.TACKLE_BOX,
|
||||
PlayerMenu.Section.BAG,
|
||||
]
|
||||
var suffixes: Array[String] = ["cooler", "tackle", "equipment"]
|
||||
for index: int in sections.size():
|
||||
player_menu.call("_show_section_immediate", sections[index])
|
||||
await process_frame
|
||||
await process_frame
|
||||
await _save_capture(suffixes[index])
|
||||
player_menu.call("_show_section_immediate", PlayerMenu.Section.COOLER)
|
||||
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
||||
var confirmation_message := player_menu.get_node(
|
||||
"%ConfirmationMessage"
|
||||
) as Label
|
||||
assert(sale_confirmation != null)
|
||||
assert(confirmation_message != null)
|
||||
confirmation_message.text = "sell selected fish?"
|
||||
sale_confirmation.visible = true
|
||||
await process_frame
|
||||
await process_frame
|
||||
await _save_capture("sale-confirmation")
|
||||
sale_confirmation.visible = false
|
||||
|
||||
|
||||
func _save_capture(suffix: String) -> void:
|
||||
var image: Image = root.get_texture().get_image()
|
||||
var path: String = "%s-%s.png" % [
|
||||
OS.get_environment("NETFISHING_NOTEPAD_CAPTURE"),
|
||||
suffix,
|
||||
]
|
||||
assert(image.save_png(path) == OK)
|
||||
1
tests/inventory_notepad_art_validation.gd.uid
Normal file
1
tests/inventory_notepad_art_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://np6jfuf8tej8
|
||||
195
tests/ui_scaling_runtime_validation.gd
Normal file
195
tests/ui_scaling_runtime_validation.gd
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
extends SceneTree
|
||||
|
||||
const UIPixelationPresenterType = preload(
|
||||
"res://ui/ui_pixelation_presenter.gd"
|
||||
)
|
||||
const UIReferencePresentationType = preload(
|
||||
"res://ui/ui_reference_presentation.gd"
|
||||
)
|
||||
const GameUIScene := preload("res://ui/game_ui.tscn")
|
||||
const TARGET_SIZES: Array[Vector2i] = [
|
||||
Vector2i(1280, 720),
|
||||
Vector2i(1920, 1080),
|
||||
Vector2i(2560, 1440),
|
||||
Vector2i(3840, 2160),
|
||||
Vector2i(640, 480),
|
||||
Vector2i(1024, 768),
|
||||
Vector2i(1729, 973),
|
||||
Vector2i(2560, 1080),
|
||||
Vector2i(3440, 1440),
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var original_size: Vector2i = root.size
|
||||
var presenter := UIPixelationPresenterType.new()
|
||||
presenter.name = "UIPresentation"
|
||||
presenter.stretch = true
|
||||
var ui_viewport := SubViewport.new()
|
||||
ui_viewport.name = "UIViewport"
|
||||
var game_ui := GameUIScene.instantiate() as GameUI
|
||||
var ui_root := game_ui.get_node("UIRoot") as Control
|
||||
var canonical_stage := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage"
|
||||
) as Control
|
||||
var chat_ui := game_ui.get_node("UIRoot/ChatUI") as ChatUI
|
||||
var player_menu := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/PlayerMenu"
|
||||
) as PlayerMenu
|
||||
var hotbar := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/Hotbar"
|
||||
) as HotbarUI
|
||||
var screen_fade := game_ui.get_node("UIRoot/ScreenFade") as Control
|
||||
var title_screen := game_ui.get_node("UIRoot/TitleScreen") as TitleScreen
|
||||
var title_content_stage := title_screen.get_node(
|
||||
"ResponsiveTitleStage"
|
||||
) as Control
|
||||
var decorative_fish_layer := title_screen.get_node(
|
||||
"DecorativeFishLayer"
|
||||
) as Control
|
||||
var decorative_bubble_layer := title_screen.get_node(
|
||||
"DecorativeBubbleLayer"
|
||||
) as Control
|
||||
ui_viewport.add_child(game_ui)
|
||||
presenter.add_child(ui_viewport)
|
||||
root.add_child(presenter)
|
||||
await process_frame
|
||||
chat_ui.set_available(true)
|
||||
print(
|
||||
"UI viewport embedding: root=%s subviewport=%s"
|
||||
% [root.gui_embed_subwindows, ui_viewport.gui_embed_subwindows]
|
||||
)
|
||||
|
||||
for target_size: Vector2i in TARGET_SIZES:
|
||||
root.size = target_size
|
||||
await process_frame
|
||||
await process_frame
|
||||
var actual_size: Vector2i = root.size
|
||||
var display_size := Vector2(actual_size)
|
||||
var expected_scale: float = UIReferencePresentationType.get_scale(
|
||||
display_size
|
||||
)
|
||||
var expected_stage_offset: Vector2 = UIReferencePresentationType.get_offset(
|
||||
display_size
|
||||
)
|
||||
assert(presenter.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(ui_root.position.is_equal_approx(Vector2.ZERO))
|
||||
var expected_visible_size: Vector2 = (
|
||||
UIReferencePresentationType.get_visible_reference_size(display_size)
|
||||
)
|
||||
assert(ui_root.size.is_equal_approx(expected_visible_size))
|
||||
assert(canonical_stage.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(canonical_stage.position.is_equal_approx(
|
||||
UIReferencePresentationType.get_stage_position(display_size)
|
||||
))
|
||||
assert(chat_ui.size.is_equal_approx(expected_visible_size))
|
||||
assert(screen_fade.size.is_equal_approx(expected_visible_size))
|
||||
assert(player_menu.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(player_menu.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(hotbar.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(hotbar.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(title_screen.size.is_equal_approx(expected_visible_size))
|
||||
assert(title_content_stage.size.is_equal_approx(
|
||||
UIReferencePresentationType.REFERENCE_SIZE
|
||||
))
|
||||
assert(title_content_stage.position.is_equal_approx(
|
||||
canonical_stage.position
|
||||
))
|
||||
assert(decorative_fish_layer.size.is_equal_approx(
|
||||
expected_visible_size
|
||||
))
|
||||
assert(decorative_bubble_layer.size.is_equal_approx(
|
||||
expected_visible_size
|
||||
))
|
||||
var left_to_right_bounds: Vector2 = (
|
||||
TitleScreen.get_decorative_fish_crossing_bounds(
|
||||
expected_visible_size.x, 160.0, 1.0
|
||||
)
|
||||
)
|
||||
var right_to_left_bounds: Vector2 = (
|
||||
TitleScreen.get_decorative_fish_crossing_bounds(
|
||||
expected_visible_size.x, 160.0, -1.0
|
||||
)
|
||||
)
|
||||
assert(left_to_right_bounds.x < -160.0)
|
||||
assert(left_to_right_bounds.y > expected_visible_size.x)
|
||||
assert(right_to_left_bounds.x > expected_visible_size.x)
|
||||
assert(right_to_left_bounds.y < -160.0)
|
||||
assert(is_equal_approx(presenter.scale.x, presenter.scale.y))
|
||||
assert(is_equal_approx(ui_root.scale.x, ui_root.scale.y))
|
||||
var effective_scale: float = presenter.scale.x * ui_root.scale.x
|
||||
assert(is_equal_approx(effective_scale, expected_scale))
|
||||
assert((
|
||||
canonical_stage.position * effective_scale
|
||||
).distance_to(expected_stage_offset) <= 0.01)
|
||||
var expected_render_height: int = mini(
|
||||
roundi(UIReferencePresentationType.REFERENCE_SIZE.y * expected_scale),
|
||||
PlayerSettings.get_ui_render_height(
|
||||
PlayerSettings.DEFAULT_UI_PIXEL_SIZE,
|
||||
roundi(UIReferencePresentationType.REFERENCE_SIZE.y),
|
||||
),
|
||||
)
|
||||
var expected_render_scale: float = (
|
||||
float(expected_render_height)
|
||||
/ UIReferencePresentationType.REFERENCE_SIZE.y
|
||||
)
|
||||
var expected_viewport_size := Vector2i(
|
||||
roundi(expected_visible_size.x * expected_render_scale),
|
||||
roundi(expected_visible_size.y * expected_render_scale),
|
||||
)
|
||||
assert(presenter.get_ui_viewport_size() == expected_viewport_size)
|
||||
assert(presenter.size.is_equal_approx(Vector2(expected_viewport_size)))
|
||||
var chat_panel := chat_ui.get_node("ChatPanel") as Control
|
||||
assert(chat_panel != null)
|
||||
assert(chat_panel.visible)
|
||||
assert(is_equal_approx(chat_panel.position.x, 0.0))
|
||||
assert(is_equal_approx(
|
||||
(chat_panel.position.y + chat_panel.size.y) * effective_scale,
|
||||
display_size.y - ChatUI.BOTTOM_MARGIN * effective_scale,
|
||||
))
|
||||
print(
|
||||
(
|
||||
"UI resize: requested=%s actual=%s viewport=%s container_scale=%.6f "
|
||||
+ "root_scale=%.6f effective_scale=%.6f stage_offset=%s "
|
||||
+ "chat_x=%.3f"
|
||||
)
|
||||
% [
|
||||
target_size,
|
||||
actual_size,
|
||||
presenter.get_ui_viewport_size(),
|
||||
presenter.scale.x,
|
||||
ui_root.scale.x,
|
||||
effective_scale,
|
||||
expected_stage_offset,
|
||||
chat_panel.position.x * effective_scale,
|
||||
]
|
||||
)
|
||||
if OS.has_environment("NETFISHING_UI_SCALE_CAPTURE"):
|
||||
await process_frame
|
||||
var capture: Image = root.get_texture().get_image()
|
||||
var capture_path: String = (
|
||||
"%s-requested-%dx%d-actual-%dx%d.png"
|
||||
) % [
|
||||
OS.get_environment("NETFISHING_UI_SCALE_CAPTURE"),
|
||||
target_size.x,
|
||||
target_size.y,
|
||||
actual_size.x,
|
||||
actual_size.y,
|
||||
]
|
||||
assert(capture.save_png(capture_path) == OK)
|
||||
|
||||
presenter.queue_free()
|
||||
root.size = original_size
|
||||
await process_frame
|
||||
print("UI scaling runtime validation: PASS")
|
||||
quit()
|
||||
1
tests/ui_scaling_runtime_validation.gd.uid
Normal file
1
tests/ui_scaling_runtime_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d1x8t4mf2kqv6
|
||||
Loading…
Add table
Add a link
Reference in a new issue