extends SceneTree const MainScene = preload("res://main/main.tscn") const FishCatchType = preload("res://fish/fish_catch.gd") var _sale_result: Array[Variant] = [] var _shop_result: Array[Variant] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var arguments: PackedStringArray = OS.get_cmdline_user_args() if arguments.has("host"): await _run_multiplayer_host() return if arguments.has("client"): await _run_multiplayer_client() return root.size = Vector2i(1280, 720) var main := MainScene.instantiate() root.add_child(main) for _frame: int in 4: await process_frame if not bool(main.get("_application_initialized")): # Use the same activation path as the real setup dialog so no modal is # left above the gameplay SubViewport during pointer validation. main.call("_activate_selected_data_path", "", true) for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) assert(bool(main.call("_prepare_private_host"))) var save_manager := main.get("_save_manager") as PlayerSaveManager assert(save_manager.initialize_new_game()) main.call("_enter_gameplay") for _frame: int in 8: await process_frame var player := main.get("_player") as Player var catalog := main.get("fish_catalog") as FishPool var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService var shop_service := main.get_node("%NetworkShopService") as NetworkShopService var session := main.get_node("%NetworkSession") as NetworkSession var reservations := ( main.get_node("%PlayerAssetReservationService") as PlayerAssetReservationService ) 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) sale_service.local_sale_finished.connect(_on_sale_finished) shop_service.local_purchase_finished.connect(_on_shop_finished) 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_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) var open_catch := _make_catch(catalog.candidates.front()) player.inventory.add_catch(open_catch) _assert_sale(player, sale_service, [open_catch.catch_id], true) 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()) print("Economy regression validation: PASS") session.disconnect_session("Economy validation complete.") main.queue_free() await process_frame quit() func _run_multiplayer_host() -> void: root.size = Vector2i(1280, 720) var main: Node = await _create_initialized_main() assert(bool(main.call("_prepare_private_host"))) var save_manager := main.get("_save_manager") as PlayerSaveManager assert(save_manager.initialize_new_game()) main.call("_enter_gameplay") var session := main.get_node("%NetworkSession") as NetworkSession assert(session.set_host_open(true)) var client_connected := false var connection_deadline: int = Time.get_ticks_msec() + 30000 while Time.get_ticks_msec() < connection_deadline: await process_frame if session.get_authenticated_peer_ids().size() >= 2: client_connected = true break assert(client_connected) var remote_peer_id := 0 for authenticated_peer_id: int in session.get_authenticated_peer_ids(): if authenticated_peer_id != 1: remote_peer_id = authenticated_peer_id break 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) 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) remote_avatar.global_position = interaction.global_position var completion_deadline: int = Time.get_ticks_msec() + 30000 while Time.get_ticks_msec() < completion_deadline: await process_frame if session.get_authenticated_peer_ids().size() < 2: break print("Economy multiplayer host validation: PASS") session.disconnect_session("Economy host validation complete.") main.queue_free() await process_frame quit() func _run_multiplayer_client() -> void: root.size = Vector2i(1280, 720) var main: Node = await _create_initialized_main() main.call("_on_title_join_game_requested", "127.0.0.1:7777") var session := main.get_node("%NetworkSession") as NetworkSession var joined := false var join_deadline: int = Time.get_ticks_msec() + 30000 while Time.get_ticks_msec() < join_deadline: await process_frame if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY: main.call("_confirm_server_trust") if session.is_joined_client() and bool(main.get("_gameplay_started")): joined = true break assert(joined) var player := main.get("_player") as Player var catalog := main.get("fish_catalog") as FishPool var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService 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 catch_ids: Array[StringName] = [] for fish: FishData in catalog.candidates: var fish_catch := _make_catch(fish) player.inventory.add_catch(fish_catch) catch_ids.append(fish_catch.catch_id) await create_timer(1.0).timeout _sale_result.clear() assert(not sale_service.request_local_sale(catch_ids).is_empty()) var sale_deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < sale_deadline: await process_frame if not _sale_result.is_empty(): break print("Client sale result: ", _sale_result) assert(not _sale_result.is_empty() and bool(_sale_result[1])) for catch_id: StringName in catch_ids: assert(not player.inventory.contains_catch_id(catch_id)) assert(not sale_service.is_local_sale_pending()) 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()) var shop_deadline: int = Time.get_ticks_msec() + 10000 while Time.get_ticks_msec() < shop_deadline: await process_frame if not _shop_result.is_empty(): break print("Client shop result: ", _shop_result) assert(not _shop_result.is_empty() and bool(_shop_result[1])) assert(not shop_service.is_local_purchase_pending()) print("Economy multiplayer client validation: PASS") session.disconnect_session("Economy client validation complete.") main.queue_free() await process_frame quit() func _create_initialized_main() -> Node: var main := MainScene.instantiate() root.add_child(main) for _frame: int in 4: await process_frame if not bool(main.get("_application_initialized")): main.call("_activate_selected_data_path", "", true) for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) return main func _test_each_species( player: Player, catalog: FishPool, sale_service: NetworkSaleService, ) -> void: for fish: FishData in catalog.candidates: assert(fish != null) var fish_catch := _make_catch(fish) player.inventory.add_catch(fish_catch) _assert_sale(player, sale_service, [fish_catch.catch_id], true) func _test_multi_sale( player: Player, catalog: FishPool, sale_service: NetworkSaleService, ) -> void: var catches: Array[FishCatch] = [ _make_catch(catalog.candidates[0]), _make_catch(catalog.candidates[1]), _make_catch(catalog.candidates[2]), ] var catch_ids: Array[StringName] = [] for fish_catch: FishCatch in catches: player.inventory.add_catch(fish_catch) catch_ids.append(fish_catch.catch_id) _assert_sale(player, sale_service, catch_ids, true) func _test_reservations( player: Player, catalog: FishPool, sale_service: NetworkSaleService, reservations: PlayerAssetReservationService, ) -> void: var reserved := _make_catch(catalog.candidates[3]) var unreserved := _make_catch(catalog.candidates[4]) player.inventory.add_catch(reserved) player.inventory.add_catch(unreserved) var reservation_id := "economy-test-reservation" assert(reservations.reserve(reservation_id, { "type": PlayerAssetReservationService.AttachmentType.FISH, "catch_id": str(reserved.catch_id), "catch": reserved.to_network_dict(), })) _assert_sale(player, sale_service, [reserved.catch_id], false, false) _assert_sale( player, sale_service, [reserved.catch_id, unreserved.catch_id], false, false, ) assert(player.inventory.contains_catch_id(reserved.catch_id)) assert(player.inventory.contains_catch_id(unreserved.catch_id)) assert(reservations.release(reservation_id)) _assert_sale( player, sale_service, [reserved.catch_id, unreserved.catch_id], true, ) func _test_anywhere_sale( player: Player, catalog: FishPool, sale_service: NetworkSaleService, ) -> void: var fish_catch := _make_catch(catalog.candidates[5]) player.inventory.add_catch(fish_catch) # 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, ) -> void: 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) await process_frame 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) 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() 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(not sale_service.is_local_sale_pending()) player_menu.close_menu() await create_timer(2.2).timeout assert(not player_menu.visible) player_menu.open_menu() await create_timer(2.2).timeout assert(player_menu.visible) assert( not sell_action.disabled or player.inventory.get_all_catches().is_empty() ) player_menu.close_menu() 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, shop_service: NetworkShopService, ) -> void: var interaction := main.get("_shop_interaction") as FishingShopInteraction assert(interaction != null) player.global_position = interaction.global_position for _frame: int in 4: await physics_frame assert(interaction.is_avatar_in_range(player)) var quantity_before: int = player.bag.get_quantity(&"coffee") _shop_result.clear() var request_id: String = shop_service.request_supply(&"coffee") assert(not request_id.is_empty()) assert(not _shop_result.is_empty() and bool(_shop_result[1])) assert(player.bag.get_quantity(&"coffee") == quantity_before + 1) 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, buyer_id ) assert((not request_id.is_empty()) == expect_request_id) assert(not _sale_result.is_empty()) assert(bool(_sale_result[1]) == expected_success) assert(not sale_service.is_local_sale_pending()) if expected_success: assert(player.wallet.get_balance() > balance_before) for catch_id: StringName in catch_ids: assert(not player.inventory.contains_catch_id(catch_id)) else: assert(player.wallet.get_balance() == balance_before) for catch_id: StringName in catch_ids: assert(player.inventory.contains_catch_id(catch_id)) func _make_catch(fish: FishData) -> FishCatch: var fish_catch := FishCatchType.new() fish_catch.fish = fish fish_catch.fish_id = fish.id fish_catch.weight_lb = fish.get_minimum_weight() fish_catch.display_scale = fish.get_display_scale_for_weight( fish_catch.weight_lb ) fish_catch.sale_value = fish.get_sale_value_for_weight( fish_catch.weight_lb ) fish_catch.ensure_identity() return fish_catch func _on_sale_finished( request_id: String, accepted: bool, message: String, catch_ids: Array[StringName], payout: int, ) -> void: _sale_result = [request_id, accepted, message, catch_ids, payout] func _on_shop_finished( request_id: String, accepted: bool, message: String, product_id: StringName, category: int, quantity: int, total_cost: int, ) -> void: _shop_result = [ request_id, accepted, message, product_id, category, quantity, total_cost, ]