fix: harden recovery flow and clean diagnostics
This commit is contained in:
parent
0158b0215f
commit
ff78710303
64 changed files with 1163 additions and 418 deletions
|
|
@ -18,4 +18,4 @@ static func get_unit_value(item: ItemDataType) -> int:
|
|||
if not is_sellable(item):
|
||||
return -1
|
||||
var purchase_price := FishingShopStockType.get_price(item.item_id)
|
||||
return purchase_price / 2 if purchase_price >= 0 else -1
|
||||
return int(float(purchase_price) / 2.0) if purchase_price >= 0 else -1
|
||||
|
|
|
|||
|
|
@ -262,10 +262,10 @@ func _cast_marker_ray(from: Vector3, to: Vector3) -> Dictionary:
|
|||
func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool:
|
||||
if hit.is_empty():
|
||||
return false
|
||||
var position: Variant = hit.get("position")
|
||||
var surface_position: Variant = hit.get("position")
|
||||
var normal_value: Variant = hit.get("normal")
|
||||
if (
|
||||
typeof(position) != TYPE_VECTOR3
|
||||
typeof(surface_position) != TYPE_VECTOR3
|
||||
or typeof(normal_value) != TYPE_VECTOR3
|
||||
):
|
||||
return false
|
||||
|
|
@ -274,7 +274,7 @@ func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool:
|
|||
return false
|
||||
surface_normal = surface_normal.normalized()
|
||||
_marker_position = (
|
||||
(position as Vector3) + surface_normal * marker_surface_offset
|
||||
(surface_position as Vector3) + surface_normal * marker_surface_offset
|
||||
)
|
||||
_marker.global_position = _marker_position
|
||||
_marker.global_basis = marker_basis_for_surface(surface_normal, facing)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ var _despawning: bool = false
|
|||
func configure(
|
||||
configured_entity_id: String,
|
||||
configured_data: GatherableDataType,
|
||||
position: Vector3,
|
||||
network_position: Vector3,
|
||||
yaw: float,
|
||||
) -> void:
|
||||
entity_id = configured_entity_id
|
||||
|
|
@ -31,7 +31,7 @@ func configure(
|
|||
type_id = data.type_id if data != null else StringName()
|
||||
# Establish the authoritative transform before constructing transient visuals.
|
||||
# Effects emitted before this point would begin at the default world origin.
|
||||
apply_network_state(position, yaw, true)
|
||||
apply_network_state(network_position, yaw, true)
|
||||
if data != null and data.is_stationary_hotspot():
|
||||
_ensure_water_spurt_visual()
|
||||
_water_spurt_elapsed = (
|
||||
|
|
@ -57,16 +57,16 @@ func configure(
|
|||
|
||||
|
||||
func apply_network_state(
|
||||
position: Vector3,
|
||||
network_position: Vector3,
|
||||
yaw: float,
|
||||
immediate: bool = false,
|
||||
) -> void:
|
||||
if _despawning or not position.is_finite() or not is_finite(yaw):
|
||||
if _despawning or not network_position.is_finite() or not is_finite(yaw):
|
||||
return
|
||||
_target_position = position
|
||||
_target_position = network_position
|
||||
_target_yaw = yaw
|
||||
if immediate or not _has_state:
|
||||
global_position = position
|
||||
global_position = network_position
|
||||
rotation.y = yaw
|
||||
_has_state = true
|
||||
|
||||
|
|
|
|||
|
|
@ -253,19 +253,19 @@ func _find_exterior_transform(
|
|||
]
|
||||
if not _is_exterior_candidate_triangle(triangle):
|
||||
continue
|
||||
var position: Vector3 = (
|
||||
var candidate_position: Vector3 = (
|
||||
triangle[0] + triangle[1] + triangle[2]
|
||||
) / 3.0
|
||||
if (
|
||||
require_spawn_clearance
|
||||
and position.distance_to(spawn_transform.origin) < 9.0
|
||||
and candidate_position.distance_to(spawn_transform.origin) < 9.0
|
||||
):
|
||||
continue
|
||||
if _too_close_to_reserved(position, reserved_transforms):
|
||||
if _too_close_to_reserved(candidate_position, reserved_transforms):
|
||||
continue
|
||||
var yaw: float = float(posmod(owner_peer_id + offset, 4)) * PI * 0.5
|
||||
var grounded_position: Vector3 = _ground_exterior_footprint(
|
||||
position, yaw, triangles, surface_index
|
||||
candidate_position, yaw, triangles, surface_index
|
||||
)
|
||||
if not grounded_position.is_finite():
|
||||
continue
|
||||
|
|
@ -299,7 +299,7 @@ func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool:
|
|||
|
||||
|
||||
func _ground_exterior_footprint(
|
||||
position: Vector3,
|
||||
candidate_position: Vector3,
|
||||
yaw: float,
|
||||
triangles: Array[PackedVector3Array],
|
||||
surface_index: Dictionary,
|
||||
|
|
@ -310,11 +310,11 @@ func _ground_exterior_footprint(
|
|||
local_sample.x, 0.0, local_sample.y
|
||||
).rotated(Vector3.UP, yaw)
|
||||
var sample := Vector2(
|
||||
position.x + rotated_sample.x,
|
||||
position.z + rotated_sample.z,
|
||||
candidate_position.x + rotated_sample.x,
|
||||
candidate_position.z + rotated_sample.z,
|
||||
)
|
||||
var height: float = _surface_height_at(
|
||||
sample, position.y, triangles, surface_index
|
||||
sample, candidate_position.y, triangles, surface_index
|
||||
)
|
||||
if not is_finite(height):
|
||||
return Vector3.INF
|
||||
|
|
@ -323,7 +323,9 @@ func _ground_exterior_footprint(
|
|||
var maximum_height: float = heights.max()
|
||||
if maximum_height - minimum_height > EXTERIOR_MAX_SURFACE_HEIGHT_DELTA:
|
||||
return Vector3.INF
|
||||
return Vector3(position.x, maximum_height, position.z)
|
||||
return Vector3(
|
||||
candidate_position.x, maximum_height, candidate_position.z
|
||||
)
|
||||
|
||||
|
||||
func _exterior_footprint_samples() -> Array[Vector2]:
|
||||
|
|
@ -434,7 +436,9 @@ func _triangle_height_at(
|
|||
)
|
||||
|
||||
|
||||
func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
|
||||
func _exterior_volume_is_clear(
|
||||
candidate_position: Vector3, yaw: float
|
||||
) -> bool:
|
||||
if not is_inside_tree() or get_world_3d() == null:
|
||||
return true
|
||||
var shape := BoxShape3D.new()
|
||||
|
|
@ -447,7 +451,8 @@ func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
|
|||
query.shape = shape
|
||||
query.transform = Transform3D(
|
||||
Basis(Vector3.UP, yaw),
|
||||
position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
|
||||
candidate_position
|
||||
+ Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
|
||||
)
|
||||
query.collision_mask = 1
|
||||
query.collide_with_areas = false
|
||||
|
|
@ -563,9 +568,11 @@ func get_interior_anchor(owner_fingerprint: String) -> Vector3:
|
|||
func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool:
|
||||
if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint):
|
||||
return false
|
||||
var transform: Transform3D = get_exterior_transform(owner_fingerprint)
|
||||
var exterior_transform: Transform3D = get_exterior_transform(
|
||||
owner_fingerprint
|
||||
)
|
||||
var door_position: Vector3 = (
|
||||
transform * _exterior_door_local(owner_fingerprint)
|
||||
exterior_transform * _exterior_door_local(owner_fingerprint)
|
||||
)
|
||||
return avatar.global_position.distance_to(door_position) <= (
|
||||
EXTERIOR_INTERACTION_DISTANCE
|
||||
|
|
@ -639,9 +646,9 @@ func interior_local_to_global(
|
|||
|
||||
func interior_global_to_local(
|
||||
owner_fingerprint: String,
|
||||
global_position: Vector3,
|
||||
world_position: Vector3,
|
||||
) -> Vector3:
|
||||
return global_position - get_interior_anchor(owner_fingerprint)
|
||||
return world_position - get_interior_anchor(owner_fingerprint)
|
||||
|
||||
|
||||
func get_floor_position_from_screen(
|
||||
|
|
@ -709,15 +716,19 @@ func get_decor_preview_transform(
|
|||
func set_decor_preview_transform(
|
||||
owner_fingerprint: String,
|
||||
instance_id: String,
|
||||
position: Vector3,
|
||||
decor_position: Vector3,
|
||||
yaw_radians: float,
|
||||
) -> bool:
|
||||
var decor: Node3D = _get_decor_presentation(
|
||||
owner_fingerprint, instance_id
|
||||
)
|
||||
if decor == null or not position.is_finite() or not is_finite(yaw_radians):
|
||||
if (
|
||||
decor == null
|
||||
or not decor_position.is_finite()
|
||||
or not is_finite(yaw_radians)
|
||||
):
|
||||
return false
|
||||
decor.position = Vector3(position.x, 0.18, position.z)
|
||||
decor.position = Vector3(decor_position.x, 0.18, decor_position.z)
|
||||
decor.rotation.y = yaw_radians
|
||||
return true
|
||||
|
||||
|
|
@ -817,9 +828,11 @@ func find_nearby_exterior_owner(avatar: Player) -> String:
|
|||
var closest: String = ""
|
||||
var closest_distance: float = EXTERIOR_INTERACTION_DISTANCE
|
||||
for fingerprint: String in _presentations_by_fingerprint:
|
||||
var transform: Transform3D = get_exterior_transform(fingerprint)
|
||||
var exterior_transform: Transform3D = get_exterior_transform(
|
||||
fingerprint
|
||||
)
|
||||
var distance: float = avatar.global_position.distance_to(
|
||||
transform * _exterior_door_local(fingerprint)
|
||||
exterior_transform * _exterior_door_local(fingerprint)
|
||||
)
|
||||
if distance <= closest_distance:
|
||||
closest = fingerprint
|
||||
|
|
@ -1261,16 +1274,16 @@ func _add_wall(
|
|||
parent: Node3D,
|
||||
node_name: String,
|
||||
size: Vector3,
|
||||
position: Vector3,
|
||||
local_position: Vector3,
|
||||
) -> void:
|
||||
_add_box(parent, node_name, size, position, Color("d6caaa"), true)
|
||||
_add_box(parent, node_name, size, local_position, Color("d6caaa"), true)
|
||||
|
||||
|
||||
func _add_box(
|
||||
parent: Node3D,
|
||||
node_name: String,
|
||||
size: Vector3,
|
||||
position: Vector3,
|
||||
local_position: Vector3,
|
||||
color: Color,
|
||||
with_collision: bool,
|
||||
invert_faces: bool = false,
|
||||
|
|
@ -1283,10 +1296,10 @@ func _add_box(
|
|||
if invert_faces:
|
||||
mesh.flip_faces = true
|
||||
mesh_instance.mesh = mesh
|
||||
mesh_instance.position = position
|
||||
mesh_instance.position = local_position
|
||||
parent.add_child(mesh_instance)
|
||||
if with_collision:
|
||||
return _add_box_collision(parent, node_name, size, position)
|
||||
return _add_box_collision(parent, node_name, size, local_position)
|
||||
return null
|
||||
|
||||
|
||||
|
|
@ -1294,11 +1307,11 @@ func _add_box_collision(
|
|||
parent: Node3D,
|
||||
node_name: String,
|
||||
size: Vector3,
|
||||
position: Vector3,
|
||||
local_position: Vector3,
|
||||
) -> StaticBody3D:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "%sCollision" % node_name
|
||||
body.position = position
|
||||
body.position = local_position
|
||||
var collision := CollisionShape3D.new()
|
||||
var shape := BoxShape3D.new()
|
||||
shape.size = size
|
||||
|
|
@ -1490,15 +1503,18 @@ func _refresh_nearest_wall() -> void:
|
|||
|
||||
|
||||
func _too_close_to_reserved(
|
||||
position: Vector3,
|
||||
candidate_position: Vector3,
|
||||
reserved_transforms: Array[Transform3D],
|
||||
) -> bool:
|
||||
for transform: Transform3D in reserved_transforms:
|
||||
if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION:
|
||||
for reserved_transform: Transform3D in reserved_transforms:
|
||||
if (
|
||||
candidate_position.distance_to(reserved_transform.origin)
|
||||
< MIN_EXTERIOR_SEPARATION
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _set_geometry_visible(root: Node, visible: bool) -> void:
|
||||
func _set_geometry_visible(root: Node, should_show: bool) -> void:
|
||||
for node: Node in root.find_children("*", "GeometryInstance3D", true, false):
|
||||
(node as GeometryInstance3D).visible = visible
|
||||
(node as GeometryInstance3D).visible = should_show
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
|
|||
return false
|
||||
_tier = int(sanitized["tier"])
|
||||
_exterior_id = exterior_id_for_tier(_tier)
|
||||
_privacy = int(sanitized["privacy"])
|
||||
_privacy = int(sanitized["privacy"]) as Privacy
|
||||
_owned_decor.clear()
|
||||
for key: Variant in (sanitized["owned_decor"] as Dictionary):
|
||||
_owned_decor[StringName(str(key))] = int(
|
||||
|
|
|
|||
32
main/main.gd
32
main/main.gd
|
|
@ -1541,6 +1541,12 @@ func _configure_popup_dialog(
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# The exit walk and its authoritative relocation own gameplay input until the
|
||||
# return fade completes. This also keeps a late cancel/pause press from
|
||||
# manipulating a menu while the world space is changing.
|
||||
if _home_exit_transition_active:
|
||||
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():
|
||||
|
|
@ -1671,6 +1677,9 @@ func _is_pause_open_request(event: InputEvent) -> bool:
|
|||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if _home_exit_transition_active:
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_gameplay_started
|
||||
and event.is_action_pressed("fish_primary")
|
||||
|
|
@ -1858,6 +1867,12 @@ func _reset_pixelation() -> void:
|
|||
func _set_gameplay_active(active: bool) -> void:
|
||||
var was_gameplay_started: bool = _gameplay_started
|
||||
_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:
|
||||
_cancel_home_entry_transition()
|
||||
_cancel_home_exit_transition(true)
|
||||
_shoreline_ambience.set_active(active)
|
||||
_rain_ambience.set_active(active)
|
||||
if active and not was_gameplay_started:
|
||||
|
|
@ -2499,6 +2514,7 @@ func _on_local_respawn_completed(entry_position: Vector3) -> void:
|
|||
func _on_remote_recovery_requested(
|
||||
peer_id: int,
|
||||
entry_position: Vector3,
|
||||
recovery_input_sequence: int,
|
||||
) -> void:
|
||||
if not _network_session.is_host():
|
||||
return
|
||||
|
|
@ -2509,6 +2525,9 @@ func _on_remote_recovery_requested(
|
|||
peer_id,
|
||||
"Fishing attempt ended."
|
||||
)
|
||||
avatar.neutralize_authoritative_input_for_relocation(
|
||||
recovery_input_sequence
|
||||
)
|
||||
var target_position := _test_world.get_water_recovery_position(
|
||||
entry_position
|
||||
)
|
||||
|
|
@ -3039,6 +3058,12 @@ func _begin_home_exit_transition(owner_fingerprint: String) -> void:
|
|||
_refresh_home_door_prompt({})
|
||||
_player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
|
||||
_player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
|
||||
# A same-frame interact can otherwise open home storage after the threshold
|
||||
# was crossed. The exit suppressor is already active, so restoring storage's
|
||||
# base snapshot cannot return effective control before the transition ends.
|
||||
_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
|
||||
)
|
||||
|
|
@ -3126,6 +3151,11 @@ func _cancel_home_exit_transition(reset_fade: bool) -> void:
|
|||
_home_exit_transition_active = false
|
||||
_home_exit_waiting_for_network = false
|
||||
_home_exit_start_position = Vector3.ZERO
|
||||
if is_instance_valid(_game_ui):
|
||||
_game_ui.set_gameplay_input_locked(false)
|
||||
_game_ui.set_rv_storage_button_visible(
|
||||
_can_show_rv_storage_button()
|
||||
)
|
||||
|
||||
|
||||
func _on_rv_storage_requested() -> void:
|
||||
|
|
@ -3247,6 +3277,7 @@ func _refresh_home_door_prompt(prompt: Dictionary) -> void:
|
|||
func _can_show_rv_storage_button() -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
and not _home_exit_transition_active
|
||||
and not _network_home.get_local_home_owner_fingerprint().is_empty()
|
||||
and _network_home.can_local_decorate()
|
||||
and not _water_recovery.is_recovery_active()
|
||||
|
|
@ -3257,6 +3288,7 @@ func _can_show_rv_storage_button() -> bool:
|
|||
func _get_home_prompt() -> Dictionary:
|
||||
if (
|
||||
not _gameplay_started
|
||||
or _home_exit_transition_active
|
||||
or _game_ui.get_player_storage().visible
|
||||
or _game_ui.get_fishing_shop().visible
|
||||
or _game_ui.get_decor_shop().visible
|
||||
|
|
|
|||
|
|
@ -1042,9 +1042,11 @@ func _valid_json_integer(value: Variant, minimum: int, maximum: int) -> bool:
|
|||
)
|
||||
|
||||
|
||||
func _make_social_request(name: String, callback: Callable) -> HTTPRequest:
|
||||
func _make_social_request(
|
||||
request_name: String, callback: Callable
|
||||
) -> HTTPRequest:
|
||||
var request := HTTPRequest.new()
|
||||
request.name = name
|
||||
request.name = request_name
|
||||
request.timeout = REQUEST_TIMEOUT_SECONDS
|
||||
add_child(request)
|
||||
request.request_completed.connect(callback)
|
||||
|
|
|
|||
|
|
@ -340,11 +340,12 @@ func _handle_request(peer_id: int, data: Dictionary) -> void:
|
|||
message["request_id"] = request_id
|
||||
message["sender_fingerprint"] = data["sender_fingerprint"]
|
||||
message["sender_signature"] = data["sender_signature"]
|
||||
ledger[request_id] = (
|
||||
message.duplicate(true)
|
||||
if _should_store_host_history()
|
||||
else true
|
||||
)
|
||||
# `true` is the intentional no-history replay sentinel. It records that the
|
||||
# request was handled without retaining a message payload for replay.
|
||||
var ledger_entry: Variant = true
|
||||
if _should_store_host_history():
|
||||
ledger_entry = message.duplicate(true)
|
||||
ledger[request_id] = ledger_entry
|
||||
while ledger.size() > 64:
|
||||
ledger.erase(ledger.keys().front())
|
||||
_request_ledgers[peer_id] = ledger
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ const ANIMATION_REFRESH_INTERVAL: float = 1.0
|
|||
const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
|
||||
const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
|
||||
const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0
|
||||
# A reliable teleport travels on a different channel from ordinary movement
|
||||
# audits. Keep the acceptance radius below Player's hard-reconciliation
|
||||
# distance so an old audit can never pull a just-relocated local player back
|
||||
# into the place they were recovered from.
|
||||
const AUTHORITATIVE_TELEPORT_SNAPSHOT_GUARD_DISTANCE: float = 2.0
|
||||
|
||||
const MOVEMENT_FLAG_JUMP: int = 1 << 0
|
||||
const MOVEMENT_FLAG_SPRINT: int = 1 << 1
|
||||
|
|
@ -77,7 +82,11 @@ signal server_trust_required(
|
|||
signal peer_identity_observed(peer_id: int, status: String)
|
||||
signal operator_status_changed(peer_id: int, is_operator: bool)
|
||||
signal server_lost(message: String)
|
||||
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
|
||||
signal remote_recovery_requested(
|
||||
peer_id: int,
|
||||
entry_position: Vector3,
|
||||
recovery_input_sequence: int,
|
||||
)
|
||||
signal remote_recovery_presentation_changed(
|
||||
peer_id: int,
|
||||
active: bool,
|
||||
|
|
@ -128,6 +137,9 @@ var _last_local_snapshot_received_msec: int = 0
|
|||
var _local_space_transition_guard_active: bool = false
|
||||
var _local_space_transition_guard_position: Vector3 = Vector3.ZERO
|
||||
var _local_space_transition_minimum_ack: int = 0
|
||||
var _local_authoritative_teleport_guard_active: bool = false
|
||||
var _local_authoritative_teleport_guard_position: Vector3 = Vector3.ZERO
|
||||
var _local_authoritative_teleport_minimum_ack: int = 0
|
||||
var _animation_refresh_accumulator: float = 0.0
|
||||
var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||
var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
|
||||
|
|
@ -601,20 +613,20 @@ func get_last_server_metadata() -> Dictionary:
|
|||
}
|
||||
|
||||
|
||||
func set_host_world_seed(seed: int) -> bool:
|
||||
return set_host_world(WorldLayout.GENERATED, seed)
|
||||
func set_host_world_seed(world_seed: int) -> bool:
|
||||
return set_host_world(WorldLayout.GENERATED, world_seed)
|
||||
|
||||
|
||||
func set_host_world(world_layout: StringName, seed: int) -> bool:
|
||||
func set_host_world(world_layout: StringName, world_seed: int) -> bool:
|
||||
if (
|
||||
state != State.INACTIVE
|
||||
or not WorldLayout.is_valid(world_layout)
|
||||
or seed <= 0
|
||||
or seed > NetworkProtocol.MAX_WORLD_SEED
|
||||
or world_seed <= 0
|
||||
or world_seed > NetworkProtocol.MAX_WORLD_SEED
|
||||
):
|
||||
return false
|
||||
_host_world_layout = world_layout
|
||||
_host_world_seed = seed
|
||||
_host_world_seed = world_seed
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -2088,10 +2100,10 @@ func _maybe_send_local_input() -> void:
|
|||
if avatar == null:
|
||||
return
|
||||
var state_hash: int = avatar.get_network_input_state_hash()
|
||||
var state_changed: bool = state_hash != _last_input_state_hash
|
||||
var input_state_changed: bool = state_hash != _last_input_state_hash
|
||||
if (
|
||||
avatar.has_active_network_input()
|
||||
or state_changed
|
||||
or input_state_changed
|
||||
or _idle_input_accumulator >= IDLE_INPUT_INTERVAL
|
||||
):
|
||||
_send_local_input()
|
||||
|
|
@ -2125,8 +2137,8 @@ func _maybe_send_local_animation_action() -> void:
|
|||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar == null:
|
||||
return
|
||||
var state: Dictionary = avatar.make_network_animation_state()
|
||||
var action: Dictionary = state.get("action", {})
|
||||
var animation_state: Dictionary = avatar.make_network_animation_state()
|
||||
var action: Dictionary = animation_state.get("action", {})
|
||||
if not NetworkPlayerAnimationProtocol.validate_action_state(action):
|
||||
return
|
||||
var signature: Array = [
|
||||
|
|
@ -2182,11 +2194,15 @@ func submit_movement_animation_action(encoded: Array) -> void:
|
|||
if not is_host() or not _registry.has_peer(sender_id):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(sender_id)
|
||||
var state: Dictionary = _decode_movement_animation_action(encoded)
|
||||
if avatar == null or state.is_empty():
|
||||
var animation_state: Dictionary = _decode_movement_animation_action(encoded)
|
||||
if avatar == null or animation_state.is_empty():
|
||||
return
|
||||
avatar.apply_authoritative_network_animation_action(state["action"])
|
||||
avatar.apply_authoritative_network_sitting_state(bool(state["sitting"]))
|
||||
avatar.apply_authoritative_network_animation_action(
|
||||
animation_state["action"]
|
||||
)
|
||||
avatar.apply_authoritative_network_sitting_state(
|
||||
bool(animation_state["sitting"])
|
||||
)
|
||||
|
||||
|
||||
static func _encode_movement_input(data: Dictionary) -> Array:
|
||||
|
|
@ -2386,18 +2402,20 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
|||
var avatar: Player = _spawn_service.get_avatar(subject_id)
|
||||
if avatar == null:
|
||||
continue
|
||||
var state: Dictionary = avatar.make_network_animation_state()
|
||||
var animation_state: Dictionary = avatar.make_network_animation_state()
|
||||
var previous: Dictionary = _last_animation_state_by_peer.get(
|
||||
subject_id, {}
|
||||
)
|
||||
if (
|
||||
not refresh_all
|
||||
and _movement_animation_signature(state)
|
||||
and _movement_animation_signature(animation_state)
|
||||
== _movement_animation_signature(previous)
|
||||
):
|
||||
continue
|
||||
_last_animation_state_by_peer[subject_id] = state.duplicate(true)
|
||||
var encoded: Array = _encode_movement_animation(subject_id, state)
|
||||
_last_animation_state_by_peer[subject_id] = animation_state.duplicate(true)
|
||||
var encoded: Array = _encode_movement_animation(
|
||||
subject_id, animation_state
|
||||
)
|
||||
if not encoded.is_empty():
|
||||
updates_by_subject[subject_id] = encoded
|
||||
if updates_by_subject.is_empty():
|
||||
|
|
@ -2416,13 +2434,13 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
|
|||
_movement_animation_states_sent += updates.size()
|
||||
|
||||
|
||||
static func _movement_animation_signature(state: Dictionary) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(state):
|
||||
static func _movement_animation_signature(animation_state: Dictionary) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(animation_state):
|
||||
return []
|
||||
var action: Dictionary = state["action"]
|
||||
var action: Dictionary = animation_state["action"]
|
||||
return [
|
||||
str(state["locomotion_id"]),
|
||||
bool(state["grounded"]),
|
||||
str(animation_state["locomotion_id"]),
|
||||
bool(animation_state["grounded"]),
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
bool(action.get("paused", false)),
|
||||
|
|
@ -2505,15 +2523,15 @@ static func _decode_movement_snapshot(value: Variant) -> Dictionary:
|
|||
|
||||
static func _encode_movement_animation(
|
||||
peer_id: int,
|
||||
state: Dictionary,
|
||||
animation_state: Dictionary,
|
||||
) -> Array:
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(state):
|
||||
if not NetworkPlayerAnimationProtocol.validate_state(animation_state):
|
||||
return []
|
||||
var action: Dictionary = state["action"]
|
||||
var action: Dictionary = animation_state["action"]
|
||||
return [
|
||||
peer_id,
|
||||
str(state["locomotion_id"]),
|
||||
bool(state["grounded"]),
|
||||
str(animation_state["locomotion_id"]),
|
||||
bool(animation_state["grounded"]),
|
||||
str(action["id"]),
|
||||
int(action["sequence"]),
|
||||
float(action["elapsed"]),
|
||||
|
|
@ -2536,7 +2554,7 @@ static func _decode_movement_animation(value: Variant) -> Dictionary:
|
|||
or typeof(fields[6]) != TYPE_BOOL
|
||||
):
|
||||
return {}
|
||||
var state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
|
||||
var animation_state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
|
||||
StringName(str(fields[1])),
|
||||
bool(fields[2]),
|
||||
StringName(str(fields[3])),
|
||||
|
|
@ -2546,10 +2564,10 @@ static func _decode_movement_animation(value: Variant) -> Dictionary:
|
|||
)
|
||||
if (
|
||||
int(fields[0]) <= 0
|
||||
or not NetworkPlayerAnimationProtocol.validate_state(state)
|
||||
or not NetworkPlayerAnimationProtocol.validate_state(animation_state)
|
||||
):
|
||||
return {}
|
||||
return {"peer_id": int(fields[0]), "state": state}
|
||||
return {"peer_id": int(fields[0]), "state": animation_state}
|
||||
|
||||
|
||||
static func _encode_movement_animation_action(
|
||||
|
|
@ -2610,7 +2628,10 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void:
|
|||
if avatar == null:
|
||||
continue
|
||||
if peer_id == local_peer_id:
|
||||
if _reject_stale_space_transition_snapshot(snapshot):
|
||||
if (
|
||||
_reject_stale_space_transition_snapshot(snapshot)
|
||||
or _reject_stale_authoritative_teleport_snapshot(snapshot)
|
||||
):
|
||||
continue
|
||||
_discard_acknowledged_movement_inputs(
|
||||
int(snapshot.get("acknowledged_input", 0))
|
||||
|
|
@ -2656,6 +2677,37 @@ func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool:
|
|||
return false
|
||||
|
||||
|
||||
func _reject_stale_authoritative_teleport_snapshot(
|
||||
snapshot: Dictionary,
|
||||
) -> bool:
|
||||
if not _local_authoritative_teleport_guard_active:
|
||||
return false
|
||||
var position_values: Array = snapshot.get("position", [])
|
||||
if position_values.size() != 3:
|
||||
return true
|
||||
var position := Vector3(
|
||||
float(position_values[0]),
|
||||
float(position_values[1]),
|
||||
float(position_values[2]),
|
||||
)
|
||||
var acknowledged_input: int = int(
|
||||
snapshot.get("acknowledged_input", 0)
|
||||
)
|
||||
if acknowledged_input < _local_authoritative_teleport_minimum_ack:
|
||||
return true
|
||||
# Movement input and reliable recovery RPCs travel on different channels.
|
||||
# A higher input acknowledgement alone therefore does not prove that this
|
||||
# snapshot was authored after the teleport; a held pre-recovery packet can
|
||||
# arrive at the host late. Only a snapshot that is still near the teleported
|
||||
# position is allowed to release the barrier.
|
||||
if position.distance_to(_local_authoritative_teleport_guard_position) > (
|
||||
AUTHORITATIVE_TELEPORT_SNAPSHOT_GUARD_DISTANCE
|
||||
):
|
||||
return true
|
||||
_local_authoritative_teleport_guard_active = false
|
||||
return false
|
||||
|
||||
|
||||
func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
|
||||
while (
|
||||
not _pending_movement_inputs.is_empty()
|
||||
|
|
@ -2823,11 +2875,16 @@ func request_safe_respawn(entry_position: Vector3) -> void:
|
|||
if not entry_position.is_finite():
|
||||
return
|
||||
if is_host():
|
||||
remote_recovery_requested.emit(1, entry_position)
|
||||
remote_recovery_requested.emit(1, entry_position, _input_sequence)
|
||||
elif state == State.JOINED_CLIENT:
|
||||
submit_safe_respawn_request.rpc_id(
|
||||
1,
|
||||
[entry_position.x, entry_position.y, entry_position.z]
|
||||
[
|
||||
entry_position.x,
|
||||
entry_position.y,
|
||||
entry_position.z,
|
||||
_input_sequence,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -2837,9 +2894,18 @@ func submit_safe_respawn_request(position_data: Array) -> void:
|
|||
if (
|
||||
not is_host()
|
||||
or not _registry.has_peer(sender_id)
|
||||
or position_data.size() != 3
|
||||
or position_data.size() not in [3, 4]
|
||||
):
|
||||
return
|
||||
var recovery_input_sequence: int = -1
|
||||
if position_data.size() == 4:
|
||||
if (
|
||||
typeof(position_data[3]) != TYPE_INT
|
||||
or int(position_data[3]) < 0
|
||||
or int(position_data[3]) > MAX_MOVEMENT_INPUT_SEQUENCE
|
||||
):
|
||||
return
|
||||
recovery_input_sequence = int(position_data[3])
|
||||
var entry_position := Vector3(
|
||||
float(position_data[0]),
|
||||
float(position_data[1]),
|
||||
|
|
@ -2847,7 +2913,11 @@ func submit_safe_respawn_request(position_data: Array) -> void:
|
|||
)
|
||||
if not entry_position.is_finite():
|
||||
return
|
||||
remote_recovery_requested.emit(sender_id, entry_position)
|
||||
remote_recovery_requested.emit(
|
||||
sender_id,
|
||||
entry_position,
|
||||
recovery_input_sequence,
|
||||
)
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
|
|
@ -2856,8 +2926,38 @@ func receive_authoritative_teleport(snapshot: Dictionary) -> void:
|
|||
return
|
||||
var peer_id: int = int(snapshot.get("peer_id", 0))
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
avatar.apply_network_teleport(snapshot)
|
||||
if avatar == null or not avatar.apply_network_teleport(snapshot):
|
||||
return
|
||||
if peer_id != multiplayer.get_unique_id():
|
||||
return
|
||||
_begin_local_authoritative_teleport_barrier(
|
||||
avatar,
|
||||
int(snapshot.get("acknowledged_input", 0)),
|
||||
)
|
||||
|
||||
|
||||
func _begin_local_authoritative_teleport_barrier(
|
||||
avatar: Player,
|
||||
acknowledged_input: int,
|
||||
) -> void:
|
||||
if avatar == null or not avatar.global_position.is_finite():
|
||||
return
|
||||
# A recovery teleport is a movement discontinuity, just like a home-space
|
||||
# transition. Drop pre-teleport client prediction and request an immediate
|
||||
# neutral input so the first post-teleport audit is unambiguous.
|
||||
_pending_movement_inputs.clear()
|
||||
_last_local_snapshot_received_msec = 0
|
||||
_last_input_state_hash = 0
|
||||
_idle_input_accumulator = IDLE_INPUT_INTERVAL
|
||||
avatar.reset_local_prediction_after_authoritative_teleport()
|
||||
_local_authoritative_teleport_guard_active = true
|
||||
_local_authoritative_teleport_guard_position = avatar.global_position
|
||||
_local_authoritative_teleport_minimum_ack = maxi(acknowledged_input, 0)
|
||||
# Water recovery has already suppressed local movement, so this sends a
|
||||
# neutral post-relocation input on the next server tick instead of waiting
|
||||
# for the regular input cadence.
|
||||
if avatar.is_water_recovery_active():
|
||||
submit_neutral_local_movement()
|
||||
|
||||
|
||||
func _expire_pending_authentication(now: float) -> void:
|
||||
|
|
@ -2987,6 +3087,9 @@ func _teardown_peer() -> void:
|
|||
_local_space_transition_guard_active = false
|
||||
_local_space_transition_guard_position = Vector3.ZERO
|
||||
_local_space_transition_minimum_ack = 0
|
||||
_local_authoritative_teleport_guard_active = false
|
||||
_local_authoritative_teleport_guard_position = Vector3.ZERO
|
||||
_local_authoritative_teleport_minimum_ack = 0
|
||||
_animation_refresh_accumulator = 0.0
|
||||
_last_animation_state_by_peer.clear()
|
||||
_pending_animation_state_by_peer.clear()
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ func get_impact_world_position() -> Vector3:
|
|||
func _sync_spring_activity() -> void:
|
||||
if _spring == null:
|
||||
return
|
||||
var is_visible := _catching_net.is_visible_in_tree()
|
||||
if is_visible and not _spring_was_visible:
|
||||
var net_is_visible: bool = _catching_net.is_visible_in_tree()
|
||||
if net_is_visible and not _spring_was_visible:
|
||||
_spring.active = true
|
||||
_spring.reset()
|
||||
elif not is_visible:
|
||||
elif not net_is_visible:
|
||||
_spring.active = false
|
||||
_spring_was_visible = is_visible
|
||||
_spring_was_visible = net_is_visible
|
||||
|
|
|
|||
113
player/player.gd
113
player/player.gd
|
|
@ -531,6 +531,7 @@ var _network_sprint: bool = false
|
|||
var _network_sneak: bool = false
|
||||
var _network_slow_walk: bool = false
|
||||
var _last_network_input_sequence: int = 0
|
||||
var _authoritative_relocation_input_quarantine: bool = false
|
||||
var _network_input_age: float = 0.0
|
||||
var _network_input_stale: bool = false
|
||||
var _network_input_stale_timeout_seconds: float = (
|
||||
|
|
@ -1383,7 +1384,7 @@ func _update_character_animation() -> void:
|
|||
var is_sneaking_pose: bool = (
|
||||
locomotion_state == LocomotionState.SNEAKING
|
||||
)
|
||||
var is_moving_horizontally: bool = (
|
||||
var is_presented_moving_horizontally: bool = (
|
||||
_is_presented_moving_horizontally()
|
||||
)
|
||||
var animation_action: Dictionary = _get_presented_animation_action()
|
||||
|
|
@ -1500,7 +1501,7 @@ func _update_character_animation() -> void:
|
|||
&"idle_loop_sit",
|
||||
]
|
||||
elif is_sneaking_pose:
|
||||
if is_moving_horizontally:
|
||||
if is_presented_moving_horizontally:
|
||||
requested_animation = [
|
||||
CHARACTER_SNEAKING_ANIMATION,
|
||||
CHARACTER_WALKING_ANIMATION,
|
||||
|
|
@ -1787,6 +1788,11 @@ func apply_network_sitting_state(should_sit: bool) -> void:
|
|||
|
||||
|
||||
func apply_authoritative_network_sitting_state(should_sit: bool) -> void:
|
||||
# Reliable animation updates use a separate channel from movement input.
|
||||
# Never let a delayed seated state re-arm motion while a recovery relocation
|
||||
# is still waiting for its explicit post-relocation neutral input.
|
||||
if _water_recovery_active or _authoritative_relocation_input_quarantine:
|
||||
return
|
||||
if should_sit and (
|
||||
not _can_begin_sitting() or _network_jump_pending
|
||||
):
|
||||
|
|
@ -2149,6 +2155,7 @@ func reset_network_movement_state() -> void:
|
|||
_network_sprint = false
|
||||
_network_sneak = false
|
||||
_network_slow_walk = false
|
||||
_authoritative_relocation_input_quarantine = false
|
||||
_network_input_age = 0.0
|
||||
_network_input_stale = false
|
||||
_network_input_stale_timeout_seconds = NETWORK_INPUT_STALE_TIMEOUT_SECONDS
|
||||
|
|
@ -2161,6 +2168,53 @@ func reset_network_movement_state() -> void:
|
|||
_local_prediction_largest_error = 0.0
|
||||
|
||||
|
||||
func neutralize_authoritative_input_for_relocation(
|
||||
recovery_input_sequence: int = -1,
|
||||
) -> void:
|
||||
# A host-side relocation must not inherit a held movement input from the
|
||||
# previous position. The client stamps its reliable recovery request with
|
||||
# the most recent input sequence it created; every lower sequence was made
|
||||
# before that request, even if it arrives on the movement channel later.
|
||||
# Keep that watermark in the acknowledgement and accept movement only after
|
||||
# an explicit neutral packet beyond it.
|
||||
var acknowledged_input: int = maxi(
|
||||
_last_network_input_sequence,
|
||||
maxi(recovery_input_sequence, 0),
|
||||
)
|
||||
reset_network_movement_state()
|
||||
_last_network_input_sequence = acknowledged_input
|
||||
_authoritative_relocation_input_quarantine = true
|
||||
_clear_authoritative_network_motion_intent()
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func _clear_authoritative_network_motion_intent() -> void:
|
||||
_network_axis = Vector2.ZERO
|
||||
_network_jump_pending = false
|
||||
_network_jump_intent_active = false
|
||||
_network_sprint = false
|
||||
_network_sneak = false
|
||||
_network_slow_walk = false
|
||||
_sit_after_landing = false
|
||||
_set_sitting(false)
|
||||
_apply_network_casting(false)
|
||||
|
||||
|
||||
func _is_neutral_authoritative_network_input(data: Dictionary) -> bool:
|
||||
var axis: Array = data.get("axis", [])
|
||||
if axis.size() != 2:
|
||||
return false
|
||||
# Sprint/sneak/slow-walk modifiers alone cannot move the avatar. Letting a
|
||||
# held modifier through preserves a responsive recovery while still requiring
|
||||
# zero movement, jump, sitting, and fishing state at the relocation barrier.
|
||||
return (
|
||||
Vector2(float(axis[0]), float(axis[1])).length_squared() <= 0.0025
|
||||
and not bool(data.get("jump", false))
|
||||
and not bool(data.get("sitting", false))
|
||||
and not bool(data.get("casting", false))
|
||||
)
|
||||
|
||||
|
||||
func apply_network_space_transition(
|
||||
target_transform: Transform3D,
|
||||
align_local_camera: bool,
|
||||
|
|
@ -2315,8 +2369,17 @@ func apply_authoritative_network_input(
|
|||
NETWORK_INPUT_STALE_TIMEOUT_SECONDS,
|
||||
NETWORK_INPUT_STALE_TIMEOUT_MAX_SECONDS,
|
||||
)
|
||||
_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
|
||||
_network_camera_yaw = float(data.get("camera_yaw", 0.0))
|
||||
if _water_recovery_active or _authoritative_relocation_input_quarantine:
|
||||
var clears_relocation_quarantine: bool = (
|
||||
_authoritative_relocation_input_quarantine
|
||||
and _is_neutral_authoritative_network_input(data)
|
||||
)
|
||||
_clear_authoritative_network_motion_intent()
|
||||
if clears_relocation_quarantine:
|
||||
_authoritative_relocation_input_quarantine = false
|
||||
return
|
||||
_network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
|
||||
var jump_intent_active: bool = bool(data.get("jump", false))
|
||||
if jump_intent_active and not _network_jump_intent_active:
|
||||
_network_jump_pending = true
|
||||
|
|
@ -2365,6 +2428,8 @@ func apply_network_animation_state(state: Dictionary) -> void:
|
|||
func apply_authoritative_network_animation_action(
|
||||
action_state: Dictionary,
|
||||
) -> void:
|
||||
if _water_recovery_active or _authoritative_relocation_input_quarantine:
|
||||
return
|
||||
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
|
||||
return
|
||||
var incoming_sequence: int = int(action_state["sequence"])
|
||||
|
|
@ -2438,6 +2503,13 @@ func apply_local_prediction_correction(
|
|||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
return
|
||||
# Water recovery owns the local transform until its reliable respawn
|
||||
# teleport arrives. Ordinary movement audits may have been authored before
|
||||
# the recovery presentation reached the host, so ignore every one of their
|
||||
# fields while recovery is active.
|
||||
if _water_recovery_active:
|
||||
_reset_local_prediction_error()
|
||||
return
|
||||
var acknowledged_input: int = parsed["acknowledged_input"]
|
||||
var sitting_intent_acknowledged: bool = (
|
||||
_sitting_intent_pending
|
||||
|
|
@ -2717,10 +2789,10 @@ static func resolve_local_prediction_transit_seconds(
|
|||
)
|
||||
|
||||
|
||||
func apply_network_teleport(snapshot: Dictionary) -> void:
|
||||
func apply_network_teleport(snapshot: Dictionary) -> bool:
|
||||
var parsed: Dictionary = _parse_network_snapshot(snapshot)
|
||||
if parsed.is_empty():
|
||||
return
|
||||
return false
|
||||
global_position = parsed["position"]
|
||||
velocity = parsed["velocity"]
|
||||
_visuals.rotation.y = parsed["visual_yaw"]
|
||||
|
|
@ -2733,6 +2805,13 @@ func apply_network_teleport(snapshot: Dictionary) -> void:
|
|||
_apply_network_target_animation_state(parsed["animation_state"])
|
||||
_network_snapshot_age = 0.0
|
||||
_network_snapshot_ready = true
|
||||
return true
|
||||
|
||||
|
||||
func reset_local_prediction_after_authoritative_teleport() -> void:
|
||||
_clear_local_network_jump_intent()
|
||||
_reset_local_prediction_error()
|
||||
_clear_local_reconciliation_offsets()
|
||||
|
||||
|
||||
func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
|
||||
|
|
@ -3070,16 +3149,18 @@ func is_movement_enabled() -> bool:
|
|||
return _movement_enabled
|
||||
|
||||
|
||||
func set_local_input_suppressed(owner: StringName, suppressed: bool) -> void:
|
||||
if owner.is_empty():
|
||||
func set_local_input_suppressed(
|
||||
suppressor_id: StringName, suppressed: bool
|
||||
) -> void:
|
||||
if suppressor_id.is_empty():
|
||||
return
|
||||
if suppressed:
|
||||
_local_input_suppressors[owner] = true
|
||||
_local_input_suppressors[suppressor_id] = true
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
_set_camera_dragging(false)
|
||||
else:
|
||||
_local_input_suppressors.erase(owner)
|
||||
_local_input_suppressors.erase(suppressor_id)
|
||||
|
||||
|
||||
func _is_movement_input_enabled() -> bool:
|
||||
|
|
@ -3101,16 +3182,16 @@ func set_camera_input_enabled(enabled: bool) -> void:
|
|||
|
||||
|
||||
func set_camera_input_suppressed(
|
||||
owner: StringName,
|
||||
suppressor_id: StringName,
|
||||
suppressed: bool,
|
||||
) -> void:
|
||||
if owner.is_empty():
|
||||
if suppressor_id.is_empty():
|
||||
return
|
||||
if suppressed:
|
||||
_camera_input_suppressors[owner] = true
|
||||
_camera_input_suppressors[suppressor_id] = true
|
||||
_set_camera_dragging(false)
|
||||
else:
|
||||
_camera_input_suppressors.erase(owner)
|
||||
_camera_input_suppressors.erase(suppressor_id)
|
||||
|
||||
|
||||
func set_camera_active(active: bool) -> void:
|
||||
|
|
@ -3198,6 +3279,8 @@ func is_camera_input_enabled() -> bool:
|
|||
|
||||
func set_water_recovery_active(active: bool) -> void:
|
||||
_water_recovery_active = active
|
||||
if active:
|
||||
_clear_authoritative_network_motion_intent()
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
|
|
@ -3374,8 +3457,8 @@ func set_active_item_is_rod(
|
|||
if _showcase_rod_state_stored:
|
||||
_showcase_rod_visibility = active_is_rod
|
||||
return
|
||||
var visibility_changed: bool = _fishing_rod.visible != active_is_rod
|
||||
if not animate_transition or not visibility_changed:
|
||||
var rod_visibility_changed: bool = _fishing_rod.visible != active_is_rod
|
||||
if not animate_transition or not rod_visibility_changed:
|
||||
_fishing_rod.visible = active_is_rod
|
||||
return
|
||||
if _has_held_show_item() or _showcase_animation_active:
|
||||
|
|
|
|||
|
|
@ -156,24 +156,24 @@ func _initialize_pool() -> void:
|
|||
|
||||
func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
|
||||
var lateral := Vector3(-direction.z, 0.0, direction.x)
|
||||
var position := (
|
||||
var puff_position := (
|
||||
source_position
|
||||
- direction * PUFF_BACK_OFFSET
|
||||
+ lateral * PUFF_SIDE_OFFSET * _foot_side
|
||||
+ Vector3.UP * PUFF_HEIGHT
|
||||
)
|
||||
_spawn_puff_at(position)
|
||||
_spawn_puff_at(puff_position)
|
||||
_foot_side *= -1.0
|
||||
|
||||
|
||||
func _spawn_puff_at(
|
||||
position: Vector3,
|
||||
puff_position: Vector3,
|
||||
size_multiplier: float = 1.0,
|
||||
) -> void:
|
||||
var variation: float = _stable_variation(_spawn_serial)
|
||||
var second_variation: float = _stable_variation(_spawn_serial + 37)
|
||||
var index: int = _next_puff
|
||||
_puff_positions[index] = position
|
||||
_puff_positions[index] = puff_position
|
||||
_puff_ages[index] = 0.0
|
||||
_puff_widths[index] = (
|
||||
lerpf(0.34, 0.43, variation) * size_multiplier
|
||||
|
|
|
|||
|
|
@ -56,8 +56,8 @@ func _initialize() -> void:
|
|||
quit(2)
|
||||
return
|
||||
|
||||
for root: String in roots:
|
||||
var normalized_root := root
|
||||
for root_path: String in roots:
|
||||
var normalized_root := root_path
|
||||
if not normalized_root.ends_with("/"):
|
||||
normalized_root += "/"
|
||||
if not normalized_root.begins_with("res://") and not normalized_root.is_absolute_path():
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ func copy() -> PlayerSettings:
|
|||
func normalize_implicit_presentation_layout() -> bool:
|
||||
if presentation_layout_customized:
|
||||
return false
|
||||
var changed: bool = (
|
||||
var layout_changed: bool = (
|
||||
chat_dock_right != DEFAULT_CHAT_DOCK_RIGHT
|
||||
or chat_mobile_mode != DEFAULT_CHAT_MOBILE_MODE
|
||||
or paint_dock_right != DEFAULT_PAINT_DOCK_RIGHT
|
||||
|
|
@ -109,7 +109,7 @@ func normalize_implicit_presentation_layout() -> bool:
|
|||
chat_dock_right = DEFAULT_CHAT_DOCK_RIGHT
|
||||
chat_mobile_mode = DEFAULT_CHAT_MOBILE_MODE
|
||||
paint_dock_right = DEFAULT_PAINT_DOCK_RIGHT
|
||||
return changed
|
||||
return layout_changed
|
||||
|
||||
|
||||
static func _is_valid_audio_volume(value: float) -> bool:
|
||||
|
|
|
|||
|
|
@ -44,18 +44,20 @@ func _init() -> void:
|
|||
assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
|
||||
assert(_has_joypad_button(&"open_chat", JOY_BUTTON_BACK))
|
||||
assert(_has_joypad_button(&"focus_gameplay", JOY_BUTTON_LEFT_SHOULDER))
|
||||
assert(
|
||||
GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS == JOY_AXIS_TRIGGER_RIGHT
|
||||
)
|
||||
assert(
|
||||
var virtual_mouse_trigger_axis: int = GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS
|
||||
var virtual_mouse_shared_trigger_axis: int = (
|
||||
GameUIType.VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS
|
||||
== JOY_AXIS_TRIGGER_LEFT
|
||||
)
|
||||
assert(
|
||||
var virtual_mouse_secondary_click_axis: int = (
|
||||
GameUIType.VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS
|
||||
== JOY_AXIS_TRIGGER_LEFT
|
||||
)
|
||||
assert(PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS == JOY_AXIS_TRIGGER_LEFT)
|
||||
var controller_zoom_trigger_axis: int = (
|
||||
PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS
|
||||
)
|
||||
assert(virtual_mouse_trigger_axis == JOY_AXIS_TRIGGER_RIGHT)
|
||||
assert(virtual_mouse_shared_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
|
||||
assert(virtual_mouse_secondary_click_axis == JOY_AXIS_TRIGGER_LEFT)
|
||||
assert(controller_zoom_trigger_axis == JOY_AXIS_TRIGGER_LEFT)
|
||||
assert(is_zero_approx(
|
||||
GameUIType.normalized_trigger_strength(-1.0, -1.0)
|
||||
))
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
|
|||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
assert(not weights.is_empty())
|
||||
assert(not vertices.is_empty())
|
||||
var components_per_vertex := weights.size() / vertices.size()
|
||||
var components_per_vertex: int = int(
|
||||
float(weights.size()) / float(vertices.size())
|
||||
)
|
||||
assert(components_per_vertex == 4 or components_per_vertex == 8)
|
||||
for offset in range(0, weights.size(), components_per_vertex):
|
||||
var total := 0.0
|
||||
|
|
|
|||
|
|
@ -367,13 +367,13 @@ func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
|
|||
if str(binding.get("kind", "")) == "button":
|
||||
var button := InputEventJoypadButton.new()
|
||||
button.device = manager.get_active_device_id()
|
||||
button.button_index = int(binding.get("button", -1))
|
||||
button.button_index = int(binding.get("button", -1)) as JoyButton
|
||||
button.pressed = true
|
||||
manager.controller_input_observed.emit(button)
|
||||
else:
|
||||
var motion := InputEventJoypadMotion.new()
|
||||
motion.device = manager.get_active_device_id()
|
||||
motion.axis = int(binding.get("axis", -1))
|
||||
motion.axis = int(binding.get("axis", -1)) as JoyAxis
|
||||
motion.axis_value = float(binding.get("direction", -1.0))
|
||||
manager.controller_input_observed.emit(motion)
|
||||
if panel._auto_map_active:
|
||||
|
|
|
|||
|
|
@ -2015,7 +2015,7 @@ func _assert_neighbor(
|
|||
% [
|
||||
origin.name,
|
||||
property,
|
||||
actual.name if actual != null else "nothing",
|
||||
str(actual.name) if actual != null else "nothing",
|
||||
expected.name,
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,10 +21,16 @@ func _run() -> void:
|
|||
assert(not bool(defaults.get("public_listing")))
|
||||
assert(not bool(defaults.get("chat_logging")))
|
||||
assert(str(defaults.get("chat_log_path")).is_empty())
|
||||
assert(NetworkChatService.BURST_COUNT == 3)
|
||||
assert(NetworkChatService.WINDOW_COUNT == 5)
|
||||
assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
|
||||
assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
|
||||
var chat_burst_count: int = NetworkChatService.BURST_COUNT
|
||||
var chat_window_count: int = NetworkChatService.WINDOW_COUNT
|
||||
var chat_window_seconds: float = NetworkChatService.WINDOW_SECONDS
|
||||
var chat_call_cooldown_milliseconds: int = (
|
||||
NetworkChatService.CALL_COOLDOWN_MILLISECONDS
|
||||
)
|
||||
assert(chat_burst_count == 3)
|
||||
assert(chat_window_count == 5)
|
||||
assert(is_equal_approx(chat_window_seconds, 10.0))
|
||||
assert(chat_call_cooldown_milliseconds == 90)
|
||||
|
||||
var path: String = ProjectSettings.globalize_path(
|
||||
"user://dedicated-server-validation.cfg"
|
||||
|
|
@ -156,14 +162,17 @@ func _run() -> void:
|
|||
str(discovery.call("_default_room_name", "River"))
|
||||
== "River's Server"
|
||||
)
|
||||
assert(
|
||||
var upnp_renew_interval_seconds: float = (
|
||||
DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
|
||||
< float(DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS)
|
||||
)
|
||||
assert(
|
||||
var upnp_mapping_duration_seconds: float = float(
|
||||
DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS
|
||||
)
|
||||
var upnp_retry_interval_seconds: float = (
|
||||
DiscoveryClient.UPNP_RETRY_INTERVAL_SECONDS
|
||||
< DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
|
||||
)
|
||||
assert(upnp_renew_interval_seconds < upnp_mapping_duration_seconds)
|
||||
assert(upnp_retry_interval_seconds < upnp_renew_interval_seconds)
|
||||
var discovery_settings_path: String = ProjectSettings.globalize_path(
|
||||
DiscoveryClient.SETTINGS_PATH
|
||||
)
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ func _run() -> void:
|
|||
assert(session.set_host_open(false))
|
||||
|
||||
await _test_host_shop_purchase(main, player, shop_service)
|
||||
await _test_host_rod_purchase(player, shop_service)
|
||||
_test_host_rod_purchase(player, shop_service)
|
||||
await _test_host_art_shop_purchase(main, player, shop_service)
|
||||
await _test_fishing_shop_sale_ui(
|
||||
main, player, catalog, sale_service, reservations
|
||||
|
|
|
|||
|
|
@ -34,20 +34,20 @@ func _run() -> void:
|
|||
var root_controls: Array[Control] = (
|
||||
FileDialogControllerNavigationType.interactive_controls(root_scope)
|
||||
)
|
||||
var directory_list: ItemList
|
||||
var parent_button: Button
|
||||
var create_folder_button: Button
|
||||
var path_edit: LineEdit
|
||||
var drive_button: MenuButton
|
||||
var refresh_button: Button
|
||||
var favorite_button: Button
|
||||
var hidden_button: Button
|
||||
var grid_button: Button
|
||||
var list_button: Button
|
||||
var filter_button: Button
|
||||
var sort_button: MenuButton
|
||||
var cancel_button: Button
|
||||
var select_button: Button
|
||||
var directory_list: ItemList = null
|
||||
var parent_button: Button = null
|
||||
var create_folder_button: Button = null
|
||||
var path_edit: LineEdit = null
|
||||
var drive_button: MenuButton = null
|
||||
var refresh_button: Button = null
|
||||
var favorite_button: Button = null
|
||||
var hidden_button: Button = null
|
||||
var grid_button: Button = null
|
||||
var list_button: Button = null
|
||||
var filter_button: Button = null
|
||||
var sort_button: MenuButton = null
|
||||
var cancel_button: Button = null
|
||||
var select_button: Button = null
|
||||
for control: Control in root_controls:
|
||||
if (
|
||||
control is ItemList
|
||||
|
|
|
|||
|
|
@ -200,15 +200,17 @@ func _run() -> void:
|
|||
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||
invalid_state["display_scale"] = 1000.0
|
||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
|
||||
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
|
||||
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(enet_channel_count == 18)
|
||||
assert(
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
== "fish_quality_v1"
|
||||
fish_quality_capability == "fish_quality_v1"
|
||||
)
|
||||
assert(
|
||||
NetworkFishShowcaseProtocol.CAPABILITY
|
||||
== &"fish_showcase_v1"
|
||||
showcase_capability == &"fish_showcase_v1"
|
||||
)
|
||||
|
||||
print("Fish hotbar showcase validation: PASS")
|
||||
|
|
|
|||
|
|
@ -26,18 +26,28 @@ func _run() -> void:
|
|||
_validate_collection_mastery()
|
||||
_validate_version_four_migration()
|
||||
_validate_fishing_protocol_v2()
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
|
||||
assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
|
||||
assert(
|
||||
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
|
||||
var enet_channel_count: int = NetworkProtocol.ENET_CHANNEL_COUNT
|
||||
var movement_animation_channel: int = (
|
||||
NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL
|
||||
)
|
||||
var movement_reconciliation_capability: String = (
|
||||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
|
||||
== "movement_reconciliation_v2"
|
||||
)
|
||||
var fishing_replication_capability: String = (
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
|
||||
)
|
||||
var fish_quality_capability: String = NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
assert(protocol_version == 12)
|
||||
assert(enet_channel_count == 18)
|
||||
assert(movement_animation_channel == 11)
|
||||
assert(
|
||||
movement_reconciliation_capability == "movement_reconciliation_v2"
|
||||
)
|
||||
assert(
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY
|
||||
== "fishing_replication_v2"
|
||||
fishing_replication_capability == "fishing_replication_v2"
|
||||
)
|
||||
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
||||
assert(fish_quality_capability == "fish_quality_v1")
|
||||
print("Fish quality validation: PASS")
|
||||
quit()
|
||||
|
||||
|
|
@ -71,11 +81,17 @@ func _validate_fishing_protocol_v2() -> void:
|
|||
NetworkFishingProtocol.validate_cast_request(invalid_sequence)
|
||||
== "Fishing request values are outside allowed limits."
|
||||
)
|
||||
assert(NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL == 10)
|
||||
assert(NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL == 15)
|
||||
assert(
|
||||
var observer_snapshot_channel: int = (
|
||||
NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL
|
||||
)
|
||||
var world_spawn_snapshot_channel: int = (
|
||||
NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL
|
||||
!= NetworkFishingProtocol.SNAPSHOT_CHANNEL
|
||||
)
|
||||
var fishing_snapshot_channel: int = NetworkFishingProtocol.SNAPSHOT_CHANNEL
|
||||
assert(observer_snapshot_channel == 10)
|
||||
assert(world_spawn_snapshot_channel == 15)
|
||||
assert(
|
||||
world_spawn_snapshot_channel != fishing_snapshot_channel
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -240,10 +256,14 @@ func _validate_barrier_challenge_curve() -> void:
|
|||
|
||||
|
||||
func _validate_fight_pacing_and_reel_upgrades() -> void:
|
||||
assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
|
||||
assert(is_equal_approx(CatchController.CHASE_START_DELAY, 1.5))
|
||||
assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
|
||||
assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))
|
||||
var chase_speed: float = CatchController.CHASE_SPEED
|
||||
var chase_start_delay: float = CatchController.CHASE_START_DELAY
|
||||
var chase_start_offset: float = CatchController.CHASE_START_OFFSET
|
||||
var base_reel_speed: float = Player.BASE_REEL_SPEED
|
||||
assert(is_equal_approx(chase_speed, 0.07))
|
||||
assert(is_equal_approx(chase_start_delay, 1.5))
|
||||
assert(is_equal_approx(chase_start_offset, 0.04))
|
||||
assert(is_equal_approx(base_reel_speed, 0.16))
|
||||
|
||||
var upgrades := PlayerFishingUpgrades.new()
|
||||
assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))
|
||||
|
|
|
|||
|
|
@ -277,6 +277,28 @@ func _run() -> void:
|
|||
water_recovery.call("_finish_recovery")
|
||||
assert(player.is_movement_enabled())
|
||||
|
||||
# Session/world teardown can interrupt the fade before its normal completion.
|
||||
# Recovery must still release FishingSpot's external input lock without
|
||||
# restoring movement that gameplay shutdown already disabled.
|
||||
water_recovery.call(
|
||||
"_on_recovery_requested",
|
||||
player,
|
||||
player.global_position.y,
|
||||
)
|
||||
assert(water_recovery.is_recovery_active())
|
||||
var screen_fade := game_ui.get_screen_fade() as ScreenFade
|
||||
assert(screen_fade != null)
|
||||
screen_fade.fade_to_black(int(water_recovery.get("_generation")))
|
||||
water_recovery.set_recovery_enabled(false)
|
||||
assert(not water_recovery.is_recovery_active())
|
||||
assert(not player.is_water_recovery_active())
|
||||
assert(not bool(fishing_spot.get("_external_input_blocked")))
|
||||
assert(not player.is_movement_enabled())
|
||||
assert(not screen_fade.visible)
|
||||
water_recovery.set_recovery_enabled(true)
|
||||
player.set_movement_enabled(true)
|
||||
player.set_camera_input_enabled(true)
|
||||
|
||||
print("Fishing authority validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
|
|
|
|||
|
|
@ -271,7 +271,6 @@ func _run_client() -> void:
|
|||
assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
|
||||
for _frame: int in 2:
|
||||
await physics_frame
|
||||
var expected_position: Vector3 = player.global_position
|
||||
var expected_direction: Vector3 = player.get_facing_direction()
|
||||
|
||||
player.set_casting_visual()
|
||||
|
|
|
|||
|
|
@ -59,11 +59,11 @@ func _run() -> void:
|
|||
|
||||
wall.queue_free()
|
||||
await process_frame
|
||||
var floor := _make_surface(
|
||||
var floor_surface := _make_surface(
|
||||
Vector3(10.0, 0.2, 10.0),
|
||||
Vector3(0.0, -0.1, -1.7),
|
||||
)
|
||||
root.add_child(floor)
|
||||
root.add_child(floor_surface)
|
||||
await physics_frame
|
||||
controller.call("_update_marker_target")
|
||||
assert(bool(controller.get("_marker_has_surface")))
|
||||
|
|
@ -80,7 +80,7 @@ func _run() -> void:
|
|||
)
|
||||
)
|
||||
|
||||
floor.queue_free()
|
||||
floor_surface.queue_free()
|
||||
controller.queue_free()
|
||||
player.queue_free()
|
||||
await process_frame
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ const Gatherables: GatherableCatalog = preload(
|
|||
const PrecipitationOcclusionType = preload(
|
||||
"res://world/environment/precipitation_occlusion.gd"
|
||||
)
|
||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||
"res://world/materials/foliage_wind.gdshader"
|
||||
)
|
||||
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
|
||||
&"foliage_wind_source_materials"
|
||||
)
|
||||
const FIRST_SEED := 13001
|
||||
const SECOND_SEED := 13002
|
||||
const RIVER_FALLBACK_SEED := 13012
|
||||
|
|
@ -130,8 +136,8 @@ func _validate_generated_region(
|
|||
var placements := generator.placement_keys()
|
||||
var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
|
||||
var center := Vector2i(
|
||||
generator.grid_size.x / 2,
|
||||
generator.grid_size.y / 2,
|
||||
int(float(generator.grid_size.x) / 2.0),
|
||||
int(float(generator.grid_size.y) / 2.0),
|
||||
)
|
||||
var center_index := center.y * generator.grid_size.x + center.x
|
||||
assert(generator.grid_size == Vector2i(20, 20))
|
||||
|
|
@ -341,7 +347,7 @@ func _validate_generated_region(
|
|||
):
|
||||
var outlet_coordinate := Vector2i(
|
||||
index % generator.grid_size.x,
|
||||
index / generator.grid_size.x,
|
||||
int(float(index) / float(generator.grid_size.x)),
|
||||
)
|
||||
outlet_coordinates.append(outlet_coordinate)
|
||||
assert(
|
||||
|
|
@ -366,7 +372,7 @@ func _validate_generated_region(
|
|||
for index: int in placements.size():
|
||||
var coordinate := Vector2i(
|
||||
index % generator.grid_size.x,
|
||||
index / generator.grid_size.x,
|
||||
int(float(index) / float(generator.grid_size.x)),
|
||||
)
|
||||
if placements[index].begins_with("chunk_0001@"):
|
||||
assert(generator._distance_from_map_boundary(coordinate) <= 1)
|
||||
|
|
@ -587,8 +593,8 @@ func _validate_generated_region(
|
|||
Vector2i.ZERO,
|
||||
)
|
||||
var spawn_coordinate := Vector2i(
|
||||
generator.grid_size.x / 2,
|
||||
generator.grid_size.y / 2,
|
||||
int(float(generator.grid_size.x) / 2.0),
|
||||
int(float(generator.grid_size.y) / 2.0),
|
||||
)
|
||||
var spawn_distance := (
|
||||
absi(coordinate.x - spawn_coordinate.x)
|
||||
|
|
@ -1146,8 +1152,8 @@ func _validate_biome_catalog(
|
|||
for biome_id: StringName in EXPECTED_BIOME_IDS:
|
||||
assert(counts.get(biome_id, 0) > 0)
|
||||
var center := Vector2i(
|
||||
generator.grid_size.x / 2,
|
||||
generator.grid_size.y / 2,
|
||||
int(float(generator.grid_size.x) / 2.0),
|
||||
int(float(generator.grid_size.y) / 2.0),
|
||||
)
|
||||
assert(region.get_biome_at(center) == &"biome_plains")
|
||||
for child: Node in generator.get_generated_chunks_root().get_children():
|
||||
|
|
@ -1504,12 +1510,27 @@ func _has_material_variant_override(
|
|||
return false
|
||||
var mesh_instance := root_node as MeshInstance3D
|
||||
if mesh_instance != null and mesh_instance.mesh != null:
|
||||
var source_materials: Dictionary = mesh_instance.get_meta(
|
||||
FOLIAGE_WIND_SOURCE_MATERIALS_META,
|
||||
{},
|
||||
)
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var override := mesh_instance.get_surface_override_material(
|
||||
surface_index
|
||||
)
|
||||
if override != null and override in variants:
|
||||
return true
|
||||
var wind_material := override as ShaderMaterial
|
||||
if (
|
||||
wind_material != null
|
||||
and wind_material.shader == FOLIAGE_WIND_SHADER
|
||||
):
|
||||
var source_material: Material = source_materials.get(
|
||||
surface_index,
|
||||
null,
|
||||
) as Material
|
||||
if source_material != null and source_material in variants:
|
||||
return true
|
||||
for child: Node in root_node.get_children():
|
||||
if _has_material_variant_override(child, variants):
|
||||
return true
|
||||
|
|
@ -1647,7 +1668,7 @@ func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
|
|||
return result
|
||||
|
||||
|
||||
func _validate_projected_terrain_materials(root: Node) -> void:
|
||||
func _validate_projected_terrain_materials(projected_root: Node) -> void:
|
||||
var expected_sizes := {
|
||||
"grass_lite": 1.75,
|
||||
"sand": 2.6,
|
||||
|
|
@ -1658,17 +1679,21 @@ func _validate_projected_terrain_materials(root: Node) -> void:
|
|||
"sand": 0,
|
||||
"dirt": 0,
|
||||
}
|
||||
_validate_projected_material_node(root, expected_sizes, surface_counts)
|
||||
_validate_projected_material_node(
|
||||
projected_root,
|
||||
expected_sizes,
|
||||
surface_counts,
|
||||
)
|
||||
for material_name: String in expected_sizes:
|
||||
assert(surface_counts[material_name] > 0)
|
||||
|
||||
|
||||
func _validate_projected_material_node(
|
||||
root: Node,
|
||||
node: Node,
|
||||
expected_sizes: Dictionary,
|
||||
surface_counts: Dictionary,
|
||||
) -> void:
|
||||
var mesh_instance := root as MeshInstance3D
|
||||
var mesh_instance := node as MeshInstance3D
|
||||
if mesh_instance != null and mesh_instance.mesh != null:
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var material := mesh_instance.get_active_material(surface_index)
|
||||
|
|
@ -1689,7 +1714,7 @@ func _validate_projected_material_node(
|
|||
)
|
||||
)
|
||||
surface_counts[material.resource_name] += 1
|
||||
for child: Node in root.get_children():
|
||||
for child: Node in node.get_children():
|
||||
_validate_projected_material_node(
|
||||
child,
|
||||
expected_sizes,
|
||||
|
|
@ -1708,8 +1733,10 @@ func _validate_pond_collision(
|
|||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
var triangle_count := (
|
||||
indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
|
||||
var triangle_count: int = (
|
||||
int(float(indices.size()) / 3.0)
|
||||
if not indices.is_empty()
|
||||
else int(float(vertices.size()) / 3.0)
|
||||
)
|
||||
for triangle_index: int in triangle_count:
|
||||
var offset := triangle_index * 3
|
||||
|
|
|
|||
|
|
@ -678,7 +678,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
|
|||
player_menu.call("_show_section_immediate", sections[index])
|
||||
await process_frame
|
||||
await process_frame
|
||||
await _save_capture(suffixes[index])
|
||||
_save_capture(suffixes[index])
|
||||
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
|
||||
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
||||
var confirmation_message := player_menu.get_node(
|
||||
|
|
@ -693,7 +693,7 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
|
|||
sale_confirmation.visible = true
|
||||
await process_frame
|
||||
await process_frame
|
||||
await _save_capture("sale-confirmation")
|
||||
_save_capture("sale-confirmation")
|
||||
sale_confirmation.visible = false
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,16 +38,18 @@ func _run() -> void:
|
|||
KeyboardMouseMappingManagerType.ROLE_INTERACT
|
||||
) == "e"
|
||||
)
|
||||
assert(
|
||||
var zoom_in_label: String = str(
|
||||
KeyboardMouseMappingManagerType.ROLE_LABELS[
|
||||
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_IN
|
||||
] == "hotbar select (+SHIFT to zoom)"
|
||||
]
|
||||
)
|
||||
assert(
|
||||
var zoom_out_label: String = str(
|
||||
KeyboardMouseMappingManagerType.ROLE_LABELS[
|
||||
KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_OUT
|
||||
] == "hotbar select (+SHIFT to zoom)"
|
||||
]
|
||||
)
|
||||
assert(zoom_in_label == "hotbar select (+SHIFT to zoom)")
|
||||
assert(zoom_out_label == "hotbar select (+SHIFT to zoom)")
|
||||
assert(
|
||||
str(defaults[
|
||||
str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
|
||||
|
|
|
|||
|
|
@ -293,25 +293,25 @@ func _wait_for_avatar_sitting(
|
|||
var deadline: int = Time.get_ticks_msec() + 10000
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
await process_frame
|
||||
var avatar: Player = spawn_service.get_avatar(peer_id)
|
||||
if avatar != null and avatar.is_sitting() == expected:
|
||||
var observed_avatar: Player = spawn_service.get_avatar(peer_id)
|
||||
if observed_avatar != null and observed_avatar.is_sitting() == expected:
|
||||
return true
|
||||
var avatar: Player = spawn_service.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
var timed_out_avatar: Player = spawn_service.get_avatar(peer_id)
|
||||
if timed_out_avatar != null:
|
||||
print(
|
||||
"sitting wait timed out: ",
|
||||
{
|
||||
"peer_id": peer_id,
|
||||
"expected": expected,
|
||||
"sitting": avatar.is_sitting(),
|
||||
"sit_after_landing": bool(avatar.get("_sit_after_landing")),
|
||||
"on_floor": avatar.is_on_floor(),
|
||||
"position": avatar.global_position,
|
||||
"velocity": avatar.velocity,
|
||||
"authoritative": bool(avatar.get(
|
||||
"sitting": timed_out_avatar.is_sitting(),
|
||||
"sit_after_landing": bool(timed_out_avatar.get("_sit_after_landing")),
|
||||
"on_floor": timed_out_avatar.is_on_floor(),
|
||||
"position": timed_out_avatar.global_position,
|
||||
"velocity": timed_out_avatar.velocity,
|
||||
"authoritative": bool(timed_out_avatar.get(
|
||||
"_network_authoritative_simulation"
|
||||
)),
|
||||
"last_input": int(avatar.get(
|
||||
"last_input": int(timed_out_avatar.get(
|
||||
"_last_network_input_sequence"
|
||||
)),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -217,11 +217,11 @@ func _validate_new_game_music_transition(main: Node) -> void:
|
|||
|
||||
|
||||
func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
|
||||
var root := main.get_node(
|
||||
var fresh_water_root := main.get_node(
|
||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||
) as Node3D
|
||||
assert(root != null and root.get_child_count() > 0)
|
||||
return root.get_child(0).get_node("VisualWater") as MeshInstance3D
|
||||
assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
|
||||
return fresh_water_root.get_child(0).get_node("VisualWater") as MeshInstance3D
|
||||
|
||||
|
||||
func _validate_ocean_fishing_coverage(
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ func _validate_page() -> void:
|
|||
== "flathead\ncatfish"
|
||||
)
|
||||
|
||||
var shared_material: Material
|
||||
var shared_material: Material = null
|
||||
var silhouette_count: int = 0
|
||||
var target_silhouette_area: float = (
|
||||
LogbookPage.CATALOG_PORTRAIT_SIZE.x
|
||||
|
|
@ -558,9 +558,9 @@ func _validate_page() -> void:
|
|||
]:
|
||||
page.call("_select_category", category)
|
||||
await create_timer(0.25).timeout
|
||||
var entries: Dictionary = page.get("_entry_buttons")
|
||||
var category_entries: Dictionary = page.get("_entry_buttons")
|
||||
assert(
|
||||
entries.size()
|
||||
category_entries.size()
|
||||
== (108 if category == LogbookCatalog.Category.FRESH_WATER else 202)
|
||||
)
|
||||
for fish: FishDataType in LogbookCatalog.ordered_species(
|
||||
|
|
@ -571,7 +571,7 @@ func _validate_page() -> void:
|
|||
var unknown_key := StringName(
|
||||
"unknown_%d" % LogbookCatalog.catalog_number(fish)
|
||||
)
|
||||
var entry := entries.get(unknown_key) as Button
|
||||
var entry := category_entries.get(unknown_key) as Button
|
||||
assert(entry != null)
|
||||
assert(entry.text.is_empty())
|
||||
assert(entry.tooltip_text.is_empty())
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ func _validate_latency_smoothing() -> void:
|
|||
avatar.set_physics_process(false)
|
||||
_validate_compact_input_encoding()
|
||||
_validate_compact_snapshot_encoding()
|
||||
_validate_authoritative_teleport_snapshot_guard()
|
||||
_validate_compact_animation_encoding()
|
||||
_validate_animation_action_ordering(avatar)
|
||||
_validate_transit_estimation()
|
||||
|
|
@ -72,8 +73,12 @@ func _validate_compact_snapshot_encoding() -> void:
|
|||
Vector3(4.5, 0.0, 0.0),
|
||||
1,
|
||||
)
|
||||
assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
|
||||
assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
|
||||
var movement_snapshot_batch_size: int = (
|
||||
NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE
|
||||
)
|
||||
var owner_snapshot_divisor: int = NetworkSession.OWNER_SNAPSHOT_DIVISOR
|
||||
assert(movement_snapshot_batch_size == 8)
|
||||
assert(owner_snapshot_divisor == 3)
|
||||
var encoded_snapshots: Array = []
|
||||
for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
|
||||
encoded_snapshots.append(
|
||||
|
|
@ -100,6 +105,50 @@ func _validate_compact_snapshot_encoding() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _validate_authoritative_teleport_snapshot_guard() -> void:
|
||||
var session := NetworkSession.new()
|
||||
var destination := Vector3(24.0, 0.0, -8.0)
|
||||
session.set("_local_authoritative_teleport_guard_active", true)
|
||||
session.set("_local_authoritative_teleport_guard_position", destination)
|
||||
session.set("_local_authoritative_teleport_minimum_ack", 48)
|
||||
|
||||
# A channel-two audit from before the reliable relocation must not move the
|
||||
# local player back to its old position.
|
||||
assert(bool(session.call(
|
||||
"_reject_stale_authoritative_teleport_snapshot",
|
||||
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 48),
|
||||
)))
|
||||
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
|
||||
assert(bool(session.call(
|
||||
"_reject_stale_authoritative_teleport_snapshot",
|
||||
_network_snapshot(destination, Vector3.ZERO, 47),
|
||||
)))
|
||||
|
||||
# A matching audit from the new location clears the barrier. A higher
|
||||
# acknowledgement at the old position is not sufficient: movement input and
|
||||
# the reliable teleport travelled on different channels, so it can still be
|
||||
# a held packet from before recovery.
|
||||
assert(not bool(session.call(
|
||||
"_reject_stale_authoritative_teleport_snapshot",
|
||||
_network_snapshot(destination + Vector3.RIGHT, Vector3.ZERO, 48),
|
||||
)))
|
||||
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
|
||||
session.set("_local_authoritative_teleport_guard_active", true)
|
||||
session.set("_local_authoritative_teleport_guard_position", destination)
|
||||
session.set("_local_authoritative_teleport_minimum_ack", 48)
|
||||
assert(bool(session.call(
|
||||
"_reject_stale_authoritative_teleport_snapshot",
|
||||
_network_snapshot(Vector3.ZERO, Vector3.ZERO, 49),
|
||||
)))
|
||||
assert(bool(session.get("_local_authoritative_teleport_guard_active")))
|
||||
assert(not bool(session.call(
|
||||
"_reject_stale_authoritative_teleport_snapshot",
|
||||
_network_snapshot(destination, Vector3.ZERO, 49),
|
||||
)))
|
||||
assert(not bool(session.get("_local_authoritative_teleport_guard_active")))
|
||||
session.free()
|
||||
|
||||
|
||||
func _validate_compact_animation_encoding() -> void:
|
||||
var paused_state := NetworkPlayerAnimationProtocol.make_state(
|
||||
NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
|
||||
|
|
@ -315,16 +364,16 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
|
||||
# Reconciliation only applies after both simulations report a grounded body.
|
||||
# Give the unit avatar a real floor so this exercises the production path.
|
||||
var floor := StaticBody3D.new()
|
||||
floor.collision_layer = 1
|
||||
floor.collision_mask = 0
|
||||
floor.position = Vector3(0.0, -0.1, 0.0)
|
||||
var floor_body := StaticBody3D.new()
|
||||
floor_body.collision_layer = 1
|
||||
floor_body.collision_mask = 0
|
||||
floor_body.position = Vector3(0.0, -0.1, 0.0)
|
||||
var floor_shape := CollisionShape3D.new()
|
||||
var floor_box := BoxShape3D.new()
|
||||
floor_box.size = Vector3(100.0, 0.2, 100.0)
|
||||
floor_shape.shape = floor_box
|
||||
floor.add_child(floor_shape)
|
||||
root.add_child(floor)
|
||||
floor_body.add_child(floor_shape)
|
||||
root.add_child(floor_body)
|
||||
await physics_frame
|
||||
avatar.set_local_control(true)
|
||||
avatar.global_position = Vector3(0.8, 0.0, 0.0)
|
||||
|
|
@ -434,6 +483,24 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
int(avatar.get("_local_prediction_hard_corrections")) == 1
|
||||
)
|
||||
|
||||
# Recovery owns all local state until its reliable teleport arrives. A
|
||||
# delayed audit must neither pull the body back nor acknowledge an old jump.
|
||||
avatar.global_position = Vector3(10.0, 0.0, 0.0)
|
||||
avatar.call("_queue_local_network_jump_intent")
|
||||
avatar.capture_network_input(20)
|
||||
avatar.set_water_recovery_active(true)
|
||||
avatar.apply_local_prediction_correction(
|
||||
drift_snapshot,
|
||||
20,
|
||||
1.0 / 30.0,
|
||||
0.0,
|
||||
0.1,
|
||||
)
|
||||
assert(avatar.global_position == Vector3(10.0, 0.0, 0.0))
|
||||
assert(bool(avatar.get("_local_network_jump_intent_pending")))
|
||||
avatar.set_water_recovery_active(false)
|
||||
avatar.call("_clear_local_network_jump_intent")
|
||||
|
||||
# Soft reconciliation must respect the same terrain collision as ordinary
|
||||
# player movement instead of phasing the capsule through a solid obstacle.
|
||||
var blocker := StaticBody3D.new()
|
||||
|
|
@ -470,7 +537,7 @@ func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
|
|||
== blocked_camera_position
|
||||
)
|
||||
blocker.queue_free()
|
||||
floor.queue_free()
|
||||
floor_body.queue_free()
|
||||
await physics_frame
|
||||
|
||||
|
||||
|
|
@ -567,6 +634,40 @@ func _validate_stale_input_expiry(avatar: Player) -> void:
|
|||
assert(not bool(avatar.get("_network_input_stale")))
|
||||
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
|
||||
|
||||
# The reliable recovery request carries the last client-created sequence.
|
||||
# A host uses it as a hard watermark, so every delayed input from before the
|
||||
# request is discarded even if it arrives after the teleport RPC.
|
||||
avatar.neutralize_authoritative_input_for_relocation(44)
|
||||
assert(int(avatar.get("_last_network_input_sequence")) == 44)
|
||||
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
|
||||
assert(not bool(avatar.get("_network_sprint")))
|
||||
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
|
||||
avatar.apply_authoritative_network_input(_movement_input(41, true))
|
||||
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
|
||||
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
|
||||
assert(int(avatar.get("_last_network_input_sequence")) == 44)
|
||||
|
||||
# A non-neutral post-watermark packet cannot restart motion. The first
|
||||
# neutral packet past the watermark releases the barrier; the following
|
||||
# input is the first one allowed to move the recovered avatar.
|
||||
avatar.apply_authoritative_network_input(_movement_input(45, false, true))
|
||||
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
|
||||
assert(bool(avatar.get("_authoritative_relocation_input_quarantine")))
|
||||
avatar.apply_authoritative_network_input(_movement_input(46, false, false))
|
||||
assert(not bool(avatar.get("_authoritative_relocation_input_quarantine")))
|
||||
avatar.apply_authoritative_network_input(_movement_input(47, false, true))
|
||||
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
|
||||
|
||||
# Recovery itself also consumes incoming movement rather than remembering a
|
||||
# held axis which could resume on the exact frame recovery presentation ends.
|
||||
avatar.set_water_recovery_active(true)
|
||||
avatar.apply_authoritative_network_input(_movement_input(48, false, true))
|
||||
assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
|
||||
avatar.set_water_recovery_active(false)
|
||||
avatar.apply_authoritative_network_input(_movement_input(42, false, false))
|
||||
avatar.apply_authoritative_network_input(_movement_input(49, false, true))
|
||||
assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
|
||||
|
||||
|
||||
func _network_snapshot(
|
||||
position: Vector3,
|
||||
|
|
|
|||
|
|
@ -307,13 +307,13 @@ func _validate_native_keyboard() -> void:
|
|||
await process_frame
|
||||
edit.grab_focus()
|
||||
assert(keyboard.is_open())
|
||||
var focus_exit_count: int = 0
|
||||
var focus_exit_state: Dictionary = {"count": 0}
|
||||
edit.focus_exited.connect(func() -> void:
|
||||
focus_exit_count += 1
|
||||
focus_exit_state["count"] = int(focus_exit_state["count"]) + 1
|
||||
)
|
||||
assert(keyboard.request_for_control(edit))
|
||||
assert(edit.has_focus())
|
||||
assert(focus_exit_count == 0)
|
||||
assert(int(focus_exit_state["count"]) == 0)
|
||||
assert(not edit.virtual_keyboard_enabled)
|
||||
assert(not edit.virtual_keyboard_show_on_focus)
|
||||
assert(keyboard.shown_controls == [edit])
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func _run() -> void:
|
|||
assert(not vertices.is_empty())
|
||||
assert(vertices.size() % 2 == 0)
|
||||
assert(indices.size() == vertices.size() * 3)
|
||||
var point_count := vertices.size() / 2
|
||||
var point_count: int = int(float(vertices.size()) / 2.0)
|
||||
var land_edge := PackedVector2Array()
|
||||
var water_edge := PackedVector2Array()
|
||||
for index: int in point_count:
|
||||
|
|
|
|||
|
|
@ -40,13 +40,16 @@ func _validate_palette() -> void:
|
|||
|
||||
|
||||
func _validate_protocol_bounds() -> void:
|
||||
assert(SurfaceDrawingProtocol.GRID_WIDTH == 16)
|
||||
assert(SurfaceDrawingProtocol.GRID_HEIGHT == 16)
|
||||
assert(SurfaceDrawingProtocol.GRID_SIZES == [16, 32, 64, 128])
|
||||
assert(is_equal_approx(SurfaceDrawingProtocol.CELL_SIZE, 0.075))
|
||||
var grid_width: int = SurfaceDrawingProtocol.GRID_WIDTH
|
||||
var grid_height: int = SurfaceDrawingProtocol.GRID_HEIGHT
|
||||
var grid_sizes: Variant = SurfaceDrawingProtocol.GRID_SIZES
|
||||
var cell_size: float = SurfaceDrawingProtocol.CELL_SIZE
|
||||
assert(grid_width == 16)
|
||||
assert(grid_height == 16)
|
||||
assert(grid_sizes == [16, 32, 64, 128])
|
||||
assert(is_equal_approx(cell_size, 0.075))
|
||||
assert(is_equal_approx(
|
||||
float(SurfaceDrawingProtocol.GRID_WIDTH)
|
||||
* SurfaceDrawingProtocol.CELL_SIZE,
|
||||
float(grid_width) * cell_size,
|
||||
1.2,
|
||||
))
|
||||
var canvas_request: Dictionary = {
|
||||
|
|
@ -157,14 +160,14 @@ func _validate_grid_snapping() -> void:
|
|||
"creator_fingerprint": FINGERPRINT_A,
|
||||
"cells": [],
|
||||
}
|
||||
var snapped: Dictionary = SurfaceDrawingPlacement.resolve(
|
||||
var snap_result: Dictionary = SurfaceDrawingPlacement.resolve(
|
||||
Vector3(1.08, 0.02, 0.08),
|
||||
Vector3.UP,
|
||||
Vector3.RIGHT,
|
||||
[anchor],
|
||||
)
|
||||
assert(bool(snapped["snapped"]))
|
||||
var snapped_origin: Vector3 = snapped["origin"]
|
||||
assert(bool(snap_result["snapped"]))
|
||||
var snapped_origin: Vector3 = snap_result["origin"]
|
||||
assert(snapped_origin.is_equal_approx(Vector3(1.2, 0.0, 0.0)))
|
||||
var unsnapped: Dictionary = SurfaceDrawingPlacement.resolve(
|
||||
Vector3(0.6, 0.0, 0.0),
|
||||
|
|
|
|||
|
|
@ -1878,7 +1878,7 @@ func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
|
|||
for index: int in generator._placements.size():
|
||||
var coordinate := Vector2i(
|
||||
index % generator.grid_size.x,
|
||||
index / generator.grid_size.x,
|
||||
int(float(index) / float(generator.grid_size.x)),
|
||||
)
|
||||
var current: TerrainChunkVariant = generator._placements[index]
|
||||
if coordinate.x > 0:
|
||||
|
|
@ -1918,8 +1918,8 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|||
"All walkable generated terrain must remain connected to spawn.",
|
||||
)
|
||||
var center := Vector2i(
|
||||
generator.grid_size.x / 2,
|
||||
generator.grid_size.y / 2,
|
||||
int(float(generator.grid_size.x) / 2.0),
|
||||
int(float(generator.grid_size.y) / 2.0),
|
||||
)
|
||||
var safe_spawn_neighbors := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -1943,7 +1943,7 @@ func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
|
|||
var placement := generator._placements[index]
|
||||
var coordinate := Vector2i(
|
||||
index % generator.grid_size.x,
|
||||
index / generator.grid_size.x,
|
||||
int(float(index) / float(generator.grid_size.x)),
|
||||
)
|
||||
if "coast" in placement.definition.tags:
|
||||
_check(
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ func _initialize() -> void:
|
|||
|
||||
|
||||
func _run() -> void:
|
||||
var root := Node.new()
|
||||
get_root().add_child(root)
|
||||
var scene_root := Node.new()
|
||||
get_root().add_child(scene_root)
|
||||
var bag := PlayerBag.new()
|
||||
var catches := FishInventory.new()
|
||||
var capacity := PlayerCoolerCapacity.new()
|
||||
var layout := PlayerInventoryLayout.new()
|
||||
var hotbar := PlayerHotbar.new()
|
||||
for node: Node in [bag, catches, capacity, layout, hotbar]:
|
||||
root.add_child(node)
|
||||
scene_root.add_child(node)
|
||||
bag.setup(ItemCatalogResource)
|
||||
layout.setup(bag, catches, ItemCatalogResource, capacity)
|
||||
bag.set_inventory_layout(layout)
|
||||
|
|
@ -109,7 +109,7 @@ func _run() -> void:
|
|||
assert(layout.get_storage_count() == 1)
|
||||
assert(layout.get_storage_capacity() == 9)
|
||||
var inventory_grid := GeneralInventoryGrid.new()
|
||||
root.add_child(inventory_grid)
|
||||
scene_root.add_child(inventory_grid)
|
||||
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
|
||||
inventory_grid.setup(
|
||||
layout,
|
||||
|
|
@ -143,7 +143,7 @@ func _run() -> void:
|
|||
)
|
||||
staged_slot.set_staged(false)
|
||||
var quality_slot := GeneralInventorySlot.new()
|
||||
root.add_child(quality_slot)
|
||||
scene_root.add_child(quality_slot)
|
||||
quality_slot.set_presentation_size(Vector2(78.0, 78.0))
|
||||
quality_slot.configure(
|
||||
0,
|
||||
|
|
@ -181,7 +181,7 @@ func _run() -> void:
|
|||
distinct_quality_colors[quality_color] = true
|
||||
assert(distinct_quality_colors.size() == FishQuality.TIER_COUNT)
|
||||
var sale_tray_slot := ShopSaleTraySlot.new()
|
||||
root.add_child(sale_tray_slot)
|
||||
scene_root.add_child(sale_tray_slot)
|
||||
sale_tray_slot.configure(
|
||||
"catch:%s" % fish_catch.catch_id,
|
||||
fish_catch.fish.display_texture,
|
||||
|
|
@ -207,7 +207,7 @@ func _run() -> void:
|
|||
expected_tray_quality_color.a = 0.96
|
||||
assert(sale_tray_style.bg_color == expected_tray_quality_color)
|
||||
var wallet := PlayerWallet.new()
|
||||
root.add_child(wallet)
|
||||
scene_root.add_child(wallet)
|
||||
assert(wallet.restore_balance(15000))
|
||||
assert(layout.get_next_backpack_cost() == 1500)
|
||||
assert(layout.purchase_backpack(wallet))
|
||||
|
|
@ -223,7 +223,7 @@ func _run() -> void:
|
|||
assert(inventory_grid.get_slots().size() == 36)
|
||||
assert(layout.get_next_backpack_cost() == -1)
|
||||
var storage_grid := GeneralInventoryGrid.new()
|
||||
root.add_child(storage_grid)
|
||||
scene_root.add_child(storage_grid)
|
||||
storage_grid.setup(
|
||||
layout,
|
||||
bag,
|
||||
|
|
@ -240,7 +240,7 @@ func _run() -> void:
|
|||
== "locked storage slot 10"
|
||||
)
|
||||
var storage_wallet := PlayerWallet.new()
|
||||
root.add_child(storage_wallet)
|
||||
scene_root.add_child(storage_wallet)
|
||||
assert(storage_wallet.restore_balance(9500))
|
||||
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
|
||||
assert(capacity.purchase(storage_wallet))
|
||||
|
|
@ -266,8 +266,8 @@ func _run() -> void:
|
|||
|
||||
var network_sale := NetworkSaleService.new()
|
||||
var session := NetworkSession.new()
|
||||
root.add_child(session)
|
||||
root.add_child(network_sale)
|
||||
scene_root.add_child(session)
|
||||
scene_root.add_child(network_sale)
|
||||
network_sale.set("_session", session)
|
||||
network_sale.set("_item_catalog", ItemCatalogResource)
|
||||
network_sale.set("_fish_catalog", FishCatalogResource)
|
||||
|
|
@ -284,5 +284,5 @@ func _run() -> void:
|
|||
assert((result.get("items", []) as Array).size() == 1)
|
||||
|
||||
print("Unified inventory validation: PASS")
|
||||
root.free()
|
||||
scene_root.free()
|
||||
quit()
|
||||
|
|
|
|||
|
|
@ -68,9 +68,9 @@ func _validate_setup_page() -> void:
|
|||
page.call("_set_world_layout", WorldLayout.STARTER_ISLAND)
|
||||
assert(not (page.get_node("%SeedSection") as Control).visible)
|
||||
var request: Dictionary = {}
|
||||
page.start_requested.connect(func(layout: StringName, seed: int) -> void:
|
||||
page.start_requested.connect(func(layout: StringName, world_seed: int) -> void:
|
||||
request["layout"] = layout
|
||||
request["seed"] = seed
|
||||
request["seed"] = world_seed
|
||||
)
|
||||
page.call("_request_start")
|
||||
assert(request.get("layout") == WorldLayout.STARTER_ISLAND)
|
||||
|
|
|
|||
|
|
@ -20,12 +20,21 @@ func _initialize() -> void:
|
|||
|
||||
|
||||
func _run() -> void:
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 12)
|
||||
assert(
|
||||
var protocol_version: int = NetworkProtocol.PROTOCOL_VERSION
|
||||
var world_spawn_capability: String = (
|
||||
NetworkWorldSpawnProtocol.CAPABILITY
|
||||
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||
)
|
||||
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
|
||||
var protocol_world_spawn_capability: String = (
|
||||
NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||
)
|
||||
var snapshot_entities_per_envelope: int = (
|
||||
NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
|
||||
)
|
||||
assert(protocol_version == 12)
|
||||
assert(
|
||||
world_spawn_capability == protocol_world_spawn_capability
|
||||
)
|
||||
assert(snapshot_entities_per_envelope <= 4)
|
||||
_validate_catalog_statuses()
|
||||
await _validate_billboard_presentation()
|
||||
_validate_envelopes()
|
||||
|
|
|
|||
|
|
@ -42,9 +42,15 @@ func _run() -> void:
|
|||
|
||||
|
||||
func _validate_clock_boundaries_and_duration() -> void:
|
||||
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
|
||||
var real_seconds_per_cycle: float = (
|
||||
WorldTimeServiceType.REAL_SECONDS_PER_CYCLE
|
||||
)
|
||||
var hours_per_real_second: float = (
|
||||
WorldTimeServiceType.HOURS_PER_REAL_SECOND
|
||||
)
|
||||
assert(real_seconds_per_cycle == 86400.0)
|
||||
assert(is_equal_approx(
|
||||
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
|
||||
hours_per_real_second,
|
||||
1.0 / 3600.0,
|
||||
))
|
||||
assert(is_equal_approx(
|
||||
|
|
|
|||
|
|
@ -382,11 +382,13 @@ func _validate_weather_presentation() -> void:
|
|||
"res://world/environment/local_storm_cloud.gdshader"
|
||||
)
|
||||
assert("cull_disabled" in cloud_shader.code)
|
||||
assert(LocalStormCloudLayer.WRAP_RADIUS >= 315.0)
|
||||
assert(is_equal_approx(LocalStormCloudLayer.MAXIMUM_OPACITY, 1.0))
|
||||
var cloud_wrap_radius: float = LocalStormCloudLayer.WRAP_RADIUS
|
||||
var cloud_maximum_opacity: float = LocalStormCloudLayer.MAXIMUM_OPACITY
|
||||
var cloud_distance_fade_end: float = LocalStormCloudLayer.DISTANCE_FADE_END
|
||||
assert(cloud_wrap_radius >= 315.0)
|
||||
assert(is_equal_approx(cloud_maximum_opacity, 1.0))
|
||||
assert(
|
||||
LocalStormCloudLayer.DISTANCE_FADE_END
|
||||
< LocalStormCloudLayer.WRAP_RADIUS
|
||||
cloud_distance_fade_end < cloud_wrap_radius
|
||||
)
|
||||
assert(is_zero_approx(float(storm_clouds.call("get_storm_amount"))))
|
||||
visuals.apply_weather_immediately(
|
||||
|
|
@ -524,10 +526,18 @@ func _validate_weather_presentation() -> void:
|
|||
assert(rain.emitting)
|
||||
assert(rain.amount_ratio > 0.99)
|
||||
assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
|
||||
var rain_emitter_offset: Vector3 = (
|
||||
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET
|
||||
)
|
||||
var rain_supported_canopy_height: float = (
|
||||
WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
|
||||
)
|
||||
var rain_canopy_clearance: float = (
|
||||
WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
|
||||
)
|
||||
assert(
|
||||
WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET.y
|
||||
>= WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
|
||||
+ WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
|
||||
rain_emitter_offset.y
|
||||
>= rain_supported_canopy_height + rain_canopy_clearance
|
||||
)
|
||||
assert(is_equal_approx(
|
||||
rain.lifetime,
|
||||
|
|
|
|||
|
|
@ -1463,8 +1463,8 @@ func is_mobile_mode() -> bool:
|
|||
return _mobile_mode
|
||||
|
||||
|
||||
func set_hud_hidden(hidden: bool) -> void:
|
||||
_hud_hidden = hidden
|
||||
func set_hud_hidden(is_hidden: bool) -> void:
|
||||
_hud_hidden = is_hidden
|
||||
_refresh_visibility()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,9 @@ static func make_icon_texture(
|
|||
Image.FORMAT_RGBA8,
|
||||
)
|
||||
canvas.fill(Color.TRANSPARENT)
|
||||
var inset := Vector2i.ONE * ((canvas_size - icon_size) / 2)
|
||||
var inset := Vector2i.ONE * floori(
|
||||
float(canvas_size - icon_size) / 2.0
|
||||
)
|
||||
canvas.blend_rect(
|
||||
image,
|
||||
Rect2i(Vector2i.ZERO, Vector2i(icon_size, icon_size)),
|
||||
|
|
|
|||
|
|
@ -93,10 +93,16 @@ static func uniform_footprint_size(
|
|||
maximum_size.x / visible_size.x,
|
||||
maximum_size.y / visible_size.y,
|
||||
)
|
||||
var scale: float = minf(area_scale, fit_scale)
|
||||
var footprint_scale: float = minf(area_scale, fit_scale)
|
||||
return Vector2(
|
||||
minf(maximum_size.x, maxf(1.0, roundf(visible_size.x * scale))),
|
||||
minf(maximum_size.y, maxf(1.0, roundf(visible_size.y * scale))),
|
||||
minf(
|
||||
maximum_size.x,
|
||||
maxf(1.0, roundf(visible_size.x * footprint_scale)),
|
||||
),
|
||||
minf(
|
||||
maximum_size.y,
|
||||
maxf(1.0, roundf(visible_size.y * footprint_scale)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -91,14 +91,14 @@ func _initialize_motion() -> void:
|
|||
_set_visual_y(_target_y())
|
||||
|
||||
|
||||
func _on_toggled(is_pressed: bool) -> void:
|
||||
if _selected and not is_pressed:
|
||||
func _on_toggled(is_button_pressed: bool) -> void:
|
||||
if _selected and not is_button_pressed:
|
||||
# A selected organizer tab is a page marker, not a collapsible toggle.
|
||||
# Restore without another signal before any lower-state frame is drawn.
|
||||
set_pressed_no_signal(true)
|
||||
refresh_state(false)
|
||||
return
|
||||
_selected = is_pressed
|
||||
_selected = is_button_pressed
|
||||
refresh_state()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -65,14 +65,14 @@ func _apply_style() -> void:
|
|||
|
||||
func configure(
|
||||
key: String,
|
||||
icon: Texture2D,
|
||||
icon_texture: Texture2D,
|
||||
label: String,
|
||||
quantity: int = 1,
|
||||
quality: int = -1,
|
||||
) -> void:
|
||||
entry_key = key
|
||||
_quality_tier = quality
|
||||
_icon.texture = icon
|
||||
_icon.texture = icon_texture
|
||||
_quantity.text = "×%d" % quantity if quantity > 1 else ""
|
||||
tooltip_text = "%s · select to remove" % label
|
||||
accessibility_name = tooltip_text
|
||||
|
|
|
|||
|
|
@ -565,7 +565,7 @@ func _popup_item_height(popup: PopupMenu, index: int) -> float:
|
|||
popup.get_theme_font_size(&"font_size")
|
||||
)
|
||||
var icon: Texture2D = popup.get_item_icon(index)
|
||||
var icon_height: float = icon.get_height() if icon != null else 0.0
|
||||
var icon_height: float = float(icon.get_height()) if icon != null else 0.0
|
||||
return maxf(font_height, icon_height) + vertical_separation
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ const CURRENCY_ICON: Texture2D = preload(
|
|||
|
||||
signal menu_visibility_changed(is_open: bool)
|
||||
signal menu_exit_started
|
||||
@warning_ignore("unused_signal")
|
||||
signal sell_fish_requested
|
||||
@warning_ignore("unused_signal")
|
||||
signal shop_cooler_return_requested
|
||||
signal shop_cooler_confirmation_cancel_requested
|
||||
|
||||
|
|
@ -395,9 +397,11 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
|||
_controller_zone = ControllerZone.CONTENT
|
||||
if section_index == int(_shop_section):
|
||||
if section_index != ShopSection.SELL_FISH:
|
||||
var showing_upgrades := section_index == ShopSection.UPGRADES
|
||||
_upgrades_content.visible = showing_upgrades
|
||||
_supplies_content.visible = not showing_upgrades
|
||||
var showing_current_upgrades: bool = (
|
||||
section_index == ShopSection.UPGRADES
|
||||
)
|
||||
_upgrades_content.visible = showing_current_upgrades
|
||||
_supplies_content.visible = not showing_current_upgrades
|
||||
_update_shop_tab_selection()
|
||||
if focus_content:
|
||||
_focus_shop_section()
|
||||
|
|
@ -470,12 +474,12 @@ func _apply_shop_controller_zone_focus_modes() -> void:
|
|||
|
||||
func _set_descendant_button_focus_mode(
|
||||
root_control: Control,
|
||||
focus_mode: Control.FocusMode,
|
||||
requested_focus_mode: Control.FocusMode,
|
||||
) -> void:
|
||||
for child: Node in root_control.find_children("*", "BaseButton", true, false):
|
||||
var button := child as BaseButton
|
||||
if button != null:
|
||||
button.focus_mode = focus_mode
|
||||
button.focus_mode = requested_focus_mode
|
||||
|
||||
|
||||
func _update_shop_tab_selection() -> void:
|
||||
|
|
@ -704,14 +708,14 @@ func deactivate_shop_cooler_page() -> void:
|
|||
|
||||
|
||||
func _set_shop_panel_background_pointer_blocking(blocking: bool) -> void:
|
||||
var mouse_filter := (
|
||||
var panel_mouse_filter := (
|
||||
Control.MOUSE_FILTER_STOP
|
||||
if blocking
|
||||
else Control.MOUSE_FILTER_IGNORE
|
||||
)
|
||||
_shop_panel.mouse_filter = mouse_filter
|
||||
_shop_panel_margin.mouse_filter = mouse_filter
|
||||
_shop_panel_layout.mouse_filter = mouse_filter
|
||||
_shop_panel.mouse_filter = panel_mouse_filter
|
||||
_shop_panel_margin.mouse_filter = panel_mouse_filter
|
||||
_shop_panel_layout.mouse_filter = panel_mouse_filter
|
||||
|
||||
|
||||
func set_shop_cooler_modal_open(is_open: bool) -> void:
|
||||
|
|
|
|||
|
|
@ -204,6 +204,7 @@ var _hotbar: PlayerHotbarType
|
|||
var _item_catalog: ItemCatalogType
|
||||
var _player_menu_open: bool = false
|
||||
var _gameplay_ui_enabled: bool = false
|
||||
var _gameplay_input_locked: bool = false
|
||||
var _gameplay_hud_hidden: bool = false
|
||||
var _fishing_spot: FishingSpotType
|
||||
var _system_menu_open: bool = false
|
||||
|
|
@ -631,6 +632,9 @@ func _prioritize_surface_drawing_pointer_input() -> void:
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _gameplay_input_locked:
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _social_prompt_open and event.is_action_pressed("ui_cancel"):
|
||||
_decline_social_prompt()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
|
@ -681,6 +685,7 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
var can_open: bool = (
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
@ -719,6 +724,7 @@ func _input(event: InputEvent) -> void:
|
|||
func _reconcile_radial_menu_input() -> void:
|
||||
var blocked: bool = (
|
||||
not _gameplay_ui_enabled
|
||||
or _gameplay_input_locked
|
||||
or _system_menu_open
|
||||
or _player_menu_open
|
||||
or _shop_open
|
||||
|
|
@ -769,6 +775,7 @@ func _close_quick_radial_menu() -> void:
|
|||
func _can_use_character_call() -> bool:
|
||||
return (
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
@ -840,6 +847,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
|||
button_event == null
|
||||
or not button_event.pressed
|
||||
or not _gameplay_ui_enabled
|
||||
or _gameplay_input_locked
|
||||
or _system_menu_open
|
||||
or _player_menu_open
|
||||
or _shop_open
|
||||
|
|
@ -1051,6 +1059,7 @@ func _mapped_virtual_mouse_stick() -> Vector2:
|
|||
func _can_start_virtual_mouse() -> bool:
|
||||
return (
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _showcase_active
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
|
|
@ -1430,6 +1439,7 @@ func _can_surface_drawing_be_active() -> bool:
|
|||
return (
|
||||
_surface_drawing_hotbar_selected
|
||||
and _gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
@ -1838,12 +1848,38 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
|||
_hotbar_ui.set_gameplay_input_enabled(false)
|
||||
else:
|
||||
_refresh_hotbar_visibility()
|
||||
_hotbar_ui.set_gameplay_input_enabled(true)
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
not _gameplay_input_locked
|
||||
)
|
||||
_refresh_fishing_panel_visibility()
|
||||
call_deferred("_start_next_experience_animation")
|
||||
_refresh_surface_drawing_activation()
|
||||
|
||||
|
||||
func set_gameplay_input_locked(locked: bool) -> void:
|
||||
if _gameplay_input_locked == locked:
|
||||
return
|
||||
_gameplay_input_locked = locked
|
||||
if locked:
|
||||
_end_virtual_mouse()
|
||||
_close_radial_menus()
|
||||
_hotbar_ui.set_drag_enabled(false)
|
||||
if _surface_drawing != null:
|
||||
_surface_drawing.deactivate()
|
||||
_refresh_rv_storage_button_visibility()
|
||||
_refresh_hotbar_visibility()
|
||||
_refresh_chat_availability()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
and not _storage_open
|
||||
)
|
||||
_refresh_surface_drawing_activation()
|
||||
|
||||
|
||||
func set_gameplay_hud_hidden(hidden: bool) -> void:
|
||||
_gameplay_hud_hidden = hidden
|
||||
if _quick_radial_menu != null:
|
||||
|
|
@ -1862,6 +1898,7 @@ func is_gameplay_hud_hidden() -> bool:
|
|||
func _can_toggle_gameplay_hud() -> bool:
|
||||
return (
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
@ -1903,6 +1940,7 @@ func set_system_menu_open(is_open: bool) -> void:
|
|||
_refresh_hotbar_visibility()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not is_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
@ -1995,6 +2033,7 @@ func _refresh_rv_storage_button_visibility() -> void:
|
|||
_rv_storage_button.visible = (
|
||||
_rv_storage_button_requested_visible
|
||||
and _gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _gameplay_hud_hidden
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
|
|
@ -2799,7 +2838,9 @@ func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
|||
_hotbar_ui.set_player_menu_context(false)
|
||||
_refresh_chat_availability()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled and not is_open
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not is_open
|
||||
)
|
||||
if not is_open:
|
||||
_hotbar_ui.set_drag_enabled(false)
|
||||
|
|
@ -2844,6 +2885,7 @@ func _on_shop_visibility_changed(is_open: bool) -> void:
|
|||
_refresh_hotbar_visibility()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not is_open
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
|
|
@ -2865,6 +2907,7 @@ func _on_storage_visibility_changed(is_open: bool) -> void:
|
|||
_refresh_hotbar_visibility()
|
||||
_hotbar_ui.set_gameplay_input_enabled(
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not is_open
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
|
|
@ -2977,6 +3020,7 @@ func _on_hotbar_presentation_transition_finished(
|
|||
) -> void:
|
||||
_hotbar_ui.set_drag_enabled(
|
||||
presentation_visible
|
||||
and not _gameplay_input_locked
|
||||
and _player_menu_open
|
||||
and _player_menu_hotbar_visible
|
||||
and not _system_menu_open
|
||||
|
|
@ -2986,6 +3030,7 @@ func _on_hotbar_presentation_transition_finished(
|
|||
func _refresh_hotbar_visibility() -> void:
|
||||
_hotbar_ui.set_presentation_visible(
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and (
|
||||
not _gameplay_hud_hidden
|
||||
or (_player_menu_open and _player_menu_hotbar_visible)
|
||||
|
|
@ -3027,6 +3072,7 @@ func _refresh_chat_availability() -> void:
|
|||
return
|
||||
var chat_available := (
|
||||
_gameplay_ui_enabled
|
||||
and not _gameplay_input_locked
|
||||
and not _system_menu_open
|
||||
and not _player_menu_open
|
||||
and not _shop_open
|
||||
|
|
|
|||
|
|
@ -100,7 +100,11 @@ func popup_file_dialog(dialog: FileDialog) -> void:
|
|||
# clamp guarantees the embedded window cannot escape the physical screen.
|
||||
dialog.size = available_size
|
||||
if dialog.get_viewport().gui_embed_subwindows:
|
||||
dialog.position = (host_size - available_size) / 2
|
||||
var centered_position := Vector2i(
|
||||
floori(float(host_size.x - available_size.x) / 2.0),
|
||||
floori(float(host_size.y - available_size.y) / 2.0),
|
||||
)
|
||||
dialog.position = centered_position
|
||||
_finalize_compact_file_dialog.call_deferred(
|
||||
dialog, host_size, available_size
|
||||
)
|
||||
|
|
|
|||
|
|
@ -329,9 +329,9 @@ func _configure_compose_controller_navigation() -> void:
|
|||
and _attachment_amount.is_visible_in_tree()
|
||||
)
|
||||
var amount_left: Control = _amount_minus if amount_visible else _compose_cancel
|
||||
var amount_middle: Control = (
|
||||
_attachment_amount if amount_visible else _compose_cancel
|
||||
)
|
||||
var amount_middle: Control = _compose_cancel
|
||||
if amount_visible:
|
||||
amount_middle = _attachment_amount
|
||||
var amount_right: Control = _amount_plus if amount_visible else _send_button
|
||||
_set_compose_neighbors(
|
||||
_greeting, _greeting, _recipient, _greeting, _body
|
||||
|
|
|
|||
|
|
@ -394,7 +394,10 @@ func _set_mode(mode: Mode, clear_connection_error: bool = true) -> void:
|
|||
_discovery_refresh_timer.stop()
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
_defer_focus_control(_address if mode == Mode.DIRECT else _server_list)
|
||||
var initial_focus: Control = _server_list
|
||||
if mode == Mode.DIRECT:
|
||||
initial_focus = _address
|
||||
_defer_focus_control(initial_focus)
|
||||
|
||||
|
||||
func _default_mode_status(mode: Mode) -> String:
|
||||
|
|
@ -688,9 +691,10 @@ func _restore_entry_selection_and_focus(
|
|||
_selected_entry = null
|
||||
_server_list.deselect_all()
|
||||
_refresh()
|
||||
_defer_focus_control(
|
||||
_server_list if selected else _current_mode_button()
|
||||
)
|
||||
var restored_focus: Control = _current_mode_button()
|
||||
if selected:
|
||||
restored_focus = _server_list
|
||||
_defer_focus_control(restored_focus)
|
||||
|
||||
|
||||
func _current_mode_button() -> Button:
|
||||
|
|
@ -1203,10 +1207,10 @@ func _recover_controller_focus(
|
|||
) -> void:
|
||||
if not is_visible_in_tree():
|
||||
return
|
||||
var owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if owner != null and owner in controls:
|
||||
var focus_owner: Control = get_viewport().gui_get_focus_owner()
|
||||
if focus_owner != null and focus_owner in controls:
|
||||
return
|
||||
if owner != null and _delete_confirmation.is_ancestor_of(owner):
|
||||
if focus_owner != null and _delete_confirmation.is_ancestor_of(focus_owner):
|
||||
return
|
||||
if fallback != null:
|
||||
_defer_focus_control(fallback)
|
||||
|
|
@ -1539,7 +1543,9 @@ func _on_back_pressed() -> void:
|
|||
if _name_entry_active:
|
||||
_clear_edit_state()
|
||||
_refresh()
|
||||
var content: Control = _address if _mode == Mode.DIRECT else _server_list
|
||||
var content: Control = _server_list
|
||||
if _mode == Mode.DIRECT:
|
||||
content = _address
|
||||
_defer_focus_control(content)
|
||||
return
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -167,8 +167,8 @@ func _on_seed_submitted(_text: String) -> void:
|
|||
|
||||
|
||||
func _request_start() -> void:
|
||||
var seed: int = get_selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
|
||||
var world_seed: int = get_selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and world_seed == 0:
|
||||
_status.text = (
|
||||
"enter some text or a whole number from 1 to %d."
|
||||
% SaveManagerType.MAX_WORLD_SEED
|
||||
|
|
@ -176,10 +176,10 @@ func _request_start() -> void:
|
|||
_seed_edit.grab_focus()
|
||||
_seed_edit.select_all()
|
||||
return
|
||||
if seed == 0:
|
||||
seed = SaveManagerType.roll_world_seed()
|
||||
if world_seed == 0:
|
||||
world_seed = SaveManagerType.roll_world_seed()
|
||||
_start_button.disabled = true
|
||||
start_requested.emit(_world_layout, seed)
|
||||
start_requested.emit(_world_layout, world_seed)
|
||||
|
||||
|
||||
func _refresh_presentation() -> void:
|
||||
|
|
@ -226,33 +226,43 @@ func _refresh_start_state() -> void:
|
|||
func _configure_controller_focus() -> void:
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
|
||||
var generated_button_bottom: Control = _start_button
|
||||
var starter_button_bottom: Control = _back_button
|
||||
var random_seed_button_bottom: Control = _start_button
|
||||
var custom_seed_button_bottom: Control = _back_button
|
||||
if generated:
|
||||
generated_button_bottom = _random_seed_button
|
||||
starter_button_bottom = _custom_seed_button
|
||||
if custom:
|
||||
random_seed_button_bottom = _seed_edit
|
||||
custom_seed_button_bottom = _seed_edit
|
||||
_set_neighbors(
|
||||
_generated_button,
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
_generated_button,
|
||||
_random_seed_button if generated else _start_button,
|
||||
generated_button_bottom,
|
||||
)
|
||||
_set_neighbors(
|
||||
_starter_button,
|
||||
_generated_button,
|
||||
_starter_button,
|
||||
_starter_button,
|
||||
_custom_seed_button if generated else _back_button,
|
||||
starter_button_bottom,
|
||||
)
|
||||
_set_neighbors(
|
||||
_random_seed_button,
|
||||
_random_seed_button,
|
||||
_custom_seed_button,
|
||||
_generated_button,
|
||||
_seed_edit if custom else _start_button,
|
||||
random_seed_button_bottom,
|
||||
)
|
||||
_set_neighbors(
|
||||
_custom_seed_button,
|
||||
_random_seed_button,
|
||||
_custom_seed_button,
|
||||
_starter_button,
|
||||
_seed_edit if custom else _back_button,
|
||||
custom_seed_button_bottom,
|
||||
)
|
||||
_set_neighbors(
|
||||
_seed_edit,
|
||||
|
|
@ -261,13 +271,11 @@ func _configure_controller_focus() -> void:
|
|||
_custom_seed_button,
|
||||
_start_button,
|
||||
)
|
||||
var action_top: Control = (
|
||||
_seed_edit
|
||||
if custom
|
||||
else _random_seed_button
|
||||
if generated
|
||||
else _generated_button
|
||||
)
|
||||
var action_top: Control = _generated_button
|
||||
if generated:
|
||||
action_top = _random_seed_button
|
||||
if custom:
|
||||
action_top = _seed_edit
|
||||
_set_neighbors(
|
||||
_start_button,
|
||||
_start_button,
|
||||
|
|
|
|||
|
|
@ -710,7 +710,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
if not _is_inventory_section(_current_section):
|
||||
return _handle_active_page_controller_input(event)
|
||||
var button_event := event as InputEventJoypadButton
|
||||
var accept_event: bool = (
|
||||
var accept_button_event: bool = (
|
||||
button_event != null
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
|
|
@ -719,7 +719,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
)
|
||||
)
|
||||
var accept_pressed: bool = (
|
||||
accept_event
|
||||
accept_button_event
|
||||
and button_event.pressed
|
||||
)
|
||||
var cancel_pressed: bool = (
|
||||
|
|
@ -777,7 +777,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
if accept_pressed:
|
||||
_confirm_controller_storage_placement()
|
||||
return true
|
||||
if accept_event:
|
||||
if accept_button_event:
|
||||
return true
|
||||
if (
|
||||
event.is_action_pressed("ui_down")
|
||||
|
|
@ -810,7 +810,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
):
|
||||
_cancel_active_item_move()
|
||||
return _try_enter_notepad_controller_ownership()
|
||||
if _current_section == Section.TACKLE_BOX and accept_event:
|
||||
if _current_section == Section.TACKLE_BOX and accept_button_event:
|
||||
# Bait and lures are unlock collections, not movable inventory slots.
|
||||
# Controller Y owns their contextual equip/notepad action; A is
|
||||
# intentionally inert and consumed so it cannot activate the Button.
|
||||
|
|
@ -831,7 +831,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
|||
if _current_section == Section.COOLER:
|
||||
_enter_inventory_sort_zone()
|
||||
return true
|
||||
if accept_event:
|
||||
if accept_button_event:
|
||||
if button_event.pressed:
|
||||
_controller_accept_held = true
|
||||
_controller_accept_hold_elapsed = 0.0
|
||||
|
|
@ -986,11 +986,14 @@ func _controller_focus_is_on_last_inventory_row() -> bool:
|
|||
var slot := focus_owner as GeneralInventorySlotType
|
||||
if slot == null:
|
||||
return false
|
||||
return (
|
||||
slot.slot_index / BAG_STORAGE_COLUMNS
|
||||
>= (_inventory_layout.get_inventory_capacity() - 1)
|
||||
/ BAG_STORAGE_COLUMNS
|
||||
var focused_row: int = int(
|
||||
float(slot.slot_index) / float(BAG_STORAGE_COLUMNS)
|
||||
)
|
||||
var final_row: int = int(
|
||||
float(_inventory_layout.get_inventory_capacity() - 1)
|
||||
/ float(BAG_STORAGE_COLUMNS)
|
||||
)
|
||||
return focused_row >= final_row
|
||||
return false
|
||||
|
||||
|
||||
|
|
@ -1139,10 +1142,13 @@ func _controller_storage_focus_is_on_last_row() -> bool:
|
|||
var slot := get_viewport().gui_get_focus_owner() as BagStorageSlotType
|
||||
if slot == null:
|
||||
return false
|
||||
return (
|
||||
slot.storage_slot_index / BAG_STORAGE_COLUMNS
|
||||
>= (_bag_slot_nodes.size() - 1) / BAG_STORAGE_COLUMNS
|
||||
var focused_row: int = int(
|
||||
float(slot.storage_slot_index) / float(BAG_STORAGE_COLUMNS)
|
||||
)
|
||||
var final_row: int = int(
|
||||
float(_bag_slot_nodes.size() - 1) / float(BAG_STORAGE_COLUMNS)
|
||||
)
|
||||
return focused_row >= final_row
|
||||
|
||||
|
||||
func _find_controller_hotbar_assignment(
|
||||
|
|
@ -4575,11 +4581,6 @@ func _refresh_economy_summary() -> void:
|
|||
if not is_node_ready():
|
||||
return
|
||||
var balance: int = _wallet.get_balance() if _wallet != null else 0
|
||||
var held_total: int = (
|
||||
_inventory.get_total_sale_value()
|
||||
if _inventory != null
|
||||
else 0
|
||||
)
|
||||
_notepad_wallet_value.set_amount(balance)
|
||||
_notepad_capacity_value.text = "%d / %d" % [
|
||||
_inventory.get_all_catches().size() if _inventory != null else 0,
|
||||
|
|
|
|||
|
|
@ -1501,10 +1501,10 @@ func _resize_feature_preview_image(image: Image, max_dimension: int) -> void:
|
|||
var largest_dimension := maxi(source_size.x, source_size.y)
|
||||
if largest_dimension <= max_dimension:
|
||||
return
|
||||
var scale := float(max_dimension) / float(largest_dimension)
|
||||
var resize_scale := float(max_dimension) / float(largest_dimension)
|
||||
image.resize(
|
||||
maxi(roundi(float(source_size.x) * scale), 1),
|
||||
maxi(roundi(float(source_size.y) * scale), 1),
|
||||
maxi(roundi(float(source_size.x) * resize_scale), 1),
|
||||
maxi(roundi(float(source_size.y) * resize_scale), 1),
|
||||
Image.INTERPOLATE_NEAREST,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ const SECTOR_COUNT: int = 3
|
|||
var _hud_hidden: bool = false
|
||||
|
||||
|
||||
func set_hud_hidden(hidden: bool) -> void:
|
||||
_hud_hidden = hidden
|
||||
func set_hud_hidden(is_hidden: bool) -> void:
|
||||
_hud_hidden = is_hidden
|
||||
_refresh_labels()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -509,21 +509,21 @@ func _export_file_selected(path: String) -> void:
|
|||
func _request_create() -> void:
|
||||
if _create_button.disabled:
|
||||
return
|
||||
var seed: int = _selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
|
||||
var world_seed: int = _selected_world_seed()
|
||||
if _world_layout == WorldLayoutType.GENERATED and world_seed == 0:
|
||||
_status.text = "enter text or a whole number from 1 to %d." % SaveManagerType.MAX_WORLD_SEED
|
||||
_seed_edit.grab_focus()
|
||||
_seed_edit.select_all()
|
||||
return
|
||||
if seed == 0:
|
||||
seed = SaveManagerType.roll_world_seed()
|
||||
if world_seed == 0:
|
||||
world_seed = SaveManagerType.roll_world_seed()
|
||||
_busy = true
|
||||
_status.text = "creating save slot..."
|
||||
_refresh_action_state()
|
||||
create_requested.emit(
|
||||
PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text),
|
||||
_world_layout,
|
||||
seed,
|
||||
world_seed,
|
||||
_duplicate_source_slot_id,
|
||||
)
|
||||
|
||||
|
|
@ -656,13 +656,27 @@ func _configure_controller_focus() -> void:
|
|||
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(_new_slot_name)
|
||||
var generated: bool = _world_layout == WorldLayoutType.GENERATED
|
||||
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
|
||||
var generated_button_bottom: Control = _create_button
|
||||
var starter_button_bottom: Control = _back_button
|
||||
var random_seed_button_bottom: Control = _create_button
|
||||
var custom_seed_button_bottom: Control = _back_button
|
||||
if generated:
|
||||
generated_button_bottom = _random_seed_button
|
||||
starter_button_bottom = _custom_seed_button
|
||||
if custom:
|
||||
random_seed_button_bottom = _seed_edit
|
||||
custom_seed_button_bottom = _seed_edit
|
||||
_set_neighbors(_new_slot_name, _new_slot_name, _new_slot_name, _new_slot_tab, _generated_button)
|
||||
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, _random_seed_button if generated else _create_button)
|
||||
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, _custom_seed_button if generated else _back_button)
|
||||
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, _seed_edit if custom else _create_button)
|
||||
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, _seed_edit if custom else _back_button)
|
||||
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, generated_button_bottom)
|
||||
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, starter_button_bottom)
|
||||
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, random_seed_button_bottom)
|
||||
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, custom_seed_button_bottom)
|
||||
_set_neighbors(_seed_edit, _seed_edit, _seed_edit, _custom_seed_button, _create_button)
|
||||
var action_top: Control = _seed_edit if custom else _random_seed_button if generated else _generated_button
|
||||
var action_top: Control = _generated_button
|
||||
if generated:
|
||||
action_top = _random_seed_button
|
||||
if custom:
|
||||
action_top = _seed_edit
|
||||
_set_neighbors(_create_button, _create_button, _back_button, action_top, _create_button)
|
||||
_set_neighbors(_back_button, _create_button, _back_button, action_top, _back_button)
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ const INTRO_MENU_REVEAL_DURATION: float = 0.30
|
|||
const INTRO_LOGO_CENTER_Y_RATIO: float = 0.43
|
||||
|
||||
signal new_game_requested(world_layout: StringName, world_seed: int)
|
||||
@warning_ignore("unused_signal")
|
||||
signal continue_game_requested
|
||||
signal slot_play_requested(slot_id: String)
|
||||
signal new_slot_requested(
|
||||
|
|
@ -89,7 +90,6 @@ enum ConfirmationAction {
|
|||
@onready var _title_presentation_scale_root: Control = (
|
||||
%TitlePresentationScaleRoot
|
||||
)
|
||||
@onready var _background: ColorRect = %Background
|
||||
@onready var _title_logo: TextureRect = %TitleLogo
|
||||
@onready var _playtest_label: Label = %PlaytestLabel
|
||||
@onready var _presentation_center: CenterContainer = %Center
|
||||
|
|
@ -1081,7 +1081,6 @@ func _on_confirmation_accepted() -> void:
|
|||
or _confirmation_action == ConfirmationAction.NONE
|
||||
):
|
||||
return
|
||||
var action: ConfirmationAction = _confirmation_action
|
||||
_confirmation_page.lock_interaction()
|
||||
_action_in_progress = true
|
||||
if not _save_manager.delete_progression_save():
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# Generated terrain initialization is runtime-only, not editor-time tooling.
|
||||
@warning_ignore("missing_tool")
|
||||
class_name GeneratedWorldRegion
|
||||
extends WorldRegion
|
||||
|
||||
|
|
@ -18,6 +20,9 @@ const RVUpgradeInteractionType = preload(
|
|||
const PrecipitationOcclusionType = preload(
|
||||
"res://world/environment/precipitation_occlusion.gd"
|
||||
)
|
||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||
"res://world/materials/foliage_wind.gdshader"
|
||||
)
|
||||
const WATER_BODY_SCENE: PackedScene = preload("res://world/water_body.tscn")
|
||||
const SALT_WATER_MATERIAL: Material = preload(
|
||||
"res://world/materials/stylized_water.tres"
|
||||
|
|
@ -74,10 +79,27 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
|||
&"grass_detail",
|
||||
&"sand_tree",
|
||||
]
|
||||
const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [
|
||||
&"leaf",
|
||||
&"leaf_light",
|
||||
&"leaf_dark",
|
||||
&"pine",
|
||||
&"leaf_variant_light",
|
||||
&"leaf_variant_mid",
|
||||
&"leaf_variant_dark",
|
||||
]
|
||||
# Retain the exact generated material override while foliage wind is active so
|
||||
# profile changes restore both the selected palette and authored surface state.
|
||||
const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = (
|
||||
&"foliage_wind_source_materials"
|
||||
)
|
||||
|
||||
@export var initial_seed := PlayerSaveManager.DEFAULT_WORLD_SEED
|
||||
@export var prop_catalog: TerrainPropCatalog
|
||||
@export var biome_catalog: TerrainBiomeCatalog
|
||||
@export_group("Foliage Wind")
|
||||
@export_range(0.0, 0.4, 0.005) var foliage_wind_strength: float = 0.36
|
||||
@export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9
|
||||
|
||||
@onready var _generator: TerrainChunkGenerator = %TerrainChunkGenerator
|
||||
@onready var _player_spawn: Marker3D = %PlayerSpawn
|
||||
|
|
@ -114,6 +136,7 @@ var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
|||
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
||||
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
|
||||
var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {}
|
||||
var _foliage_wind_enabled: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -122,18 +145,19 @@ func _ready() -> void:
|
|||
_validate_biome_catalog()
|
||||
_configure_static_water()
|
||||
_build_shoreline_reference()
|
||||
_set_foliage_wind_enabled(not _light_performance_profile)
|
||||
|
||||
|
||||
func generate_world(seed: int) -> bool:
|
||||
if seed <= 0 or seed > PlayerSaveManager.MAX_WORLD_SEED:
|
||||
func generate_world(world_seed: int) -> bool:
|
||||
if world_seed <= 0 or world_seed > PlayerSaveManager.MAX_WORLD_SEED:
|
||||
return false
|
||||
if (
|
||||
seed == _current_seed
|
||||
world_seed == _current_seed
|
||||
and _generator.get_generated_chunks_root() != null
|
||||
):
|
||||
return true
|
||||
_current_seed = seed
|
||||
_generator.generation_seed = seed
|
||||
_current_seed = world_seed
|
||||
_generator.generation_seed = world_seed
|
||||
return _generator.generate()
|
||||
|
||||
|
||||
|
|
@ -191,6 +215,7 @@ func get_biome_at(coordinate: Vector2i) -> StringName:
|
|||
|
||||
func set_light_performance_profile(enabled: bool) -> void:
|
||||
_light_performance_profile = enabled
|
||||
_set_foliage_wind_enabled(not enabled)
|
||||
_apply_water_materials()
|
||||
|
||||
|
||||
|
|
@ -224,8 +249,8 @@ func get_spawn_surface_triangles(
|
|||
return triangles
|
||||
|
||||
|
||||
func is_home_exterior_position_accessible(position: Vector3) -> bool:
|
||||
var local_position := _generator.to_local(position)
|
||||
func is_home_exterior_position_accessible(world_position: Vector3) -> bool:
|
||||
var local_position := _generator.to_local(world_position)
|
||||
if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE:
|
||||
return true
|
||||
var coordinate := _generator.coordinate_for_local_position(local_position)
|
||||
|
|
@ -310,12 +335,12 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
group_counts[group_key] = 0
|
||||
_placed_group_coordinates[group_key] = []
|
||||
for record: Dictionary in records:
|
||||
var position: Vector3 = record.get("position", Vector3.ZERO)
|
||||
var record_position: Vector3 = record.get("position", Vector3.ZERO)
|
||||
var tags: PackedStringArray = record.get(
|
||||
"tags", PackedStringArray()
|
||||
)
|
||||
if "spawn" in tags:
|
||||
spawn_position = position
|
||||
spawn_position = record_position
|
||||
continue
|
||||
var coordinate: Vector2i = record.get(
|
||||
"coordinate",
|
||||
|
|
@ -418,6 +443,7 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
# generated world is assembled. Mark the final decoration tree once every
|
||||
# prop and material variant is in place so rain canopy occlusion persists.
|
||||
_mark_precipitation_occluders()
|
||||
_set_foliage_wind_enabled(_foliage_wind_enabled)
|
||||
world_generated.emit(_current_seed, summary)
|
||||
|
||||
|
||||
|
|
@ -520,6 +546,127 @@ func _mark_precipitation_occluders() -> void:
|
|||
PrecipitationOcclusionType.mark_tree_meshes(prop)
|
||||
|
||||
|
||||
func _set_foliage_wind_enabled(enabled: bool) -> void:
|
||||
_foliage_wind_enabled = enabled
|
||||
for prop: Node in _decorations.get_children():
|
||||
_apply_foliage_wind_to_node(prop, enabled)
|
||||
|
||||
|
||||
func _apply_foliage_wind_to_node(root_node: Node, enabled: bool) -> void:
|
||||
var mesh_instance := root_node as MeshInstance3D
|
||||
if mesh_instance != null:
|
||||
if enabled:
|
||||
_apply_foliage_wind_to_mesh(mesh_instance)
|
||||
else:
|
||||
_remove_foliage_wind_from_mesh(mesh_instance)
|
||||
for child: Node in root_node.get_children():
|
||||
_apply_foliage_wind_to_node(child, enabled)
|
||||
|
||||
|
||||
func _remove_foliage_wind_from_mesh(mesh_instance: MeshInstance3D) -> void:
|
||||
if mesh_instance.mesh == null:
|
||||
return
|
||||
var source_materials: Dictionary = mesh_instance.get_meta(
|
||||
FOLIAGE_WIND_SOURCE_MATERIALS_META,
|
||||
{},
|
||||
)
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var override_material: Material = (
|
||||
mesh_instance.get_surface_override_material(surface_index)
|
||||
)
|
||||
var shader_material := override_material as ShaderMaterial
|
||||
if (
|
||||
shader_material != null
|
||||
and shader_material.shader == FOLIAGE_WIND_SHADER
|
||||
):
|
||||
var source_material: Material = source_materials.get(
|
||||
surface_index,
|
||||
null,
|
||||
) as Material
|
||||
mesh_instance.set_surface_override_material(
|
||||
surface_index,
|
||||
source_material,
|
||||
)
|
||||
source_materials.erase(surface_index)
|
||||
if source_materials.is_empty():
|
||||
mesh_instance.remove_meta(FOLIAGE_WIND_SOURCE_MATERIALS_META)
|
||||
else:
|
||||
mesh_instance.set_meta(
|
||||
FOLIAGE_WIND_SOURCE_MATERIALS_META,
|
||||
source_materials,
|
||||
)
|
||||
|
||||
|
||||
func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void:
|
||||
if mesh_instance.mesh == null:
|
||||
return
|
||||
var source_materials: Dictionary = mesh_instance.get_meta(
|
||||
FOLIAGE_WIND_SOURCE_MATERIALS_META,
|
||||
{},
|
||||
)
|
||||
var bounds: AABB = mesh_instance.get_aabb()
|
||||
var mesh_global_scale: Vector3 = mesh_instance.global_basis.get_scale().abs()
|
||||
var horizontal_scale: float = maxf(
|
||||
(mesh_global_scale.x + mesh_global_scale.z) * 0.5,
|
||||
0.001,
|
||||
)
|
||||
var local_strength: float = foliage_wind_strength / horizontal_scale
|
||||
var phase: float = fposmod(
|
||||
mesh_instance.global_position.x * 0.73
|
||||
+ mesh_instance.global_position.z * 0.41,
|
||||
TAU,
|
||||
)
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var source_material: Material = mesh_instance.get_active_material(
|
||||
surface_index
|
||||
)
|
||||
if not source_material is StandardMaterial3D:
|
||||
continue
|
||||
if not FOLIAGE_MATERIAL_NAMES.has(
|
||||
StringName(source_material.resource_name)
|
||||
):
|
||||
continue
|
||||
if not source_materials.has(surface_index):
|
||||
source_materials[surface_index] = (
|
||||
mesh_instance.get_surface_override_material(surface_index)
|
||||
)
|
||||
var source_standard := source_material as StandardMaterial3D
|
||||
var wind_material := ShaderMaterial.new()
|
||||
wind_material.shader = FOLIAGE_WIND_SHADER
|
||||
wind_material.set_shader_parameter(
|
||||
"albedo_color",
|
||||
source_standard.albedo_color,
|
||||
)
|
||||
wind_material.set_shader_parameter(
|
||||
"material_roughness",
|
||||
source_standard.roughness,
|
||||
)
|
||||
wind_material.set_shader_parameter(
|
||||
"material_metallic",
|
||||
source_standard.metallic,
|
||||
)
|
||||
wind_material.set_shader_parameter("local_min_y", bounds.position.y)
|
||||
wind_material.set_shader_parameter(
|
||||
"local_height",
|
||||
maxf(bounds.size.y, 0.001),
|
||||
)
|
||||
wind_material.set_shader_parameter(
|
||||
"local_wind_strength",
|
||||
local_strength,
|
||||
)
|
||||
wind_material.set_shader_parameter("wind_speed", foliage_wind_speed)
|
||||
wind_material.set_shader_parameter("wind_phase", phase)
|
||||
mesh_instance.set_surface_override_material(
|
||||
surface_index,
|
||||
wind_material,
|
||||
)
|
||||
if not source_materials.is_empty():
|
||||
mesh_instance.set_meta(
|
||||
FOLIAGE_WIND_SOURCE_MATERIALS_META,
|
||||
source_materials,
|
||||
)
|
||||
|
||||
|
||||
func _assign_biomes(
|
||||
records: Array[Dictionary],
|
||||
summary: Dictionary,
|
||||
|
|
@ -1056,12 +1203,12 @@ func _prop_yaw(
|
|||
)
|
||||
|
||||
|
||||
func _nearest_ocean_direction(position: Vector3) -> Vector2:
|
||||
func _nearest_ocean_direction(world_position: Vector3) -> Vector2:
|
||||
var half_extents := get_playable_half_extents()
|
||||
var distance_x := half_extents.x - absf(position.x)
|
||||
var distance_z := half_extents.y - absf(position.z)
|
||||
var direction_x := Vector2.RIGHT if position.x >= 0.0 else Vector2.LEFT
|
||||
var direction_z := Vector2.DOWN if position.z >= 0.0 else Vector2.UP
|
||||
var distance_x := half_extents.x - absf(world_position.x)
|
||||
var distance_z := half_extents.y - absf(world_position.z)
|
||||
var direction_x := Vector2.RIGHT if world_position.x >= 0.0 else Vector2.LEFT
|
||||
var direction_z := Vector2.DOWN if world_position.z >= 0.0 else Vector2.UP
|
||||
if absf(distance_x - distance_z) <= _generator.catalog.chunk_size * 0.35:
|
||||
return (direction_x + direction_z).normalized()
|
||||
return direction_x if distance_x < distance_z else direction_z
|
||||
|
|
@ -1193,8 +1340,8 @@ func _spawn_distance_eligible_definitions(
|
|||
) -> Array[TerrainPropDefinition]:
|
||||
var result: Array[TerrainPropDefinition] = []
|
||||
var center := Vector2i(
|
||||
_generator.grid_size.x / 2,
|
||||
_generator.grid_size.y / 2,
|
||||
int(float(_generator.grid_size.x) / 2.0),
|
||||
int(float(_generator.grid_size.y) / 2.0),
|
||||
)
|
||||
var distance := (
|
||||
absi(coordinate.x - center.x)
|
||||
|
|
@ -1233,7 +1380,7 @@ func _record_prop_group_coordinate(
|
|||
|
||||
func _has_preferred_prop_near(
|
||||
definition: TerrainPropDefinition,
|
||||
position: Vector3,
|
||||
world_position: Vector3,
|
||||
) -> bool:
|
||||
for index: int in _placed_prop_positions.size():
|
||||
if (
|
||||
|
|
@ -1242,7 +1389,7 @@ func _has_preferred_prop_near(
|
|||
):
|
||||
continue
|
||||
var other := _placed_prop_positions[index]
|
||||
if Vector2(position.x - other.x, position.z - other.z).length() <= (
|
||||
if Vector2(world_position.x - other.x, world_position.z - other.z).length() <= (
|
||||
definition.preferred_nearby_radius
|
||||
):
|
||||
return true
|
||||
|
|
@ -1323,12 +1470,12 @@ func _biome_group_key(
|
|||
return StringName("%s:%s" % [biome_id, group])
|
||||
|
||||
|
||||
func _has_prop_clearance(position: Vector3, radius: float) -> bool:
|
||||
func _has_prop_clearance(world_position: Vector3, radius: float) -> bool:
|
||||
for index: int in _placed_prop_positions.size():
|
||||
var other := _placed_prop_positions[index]
|
||||
var distance := Vector2(
|
||||
position.x - other.x,
|
||||
position.z - other.z,
|
||||
world_position.x - other.x,
|
||||
world_position.z - other.z,
|
||||
).length()
|
||||
if distance < radius + _placed_prop_clearance_radii[index]:
|
||||
return false
|
||||
|
|
@ -1584,12 +1731,12 @@ func _safe_recovery_surface_position(candidate: Vector3) -> Variant:
|
|||
|
||||
|
||||
func _water_recovery_surface_triangles_near(
|
||||
position: Vector3,
|
||||
world_position: Vector3,
|
||||
) -> Array[PackedVector3Array]:
|
||||
var result: Array[PackedVector3Array] = []
|
||||
if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0:
|
||||
return result
|
||||
var local_position := _generator.to_local(position)
|
||||
var local_position := _generator.to_local(world_position)
|
||||
var half_grid := Vector2(
|
||||
float(_generator.grid_size.x - 1) * 0.5,
|
||||
float(_generator.grid_size.y - 1) * 0.5,
|
||||
|
|
@ -1662,12 +1809,12 @@ func _surface_triangles_for_coordinate(
|
|||
|
||||
|
||||
func _surface_height_at(
|
||||
position: Vector3,
|
||||
world_position: Vector3,
|
||||
triangles: Array[PackedVector3Array],
|
||||
) -> float:
|
||||
var highest := -INF
|
||||
var segment_start := position + Vector3.UP * 100.0
|
||||
var segment_end := position + Vector3.DOWN * 100.0
|
||||
var segment_start := world_position + Vector3.UP * 100.0
|
||||
var segment_end := world_position + Vector3.DOWN * 100.0
|
||||
for triangle: PackedVector3Array in triangles:
|
||||
if triangle.size() != 3:
|
||||
continue
|
||||
|
|
@ -1684,13 +1831,13 @@ func _surface_height_at(
|
|||
|
||||
|
||||
func _surface_supports_prop_footprint(
|
||||
position: Vector3,
|
||||
world_position: Vector3,
|
||||
surface_height: float,
|
||||
radius: float,
|
||||
triangles: Array[PackedVector3Array],
|
||||
) -> bool:
|
||||
for direction: Vector2 in PROP_SURFACE_SAMPLE_DIRECTIONS:
|
||||
var sample_position := position + Vector3(
|
||||
var sample_position := world_position + Vector3(
|
||||
direction.x * radius,
|
||||
0.0,
|
||||
direction.y * radius,
|
||||
|
|
|
|||
|
|
@ -282,11 +282,13 @@ func _resolved_layout_validation_error(
|
|||
if counts.get(required_id, 0) <= 0:
|
||||
return "required chunk %s is missing." % required_id
|
||||
if force_center_chunk_id != &"":
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
var center_index := center.y * grid_size.x + center.x
|
||||
if layout[center_index].definition.stable_id != force_center_chunk_id:
|
||||
return "the forced center chunk is missing from the center cell."
|
||||
for index: int in layout.size():
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var current := layout[index]
|
||||
if not _variant_respects_ocean_boundary(current, coordinate):
|
||||
|
|
@ -956,6 +958,7 @@ func _candidate_has_domain_support(
|
|||
index: int,
|
||||
domains: Dictionary,
|
||||
) -> bool:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var possible_required_neighbors := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -1015,6 +1018,7 @@ func _placed_neighbor_requirements_have_domain_support(
|
|||
var placement := _placements[index]
|
||||
if placement == null or placement.definition.minimum_required_neighbors <= 0:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var possible_neighbors := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -1205,6 +1209,7 @@ func _candidate_dynamic_cell_rules_are_satisfied(
|
|||
) -> bool:
|
||||
if not _definition_has_capacity(candidate.definition):
|
||||
return false
|
||||
@warning_ignore("integer_division")
|
||||
var center_index := (grid_size.y / 2) * grid_size.x + grid_size.x / 2
|
||||
if (
|
||||
index != center_index
|
||||
|
|
@ -1288,12 +1293,15 @@ func _candidate_can_occupy_cell(
|
|||
candidate: TerrainChunkVariant,
|
||||
index: int,
|
||||
) -> bool:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
if (
|
||||
_cell_requires_reserved_grass(index)
|
||||
and candidate.definition.stable_id != elevated_cliff_base_chunk_id
|
||||
and not (
|
||||
coordinate == Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
coordinate == center
|
||||
and force_center_chunk_id != &""
|
||||
and candidate.definition.stable_id == force_center_chunk_id
|
||||
and "grass" in candidate.definition.tags
|
||||
|
|
@ -1301,7 +1309,6 @@ func _candidate_can_occupy_cell(
|
|||
)
|
||||
):
|
||||
return false
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
if (
|
||||
coordinate == center
|
||||
and force_center_chunk_id != &""
|
||||
|
|
@ -1353,6 +1360,7 @@ func _prepare_elevated_feature_region() -> bool:
|
|||
"Elevated cliff feature requires at least an 11x11 terrain grid."
|
||||
)
|
||||
return false
|
||||
@warning_ignore("integer_division")
|
||||
var forced_center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
var candidates: Array[Vector2i] = []
|
||||
for row: int in range(
|
||||
|
|
@ -1668,6 +1676,7 @@ func _lake_feature_candidates(
|
|||
var result: Array[Dictionary] = []
|
||||
if footprint_size.x < 2 or footprint_size.y < 2:
|
||||
return result
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
for row: int in range(
|
||||
LAKE_FEATURE_CLEARANCE + 1,
|
||||
|
|
@ -1961,6 +1970,7 @@ func _river_feature_candidate_is_valid(
|
|||
or outlet_coordinates.size() != 2
|
||||
):
|
||||
return false
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
var source_lookup: Dictionary[Vector2i, bool] = {}
|
||||
var source_exit_lookup: Dictionary[Vector2i, bool] = {}
|
||||
|
|
@ -2527,8 +2537,10 @@ func _build_grid_solver_caches() -> void:
|
|||
_neighbor_indices.resize(_placements.size())
|
||||
_static_cell_candidate_masks.resize(_placements.size())
|
||||
_static_cell_candidate_masks.fill(0)
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
for index: int in _placements.size():
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var neighbors := PackedInt32Array([-1, -1, -1, -1])
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -2596,6 +2608,7 @@ func _apply_grass_sand_smoothing() -> bool:
|
|||
return false
|
||||
|
||||
var candidates: Array[Dictionary] = []
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
for index: int in _placements.size():
|
||||
var placement := _placements[index]
|
||||
|
|
@ -2604,6 +2617,7 @@ func _apply_grass_sand_smoothing() -> bool:
|
|||
smoothing_sand_chunk_id,
|
||||
]:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
if (
|
||||
_coordinate_is_on_boundary(coordinate)
|
||||
|
|
@ -2646,6 +2660,7 @@ func _apply_grass_sand_smoothing() -> bool:
|
|||
if replacement_count >= maximum_smoothing_placements:
|
||||
break
|
||||
var index := int(candidate["index"])
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var diagonal := candidate["variant"] as TerrainChunkVariant
|
||||
if (
|
||||
|
|
@ -3260,6 +3275,7 @@ func _primary_elevated_top_coordinate() -> Vector2i:
|
|||
if _coordinate_has_primary_elevated_top(_elevated_feature_center):
|
||||
return _elevated_feature_center
|
||||
for index: int in _placements.size():
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
if not _coordinate_has_primary_elevated_top(coordinate):
|
||||
continue
|
||||
|
|
@ -3635,11 +3651,11 @@ func _weighted_candidate_order(
|
|||
candidate.definition,
|
||||
):
|
||||
weight *= long_repeat_weight_multiplier
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
if (
|
||||
candidate.definition.prefers_map_boundary
|
||||
and _coordinate_is_on_boundary(
|
||||
Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
)
|
||||
and _coordinate_is_on_boundary(coordinate)
|
||||
):
|
||||
weight *= boundary_preference_multiplier
|
||||
remaining.append(candidate)
|
||||
|
|
@ -3669,6 +3685,7 @@ func _adjacent_definition_repeat_count(
|
|||
index: int,
|
||||
definition: TerrainChunkDefinition,
|
||||
) -> int:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var count := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -3690,6 +3707,7 @@ func _preferred_neighbor_count(
|
|||
index: int,
|
||||
definition: TerrainChunkDefinition,
|
||||
) -> int:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var count := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -3711,6 +3729,7 @@ func _long_definition_run_count(
|
|||
index: int,
|
||||
definition: TerrainChunkDefinition,
|
||||
) -> int:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var count := 0
|
||||
for axis: Vector2i in [Vector2i.RIGHT, Vector2i.DOWN]:
|
||||
|
|
@ -3762,6 +3781,7 @@ func _neighbor_requirement_validation_error(
|
|||
var placement := layout[index]
|
||||
if placement == null or placement.definition.minimum_required_neighbors <= 0:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var matching_neighbors := 0
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -3809,6 +3829,7 @@ func _walkable_connectivity_validation_error(
|
|||
return ""
|
||||
var start_index := walkable_indices[0]
|
||||
if force_center_chunk_id != &"":
|
||||
@warning_ignore("integer_division")
|
||||
var center := Vector2i(grid_size.x / 2, grid_size.y / 2)
|
||||
var center_index := center.y * grid_size.x + center.x
|
||||
if (
|
||||
|
|
@ -3822,6 +3843,7 @@ func _walkable_connectivity_validation_error(
|
|||
while pending_cursor < pending.size():
|
||||
var index := pending[pending_cursor]
|
||||
pending_cursor += 1
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var current := layout[index]
|
||||
for edge_value: int in TerrainChunkTopology.Edge.values():
|
||||
|
|
@ -3892,6 +3914,7 @@ func _least_repeated_equivalent_rotation(
|
|||
definition: TerrainChunkDefinition,
|
||||
rotations: PackedInt32Array,
|
||||
) -> int:
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var best_score := 3
|
||||
var best_rotations := PackedInt32Array()
|
||||
|
|
@ -3944,6 +3967,7 @@ func _build_solution_root() -> Node3D:
|
|||
var variant := _placements[index]
|
||||
if variant == null:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
var chunk_root := _instantiate_chunk_root(variant.definition)
|
||||
if chunk_root == null:
|
||||
|
|
@ -4325,6 +4349,7 @@ func _count_adjacent_repeat_edges() -> int:
|
|||
var current := _placements[index]
|
||||
if current == null:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
if coordinate.x > 0:
|
||||
var west := _placements[index - 1]
|
||||
|
|
@ -4383,6 +4408,7 @@ func placement_records() -> Array[Dictionary]:
|
|||
var variant: TerrainChunkVariant = _placements[index]
|
||||
if variant == null:
|
||||
continue
|
||||
@warning_ignore("integer_division")
|
||||
var coordinate := Vector2i(index % grid_size.x, index / grid_size.x)
|
||||
result.append({
|
||||
"coordinate": coordinate,
|
||||
|
|
@ -4414,7 +4440,7 @@ func chunk_position(coordinate: Vector2i) -> Vector3:
|
|||
)
|
||||
|
||||
|
||||
func coordinate_for_local_position(position: Vector3) -> Vector2i:
|
||||
func coordinate_for_local_position(local_position: Vector3) -> Vector2i:
|
||||
if catalog == null or catalog.chunk_size <= 0.0:
|
||||
return Vector2i(-1, -1)
|
||||
var half_grid := Vector2(
|
||||
|
|
@ -4422,8 +4448,8 @@ func coordinate_for_local_position(position: Vector3) -> Vector2i:
|
|||
float(grid_size.y - 1) * 0.5,
|
||||
)
|
||||
return Vector2i(
|
||||
roundi(position.x / catalog.chunk_size + half_grid.x),
|
||||
roundi(position.z / catalog.chunk_size + half_grid.y),
|
||||
roundi(local_position.x / catalog.chunk_size + half_grid.x),
|
||||
roundi(local_position.z / catalog.chunk_size + half_grid.y),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -196,36 +196,36 @@ func get_saltwater_surface_height() -> float:
|
|||
)
|
||||
|
||||
|
||||
func is_home_exterior_position_accessible(position: Vector3) -> bool:
|
||||
func is_home_exterior_position_accessible(world_position: Vector3) -> bool:
|
||||
return (
|
||||
_active_region != null
|
||||
and _active_region.is_home_exterior_position_accessible(position)
|
||||
and _active_region.is_home_exterior_position_accessible(world_position)
|
||||
)
|
||||
|
||||
|
||||
func activate_world(layout: StringName, seed: int) -> bool:
|
||||
func activate_world(layout: StringName, world_seed: int) -> bool:
|
||||
if (
|
||||
not WorldLayoutType.is_valid(layout)
|
||||
or seed <= 0
|
||||
or seed > PlayerSaveManager.MAX_WORLD_SEED
|
||||
or world_seed <= 0
|
||||
or world_seed > PlayerSaveManager.MAX_WORLD_SEED
|
||||
):
|
||||
return false
|
||||
if _active_region == null or _active_region.region_id != layout:
|
||||
if not _replace_active_region(layout, seed):
|
||||
if not _replace_active_region(layout, world_seed):
|
||||
return false
|
||||
_world_layout = layout
|
||||
_world_seed = seed
|
||||
_world_seed = world_seed
|
||||
if layout == WorldLayoutType.GENERATED:
|
||||
if not _active_region.has_method("generate_world"):
|
||||
return false
|
||||
if not bool(_active_region.call("generate_world", seed)):
|
||||
if not bool(_active_region.call("generate_world", world_seed)):
|
||||
return false
|
||||
_configure_world_coverage()
|
||||
return true
|
||||
|
||||
|
||||
func generate_world(seed: int) -> bool:
|
||||
return activate_world(WorldLayoutType.GENERATED, seed)
|
||||
func generate_world(world_seed: int) -> bool:
|
||||
return activate_world(WorldLayoutType.GENERATED, world_seed)
|
||||
|
||||
|
||||
func get_world_layout() -> StringName:
|
||||
|
|
@ -293,7 +293,7 @@ func _find_active_region() -> WorldRegion:
|
|||
return null
|
||||
|
||||
|
||||
func _replace_active_region(layout: StringName, seed: int) -> bool:
|
||||
func _replace_active_region(layout: StringName, world_seed: int) -> bool:
|
||||
var region_scene: PackedScene = (
|
||||
GENERATED_REGION_SCENE
|
||||
if layout == WorldLayoutType.GENERATED
|
||||
|
|
@ -303,7 +303,7 @@ func _replace_active_region(layout: StringName, seed: int) -> bool:
|
|||
if replacement == null:
|
||||
return false
|
||||
if layout == WorldLayoutType.GENERATED:
|
||||
replacement.set("initial_seed", seed)
|
||||
replacement.set("initial_seed", world_seed)
|
||||
if _active_region != null:
|
||||
_regions_root.remove_child(_active_region)
|
||||
_active_region.free()
|
||||
|
|
@ -345,8 +345,8 @@ func _configure_world_coverage() -> void:
|
|||
coverage_shape.size = Vector3(half.x * 2.0, 4.0, half.y * 2.0)
|
||||
|
||||
|
||||
func _set_bound(body: Node3D, position: Vector3, size: Vector3) -> void:
|
||||
body.position = position
|
||||
func _set_bound(body: Node3D, world_position: Vector3, size: Vector3) -> void:
|
||||
body.position = world_position
|
||||
var collision := body.get_node("Shape") as CollisionShape3D
|
||||
var shape := collision.shape as BoxShape3D
|
||||
if shape != null:
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ func update_world_context(
|
|||
safe_points: Array[SafeRespawnPoint],
|
||||
recovery_position_resolver: Callable = Callable(),
|
||||
) -> void:
|
||||
# A world change replaces the trigger set and recovery resolver. Never let
|
||||
# an in-flight recovery retain its old-world input lock or fade callback.
|
||||
# The caller may be changing worlds while gameplay remains active, so this
|
||||
# variant restores the control state that recovery itself captured.
|
||||
_abort_active_recovery(true)
|
||||
for water_trigger: PlayerWaterTrigger in _water_triggers:
|
||||
if (
|
||||
water_trigger != null
|
||||
|
|
@ -117,7 +122,15 @@ func _process(delta: float) -> void:
|
|||
|
||||
|
||||
func set_recovery_enabled(enabled: bool) -> void:
|
||||
if _recovery_enabled == enabled:
|
||||
return
|
||||
_recovery_enabled = enabled
|
||||
if not enabled:
|
||||
# Gameplay/session teardown can happen while the screen is fading. The
|
||||
# normal finish callback would then never be allowed to leave the fishing
|
||||
# input gate, which makes the next session appear unable to fish. Preserve
|
||||
# the caller's disabled controls while releasing recovery-owned state.
|
||||
_abort_active_recovery(false)
|
||||
|
||||
|
||||
func is_recovery_active() -> bool:
|
||||
|
|
@ -125,15 +138,7 @@ func is_recovery_active() -> bool:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_generation += 1
|
||||
if _screen_fade != null and is_instance_valid(_screen_fade):
|
||||
_screen_fade.reset_immediately()
|
||||
if _player != null and is_instance_valid(_player):
|
||||
_player.restore_gameplay_orientation_after_recovery()
|
||||
_player.set_water_recovery_active(false)
|
||||
_player.set_movement_enabled(_prior_movement_enabled)
|
||||
_player.set_camera_input_enabled(_prior_camera_input_enabled)
|
||||
state = RecoveryState.IDLE
|
||||
_abort_active_recovery(true)
|
||||
|
||||
|
||||
func _on_recovery_requested(
|
||||
|
|
@ -225,3 +230,26 @@ func _finish_recovery() -> void:
|
|||
state = RecoveryState.IDLE
|
||||
_bob_elapsed = 0.0
|
||||
recovery_finished.emit()
|
||||
|
||||
|
||||
func _abort_active_recovery(restore_controls: bool) -> void:
|
||||
if state == RecoveryState.IDLE:
|
||||
return
|
||||
# Invalidate every in-flight ScreenFade completion before returning control.
|
||||
# This makes cancellation safe whether recovery was bobbing, fading out, or
|
||||
# fading in.
|
||||
_generation += 1
|
||||
if _screen_fade != null and is_instance_valid(_screen_fade):
|
||||
_screen_fade.reset_immediately()
|
||||
if _player != null and is_instance_valid(_player):
|
||||
_player.restore_gameplay_orientation_after_recovery()
|
||||
_player.set_water_recovery_active(false)
|
||||
_player.velocity = Vector3.ZERO
|
||||
if restore_controls:
|
||||
_player.set_movement_enabled(_prior_movement_enabled)
|
||||
_player.set_camera_input_enabled(_prior_camera_input_enabled)
|
||||
if _fishing_spot != null and is_instance_valid(_fishing_spot):
|
||||
_fishing_spot.end_water_recovery()
|
||||
state = RecoveryState.IDLE
|
||||
_bob_elapsed = 0.0
|
||||
recovery_finished.emit()
|
||||
|
|
|
|||
|
|
@ -381,8 +381,8 @@ func _set_remote_datetime(time_hours: float, date_id: String) -> void:
|
|||
"year": int(date_id.substr(0, 4)),
|
||||
"month": int(date_id.substr(5, 2)),
|
||||
"day": int(date_id.substr(8, 2)),
|
||||
"hour": total_seconds / 3600,
|
||||
"minute": (total_seconds / 60) % 60,
|
||||
"hour": int(float(total_seconds) / 3600.0),
|
||||
"minute": int(float(total_seconds) / 60.0) % 60,
|
||||
"second": total_seconds % 60,
|
||||
}
|
||||
_set_calendar_date_id(date_id)
|
||||
|
|
@ -432,11 +432,14 @@ static func _is_leap_year(year: int) -> bool:
|
|||
|
||||
static func _weekday_for_date(year: int, month: int, day: int) -> int:
|
||||
var adjusted_year: int = year - 1 if month < 3 else year
|
||||
var leap_years: int = int(float(adjusted_year) / 4.0)
|
||||
var skipped_centuries: int = int(float(adjusted_year) / 100.0)
|
||||
var restored_quadricentennials: int = int(float(adjusted_year) / 400.0)
|
||||
return posmod(
|
||||
adjusted_year
|
||||
+ adjusted_year / 4
|
||||
- adjusted_year / 100
|
||||
+ adjusted_year / 400
|
||||
+ leap_years
|
||||
- skipped_centuries
|
||||
+ restored_quadricentennials
|
||||
+ WEEKDAY_MONTH_OFFSETS[month - 1]
|
||||
+ day,
|
||||
7,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue