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
648
main/main.gd
648
main/main.gd
|
|
@ -146,9 +146,6 @@ const TITLE_MUSIC_PATH: String = (
|
|||
const NEW_GAME_MUSIC_PATH: String = "res://audio/music/world/tulips.wav"
|
||||
const DUSK_MUSIC_PATH: String = "res://audio/music/world/craft.mp3"
|
||||
const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
|
||||
const PLAYER_MENU_PATTERN_SCALE: float = 0.85
|
||||
const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0)
|
||||
const SHOP_PATTERN_SCALE: float = 1.75
|
||||
const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10
|
||||
const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||
const HOME_ENTRY_INPUT_OWNER: StringName = &"home_entry_transition"
|
||||
|
|
@ -158,6 +155,8 @@ const HOME_ENTRY_WALK_DISTANCE: float = 1.05
|
|||
const HOME_ENTRY_WALK_DURATION: float = 0.55
|
||||
const HOME_EXIT_WALK_DISTANCE: float = 1.25
|
||||
const HOME_EXIT_WALK_DURATION: float = 0.62
|
||||
const SHOP_NPC_HANDOFF_HYSTERESIS_METERS: float = 0.1
|
||||
const SHOP_NPC_CONVERSATION_INPUT_OWNER: StringName = &"shop_npc_conversation"
|
||||
|
||||
@export var fish_catalog: FishPoolType
|
||||
@export var pelican_buyer_profile: FishBuyerProfileType
|
||||
|
|
@ -263,14 +262,16 @@ const HOME_EXIT_WALK_DURATION: float = 0.62
|
|||
@onready var _world_gatherables_root: Node3D = $WorldGatherables
|
||||
@onready var _home_world: HomeWorldServiceType = %HomeWorldService
|
||||
@onready var _title_background: ColorRect = %TitleBackground
|
||||
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
||||
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
|
||||
|
||||
var _gameplay_started: bool = false
|
||||
var _save_slots := PlayerSaveSlotCatalogType.new()
|
||||
var _shop_interaction: FishingShopInteractionType
|
||||
var _decor_shop_interaction: DecorShopInteractionType
|
||||
var _rv_upgrade_shop_interaction: RVUpgradeInteractionType
|
||||
var _active_shop_npc_interaction: FishingShopInteractionType
|
||||
var _engaged_shop_npc_interaction: FishingShopInteractionType
|
||||
var _shop_conversation_shop_opened: bool = false
|
||||
var _shop_conversation_camera_restoring: bool = false
|
||||
var _storage_interaction: PlayerStorageInteractionType
|
||||
var _title_music_tween: Tween
|
||||
var _title_music_transition_generation: int = 0
|
||||
|
|
@ -279,8 +280,6 @@ var _dusk_music_played_for_natural_day: bool = false
|
|||
var _quit_in_progress: bool = false
|
||||
var _join_requested_from_title: bool = false
|
||||
var _join_requested_from_pause: bool = false
|
||||
var _player_menu_backdrop_tween: Tween
|
||||
var _shop_backdrop_tween: Tween
|
||||
var _pending_join_endpoint: String = ""
|
||||
var _social_prompt_queue: Array[Dictionary] = []
|
||||
var _active_social_prompt: Dictionary = {}
|
||||
|
|
@ -307,6 +306,7 @@ var _home_exit_start_position: Vector3 = Vector3.ZERO
|
|||
var _home_exit_walk_tween: Tween
|
||||
var _home_entry_transition_active: bool = false
|
||||
var _home_entry_walk_tween: Tween
|
||||
var _observed_cancel_input_frame: int = -1
|
||||
|
||||
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
|
||||
var _rain_ambience: RainAmbienceType
|
||||
|
|
@ -341,8 +341,6 @@ func _ready() -> void:
|
|||
_player,
|
||||
_test_world.get_saltwater_shoreline_mesh(),
|
||||
)
|
||||
get_window().size_changed.connect(_resize_native_overlays)
|
||||
_resize_native_overlays()
|
||||
if not _settings_manager.settings_changed.is_connected(
|
||||
_apply_runtime_settings
|
||||
):
|
||||
|
|
@ -370,44 +368,6 @@ func _configure_presentation_performance_profile(light_profile: bool) -> void:
|
|||
"animation_enabled",
|
||||
not light_profile,
|
||||
)
|
||||
var menu_pattern_material := (
|
||||
_player_menu_backdrop.material as ShaderMaterial
|
||||
)
|
||||
if menu_pattern_material != null:
|
||||
menu_pattern_material.set_shader_parameter(
|
||||
"animation_enabled",
|
||||
not light_profile,
|
||||
)
|
||||
menu_pattern_material.set_shader_parameter(
|
||||
"display_scale",
|
||||
PLAYER_MENU_PATTERN_SCALE,
|
||||
)
|
||||
menu_pattern_material.set_shader_parameter(
|
||||
"scroll_velocity_pixels",
|
||||
(
|
||||
Vector2.ZERO
|
||||
if light_profile
|
||||
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
|
||||
),
|
||||
)
|
||||
var shop_pattern_material := _shop_backdrop.material as ShaderMaterial
|
||||
if shop_pattern_material != null:
|
||||
shop_pattern_material.set_shader_parameter(
|
||||
"animation_enabled",
|
||||
not light_profile,
|
||||
)
|
||||
shop_pattern_material.set_shader_parameter(
|
||||
"display_scale",
|
||||
SHOP_PATTERN_SCALE,
|
||||
)
|
||||
shop_pattern_material.set_shader_parameter(
|
||||
"scroll_velocity_pixels",
|
||||
(
|
||||
Vector2.ZERO
|
||||
if light_profile
|
||||
else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
func _configure_audio_performance_profile(light_profile: bool) -> void:
|
||||
|
|
@ -437,8 +397,6 @@ func _is_dedicated_server_runtime() -> bool:
|
|||
func _disable_dedicated_presentation_tree() -> void:
|
||||
for path: NodePath in [
|
||||
NodePath("TitleBackgroundLayer"),
|
||||
NodePath("PlayerMenuBackdrop"),
|
||||
NodePath("ShopBackdrop"),
|
||||
NodePath("WorldPixelationPostprocess"),
|
||||
NodePath("UIPresentation"),
|
||||
NodePath("PixelationResetOverlay"),
|
||||
|
|
@ -779,6 +737,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||
_rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop()
|
||||
_active_shop_npc_interaction = null
|
||||
_storage_interaction = _test_world.get_player_storage()
|
||||
if not dedicated:
|
||||
if not _home_state.changed.is_connected(_on_home_state_changed):
|
||||
|
|
@ -818,6 +777,8 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_player_jobs,
|
||||
_player,
|
||||
_home_state,
|
||||
_network_profile if not dedicated else null,
|
||||
_appearance_store if not dedicated else null,
|
||||
)
|
||||
_home_world.setup(
|
||||
_test_world,
|
||||
|
|
@ -1045,6 +1006,9 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_fishing_spot,
|
||||
_game_ui.get_home_decor_toolbar(),
|
||||
)
|
||||
_home_decor_controller.exit_requested.connect(
|
||||
_on_home_toolbar_exit_requested
|
||||
)
|
||||
_game_ui.get_player_storage().decorate_requested.connect(
|
||||
_home_decor_controller.begin_decorating
|
||||
)
|
||||
|
|
@ -1117,12 +1081,19 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_game_ui.passive_pointer_ui_changed.connect(
|
||||
_ui_pixelation.set_passive_pointer_ui_enabled
|
||||
)
|
||||
_game_ui.player_menu_backdrop_visibility_changed.connect(
|
||||
_set_player_menu_backdrop_visible
|
||||
)
|
||||
_game_ui.shop_backdrop_visibility_changed.connect(
|
||||
_set_shop_backdrop_visible
|
||||
_game_ui.get_node("%PlayerMenu").connect(
|
||||
&"menu_visibility_changed",
|
||||
_on_player_menu_conversation_visibility_changed
|
||||
)
|
||||
for shop_menu: Node in [
|
||||
_game_ui.get_fishing_shop(),
|
||||
_game_ui.get_decor_shop(),
|
||||
_game_ui.get_rv_upgrade_shop(),
|
||||
]:
|
||||
shop_menu.connect(
|
||||
&"menu_visibility_changed",
|
||||
_on_shop_conversation_menu_visibility_changed
|
||||
)
|
||||
_pixelation_reset.return_to_settings_requested.connect(
|
||||
_game_ui.focus_open_settings_back_button
|
||||
)
|
||||
|
|
@ -1167,6 +1138,9 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
pause_menu.menu_visibility_changed.connect(
|
||||
_game_ui.set_system_menu_open
|
||||
)
|
||||
pause_menu.menu_visibility_changed.connect(
|
||||
_on_pause_menu_conversation_visibility_changed
|
||||
)
|
||||
_player.hotbar.selected_assignment_changed.connect(
|
||||
_on_active_hotbar_item_changed
|
||||
)
|
||||
|
|
@ -1441,8 +1415,15 @@ func _on_data_root_changed() -> void:
|
|||
_save_manager.set_autosave_enabled(false)
|
||||
_save_manager.cancel_pending_autosave()
|
||||
_configure_portable_stores()
|
||||
if not _network_profile_service.reload_persisted_profile():
|
||||
var profile_reloaded: bool = (
|
||||
_network_profile_service.reload_persisted_profile()
|
||||
)
|
||||
if not profile_reloaded:
|
||||
push_warning("The selected data folder's player profile could not be loaded.")
|
||||
else:
|
||||
_save_manager.capture_legacy_character_fallback()
|
||||
if not _save_manager.load_character_profile():
|
||||
push_warning("The active save's character profile could not be loaded.")
|
||||
_network_session.set_local_appearance_snapshot(
|
||||
_appearance_store.get_snapshot()
|
||||
)
|
||||
|
|
@ -1547,6 +1528,25 @@ func _input(event: InputEvent) -> void:
|
|||
if _home_exit_transition_active:
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
# While dialogue is active the conversation owns the complete input stream.
|
||||
# The second interaction press advances into the shop; cancel safely leaves
|
||||
# the conversation without opening another menu behind it.
|
||||
if (
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
and not _shop_conversation_shop_opened
|
||||
):
|
||||
if (
|
||||
event.is_action_pressed("interact")
|
||||
and not (event is InputEventKey and event.echo)
|
||||
):
|
||||
_open_engaged_shop()
|
||||
elif (
|
||||
event.is_action_pressed("ui_cancel")
|
||||
or event.is_action_pressed("open_system_menu")
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
# Do not let controller aliases close or manipulate the world/menu behind
|
||||
# the modal on-screen keyboard. The keyboard consumes the event itself.
|
||||
if _game_ui.is_controller_text_entry_open():
|
||||
|
|
@ -1564,6 +1564,8 @@ func _input(event: InputEvent) -> void:
|
|||
or (event is InputEventKey and event.echo)
|
||||
):
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_observed_cancel_input_frame = Engine.get_process_frames()
|
||||
var pause_open_requested: bool = _is_pause_open_request(event)
|
||||
var title_screen: TitleScreenType = _game_ui.get_title_screen()
|
||||
if title_screen.visible or not _gameplay_started:
|
||||
|
|
@ -1712,31 +1714,16 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
_refresh_home_door_prompt({})
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
# All shop prompts and interactions resolve through the same nearest-NPC
|
||||
# selection. This prevents overlapping trigger margins from making a fixed
|
||||
# shop priority open (or speak for) someone the player is not facing.
|
||||
_refresh_shop_npc_speech_state()
|
||||
var active_shop_interaction := _get_active_shop_npc_interaction()
|
||||
if (
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
active_shop_interaction != null
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _game_ui.get_rv_upgrade_shop().open_shop()
|
||||
):
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _game_ui.get_decor_shop().open_shop()
|
||||
):
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _game_ui.get_fishing_shop().open_shop()
|
||||
):
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
_begin_shop_npc_conversation(active_shop_interaction)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
|
|
@ -1750,25 +1737,33 @@ func _process(delta: float) -> void:
|
|||
)
|
||||
_print_dedicated_metrics()
|
||||
return
|
||||
# Native Godot tooltip popups can take ownership of Escape before this
|
||||
# scene's _input callback sees it. Poll the action once as a fallback so a
|
||||
# visible tooltip never forces a second Escape press to close its menu.
|
||||
if (
|
||||
Input.is_action_just_pressed("ui_cancel")
|
||||
and _observed_cancel_input_frame != Engine.get_process_frames()
|
||||
):
|
||||
_consume_tooltip_captured_escape()
|
||||
_maybe_begin_home_exit_transition()
|
||||
var show_rv_upgrade_prompt := _can_show_rv_upgrade_shop_prompt()
|
||||
var show_decor_shop_prompt := _can_show_decor_shop_prompt()
|
||||
var show_shop_prompt := (
|
||||
show_rv_upgrade_prompt
|
||||
or show_decor_shop_prompt
|
||||
or _can_show_shop_prompt()
|
||||
# Shop close animations/signals may finish after the input frame that asked
|
||||
# for them. Keep the cinematic held until the UI is actually gone.
|
||||
if (
|
||||
_shop_conversation_shop_opened
|
||||
and is_instance_valid(_engaged_shop_npc_interaction)
|
||||
and not _is_any_shop_menu_visible()
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
# Re-evaluate while moving, not only when an Area3D emits enter/exit. This
|
||||
# gives overlapping edge cases an immediate nearest-character handoff.
|
||||
_refresh_shop_npc_speech_state()
|
||||
var active_shop_interaction := _get_active_shop_npc_interaction()
|
||||
var show_shop_prompt := _can_show_active_shop_prompt(
|
||||
active_shop_interaction
|
||||
)
|
||||
var shop_prompt_anchor := (
|
||||
_rv_upgrade_shop_interaction.get_prompt_anchor_position()
|
||||
if show_rv_upgrade_prompt
|
||||
else (
|
||||
_decor_shop_interaction.get_prompt_anchor_position()
|
||||
if show_decor_shop_prompt
|
||||
else (
|
||||
_shop_interaction.get_prompt_anchor_position()
|
||||
if _shop_interaction != null else Vector3.ZERO
|
||||
)
|
||||
)
|
||||
active_shop_interaction.get_prompt_anchor_position()
|
||||
if active_shop_interaction != null else Vector3.ZERO
|
||||
)
|
||||
_game_ui.set_shop_prompt_visible(
|
||||
show_shop_prompt,
|
||||
|
|
@ -1781,6 +1776,28 @@ func _process(delta: float) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _consume_tooltip_captured_escape() -> bool:
|
||||
if (
|
||||
not _gameplay_started
|
||||
or _game_ui.get_title_screen().visible
|
||||
or _home_exit_transition_active
|
||||
or _game_ui.is_controller_text_entry_open()
|
||||
or _game_ui.is_input_mapping_capturing()
|
||||
):
|
||||
return false
|
||||
if _game_ui.get_player_storage().consume_escape():
|
||||
return true
|
||||
if _game_ui.get_decor_shop().consume_escape():
|
||||
return true
|
||||
if _game_ui.get_rv_upgrade_shop().consume_escape():
|
||||
return true
|
||||
if _game_ui.get_fishing_shop().consume_escape():
|
||||
return true
|
||||
if _game_ui.get_pause_menu().handle_escape():
|
||||
return true
|
||||
return _game_ui.consume_player_menu_escape()
|
||||
|
||||
|
||||
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||
if settings == null:
|
||||
return
|
||||
|
|
@ -1866,11 +1883,22 @@ func _reset_pixelation() -> void:
|
|||
|
||||
func _set_gameplay_active(active: bool) -> void:
|
||||
var was_gameplay_started: bool = _gameplay_started
|
||||
if not active and was_gameplay_started and _save_manager.is_dirty():
|
||||
# Session teardown can mutate transient multiplayer state immediately
|
||||
# before this method runs. Preserve any real progression first, then
|
||||
# discard teardown-only dirty notifications once gameplay is inactive.
|
||||
_save_manager.save_if_dirty()
|
||||
_gameplay_started = active
|
||||
# Session loss, a world switch, or a return to title can interrupt the home
|
||||
# handoff before its fade callback runs. Do not carry its input suppressors
|
||||
# or black screen into the next gameplay session.
|
||||
if not active:
|
||||
if (
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or _shop_conversation_camera_restoring
|
||||
or _player.is_shop_conversation_camera_active()
|
||||
):
|
||||
_end_shop_npc_conversation(true)
|
||||
_cancel_home_entry_transition()
|
||||
_cancel_home_exit_transition(true)
|
||||
_shoreline_ambience.set_active(active)
|
||||
|
|
@ -1901,8 +1929,7 @@ func _set_gameplay_active(active: bool) -> void:
|
|||
_refresh_active_hotbar_item()
|
||||
else:
|
||||
_player_jobs.end_progression_session()
|
||||
_set_player_menu_backdrop_visible(false)
|
||||
_set_shop_backdrop_visible(false)
|
||||
_save_manager.cancel_pending_autosave()
|
||||
|
||||
|
||||
func _on_natural_time_advanced(advanced_hours: float) -> void:
|
||||
|
|
@ -1953,85 +1980,6 @@ static func _natural_interval_crosses_hour(
|
|||
)
|
||||
|
||||
|
||||
func _set_player_menu_backdrop_visible(requested_visible: bool) -> void:
|
||||
if _player_menu_backdrop_tween != null:
|
||||
_player_menu_backdrop_tween.kill()
|
||||
_player_menu_backdrop_tween = null
|
||||
var should_show: bool = requested_visible and _gameplay_started
|
||||
if should_show:
|
||||
var was_visible: bool = _player_menu_backdrop.visible
|
||||
_player_menu_backdrop.visible = true
|
||||
if not was_visible:
|
||||
_player_menu_backdrop.modulate.a = 0.0
|
||||
_player_menu_backdrop_tween = create_tween()
|
||||
_player_menu_backdrop_tween.tween_property(
|
||||
_player_menu_backdrop,
|
||||
"modulate:a",
|
||||
1.0,
|
||||
UIMotion.PLAYER_MENU_ENTER_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
return
|
||||
if not _player_menu_backdrop.visible:
|
||||
return
|
||||
_player_menu_backdrop_tween = create_tween()
|
||||
_player_menu_backdrop_tween.tween_property(
|
||||
_player_menu_backdrop,
|
||||
"modulate:a",
|
||||
0.0,
|
||||
UIMotion.PLAYER_MENU_EXIT_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
_player_menu_backdrop_tween.finished.connect(
|
||||
func() -> void:
|
||||
_player_menu_backdrop.visible = false
|
||||
_player_menu_backdrop.modulate.a = 1.0
|
||||
_player_menu_backdrop_tween = null
|
||||
)
|
||||
|
||||
|
||||
func _set_shop_backdrop_visible(requested_visible: bool) -> void:
|
||||
if _shop_backdrop_tween != null:
|
||||
_shop_backdrop_tween.kill()
|
||||
_shop_backdrop_tween = null
|
||||
var should_show: bool = requested_visible and _gameplay_started
|
||||
if should_show:
|
||||
var was_visible: bool = _shop_backdrop.visible
|
||||
_shop_backdrop.visible = true
|
||||
if not was_visible:
|
||||
_shop_backdrop.modulate.a = 0.0
|
||||
_shop_backdrop_tween = create_tween()
|
||||
_shop_backdrop_tween.tween_property(
|
||||
_shop_backdrop,
|
||||
"modulate:a",
|
||||
1.0,
|
||||
UIMotion.PLAYER_MENU_ENTER_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
return
|
||||
if not _shop_backdrop.visible:
|
||||
return
|
||||
_shop_backdrop_tween = create_tween()
|
||||
_shop_backdrop_tween.tween_property(
|
||||
_shop_backdrop,
|
||||
"modulate:a",
|
||||
0.0,
|
||||
UIMotion.PLAYER_MENU_EXIT_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
_shop_backdrop_tween.finished.connect(
|
||||
func() -> void:
|
||||
_shop_backdrop.visible = false
|
||||
_shop_backdrop.modulate.a = 1.0
|
||||
_shop_backdrop_tween = null
|
||||
)
|
||||
|
||||
|
||||
func _resize_native_overlays() -> void:
|
||||
if not is_node_ready():
|
||||
return
|
||||
_player_menu_backdrop.position = Vector2.ZERO
|
||||
_player_menu_backdrop.size = Vector2(get_window().size)
|
||||
_shop_backdrop.position = Vector2.ZERO
|
||||
_shop_backdrop.size = Vector2(get_window().size)
|
||||
|
||||
|
||||
func _on_new_game_requested(
|
||||
world_layout: StringName,
|
||||
world_seed: int,
|
||||
|
|
@ -2039,6 +1987,15 @@ func _on_new_game_requested(
|
|||
if _gameplay_started or _quit_in_progress:
|
||||
return
|
||||
_start_new_game_music()
|
||||
if (
|
||||
not _save_manager.delete_progression_save()
|
||||
or not _save_manager.initialize_new_game(world_seed, world_layout)
|
||||
):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not initialize local progression. Existing data was preserved where possible."
|
||||
)
|
||||
return
|
||||
if not _apply_world(world_layout, world_seed, true):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
|
|
@ -2054,16 +2011,6 @@ func _on_new_game_requested(
|
|||
if not _prepare_private_host():
|
||||
_show_title_music(true)
|
||||
return
|
||||
if (
|
||||
not _save_manager.delete_progression_save()
|
||||
or not _save_manager.initialize_new_game(world_seed, world_layout)
|
||||
):
|
||||
_network_session.disconnect_session("New Game setup failed.")
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not initialize local progression. Existing data was preserved where possible."
|
||||
)
|
||||
return
|
||||
_enter_gameplay()
|
||||
|
||||
|
||||
|
|
@ -2152,6 +2099,18 @@ func _on_new_slot_requested(
|
|||
_on_slot_play_requested(slot_id)
|
||||
return
|
||||
_start_new_game_music()
|
||||
# Reset progression before the private session publishes its first home
|
||||
# manifest. Doing this after hosting allowed the previous slot's RV tier to
|
||||
# be announced and spawned into an otherwise brand-new save.
|
||||
if (
|
||||
not _save_manager.delete_progression_save()
|
||||
or not _save_manager.initialize_new_game(world_seed, world_layout)
|
||||
):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not initialize local progression."
|
||||
)
|
||||
return
|
||||
if not _apply_world(world_layout, world_seed, true):
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
|
|
@ -2167,16 +2126,6 @@ func _on_new_slot_requested(
|
|||
if not _prepare_private_host():
|
||||
_show_title_music(true)
|
||||
return
|
||||
if (
|
||||
not _save_manager.delete_progression_save()
|
||||
or not _save_manager.initialize_new_game(world_seed, world_layout)
|
||||
):
|
||||
_network_session.disconnect_session("New Game setup failed.")
|
||||
_show_title_music(true)
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"Could not initialize local progression."
|
||||
)
|
||||
return
|
||||
_save_slots.mark_played(slot_id)
|
||||
_enter_gameplay()
|
||||
|
||||
|
|
@ -2243,10 +2192,17 @@ func _on_title_join_game_requested(requested_route: Variant) -> void:
|
|||
"That connection address is invalid."
|
||||
)
|
||||
return
|
||||
var active_slot: Dictionary = _save_slots.ensure_active_slot()
|
||||
if not bool(active_slot.get("ok", false)):
|
||||
var online_slot: Dictionary = _save_slots.ensure_online_slot()
|
||||
if not bool(online_slot.get("ok", false)):
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
str(active_slot.get("message", "Local progression is unavailable."))
|
||||
str(online_slot.get(
|
||||
"message", "Online progression is unavailable."
|
||||
))
|
||||
)
|
||||
return
|
||||
if not _save_manager.load_character_profile():
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"The selected online character could not be loaded."
|
||||
)
|
||||
return
|
||||
if _network_session.state != NetworkSessionType.State.INACTIVE:
|
||||
|
|
@ -2356,7 +2312,9 @@ func _show_next_social_prompt() -> void:
|
|||
_active_social_prompt = _social_prompt_queue.pop_front()
|
||||
var kind := str(_active_social_prompt.get("kind", ""))
|
||||
var display_name := str(
|
||||
_active_social_prompt.get("display_name", "Player")
|
||||
_active_social_prompt.get(
|
||||
"display_name", NetworkProfilePreferences.DEFAULT_DISPLAY_NAME
|
||||
)
|
||||
)
|
||||
var shown: bool = false
|
||||
if kind == "friend_request":
|
||||
|
|
@ -2468,7 +2426,11 @@ func _on_network_join_authenticated() -> void:
|
|||
var progression_ready: bool = (
|
||||
_save_manager.load_player_data()
|
||||
if inspection.can_continue()
|
||||
else _save_manager.initialize_new_game()
|
||||
else _save_manager.initialize_new_game(
|
||||
PlayerSaveManager.DEFAULT_WORLD_SEED,
|
||||
WorldLayout.GENERATED,
|
||||
false,
|
||||
)
|
||||
)
|
||||
if not progression_ready:
|
||||
_network_session.disconnect_session(
|
||||
|
|
@ -2662,6 +2624,12 @@ func _apply_world(
|
|||
|
||||
|
||||
func _refresh_active_world_bindings() -> void:
|
||||
if (
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or _shop_conversation_camera_restoring
|
||||
or _player.is_shop_conversation_camera_active()
|
||||
):
|
||||
_end_shop_npc_conversation(true)
|
||||
if is_instance_valid(_shop_interaction):
|
||||
if _shop_interaction.local_player_range_changed.is_connected(
|
||||
_on_shop_range_changed
|
||||
|
|
@ -2693,6 +2661,7 @@ func _refresh_active_world_bindings() -> void:
|
|||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||
_rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop()
|
||||
_active_shop_npc_interaction = null
|
||||
_storage_interaction = _test_world.get_player_storage()
|
||||
_network_sale.set_shop_interaction(_shop_interaction)
|
||||
_network_shop.set_shop_interaction(_shop_interaction)
|
||||
|
|
@ -2747,6 +2716,12 @@ func _refresh_active_world_bindings() -> void:
|
|||
|
||||
|
||||
func _on_water_recovery_starting() -> void:
|
||||
if (
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or _shop_conversation_camera_restoring
|
||||
or _player.is_shop_conversation_camera_active()
|
||||
):
|
||||
_end_shop_npc_conversation(true)
|
||||
_local_recovery_attempt_id = (
|
||||
"recovery:%s"
|
||||
% Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
|
|
@ -2957,6 +2932,15 @@ func _finish_quit() -> void:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if (
|
||||
is_instance_valid(_player)
|
||||
and (
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or _shop_conversation_camera_restoring
|
||||
or _player.is_shop_conversation_camera_active()
|
||||
)
|
||||
):
|
||||
_end_shop_npc_conversation(true)
|
||||
_cancel_home_entry_transition()
|
||||
_cancel_home_exit_transition(true)
|
||||
if _dedicated_runtime and _network_session != null:
|
||||
|
|
@ -2976,10 +2960,15 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _on_shop_range_changed(in_range: bool) -> void:
|
||||
_refresh_shop_npc_speech_state()
|
||||
# Speech ownership is resolved once per frame after all Area3D enter/exit
|
||||
# signals have settled, avoiding a one-frame clear during zone handoffs.
|
||||
if not in_range:
|
||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
if (
|
||||
_engaged_shop_npc_interaction == _shop_interaction
|
||||
and not _shop_conversation_shop_opened
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _on_storage_range_changed(in_range: bool) -> void:
|
||||
|
|
@ -2993,17 +2982,23 @@ func _on_storage_range_changed(in_range: bool) -> void:
|
|||
|
||||
|
||||
func _on_decor_shop_range_changed(in_range: bool) -> void:
|
||||
_refresh_shop_npc_speech_state()
|
||||
if not in_range:
|
||||
_game_ui.get_decor_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
if (
|
||||
_engaged_shop_npc_interaction == _decor_shop_interaction
|
||||
and not _shop_conversation_shop_opened
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _on_rv_upgrade_shop_range_changed(in_range: bool) -> void:
|
||||
_refresh_shop_npc_speech_state()
|
||||
if not in_range:
|
||||
_game_ui.get_rv_upgrade_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
if (
|
||||
_engaged_shop_npc_interaction == _rv_upgrade_shop_interaction
|
||||
and not _shop_conversation_shop_opened
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _on_home_state_changed() -> void:
|
||||
|
|
@ -3015,6 +3010,11 @@ func _on_local_home_space_changed(
|
|||
owner_fingerprint: String,
|
||||
) -> void:
|
||||
var inside_rv: bool = String(space_id).begins_with("rv:")
|
||||
_world_time_visuals.set_local_home_interior_active(inside_rv)
|
||||
_rain_ambience.set_suppressed(inside_rv)
|
||||
_shoreline_ambience.set_suppressed(inside_rv)
|
||||
_surface_drawings_root.visible = not inside_rv
|
||||
_world_gatherables_root.visible = not inside_rv
|
||||
_refresh_active_hotbar_item()
|
||||
_fishing_spot.set_fishing_environment_enabled(not inside_rv)
|
||||
_game_ui.set_rv_interior_camera_mode(
|
||||
|
|
@ -3094,7 +3094,12 @@ func _maybe_begin_home_exit_transition() -> void:
|
|||
_begin_home_exit_transition(owner_fingerprint)
|
||||
|
||||
|
||||
func _begin_home_exit_transition(owner_fingerprint: String) -> void:
|
||||
func _begin_home_exit_transition(
|
||||
owner_fingerprint: String,
|
||||
skip_walk: bool = false,
|
||||
) -> void:
|
||||
if owner_fingerprint.is_empty() or _home_exit_transition_active:
|
||||
return
|
||||
_home_exit_transition_active = true
|
||||
_home_exit_waiting_for_network = false
|
||||
_home_exit_start_position = _player.global_position
|
||||
|
|
@ -3107,8 +3112,11 @@ func _begin_home_exit_transition(owner_fingerprint: String) -> void:
|
|||
_game_ui.get_player_storage().close_storage()
|
||||
_game_ui.set_rv_storage_button_visible(false)
|
||||
_game_ui.set_gameplay_input_locked(true)
|
||||
var exit_direction: Vector3 = _home_world.get_interior_exit_direction(
|
||||
owner_fingerprint
|
||||
if skip_walk:
|
||||
_on_home_exit_walk_finished()
|
||||
return
|
||||
var exit_direction: Vector3 = _home_world.get_interior_exit_direction_near(
|
||||
owner_fingerprint, _player.global_position
|
||||
)
|
||||
_player.face_world_direction(exit_direction)
|
||||
_player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION)
|
||||
|
|
@ -3127,6 +3135,19 @@ func _begin_home_exit_transition(owner_fingerprint: String) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _on_home_toolbar_exit_requested() -> void:
|
||||
if _home_exit_transition_active:
|
||||
return
|
||||
var owner_fingerprint: String = (
|
||||
_network_home.get_local_home_owner_fingerprint()
|
||||
)
|
||||
if owner_fingerprint.is_empty():
|
||||
return
|
||||
if _home_entry_transition_active:
|
||||
_cancel_home_entry_transition()
|
||||
_begin_home_exit_transition(owner_fingerprint, true)
|
||||
|
||||
|
||||
func _on_home_exit_walk_finished() -> void:
|
||||
_home_exit_walk_tween = null
|
||||
if not _home_exit_transition_active:
|
||||
|
|
@ -3209,35 +3230,33 @@ func _on_rv_storage_requested() -> void:
|
|||
|
||||
func _can_show_shop_prompt() -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
and _shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
_get_active_shop_npc_interaction() == _shop_interaction
|
||||
and _can_show_active_shop_prompt(_shop_interaction)
|
||||
)
|
||||
|
||||
|
||||
func _can_show_decor_shop_prompt() -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
and _decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
_get_active_shop_npc_interaction() == _decor_shop_interaction
|
||||
and _can_show_active_shop_prompt(_decor_shop_interaction)
|
||||
)
|
||||
|
||||
|
||||
func _can_show_rv_upgrade_shop_prompt() -> bool:
|
||||
return (
|
||||
_get_active_shop_npc_interaction() == _rv_upgrade_shop_interaction
|
||||
and _can_show_active_shop_prompt(_rv_upgrade_shop_interaction)
|
||||
)
|
||||
|
||||
|
||||
func _can_show_active_shop_prompt(
|
||||
interaction: FishingShopInteractionType,
|
||||
) -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
and _rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
and not _shop_conversation_camera_restoring
|
||||
and interaction != null
|
||||
and interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
|
|
@ -3247,10 +3266,8 @@ func _can_show_rv_upgrade_shop_prompt() -> bool:
|
|||
|
||||
|
||||
func _can_show_any_shop_prompt() -> bool:
|
||||
return (
|
||||
_can_show_shop_prompt()
|
||||
or _can_show_decor_shop_prompt()
|
||||
or _can_show_rv_upgrade_shop_prompt()
|
||||
return _can_show_active_shop_prompt(
|
||||
_get_active_shop_npc_interaction()
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -3272,34 +3289,181 @@ func _is_any_shop_npc_in_range() -> bool:
|
|||
|
||||
|
||||
func _refresh_shop_npc_speech_state() -> void:
|
||||
if (
|
||||
_engaged_shop_npc_interaction != null
|
||||
and (
|
||||
not is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or not _engaged_shop_npc_interaction.is_local_player_in_range()
|
||||
)
|
||||
):
|
||||
if _shop_conversation_shop_opened and _is_any_shop_menu_visible():
|
||||
_game_ui.set_shop_npc_player_in_range(false)
|
||||
return
|
||||
_end_shop_npc_conversation(false)
|
||||
var interaction := _get_active_shop_npc_interaction()
|
||||
var dialogue_text: String = ""
|
||||
if interaction != null:
|
||||
dialogue_text = interaction.get_dialogue_text(_home_state.get_tier())
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
interaction != null,
|
||||
interaction != null and not _shop_conversation_shop_opened,
|
||||
dialogue_text,
|
||||
interaction.get_instance_id() if interaction != null else 0,
|
||||
interaction == _engaged_shop_npc_interaction
|
||||
and not _shop_conversation_shop_opened,
|
||||
)
|
||||
|
||||
|
||||
func _get_active_shop_npc_interaction() -> FishingShopInteractionType:
|
||||
# Match the interaction and prompt priority used above: RV, decor, fishing.
|
||||
if (
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
is_instance_valid(_engaged_shop_npc_interaction)
|
||||
and _engaged_shop_npc_interaction.is_local_player_in_range()
|
||||
):
|
||||
return _rv_upgrade_shop_interaction
|
||||
_active_shop_npc_interaction = _engaged_shop_npc_interaction
|
||||
return _active_shop_npc_interaction
|
||||
var candidates: Array[FishingShopInteractionType] = []
|
||||
if _shop_interaction != null:
|
||||
candidates.append(_shop_interaction)
|
||||
if _decor_shop_interaction != null:
|
||||
candidates.append(_decor_shop_interaction)
|
||||
if _rv_upgrade_shop_interaction != null:
|
||||
candidates.append(_rv_upgrade_shop_interaction)
|
||||
var nearest: FishingShopInteractionType
|
||||
var nearest_distance_squared: float = INF
|
||||
for interaction: FishingShopInteractionType in candidates:
|
||||
if not interaction.is_local_player_in_range():
|
||||
continue
|
||||
var distance_squared := interaction.get_local_player_distance_squared()
|
||||
if distance_squared < nearest_distance_squared:
|
||||
nearest = interaction
|
||||
nearest_distance_squared = distance_squared
|
||||
if (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
nearest != null
|
||||
and nearest != _active_shop_npc_interaction
|
||||
and is_instance_valid(_active_shop_npc_interaction)
|
||||
and _active_shop_npc_interaction.is_local_player_in_range()
|
||||
):
|
||||
return _decor_shop_interaction
|
||||
var active_distance_squared := (
|
||||
_active_shop_npc_interaction.get_local_player_distance_squared()
|
||||
)
|
||||
# Require a small, visible distance advantage before ownership switches.
|
||||
# This keeps a player standing on the zone seam from chattering between
|
||||
# two NPCs while still handing off well before the old compact zone ends.
|
||||
if (
|
||||
sqrt(nearest_distance_squared)
|
||||
+ SHOP_NPC_HANDOFF_HYSTERESIS_METERS
|
||||
>= sqrt(active_distance_squared)
|
||||
):
|
||||
nearest = _active_shop_npc_interaction
|
||||
_active_shop_npc_interaction = nearest
|
||||
return _active_shop_npc_interaction
|
||||
|
||||
|
||||
func _begin_shop_npc_conversation(
|
||||
interaction: FishingShopInteractionType,
|
||||
) -> bool:
|
||||
if (
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
_shop_conversation_camera_restoring
|
||||
or is_instance_valid(_engaged_shop_npc_interaction)
|
||||
or interaction == null
|
||||
or not interaction.is_local_player_in_range()
|
||||
or not _fishing_spot.can_open_fishing_shop()
|
||||
):
|
||||
return _shop_interaction
|
||||
return null
|
||||
return false
|
||||
_engaged_shop_npc_interaction = interaction
|
||||
_active_shop_npc_interaction = interaction
|
||||
_shop_conversation_shop_opened = false
|
||||
interaction.set_engaged(true)
|
||||
_player.begin_shop_conversation_camera(
|
||||
interaction.get_shopkeeper_position()
|
||||
)
|
||||
_player.set_local_input_suppressed(
|
||||
SHOP_NPC_CONVERSATION_INPUT_OWNER, true
|
||||
)
|
||||
_player.set_camera_input_suppressed(
|
||||
SHOP_NPC_CONVERSATION_INPUT_OWNER, true
|
||||
)
|
||||
_refresh_shop_npc_speech_state()
|
||||
return true
|
||||
|
||||
|
||||
func _open_engaged_shop() -> bool:
|
||||
var interaction := _engaged_shop_npc_interaction
|
||||
if (
|
||||
interaction == null
|
||||
or not is_instance_valid(interaction)
|
||||
or not interaction.is_local_player_in_range()
|
||||
or _shop_conversation_shop_opened
|
||||
):
|
||||
return false
|
||||
var opened: bool = false
|
||||
if interaction == _rv_upgrade_shop_interaction:
|
||||
opened = _game_ui.get_rv_upgrade_shop().open_shop()
|
||||
elif interaction == _decor_shop_interaction:
|
||||
opened = _game_ui.get_decor_shop().open_shop()
|
||||
elif interaction == _shop_interaction:
|
||||
opened = _game_ui.get_fishing_shop().open_shop()
|
||||
if not opened:
|
||||
return false
|
||||
_shop_conversation_shop_opened = true
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
_refresh_shop_npc_speech_state()
|
||||
return true
|
||||
|
||||
|
||||
func _end_shop_npc_conversation(immediate_camera: bool = false) -> void:
|
||||
if _shop_conversation_camera_restoring and not immediate_camera:
|
||||
return
|
||||
var interaction := _engaged_shop_npc_interaction
|
||||
if is_instance_valid(interaction):
|
||||
interaction.set_engaged(false)
|
||||
_engaged_shop_npc_interaction = null
|
||||
_shop_conversation_shop_opened = false
|
||||
_game_ui.set_shop_npc_player_in_range(false)
|
||||
var camera_was_active := _player.is_shop_conversation_camera_active()
|
||||
_shop_conversation_camera_restoring = camera_was_active and not immediate_camera
|
||||
_player.restore_shop_conversation_camera(
|
||||
immediate_camera,
|
||||
_finish_shop_npc_conversation_restore,
|
||||
)
|
||||
|
||||
|
||||
func _finish_shop_npc_conversation_restore() -> void:
|
||||
_player.set_local_input_suppressed(
|
||||
SHOP_NPC_CONVERSATION_INPUT_OWNER, false
|
||||
)
|
||||
_player.set_camera_input_suppressed(
|
||||
SHOP_NPC_CONVERSATION_INPUT_OWNER, false
|
||||
)
|
||||
_shop_conversation_camera_restoring = false
|
||||
if _gameplay_started and is_instance_valid(_game_ui):
|
||||
_refresh_shop_npc_speech_state()
|
||||
|
||||
|
||||
func _is_any_shop_menu_visible() -> bool:
|
||||
return (
|
||||
_game_ui.get_fishing_shop().visible
|
||||
or _game_ui.get_decor_shop().visible
|
||||
or _game_ui.get_rv_upgrade_shop().visible
|
||||
)
|
||||
|
||||
|
||||
func _on_shop_conversation_menu_visibility_changed(is_open: bool) -> void:
|
||||
if (
|
||||
not is_open
|
||||
and _shop_conversation_shop_opened
|
||||
and not _is_any_shop_menu_visible()
|
||||
):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _on_pause_menu_conversation_visibility_changed(is_open: bool) -> void:
|
||||
if is_open and is_instance_valid(_engaged_shop_npc_interaction):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _on_player_menu_conversation_visibility_changed(is_open: bool) -> void:
|
||||
if is_open and is_instance_valid(_engaged_shop_npc_interaction):
|
||||
_end_shop_npc_conversation(false)
|
||||
|
||||
|
||||
func _can_show_storage_prompt() -> bool:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue