Integrate per-save identity and interaction UI
This commit is contained in:
parent
ac82a28f3b
commit
4fda6e97f6
58 changed files with 3471 additions and 461 deletions
|
|
@ -409,7 +409,17 @@ func _run() -> void:
|
|||
# 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()
|
||||
var chat_anchor: Vector3 = player.get_nameplate_anchor_position()
|
||||
assert(player.get_chat_anchor_position().is_equal_approx(chat_anchor))
|
||||
var held_gear_probe := MeshInstance3D.new()
|
||||
var held_gear_mesh := BoxMesh.new()
|
||||
held_gear_mesh.size = Vector3(0.5, 4.0, 0.5)
|
||||
held_gear_probe.mesh = held_gear_mesh
|
||||
held_gear_probe.position = Vector3(0.0, 6.0, 0.0)
|
||||
player.get_node("%Visuals").add_child(held_gear_probe)
|
||||
assert(player.get_nameplate_anchor_position().is_equal_approx(chat_anchor))
|
||||
assert(player.get_chat_anchor_position().is_equal_approx(chat_anchor))
|
||||
held_gear_probe.free()
|
||||
normal_camera.look_at(
|
||||
normal_camera.global_position
|
||||
- (chat_anchor - normal_camera.global_position),
|
||||
|
|
@ -417,6 +427,120 @@ func _run() -> void:
|
|||
)
|
||||
assert(normal_camera.is_position_behind(chat_anchor))
|
||||
assert(not free_camera.is_position_behind(chat_anchor))
|
||||
free_camera.look_at(chat_anchor, Vector3.UP)
|
||||
chat_ui.call("_update_nameplates")
|
||||
var nameplate_layer := chat_ui.get("_nameplate_layer") as Control
|
||||
var nameplates: Dictionary = chat_ui.get("_nameplates")
|
||||
var local_nameplate: Dictionary = nameplates.get(
|
||||
session.get_local_peer_id(), {}
|
||||
)
|
||||
var nameplate_control := local_nameplate.get("control") as Control
|
||||
var nameplate_name := local_nameplate.get("name") as Label
|
||||
var nameplate_title := local_nameplate.get("title") as Label
|
||||
var local_record := session.get_peer_record(session.get_local_peer_id())
|
||||
assert(
|
||||
nameplate_control != null and nameplate_control.visible,
|
||||
(
|
||||
"local nameplate must be visible (available=%s world=%s layer=%s "
|
||||
+ "shared=%s remote_visible=%s behind=%s occluded=%s screen=%s viewport=%s)"
|
||||
)
|
||||
% [
|
||||
bool(chat_ui.get("_available")),
|
||||
chat_ui.is_world_speech_visible(),
|
||||
nameplate_layer.visible,
|
||||
bool(chat_ui.call(
|
||||
"_speaker_shares_local_space", session.get_local_peer_id()
|
||||
)),
|
||||
player.is_remote_presentation_visible(),
|
||||
free_camera.is_position_behind(chat_anchor),
|
||||
bool(chat_ui.call(
|
||||
"_is_speech_world_occluded", free_camera, player, chat_anchor
|
||||
)),
|
||||
free_camera.unproject_position(chat_anchor)
|
||||
/ float(chat_ui.get("_output_scale")),
|
||||
chat_ui.get_viewport_rect().size,
|
||||
],
|
||||
)
|
||||
assert(nameplate_name != null and nameplate_title != null)
|
||||
assert(nameplate_name.text == local_record.display_name)
|
||||
assert(nameplate_title.text == local_record.title)
|
||||
assert(
|
||||
nameplate_name.get_theme_font_size("font_size")
|
||||
== ChatUI.NAMEPLATE_NAME_FONT_SIZE
|
||||
)
|
||||
assert(
|
||||
nameplate_title.get_theme_font_size("font_size")
|
||||
== ChatUI.NAMEPLATE_TITLE_FONT_SIZE
|
||||
)
|
||||
assert(
|
||||
nameplate_name.get_theme_font_size("font_size")
|
||||
> nameplate_title.get_theme_font_size("font_size")
|
||||
)
|
||||
assert(ChatUI.NAMEPLATE_NAME_FONT_SIZE == 20)
|
||||
assert(ChatUI.NAMEPLATE_TITLE_TOP < ChatUI.NAMEPLATE_NAME_HEIGHT)
|
||||
assert(ChatUI.NAMEPLATE_HEIGHT < 42.0)
|
||||
assert(nameplate_title.position.y == ChatUI.NAMEPLATE_TITLE_TOP)
|
||||
for flat_label: Label in [nameplate_name, nameplate_title]:
|
||||
assert(flat_label.get_theme_color("font_color") == Color.WHITE)
|
||||
assert(flat_label.get_theme_constant("outline_size") == 0)
|
||||
assert(flat_label.get_theme_color("font_outline_color").a == 0.0)
|
||||
assert(flat_label.get_theme_color("font_shadow_color").a == 0.0)
|
||||
|
||||
# Persistent remote labels obey both the authoritative home-space boundary
|
||||
# and the avatar presentation mask. They return only when the avatar shares
|
||||
# the local space and its world presentation is visible.
|
||||
var spawn_service := main.get_node(
|
||||
"%PlayerSpawnService"
|
||||
) as PlayerSpawnService
|
||||
var registry := session.get("_registry") as PeerRegistry
|
||||
const NAMEPLATE_TEST_PEER_ID: int = 9191
|
||||
assert(registry.add_peer(
|
||||
NAMEPLATE_TEST_PEER_ID,
|
||||
"nameplate-test-profile",
|
||||
"remote stray",
|
||||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
"",
|
||||
"",
|
||||
PackedStringArray(),
|
||||
"trailblazer",
|
||||
))
|
||||
var remote_transform: Transform3D = player.global_transform
|
||||
remote_transform.origin += Vector3(0.8, 0.0, 0.0)
|
||||
var remote_avatar := spawn_service.spawn_remote_player(
|
||||
NAMEPLATE_TEST_PEER_ID,
|
||||
remote_transform,
|
||||
)
|
||||
assert(remote_avatar != null)
|
||||
assert(session.set_peer_space(NAMEPLATE_TEST_PEER_ID, &"rv:privacy-test"))
|
||||
remote_avatar.set_remote_presentation_visible(true)
|
||||
chat_ui.call("_update_nameplates")
|
||||
var remote_nameplate: Dictionary = nameplates.get(
|
||||
NAMEPLATE_TEST_PEER_ID, {}
|
||||
)
|
||||
var remote_plate := remote_nameplate.get("control") as Control
|
||||
assert(remote_plate != null and not remote_plate.visible)
|
||||
assert(session.set_peer_space(
|
||||
NAMEPLATE_TEST_PEER_ID,
|
||||
session.get_peer_space(session.get_local_peer_id()),
|
||||
))
|
||||
remote_avatar.set_remote_presentation_visible(false)
|
||||
chat_ui.call("_update_nameplates")
|
||||
assert(not remote_plate.visible)
|
||||
remote_avatar.set_remote_presentation_visible(true)
|
||||
free_camera.look_at(
|
||||
remote_avatar.get_nameplate_anchor_position(), Vector3.UP
|
||||
)
|
||||
chat_ui.call("_update_nameplates")
|
||||
assert(remote_plate.visible)
|
||||
assert((remote_nameplate.get("name") as Label).text == "remote stray")
|
||||
assert((remote_nameplate.get("title") as Label).text == "trailblazer")
|
||||
spawn_service.remove_peer(NAMEPLATE_TEST_PEER_ID)
|
||||
registry.remove_peer(NAMEPLATE_TEST_PEER_ID)
|
||||
chat_ui.call("_update_nameplates")
|
||||
assert(not nameplates.has(NAMEPLATE_TEST_PEER_ID))
|
||||
free_camera.look_at(chat_anchor, Vector3.UP)
|
||||
chat_ui.call("_update_nameplates")
|
||||
assert(nameplate_control.visible)
|
||||
var shop_prompt := game_ui.get_node("%ShopPrompt") as Control
|
||||
var storage_prompt := game_ui.get_node("%StoragePrompt") as Control
|
||||
game_ui.set_shop_prompt_visible(true, chat_anchor)
|
||||
|
|
@ -432,11 +556,19 @@ func _run() -> void:
|
|||
assert(not first_speech_mouth.is_empty())
|
||||
assert(not second_speech_mouth.is_empty())
|
||||
assert(first_speech_mouth != second_speech_mouth)
|
||||
free_camera.look_at(chat_anchor, Vector3.UP)
|
||||
chat_ui.call("_update_nameplates")
|
||||
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)
|
||||
assert(
|
||||
speech_bubble.position.y
|
||||
+ speech_bubble.size.y
|
||||
+ ChatUI.SPEECH_POINTER_HEIGHT
|
||||
<= nameplate_control.position.y
|
||||
)
|
||||
normal_camera.global_transform = normal_camera_transform
|
||||
await create_timer(0.6).timeout
|
||||
assert(not bool(player.get("_speech_mouth_active")))
|
||||
|
|
@ -448,6 +580,7 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(game_ui.is_gameplay_hud_hidden())
|
||||
assert(chat_ui.is_hud_hidden())
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(not (
|
||||
game_ui.get_node("%GameplayTransientHUD") as Control
|
||||
).visible)
|
||||
|
|
@ -479,6 +612,8 @@ func _run() -> void:
|
|||
chat_ui.open_chat()
|
||||
await process_frame
|
||||
assert(chat_ui.is_open() and chat_panel.visible)
|
||||
assert(not chat_ui.is_hud_hidden())
|
||||
assert(not nameplate_layer.visible)
|
||||
chat_ui.refocus_gameplay()
|
||||
assert(chat_ui.is_hud_hidden())
|
||||
var cancel_button := InputEventJoypadButton.new()
|
||||
|
|
@ -503,6 +638,7 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(not game_ui.is_gameplay_hud_hidden())
|
||||
assert(not chat_ui.is_hud_hidden())
|
||||
assert(nameplate_layer.visible)
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ const MainScene: PackedScene = preload("res://main/main.tscn")
|
|||
const TEST_PORT: int = 18152
|
||||
const PREJOIN_MESSAGE: String = "private prejoin message"
|
||||
const LIVE_MESSAGE: String = "live message after join"
|
||||
const HOST_ROLEPLAY: String = "/me grabs [b]his fishing rod[/b]"
|
||||
const CLIENT_ROLEPLAY: String = "/me waves [color=red]hello[/color]"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
|
|
@ -39,11 +41,34 @@ func _run_host() -> void:
|
|||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
assert(not chat_service.send_local_message("/me"))
|
||||
assert(chat_service.send_local_message(PREJOIN_MESSAGE))
|
||||
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||
assert(remote_peer_id > 1)
|
||||
await create_timer(1.0).timeout
|
||||
assert(chat_service.send_local_message(LIVE_MESSAGE))
|
||||
assert(chat_service.send_local_message(HOST_ROLEPLAY))
|
||||
var client_roleplay_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < client_roleplay_deadline
|
||||
and not _history_contains(chat_service, CLIENT_ROLEPLAY)
|
||||
):
|
||||
await process_frame
|
||||
assert(_history_contains(chat_service, CLIENT_ROLEPLAY))
|
||||
var client_roleplay: Dictionary = _history_message(
|
||||
chat_service, CLIENT_ROLEPLAY
|
||||
)
|
||||
assert(int(client_roleplay.get("sender_peer_id", 0)) == remote_peer_id)
|
||||
assert(
|
||||
str(client_roleplay.get("sender_display_name", ""))
|
||||
== session.get_peer_record(remote_peer_id).display_name
|
||||
)
|
||||
_assert_roleplay_rendered(
|
||||
main,
|
||||
str(client_roleplay["sender_display_name"]),
|
||||
"waves [color=red]hello[/color]",
|
||||
)
|
||||
_assert_no_world_speech(main, remote_peer_id)
|
||||
var disconnect_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < disconnect_deadline
|
||||
|
|
@ -75,11 +100,39 @@ func _run_client() -> void:
|
|||
var live_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < live_deadline
|
||||
and not _history_contains(chat_service, LIVE_MESSAGE)
|
||||
and (
|
||||
not _history_contains(chat_service, LIVE_MESSAGE)
|
||||
or not _history_contains(chat_service, HOST_ROLEPLAY)
|
||||
)
|
||||
):
|
||||
await process_frame
|
||||
assert(_history_contains(chat_service, LIVE_MESSAGE))
|
||||
assert(_history_contains(chat_service, HOST_ROLEPLAY))
|
||||
assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
|
||||
var host_roleplay: Dictionary = _history_message(
|
||||
chat_service, HOST_ROLEPLAY
|
||||
)
|
||||
var host_peer_id: int = int(host_roleplay.get("sender_peer_id", 0))
|
||||
assert(host_peer_id > 0)
|
||||
assert(
|
||||
str(host_roleplay.get("sender_display_name", ""))
|
||||
== session.get_peer_record(host_peer_id).display_name
|
||||
)
|
||||
_assert_roleplay_rendered(
|
||||
main,
|
||||
str(host_roleplay["sender_display_name"]),
|
||||
"grabs [b]his fishing rod[/b]",
|
||||
)
|
||||
_assert_world_speech_body(main, host_peer_id, LIVE_MESSAGE)
|
||||
assert(chat_service.send_local_message(CLIENT_ROLEPLAY))
|
||||
var confirmation_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < confirmation_deadline
|
||||
and not _history_contains(chat_service, CLIENT_ROLEPLAY)
|
||||
):
|
||||
await process_frame
|
||||
assert(_history_contains(chat_service, CLIENT_ROLEPLAY))
|
||||
_assert_no_world_speech(main, session.get_local_peer_id())
|
||||
print("Chat privacy multiplayer client validation: PASS")
|
||||
await _session_cleanup(main, session)
|
||||
|
||||
|
|
@ -91,6 +144,59 @@ func _history_contains(service: NetworkChatService, body: String) -> bool:
|
|||
)
|
||||
|
||||
|
||||
func _history_message(
|
||||
service: NetworkChatService,
|
||||
body: String,
|
||||
) -> Dictionary:
|
||||
for message: Dictionary in service.get_history():
|
||||
if str(message.get("body", "")) == body:
|
||||
return message
|
||||
return {}
|
||||
|
||||
|
||||
func _assert_roleplay_rendered(
|
||||
main: Node,
|
||||
display_name: String,
|
||||
action: String,
|
||||
) -> void:
|
||||
var chat_ui := main.get_node("%GameUI").get_node("%ChatUI") as ChatUI
|
||||
var history := chat_ui.get("_history") as RichTextLabel
|
||||
assert(history != null)
|
||||
var expected: String = "%s %s" % [display_name, action]
|
||||
assert(history.get_parsed_text().contains(expected))
|
||||
assert(not history.get_parsed_text().contains(
|
||||
"%s: /me" % display_name
|
||||
))
|
||||
assert(not history.bbcode_enabled)
|
||||
var italic_font := history.get_theme_font("italics_font") as FontVariation
|
||||
assert(italic_font != null)
|
||||
assert(not is_zero_approx(italic_font.variation_transform.y.x))
|
||||
|
||||
|
||||
func _assert_no_world_speech(main: Node, peer_id: int) -> void:
|
||||
var chat_ui := main.get_node("%GameUI").get_node("%ChatUI") as ChatUI
|
||||
var speech: Dictionary = chat_ui.get("_speech")
|
||||
assert(not speech.has(peer_id))
|
||||
|
||||
|
||||
func _assert_world_speech_body(
|
||||
main: Node,
|
||||
peer_id: int,
|
||||
expected_body: String,
|
||||
) -> void:
|
||||
var chat_ui := main.get_node("%GameUI").get_node("%ChatUI") as ChatUI
|
||||
var speech: Dictionary = chat_ui.get("_speech")
|
||||
assert(speech.has(peer_id))
|
||||
var state: Dictionary = speech[peer_id]
|
||||
var bubble := state.get("bubble") as PanelContainer
|
||||
assert(bubble != null)
|
||||
for child: Node in bubble.get_children():
|
||||
if child is Label:
|
||||
assert((child as Label).text == expected_body)
|
||||
return
|
||||
assert(false, "Speech bubble label is missing.")
|
||||
|
||||
|
||||
func _create_initialized_main() -> Node:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
|
|
|
|||
|
|
@ -187,6 +187,19 @@ func _validate_primary_menu_navigation() -> void:
|
|||
)
|
||||
for title_control: Control in title_controls:
|
||||
_assert_directionally_reachable(title_control, title_controls)
|
||||
var version_row := title.get_node("%VersionRow") as Control
|
||||
title.call("_on_continue_pressed")
|
||||
await process_frame
|
||||
_expect(
|
||||
not version_row.visible,
|
||||
"The title version remains visible behind the Save Slots panel.",
|
||||
)
|
||||
title.call("_close_save_slots")
|
||||
await process_frame
|
||||
_expect(
|
||||
version_row.visible,
|
||||
"The title version was not restored after closing Save Slots.",
|
||||
)
|
||||
title.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
|
@ -221,7 +234,17 @@ func _validate_save_slots_navigation() -> void:
|
|||
await create_timer(0.25).timeout
|
||||
var saves_tab := page.get_node("%SavesTab") as Button
|
||||
var new_tab := page.get_node("%NewSlotTab") as Button
|
||||
var main_panel := page.get_node("%MainPanel") as PanelContainer
|
||||
var content_panel := page.get_node("%ContentPanel") as PanelContainer
|
||||
var back_button := page.get_node("%BackButton") as Button
|
||||
_expect(
|
||||
main_panel.size.is_equal_approx(Vector2(880.0, 660.0)),
|
||||
"Save Slots changed the fixed 880 by 660 outer panel size.",
|
||||
)
|
||||
_expect(
|
||||
main_panel.get_global_rect().encloses(back_button.get_global_rect()),
|
||||
"The Save Slots Back button extends beyond the outer panel.",
|
||||
)
|
||||
var tab_overlap: float = (
|
||||
saves_tab.get_global_rect().end.y
|
||||
- content_panel.get_global_rect().position.y
|
||||
|
|
@ -258,6 +281,40 @@ func _validate_save_slots_navigation() -> void:
|
|||
var duplicate_slot := page.get_node("%DuplicateSlotButton") as Button
|
||||
var export_slot := page.get_node("%ExportSlotButton") as Button
|
||||
var delete_slot := page.get_node("%DeleteSlotButton") as Button
|
||||
var slot_summary := page.get_node("%SlotSummary") as RichTextLabel
|
||||
slot_summary.text = str(page.call("_format_slot_summary", {
|
||||
"has_save": true,
|
||||
"player_level": 100,
|
||||
"catch_count": 9999,
|
||||
"discovered_species_count": 999,
|
||||
"wallet_balance": 999999,
|
||||
"world_layout": &"generated",
|
||||
"world_seed": 2147483647,
|
||||
"last_played_at_unix": 1788282000,
|
||||
}))
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
var content_rect: Rect2 = content_panel.get_global_rect()
|
||||
for control: Control in [
|
||||
import_button,
|
||||
page.get_node("%SelectedSlotName") as Control,
|
||||
slot_summary,
|
||||
page.get_node("%OnlineSlotButton") as Control,
|
||||
play_slot,
|
||||
rename_slot,
|
||||
duplicate_slot,
|
||||
export_slot,
|
||||
delete_slot,
|
||||
]:
|
||||
_expect(
|
||||
content_rect.encloses(control.get_global_rect()),
|
||||
"Save-slot control %s extends beyond the content panel."
|
||||
% control.name,
|
||||
)
|
||||
_expect(
|
||||
content_rect.end.y <= back_button.get_global_rect().position.y,
|
||||
"Save-slot content overlaps the Back button.",
|
||||
)
|
||||
_expect(
|
||||
saves_tab.find_valid_focus_neighbor(SIDE_BOTTOM) == import_button,
|
||||
"An empty Save Slots page does not lead from its tab to Import Save.",
|
||||
|
|
@ -960,6 +1017,18 @@ func _validate_profile_confirmation_focus() -> void:
|
|||
page.call("set_interactive", true)
|
||||
page.call("reset_controller_zone")
|
||||
var name_edit := page.get("_name_edit") as LineEdit
|
||||
var customize := page.get("_customize_button") as Button
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
_expect(
|
||||
root.gui_get_focus_owner() == customize,
|
||||
"Opening Profile activated the player-name field instead of its "
|
||||
+ "non-text customization action.",
|
||||
)
|
||||
_expect(
|
||||
not name_edit.has_focus(),
|
||||
"Opening Profile activated the player-name field without user input.",
|
||||
)
|
||||
name_edit.grab_focus()
|
||||
page.call("_on_name_changed", "V")
|
||||
_expect(
|
||||
|
|
|
|||
|
|
@ -49,12 +49,16 @@ func _run() -> void:
|
|||
|
||||
var player := main.get("_player") as Player
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
var decor_interaction := main.get(
|
||||
"_decor_shop_interaction"
|
||||
) as DecorShopInteraction
|
||||
var game_ui := main.get("_game_ui") as GameUI
|
||||
var chat_service := main.get_node(
|
||||
"%NetworkChatService"
|
||||
) as NetworkChatService
|
||||
_expect(player != null, "local player is unavailable")
|
||||
_expect(interaction != null, "shop interaction is unavailable")
|
||||
_expect(decor_interaction != null, "decor interaction is unavailable")
|
||||
_expect(game_ui != null, "game UI is unavailable")
|
||||
_expect(chat_service != null, "network chat service is unavailable")
|
||||
if (
|
||||
|
|
@ -70,6 +74,136 @@ func _run() -> void:
|
|||
interaction.is_local_player_in_range(),
|
||||
"local player did not enter the shop interaction range",
|
||||
)
|
||||
var shop_prompt := game_ui.get_node("%ShopPrompt") as Control
|
||||
var shop_message := game_ui.get_node("%ShopPromptMessage") as Label
|
||||
_expect(
|
||||
not bool(game_ui.get("_shop_npc_dialogue_active"))
|
||||
and not shop_message.visible,
|
||||
"shopkeeper spoke before the player initiated conversation",
|
||||
)
|
||||
_expect(
|
||||
shop_prompt.size == Vector2(30.0, 30.0),
|
||||
"nearby shop prompt was not reduced to the compact interaction glyph",
|
||||
)
|
||||
var fixed_area_transform := interaction.global_transform
|
||||
var shopkeeper := interaction.get_node("../Shopkeeper") as WorldCharacterDisplay
|
||||
var shopkeeper_start := shopkeeper.position
|
||||
var camera_yaw := player.get_node("CameraYaw") as Node3D
|
||||
var camera_pitch := player.get_node("CameraYaw/CameraPitch") as Node3D
|
||||
var spring_arm := player.get_node(
|
||||
"CameraYaw/CameraPitch/SpringArm3D"
|
||||
) as SpringArm3D
|
||||
var initial_camera_yaw_transform := camera_yaw.transform
|
||||
var initial_camera_pitch := camera_pitch.rotation
|
||||
var initial_spring_length := spring_arm.spring_length
|
||||
# Route planning treats the player as a live obstacle. Move the validation
|
||||
# avatar clear before forcing an ambient patrol leg.
|
||||
player.global_position = interaction.global_position + Vector3(20.0, 0.0, 0.0)
|
||||
interaction.call("_refresh_local_player_proximity")
|
||||
interaction.call("_choose_roaming_destination")
|
||||
var roaming_destination: Vector3 = interaction.get(
|
||||
"_roaming_destination"
|
||||
)
|
||||
var safe_route: Dictionary = interaction.call(
|
||||
"_resolve_safe_route_destination",
|
||||
roaming_destination,
|
||||
)
|
||||
_expect(
|
||||
not safe_route.is_empty(),
|
||||
"shopkeeper accepted a destination that failed its terrain route probe",
|
||||
)
|
||||
var initial_route_offset := roaming_destination - shopkeeper.position
|
||||
shopkeeper.rotation.y = (
|
||||
atan2(initial_route_offset.x, initial_route_offset.z) + PI
|
||||
)
|
||||
interaction.call("_process", 0.05)
|
||||
_expect(
|
||||
shopkeeper.position.is_equal_approx(shopkeeper_start)
|
||||
and not shopkeeper.is_locomotion_walking(),
|
||||
"shopkeeper translated before finishing its turn-in-place",
|
||||
)
|
||||
# A patrol leg deliberately starts by turning in place. Advance in small
|
||||
# slices until the body is aligned and translation begins.
|
||||
for _turn_frame: int in 20:
|
||||
interaction.call("_process", 0.05)
|
||||
if shopkeeper.position.distance_to(shopkeeper_start) > 0.001:
|
||||
break
|
||||
_expect(
|
||||
interaction.global_transform.is_equal_approx(fixed_area_transform),
|
||||
"ambient shopkeeper roaming moved the authoritative interaction area",
|
||||
)
|
||||
_expect(
|
||||
shopkeeper.position.distance_to(shopkeeper_start) > 0.001
|
||||
and shopkeeper.is_locomotion_walking(),
|
||||
"shopkeeper did not walk toward an ambient roaming destination",
|
||||
)
|
||||
var roaming_direction := (
|
||||
shopkeeper.position - shopkeeper_start
|
||||
).normalized()
|
||||
var roaming_origin: Vector3 = interaction.get("_roaming_origin")
|
||||
_expect(
|
||||
roaming_destination.distance_to(shopkeeper_start)
|
||||
>= interaction.minimum_route_distance
|
||||
and roaming_destination.distance_to(shopkeeper_start)
|
||||
<= interaction.maximum_route_distance + 0.001
|
||||
and roaming_destination.distance_to(roaming_origin)
|
||||
<= interaction.roaming_radius + 0.001
|
||||
and shopkeeper.basis.z.normalized().dot(roaming_direction) > 0.95,
|
||||
(
|
||||
"shopkeeper patrol leg was unsafe, too short, or did not face "
|
||||
+ "its travel direction"
|
||||
),
|
||||
)
|
||||
|
||||
# Ambient visuals may roam well beyond their original compact trigger, but
|
||||
# the trigger itself must remain fixed for multiplayer authority. Local UI
|
||||
# eligibility follows the moving character, while the host validates a
|
||||
# conservative authored-origin envelope that covers the full patrol.
|
||||
var captured_roaming_origin: Vector3 = interaction.get(
|
||||
"_roaming_origin"
|
||||
)
|
||||
interaction.roaming_enabled = false
|
||||
shopkeeper.position = captured_roaming_origin + Vector3(2.6, 0.0, 0.0)
|
||||
interaction.setup_local_player(player)
|
||||
_expect(
|
||||
(interaction.get("_roaming_origin") as Vector3).is_equal_approx(
|
||||
captured_roaming_origin
|
||||
),
|
||||
"rebinding the player recaptured a wandered position as the patrol origin",
|
||||
)
|
||||
player.global_position = shopkeeper.global_position + Vector3(1.4, 0.0, 0.0)
|
||||
interaction.call("_refresh_local_player_proximity")
|
||||
_expect(
|
||||
interaction.is_local_player_in_range()
|
||||
and interaction.is_avatar_in_range(player),
|
||||
"moving-shopkeeper reach was not covered locally and authoritatively",
|
||||
)
|
||||
player.global_position = interaction.call("_get_roaming_origin_global")
|
||||
interaction.call("_refresh_local_player_proximity")
|
||||
_expect(
|
||||
not interaction.is_local_player_in_range()
|
||||
and interaction.is_avatar_in_range(player),
|
||||
"local proximity still followed the fixed authored Area3D",
|
||||
)
|
||||
var authority_radius := interaction.get_authority_interaction_radius()
|
||||
player.global_position = (
|
||||
interaction.call("_get_roaming_origin_global")
|
||||
+ Vector3(authority_radius + 0.1, 0.0, 0.0)
|
||||
)
|
||||
_expect(
|
||||
not interaction.is_avatar_in_range(player),
|
||||
"shop authority accepted a player beyond the fixed patrol envelope",
|
||||
)
|
||||
shopkeeper.position = shopkeeper_start
|
||||
interaction.roaming_enabled = true
|
||||
player.global_position = (
|
||||
shopkeeper.global_position + Vector3(0.0, 0.0, -1.0)
|
||||
)
|
||||
interaction.call("_refresh_local_player_proximity")
|
||||
_expect(
|
||||
interaction.global_transform.is_equal_approx(fixed_area_transform),
|
||||
"proximity validation moved the authoritative interaction area",
|
||||
)
|
||||
var character_calls: Array[String] = []
|
||||
var capture_call := func(
|
||||
_peer_id: int,
|
||||
|
|
@ -94,12 +228,57 @@ func _run() -> void:
|
|||
main.call("_unhandled_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
game_ui.get_fishing_shop().visible,
|
||||
"Y did not open the shop while the player was in range",
|
||||
not game_ui.get_fishing_shop().visible
|
||||
and main.get("_engaged_shop_npc_interaction") == interaction
|
||||
and interaction.is_engaged(),
|
||||
"first Y press did not enter shopkeeper dialogue without opening",
|
||||
)
|
||||
_expect(
|
||||
bool(game_ui.get("_shop_npc_dialogue_active"))
|
||||
and shop_message.visible
|
||||
and shop_message.text == FishingShopInteraction.DIALOGUE_TEXT,
|
||||
"engaged shopkeeper dialogue was not presented",
|
||||
)
|
||||
_expect(
|
||||
not bool(player.call("_is_movement_input_enabled"))
|
||||
and not bool(player.call("_is_camera_input_enabled"))
|
||||
and player.is_shop_conversation_camera_active(),
|
||||
"shopkeeper dialogue did not lock movement and hold its camera frame",
|
||||
)
|
||||
_expect(
|
||||
character_calls.is_empty(),
|
||||
"Y played a character call instead of opening the shop",
|
||||
"Y played a character call instead of starting shopkeeper dialogue",
|
||||
)
|
||||
await create_timer(0.5).timeout
|
||||
var player_to_shopkeeper := (
|
||||
shopkeeper.global_position - player.global_position
|
||||
)
|
||||
player_to_shopkeeper.y = 0.0
|
||||
player_to_shopkeeper = player_to_shopkeeper.normalized()
|
||||
_expect(
|
||||
camera_yaw.global_basis.x.normalized().dot(player_to_shopkeeper)
|
||||
> 0.9
|
||||
and spring_arm.spring_length < initial_spring_length,
|
||||
"conversation camera did not frame player-left and NPC-right",
|
||||
)
|
||||
_expect(
|
||||
shopkeeper.global_basis.z.normalized().dot(
|
||||
(player.global_position - shopkeeper.global_position).normalized()
|
||||
) > 0.9
|
||||
and not shopkeeper.is_locomotion_walking(),
|
||||
"engaged shopkeeper did not stop and face the player",
|
||||
)
|
||||
main.call("_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
game_ui.get_fishing_shop().visible
|
||||
and bool(main.get("_shop_conversation_shop_opened")),
|
||||
"second Y press did not dismiss dialogue and open the shop",
|
||||
)
|
||||
_expect(
|
||||
main.get("_engaged_shop_npc_interaction") == interaction
|
||||
and not bool(player.call("_is_camera_input_enabled")),
|
||||
"shop opening released the conversation frame before close",
|
||||
)
|
||||
player.global_position = interaction.global_position + Vector3(20.0, 0.0, 0.0)
|
||||
for _frame: int in 6:
|
||||
|
|
@ -108,6 +287,12 @@ func _run() -> void:
|
|||
if not game_ui.get_fishing_shop().visible:
|
||||
break
|
||||
await process_frame
|
||||
for _frame: int in 40:
|
||||
if not player.is_shop_conversation_camera_active():
|
||||
break
|
||||
await process_frame
|
||||
if player.is_shop_conversation_camera_active():
|
||||
await create_timer(0.55).timeout
|
||||
_expect(
|
||||
not interaction.is_local_player_in_range(),
|
||||
"local player did not leave the shop interaction range",
|
||||
|
|
@ -116,12 +301,219 @@ func _run() -> void:
|
|||
not game_ui.get_fishing_shop().visible,
|
||||
"shop did not close after leaving its interaction range",
|
||||
)
|
||||
_expect(
|
||||
main.get("_engaged_shop_npc_interaction") == null
|
||||
and bool(player.call("_is_movement_input_enabled"))
|
||||
and bool(player.call("_is_camera_input_enabled")),
|
||||
(
|
||||
"shop close did not restore conversation controls and camera "
|
||||
+ "(engaged=%s movement=%s camera=%s restoring=%s)"
|
||||
) % [
|
||||
main.get("_engaged_shop_npc_interaction"),
|
||||
player.call("_is_movement_input_enabled"),
|
||||
player.call("_is_camera_input_enabled"),
|
||||
main.get("_shop_conversation_camera_restoring"),
|
||||
],
|
||||
)
|
||||
_expect(
|
||||
camera_yaw.transform.is_equal_approx(
|
||||
initial_camera_yaw_transform
|
||||
)
|
||||
and camera_pitch.rotation.is_equal_approx(initial_camera_pitch)
|
||||
and is_equal_approx(spring_arm.spring_length, initial_spring_length),
|
||||
"conversation camera did not restore its exact pre-dialogue frame",
|
||||
)
|
||||
game_ui.call("_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
character_calls.size() == 1,
|
||||
"Y did not play a character call away from an interaction",
|
||||
)
|
||||
|
||||
# Deliberately overlap two shop margins to validate the transition case.
|
||||
# Whichever character the player is actually closest to must own both the
|
||||
# dialogue/prompt and the interaction button, regardless of shop type.
|
||||
if decor_interaction != null:
|
||||
var fishing_root := interaction.get_parent() as Node3D
|
||||
var decor_root := decor_interaction.get_parent() as Node3D
|
||||
var original_decor_transform := decor_root.global_transform
|
||||
interaction.roaming_enabled = false
|
||||
decor_interaction.roaming_enabled = false
|
||||
(interaction.get_node("../Shopkeeper") as Node3D).position = Vector3.ZERO
|
||||
(decor_interaction.get_node("../Shopkeeper") as Node3D).position = Vector3.ZERO
|
||||
decor_root.global_position = (
|
||||
fishing_root.global_position + Vector3(1.1, 0.0, 0.0)
|
||||
)
|
||||
player.global_position = decor_root.global_position
|
||||
for _frame: int in 6:
|
||||
await physics_frame
|
||||
_expect(
|
||||
interaction.is_local_player_in_range()
|
||||
and decor_interaction.is_local_player_in_range(),
|
||||
"overlapping shop transition margins were not established",
|
||||
)
|
||||
_expect(
|
||||
main.call("_get_active_shop_npc_interaction")
|
||||
== decor_interaction,
|
||||
"nearest decor NPC did not own the overlapping interaction",
|
||||
)
|
||||
_expect(
|
||||
int(game_ui.get("_shop_npc_source_id"))
|
||||
== decor_interaction.get_instance_id()
|
||||
and shop_message.text
|
||||
== DecorShopInteraction.DECOR_DIALOGUE_TEXT,
|
||||
"decor dialogue did not follow the active NPC handoff",
|
||||
)
|
||||
player.global_position = (
|
||||
fishing_root.global_position + Vector3(0.57, 0.0, 0.0)
|
||||
)
|
||||
for _frame: int in 3:
|
||||
await physics_frame
|
||||
_expect(
|
||||
main.call("_get_active_shop_npc_interaction")
|
||||
== decor_interaction,
|
||||
"shop handoff hysteresis did not stabilize a near-tie",
|
||||
)
|
||||
player.global_position = (
|
||||
fishing_root.global_position + Vector3(0.45, 0.0, 0.0)
|
||||
)
|
||||
for _frame: int in 3:
|
||||
await physics_frame
|
||||
_expect(
|
||||
main.call("_get_active_shop_npc_interaction")
|
||||
== interaction,
|
||||
"shop ownership did not switch after a clear distance lead",
|
||||
)
|
||||
_expect(
|
||||
int(game_ui.get("_shop_npc_source_id"))
|
||||
== interaction.get_instance_id()
|
||||
and shop_message.text == FishingShopInteraction.DIALOGUE_TEXT,
|
||||
"fishing dialogue did not replace the prior NPC cleanly",
|
||||
)
|
||||
player.global_position = decor_root.global_position
|
||||
for _frame: int in 3:
|
||||
await physics_frame
|
||||
game_ui.call("_input", press)
|
||||
main.call("_unhandled_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
not game_ui.get_decor_shop().visible
|
||||
and main.get("_engaged_shop_npc_interaction")
|
||||
== decor_interaction,
|
||||
"first Y did not engage the nearest decor NPC in an overlap",
|
||||
)
|
||||
main.call("_input", press)
|
||||
await process_frame
|
||||
_expect(
|
||||
game_ui.get_decor_shop().visible
|
||||
and main.get("_engaged_shop_npc_interaction")
|
||||
== decor_interaction,
|
||||
"second Y did not open the engaged decor shop",
|
||||
)
|
||||
_expect(
|
||||
character_calls.size() == 1,
|
||||
"Y emitted a character call instead of opening the decor shop",
|
||||
)
|
||||
game_ui.get_decor_shop().close_shop()
|
||||
for _frame: int in 40:
|
||||
if not player.is_shop_conversation_camera_active():
|
||||
break
|
||||
await process_frame
|
||||
if player.is_shop_conversation_camera_active():
|
||||
await create_timer(0.55).timeout
|
||||
player.global_position = fishing_root.global_position
|
||||
for _frame: int in 6:
|
||||
await physics_frame
|
||||
_expect(
|
||||
main.call("_get_active_shop_npc_interaction")
|
||||
== interaction,
|
||||
"shop ownership did not hand back to the nearer fishing NPC",
|
||||
)
|
||||
decor_root.global_transform = original_decor_transform
|
||||
|
||||
# Idle visuals can still begin a frame overlapped (for example after two
|
||||
# independently planned routes converge). They must separate without
|
||||
# moving either authoritative Area3D, deadlocking on their reservations,
|
||||
# or waiting for one NPC to already be in the walking state.
|
||||
var fishing_visual := interaction.get_node(
|
||||
"../Shopkeeper"
|
||||
) as WorldCharacterDisplay
|
||||
var decor_visual := decor_interaction.get_node(
|
||||
"../Shopkeeper"
|
||||
) as WorldCharacterDisplay
|
||||
var fishing_visual_transform := fishing_visual.global_transform
|
||||
var decor_visual_transform := decor_visual.global_transform
|
||||
var fishing_area_transform := interaction.global_transform
|
||||
var decor_area_transform := decor_interaction.global_transform
|
||||
player.global_position = fishing_visual.global_position + Vector3(20.0, 0.0, 0.0)
|
||||
interaction.roaming_enabled = true
|
||||
decor_interaction.roaming_enabled = true
|
||||
interaction.call("_schedule_idle")
|
||||
decor_interaction.call("_schedule_idle")
|
||||
var crossing_center := fishing_visual.global_position
|
||||
decor_visual.global_position = (
|
||||
crossing_center + Vector3(1.0, 0.0, -1.0)
|
||||
)
|
||||
decor_interaction.set(
|
||||
"_roaming_destination",
|
||||
decor_visual.get_parent_node_3d().to_local(
|
||||
crossing_center + Vector3(1.0, 0.0, 1.0)
|
||||
),
|
||||
)
|
||||
decor_interaction.set("_roaming_walking", true)
|
||||
_expect(
|
||||
bool(interaction.call(
|
||||
"_route_conflicts_with_reserved_corridors",
|
||||
crossing_center,
|
||||
crossing_center + Vector3(2.0, 0.0, 0.0),
|
||||
)),
|
||||
"crossing shopkeeper route corridors were not reserved",
|
||||
)
|
||||
decor_interaction.call("_schedule_idle")
|
||||
var fishing_priority: String = interaction.call(
|
||||
"_traffic_priority_key"
|
||||
)
|
||||
var decor_priority: String = decor_interaction.call(
|
||||
"_traffic_priority_key"
|
||||
)
|
||||
var blocked_outward_direction := (
|
||||
Vector3.RIGHT
|
||||
if fishing_priority < decor_priority
|
||||
else Vector3.LEFT
|
||||
)
|
||||
fishing_visual.global_position = (
|
||||
interaction.call("_get_roaming_origin_global")
|
||||
+ blocked_outward_direction * interaction.roaming_radius
|
||||
)
|
||||
decor_visual.global_position = fishing_visual.global_position
|
||||
var overlapped_position := fishing_visual.global_position
|
||||
for _separation_frame: int in 80:
|
||||
interaction.call("_process", 0.05)
|
||||
decor_interaction.call("_process", 0.05)
|
||||
var separation := fishing_visual.global_position - decor_visual.global_position
|
||||
separation.y = 0.0
|
||||
if separation.length() >= interaction.other_shopkeeper_clearance:
|
||||
break
|
||||
var resolved_separation := (
|
||||
fishing_visual.global_position - decor_visual.global_position
|
||||
)
|
||||
resolved_separation.y = 0.0
|
||||
_expect(
|
||||
resolved_separation.length()
|
||||
>= interaction.other_shopkeeper_clearance - 0.02
|
||||
and fishing_visual.global_position.distance_to(overlapped_position)
|
||||
> 0.1,
|
||||
"idle-overlapped shopkeepers did not separate and resume safely",
|
||||
)
|
||||
_expect(
|
||||
interaction.global_transform.is_equal_approx(fishing_area_transform)
|
||||
and decor_interaction.global_transform.is_equal_approx(
|
||||
decor_area_transform
|
||||
),
|
||||
"live shopkeeper separation moved an authoritative Area3D",
|
||||
)
|
||||
fishing_visual.global_transform = fishing_visual_transform
|
||||
decor_visual.global_transform = decor_visual_transform
|
||||
chat_service.character_call_received.disconnect(capture_call)
|
||||
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
|
|
|
|||
|
|
@ -622,7 +622,6 @@ func _test_fishing_shop_sale_ui(
|
|||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var shop := game_ui.get_node("%FishingShop") as FishingShop
|
||||
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||
var shop_backdrop := main.get_node("%ShopBackdrop") as ColorRect
|
||||
var ui_viewport := main.get_node(
|
||||
"UIPresentation/UIViewport"
|
||||
) as SubViewport
|
||||
|
|
@ -630,7 +629,6 @@ func _test_fishing_shop_sale_ui(
|
|||
interaction != null
|
||||
and shop != null
|
||||
and player_menu != null
|
||||
and shop_backdrop != null
|
||||
and ui_viewport != null
|
||||
)
|
||||
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||
|
|
@ -701,8 +699,7 @@ func _test_fishing_shop_sale_ui(
|
|||
assert(shop.open_shop())
|
||||
for _frame: int in 2:
|
||||
await process_frame
|
||||
assert(shop_backdrop.visible)
|
||||
assert(shop_backdrop.material is ShaderMaterial)
|
||||
assert(main.get_node_or_null("%ShopBackdrop") == null)
|
||||
assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer"))
|
||||
var shop_panel := shop.get_node("%ShopPanel") as PanelContainer
|
||||
assert(shop_panel != null and shop_panel.size == Vector2(980.0, 600.0))
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ func _run() -> void:
|
|||
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
|
||||
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
var showcase_capability: StringName = NetworkFishShowcaseProtocol.CAPABILITY
|
||||
assert(protocol_version == 12)
|
||||
assert(protocol_version == 13)
|
||||
assert(enet_channel_count == 18)
|
||||
assert(
|
||||
fish_quality_capability == "fish_quality_v1"
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func _run() -> void:
|
|||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
|
||||
)
|
||||
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
assert(protocol_version == 12)
|
||||
assert(protocol_version == 13)
|
||||
assert(enet_channel_count == 18)
|
||||
assert(movement_animation_channel == 11)
|
||||
assert(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func _run_host() -> void:
|
|||
"%PlayerSpawnService"
|
||||
) as PlayerSpawnService
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var home_state := main.get_node("%PlayerHomeState") as PlayerHomeState
|
||||
assert(bool(main.call(
|
||||
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||
)))
|
||||
|
|
@ -59,6 +60,60 @@ func _run_host() -> void:
|
|||
await physics_frame
|
||||
assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
|
||||
assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint))
|
||||
var upgrade_result_id := "home-test-upgrade"
|
||||
network_home.authorize_home_upgrade(
|
||||
session.get_local_peer_id(),
|
||||
PlayerHomeState.STARTER_RV_TIER,
|
||||
upgrade_result_id,
|
||||
)
|
||||
assert(home_state.upgrade_to_next_tier())
|
||||
var rv_deadline: int = Time.get_ticks_msec() + 5000
|
||||
while (
|
||||
Time.get_ticks_msec() < rv_deadline
|
||||
and home_world.get_interior_exit_positions(host_fingerprint).size() < 2
|
||||
):
|
||||
await process_frame
|
||||
var rv_exits: Array[Vector3] = home_world.get_interior_exit_positions(
|
||||
host_fingerprint
|
||||
)
|
||||
assert(rv_exits.size() == 2)
|
||||
network_home.finalize_home_upgrade(
|
||||
session.get_local_peer_id(), upgrade_result_id, true
|
||||
)
|
||||
assert(rv_exits[0].x < rv_exits[1].x)
|
||||
for exit_position: Vector3 in rv_exits:
|
||||
host_avatar.global_position = (
|
||||
exit_position
|
||||
+ home_world.get_interior_exit_direction_near(
|
||||
host_fingerprint, exit_position
|
||||
) * 0.05
|
||||
)
|
||||
assert(home_world.has_avatar_crossed_interior_exit_threshold(
|
||||
host_avatar, host_fingerprint
|
||||
))
|
||||
host_avatar.global_position = rv_exits[0] + Vector3(0.0, 0.0, 1.1)
|
||||
assert(not home_world.has_avatar_crossed_interior_exit_threshold(
|
||||
host_avatar, host_fingerprint
|
||||
))
|
||||
var presentations: Dictionary = home_world.get(
|
||||
"_presentations_by_fingerprint"
|
||||
)
|
||||
var host_presentation: Dictionary = presentations.get(host_fingerprint, {})
|
||||
var host_interior := host_presentation.get("interior") as Node3D
|
||||
assert(host_interior != null)
|
||||
var wall_collision := host_interior.find_child(
|
||||
"WallWestCollision", true, false
|
||||
) as StaticBody3D
|
||||
assert(wall_collision != null)
|
||||
assert(bool(wall_collision.get_meta(
|
||||
HomeWorldService.INTERIOR_SHELL_COLLISION_META, false
|
||||
)))
|
||||
var wall_shape := wall_collision.get_child(0) as CollisionShape3D
|
||||
assert(wall_shape != null)
|
||||
assert((wall_shape.shape as ConcavePolygonShape3D).backface_collision)
|
||||
host_avatar.global_position = host_spawn.origin
|
||||
assert(network_home.request_enter(host_fingerprint))
|
||||
await physics_frame
|
||||
|
||||
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||
assert(remote_peer_id > 1)
|
||||
|
|
@ -66,6 +121,53 @@ func _run_host() -> void:
|
|||
var remote_fingerprint: String = remote_record.identity_fingerprint
|
||||
assert(await _wait_for_home(network_home, remote_fingerprint))
|
||||
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||
assert(not remote_avatar.is_remote_presentation_visible())
|
||||
var chat_ui := main.get_node("%GameUI").get_node("%ChatUI") as ChatUI
|
||||
chat_ui.call(
|
||||
"_show_speech_bubble", remote_peer_id, "outside", remote_fingerprint
|
||||
)
|
||||
var speech_states: Dictionary = chat_ui.get("_speech")
|
||||
assert(not speech_states.has(remote_peer_id))
|
||||
var toolbar := main.get_node("%GameUI").get_node(
|
||||
"%HomeDecorToolbar"
|
||||
) as HomeDecorToolbar
|
||||
assert(toolbar.visible)
|
||||
var exit_button := toolbar.find_child(
|
||||
"ExitRVButton", true, false
|
||||
) as Button
|
||||
assert(exit_button != null and exit_button.text == "Exit RV")
|
||||
var storage := main.get_node("%GameUI").get_node(
|
||||
"%PlayerStorage"
|
||||
) as PlayerStorage
|
||||
assert(storage.open_storage_from_home(true))
|
||||
var close_storage_event := InputEventAction.new()
|
||||
close_storage_event.action = &"interact"
|
||||
close_storage_event.pressed = true
|
||||
storage._unhandled_input(close_storage_event)
|
||||
assert(not storage.visible)
|
||||
exit_button.pressed.emit()
|
||||
var exit_deadline: int = Time.get_ticks_msec() + 5000
|
||||
while (
|
||||
Time.get_ticks_msec() < exit_deadline
|
||||
and not network_home.get_local_home_owner_fingerprint().is_empty()
|
||||
):
|
||||
await process_frame
|
||||
assert(network_home.get_local_home_owner_fingerprint().is_empty())
|
||||
var visibility_deadline: int = Time.get_ticks_msec() + 1000
|
||||
while (
|
||||
Time.get_ticks_msec() < visibility_deadline
|
||||
and is_instance_valid(remote_avatar)
|
||||
and not remote_avatar.is_remote_presentation_visible()
|
||||
):
|
||||
await process_frame
|
||||
assert(is_instance_valid(remote_avatar))
|
||||
assert(remote_avatar.is_remote_presentation_visible())
|
||||
chat_ui.call(
|
||||
"_show_speech_bubble", remote_peer_id, "inside", remote_fingerprint
|
||||
)
|
||||
speech_states = chat_ui.get("_speech")
|
||||
assert(speech_states.has(remote_peer_id))
|
||||
chat_ui.call("_on_peer_removed", remote_peer_id)
|
||||
var remote_spawn: Transform3D = home_world.get_exterior_spawn_transform(
|
||||
remote_fingerprint
|
||||
)
|
||||
|
|
@ -124,6 +226,10 @@ func _run_client() -> void:
|
|||
player.global_position, exterior_spawn.origin
|
||||
))
|
||||
assert(_faces_away_from_home(player, home_world, fingerprint))
|
||||
# Keep the peer present long enough for the host to rebuild the upgraded RV,
|
||||
# exercise late-spawn visibility, and use the toolbar transition back to the
|
||||
# shared world. A two-second grace period raced the host on slower machines.
|
||||
await create_timer(10.0).timeout
|
||||
print("Home spawn multiplayer client validation: PASS")
|
||||
await _cleanup(main, session)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,21 +105,7 @@ func _validate_main_profile(main: Node) -> void:
|
|||
assert(not bool(title_material.get_shader_parameter(
|
||||
"animation_enabled"
|
||||
)))
|
||||
for backdrop_name: StringName in [
|
||||
&"PlayerMenuBackdrop",
|
||||
&"ShopBackdrop",
|
||||
]:
|
||||
var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
|
||||
var backdrop_material := backdrop.material as ShaderMaterial
|
||||
assert(backdrop_material != null)
|
||||
assert(not bool(backdrop_material.get_shader_parameter(
|
||||
"animation_enabled"
|
||||
)))
|
||||
assert(
|
||||
backdrop_material.get_shader_parameter(
|
||||
"scroll_velocity_pixels"
|
||||
) == Vector2.ZERO
|
||||
)
|
||||
_validate_no_full_screen_menu_backdrops(main)
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var title_screen := game_ui.get_title_screen()
|
||||
assert(title_screen != null)
|
||||
|
|
@ -166,21 +152,7 @@ func _validate_normal_profile(main: Node) -> void:
|
|||
var title_material := title_background.material as ShaderMaterial
|
||||
assert(title_material != null)
|
||||
assert(bool(title_material.get_shader_parameter("animation_enabled")))
|
||||
for backdrop_name: StringName in [
|
||||
&"PlayerMenuBackdrop",
|
||||
&"ShopBackdrop",
|
||||
]:
|
||||
var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
|
||||
var backdrop_material := backdrop.material as ShaderMaterial
|
||||
assert(backdrop_material != null)
|
||||
assert(bool(backdrop_material.get_shader_parameter(
|
||||
"animation_enabled"
|
||||
)))
|
||||
assert(
|
||||
backdrop_material.get_shader_parameter(
|
||||
"scroll_velocity_pixels"
|
||||
) != Vector2.ZERO
|
||||
)
|
||||
_validate_no_full_screen_menu_backdrops(main)
|
||||
var visuals: WorldTimeVisualController = main.get_node(
|
||||
"%WorldTimeVisualController"
|
||||
)
|
||||
|
|
@ -216,6 +188,11 @@ func _validate_new_game_music_transition(main: Node) -> void:
|
|||
assert(title_music.playing)
|
||||
|
||||
|
||||
func _validate_no_full_screen_menu_backdrops(main: Node) -> void:
|
||||
assert(main.get_node_or_null("%PlayerMenuBackdrop") == null)
|
||||
assert(main.get_node_or_null("%ShopBackdrop") == null)
|
||||
|
||||
|
||||
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
|
||||
var fresh_water_root := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ func _run() -> void:
|
|||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||
var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage
|
||||
var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect
|
||||
var hotbar := game_ui.get_node("%Hotbar") as Control
|
||||
var fishing_spot := main.get("_fishing_spot") as FishingSpot
|
||||
assert(logbook.get("_fishing_spot") == fishing_spot)
|
||||
|
|
@ -49,7 +48,7 @@ func _run() -> void:
|
|||
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
|
||||
assert(player_menu.visible)
|
||||
assert(logbook.visible)
|
||||
assert(backdrop.visible)
|
||||
assert(main.get_node_or_null("%PlayerMenuBackdrop") == null)
|
||||
assert(not bool(game_ui.get("_player_menu_hotbar_visible")))
|
||||
assert(not hotbar.visible)
|
||||
|
||||
|
|
|
|||
|
|
@ -195,11 +195,56 @@ func _validate_compact_animation_encoding() -> void:
|
|||
func _validate_airborne_sitting(avatar: Player) -> void:
|
||||
avatar.reset_network_movement_state()
|
||||
avatar.set_local_control(true)
|
||||
avatar.set("_sit_after_landing", true)
|
||||
avatar.global_position = Vector3(0.0, 2.0, 0.0)
|
||||
avatar.velocity = Vector3(0.0, -1.0, 0.0)
|
||||
avatar.toggle_sitting()
|
||||
assert(avatar.get_network_sitting_intent())
|
||||
assert(not avatar.get_network_sitting_state())
|
||||
assert(bool(avatar.get("_sitting_intent_pending")))
|
||||
assert(not bool(avatar.make_network_snapshot(2)["sitting"]))
|
||||
assert(bool(avatar.capture_network_input(1)["sitting"]))
|
||||
|
||||
# Reproduce the high-RTT owner audit race: the host has reached the ground
|
||||
# and reports the deferred sit while this client's predicted capsule is still
|
||||
# airborne. Repeated seated snapshots must not zero its falling velocity.
|
||||
var delayed_sit_snapshot: Dictionary = _network_snapshot(
|
||||
avatar.global_position,
|
||||
Vector3.ZERO,
|
||||
1,
|
||||
)
|
||||
delayed_sit_snapshot["sitting"] = true
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_sit_snapshot,
|
||||
1,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(not avatar.is_sitting())
|
||||
assert(bool(avatar.get("_sit_after_landing")))
|
||||
assert(avatar.velocity.y < 0.0)
|
||||
var airborne_height: float = avatar.global_position.y
|
||||
avatar.call("_simulate_movement_physics", 0.1)
|
||||
assert(avatar.global_position.y < airborne_height)
|
||||
assert(avatar.velocity.y < -1.0)
|
||||
for _delayed_audit: int in 3:
|
||||
delayed_sit_snapshot["position"] = [
|
||||
avatar.global_position.x,
|
||||
avatar.global_position.y,
|
||||
avatar.global_position.z,
|
||||
]
|
||||
var falling_velocity: float = avatar.velocity.y
|
||||
avatar.apply_local_prediction_correction(
|
||||
delayed_sit_snapshot,
|
||||
1,
|
||||
1.0 / 30.0,
|
||||
0.3,
|
||||
0.1,
|
||||
)
|
||||
assert(not avatar.is_sitting())
|
||||
assert(is_equal_approx(avatar.velocity.y, falling_velocity))
|
||||
avatar.call("_simulate_movement_physics", 0.1)
|
||||
assert(avatar.global_position.y < airborne_height)
|
||||
avatar.call("_queue_local_network_jump_intent")
|
||||
var jumping_input: Dictionary = avatar.capture_network_input(2)
|
||||
assert(bool(jumping_input["jump"]))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ func _initialize() -> void:
|
|||
func _validate_protocol() -> void:
|
||||
_validate_type_and_empty_handling()
|
||||
_validate_message_cleanup()
|
||||
_validate_roleplay_commands()
|
||||
_validate_protocol_limits()
|
||||
print("Network chat protocol validation: PASS")
|
||||
quit()
|
||||
|
|
@ -34,6 +35,28 @@ func _validate_message_cleanup() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _validate_roleplay_commands() -> void:
|
||||
assert(NetworkChatProtocol.is_roleplay_body("/me waves"))
|
||||
assert(NetworkChatProtocol.is_roleplay_body("/ME waves"))
|
||||
assert(NetworkChatProtocol.is_roleplay_body("/me"))
|
||||
assert(not NetworkChatProtocol.is_roleplay_body("/meow"))
|
||||
assert(NetworkChatProtocol.roleplay_action("/me waves") == "waves")
|
||||
assert(NetworkChatProtocol.roleplay_action(
|
||||
"/me [color=red]waves[/color]"
|
||||
) == "[color=red]waves[/color]")
|
||||
assert(NetworkChatProtocol.valid_user_body("/me waves"))
|
||||
assert(not NetworkChatProtocol.valid_user_body("/me"))
|
||||
assert(not NetworkChatProtocol.valid_user_body("/me "))
|
||||
assert(NetworkChatProtocol.valid_user_body("/meow"))
|
||||
var signed_fields: Array = NetworkChatProtocol.signature_fields({
|
||||
"session_id": "session",
|
||||
"request_id": "request",
|
||||
"sender_fingerprint": "fingerprint",
|
||||
"body": "/me waves",
|
||||
})
|
||||
assert(signed_fields.back() == "/me waves")
|
||||
|
||||
|
||||
func _validate_protocol_limits() -> void:
|
||||
var maximum_body: String = "a".repeat(
|
||||
NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ func _run_host() -> void:
|
|||
var profile := main.get_node(
|
||||
"%NetworkProfilePreferences"
|
||||
) as NetworkProfilePreferences
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
assert(save_manager.initialize_new_game())
|
||||
assert(profile.set_profile_identity(
|
||||
profile.display_name,
|
||||
"deep",
|
||||
|
|
@ -55,7 +57,6 @@ func _run_host() -> void:
|
|||
profile.call_id,
|
||||
"kim",
|
||||
))
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
assert(bool(main.call(
|
||||
"_apply_world", WorldLayout.STARTER_ISLAND, 1, true
|
||||
)))
|
||||
|
|
@ -67,7 +68,6 @@ func _run_host() -> void:
|
|||
assert(local_avatar != null)
|
||||
assert(local_avatar.get_animalese_voice_id() == "deep")
|
||||
assert(local_avatar.get_animalese_sample_set_id() == "kim")
|
||||
assert(save_manager.initialize_new_game())
|
||||
main.call("_enter_gameplay")
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
|
|
@ -99,6 +99,7 @@ func _run_host() -> void:
|
|||
await _wait_for_remote_appearance(session, remote_peer_id, changed)
|
||||
await _wait_for_remote_appearance(session, remote_peer_id, original)
|
||||
await _wait_for_remote_appearance(session, remote_peer_id, changed)
|
||||
await _wait_for_remote_title(session, remote_peer_id, "trail guide")
|
||||
var avatar := main.get_node("%PlayerSpawnService").get_avatar(
|
||||
remote_peer_id
|
||||
) as Player
|
||||
|
|
@ -187,6 +188,7 @@ func _run_client() -> void:
|
|||
service.get_persisted_speech_speed_id(),
|
||||
service.get_persisted_call_id(),
|
||||
"kim",
|
||||
"trail guide",
|
||||
))
|
||||
var apply_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while Time.get_ticks_msec() < apply_deadline and apply_results.is_empty():
|
||||
|
|
@ -196,6 +198,11 @@ func _run_client() -> void:
|
|||
assert(service.get_persisted_appearance() == changed)
|
||||
assert(service.get_persisted_voice_id() == "deep")
|
||||
assert(service.get_persisted_sample_set_id() == "kim")
|
||||
assert(service.get_persisted_title() == "trail guide")
|
||||
assert(
|
||||
session.get_peer_record(session.get_local_peer_id()).title
|
||||
== "trail guide"
|
||||
)
|
||||
assert(Dictionary(session.get("_local_appearance_snapshot")) == changed)
|
||||
var player := main.get_node("%PlayerSpawnService").get_local_player() as Player
|
||||
assert(player.get_animalese_voice_id() == "deep")
|
||||
|
|
@ -278,6 +285,20 @@ func _wait_for_remote_appearance(
|
|||
assert(false, "Timed out waiting for a remote appearance update.")
|
||||
|
||||
|
||||
func _wait_for_remote_title(
|
||||
session: NetworkSession,
|
||||
peer_id: int,
|
||||
expected: String,
|
||||
) -> void:
|
||||
var deadline: int = Time.get_ticks_msec() + 8000
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
await process_frame
|
||||
var record := session.get_peer_record(peer_id)
|
||||
if record != null and record.title == expected:
|
||||
return
|
||||
assert(false, "Timed out waiting for a remote player title update.")
|
||||
|
||||
|
||||
func _create_initialized_main() -> Node:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
|
|
|
|||
151
tests/profile_title_validation.gd
Normal file
151
tests/profile_title_validation.gd
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
extends SceneTree
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_profile_defaults_and_legacy_data()
|
||||
_validate_network_payloads()
|
||||
_validate_peer_registry_title()
|
||||
await _validate_profile_layout()
|
||||
print("Profile title validation: PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _validate_profile_defaults_and_legacy_data() -> void:
|
||||
var preferences := NetworkProfilePreferences.new()
|
||||
assert(preferences.display_name == "stray")
|
||||
assert(preferences.title == "newbie")
|
||||
assert(NetworkProfilePreferences.is_valid_title("trail guide"))
|
||||
assert(not NetworkProfilePreferences.is_valid_title(""))
|
||||
assert(not NetworkProfilePreferences.is_valid_title("x".repeat(
|
||||
NetworkProtocol.MAX_PLAYER_TITLE_LENGTH + 1
|
||||
)))
|
||||
preferences.profile_id = "profile-id"
|
||||
preferences.created_at_unix = 1
|
||||
var save_data := preferences.to_save_data()
|
||||
assert(str(save_data.get("title", "")) == "newbie")
|
||||
assert(NetworkProfilePreferences.validate_save_data(save_data))
|
||||
# Title was added without invalidating existing per-save character records.
|
||||
var legacy_save := save_data.duplicate(true)
|
||||
legacy_save.erase("title")
|
||||
assert(NetworkProfilePreferences.validate_save_data(legacy_save))
|
||||
legacy_save["title"] = 7
|
||||
assert(not NetworkProfilePreferences.validate_save_data(legacy_save))
|
||||
preferences.free()
|
||||
|
||||
|
||||
func _validate_network_payloads() -> void:
|
||||
var appearance := CharacterCustomizationCatalog.default_snapshot()
|
||||
var fingerprint := "a".repeat(64)
|
||||
var hello := NetworkProtocol.make_client_hello(
|
||||
"profile-id",
|
||||
"stray",
|
||||
"client-nonce",
|
||||
appearance,
|
||||
fingerprint,
|
||||
PackedByteArray(),
|
||||
"trail guide",
|
||||
)
|
||||
assert(NetworkProtocol.validate_client_hello(hello).is_empty())
|
||||
assert(str(hello.get("player_title", "")) == "trail guide")
|
||||
var invalid_hello := hello.duplicate(true)
|
||||
invalid_hello["player_title"] = ""
|
||||
assert(not NetworkProtocol.validate_client_hello(invalid_hello).is_empty())
|
||||
|
||||
var check_request := {
|
||||
"request_id": "check",
|
||||
"session_id": "session",
|
||||
"display_name": "stray",
|
||||
}
|
||||
assert(NetworkProfileProtocol.valid_check_request(check_request))
|
||||
var apply_request := check_request.duplicate(true)
|
||||
apply_request.merge({
|
||||
"player_title": "trail guide",
|
||||
"appearance": appearance,
|
||||
"use_anyway": false,
|
||||
"sender_fingerprint": fingerprint,
|
||||
"sender_signature": PackedByteArray(),
|
||||
})
|
||||
assert(NetworkProfileProtocol.valid_apply_request(apply_request))
|
||||
var signed_fields := NetworkProfileProtocol.signature_fields(apply_request)
|
||||
assert(signed_fields.has("trail guide"))
|
||||
apply_request.erase("player_title")
|
||||
assert(not NetworkProfileProtocol.valid_apply_request(apply_request))
|
||||
|
||||
|
||||
func _validate_peer_registry_title() -> void:
|
||||
var registry := PeerRegistry.new()
|
||||
assert(registry.add_peer(
|
||||
2,
|
||||
"profile-id",
|
||||
"stray",
|
||||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
"",
|
||||
"",
|
||||
PackedStringArray(),
|
||||
"trail guide",
|
||||
))
|
||||
assert(registry.get_peer(2).title == "trail guide")
|
||||
assert(registry.update_title(2, "river scout"))
|
||||
assert(registry.get_peer(2).title == "river scout")
|
||||
assert(not registry.update_title(2, ""))
|
||||
assert(not registry.add_peer(
|
||||
3,
|
||||
"invalid-profile",
|
||||
"stray",
|
||||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
"",
|
||||
"",
|
||||
PackedStringArray(),
|
||||
"",
|
||||
))
|
||||
|
||||
|
||||
func _validate_profile_layout() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var page := ProfilePage.new()
|
||||
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
root.add_child(page)
|
||||
await process_frame
|
||||
await process_frame
|
||||
var name_label := page.get("_name_label") as Label
|
||||
var name_edit := page.get("_name_edit") as LineEdit
|
||||
var title_edit := page.get("_title_edit") as LineEdit
|
||||
var identity := page.find_child("IdentityFingerprint", true, false) as Label
|
||||
var identity_stack := page.find_child(
|
||||
"ProfileIdentityStack", true, false
|
||||
) as VBoxContainer
|
||||
var experience_row := page.find_child(
|
||||
"ProfileExperienceRow", true, false
|
||||
) as HBoxContainer
|
||||
assert(name_label != null and name_edit != null and title_edit != null)
|
||||
assert(name_label.text == "name")
|
||||
assert(identity != null and identity_stack != null and experience_row != null)
|
||||
assert(name_edit.placeholder_text == "stray")
|
||||
assert(title_edit.placeholder_text == "newbie")
|
||||
assert(title_edit.global_position.y > name_edit.global_position.y)
|
||||
assert(is_equal_approx(title_edit.position.x, name_edit.position.x))
|
||||
assert(identity.get_parent() == identity_stack)
|
||||
assert(experience_row.get_parent() == identity_stack)
|
||||
assert(identity.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT)
|
||||
assert(identity_stack.global_position.x > name_edit.global_position.x)
|
||||
assert(name_label.tooltip_text.contains(
|
||||
"Shown to other players in multiplayer."
|
||||
))
|
||||
page.call("_set_name_status", "Name is available in this game.")
|
||||
assert(name_label.tooltip_text.contains(
|
||||
"Name is available in this game."
|
||||
))
|
||||
for visible_label: Label in page.find_children("*", "Label", true, false):
|
||||
if visible_label.visible:
|
||||
assert(visible_label.text != "Shown to other players in multiplayer.")
|
||||
assert(visible_label.text != "Name is available in this game.")
|
||||
var account_controls: Array = page.call("_account_controller_controls")
|
||||
assert(name_edit in account_controls)
|
||||
assert(title_edit in account_controls)
|
||||
page.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
1
tests/profile_title_validation.gd.uid
Normal file
1
tests/profile_title_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cat806v782sr7
|
||||
|
|
@ -23,14 +23,35 @@ func _run() -> void:
|
|||
var player := main.get("_player") as Player
|
||||
var data_root := main.get("_data_root") as PlayerDataRoot
|
||||
var home_state := main.get("_home_state") as PlayerHomeState
|
||||
var network_profile := main.get_node(
|
||||
"%NetworkProfilePreferences"
|
||||
) as NetworkProfilePreferences
|
||||
var appearance_store := main.get_node(
|
||||
"%PlayerAppearanceStore"
|
||||
) as PlayerAppearanceStore
|
||||
assert(
|
||||
save_manager != null
|
||||
and player != null
|
||||
and data_root != null
|
||||
and home_state != null
|
||||
and network_profile != null
|
||||
and appearance_store != null
|
||||
)
|
||||
assert(save_manager.initialize_new_game(864209))
|
||||
save_manager.set_autosave_enabled(true)
|
||||
var first_profile_id: String = network_profile.profile_id
|
||||
var first_appearance: Dictionary = appearance_store.get_snapshot()
|
||||
first_appearance["scale"] = 0.8
|
||||
assert(CharacterCustomizationCatalog.validate_snapshot(first_appearance))
|
||||
assert(network_profile.set_profile_identity(
|
||||
"first save character",
|
||||
"deep",
|
||||
network_profile.speech_speed_id,
|
||||
network_profile.call_id,
|
||||
network_profile.sample_set_id,
|
||||
"trailblazer",
|
||||
))
|
||||
assert(appearance_store.save_snapshot(first_appearance))
|
||||
assert(player.wallet.restore_balance(4321))
|
||||
assert(player.bag.add_item(&"coffee"))
|
||||
assert(home_state.add_owned_decor(&"basic_cube"))
|
||||
|
|
@ -52,6 +73,39 @@ func _run() -> void:
|
|||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
|
||||
assert(bool(decoded.get("ok", false)))
|
||||
assert(int((decoded["data"] as Dictionary)["save_version"]) == 11)
|
||||
var saved_character: Dictionary = (
|
||||
(decoded["data"] as Dictionary).get("character", {})
|
||||
)
|
||||
assert(NetworkProfilePreferences.validate_save_data(
|
||||
(saved_character.get("profile", {}) as Dictionary)
|
||||
))
|
||||
assert(str((saved_character["profile"] as Dictionary)["profile_id"])
|
||||
== first_profile_id)
|
||||
assert(str((saved_character["profile"] as Dictionary)["display_name"])
|
||||
== "first save character")
|
||||
assert(str((saved_character["profile"] as Dictionary)["title"])
|
||||
== "trailblazer")
|
||||
assert(
|
||||
(saved_character["appearance"] as Dictionary)
|
||||
== first_appearance
|
||||
)
|
||||
# Starting another save must feel like a new character, then loading this
|
||||
# archive must restore only this slot's name, voice, and appearance.
|
||||
assert(save_manager.initialize_new_game(97531))
|
||||
assert(network_profile.profile_id != first_profile_id)
|
||||
assert(network_profile.display_name == "stray")
|
||||
assert(network_profile.title == "newbie")
|
||||
assert(network_profile.voice_id != "deep")
|
||||
assert(
|
||||
appearance_store.get_snapshot()
|
||||
== CharacterCustomizationCatalog.default_snapshot()
|
||||
)
|
||||
assert(save_manager.load_player_data())
|
||||
assert(network_profile.profile_id == first_profile_id)
|
||||
assert(network_profile.display_name == "first save character")
|
||||
assert(network_profile.title == "trailblazer")
|
||||
assert(network_profile.voice_id == "deep")
|
||||
assert(appearance_store.get_snapshot() == first_appearance)
|
||||
assert(not PlayerHomeState.sanitize_save_data(
|
||||
(decoded["data"] as Dictionary).get("home", {})
|
||||
).is_empty())
|
||||
|
|
@ -122,6 +176,8 @@ func _run() -> void:
|
|||
var adopted_slot_id: String = str(adopted_slots.front().get("slot_id", ""))
|
||||
assert(bool(adopted_slots.front().get("legacy", false)))
|
||||
assert(bool(adopted_slots.front().get("has_save", false)))
|
||||
assert(save_slots.get_online_slot_id() == adopted_slot_id)
|
||||
assert(bool(adopted_slots.front().get("online", false)))
|
||||
|
||||
var duplicated: Dictionary = save_slots.duplicate_slot(
|
||||
adopted_slot_id,
|
||||
|
|
@ -135,6 +191,9 @@ func _run() -> void:
|
|||
assert(int(duplicate_summary.get("wallet_balance", -1)) == 4321)
|
||||
assert(int(duplicate_summary.get("world_seed", -1)) == 97531)
|
||||
assert(save_slots.activate_slot(duplicated_slot_id))
|
||||
assert(save_slots.mark_slot_for_online(duplicated_slot_id))
|
||||
assert(save_slots.get_online_slot_id() == duplicated_slot_id)
|
||||
assert(bool(save_slots.get_slot(duplicated_slot_id).get("online", false)))
|
||||
assert(save_manager.load_player_data())
|
||||
assert(player.wallet.get_balance() == 4321)
|
||||
assert(save_manager.get_world_seed() == 97531)
|
||||
|
|
@ -157,6 +216,7 @@ func _run() -> void:
|
|||
assert(save_slots.delete_slot(duplicated_slot_id))
|
||||
assert(save_slots.get_slot(duplicated_slot_id).is_empty())
|
||||
assert(save_slots.list_slots().size() == 2)
|
||||
assert(save_slots.get_online_slot_id() == save_slots.get_active_slot_id())
|
||||
|
||||
var settings_panels: Array[Node] = main.find_children(
|
||||
"*", "SettingsPanel", true, false
|
||||
|
|
|
|||
166
tests/save_reload_conflict_validation.gd
Normal file
166
tests/save_reload_conflict_validation.gd
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const WAIT_MILLISECONDS: int = 20_000
|
||||
|
||||
var _conflicts: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
|
||||
var data_root := main.get("_data_root") as PlayerDataRoot
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var save_slots := main.get("_save_slots") as PlayerSaveSlotCatalog
|
||||
var player := main.get("_player") as Player
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var network_profile := main.get_node(
|
||||
"%NetworkProfilePreferences"
|
||||
) as NetworkProfilePreferences
|
||||
var appearance_store := main.get_node(
|
||||
"%PlayerAppearanceStore"
|
||||
) as PlayerAppearanceStore
|
||||
assert(
|
||||
data_root != null
|
||||
and save_manager != null
|
||||
and save_slots != null
|
||||
and player != null
|
||||
and session != null
|
||||
and network_profile != null
|
||||
and appearance_store != null
|
||||
)
|
||||
var conflict_dialog_callback := Callable(main, "_on_portable_conflict")
|
||||
if data_root.conflict_detected.is_connected(conflict_dialog_callback):
|
||||
data_root.conflict_detected.disconnect(conflict_dialog_callback)
|
||||
data_root.conflict_detected.connect(_on_conflict_detected)
|
||||
if OS.get_environment(
|
||||
PlayerDataRoot.ISOLATED_VALIDATION_ENVIRONMENT
|
||||
) == "1":
|
||||
assert(
|
||||
PlayerDataRoot.ISOLATED_VALIDATION_DIRECTORY
|
||||
in data_root.root_path
|
||||
)
|
||||
|
||||
main.call(
|
||||
"_on_new_slot_requested",
|
||||
"reload regression",
|
||||
WorldLayout.STARTER_ISLAND,
|
||||
424242,
|
||||
"",
|
||||
)
|
||||
assert(await _wait_for_gameplay(main, true))
|
||||
var slot_id: String = save_slots.get_active_slot_id()
|
||||
assert(not slot_id.is_empty())
|
||||
assert(player.wallet.restore_balance(321))
|
||||
assert(network_profile.set_profile_identity(
|
||||
"reload stray",
|
||||
"deep",
|
||||
network_profile.speech_speed_id,
|
||||
network_profile.call_id,
|
||||
network_profile.sample_set_id,
|
||||
"returner",
|
||||
))
|
||||
var custom_appearance: Dictionary = appearance_store.get_snapshot()
|
||||
custom_appearance["scale"] = 0.8
|
||||
assert(appearance_store.save_snapshot(custom_appearance))
|
||||
main.call("_on_return_to_title_requested")
|
||||
assert(await _wait_for_gameplay(main, false))
|
||||
assert(not save_manager.is_dirty())
|
||||
|
||||
# Establish the exact reported lifecycle once: load a local slot, return to
|
||||
# title, then immediately select that same slot again in this process.
|
||||
main.call("_on_slot_play_requested", slot_id)
|
||||
assert(await _wait_for_gameplay(main, true))
|
||||
main.call("_on_return_to_title_requested")
|
||||
assert(await _wait_for_gameplay(main, false))
|
||||
assert(not save_manager.is_dirty())
|
||||
|
||||
# A selected save owns its character state. An out-of-band edit to the
|
||||
# compatibility profile mirror must not make that valid save unloadable or
|
||||
# be overwritten merely by loading it. A later explicit profile edit must
|
||||
# still detect the external mutation.
|
||||
var profile_path: String = data_root.path_for(&"network_profile")
|
||||
var external_profile_bytes := PortableFileGuard.read_bytes(profile_path)
|
||||
assert(not external_profile_bytes.is_empty())
|
||||
external_profile_bytes.append(0x0a)
|
||||
assert(_write_external_bytes(profile_path, external_profile_bytes))
|
||||
_conflicts.clear()
|
||||
main.call("_on_slot_play_requested", slot_id)
|
||||
assert(await _wait_for_gameplay(main, true))
|
||||
assert(
|
||||
_conflicts.is_empty(),
|
||||
"same-process same-slot reload reported false conflicts: %s"
|
||||
% [_conflicts],
|
||||
)
|
||||
assert(player.wallet.get_balance() == 321)
|
||||
assert(network_profile.display_name == "reload stray")
|
||||
assert(network_profile.title == "returner")
|
||||
assert(appearance_store.get_snapshot() == custom_appearance)
|
||||
assert(
|
||||
PortableFileGuard.read_bytes(profile_path)
|
||||
== external_profile_bytes
|
||||
)
|
||||
_conflicts.clear()
|
||||
assert(not network_profile.set_display_name("explicit overwrite"))
|
||||
assert(_conflicts.size() == 1)
|
||||
assert(FileAccess.file_exists(str(_conflicts.front().get("path", ""))))
|
||||
|
||||
# A real out-of-band edit must still be caught. Mutating the canonical bytes
|
||||
# after load simulates a Syncthing/device replacement while this process is
|
||||
# holding an older expected hash.
|
||||
var save_path: String = str(save_manager.get("_save_path"))
|
||||
var external_bytes := PortableFileGuard.read_bytes(save_path)
|
||||
assert(not external_bytes.is_empty())
|
||||
external_bytes.append(0x7f)
|
||||
assert(_write_external_bytes(save_path, external_bytes))
|
||||
assert(player.wallet.restore_balance(654))
|
||||
_conflicts.clear()
|
||||
assert(not save_manager.save_now())
|
||||
assert(_conflicts.size() == 1)
|
||||
assert(FileAccess.file_exists(str(_conflicts.front().get("path", ""))))
|
||||
|
||||
save_manager.set_autosave_enabled(false)
|
||||
save_manager.cancel_pending_autosave()
|
||||
session.disconnect_session("Save reload conflict validation complete.")
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
print("Save reload conflict validation: PASS")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _wait_for_gameplay(main: Node, expected: bool) -> bool:
|
||||
var deadline: int = Time.get_ticks_msec() + WAIT_MILLISECONDS
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
if bool(main.get("_gameplay_started")) == expected:
|
||||
return true
|
||||
await process_frame
|
||||
return false
|
||||
|
||||
|
||||
func _on_conflict_detected(message: String, path: String) -> void:
|
||||
_conflicts.append({"message": message, "path": path})
|
||||
|
||||
|
||||
func _write_external_bytes(path: String, bytes: PackedByteArray) -> bool:
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_buffer(bytes)
|
||||
file.flush()
|
||||
var succeeded: bool = file.get_error() == OK
|
||||
file.close()
|
||||
return succeeded
|
||||
1
tests/save_reload_conflict_validation.gd.uid
Normal file
1
tests/save_reload_conflict_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cmd4elmipmywa
|
||||
|
|
@ -123,6 +123,32 @@ func _run_client() -> void:
|
|||
)
|
||||
await _wait_for_join(main, session, SECOND_HOST_PORT)
|
||||
assert(bool(main.get("_gameplay_started")))
|
||||
var save_slots := main.get("_save_slots") as PlayerSaveSlotCatalog
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
var local_slot_id: String = save_slots.get_active_slot_id()
|
||||
assert(not local_slot_id.is_empty())
|
||||
main.call("_on_return_to_title_requested")
|
||||
var title_deadline: int = Time.get_ticks_msec() + WAIT_SECONDS * 1000
|
||||
while (
|
||||
Time.get_ticks_msec() < title_deadline
|
||||
and session.state != NetworkSession.State.INACTIVE
|
||||
):
|
||||
await process_frame
|
||||
assert(session.state == NetworkSession.State.INACTIVE)
|
||||
assert(not bool(main.get("_gameplay_started")))
|
||||
assert(not save_manager.is_dirty())
|
||||
main.call("_on_slot_play_requested", local_slot_id)
|
||||
var local_deadline: int = Time.get_ticks_msec() + WAIT_SECONDS * 1000
|
||||
while (
|
||||
Time.get_ticks_msec() < local_deadline
|
||||
and (
|
||||
not session.is_host()
|
||||
or not bool(main.get("_gameplay_started"))
|
||||
)
|
||||
):
|
||||
await process_frame
|
||||
assert(session.is_host())
|
||||
assert(bool(main.get("_gameplay_started")))
|
||||
print("Session switch multiplayer client validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
|
|
|
|||
43
tests/tooltip_escape_validation.gd
Normal file
43
tests/tooltip_escape_validation.gd
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
|
||||
var game_ui: GameUI = main.get("_game_ui") as GameUI
|
||||
var player_menu: PlayerMenu = game_ui.get_node("%PlayerMenu") as PlayerMenu
|
||||
assert(game_ui != null and player_menu != null)
|
||||
game_ui.get_title_screen().hide()
|
||||
main.set("_gameplay_started", true)
|
||||
player_menu.show()
|
||||
player_menu.set("_transitioning", false)
|
||||
|
||||
# Input.action_press updates global action state without delivering an
|
||||
# InputEvent to Main._input. That reproduces the path used when a native
|
||||
# tooltip window consumes Escape before the gameplay viewport receives it.
|
||||
Input.action_press("ui_cancel")
|
||||
await process_frame
|
||||
Input.action_release("ui_cancel")
|
||||
for _frame: int in 40:
|
||||
await process_frame
|
||||
assert(not player_menu.visible)
|
||||
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
print("Tooltip Escape validation: PASS")
|
||||
quit(0)
|
||||
1
tests/tooltip_escape_validation.gd.uid
Normal file
1
tests/tooltip_escape_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cwc8lnci5ccph
|
||||
|
|
@ -39,6 +39,15 @@ func _run() -> void:
|
|||
var canonical_stage := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage"
|
||||
) as Control
|
||||
var gameplay_screen_effects := game_ui.get_node(
|
||||
"UIRoot/GameplayScreenEffects"
|
||||
) as Control
|
||||
var fishing_danger_vignette := game_ui.get_node(
|
||||
"UIRoot/GameplayScreenEffects/FishingDangerVignette"
|
||||
) as ColorRect
|
||||
var gameplay_transient_hud := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/GameplayTransientHUD"
|
||||
) as Control
|
||||
var chat_ui := game_ui.get_node("UIRoot/ChatUI") as ChatUI
|
||||
var player_menu := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/PlayerMenu"
|
||||
|
|
@ -49,9 +58,15 @@ func _run() -> void:
|
|||
var hotbar := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/Hotbar"
|
||||
) as HotbarUI
|
||||
var home_decor_toolbar := game_ui.get_node(
|
||||
"UIRoot/CanonicalStage/HomeDecorToolbar"
|
||||
) as HomeDecorToolbar
|
||||
assert(game_ui.get_node_or_null(
|
||||
"UIRoot/CanonicalStage/GameplayTransientHUD/EffectStatus"
|
||||
) == null)
|
||||
assert(game_ui.get_node_or_null(
|
||||
"UIRoot/CanonicalStage/GameplayTransientHUD/FishingDangerVignette"
|
||||
) == null)
|
||||
var screen_fade := game_ui.get_node("UIRoot/ScreenFade") as Control
|
||||
var title_screen := game_ui.get_node("UIRoot/TitleScreen") as TitleScreen
|
||||
var title_content_stage := title_screen.get_node(
|
||||
|
|
@ -71,15 +86,63 @@ func _run() -> void:
|
|||
root.add_child(presenter)
|
||||
await process_frame
|
||||
chat_ui.set_available(true)
|
||||
var nameplate_layer := chat_ui.get("_nameplate_layer") as Control
|
||||
var speech_layer := chat_ui.get("_speech_layer") as Control
|
||||
assert(nameplate_layer != null and nameplate_layer.visible)
|
||||
assert(speech_layer != null and speech_layer.visible)
|
||||
home_decor_toolbar.set_active(true)
|
||||
assert(home_decor_toolbar.visible)
|
||||
game_ui.call("_on_player_menu_visibility_changed", true)
|
||||
assert(not chat_ui.is_world_speech_visible())
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(not speech_layer.visible)
|
||||
assert(not home_decor_toolbar.visible)
|
||||
game_ui.call("_on_player_menu_visibility_changed", false)
|
||||
assert(chat_ui.is_world_speech_visible())
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(speech_layer.visible)
|
||||
chat_ui.set_available(true)
|
||||
assert(nameplate_layer.visible)
|
||||
chat_ui.set_available(false)
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(speech_layer.visible)
|
||||
chat_ui.set_available(true)
|
||||
assert(nameplate_layer.visible)
|
||||
chat_ui.set_hud_hidden(true)
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(speech_layer.visible)
|
||||
chat_ui.set_hud_hidden(false)
|
||||
assert(nameplate_layer.visible)
|
||||
chat_ui.set_world_nameplates_hud_hidden(true)
|
||||
assert(not chat_ui.is_hud_hidden())
|
||||
assert(not nameplate_layer.visible)
|
||||
assert(speech_layer.visible)
|
||||
chat_ui.set_world_nameplates_hud_hidden(false)
|
||||
assert(nameplate_layer.visible)
|
||||
assert(home_decor_toolbar.visible)
|
||||
game_ui.set("_storage_open", true)
|
||||
game_ui.call("_emit_interactive_pointer_ui_changed")
|
||||
assert(not home_decor_toolbar.visible)
|
||||
game_ui.call("_on_player_menu_visibility_changed", true)
|
||||
game_ui.set("_storage_open", false)
|
||||
game_ui.call("_emit_interactive_pointer_ui_changed")
|
||||
assert(not home_decor_toolbar.visible)
|
||||
game_ui.call("_on_player_menu_visibility_changed", false)
|
||||
assert(home_decor_toolbar.visible)
|
||||
home_decor_toolbar.set_active(false)
|
||||
game_ui.set("_gameplay_ui_enabled", true)
|
||||
game_ui.set("_gameplay_hud_hidden", false)
|
||||
game_ui.call("_refresh_gameplay_hud_visibility")
|
||||
assert(gameplay_screen_effects.visible)
|
||||
assert(gameplay_transient_hud.visible)
|
||||
game_ui.set("_system_menu_open", true)
|
||||
game_ui.call("_refresh_gameplay_hud_visibility")
|
||||
assert(not gameplay_screen_effects.visible)
|
||||
assert(not gameplay_transient_hud.visible)
|
||||
game_ui.set("_system_menu_open", false)
|
||||
game_ui.set("_gameplay_ui_enabled", false)
|
||||
game_ui.call("_refresh_gameplay_hud_visibility")
|
||||
chat_ui.set_available(true)
|
||||
chat_ui.call(
|
||||
"_set_presentation_state",
|
||||
ChatUI.PresentationState.EXPANDED,
|
||||
|
|
@ -121,7 +184,16 @@ func _run() -> void:
|
|||
assert(canonical_stage.position.is_equal_approx(
|
||||
UIReferencePresentationType.get_stage_position(display_size)
|
||||
))
|
||||
assert(gameplay_screen_effects.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(gameplay_screen_effects.size.is_equal_approx(
|
||||
expected_visible_size
|
||||
))
|
||||
assert(fishing_danger_vignette.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(fishing_danger_vignette.size.is_equal_approx(
|
||||
expected_visible_size
|
||||
))
|
||||
assert(chat_ui.size.is_equal_approx(expected_visible_size))
|
||||
assert(nameplate_layer.size.is_equal_approx(expected_visible_size))
|
||||
assert(screen_fade.size.is_equal_approx(expected_visible_size))
|
||||
assert(player_menu.position.is_equal_approx(Vector2.ZERO))
|
||||
assert(player_menu.size.is_equal_approx(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ const GatheringControllerType = preload(
|
|||
)
|
||||
const WorldGatherableType = preload("res://gathering/world_gatherable.gd")
|
||||
|
||||
const CRAB_PIXEL_SIZE: float = 0.0005
|
||||
const CRAB_PIXEL_SIZE: float = 0.01
|
||||
const CRAB_SURFACE_OFFSET: float = 0.11
|
||||
const CRAB_SPRITE_EXTENT: float = 0.64
|
||||
const REDUCED_CATCH_RING_RADIUS: float = 0.175
|
||||
const NET_STRIKE_MARKER_DISTANCE: float = 0.85
|
||||
const CalendarSeasonType = preload("res://world/calendar_season.gd")
|
||||
|
|
@ -30,7 +32,7 @@ func _run() -> void:
|
|||
var snapshot_entities_per_envelope: int = (
|
||||
NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
|
||||
)
|
||||
assert(protocol_version == 12)
|
||||
assert(protocol_version == 13)
|
||||
assert(
|
||||
world_spawn_capability == protocol_world_spawn_capability
|
||||
)
|
||||
|
|
@ -56,6 +58,10 @@ func _validate_catalog_statuses() -> void:
|
|||
CRAB_PIXEL_SIZE,
|
||||
)
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
brown.sprite_surface_offset,
|
||||
CRAB_SURFACE_OFFSET,
|
||||
))
|
||||
_validate_quality_behavior(brown)
|
||||
assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0))
|
||||
assert(is_equal_approx(brown.capture_respawn_max_seconds, 720.0))
|
||||
|
|
@ -121,6 +127,11 @@ func _validate_catalog_statuses() -> void:
|
|||
0.4,
|
||||
))
|
||||
assert(blue.maximum_surface_population == 16)
|
||||
assert(is_equal_approx(blue.sprite_pixel_size, CRAB_PIXEL_SIZE))
|
||||
assert(is_equal_approx(
|
||||
blue.sprite_surface_offset,
|
||||
CRAB_SURFACE_OFFSET,
|
||||
))
|
||||
assert(blue.target_population_for_surface_area(100.0) == 4)
|
||||
assert(blue.target_population_for_surface_area(2250.0) == 9)
|
||||
assert(FishCatalog.get_fish_by_id(&"crab_blue") == blue.catch_data)
|
||||
|
|
@ -138,6 +149,10 @@ func _validate_catalog_statuses() -> void:
|
|||
CRAB_PIXEL_SIZE,
|
||||
)
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
entry.sprite_surface_offset,
|
||||
CRAB_SURFACE_OFFSET,
|
||||
))
|
||||
assert(entry.catch_data.collection_method == FishData.CollectionMethod.NET)
|
||||
assert(
|
||||
entry.catch_data.logbook_section
|
||||
|
|
@ -182,13 +197,22 @@ func _validate_catalog_statuses() -> void:
|
|||
|
||||
func _validate_billboard_presentation() -> void:
|
||||
var gatherable := WorldGatherableType.new()
|
||||
gatherable.call("_ensure_visual")
|
||||
root.add_child(gatherable)
|
||||
var brown: GatherableData = Gatherables.get_entry(&"crab_brown")
|
||||
gatherable.configure("crab-size-validation", brown, Vector3.ZERO, 0.0)
|
||||
var sprite := gatherable.get_node("GatherableSprite") as Sprite3D
|
||||
assert(sprite != null)
|
||||
assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
|
||||
assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
|
||||
assert(not sprite.shaded)
|
||||
gatherable.free()
|
||||
assert(is_equal_approx(sprite.pixel_size, CRAB_PIXEL_SIZE))
|
||||
assert(is_equal_approx(sprite.position.y, CRAB_SURFACE_OFFSET))
|
||||
assert(is_equal_approx(
|
||||
float(sprite.texture.get_width()) * sprite.pixel_size,
|
||||
CRAB_SPRITE_EXTENT,
|
||||
))
|
||||
gatherable.queue_free()
|
||||
await process_frame
|
||||
|
||||
var unconfigured_hotspot := WorldGatherableType.new()
|
||||
unconfigured_hotspot.call("_ensure_water_spurt_visual")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue