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:
parent
1db1a5b754
commit
3b84bfe3a0
97 changed files with 7869 additions and 982 deletions
|
|
@ -79,8 +79,6 @@ func _ready() -> void:
|
|||
_validate_biome_catalog()
|
||||
_configure_static_water()
|
||||
_build_shoreline_reference()
|
||||
if not generate_world(initial_seed):
|
||||
push_error("The initial generated world could not be built.")
|
||||
|
||||
|
||||
func generate_world(seed: int) -> bool:
|
||||
|
|
@ -100,6 +98,10 @@ func get_generation_seed() -> int:
|
|||
return _current_seed
|
||||
|
||||
|
||||
func is_world_generated() -> bool:
|
||||
return is_instance_valid(_generator.get_generated_chunks_root())
|
||||
|
||||
|
||||
func get_playable_half_extents() -> Vector2:
|
||||
var size := Vector2(
|
||||
float(_generator.grid_size.x),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const MAX_PACKED_SOLVER_VARIANTS := 62
|
|||
@export var generation_seed := 13001
|
||||
@export var generate_on_ready := true
|
||||
@export var build_collision := true
|
||||
@export_range(1, 16, 1) var collision_batch_size := 4
|
||||
@export var show_chunk_labels := false
|
||||
@export var force_center_chunk_id: StringName = &"chunk_0000"
|
||||
@export var required_chunk_ids := PackedStringArray()
|
||||
|
|
@ -3928,9 +3929,102 @@ func _build_solution_root() -> Node3D:
|
|||
if not _add_stacked_elevated_chunks(solution_root):
|
||||
solution_root.free()
|
||||
return null
|
||||
if build_collision and collision_batch_size > 1:
|
||||
_batch_generated_terrain_collision(solution_root)
|
||||
return solution_root
|
||||
|
||||
|
||||
func _batch_generated_terrain_collision(solution_root: Node3D) -> void:
|
||||
var shapes_by_batch: Dictionary[Vector2i, Array] = {}
|
||||
var host_by_batch: Dictionary[Vector2i, Node3D] = {}
|
||||
var bodies_to_remove: Array[StaticBody3D] = []
|
||||
for chunk_node: Node in solution_root.get_children():
|
||||
var chunk_root := chunk_node as Node3D
|
||||
if chunk_root == null:
|
||||
continue
|
||||
var coordinate: Vector2i = chunk_root.get_meta(
|
||||
&"terrain_chunk_coordinate",
|
||||
Vector2i.ZERO,
|
||||
)
|
||||
var batch_coordinate := Vector2i(
|
||||
floori(float(coordinate.x) / float(collision_batch_size)),
|
||||
floori(float(coordinate.y) / float(collision_batch_size)),
|
||||
)
|
||||
if not host_by_batch.has(batch_coordinate):
|
||||
host_by_batch[batch_coordinate] = chunk_root
|
||||
var batch_shapes: Array = shapes_by_batch.get(
|
||||
batch_coordinate,
|
||||
[],
|
||||
)
|
||||
for value: Node in chunk_root.find_children(
|
||||
"TerrainShape",
|
||||
"CollisionShape3D",
|
||||
true,
|
||||
false,
|
||||
):
|
||||
var collision_shape := value as CollisionShape3D
|
||||
if collision_shape == null:
|
||||
continue
|
||||
var collision_body := collision_shape.get_parent() as StaticBody3D
|
||||
if collision_shape.shape == null or collision_body == null:
|
||||
continue
|
||||
var shape_to_solution := _transform_to_ancestor(
|
||||
collision_shape,
|
||||
solution_root,
|
||||
)
|
||||
batch_shapes.append({
|
||||
"shape": collision_shape.shape,
|
||||
"transform": shape_to_solution,
|
||||
})
|
||||
if collision_body not in bodies_to_remove:
|
||||
bodies_to_remove.append(collision_body)
|
||||
shapes_by_batch[batch_coordinate] = batch_shapes
|
||||
while not bodies_to_remove.is_empty():
|
||||
var collision_body: StaticBody3D = bodies_to_remove.pop_back()
|
||||
collision_body.free()
|
||||
for batch_coordinate: Vector2i in shapes_by_batch:
|
||||
var batch_shapes: Array = shapes_by_batch[batch_coordinate]
|
||||
if batch_shapes.is_empty():
|
||||
continue
|
||||
var host_root: Node3D = host_by_batch.get(batch_coordinate)
|
||||
if host_root == null:
|
||||
continue
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "TerrainCollisionBatch_%d_%d" % [
|
||||
batch_coordinate.x,
|
||||
batch_coordinate.y,
|
||||
]
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 0
|
||||
body.set_meta(&"terrain_collision_batch", batch_coordinate)
|
||||
host_root.add_child(body)
|
||||
var solution_to_body: Transform3D = host_root.transform.affine_inverse()
|
||||
for shape_index: int in batch_shapes.size():
|
||||
var record: Dictionary = batch_shapes[shape_index]
|
||||
var collision := CollisionShape3D.new()
|
||||
collision.name = "TerrainShape_%d" % shape_index
|
||||
collision.shape = record.get("shape") as Shape3D
|
||||
collision.transform = (
|
||||
solution_to_body
|
||||
* (record.get("transform", Transform3D.IDENTITY) as Transform3D)
|
||||
)
|
||||
body.add_child(collision)
|
||||
|
||||
|
||||
static func _transform_to_ancestor(
|
||||
node: Node3D,
|
||||
ancestor: Node3D,
|
||||
) -> Transform3D:
|
||||
var result := Transform3D.IDENTITY
|
||||
var current: Node3D = node
|
||||
while current != ancestor:
|
||||
result = current.transform * result
|
||||
current = current.get_parent() as Node3D
|
||||
if current == null:
|
||||
return Transform3D.IDENTITY
|
||||
return result
|
||||
|
||||
|
||||
func _add_stacked_elevated_chunks(solution_root: Node3D) -> bool:
|
||||
for index: int in _stacked_elevated_placements.size():
|
||||
var record := _stacked_elevated_placements[index]
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ func _ready() -> void:
|
|||
return
|
||||
body_entered.connect(_on_body_entered)
|
||||
body_exited.connect(_on_body_exited)
|
||||
# Generated worlds can contain many independent water volumes. Keep an
|
||||
# empty trigger completely asleep instead of polling an empty array on every
|
||||
# physics tick.
|
||||
set_physics_process(false)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
|
|
@ -57,6 +61,8 @@ func _physics_process(_delta: float) -> void:
|
|||
if entry_height <= active_surface_height - entry_depth_threshold:
|
||||
_triggered_players[player_key] = true
|
||||
recovery_requested.emit(player, active_surface_height)
|
||||
if _tracked_players.is_empty():
|
||||
set_physics_process(false)
|
||||
|
||||
|
||||
func get_surface_height() -> float:
|
||||
|
|
@ -80,6 +86,7 @@ func _on_body_entered(body: Node3D) -> void:
|
|||
if player == null or player in _tracked_players:
|
||||
return
|
||||
_tracked_players.append(player)
|
||||
set_physics_process(true)
|
||||
|
||||
|
||||
func _on_body_exited(body: Node3D) -> void:
|
||||
|
|
@ -88,3 +95,5 @@ func _on_body_exited(body: Node3D) -> void:
|
|||
return
|
||||
_tracked_players.erase(player)
|
||||
_triggered_players.erase(StringName(str(player.get_instance_id())))
|
||||
if _tracked_players.is_empty():
|
||||
set_physics_process(false)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const WORLD_BOUNDARY_SHORELINE_CLEARANCE := 18.0
|
|||
var _world_layout: StringName = WorldLayoutType.GENERATED
|
||||
var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
var _light_performance_profile: bool = false
|
||||
var _dedicated_simulation: bool = false
|
||||
|
||||
|
||||
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||
|
|
@ -74,6 +75,62 @@ func set_light_performance_profile(enabled: bool) -> void:
|
|||
_active_region.set_light_performance_profile(enabled)
|
||||
|
||||
|
||||
func set_dedicated_simulation(enabled: bool) -> void:
|
||||
_dedicated_simulation = enabled
|
||||
if _active_region != null:
|
||||
_set_region_presentation_enabled(_active_region, not enabled)
|
||||
_world_environment.process_mode = (
|
||||
Node.PROCESS_MODE_DISABLED if enabled else Node.PROCESS_MODE_INHERIT
|
||||
)
|
||||
_sun.visible = not enabled
|
||||
_sun.process_mode = (
|
||||
Node.PROCESS_MODE_DISABLED if enabled else Node.PROCESS_MODE_INHERIT
|
||||
)
|
||||
|
||||
|
||||
func _set_region_presentation_enabled(
|
||||
region: WorldRegion,
|
||||
enabled: bool,
|
||||
) -> void:
|
||||
for class_name_value: String in [
|
||||
"WaterSurfaceMotion",
|
||||
"LocalStormCloudLayer",
|
||||
"WorldCharacterDisplay",
|
||||
]:
|
||||
for value: Node in region.find_children(
|
||||
"*", class_name_value, true, false
|
||||
):
|
||||
value.process_mode = (
|
||||
Node.PROCESS_MODE_INHERIT
|
||||
if enabled
|
||||
else Node.PROCESS_MODE_DISABLED
|
||||
)
|
||||
for value: Node in region.find_children("*", "GeometryInstance3D", true, false):
|
||||
(value as GeometryInstance3D).visible = enabled
|
||||
for value: Node in region.find_children("*", "GPUParticles3D", true, false):
|
||||
var particles := value as GPUParticles3D
|
||||
particles.emitting = enabled
|
||||
particles.process_mode = (
|
||||
Node.PROCESS_MODE_INHERIT if enabled else Node.PROCESS_MODE_DISABLED
|
||||
)
|
||||
for value: Node in region.find_children("*", "AnimationPlayer", true, false):
|
||||
var animation_player := value as AnimationPlayer
|
||||
animation_player.active = enabled
|
||||
animation_player.process_mode = (
|
||||
Node.PROCESS_MODE_INHERIT if enabled else Node.PROCESS_MODE_DISABLED
|
||||
)
|
||||
for class_name_value: String in ["AudioStreamPlayer", "AudioStreamPlayer3D"]:
|
||||
for value: Node in region.find_children(
|
||||
"*", class_name_value, true, false
|
||||
):
|
||||
value.call("stop")
|
||||
value.process_mode = (
|
||||
Node.PROCESS_MODE_INHERIT
|
||||
if enabled
|
||||
else Node.PROCESS_MODE_DISABLED
|
||||
)
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
return (
|
||||
_active_region.get_fishable_water_regions()
|
||||
|
|
@ -129,6 +186,17 @@ func get_generation_seed() -> int:
|
|||
return _world_seed
|
||||
|
||||
|
||||
func is_world_ready() -> bool:
|
||||
if _active_region == null:
|
||||
return false
|
||||
if _world_layout != WorldLayoutType.GENERATED:
|
||||
return true
|
||||
return (
|
||||
_active_region.has_method(&"is_world_generated")
|
||||
and bool(_active_region.call(&"is_world_generated"))
|
||||
)
|
||||
|
||||
|
||||
func get_diggable_area_triangles(
|
||||
area_id: StringName,
|
||||
) -> Array[PackedVector3Array]:
|
||||
|
|
@ -182,6 +250,8 @@ func _replace_active_region(layout: StringName, seed: int) -> bool:
|
|||
_active_region = replacement
|
||||
_regions_root.add_child(_active_region)
|
||||
_active_region.set_light_performance_profile(_light_performance_profile)
|
||||
if _dedicated_simulation:
|
||||
_set_region_presentation_enabled(_active_region, false)
|
||||
return true
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue