Add gathering, unified inventory, and real-time world
This commit is contained in:
parent
173371e6fc
commit
fa57edca83
113 changed files with 6700 additions and 1307 deletions
114
world/digging/diggable_area_3d.gd
Normal file
114
world/digging/diggable_area_3d.gd
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
@tool
|
||||
class_name DiggableArea3D
|
||||
extends Node3D
|
||||
|
||||
@export var area_id: StringName
|
||||
@export_node_path("Node3D") var terrain_source: NodePath
|
||||
@export var surface_materials: Array[StringName] = []
|
||||
@export var generation_bounds := Rect2(-50.0, -50.0, 100.0, 100.0)
|
||||
@export_range(-100.0, 100.0, 0.01) var minimum_global_y: float = -100.0
|
||||
@export_range(-100.0, 100.0, 0.01) var maximum_global_y: float = 100.0
|
||||
@export_range(0.0, 1.0, 0.01) var minimum_up_dot: float = 0.6
|
||||
|
||||
|
||||
func get_surface_triangles() -> Array[PackedVector3Array]:
|
||||
var triangles: Array[PackedVector3Array] = []
|
||||
if area_id.is_empty() or surface_materials.is_empty():
|
||||
return triangles
|
||||
var terrain_root: Node = get_node_or_null(terrain_source)
|
||||
if terrain_root == null:
|
||||
return triangles
|
||||
for mesh_instance: MeshInstance3D in _collect_mesh_instances(terrain_root):
|
||||
var mesh: Mesh = mesh_instance.mesh
|
||||
if mesh == null:
|
||||
continue
|
||||
for surface_index: int in mesh.get_surface_count():
|
||||
var material: Material = mesh_instance.get_active_material(surface_index)
|
||||
if (
|
||||
material == null
|
||||
or not surface_materials.has(StringName(material.resource_name))
|
||||
):
|
||||
continue
|
||||
_append_surface_triangles(
|
||||
triangles,
|
||||
mesh_instance,
|
||||
mesh.surface_get_arrays(surface_index),
|
||||
)
|
||||
return triangles
|
||||
|
||||
|
||||
func _append_surface_triangles(
|
||||
result: Array[PackedVector3Array],
|
||||
mesh_instance: MeshInstance3D,
|
||||
arrays: Array,
|
||||
) -> void:
|
||||
if arrays.size() <= Mesh.ARRAY_INDEX:
|
||||
return
|
||||
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
if vertices.is_empty():
|
||||
return
|
||||
if indices.is_empty():
|
||||
for vertex_index: int in range(0, vertices.size() - 2, 3):
|
||||
_append_triangle(
|
||||
result,
|
||||
mesh_instance.to_global(vertices[vertex_index]),
|
||||
mesh_instance.to_global(vertices[vertex_index + 1]),
|
||||
mesh_instance.to_global(vertices[vertex_index + 2]),
|
||||
)
|
||||
return
|
||||
for index_offset: int in range(0, indices.size() - 2, 3):
|
||||
_append_triangle(
|
||||
result,
|
||||
mesh_instance.to_global(vertices[indices[index_offset]]),
|
||||
mesh_instance.to_global(vertices[indices[index_offset + 1]]),
|
||||
mesh_instance.to_global(vertices[indices[index_offset + 2]]),
|
||||
)
|
||||
|
||||
|
||||
func _append_triangle(
|
||||
result: Array[PackedVector3Array],
|
||||
a: Vector3,
|
||||
b: Vector3,
|
||||
c: Vector3,
|
||||
) -> void:
|
||||
if (
|
||||
a.y < minimum_global_y
|
||||
or b.y < minimum_global_y
|
||||
or c.y < minimum_global_y
|
||||
or a.y > maximum_global_y
|
||||
or b.y > maximum_global_y
|
||||
or c.y > maximum_global_y
|
||||
):
|
||||
return
|
||||
var center := (a + b + c) / 3.0
|
||||
if not generation_bounds.has_point(Vector2(center.x, center.z)):
|
||||
return
|
||||
var cross := (b - a).cross(c - a)
|
||||
if cross.length_squared() <= 0.0000001:
|
||||
return
|
||||
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
|
||||
return
|
||||
result.append(PackedVector3Array([a, b, c]))
|
||||
|
||||
|
||||
func _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||
var meshes: Array[MeshInstance3D] = []
|
||||
if root is MeshInstance3D:
|
||||
meshes.append(root as MeshInstance3D)
|
||||
for child: Node in root.get_children():
|
||||
meshes.append_array(_collect_mesh_instances(child))
|
||||
return meshes
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
if area_id.is_empty():
|
||||
warnings.append("Diggable area ID is required.")
|
||||
if get_node_or_null(terrain_source) == null:
|
||||
warnings.append("Diggable area terrain source is unavailable.")
|
||||
if surface_materials.is_empty():
|
||||
warnings.append("At least one terrain material is required.")
|
||||
if maximum_global_y < minimum_global_y:
|
||||
warnings.append("Maximum height must not be below minimum height.")
|
||||
return warnings
|
||||
1
world/digging/diggable_area_3d.gd.uid
Normal file
1
world/digging/diggable_area_3d.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d33f4164b78ww
|
||||
|
|
@ -26,6 +26,65 @@ uniform float moon_edge_softness : hint_range(0.001, 0.03) = 0.003;
|
|||
uniform float moon_halo_radius : hint_range(0.02, 0.35) = 0.11;
|
||||
uniform float moon_halo_strength : hint_range(0.0, 1.0) = 0.12;
|
||||
|
||||
uniform vec4 star_color : source_color = vec4(0.72, 0.82, 0.96, 1.0);
|
||||
uniform float star_visibility : hint_range(0.0, 1.0) = 0.0;
|
||||
uniform float star_strength : hint_range(0.0, 1.0) = 0.72;
|
||||
|
||||
|
||||
float star_hash(vec2 point) {
|
||||
vec3 value = fract(vec3(point.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||||
value += dot(value, value.yzx + 33.33);
|
||||
return fract((value.x + value.y) * value.z);
|
||||
}
|
||||
|
||||
|
||||
vec2 star_hash2(vec2 point) {
|
||||
return vec2(
|
||||
star_hash(point),
|
||||
star_hash(point + vec2(19.19, 7.73))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
float procedural_star_field(vec3 view_direction) {
|
||||
// Map the upper hemisphere to one stable world-direction diamond. This
|
||||
// avoids a longitude seam and keeps the stars fixed as the camera moves.
|
||||
float direction_sum = max(
|
||||
abs(view_direction.x)
|
||||
+ abs(view_direction.y)
|
||||
+ abs(view_direction.z),
|
||||
0.0001
|
||||
);
|
||||
vec2 star_uv = view_direction.xz / direction_sum * 0.5 + 0.5;
|
||||
vec2 star_point = star_uv * 190.0;
|
||||
vec2 cell = floor(star_point);
|
||||
vec2 local_point = fract(star_point);
|
||||
float spawn_roll = star_hash(cell + vec2(3.17, 11.83));
|
||||
vec2 point_center = mix(
|
||||
vec2(0.18),
|
||||
vec2(0.82),
|
||||
star_hash2(cell + vec2(41.0, 73.0))
|
||||
);
|
||||
float point_radius = mix(
|
||||
0.065,
|
||||
0.13,
|
||||
star_hash(cell + vec2(89.0, 17.0))
|
||||
);
|
||||
float point_shape = 1.0 - smoothstep(
|
||||
point_radius,
|
||||
point_radius + 0.045,
|
||||
distance(local_point, point_center)
|
||||
);
|
||||
float point_exists = step(0.965, spawn_roll);
|
||||
float point_brightness = mix(
|
||||
0.42,
|
||||
1.0,
|
||||
smoothstep(0.965, 1.0, spawn_roll)
|
||||
);
|
||||
float upper_sky_fade = smoothstep(0.055, 0.22, view_direction.y);
|
||||
return point_shape * point_exists * point_brightness * upper_sky_fade;
|
||||
}
|
||||
|
||||
|
||||
void sky() {
|
||||
vec3 view_direction = normalize(EYEDIR);
|
||||
|
|
@ -50,6 +109,12 @@ void sky() {
|
|||
view_direction.y
|
||||
)
|
||||
);
|
||||
float stars = procedural_star_field(view_direction);
|
||||
base_color = mix(
|
||||
base_color,
|
||||
star_color.rgb,
|
||||
stars * star_visibility * star_strength
|
||||
);
|
||||
|
||||
float alignment = dot(view_direction, normalize(sun_direction));
|
||||
float disc_outer = cos(sun_radius + sun_edge_softness);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ shader_parameter/moon_radius = 0.045
|
|||
shader_parameter/moon_edge_softness = 0.003
|
||||
shader_parameter/moon_halo_radius = 0.11
|
||||
shader_parameter/moon_halo_strength = 0.12
|
||||
shader_parameter/star_color = Color(0.72, 0.82, 0.96, 1)
|
||||
shader_parameter/star_visibility = 0.0
|
||||
shader_parameter/star_strength = 0.72
|
||||
|
||||
[resource]
|
||||
sky_material = SubResource("ShaderMaterial_netfishing")
|
||||
|
|
|
|||
30
world/gathering/gatherable_anchor_set_3d.gd
Normal file
30
world/gathering/gatherable_anchor_set_3d.gd
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@tool
|
||||
class_name GatherableAnchorSet3D
|
||||
extends Node3D
|
||||
|
||||
@export var anchor_set_id: StringName
|
||||
|
||||
|
||||
func get_spawn_positions() -> PackedVector3Array:
|
||||
var positions := PackedVector3Array()
|
||||
_collect_marker_positions(self, positions)
|
||||
return positions
|
||||
|
||||
|
||||
func _collect_marker_positions(
|
||||
root_node: Node,
|
||||
positions: PackedVector3Array,
|
||||
) -> void:
|
||||
for child: Node in root_node.get_children():
|
||||
if child is Marker3D:
|
||||
positions.append((child as Marker3D).global_position)
|
||||
_collect_marker_positions(child, positions)
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
if anchor_set_id.is_empty():
|
||||
warnings.append("Gatherable anchor-set ID is required.")
|
||||
if get_spawn_positions().is_empty():
|
||||
warnings.append("Add at least one Marker3D spawn anchor.")
|
||||
return warnings
|
||||
1
world/gathering/gatherable_anchor_set_3d.gd.uid
Normal file
1
world/gathering/gatherable_anchor_set_3d.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b6xfixi21qllm
|
||||
60
world/interactables/player_storage_box.tscn
Normal file
60
world/interactables/player_storage_box.tscn
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
[gd_scene load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/player_storage_interaction.gd" id="1_interaction"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="BoxMaterial"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.047, 0.22, 0.27, 1)
|
||||
texture_filter = 0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="LidMaterial"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.12, 0.52, 0.58, 1)
|
||||
texture_filter = 0
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh"]
|
||||
material = SubResource("BoxMaterial")
|
||||
size = Vector3(1.5, 0.7, 0.9)
|
||||
|
||||
[sub_resource type="BoxMesh" id="LidMesh"]
|
||||
material = SubResource("LidMaterial")
|
||||
size = Vector3(1.62, 0.16, 1.02)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="CollisionShape"]
|
||||
size = Vector3(1.62, 0.86, 1.02)
|
||||
|
||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||
radius = 2.3
|
||||
|
||||
[node name="PlayerStorageBox" type="Node3D"]
|
||||
|
||||
[node name="Body" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.35, 0)
|
||||
mesh = SubResource("BoxMesh")
|
||||
cast_shadow = 0
|
||||
|
||||
[node name="Lid" type="MeshInstance3D" parent="."]
|
||||
position = Vector3(0, 0.76, 0)
|
||||
mesh = SubResource("LidMesh")
|
||||
cast_shadow = 0
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
collision_layer = 1
|
||||
collision_mask = 0
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
position = Vector3(0, 0.43, 0)
|
||||
shape = SubResource("CollisionShape")
|
||||
|
||||
[node name="InteractionArea" type="Area3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 0.55, 0)
|
||||
collision_layer = 0
|
||||
collision_mask = 2
|
||||
script = ExtResource("1_interaction")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="InteractionArea"]
|
||||
shape = SubResource("InteractionShape")
|
||||
|
||||
[node name="PromptAnchor" type="Marker3D" parent="."]
|
||||
position = Vector3(0, 1.15, 0)
|
||||
53
world/player_storage_interaction.gd
Normal file
53
world/player_storage_interaction.gd
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
class_name PlayerStorageInteraction
|
||||
extends Area3D
|
||||
|
||||
signal local_player_range_changed(in_range: bool)
|
||||
|
||||
@export_node_path("Marker3D") var prompt_anchor_path: NodePath = ^"../PromptAnchor"
|
||||
|
||||
var _local_player: Player
|
||||
var _local_player_in_range: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
body_entered.connect(_on_body_entered)
|
||||
body_exited.connect(_on_body_exited)
|
||||
|
||||
|
||||
func setup_local_player(player: Player) -> void:
|
||||
_local_player = player
|
||||
_refresh_overlaps()
|
||||
|
||||
|
||||
func is_local_player_in_range() -> bool:
|
||||
return _local_player_in_range
|
||||
|
||||
|
||||
func get_prompt_anchor_position() -> Vector3:
|
||||
var anchor := get_node_or_null(prompt_anchor_path) as Marker3D
|
||||
return anchor.global_position if anchor != null else global_position + Vector3.UP
|
||||
|
||||
|
||||
func _refresh_overlaps() -> void:
|
||||
_set_local_player_in_range(
|
||||
_local_player != null
|
||||
and is_instance_valid(_local_player)
|
||||
and _local_player in get_overlapping_bodies()
|
||||
)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body == _local_player:
|
||||
_set_local_player_in_range(true)
|
||||
|
||||
|
||||
func _on_body_exited(body: Node3D) -> void:
|
||||
if body == _local_player:
|
||||
_set_local_player_in_range(false)
|
||||
|
||||
|
||||
func _set_local_player_in_range(in_range: bool) -> void:
|
||||
if _local_player_in_range == in_range:
|
||||
return
|
||||
_local_player_in_range = in_range
|
||||
local_player_range_changed.emit(in_range)
|
||||
1
world/player_storage_interaction.gd.uid
Normal file
1
world/player_storage_interaction.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://7sl42teh8qgb
|
||||
|
|
@ -5,6 +5,9 @@ extends WorldRegion
|
|||
const FishingShopInteractionType = preload(
|
||||
"res://world/fishing_shop_interaction.gd"
|
||||
)
|
||||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||
"res://world/materials/foliage_wind.gdshader"
|
||||
)
|
||||
|
|
@ -39,6 +42,10 @@ var player_spawn_path: NodePath = ^"PlayerSpawn"
|
|||
var fishing_shop_path: NodePath = (
|
||||
^"Interactables/FishingShopWorld/InteractionArea"
|
||||
)
|
||||
@export_node_path("Area3D")
|
||||
var player_storage_path: NodePath = (
|
||||
^"Interactables/PlayerStorageBox/InteractionArea"
|
||||
)
|
||||
@export_group("Terrain Collision")
|
||||
# The imported GLB is the collision authority. Rebuild once per region load so
|
||||
# newly authored terrain and props cannot retain a stale saved fallback shape.
|
||||
|
|
@ -67,6 +74,10 @@ func get_fishing_shop() -> FishingShopInteractionType:
|
|||
return get_node_or_null(fishing_shop_path) as FishingShopInteractionType
|
||||
|
||||
|
||||
func get_player_storage() -> PlayerStorageInteractionType:
|
||||
return get_node_or_null(player_storage_path) as PlayerStorageInteractionType
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
||||
|
||||
|
|
@ -363,4 +374,6 @@ func _get_configuration_warnings() -> PackedStringArray:
|
|||
warnings.append("PlayerSpawn marker is missing.")
|
||||
if get_node_or_null(fishing_shop_path) == null:
|
||||
warnings.append("Fishing Shop placement is missing.")
|
||||
if get_node_or_null(player_storage_path) == null:
|
||||
warnings.append("Private player storage placement is missing.")
|
||||
return warnings
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,6 +6,8 @@ extends Node3D
|
|||
@export var fishable_water_root: NodePath = ^"FishingWater"
|
||||
@export var water_recovery_root: NodePath = ^"WaterRecovery"
|
||||
@export var safe_respawns_root: NodePath = ^"SafeRespawns"
|
||||
@export var diggable_area_root: NodePath = ^"DiggableAreas"
|
||||
@export var gatherable_anchor_root: NodePath = ^"GatherableAnchors"
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
|
|
@ -38,6 +40,44 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]:
|
|||
return points
|
||||
|
||||
|
||||
func get_diggable_areas() -> Array[DiggableArea3D]:
|
||||
var areas: Array[DiggableArea3D] = []
|
||||
var root: Node = get_node_or_null(diggable_area_root)
|
||||
if root == null:
|
||||
return areas
|
||||
_collect_diggable_areas(root, areas)
|
||||
return areas
|
||||
|
||||
|
||||
func get_diggable_area(area_id: StringName) -> DiggableArea3D:
|
||||
if area_id.is_empty():
|
||||
return null
|
||||
for area: DiggableArea3D in get_diggable_areas():
|
||||
if area.area_id == area_id:
|
||||
return area
|
||||
return null
|
||||
|
||||
|
||||
func get_gatherable_anchor_sets() -> Array[GatherableAnchorSet3D]:
|
||||
var anchor_sets: Array[GatherableAnchorSet3D] = []
|
||||
var root: Node = get_node_or_null(gatherable_anchor_root)
|
||||
if root == null:
|
||||
return anchor_sets
|
||||
_collect_gatherable_anchor_sets(root, anchor_sets)
|
||||
return anchor_sets
|
||||
|
||||
|
||||
func get_gatherable_anchor_set(
|
||||
anchor_set_id: StringName,
|
||||
) -> GatherableAnchorSet3D:
|
||||
if anchor_set_id.is_empty():
|
||||
return null
|
||||
for anchor_set: GatherableAnchorSet3D in get_gatherable_anchor_sets():
|
||||
if anchor_set.anchor_set_id == anchor_set_id:
|
||||
return anchor_set
|
||||
return null
|
||||
|
||||
|
||||
func _collect_fishable_water_regions(
|
||||
root: Node,
|
||||
regions: Array[FishableWaterRegion],
|
||||
|
|
@ -58,3 +98,25 @@ func _collect_water_recovery_triggers(
|
|||
if trigger != null:
|
||||
triggers.append(trigger)
|
||||
_collect_water_recovery_triggers(child, triggers)
|
||||
|
||||
|
||||
func _collect_diggable_areas(
|
||||
root: Node,
|
||||
areas: Array[DiggableArea3D],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var area := child as DiggableArea3D
|
||||
if area != null:
|
||||
areas.append(area)
|
||||
_collect_diggable_areas(child, areas)
|
||||
|
||||
|
||||
func _collect_gatherable_anchor_sets(
|
||||
root: Node,
|
||||
anchor_sets: Array[GatherableAnchorSet3D],
|
||||
) -> void:
|
||||
for child: Node in root.get_children():
|
||||
var anchor_set := child as GatherableAnchorSet3D
|
||||
if anchor_set != null:
|
||||
anchor_sets.append(anchor_set)
|
||||
_collect_gatherable_anchor_sets(child, anchor_sets)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ extends Node3D
|
|||
const FishingShopInteractionType = preload(
|
||||
"res://world/fishing_shop_interaction.gd"
|
||||
)
|
||||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
|
||||
@onready var _regions_root: Node3D = $Regions
|
||||
@onready var _starter_island: StarterIslandRegion = (
|
||||
|
|
@ -15,6 +18,9 @@ const FishingShopInteractionType = preload(
|
|||
@onready var _fishing_shop: FishingShopInteractionType = (
|
||||
_starter_island.get_fishing_shop()
|
||||
)
|
||||
@onready var _player_storage: PlayerStorageInteractionType = (
|
||||
_starter_island.get_player_storage()
|
||||
)
|
||||
@onready var _world_environment: WorldEnvironment = $Environment/WorldEnvironment
|
||||
@onready var _sun: DirectionalLight3D = $Environment/Sun
|
||||
|
||||
|
|
@ -38,6 +44,10 @@ func get_fishing_shop() -> FishingShopInteractionType:
|
|||
return _fishing_shop
|
||||
|
||||
|
||||
func get_player_storage() -> PlayerStorageInteractionType:
|
||||
return _player_storage
|
||||
|
||||
|
||||
func get_player_spawn_transform() -> Transform3D:
|
||||
return _starter_island.get_player_spawn_transform()
|
||||
|
||||
|
|
@ -75,6 +85,28 @@ func get_spawn_surface_triangles(
|
|||
)
|
||||
|
||||
|
||||
func get_diggable_area_triangles(
|
||||
area_id: StringName,
|
||||
) -> Array[PackedVector3Array]:
|
||||
for region: WorldRegion in _get_regions():
|
||||
var area: DiggableArea3D = region.get_diggable_area(area_id)
|
||||
if area != null:
|
||||
return area.get_surface_triangles()
|
||||
return []
|
||||
|
||||
|
||||
func get_gatherable_spawn_positions(
|
||||
anchor_set_id: StringName,
|
||||
) -> PackedVector3Array:
|
||||
for region: WorldRegion in _get_regions():
|
||||
var anchor_set: GatherableAnchorSet3D = (
|
||||
region.get_gatherable_anchor_set(anchor_set_id)
|
||||
)
|
||||
if anchor_set != null:
|
||||
return anchor_set.get_spawn_positions()
|
||||
return PackedVector3Array()
|
||||
|
||||
|
||||
func _get_regions() -> Array[WorldRegion]:
|
||||
var regions: Array[WorldRegion] = []
|
||||
for child: Node in _regions_root.get_children():
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ enum Phase {
|
|||
}
|
||||
|
||||
const HOURS_PER_DAY: float = 24.0
|
||||
const REAL_SECONDS_PER_CYCLE: float = 60.0 * 60.0
|
||||
const REAL_SECONDS_PER_CYCLE: float = 24.0 * 60.0 * 60.0
|
||||
const HOURS_PER_REAL_SECOND: float = HOURS_PER_DAY / REAL_SECONDS_PER_CYCLE
|
||||
const SYSTEM_CLOCK_SAMPLE_INTERVAL_SECONDS: float = 0.25
|
||||
const DAY_START_HOUR: float = 8.0
|
||||
const NIGHT_START_HOUR: float = 20.0
|
||||
const TRANSITION_HALF_HOURS: float = 0.5
|
||||
|
|
@ -27,10 +28,15 @@ const DEFAULT_START_HOUR: float = DAY_START_HOUR
|
|||
|
||||
var _time_hours: float = DEFAULT_START_HOUR
|
||||
var _persistent_time_hours: float = DEFAULT_START_HOUR
|
||||
var _persistence_tracking_enabled: bool = false
|
||||
var _running: bool = false
|
||||
var _system_clock_authority: bool = false
|
||||
var _editor_time_override: bool = false
|
||||
var _phase: Phase = Phase.DAWN
|
||||
var _last_emitted_clock_minute: int = floori(DEFAULT_START_HOUR * 60.0)
|
||||
var _system_clock_sample_elapsed: float = 0.0
|
||||
var _last_system_unix_seconds: float = -1.0
|
||||
var _calendar_date_id: String = ""
|
||||
var _local_datetime: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -38,17 +44,54 @@ func _ready() -> void:
|
|||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _running:
|
||||
return
|
||||
if _system_clock_authority and not _editor_time_override:
|
||||
_system_clock_sample_elapsed += delta
|
||||
if _system_clock_sample_elapsed >= SYSTEM_CLOCK_SAMPLE_INTERVAL_SECONDS:
|
||||
_system_clock_sample_elapsed = 0.0
|
||||
_sample_system_clock(false)
|
||||
return
|
||||
advance_time(delta)
|
||||
|
||||
|
||||
func begin_session(start_hour: float = DEFAULT_START_HOUR) -> void:
|
||||
func begin_authoritative_session() -> void:
|
||||
_running = true
|
||||
_system_clock_authority = true
|
||||
_editor_time_override = false
|
||||
_system_clock_sample_elapsed = 0.0
|
||||
_last_system_unix_seconds = -1.0
|
||||
set_process(true)
|
||||
_sample_system_clock(true)
|
||||
|
||||
|
||||
func begin_remote_session() -> void:
|
||||
_running = true
|
||||
_system_clock_authority = false
|
||||
_editor_time_override = false
|
||||
_system_clock_sample_elapsed = 0.0
|
||||
_last_system_unix_seconds = -1.0
|
||||
set_process(true)
|
||||
|
||||
|
||||
func begin_test_session(start_hour: float = DEFAULT_START_HOUR) -> void:
|
||||
_running = true
|
||||
_system_clock_authority = false
|
||||
_editor_time_override = false
|
||||
_system_clock_sample_elapsed = 0.0
|
||||
_last_system_unix_seconds = -1.0
|
||||
set_process(false)
|
||||
_set_time_hours(start_hour, true)
|
||||
|
||||
|
||||
func end_session() -> void:
|
||||
_running = false
|
||||
_system_clock_authority = false
|
||||
_editor_time_override = false
|
||||
_system_clock_sample_elapsed = 0.0
|
||||
_last_system_unix_seconds = -1.0
|
||||
_calendar_date_id = ""
|
||||
_local_datetime = {}
|
||||
set_process(false)
|
||||
_set_time_hours(DEFAULT_START_HOUR, true)
|
||||
|
||||
|
|
@ -56,11 +99,8 @@ func end_session() -> void:
|
|||
func advance_time(real_seconds: float) -> void:
|
||||
if not _running or real_seconds <= 0.0:
|
||||
return
|
||||
var advanced_hours := real_seconds * HOURS_PER_REAL_SECOND
|
||||
_set_time_hours(
|
||||
_time_hours + advanced_hours,
|
||||
false,
|
||||
)
|
||||
var advanced_hours: float = real_seconds * HOURS_PER_REAL_SECOND
|
||||
_set_time_hours(_time_hours + advanced_hours, false)
|
||||
natural_time_advanced.emit(advanced_hours)
|
||||
|
||||
|
||||
|
|
@ -71,23 +111,39 @@ func synchronize_time(authoritative_time_hours: float) -> void:
|
|||
|
||||
|
||||
func set_authoritative_time(time_hours: float) -> bool:
|
||||
# This hook intentionally exists only in the Godot editor/test runtime.
|
||||
# Exported clients and dedicated servers always follow their authoritative
|
||||
# machine clock and cannot expose mutable time commands to players.
|
||||
if (
|
||||
not is_finite(time_hours)
|
||||
not OS.has_feature("editor")
|
||||
or not is_finite(time_hours)
|
||||
or time_hours < 0.0
|
||||
or time_hours >= HOURS_PER_DAY
|
||||
):
|
||||
return false
|
||||
_editor_time_override = true
|
||||
_set_time_hours(time_hours, true)
|
||||
authoritative_time_set.emit(_time_hours)
|
||||
return true
|
||||
|
||||
|
||||
func set_persistence_tracking_enabled(enabled: bool) -> void:
|
||||
if _persistence_tracking_enabled == enabled:
|
||||
func clear_editor_time_override() -> void:
|
||||
if not _editor_time_override:
|
||||
return
|
||||
if _persistence_tracking_enabled:
|
||||
_persistent_time_hours = _time_hours
|
||||
_persistence_tracking_enabled = enabled
|
||||
_editor_time_override = false
|
||||
_last_system_unix_seconds = -1.0
|
||||
if _system_clock_authority:
|
||||
_sample_system_clock(true)
|
||||
|
||||
|
||||
func is_using_system_clock() -> bool:
|
||||
return _system_clock_authority and not _editor_time_override
|
||||
|
||||
|
||||
func set_persistence_tracking_enabled(_enabled: bool) -> void:
|
||||
# Kept as a compatibility seam for schema-4 saves. Real-time sessions never
|
||||
# use saved time as an authority.
|
||||
pass
|
||||
|
||||
|
||||
func restore_persistent_time_hours(time_hours: float) -> bool:
|
||||
|
|
@ -98,19 +154,27 @@ func restore_persistent_time_hours(time_hours: float) -> bool:
|
|||
):
|
||||
return false
|
||||
_persistent_time_hours = time_hours
|
||||
if _persistence_tracking_enabled:
|
||||
_set_time_hours(_persistent_time_hours, true)
|
||||
return true
|
||||
|
||||
|
||||
func get_persistent_time_hours() -> float:
|
||||
return _persistent_time_hours
|
||||
return _time_hours if _running else _persistent_time_hours
|
||||
|
||||
|
||||
func get_time_hours() -> float:
|
||||
return _time_hours
|
||||
|
||||
|
||||
func get_calendar_date_id() -> String:
|
||||
return _calendar_date_id
|
||||
|
||||
|
||||
func get_calendar_cycle_id(rollover_hour: float) -> String:
|
||||
if _local_datetime.is_empty():
|
||||
return ""
|
||||
return calendar_cycle_id_from_datetime(_local_datetime, rollover_hour)
|
||||
|
||||
|
||||
func get_phase() -> Phase:
|
||||
return _phase
|
||||
|
||||
|
|
@ -127,6 +191,40 @@ func get_clock_text() -> String:
|
|||
return format_clock_time(_time_hours)
|
||||
|
||||
|
||||
static func time_hours_from_datetime(datetime: Dictionary) -> float:
|
||||
var hour: int = clampi(int(datetime.get("hour", 0)), 0, 23)
|
||||
var minute: int = clampi(int(datetime.get("minute", 0)), 0, 59)
|
||||
var second: int = clampi(int(datetime.get("second", 0)), 0, 59)
|
||||
return (
|
||||
float(hour)
|
||||
+ float(minute) / 60.0
|
||||
+ float(second) / (60.0 * 60.0)
|
||||
)
|
||||
|
||||
|
||||
static func date_id_from_datetime(datetime: Dictionary) -> String:
|
||||
return "%04d-%02d-%02d" % [
|
||||
int(datetime.get("year", 0)),
|
||||
int(datetime.get("month", 0)),
|
||||
int(datetime.get("day", 0)),
|
||||
]
|
||||
|
||||
|
||||
static func calendar_cycle_id_from_datetime(
|
||||
datetime: Dictionary,
|
||||
rollover_hour: float,
|
||||
) -> String:
|
||||
var cycle_datetime: Dictionary = datetime.duplicate(true)
|
||||
if time_hours_from_datetime(datetime) < rollover_hour:
|
||||
var previous_day_unix: int = (
|
||||
Time.get_unix_time_from_datetime_dict(datetime) - 24 * 60 * 60
|
||||
)
|
||||
cycle_datetime = Time.get_datetime_dict_from_unix_time(
|
||||
previous_day_unix
|
||||
)
|
||||
return date_id_from_datetime(cycle_datetime)
|
||||
|
||||
|
||||
static func phase_for_hour(time_hours: float) -> Phase:
|
||||
var hour: float = _normalized_hour(time_hours)
|
||||
if hour >= DAWN_START_HOUR and hour < DAWN_END_HOUR:
|
||||
|
|
@ -153,6 +251,21 @@ static func format_clock_time(time_hours: float) -> String:
|
|||
]
|
||||
|
||||
|
||||
func _sample_system_clock(force_emit: bool) -> void:
|
||||
var datetime: Dictionary = Time.get_datetime_dict_from_system(false)
|
||||
var unix_seconds: float = Time.get_unix_time_from_system()
|
||||
var previous_unix_seconds: float = _last_system_unix_seconds
|
||||
_last_system_unix_seconds = unix_seconds
|
||||
_local_datetime = datetime.duplicate(true)
|
||||
_calendar_date_id = date_id_from_datetime(datetime)
|
||||
_set_time_hours(time_hours_from_datetime(datetime), force_emit)
|
||||
if previous_unix_seconds < 0.0:
|
||||
return
|
||||
var advanced_seconds: float = unix_seconds - previous_unix_seconds
|
||||
if advanced_seconds > 0.0:
|
||||
natural_time_advanced.emit(advanced_seconds / 3600.0)
|
||||
|
||||
|
||||
func _set_time_hours(time_hours: float, force_emit: bool) -> void:
|
||||
var normalized: float = _normalized_hour(time_hours)
|
||||
var next_phase: Phase = phase_for_hour(normalized)
|
||||
|
|
@ -163,8 +276,6 @@ func _set_time_hours(time_hours: float, force_emit: bool) -> void:
|
|||
)
|
||||
_time_hours = normalized
|
||||
_phase = next_phase
|
||||
if _persistence_tracking_enabled:
|
||||
_persistent_time_hours = normalized
|
||||
if phase_was_changed:
|
||||
phase_changed.emit(_phase)
|
||||
if force_emit or clock_minute_changed or phase_was_changed:
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ const WARM_SUN := Color(1.0, 0.58, 0.30)
|
|||
const WARM_WATER_TINT := Color(0.78, 0.62, 0.58)
|
||||
const WARM_SUN_DISC := Color(1.0, 0.45, 0.16)
|
||||
const MOON_DISC := Color(0.78, 0.88, 1.0)
|
||||
const STAR_COLOR := Color(0.72, 0.82, 0.96)
|
||||
const STAR_STRENGTH: float = 0.72
|
||||
const DAY_CLOUD_LIGHT := Color(0.70, 0.76, 0.77)
|
||||
const DAY_CLOUD_SHADOW := Color(0.31, 0.38, 0.40)
|
||||
const NIGHT_CLOUD_LIGHT := Color(0.18, 0.22, 0.30)
|
||||
|
|
@ -425,6 +427,12 @@ func _apply_time(time_hours: float) -> void:
|
|||
"moon_visibility",
|
||||
(1.0 - daylight) * _weather_value(1.0, 0.72, 0.36, 1.0),
|
||||
)
|
||||
_sky_material.set_shader_parameter("star_color", STAR_COLOR)
|
||||
_sky_material.set_shader_parameter("star_strength", STAR_STRENGTH)
|
||||
_sky_material.set_shader_parameter(
|
||||
"star_visibility",
|
||||
(1.0 - daylight) * _weather_value(1.0, 0.18, 0.03, 0.0),
|
||||
)
|
||||
_sun.light_color = _blended_color(
|
||||
NIGHT_SUN, DAY_SUN, WARM_SUN, daylight, warmth
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,13 +11,22 @@ enum Weather {
|
|||
}
|
||||
|
||||
const DEFAULT_WEATHER: Weather = Weather.SUNNY
|
||||
const SUNNY_DURATION_RANGE := Vector2(480.0, 900.0)
|
||||
const CLOUDY_DURATION_RANGE := Vector2(300.0, 720.0)
|
||||
const RAINY_DURATION_RANGE := Vector2(300.0, 600.0)
|
||||
const FOGGY_DURATION_RANGE := Vector2(300.0, 600.0)
|
||||
const MAX_PERSISTED_SECONDS: float = 1800.0
|
||||
const DAILY_PLAN_SEGMENT_HOURS: float = 2.0
|
||||
const DAILY_PLAN_SEGMENT_COUNT: int = 12
|
||||
const WEATHER_PERIOD_SECONDS: float = 60.0 * 60.0
|
||||
const SUNNY_DURATION_RANGE := Vector2(
|
||||
WEATHER_PERIOD_SECONDS, WEATHER_PERIOD_SECONDS
|
||||
)
|
||||
const CLOUDY_DURATION_RANGE := Vector2(
|
||||
WEATHER_PERIOD_SECONDS, WEATHER_PERIOD_SECONDS
|
||||
)
|
||||
const RAINY_DURATION_RANGE := Vector2(
|
||||
WEATHER_PERIOD_SECONDS, WEATHER_PERIOD_SECONDS
|
||||
)
|
||||
const FOGGY_DURATION_RANGE := Vector2(
|
||||
WEATHER_PERIOD_SECONDS, WEATHER_PERIOD_SECONDS
|
||||
)
|
||||
const MAX_PERSISTED_SECONDS: float = WEATHER_PERIOD_SECONDS
|
||||
const DAILY_PLAN_SEGMENT_HOURS: float = 1.0
|
||||
const DAILY_PLAN_SEGMENT_COUNT: int = 24
|
||||
|
||||
var _weather: Weather = DEFAULT_WEATHER
|
||||
var _seconds_remaining: float = SUNNY_DURATION_RANGE.x
|
||||
|
|
@ -48,18 +57,11 @@ func begin_authoritative_session(seed_value: int) -> void:
|
|||
set_process(true)
|
||||
_clear_manual_override()
|
||||
if _daily_schedule.is_empty() or _world_time == null:
|
||||
if _has_persistent_state:
|
||||
_set_weather(
|
||||
_persistent_weather,
|
||||
_persistent_seconds_remaining,
|
||||
true,
|
||||
)
|
||||
else:
|
||||
_set_weather(
|
||||
DEFAULT_WEATHER,
|
||||
_roll_duration(DEFAULT_WEATHER),
|
||||
true,
|
||||
)
|
||||
_set_weather(
|
||||
DEFAULT_WEATHER,
|
||||
_roll_duration(DEFAULT_WEATHER),
|
||||
true,
|
||||
)
|
||||
else:
|
||||
_update_scheduled_weather(true)
|
||||
|
||||
|
|
@ -104,7 +106,12 @@ func apply_authoritative_snapshot(
|
|||
|
||||
|
||||
func set_authoritative_weather(weather: Weather) -> bool:
|
||||
if not _running_authority or not is_valid_weather(int(weather)):
|
||||
# Manual weather is an editor/test hook, not a player or operator command.
|
||||
if (
|
||||
not OS.has_feature("editor")
|
||||
or not _running_authority
|
||||
or not is_valid_weather(int(weather))
|
||||
):
|
||||
return false
|
||||
var duration: float = _roll_duration(weather)
|
||||
if not _daily_schedule.is_empty() and _world_time != null:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue