feat: expand progression and multiplayer systems

Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
This commit is contained in:
Alexander Sellite 2026-08-23 20:48:38 -04:00
parent 1db1a5b754
commit 3b84bfe3a0
97 changed files with 7869 additions and 982 deletions

View file

@ -46,7 +46,7 @@ class Barrier:
# Fight movement uses one shared baseline. Species and quality difficulty comes
# from barrier placement and health, while reel upgrades affect only the
# player's progress speed.
const CHASE_START_DELAY: float = 1.0
const CHASE_START_DELAY: float = 1.5
const CHASE_START_OFFSET: float = 0.04
const CHASE_SPEED: float = 0.07

View file

@ -68,6 +68,7 @@ signal showcase_changed(
)
signal bite_activated
signal bite_prompt_changed(is_visible: bool)
signal fishing_input_priority_changed(active: bool)
signal ready_for_equipment_refresh
enum FishingState {
@ -182,6 +183,7 @@ var _network_active_barrier_index: int = -1
var _bite_rng: RandomNumberGenerator = RandomNumberGenerator.new()
var _pending_cleanup_message: String = ""
var _bite_confirmation_pending: bool = false
var _fishing_input_priority_active: bool = false
var _bite_confirmation_requested: bool = false
@ -458,6 +460,7 @@ func _secure_showcase_catch_for_recovery() -> void:
func _exit_tree() -> void:
_stop_fight_audio()
_stop_reeling_audio()
_set_fishing_input_priority(false)
_showcase_restore_generation += 1
if (
state == FishingState.SHOWING_CATCH
@ -777,6 +780,8 @@ func confirm_pending_bite() -> void:
func _set_bite_confirmation_pending(is_pending: bool) -> void:
if is_pending:
_set_fishing_input_priority(true)
if _bite_confirmation_pending == is_pending:
if not is_pending:
_bite_confirmation_requested = false
@ -786,6 +791,17 @@ func _set_bite_confirmation_pending(is_pending: bool) -> void:
bite_prompt_changed.emit(is_pending)
func _set_fishing_input_priority(active: bool) -> void:
if _fishing_input_priority_active == active:
return
_fishing_input_priority_active = active
fishing_input_priority_changed.emit(active)
func is_fishing_input_priority_active() -> bool:
return _fishing_input_priority_active
func is_returning() -> bool:
return state == FishingState.RETURNING
@ -1126,6 +1142,7 @@ func _activate_bite(confirmation_override: bool = false) -> void:
):
_cancel_attempt()
return
_set_fishing_input_priority(true)
if (
not confirmation_override
and _active_lure_has_effect(&"deferred_fight")
@ -1346,7 +1363,7 @@ func _store_catch_progression(fish_catch: FishCatchType) -> void:
)
)
_local_inventory.add_catch(fish_catch)
_local_collection_log.mark_quality_discovered(
_local_collection_log.record_catch(
fish_catch.fish_id,
fish_catch.quality,
)
@ -1414,6 +1431,7 @@ func _finalize_attempt_cleanup(
cooldown_message: String,
restore_movement: bool = true,
) -> void:
_set_bite_confirmation_pending(false)
_showcase_restore_generation += 1
if _active_player != null:
_active_player.set_fighting_visual(false)
@ -1458,6 +1476,7 @@ func _finalize_attempt_cleanup(
_state_time_remaining = cooldown_duration
_cooldown_status = cooldown_message
status_changed.emit(_cooldown_status)
_set_fishing_input_priority(false)
func _return_to_ready() -> void:
@ -1466,6 +1485,7 @@ func _return_to_ready() -> void:
_cooldown_status = ""
status_changed.emit("")
ready_for_equipment_refresh.emit()
_set_fishing_input_priority(false)
func _cancel_attempt() -> void:
@ -1775,6 +1795,7 @@ func _on_network_bite_started(_attempt_id: String) -> void:
if state != FishingState.WAITING_FOR_BITE:
return
_stop_reeling_audio()
_set_fishing_input_priority(true)
_set_bite_confirmation_pending(false)
state = FishingState.FIGHTING
if _active_player != null:

View file

@ -18,6 +18,8 @@ var _showcase_tween: Tween
var _return_showcase_catch: FishCatch
var _bobber_idle_elapsed: float = 0.0
var _bobber_base_scale: Vector3 = Vector3.ONE
var _attempt_id: String = ""
var _observer_fighting_active: bool = false
func setup(owning_player: Player) -> void:
@ -43,7 +45,11 @@ func setup(owning_player: Player) -> void:
cleanup()
func show_cast(origin: Vector3, target: Vector3) -> void:
func show_cast(
origin: Vector3,
target: Vector3,
attempt_id: String = "",
) -> void:
if (
_owner == null
or not origin.is_finite()
@ -52,6 +58,8 @@ func show_cast(origin: Vector3, target: Vector3) -> void:
return
_kill_cast_tween()
_active = true
_attempt_id = attempt_id
_observer_fighting_active = false
_bobber_base_scale = Vector3.ONE * _owner.get_character_visual_scale()
_target = origin
_pending_target = target
@ -86,10 +94,48 @@ func update_bobber(world_position: Vector3) -> void:
_redraw_line()
func synchronize_active(
attempt_id: String,
world_position: Vector3,
fighting: bool,
) -> void:
if (
_owner == null
or attempt_id.is_empty()
or not world_position.is_finite()
):
return
if _active and not _attempt_id.is_empty() and _attempt_id != attempt_id:
cleanup()
if _attempt_id.is_empty():
_attempt_id = attempt_id
if not _active:
_active = true
_bobber_base_scale = Vector3.ONE * _owner.get_character_visual_scale()
_target = world_position
_pending_target = world_position
_bobber_idle_elapsed = 0.0
_bobber.global_position = world_position
_bobber.scale = _bobber_base_scale
_bobber.visible = true
_line.visible = true
_owner.set_active_item_is_rod(true)
_owner.set_fishing_visual(true)
_redraw_line()
else:
update_bobber(world_position)
if fighting != _observer_fighting_active:
_observer_fighting_active = fighting
_owner.set_fighting_visual(fighting)
if not fighting:
_owner.set_fishing_visual(true)
func show_bite() -> void:
if not _active:
return
if _owner != null and is_instance_valid(_owner):
_observer_fighting_active = true
_owner.set_fighting_visual(true)
var tween: Tween = create_tween()
tween.tween_property(_bobber, "scale", _bobber_base_scale * 0.7, 0.08)
@ -131,6 +177,8 @@ func cleanup() -> void:
_kill_return_tween()
_kill_showcase_tween()
_active = false
_attempt_id = ""
_observer_fighting_active = false
_pending_target = Vector3.ZERO
_return_showcase_catch = null
_bobber_idle_elapsed = 0.0