straywild/tests/world_time_validation.gd

571 lines
20 KiB
GDScript

extends SceneTree
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldTimeVisualControllerType = preload(
"res://world/world_time_visual_controller.gd"
)
const NetworkWorldTimeServiceType = preload(
"res://network/network_world_time_service.gd"
)
const FishingContextType = preload("res://fishing/fishing_context.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd"
)
const WeatherIconType = preload("res://ui/weather_icon.gd")
const CalendarSeasonType = preload("res://world/calendar_season.gd")
const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
const SaltWaterMaterial: ShaderMaterial = preload(
"res://world/materials/stylized_water.tres"
)
const FreshWaterMaterial: ShaderMaterial = preload(
"res://world/materials/stylized_water_fresh.tres"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_validate_clock_boundaries_and_duration()
_validate_system_clock_authority()
_validate_persistent_host_clock()
_validate_fishing_availability()
_validate_fishing_spot_context()
_validate_network_snapshot_bounds()
_validate_environment_presentation()
_validate_weather_clock_icon()
print("World time validation: PASS")
quit()
func _validate_clock_boundaries_and_duration() -> void:
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
assert(is_equal_approx(
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
1.0 / 3600.0,
))
assert(is_equal_approx(
WorldTimeServiceType.time_hours_from_datetime({
"hour": 17, "minute": 30, "second": 18,
}),
17.505,
))
assert(
WorldTimeServiceType.date_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
}) == "2026-08-18"
)
assert(WorldTimeServiceType.is_valid_calendar_date_id("2026-08-18"))
assert(WorldTimeServiceType.is_valid_calendar_date_id("2028-02-29"))
assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-02-29"))
assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-13-01"))
assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-8-18"))
assert(
WorldTimeServiceType.format_calendar_date("2026-08-18")
== "tuesday, august 18"
)
assert(
CalendarSeasonType.from_date_id("2026-03-01")
== CalendarSeasonType.Season.SPRING
)
assert(
CalendarSeasonType.from_date_id("2026-06-01")
== CalendarSeasonType.Season.SUMMER
)
assert(
CalendarSeasonType.from_date_id("2026-09-01")
== CalendarSeasonType.Season.FALL
)
assert(
CalendarSeasonType.from_date_id("2026-12-01")
== CalendarSeasonType.Season.WINTER
)
assert(
CalendarSeasonType.from_date_id("invalid")
== CalendarSeasonType.UNKNOWN
)
assert(
WorldTimeServiceType.calendar_cycle_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
"hour": 7, "minute": 59, "second": 0,
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-17"
)
assert(
WorldTimeServiceType.calendar_cycle_id_from_datetime({
"year": 2026, "month": 8, "day": 18,
"hour": 8, "minute": 0, "second": 0,
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-18"
)
assert(
WorldTimeServiceType.phase_for_hour(7.49)
== WorldTimeServiceType.Phase.NIGHT
)
assert(
WorldTimeServiceType.phase_for_hour(7.5)
== WorldTimeServiceType.Phase.DAWN
)
assert(
WorldTimeServiceType.phase_for_hour(8.49)
== WorldTimeServiceType.Phase.DAWN
)
assert(
WorldTimeServiceType.phase_for_hour(8.5)
== WorldTimeServiceType.Phase.DAY
)
assert(
WorldTimeServiceType.phase_for_hour(19.5)
== WorldTimeServiceType.Phase.DUSK
)
assert(
WorldTimeServiceType.phase_for_hour(20.5)
== WorldTimeServiceType.Phase.NIGHT
)
assert(WorldTimeServiceType.format_clock_time(0.0) == "12:00 am")
assert(WorldTimeServiceType.format_clock_time(8.0) == "8:00 am")
assert(WorldTimeServiceType.format_clock_time(20.5) == "8:30 pm")
var clock := WorldTimeServiceType.new()
root.add_child(clock)
clock.begin_test_session(8.0, "2026-08-18")
assert(clock.get_calendar_date_id() == "2026-08-18")
assert(clock.get_calendar_text() == "tuesday, august 18")
assert(clock.get_season() == CalendarSeasonType.Season.SUMMER)
assert(clock.get_season_text() == "summer")
clock.advance_time(12.0 * 60.0 * 60.0)
assert(is_equal_approx(clock.get_time_hours(), 20.0))
assert(clock.is_night_period())
assert(clock.is_transition())
clock.advance_time(12.0 * 60.0 * 60.0)
assert(is_equal_approx(clock.get_time_hours(), 8.0))
assert(not clock.is_night_period())
assert(clock.is_transition())
# Small high-refresh deltas must still advance the displayed minute. Using
# is_equal_approx() per frame used to suppress every clock notification.
var emitted_times: Array[float] = []
clock.time_changed.connect(
func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
emitted_times.append(time_hours)
)
clock.begin_test_session(12.0 + 31.0 / 60.0)
emitted_times.clear()
for _frame: int in 7201:
clock.advance_time(1.0 / 60.0)
assert(clock.get_clock_text() == "12:33 pm")
assert(emitted_times.size() >= 2)
clock.queue_free()
func _validate_persistent_host_clock() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
assert(clock.restore_persistent_time_hours(18.75))
clock.begin_test_session(18.75)
assert(is_equal_approx(clock.get_time_hours(), 18.75))
clock.synchronize_time(19.25)
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
clock.synchronize_time(6.5)
assert(is_equal_approx(clock.get_time_hours(), 6.5))
assert(is_equal_approx(clock.get_persistent_time_hours(), 6.5))
assert(not clock.restore_persistent_time_hours(-1.0))
assert(not clock.restore_persistent_time_hours(24.0))
clock.queue_free()
func _validate_system_clock_authority() -> void:
var clock := WorldTimeServiceType.new()
root.add_child(clock)
clock.begin_authoritative_session()
var system_datetime: Dictionary = Time.get_datetime_dict_from_system(false)
var system_hour: float = WorldTimeServiceType.time_hours_from_datetime(
system_datetime
)
assert(clock.is_using_system_clock())
assert(_wrapped_time_difference(
clock.get_time_hours(), system_hour
) < 2.0 / 3600.0)
assert(
clock.get_calendar_date_id()
== WorldTimeServiceType.date_id_from_datetime(system_datetime)
)
assert(clock.set_authoritative_time(12.0))
assert(not clock.is_using_system_clock())
clock.clear_editor_time_override()
assert(clock.is_using_system_clock())
clock.queue_free()
func _validate_fishing_availability() -> void:
var day_context := FishingContextType.new()
day_context.location_tags = [&"starter_pond"]
day_context.is_night = false
var night_context := FishingContextType.new()
night_context.location_tags = [&"starter_pond"]
night_context.is_night = true
var transition_context := FishingContextType.new()
transition_context.location_tags = [&"starter_pond"]
transition_context.is_night = true
transition_context.is_day_night_transition = true
for fish_id: StringName in [&"bluegill", &"sunfish"]:
var day_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
assert(day_fish.availability.is_available(day_context))
assert(not day_fish.availability.is_available(night_context))
assert(day_fish.availability.is_available(transition_context))
for fish_id: StringName in [
&"catfish_blue",
&"catfish_channel",
&"catfish_flathead",
&"catfish_white",
]:
var night_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
assert(not night_fish.availability.is_available(day_context))
assert(night_fish.availability.is_available(night_context))
assert(night_fish.availability.is_available(transition_context))
for fish_id: StringName in [
&"bass",
&"carp",
&"tuna_albacore",
&"tuna_bigeye",
&"tuna_bluefin",
&"tuna_skipjack",
&"tuna_yellowfin",
&"goby_round",
&"salmon_atlantic",
&"salmon_chum",
&"salmon_coho",
&"salmon_pink",
&"salmon_sockeye",
]:
var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
assert(all_time_fish.availability.is_available(day_context))
assert(all_time_fish.availability.is_available(night_context))
assert(all_time_fish.availability.is_available(transition_context))
func _validate_fishing_spot_context() -> void:
var clock := WorldTimeServiceType.new()
clock.begin_test_session(20.25, "2026-12-18")
var fishing_spot := FishingSpotType.new()
fishing_spot.set("_world_time", clock)
var region := FishableWaterRegionType.new()
region.location_tags = [&"starter_pond"]
region.water_type = WaterType.Type.FRESH_WATER
var dusk_context: FishingContext = fishing_spot.build_network_context(region)
assert(dusk_context.is_night)
assert(dusk_context.is_day_night_transition)
assert(dusk_context.season == CalendarSeasonType.Season.WINTER)
clock.synchronize_time(14.0)
var day_context: FishingContext = fishing_spot.build_network_context(region)
assert(not day_context.is_night)
assert(not day_context.is_day_night_transition)
region.free()
fishing_spot.free()
clock.free()
func _validate_network_snapshot_bounds() -> void:
assert(NetworkWorldTimeServiceType.validate_snapshot({
"session_id": "session",
"time_hours": 8.25,
"calendar_date_id": "2026-08-18",
"sequence": 1,
}))
assert(not NetworkWorldTimeServiceType.validate_snapshot({
"session_id": "session",
"time_hours": 24.0,
"calendar_date_id": "2026-08-18",
"sequence": 1,
}))
assert(not NetworkWorldTimeServiceType.validate_snapshot({
"session_id": "",
"time_hours": 8.0,
"calendar_date_id": "2026-08-18",
"sequence": 1,
}))
assert(not NetworkWorldTimeServiceType.validate_snapshot({
"session_id": "session",
"time_hours": 8.0,
"calendar_date_id": "2026-02-29",
"sequence": 1,
}))
func _validate_environment_presentation() -> void:
var world_root := Node3D.new()
root.add_child(world_root)
var world_environment := WorldEnvironment.new()
var environment := Environment.new()
var sky := Sky.new()
var sky_material := ShaderMaterial.new()
sky_material.shader = preload(
"res://world/environment/netfishing_sky.gdshader"
)
sky.sky_material = sky_material
environment.sky = sky
environment.adjustment_enabled = true
environment.fog_enabled = true
world_environment.environment = environment
world_root.add_child(world_environment)
var sun := DirectionalLight3D.new()
world_root.add_child(sun)
var clock := WorldTimeServiceType.new()
world_root.add_child(clock)
var visuals := WorldTimeVisualControllerType.new()
world_root.add_child(visuals)
visuals.setup(clock, world_environment, sun)
visuals.apply_time_immediately(12.0)
var runtime_environment: Environment = world_environment.environment
var runtime_sky_material := (
runtime_environment.sky.sky_material as ShaderMaterial
)
var day_horizon_value: Variant = runtime_sky_material.get_shader_parameter(
"sky_horizon_color"
)
assert(day_horizon_value is Color)
var day_horizon: Color = day_horizon_value as Color
var day_sun_visibility: float = float(
runtime_sky_material.get_shader_parameter("sun_visibility")
)
var day_sun_direction: Vector3 = (
runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
)
assert(day_sun_visibility > 0.99)
assert(day_sun_direction.y > 0.85)
assert(
float(runtime_sky_material.get_shader_parameter("moon_visibility"))
< 0.01
)
assert(
float(runtime_sky_material.get_shader_parameter("star_visibility"))
< 0.01
)
var day_ambient_energy: float = runtime_environment.ambient_light_energy
assert(not runtime_environment.fog_enabled)
assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
var water_shader: Shader = preload(
"res://world/materials/stylized_water.gdshader"
)
var sky_shader: Shader = preload(
"res://world/environment/netfishing_sky.gdshader"
)
assert("uniform float fog_horizon_occlusion" in sky_shader.code)
assert("fog_horizon_color.rgb" in sky_shader.code)
assert("uniform float star_visibility" in sky_shader.code)
assert("float procedural_star_field" in sky_shader.code)
assert("upper_sky_fade" in sky_shader.code)
assert("fog_disabled" in water_shader.code)
assert("surface_view_position = view_vertex.xyz" in water_shader.code)
assert("surface_view_distance = length(surface_view_position)" in water_shader.code)
assert(
"max(weather_fog_end, weather_fog_begin + 1.0),\n"
+ "\t\tsurface_view_distance"
in water_shader.code
)
assert("weather_fog_near_amount" in water_shader.code)
assert("float weather_fog_alpha = mix(" in water_shader.code)
assert("ALPHA = clamp(water_alpha, 0.1, weather_fog_alpha)" in water_shader.code)
assert(
"water_alpha *= mix(0.82, 1.0, contact_stability);"
in water_shader.code
)
assert(is_zero_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
)))
assert(is_equal_approx(float(
SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
), WorldTimeVisualController.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
var day_water_brightness := float(
SaltWaterMaterial.get_shader_parameter("environment_brightness")
)
assert(day_water_brightness > 0.99)
assert(is_equal_approx(
float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
day_water_brightness,
))
visuals.apply_time_immediately(0.0)
var night_horizon: Color = runtime_sky_material.get_shader_parameter(
"sky_horizon_color"
) as Color
assert(night_horizon != day_horizon)
assert(
float(runtime_sky_material.get_shader_parameter("sun_visibility"))
< 0.01
)
var night_moon_visibility: float = float(
runtime_sky_material.get_shader_parameter("moon_visibility")
)
var night_moon_direction: Vector3 = (
runtime_sky_material.get_shader_parameter("moon_direction") as Vector3
)
var night_sun_direction: Vector3 = (
runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
)
assert(night_moon_visibility > 0.99)
assert(
float(runtime_sky_material.get_shader_parameter("star_visibility"))
> 0.99
)
assert((
runtime_sky_material.get_shader_parameter("star_color") as Color
).is_equal_approx(WorldTimeVisualController.STAR_COLOR))
assert(is_equal_approx(
float(runtime_sky_material.get_shader_parameter("star_strength")),
WorldTimeVisualController.STAR_STRENGTH,
))
assert(night_moon_direction.y > 0.85)
assert(night_moon_direction.is_equal_approx(-night_sun_direction))
assert(runtime_environment.ambient_light_energy < day_ambient_energy)
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
)))
var night_water_brightness := float(
SaltWaterMaterial.get_shader_parameter("environment_brightness")
)
assert(night_water_brightness < day_water_brightness * 0.4)
assert(is_equal_approx(
float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
night_water_brightness,
))
var night_water_tint := (
SaltWaterMaterial.get_shader_parameter("environment_tint") as Color
)
assert(night_water_tint.get_luminance() < 0.5)
var night_background_energy := runtime_environment.background_energy_multiplier
var night_ambient_energy := runtime_environment.ambient_light_energy
var night_fog_light_energy := runtime_environment.fog_light_energy
var night_adjustment_brightness := runtime_environment.adjustment_brightness
var night_adjustment_saturation := runtime_environment.adjustment_saturation
visuals.apply_weather_immediately(WorldWeatherService.Weather.FOGGY)
visuals.apply_time_immediately(0.0)
assert(runtime_environment.fog_enabled)
assert(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
) > 0.99)
assert(is_equal_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
), WorldTimeVisualController.FOGGY_NIGHT_WATER_NEAR_FOG_AMOUNT))
assert(is_equal_approx(float(
SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
), WorldTimeVisualController.FOGGY_NIGHT_SURFACE_HIGHLIGHT_FLOOR))
assert(is_equal_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
), 1.0))
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("star_visibility")
)))
assert((
runtime_sky_material.get_shader_parameter("fog_horizon_color") as Color
).is_equal_approx(WorldTimeVisualController.NIGHT_FOG))
var night_rendered_fog_color := (
runtime_environment.fog_light_color.srgb_to_linear()
)
night_rendered_fog_color.r *= runtime_environment.fog_light_energy
night_rendered_fog_color.g *= runtime_environment.fog_light_energy
night_rendered_fog_color.b *= runtime_environment.fog_light_energy
night_rendered_fog_color.a = 1.0
night_rendered_fog_color = night_rendered_fog_color.linear_to_srgb()
var night_water_fog_color := (
SaltWaterMaterial.get_shader_parameter("weather_fog_color") as Color
)
var corrected_night_fog_color := night_rendered_fog_color
corrected_night_fog_color.r *= (
WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.r
)
corrected_night_fog_color.g *= (
WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.g
)
corrected_night_fog_color.b *= (
WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.b
)
assert(night_water_fog_color.is_equal_approx(corrected_night_fog_color))
var foggy_sky_horizon := (
runtime_sky_material.get_shader_parameter("sky_horizon_color") as Color
)
assert(foggy_sky_horizon.is_equal_approx(night_horizon))
assert(is_equal_approx(runtime_environment.fog_sky_affect, 1.0))
assert(is_zero_approx(runtime_environment.fog_aerial_perspective))
assert(is_equal_approx(runtime_environment.fog_depth_begin, 3.0))
assert(is_equal_approx(runtime_environment.fog_depth_end, 18.0))
assert(is_equal_approx(
float(SaltWaterMaterial.get_shader_parameter("environment_brightness")),
night_water_brightness,
))
assert(is_equal_approx(
runtime_environment.background_energy_multiplier,
night_background_energy,
))
assert(is_equal_approx(
runtime_environment.ambient_light_energy,
night_ambient_energy,
))
assert(is_equal_approx(
runtime_environment.fog_light_energy,
night_fog_light_energy,
))
assert(is_equal_approx(
runtime_environment.adjustment_brightness,
night_adjustment_brightness,
))
assert(is_equal_approx(
runtime_environment.adjustment_saturation,
night_adjustment_saturation,
))
visuals.apply_time_immediately(8.0)
assert(is_equal_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
), WorldTimeVisualController.FOGGY_NIGHT_WATER_NEAR_FOG_AMOUNT))
assert(is_equal_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
), 1.0))
visuals.apply_time_immediately(20.0)
assert(is_zero_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
)))
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
)))
visuals.apply_weather_immediately(WorldWeatherService.Weather.SUNNY)
visuals.apply_time_immediately(20.0)
assert(not runtime_environment.fog_enabled)
assert(is_zero_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
)))
assert(is_zero_approx(float(
SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
)))
assert(is_equal_approx(float(
SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
), WorldTimeVisualController.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
assert(is_zero_approx(float(
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
)))
var dusk_star_visibility := float(
runtime_sky_material.get_shader_parameter("star_visibility")
)
assert(dusk_star_visibility > 0.45 and dusk_star_visibility < 0.55)
var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
"sky_horizon_color"
) as Color
assert(dusk_horizon.r > day_horizon.r)
world_root.queue_free()
func _validate_weather_clock_icon() -> void:
var weather_icon := WeatherIconType.new()
root.add_child(weather_icon)
assert(weather_icon.tooltip_text == "clear")
assert(not weather_icon.is_nighttime())
weather_icon.set_nighttime(true)
assert(weather_icon.is_nighttime())
weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
assert(weather_icon.tooltip_text == "cloudy")
weather_icon.queue_free()
static func _wrapped_time_difference(left: float, right: float) -> float:
var difference: float = absf(left - right)
return minf(difference, WorldTimeServiceType.HOURS_PER_DAY - difference)