2026-08-02 00:45:07 -04:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
|
|
|
|
const TEST_PORT: int = 18136
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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")))
|
|
|
|
|
assert(bool(main.call("_prepare_private_host")))
|
|
|
|
|
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 player := main.get("_player") as Player
|
2026-08-21 12:29:51 -04:00
|
|
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
2026-08-13 14:23:11 -04:00
|
|
|
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
|
|
|
|
assert(sprint_dust != null)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 0)
|
|
|
|
|
sprint_dust.update_trail(
|
|
|
|
|
0.016,
|
|
|
|
|
Vector3.ZERO,
|
|
|
|
|
Vector3(4.5, 0.0, 0.0),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 0)
|
|
|
|
|
sprint_dust.update_trail(
|
|
|
|
|
0.016,
|
|
|
|
|
Vector3(0.5, 0.0, 0.0),
|
|
|
|
|
Vector3(4.5, 0.0, 0.0),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 1)
|
|
|
|
|
var planted_puff_position: Vector3 = (
|
|
|
|
|
sprint_dust.get_first_active_puff_position()
|
|
|
|
|
)
|
|
|
|
|
sprint_dust.update_trail(
|
|
|
|
|
0.08,
|
|
|
|
|
Vector3(0.75, 0.0, 0.0),
|
|
|
|
|
Vector3(4.5, 0.0, 0.0),
|
|
|
|
|
true,
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
sprint_dust.get_first_active_puff_position()
|
|
|
|
|
== planted_puff_position
|
|
|
|
|
)
|
|
|
|
|
sprint_dust.update_trail(
|
|
|
|
|
0.08,
|
|
|
|
|
Vector3(0.75, 0.0, 0.0),
|
|
|
|
|
Vector3.ZERO,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 1)
|
|
|
|
|
sprint_dust.clear_trail()
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 0)
|
|
|
|
|
sprint_dust.emit_landing_burst(Vector3.ZERO, Vector3.FORWARD)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 5)
|
|
|
|
|
sprint_dust.update_trail(
|
|
|
|
|
0.08,
|
|
|
|
|
Vector3.ZERO,
|
|
|
|
|
Vector3.ZERO,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
assert(sprint_dust.get_active_puff_count() == 5)
|
|
|
|
|
sprint_dust.clear_trail()
|
2026-08-02 00:45:07 -04:00
|
|
|
var service := main.get_node(
|
|
|
|
|
"%NetworkSurfaceDrawingService"
|
|
|
|
|
) as NetworkSurfaceDrawingService
|
|
|
|
|
var game_ui := main.get_node("%GameUI") as GameUI
|
|
|
|
|
var toolbar := game_ui.get_node(
|
|
|
|
|
"%SurfaceDrawingToolbar"
|
|
|
|
|
) as SurfaceDrawingToolbar
|
2026-08-02 09:20:44 -04:00
|
|
|
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(player != null and service != null and toolbar != null)
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(chat_ui != null)
|
2026-08-20 17:53:22 -04:00
|
|
|
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.",
|
|
|
|
|
)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(bool(game_ui.call("_can_start_virtual_mouse")))
|
2026-08-19 10:02:28 -04:00
|
|
|
var virtual_pointer_states: Array[bool] = []
|
|
|
|
|
game_ui.virtual_pointer_mode_changed.connect(
|
|
|
|
|
func(active: bool) -> void: virtual_pointer_states.append(active)
|
|
|
|
|
)
|
2026-08-03 20:49:19 -04:00
|
|
|
game_ui.call("_begin_virtual_mouse", 0)
|
|
|
|
|
var virtual_cursor := game_ui.get_node(
|
|
|
|
|
"%ControllerVirtualCursor"
|
|
|
|
|
) as ControllerVirtualCursor
|
|
|
|
|
assert(virtual_cursor.visible)
|
2026-08-19 10:02:28 -04:00
|
|
|
assert(virtual_pointer_states == [true])
|
|
|
|
|
# PopupMenu subwindows can consume the physical shoulder release. The
|
|
|
|
|
# polled primary-button state must still release the synthetic mouse click.
|
|
|
|
|
game_ui.set("_virtual_mouse_button_mask", MOUSE_BUTTON_MASK_LEFT)
|
|
|
|
|
game_ui.call("_sync_virtual_mouse_primary_button", false)
|
|
|
|
|
assert(int(game_ui.get("_virtual_mouse_button_mask")) == 0)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(not player.is_camera_input_enabled())
|
|
|
|
|
game_ui.call("_end_virtual_mouse")
|
|
|
|
|
assert(not virtual_cursor.visible)
|
2026-08-19 10:02:28 -04:00
|
|
|
assert(virtual_pointer_states == [true, false])
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(player.is_camera_input_enabled())
|
2026-08-02 09:20:44 -04:00
|
|
|
var settings_panel := game_ui.get(
|
|
|
|
|
"_pause_settings_panel"
|
|
|
|
|
) as SettingsPanel
|
2026-08-16 17:08:19 -04:00
|
|
|
var chat_dock_selector := settings_panel.get_node(
|
|
|
|
|
"%ChatDockSelector"
|
|
|
|
|
) as OptionButton
|
|
|
|
|
var chat_mode_selector := settings_panel.get_node(
|
|
|
|
|
"%ChatModeSelector"
|
|
|
|
|
) as OptionButton
|
|
|
|
|
var paint_dock_selector := settings_panel.get_node(
|
|
|
|
|
"%PaintDockSelector"
|
|
|
|
|
) as OptionButton
|
|
|
|
|
assert(chat_dock_selector != null)
|
|
|
|
|
assert(chat_mode_selector != null)
|
|
|
|
|
assert(paint_dock_selector != null)
|
|
|
|
|
assert(chat_dock_selector.custom_minimum_size.x == 300.0)
|
|
|
|
|
assert(chat_mode_selector.custom_minimum_size.x == 300.0)
|
|
|
|
|
assert(paint_dock_selector.custom_minimum_size.x == 300.0)
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(
|
2026-08-16 17:08:19 -04:00
|
|
|
settings_panel.find_children(
|
|
|
|
|
"*", "BubbleButton", true, false
|
|
|
|
|
).is_empty()
|
2026-08-02 09:20:44 -04:00
|
|
|
)
|
|
|
|
|
assert(not chat_ui.is_docked_right())
|
|
|
|
|
assert(toolbar.is_docked_right())
|
|
|
|
|
var settings_manager := main.get_node(
|
|
|
|
|
"%PlayerSettingsManager"
|
|
|
|
|
) as PlayerSettingsManager
|
|
|
|
|
assert(settings_manager != null)
|
|
|
|
|
var changed_docks: PlayerSettings = settings_manager.current_settings.copy()
|
|
|
|
|
changed_docks.chat_dock_right = true
|
2026-08-03 20:49:19 -04:00
|
|
|
changed_docks.chat_mobile_mode = true
|
2026-08-02 09:20:44 -04:00
|
|
|
changed_docks.paint_dock_right = false
|
|
|
|
|
assert(settings_manager.apply_settings(changed_docks))
|
|
|
|
|
await process_frame
|
2026-08-19 10:02:28 -04:00
|
|
|
assert(not chat_ui.is_docked_right())
|
|
|
|
|
assert(not chat_ui.is_mobile_mode())
|
|
|
|
|
assert(toolbar.is_docked_right())
|
|
|
|
|
changed_docks.presentation_layout_customized = true
|
|
|
|
|
assert(settings_manager.apply_settings(changed_docks))
|
|
|
|
|
await process_frame
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(chat_ui.is_docked_right())
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(chat_ui.is_mobile_mode())
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(not toolbar.is_docked_right())
|
2026-08-03 20:49:19 -04:00
|
|
|
var chat_panel := chat_ui.get_node("ChatPanel") as PanelContainer
|
|
|
|
|
var chat_height_button := chat_ui.get_node("ChatHeightButton") as Button
|
2026-08-13 01:31:05 -04:00
|
|
|
var chat_status := chat_ui.find_child(
|
|
|
|
|
"ChatStatus", true, false
|
|
|
|
|
) as Label
|
|
|
|
|
assert(chat_status != null)
|
|
|
|
|
assert(
|
|
|
|
|
chat_status.autowrap_mode
|
|
|
|
|
== TextServer.AUTOWRAP_WORD_SMART
|
|
|
|
|
)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(is_zero_approx(chat_panel.position.y))
|
|
|
|
|
assert(is_equal_approx(chat_panel.size.x, ChatUI.MOBILE_COMPACT_WIDTH))
|
|
|
|
|
assert(not chat_height_button.visible)
|
2026-08-02 09:20:44 -04:00
|
|
|
var reloaded_settings := PlayerSettingsManager.new()
|
|
|
|
|
root.add_child(reloaded_settings)
|
|
|
|
|
assert(reloaded_settings.load_settings())
|
|
|
|
|
assert(reloaded_settings.current_settings.chat_dock_right)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(reloaded_settings.current_settings.chat_mobile_mode)
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(not reloaded_settings.current_settings.paint_dock_right)
|
2026-08-19 10:02:28 -04:00
|
|
|
assert(reloaded_settings.current_settings.presentation_layout_customized)
|
2026-08-02 09:20:44 -04:00
|
|
|
reloaded_settings.queue_free()
|
2026-08-19 10:02:28 -04:00
|
|
|
var legacy_data: Dictionary = JSON.parse_string(
|
|
|
|
|
FileAccess.get_file_as_string(PlayerSettingsManager.SETTINGS_PATH)
|
|
|
|
|
) as Dictionary
|
|
|
|
|
var legacy_presentation := legacy_data.get("presentation", {}) as Dictionary
|
|
|
|
|
legacy_presentation.erase("layout_customized")
|
|
|
|
|
legacy_data["presentation"] = legacy_presentation
|
|
|
|
|
var legacy_file := FileAccess.open(
|
|
|
|
|
PlayerSettingsManager.SETTINGS_PATH,
|
|
|
|
|
FileAccess.WRITE,
|
|
|
|
|
)
|
|
|
|
|
assert(legacy_file != null)
|
|
|
|
|
legacy_file.store_string(JSON.stringify(legacy_data, "\t"))
|
|
|
|
|
legacy_file.close()
|
|
|
|
|
var migrated_legacy_settings := PlayerSettingsManager.new()
|
|
|
|
|
root.add_child(migrated_legacy_settings)
|
|
|
|
|
assert(migrated_legacy_settings.load_settings())
|
|
|
|
|
assert(not migrated_legacy_settings.current_settings.chat_dock_right)
|
|
|
|
|
assert(not migrated_legacy_settings.current_settings.chat_mobile_mode)
|
|
|
|
|
assert(migrated_legacy_settings.current_settings.paint_dock_right)
|
|
|
|
|
assert(
|
|
|
|
|
not migrated_legacy_settings.current_settings.presentation_layout_customized
|
|
|
|
|
)
|
|
|
|
|
migrated_legacy_settings.queue_free()
|
2026-08-02 09:20:44 -04:00
|
|
|
var default_docks: PlayerSettings = settings_manager.current_settings.copy()
|
|
|
|
|
default_docks.chat_dock_right = false
|
2026-08-03 20:49:19 -04:00
|
|
|
default_docks.chat_mobile_mode = false
|
2026-08-02 09:20:44 -04:00
|
|
|
default_docks.paint_dock_right = true
|
|
|
|
|
assert(settings_manager.apply_settings(default_docks))
|
|
|
|
|
await process_frame
|
2026-08-13 01:31:05 -04:00
|
|
|
chat_ui.call(
|
|
|
|
|
"_set_status",
|
|
|
|
|
(
|
|
|
|
|
"Unknown command: /this-command-name-is-intentionally-long-"
|
|
|
|
|
+ "enough-to-require-smart-character-wrapping"
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(is_equal_approx(chat_panel.size.x, ChatUI.PANEL_WIDTH))
|
|
|
|
|
assert(chat_status.size.x <= ChatUI.PANEL_WIDTH)
|
|
|
|
|
chat_ui.call("_set_status", "")
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(not chat_ui.is_docked_right())
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(not chat_ui.is_mobile_mode())
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(toolbar.is_docked_right())
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(chat_height_button.visible)
|
|
|
|
|
var select_button := InputEventJoypadButton.new()
|
|
|
|
|
select_button.button_index = JOY_BUTTON_BACK
|
|
|
|
|
select_button.pressed = true
|
|
|
|
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
2026-08-11 20:02:54 -04:00
|
|
|
await process_frame
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(chat_ui.is_open())
|
|
|
|
|
assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_STOP)
|
|
|
|
|
var typed_chat_entry := chat_ui.find_child(
|
|
|
|
|
"ChatEntry", true, false
|
|
|
|
|
) as LineEdit
|
|
|
|
|
assert(typed_chat_entry != null)
|
|
|
|
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
|
|
|
|
var accept_button := InputEventJoypadButton.new()
|
|
|
|
|
accept_button.button_index = JOY_BUTTON_A
|
|
|
|
|
accept_button.pressed = true
|
|
|
|
|
assert(bool(game_ui.call("_handle_controller_chat_controls", accept_button)))
|
2026-08-15 09:53:39 -04:00
|
|
|
var ui_pixelation := main.get("_ui_pixelation") as UIPixelationPresenter
|
|
|
|
|
assert(ui_pixelation != null)
|
|
|
|
|
var on_screen_keyboard := ui_pixelation.get(
|
|
|
|
|
"_on_screen_keyboard"
|
|
|
|
|
) as OnScreenKeyboard
|
|
|
|
|
assert(on_screen_keyboard != null)
|
|
|
|
|
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
|
|
|
|
assert(typed_chat_entry.virtual_keyboard_enabled)
|
|
|
|
|
assert(not on_screen_keyboard.is_open())
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(typed_chat_entry.has_focus())
|
|
|
|
|
else:
|
|
|
|
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
|
|
|
|
assert(on_screen_keyboard.is_open())
|
|
|
|
|
on_screen_keyboard.call("_close_keyboard", true)
|
|
|
|
|
assert(typed_chat_entry.has_focus())
|
2026-08-03 20:49:19 -04:00
|
|
|
var left_bumper := InputEventJoypadButton.new()
|
|
|
|
|
left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
|
|
|
|
|
left_bumper.pressed = true
|
|
|
|
|
assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
|
|
|
|
|
assert(not chat_ui.is_open())
|
|
|
|
|
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(not chat_ui.is_open())
|
2026-08-11 20:02:54 -04:00
|
|
|
assert(not typed_chat_entry.virtual_keyboard_enabled)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
|
|
|
|
assert(not chat_ui.is_open())
|
|
|
|
|
assert(chat_ui.is_collapsed())
|
|
|
|
|
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
|
|
|
|
|
assert(chat_ui.is_open())
|
|
|
|
|
chat_ui.refocus_gameplay()
|
2026-08-16 16:40:10 -04:00
|
|
|
chat_ui.open_chat()
|
|
|
|
|
assert(chat_ui.is_open())
|
2026-08-18 21:59:34 -04:00
|
|
|
typed_chat_entry.text = "validation message"
|
2026-08-16 16:40:10 -04:00
|
|
|
chat_ui.call("_send")
|
|
|
|
|
assert(not chat_ui.is_open())
|
|
|
|
|
assert(not typed_chat_entry.has_focus())
|
|
|
|
|
assert(not bool(chat_ui.get("_input_lock_applied")))
|
2026-08-18 21:59:34 -04:00
|
|
|
assert(not chat_status.visible)
|
|
|
|
|
assert(chat_status.text.is_empty())
|
2026-08-13 01:31:05 -04:00
|
|
|
var quick_menu := game_ui.get_node(
|
|
|
|
|
"%QuickRadialMenu"
|
|
|
|
|
) as QuickRadialMenu
|
|
|
|
|
var emote_menu := game_ui.get_node(
|
|
|
|
|
"%EmoteRadialMenu"
|
|
|
|
|
) as EmoteRadialMenu
|
|
|
|
|
assert(quick_menu != null and emote_menu != null)
|
|
|
|
|
assert(QuickRadialMenu.ACTIONS.size() == QuickRadialMenu.SECTOR_COUNT)
|
2026-08-15 09:53:30 -04:00
|
|
|
assert(QuickRadialMenu.ACTIONS == [&"chat", &"freecam", &"hud"])
|
2026-08-13 01:31:05 -04:00
|
|
|
game_ui.call("_on_quick_action_selected", &"freecam")
|
|
|
|
|
assert(player.is_free_camera_active())
|
2026-08-14 22:12:13 -04:00
|
|
|
var free_camera := player.get_active_gameplay_camera()
|
|
|
|
|
assert(free_camera.name == &"FreeCamera")
|
|
|
|
|
var free_camera_yaw_before: float = free_camera.global_rotation.y
|
|
|
|
|
var mapping_manager := main.get(
|
|
|
|
|
"_controller_mapping_manager"
|
|
|
|
|
) as ControllerMappingManager
|
|
|
|
|
var right_stick_motion := InputEventJoypadMotion.new()
|
|
|
|
|
right_stick_motion.device = mapping_manager.get_active_device_id()
|
|
|
|
|
right_stick_motion.axis = JOY_AXIS_RIGHT_X
|
|
|
|
|
right_stick_motion.axis_value = 0.9
|
|
|
|
|
Input.parse_input_event(right_stick_motion)
|
|
|
|
|
await create_timer(0.1).timeout
|
|
|
|
|
assert(
|
|
|
|
|
absf(angle_difference(
|
|
|
|
|
free_camera.global_rotation.y,
|
|
|
|
|
free_camera_yaw_before,
|
|
|
|
|
)) > 0.02
|
|
|
|
|
)
|
|
|
|
|
right_stick_motion.axis_value = 0.0
|
|
|
|
|
Input.parse_input_event(right_stick_motion)
|
|
|
|
|
await process_frame
|
2026-08-21 12:29:51 -04:00
|
|
|
|
|
|
|
|
var free_camera_body := player.get("_free_camera_body") as CharacterBody3D
|
|
|
|
|
assert(free_camera_body != null)
|
2026-08-22 22:56:30 -04:00
|
|
|
# A released fishing cast locks only the avatar. The detached camera must
|
|
|
|
|
# continue to accept translation while that movement lock is active.
|
|
|
|
|
player.set_movement_enabled(false)
|
|
|
|
|
var cast_locked_camera_position: Vector3 = free_camera_body.global_position
|
|
|
|
|
Input.action_press("move_forward")
|
|
|
|
|
for _frame: int in 3:
|
|
|
|
|
await physics_frame
|
|
|
|
|
Input.action_release("move_forward")
|
|
|
|
|
assert(not free_camera_body.global_position.is_equal_approx(
|
|
|
|
|
cast_locked_camera_position
|
|
|
|
|
))
|
|
|
|
|
player.set_movement_enabled(true)
|
|
|
|
|
|
|
|
|
|
# Chat owns all movement while text entry is active, including developer
|
|
|
|
|
# freecam movement which reads the same WASD actions as the player.
|
2026-08-21 12:29:51 -04:00
|
|
|
chat_ui.open_chat()
|
|
|
|
|
assert(chat_ui.is_open())
|
|
|
|
|
var free_camera_position_before: Vector3 = free_camera_body.global_position
|
|
|
|
|
Input.action_press("move_forward")
|
|
|
|
|
for _frame: int in 3:
|
|
|
|
|
await physics_frame
|
|
|
|
|
Input.action_release("move_forward")
|
|
|
|
|
assert(free_camera_body.global_position.is_equal_approx(
|
|
|
|
|
free_camera_position_before
|
|
|
|
|
))
|
|
|
|
|
chat_ui.refocus_gameplay()
|
|
|
|
|
|
|
|
|
|
# Typewriter speech cycles the shared open-mouth faceplates, then restores
|
|
|
|
|
# the player's authored mouth. World speech also follows the active freecam,
|
|
|
|
|
# not the inactive normal gameplay camera.
|
|
|
|
|
var normal_camera := player.get_gameplay_camera()
|
|
|
|
|
var normal_camera_transform: Transform3D = normal_camera.global_transform
|
|
|
|
|
var chat_anchor: Vector3 = player.get_chat_anchor_position()
|
|
|
|
|
normal_camera.look_at(
|
|
|
|
|
normal_camera.global_position
|
|
|
|
|
- (chat_anchor - normal_camera.global_position),
|
|
|
|
|
Vector3.UP,
|
|
|
|
|
)
|
|
|
|
|
assert(normal_camera.is_position_behind(chat_anchor))
|
|
|
|
|
assert(not free_camera.is_position_behind(chat_anchor))
|
|
|
|
|
chat_ui.show_local_speech("hello there friend")
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(bool(player.get("_speech_mouth_active")))
|
|
|
|
|
var first_speech_mouth: String = str(player.get("_speech_mouth_id"))
|
|
|
|
|
await create_timer(0.12).timeout
|
|
|
|
|
var second_speech_mouth: String = str(player.get("_speech_mouth_id"))
|
|
|
|
|
assert(not first_speech_mouth.is_empty())
|
|
|
|
|
assert(not second_speech_mouth.is_empty())
|
|
|
|
|
assert(first_speech_mouth != second_speech_mouth)
|
|
|
|
|
chat_ui.call("_update_speech")
|
|
|
|
|
var speech: Dictionary = chat_ui.get("_speech")
|
|
|
|
|
var local_speech: Dictionary = speech.get(session.get_local_peer_id(), {})
|
|
|
|
|
var speech_bubble := local_speech.get("bubble") as PanelContainer
|
|
|
|
|
assert(speech_bubble != null and speech_bubble.visible)
|
|
|
|
|
normal_camera.global_transform = normal_camera_transform
|
|
|
|
|
await create_timer(0.6).timeout
|
|
|
|
|
assert(not bool(player.get("_speech_mouth_active")))
|
|
|
|
|
chat_ui.call("_on_peer_removed", session.get_local_peer_id())
|
|
|
|
|
|
2026-08-13 01:31:05 -04:00
|
|
|
game_ui.call("_on_quick_action_selected", &"freecam")
|
|
|
|
|
assert(not player.is_free_camera_active())
|
|
|
|
|
game_ui.call("_on_quick_action_selected", &"hud")
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(game_ui.is_gameplay_hud_hidden())
|
|
|
|
|
assert(chat_ui.is_hud_hidden())
|
|
|
|
|
assert(not (
|
|
|
|
|
game_ui.get_node("%GameplayTransientHUD") as Control
|
|
|
|
|
).visible)
|
|
|
|
|
assert(not (
|
|
|
|
|
game_ui.get_node("%ExperiencePresentation") as Control
|
|
|
|
|
).visible)
|
2026-08-20 17:53:22 -04:00
|
|
|
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)
|
2026-08-13 01:31:05 -04:00
|
|
|
quick_menu.open_menu(true)
|
|
|
|
|
assert(quick_menu.visible and quick_menu.is_open())
|
|
|
|
|
quick_menu.close_menu()
|
|
|
|
|
emote_menu.open_menu(true)
|
|
|
|
|
assert(emote_menu.visible and emote_menu.is_open())
|
|
|
|
|
emote_menu.close_menu()
|
|
|
|
|
chat_ui.open_chat()
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(chat_ui.is_open() and chat_panel.visible)
|
|
|
|
|
chat_ui.refocus_gameplay()
|
|
|
|
|
assert(chat_ui.is_hud_hidden())
|
2026-08-03 20:49:19 -04:00
|
|
|
var cancel_button := InputEventJoypadButton.new()
|
|
|
|
|
cancel_button.button_index = JOY_BUTTON_B
|
|
|
|
|
cancel_button.pressed = true
|
|
|
|
|
assert(not bool(main.call("_is_pause_open_request", cancel_button)))
|
|
|
|
|
var start_button := InputEventJoypadButton.new()
|
|
|
|
|
start_button.button_index = JOY_BUTTON_START
|
|
|
|
|
start_button.pressed = true
|
|
|
|
|
assert(bool(main.call("_is_pause_open_request", start_button)))
|
2026-08-15 10:16:09 -04:00
|
|
|
await _validate_pause_browser_transition(game_ui, player)
|
2026-08-03 20:49:19 -04:00
|
|
|
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
|
|
|
|
player_menu.open_section(PlayerMenu.Section.PROFILE)
|
|
|
|
|
for _frame: int in 12:
|
|
|
|
|
await process_frame
|
2026-08-13 01:31:05 -04:00
|
|
|
assert(game_ui.is_gameplay_hud_hidden())
|
|
|
|
|
assert(player_menu.visible)
|
2026-08-03 20:49:19 -04:00
|
|
|
assert(root.gui_get_focus_owner() is not LineEdit)
|
2026-08-13 15:24:23 -04:00
|
|
|
player_menu.close_menu(PlayerMenu.CloseReason.SESSION_END)
|
|
|
|
|
await process_frame
|
2026-08-13 01:31:05 -04:00
|
|
|
game_ui.call("_on_quick_action_selected", &"hud")
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(not game_ui.is_gameplay_hud_hidden())
|
|
|
|
|
assert(not chat_ui.is_hud_hidden())
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(not service.can_activate())
|
|
|
|
|
assert(not service.is_active() and not toolbar.visible)
|
|
|
|
|
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
|
2026-08-13 15:24:23 -04:00
|
|
|
player.hotbar.select_slot(1)
|
|
|
|
|
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
|
|
|
|
assert(not service.can_activate())
|
2026-08-02 00:45:07 -04:00
|
|
|
var paint_key := InputEventKey.new()
|
|
|
|
|
paint_key.physical_keycode = KEY_P
|
|
|
|
|
paint_key.pressed = true
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(not service.handle_input(paint_key, true))
|
|
|
|
|
assert(not service.is_active())
|
|
|
|
|
player.hotbar.select_slot(0)
|
2026-08-02 00:45:07 -04:00
|
|
|
await process_frame
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(service.is_active() and not service.is_placement_mode())
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(toolbar.visible)
|
2026-08-20 17:53:22 -04:00
|
|
|
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)
|
2026-08-02 09:20:44 -04:00
|
|
|
var ui_root := game_ui.get_node("%UIRoot") as Control
|
|
|
|
|
assert(toolbar.get_parent() == ui_root)
|
|
|
|
|
assert(is_zero_approx(toolbar.position.y))
|
|
|
|
|
assert(
|
|
|
|
|
is_equal_approx(
|
|
|
|
|
toolbar.get_global_rect().end.x,
|
|
|
|
|
ui_root.get_global_rect().end.x,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(toolbar.get_global_rect().end.x <= 1280.0)
|
|
|
|
|
assert(toolbar.get_global_rect().end.y <= 720.0)
|
2026-08-02 09:20:44 -04:00
|
|
|
var top_panel := toolbar.get_node("%TopPanel") as PanelContainer
|
|
|
|
|
var color_panel := toolbar.get_node("%ColorPanel") as PanelContainer
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(is_equal_approx(toolbar.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
|
|
|
|
|
assert(is_equal_approx(top_panel.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(top_panel.position.is_equal_approx(Vector2.ZERO))
|
|
|
|
|
assert(color_panel.position.x > 0.0)
|
|
|
|
|
assert(is_equal_approx(color_panel.get_rect().end.x, top_panel.get_rect().end.x))
|
|
|
|
|
assert(color_panel.position.y < top_panel.get_rect().end.y)
|
|
|
|
|
assert(color_panel.get_rect().end.y > top_panel.get_rect().end.y)
|
|
|
|
|
var toolbar_origin: Vector2 = toolbar.get_global_rect().position
|
|
|
|
|
var top_pointer := InputEventMouseMotion.new()
|
|
|
|
|
top_pointer.position = toolbar_origin + Vector2(200.0, 24.0)
|
|
|
|
|
assert(toolbar.owns_pointer_event(top_pointer))
|
|
|
|
|
var rail_pointer := InputEventMouseMotion.new()
|
|
|
|
|
rail_pointer.position = color_panel.get_global_rect().get_center()
|
|
|
|
|
assert(toolbar.owns_pointer_event(rail_pointer))
|
|
|
|
|
var empty_pointer := InputEventMouseMotion.new()
|
|
|
|
|
empty_pointer.position = toolbar_origin + Vector2(200.0, 180.0)
|
|
|
|
|
assert(not toolbar.owns_pointer_event(empty_pointer))
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
var brush_option := toolbar.get_node("%BrushOption") as OptionButton
|
|
|
|
|
var grid_option := toolbar.get_node("%GridOption") as OptionButton
|
2026-08-02 09:20:44 -04:00
|
|
|
var mode_button := toolbar.get_node("%ModeButton") as Button
|
|
|
|
|
var eraser_button := toolbar.get_node("%EraserButton") as Button
|
|
|
|
|
var undo_button := toolbar.get_node("%UndoButton") as Button
|
|
|
|
|
var hide_button := toolbar.get_node("%HideGuideButton") as Button
|
|
|
|
|
var restore_button := toolbar.get_node("%RestoreGuideButton") as Button
|
|
|
|
|
var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button
|
2026-08-21 21:01:21 -04:00
|
|
|
var export_button := toolbar.get_node("%ExportButton") as Button
|
|
|
|
|
var stamp_button := toolbar.get_node("%StampButton") as Button
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(
|
|
|
|
|
mode_button != null
|
|
|
|
|
and eraser_button != null
|
|
|
|
|
and undo_button != null
|
|
|
|
|
and hide_button != null
|
|
|
|
|
and restore_button != null
|
|
|
|
|
and finalize_button != null
|
2026-08-21 21:01:21 -04:00
|
|
|
and export_button != null
|
|
|
|
|
and stamp_button != null
|
2026-08-02 09:20:44 -04:00
|
|
|
)
|
2026-08-21 21:01:21 -04:00
|
|
|
assert(export_button.text == "png")
|
|
|
|
|
assert(export_button.tooltip_text == "export aimed artwork as PNG")
|
|
|
|
|
assert(stamp_button.text == "stamp")
|
|
|
|
|
assert(stamp_button.disabled)
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(toolbar.get_node_or_null("%CloseButton") == null)
|
|
|
|
|
var expected_toolbar_icons: Dictionary[Button, String] = {
|
|
|
|
|
mode_button: "/art/art_kit_marker.png",
|
|
|
|
|
eraser_button: "/art/art_kit_eraser.png",
|
|
|
|
|
hide_button: "/art/art_kit_grid_hidden_light.png",
|
|
|
|
|
restore_button: "/art/art_kit_grid_restore_light.png",
|
|
|
|
|
finalize_button: "/art/art_kit_grid_finish_light.png",
|
|
|
|
|
}
|
|
|
|
|
for icon_button: Button in expected_toolbar_icons:
|
|
|
|
|
assert(icon_button.text.is_empty())
|
|
|
|
|
assert(icon_button.icon != null)
|
|
|
|
|
assert(
|
|
|
|
|
icon_button.icon.resource_path.ends_with(
|
|
|
|
|
expected_toolbar_icons[icon_button]
|
|
|
|
|
)
|
2026-08-13 14:53:39 -04:00
|
|
|
)
|
2026-08-13 15:24:23 -04:00
|
|
|
for icon_button: Button in [
|
|
|
|
|
eraser_button,
|
|
|
|
|
undo_button,
|
|
|
|
|
hide_button,
|
|
|
|
|
restore_button,
|
|
|
|
|
finalize_button,
|
2026-08-21 21:01:21 -04:00
|
|
|
export_button,
|
|
|
|
|
stamp_button,
|
2026-08-13 15:24:23 -04:00
|
|
|
]:
|
2026-08-13 14:53:39 -04:00
|
|
|
assert(icon_button.custom_minimum_size == Vector2(48, 44))
|
|
|
|
|
assert(icon_button.get_theme_constant("icon_max_width") == 40)
|
|
|
|
|
var icon_style := (
|
|
|
|
|
icon_button.get_theme_stylebox("normal") as StyleBoxFlat
|
|
|
|
|
)
|
|
|
|
|
assert(is_equal_approx(icon_style.content_margin_left, 4.0))
|
|
|
|
|
assert(is_equal_approx(icon_style.content_margin_top, 2.0))
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(mode_button.custom_minimum_size == Vector2(48, 48))
|
|
|
|
|
assert(mode_button.get_theme_constant("icon_max_width") == 40)
|
2026-08-21 21:01:21 -04:00
|
|
|
assert(export_button.get_index() > finalize_button.get_index())
|
|
|
|
|
assert(stamp_button.get_index() > export_button.get_index())
|
|
|
|
|
assert(mode_button.get_index() > stamp_button.get_index())
|
2026-08-13 14:53:39 -04:00
|
|
|
assert(hide_button.button_group != null)
|
|
|
|
|
assert(hide_button.button_group == restore_button.button_group)
|
|
|
|
|
assert(hide_button.button_group == finalize_button.button_group)
|
|
|
|
|
assert(hide_button.button_group.allow_unpress)
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(brush_option.item_count == 4)
|
|
|
|
|
assert(grid_option.item_count == 4)
|
2026-08-15 20:38:58 -04:00
|
|
|
for pointer_only_control: Control in [
|
|
|
|
|
brush_option,
|
|
|
|
|
grid_option,
|
|
|
|
|
mode_button,
|
|
|
|
|
eraser_button,
|
|
|
|
|
undo_button,
|
|
|
|
|
hide_button,
|
|
|
|
|
restore_button,
|
|
|
|
|
finalize_button,
|
2026-08-21 21:01:21 -04:00
|
|
|
export_button,
|
|
|
|
|
stamp_button,
|
2026-08-15 20:38:58 -04:00
|
|
|
]:
|
|
|
|
|
assert(pointer_only_control.focus_mode == Control.FOCUS_NONE)
|
|
|
|
|
assert(brush_option.get_popup().unfocusable)
|
|
|
|
|
assert(grid_option.get_popup().unfocusable)
|
|
|
|
|
brush_option.get_popup().popup(Rect2i(100, 100, 220, 220))
|
|
|
|
|
await process_frame
|
|
|
|
|
game_ui.call("_begin_virtual_mouse", 0)
|
|
|
|
|
game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0))
|
|
|
|
|
var brush_popup_cursor := toolbar.get(
|
|
|
|
|
"_brush_popup_cursor"
|
|
|
|
|
) as ControllerVirtualCursor
|
|
|
|
|
assert(brush_popup_cursor != null and brush_popup_cursor.visible)
|
|
|
|
|
assert(brush_popup_cursor.get_parent() == brush_option.get_popup())
|
|
|
|
|
assert(brush_popup_cursor.z_index == RenderingServer.CANVAS_ITEM_Z_MAX)
|
|
|
|
|
assert(not virtual_cursor.visible)
|
|
|
|
|
brush_option.get_popup().hide()
|
|
|
|
|
game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0))
|
|
|
|
|
assert(not brush_popup_cursor.visible)
|
|
|
|
|
assert(virtual_cursor.visible)
|
|
|
|
|
game_ui.call("_end_virtual_mouse")
|
2026-08-15 14:28:13 -04:00
|
|
|
var expected_brush_icons: Array[String] = [
|
|
|
|
|
"art_kit_marker_tip_fine.png",
|
|
|
|
|
"art_kit_marker_tip_thin.png",
|
|
|
|
|
"art_kit_marker_tip_mid.png",
|
|
|
|
|
"art_kit_marker_tip_thick.png",
|
|
|
|
|
]
|
|
|
|
|
var expected_grid_icons: Array[String] = [
|
|
|
|
|
"art_kit_grid_small_light.png",
|
|
|
|
|
"art_kit_grid_medium_light.png",
|
|
|
|
|
"art_kit_grid_large_light.png",
|
|
|
|
|
"art_kit_grid_xl_light.png",
|
|
|
|
|
]
|
|
|
|
|
for index: int in 4:
|
|
|
|
|
assert(brush_option.get_item_text(index).is_empty())
|
|
|
|
|
assert(grid_option.get_item_text(index).is_empty())
|
|
|
|
|
assert(brush_option.get_item_icon(index) != null)
|
|
|
|
|
assert(grid_option.get_item_icon(index) != null)
|
2026-08-19 10:02:28 -04:00
|
|
|
var brush_unlocked: bool = index == 0
|
|
|
|
|
var grid_unlocked: bool = index == 0
|
|
|
|
|
if brush_unlocked:
|
|
|
|
|
assert(
|
|
|
|
|
str(brush_option.get_item_icon(index).get_meta(
|
|
|
|
|
&"channel_mask_source", ""
|
|
|
|
|
)).ends_with(
|
|
|
|
|
expected_brush_icons[index]
|
|
|
|
|
)
|
2026-08-15 14:28:13 -04:00
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
else:
|
|
|
|
|
assert(bool(brush_option.get_item_icon(index).get_meta(
|
|
|
|
|
&"locked_content_icon", false
|
|
|
|
|
)))
|
|
|
|
|
if grid_unlocked:
|
|
|
|
|
assert(
|
|
|
|
|
grid_option.get_item_icon(index).resource_path.ends_with(
|
|
|
|
|
expected_grid_icons[index]
|
|
|
|
|
)
|
2026-08-15 14:28:13 -04:00
|
|
|
)
|
2026-08-19 10:02:28 -04:00
|
|
|
else:
|
|
|
|
|
assert(bool(grid_option.get_item_icon(index).get_meta(
|
|
|
|
|
&"locked_content_icon", false
|
|
|
|
|
)))
|
|
|
|
|
var expected_lock_alpha: float = 1.0 if index == 0 else 0.72
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
brush_option.get_popup().get_item_icon_modulate(index).a,
|
|
|
|
|
expected_lock_alpha,
|
|
|
|
|
))
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
grid_option.get_popup().get_item_icon_modulate(index).a,
|
|
|
|
|
expected_lock_alpha,
|
|
|
|
|
))
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(not brush_option.get_popup().is_item_disabled(0))
|
|
|
|
|
assert(brush_option.get_popup().is_item_disabled(1))
|
|
|
|
|
assert(not grid_option.get_popup().is_item_disabled(0))
|
|
|
|
|
assert(grid_option.get_popup().is_item_disabled(1))
|
|
|
|
|
var color_buttons: Dictionary = toolbar.get("_color_buttons")
|
|
|
|
|
assert(color_buttons.size() == SurfaceDrawingPalette.COLORS.size())
|
2026-08-15 20:38:58 -04:00
|
|
|
for color_button: Button in color_buttons.values():
|
|
|
|
|
assert(color_button.focus_mode == Control.FOCUS_NONE)
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(not (color_buttons[&"chalk_white"] as Button).disabled)
|
|
|
|
|
assert((color_buttons[&"ocean_teal"] as Button).disabled)
|
2026-08-19 10:02:28 -04:00
|
|
|
var base_color_lock := (
|
|
|
|
|
(color_buttons[&"chalk_white"] as Button).get_node(
|
|
|
|
|
"UnlockStateIcon"
|
|
|
|
|
) as TextureRect
|
|
|
|
|
)
|
|
|
|
|
var locked_color_lock := (
|
|
|
|
|
(color_buttons[&"ocean_teal"] as Button).get_node(
|
|
|
|
|
"UnlockStateIcon"
|
|
|
|
|
) as TextureRect
|
|
|
|
|
)
|
|
|
|
|
assert(not base_color_lock.visible)
|
|
|
|
|
assert(locked_color_lock.visible)
|
|
|
|
|
assert(locked_color_lock.size == Vector2(18.0, 18.0))
|
|
|
|
|
assert(is_equal_approx(locked_color_lock.modulate.a, 0.34))
|
|
|
|
|
var popup_lock_icon := toolbar.get("_popup_lock_icon") as Texture2D
|
|
|
|
|
assert(popup_lock_icon.get_width() == 40)
|
|
|
|
|
assert(popup_lock_icon.get_height() == 40)
|
|
|
|
|
assert(int(popup_lock_icon.get_meta(
|
|
|
|
|
&"locked_content_icon_size", 0
|
|
|
|
|
)) == 24)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
for product_id: StringName in [
|
2026-08-21 21:01:21 -04:00
|
|
|
&"marker_ocean_teal",
|
|
|
|
|
&"brush_4x",
|
|
|
|
|
&"grid_128x",
|
|
|
|
|
PlayerArtUnlocks.STAMP_PRODUCT_ID,
|
2026-08-02 00:45:07 -04:00
|
|
|
]:
|
|
|
|
|
assert(player.art_unlocks.unlock_product(product_id))
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(not (color_buttons[&"ocean_teal"] as Button).disabled)
|
|
|
|
|
assert(not brush_option.get_popup().is_item_disabled(3))
|
|
|
|
|
assert(not grid_option.get_popup().is_item_disabled(3))
|
2026-08-21 21:01:21 -04:00
|
|
|
assert(not stamp_button.disabled)
|
|
|
|
|
var stamp_library_panel := toolbar.get_node(
|
|
|
|
|
"%StampLibraryPanel"
|
|
|
|
|
) as PanelContainer
|
|
|
|
|
var stamp_library_close := toolbar.get_node(
|
|
|
|
|
"%StampLibraryClose"
|
|
|
|
|
) as Button
|
|
|
|
|
assert(stamp_library_panel != null and not stamp_library_panel.visible)
|
|
|
|
|
stamp_button.pressed.emit()
|
|
|
|
|
assert(stamp_library_panel.visible)
|
|
|
|
|
var library_pointer := InputEventMouseMotion.new()
|
|
|
|
|
library_pointer.position = stamp_library_panel.get_global_rect().get_center()
|
|
|
|
|
assert(toolbar.owns_pointer_event(library_pointer))
|
|
|
|
|
stamp_library_close.pressed.emit()
|
|
|
|
|
assert(not stamp_library_panel.visible)
|
2026-08-19 10:02:28 -04:00
|
|
|
assert(not locked_color_lock.visible)
|
|
|
|
|
assert(str(brush_option.get_item_icon(3).get_meta(
|
|
|
|
|
&"channel_mask_source", ""
|
|
|
|
|
)).ends_with(expected_brush_icons[3]))
|
|
|
|
|
assert(grid_option.get_item_icon(3).resource_path.ends_with(
|
|
|
|
|
expected_grid_icons[3]
|
|
|
|
|
))
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
brush_option.get_popup().get_item_icon_modulate(3).a, 1.0
|
|
|
|
|
))
|
|
|
|
|
assert(is_equal_approx(
|
|
|
|
|
grid_option.get_popup().get_item_icon_modulate(3).a, 1.0
|
|
|
|
|
))
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(service.set_color_id(&"ocean_teal"))
|
|
|
|
|
assert(service.set_brush_size(4))
|
|
|
|
|
assert(service.set_grid_size(128))
|
|
|
|
|
assert(service.get_color_id() == &"ocean_teal")
|
|
|
|
|
assert(service.get_brush_size() == 4)
|
|
|
|
|
assert(service.get_grid_size() == 128)
|
2026-08-13 16:25:49 -04:00
|
|
|
var marker_mode_material := mode_button.material as ShaderMaterial
|
|
|
|
|
assert(marker_mode_material != null)
|
2026-08-15 20:38:58 -04:00
|
|
|
assert(brush_option.material == null)
|
|
|
|
|
assert(grid_option.material == null)
|
2026-08-13 16:25:49 -04:00
|
|
|
assert(
|
|
|
|
|
marker_mode_material.shader.resource_path.ends_with(
|
|
|
|
|
"/ui/art_kit_marker_icon.gdshader"
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
assert(
|
|
|
|
|
marker_mode_material.get_shader_parameter("marker_color")
|
|
|
|
|
== SurfaceDrawingPalette.get_color(&"ocean_teal")
|
|
|
|
|
)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
2026-08-02 09:20:44 -04:00
|
|
|
mode_button.pressed.emit()
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(service.is_placement_mode())
|
|
|
|
|
assert(
|
|
|
|
|
mode_button.icon.resource_path.ends_with(
|
|
|
|
|
"/art/art_kit_grid_light.png"
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-08-02 09:20:44 -04:00
|
|
|
eraser_button.pressed.emit()
|
|
|
|
|
assert(service.is_eraser_mode())
|
|
|
|
|
assert(eraser_button.button_pressed)
|
|
|
|
|
hide_button.pressed.emit()
|
|
|
|
|
assert(service.is_placement_mode())
|
|
|
|
|
assert(not service.is_eraser_mode())
|
|
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.HIDE
|
|
|
|
|
)
|
|
|
|
|
var world_click := InputEventMouseButton.new()
|
|
|
|
|
world_click.button_index = MOUSE_BUTTON_LEFT
|
|
|
|
|
world_click.pressed = true
|
|
|
|
|
assert(service.handle_input(world_click, true))
|
2026-08-13 14:53:39 -04:00
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.HIDE
|
|
|
|
|
)
|
|
|
|
|
assert(service.is_placement_mode())
|
|
|
|
|
assert(not service.is_eraser_mode())
|
|
|
|
|
assert(hide_button.button_pressed)
|
|
|
|
|
hide_button.pressed.emit()
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.NONE
|
|
|
|
|
)
|
2026-08-13 14:53:39 -04:00
|
|
|
assert(not service.is_placement_mode() and service.is_eraser_mode())
|
2026-08-02 09:20:44 -04:00
|
|
|
|
|
|
|
|
restore_button.pressed.emit()
|
|
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.RESTORE
|
|
|
|
|
)
|
2026-08-13 14:53:39 -04:00
|
|
|
finalize_button.pressed.emit()
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
2026-08-13 14:53:39 -04:00
|
|
|
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
|
2026-08-02 09:20:44 -04:00
|
|
|
)
|
2026-08-13 14:53:39 -04:00
|
|
|
assert(not restore_button.button_pressed)
|
|
|
|
|
assert(finalize_button.button_pressed)
|
|
|
|
|
assert(service.handle_input(world_click, true))
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.FINALIZE
|
|
|
|
|
)
|
2026-08-13 14:53:39 -04:00
|
|
|
finalize_button.pressed.emit()
|
|
|
|
|
assert(
|
|
|
|
|
service.get_armed_guide_action()
|
|
|
|
|
== NetworkSurfaceDrawingService.GuideAction.NONE
|
|
|
|
|
)
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(not service.is_placement_mode() and service.is_eraser_mode())
|
|
|
|
|
(color_buttons[&"ocean_teal"] as Button).pressed.emit()
|
|
|
|
|
assert(not service.is_eraser_mode())
|
|
|
|
|
undo_button.pressed.emit()
|
|
|
|
|
assert(service.is_active())
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(not service.handle_input(paint_key, true))
|
2026-08-02 09:20:44 -04:00
|
|
|
assert(service.is_active() and toolbar.visible)
|
2026-08-02 00:45:07 -04:00
|
|
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
|
|
|
|
await process_frame
|
2026-08-13 14:53:39 -04:00
|
|
|
var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
|
|
|
|
|
var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
|
|
|
|
|
var pocket_animation_player := player.get(
|
|
|
|
|
"_character_animation_player"
|
|
|
|
|
) as AnimationPlayer
|
|
|
|
|
var pocket_animation_name: StringName = player.get(
|
|
|
|
|
"_pocket_visual_animation"
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
not held_art_kit_display.visible
|
|
|
|
|
and pocket_animation_player != null
|
|
|
|
|
and not pocket_animation_name.is_empty()
|
|
|
|
|
and pocket_animation_player.has_animation(pocket_animation_name)
|
|
|
|
|
):
|
|
|
|
|
var pocket_animation: Animation = (
|
|
|
|
|
pocket_animation_player.get_animation(pocket_animation_name)
|
|
|
|
|
)
|
|
|
|
|
await create_timer(pocket_animation.length * 0.55).timeout
|
|
|
|
|
assert(held_art_kit_display.visible)
|
|
|
|
|
assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
|
|
|
|
|
assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
|
|
|
|
|
assert(is_zero_approx(angle_difference(
|
|
|
|
|
held_art_kit_sprite.rotation.z,
|
|
|
|
|
-PI * 0.5,
|
|
|
|
|
)))
|
|
|
|
|
assert(held_art_kit_sprite.flip_h)
|
2026-08-10 21:40:55 -04:00
|
|
|
assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
|
2026-08-02 00:45:07 -04:00
|
|
|
assert(service.is_active() and toolbar.visible)
|
|
|
|
|
assert(service.get_grid_size() == 128)
|
2026-08-10 21:40:55 -04:00
|
|
|
assert(service.handle_input(world_click, true))
|
|
|
|
|
assert(service.is_active() and toolbar.visible)
|
2026-08-13 15:24:23 -04:00
|
|
|
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
|
|
|
|
var wheel_down := InputEventMouseButton.new()
|
|
|
|
|
wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
|
|
|
|
wheel_down.pressed = true
|
|
|
|
|
assert(not service.handle_input(wheel_down, true))
|
|
|
|
|
hotbar_ui._unhandled_input(wheel_down)
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(player.hotbar.get_selected_slot() == 1)
|
|
|
|
|
assert(not service.is_active() and not toolbar.visible)
|
|
|
|
|
|
|
|
|
|
var number_one := InputEventKey.new()
|
|
|
|
|
number_one.physical_keycode = KEY_1
|
|
|
|
|
number_one.pressed = true
|
|
|
|
|
assert(not service.handle_input(number_one, true))
|
|
|
|
|
hotbar_ui._unhandled_input(number_one)
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(service.is_active() and toolbar.visible)
|
|
|
|
|
assert(player.hotbar.get_selected_slot() == 0)
|
|
|
|
|
var number_two := InputEventKey.new()
|
|
|
|
|
number_two.physical_keycode = KEY_2
|
|
|
|
|
number_two.pressed = true
|
|
|
|
|
assert(not service.handle_input(number_two, true))
|
|
|
|
|
hotbar_ui._unhandled_input(number_two)
|
2026-08-10 21:40:55 -04:00
|
|
|
await process_frame
|
2026-08-13 15:24:23 -04:00
|
|
|
assert(player.hotbar.get_selected_slot() == 1)
|
2026-08-10 21:40:55 -04:00
|
|
|
assert(not service.is_active() and not toolbar.visible)
|
2026-08-02 00:45:07 -04:00
|
|
|
|
|
|
|
|
print("Art tools validation: PASS")
|
|
|
|
|
session.disconnect_session("")
|
|
|
|
|
main.queue_free()
|
2026-08-11 20:02:54 -04:00
|
|
|
for _frame: int in 4:
|
|
|
|
|
await process_frame
|
|
|
|
|
await create_timer(0.1).timeout
|
2026-08-02 00:45:07 -04:00
|
|
|
quit()
|
2026-08-15 10:16:09 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _validate_pause_browser_transition(
|
|
|
|
|
game_ui: GameUI,
|
|
|
|
|
player: Player,
|
|
|
|
|
) -> void:
|
|
|
|
|
var pause_menu := game_ui.get_pause_menu()
|
|
|
|
|
var root_page := pause_menu.get_node("%RootPage") as Control
|
|
|
|
|
var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
|
|
|
|
|
pause_menu.open_menu()
|
|
|
|
|
while bool(pause_menu.get("_root_transition_active")):
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(pause_menu.visible and root_page.visible)
|
|
|
|
|
assert(not bool(player.call("_is_movement_input_enabled")))
|
|
|
|
|
|
|
|
|
|
pause_menu.call("_open_join_game")
|
|
|
|
|
while not join_page.visible:
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(bool(pause_menu.get("_root_transition_active")))
|
|
|
|
|
pause_menu.call("_close_join_game")
|
|
|
|
|
assert(not join_page.visible)
|
|
|
|
|
# This is the issue #73 input: Escape arrives while the root is returning.
|
|
|
|
|
assert(pause_menu.handle_escape())
|
|
|
|
|
while bool(pause_menu.get("_root_transition_active")):
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(pause_menu.visible and root_page.visible)
|
|
|
|
|
assert(not bool(player.call("_is_movement_input_enabled")))
|
|
|
|
|
|
|
|
|
|
assert(pause_menu.handle_escape())
|
|
|
|
|
while pause_menu.visible:
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(player.is_movement_enabled())
|
|
|
|
|
|
|
|
|
|
# Fishing can complete while pause owns local input. Closing pause must only
|
|
|
|
|
# release its own lock, not restore the stale movement value from open time.
|
|
|
|
|
player.set_movement_enabled(false)
|
|
|
|
|
pause_menu.open_menu()
|
|
|
|
|
while bool(pause_menu.get("_root_transition_active")):
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(not bool(player.call("_is_movement_input_enabled")))
|
|
|
|
|
player.set_movement_enabled(true)
|
|
|
|
|
assert(player.is_movement_enabled())
|
|
|
|
|
assert(not bool(player.call("_is_movement_input_enabled")))
|
|
|
|
|
pause_menu.close_menu(PauseMenu.CloseReason.USER_RETURN)
|
|
|
|
|
assert(player.is_movement_enabled())
|
|
|
|
|
assert(bool(player.call("_is_movement_input_enabled")))
|
|
|
|
|
|
|
|
|
|
# Opening pause during a right-click camera drag must not snapshot and later
|
|
|
|
|
# restore the drag's transient captured mouse mode after the drag is cancelled.
|
|
|
|
|
assert(
|
|
|
|
|
pause_menu.call(
|
|
|
|
|
"_get_restorable_mouse_mode",
|
|
|
|
|
Input.MOUSE_MODE_CAPTURED,
|
|
|
|
|
) == Input.MOUSE_MODE_VISIBLE
|
|
|
|
|
)
|
|
|
|
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
2026-08-16 12:26:17 -04:00
|
|
|
var drag_press := InputEventMouseButton.new()
|
|
|
|
|
drag_press.button_index = MOUSE_BUTTON_RIGHT
|
|
|
|
|
drag_press.button_mask = MOUSE_BUTTON_MASK_RIGHT
|
|
|
|
|
drag_press.pressed = true
|
|
|
|
|
player.call("_input", drag_press)
|
|
|
|
|
assert(bool(player.get("_camera_dragging")))
|
|
|
|
|
var camera_yaw := player.get_node("%CameraYaw") as Node3D
|
|
|
|
|
var yaw_before_drag: float = camera_yaw.rotation.y
|
|
|
|
|
var drag_motion := InputEventMouseMotion.new()
|
|
|
|
|
drag_motion.button_mask = MOUSE_BUTTON_MASK_RIGHT
|
|
|
|
|
drag_motion.relative = Vector2(24.0, -8.0)
|
2026-08-16 16:40:10 -04:00
|
|
|
drag_motion.screen_relative = drag_motion.relative
|
2026-08-16 12:26:17 -04:00
|
|
|
player.call("_input", drag_motion)
|
|
|
|
|
assert(not is_equal_approx(camera_yaw.rotation.y, yaw_before_drag))
|
2026-08-16 16:40:10 -04:00
|
|
|
await process_frame
|
|
|
|
|
assert(bool(player.get("_camera_dragging")))
|
|
|
|
|
if DisplayServer.get_name() != "headless":
|
|
|
|
|
assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
|
2026-08-16 12:26:17 -04:00
|
|
|
var drag_release := InputEventMouseButton.new()
|
|
|
|
|
drag_release.button_index = MOUSE_BUTTON_RIGHT
|
|
|
|
|
drag_release.button_mask = 0
|
|
|
|
|
drag_release.pressed = false
|
|
|
|
|
player.call("_input", drag_release)
|
|
|
|
|
assert(not bool(player.get("_camera_dragging")))
|
|
|
|
|
player.call("_input", drag_press)
|
2026-08-15 10:16:09 -04:00
|
|
|
assert(bool(player.get("_camera_dragging")))
|
|
|
|
|
pause_menu.open_menu()
|
|
|
|
|
while bool(pause_menu.get("_root_transition_active")):
|
|
|
|
|
await process_frame
|
|
|
|
|
assert(not bool(player.get("_camera_dragging")))
|
|
|
|
|
assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
|
|
|
|
|
pause_menu.close_menu(PauseMenu.CloseReason.USER_RETURN)
|
|
|
|
|
assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
|