Expand gameplay systems and interface controls

This commit is contained in:
Alexander Sellite 2026-08-20 17:53:22 -04:00
parent 09b7a78533
commit 2710544070
46 changed files with 2072 additions and 153 deletions

View file

@ -86,6 +86,11 @@ func _run() -> void:
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
assert(player != null and service != null and toolbar != null)
assert(chat_ui != null)
assert(toolbar.get_parent() == chat_ui.get_parent())
assert(
toolbar.get_index() > chat_ui.get_index(),
"The art-kit toolbar must receive overlapping pointer input before Chat.",
)
assert(bool(game_ui.call("_can_start_virtual_mouse")))
var virtual_pointer_states: Array[bool] = []
game_ui.virtual_pointer_mode_changed.connect(
@ -317,6 +322,22 @@ func _run() -> void:
assert(not (
game_ui.get_node("%ExperiencePresentation") as Control
).visible)
var hud_fishing_spot := main.get_node("%FishingSpot") as FishingSpot
assert(hud_fishing_spot != null)
hud_fishing_spot.state = FishingSpot.FishingState.SHOWING_CATCH
game_ui.call("_refresh_gameplay_hud_visibility")
assert((
game_ui.get_node("%GameplayTransientHUD") as Control
).visible)
assert(not (
game_ui.get_node("%ExperiencePresentation") as Control
).visible)
assert(chat_ui.is_hud_hidden())
hud_fishing_spot.state = FishingSpot.FishingState.READY
game_ui.call("_refresh_gameplay_hud_visibility")
assert(not (
game_ui.get_node("%GameplayTransientHUD") as Control
).visible)
quick_menu.open_menu(true)
assert(quick_menu.visible and quick_menu.is_open())
quick_menu.close_menu()
@ -365,6 +386,12 @@ func _run() -> void:
await process_frame
assert(service.is_active() and not service.is_placement_mode())
assert(toolbar.visible)
chat_ui.open_chat()
assert(chat_ui.is_open())
assert(not service.is_active() and not toolbar.visible)
chat_ui.close_chat()
assert(not chat_ui.is_open())
assert(service.is_active() and toolbar.visible)
var ui_root := game_ui.get_node("%UIRoot") as Control
assert(toolbar.get_parent() == ui_root)
assert(is_zero_approx(toolbar.position.y))

View file

@ -0,0 +1,120 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const TEST_PORT: int = 18152
const PREJOIN_MESSAGE: String = "private prejoin message"
const LIVE_MESSAGE: String = "live message after join"
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("Chat privacy 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
assert(session.start_private_host(TEST_PORT))
assert(save_manager.initialize_new_game())
main.call("_enter_gameplay")
for _frame: int in 4:
await physics_frame
assert(session.set_host_open(true))
var chat_service := main.get_node(
"%NetworkChatService"
) as NetworkChatService
assert(chat_service.send_local_message(PREJOIN_MESSAGE))
var remote_peer_id: int = await _wait_for_remote_peer(session)
assert(remote_peer_id > 1)
await create_timer(1.0).timeout
assert(chat_service.send_local_message(LIVE_MESSAGE))
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
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("Chat privacy multiplayer host validation: PASS")
await _session_cleanup(main, session)
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 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")):
break
assert(session.is_joined_client())
var chat_service := main.get_node(
"%NetworkChatService"
) as NetworkChatService
await create_timer(0.5).timeout
assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
var live_deadline: int = Time.get_ticks_msec() + 8000
while (
Time.get_ticks_msec() < live_deadline
and not _history_contains(chat_service, LIVE_MESSAGE)
):
await process_frame
assert(_history_contains(chat_service, LIVE_MESSAGE))
assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
print("Chat privacy multiplayer client validation: PASS")
await _session_cleanup(main, session)
func _history_contains(service: NetworkChatService, body: String) -> bool:
return service.get_history().any(
func(message: Dictionary) -> bool:
return str(message.get("body", "")) == body
)
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
func _wait_for_remote_peer(session: NetworkSession) -> int:
var deadline: int = Time.get_ticks_msec() + 20000
while Time.get_ticks_msec() < deadline:
await process_frame
for peer_id: int in session.get_authenticated_peer_ids():
if peer_id != session.get_local_peer_id():
return peer_id
return 0
func _session_cleanup(main: Node, session: NetworkSession) -> void:
session.disconnect_session("")
main.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

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

View file

@ -19,6 +19,12 @@ func _run() -> void:
defaults.call("_validate")
assert(defaults.is_valid())
assert(not bool(defaults.get("public_listing")))
assert(not bool(defaults.get("chat_logging")))
assert(str(defaults.get("chat_log_path")).is_empty())
assert(NetworkChatService.BURST_COUNT == 3)
assert(NetworkChatService.WINDOW_COUNT == 5)
assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
var path: String = ProjectSettings.globalize_path(
"user://dedicated-server-validation.cfg"
@ -32,6 +38,12 @@ func _run() -> void:
file.set_value("server", "public", true)
file.set_value("server", "data_directory", "/tmp/configured-server")
file.set_value("discovery", "url", "https://discovery.netfishing.org/")
file.set_value("privacy", "chat_logging", true)
file.set_value(
"privacy",
"chat_log_path",
"/tmp/configured-server/logs/chat.jsonl",
)
file.set_value(
"moderation", "operators", PackedStringArray([OPERATOR_B, OPERATOR_A])
)
@ -48,6 +60,11 @@ func _run() -> void:
assert(int(configured.get("world_seed")) == 928374)
assert(bool(configured.get("public_listing")))
assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org")
assert(bool(configured.get("chat_logging")))
assert(
str(configured.get("chat_log_path"))
== "/tmp/configured-server/logs/chat.jsonl"
)
assert(
configured.get("operator_fingerprints")
== PackedStringArray([OPERATOR_A, OPERATOR_B])
@ -89,6 +106,51 @@ func _run() -> void:
invalid_seed.call("_validate")
assert(not invalid_seed.is_valid())
var default_chat_path := ConfigType.new()
default_chat_path.set("data_directory", "/tmp/default-chat-path-server")
default_chat_path.set("chat_logging", true)
default_chat_path.call("_validate")
assert(default_chat_path.is_valid())
assert(
str(default_chat_path.get("chat_log_path"))
== "/tmp/default-chat-path-server/logs/chat.jsonl"
)
var invalid_chat_path := ConfigType.new()
invalid_chat_path.set("data_directory", "/tmp/invalid-chat-path-server")
invalid_chat_path.set("chat_logging", true)
invalid_chat_path.set("chat_log_path", "relative/chat.jsonl")
invalid_chat_path.call("_validate")
assert(not invalid_chat_path.is_valid())
var chat_log_validation_path: String = ProjectSettings.globalize_path(
"user://dedicated-chat-log-validation.jsonl"
)
var chat_service := NetworkChatService.new()
assert(not chat_service.configure_dedicated_history(
true,
"relative/chat.jsonl",
))
assert(chat_service.configure_dedicated_history(
true,
chat_log_validation_path,
))
assert(FileAccess.file_exists(chat_log_validation_path))
chat_service.call("_append_chat_log", {
"sender_display_name": "Privacy Tester",
"body": "explicitly retained message",
})
var logged_message: Variant = JSON.parse_string(
FileAccess.get_file_as_string(chat_log_validation_path).strip_edges()
)
assert(typeof(logged_message) == TYPE_DICTIONARY)
assert(str(logged_message.get("recorded_at_utc", "")).ends_with("Z"))
assert(logged_message.get("sender_display_name") == "Privacy Tester")
assert(logged_message.get("body") == "explicitly retained message")
assert(chat_service.configure_dedicated_history(false, ""))
assert(DirAccess.remove_absolute(chat_log_validation_path) == OK)
chat_service.free()
var discovery := DiscoveryClient.new()
assert(
str(discovery.call("_default_room_name", "River"))

View file

@ -54,6 +54,21 @@ func _initialize() -> void:
assert(FishingShopStockType.get_price(&"magnet") == 250)
assert(FishingShopStockType.get_stock_item_ids().has(&"magnet"))
assert(FishingShopStockType.is_permanent_unlock(&"magnet", magnet))
var fishing_net: ItemDataType = (
ItemCatalogResource.get_available_item_by_id(&"fishing_net")
)
assert(fishing_net != null)
assert(fishing_net.category == ItemDataType.Category.TOOL)
assert(fishing_net.icon == null)
assert(not fishing_net.usable)
assert(not fishing_net.equippable)
assert(not fishing_net.hotbar_allowed)
assert(FishingShopStockType.get_price(&"fishing_net") == 250)
assert(FishingShopStockType.get_stock_item_ids().has(&"fishing_net"))
assert(FishingShopStockType.is_permanent_unlock(
&"fishing_net", fishing_net
))
assert(not FishingShopStockType.is_gathering_tool(&"fishing_net"))
var crab_net: ItemDataType = ItemCatalogResource.get_available_item_by_id(
&"crab_net"
)

View file

@ -98,29 +98,27 @@ func _run() -> void:
assert(save_manager.save_now())
var save_path: String = str(save_manager.get("_save_path"))
var save_file := FileAccess.open(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 hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
assert(bool(decoded.get("ok", false)))
var parsed: Dictionary = decoded["data"]
var hotbar_data: Dictionary = parsed["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"]) == 9)
assert(int(parsed["save_version"]) == 10)
assert(
int((parsed as Dictionary)["experience"]["total_experience"])
int(parsed["experience"]["total_experience"])
== 125
)
assert(absf(
float((parsed as Dictionary)["world"]["time_hours"]) - saved_time_hours
float(parsed["world"]["time_hours"]) - saved_time_hours
) < 0.01)
assert(
int((parsed as Dictionary)["world"]["weather"])
int(parsed["world"]["weather"])
== int(saved_weather)
)
assert(absf(
float(
(parsed as Dictionary)["world"][
parsed["world"][
"weather_seconds_remaining"
]
) - saved_weather_seconds

View file

@ -433,7 +433,7 @@ func _validate_version_four_migration() -> void:
version_four,
4,
)
assert(int(migrated.get("save_version", -1)) == 9)
assert(int(migrated.get("save_version", -1)) == 10)
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
assert(
is_equal_approx(

View file

@ -32,6 +32,7 @@ func _run() -> void:
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
var catalog := main.get("fish_catalog") as FishPool
assert(player != null and fishing_spot != null and catalog != null)
fishing_spot.minimum_showcase_duration = 0.15
assert(player.bag.add_item(&"crab_net"))
assert(player.hotbar.assign_item(0, &"crab_net"))
assert(player.hotbar.select_slot(0))
@ -60,13 +61,20 @@ func _run() -> void:
assert(not player.is_movement_enabled())
Input.action_release("fish_primary")
await process_frame
assert(bool(fishing_spot.get("_put_away_press_armed")))
assert(not bool(fishing_spot.get("_put_away_press_armed")))
var pocket_event := InputEventAction.new()
pocket_event.action = &"fish_primary"
pocket_event.pressed = true
Input.parse_input_event(pocket_event)
await process_frame
assert(fishing_spot.get("_pending_catch") == crab_catch)
Input.action_release("fish_primary")
await create_timer(0.2).timeout
await process_frame
assert(bool(fishing_spot.get("_put_away_press_armed")))
Input.parse_input_event(pocket_event)
await process_frame
assert(fishing_spot.get("_pending_catch") == null)
var pocket_deadline: int = Time.get_ticks_msec() + 6000

View file

@ -1,6 +1,9 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
const ItemCatalogResource: ItemCatalog = preload(
"res://items/catalog/item_catalog.tres"
)
func _initialize() -> void:
@ -27,29 +30,34 @@ func _run() -> void:
save_manager.set_autosave_enabled(true)
assert(player.bag.add_item(&"art_kit"))
assert(player.bag.add_item(&"coffee"))
assert(player.bag.add_item(&"worms"))
assert(player.bag.add_item(&"the_standby"))
assert(player.equip_bait(ItemCatalogResource.get_item_by_id(&"worms")))
assert(player.equip_lure(ItemCatalogResource.get_item_by_id(&"the_standby")))
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
assert(player.bag.move_item_to_storage_slot(&"coffee", 12))
assert(save_manager.save_now())
var save_file := FileAccess.open(
str(save_manager.get("_save_path")),
FileAccess.READ,
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
str(save_manager.get("_save_path"))
)
assert(save_file != null)
var parsed: Variant = JSON.parse_string(save_file.get_as_text())
save_file.close()
assert(typeof(parsed) == TYPE_DICTIONARY)
var records: Array = (parsed as Dictionary)["bag"]["items"]
assert(int((parsed as Dictionary)["world"]["seed"]) == TEST_WORLD_SEED)
assert(bool(decoded.get("ok", false)))
var parsed: Dictionary = decoded["data"]
var records: Array = parsed["bag"]["items"]
assert(int(parsed["world"]["seed"]) == TEST_WORLD_SEED)
assert(_saved_slot(records, &"basic_fishing_rod") == 14)
assert(_saved_slot(records, &"coffee") == 12)
assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
player.unequip_bait()
player.unequip_lure()
assert(save_manager.load_player_data())
assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
assert(player.bag.get_storage_slot(&"coffee") == 12)
assert(player.active_bait_id == &"worms")
assert(player.active_lure_id == &"the_standby")
main.queue_free()
for _frame: int in 4:

View file

@ -49,6 +49,7 @@ func _run() -> void:
_validate_weather_guarantees(board)
_validate_late_board_weather_fallback()
_validate_remote_tamper_rejection(jobs, board)
_validate_creature_jobs(jobs)
var sell_job: Dictionary = _find_job(
jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
@ -195,15 +196,12 @@ func _run() -> void:
assert(session.set_host_open(false))
assert(save_manager.save_now())
var save_file := FileAccess.open(
str(save_manager.get("_save_path")), FileAccess.READ
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
str(save_manager.get("_save_path"))
)
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)) == 9)
assert(bool(decoded.get("ok", false)))
var save_data: Dictionary = decoded["data"]
assert(int(save_data.get("save_version", -1)) == 10)
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
_validate_pause_session_switch(main, session)
@ -263,6 +261,100 @@ func _validate_unavailable_fishnet_layout() -> void:
await process_frame
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
func _validate_weather_guarantees(board: Dictionary) -> void:
var jobs: Array = board.get("jobs", [])
var schedule: Array = board.get("weather_schedule", [])

View file

@ -7,6 +7,12 @@ const KeyboardMouseMappingPanelType = preload(
"res://ui/keyboard_mouse_mapping_panel.gd"
)
const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
const FishingSpotScene = preload("res://fishing/fishing_spot.tscn")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const CatchDifficultyProfileType = preload(
"res://fishing/catch_difficulty_profile.gd"
)
const PlayerType = preload("res://player/player.gd")
func _initialize() -> void:
@ -27,6 +33,13 @@ func _run() -> void:
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
].get("kind", "")) == "mouse_button"
)
assert(
str(defaults[
str(KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL)
].get("kind", "")) == "key"
)
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
assert(_event_count(&"reel_alternate", true) == 0)
assert(
str(defaults[
str(KeyboardMouseMappingManagerType.ROLE_CHAT)
@ -60,6 +73,15 @@ func _run() -> void:
))
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_XBUTTON1))
assert(not _has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
var alternate_reel_key := InputEventKey.new()
alternate_reel_key.physical_keycode = KEY_G
alternate_reel_key.pressed = true
assert(manager.set_binding(
KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL,
manager.binding_from_event(alternate_reel_key),
))
assert(_has_physical_key(&"reel_alternate", KEY_G))
assert(_event_count(&"reel_alternate", true) == 0)
var joypad_event := InputEventJoypadButton.new()
joypad_event.button_index = JOY_BUTTON_A
@ -117,11 +139,64 @@ func _run() -> void:
))
assert(_has_physical_key(&"jump", KEY_SPACE))
assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
assert(_event_count(&"reel_alternate", true) == 0)
assert(_has_physical_key(&"open_chat", KEY_T))
assert(_event_count(&"jump", true) == joypad_events_before)
var legacy_bindings: Dictionary = manager.get_active_bindings()
legacy_bindings.erase(str(
KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL
))
legacy_bindings[str(KeyboardMouseMappingManagerType.ROLE_JUMP)] = (
manager.binding_from_event(rebind_key)
)
var legacy_file := FileAccess.open(
KeyboardMouseMappingManagerType.MAPPING_PATH,
FileAccess.WRITE,
)
assert(legacy_file != null)
legacy_file.store_string(JSON.stringify({
"format_version": KeyboardMouseMappingManagerType.LEGACY_FORMAT_VERSION,
"bindings": legacy_bindings,
}))
legacy_file.close()
var migrated_manager := KeyboardMouseMappingManagerType.new()
root.add_child(migrated_manager)
await process_frame
assert(migrated_manager.has_custom_mapping())
assert(_has_physical_key(&"jump", KEY_R))
assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
assert(migrated_manager.reset_mapping())
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
root.add_child(fishing_spot)
await process_frame
var local_player := PlayerType.new()
fishing_spot.set("_local_player", local_player)
fishing_spot.set("_gameplay_input_enabled", true)
fishing_spot.state = FishingSpotType.FishingState.FIGHTING
var catch_controller := fishing_spot.get_node("%CatchController")
var catch_profile := CatchDifficultyProfileType.new()
catch_profile.barrier_count_min = 0
catch_profile.barrier_count_max = 0
catch_controller.start_encounter(catch_profile, 1.0, 1)
var alternate_press := InputEventAction.new()
alternate_press.action = &"reel_alternate"
alternate_press.pressed = true
fishing_spot._unhandled_input(alternate_press)
assert(bool(catch_controller.get("_reel_input_held")))
var alternate_release := InputEventAction.new()
alternate_release.action = &"reel_alternate"
alternate_release.pressed = false
fishing_spot._unhandled_input(alternate_release)
assert(not bool(catch_controller.get("_reel_input_held")))
fishing_spot.free()
local_player.free()
settings_panel.queue_free()
panel.queue_free()
migrated_manager.queue_free()
manager.queue_free()
print("Keyboard and mouse mapping validation: PASS")
quit(0)

View file

@ -43,6 +43,7 @@ func _run() -> void:
await process_frame
assert(bool(normal_main.get("_application_initialized")))
_validate_normal_profile(normal_main)
_validate_new_game_music_transition(normal_main)
_stop_audio_players(normal_main)
normal_main.queue_free()
for _frame: int in 8:
@ -115,6 +116,7 @@ func _validate_main_profile(main: Node) -> void:
assert(player_menu != null)
assert(not player_menu.is_cooler_water_effect_enabled())
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
assert(
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream == null
@ -183,6 +185,7 @@ func _validate_normal_profile(main: Node) -> void:
assert(clouds.get_node_or_null("CloudField") is MultiMeshInstance3D)
assert(clouds.get_node_or_null("CloudCeiling") == null)
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
assert(
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null
@ -192,6 +195,18 @@ func _validate_normal_profile(main: Node) -> void:
)
func _validate_new_game_music_transition(main: Node) -> void:
var title_music := main.get_node("%TitleMusic") as AudioStreamPlayer
var new_game_music := main.get_node("%NewGameMusic") as AudioStreamPlayer
assert(title_music.playing)
main.call("_start_new_game_music")
assert(not title_music.playing)
assert(new_game_music.playing)
main.call("_show_title_music", true)
assert(not new_game_music.playing)
assert(title_music.playing)
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
var root := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"

View file

@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
version_five,
5,
)
assert(int(migrated.get("save_version", -1)) == 9)
assert(int(migrated.get("save_version", -1)) == 10)
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

@ -152,15 +152,10 @@ func _run_client() -> void:
var chat_service := main.get_node(
"%NetworkChatService"
) as NetworkChatService
var history_deadline: int = Time.get_ticks_msec() + 8000
while Time.get_ticks_msec() < history_deadline:
if chat_service.get_history().any(
func(message: Dictionary) -> bool:
return str(message.get("body", "")) == HOST_HISTORY_MESSAGE
):
break
await process_frame
assert(chat_service.get_history().any(
# Player-hosted rooms deliberately send no prior scrollback to joining or
# reconnecting clients. Live messages sent after authentication still work.
await create_timer(0.5).timeout
assert(not chat_service.get_history().any(
func(message: Dictionary) -> bool:
return str(message.get("body", "")) == HOST_HISTORY_MESSAGE
))

View file

@ -0,0 +1,135 @@
extends SceneTree
const MainScene: PackedScene = preload("res://main/main.tscn")
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 save_manager := main.get("_save_manager") as PlayerSaveManager
var player := main.get("_player") as Player
var data_root := main.get("_data_root") as PlayerDataRoot
assert(save_manager != null and player != null and data_root != null)
assert(save_manager.initialize_new_game(864209))
save_manager.set_autosave_enabled(true)
assert(player.wallet.restore_balance(4321))
assert(player.bag.add_item(&"coffee"))
assert(save_manager.save_now())
var save_path: String = str(save_manager.get("_save_path"))
assert(save_path.ends_with("player/player_save.nfsave"))
assert(FileAccess.file_exists(save_path))
assert(not FileAccess.file_exists(save_path.get_base_dir().path_join(
"player_save.json"
)))
_assert_opaque(save_path)
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
assert(bool(decoded.get("ok", false)))
assert(int((decoded["data"] as Dictionary)["save_version"]) == 10)
var archive_path: String = data_root.progression_backup_directory().path_join(
"progression-test.nfsave"
)
var exported: Dictionary = save_manager.export_progression_archive(
archive_path
)
assert(bool(exported.get("ok", false)))
assert(FileAccess.file_exists(archive_path))
_assert_opaque(archive_path)
var inspected: Dictionary = save_manager.inspect_progression_archive(
archive_path
)
assert(bool(inspected.get("ok", false)))
assert(int(inspected.get("wallet_balance", -1)) == 4321)
assert(int(inspected.get("world_seed", -1)) == 864209)
assert(player.wallet.restore_balance(7))
assert(save_manager.save_now())
var imported: Dictionary = save_manager.import_progression_archive(
archive_path
)
assert(bool(imported.get("ok", false)))
assert(save_manager.load_player_data())
assert(player.wallet.get_balance() == 4321)
assert(save_manager.get_world_seed() == 864209)
var legacy_data: Dictionary = (
ProgressionSaveCodec.read_local_save(save_path)["data"]
)
assert(save_manager.delete_progression_save())
var legacy_path: String = save_path.get_base_dir().path_join(
"player_save.json"
)
var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE)
assert(legacy_file != null)
legacy_file.store_string(JSON.stringify(legacy_data, "\t"))
legacy_file.close()
assert(save_manager.load_player_data())
assert(FileAccess.file_exists(save_path))
assert(not FileAccess.file_exists(legacy_path))
_assert_opaque(save_path)
var settings_panels: Array[Node] = main.find_children(
"*", "SettingsPanel", true, false
)
assert(settings_panels.size() == 2)
for settings_panel: SettingsPanel in settings_panels:
var export_button := settings_panel.get_node(
"%ExportProgression"
) as Button
var import_button := settings_panel.get_node(
"%ImportProgression"
) as Button
assert(export_button != null and import_button != null)
assert(not export_button.disabled and not import_button.disabled)
assert(
export_button.get_node(export_button.focus_neighbor_right)
== import_button
)
assert(
import_button.get_node(import_button.focus_neighbor_left)
== export_button
)
var migrated_root: String = data_root.root_path.get_base_dir().path_join(
"progression-migrated-data"
)
var data_migration: Dictionary = PortableDataMigration.migrate_active_to(
data_root, migrated_root
)
assert(bool(data_migration.get("ok", false)))
var migrated_save: String = migrated_root.path_join(
"player/player_save.nfsave"
)
assert(FileAccess.file_exists(migrated_save))
assert(bool(
ProgressionSaveCodec.read_local_save(migrated_save).get("ok", false)
))
main.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
print("Progression archive validation: PASS")
quit()
func _assert_opaque(path: String) -> void:
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
assert(not bytes.is_empty())
var encoded_hex: String = bytes.hex_encode()
assert("save_version".to_utf8_buffer().hex_encode() not in encoded_hex)
assert("wallet".to_utf8_buffer().hex_encode() not in encoded_hex)

View file

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