Add gathering, unified inventory, and real-time world

This commit is contained in:
Alexander Sellite 2026-08-18 18:19:36 -04:00
parent 173371e6fc
commit fa57edca83
113 changed files with 6700 additions and 1307 deletions

View 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

View file

@ -0,0 +1 @@
uid://d33f4164b78ww

View file

@ -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);

View file

@ -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")

View 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

View file

@ -0,0 +1 @@
uid://b6xfixi21qllm

View 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)

View 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)

View file

@ -0,0 +1 @@
uid://7sl42teh8qgb

View file

@ -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

View file

@ -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)

View file

@ -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():

View file

@ -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:

View file

@ -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
)

View file

@ -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: