Add synchronized Fishnet jobs and laptop UI
This commit is contained in:
parent
46df74361a
commit
26fbd41132
39 changed files with 3257 additions and 188 deletions
|
|
@ -31,6 +31,9 @@ func _run() -> void:
|
|||
|
||||
var player := main.get("_player") as Player
|
||||
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||
var world_weather := (
|
||||
main.get_node("%WorldWeatherService") as WorldWeatherService
|
||||
)
|
||||
world_time.synchronize_time(19.75)
|
||||
assert(player.experience.award_experience(125))
|
||||
var fish_catalog := main.get("fish_catalog") as FishPool
|
||||
|
|
@ -92,6 +95,10 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(not service.is_local_showcase_visible())
|
||||
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
|
||||
var saved_weather: WorldWeatherService.Weather = (
|
||||
world_weather.get_weather()
|
||||
)
|
||||
var saved_weather_seconds: float = world_weather.get_seconds_remaining()
|
||||
|
||||
assert(save_manager.save_now())
|
||||
var save_path: String = str(save_manager.get("_save_path"))
|
||||
|
|
@ -103,17 +110,25 @@ 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"]) == 6)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 7)
|
||||
assert(
|
||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||
== 125
|
||||
)
|
||||
assert(absf(
|
||||
float((parsed as Dictionary)["world"]["time_hours"]) - 19.75
|
||||
) < 0.01)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
float((parsed as Dictionary)["world"]["time_hours"]),
|
||||
19.75,
|
||||
)
|
||||
int((parsed as Dictionary)["world"]["weather"])
|
||||
== int(saved_weather)
|
||||
)
|
||||
assert(absf(
|
||||
float(
|
||||
(parsed as Dictionary)["world"][
|
||||
"weather_seconds_remaining"
|
||||
]
|
||||
) - saved_weather_seconds
|
||||
) < 0.1)
|
||||
var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"]
|
||||
assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
|
||||
var saved_masks: Dictionary = (
|
||||
|
|
@ -127,9 +142,18 @@ func _run() -> void:
|
|||
assert(player.hotbar.clear_slot(1))
|
||||
assert(player.experience.restore_total_experience(0))
|
||||
world_time.synchronize_time(8.0)
|
||||
world_weather.clear_daily_plan()
|
||||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.FOGGY,
|
||||
10.0,
|
||||
)
|
||||
assert(save_manager.load_player_data())
|
||||
assert(player.experience.get_total_experience() == 125)
|
||||
assert(is_equal_approx(world_time.get_time_hours(), 19.75))
|
||||
assert(absf(world_time.get_time_hours() - 19.75) < 0.01)
|
||||
assert(world_weather.get_weather() == saved_weather)
|
||||
assert(absf(
|
||||
world_weather.get_seconds_remaining() - saved_weather_seconds
|
||||
) < 2.0)
|
||||
assert(player.experience.get_level() == 2)
|
||||
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||
assert(player.hotbar.get_selected_slot() == 1)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_tiers_and_distribution()
|
||||
_validate_barrier_challenge_curve()
|
||||
_validate_fight_pacing_and_reel_upgrades()
|
||||
_validate_catch_round_trip_and_sale()
|
||||
_validate_mail_round_trip()
|
||||
_validate_collection_mastery()
|
||||
|
|
@ -72,6 +74,115 @@ func _validate_tiers_and_distribution() -> void:
|
|||
assert(absf(observed - expected) < 0.015)
|
||||
|
||||
|
||||
func _validate_barrier_challenge_curve() -> void:
|
||||
assert(
|
||||
FishQualityType.BARRIER_HEALTH_MULTIPLIERS.size()
|
||||
== FishQualityType.TIER_COUNT
|
||||
)
|
||||
var expected_health: Array[int] = [8, 10, 13, 18, 26]
|
||||
var previous_health: int = 0
|
||||
for quality: int in FishQualityType.TIER_COUNT:
|
||||
var health: int = FishQualityType.apply_barrier_health(8, quality)
|
||||
assert(health == expected_health[quality])
|
||||
assert(health > previous_health)
|
||||
previous_health = health
|
||||
assert(FishQualityType.apply_barrier_health(8, -1) == 8)
|
||||
assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4)
|
||||
|
||||
var profile := CatchDifficultyProfile.new()
|
||||
profile.barrier_count_min = 1
|
||||
profile.barrier_count_max = 1
|
||||
profile.barrier_health_min = 8
|
||||
profile.barrier_health_max = 8
|
||||
profile.first_barrier_margin = 0.2
|
||||
profile.final_barrier_margin = 0.2
|
||||
profile.minimum_barrier_spacing = 0.1
|
||||
var controller := CatchController.new()
|
||||
root.add_child(controller)
|
||||
for quality: int in FishQualityType.TIER_COUNT:
|
||||
controller.start_authoritative_encounter(
|
||||
profile,
|
||||
0.1,
|
||||
1,
|
||||
818181,
|
||||
quality,
|
||||
)
|
||||
var barriers_value: Variant = controller.get("_barriers")
|
||||
assert(barriers_value is Array)
|
||||
var barriers: Array = barriers_value as Array
|
||||
assert(barriers.size() == 1)
|
||||
var barrier := barriers[0] as RefCounted
|
||||
assert(barrier != null)
|
||||
assert(int(barrier.get("maximum_health")) == expected_health[quality])
|
||||
controller.queue_free()
|
||||
|
||||
var shiny_health: int = FishQualityType.apply_barrier_health(
|
||||
8,
|
||||
FishQualityType.Tier.SHINY,
|
||||
)
|
||||
var base_power_clicks: int = ceili(float(shiny_health) / 1.0)
|
||||
var max_power_clicks: int = ceili(
|
||||
float(shiny_health)
|
||||
/ float(PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL + 1)
|
||||
)
|
||||
assert(base_power_clicks == 26)
|
||||
assert(max_power_clicks == 7)
|
||||
assert(max_power_clicks < base_power_clicks)
|
||||
|
||||
|
||||
func _validate_fight_pacing_and_reel_upgrades() -> void:
|
||||
assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
|
||||
assert(is_equal_approx(CatchController.CHASE_START_DELAY, 0.5))
|
||||
assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
|
||||
assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))
|
||||
|
||||
var upgrades := PlayerFishingUpgrades.new()
|
||||
assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))
|
||||
assert(
|
||||
upgrades.restore_levels(
|
||||
PlayerFishingUpgrades.MAX_REEL_SPEED_LEVEL,
|
||||
0,
|
||||
)
|
||||
)
|
||||
var upgraded_multiplier: float = upgrades.get_reel_speed_multiplier()
|
||||
assert(is_equal_approx(upgraded_multiplier, 1.5))
|
||||
|
||||
var profile := CatchDifficultyProfile.new()
|
||||
profile.barrier_count_min = 0
|
||||
profile.barrier_count_max = 0
|
||||
var controller := CatchController.new()
|
||||
root.add_child(controller)
|
||||
controller.start_authoritative_encounter(
|
||||
profile,
|
||||
Player.BASE_REEL_SPEED,
|
||||
1,
|
||||
919191,
|
||||
)
|
||||
controller.set_reel_input(true)
|
||||
controller.call("_update_free_reeling", 1.0)
|
||||
var base_progress: float = controller.progress
|
||||
assert(is_equal_approx(base_progress, Player.BASE_REEL_SPEED))
|
||||
|
||||
controller.reset()
|
||||
controller.start_authoritative_encounter(
|
||||
profile,
|
||||
Player.BASE_REEL_SPEED * upgraded_multiplier,
|
||||
1,
|
||||
919191,
|
||||
)
|
||||
controller.set_reel_input(true)
|
||||
controller.call("_update_free_reeling", 1.0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
controller.progress,
|
||||
Player.BASE_REEL_SPEED * upgraded_multiplier,
|
||||
)
|
||||
)
|
||||
assert(controller.progress > base_progress)
|
||||
controller.queue_free()
|
||||
upgrades.queue_free()
|
||||
|
||||
|
||||
func _validate_catch_round_trip_and_sale() -> void:
|
||||
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||
assert(fish != null)
|
||||
|
|
@ -242,7 +353,7 @@ func _validate_version_four_migration() -> void:
|
|||
version_four,
|
||||
4,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 6)
|
||||
assert(int(migrated.get("save_version", -1)) == 7)
|
||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
|
|
@ -259,4 +370,7 @@ func _validate_version_four_migration() -> void:
|
|||
var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
|
||||
assert(int(masks["bluegill"]) == boring_bit)
|
||||
assert(int(masks["carp"]) == boring_bit)
|
||||
assert(
|
||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
||||
)
|
||||
manager.queue_free()
|
||||
|
|
|
|||
111
tests/job_multiplayer_validation.gd
Normal file
111
tests/job_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const TEST_PORT: int = 18139
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if arguments.has("host"):
|
||||
await _run_host()
|
||||
return
|
||||
if arguments.has("client"):
|
||||
await _run_client()
|
||||
return
|
||||
push_error("Job multiplayer validation needs host or client mode.")
|
||||
quit(1)
|
||||
|
||||
|
||||
func _run_host() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
|
||||
assert(session.start_private_host(TEST_PORT))
|
||||
assert(save_manager.initialize_new_game())
|
||||
main.call("_enter_gameplay")
|
||||
await process_frame
|
||||
assert(session.set_host_open(true))
|
||||
var host_board: Dictionary = jobs.get_host_board_network_data()
|
||||
assert(PlayerJobService.validate_board(host_board))
|
||||
|
||||
var remote_peer_id: int = 0
|
||||
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
|
||||
await process_frame
|
||||
for peer_id: int in session.get_authenticated_peer_ids():
|
||||
if peer_id != session.get_local_peer_id():
|
||||
remote_peer_id = peer_id
|
||||
break
|
||||
assert(remote_peer_id > 1)
|
||||
assert(session.peer_supports_capability(
|
||||
remote_peer_id, NetworkProtocol.JOBS_CAPABILITY
|
||||
))
|
||||
|
||||
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||
while (
|
||||
Time.get_ticks_msec() < disconnect_deadline
|
||||
and session.is_authenticated_peer(remote_peer_id)
|
||||
):
|
||||
await process_frame
|
||||
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||
print("Job multiplayer host validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _run_client() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
main.call(
|
||||
"_on_title_join_game_requested",
|
||||
"127.0.0.1:%d" % TEST_PORT,
|
||||
)
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var jobs := main.get_node("%PlayerJobService") as PlayerJobService
|
||||
var weather := main.get_node("%WorldWeatherService") as WorldWeatherService
|
||||
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < join_deadline:
|
||||
await process_frame
|
||||
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||
main.call("_confirm_server_trust")
|
||||
if (
|
||||
session.is_joined_client()
|
||||
and bool(main.get("_gameplay_started"))
|
||||
and not jobs.get_plan_id().is_empty()
|
||||
):
|
||||
break
|
||||
assert(session.is_joined_client())
|
||||
assert(session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY))
|
||||
assert(not jobs.get_plan_id().is_empty())
|
||||
assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
|
||||
assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
|
||||
assert(weather.get_daily_plan_id().is_empty())
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var player_menu := game_ui.get("_player_menu") as PlayerMenu
|
||||
player_menu.call("_show_section_immediate", PlayerMenu.Section.NET)
|
||||
assert((player_menu.get_node("%TheNetPage") as TheNetPage).visible)
|
||||
print("Job multiplayer client validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _create_initialized_main() -> Node:
|
||||
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")))
|
||||
return main
|
||||
1
tests/job_multiplayer_validation.gd.uid
Normal file
1
tests/job_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c4y0pmxk2bhmu
|
||||
256
tests/job_system_validation.gd
Normal file
256
tests/job_system_validation.gd
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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)
|
||||
join_page.call("_refresh")
|
||||
var session_summary := (
|
||||
join_page.get_node("%SessionSummary") as Label
|
||||
)
|
||||
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())
|
||||
var save_file := FileAccess.open(
|
||||
str(save_manager.get("_save_path")), FileAccess.READ
|
||||
)
|
||||
assert(save_file != null)
|
||||
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
|
||||
save_file.close()
|
||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||
var save_data: Dictionary = parsed
|
||||
assert(int(save_data.get("save_version", -1)) == 7)
|
||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
print("Job system validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
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 {}
|
||||
1
tests/job_system_validation.gd.uid
Normal file
1
tests/job_system_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b2grposd30lkw
|
||||
|
|
@ -143,11 +143,14 @@ func _validate_save_migration() -> void:
|
|||
version_five,
|
||||
5,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 6)
|
||||
assert(int(migrated.get("save_version", -1)) == 7)
|
||||
var experience_data: Dictionary = migrated.get("experience", {})
|
||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||
var world_data: Dictionary = migrated.get("world", {})
|
||||
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
|
||||
assert(
|
||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
||||
)
|
||||
manager.queue_free()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ func _run_host() -> void:
|
|||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.RAINY, 300.0
|
||||
)
|
||||
assert(
|
||||
world_weather.get_persistent_weather()
|
||||
== WorldWeatherService.Weather.RAINY
|
||||
)
|
||||
assert(session.set_host_open(true))
|
||||
|
||||
var remote_peer_id: int = 0
|
||||
|
|
@ -65,6 +69,10 @@ func _run_host() -> void:
|
|||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.FOGGY, 300.0
|
||||
)
|
||||
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
|
||||
|
|
@ -105,6 +113,10 @@ func _run_client() -> void:
|
|||
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 (
|
||||
|
|
@ -162,6 +174,14 @@ 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)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,20 @@ func _validate_clock_boundaries_and_duration() -> void:
|
|||
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
||||
assert(not clock.is_night_period())
|
||||
assert(clock.is_transition())
|
||||
|
||||
# Small high-refresh deltas must still advance the displayed minute. Using
|
||||
# is_equal_approx() per frame used to suppress every clock notification.
|
||||
var emitted_times: Array[float] = []
|
||||
clock.time_changed.connect(
|
||||
func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
|
||||
emitted_times.append(time_hours)
|
||||
)
|
||||
clock.begin_session(12.0 + 31.0 / 60.0)
|
||||
emitted_times.clear()
|
||||
for _frame: int in 1201:
|
||||
clock.advance_time(1.0 / 240.0)
|
||||
assert(clock.get_clock_text() == "12:33 pm")
|
||||
assert(emitted_times.size() >= 2)
|
||||
clock.queue_free()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_weather_scheduler()
|
||||
_validate_weather_persistence()
|
||||
_validate_snapshot_bounds()
|
||||
_validate_fishing_weather_seams()
|
||||
_validate_fishing_spot_context()
|
||||
|
|
@ -73,6 +74,51 @@ func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _validate_weather_persistence() -> void:
|
||||
var weather := WorldWeatherServiceType.new()
|
||||
root.add_child(weather)
|
||||
weather.set_persistence_tracking_enabled(true)
|
||||
weather.begin_authoritative_session(20260802)
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.RAINY,
|
||||
200.0,
|
||||
)
|
||||
weather.advance_weather(12.5)
|
||||
assert(weather.has_persistent_state())
|
||||
assert(
|
||||
weather.get_persistent_weather()
|
||||
== WorldWeatherServiceType.Weather.RAINY
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
weather.get_persistent_seconds_remaining(),
|
||||
187.5,
|
||||
))
|
||||
weather.set_persistence_tracking_enabled(false)
|
||||
weather.begin_remote_session()
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.FOGGY,
|
||||
45.0,
|
||||
)
|
||||
assert(weather.is_foggy())
|
||||
assert(
|
||||
weather.get_persistent_weather()
|
||||
== WorldWeatherServiceType.Weather.RAINY
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
weather.get_persistent_seconds_remaining(),
|
||||
187.5,
|
||||
))
|
||||
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(not weather.restore_persistent_state(
|
||||
WorldWeatherServiceType.Weather.RAINY,
|
||||
WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0,
|
||||
))
|
||||
weather.queue_free()
|
||||
|
||||
|
||||
func _validate_snapshot_bounds() -> void:
|
||||
assert(NetworkWorldWeatherServiceType.validate_snapshot({
|
||||
"session_id": "session",
|
||||
|
|
@ -187,6 +233,22 @@ func _validate_weather_presentation() -> void:
|
|||
assert(rain != null)
|
||||
assert(rain.emitting)
|
||||
assert(rain.amount_ratio > 0.99)
|
||||
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
|
||||
var rain_material := rain.process_material as ParticleProcessMaterial
|
||||
assert(rain_material != null)
|
||||
assert(is_equal_approx(
|
||||
rain_material.initial_velocity_min,
|
||||
WorldTimeVisualControllerType.RAIN_VELOCITY_MIN,
|
||||
))
|
||||
assert(is_equal_approx(
|
||||
rain_material.initial_velocity_max,
|
||||
WorldTimeVisualControllerType.RAIN_VELOCITY_MAX,
|
||||
))
|
||||
var rain_mesh := rain.draw_pass_1 as BoxMesh
|
||||
assert(rain_mesh != null)
|
||||
assert(rain_mesh.size.is_equal_approx(
|
||||
WorldTimeVisualControllerType.RAIN_DROP_SIZE
|
||||
))
|
||||
visuals.apply_weather_immediately(
|
||||
WorldWeatherServiceType.Weather.SUNNY
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue