2026-08-03 00:43:43 -04:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
|
|
|
|
const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
call_deferred("_run")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _run() -> void:
|
|
|
|
|
root.size = Vector2i(1280, 720)
|
|
|
|
|
var main: Node = MainScene.instantiate()
|
|
|
|
|
root.add_child(main)
|
|
|
|
|
for _frame: int in 4:
|
|
|
|
|
await process_frame
|
|
|
|
|
if not bool(main.get("_application_initialized")):
|
|
|
|
|
main.call("_activate_selected_data_path", "", true)
|
|
|
|
|
for _frame: int in 8:
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(bool(main.get("_application_initialized")))
|
|
|
|
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
|
|
|
|
assert(session != null)
|
|
|
|
|
var occupied_port := PacketPeerUDP.new()
|
|
|
|
|
assert(occupied_port.bind(18137, "127.0.0.1") == OK)
|
|
|
|
|
assert(session.start_private_host(18137, 3))
|
|
|
|
|
assert(session.get_host_port() == 18138)
|
|
|
|
|
occupied_port.close()
|
|
|
|
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
|
|
|
|
assert(save_manager.initialize_new_game())
|
|
|
|
|
main.call("_enter_gameplay")
|
|
|
|
|
await physics_frame
|
|
|
|
|
await physics_frame
|
|
|
|
|
|
|
|
|
|
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
|
|
|
|
|
var weather := main.get_node("%WorldWeatherService") as WorldWeatherService
|
|
|
|
|
var network_fishing := main.get(
|
|
|
|
|
"_network_fishing"
|
|
|
|
|
) as NetworkFishingService
|
|
|
|
|
var network_sale := main.get("_network_sale") as NetworkSaleService
|
|
|
|
|
var player := main.get("_player") as Player
|
|
|
|
|
assert(jobs != null and weather != null and player != null)
|
|
|
|
|
assert(network_fishing != null and network_sale != null)
|
|
|
|
|
|
|
|
|
|
var board: Dictionary = jobs.get_host_board_network_data()
|
|
|
|
|
assert(PlayerJobService.validate_board(board))
|
|
|
|
|
assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
|
|
|
|
|
assert(weather.get_daily_plan_id() == jobs.get_plan_id())
|
|
|
|
|
_validate_weather_guarantees(board)
|
|
|
|
|
_validate_late_board_weather_fallback()
|
|
|
|
|
_validate_remote_tamper_rejection(jobs, board)
|
2026-08-20 17:53:22 -04:00
|
|
|
_validate_creature_jobs(jobs)
|
2026-08-03 00:43:43 -04:00
|
|
|
|
|
|
|
|
var sell_job: Dictionary = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
|
|
|
|
|
)
|
|
|
|
|
assert(not sell_job.is_empty())
|
|
|
|
|
var sell_target: int = int(sell_job.get("target", 0))
|
|
|
|
|
var sold_ids: Array[StringName] = []
|
|
|
|
|
for index: int in sell_target * 2:
|
|
|
|
|
sold_ids.append(StringName("job-sale-%d" % index))
|
|
|
|
|
network_sale.local_sale_finished.emit(
|
|
|
|
|
"job-test", true, "sold", sold_ids, 1
|
|
|
|
|
)
|
|
|
|
|
await process_frame
|
|
|
|
|
var pending: Array[Dictionary] = jobs.get_pending_rewards()
|
|
|
|
|
assert(pending.size() == 1)
|
|
|
|
|
var refreshed_sell: Dictionary = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
|
|
|
|
|
)
|
|
|
|
|
assert(int(refreshed_sell.get("progress", -1)) == sell_target)
|
|
|
|
|
assert(int(refreshed_sell.get("completed_count", 0)) == 1)
|
|
|
|
|
|
|
|
|
|
var game_ui := main.get_node("%GameUI") as GameUI
|
|
|
|
|
var player_menu := game_ui.get("_player_menu") as PlayerMenu
|
|
|
|
|
assert(player_menu != null)
|
|
|
|
|
player_menu.open_menu()
|
|
|
|
|
for _frame: int in 24:
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(player_menu.visible)
|
|
|
|
|
player_menu.call("_show_section_immediate", PlayerMenu.Section.NET)
|
|
|
|
|
var net_page := player_menu.get_node("%TheNetPage") as TheNetPage
|
|
|
|
|
assert(net_page.visible)
|
|
|
|
|
assert((player_menu.get_node("%TheNetTab") as Button).button_pressed)
|
|
|
|
|
assert(
|
|
|
|
|
(player_menu.get_node("%NavigationCluster") as Control).get_child_count()
|
|
|
|
|
== 7
|
|
|
|
|
)
|
2026-08-21 12:30:00 -04:00
|
|
|
net_page.call("_select_view", TheNetPage.View.LIFETIME)
|
|
|
|
|
await process_frame
|
|
|
|
|
_validate_fishnet_bounds(net_page)
|
feat: expand progression and multiplayer systems
Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
2026-08-23 20:48:38 -04:00
|
|
|
await _validate_compact_value_tooltips(net_page)
|
2026-08-13 21:58:43 -04:00
|
|
|
await _validate_unavailable_fishnet_layout()
|
2026-08-03 00:43:43 -04:00
|
|
|
|
|
|
|
|
var wallet_before: int = player.wallet.get_balance()
|
|
|
|
|
var experience_before: int = player.experience.get_total_experience()
|
|
|
|
|
var first_claim_id: String = str(pending[0].get("claim_id", ""))
|
|
|
|
|
net_page.call("_claim", first_claim_id)
|
|
|
|
|
await process_frame
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(bool(game_ui.get("_experience_animation_active")))
|
|
|
|
|
var experience_presentation := (
|
|
|
|
|
game_ui.get_node("%ExperiencePresentation") as Control
|
|
|
|
|
)
|
|
|
|
|
assert(experience_presentation.visible)
|
|
|
|
|
assert((game_ui.get_node("%ExperienceProgressPanel") as Control).visible)
|
|
|
|
|
assert((game_ui.get_node("%ExperienceBubble") as Control).visible)
|
|
|
|
|
assert(
|
|
|
|
|
experience_presentation.get_index()
|
|
|
|
|
== experience_presentation.get_parent().get_child_count() - 1
|
|
|
|
|
)
|
|
|
|
|
assert(not (game_ui.get_node("%GameplayTransientHUD") as Control).visible)
|
|
|
|
|
assert(player_menu.visible)
|
|
|
|
|
assert(not jobs.claim(first_claim_id))
|
|
|
|
|
assert(
|
|
|
|
|
player.wallet.get_balance()
|
|
|
|
|
== wallet_before + int(pending[0].get("fish_coin", 0))
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
player.experience.get_total_experience()
|
|
|
|
|
== experience_before + int(pending[0].get("experience", 0))
|
|
|
|
|
)
|
|
|
|
|
refreshed_sell = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
|
|
|
|
|
)
|
|
|
|
|
assert(int(refreshed_sell.get("progress", -1)) == sell_target)
|
|
|
|
|
assert(int(refreshed_sell.get("completed_count", 0)) == 1)
|
|
|
|
|
assert(not bool(refreshed_sell.get("claimable", true)))
|
|
|
|
|
network_sale.local_sale_finished.emit(
|
|
|
|
|
"job-test-repeat", true, "sold", sold_ids, 1
|
|
|
|
|
)
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(jobs.get_pending_rewards().is_empty())
|
|
|
|
|
refreshed_sell = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
|
|
|
|
|
)
|
|
|
|
|
assert(int(refreshed_sell.get("progress", -1)) == sell_target)
|
|
|
|
|
assert(int(refreshed_sell.get("completed_count", 0)) == 1)
|
|
|
|
|
|
|
|
|
|
var fresh_job: Dictionary = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER
|
|
|
|
|
)
|
|
|
|
|
assert(not fresh_job.is_empty())
|
|
|
|
|
var bluegill: FishData = Catalog.get_fish_by_id(&"bluegill")
|
|
|
|
|
assert(bluegill != null)
|
|
|
|
|
for index: int in int(fresh_job.get("target", 0)):
|
|
|
|
|
var fish_catch := FishCatch.new()
|
|
|
|
|
fish_catch.fish = bluegill
|
|
|
|
|
fish_catch.fish_id = bluegill.id
|
|
|
|
|
fish_catch.catch_id = StringName("job-catch-%d" % index)
|
|
|
|
|
fish_catch.catch_sequence = index + 1
|
|
|
|
|
fish_catch.weight_lb = bluegill.get_minimum_weight()
|
|
|
|
|
fish_catch.display_scale = bluegill.get_display_scale_for_weight(
|
|
|
|
|
fish_catch.weight_lb
|
|
|
|
|
)
|
|
|
|
|
fish_catch.quality = FishQuality.Tier.BORING
|
|
|
|
|
fish_catch.sale_value = bluegill.get_sale_value_for_weight(
|
|
|
|
|
fish_catch.weight_lb
|
|
|
|
|
)
|
|
|
|
|
assert(fish_catch.is_valid())
|
|
|
|
|
network_fishing.local_catch_received.emit(fish_catch)
|
|
|
|
|
await process_frame
|
|
|
|
|
var refreshed_fresh: Dictionary = _find_job(
|
|
|
|
|
jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER
|
|
|
|
|
)
|
|
|
|
|
assert(int(refreshed_fresh.get("completed_count", 0)) == 1)
|
|
|
|
|
|
|
|
|
|
var pause_menu := game_ui.get_pause_menu()
|
|
|
|
|
var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
|
|
|
|
|
assert(join_page != null)
|
2026-08-13 21:58:43 -04:00
|
|
|
pause_menu.show()
|
|
|
|
|
join_page.set_status("Connection timed out.")
|
|
|
|
|
join_page.show()
|
|
|
|
|
join_page.call(
|
|
|
|
|
"_on_discovery_status_changed", "2 public rooms found", false
|
|
|
|
|
)
|
|
|
|
|
var join_status := join_page.get_node("%Status") as Label
|
|
|
|
|
assert(join_status.text == "Could not reach the server.")
|
|
|
|
|
join_page.hide()
|
|
|
|
|
join_page.call("_set_mode", JoinGamePage.Mode.DIRECT)
|
|
|
|
|
join_page.call("_set_mode", JoinGamePage.Mode.DISCOVER)
|
|
|
|
|
join_page.show()
|
|
|
|
|
join_page.call(
|
|
|
|
|
"_on_discovery_status_changed", "2 public rooms found", false
|
|
|
|
|
)
|
|
|
|
|
assert(join_status.text == "2 public rooms found")
|
|
|
|
|
join_page.close_page()
|
|
|
|
|
pause_menu.hide()
|
2026-08-03 00:43:43 -04:00
|
|
|
join_page.call("_refresh")
|
|
|
|
|
var session_summary := (
|
|
|
|
|
join_page.get_node("%SessionSummary") as Label
|
|
|
|
|
)
|
2026-08-11 16:35:33 -04:00
|
|
|
assert(not join_page.has_node(
|
|
|
|
|
"Paper/Margin/Layout/Actions/OpenCloseButton"
|
|
|
|
|
))
|
2026-08-03 00:43:43 -04:00
|
|
|
assert("UDP 18138" in session_summary.text)
|
|
|
|
|
assert(session.set_host_open(true))
|
|
|
|
|
await process_frame
|
|
|
|
|
assert("UDP 18138" in session_summary.text)
|
|
|
|
|
assert(session.set_host_open(false))
|
|
|
|
|
|
|
|
|
|
assert(save_manager.save_now())
|
2026-08-20 17:53:22 -04:00
|
|
|
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
|
|
|
|
|
str(save_manager.get("_save_path"))
|
2026-08-03 00:43:43 -04:00
|
|
|
)
|
2026-08-20 17:53:22 -04:00
|
|
|
assert(bool(decoded.get("ok", false)))
|
|
|
|
|
var save_data: Dictionary = decoded["data"]
|
|
|
|
|
assert(int(save_data.get("save_version", -1)) == 10)
|
2026-08-03 00:43:43 -04:00
|
|
|
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
|
|
|
|
|
2026-08-13 22:20:43 -04:00
|
|
|
_validate_pause_session_switch(main, session)
|
2026-08-03 00:43:43 -04:00
|
|
|
main.queue_free()
|
2026-08-11 17:57:09 -04:00
|
|
|
for _frame: int in 4:
|
|
|
|
|
await process_frame
|
2026-08-13 21:58:43 -04:00
|
|
|
await create_timer(0.1).timeout
|
2026-08-03 00:43:43 -04:00
|
|
|
print("Job system validation: PASS")
|
|
|
|
|
quit()
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 22:20:43 -04:00
|
|
|
func _validate_pause_session_switch(
|
|
|
|
|
main: Node,
|
|
|
|
|
session: NetworkSession,
|
|
|
|
|
) -> void:
|
|
|
|
|
var discovery := main.get_node("%DiscoveryClient") as DiscoveryClient
|
|
|
|
|
discovery.set("_pending_join_endpoint", "127.0.0.1:18141")
|
|
|
|
|
discovery.set("_pending_join_room_id", "session-switch-room")
|
|
|
|
|
discovery.set("_pending_join_token", "session-switch-token")
|
|
|
|
|
discovery.call(
|
|
|
|
|
"_set_public_join_state",
|
|
|
|
|
DiscoveryClient.PublicJoinState.CONNECTING,
|
|
|
|
|
)
|
|
|
|
|
main.call("_on_pause_join_game_requested", "127.0.0.1:18141")
|
|
|
|
|
assert(session.state == NetworkSession.State.CONNECTING)
|
|
|
|
|
assert(discovery.get("_pending_join_token") == "session-switch-token")
|
|
|
|
|
var join_probe_timer := discovery.get("_join_probe_timer") as Timer
|
|
|
|
|
assert(join_probe_timer != null and not join_probe_timer.is_stopped())
|
|
|
|
|
session.cancel_connection()
|
|
|
|
|
assert(session.state == NetworkSession.State.INACTIVE)
|
|
|
|
|
assert(str(discovery.get("_pending_join_token")).is_empty())
|
|
|
|
|
|
|
|
|
|
|
2026-08-13 21:58:43 -04:00
|
|
|
func _validate_unavailable_fishnet_layout() -> void:
|
|
|
|
|
var unavailable_page := TheNetPage.new()
|
|
|
|
|
unavailable_page.size = Vector2(1280.0, 720.0)
|
|
|
|
|
root.add_child(unavailable_page)
|
|
|
|
|
for _frame: int in 3:
|
|
|
|
|
await process_frame
|
|
|
|
|
unavailable_page.call("_refresh_forecast", true)
|
|
|
|
|
await process_frame
|
|
|
|
|
var forecast_list := unavailable_page.get("_forecast_list") as HBoxContainer
|
|
|
|
|
assert(forecast_list != null)
|
|
|
|
|
assert(forecast_list.size.x >= 272.0)
|
|
|
|
|
assert(forecast_list.size.y <= 66.0)
|
|
|
|
|
assert(forecast_list.get_child_count() == 1)
|
|
|
|
|
var placeholder := forecast_list.get_child(0) as Label
|
|
|
|
|
assert(placeholder != null)
|
|
|
|
|
assert(placeholder.size.x >= 252.0)
|
|
|
|
|
assert(placeholder.size.y <= 66.0)
|
|
|
|
|
assert(placeholder.autowrap_mode == TextServer.AUTOWRAP_OFF)
|
|
|
|
|
var tabs := unavailable_page.get("_tabs") as HBoxContainer
|
|
|
|
|
assert(tabs != null)
|
|
|
|
|
assert(tabs.position.y + tabs.size.y < UtilityPageStyle.LAPTOP_RECT.end.y)
|
|
|
|
|
unavailable_page.queue_free()
|
|
|
|
|
for _frame: int in 2:
|
|
|
|
|
await process_frame
|
|
|
|
|
|
|
|
|
|
|
2026-08-21 12:30:00 -04:00
|
|
|
func _validate_fishnet_bounds(page: TheNetPage) -> void:
|
|
|
|
|
var laptop := page.get_node("UtilityMainBox") as PanelContainer
|
|
|
|
|
var forecast_column := page.find_child(
|
|
|
|
|
"ForecastColumn", true, false
|
|
|
|
|
) as VBoxContainer
|
|
|
|
|
var refresh_label := page.find_child(
|
|
|
|
|
"DailyRefreshLabel", true, false
|
|
|
|
|
) as Label
|
|
|
|
|
var jobs_scroll := page.find_child("JobsScroll", true, false) as ScrollContainer
|
|
|
|
|
assert(laptop != null)
|
|
|
|
|
assert(forecast_column != null and refresh_label != null)
|
|
|
|
|
assert(jobs_scroll != null)
|
|
|
|
|
var laptop_rect: Rect2 = laptop.get_global_rect()
|
|
|
|
|
assert(forecast_column.get_global_rect().end.x <= laptop_rect.end.x)
|
|
|
|
|
assert(refresh_label.get_global_rect().end.x <= laptop_rect.end.x)
|
|
|
|
|
assert(refresh_label.get_global_rect().end.y <= laptop_rect.end.y)
|
|
|
|
|
assert(jobs_scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED)
|
|
|
|
|
var list := page.get("_list") as VBoxContainer
|
|
|
|
|
assert(list != null)
|
|
|
|
|
for row: Control in list.get_children():
|
|
|
|
|
assert(row.size.x <= jobs_scroll.size.x + 0.5)
|
|
|
|
|
|
|
|
|
|
|
feat: expand progression and multiplayer systems
Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
2026-08-23 20:48:38 -04:00
|
|
|
func _validate_compact_value_tooltips(page: TheNetPage) -> void:
|
|
|
|
|
assert(str(page.call("_compact_integer", 2400)) == "2.4k")
|
|
|
|
|
assert(str(page.call("_compact_integer", 2500)) == "2.5k")
|
|
|
|
|
var list := page.get("_list") as VBoxContainer
|
|
|
|
|
assert(list != null)
|
|
|
|
|
var jobs: Array[Dictionary] = [{
|
|
|
|
|
"title": "compact tooltip check",
|
|
|
|
|
"description": "compact values keep their exact values on hover",
|
|
|
|
|
"target": 2500,
|
|
|
|
|
"progress": 2400,
|
|
|
|
|
"fish_coin": 1200,
|
|
|
|
|
"experience": 3400,
|
|
|
|
|
}]
|
|
|
|
|
page.call("_build_job_rows", jobs, "")
|
|
|
|
|
await process_frame
|
|
|
|
|
var row := list.get_child(list.get_child_count() - 1) as PanelContainer
|
|
|
|
|
assert(row != null)
|
|
|
|
|
var progress_bar: ProgressBar
|
|
|
|
|
var progress_count: Label
|
|
|
|
|
var reward: VBoxContainer
|
|
|
|
|
for child: Node in row.find_children("*", "ProgressBar", true, false):
|
|
|
|
|
progress_bar = child as ProgressBar
|
|
|
|
|
break
|
|
|
|
|
for child: Node in row.find_children("*", "Label", true, false):
|
|
|
|
|
var label := child as Label
|
|
|
|
|
if label != null and label.text == "2.4k / 2.5k":
|
|
|
|
|
progress_count = label
|
|
|
|
|
break
|
|
|
|
|
for child: Node in row.find_children("*", "VBoxContainer", true, false):
|
|
|
|
|
var candidate := child as VBoxContainer
|
|
|
|
|
if candidate != null and candidate.tooltip_text == (
|
|
|
|
|
"1200 fish coins · 3400 xp"
|
|
|
|
|
):
|
|
|
|
|
reward = candidate
|
|
|
|
|
break
|
|
|
|
|
assert(progress_bar != null)
|
|
|
|
|
assert(progress_bar.tooltip_text == "2400 / 2500")
|
|
|
|
|
assert(progress_bar.mouse_filter == Control.MOUSE_FILTER_STOP)
|
|
|
|
|
assert(progress_count != null)
|
|
|
|
|
assert(progress_count.tooltip_text == "2400 / 2500")
|
|
|
|
|
assert(progress_count.mouse_filter == Control.MOUSE_FILTER_STOP)
|
|
|
|
|
assert(reward != null)
|
|
|
|
|
assert(reward.mouse_filter == Control.MOUSE_FILTER_STOP)
|
|
|
|
|
row.queue_free()
|
|
|
|
|
await process_frame
|
|
|
|
|
|
|
|
|
|
|
2026-08-20 17:53:22 -04:00
|
|
|
func _validate_creature_jobs(jobs: PlayerJobService) -> void:
|
|
|
|
|
var fish_count: int = 0
|
|
|
|
|
var insect_count: int = 0
|
|
|
|
|
var shellfish_count: int = 0
|
|
|
|
|
var candidates: Array[FishData] = []
|
|
|
|
|
for fish: FishData in Catalog.candidates:
|
|
|
|
|
if fish == null or not fish.is_selectable():
|
|
|
|
|
continue
|
|
|
|
|
candidates.append(fish)
|
|
|
|
|
match fish.get_creature_group():
|
|
|
|
|
FishData.CreatureGroup.FISH:
|
|
|
|
|
fish_count += 1
|
|
|
|
|
FishData.CreatureGroup.INSECT:
|
|
|
|
|
insect_count += 1
|
|
|
|
|
FishData.CreatureGroup.SHELLFISH:
|
|
|
|
|
shellfish_count += 1
|
|
|
|
|
assert(fish_count > 0 and insect_count > 0 and shellfish_count > 0)
|
|
|
|
|
|
|
|
|
|
var lifetime: Array[Dictionary] = jobs.get_lifetime_jobs()
|
|
|
|
|
var fish_discovery: Dictionary = _find_job(
|
|
|
|
|
lifetime, JobCatalog.Kind.DISCOVER_SPECIES
|
|
|
|
|
)
|
|
|
|
|
assert(int(fish_discovery.get("target", -1)) == fish_count)
|
|
|
|
|
for creature_group: int in [
|
|
|
|
|
FishData.CreatureGroup.INSECT,
|
|
|
|
|
FishData.CreatureGroup.SHELLFISH,
|
|
|
|
|
]:
|
|
|
|
|
assert(not _find_creature_job(
|
|
|
|
|
lifetime,
|
|
|
|
|
JobCatalog.Kind.CATCH_CREATURE_GROUP,
|
|
|
|
|
creature_group,
|
|
|
|
|
).is_empty())
|
|
|
|
|
assert(not _find_creature_job(
|
|
|
|
|
lifetime,
|
|
|
|
|
JobCatalog.Kind.DISCOVER_CREATURE_GROUP,
|
|
|
|
|
creature_group,
|
|
|
|
|
).is_empty())
|
|
|
|
|
assert(not _find_creature_job(
|
|
|
|
|
lifetime,
|
|
|
|
|
JobCatalog.Kind.MASTER_CREATURE_GROUP,
|
|
|
|
|
creature_group,
|
|
|
|
|
).is_empty())
|
|
|
|
|
|
|
|
|
|
var daily_groups: Dictionary[int, bool] = {}
|
|
|
|
|
for seed_index: int in 64:
|
|
|
|
|
for job: Dictionary in JobCatalog.generate_daily_jobs(
|
|
|
|
|
"creature-job-validation-%d" % seed_index,
|
|
|
|
|
candidates,
|
|
|
|
|
true,
|
|
|
|
|
):
|
|
|
|
|
if int(job.get("kind", -1)) == JobCatalog.Kind.CATCH_CREATURE_GROUP:
|
|
|
|
|
daily_groups[int(job.get("creature_group", -1))] = true
|
|
|
|
|
assert(daily_groups.has(FishData.CreatureGroup.INSECT))
|
|
|
|
|
assert(daily_groups.has(FishData.CreatureGroup.SHELLFISH))
|
|
|
|
|
|
|
|
|
|
var beetle: FishData = Catalog.get_fish_by_id(&"beetle_stag_common")
|
|
|
|
|
assert(beetle != null)
|
|
|
|
|
var beetle_catch := _make_test_catch(beetle, &"job-beetle")
|
|
|
|
|
jobs.call("_on_authoritative_catch", beetle_catch)
|
|
|
|
|
var statistics: Dictionary = jobs.to_save_data().get("statistics", {})
|
|
|
|
|
assert(int(statistics.get("fish_caught", -1)) == 0)
|
|
|
|
|
assert(int(statistics.get("insects_caught", -1)) == 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _find_creature_job(
|
|
|
|
|
jobs: Array[Dictionary],
|
|
|
|
|
kind: JobCatalog.Kind,
|
|
|
|
|
creature_group: int,
|
|
|
|
|
) -> Dictionary:
|
|
|
|
|
for job: Dictionary in jobs:
|
|
|
|
|
if (
|
|
|
|
|
int(job.get("kind", -1)) == kind
|
|
|
|
|
and int(job.get("creature_group", -1)) == creature_group
|
|
|
|
|
):
|
|
|
|
|
return job
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _make_test_catch(fish: FishData, catch_id: StringName) -> FishCatch:
|
|
|
|
|
var fish_catch := FishCatch.new()
|
|
|
|
|
fish_catch.fish = fish
|
|
|
|
|
fish_catch.fish_id = fish.id
|
|
|
|
|
fish_catch.catch_id = catch_id
|
|
|
|
|
fish_catch.catch_sequence = 1
|
|
|
|
|
fish_catch.weight_lb = fish.get_minimum_weight()
|
|
|
|
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
|
|
|
|
fish_catch.weight_lb
|
|
|
|
|
)
|
|
|
|
|
fish_catch.quality = FishQuality.Tier.BORING
|
|
|
|
|
fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
|
|
|
|
|
assert(fish_catch.is_valid())
|
|
|
|
|
return fish_catch
|
|
|
|
|
|
|
|
|
|
|
2026-08-03 00:43:43 -04:00
|
|
|
func _validate_weather_guarantees(board: Dictionary) -> void:
|
|
|
|
|
var jobs: Array = board.get("jobs", [])
|
|
|
|
|
var schedule: Array = board.get("weather_schedule", [])
|
|
|
|
|
var anchor_index: int = int(board.get("schedule_anchor_index", 0))
|
|
|
|
|
for required_weather: int in JobCatalog.weather_requirements(jobs):
|
|
|
|
|
var occurrences: int = 0
|
|
|
|
|
var has_later_window: bool = false
|
|
|
|
|
for index: int in schedule.size():
|
|
|
|
|
var entry: Dictionary = schedule[index]
|
|
|
|
|
if int(entry.get("weather", -1)) != required_weather:
|
|
|
|
|
continue
|
|
|
|
|
occurrences += 1
|
|
|
|
|
if index > anchor_index:
|
|
|
|
|
has_later_window = true
|
|
|
|
|
assert(occurrences >= 2)
|
|
|
|
|
assert(has_later_window)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _validate_late_board_weather_fallback() -> void:
|
|
|
|
|
var late_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
|
|
|
|
|
"late-board-validation", [], false
|
|
|
|
|
)
|
|
|
|
|
assert(JobCatalog.weather_requirements(late_jobs).is_empty())
|
|
|
|
|
var anchored_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
|
|
|
|
|
"anchored-board-validation", [], true
|
|
|
|
|
)
|
|
|
|
|
var anchored_schedule: Array[Dictionary] = (
|
|
|
|
|
JobCatalog.generate_weather_schedule(
|
|
|
|
|
"anchored-board-validation", anchored_jobs, 9
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
for required_weather: int in JobCatalog.weather_requirements(anchored_jobs):
|
|
|
|
|
var occurrence_indices: Array[int] = []
|
|
|
|
|
for index: int in anchored_schedule.size():
|
|
|
|
|
var entry: Dictionary = anchored_schedule[index]
|
|
|
|
|
if (
|
|
|
|
|
index >= 9
|
|
|
|
|
and int(entry.get("weather", -1)) == required_weather
|
|
|
|
|
):
|
|
|
|
|
occurrence_indices.append(index)
|
|
|
|
|
assert(occurrence_indices.size() >= 2)
|
|
|
|
|
assert(occurrence_indices[0] >= 9)
|
|
|
|
|
assert(occurrence_indices[-1] > 9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _validate_remote_tamper_rejection(
|
|
|
|
|
jobs: PlayerJobService,
|
|
|
|
|
board: Dictionary,
|
|
|
|
|
) -> void:
|
|
|
|
|
var tampered: Dictionary = board.duplicate(true)
|
|
|
|
|
var tampered_jobs: Array = tampered.get("jobs", [])
|
|
|
|
|
var first_job: Dictionary = tampered_jobs[0]
|
|
|
|
|
first_job["fish_coin"] = int(first_job.get("fish_coin", 0)) + 1
|
|
|
|
|
tampered_jobs[0] = first_job
|
|
|
|
|
tampered["jobs"] = tampered_jobs
|
|
|
|
|
assert(PlayerJobService.validate_board(tampered))
|
|
|
|
|
assert(not jobs.apply_remote_board(tampered))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _find_job(jobs: Array[Dictionary], kind: JobCatalog.Kind) -> Dictionary:
|
|
|
|
|
for job: Dictionary in jobs:
|
|
|
|
|
if int(job.get("kind", -1)) == int(kind):
|
|
|
|
|
return job
|
|
|
|
|
return {}
|