347 lines
8.5 KiB
GDScript
347 lines
8.5 KiB
GDScript
class_name WorldWeatherService
|
|
extends Node
|
|
|
|
signal weather_changed(weather: Weather, seconds_remaining: float)
|
|
|
|
enum Weather {
|
|
SUNNY,
|
|
CLOUDY,
|
|
RAINY,
|
|
FOGGY,
|
|
}
|
|
|
|
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
|
|
|
|
var _weather: Weather = DEFAULT_WEATHER
|
|
var _seconds_remaining: float = SUNNY_DURATION_RANGE.x
|
|
var _persistent_weather: Weather = DEFAULT_WEATHER
|
|
var _persistent_seconds_remaining: float = SUNNY_DURATION_RANGE.x
|
|
var _has_persistent_state: bool = false
|
|
var _persistence_tracking_enabled: bool = false
|
|
var _running_authority: bool = false
|
|
var _rng := RandomNumberGenerator.new()
|
|
var _daily_plan_id: String = ""
|
|
var _daily_schedule: Array[Dictionary] = []
|
|
var _world_time: WorldTimeService
|
|
|
|
|
|
func _ready() -> void:
|
|
set_process(false)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
advance_weather(delta)
|
|
|
|
|
|
func begin_authoritative_session(seed_value: int) -> void:
|
|
_rng.seed = seed_value
|
|
_running_authority = true
|
|
set_process(true)
|
|
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,
|
|
)
|
|
else:
|
|
_update_scheduled_weather(true)
|
|
|
|
|
|
func begin_remote_session() -> void:
|
|
_running_authority = false
|
|
set_process(false)
|
|
_set_weather(DEFAULT_WEATHER, SUNNY_DURATION_RANGE.x, true)
|
|
|
|
|
|
func end_session() -> void:
|
|
_running_authority = false
|
|
set_process(false)
|
|
_set_weather(DEFAULT_WEATHER, SUNNY_DURATION_RANGE.x, true)
|
|
|
|
|
|
func advance_weather(real_seconds: float) -> void:
|
|
if not _running_authority or real_seconds <= 0.0:
|
|
return
|
|
if not _daily_schedule.is_empty() and _world_time != null:
|
|
_update_scheduled_weather(false)
|
|
return
|
|
_seconds_remaining -= real_seconds
|
|
while _seconds_remaining <= 0.0:
|
|
var overrun: float = -_seconds_remaining
|
|
var next_weather: Weather = _choose_next_weather()
|
|
_set_weather(next_weather, _roll_duration(next_weather), true)
|
|
_seconds_remaining -= overrun
|
|
if _persistence_tracking_enabled:
|
|
_store_persistent_state()
|
|
|
|
|
|
func apply_authoritative_snapshot(
|
|
weather: Weather,
|
|
seconds_remaining: float,
|
|
) -> void:
|
|
if not is_valid_weather(int(weather)) or not is_finite(seconds_remaining):
|
|
return
|
|
_set_weather(weather, maxf(seconds_remaining, 0.0), false)
|
|
|
|
|
|
func set_persistence_tracking_enabled(enabled: bool) -> void:
|
|
if _persistence_tracking_enabled == enabled:
|
|
return
|
|
if _persistence_tracking_enabled:
|
|
_store_persistent_state()
|
|
_persistence_tracking_enabled = enabled
|
|
|
|
|
|
func restore_persistent_state(
|
|
weather: Weather,
|
|
seconds_remaining: float,
|
|
) -> bool:
|
|
if not is_valid_persistent_state(weather, seconds_remaining):
|
|
return false
|
|
_persistent_weather = weather
|
|
_persistent_seconds_remaining = seconds_remaining
|
|
_has_persistent_state = true
|
|
if _persistence_tracking_enabled:
|
|
_set_weather(
|
|
_persistent_weather,
|
|
_persistent_seconds_remaining,
|
|
true,
|
|
)
|
|
return true
|
|
|
|
|
|
func reset_persistent_state() -> void:
|
|
_has_persistent_state = false
|
|
_persistent_weather = DEFAULT_WEATHER
|
|
_persistent_seconds_remaining = SUNNY_DURATION_RANGE.x
|
|
if _persistence_tracking_enabled and _running_authority:
|
|
_set_weather(
|
|
DEFAULT_WEATHER,
|
|
_roll_duration(DEFAULT_WEATHER),
|
|
true,
|
|
)
|
|
|
|
|
|
func has_persistent_state() -> bool:
|
|
return _has_persistent_state
|
|
|
|
|
|
func get_persistent_weather() -> Weather:
|
|
return _persistent_weather
|
|
|
|
|
|
func get_persistent_seconds_remaining() -> float:
|
|
return _persistent_seconds_remaining
|
|
|
|
|
|
func get_weather() -> Weather:
|
|
return _weather
|
|
|
|
|
|
func get_seconds_remaining() -> float:
|
|
return _seconds_remaining
|
|
|
|
|
|
func is_raining() -> bool:
|
|
return _weather == Weather.RAINY
|
|
|
|
|
|
func is_foggy() -> bool:
|
|
return _weather == Weather.FOGGY
|
|
|
|
|
|
func get_weather_name() -> String:
|
|
return weather_name(_weather)
|
|
|
|
|
|
func configure_daily_plan(
|
|
plan_id: String,
|
|
schedule: Array,
|
|
world_time: WorldTimeService,
|
|
) -> bool:
|
|
if (
|
|
plan_id.is_empty()
|
|
or not is_valid_daily_plan_schedule(schedule)
|
|
or world_time == null
|
|
):
|
|
return false
|
|
_daily_plan_id = plan_id
|
|
_daily_schedule.clear()
|
|
for value: Variant in schedule:
|
|
_daily_schedule.append((value as Dictionary).duplicate(true))
|
|
_world_time = world_time
|
|
if _running_authority:
|
|
_update_scheduled_weather(true)
|
|
return true
|
|
|
|
|
|
func clear_daily_plan() -> void:
|
|
_daily_plan_id = ""
|
|
_daily_schedule.clear()
|
|
_world_time = null
|
|
|
|
|
|
func get_daily_plan_id() -> String:
|
|
return _daily_plan_id
|
|
|
|
|
|
static func is_valid_weather(value: int) -> bool:
|
|
return value >= Weather.SUNNY and value <= Weather.FOGGY
|
|
|
|
|
|
static func is_valid_persistent_state(
|
|
weather: Weather,
|
|
seconds_remaining: float,
|
|
) -> bool:
|
|
return (
|
|
is_valid_weather(int(weather))
|
|
and is_finite(seconds_remaining)
|
|
and seconds_remaining >= 0.0
|
|
and seconds_remaining <= MAX_PERSISTED_SECONDS
|
|
)
|
|
|
|
|
|
static func is_valid_daily_plan_schedule(value: Variant) -> bool:
|
|
if typeof(value) != TYPE_ARRAY:
|
|
return false
|
|
var schedule: Array = value
|
|
if schedule.size() != DAILY_PLAN_SEGMENT_COUNT:
|
|
return false
|
|
for entry_value: Variant in schedule:
|
|
if typeof(entry_value) != TYPE_DICTIONARY:
|
|
return false
|
|
var entry: Dictionary = entry_value
|
|
if (
|
|
typeof(entry.get("start_hour")) not in [TYPE_FLOAT, TYPE_INT]
|
|
or not is_finite(float(entry.get("start_hour", -1.0)))
|
|
or float(entry.get("start_hour", -1.0)) < 0.0
|
|
or float(entry.get("start_hour", -1.0)) >= 24.0
|
|
or not _is_bounded_integer(
|
|
entry.get("weather"), Weather.SUNNY, Weather.FOGGY
|
|
)
|
|
or not is_valid_weather(int(entry.get("weather", -1)))
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
static func _is_bounded_integer(
|
|
value: Variant,
|
|
minimum: int,
|
|
maximum: int,
|
|
) -> bool:
|
|
if typeof(value) == TYPE_INT:
|
|
return int(value) >= minimum and int(value) <= maximum
|
|
if typeof(value) != TYPE_FLOAT:
|
|
return false
|
|
var number: float = float(value)
|
|
return (
|
|
is_finite(number)
|
|
and number >= float(minimum)
|
|
and number <= float(maximum)
|
|
and is_equal_approx(number, round(number))
|
|
)
|
|
|
|
|
|
static func weather_name(weather: Weather) -> String:
|
|
match weather:
|
|
Weather.SUNNY:
|
|
return "sunny"
|
|
Weather.CLOUDY:
|
|
return "cloudy"
|
|
Weather.RAINY:
|
|
return "rainy"
|
|
Weather.FOGGY:
|
|
return "foggy"
|
|
return "unknown"
|
|
|
|
|
|
func _set_weather(
|
|
weather: Weather,
|
|
seconds_remaining: float,
|
|
force_emit: bool,
|
|
) -> void:
|
|
var changed: bool = weather != _weather
|
|
_weather = weather
|
|
_seconds_remaining = maxf(seconds_remaining, 0.0)
|
|
if _persistence_tracking_enabled:
|
|
_store_persistent_state()
|
|
if force_emit or changed:
|
|
weather_changed.emit(_weather, _seconds_remaining)
|
|
|
|
|
|
func _store_persistent_state() -> void:
|
|
_persistent_weather = _weather
|
|
_persistent_seconds_remaining = _seconds_remaining
|
|
_has_persistent_state = true
|
|
|
|
|
|
func _choose_next_weather() -> Weather:
|
|
match _weather:
|
|
Weather.SUNNY:
|
|
return Weather.CLOUDY
|
|
Weather.RAINY, Weather.FOGGY:
|
|
return Weather.CLOUDY
|
|
Weather.CLOUDY:
|
|
var roll: float = _rng.randf()
|
|
if roll < 0.45:
|
|
return Weather.SUNNY
|
|
if roll < 0.80:
|
|
return Weather.RAINY
|
|
return Weather.FOGGY
|
|
return Weather.SUNNY
|
|
|
|
|
|
func _roll_duration(weather: Weather) -> float:
|
|
var duration_range: Vector2 = _duration_range(weather)
|
|
return _rng.randf_range(duration_range.x, duration_range.y)
|
|
|
|
|
|
func _duration_range(weather: Weather) -> Vector2:
|
|
match weather:
|
|
Weather.SUNNY:
|
|
return SUNNY_DURATION_RANGE
|
|
Weather.CLOUDY:
|
|
return CLOUDY_DURATION_RANGE
|
|
Weather.RAINY:
|
|
return RAINY_DURATION_RANGE
|
|
Weather.FOGGY:
|
|
return FOGGY_DURATION_RANGE
|
|
return SUNNY_DURATION_RANGE
|
|
|
|
|
|
func _update_scheduled_weather(force_emit: bool) -> void:
|
|
if _daily_schedule.is_empty() or _world_time == null:
|
|
return
|
|
var elapsed_hours: float = fposmod(
|
|
_world_time.get_time_hours() - WorldTimeService.DAY_START_HOUR,
|
|
WorldTimeService.HOURS_PER_DAY,
|
|
)
|
|
var segment_hours: float = DAILY_PLAN_SEGMENT_HOURS
|
|
var segment_index: int = clampi(
|
|
floori(elapsed_hours / segment_hours),
|
|
0,
|
|
_daily_schedule.size() - 1,
|
|
)
|
|
var entry: Dictionary = _daily_schedule[segment_index]
|
|
var weather: Weather = int(entry.get("weather", DEFAULT_WEATHER)) as Weather
|
|
var next_boundary: float = float(segment_index + 1) * segment_hours
|
|
var hours_remaining: float = maxf(next_boundary - elapsed_hours, 0.0)
|
|
var seconds_remaining: float = (
|
|
hours_remaining / WorldTimeService.HOURS_PER_REAL_SECOND
|
|
)
|
|
_set_weather(weather, seconds_remaining, force_emit)
|