Fix multiplayer parity and release regressions
This commit is contained in:
parent
b18d9392d8
commit
c0ff78322d
17 changed files with 378 additions and 17 deletions
|
|
@ -9,6 +9,7 @@ enum Status {
|
|||
INVALID_BUYER,
|
||||
INVALID_OFFER,
|
||||
INVALID_SELECTION,
|
||||
RESERVED,
|
||||
TRANSACTION_FAILED,
|
||||
}
|
||||
|
||||
|
|
@ -46,5 +47,7 @@ func get_message() -> String:
|
|||
return "invalid buyer offer."
|
||||
Status.INVALID_SELECTION:
|
||||
return "the fish selection is invalid."
|
||||
Status.RESERVED:
|
||||
return "reserved fish cannot be sold."
|
||||
_:
|
||||
return "transaction failed."
|
||||
|
|
|
|||
|
|
@ -11,14 +11,17 @@ signal sale_completed(result: FishSaleResultType)
|
|||
|
||||
var _inventory: FishInventoryType
|
||||
var _wallet: PlayerWalletType
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
||||
|
||||
func setup(
|
||||
inventory: FishInventoryType,
|
||||
wallet: PlayerWalletType,
|
||||
reservations: PlayerAssetReservationService = null,
|
||||
) -> void:
|
||||
_inventory = inventory
|
||||
_wallet = wallet
|
||||
_reservations = reservations
|
||||
|
||||
|
||||
func can_sell(
|
||||
|
|
@ -114,6 +117,12 @@ func _validate_batch(
|
|||
if fish_catch == null:
|
||||
result.status = FishSaleResultType.Status.NOT_FOUND
|
||||
return result
|
||||
if (
|
||||
_reservations != null
|
||||
and _reservations.is_fish_reserved(catch_id)
|
||||
):
|
||||
result.status = FishSaleResultType.Status.RESERVED
|
||||
return result
|
||||
if fish_catch.is_favorited:
|
||||
contains_favorite = true
|
||||
if fish_catch.sale_value < 0:
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@ func show_reel_position(world_position: Vector3, _input_held: bool) -> void:
|
|||
func play_outcome(outcome: StringName) -> void:
|
||||
if _mode == VisualMode.NONE:
|
||||
cleanup()
|
||||
# Gameplay completion must not depend on a presentation tween still
|
||||
# being active. The caller owns idempotency for the outcome.
|
||||
outcome_completed.emit(outcome)
|
||||
return
|
||||
|
||||
_kill_active_tween()
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ var _pending_catch: FishCatchType
|
|||
var _showcase_ready: bool = false
|
||||
var _put_away_press_armed: bool = false
|
||||
var _showcase_restore_generation: int = 0
|
||||
var _showcase_outcome_completed: bool = false
|
||||
var _network_auto_click_accumulator: float = 0.0
|
||||
var _network_active_barrier_index: int = -1
|
||||
|
||||
|
|
@ -874,6 +875,7 @@ func _on_catch_completed() -> void:
|
|||
return
|
||||
state = FishingState.SHOWING_CATCH
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
_catch_controller.reset()
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
|
|
@ -896,8 +898,10 @@ func _on_outcome_completed(outcome: StringName) -> void:
|
|||
or state != FishingState.SHOWING_CATCH
|
||||
or _active_player == null
|
||||
or _pending_catch == null
|
||||
or _showcase_outcome_completed
|
||||
):
|
||||
return
|
||||
_showcase_outcome_completed = true
|
||||
_showcase_ready = true
|
||||
_active_player.begin_catch_showcase(_pending_catch)
|
||||
showcase_changed.emit(
|
||||
|
|
@ -923,6 +927,7 @@ func _put_away_catch() -> void:
|
|||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
_showcase_restore_generation += 1
|
||||
|
|
@ -973,6 +978,7 @@ func _cleanup_attempt(
|
|||
_selected_fish = null
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
_catch_controller.reset()
|
||||
|
|
@ -1257,6 +1263,7 @@ func _on_network_catch_received(fish_catch: FishCatchType) -> void:
|
|||
_pending_catch = fish_catch
|
||||
state = FishingState.SHOWING_CATCH
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
catch_display_changed.emit(
|
||||
0.0, 0.0, PackedFloat32Array(), PackedInt32Array(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ signal catches_changed
|
|||
|
||||
var _catches: Array[FishCatchType] = []
|
||||
var _next_catch_sequence: int = 1
|
||||
var _reservation_service: PlayerAssetReservationService
|
||||
|
||||
|
||||
func set_reservation_service(
|
||||
reservation_service: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
_reservation_service = reservation_service
|
||||
|
||||
|
||||
func add_catch(fish_catch: FishCatchType) -> void:
|
||||
|
|
@ -101,6 +108,25 @@ func remove_catch_by_id(catch_id: StringName) -> FishCatchType:
|
|||
|
||||
func remove_catches_by_ids(
|
||||
catch_ids: Array[StringName],
|
||||
) -> Array[FishCatchType]:
|
||||
return _remove_catches_by_ids(catch_ids, "", "")
|
||||
|
||||
|
||||
func remove_reserved_catch_for_mail_transfer(
|
||||
catch_id: StringName,
|
||||
reservation_id: String,
|
||||
transfer_id: String,
|
||||
) -> FishCatchType:
|
||||
var removed := _remove_catches_by_ids(
|
||||
[catch_id], reservation_id, transfer_id
|
||||
)
|
||||
return removed.front() if removed.size() == 1 else null
|
||||
|
||||
|
||||
func _remove_catches_by_ids(
|
||||
catch_ids: Array[StringName],
|
||||
mail_reservation_id: String,
|
||||
mail_transfer_id: String,
|
||||
) -> Array[FishCatchType]:
|
||||
var removed: Array[FishCatchType] = []
|
||||
if catch_ids.is_empty():
|
||||
|
|
@ -115,6 +141,17 @@ func remove_catches_by_ids(
|
|||
if fish_catch == null:
|
||||
removed.clear()
|
||||
return removed
|
||||
if (
|
||||
_reservation_service != null
|
||||
and _reservation_service.is_fish_reserved(catch_id)
|
||||
and not _reservation_service.authorize_mail_fish_removal(
|
||||
mail_reservation_id,
|
||||
catch_id,
|
||||
mail_transfer_id,
|
||||
)
|
||||
):
|
||||
removed.clear()
|
||||
return removed
|
||||
removed.append(fish_catch)
|
||||
|
||||
var remaining: Array[FishCatchType] = []
|
||||
|
|
|
|||
58
main/main.gd
58
main/main.gd
|
|
@ -133,6 +133,7 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
|||
)
|
||||
@onready var _players_root: Node3D = $Players
|
||||
@onready var _title_background: ColorRect = %TitleBackground
|
||||
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
||||
|
||||
var _gameplay_started: bool = false
|
||||
var _shop_interaction: FishingShopInteractionType
|
||||
|
|
@ -152,10 +153,13 @@ var _data_folder_picker_generation: int = 0
|
|||
var _restore_data_setup_after_picker: bool = false
|
||||
var _application_initialized := false
|
||||
var _pending_existing_root_path := ""
|
||||
var _local_recovery_attempt_id: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DisplayServer.window_set_title("NETfishing")
|
||||
get_window().size_changed.connect(_resize_native_overlays)
|
||||
_resize_native_overlays()
|
||||
if not _settings_manager.settings_changed.is_connected(
|
||||
_apply_runtime_settings
|
||||
):
|
||||
|
|
@ -254,11 +258,15 @@ func _initialize_after_data_root() -> void:
|
|||
_network_session.remote_recovery_requested.connect(
|
||||
_on_remote_recovery_requested
|
||||
)
|
||||
_network_session.remote_recovery_presentation_changed.connect(
|
||||
_on_remote_recovery_presentation_changed
|
||||
)
|
||||
|
||||
|
||||
_player.fish_sale_service.setup(
|
||||
_player.inventory,
|
||||
_player.wallet
|
||||
_player.wallet,
|
||||
_asset_reservations
|
||||
)
|
||||
_player.bag.setup(item_catalog)
|
||||
_player.hotbar.setup(_player.bag, item_catalog)
|
||||
|
|
@ -282,6 +290,7 @@ func _initialize_after_data_root() -> void:
|
|||
_asset_reservations.setup(
|
||||
_player.wallet, _player.inventory, _player.bag, item_catalog
|
||||
)
|
||||
_player.inventory.set_reservation_service(_asset_reservations)
|
||||
_network_mail.setup(
|
||||
_network_session,
|
||||
_asset_reservations,
|
||||
|
|
@ -426,6 +435,9 @@ func _initialize_after_data_root() -> void:
|
|||
_game_ui.interactive_pointer_ui_changed.connect(
|
||||
_ui_pixelation.set_interactive_ui_open
|
||||
)
|
||||
_game_ui.player_menu_backdrop_visibility_changed.connect(
|
||||
_set_player_menu_backdrop_visible
|
||||
)
|
||||
_pixelation_reset.return_to_settings_requested.connect(
|
||||
_game_ui.focus_open_settings_back_button
|
||||
)
|
||||
|
|
@ -475,6 +487,9 @@ func _initialize_after_data_root() -> void:
|
|||
_water_recovery.recovery_starting.connect(
|
||||
_on_water_recovery_starting
|
||||
)
|
||||
_water_recovery.recovery_finished.connect(
|
||||
_on_water_recovery_finished
|
||||
)
|
||||
_water_recovery.local_respawn_completed.connect(
|
||||
_on_local_respawn_completed
|
||||
)
|
||||
|
|
@ -885,6 +900,19 @@ func _set_gameplay_active(active: bool) -> void:
|
|||
_save_manager.set_autosave_enabled(active)
|
||||
if active:
|
||||
_refresh_active_hotbar_item()
|
||||
else:
|
||||
_set_player_menu_backdrop_visible(false)
|
||||
|
||||
|
||||
func _set_player_menu_backdrop_visible(is_visible: bool) -> void:
|
||||
_player_menu_backdrop.visible = is_visible and _gameplay_started
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
func _on_new_game_requested() -> void:
|
||||
|
|
@ -1128,11 +1156,39 @@ func _on_reset_progress_requested() -> void:
|
|||
|
||||
|
||||
func _on_water_recovery_starting() -> void:
|
||||
_local_recovery_attempt_id = (
|
||||
"recovery:%s"
|
||||
% Crypto.new().generate_random_bytes(16).hex_encode()
|
||||
)
|
||||
_network_session.request_recovery_presentation(
|
||||
true, _local_recovery_attempt_id
|
||||
)
|
||||
_game_ui.close_player_menu_for_water_recovery()
|
||||
_game_ui.get_pause_menu().close_for_water_recovery()
|
||||
_game_ui.get_fishing_shop().close_for_water_recovery()
|
||||
|
||||
|
||||
func _on_water_recovery_finished() -> void:
|
||||
if _local_recovery_attempt_id.is_empty():
|
||||
return
|
||||
_network_session.request_recovery_presentation(
|
||||
false, _local_recovery_attempt_id
|
||||
)
|
||||
_local_recovery_attempt_id = ""
|
||||
|
||||
|
||||
func _on_remote_recovery_presentation_changed(
|
||||
peer_id: int,
|
||||
active: bool,
|
||||
_attempt_id: String,
|
||||
) -> void:
|
||||
var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
|
||||
if avatar == null or avatar == _player:
|
||||
return
|
||||
avatar.set_water_recovery_active(active)
|
||||
avatar.set_remote_recovery_presentation(active)
|
||||
|
||||
|
||||
func _on_active_hotbar_item_changed(
|
||||
_slot_index: int,
|
||||
item_id: StringName,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="PlayerMenuBackdrop" type="ColorRect" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
z_index = 10
|
||||
mouse_filter = 2
|
||||
color = Color(0.015, 0.02, 0.03, 0.72)
|
||||
|
||||
[node name="InterfaceFontController" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("39_fonts")
|
||||
|
|
@ -192,6 +199,7 @@ unique_name_in_owner = true
|
|||
|
||||
[node name="UIPresentation" type="SubViewportContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
z_index = 20
|
||||
texture_filter = 1
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 648.0
|
||||
|
|
|
|||
|
|
@ -552,7 +552,7 @@ func _commit_sender(transfer_id: String, letter: Dictionary) -> void:
|
|||
var snapshot := _capture_assets()
|
||||
var applied := (
|
||||
_reservations.has_reservation(reservation_id)
|
||||
and _reservations.commit_removal(reservation_id)
|
||||
and _reservations.commit_removal(reservation_id, transfer_id)
|
||||
and _save_manager.save_if_dirty()
|
||||
)
|
||||
if not applied:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ signal server_trust_required(
|
|||
signal peer_identity_observed(peer_id: int, status: String)
|
||||
signal server_lost
|
||||
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
|
||||
signal remote_recovery_presentation_changed(
|
||||
peer_id: int,
|
||||
active: bool,
|
||||
attempt_id: String,
|
||||
)
|
||||
|
||||
enum State {
|
||||
INACTIVE,
|
||||
|
|
@ -84,6 +89,7 @@ var _pending_identity_challenges: Dictionary[int, Dictionary] = {}
|
|||
var _authenticated_identity_cache: Dictionary[int, Dictionary] = {}
|
||||
var _client_identity_attempt: Dictionary = {}
|
||||
var _pending_server_proof: Dictionary = {}
|
||||
var _recovery_attempts: Dictionary[int, String] = {}
|
||||
var _server_identity_fingerprint: String = ""
|
||||
var _server_identity_public_key: String = ""
|
||||
var _session_identity_keys: Dictionary[String, String] = {}
|
||||
|
|
@ -668,6 +674,12 @@ func _on_peer_disconnected(peer_id: int) -> void:
|
|||
_pending_authentication.erase(peer_id)
|
||||
_pending_identity_challenges.erase(peer_id)
|
||||
_authenticated_identity_cache.erase(peer_id)
|
||||
var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||
if not recovery_attempt.is_empty():
|
||||
_recovery_attempts.erase(peer_id)
|
||||
remote_recovery_presentation_changed.emit(
|
||||
peer_id, false, recovery_attempt
|
||||
)
|
||||
if _registry.has_peer(peer_id):
|
||||
_registry.remove_peer(peer_id)
|
||||
_spawn_service.remove_peer(peer_id)
|
||||
|
|
@ -1320,6 +1332,12 @@ func _send_local_input() -> void:
|
|||
submit_movement_input.rpc_id(1, input)
|
||||
|
||||
|
||||
func submit_neutral_local_movement() -> void:
|
||||
if state != State.JOINED_CLIENT:
|
||||
return
|
||||
_send_local_input()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
|
||||
func submit_movement_input(data: Dictionary) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
|
|
@ -1400,6 +1418,99 @@ func publish_authoritative_teleport(peer_id: int) -> void:
|
|||
)
|
||||
|
||||
|
||||
func request_recovery_presentation(
|
||||
active: bool,
|
||||
attempt_id: String,
|
||||
) -> void:
|
||||
if (
|
||||
attempt_id.is_empty()
|
||||
or attempt_id.length() > 96
|
||||
or not is_gameplay_session_active()
|
||||
):
|
||||
return
|
||||
if is_host():
|
||||
_set_authoritative_recovery_presentation(
|
||||
get_local_peer_id(), active, attempt_id
|
||||
)
|
||||
elif state == State.JOINED_CLIENT:
|
||||
submit_recovery_presentation.rpc_id(
|
||||
1, _session_id, active, attempt_id
|
||||
)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
func submit_recovery_presentation(
|
||||
session_id: String,
|
||||
active: bool,
|
||||
attempt_id: String,
|
||||
) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if (
|
||||
not is_host()
|
||||
or not _registry.has_peer(sender_id)
|
||||
or session_id != _session_id
|
||||
or attempt_id.is_empty()
|
||||
or attempt_id.length() > 96
|
||||
):
|
||||
return
|
||||
_set_authoritative_recovery_presentation(sender_id, active, attempt_id)
|
||||
|
||||
|
||||
func _set_authoritative_recovery_presentation(
|
||||
peer_id: int,
|
||||
active: bool,
|
||||
attempt_id: String,
|
||||
) -> void:
|
||||
var current_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||
if active:
|
||||
if not current_attempt.is_empty() and current_attempt != attempt_id:
|
||||
return
|
||||
elif current_attempt != attempt_id:
|
||||
return
|
||||
var data := {
|
||||
"session_id": _session_id,
|
||||
"generation": _operation_generation,
|
||||
"peer_id": peer_id,
|
||||
"active": active,
|
||||
"attempt_id": attempt_id,
|
||||
}
|
||||
_apply_recovery_presentation(data)
|
||||
receive_recovery_presentation.rpc(data)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_recovery_presentation(data: Dictionary) -> void:
|
||||
_apply_recovery_presentation(data)
|
||||
|
||||
|
||||
func _apply_recovery_presentation(data: Dictionary) -> void:
|
||||
if (
|
||||
typeof(data.get("session_id")) != TYPE_STRING
|
||||
or str(data["session_id"]) != _session_id
|
||||
or typeof(data.get("generation")) != TYPE_INT
|
||||
or int(data["generation"]) < 1
|
||||
or typeof(data.get("peer_id")) != TYPE_INT
|
||||
or typeof(data.get("active")) != TYPE_BOOL
|
||||
or typeof(data.get("attempt_id")) != TYPE_STRING
|
||||
or str(data["attempt_id"]).is_empty()
|
||||
or str(data["attempt_id"]).length() > 96
|
||||
):
|
||||
return
|
||||
var peer_id: int = data["peer_id"]
|
||||
var attempt_id: String = data["attempt_id"]
|
||||
var active: bool = data["active"]
|
||||
var current_attempt: String = _recovery_attempts.get(peer_id, "")
|
||||
if active:
|
||||
if not current_attempt.is_empty() and current_attempt != attempt_id:
|
||||
return
|
||||
_recovery_attempts[peer_id] = attempt_id
|
||||
elif current_attempt != attempt_id:
|
||||
return
|
||||
else:
|
||||
_recovery_attempts.erase(peer_id)
|
||||
remote_recovery_presentation_changed.emit(peer_id, active, attempt_id)
|
||||
|
||||
|
||||
func request_safe_respawn(entry_position: Vector3) -> void:
|
||||
if not entry_position.is_finite():
|
||||
return
|
||||
|
|
@ -1535,6 +1646,11 @@ func _fail(message: String) -> void:
|
|||
|
||||
|
||||
func _teardown_peer() -> void:
|
||||
for peer_id: int in _recovery_attempts.keys():
|
||||
remote_recovery_presentation_changed.emit(
|
||||
peer_id, false, _recovery_attempts[peer_id]
|
||||
)
|
||||
_recovery_attempts.clear()
|
||||
_pending_authentication.clear()
|
||||
_pending_identity_challenges.clear()
|
||||
_authenticated_identity_cache.clear()
|
||||
|
|
|
|||
|
|
@ -105,6 +105,9 @@ var _camera_dragging: bool = false
|
|||
var _camera_input_enabled: bool = true
|
||||
var _movement_enabled: bool = true
|
||||
var _water_recovery_active: bool = false
|
||||
var _remote_recovery_presentation_active: bool = false
|
||||
var _remote_recovery_visual_origin: Vector3
|
||||
var _remote_recovery_elapsed: float = 0.0
|
||||
var _target_zoom: float = 5.0
|
||||
var _showcase_rod_visibility: bool = true
|
||||
var _remote_presentation_visible := true
|
||||
|
|
@ -212,6 +215,14 @@ func _physics_process(delta: float) -> void:
|
|||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _remote_recovery_presentation_active:
|
||||
_remote_recovery_elapsed += delta
|
||||
_visuals.position = (
|
||||
_remote_recovery_visual_origin
|
||||
+ Vector3.UP
|
||||
* sin(_remote_recovery_elapsed * 2.0 * TAU)
|
||||
* 0.08
|
||||
)
|
||||
if not local_control_enabled:
|
||||
return
|
||||
|
||||
|
|
@ -338,6 +349,16 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
|
|||
|
||||
|
||||
func capture_network_input(sequence: int) -> Dictionary:
|
||||
if not _movement_enabled or _water_recovery_active:
|
||||
return {
|
||||
"sequence": sequence,
|
||||
"axis": [0.0, 0.0],
|
||||
"camera_yaw": _camera_yaw.global_rotation.y,
|
||||
"jump": false,
|
||||
"sprint": false,
|
||||
"sneak": false,
|
||||
"slow_walk": false,
|
||||
}
|
||||
var axis: Vector2 = Input.get_vector(
|
||||
"move_left",
|
||||
"move_right",
|
||||
|
|
@ -525,6 +546,17 @@ func is_water_recovery_active() -> bool:
|
|||
return _water_recovery_active
|
||||
|
||||
|
||||
func set_remote_recovery_presentation(active: bool) -> void:
|
||||
if local_control_enabled or active == _remote_recovery_presentation_active:
|
||||
return
|
||||
_remote_recovery_presentation_active = active
|
||||
_remote_recovery_elapsed = 0.0
|
||||
if active:
|
||||
_remote_recovery_visual_origin = _visuals.position
|
||||
else:
|
||||
_visuals.position = _remote_recovery_visual_origin
|
||||
|
||||
|
||||
func prepare_for_water_recovery() -> void:
|
||||
_restore_gameplay_presentation_for_recovery()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ var _inventory: FishInventory
|
|||
var _bag: PlayerBag
|
||||
var _catalog: ItemCatalog
|
||||
var _reservations: Dictionary[String, Dictionary] = {}
|
||||
var _mail_transfer_authorizations: Dictionary[String, Dictionary] = {}
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -63,6 +64,7 @@ func reserve(reservation_id: String, attachment: Dictionary) -> bool:
|
|||
func release(reservation_id: String) -> bool:
|
||||
if not _reservations.erase(reservation_id):
|
||||
return false
|
||||
_mail_transfer_authorizations.erase(reservation_id)
|
||||
reservations_changed.emit()
|
||||
return true
|
||||
|
||||
|
|
@ -71,9 +73,46 @@ func release_all() -> void:
|
|||
if _reservations.is_empty():
|
||||
return
|
||||
_reservations.clear()
|
||||
_mail_transfer_authorizations.clear()
|
||||
reservations_changed.emit()
|
||||
|
||||
|
||||
func authorize_mail_transfer(
|
||||
reservation_id: String,
|
||||
catch_id: StringName,
|
||||
transfer_id: String,
|
||||
) -> bool:
|
||||
var attachment := get_reservation(reservation_id)
|
||||
if (
|
||||
transfer_id.is_empty()
|
||||
or transfer_id.length() > 96
|
||||
or attachment.is_empty()
|
||||
or int(attachment.get("type", 0)) != AttachmentType.FISH
|
||||
or StringName(str(attachment.get("catch_id", ""))) != catch_id
|
||||
):
|
||||
return false
|
||||
_mail_transfer_authorizations[reservation_id] = {
|
||||
"catch_id": catch_id,
|
||||
"transfer_id": transfer_id,
|
||||
}
|
||||
return true
|
||||
|
||||
|
||||
func authorize_mail_fish_removal(
|
||||
reservation_id: String,
|
||||
catch_id: StringName,
|
||||
transfer_id: String,
|
||||
) -> bool:
|
||||
var authorization: Dictionary = _mail_transfer_authorizations.get(
|
||||
reservation_id, {}
|
||||
)
|
||||
return (
|
||||
not authorization.is_empty()
|
||||
and StringName(authorization.get("catch_id", StringName())) == catch_id
|
||||
and str(authorization.get("transfer_id", "")) == transfer_id
|
||||
)
|
||||
|
||||
|
||||
func has_reservation(reservation_id: String) -> bool:
|
||||
return _reservations.has(reservation_id)
|
||||
|
||||
|
|
@ -138,7 +177,10 @@ func can_commit(reservation_id: String) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func commit_removal(reservation_id: String) -> bool:
|
||||
func commit_removal(
|
||||
reservation_id: String,
|
||||
mail_transfer_id: String = "",
|
||||
) -> bool:
|
||||
var attachment := get_reservation(reservation_id)
|
||||
if attachment.is_empty() or not can_commit(reservation_id):
|
||||
return false
|
||||
|
|
@ -147,9 +189,16 @@ func commit_removal(reservation_id: String) -> bool:
|
|||
AttachmentType.FISH_COIN:
|
||||
applied = _wallet.debit(int(attachment["amount"]))
|
||||
AttachmentType.FISH:
|
||||
applied = _inventory.remove_catch_by_id(
|
||||
StringName(str(attachment["catch_id"]))
|
||||
) != null
|
||||
var catch_id := StringName(str(attachment["catch_id"]))
|
||||
if authorize_mail_transfer(
|
||||
reservation_id, catch_id, mail_transfer_id
|
||||
):
|
||||
applied = (
|
||||
_inventory.remove_reserved_catch_for_mail_transfer(
|
||||
catch_id, reservation_id, mail_transfer_id
|
||||
)
|
||||
!= null
|
||||
)
|
||||
AttachmentType.CONSUMABLE:
|
||||
applied = _bag.remove_item(
|
||||
StringName(str(attachment["item_id"])),
|
||||
|
|
@ -157,6 +206,8 @@ func commit_removal(reservation_id: String) -> bool:
|
|||
)
|
||||
if applied:
|
||||
release(reservation_id)
|
||||
else:
|
||||
_mail_transfer_authorizations.erase(reservation_id)
|
||||
return applied
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ func open_chat() -> void:
|
|||
_player.set_movement_enabled(false)
|
||||
_player.set_camera_input_enabled(false)
|
||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||
# Send the host a zeroed frame immediately so it cannot keep applying the
|
||||
# last movement state while this LineEdit owns keyboard input.
|
||||
_session.submit_neutral_local_movement()
|
||||
_panel.show()
|
||||
_entry.show()
|
||||
_entry.grab_focus()
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ enum CloseReason {
|
|||
}
|
||||
|
||||
@onready var _wallet_label: Label = %WalletLabel
|
||||
@onready var _fish_sales: VBoxContainer = (
|
||||
$ShopPanel/Margin/Layout/Body/FishSales
|
||||
)
|
||||
@onready var _fish_list: ItemList = %FishList
|
||||
@onready var _sales_title: Label = %SalesTitle
|
||||
@onready var _fish_empty: Label = %FishEmpty
|
||||
|
|
@ -94,6 +97,9 @@ var _closing: bool = false
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
# Catch selling belongs to the Pelican action in the Player Menu. Keep the
|
||||
# legacy shop sales controls out of the active interface.
|
||||
_fish_sales.hide()
|
||||
%CloseButton.pressed.connect(close_shop)
|
||||
_fish_list.item_selected.connect(_on_fish_selected)
|
||||
_fish_list.item_clicked.connect(_on_fish_clicked)
|
||||
|
|
@ -425,8 +431,15 @@ func _refresh_upgrades() -> void:
|
|||
reel_cost < 0
|
||||
or _transaction_in_progress
|
||||
or _closing
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_purchase()
|
||||
or not _upgrades.can_purchase_reel_speed(_wallet)
|
||||
)
|
||||
_reel_purchase.tooltip_text = (
|
||||
"Purchases are unavailable in this session."
|
||||
if _network_shop == null or not _network_shop.can_request_purchase()
|
||||
else ""
|
||||
)
|
||||
if reel_cost < 0:
|
||||
_reel_effect.text = "%.2f×" % _upgrades.get_reel_speed_multiplier()
|
||||
_reel_cost.text = "max"
|
||||
|
|
@ -447,8 +460,11 @@ func _refresh_upgrades() -> void:
|
|||
barrier_cost < 0
|
||||
or _transaction_in_progress
|
||||
or _closing
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_purchase()
|
||||
or not _upgrades.can_purchase_barrier_power(_wallet)
|
||||
)
|
||||
_barrier_purchase.tooltip_text = _reel_purchase.tooltip_text
|
||||
if barrier_cost < 0:
|
||||
_barrier_effect.text = "%d damage" % _upgrades.get_barrier_damage()
|
||||
_barrier_cost.text = "max"
|
||||
|
|
@ -488,11 +504,18 @@ func _refresh_supplies() -> void:
|
|||
button.disabled = (
|
||||
_transaction_in_progress
|
||||
or _closing
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_purchase()
|
||||
or not _bag.can_add_item(item_id, 1)
|
||||
or not _wallet.can_afford(
|
||||
FishingShopStockType.get_price(item_id)
|
||||
)
|
||||
)
|
||||
button.tooltip_text = (
|
||||
"Purchases are unavailable in this session."
|
||||
if _network_shop == null or not _network_shop.can_request_purchase()
|
||||
else item.description
|
||||
)
|
||||
button.pressed.connect(_purchase_supply.bind(item_id))
|
||||
_supplies_list.add_child(button)
|
||||
|
||||
|
|
@ -508,8 +531,11 @@ func _refresh_cooler_capacity() -> void:
|
|||
cost < 0
|
||||
or _transaction_in_progress
|
||||
or _closing
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_purchase()
|
||||
or not _cooler_capacity.can_purchase(_wallet)
|
||||
)
|
||||
_cooler_purchase.tooltip_text = _reel_purchase.tooltip_text
|
||||
if cost < 0:
|
||||
_cooler_effect.text = "%d fish" % _cooler_capacity.get_capacity()
|
||||
_cooler_cost.text = "max"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const ChatUIType = preload("res://ui/chat_ui.gd")
|
|||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||
signal crisp_reset_focus_requested
|
||||
signal interactive_pointer_ui_changed(is_open: bool)
|
||||
signal player_menu_backdrop_visibility_changed(is_visible: bool)
|
||||
|
||||
@onready var _status_label: Label = %StatusLabel
|
||||
@onready var _catch_track: Control = %CatchTrack
|
||||
|
|
@ -490,6 +491,7 @@ func _on_showcase_changed(
|
|||
|
||||
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||
_player_menu_open = is_open
|
||||
player_menu_backdrop_visibility_changed.emit(is_open)
|
||||
_refresh_chat_availability()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled and not is_open
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ var _sale_service: FishSaleServiceType
|
|||
var _network_session: NetworkSessionType
|
||||
var _network_sale_service: NetworkSaleService
|
||||
var _network_mail_service: NetworkMailService
|
||||
var _reservations: PlayerAssetReservationService
|
||||
var _network_profile_service: NetworkProfileService
|
||||
var _network_player_list: NetworkPlayerListService
|
||||
var _default_buyer: FishBuyerProfileType
|
||||
|
|
@ -389,6 +390,7 @@ func setup(
|
|||
_network_session = network_session
|
||||
_network_sale_service = network_sale_service
|
||||
_network_mail_service = network_mail_service
|
||||
_reservations = reservations
|
||||
_network_profile_service = network_profile_service
|
||||
_network_player_list = network_player_list
|
||||
_mail_page.setup(
|
||||
|
|
@ -2436,6 +2438,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
|
|||
else -1
|
||||
)
|
||||
_cooler_weight_value.text = "%.2f" % fish_catch.weight_lb
|
||||
if (
|
||||
_reservations != null
|
||||
and _reservations.is_fish_reserved(fish_catch.catch_id)
|
||||
):
|
||||
_cooler_detail_name.text += " • reserved in mail"
|
||||
_cooler_weight_unit.text = "lb"
|
||||
if buyer_offer >= 0 and _default_buyer != null:
|
||||
_cooler_offer_label.text = "%s offer" % _default_buyer.display_name
|
||||
|
|
@ -2734,6 +2741,16 @@ func _on_network_sale_finished(
|
|||
)
|
||||
_refresh_all()
|
||||
if _current_section == Section.COOLER:
|
||||
call_deferred("_restore_inventory_tab_focus")
|
||||
|
||||
|
||||
func _restore_inventory_tab_focus() -> void:
|
||||
if (
|
||||
visible
|
||||
and _current_section == Section.COOLER
|
||||
and _inventory_tab.focus_mode != Control.FOCUS_NONE
|
||||
and _inventory_tab.is_visible_in_tree()
|
||||
):
|
||||
_inventory_tab.grab_focus()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,17 +44,6 @@ mouse_filter = 2
|
|||
theme = ExtResource("2_theme")
|
||||
script = ExtResource("1_menu")
|
||||
|
||||
[node name="Dimmer" type="ColorRect" parent="."]
|
||||
z_index = -20
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
color = Color(0.015, 0.02, 0.03, 0.72)
|
||||
|
||||
[node name="ResponsivePlayerMenuStage" type="Control" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name WaterRecoveryController
|
|||
extends Node
|
||||
|
||||
signal recovery_starting
|
||||
signal recovery_finished
|
||||
signal local_respawn_completed(entry_position: Vector3)
|
||||
|
||||
enum RecoveryState {
|
||||
|
|
@ -177,3 +178,4 @@ func _finish_recovery() -> void:
|
|||
_fishing_spot.end_water_recovery()
|
||||
state = RecoveryState.IDLE
|
||||
_bob_elapsed = 0.0
|
||||
recovery_finished.emit()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue