Prepare v0.6.6-alpha release

This commit is contained in:
Alexander Sellite 2026-08-11 20:02:54 -04:00
parent 96df168851
commit b044d35021
29 changed files with 568 additions and 134 deletions

View file

@ -9,7 +9,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.5-alpha/windows-x86_64/NETfishing.exe"
export_path="builds/v0.6.6-alpha/windows-x86_64/NETfishing.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -35,11 +35,11 @@ application/modify_resources=true
application/icon="res://art/exported/system_icons/netfishing.ico"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version="0.6.5.0"
application/product_version="0.6.5.0"
application/file_version="0.6.6.0"
application/product_version="0.6.6.0"
application/company_name=""
application/product_name="NETfishing"
application/file_description="NETfishing v0.6.5-alpha"
application/file_description="NETfishing v0.6.6-alpha"
application/copyright=""
application/trademarks=""
application/export_angle=0
@ -62,7 +62,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.5-alpha/linux-arm64/NETfishing.arm64"
export_path="builds/v0.6.6-alpha/linux-arm64/NETfishing.arm64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -92,7 +92,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.5-alpha/macos/NETfishing.zip"
export_path="builds/v0.6.6-alpha/macos/NETfishing.zip"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -108,8 +108,8 @@ custom_template/release=""
application/bundle_identifier="io.woofmeow.netfishing"
application/icon="res://art/exported/system_icons/netfishing_1024.png"
application/icon_interpolation=0
application/short_version="0.6.4"
application/version="0.6.4"
application/short_version="0.6.6"
application/version="0.6.6"
application/architecture="universal"
codesign/enable=false
notarization/enable=false
@ -125,7 +125,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*,tests/*"
export_path="builds/v0.6.5-alpha/server-linux-x86_64/NETfishingServer.x86_64"
export_path="builds/v0.6.6-alpha/server-linux-x86_64/NETfishingServer.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -155,7 +155,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.5-alpha/android/NETfishing.apk"
export_path="builds/v0.6.6-alpha/android/NETfishing.apk"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@ -176,8 +176,8 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=60500
version/name="v0.6.5-alpha"
version/code=60600
version/name="v0.6.6-alpha"
package/unique_name="io.woofmeow.netfishing"
package/name="NETfishing"
package/signed=true
@ -214,7 +214,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter="builds/*,playtest/*,scripts/*"
export_path="builds/v0.6.5-alpha/linux-x86_64/NETfishing.x86_64"
export_path="builds/v0.6.6-alpha/linux-x86_64/NETfishing.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""

View file

@ -431,7 +431,7 @@ func _draw_line_ribbon(points: PackedVector3Array) -> void:
return
var half_thickness: float = line_thickness * 0.5
_line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
var ribbon_vertices := PackedVector3Array()
for point_index: int in range(1, points.size()):
var start: Vector3 = points[point_index - 1]
var end: Vector3 = points[point_index]
@ -452,12 +452,17 @@ func _draw_line_ribbon(points: PackedVector3Array) -> void:
var start_right: Vector3 = to_local(start + side)
var end_right: Vector3 = to_local(end + side)
var end_left: Vector3 = to_local(end - side)
_line_mesh.surface_add_vertex(start_left)
_line_mesh.surface_add_vertex(start_right)
_line_mesh.surface_add_vertex(end_right)
_line_mesh.surface_add_vertex(start_left)
_line_mesh.surface_add_vertex(end_right)
_line_mesh.surface_add_vertex(end_left)
ribbon_vertices.append(start_left)
ribbon_vertices.append(start_right)
ribbon_vertices.append(end_right)
ribbon_vertices.append(start_left)
ribbon_vertices.append(end_right)
ribbon_vertices.append(end_left)
if ribbon_vertices.is_empty():
return
_line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
for vertex: Vector3 in ribbon_vertices:
_line_mesh.surface_add_vertex(vertex)
_line_mesh.surface_end()

View file

@ -1,12 +1,13 @@
NETfishing
v0.6.5-alpha
Alpha 0.6.5
v0.6.6-alpha
Alpha 0.6.6
Thank you for trying this early private playtest.
NETfishing is currently a local gameplay prototype for a future
multiplayer-first game. Networking and multiplayer are NOT implemented in
this build.
NETfishing is an early multiplayer fishing game. This alpha includes local
progression, player-hosted and dedicated multiplayer sessions, public room
discovery, and direct UDP connections. Expect rough edges and please keep a
backup of any save or identity data you care about.
INSTALLATION
@ -60,6 +61,9 @@ FISHING
FEATURES TO TRY
- Hosting a private or public room and joining compatible online sessions
- Direct-IP, saved-server, recent-server, and public-room connection flows
- Text chat, player calls, player lists, relationships, and moderation tools
- Movement, jumping, camera rotation, and zoom
- Short, medium, and maximum-distance casts
- Valid water and invalid land casts
@ -101,11 +105,11 @@ Please include:
- How movement, casting, chase pressure, barrier clicking, menus, and camera
controls felt
KNOWN PRE-ALPHA LIMITATIONS
KNOWN ALPHA LIMITATIONS
- Multiplayer/networking is not implemented.
- Fish and character art are temporary.
- UI and balance values are placeholders.
- There is no audio.
- Public sessions remain direct connections; restrictive NAT or carrier-grade
networks may prevent some players from joining.
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
- Some fish and character artwork remains temporary.
- There is no installer or automatic updater.
- Windows builds are unsigned.

View file

@ -11,7 +11,7 @@ config_version=5
[application]
config/name="NETFISHING"
config/version="0.6.5-alpha"
config/version="0.6.6-alpha"
run/main_scene="res://main/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
config/icon="res://art/exported/system_icons/netfishing_256.png"

View file

@ -4,12 +4,12 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.6.5-alpha"
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.6.6-alpha"
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
readonly README_SOURCE="${PROJECT_ROOT}/playtest/README-PLAYTEST.txt"
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.6.5-alpha-windows-x86_64.zip"
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.6.5-alpha-linux-x86_64.zip"
readonly WINDOWS_ZIP="${BUILD_ROOT}/NETfishing-v0.6.6-alpha-windows-x86_64.zip"
readonly LINUX_ZIP="${BUILD_ROOT}/NETfishing-v0.6.6-alpha-linux-x86_64.zip"
readonly GODOT_BIN="${GODOT_BIN:-godot}"
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then

View file

@ -109,6 +109,7 @@ func _run() -> void:
select_button.button_index = JOY_BUTTON_BACK
select_button.pressed = true
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
await process_frame
assert(chat_ui.is_open())
assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_STOP)
var typed_chat_entry := chat_ui.find_child(
@ -121,6 +122,8 @@ func _run() -> void:
accept_button.pressed = true
assert(bool(game_ui.call("_handle_controller_chat_controls", accept_button)))
assert(typed_chat_entry.virtual_keyboard_enabled)
await process_frame
assert(typed_chat_entry.has_focus())
var left_bumper := InputEventJoypadButton.new()
left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
left_bumper.pressed = true
@ -129,10 +132,8 @@ func _run() -> void:
assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_IGNORE)
assert(not typed_chat_entry.virtual_keyboard_enabled)
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
assert(chat_ui.is_open())
assert(not typed_chat_entry.virtual_keyboard_enabled)
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
assert(not chat_ui.is_open())
assert(not typed_chat_entry.virtual_keyboard_enabled)
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
assert(not chat_ui.is_open())
assert(chat_ui.is_collapsed())
@ -315,5 +316,7 @@ func _run() -> void:
var session := main.get_node("%NetworkSession") as NetworkSession
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -39,6 +39,15 @@ func _run() -> void:
standard_button.material != null,
"ordinary controller focus receives inversion",
)
var pointer_motion := InputEventMouseMotion.new()
pointer_motion.position = Vector2(4.0, 4.0)
presentation._input(pointer_motion)
await process_frame
_expect(
standard_button.material == null,
"mouse motion clears controller focus presentation",
)
presentation._input(controller_event)
authored_selector.grab_focus()
await process_frame

View file

@ -23,14 +23,45 @@ func _run() -> void:
var original := _make_button("cute", "cute (show variants)")
option_list.add_child(original)
await process_frame
original.grab_focus()
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"programmatic focus stays pending outside navigation mode",
)
var keyboard_navigation := InputEventKey.new()
keyboard_navigation.keycode = KEY_DOWN
keyboard_navigation.pressed = true
recovery._input(keyboard_navigation)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == original,
"keyboard navigation restores pending initial focus",
)
var menu_shortcut := InputEventKey.new()
menu_shortcut.physical_keycode = KEY_TAB
menu_shortcut.pressed = true
recovery._input(menu_shortcut)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"the menu shortcut does not leave a pseudo-focused option",
)
var controller_event := InputEventJoypadButton.new()
controller_event.button_index = JOY_BUTTON_A
controller_event.pressed = true
recovery._input(controller_event)
original.grab_focus()
await process_frame
_expect(root.gui_get_focus_owner() == original, "original option receives focus")
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == original,
"controller navigation restores pending initial focus",
)
option_list.remove_child(original)
original.queue_free()
@ -53,6 +84,63 @@ func _run() -> void:
root.gui_get_focus_owner() == explicit_target,
"explicit focus changes take precedence over recovery",
)
var pointer_motion := InputEventMouseMotion.new()
pointer_motion.position = Vector2(4.0, 4.0)
recovery._input(pointer_motion)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse motion releases controller focus",
)
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse motion prevents automatic focus recovery",
)
recovery._input(controller_event)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == explicit_target,
"controller navigation can resume from pointer mode",
)
var pointer_press := InputEventMouseButton.new()
pointer_press.button_index = MOUSE_BUTTON_LEFT
pointer_press.pressed = true
recovery._input(pointer_press)
await process_frame
_expect(
root.gui_get_focus_owner() == explicit_target,
"mouse-down preserves focus through button activation",
)
var pointer_release := InputEventMouseButton.new()
pointer_release.button_index = MOUSE_BUTTON_LEFT
pointer_release.pressed = false
recovery._input(pointer_release)
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"mouse-up clears the completed button focus",
)
recovery._input(controller_event)
await process_frame
await process_frame
var outside_button := _make_button("outside", "outside")
stage.add_child(outside_button)
option_list.hide()
root.gui_release_focus()
await process_frame
await process_frame
_expect(
root.gui_get_focus_owner() == null,
"focus recovery never escapes a hidden menu scope",
)
option_list.show()
explicit_target.grab_focus()
await process_frame
var leave_world_ui_event := InputEventJoypadButton.new()
leave_world_ui_event.button_index = JOY_BUTTON_LEFT_SHOULDER
leave_world_ui_event.pressed = true
@ -64,6 +152,14 @@ func _run() -> void:
root.gui_get_focus_owner() == null,
"LB intentionally leaving world UI is never recovered",
)
var accessibility_only := Control.new()
accessibility_only.focus_mode = Control.FOCUS_ACCESSIBILITY
stage.add_child(accessibility_only)
await process_frame
_expect(
not bool(recovery.call("_is_focusable", accessibility_only)),
"accessibility-only controls are not recovery targets",
)
stage.queue_free()
if _failures.is_empty():

View file

@ -127,7 +127,9 @@ func _run_multiplayer_host() -> void:
print("Economy multiplayer host validation: PASS")
session.disconnect_session("Economy host validation complete.")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -229,7 +231,9 @@ func _run_multiplayer_client() -> void:
print("Economy multiplayer client validation: PASS")
session.disconnect_session("Economy client validation complete.")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -408,7 +412,7 @@ func _test_player_menu_sale(
assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP)
assert(sell_action.focus_mode == Control.FOCUS_ALL)
assert(ui_viewport != null)
await _activate_focused_button(sell_action, ui_viewport)
await _activate_pointer_control(sell_action, ui_viewport)
await process_frame
assert(confirmation.visible)
assert(confirm_button.visible and not confirm_button.disabled)
@ -417,7 +421,7 @@ func _test_player_menu_sale(
> (player_menu.get_node("%CoolerOuterWall") as Control).z_index
)
_sale_result.clear()
await _activate_focused_button(confirm_button, ui_viewport)
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))
@ -436,18 +440,29 @@ func _test_player_menu_sale(
await create_timer(2.2).timeout
func _activate_focused_button(
button: Button,
func _activate_pointer_control(
control: Control,
ui_viewport: SubViewport,
) -> void:
button.grab_focus()
var presenter := ui_viewport.get_parent() as SubViewportContainer
assert(presenter != null)
var local_center: Vector2 = control.get_global_transform_with_canvas() * (
control.size * 0.5
)
var center: Vector2 = presenter.position + local_center * presenter.scale
var motion := InputEventMouseMotion.new()
motion.position = center
motion.global_position = center
root.push_input(motion, true)
await process_frame
assert(button.has_focus())
assert(ui_viewport.gui_get_hovered_control() == control)
for is_pressed: bool in [true, false]:
var accept := InputEventAction.new()
accept.action = &"ui_accept"
accept.pressed = is_pressed
ui_viewport.push_input(accept, false)
var click := InputEventMouseButton.new()
click.button_index = MOUSE_BUTTON_LEFT
click.position = center
click.global_position = center
click.pressed = is_pressed
root.push_input(click, true)
await process_frame
@ -560,14 +575,14 @@ func _test_fishing_shop_sale_ui(
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")
await _activate_focused_button(art_supplies_tab, ui_viewport)
await _activate_pointer_control(art_supplies_tab, ui_viewport)
await process_frame
var stock_sections: Array[String] = []
for child: Node in shop.get_node("%SuppliesList").get_children():
if child is Label:
stock_sections.append((child as Label).text)
assert(stock_sections == ["art kit", "markers", "brushes", "grids"])
await _activate_focused_button(sell_mode, ui_viewport)
await _activate_pointer_control(sell_mode, ui_viewport)
await process_frame
assert(shop.visible and not player_menu.visible)
assert(not shop.has_node("ShopPanel/Margin/Layout/Body/FishSales"))
@ -584,6 +599,11 @@ func _test_fishing_shop_sale_ui(
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
@ -594,10 +614,10 @@ func _test_fishing_shop_sale_ui(
var reserved_button := fish_nodes.get(reserved_catch.catch_id) as Button
assert(fish_button != null and fish_button.visible)
assert(reserved_button != null and reserved_button.visible)
await _activate_focused_button(fish_button, ui_viewport)
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_focused_button(sell_button, ui_viewport)
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
@ -607,14 +627,14 @@ func _test_fishing_shop_sale_ui(
> cooler_outer_wall.z_index
)
_sale_result.clear()
await _activate_focused_button(confirm_button, ui_viewport)
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(player.wallet.get_balance() == balance_before + fish_catch.sale_value)
assert(not sale_service.is_local_sale_pending())
assert(reservations.release(reservation_id))
await _activate_focused_button(shop_tabs[0] as Button, ui_viewport)
await _activate_pointer_control(shop_tabs[0] as Button, ui_viewport)
await process_frame
assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible)
assert(not (shop.get_node("%ShopCoolerPage") as Control).visible)

View file

@ -3,7 +3,6 @@ extends SceneTree
const MainScene = preload("res://main/main.tscn")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
func _initialize() -> void:
@ -40,7 +39,6 @@ func _run() -> void:
var service := main.get_node(
"%NetworkFishShowcaseService"
) as NetworkFishShowcaseService
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
assert(player != null and fish_catalog != null and service != null)
var fish: FishDataType = fish_catalog.get_fish_by_id(&"bluegill")
assert(fish != null)
@ -83,22 +81,18 @@ func _run() -> void:
assert(player.hotbar.get_selected_fish_catch_id() == fish_catch.catch_id)
assert(player.hotbar.get_selected_item_id().is_empty())
var press := InputEventMouseButton.new()
press.button_index = MOUSE_BUTTON_LEFT
press.pressed = true
fishing_spot.call("_unhandled_input", press)
await process_frame
assert(service.is_local_showcase_visible())
assert(service.get_local_showcase_catch_id() == fish_catch.catch_id)
assert((player.get_node("%HeldFishDisplay") as Node3D).visible)
fishing_spot.call("_unhandled_input", press)
await _wait_for_held_fish_visibility(player, true)
assert(service.toggle_selected_fish())
await process_frame
assert(not service.is_local_showcase_visible())
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
await _wait_for_held_fish_visibility(player, false)
var saved_weather: WorldWeatherService.Weather = (
world_weather.get_weather()
)
var saved_weather_seconds: float = world_weather.get_seconds_remaining()
var saved_time_hours: float = world_time.get_time_hours()
assert(save_manager.save_now())
var save_path: String = str(save_manager.get("_save_path"))
@ -116,7 +110,7 @@ func _run() -> void:
== 125
)
assert(absf(
float((parsed as Dictionary)["world"]["time_hours"]) - 19.75
float((parsed as Dictionary)["world"]["time_hours"]) - saved_time_hours
) < 0.01)
assert(
int((parsed as Dictionary)["world"]["weather"])
@ -149,7 +143,7 @@ func _run() -> void:
)
assert(save_manager.load_player_data())
assert(player.experience.get_total_experience() == 125)
assert(absf(world_time.get_time_hours() - 19.75) < 0.01)
assert(absf(world_time.get_time_hours() - saved_time_hours) < 0.01)
assert(world_weather.get_weather() == saved_weather)
assert(absf(
world_weather.get_seconds_remaining() - saved_weather_seconds
@ -163,14 +157,13 @@ func _run() -> void:
FishQuality.Tier.EXCEPTIONAL,
)
)
assert(not service.is_local_showcase_visible())
assert(service.toggle_selected_fish())
assert(service.is_local_showcase_visible())
await _wait_for_held_fish_visibility(player, true)
assert(player.inventory.remove_catch_by_id(fish_catch.catch_id) != null)
await process_frame
assert(player.hotbar.get_fish_catch_id(1).is_empty())
assert(not service.is_local_showcase_visible())
assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible)
await _wait_for_held_fish_visibility(player, false)
var valid_state: Dictionary = {
"session_id": "session",
@ -202,5 +195,26 @@ func _run() -> void:
print("Fish hotbar showcase validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
func _wait_for_held_fish_visibility(
player: Player,
expected_visible: bool,
) -> void:
var held_fish_display := player.get("_held_fish_display") as Node3D
assert(held_fish_display != null)
var deadline_msec: int = Time.get_ticks_msec() + 2000
while (
held_fish_display.visible != expected_visible
and Time.get_ticks_msec() < deadline_msec
):
await process_frame
assert(
held_fish_display.visible == expected_visible,
"Held fish display did not become %s within the timeout."
% expected_visible,
)

View file

@ -35,8 +35,8 @@ func _run_host() -> void:
var fish_catch: FishCatch = _add_bluegill(main, player)
assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
assert(player.hotbar.select_slot(1))
assert(service.toggle_selected_fish())
assert(service.is_local_showcase_visible())
await _wait_for_held_fish_visibility(player, true)
assert(session.set_host_open(true))
var remote_peer_id: int = 0
@ -66,7 +66,9 @@ func _run_host() -> void:
print("Fish showcase multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -118,8 +120,8 @@ func _run_client() -> void:
var fish_catch: FishCatch = _add_bluegill(main, player)
assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
assert(player.hotbar.select_slot(1))
assert(service.toggle_selected_fish())
assert((player.get("_held_fish_display") as Node3D).visible)
assert(service.is_local_showcase_visible())
await _wait_for_held_fish_visibility(player, true)
await create_timer(2.0).timeout
assert(service.toggle_selected_fish())
var held_display := player.get("_held_fish_display") as Node3D
@ -132,7 +134,9 @@ func _run_client() -> void:
print("Fish showcase multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -166,3 +170,18 @@ func _create_initialized_main() -> Node:
await process_frame
assert(bool(main.get("_application_initialized")))
return main
func _wait_for_held_fish_visibility(
player: Player,
expected_visible: bool,
) -> void:
var held_fish_display := player.get("_held_fish_display") as Node3D
assert(held_fish_display != null)
var deadline_msec: int = Time.get_ticks_msec() + 2000
while (
held_fish_display.visible != expected_visible
and Time.get_ticks_msec() < deadline_msec
):
await process_frame
assert(held_fish_display.visible == expected_visible)

View file

@ -36,8 +36,14 @@ func _run() -> void:
assert(not session.is_open_host())
assert(service != null and fishing_spot != null and player != null)
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
var pond := main.get_node(
"TestWorld/Regions/StarterIslandRegion/WaterBodies/Pond"
) as Node3D
var pond_region := pond.get_node("FishingRegion") as FishableWaterRegion
assert(pond != null and pond_region != null)
var pond_surface_y: float = pond_region.get_surface_height()
player.global_position = Vector3(-0.5, 3.95, 2.1)
player.global_position = pond.global_position + Vector3(8.9, 1.44, 0.0)
var visuals := player.get_node("Visuals") as Node3D
visuals.rotation.y = PI * 0.5
for _frame: int in 4:
@ -50,8 +56,8 @@ func _run() -> void:
fishing_spot.set("_cast_charge", 0.32)
fishing_spot.call("_update_cast_charge", 0.0)
var aimed_target: Vector3 = fishing_spot.get("_cast_target")
assert(aimed_target.x < -1.35)
assert(is_equal_approx(aimed_target.y, 2.51))
assert(aimed_target.x < player.global_position.x - 0.85)
assert(is_equal_approx(aimed_target.y, pond_surface_y))
assert(fishing_spot.is_target_fishable(aimed_target))
fishing_spot.call("_confirm_cast")
assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
@ -68,7 +74,7 @@ func _run() -> void:
var attempts: Dictionary = service.get("_attempts")
var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
assert(attempt != null)
assert(is_equal_approx(attempt.target.y, 2.51))
assert(is_equal_approx(attempt.target.y, pond_surface_y))
assert(attempt.bobber_position.is_equal_approx(attempt.target))
fishing_spot.set("_withdrawal_input_held", true)
@ -124,29 +130,42 @@ func _run() -> void:
fish,
)
assert(fish_catch != null and fish_catch.is_valid())
var baseline_controller := CatchController.new()
root.add_child(baseline_controller)
baseline_controller.start_authoritative_encounter(
var reference_controller := CatchController.new()
root.add_child(reference_controller)
reference_controller.start_authoritative_encounter(
fish.catch_profile,
attempt.reel_speed,
attempt.barrier_damage,
attempt.encounter_seed,
FishQuality.Tier.BORING,
fish_catch.quality,
int(fish.rarity),
fish.get_weight_percentile(fish_catch.weight_lb),
)
var quality_barriers: Array = attempt.controller.get("_barriers")
var baseline_barriers: Array = baseline_controller.get("_barriers")
assert(quality_barriers.size() == baseline_barriers.size())
var reference_barriers: Array = reference_controller.get("_barriers")
assert(quality_barriers.size() == reference_barriers.size())
for barrier_index: int in quality_barriers.size():
var quality_barrier: RefCounted = quality_barriers[barrier_index]
var baseline_barrier: RefCounted = baseline_barriers[barrier_index]
var reference_barrier: RefCounted = reference_barriers[barrier_index]
assert(
int(quality_barrier.get("maximum_health"))
== FishQuality.apply_barrier_health(
int(baseline_barrier.get("maximum_health")),
fish_catch.quality,
is_equal_approx(
float(quality_barrier.get("position")),
float(reference_barrier.get("position")),
)
)
baseline_controller.queue_free()
assert(
int(quality_barrier.get("maximum_health"))
== int(reference_barrier.get("maximum_health"))
)
assert(
int(quality_barrier.get("maximum_health"))
>= FishQuality.BARRIER_HEALTH_MINIMUMS[fish_catch.quality]
)
assert(
int(quality_barrier.get("maximum_health"))
<= FishQuality.BARRIER_HEALTH_MAXIMUMS[fish_catch.quality]
)
reference_controller.queue_free()
service.call("_cancel_attempt", session.get_local_peer_id(), "")
await process_frame
assert(not service.has_local_attempt())
@ -154,5 +173,7 @@ func _run() -> void:
print("Fishing authority validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -150,7 +150,9 @@ func _run_host() -> void:
print("Fishing multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -325,7 +327,9 @@ func _run_client() -> void:
print("Fishing multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -266,11 +266,47 @@ func _validate_bite_wait_distribution() -> void:
if wait_seconds >= FishingSpotType.BITE_LONG_MAX_SECONDS:
very_long_count += 1
var average_wait_seconds: float = total_wait_seconds / 10000.0
assert(quick_count >= 2300 and quick_count <= 2700)
var quick_ratio: float = float(quick_count) / 10000.0
var very_long_ratio: float = float(very_long_count) / 10000.0
var very_long_probability: float = 1.0 - (
FishingSpotType.BITE_QUICK_PROBABILITY
+ FishingSpotType.BITE_TYPICAL_PROBABILITY
+ FishingSpotType.BITE_LONG_PROBABILITY
)
var expected_average_wait_seconds: float = (
FishingSpotType.BITE_QUICK_PROBABILITY
* (
FishingSpotType.BITE_QUICK_MIN_SECONDS
+ FishingSpotType.BITE_QUICK_MAX_SECONDS
)
* 0.5
+ FishingSpotType.BITE_TYPICAL_PROBABILITY
* (
FishingSpotType.BITE_QUICK_MAX_SECONDS
+ FishingSpotType.BITE_TYPICAL_MAX_SECONDS
)
* 0.5
+ FishingSpotType.BITE_LONG_PROBABILITY
* (
FishingSpotType.BITE_TYPICAL_MAX_SECONDS
+ FishingSpotType.BITE_LONG_MAX_SECONDS
)
* 0.5
+ very_long_probability
* (
FishingSpotType.BITE_LONG_MAX_SECONDS
+ FishingSpotType.BITE_MAX_SECONDS
)
* 0.5
)
assert(absf(
quick_ratio - FishingSpotType.BITE_QUICK_PROBABILITY
) <= 0.02)
assert(typical_or_long_count > quick_count)
assert(very_long_count >= 100 and very_long_count <= 300)
assert(average_wait_seconds >= 45.0)
assert(average_wait_seconds <= 49.0)
assert(absf(very_long_ratio - very_long_probability) <= 0.005)
assert(absf(
average_wait_seconds - expected_average_wait_seconds
) <= 1.0)
fishing_spot.queue_free()
await process_frame
@ -305,11 +341,16 @@ func _validate_remote_presentation() -> void:
fish_catch.sale_value = 1
assert(fish_catch.is_valid())
presentation.play_return(fish_catch)
await create_timer(0.6).timeout
var catch_display := player.find_child(
"CatchDisplay", true, false
) as Node3D
assert(catch_display != null)
var showcase_deadline_msec: int = Time.get_ticks_msec() + 2000
while (
not catch_display.visible
and Time.get_ticks_msec() < showcase_deadline_msec
):
await process_frame
assert(catch_display.visible)
await presentation.return_completed
assert(not catch_display.visible)

View file

@ -62,7 +62,9 @@ func _run() -> void:
await _capture_inventory_pages(player_menu)
print("Inventory notepad artwork validation: PASS")
presentation_stage.queue_free()
await process_frame
for _frame: int in 10:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -56,7 +56,9 @@ func _run_host() -> void:
print("Job multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -93,7 +95,9 @@ func _run_client() -> void:
print("Job multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -79,10 +79,15 @@ func _run() -> void:
"_show_section_immediate", PlayerMenu.Section.PROFILE
)
await process_frame
assert(not bool(
assert(bool(
player_menu.call("_handle_direct_page_shortcut", shortcut)
))
await create_timer(0.5).timeout
assert(player_menu.visible)
assert(logbook.visible)
assert(
player_menu.get("_current_section") == PlayerMenu.Section.LOGBOOK
)
player_menu.close_menu(PlayerMenu.CloseReason.TEARDOWN, false)
print(
@ -90,7 +95,8 @@ func _run() -> void:
% [root.size.x, root.size.y]
)
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
quit()

View file

@ -171,7 +171,7 @@ func _validate_page() -> void:
page.activate()
await process_frame
assert(page.get("_category") == WaterType.Type.FRESH_WATER)
assert((page.get("_catalog_grid") as GridContainer).columns == 5)
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
var initial_detail_body := page.get("_detail_body") as VBoxContainer
assert(not initial_detail_body.get_parent() is ScrollContainer)
assert(
@ -209,7 +209,7 @@ func _validate_page() -> void:
assert(portrait.source_texture == fish.display_texture)
assert(
portrait.custom_minimum_size
== LogbookPortraitType.ENTRY_FRAME_SIZE
== LogbookPage.CATALOG_PORTRAIT_SIZE
)
assert(
portrait.expand_mode

View file

@ -12,6 +12,14 @@ func _run() -> void:
root.add_child(game_ui)
await process_frame
game_ui.set("_gameplay_ui_enabled", true)
var panel := game_ui.get_node("%ExperienceProgressPanel") as PanelContainer
var bubble := game_ui.get_node("%ExperienceBubble") as PanelContainer
var bubble_label := game_ui.get_node("%ExperienceBubbleLabel") as Label
var award_label := game_ui.get_node("%ExperienceAwardLabel") as Label
assert(panel != null and bubble != null)
assert(not panel.visible and not bubble.visible)
game_ui.call("_on_showcase_changed", "bluegill", "common", 1.0, 0, true)
await process_frame
game_ui.call(
"_on_experience_awarded",
50,
@ -20,13 +28,6 @@ func _run() -> void:
1,
1,
)
var panel := game_ui.get_node("%ExperienceProgressPanel") as PanelContainer
var bubble := game_ui.get_node("%ExperienceBubble") as PanelContainer
var bubble_label := game_ui.get_node("%ExperienceBubbleLabel") as Label
var award_label := game_ui.get_node("%ExperienceAwardLabel") as Label
assert(panel != null and bubble != null)
assert(not panel.visible and not bubble.visible)
game_ui.call("_on_showcase_changed", "bluegill", "common", 1.0, 0, true)
await process_frame
assert(not panel.visible and not bubble.visible)
game_ui.call("_on_showcase_changed", "", "", 0.0, 0, false)
@ -43,5 +44,8 @@ func _run() -> void:
await create_timer(1.2).timeout
assert(not panel.visible and not bubble.visible)
game_ui.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
print("Player experience UI validation: PASS")
quit()

View file

@ -114,7 +114,9 @@ func _run_host() -> void:
print("Surface drawing multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -207,7 +209,9 @@ func _run_client() -> void:
print("Surface drawing multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -110,7 +110,9 @@ func _run() -> void:
print("Surface drawing runtime validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -21,6 +21,8 @@ func _run() -> void:
await _validate_blocked_author_visibility()
_validate_peer_capability_tracking()
print("Surface drawing validation: PASS")
for _frame: int in 4:
await process_frame
quit()
@ -118,6 +120,7 @@ func _validate_art_unlocks() -> void:
assert(unlocks.is_brush_size_unlocked(4))
assert(unlocks.is_grid_size_unlocked(128))
assert(not unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK + 1))
unlocks.free()
func _validate_grid_snapping() -> void:
@ -265,6 +268,7 @@ func _validate_blocked_author_visibility() -> void:
})
var world := Node3D.new()
root.add_child(world)
world.add_child(relationships)
var canvas := SurfaceDrawingCanvas.new()
world.add_child(canvas)
var state: Dictionary = {

View file

@ -205,6 +205,8 @@ func _run() -> void:
presenter.queue_free()
root.size = original_size
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
print("UI scaling runtime validation: PASS")
quit()

View file

@ -83,7 +83,9 @@ func _run_host() -> void:
print("World time multiplayer host validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()
@ -195,7 +197,9 @@ func _run_client() -> void:
print("World time multiplayer client validation: PASS")
session.disconnect_session("")
main.queue_free()
await process_frame
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()

View file

@ -41,6 +41,8 @@ func _input(event: InputEvent) -> void:
CONTROLLER_MOTION_THRESHOLD
):
_set_controller_active(true)
elif event is InputEventMouseMotion:
_set_controller_active(false)
elif event is InputEventMouseButton:
if (event as InputEventMouseButton).pressed:
_set_controller_active(false)

View file

@ -3,11 +3,28 @@ extends Node
const CONTROLLER_AXIS_THRESHOLD: float = 0.35
const SEMANTIC_MATCH_BONUS: float = 1000000.0
const KEYBOARD_NAVIGATION_ACTIONS: Array[StringName] = [
&"ui_accept",
&"ui_cancel",
&"ui_up",
&"ui_down",
&"ui_left",
&"ui_right",
&"ui_focus_next",
&"ui_focus_prev",
&"ui_page_up",
&"ui_page_down",
&"ui_home",
&"ui_end",
]
var _controller_active: bool = false
var _focus_navigation_active: bool = false
var _last_focus_center: Vector2 = Vector2.ZERO
var _last_focus_key: String = ""
var _scope_chain: Array[WeakRef] = []
var _pending_focus: WeakRef
var _pointer_button_down: bool = false
var _recovery_generation: int = 0
@ -27,23 +44,51 @@ func _input(event: InputEvent) -> void:
if event is InputEventJoypadButton:
var button_event := event as InputEventJoypadButton
if button_event.pressed:
_pointer_button_down = false
_controller_active = true
_focus_navigation_active = true
if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER:
_recovery_generation += 1
_scope_chain.clear()
_pending_focus = null
return
_request_pending_focus()
return
if event is InputEventJoypadMotion:
if absf((event as InputEventJoypadMotion).axis_value) >= (
CONTROLLER_AXIS_THRESHOLD
):
_pointer_button_down = false
_controller_active = true
_focus_navigation_active = true
_request_pending_focus()
return
if event is InputEventMouseMotion:
_pointer_button_down = (
(event as InputEventMouseMotion).button_mask != 0
)
_leave_focus_navigation(not _pointer_button_down)
return
if event is InputEventMouseButton:
if (event as InputEventMouseButton).pressed:
_controller_active = false
var mouse_button := event as InputEventMouseButton
if mouse_button.button_index in [
MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT,
MOUSE_BUTTON_MIDDLE,
]:
_pointer_button_down = mouse_button.pressed
_leave_focus_navigation(false)
if not mouse_button.pressed:
_release_current_pointer_focus.call_deferred()
return
if event is InputEventKey and (event as InputEventKey).pressed:
_pointer_button_down = false
_controller_active = false
if _is_keyboard_navigation_event(event):
_focus_navigation_active = true
_request_pending_focus()
else:
_leave_focus_navigation()
func _process(_delta: float) -> void:
@ -58,8 +103,16 @@ func _process(_delta: float) -> void:
func _on_gui_focus_changed(control: Control) -> void:
_recovery_generation += 1
if control != null:
if _controller_active and control is BaseButton:
_remember_focus(control)
if _focus_navigation_active:
_pending_focus = null
if _controller_active and control is BaseButton:
_remember_focus(control)
return
if _keeps_pointer_focus(control):
return
if _is_focusable(control):
_pending_focus = weakref(control)
_release_focus_if_inactive.call_deferred(control)
return
if not _controller_active or _scope_chain.is_empty():
return
@ -92,13 +145,18 @@ func _recover_focus(generation: int) -> void:
scope == null
or not is_instance_valid(scope)
or not scope.is_inside_tree()
or not scope.is_visible_in_tree()
):
continue
if not scope.is_visible_in_tree():
_scope_chain.clear()
return
var replacement := _best_replacement_in(scope)
if replacement != null:
replacement.grab_focus()
return
else:
_scope_chain.clear()
return
_scope_chain.clear()
func _best_replacement_in(scope: Control) -> Control:
@ -124,7 +182,7 @@ func _is_focusable(control: Control) -> bool:
control == null
or not control.is_inside_tree()
or not control.is_visible_in_tree()
or control.focus_mode == Control.FOCUS_NONE
or control.focus_mode not in [Control.FOCUS_CLICK, Control.FOCUS_ALL]
):
return false
var button := control as BaseButton
@ -140,3 +198,80 @@ func _semantic_key(control: Control) -> String:
tooltip = tooltip.trim_suffix(" (show variants)")
tooltip = tooltip.trim_suffix(" (hide variants)")
return "%s|%s|%s" % [control.get_class(), label, tooltip]
func _leave_focus_navigation(release_focus: bool = true) -> void:
_controller_active = false
_focus_navigation_active = false
_recovery_generation += 1
_scope_chain.clear()
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner == null or _keeps_pointer_focus(focus_owner):
return
if _is_focusable(focus_owner):
_pending_focus = weakref(focus_owner)
if release_focus:
_release_focus_if_inactive.call_deferred(focus_owner)
func _release_focus_if_inactive(control: Control) -> void:
if (
_focus_navigation_active
or _pointer_button_down
or control == null
or not is_instance_valid(control)
or get_viewport().gui_get_focus_owner() != control
or _keeps_pointer_focus(control)
):
return
get_viewport().gui_release_focus()
func _release_current_pointer_focus() -> void:
var focus_owner: Control = get_viewport().gui_get_focus_owner()
if focus_owner != null:
_release_focus_if_inactive(focus_owner)
func _request_pending_focus() -> void:
if (
not _focus_navigation_active
or _pending_focus == null
or get_viewport().gui_get_focus_owner() != null
):
return
_restore_pending_focus.call_deferred(_recovery_generation)
func _restore_pending_focus(generation: int) -> void:
if (
generation != _recovery_generation
or not _focus_navigation_active
or _pending_focus == null
or get_viewport().gui_get_focus_owner() != null
):
return
var target := _pending_focus.get_ref() as Control
if not _is_focusable(target):
_pending_focus = null
return
_pending_focus = null
target.grab_focus()
func _is_keyboard_navigation_event(event: InputEvent) -> bool:
if event.is_action_pressed(&"open_backpack"):
return false
if (
event.is_action_pressed(&"ui_cancel")
and get_viewport().gui_get_focus_owner() == null
):
return false
for action: StringName in KEYBOARD_NAVIGATION_ACTIONS:
if event.is_action_pressed(action):
return true
return false
func _keeps_pointer_focus(control: Control) -> bool:
return control is LineEdit or control is TextEdit

View file

@ -74,6 +74,8 @@ const SUPPLY_PRICE_HORIZONTAL_PADDING: float = 12.0
@onready var _wallet_label: Label = %WalletLabel
@onready var _shop_panel: PanelContainer = %ShopPanel
@onready var _shop_panel_margin: MarginContainer = $ShopPanel/Margin
@onready var _shop_panel_layout: VBoxContainer = $ShopPanel/Margin/Layout
@onready var _shop_cooler_page: Control = %ShopCoolerPage
@onready var _shop_cooler_mount: Control = %ShopCoolerMount
@onready var _shop_body: HBoxContainer = $ShopPanel/Margin/Layout/Body
@ -367,6 +369,7 @@ func get_shop_cooler_mount() -> Control:
func activate_shop_cooler_page() -> void:
_cooler_page_active = true
_shop_panel.show()
_set_shop_panel_background_pointer_blocking(false)
_shop_body.hide()
_feedback.hide()
_shop_cooler_page.show()
@ -377,6 +380,7 @@ func deactivate_shop_cooler_page() -> void:
_shop_tab_bar.show()
_cooler_page_active = false
_cooler_modal_open = false
_set_shop_panel_background_pointer_blocking(true)
for tab: OrganizerTab in _shop_tabs:
tab.disabled = false
_shop_cooler_page.hide()
@ -385,6 +389,17 @@ func deactivate_shop_cooler_page() -> void:
_feedback.show()
func _set_shop_panel_background_pointer_blocking(blocking: bool) -> void:
var mouse_filter := (
Control.MOUSE_FILTER_STOP
if blocking
else Control.MOUSE_FILTER_IGNORE
)
_shop_panel.mouse_filter = mouse_filter
_shop_panel_margin.mouse_filter = mouse_filter
_shop_panel_layout.mouse_filter = mouse_filter
func set_shop_cooler_modal_open(is_open: bool) -> void:
_cooler_modal_open = is_open
for tab: OrganizerTab in _shop_tabs:

View file

@ -351,7 +351,10 @@ func _open_confirmation(
else BubbleConfirmationPageType.InitialFocus.CONFIRM
)
)
_begin_root_exit(_show_confirmation)
# Confirmation actions reject input while the root page is still leaving.
# Finish that exit before exposing an interactive confirmation page so a
# pointer click can never land in the overlap window and be discarded.
_begin_root_exit(_show_confirmation, false)
func _show_confirmation() -> void:
@ -450,7 +453,10 @@ func _finish_root_entry(generation: int) -> void:
_root_transition_active = false
func _begin_root_exit(completed: Callable) -> void:
func _begin_root_exit(
completed: Callable,
overlap_next_page: bool = true,
) -> void:
if _root_transition_active or not _root_page.visible:
return
_root_transition_generation += 1
@ -458,9 +464,14 @@ func _begin_root_exit(completed: Callable) -> void:
_emit_transition_flurry()
var generation: int = _root_transition_generation
_root_page.transition_out(
_finish_root_exit.bind(generation),
_finish_root_exit.bind(
generation,
Callable() if overlap_next_page else completed,
),
0.0
)
if not overlap_next_page:
return
await get_tree().create_timer(
UIMotion.BUBBLE_TRANSITION_OVERLAP_DELAY
).timeout
@ -471,10 +482,13 @@ func _begin_root_exit(completed: Callable) -> void:
func _finish_root_exit(
generation: int,
completed: Callable,
) -> void:
if generation != _root_transition_generation or not visible:
return
_root_transition_active = false
if completed.is_valid():
completed.call()
func _finish_user_close() -> void:

View file

@ -209,7 +209,7 @@ unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
theme_override_font_sizes/font_size = 22
text = "v0.6.5-alpha"
text = "v0.6.6-alpha"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]