Add gathering, unified inventory, and real-time world

This commit is contained in:
Alexander Sellite 2026-08-18 18:19:36 -04:00
parent 173371e6fc
commit fa57edca83
113 changed files with 6700 additions and 1307 deletions

View file

@ -18,11 +18,10 @@ const VoiceProfilesType = preload(
)
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
const PlayerMenuType = preload("res://ui/player_menu.gd")
const BagItemSpriteScene = preload(
"res://ui/components/bubble_menu/bag_item_sprite.tscn"
)
const OwnedItemType = preload("res://items/owned_item.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const ItemCatalogResource: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
const PlayersPageType = preload("res://ui/players_page.gd")
const TheNetPageType = preload("res://ui/the_net_page.gd")
const ControllerMappingManagerType = preload(
@ -1077,21 +1076,63 @@ func _validate_inventory_tab_zone_transitions() -> void:
var menu := PlayerMenuScene.instantiate() as Control
root.add_child(menu)
await process_frame
var inventory_state := Node.new()
root.add_child(inventory_state)
var bag := PlayerBag.new()
var catches := FishInventory.new()
var storage_capacity := PlayerCoolerCapacity.new()
var layout := PlayerInventoryLayout.new()
var hotbar := PlayerHotbarType.new()
for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
inventory_state.add_child(node)
bag.setup(ItemCatalogResource)
layout.setup(bag, catches, ItemCatalogResource, storage_capacity)
bag.set_inventory_layout(layout)
catches.set_inventory_layout(layout)
hotbar.setup(bag, ItemCatalogResource, catches, layout)
assert(bag.add_item(&"basic_fishing_rod", 1))
assert(bag.add_item(&"coffee", 1))
assert(layout.move_entry(
PlayerInventoryLayout.EntryKind.ITEM,
&"basic_fishing_rod",
PlayerInventoryLayout.InventoryContainer.INVENTORY,
9,
))
assert(hotbar.assign_item(0, &"coffee"))
menu.set("_bag", bag)
menu.set("_inventory", catches)
menu.set("_hotbar", hotbar)
menu.set("_inventory_layout", layout)
menu.set("_item_catalog", ItemCatalogResource)
var inventory_grid := menu.get("_general_inventory_grid") as GeneralInventoryGrid
inventory_grid.setup(
layout,
bag,
catches,
hotbar,
ItemCatalogResource,
PlayerInventoryLayout.InventoryContainer.INVENTORY,
)
menu.visible = true
menu.call(
"_show_section_immediate",
PlayerMenuType.Section.COOLER,
PlayerMenuType.Section.BAG,
)
menu.call("_set_content_interactive", true)
menu.call("_enter_inventory_tabs_zone")
for _frame: int in 2:
await process_frame
var cooler_tab := menu.get_node("%CoolerSubTab") as Button
var equipment_tab := menu.get_node("%BagSubTab") as Button
var items_tab := menu.get_node("%ItemsSubTab") as Button
var inventory_tab := menu.get_node("%BagSubTab") as Button
var tackle_tab := menu.get_node("%TackleSubTab") as Button
_expect(
root.gui_get_focus_owner() == cooler_tab,
"Inventory tab zone did not begin on the active Cooler tab.",
root.gui_get_focus_owner() == inventory_tab,
"Inventory tab zone did not begin on the unified Inventory tab.",
)
_expect(inventory_tab.text == "Inventory", "Unified tab is not labelled Inventory.")
_expect(
not (menu.get_node("%CoolerSubTab") as Button).visible
and not (menu.get_node("%ItemsSubTab") as Button).visible,
"Legacy Cooler or Items tabs remain visible.",
)
var right := InputEventAction.new()
@ -1103,8 +1144,8 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
await _wait_for_player_menu_page_transition(menu)
_expect(
menu.get("_current_section") == PlayerMenuType.Section.BAG,
"Controller Right did not switch from Cooler to Equipment.",
menu.get("_current_section") == PlayerMenuType.Section.TACKLE_BOX,
"Controller Right did not switch from Inventory to Tackle.",
)
_expect(
menu.get("_controller_ownership")
@ -1112,7 +1153,7 @@ func _validate_inventory_tab_zone_transitions() -> void:
"Changing Inventory tabs entered the item-content zone without A.",
)
_expect(
root.gui_get_focus_owner() == equipment_tab,
root.gui_get_focus_owner() == tackle_tab,
"Changing Inventory tabs did not keep focus on the selected tab.",
)
var down := InputEventAction.new()
@ -1124,59 +1165,33 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
await process_frame
_expect(
root.gui_get_focus_owner() == equipment_tab,
root.gui_get_focus_owner() == tackle_tab,
"Controller Down escaped the Inventory tab zone before A.",
)
_expect(
bool(menu.call("_handle_controller_ownership_input", right)),
"Equipment-to-Items navigation did not consume controller Right.",
)
await _wait_for_player_menu_page_transition(menu)
_expect(
root.gui_get_focus_owner() == items_tab,
"Items tab selection did not retain tab focus before A (focus=%s)."
% root.gui_get_focus_owner(),
)
_expect(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
"Items tab selection changed controller zones before A.",
)
var left := InputEventAction.new()
left.action = &"ui_left"
left.pressed = true
_expect(
bool(menu.call("_handle_controller_ownership_input", left)),
"Items-to-Equipment navigation did not consume controller Left.",
"Tackle-to-Inventory navigation did not consume controller Left.",
)
_expect(
menu.get("_current_section") == PlayerMenuType.Section.BAG
or bool(menu.get("_page_transitioning")),
"Tackle-to-Inventory input did not start the Inventory transition.",
)
await _wait_for_player_menu_page_transition(menu)
_expect(
root.gui_get_focus_owner() == equipment_tab,
"Returning to Equipment did not retain tab focus before A.",
root.gui_get_focus_owner() == inventory_tab,
"Returning to Inventory did not retain tab focus before A.",
)
# Add three representative equipment entries so entering the content zone
# exercises the real five-column directional layout.
var item_field := menu.get_node("%BagItemField") as Control
var bag_nodes: Dictionary = menu.get("_bag_item_nodes")
var owned_items: Array[OwnedItemType] = []
for index: int in 3:
var owned := OwnedItemType.new()
owned.item_id = StringName("controller_test_item_%d" % index)
owned_items.append(owned)
var item_node := BagItemSpriteScene.instantiate() as Button
item_node.set("item_id", owned.item_id)
item_node.position = Vector2(60.0 + float(index) * 180.0, 40.0)
item_field.add_child(item_node)
bag_nodes[owned.item_id] = item_node
menu.set("_sorted_bag_items", owned_items)
menu.call("_set_content_interactive", true)
menu.call("_configure_bag_item_focus")
for item_node: Control in bag_nodes.values():
var active_slots := inventory_grid.get_slots()
for slot: GeneralInventorySlot in active_slots:
_expect(
item_node.focus_mode == Control.FOCUS_NONE,
"Equipment content remained focusable while tabs owned the controller.",
slot.focus_mode == Control.FOCUS_NONE,
"Inventory slots remained focusable while tabs owned the controller.",
)
var accept := InputEventJoypadButton.new()
@ -1193,34 +1208,45 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
for _frame: int in 2:
await process_frame
var first_item := bag_nodes[owned_items[0].item_id] as Control
var second_item := bag_nodes[owned_items[1].item_id] as Control
for item_node: Control in bag_nodes.values():
_expect(
menu.get("_current_section") == PlayerMenuType.Section.BAG,
"Entering Inventory contents changed section to %s."
% menu.get("_current_section"),
)
var first_item := active_slots[0] as Control
var second_item := active_slots[1] as Control
for slot: GeneralInventorySlot in active_slots:
_expect(
item_node.focus_mode == Control.FOCUS_ALL,
"Accepting Equipment did not enable its content focus zone.",
slot.focus_mode == Control.FOCUS_ALL,
"Accepting Inventory did not enable its content focus zone.",
)
_expect(
root.gui_get_focus_owner() == first_item,
"Entering Equipment did not focus its first item.",
"Entering Inventory did not focus its first slot.",
)
_expect(
first_item.focus_neighbor_right == first_item.get_path_to(second_item),
"Equipment items do not provide horizontal controller navigation.",
"Inventory slots do not provide horizontal controller navigation.",
)
Input.parse_input_event(right)
for _frame: int in 2:
await process_frame
_expect(
root.gui_get_focus_owner() == second_item,
"Controller Right did not move between Equipment items.",
menu.get("_current_section") == PlayerMenuType.Section.BAG,
"Inventory slot navigation changed section to %s."
% menu.get("_current_section"),
)
right.pressed = false
Input.parse_input_event(right)
right.pressed = true
_expect(
root.gui_get_focus_owner() == second_item,
"Controller Right did not move between Inventory slots.",
)
var right_release := InputEventAction.new()
right_release.action = &"ui_right"
right_release.pressed = false
Input.parse_input_event(right_release)
_expect(
bool(menu.call("consume_escape")),
"Global player-menu Back did not consume Equipment contents.",
"Global player-menu Back did not consume Inventory contents.",
)
_expect(
menu.visible,
@ -1233,90 +1259,102 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
for _frame: int in 2:
await process_frame
_expect(
menu.get("_current_section") == PlayerMenuType.Section.BAG,
"Returning from Inventory contents changed section to %s."
% menu.get("_current_section"),
)
_expect(
bool(menu.call("_handle_controller_ownership_input", accept)),
"Inventory tab zone did not re-enter Equipment contents.",
"Inventory tab zone did not re-enter Inventory contents.",
)
for _frame: int in 2:
await process_frame
var favorite := menu.get_node("%FavoriteBubble") as BaseButton
var sell := menu.get_node("%SellBubble") as BaseButton
var sell_all := menu.get_node("%SellAllBubble") as BaseButton
for action: BaseButton in [favorite, sell, sell_all]:
action.disabled = false
action.focus_mode = Control.FOCUS_ALL
var notepad_actions: Array[BaseButton] = [favorite, sell, sell_all]
menu.call(
"_configure_controller_notepad_action_focus",
notepad_actions,
)
var notepad_source := active_slots[9]
notepad_source.grab_focus()
var context_press := InputEventJoypadButton.new()
context_press.button_index = JOY_BUTTON_Y
context_press.pressed = true
_expect(
sell.get_node(sell.focus_neighbor_bottom) == sell_all,
"Sell All is not reachable below Sell Fish in the notepad zone.",
)
_expect(
sell_all.get_node(sell_all.focus_neighbor_top) == sell,
"Sell All does not return to the upper notepad actions.",
)
menu.set(
"_controller_ownership",
PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
)
menu.set("_controller_source_section", PlayerMenuType.Section.BAG)
menu.set("_controller_source_identity", owned_items[1].item_id)
menu.call("_apply_inventory_controller_zone_focus_modes")
_expect(
bool(menu.call("consume_escape")),
"Global player-menu Back did not consume the Inventory notepad zone.",
)
_expect(
menu.visible,
"Global player-menu Back closed Inventory from its notepad zone.",
bool(menu.call(
"_handle_controller_ownership_input", context_press
)),
"Controller Y did not open the selected Inventory notepad.",
)
_expect(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
"Inventory notepad did not become the active controller zone.",
)
_expect(
(menu.get_node("%BagDetailConstellation") as Control).visible,
"Inventory notepad remained hidden after controller Y.",
)
_expect(
bool(menu.call("consume_escape")),
"Controller B did not close the Inventory notepad.",
)
_expect(
not (menu.get_node("%BagDetailConstellation") as Control).visible
and menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
"Global player-menu Back did not return the notepad to Inventory contents.",
"Closing the Inventory notepad did not restore the item zone.",
)
for _frame: int in 2:
await process_frame
var hotbar := PlayerHotbarType.new()
menu.set("_hotbar", hotbar)
var hotbar_slots: Array[StringName] = []
hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
hotbar_slots.fill(StringName())
hotbar.set("_slots", hotbar_slots)
var hotbar_fish_slots: Array[StringName] = []
hotbar_fish_slots.resize(PlayerHotbarType.SLOT_COUNT)
hotbar_fish_slots.fill(StringName())
hotbar_fish_slots[0] = &"controller_test_fish"
hotbar.set("_fish_slots", hotbar_fish_slots)
var accept_release := InputEventJoypadButton.new()
accept_release.button_index = JOY_BUTTON_A
accept_release.pressed = false
notepad_source.grab_focus()
menu.call("_handle_controller_ownership_input", accept)
menu.call("_handle_controller_ownership_input", accept_release)
_expect(
str(menu.get("_inventory_move_identity")) == "basic_fishing_rod",
"Controller A did not pick up the focused Inventory item.",
)
var move_target := active_slots[10]
move_target.grab_focus()
menu.call("_handle_controller_ownership_input", accept)
menu.call("_handle_controller_ownership_input", accept_release)
_expect(
layout.get_key_at(
PlayerInventoryLayout.InventoryContainer.INVENTORY, 10
) == PlayerInventoryLayout.item_key(&"basic_fishing_rod"),
"Controller A did not place the picked-up item in its target slot.",
)
var management_requests: Array[int] = [0]
menu.controller_hotbar_management_requested.connect(
func(_initial_slot: int) -> void:
management_requests[0] += 1
)
var hotbar_owned := OwnedItemType.new()
hotbar_owned.item_id = &"controller_hotbar_source"
var hotbar_source := BagItemSpriteScene.instantiate() as Button
hotbar_source.set("item_id", hotbar_owned.item_id)
hotbar_source.position = Vector2(60.0, 40.0)
item_field.add_child(hotbar_source)
bag_nodes[hotbar_owned.item_id] = hotbar_source
var hotbar_items: Array[OwnedItemType] = [hotbar_owned]
menu.set("_sorted_bag_items", hotbar_items)
menu.set(
"_controller_ownership",
PlayerMenuType.ControllerOwnership.ITEM_LIST,
)
menu.call("_apply_inventory_controller_zone_focus_modes")
menu.call("_configure_bag_item_focus")
var hotbar_source := active_slots[10]
_expect(
menu.get("_current_section") == PlayerMenuType.Section.BAG,
"Hotbar navigation test left the unified Inventory section (%s)."
% menu.get("_current_section"),
)
_expect(
hotbar_source.focus_mode == Control.FOCUS_ALL,
"Final active Inventory row was not controller-focusable.",
)
hotbar_source.grab_focus()
_expect(
root.gui_get_focus_owner() == hotbar_source,
"Final Inventory row did not accept controller focus before Hotbar entry.",
)
_expect(
bool(menu.call("_controller_focus_is_on_last_inventory_row")),
"Focused Inventory slot was not recognized as part of the final row.",
)
_expect(
bool(menu.call("_handle_controller_ownership_input", down)),
"Down from the final Equipment row did not enter the hotbar zone.",
"Down from the final Inventory row did not enter the hotbar zone.",
)
_expect(
menu.get("_controller_ownership")
@ -1358,9 +1396,10 @@ func _validate_inventory_tab_zone_transitions() -> void:
"Hotbar management did not consume controller A.",
)
_expect(
hotbar.get_fish_catch_id(0).is_empty(),
"Controller A did not remove the selected fish hotbar assignment.",
hotbar.get_item_id(0).is_empty(),
"Controller A did not return the selected hotbar item to Inventory.",
)
_expect(layout.is_item_in_inventory(&"coffee"), "Cleared hotbar item was lost.")
_expect(
bool(menu.call("consume_escape")),
"Global player-menu Back did not consume hotbar management.",
@ -1376,8 +1415,8 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
for _frame: int in 2:
await process_frame
hotbar.free()
menu.queue_free()
inventory_state.queue_free()
await process_frame

View file

@ -0,0 +1,91 @@
extends SceneTree
const StarterIslandScene = preload(
"res://world/regions/starter_island_region.tscn"
)
const ShovelAttachmentScene = preload("res://player/shovel_attachment.tscn")
const ItemCatalogResource: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
const Gatherables: GatherableCatalog = preload(
"res://gathering/catalog/gatherable_catalog.tres"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_catalog_content()
_validate_flat_shovel()
await _validate_beach_authoring()
print("Digging prototype validation: PASS")
quit()
func _validate_catalog_content() -> void:
var shovel: ItemData = ItemCatalogResource.get_available_item_by_id(
&"standard_shovel"
)
assert(shovel != null)
assert(shovel.category == ItemData.Category.TOOL)
assert(shovel.equippable and shovel.hotbar_allowed)
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
assert(clam != null and clam.is_available())
assert(clam.required_tool_id == shovel.item_id)
assert(clam.diggable_area_id == &"starter_beach")
assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
assert(not clam.requires_sneaking)
assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
func _validate_flat_shovel() -> void:
var attachment := ShovelAttachmentScene.instantiate() as BoneAttachment3D
assert(attachment != null)
var shovel := attachment.get_node("Shovel") as Node3D
assert(shovel != null)
var meshes: Array[MeshInstance3D] = []
_collect_meshes(shovel, meshes)
assert(meshes.size() == 5)
for mesh_instance: MeshInstance3D in meshes:
assert(
mesh_instance.cast_shadow
== GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
var material := mesh_instance.mesh.surface_get_material(0) as StandardMaterial3D
assert(material != null)
assert(material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
assert(material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
assert(material.albedo_texture == null)
assert(is_equal_approx(material.albedo_color.a, 1.0))
attachment.free()
func _validate_beach_authoring() -> void:
var region := StarterIslandScene.instantiate() as WorldRegion
root.add_child(region)
await process_frame
var area: DiggableArea3D = region.get_diggable_area(&"starter_beach")
assert(area != null)
assert(area.terrain_source == NodePath("../../Terrain/Visual"))
assert(area.surface_materials.size() == 1)
assert(area.surface_materials[0] == &"sand")
var triangles: Array[PackedVector3Array] = area.get_surface_triangles()
assert(not triangles.is_empty())
for triangle: PackedVector3Array in triangles:
assert(triangle.size() == 3)
var center := (triangle[0] + triangle[1] + triangle[2]) / 3.0
assert(area.generation_bounds.has_point(Vector2(center.x, center.z)))
assert(center.y <= area.maximum_global_y + 0.001)
region.queue_free()
func _collect_meshes(
root_node: Node,
result: Array[MeshInstance3D],
) -> void:
for child: Node in root_node.get_children():
if child is MeshInstance3D:
result.append(child as MeshInstance3D)
_collect_meshes(child, result)

View file

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

View file

@ -48,8 +48,8 @@ func _run() -> void:
as PlayerAssetReservationService
)
assert(player != null)
assert(catalog != null and catalog.candidates.size() == 314)
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 54)
assert(catalog != null and catalog.candidates.size() == 316)
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 56)
assert(sale_service != null)
assert(shop_service != null)
assert(session != null and session.is_host())
@ -62,7 +62,6 @@ func _run() -> void:
_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))
@ -78,6 +77,8 @@ func _run() -> void:
await _test_fishing_shop_sale_ui(
main, player, catalog, sale_service, reservations
)
_test_host_backpack_purchase(player, shop_service)
_test_host_storage_purchase(player, shop_service)
assert(not sale_service.is_local_sale_pending())
assert(not shop_service.is_local_purchase_pending())
@ -217,6 +218,17 @@ func _run_multiplayer_client() -> void:
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())
var backpack_cost := player.inventory_layout.get_next_backpack_cost()
if player.wallet.get_balance() < backpack_cost:
assert(player.wallet.credit(
backpack_cost - player.wallet.get_balance()
))
_shop_result.clear()
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
while _shop_result.is_empty():
await process_frame
assert(bool(_shop_result[1]))
assert(player.inventory_layout.get_inventory_capacity() == 18)
var required_art_balance: int = (
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE
)
@ -377,80 +389,39 @@ func _test_host_shop_sale(
)
var species_ids: Array[StringName] = []
var expected_payout: int = 0
var species_balance_before: int = player.wallet.get_balance()
for fish: FishData in catalog.candidates:
if not fish.active:
continue
var fish_catch := _make_catch(fish)
player.inventory.add_catch(fish_catch)
assert(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,
)
if species_ids.size() >= 5:
_assert_sale(
player,
sale_service,
species_ids,
true,
true,
NetworkSaleService.MAIN_SHOP_BUYER_ID,
)
species_ids.clear()
if not species_ids.is_empty():
_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(ui_viewport != null)
await _activate_pointer_control(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_pointer_control(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_pointer_control(
control: Control,
ui_viewport: SubViewport,
@ -547,6 +518,48 @@ func _test_host_rod_purchase(
assert(not shop_service.is_local_purchase_pending())
func _test_host_backpack_purchase(
player: Player,
shop_service: NetworkShopService,
) -> void:
var layout := player.inventory_layout
assert(layout.get_inventory_capacity() == 9)
var total_cost: int = 0
for cost: int in PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS:
total_cost += cost
if player.wallet.get_balance() < total_cost:
assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
var balance_before := player.wallet.get_balance()
for expected_capacity: int in [18, 27, 36]:
_shop_result.clear()
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
assert(layout.get_inventory_capacity() == expected_capacity)
assert(player.wallet.get_balance() == balance_before - total_cost)
assert(layout.get_next_backpack_cost() == -1)
func _test_host_storage_purchase(
player: Player,
shop_service: NetworkShopService,
) -> void:
var capacity := player.cooler_capacity
assert(capacity.get_capacity() == 9)
var total_cost: int = 0
for cost: int in PlayerCoolerCapacity.EXPANSION_COSTS:
total_cost += cost
if player.wallet.get_balance() < total_cost:
assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
var balance_before := player.wallet.get_balance()
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
_shop_result.clear()
assert(not shop_service.request_cooler_capacity_upgrade().is_empty())
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
assert(capacity.get_capacity() == expected_capacity)
assert(player.wallet.get_balance() == balance_before - total_cost)
assert(capacity.get_next_cost() == -1)
func _test_fishing_shop_sale_ui(
main: Node,
player: Player,
@ -608,7 +621,11 @@ func _test_fishing_shop_sale_ui(
"QuantityBadge/Quantity"
) as Label
assert(tackle_quantity.text == expected_bait_supply)
assert(tackle_bait_button.tooltip_text.contains(expected_bait_supply))
assert(
str(tackle_bait_button.get_meta(
&"inventory_context_text", ""
)).contains(expected_bait_supply)
)
var tackle_badge := tackle_quantity.get_parent() as Panel
assert(
is_equal_approx(
@ -654,7 +671,10 @@ func _test_fishing_shop_sale_ui(
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"
"ReelPurchase",
"BarrierPurchase",
"CoolerPurchase",
"BackpackPurchase",
]:
var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
assert(upgrade_button != null)
@ -662,6 +682,15 @@ func _test_fishing_shop_sale_ui(
assert(upgrade_button.text.is_empty())
assert(upgrade_button.icon != null)
assert(upgrade_button.tooltip_text.contains("level"))
for placeholder_name: String in ["CoolerPurchase", "BackpackPurchase"]:
var placeholder_button := shop.get_node(
"%%%s" % placeholder_name
) as Button
assert(
placeholder_button.icon.resource_path.ends_with(
"/pictograms/x_light.png"
)
)
assert(not shop.has_node("%ReelLevel"))
assert(not shop.has_node("%BarrierEffect"))
assert(not shop.has_node("%CoolerLevel"))
@ -674,7 +703,9 @@ func _test_fishing_shop_sale_ui(
"/shop/32_currency.png"
)
)
for cost_name: String in ["ReelCost", "BarrierCost", "CoolerCost"]:
for cost_name: String in [
"ReelCost", "BarrierCost", "CoolerCost", "BackpackCost"
]:
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
@ -690,7 +721,7 @@ func _test_fishing_shop_sale_ui(
var art_supplies_tab := shop_tabs[4] as Button
assert(art_supplies_tab != null and art_supplies_tab.text == "Art Supplies")
var sell_mode := shop_tabs[5] as Button
assert(sell_mode != null and sell_mode.text == "Sell Fish")
assert(sell_mode != null and sell_mode.text == "Sell")
var equipment_tab := shop_tabs[3] as Button
assert(equipment_tab != null and equipment_tab.text == "Equipment")
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
@ -881,44 +912,47 @@ func _test_fishing_shop_sale_ui(
assert((shop.get_node("%ShopPanel") as Control).visible)
assert(not (shop.get_node("ShopPanel/Margin/Layout/Body") as Control).visible)
assert(not (shop.get_node("%Feedback") as Control).visible)
var mounted_cooler := player_menu.get("_cooler_page") as Control
assert(mounted_cooler != null and mounted_cooler.visible)
var sell_inventory := shop.get("_sell_inventory") as ShopSellInventory
assert(sell_inventory != null and sell_inventory.visible)
var sell_tray_grid := sell_inventory.get("_tray_grid") as GridContainer
assert(
mounted_cooler.get_parent() == shop.get_node("%ShopCoolerMount")
sell_tray_grid.columns == PlayerInventoryLayout.INVENTORY_COLUMNS
)
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)
var cooler_sort_option := player_menu.get("_cooler_sort_option") as Control
await _activate_pointer_control(cooler_sort_option, ui_viewport)
var cooler_choice_panel := cooler_sort_option.get("_choice_panel") as Control
assert(cooler_choice_panel.visible)
cooler_sort_option.call("close_choices")
assert(
StringName(
(player_menu.get("_sale_buyer_override") as FishBuyerProfile).id
) == NetworkSaleService.MAIN_SHOP_BUYER_ID
sell_tray_grid.get_theme_constant("h_separation")
== GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
)
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_pointer_control(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_pointer_control(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
sell_inventory.get_parent() == shop.get_node("%ShopCoolerMount")
)
var staged: Dictionary = sell_inventory.get("_staged")
sell_inventory.call(
"_stage",
PlayerInventoryLayout.EntryKind.CATCH,
reserved_catch.catch_id,
)
assert(staged.is_empty())
sell_inventory.call(
"_stage",
PlayerInventoryLayout.EntryKind.CATCH,
fish_catch.catch_id,
)
assert(staged.has(PlayerInventoryLayout.catch_key(fish_catch.catch_id)))
assert((sell_inventory.get("_feedback") as Label).text.is_empty())
var source_grid := sell_inventory.get("_inventory_grid") as GeneralInventoryGrid
var staged_source_found := false
for source_slot: GeneralInventorySlot in source_grid.get_slots():
if source_slot.entry_identity == fish_catch.catch_id:
staged_source_found = bool(source_slot.get("_staged"))
break
assert(staged_source_found)
var feedback := sell_inventory.get("_feedback") as Label
var total_label := sell_inventory.get("_total_label") as Label
var sell_button := sell_inventory.get("_sell_button") as Button
assert(feedback.get_index() < total_label.get_parent().get_index())
assert(total_label.get_parent().get_index() < sell_button.get_index())
_sale_result.clear()
await _activate_pointer_control(confirm_button, ui_viewport)
sell_inventory.call("_submit_sale")
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))
@ -929,7 +963,7 @@ func _test_fishing_shop_sale_ui(
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())
assert(not sell_inventory.is_visible_in_tree())
shop.close_shop()
await shop.menu_visibility_changed
assert(not shop.visible)

View file

@ -55,6 +55,17 @@ func _initialize() -> void:
assert(crab_net != null)
assert(crab_net.icon != null)
assert(crab_net.icon.resource_path.ends_with("/equipment/temp_net.png"))
var shovel: ItemDataType = ItemCatalogResource.get_available_item_by_id(
&"standard_shovel"
)
assert(shovel != null)
assert(shovel.category == ItemDataType.Category.TOOL)
assert(shovel.icon != null)
assert(shovel.equippable)
assert(shovel.hotbar_allowed)
assert(FishingShopStockType.get_price(&"standard_shovel") == 75)
assert(FishingShopStockType.get_stock_item_ids().has(&"standard_shovel"))
assert(FishingShopStockType.is_permanent_unlock(&"standard_shovel", shovel))
var wallet := PlayerWalletType.new()
wallet.current_balance = 250

View file

@ -121,7 +121,7 @@ func _validate_weight_based_display_scale() -> void:
func _validate_catalog_and_pools() -> void:
assert(Catalog.candidates.size() == 314)
assert(Catalog.candidates.size() == 316)
assert(PondPool.candidates.size() == 19)
assert(OceanPool.candidates.size() == 34)
var active_count: int = 0
@ -142,7 +142,7 @@ func _validate_catalog_and_pools() -> void:
inactive_count += 1
assert(not fish.is_selectable())
assert(fish.display_texture == null)
assert(active_count == 54)
assert(active_count == 56)
assert(inactive_count == 260)
var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"bowfin")
assert(inactive_fish != null and not inactive_fish.active)
@ -455,6 +455,7 @@ func _validate_catches_and_authoritative_sale() -> void:
1,
"catalog_sale_%d" % catch_sequence,
[loaded.to_network_dict()],
[],
PelicanBuyer,
)
assert(bool(sale_result.get("accepted", false)))

View file

@ -104,7 +104,7 @@ func _run() -> void:
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
assert(int((parsed as Dictionary)["save_version"]) == 7)
assert(int((parsed as Dictionary)["save_version"]) == 8)
assert(
int((parsed as Dictionary)["experience"]["total_experience"])
== 125

View file

@ -305,6 +305,7 @@ func _validate_catch_round_trip_and_sale() -> void:
1,
"quality_sale",
[network_data],
[],
PelicanBuyer,
)
assert(bool(accepted.get("accepted", false)))
@ -336,6 +337,7 @@ func _validate_catch_round_trip_and_sale() -> void:
1,
"quality_sale_forged",
[forged],
[],
PelicanBuyer,
)
assert(not bool(rejected.get("accepted", false)))
@ -431,7 +433,7 @@ func _validate_version_four_migration() -> void:
version_four,
4,
)
assert(int(migrated.get("save_version", -1)) == 7)
assert(int(migrated.get("save_version", -1)) == 8)
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
assert(
is_equal_approx(

View file

@ -288,9 +288,9 @@ func _validate_fur_color_ui(snapshot: Dictionary) -> void:
var picker_popup := picker.get_popup()
var picker_control := picker.get_picker()
assert(color_panel != null and color_panel.size.x > 360.0)
assert(channel_grid != null and channel_grid.columns == 4)
assert(channel_grid != null and channel_grid.columns == 2)
assert(channel_grid.get_child_count() == 4)
assert(palette_grid != null and palette_grid.columns == 10)
assert(palette_grid != null and palette_grid.columns == 6)
assert(palette_grid.get_child_count() == 30)
for option_button: Button in palette_grid.get_children():
assert(

View file

@ -1,14 +1,17 @@
extends SceneTree
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
const HotbarScene = preload("res://ui/hotbar.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 LEGACY_COOLER_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
const LEGACY_COOLER_NOTEPAD_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
const INVENTORY_PANEL_RECT := Rect2(199.0, 166.0, 882.0, 484.0)
const NOTEPAD_HOST_RECT := Rect2(501.0, 166.0, 278.0, 484.0)
const NOTEPAD_ART_RECT := Rect2(
NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
EXPECTED_ART_SIZE,
@ -57,6 +60,11 @@ func _run() -> void:
+ InventoryNotepad.NOTEPAD_ART_OFFSET
))
_validate_shared_inventory_geometry(player_menu)
_validate_first_open_inventory(player_menu)
await _validate_inventory_grid_centering(player_menu, presentation_stage)
_validate_utility_page_geometry(player_menu)
await _validate_profile_content_bounds(player_menu)
await _validate_hotbar_centering(presentation_stage)
_validate_inventory_layering(player_menu)
_validate_tackle_to_items_transition(player_menu)
_validate_cooler_notepad_typography(player_menu)
@ -103,23 +111,151 @@ func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
for node_name: StringName in [
&"CoolerOuterWall",
&"BagOuterWall",
&"TackleMainPanel",
]:
var main_panel := player_menu.get_node("%%%s" % node_name) as Control
assert(main_panel != null)
assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
assert(main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size))
assert(
main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size),
"%s was %s, expected %s" % [
node_name,
main_panel.size,
INVENTORY_PANEL_RECT.size,
],
)
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
var cooler_notepad := player_menu.get_node("%DetailConstellation") as Control
assert(cooler_panel.position.is_equal_approx(
LEGACY_COOLER_PANEL_RECT.position
))
assert(cooler_panel.size.is_equal_approx(LEGACY_COOLER_PANEL_RECT.size))
assert(cooler_notepad.position.is_equal_approx(
LEGACY_COOLER_NOTEPAD_RECT.position
))
assert(cooler_notepad.size.is_equal_approx(
LEGACY_COOLER_NOTEPAD_RECT.size
))
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.position.is_equal_approx(NOTEPAD_HOST_RECT.position))
assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
assert(is_equal_approx(INVENTORY_PANEL_RECT.get_center().x, 640.0))
func _validate_first_open_inventory(player_menu: PlayerMenu) -> void:
var legacy_slots: Array = player_menu.get("_bag_slot_nodes") as Array
assert(legacy_slots.is_empty())
var old_slot_nodes: Array[Node] = player_menu.find_children(
"*", "BagStorageSlot", true, false
)
assert(old_slot_nodes.is_empty())
func _validate_inventory_grid_centering(
player_menu: PlayerMenu,
stage: Control,
) -> void:
var grid := player_menu.get("_general_inventory_grid") as Control
assert(grid != null)
grid.custom_minimum_size = Vector2(782.0, 342.0)
grid.size = grid.custom_minimum_size
player_menu.call("_layout_general_inventory_grid")
await process_frame
assert(is_equal_approx(grid.position.y, 39.0))
assert(
is_equal_approx(
grid.get_global_rect().get_center().x,
stage.get_global_rect().get_center().x,
),
"inventory center %s did not match stage center %s" % [
grid.get_global_rect().get_center().x,
stage.get_global_rect().get_center().x,
],
)
func _validate_utility_page_geometry(player_menu: PlayerMenu) -> void:
for page_name: StringName in [
&"TheNetPage",
&"MailPage",
&"ProfilePage",
&"PlayersPage",
]:
var page := player_menu.get_node("%%%s" % page_name) as Control
assert(page != null)
var shell := page.find_child(
"UtilityMainBox", true, false
) as Control
assert(shell != null)
assert(shell.position.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.position))
assert(shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size))
assert(is_equal_approx(shell.get_rect().get_center().x, 640.0))
func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void:
var profile := player_menu.get_node("%ProfilePage") as ProfilePage
assert(profile != null)
var menu_was_visible: bool = player_menu.visible
var profile_was_visible: bool = profile.visible
player_menu.visible = true
profile.visible = true
await process_frame
await process_frame
var option_list := profile.get("_option_list") as Control
var body := option_list.get_parent() as Control
var preview := profile.get("_preview") as Control
var shell := profile.find_child("UtilityMainBox", true, false) as Control
assert(option_list != null and body != null and preview != null and shell != null)
for category_id: String in ["fur_pattern", "voice"]:
profile.call("_select_category", category_id)
await process_frame
await process_frame
assert(
body.get_combined_minimum_size().x <= body.size.x,
"%s customization content overflowed its body: %s > %s" % [
category_id,
body.get_combined_minimum_size().x,
body.size.x,
],
)
assert(
preview.get_global_rect().end.x
<= shell.get_global_rect().end.x,
"%s preview overflowed the utility content area" % category_id,
)
profile.visible = profile_was_visible
player_menu.visible = menu_was_visible
func _validate_hotbar_centering(parent: Control) -> void:
var hotbar := HotbarScene.instantiate() as HotbarUI
parent.add_child(hotbar)
await process_frame
var presentation := hotbar.get_node(
"%HotbarPresentationScaleRoot"
) as Control
var field := hotbar.get_node("%BubbleField") as Control
hotbar.set_player_menu_context(true)
var displayed_center_x: float = (
presentation.position.x
+ field.get_rect().get_center().x * presentation.scale.x
)
assert(is_equal_approx(displayed_center_x, 640.0))
var displayed_top: float = (
presentation.position.y + field.position.y * presentation.scale.y
)
var displayed_bottom: float = (
displayed_top + field.size.y * presentation.scale.y
)
assert(displayed_top < INVENTORY_PANEL_RECT.end.y)
assert(displayed_bottom > INVENTORY_PANEL_RECT.end.y)
hotbar.queue_free()
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
@ -136,13 +272,31 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
assert(cooler_panel != null)
assert(tackle_panel != null)
assert(inventory_tabs != null)
assert(items_tab != null and items_tab.text == "Items")
assert(bait_list != null and bait_list.columns == 3)
assert(lure_list != null and lure_list.columns == 3)
assert(items_tab != null and not items_tab.visible)
assert(not (player_menu.get_node("%CoolerSubTab") as Button).visible)
assert((player_menu.get_node("%BagSubTab") as Button).text == "Inventory")
assert(bait_list != null and bait_list.columns == 4)
assert(lure_list != null and lure_list.columns == 4)
assert(bait_list.get_theme_constant("h_separation") == 28)
assert(lure_list.get_theme_constant("h_separation") == 28)
assert(sale_confirmation.z_index > cooler_panel.z_index)
assert(sale_confirmation.z_index > tackle_panel.z_index)
assert(
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
> bag_panel.z_index
)
assert(
(player_menu.get_node("%BagModalBlocker") as Control).z_index
> bag_panel.z_index
)
assert(
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
> (player_menu.get_node("%BagModalBlocker") as Control).z_index
)
assert(
(player_menu.get_node("%TackleDetailPanel") as Control).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)))
@ -159,25 +313,55 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
var empty_state := player_menu.get_node("%BagEmptyState") as Label
player_menu.set("_bag_view", PlayerMenu.BagView.EQUIPMENT)
player_menu.call("_refresh_bag")
assert(empty_state.text == "No equipment in your Bag.")
var inventory_notepad := player_menu.get_node(
"%BagDetailBubble"
) as InventoryNotepad
assert(inventory_notepad != null)
assert(inventory_notepad.title_text == "inventory notes")
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
)
player_menu.call("_show_bag_view", PlayerMenu.BagView.CONSUMABLES)
assert(
player_menu.get("_current_section") == PlayerMenu.Section.BAG
)
assert(
player_menu.get("_bag_view") == PlayerMenu.BagView.CONSUMABLES
)
assert(empty_state.text == "No items in your Bag.")
player_menu.call("_show_inventory_tab", 0)
player_menu.call("_cancel_page_tween")
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.COOLER
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
player_menu.call("_update_bag_detail")
assert(player_menu.get("_current_section") == PlayerMenu.Section.BAG)
assert(not (player_menu.get_node("%BagDetailConstellation") as Control).visible)
assert(
(player_menu.get_node("%BagSpriteDetailData") as Label).text
== "select an item for details."
)
player_menu.visible = true
player_menu.call("_set_content_interactive", true)
var no_actions: Array[BaseButton] = []
player_menu.call(
"_open_inventory_notepad",
PlayerMenu.Section.BAG,
StringName("modal-test"),
no_actions,
)
assert((player_menu.get_node("%BagDetailConstellation") as Control).visible)
assert((player_menu.get_node("%BagModalBlocker") as Control).visible)
assert(
(player_menu.get_node("%BagModalBlocker") as Control).mouse_filter
== Control.MOUSE_FILTER_STOP
)
assert(
(player_menu.get_node("%InventoryTab") as Button).focus_mode
== Control.FOCUS_NONE
)
assert(
(player_menu.get_node("%BagSpriteDetailData") as Label).get_theme_font(
"font"
) == InventoryNotepad.NOTEPAD_FONT
)
player_menu.call(
"_release_controller_ownership", false, false
)
assert(not (player_menu.get_node("%BagModalBlocker") as Control).visible)
player_menu.call("_set_content_interactive", false)
player_menu.visible = false
player_menu.call("_cancel_page_tween")
func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
@ -314,17 +498,16 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
return
player_menu.visible = true
var sections: Array[PlayerMenu.Section] = [
PlayerMenu.Section.COOLER,
PlayerMenu.Section.BAG,
PlayerMenu.Section.TACKLE_BOX,
]
var suffixes: Array[String] = ["cooler", "equipment", "tackle"]
var suffixes: Array[String] = ["inventory", "tackle"]
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)
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var confirmation_message := player_menu.get_node(
"%ConfirmationMessage"

View file

@ -1,31 +1,7 @@
extends SceneTree
const BagItemSpriteType = preload(
"res://ui/components/bubble_menu/bag_item_sprite.gd"
)
const BagStorageSlotType = preload(
"res://ui/components/bubble_menu/bag_storage_slot.gd"
)
const Catalog: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
const OwnedItemType = preload("res://items/owned_item.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
const PlayerMenuType = preload("res://ui/player_menu.gd")
const EQUIPMENT_IDS: Array[StringName] = [
&"basic_fishing_rod",
&"art_kit",
&"crab_net",
&"magnet",
]
const ITEM_IDS: Array[StringName] = [
&"coffee",
&"energy_drink",
&"snack",
]
const Catalog: ItemCatalog = preload("res://items/catalog/item_catalog.tres")
const Bluegill: FishData = preload("res://fish/species/bluegill/bluegill.tres")
func _initialize() -> void:
@ -33,140 +9,119 @@ func _initialize() -> void:
func _run() -> void:
root.size = Vector2i(1280, 720)
var bag := PlayerBagType.new()
var state := Node.new()
root.add_child(state)
var bag := PlayerBag.new()
var catches := FishInventory.new()
var storage_capacity := PlayerCoolerCapacity.new()
var layout := PlayerInventoryLayout.new()
var hotbar := PlayerHotbar.new()
for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
state.add_child(node)
bag.setup(Catalog)
for item_id: StringName in EQUIPMENT_IDS + ITEM_IDS:
assert(bag.add_item(item_id))
for index: int in EQUIPMENT_IDS.size():
assert(bag.get_storage_slot(EQUIPMENT_IDS[index]) == index)
for index: int in ITEM_IDS.size():
assert(bag.get_storage_slot(ITEM_IDS[index]) == index)
layout.setup(bag, catches, Catalog, storage_capacity)
bag.set_inventory_layout(layout)
catches.set_inventory_layout(layout)
hotbar.setup(bag, Catalog, catches, layout)
assert(bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
assert(bag.get_storage_slot(&"basic_fishing_rod") == 14)
assert(bag.move_item_to_storage_slot(&"crab_net", 14))
assert(bag.get_storage_slot(&"crab_net") == 14)
assert(bag.get_storage_slot(&"basic_fishing_rod") == 2)
var saved_record: Dictionary = bag.get_owned_item(
&"crab_net"
).to_save_dict()
assert(int(saved_record.get("storage_slot", -1)) == 14)
assert(bag.add_item(&"basic_fishing_rod", 1))
assert(bag.add_item(&"coffee", 3))
var fish_catch := _make_bluegill()
assert(catches.add_catch(fish_catch))
var legacy_equipment := OwnedItemType.new()
legacy_equipment.item_id = &"basic_fishing_rod"
var legacy_item := OwnedItemType.new()
legacy_item.item_id = &"coffee"
var legacy_records: Array[OwnedItemType] = [
legacy_equipment,
legacy_item,
]
var legacy_bag := PlayerBagType.new()
legacy_bag.setup(Catalog)
assert(legacy_bag.replace_all_items(legacy_records))
assert(legacy_bag.get_storage_slot(&"basic_fishing_rod") == 0)
assert(legacy_bag.get_storage_slot(&"coffee") == 0)
legacy_bag.free()
var menu := PlayerMenuScene.instantiate() as PlayerMenu
root.add_child(menu)
await process_frame
menu.set("_bag", bag)
var hotbar := PlayerHotbarType.new()
hotbar.setup(bag, Catalog)
menu.set("_hotbar", hotbar)
menu.set("_item_catalog", Catalog)
menu.set("_bag_view", PlayerMenuType.BagView.EQUIPMENT)
menu.visible = true
menu.call("_show_section_immediate", PlayerMenuType.Section.BAG)
menu.call("_set_content_interactive", true)
menu.call("_refresh_bag")
await process_frame
_validate_grid(menu, EQUIPMENT_IDS.size())
var item_nodes: Dictionary = menu.get("_bag_item_nodes")
var crab_node := item_nodes.get(&"crab_net") as BagItemSpriteType
assert(crab_node != null)
menu.set(
"_controller_ownership",
PlayerMenuType.ControllerOwnership.ITEM_LIST,
var inventory_grid := GeneralInventoryGrid.new()
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
root.add_child(inventory_grid)
inventory_grid.setup(
layout,
bag,
catches,
hotbar,
Catalog,
PlayerInventoryLayout.InventoryContainer.INVENTORY,
)
menu.call("_apply_inventory_controller_zone_focus_modes")
crab_node.grab_focus()
assert(bool(menu.call("_try_begin_controller_storage_placement")))
await process_frame
assert(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
)
var slots: Array = menu.get("_bag_slot_nodes")
var bottom_slot := slots[12] as BagStorageSlotType
bottom_slot.grab_focus()
var down := InputEventAction.new()
down.action = &"ui_down"
down.pressed = true
assert(bool(menu.call("_handle_controller_ownership_input", down)))
assert(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.HOTBAR_PLACEMENT
)
var up := InputEventAction.new()
up.action = &"ui_up"
up.pressed = true
assert(bool(menu.call("_handle_controller_ownership_input", up)))
assert(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
var storage_grid := GeneralInventoryGrid.new()
root.add_child(storage_grid)
storage_grid.setup(
layout,
bag,
catches,
hotbar,
Catalog,
PlayerInventoryLayout.InventoryContainer.STORAGE,
)
await process_frame
assert(root.gui_get_focus_owner() == bottom_slot)
assert(StringName(menu.get("_controller_storage_identity")) == &"crab_net")
var target_slot := slots[7] as BagStorageSlotType
target_slot.grab_focus()
menu.call("_confirm_controller_storage_placement")
await process_frame
assert(bag.get_storage_slot(&"crab_net") == 7)
assert(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.ITEM_LIST
var inventory_slots: Array = inventory_grid.get("_slots")
var storage_slots: Array = storage_grid.get("_slots")
assert(inventory_slots.size() == 36)
assert(inventory_grid.get_slots().size() == 9)
assert(inventory_grid.custom_minimum_size == Vector2(782.0, 342.0))
assert(storage_capacity.get_capacity() == 9)
assert(storage_slots.size() == 72)
assert(storage_grid.get_slots().size() == 9)
assert((storage_slots[0] as Control).custom_minimum_size == Vector2(52.0, 52.0))
assert((storage_slots[9] as GeneralInventorySlot).disabled)
var coffee_slot := _find_slot(inventory_slots, &"coffee")
assert(coffee_slot != null)
(inventory_slots[7] as GeneralInventorySlot).call(
"_drop_data",
Vector2.ZERO,
{"kind": "bag_item", "item_id": "coffee"},
)
assert(int(layout.get_entry(
PlayerInventoryLayout.EntryKind.ITEM, &"coffee"
).get("slot", -1)) == 7)
assert(coffee_slot.entry_identity.is_empty())
menu.call("_show_bag_view", PlayerMenuType.BagView.CONSUMABLES)
await process_frame
_validate_grid(menu, ITEM_IDS.size())
menu.call("_on_bag_item_dropped", &"coffee", 12)
await process_frame
assert(bag.get_storage_slot(&"coffee") == 12)
(storage_slots[0] as GeneralInventorySlot).call(
"_drop_data",
Vector2.ZERO,
{"kind": "cooler_fish", "catch_id": String(fish_catch.catch_id)},
)
assert(layout.get_container(
PlayerInventoryLayout.EntryKind.CATCH, fish_catch.catch_id
) == PlayerInventoryLayout.InventoryContainer.STORAGE)
assert(hotbar.assign_item(0, &"basic_fishing_rod"))
assert(not layout.is_item_in_inventory(&"basic_fishing_rod"))
(inventory_slots[8] as GeneralInventorySlot).call(
"_drop_data",
Vector2.ZERO,
{"kind": "hotbar_slot", "slot_index": 0},
)
assert(hotbar.get_item_id(0).is_empty())
assert(layout.is_item_in_inventory(&"basic_fishing_rod"))
assert(int(layout.get_entry(
PlayerInventoryLayout.EntryKind.ITEM, &"basic_fishing_rod"
).get("slot", -1)) == 8)
menu.queue_free()
hotbar.free()
bag.free()
await process_frame
print("Inventory storage validation: PASS")
state.free()
inventory_grid.free()
storage_grid.free()
quit()
func _validate_grid(menu: PlayerMenu, expected_items: int) -> void:
var slots: Array = menu.get("_bag_slot_nodes")
assert(slots.size() == PlayerBagType.MIN_STORAGE_SLOT_COUNT)
var seen_positions: Dictionary[Vector2, bool] = {}
for index: int in slots.size():
var slot := slots[index] as BagStorageSlotType
assert(slot != null)
assert(slot.storage_slot_index == index)
assert(not seen_positions.has(slot.position))
seen_positions[slot.position] = true
var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
assert(normal != null)
assert(normal.bg_color.a >= 0.7)
assert((slots[1] as Control).position.x > (slots[0] as Control).position.x)
assert((slots[5] as Control).position.y > (slots[0] as Control).position.y)
var item_nodes := menu.get("_bag_item_nodes") as Dictionary
assert(
item_nodes.size() == expected_items,
"expected %d visible bag items, found %d: %s" % [
expected_items,
item_nodes.size(),
str(item_nodes.keys()),
],
func _make_bluegill() -> FishCatch:
var fish_catch := FishCatch.new()
fish_catch.fish = Bluegill
fish_catch.fish_id = Bluegill.id
fish_catch.catch_id = &"bluegill:inventory_storage_test"
fish_catch.weight_lb = Bluegill.get_minimum_weight()
fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
fish_catch.weight_lb
)
fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
fish_catch.weight_lb
)
return fish_catch
func _find_slot(slots: Array, identity: StringName) -> GeneralInventorySlot:
for candidate: Variant in slots:
var slot := candidate as GeneralInventorySlot
if slot != null and slot.entry_identity == identity:
return slot
return null

View file

@ -203,7 +203,7 @@ func _run() -> void:
save_file.close()
assert(typeof(parsed) == TYPE_DICTIONARY)
var save_data: Dictionary = parsed
assert(int(save_data.get("save_version", -1)) == 7)
assert(int(save_data.get("save_version", -1)) == 8)
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
_validate_pause_session_switch(main, session)

View file

@ -108,9 +108,9 @@ func _validate_save_round_trip(
var player := main.get("_player") as Player
var catalog := main.get("fish_catalog") as FishPool
assert(catalog != null)
assert(catalog.candidates.size() == 314)
assert(catalog.candidates.size() == 316)
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
assert(active_species.size() == 54)
assert(active_species.size() == 56)
for index: int in 4:
_add_test_catch(player, active_species[index])
assert(save_manager.save_now())
@ -131,7 +131,7 @@ func _validate_save_round_trip(
assert(player.inventory.replace_all_catches(no_catches, 1))
assert(player.collection_log.replace_discovered_ids(no_discoveries))
assert(save_manager.load_player_data())
assert(player.inventory.get_all_catches().size() == 54)
assert(player.inventory.get_all_catches().size() == 56)
for fish: FishData in active_species:
assert(player.inventory.get_count(fish.id) == 1)
assert(player.collection_log.has_discovered(fish.id))

View file

@ -40,9 +40,9 @@ func _run() -> void:
func _validate_catalog() -> void:
assert(CatalogResource.candidates.size() == 314)
assert(CatalogResource.candidates.size() == 316)
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
assert(ordered.size() == 54)
assert(ordered.size() == 56)
var previous_number: int = 0
var catalog_numbers: Dictionary[int, bool] = {}
for fish: FishDataType in CatalogResource.candidates:
@ -113,6 +113,10 @@ func _validate_page() -> void:
LogbookCatalog.Category.OTHER,
])
assert(category_tabs.size() == category_tab_categories.size())
assert(
LogbookCatalog.category_label(LogbookCatalog.Category.OTHER)
== "Insects"
)
for tab_node: Variant in category_tabs:
var category_tab := tab_node as Button
assert(category_tab.size == LogbookPage.LOGBOOK_TAB_SIZE)
@ -203,16 +207,18 @@ func _validate_page() -> void:
page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
await create_timer(0.25).timeout
assert((page.get("_entry_buttons") as Dictionary).size() == 1)
assert((page.get("_entry_buttons") as Dictionary).size() == 2)
assert(
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
)
assert(
(page.get("_entry_buttons") as Dictionary).has(&"unknown_6406")
)
page.call("_select_category", LogbookCatalog.Category.OTHER)
await create_timer(0.25).timeout
assert((page.get("_entry_buttons") as Dictionary).is_empty())
assert((page.get("_entry_buttons") as Dictionary).size() == 1)
assert(
(page.get("_empty_state") as Label).text
== "No entries available."
(page.get("_entry_buttons") as Dictionary).has(&"unknown_8001")
)
page.call("_select_category", LogbookCatalog.Category.FRESH_WATER)
await create_timer(0.25).timeout

View file

@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
version_five,
5,
)
assert(int(migrated.get("save_version", -1)) == 7)
assert(int(migrated.get("save_version", -1)) == 8)
var experience_data: Dictionary = migrated.get("experience", {})
assert(int(experience_data.get("total_experience", -1)) == 0)
var world_data: Dictionary = migrated.get("world", {})

View file

@ -62,10 +62,14 @@ func _run() -> void:
assert(StringName(player.get("_animation_action_id")).is_empty())
assert(animation_player.current_animation == &"idle")
Input.action_press(&"sneak")
var visuals := player.get_node("Visuals") as Node3D
assert(player.play_net_strike_visual())
player.resolve_net_strike_visual(true)
player.net_success_contact_pause_duration = 0.05
player.showcase_turn_duration = 0.05
player.showcase_camera_transition_duration = 0.05
player.net_showcase_camera_yaw_offset = 0.0
var crab_catch := FishCatch.new()
crab_catch.fish = CrabBrown
crab_catch.fish_id = CrabBrown.id
@ -89,9 +93,27 @@ func _run() -> void:
await contact_pause.finished
assert(StringName(player.get("_animation_action_id")).is_empty())
assert(bool(player.get("_showcase_animation_active")))
var stored_showcase_rotation: Vector3 = player.get(
"_showcase_visual_rotation"
)
await create_timer(0.1).timeout
assert(
absf(wrapf(
visuals.rotation.y - stored_showcase_rotation.y,
-PI,
PI,
)) > 3.0
)
var catch_display := player.get("_catch_display") as Node3D
assert(catch_display != null and catch_display.visible)
player.end_catch_showcase(Callable(), true)
player.end_catch_showcase(Callable(), true, true)
assert(
absf(wrapf(
visuals.rotation.y - stored_showcase_rotation.y,
-PI,
PI,
)) < 0.01
)
Input.action_release(&"sneak")
player.queue_free()

View file

@ -45,6 +45,17 @@ func _run() -> void:
for owned: OwnedItem in tackle_items:
sorted_ids.append(owned.item_id)
assert(sorted_ids == EXPECTED_BAIT_ORDER)
var bag := PlayerBag.new()
bag.setup(Catalog)
assert(bag.add_item(&"worms", 1))
assert(bag.add_item(&"shrimp", 1))
var worms_slot: int = bag.get_storage_slot(&"worms")
var shrimp_slot: int = bag.get_storage_slot(&"shrimp")
assert(worms_slot >= 0 and shrimp_slot >= 0 and worms_slot != shrimp_slot)
assert(bag.move_item_to_storage_slot(&"worms", shrimp_slot))
assert(bag.get_storage_slot(&"worms") == shrimp_slot)
assert(bag.get_storage_slot(&"shrimp") == worms_slot)
bag.free()
player_menu.free()
print("Tackle order validation: PASS")
quit()

View file

@ -0,0 +1,99 @@
extends SceneTree
const StarterIslandScene = preload(
"res://world/regions/starter_island_region.tscn"
)
const Gatherables: GatherableCatalog = preload(
"res://gathering/catalog/gatherable_catalog.tres"
)
const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_beetle_data()
await _validate_tree_anchors()
_validate_anchored_presentation()
_validate_three_dimensional_targeting()
print("Tree gathering prototype validation: PASS")
quit()
func _validate_beetle_data() -> void:
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
assert(beetle != null and beetle.is_available())
assert(beetle.catch_data == FishCatalog.get_fish_by_id(&"beetle_stag_common"))
assert(beetle.catch_data.display_texture != null)
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
assert(beetle.catch_data.collection_group == &"Beetles")
assert(beetle.required_tool_id == &"crab_net")
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
assert(beetle.population == 3)
assert(is_equal_approx(beetle.sprite_pixel_size, 0.005))
assert(beetle.is_stationary_spawn())
assert(not beetle.can_be_scared())
func _validate_tree_anchors() -> void:
var region := StarterIslandScene.instantiate() as WorldRegion
root.add_child(region)
await process_frame
var anchor_set: GatherableAnchorSet3D = region.get_gatherable_anchor_set(
&"starter_reachable_tree_trunks"
)
assert(anchor_set != null)
var positions: PackedVector3Array = anchor_set.get_spawn_positions()
assert(positions.size() == 8)
for position: Vector3 in positions:
assert(position.is_finite())
assert(position.y >= 4.3 and position.y <= 4.5)
region.queue_free()
func _validate_anchored_presentation() -> void:
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
var presentation := WorldGatherable.new()
root.add_child(presentation)
presentation.configure("beetle-visual", beetle, Vector3.ZERO, 0.0)
var sprite := presentation.get_node("GatherableSprite") as Sprite3D
assert(sprite != null)
assert(is_zero_approx(sprite.position.y))
assert(is_equal_approx(sprite.pixel_size, 0.005))
assert(not sprite.shaded)
assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
presentation.queue_free()
func _validate_three_dimensional_targeting() -> void:
var service := NetworkWorldSpawnService.new()
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
service.set(
"_entities",
{
"beetle-test": {
"entity_id": "beetle-test",
"type_id": &"beetle_stag_common",
"data": beetle,
"position": Vector3(2.0, 4.4, 3.0),
"locked": false,
},
},
)
assert(
service.find_capture_target(
Vector3(2.0, 4.4, 3.0),
&"crab_net",
)
== "beetle-test"
)
assert(
service.find_capture_target(
Vector3(2.0, 3.0, 3.0),
&"crab_net",
).is_empty()
)
service.free()

View file

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

View file

@ -0,0 +1,199 @@
extends SceneTree
const ItemCatalogResource: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
const Bluegill: FishData = preload(
"res://fish/species/bluegill/bluegill.tres"
)
const MainShopBuyer: FishBuyerProfile = preload(
"res://economy/buyers/main_fishing_shop.tres"
)
const FishCatalogResource: FishPool = preload(
"res://fish/pools/fish_catalog.tres"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var root := Node.new()
get_root().add_child(root)
var bag := PlayerBag.new()
var catches := FishInventory.new()
var capacity := PlayerCoolerCapacity.new()
var layout := PlayerInventoryLayout.new()
var hotbar := PlayerHotbar.new()
for node: Node in [bag, catches, capacity, layout, hotbar]:
root.add_child(node)
bag.setup(ItemCatalogResource)
layout.setup(bag, catches, ItemCatalogResource, capacity)
bag.set_inventory_layout(layout)
catches.set_inventory_layout(layout)
hotbar.setup(bag, ItemCatalogResource, catches, layout)
assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36])
assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72])
assert(bag.add_item(&"coffee", 3))
assert(layout.get_inventory_count() == 1)
assert(layout.get_inventory_capacity() == 9)
assert(layout.get_hotbar_count() == 0)
assert(hotbar.assign_item(2, &"coffee"))
assert(layout.get_inventory_count() == 0)
assert(layout.get_hotbar_count() == 1)
assert(
layout.get_container(PlayerInventoryLayout.EntryKind.ITEM, &"coffee")
== PlayerInventoryLayout.InventoryContainer.HOTBAR
)
assert(hotbar.clear_slot(2))
assert(layout.get_inventory_count() == 1)
assert(layout.get_hotbar_count() == 0)
var fish_catch := FishCatch.new()
fish_catch.fish = Bluegill
fish_catch.fish_id = Bluegill.id
fish_catch.catch_id = &"bluegill:unified_inventory_test"
fish_catch.weight_lb = Bluegill.get_minimum_weight()
fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
fish_catch.weight_lb
)
fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
fish_catch.weight_lb
)
assert(catches.add_catch(fish_catch))
assert(layout.get_inventory_count() == 2)
assert(layout.move_entry_to_first_free(
PlayerInventoryLayout.EntryKind.CATCH,
fish_catch.catch_id,
PlayerInventoryLayout.InventoryContainer.STORAGE,
))
assert(layout.get_inventory_count() == 1)
assert(layout.get_storage_count() == 1)
assert(layout.get_storage_capacity() == 9)
var inventory_grid := GeneralInventoryGrid.new()
root.add_child(inventory_grid)
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
inventory_grid.setup(
layout,
bag,
catches,
hotbar,
ItemCatalogResource,
PlayerInventoryLayout.InventoryContainer.INVENTORY,
)
var all_inventory_slots: Array = inventory_grid.get("_slots")
assert(all_inventory_slots.size() == 36)
assert(inventory_grid.get_slots().size() == 9)
for slot_index: int in all_inventory_slots.size():
var slot := all_inventory_slots[slot_index] as GeneralInventorySlot
assert(slot.custom_minimum_size == Vector2(78.0, 78.0))
assert(slot.disabled == (slot_index >= 9))
assert(slot.tooltip_text.is_empty())
var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
assert(normal != null and normal.corner_radius_top_left == 39)
var locked_slot := all_inventory_slots[9] as GeneralInventorySlot
var locked_icon := locked_slot.get("_icon") as TextureRect
assert(locked_icon != null)
assert(is_equal_approx(locked_icon.size.x, 24.0))
assert(is_equal_approx(locked_icon.size.y, 24.0))
assert(is_equal_approx(locked_icon.modulate.a, 0.18))
var staged_slot := all_inventory_slots[0] as GeneralInventorySlot
staged_slot.set_staged(true)
assert(
(staged_slot.get_theme_stylebox("normal") as StyleBoxFlat).bg_color
== Color(UtilityPageStyle.OCEAN_SELECTED, 0.92)
)
staged_slot.set_staged(false)
var sale_tray_slot := ShopSaleTraySlot.new()
root.add_child(sale_tray_slot)
assert(
sale_tray_slot.custom_minimum_size
== GeneralInventoryGrid.DEFAULT_SLOT_SIZE
)
var sale_tray_style := sale_tray_slot.get_theme_stylebox(
"normal"
) as StyleBoxFlat
assert(sale_tray_style != null)
assert(sale_tray_style.corner_radius_top_left == 26)
var wallet := PlayerWallet.new()
root.add_child(wallet)
assert(wallet.restore_balance(15000))
assert(layout.get_next_backpack_cost() == 1500)
assert(layout.purchase_backpack(wallet))
assert(layout.get_inventory_capacity() == 18)
assert(inventory_grid.get_slots().size() == 18)
assert(layout.get_next_backpack_cost() == 4500)
assert(layout.purchase_backpack(wallet))
assert(layout.get_inventory_capacity() == 27)
assert(inventory_grid.get_slots().size() == 27)
assert(layout.get_next_backpack_cost() == 9000)
assert(layout.purchase_backpack(wallet))
assert(layout.get_inventory_capacity() == 36)
assert(inventory_grid.get_slots().size() == 36)
assert(layout.get_next_backpack_cost() == -1)
var storage_grid := GeneralInventoryGrid.new()
root.add_child(storage_grid)
storage_grid.setup(
layout,
bag,
catches,
hotbar,
ItemCatalogResource,
PlayerInventoryLayout.InventoryContainer.STORAGE,
)
var all_storage_slots: Array = storage_grid.get("_slots")
assert(all_storage_slots.size() == 72)
assert(storage_grid.get_slots().size() == 9)
assert(
(all_storage_slots[9] as GeneralInventorySlot).accessibility_name
== "locked storage slot 10"
)
var storage_wallet := PlayerWallet.new()
root.add_child(storage_wallet)
assert(storage_wallet.restore_balance(9500))
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
assert(capacity.purchase(storage_wallet))
assert(layout.get_storage_capacity() == expected_capacity)
assert(storage_grid.get_slots().size() == expected_capacity)
assert(capacity.get_next_capacity() == -1)
assert(capacity.get_next_cost() == -1)
var saved := layout.to_save_data()
assert(layout.move_entry_to_first_free(
PlayerInventoryLayout.EntryKind.ITEM,
&"coffee",
PlayerInventoryLayout.InventoryContainer.STORAGE,
))
assert(layout.restore_from_save_data(saved))
assert(layout.is_item_in_inventory(&"coffee"))
assert(
layout.get_container(
PlayerInventoryLayout.EntryKind.CATCH,
fish_catch.catch_id,
) == PlayerInventoryLayout.InventoryContainer.STORAGE
)
var network_sale := NetworkSaleService.new()
var session := NetworkSession.new()
root.add_child(session)
root.add_child(network_sale)
network_sale.set("_session", session)
network_sale.set("_item_catalog", ItemCatalogResource)
network_sale.set("_fish_catalog", FishCatalogResource)
var result: Dictionary = network_sale.call(
"_build_authoritative_result",
1,
"mixed_sale_test",
[],
[{"item_id": "coffee", "quantity": 2}],
MainShopBuyer,
)
assert(bool(result.get("accepted", false)))
assert(int(result.get("payout", -1)) == 20)
assert((result.get("items", []) as Array).size() == 1)
print("Unified inventory validation: PASS")
root.free()
quit()

View file

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

View file

@ -2,7 +2,12 @@ extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18170
const EXPECTED_POPULATION: int = 2
const EXPECTED_POPULATION: int = 8
const EXPECTED_BY_TYPE: Dictionary = {
&"crab_brown": 2,
&"clam_manila": 3,
&"beetle_stag_common": 3,
}
func _initialize() -> void:
@ -28,6 +33,7 @@ func _run_host() -> void:
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
assert(session.set_host_open(true))
await _wait_for_population(service)
_freeze_timed_entries(service)
_validate_population(service, true)
var remote_peer_id: int = 0
@ -83,6 +89,9 @@ func _run_client() -> void:
))
await _wait_for_population(service)
_validate_population(service, false)
# Give the host process time to observe the authenticated peer before this
# deliberately short client validation disconnects.
await create_timer(0.75).timeout
print("World spawn multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
@ -106,8 +115,10 @@ func _validate_population(service: Node, expect_authoritative_state: bool) -> vo
var presentations: Dictionary = service.get("_presentations")
assert(entities.size() == EXPECTED_POPULATION)
assert(presentations.size() == EXPECTED_POPULATION)
var counts: Dictionary = {}
for state: Dictionary in entities.values():
assert(state.get("type_id") == &"crab_brown")
var type_id := state.get("type_id") as StringName
counts[type_id] = int(counts.get(type_id, 0)) + 1
var position: Variant = state.get("position")
assert(typeof(position) == TYPE_VECTOR3)
assert((position as Vector3).is_finite())
@ -120,27 +131,43 @@ func _validate_population(service: Node, expect_authoritative_state: bool) -> vo
if expect_authoritative_state:
var quality: int = int(state.get("quality", -1))
assert(FishQuality.is_valid(quality))
assert(entry.get_movement_speed_for_quality(quality) > 0.0)
assert(entry.get_scare_radius_for_quality(quality) > 0.0)
if entry.is_stationary_spawn():
assert(entry.get_movement_speed_for_quality(quality) <= 0.0)
assert(not entry.can_be_scared())
else:
assert(entry.get_movement_speed_for_quality(quality) > 0.0)
assert(entry.get_scare_radius_for_quality(quality) > 0.0)
assert(counts == EXPECTED_BY_TYPE)
func _freeze_timed_entries(service: Node) -> void:
for state: Dictionary in (service.get("_entities") as Dictionary).values():
var entry := state.get("data") as GatherableData
if entry != null and entry.active_lifetime_seconds > 0.0:
state["expires_at"] = INF
func _validate_respawn_budget(service: Node) -> void:
var entities: Dictionary = service.get("_entities")
var entity_ids: Array = entities.keys()
assert(entity_ids.size() == EXPECTED_POPULATION)
for entity_id: Variant in entity_ids:
var crab_ids: Array[String] = []
for entity_id: String in entities:
var state: Dictionary = entities[entity_id]
if state.get("type_id") == &"crab_brown":
crab_ids.append(entity_id)
assert(crab_ids.size() == 2)
for entity_id: String in crab_ids:
service.call(
"_despawn_entity",
str(entity_id),
entity_id,
&"captured",
false,
true,
)
assert((service.get("_entities") as Dictionary).is_empty())
assert((service.get("_entities") as Dictionary).size() == 6)
var now: float = Time.get_ticks_msec() / 1000.0
var respawns: Array = service.get("_respawns")
assert(respawns.size() == EXPECTED_POPULATION)
assert(respawns.size() == crab_ids.size())
for index: int in respawns.size():
var respawn: Dictionary = respawns[index]
var delay: float = float(respawn.get("due", 0.0)) - now
@ -149,19 +176,20 @@ func _validate_respawn_budget(service: Node) -> void:
respawns[index] = respawn
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 1)
assert((service.get("_entities") as Dictionary).size() == 7)
assert((service.get("_respawns") as Array).size() == 1)
var next_by_type: Dictionary = service.get("_next_respawn_by_type")
var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0))
assert(next_allowed - now >= 179.0)
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 1)
assert((service.get("_entities") as Dictionary).size() == 7)
assert((service.get("_respawns") as Array).size() == 1)
next_by_type[&"crab_brown"] = now - 1.0
service.call("_update_respawns")
assert((service.get("_entities") as Dictionary).size() == 2)
assert((service.get("_entities") as Dictionary).size() == 8)
assert((service.get("_respawns") as Array).is_empty())
_validate_population(service, true)
func _create_initialized_main() -> Node:

View file

@ -52,6 +52,30 @@ func _validate_catalog_statuses() -> void:
assert(is_equal_approx(brown.minimum_surface_y, -0.44))
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
assert(not brown.catch_data.is_fishable())
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
assert(clam != null and clam.is_available())
assert(clam.catch_data.collection_method == FishData.CollectionMethod.DIGGING)
assert(clam.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
assert(clam.required_tool_id == &"standard_shovel")
assert(clam.diggable_area_id == &"starter_beach")
assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
assert(clam.is_stationary_hotspot())
assert(not clam.requires_sneaking)
assert(not clam.can_be_scared())
assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
assert(FishCatalog.get_fish_by_id(&"clam_manila") == clam.catch_data)
assert(not clam.catch_data.is_fishable())
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
assert(beetle != null and beetle.is_available())
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
assert(beetle.required_tool_id == &"crab_net")
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
assert(beetle.is_stationary_spawn())
assert(not beetle.is_stationary_hotspot())
assert(not beetle.requires_sneaking)
assert(not beetle.can_be_scared())
assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data)
assert(not beetle.catch_data.is_fishable())
for type_id: StringName in [
&"crab_ghost",
@ -76,8 +100,10 @@ func _validate_catalog_statuses() -> void:
)
var available: Array[GatherableData] = Gatherables.get_available_entries()
assert(available.size() == 1)
assert(available.front() == brown)
assert(available.size() == 3)
assert(available.has(brown))
assert(available.has(clam))
assert(available.has(beetle))
var rng := RandomNumberGenerator.new()
rng.seed = 24680
var captured_delay: float = brown.get_respawn_delay(&"captured", rng)
@ -111,6 +137,18 @@ func _validate_billboard_presentation() -> void:
assert(not sprite.shaded)
gatherable.free()
var hotspot := WorldGatherableType.new()
hotspot.call("_ensure_water_spurt_visual")
var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
assert(hole != null)
assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
var hole_material := hole.mesh.surface_get_material(0) as StandardMaterial3D
assert(hole_material != null)
assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
hotspot.queue_free()
func _validate_quality_behavior(entry: GatherableData) -> void:
var prior_speed: float = -1.0

View file

@ -31,19 +31,10 @@ func _run_host() -> void:
main.get_node("%WorldWeatherService") as WorldWeatherService
)
assert(session.start_private_host(TEST_PORT))
var game_ui := main.get_node("%GameUI") as CanvasLayer
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
assert(chat_ui.call("_handle_chat_command", "/weather clear"))
assert(world_weather.get_weather() == WorldWeatherService.Weather.SUNNY)
world_time.synchronize_time(INITIAL_HOST_TIME)
assert(is_equal_approx(
world_time.get_persistent_time_hours(), INITIAL_HOST_TIME
assert(world_time.set_authoritative_time(INITIAL_HOST_TIME))
assert(world_weather.set_authoritative_weather(
WorldWeatherService.Weather.RAINY
))
assert(chat_ui.call("_handle_chat_command", "/weather rainy"))
assert(
world_weather.get_persistent_weather()
== WorldWeatherService.Weather.RAINY
)
assert(session.set_host_open(true))
var remote_peer_id: int = 0
@ -61,35 +52,12 @@ func _run_host() -> void:
assert(session.peer_supports_capability(
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
))
var remote_record: PeerRegistry.PeerRecord = session.get_peer_record(
remote_peer_id
)
assert(remote_record != null and remote_record.identity_authenticated)
assert(session.set_peer_operator(
remote_peer_id,
remote_record.identity_fingerprint,
true,
))
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
var command_deadline: int = Time.get_ticks_msec() + 10000
while (
Time.get_ticks_msec() < command_deadline
and _wrapped_time_difference(
world_time.get_time_hours(), UPDATED_HOST_TIME
) > TIME_TOLERANCE_HOURS
):
await process_frame
assert(is_equal_approx(
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
await create_timer(1.0).timeout
assert(world_time.set_authoritative_time(UPDATED_HOST_TIME))
assert(world_weather.set_authoritative_weather(
WorldWeatherService.Weather.FOGGY
))
var fog_deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < fog_deadline and not world_weather.is_foggy():
await process_frame
assert(
world_weather.get_persistent_weather()
== WorldWeatherService.Weather.FOGGY
)
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
while (
Time.get_ticks_msec() < disconnect_deadline
@ -131,12 +99,6 @@ func _run_client() -> void:
assert(session.supports_server_capability(
NetworkProtocol.WORLD_WEATHER_CAPABILITY
))
assert(world_time.restore_persistent_time_hours(15.25))
assert(world_weather.restore_persistent_state(
WorldWeatherService.Weather.CLOUDY,
222.0,
))
var initial_deadline: int = Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < initial_deadline
@ -170,11 +132,7 @@ func _run_client() -> void:
assert(is_equal_approx(clock_panel.position.y, 10.0))
assert(is_equal_approx(weather_icon.position.y, 10.0))
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
var operator_deadline: int = Time.get_ticks_msec() + 10000
while Time.get_ticks_msec() < operator_deadline and not session.is_local_operator():
await process_frame
assert(session.is_local_operator())
assert(chat_ui.call("_handle_chat_command", "/time night"))
assert(not chat_ui.has_method("_handle_chat_command"))
var update_deadline: int = Time.get_ticks_msec() + 10000
while (
@ -188,10 +146,7 @@ func _run_client() -> void:
world_time.get_time_hours(), UPDATED_HOST_TIME
) <= TIME_TOLERANCE_HOURS)
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
assert(clock_label.text == world_time.get_clock_text())
await create_timer(0.6).timeout
assert(chat_ui.call("_handle_chat_command", "/weather foggy"))
var fog_deadline: int = Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < fog_deadline
@ -200,14 +155,6 @@ func _run_client() -> void:
await process_frame
assert(world_weather.is_foggy())
assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY)
assert(
world_weather.get_persistent_weather()
== WorldWeatherService.Weather.CLOUDY
)
assert(is_equal_approx(
world_weather.get_persistent_seconds_remaining(),
222.0,
))
chat_ui.call("set_dock_right", true)
await process_frame
assert(clock_panel.position.x > 1000.0)

View file

@ -29,6 +29,7 @@ func _initialize() -> void:
func _run() -> void:
_validate_clock_boundaries_and_duration()
_validate_system_clock_authority()
_validate_persistent_host_clock()
_validate_fishing_availability()
_validate_fishing_spot_context()
@ -40,7 +41,34 @@ func _run() -> void:
func _validate_clock_boundaries_and_duration() -> void:
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 3600.0)
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
assert(is_equal_approx(
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
1.0 / 3600.0,
))
assert(is_equal_approx(
WorldTimeServiceType.time_hours_from_datetime({
"hour": 17, "minute": 30, "second": 18,
}),
17.505,
))
assert(
WorldTimeServiceType.date_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
}) == "2026-08-18"
)
assert(
WorldTimeServiceType.calendar_cycle_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
"hour": 7, "minute": 59, "second": 0,
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-17"
)
assert(
WorldTimeServiceType.calendar_cycle_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
"hour": 8, "minute": 0, "second": 0,
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-18"
)
assert(
WorldTimeServiceType.phase_for_hour(7.49)
== WorldTimeServiceType.Phase.NIGHT
@ -71,12 +99,12 @@ func _validate_clock_boundaries_and_duration() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
clock.begin_session(8.0)
clock.advance_time(1800.0)
clock.begin_test_session(8.0)
clock.advance_time(12.0 * 60.0 * 60.0)
assert(is_equal_approx(clock.get_time_hours(), 20.0))
assert(clock.is_night_period())
assert(clock.is_transition())
clock.advance_time(1800.0)
clock.advance_time(12.0 * 60.0 * 60.0)
assert(is_equal_approx(clock.get_time_hours(), 8.0))
assert(not clock.is_night_period())
assert(clock.is_transition())
@ -88,10 +116,10 @@ func _validate_clock_boundaries_and_duration() -> void:
func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
emitted_times.append(time_hours)
)
clock.begin_session(12.0 + 31.0 / 60.0)
clock.begin_test_session(12.0 + 31.0 / 60.0)
emitted_times.clear()
for _frame: int in 1201:
clock.advance_time(1.0 / 240.0)
for _frame: int in 7201:
clock.advance_time(1.0 / 60.0)
assert(clock.get_clock_text() == "12:33 pm")
assert(emitted_times.size() >= 2)
clock.queue_free()
@ -101,20 +129,41 @@ func _validate_persistent_host_clock() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
assert(clock.restore_persistent_time_hours(18.75))
clock.set_persistence_tracking_enabled(true)
clock.begin_session(clock.get_persistent_time_hours())
clock.begin_test_session(18.75)
assert(is_equal_approx(clock.get_time_hours(), 18.75))
clock.synchronize_time(19.25)
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
clock.set_persistence_tracking_enabled(false)
clock.synchronize_time(6.5)
assert(is_equal_approx(clock.get_time_hours(), 6.5))
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
assert(is_equal_approx(clock.get_persistent_time_hours(), 6.5))
assert(not clock.restore_persistent_time_hours(-1.0))
assert(not clock.restore_persistent_time_hours(24.0))
clock.queue_free()
func _validate_system_clock_authority() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
clock.begin_authoritative_session()
var system_datetime: Dictionary = Time.get_datetime_dict_from_system(false)
var system_hour: float = WorldTimeServiceType.time_hours_from_datetime(
system_datetime
)
assert(clock.is_using_system_clock())
assert(_wrapped_time_difference(
clock.get_time_hours(), system_hour
) < 2.0 / 3600.0)
assert(
clock.get_calendar_date_id()
== WorldTimeServiceType.date_id_from_datetime(system_datetime)
)
assert(clock.set_authoritative_time(12.0))
assert(not clock.is_using_system_clock())
clock.clear_editor_time_override()
assert(clock.is_using_system_clock())
clock.queue_free()
func _validate_fishing_availability() -> void:
var day_context := FishingContextType.new()
day_context.location_tags = [&"starter_pond"]
@ -165,7 +214,7 @@ func _validate_fishing_availability() -> void:
func _validate_fishing_spot_context() -> void:
var clock := WorldTimeServiceType.new()
clock.begin_session(20.25)
clock.begin_test_session(20.25)
var fishing_spot := FishingSpotType.new()
fishing_spot.set("_world_time", clock)
var region := FishableWaterRegionType.new()
@ -246,6 +295,10 @@ func _validate_environment_presentation() -> void:
float(runtime_sky_material.get_shader_parameter("moon_visibility"))
< 0.01
)
assert(
float(runtime_sky_material.get_shader_parameter("star_visibility"))
< 0.01
)
var day_ambient_energy: float = runtime_environment.ambient_light_energy
assert(not runtime_environment.fog_enabled)
assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
@ -257,6 +310,9 @@ func _validate_environment_presentation() -> void:
)
assert("uniform float fog_horizon_occlusion" in sky_shader.code)
assert("fog_horizon_color.rgb" in sky_shader.code)
assert("uniform float star_visibility" in sky_shader.code)
assert("float procedural_star_field" in sky_shader.code)
assert("upper_sky_fade" in sky_shader.code)
assert("fog_disabled" in water_shader.code)
assert("surface_view_position = view_vertex.xyz" in water_shader.code)
assert("surface_view_distance = length(surface_view_position)" in water_shader.code)
@ -301,6 +357,17 @@ func _validate_environment_presentation() -> void:
runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
)
assert(night_moon_visibility > 0.99)
assert(
float(runtime_sky_material.get_shader_parameter("star_visibility"))
> 0.99
)
assert((
runtime_sky_material.get_shader_parameter("star_color") as Color
).is_equal_approx(WorldTimeVisualController.STAR_COLOR))
assert(is_equal_approx(
float(runtime_sky_material.get_shader_parameter("star_strength")),
WorldTimeVisualController.STAR_STRENGTH,
))
assert(night_moon_direction.y > 0.85)
assert(night_moon_direction.is_equal_approx(-night_sun_direction))
assert(runtime_environment.ambient_light_energy < day_ambient_energy)
@ -339,6 +406,9 @@ func _validate_environment_presentation() -> void:
assert(is_equal_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
), 1.0))
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("star_visibility")
)))
assert((
runtime_sky_material.get_shader_parameter("fog_horizon_color") as Color
).is_equal_approx(WorldTimeVisualController.NIGHT_FOG))
@ -425,6 +495,10 @@ func _validate_environment_presentation() -> void:
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
)))
var dusk_star_visibility := float(
runtime_sky_material.get_shader_parameter("star_visibility")
)
assert(dusk_star_visibility > 0.45 and dusk_star_visibility < 0.55)
var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
"sky_horizon_color"
) as Color
@ -442,3 +516,8 @@ func _validate_weather_clock_icon() -> void:
weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
assert(weather_icon.tooltip_text == "cloudy")
weather_icon.queue_free()
static func _wrapped_time_difference(left: float, right: float) -> float:
var difference: float = absf(left - right)
return minf(difference, WorldTimeServiceType.HOURS_PER_DAY - difference)

View file

@ -26,6 +26,7 @@ func _run() -> void:
_validate_weather_scheduler()
_validate_authoritative_weather_override()
_validate_weather_persistence()
_validate_legacy_forecast_save_migration()
_validate_snapshot_bounds()
_validate_fishing_weather_seams()
_validate_fishing_spot_context()
@ -65,14 +66,9 @@ func _validate_weather_scheduler() -> void:
func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
var seconds: float = weather.get_seconds_remaining()
match weather.get_weather():
WorldWeatherServiceType.Weather.SUNNY:
return seconds >= 480.0 and seconds <= 900.0
WorldWeatherServiceType.Weather.CLOUDY:
return seconds >= 300.0 and seconds <= 720.0
WorldWeatherServiceType.Weather.RAINY, WorldWeatherServiceType.Weather.FOGGY:
return seconds >= 300.0 and seconds <= 600.0
return false
return is_equal_approx(
seconds, WorldWeatherServiceType.WEATHER_PERIOD_SECONDS
)
func _validate_authoritative_weather_override() -> void:
@ -94,7 +90,7 @@ func _validate_authoritative_weather_override() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
clock.begin_session(WorldTimeServiceType.DAY_START_HOUR)
clock.begin_test_session(WorldTimeServiceType.DAY_START_HOUR)
var schedule: Array[Dictionary] = []
for index: int in WorldWeatherServiceType.DAILY_PLAN_SEGMENT_COUNT:
schedule.append({
@ -159,8 +155,11 @@ func _validate_weather_persistence() -> void:
))
weather.set_persistence_tracking_enabled(true)
weather.begin_authoritative_session(20260803)
assert(weather.is_raining())
assert(is_equal_approx(weather.get_seconds_remaining(), 187.5))
assert(weather.get_weather() == WorldWeatherServiceType.DEFAULT_WEATHER)
assert(is_equal_approx(
weather.get_seconds_remaining(),
WorldWeatherServiceType.WEATHER_PERIOD_SECONDS,
))
assert(not weather.restore_persistent_state(
WorldWeatherServiceType.Weather.RAINY,
WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0,
@ -184,11 +183,49 @@ func _validate_snapshot_bounds() -> void:
assert(not NetworkWorldWeatherServiceType.validate_snapshot({
"session_id": "session",
"weather": int(WorldWeatherServiceType.Weather.RAINY),
"seconds_remaining": 1801.0,
"seconds_remaining": 3601.0,
"sequence": 2,
}))
func _validate_legacy_forecast_save_migration() -> void:
var legacy_schedule: Array[Dictionary] = []
for index: int in JobCatalog.LEGACY_WEATHER_SEGMENT_COUNT:
legacy_schedule.append({
"start_hour": fposmod(
WorldTimeService.DAY_START_HOUR
+ float(index) * JobCatalog.LEGACY_WEATHER_SEGMENT_HOURS,
WorldTimeService.HOURS_PER_DAY,
),
"weather": int(WorldWeatherService.Weather.SUNNY),
})
var legacy_job: Dictionary = {
"id": "legacy-catch",
"title": "legacy catch",
"description": "catch one fish",
"kind": int(JobCatalog.Kind.CATCH_TOTAL),
"target": 1,
"fish_coin": 1,
"experience": 1,
}
var save_data: Dictionary = PlayerJobService.default_save_data()
save_data["host_board"] = {
"plan_id": "legacy-plan",
"cycle": 4,
"schedule_anchor_index": 0,
"jobs": [legacy_job],
"weather_schedule": legacy_schedule,
}
save_data["active_plan_id"] = "legacy-plan"
assert(PlayerJobService.validate_save_data(save_data))
var jobs := PlayerJobService.new()
assert(jobs.restore_from_save_data(save_data))
var migrated: Dictionary = jobs.to_save_data()
assert((migrated.get("host_board", {}) as Dictionary).is_empty())
assert(str(migrated.get("active_plan_id", "")).is_empty())
jobs.free()
func _validate_fishing_weather_seams() -> void:
var clear_context := FishingContextType.new()
var rain_context := FishingContextType.new()
@ -255,7 +292,7 @@ func _validate_weather_presentation() -> void:
world_root.add_child(sun)
var clock := WorldTimeServiceType.new()
world_root.add_child(clock)
clock.begin_session(14.0)
clock.begin_test_session(14.0)
var weather := WorldWeatherServiceType.new()
world_root.add_child(weather)
weather.begin_remote_session()
@ -279,6 +316,34 @@ func _validate_weather_presentation() -> void:
var runtime_sky_material := (
runtime_environment.sky.sky_material as ShaderMaterial
)
clock.set_authoritative_time(0.0)
visuals.apply_time_immediately(0.0)
assert(float(
runtime_sky_material.get_shader_parameter("star_visibility")
) > 0.99)
visuals.apply_weather_immediately(
WorldWeatherServiceType.Weather.CLOUDY
)
assert(is_equal_approx(float(
runtime_sky_material.get_shader_parameter("star_visibility")
), 0.18))
visuals.apply_weather_immediately(
WorldWeatherServiceType.Weather.RAINY
)
assert(is_equal_approx(float(
runtime_sky_material.get_shader_parameter("star_visibility")
), 0.03))
visuals.apply_weather_immediately(
WorldWeatherServiceType.Weather.FOGGY
)
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("star_visibility")
)))
visuals.apply_weather_immediately(
WorldWeatherServiceType.Weather.SUNNY
)
clock.set_authoritative_time(14.0)
visuals.apply_time_immediately(14.0)
var clear_background_energy: float = (
runtime_environment.background_energy_multiplier
)