Add synchronized time and weather systems
This commit is contained in:
parent
30ad9215ca
commit
58fb3d6605
37 changed files with 1900 additions and 17 deletions
10
fish/day_availability.tres
Normal file
10
fish/day_availability.tres
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[gd_resource type="Resource" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://fish/fish_availability.gd" id="1_availability"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_availability")
|
||||
allow_day = true
|
||||
allow_night = false
|
||||
preferred_bait_tags = Array[StringName]([&"worm"])
|
||||
preferred_bait_weight_multiplier = 1.35
|
||||
|
|
@ -10,22 +10,35 @@ const FishingContextType = preload("res://fishing/fishing_context.gd")
|
|||
@export var allow_night: bool = true
|
||||
@export var require_rain: bool = false
|
||||
@export var forbid_rain: bool = false
|
||||
@export var require_fog: bool = false
|
||||
@export var forbid_fog: bool = false
|
||||
@export var required_bait_tags: Array[StringName] = []
|
||||
@export var preferred_bait_tags: Array[StringName] = []
|
||||
@export_range(0.01, 10.0, 0.01) var preferred_bait_weight_multiplier: float = 1.25
|
||||
|
||||
|
||||
func is_available(context: FishingContextType) -> bool:
|
||||
if context == null or (require_rain and forbid_rain):
|
||||
if (
|
||||
context == null
|
||||
or (require_rain and forbid_rain)
|
||||
or (require_fog and forbid_fog)
|
||||
):
|
||||
return false
|
||||
if context.is_night and not allow_night:
|
||||
if context.is_day_night_transition:
|
||||
if not allow_day and not allow_night:
|
||||
return false
|
||||
elif context.is_night and not allow_night:
|
||||
return false
|
||||
if not context.is_night and not allow_day:
|
||||
elif not context.is_night and not allow_day:
|
||||
return false
|
||||
if require_rain and not context.is_raining:
|
||||
return false
|
||||
if forbid_rain and context.is_raining:
|
||||
return false
|
||||
if require_fog and not context.is_foggy:
|
||||
return false
|
||||
if forbid_fog and context.is_foggy:
|
||||
return false
|
||||
if (
|
||||
not allowed_location_tags.is_empty()
|
||||
and not _has_any_match(allowed_location_tags, context.location_tags)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
|
||||
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
|
||||
[ext_resource type="Resource" path="res://fish/default_availability.tres" id="3_availability"]
|
||||
[ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"]
|
||||
[ext_resource type="Texture2D" path="res://fish/species/bluegill/fish_bluegill.png" id="4_texture"]
|
||||
|
||||
[resource]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
[sub_resource type="Resource" id="BlueCatfishAvailability"]
|
||||
script = ExtResource("3_availability_script")
|
||||
allowed_location_tags = Array[StringName]([&"starter_pond"])
|
||||
allow_day = false
|
||||
allow_night = true
|
||||
preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
|
||||
preferred_bait_weight_multiplier = 1.35
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
[sub_resource type="Resource" id="ChannelCatfishAvailability"]
|
||||
script = ExtResource("3_availability_script")
|
||||
allowed_location_tags = Array[StringName]([&"starter_pond"])
|
||||
allow_day = false
|
||||
allow_night = true
|
||||
preferred_bait_tags = Array[StringName]([&"worm"])
|
||||
preferred_bait_weight_multiplier = 1.35
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
[sub_resource type="Resource" id="FlatheadCatfishAvailability"]
|
||||
script = ExtResource("3_availability_script")
|
||||
allowed_location_tags = Array[StringName]([&"starter_pond"])
|
||||
allow_day = false
|
||||
allow_night = true
|
||||
preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
|
||||
preferred_bait_weight_multiplier = 1.35
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
[sub_resource type="Resource" id="WhiteCatfishAvailability"]
|
||||
script = ExtResource("3_availability_script")
|
||||
allowed_location_tags = Array[StringName]([&"starter_pond"])
|
||||
allow_day = false
|
||||
allow_night = true
|
||||
preferred_bait_tags = Array[StringName]([&"worm"])
|
||||
preferred_bait_weight_multiplier = 1.35
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
|
||||
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
|
||||
[ext_resource type="Resource" path="res://fish/default_availability.tres" id="3_availability"]
|
||||
[ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"]
|
||||
[ext_resource type="Texture2D" path="res://fish/species/sunfish/fish_sunfish.png" id="4_texture"]
|
||||
|
||||
[resource]
|
||||
|
|
|
|||
|
|
@ -6,4 +6,6 @@ var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
|
|||
var active_event_tags: Array[StringName] = []
|
||||
var active_bait_tags: Array[StringName] = []
|
||||
var is_night: bool = false
|
||||
var is_day_night_transition: bool = false
|
||||
var is_raining: bool = false
|
||||
var is_foggy: bool = false
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ const FishingSurfaceResolverType = preload(
|
|||
"res://fishing/fishing_surface_resolver.gd"
|
||||
)
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
|
||||
signal status_changed(status: String)
|
||||
signal catch_display_changed(
|
||||
|
|
@ -108,8 +112,6 @@ const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
|||
@export var deterministic_selection_seed: int = 24680
|
||||
|
||||
@export_category("Context Testing")
|
||||
@export var context_is_night: bool = false
|
||||
@export var context_is_raining: bool = false
|
||||
@export var context_event_tags: Array[StringName] = []
|
||||
@export var context_bait_tags: Array[StringName] = []
|
||||
|
||||
|
|
@ -132,6 +134,8 @@ var _network_item_use: NetworkItemUseService
|
|||
var _cooler_capacity: PlayerCoolerCapacityType
|
||||
var _network_session: NetworkSessionType
|
||||
var _network_fishing: NetworkFishingServiceType
|
||||
var _world_time: WorldTimeServiceType
|
||||
var _world_weather: WorldWeatherServiceType
|
||||
var _active_player: PlayerType
|
||||
var _state_time_remaining: float = 0.0
|
||||
var _cast_charge: float = 0.0
|
||||
|
|
@ -190,6 +194,8 @@ func setup(
|
|||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
network_session: NetworkSessionType = null,
|
||||
network_fishing: NetworkFishingServiceType = null,
|
||||
world_time: WorldTimeServiceType = null,
|
||||
world_weather: WorldWeatherServiceType = null,
|
||||
) -> void:
|
||||
_local_player = local_player
|
||||
_local_inventory = local_inventory
|
||||
|
|
@ -207,6 +213,8 @@ func setup(
|
|||
_cooler_capacity = cooler_capacity
|
||||
_network_session = network_session
|
||||
_network_fishing = network_fishing
|
||||
_world_time = world_time
|
||||
_world_weather = world_weather
|
||||
if _network_fishing != null:
|
||||
_network_fishing.local_cast_accepted.connect(
|
||||
_on_network_cast_accepted
|
||||
|
|
@ -1296,8 +1304,12 @@ func _build_fishing_context(
|
|||
context.water_type = region.water_type
|
||||
context.active_event_tags = context_event_tags.duplicate()
|
||||
context.active_bait_tags = context_bait_tags.duplicate()
|
||||
context.is_night = context_is_night
|
||||
context.is_raining = context_is_raining
|
||||
if _world_time != null:
|
||||
context.is_night = _world_time.is_night_period()
|
||||
context.is_day_night_transition = _world_time.is_transition()
|
||||
if _world_weather != null:
|
||||
context.is_raining = _world_weather.is_raining()
|
||||
context.is_foggy = _world_weather.is_foggy()
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
|||
37
main/main.gd
37
main/main.gd
|
|
@ -77,6 +77,17 @@ const PlayerAppearanceStoreType = preload(
|
|||
const NetworkProfileServiceType = preload(
|
||||
"res://network/network_profile_service.gd"
|
||||
)
|
||||
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 WorldWeatherServiceType = preload("res://world/world_weather_service.gd")
|
||||
const NetworkWorldWeatherServiceType = preload(
|
||||
"res://network/network_world_weather_service.gd"
|
||||
)
|
||||
|
||||
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||
const PLAYER_MENU_PATTERN_SCALE: float = 0.85
|
||||
|
|
@ -108,6 +119,17 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
|||
%WorldPixelationPostprocess
|
||||
)
|
||||
@onready var _network_session: NetworkSessionType = %NetworkSession
|
||||
@onready var _world_time: WorldTimeServiceType = %WorldTimeService
|
||||
@onready var _world_time_visuals: WorldTimeVisualControllerType = (
|
||||
%WorldTimeVisualController
|
||||
)
|
||||
@onready var _network_world_time: NetworkWorldTimeServiceType = (
|
||||
%NetworkWorldTimeService
|
||||
)
|
||||
@onready var _world_weather: WorldWeatherServiceType = %WorldWeatherService
|
||||
@onready var _network_world_weather: NetworkWorldWeatherServiceType = (
|
||||
%NetworkWorldWeatherService
|
||||
)
|
||||
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
|
||||
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
|
||||
@onready var _network_profile: NetworkProfilePreferencesType = (
|
||||
|
|
@ -263,6 +285,15 @@ func _initialize_after_data_root() -> void:
|
|||
_server_trust,
|
||||
_host_bans,
|
||||
)
|
||||
_world_time_visuals.setup(
|
||||
_world_time,
|
||||
_test_world.get_world_environment(),
|
||||
_test_world.get_sun(),
|
||||
_world_weather,
|
||||
_player,
|
||||
)
|
||||
_network_world_time.setup(_network_session, _world_time)
|
||||
_network_world_weather.setup(_network_session, _world_weather)
|
||||
_identity_backups.setup(_data_root, _player_identity, _host_identity)
|
||||
_network_profile_service.setup(
|
||||
_network_session,
|
||||
|
|
@ -428,7 +459,9 @@ func _initialize_after_data_root() -> void:
|
|||
_network_item_use,
|
||||
_player.cooler_capacity,
|
||||
_network_session,
|
||||
_network_fishing
|
||||
_network_fishing,
|
||||
_world_time,
|
||||
_world_weather,
|
||||
)
|
||||
_game_ui.setup(
|
||||
_player,
|
||||
|
|
@ -460,6 +493,8 @@ func _initialize_after_data_root() -> void:
|
|||
_settings_manager,
|
||||
_network_surface_drawing,
|
||||
_player.art_unlocks,
|
||||
_world_time,
|
||||
_world_weather,
|
||||
)
|
||||
_game_ui.setup_data_and_identity(
|
||||
_data_root,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@
|
|||
[ext_resource type="PackedScene" uid="uid://w7n4gjqq1juc" path="res://art/exported/characters/base/netfishing_base_character.glb" id="44_4pcu1"]
|
||||
[ext_resource type="Script" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
|
||||
[ext_resource type="Script" path="res://network/network_surface_drawing_service.gd" id="46_surface_drawing"]
|
||||
[ext_resource type="Script" path="res://world/world_time_service.gd" id="47_world_time"]
|
||||
[ext_resource type="Script" path="res://world/world_time_visual_controller.gd" id="48_world_time_visuals"]
|
||||
[ext_resource type="Script" path="res://network/network_world_time_service.gd" id="49_network_world_time"]
|
||||
[ext_resource type="Script" path="res://world/world_weather_service.gd" id="50_world_weather"]
|
||||
[ext_resource type="Script" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||
shader = ExtResource("40_title_water")
|
||||
|
|
@ -113,6 +118,26 @@ mouse_filter = 2
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("39_fonts")
|
||||
|
||||
[node name="WorldTimeService" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("47_world_time")
|
||||
|
||||
[node name="WorldTimeVisualController" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("48_world_time_visuals")
|
||||
|
||||
[node name="NetworkWorldTimeService" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("49_network_world_time")
|
||||
|
||||
[node name="WorldWeatherService" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("50_world_weather")
|
||||
|
||||
[node name="NetworkWorldWeatherService" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("51_network_world_weather")
|
||||
|
||||
[node name="PlayerDataRoot" type="Node" parent="." unique_id=281868360]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("37_data_root")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ const MAIL_RELIABLE_CHANNEL: int = 9
|
|||
const ENET_CHANNEL_COUNT: int = 10
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
||||
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||
|
||||
enum RejectionCode {
|
||||
NONE,
|
||||
|
|
@ -87,6 +89,8 @@ static func make_client_hello(
|
|||
"client_nonce": client_nonce,
|
||||
"capability_flags": PackedStringArray([
|
||||
SURFACE_DRAWING_CAPABILITY,
|
||||
WORLD_TIME_CAPABILITY,
|
||||
WORLD_WEATHER_CAPABILITY,
|
||||
]),
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
"identity_fingerprint": identity_fingerprint,
|
||||
|
|
@ -204,6 +208,8 @@ static func make_server_hello(
|
|||
"equipment_v1",
|
||||
"fish_showcase_v1",
|
||||
SURFACE_DRAWING_CAPABILITY,
|
||||
WORLD_TIME_CAPABILITY,
|
||||
WORLD_WEATHER_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
|
|||
|
|
@ -168,7 +168,11 @@ func start_private_host(port: int = DEFAULT_PORT) -> bool:
|
|||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
_player_identity.fingerprint,
|
||||
_player_identity.public_pem,
|
||||
PackedStringArray([NetworkProtocol.SURFACE_DRAWING_CAPABILITY]),
|
||||
PackedStringArray([
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
var host_profile_hello := NetworkProtocol.make_client_hello(
|
||||
|
|
@ -364,6 +368,8 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
@ -1074,7 +1080,11 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
_player_identity.fingerprint,
|
||||
_player_identity.public_pem,
|
||||
PackedStringArray([NetworkProtocol.SURFACE_DRAWING_CAPABILITY]),
|
||||
PackedStringArray([
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
var local_record := _registry.get_peer(local_peer_id)
|
||||
|
|
|
|||
135
network/network_world_time_service.gd
Normal file
135
network/network_world_time_service.gd
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
class_name NetworkWorldTimeService
|
||||
extends Node
|
||||
|
||||
const SYNC_INTERVAL_SECONDS: float = 5.0
|
||||
const MAX_SESSION_ID_LENGTH: int = 96
|
||||
|
||||
var _session: NetworkSession
|
||||
var _world_time: WorldTimeService
|
||||
var _sync_elapsed: float = 0.0
|
||||
var _sequence: int = 0
|
||||
var _last_received_sequence: int = -1
|
||||
var _active_session_id: String = ""
|
||||
|
||||
|
||||
func setup(session: NetworkSession, world_time: WorldTimeService) -> void:
|
||||
_session = session
|
||||
_world_time = world_time
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _session == null or _world_time == null or not _session.is_host():
|
||||
return
|
||||
_sync_elapsed += delta
|
||||
if _sync_elapsed < SYNC_INTERVAL_SECONDS:
|
||||
return
|
||||
_sync_elapsed = 0.0
|
||||
_broadcast_snapshot()
|
||||
|
||||
|
||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||
if _world_time == null or _session == null:
|
||||
return
|
||||
if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
|
||||
if _active_session_id != _session.get_session_id():
|
||||
_active_session_id = _session.get_session_id()
|
||||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.begin_session()
|
||||
return
|
||||
if state == NetworkSession.State.JOINED_CLIENT:
|
||||
_active_session_id = _session.get_session_id()
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
if _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
):
|
||||
_world_time.begin_session()
|
||||
else:
|
||||
_world_time.end_session()
|
||||
return
|
||||
if state in [
|
||||
NetworkSession.State.INACTIVE,
|
||||
NetworkSession.State.DISCONNECTING,
|
||||
NetworkSession.State.CONNECTION_FAILED,
|
||||
NetworkSession.State.SERVER_LOST,
|
||||
]:
|
||||
_active_session_id = ""
|
||||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.end_session()
|
||||
|
||||
|
||||
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_host()
|
||||
or not _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
)
|
||||
):
|
||||
return
|
||||
_send_snapshot(peer_id)
|
||||
|
||||
|
||||
func _broadcast_snapshot() -> void:
|
||||
if _session == null or not _session.is_host():
|
||||
return
|
||||
for peer_id: int in _session.get_authenticated_peer_ids():
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
continue
|
||||
if _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
):
|
||||
_send_snapshot(peer_id)
|
||||
|
||||
|
||||
func _send_snapshot(peer_id: int) -> void:
|
||||
_sequence += 1
|
||||
receive_world_time_snapshot.rpc_id(peer_id, {
|
||||
"session_id": _session.get_session_id(),
|
||||
"time_hours": _world_time.get_time_hours(),
|
||||
"sequence": _sequence,
|
||||
})
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_world_time_snapshot(data: Dictionary) -> void:
|
||||
if (
|
||||
_session == null
|
||||
or _world_time == null
|
||||
or not _session.is_joined_client()
|
||||
or not _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
)
|
||||
or not validate_snapshot(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
):
|
||||
return
|
||||
var sequence: int = int(data["sequence"])
|
||||
if sequence <= _last_received_sequence:
|
||||
return
|
||||
_last_received_sequence = sequence
|
||||
_world_time.synchronize_time(float(data["time_hours"]))
|
||||
|
||||
|
||||
static func validate_snapshot(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
typeof(value.get("session_id")) == TYPE_STRING
|
||||
and not str(value["session_id"]).is_empty()
|
||||
and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
|
||||
and typeof(value.get("time_hours")) in [TYPE_FLOAT, TYPE_INT]
|
||||
and is_finite(float(value["time_hours"]))
|
||||
and float(value["time_hours"]) >= 0.0
|
||||
and float(value["time_hours"]) < WorldTimeService.HOURS_PER_DAY
|
||||
and typeof(value.get("sequence")) == TYPE_INT
|
||||
and int(value["sequence"]) >= 0
|
||||
)
|
||||
1
network/network_world_time_service.gd.uid
Normal file
1
network/network_world_time_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://blig76aes25j
|
||||
156
network/network_world_weather_service.gd
Normal file
156
network/network_world_weather_service.gd
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
class_name NetworkWorldWeatherService
|
||||
extends Node
|
||||
|
||||
const SYNC_INTERVAL_SECONDS: float = 15.0
|
||||
const MAX_SESSION_ID_LENGTH: int = 96
|
||||
const MAX_REMAINING_SECONDS: float = 1800.0
|
||||
|
||||
var _session: NetworkSession
|
||||
var _world_weather: WorldWeatherService
|
||||
var _sync_elapsed: float = 0.0
|
||||
var _sequence: int = 0
|
||||
var _last_received_sequence: int = -1
|
||||
var _active_session_id: String = ""
|
||||
|
||||
|
||||
func setup(
|
||||
session: NetworkSession,
|
||||
world_weather: WorldWeatherService,
|
||||
) -> void:
|
||||
_session = session
|
||||
_world_weather = world_weather
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||
_world_weather.weather_changed.connect(_on_weather_changed)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _session == null or _world_weather == null or not _session.is_host():
|
||||
return
|
||||
_sync_elapsed += delta
|
||||
if _sync_elapsed < SYNC_INTERVAL_SECONDS:
|
||||
return
|
||||
_sync_elapsed = 0.0
|
||||
_broadcast_snapshot()
|
||||
|
||||
|
||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||
if _world_weather == null or _session == null:
|
||||
return
|
||||
if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
|
||||
if _active_session_id != _session.get_session_id():
|
||||
_active_session_id = _session.get_session_id()
|
||||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_weather.begin_authoritative_session(
|
||||
_active_session_id.hash()
|
||||
)
|
||||
return
|
||||
if state == NetworkSession.State.JOINED_CLIENT:
|
||||
_active_session_id = _session.get_session_id()
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
if _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
):
|
||||
_world_weather.begin_remote_session()
|
||||
else:
|
||||
_world_weather.end_session()
|
||||
return
|
||||
if state in [
|
||||
NetworkSession.State.INACTIVE,
|
||||
NetworkSession.State.DISCONNECTING,
|
||||
NetworkSession.State.CONNECTION_FAILED,
|
||||
NetworkSession.State.SERVER_LOST,
|
||||
]:
|
||||
_active_session_id = ""
|
||||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_weather.end_session()
|
||||
|
||||
|
||||
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_host()
|
||||
or not _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
)
|
||||
):
|
||||
return
|
||||
_send_snapshot(peer_id)
|
||||
|
||||
|
||||
func _on_weather_changed(
|
||||
_weather: WorldWeatherService.Weather,
|
||||
_seconds_remaining: float,
|
||||
) -> void:
|
||||
if _session != null and _session.is_host():
|
||||
_broadcast_snapshot()
|
||||
|
||||
|
||||
func _broadcast_snapshot() -> void:
|
||||
if _session == null or not _session.is_host():
|
||||
return
|
||||
for peer_id: int in _session.get_authenticated_peer_ids():
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
continue
|
||||
if _session.peer_supports_capability(
|
||||
peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
):
|
||||
_send_snapshot(peer_id)
|
||||
|
||||
|
||||
func _send_snapshot(peer_id: int) -> void:
|
||||
_sequence += 1
|
||||
receive_world_weather_snapshot.rpc_id(peer_id, {
|
||||
"session_id": _session.get_session_id(),
|
||||
"weather": int(_world_weather.get_weather()),
|
||||
"seconds_remaining": _world_weather.get_seconds_remaining(),
|
||||
"sequence": _sequence,
|
||||
})
|
||||
|
||||
|
||||
@rpc("authority", "call_remote", "reliable", 0)
|
||||
func receive_world_weather_snapshot(data: Dictionary) -> void:
|
||||
if (
|
||||
_session == null
|
||||
or _world_weather == null
|
||||
or not _session.is_joined_client()
|
||||
or not _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
)
|
||||
or not validate_snapshot(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
):
|
||||
return
|
||||
var sequence: int = int(data["sequence"])
|
||||
if sequence <= _last_received_sequence:
|
||||
return
|
||||
_last_received_sequence = sequence
|
||||
_world_weather.apply_authoritative_snapshot(
|
||||
int(data["weather"]) as WorldWeatherService.Weather,
|
||||
float(data["seconds_remaining"]),
|
||||
)
|
||||
|
||||
|
||||
static func validate_snapshot(data: Variant) -> bool:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return false
|
||||
var value: Dictionary = data
|
||||
return (
|
||||
typeof(value.get("session_id")) == TYPE_STRING
|
||||
and not str(value["session_id"]).is_empty()
|
||||
and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
|
||||
and typeof(value.get("weather")) == TYPE_INT
|
||||
and WorldWeatherService.is_valid_weather(int(value["weather"]))
|
||||
and typeof(value.get("seconds_remaining")) in [TYPE_FLOAT, TYPE_INT]
|
||||
and is_finite(float(value["seconds_remaining"]))
|
||||
and float(value["seconds_remaining"]) >= 0.0
|
||||
and float(value["seconds_remaining"]) <= MAX_REMAINING_SECONDS
|
||||
and typeof(value.get("sequence")) == TYPE_INT
|
||||
and int(value["sequence"]) >= 0
|
||||
)
|
||||
1
network/network_world_weather_service.gd.uid
Normal file
1
network/network_world_weather_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://llj46fx0oco7
|
||||
|
|
@ -107,12 +107,17 @@ func _validate_catalog_and_pools() -> void:
|
|||
var pond_context := FishingContextType.new()
|
||||
pond_context.location_tags = [&"starter_pond"]
|
||||
pond_context.water_type = WaterType.Type.FRESH_WATER
|
||||
var night_pond_context := FishingContextType.new()
|
||||
night_pond_context.location_tags = [&"starter_pond"]
|
||||
night_pond_context.water_type = WaterType.Type.FRESH_WATER
|
||||
night_pond_context.is_night = true
|
||||
var ocean_context := FishingContextType.new()
|
||||
ocean_context.location_tags = [&"coast", &"ocean"]
|
||||
ocean_context.water_type = WaterType.Type.SALT_WATER
|
||||
for fish_id: StringName in CATFISH_IDS:
|
||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||
assert(fish.availability.is_available(pond_context))
|
||||
assert(not fish.availability.is_available(pond_context))
|
||||
assert(fish.availability.is_available(night_pond_context))
|
||||
assert(not fish.availability.is_available(ocean_context))
|
||||
var single_species_pool := FishPoolType.new()
|
||||
single_species_pool.candidates = [fish]
|
||||
|
|
@ -122,7 +127,7 @@ func _validate_catalog_and_pools() -> void:
|
|||
selector.begin_roll()
|
||||
assert(
|
||||
selector.select_fish(
|
||||
single_species_pool, pond_context, collection
|
||||
single_species_pool, night_pond_context, collection
|
||||
) == fish
|
||||
)
|
||||
collection.free()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ func _initialize() -> void:
|
|||
func _run() -> void:
|
||||
var main := MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 6:
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
|
|
@ -45,7 +49,7 @@ func _run() -> void:
|
|||
assert(not hotbar.visible)
|
||||
|
||||
var entry_buttons: Dictionary = logbook.get("_entry_buttons")
|
||||
assert(entry_buttons.size() == 0)
|
||||
assert(entry_buttons.size() == 6)
|
||||
await _capture_if_requested("-unknown")
|
||||
logbook.call(
|
||||
"_select_category", WaterType.Type.FRESH_WATER
|
||||
|
|
|
|||
186
tests/world_time_multiplayer_validation.gd
Normal file
186
tests/world_time_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene = preload("res://main/main.tscn")
|
||||
const TEST_PORT: int = 17983
|
||||
const INITIAL_HOST_TIME: float = 19.75
|
||||
const UPDATED_HOST_TIME: float = 20.75
|
||||
const TIME_TOLERANCE_HOURS: float = 0.05
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if arguments.has("host"):
|
||||
await _run_host()
|
||||
return
|
||||
if arguments.has("client"):
|
||||
await _run_client()
|
||||
return
|
||||
push_error("World time multiplayer validation needs host or client mode.")
|
||||
quit(1)
|
||||
|
||||
|
||||
func _run_host() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||
var world_weather := (
|
||||
main.get_node("%WorldWeatherService") as WorldWeatherService
|
||||
)
|
||||
assert(session.start_private_host(TEST_PORT))
|
||||
world_time.synchronize_time(INITIAL_HOST_TIME)
|
||||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.RAINY, 300.0
|
||||
)
|
||||
assert(session.set_host_open(true))
|
||||
|
||||
var remote_peer_id: int = 0
|
||||
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
|
||||
await process_frame
|
||||
for peer_id: int in session.get_authenticated_peer_ids():
|
||||
if peer_id != session.get_local_peer_id():
|
||||
remote_peer_id = peer_id
|
||||
break
|
||||
assert(remote_peer_id > 1)
|
||||
assert(session.peer_supports_capability(
|
||||
remote_peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
))
|
||||
assert(session.peer_supports_capability(
|
||||
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
))
|
||||
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
||||
|
||||
await create_timer(1.0).timeout
|
||||
world_time.synchronize_time(UPDATED_HOST_TIME)
|
||||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.FOGGY, 300.0
|
||||
)
|
||||
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||
while (
|
||||
Time.get_ticks_msec() < disconnect_deadline
|
||||
and session.is_authenticated_peer(remote_peer_id)
|
||||
):
|
||||
await process_frame
|
||||
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||
print("World time multiplayer host validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _run_client() -> void:
|
||||
var main: Node = await _create_initialized_main()
|
||||
main.call(
|
||||
"_on_title_join_game_requested",
|
||||
"127.0.0.1:%d" % TEST_PORT,
|
||||
)
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||
var world_weather := (
|
||||
main.get_node("%WorldWeatherService") as WorldWeatherService
|
||||
)
|
||||
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < join_deadline:
|
||||
await process_frame
|
||||
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||
main.call("_confirm_server_trust")
|
||||
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||
break
|
||||
assert(session.is_joined_client())
|
||||
assert(session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
))
|
||||
assert(session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
))
|
||||
|
||||
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < initial_deadline
|
||||
and _wrapped_time_difference(
|
||||
world_time.get_time_hours(), INITIAL_HOST_TIME
|
||||
) > TIME_TOLERANCE_HOURS
|
||||
):
|
||||
await process_frame
|
||||
assert(_wrapped_time_difference(
|
||||
world_time.get_time_hours(), INITIAL_HOST_TIME
|
||||
) <= TIME_TOLERANCE_HOURS)
|
||||
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
||||
var weather_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < weather_deadline
|
||||
and not world_weather.is_raining()
|
||||
):
|
||||
await process_frame
|
||||
assert(world_weather.is_raining())
|
||||
var game_ui := main.get_node("%GameUI") as CanvasLayer
|
||||
var chat_ui := game_ui.get_node("%ChatUI") as Control
|
||||
var clock_panel := chat_ui.get_node("WorldClockPanel") as PanelContainer
|
||||
var clock_label := clock_panel.get_node("WorldClockLabel") as Label
|
||||
var weather_icon := chat_ui.get_node("WorldWeatherIcon") as WeatherIcon
|
||||
var chat_panel := chat_ui.get_node("ChatPanel") as PanelContainer
|
||||
assert(clock_panel.visible)
|
||||
assert(clock_label.text == world_time.get_clock_text())
|
||||
assert(clock_label.text.ends_with(" pm"))
|
||||
assert(weather_icon.visible)
|
||||
assert(weather_icon.get_weather() == WorldWeatherService.Weather.RAINY)
|
||||
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
|
||||
|
||||
var update_deadline: int = Time.get_ticks_msec() + 10000
|
||||
while (
|
||||
Time.get_ticks_msec() < update_deadline
|
||||
and _wrapped_time_difference(
|
||||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||
) > TIME_TOLERANCE_HOURS
|
||||
):
|
||||
await process_frame
|
||||
assert(_wrapped_time_difference(
|
||||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||
) <= TIME_TOLERANCE_HOURS)
|
||||
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
||||
assert(clock_label.text == world_time.get_clock_text())
|
||||
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
Time.get_ticks_msec() < fog_deadline
|
||||
and not world_weather.is_foggy()
|
||||
):
|
||||
await process_frame
|
||||
assert(world_weather.is_foggy())
|
||||
assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY)
|
||||
chat_ui.call("set_dock_right", true)
|
||||
await process_frame
|
||||
assert(clock_panel.position.x > 1000.0)
|
||||
assert(weather_icon.position.x < clock_panel.position.x)
|
||||
chat_ui.call("set_dock_right", false)
|
||||
await process_frame
|
||||
assert(clock_panel.position.x < 20.0)
|
||||
assert(weather_icon.position.x > clock_panel.position.x)
|
||||
print("World time multiplayer client validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
|
||||
|
||||
func _create_initialized_main() -> Node:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main := MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
return main
|
||||
|
||||
|
||||
func _wrapped_time_difference(left: float, right: float) -> float:
|
||||
var direct: float = absf(left - right)
|
||||
return minf(direct, WorldTimeService.HOURS_PER_DAY - direct)
|
||||
1
tests/world_time_multiplayer_validation.gd.uid
Normal file
1
tests/world_time_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://6fal0btp6sgr
|
||||
182
tests/world_time_validation.gd
Normal file
182
tests/world_time_validation.gd
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
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 Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_clock_boundaries_and_duration()
|
||||
_validate_fishing_availability()
|
||||
_validate_fishing_spot_context()
|
||||
_validate_network_snapshot_bounds()
|
||||
_validate_environment_presentation()
|
||||
print("World time validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_clock_boundaries_and_duration() -> void:
|
||||
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 3600.0)
|
||||
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_session(8.0)
|
||||
clock.advance_time(1800.0)
|
||||
assert(is_equal_approx(clock.get_time_hours(), 20.0))
|
||||
assert(clock.is_night_period())
|
||||
assert(clock.is_transition())
|
||||
clock.advance_time(1800.0)
|
||||
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
||||
assert(not clock.is_night_period())
|
||||
assert(clock.is_transition())
|
||||
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"]:
|
||||
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_session(20.25)
|
||||
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)
|
||||
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,
|
||||
"sequence": 1,
|
||||
}))
|
||||
assert(not NetworkWorldTimeServiceType.validate_snapshot({
|
||||
"session_id": "session",
|
||||
"time_hours": 24.0,
|
||||
"sequence": 1,
|
||||
}))
|
||||
assert(not NetworkWorldTimeServiceType.validate_snapshot({
|
||||
"session_id": "",
|
||||
"time_hours": 8.0,
|
||||
"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()
|
||||
sky.sky_material = ProceduralSkyMaterial.new()
|
||||
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 ProceduralSkyMaterial
|
||||
)
|
||||
var day_horizon: Color = runtime_sky_material.sky_horizon_color
|
||||
var day_ambient_energy: float = runtime_environment.ambient_light_energy
|
||||
visuals.apply_time_immediately(0.0)
|
||||
assert(runtime_sky_material.sky_horizon_color != day_horizon)
|
||||
assert(runtime_environment.ambient_light_energy < day_ambient_energy)
|
||||
visuals.apply_time_immediately(20.0)
|
||||
assert(runtime_sky_material.sky_horizon_color.r > day_horizon.r)
|
||||
world_root.queue_free()
|
||||
1
tests/world_time_validation.gd.uid
Normal file
1
tests/world_time_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://buiitafgrgm4b
|
||||
190
tests/world_weather_validation.gd
Normal file
190
tests/world_weather_validation.gd
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
extends SceneTree
|
||||
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
const NetworkWorldWeatherServiceType = preload(
|
||||
"res://network/network_world_weather_service.gd"
|
||||
)
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldTimeVisualControllerType = preload(
|
||||
"res://world/world_time_visual_controller.gd"
|
||||
)
|
||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||
const FishAvailabilityType = preload("res://fish/fish_availability.gd")
|
||||
const FishableWaterRegionType = preload(
|
||||
"res://world/fishable_water_region.gd"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_weather_scheduler()
|
||||
_validate_snapshot_bounds()
|
||||
_validate_fishing_weather_seams()
|
||||
_validate_fishing_spot_context()
|
||||
_validate_weather_presentation()
|
||||
print("World weather validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_weather_scheduler() -> void:
|
||||
var weather := WorldWeatherServiceType.new()
|
||||
root.add_child(weather)
|
||||
weather.begin_authoritative_session(20260802)
|
||||
assert(weather.get_weather() == WorldWeatherServiceType.Weather.SUNNY)
|
||||
assert(_duration_is_valid(weather))
|
||||
weather.advance_weather(weather.get_seconds_remaining() + 0.01)
|
||||
assert(weather.get_weather() == WorldWeatherServiceType.Weather.CLOUDY)
|
||||
assert(_duration_is_valid(weather))
|
||||
weather.advance_weather(weather.get_seconds_remaining() + 0.01)
|
||||
assert(weather.get_weather() in [
|
||||
WorldWeatherServiceType.Weather.SUNNY,
|
||||
WorldWeatherServiceType.Weather.RAINY,
|
||||
WorldWeatherServiceType.Weather.FOGGY,
|
||||
])
|
||||
assert(_duration_is_valid(weather))
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.RAINY, 120.0
|
||||
)
|
||||
assert(weather.is_raining())
|
||||
assert(not weather.is_foggy())
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.FOGGY, 120.0
|
||||
)
|
||||
assert(weather.is_foggy())
|
||||
assert(not weather.is_raining())
|
||||
weather.queue_free()
|
||||
|
||||
|
||||
func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
|
||||
var seconds: float = weather.get_seconds_remaining()
|
||||
match weather.get_weather():
|
||||
WorldWeatherServiceType.Weather.SUNNY:
|
||||
return seconds >= 480.0 and seconds <= 900.0
|
||||
WorldWeatherServiceType.Weather.CLOUDY:
|
||||
return seconds >= 300.0 and seconds <= 720.0
|
||||
WorldWeatherServiceType.Weather.RAINY, WorldWeatherServiceType.Weather.FOGGY:
|
||||
return seconds >= 300.0 and seconds <= 600.0
|
||||
return false
|
||||
|
||||
|
||||
func _validate_snapshot_bounds() -> void:
|
||||
assert(NetworkWorldWeatherServiceType.validate_snapshot({
|
||||
"session_id": "session",
|
||||
"weather": int(WorldWeatherServiceType.Weather.FOGGY),
|
||||
"seconds_remaining": 300.0,
|
||||
"sequence": 2,
|
||||
}))
|
||||
assert(not NetworkWorldWeatherServiceType.validate_snapshot({
|
||||
"session_id": "session",
|
||||
"weather": 99,
|
||||
"seconds_remaining": 300.0,
|
||||
"sequence": 2,
|
||||
}))
|
||||
assert(not NetworkWorldWeatherServiceType.validate_snapshot({
|
||||
"session_id": "session",
|
||||
"weather": int(WorldWeatherServiceType.Weather.RAINY),
|
||||
"seconds_remaining": 1801.0,
|
||||
"sequence": 2,
|
||||
}))
|
||||
|
||||
|
||||
func _validate_fishing_weather_seams() -> void:
|
||||
var clear_context := FishingContextType.new()
|
||||
var rain_context := FishingContextType.new()
|
||||
rain_context.is_raining = true
|
||||
var fog_context := FishingContextType.new()
|
||||
fog_context.is_foggy = true
|
||||
fog_context.is_night = true
|
||||
var rain_only := FishAvailabilityType.new()
|
||||
rain_only.require_rain = true
|
||||
assert(not rain_only.is_available(clear_context))
|
||||
assert(rain_only.is_available(rain_context))
|
||||
var fog_only := FishAvailabilityType.new()
|
||||
fog_only.allow_day = false
|
||||
fog_only.require_fog = true
|
||||
assert(not fog_only.is_available(clear_context))
|
||||
assert(fog_only.is_available(fog_context))
|
||||
var no_fog := FishAvailabilityType.new()
|
||||
no_fog.forbid_fog = true
|
||||
assert(no_fog.is_available(clear_context))
|
||||
assert(not no_fog.is_available(fog_context))
|
||||
|
||||
|
||||
func _validate_fishing_spot_context() -> void:
|
||||
var weather := WorldWeatherServiceType.new()
|
||||
weather.begin_remote_session()
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.FOGGY, 200.0
|
||||
)
|
||||
var fishing_spot := FishingSpotType.new()
|
||||
fishing_spot.set("_world_weather", weather)
|
||||
var region := FishableWaterRegionType.new()
|
||||
region.location_tags = [&"starter_pond"]
|
||||
var context: FishingContext = fishing_spot.build_network_context(region)
|
||||
assert(context.is_foggy)
|
||||
assert(not context.is_raining)
|
||||
weather.apply_authoritative_snapshot(
|
||||
WorldWeatherServiceType.Weather.RAINY, 200.0
|
||||
)
|
||||
context = fishing_spot.build_network_context(region)
|
||||
assert(context.is_raining)
|
||||
assert(not context.is_foggy)
|
||||
region.free()
|
||||
fishing_spot.free()
|
||||
weather.free()
|
||||
|
||||
|
||||
func _validate_weather_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()
|
||||
sky.sky_material = ProceduralSkyMaterial.new()
|
||||
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)
|
||||
clock.begin_session(14.0)
|
||||
var weather := WorldWeatherServiceType.new()
|
||||
world_root.add_child(weather)
|
||||
weather.begin_remote_session()
|
||||
var rain_target := Node3D.new()
|
||||
world_root.add_child(rain_target)
|
||||
var visuals := WorldTimeVisualControllerType.new()
|
||||
world_root.add_child(visuals)
|
||||
visuals.setup(
|
||||
clock, world_environment, sun, weather, rain_target
|
||||
)
|
||||
visuals.apply_weather_immediately(
|
||||
WorldWeatherServiceType.Weather.FOGGY
|
||||
)
|
||||
var runtime_environment: Environment = world_environment.environment
|
||||
assert(runtime_environment.fog_depth_begin <= 4.01)
|
||||
assert(runtime_environment.fog_depth_end <= 42.01)
|
||||
assert(runtime_environment.fog_sky_affect >= 0.93)
|
||||
assert(runtime_environment.fog_aerial_perspective >= 0.91)
|
||||
assert(runtime_environment.adjustment_saturation < 0.70)
|
||||
visuals.apply_weather_immediately(
|
||||
WorldWeatherServiceType.Weather.RAINY
|
||||
)
|
||||
var rain := visuals.get_node("LocalRain") as GPUParticles3D
|
||||
assert(rain != null)
|
||||
assert(rain.emitting)
|
||||
assert(rain.amount_ratio > 0.99)
|
||||
visuals.apply_weather_immediately(
|
||||
WorldWeatherServiceType.Weather.SUNNY
|
||||
)
|
||||
assert(not rain.emitting)
|
||||
world_root.queue_free()
|
||||
1
tests/world_weather_validation.gd.uid
Normal file
1
tests/world_weather_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cdbmbag58okjb
|
||||
132
ui/chat_ui.gd
132
ui/chat_ui.gd
|
|
@ -20,6 +20,16 @@ const HANDLE_SIZE := Vector2(34, 42)
|
|||
const HANDLE_GAP: float = 6.0
|
||||
const COLLAPSED_REVEAL_WIDTH: float = 8.0
|
||||
const HINT_EDGE_MARGIN: float = 4.0
|
||||
const CLOCK_SIZE := Vector2(116.0, 34.0)
|
||||
const CLOCK_EDGE_MARGIN: float = 10.0
|
||||
const CLOCK_PANEL_GAP: float = 8.0
|
||||
const WEATHER_ICON_SIZE := Vector2(34.0, 34.0)
|
||||
const WEATHER_ICON_GAP: float = 6.0
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
const WeatherIconType = preload("res://ui/weather_icon.gd")
|
||||
|
||||
enum PresentationState {
|
||||
COLLAPSED,
|
||||
|
|
@ -44,6 +54,9 @@ var _height_button: Button
|
|||
var _unread_indicator: Label
|
||||
var _hint: Label
|
||||
var _speech_layer: Control
|
||||
var _clock_panel: PanelContainer
|
||||
var _clock_label: Label
|
||||
var _weather_icon: WeatherIconType
|
||||
var _speech: Dictionary[int, Dictionary] = {}
|
||||
var _draft_save_timer: Timer
|
||||
var _opacity_tween: Tween
|
||||
|
|
@ -62,6 +75,8 @@ var _prior_movement: bool = true
|
|||
var _prior_camera: bool = true
|
||||
var _output_scale: float = 1.0
|
||||
var _dock_right: bool = false
|
||||
var _world_time: WorldTimeServiceType
|
||||
var _world_weather: WorldWeatherServiceType
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -79,6 +94,8 @@ func setup(
|
|||
player: Player,
|
||||
fishing_spot: FishingSpot,
|
||||
settings: PlayerSettingsManager,
|
||||
world_time: WorldTimeServiceType,
|
||||
world_weather: WorldWeatherServiceType,
|
||||
) -> void:
|
||||
_service = service
|
||||
_session = session
|
||||
|
|
@ -86,6 +103,29 @@ func setup(
|
|||
_player = player
|
||||
_fishing_spot = fishing_spot
|
||||
_settings = settings
|
||||
_world_time = world_time
|
||||
_world_weather = world_weather
|
||||
if (
|
||||
_world_time != null
|
||||
and not _world_time.time_changed.is_connected(
|
||||
_on_world_time_changed
|
||||
)
|
||||
):
|
||||
_world_time.time_changed.connect(_on_world_time_changed)
|
||||
_on_world_time_changed(
|
||||
_world_time.get_time_hours(), _world_time.get_phase()
|
||||
)
|
||||
if (
|
||||
_world_weather != null
|
||||
and not _world_weather.weather_changed.is_connected(
|
||||
_on_world_weather_changed
|
||||
)
|
||||
):
|
||||
_world_weather.weather_changed.connect(_on_world_weather_changed)
|
||||
_on_world_weather_changed(
|
||||
_world_weather.get_weather(),
|
||||
_world_weather.get_seconds_remaining(),
|
||||
)
|
||||
set_dock_right(_settings.current_settings.chat_dock_right)
|
||||
_service.message_received.connect(_on_message)
|
||||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||
|
|
@ -184,6 +224,31 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _build_ui() -> void:
|
||||
_clock_panel = PanelContainer.new()
|
||||
_clock_panel.name = "WorldClockPanel"
|
||||
_clock_panel.size = CLOCK_SIZE
|
||||
_clock_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_clock_panel.add_theme_stylebox_override(
|
||||
"panel", _clock_panel_style()
|
||||
)
|
||||
add_child(_clock_panel)
|
||||
_clock_label = Label.new()
|
||||
_clock_label.name = "WorldClockLabel"
|
||||
_clock_label.text = "8:00 am"
|
||||
_clock_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_clock_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_clock_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
_clock_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
|
||||
_clock_label.add_theme_font_size_override("font_size", 18)
|
||||
_clock_label.add_theme_color_override(
|
||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||
)
|
||||
_clock_panel.add_child(_clock_label)
|
||||
_weather_icon = WeatherIconType.new()
|
||||
_weather_icon.name = "WorldWeatherIcon"
|
||||
_weather_icon.size = WEATHER_ICON_SIZE
|
||||
_weather_icon.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(_weather_icon)
|
||||
_panel = PanelContainer.new()
|
||||
_panel.name = "ChatPanel"
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
|
@ -326,6 +391,8 @@ func _build_ui() -> void:
|
|||
_draft_save_timer.timeout.connect(_flush_draft)
|
||||
add_child(_draft_save_timer)
|
||||
_panel.hide()
|
||||
_clock_panel.hide()
|
||||
_weather_icon.hide()
|
||||
_collapse_button.hide()
|
||||
_height_button.hide()
|
||||
_unread_indicator.hide()
|
||||
|
|
@ -346,6 +413,16 @@ func _chat_panel_style() -> StyleBoxFlat:
|
|||
return style
|
||||
|
||||
|
||||
func _clock_panel_style() -> StyleBoxFlat:
|
||||
var style := _borderless_style(Color(0.025, 0.13, 0.19, 0.94))
|
||||
style.set_corner_radius_all(12)
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
style.content_margin_top = 4
|
||||
style.content_margin_bottom = 4
|
||||
return style
|
||||
|
||||
|
||||
func _borderless_style(color: Color) -> StyleBoxFlat:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
|
|
@ -525,11 +602,15 @@ func _refresh_history() -> void:
|
|||
func _refresh_visibility() -> void:
|
||||
if not _available:
|
||||
_panel.hide()
|
||||
_clock_panel.hide()
|
||||
_weather_icon.hide()
|
||||
_collapse_button.hide()
|
||||
_height_button.hide()
|
||||
_unread_indicator.hide()
|
||||
_hint.hide()
|
||||
return
|
||||
_clock_panel.show()
|
||||
_weather_icon.show()
|
||||
_collapse_button.show()
|
||||
var collapsed := _presentation_state == PresentationState.COLLAPSED
|
||||
_panel.show()
|
||||
|
|
@ -737,6 +818,25 @@ func _layout_presentation(animate: bool) -> void:
|
|||
panel_x = -(PANEL_WIDTH - COLLAPSED_REVEAL_WIDTH) if collapsed else 0.0
|
||||
var target_position := Vector2(panel_x, bottom - height)
|
||||
var target_size := Vector2(PANEL_WIDTH, height)
|
||||
var clock_x: float = (
|
||||
viewport_width - CLOCK_SIZE.x - CLOCK_EDGE_MARGIN
|
||||
if _dock_right else CLOCK_EDGE_MARGIN
|
||||
)
|
||||
var clock_position := Vector2(
|
||||
clock_x,
|
||||
maxf(
|
||||
CLOCK_EDGE_MARGIN,
|
||||
target_position.y - CLOCK_SIZE.y - CLOCK_PANEL_GAP,
|
||||
),
|
||||
)
|
||||
var weather_icon_x: float = (
|
||||
clock_position.x - WEATHER_ICON_GAP - WEATHER_ICON_SIZE.x
|
||||
if _dock_right
|
||||
else clock_position.x + CLOCK_SIZE.x + WEATHER_ICON_GAP
|
||||
)
|
||||
var weather_icon_position := Vector2(
|
||||
weather_icon_x, clock_position.y
|
||||
)
|
||||
var handle_stack_height := HANDLE_SIZE.y * 2.0 + HANDLE_GAP
|
||||
var handle_stack_top := (
|
||||
bottom - COMPACT_HEIGHT * 0.5 - handle_stack_height * 0.5
|
||||
|
|
@ -766,6 +866,10 @@ func _layout_presentation(animate: bool) -> void:
|
|||
if not animate:
|
||||
_panel.position = target_position
|
||||
_panel.size = target_size
|
||||
_clock_panel.position = clock_position
|
||||
_clock_panel.size = CLOCK_SIZE
|
||||
_weather_icon.position = weather_icon_position
|
||||
_weather_icon.size = WEATHER_ICON_SIZE
|
||||
_collapse_button.position = collapse_position
|
||||
_height_button.position = height_position
|
||||
_unread_indicator.position = unread_position
|
||||
|
|
@ -779,6 +883,18 @@ func _layout_presentation(animate: bool) -> void:
|
|||
_height_tween.tween_property(
|
||||
_panel, "size", target_size, UIMotion.CHAT_RESIZE_DURATION
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
_height_tween.tween_property(
|
||||
_clock_panel,
|
||||
"position",
|
||||
clock_position,
|
||||
UIMotion.CHAT_RESIZE_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
_height_tween.tween_property(
|
||||
_weather_icon,
|
||||
"position",
|
||||
weather_icon_position,
|
||||
UIMotion.CHAT_RESIZE_DURATION,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
_height_tween.tween_property(
|
||||
_collapse_button,
|
||||
"position",
|
||||
|
|
@ -836,6 +952,22 @@ func _on_viewport_resized() -> void:
|
|||
_layout_presentation(false)
|
||||
|
||||
|
||||
func _on_world_time_changed(
|
||||
_time_hours: float,
|
||||
_phase: WorldTimeService.Phase,
|
||||
) -> void:
|
||||
if _clock_label != null and _world_time != null:
|
||||
_clock_label.text = _world_time.get_clock_text()
|
||||
|
||||
|
||||
func _on_world_weather_changed(
|
||||
weather: WorldWeatherService.Weather,
|
||||
_seconds_remaining: float,
|
||||
) -> void:
|
||||
if _weather_icon != null:
|
||||
_weather_icon.set_weather(weather)
|
||||
|
||||
|
||||
func _on_exterior_handle_entered() -> void:
|
||||
_panel_hovered = true
|
||||
_update_panel_opacity(true)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ const SurfaceDrawingToolbarType = preload(
|
|||
const PlayerSettingsManagerType = preload(
|
||||
"res://settings/player_settings_manager.gd"
|
||||
)
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
|
||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||
signal crisp_reset_focus_requested
|
||||
|
|
@ -145,13 +149,15 @@ func setup(
|
|||
settings_manager: PlayerSettingsManagerType,
|
||||
surface_drawing: NetworkSurfaceDrawingService,
|
||||
art_unlocks: PlayerArtUnlocks,
|
||||
world_time: WorldTimeServiceType,
|
||||
world_weather: WorldWeatherServiceType,
|
||||
) -> void:
|
||||
_player = player
|
||||
_fishing_spot = fishing_spot
|
||||
_item_effects = item_effects
|
||||
_chat_ui.setup(
|
||||
network_chat_service, network_session, spawn_service, player,
|
||||
fishing_spot, settings_manager
|
||||
fishing_spot, settings_manager, world_time, world_weather
|
||||
)
|
||||
_title_settings_panel.setup_network_profile(
|
||||
network_profile, network_session
|
||||
|
|
|
|||
80
ui/weather_icon.gd
Normal file
80
ui/weather_icon.gd
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
class_name WeatherIcon
|
||||
extends Control
|
||||
|
||||
const BUBBLE_COLOR := Color(0.025, 0.13, 0.19, 0.94)
|
||||
const ICON_COLOR := Color(0.78, 0.91, 0.95)
|
||||
const SUN_COLOR := Color(0.98, 0.82, 0.34)
|
||||
const RAIN_COLOR := Color(0.28, 0.73, 0.82)
|
||||
|
||||
var _weather: WorldWeatherService.Weather = WorldWeatherService.Weather.SUNNY
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
focus_mode = Control.FOCUS_NONE
|
||||
custom_minimum_size = Vector2(34.0, 34.0)
|
||||
tooltip_text = "sunny"
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func set_weather(weather: WorldWeatherService.Weather) -> void:
|
||||
if _weather == weather:
|
||||
return
|
||||
_weather = weather
|
||||
tooltip_text = WorldWeatherService.weather_name(_weather)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_weather() -> WorldWeatherService.Weather:
|
||||
return _weather
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var center: Vector2 = size * 0.5
|
||||
var radius: float = minf(size.x, size.y) * 0.5
|
||||
draw_circle(center, radius, BUBBLE_COLOR)
|
||||
match _weather:
|
||||
WorldWeatherService.Weather.SUNNY:
|
||||
_draw_sun(center)
|
||||
WorldWeatherService.Weather.CLOUDY:
|
||||
_draw_cloud(center + Vector2(0.0, 1.0))
|
||||
WorldWeatherService.Weather.RAINY:
|
||||
_draw_cloud(center + Vector2(0.0, -2.0))
|
||||
for x_offset: float in PackedFloat32Array([-6.0, 0.0, 6.0]):
|
||||
draw_line(
|
||||
center + Vector2(x_offset + 1.0, 5.0),
|
||||
center + Vector2(x_offset - 1.0, 9.0),
|
||||
RAIN_COLOR,
|
||||
2.0,
|
||||
true,
|
||||
)
|
||||
WorldWeatherService.Weather.FOGGY:
|
||||
for y_offset: float in PackedFloat32Array([-6.0, 0.0, 6.0]):
|
||||
draw_line(
|
||||
center + Vector2(-9.0, y_offset),
|
||||
center + Vector2(9.0, y_offset),
|
||||
ICON_COLOR,
|
||||
2.0,
|
||||
true,
|
||||
)
|
||||
|
||||
|
||||
func _draw_sun(center: Vector2) -> void:
|
||||
draw_circle(center, 5.0, SUN_COLOR)
|
||||
for index: int in 8:
|
||||
var angle: float = TAU * float(index) / 8.0
|
||||
var direction := Vector2(cos(angle), sin(angle))
|
||||
draw_line(
|
||||
center + direction * 8.0,
|
||||
center + direction * 11.0,
|
||||
SUN_COLOR,
|
||||
2.0,
|
||||
true,
|
||||
)
|
||||
|
||||
|
||||
func _draw_cloud(center: Vector2) -> void:
|
||||
draw_circle(center + Vector2(-5.0, 0.0), 5.0, ICON_COLOR)
|
||||
draw_circle(center + Vector2(0.0, -3.0), 6.0, ICON_COLOR)
|
||||
draw_circle(center + Vector2(6.0, 0.0), 5.0, ICON_COLOR)
|
||||
draw_rect(Rect2(center + Vector2(-9.0, 0.0), Vector2(18.0, 5.0)), ICON_COLOR)
|
||||
1
ui/weather_icon.gd.uid
Normal file
1
ui/weather_icon.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://crcognj0s8ntq
|
||||
|
|
@ -15,6 +15,8 @@ const FishingShopInteractionType = preload(
|
|||
@onready var _fishing_shop: FishingShopInteractionType = (
|
||||
_starter_island.get_fishing_shop()
|
||||
)
|
||||
@onready var _world_environment: WorldEnvironment = $Environment/WorldEnvironment
|
||||
@onready var _sun: DirectionalLight3D = $Environment/Sun
|
||||
|
||||
|
||||
func get_player_water_triggers() -> Array[PlayerWaterTrigger]:
|
||||
|
|
@ -40,6 +42,14 @@ func get_player_spawn_transform() -> Transform3D:
|
|||
return _starter_island.get_player_spawn_transform()
|
||||
|
||||
|
||||
func get_world_environment() -> WorldEnvironment:
|
||||
return _world_environment
|
||||
|
||||
|
||||
func get_sun() -> DirectionalLight3D:
|
||||
return _sun
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
var waters: Array[FishableWaterRegion] = []
|
||||
for region: WorldRegion in _get_regions():
|
||||
|
|
|
|||
126
world/world_time_service.gd
Normal file
126
world/world_time_service.gd
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
class_name WorldTimeService
|
||||
extends Node
|
||||
|
||||
signal time_changed(time_hours: float, phase: Phase)
|
||||
signal phase_changed(phase: Phase)
|
||||
|
||||
enum Phase {
|
||||
DAWN,
|
||||
DAY,
|
||||
DUSK,
|
||||
NIGHT,
|
||||
}
|
||||
|
||||
const HOURS_PER_DAY: float = 24.0
|
||||
const REAL_SECONDS_PER_CYCLE: float = 60.0 * 60.0
|
||||
const HOURS_PER_REAL_SECOND: float = HOURS_PER_DAY / REAL_SECONDS_PER_CYCLE
|
||||
const DAY_START_HOUR: float = 8.0
|
||||
const NIGHT_START_HOUR: float = 20.0
|
||||
const TRANSITION_HALF_HOURS: float = 0.5
|
||||
const DAWN_START_HOUR: float = DAY_START_HOUR - TRANSITION_HALF_HOURS
|
||||
const DAWN_END_HOUR: float = DAY_START_HOUR + TRANSITION_HALF_HOURS
|
||||
const DUSK_START_HOUR: float = NIGHT_START_HOUR - TRANSITION_HALF_HOURS
|
||||
const DUSK_END_HOUR: float = NIGHT_START_HOUR + TRANSITION_HALF_HOURS
|
||||
const DEFAULT_START_HOUR: float = DAY_START_HOUR
|
||||
|
||||
var _time_hours: float = DEFAULT_START_HOUR
|
||||
var _running: bool = false
|
||||
var _phase: Phase = Phase.DAWN
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(false)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
advance_time(delta)
|
||||
|
||||
|
||||
func begin_session(start_hour: float = DEFAULT_START_HOUR) -> void:
|
||||
_running = true
|
||||
set_process(true)
|
||||
_set_time_hours(start_hour, true)
|
||||
|
||||
|
||||
func end_session() -> void:
|
||||
_running = false
|
||||
set_process(false)
|
||||
_set_time_hours(DEFAULT_START_HOUR, true)
|
||||
|
||||
|
||||
func advance_time(real_seconds: float) -> void:
|
||||
if not _running or real_seconds <= 0.0:
|
||||
return
|
||||
_set_time_hours(
|
||||
_time_hours + real_seconds * HOURS_PER_REAL_SECOND,
|
||||
false,
|
||||
)
|
||||
|
||||
|
||||
func synchronize_time(authoritative_time_hours: float) -> void:
|
||||
if not is_finite(authoritative_time_hours):
|
||||
return
|
||||
_set_time_hours(authoritative_time_hours, true)
|
||||
|
||||
|
||||
func get_time_hours() -> float:
|
||||
return _time_hours
|
||||
|
||||
|
||||
func get_phase() -> Phase:
|
||||
return _phase
|
||||
|
||||
|
||||
func is_night_period() -> bool:
|
||||
return _time_hours < DAY_START_HOUR or _time_hours >= NIGHT_START_HOUR
|
||||
|
||||
|
||||
func is_transition() -> bool:
|
||||
return _phase in [Phase.DAWN, Phase.DUSK]
|
||||
|
||||
|
||||
func get_clock_text() -> String:
|
||||
return format_clock_time(_time_hours)
|
||||
|
||||
|
||||
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:
|
||||
return Phase.DAWN
|
||||
if hour >= DAWN_END_HOUR and hour < DUSK_START_HOUR:
|
||||
return Phase.DAY
|
||||
if hour >= DUSK_START_HOUR and hour < DUSK_END_HOUR:
|
||||
return Phase.DUSK
|
||||
return Phase.NIGHT
|
||||
|
||||
|
||||
static func format_clock_time(time_hours: float) -> String:
|
||||
var hour: float = _normalized_hour(time_hours)
|
||||
var total_minutes: int = floori(hour * 60.0)
|
||||
var hour_24: int = floori(float(total_minutes) / 60.0)
|
||||
var minute: int = total_minutes % 60
|
||||
var hour_12: int = hour_24 % 12
|
||||
if hour_12 == 0:
|
||||
hour_12 = 12
|
||||
return "%d:%02d %s" % [
|
||||
hour_12,
|
||||
minute,
|
||||
"am" if hour_24 < 12 else "pm",
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
var phase_was_changed: bool = next_phase != _phase
|
||||
var time_was_changed: bool = not is_equal_approx(normalized, _time_hours)
|
||||
_time_hours = normalized
|
||||
_phase = next_phase
|
||||
if phase_was_changed:
|
||||
phase_changed.emit(_phase)
|
||||
if force_emit or time_was_changed or phase_was_changed:
|
||||
time_changed.emit(_time_hours, _phase)
|
||||
|
||||
|
||||
static func _normalized_hour(time_hours: float) -> float:
|
||||
return fposmod(time_hours, HOURS_PER_DAY)
|
||||
1
world/world_time_service.gd.uid
Normal file
1
world/world_time_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://5lceyulaglpf
|
||||
389
world/world_time_visual_controller.gd
Normal file
389
world/world_time_visual_controller.gd
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
class_name WorldTimeVisualController
|
||||
extends Node
|
||||
|
||||
const UPDATE_INTERVAL_SECONDS: float = 0.1
|
||||
const SUN_YAW_DEGREES: float = -32.0
|
||||
const WEATHER_TRANSITION_SECONDS: float = 10.0
|
||||
const RAIN_EMITTER_OFFSET := Vector3(0.0, 7.0, 0.0)
|
||||
|
||||
const DAY_SKY_TOP := Color(0.204, 0.498, 0.643)
|
||||
const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847)
|
||||
const DAY_GROUND_BOTTOM := Color(0.157, 0.361, 0.439)
|
||||
const DAY_GROUND_HORIZON := Color(0.549, 0.749, 0.765)
|
||||
const DAY_AMBIENT := Color(0.82, 0.90, 0.92)
|
||||
const DAY_FOG := Color(0.549, 0.749, 0.765)
|
||||
const DAY_SUN := Color(0.88, 0.94, 0.96)
|
||||
|
||||
const NIGHT_SKY_TOP := Color(0.026, 0.050, 0.105)
|
||||
const NIGHT_SKY_HORIZON := Color(0.10, 0.17, 0.28)
|
||||
const NIGHT_GROUND_BOTTOM := Color(0.020, 0.040, 0.080)
|
||||
const NIGHT_GROUND_HORIZON := Color(0.075, 0.135, 0.22)
|
||||
const NIGHT_AMBIENT := Color(0.28, 0.35, 0.48)
|
||||
const NIGHT_FOG := Color(0.13, 0.21, 0.32)
|
||||
const NIGHT_SUN := Color(0.35, 0.44, 0.62)
|
||||
|
||||
const WARM_SKY_TOP := Color(0.31, 0.30, 0.42)
|
||||
const WARM_SKY_HORIZON := Color(0.96, 0.48, 0.22)
|
||||
const WARM_GROUND_BOTTOM := Color(0.20, 0.14, 0.16)
|
||||
const WARM_GROUND_HORIZON := Color(0.82, 0.34, 0.18)
|
||||
const WARM_AMBIENT := Color(0.92, 0.58, 0.40)
|
||||
const WARM_FOG := Color(0.74, 0.39, 0.27)
|
||||
const WARM_SUN := Color(1.0, 0.58, 0.30)
|
||||
|
||||
const CLOUDY_TINT := Color(0.53, 0.61, 0.66)
|
||||
const RAINY_TINT := Color(0.31, 0.42, 0.50)
|
||||
const FOGGY_TINT := Color(0.62, 0.70, 0.72)
|
||||
|
||||
var _time_service: WorldTimeService
|
||||
var _weather_service: WorldWeatherService
|
||||
var _world_environment: WorldEnvironment
|
||||
var _sun: DirectionalLight3D
|
||||
var _rain_target: Node3D
|
||||
var _environment: Environment
|
||||
var _sky_material: ProceduralSkyMaterial
|
||||
var _rain: GPUParticles3D
|
||||
var _elapsed: float = 0.0
|
||||
var _weather_from: WorldWeatherService.Weather = (
|
||||
WorldWeatherService.Weather.SUNNY
|
||||
)
|
||||
var _weather_to: WorldWeatherService.Weather = (
|
||||
WorldWeatherService.Weather.SUNNY
|
||||
)
|
||||
var _weather_transition: float = 1.0
|
||||
|
||||
|
||||
func setup(
|
||||
time_service: WorldTimeService,
|
||||
world_environment: WorldEnvironment,
|
||||
sun: DirectionalLight3D,
|
||||
weather_service: WorldWeatherService = null,
|
||||
rain_target: Node3D = null,
|
||||
) -> void:
|
||||
_time_service = time_service
|
||||
_weather_service = weather_service
|
||||
_world_environment = world_environment
|
||||
_sun = sun
|
||||
_rain_target = rain_target
|
||||
if not _prepare_runtime_environment():
|
||||
set_process(false)
|
||||
return
|
||||
_prepare_rain()
|
||||
if _weather_service != null:
|
||||
_weather_from = _weather_service.get_weather()
|
||||
_weather_to = _weather_from
|
||||
_weather_transition = 1.0
|
||||
if not _weather_service.weather_changed.is_connected(
|
||||
_on_weather_changed
|
||||
):
|
||||
_weather_service.weather_changed.connect(_on_weather_changed)
|
||||
set_process(true)
|
||||
_apply_time(_time_service.get_time_hours())
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _time_service == null:
|
||||
return
|
||||
_update_rain_position()
|
||||
if _weather_transition < 1.0:
|
||||
_weather_transition = minf(
|
||||
_weather_transition + delta / WEATHER_TRANSITION_SECONDS,
|
||||
1.0,
|
||||
)
|
||||
_elapsed += delta
|
||||
if _elapsed < UPDATE_INTERVAL_SECONDS:
|
||||
return
|
||||
_elapsed = 0.0
|
||||
_apply_time(_time_service.get_time_hours())
|
||||
|
||||
|
||||
func apply_time_immediately(time_hours: float) -> void:
|
||||
_apply_time(time_hours)
|
||||
|
||||
|
||||
func apply_weather_immediately(
|
||||
weather: WorldWeatherService.Weather,
|
||||
) -> void:
|
||||
_weather_from = weather
|
||||
_weather_to = weather
|
||||
_weather_transition = 1.0
|
||||
if _time_service != null:
|
||||
_apply_time(_time_service.get_time_hours())
|
||||
|
||||
|
||||
func _prepare_runtime_environment() -> bool:
|
||||
if (
|
||||
_time_service == null
|
||||
or _world_environment == null
|
||||
or _world_environment.environment == null
|
||||
or _sun == null
|
||||
):
|
||||
push_error("World time visuals require an environment and sun.")
|
||||
return false
|
||||
_environment = _world_environment.environment.duplicate(true) as Environment
|
||||
if _environment == null or _environment.sky == null:
|
||||
push_error("World time visuals could not duplicate the world environment.")
|
||||
return false
|
||||
var runtime_sky: Sky = _environment.sky.duplicate(true) as Sky
|
||||
if runtime_sky == null or runtime_sky.sky_material == null:
|
||||
push_error("World time visuals require a procedural sky material.")
|
||||
return false
|
||||
_sky_material = (
|
||||
runtime_sky.sky_material.duplicate(true) as ProceduralSkyMaterial
|
||||
)
|
||||
if _sky_material == null:
|
||||
push_error("World time visuals require a ProceduralSkyMaterial.")
|
||||
return false
|
||||
runtime_sky.sky_material = _sky_material
|
||||
_environment.sky = runtime_sky
|
||||
_world_environment.environment = _environment
|
||||
return true
|
||||
|
||||
|
||||
func _prepare_rain() -> void:
|
||||
_rain = GPUParticles3D.new()
|
||||
_rain.name = "LocalRain"
|
||||
_rain.amount = 480
|
||||
_rain.amount_ratio = 0.0
|
||||
_rain.lifetime = 1.25
|
||||
_rain.fixed_fps = 30
|
||||
_rain.local_coords = false
|
||||
_rain.visibility_aabb = AABB(
|
||||
Vector3(-7.0, -9.0, -7.0), Vector3(14.0, 12.0, 14.0)
|
||||
)
|
||||
var process_material := ParticleProcessMaterial.new()
|
||||
process_material.emission_shape = (
|
||||
ParticleProcessMaterial.EMISSION_SHAPE_BOX
|
||||
)
|
||||
process_material.emission_box_extents = Vector3(6.5, 1.0, 6.5)
|
||||
process_material.direction = Vector3.DOWN
|
||||
process_material.spread = 5.0
|
||||
process_material.initial_velocity_min = 11.0
|
||||
process_material.initial_velocity_max = 15.0
|
||||
process_material.gravity = Vector3(0.0, -2.0, 0.0)
|
||||
_rain.process_material = process_material
|
||||
var drop_mesh := BoxMesh.new()
|
||||
drop_mesh.size = Vector3(0.018, 0.42, 0.018)
|
||||
var drop_material := StandardMaterial3D.new()
|
||||
drop_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
drop_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
drop_material.albedo_color = Color(0.48, 0.78, 0.90, 0.72)
|
||||
drop_mesh.material = drop_material
|
||||
_rain.draw_pass_1 = drop_mesh
|
||||
add_child(_rain)
|
||||
_update_rain_position()
|
||||
|
||||
|
||||
func _apply_time(time_hours: float) -> void:
|
||||
if _environment == null or _sky_material == null or _sun == null:
|
||||
return
|
||||
var hour: float = fposmod(time_hours, WorldTimeService.HOURS_PER_DAY)
|
||||
var daylight: float = _daylight_amount(hour)
|
||||
var warmth: float = _transition_warmth(hour)
|
||||
var sky_top: Color = _blended_color(
|
||||
NIGHT_SKY_TOP, DAY_SKY_TOP, WARM_SKY_TOP, daylight, warmth
|
||||
)
|
||||
var sky_horizon: Color = _blended_color(
|
||||
NIGHT_SKY_HORIZON,
|
||||
DAY_SKY_HORIZON,
|
||||
WARM_SKY_HORIZON,
|
||||
daylight,
|
||||
warmth,
|
||||
)
|
||||
var ground_bottom: Color = _blended_color(
|
||||
NIGHT_GROUND_BOTTOM,
|
||||
DAY_GROUND_BOTTOM,
|
||||
WARM_GROUND_BOTTOM,
|
||||
daylight,
|
||||
warmth,
|
||||
)
|
||||
var ground_horizon: Color = _blended_color(
|
||||
NIGHT_GROUND_HORIZON,
|
||||
DAY_GROUND_HORIZON,
|
||||
WARM_GROUND_HORIZON,
|
||||
daylight,
|
||||
warmth,
|
||||
)
|
||||
var ambient: Color = _blended_color(
|
||||
NIGHT_AMBIENT, DAY_AMBIENT, WARM_AMBIENT, daylight, warmth
|
||||
)
|
||||
var fog_color: Color = _blended_color(
|
||||
NIGHT_FOG, DAY_FOG, WARM_FOG, daylight, warmth
|
||||
)
|
||||
var weather_tint: Color = _weather_color()
|
||||
var tint_strength: float = _weather_value(
|
||||
0.0, 0.30, 0.48, 0.62
|
||||
)
|
||||
_sky_material.sky_top_color = sky_top.lerp(weather_tint, tint_strength)
|
||||
_sky_material.sky_horizon_color = sky_horizon.lerp(
|
||||
weather_tint, tint_strength
|
||||
)
|
||||
_sky_material.ground_bottom_color = ground_bottom.lerp(
|
||||
weather_tint, tint_strength * 0.62
|
||||
)
|
||||
_sky_material.ground_horizon_color = ground_horizon.lerp(
|
||||
weather_tint, tint_strength * 0.76
|
||||
)
|
||||
_environment.background_energy_multiplier = (
|
||||
lerpf(0.52, 0.85, daylight)
|
||||
* _weather_value(1.0, 0.82, 0.66, 0.74)
|
||||
)
|
||||
_environment.ambient_light_color = ambient.lerp(
|
||||
weather_tint, tint_strength * 0.45
|
||||
)
|
||||
_environment.ambient_light_energy = (
|
||||
lerpf(0.66, 1.08, daylight)
|
||||
* _weather_value(1.0, 0.88, 0.76, 0.82)
|
||||
)
|
||||
_environment.fog_light_color = fog_color.lerp(
|
||||
weather_tint, tint_strength * 0.82
|
||||
)
|
||||
_environment.fog_light_energy = (
|
||||
lerpf(0.56, 0.82, daylight)
|
||||
* _weather_value(1.0, 0.92, 0.82, 1.05)
|
||||
)
|
||||
_environment.fog_aerial_perspective = _weather_value(
|
||||
0.35, 0.48, 0.62, 0.92
|
||||
)
|
||||
_environment.fog_sky_affect = _weather_value(
|
||||
0.35, 0.48, 0.68, 0.94
|
||||
)
|
||||
_environment.fog_depth_curve = _weather_value(
|
||||
1.6, 1.5, 1.4, 1.18
|
||||
)
|
||||
_environment.fog_depth_begin = _weather_value(
|
||||
42.0, 32.0, 20.0, 4.0
|
||||
)
|
||||
_environment.fog_depth_end = _weather_value(
|
||||
170.0, 140.0, 95.0, 42.0
|
||||
)
|
||||
_environment.adjustment_brightness = (
|
||||
lerpf(0.93, 0.98, daylight)
|
||||
* _weather_value(1.0, 0.97, 0.92, 0.96)
|
||||
)
|
||||
_environment.adjustment_saturation = (
|
||||
lerpf(0.82, 0.91, daylight)
|
||||
* _weather_value(1.0, 0.88, 0.78, 0.70)
|
||||
)
|
||||
_sun.rotation_degrees = Vector3(
|
||||
-360.0 * fposmod(hour - WorldTimeService.DAY_START_HOUR, 24.0) / 24.0,
|
||||
SUN_YAW_DEGREES,
|
||||
0.0,
|
||||
)
|
||||
_sun.light_color = _blended_color(
|
||||
NIGHT_SUN, DAY_SUN, WARM_SUN, daylight, warmth
|
||||
)
|
||||
_sun.light_energy = (
|
||||
lerpf(0.0, 0.10, daylight)
|
||||
* _weather_value(1.0, 0.55, 0.25, 0.35)
|
||||
)
|
||||
_update_rain_amount()
|
||||
|
||||
|
||||
func _on_weather_changed(
|
||||
weather: WorldWeatherService.Weather,
|
||||
_seconds_remaining: float,
|
||||
) -> void:
|
||||
if weather == _weather_to:
|
||||
return
|
||||
_weather_from = _weather_to
|
||||
_weather_to = weather
|
||||
_weather_transition = 0.0
|
||||
|
||||
|
||||
func _weather_value(
|
||||
sunny: float,
|
||||
cloudy: float,
|
||||
rainy: float,
|
||||
foggy: float,
|
||||
) -> float:
|
||||
return lerpf(
|
||||
_weather_state_value(_weather_from, sunny, cloudy, rainy, foggy),
|
||||
_weather_state_value(_weather_to, sunny, cloudy, rainy, foggy),
|
||||
_weather_transition,
|
||||
)
|
||||
|
||||
|
||||
func _weather_state_value(
|
||||
weather: WorldWeatherService.Weather,
|
||||
sunny: float,
|
||||
cloudy: float,
|
||||
rainy: float,
|
||||
foggy: float,
|
||||
) -> float:
|
||||
match weather:
|
||||
WorldWeatherService.Weather.CLOUDY:
|
||||
return cloudy
|
||||
WorldWeatherService.Weather.RAINY:
|
||||
return rainy
|
||||
WorldWeatherService.Weather.FOGGY:
|
||||
return foggy
|
||||
return sunny
|
||||
|
||||
|
||||
func _weather_color() -> Color:
|
||||
return _weather_state_color(_weather_from).lerp(
|
||||
_weather_state_color(_weather_to), _weather_transition
|
||||
)
|
||||
|
||||
|
||||
func _weather_state_color(
|
||||
weather: WorldWeatherService.Weather,
|
||||
) -> Color:
|
||||
match weather:
|
||||
WorldWeatherService.Weather.CLOUDY:
|
||||
return CLOUDY_TINT
|
||||
WorldWeatherService.Weather.RAINY:
|
||||
return RAINY_TINT
|
||||
WorldWeatherService.Weather.FOGGY:
|
||||
return FOGGY_TINT
|
||||
return Color.WHITE
|
||||
|
||||
|
||||
func _update_rain_amount() -> void:
|
||||
if _rain == null:
|
||||
return
|
||||
var rain_amount: float = _weather_value(0.0, 0.0, 1.0, 0.0)
|
||||
_rain.amount_ratio = rain_amount
|
||||
_rain.emitting = rain_amount > 0.01
|
||||
|
||||
|
||||
func _update_rain_position() -> void:
|
||||
if _rain == null or _rain_target == null:
|
||||
return
|
||||
_rain.global_position = _rain_target.global_position + RAIN_EMITTER_OFFSET
|
||||
|
||||
|
||||
func _daylight_amount(hour: float) -> float:
|
||||
if hour >= WorldTimeService.DAWN_START_HOUR and hour < WorldTimeService.DAWN_END_HOUR:
|
||||
return smoothstep(
|
||||
WorldTimeService.DAWN_START_HOUR,
|
||||
WorldTimeService.DAWN_END_HOUR,
|
||||
hour,
|
||||
)
|
||||
if hour >= WorldTimeService.DAWN_END_HOUR and hour < WorldTimeService.DUSK_START_HOUR:
|
||||
return 1.0
|
||||
if hour >= WorldTimeService.DUSK_START_HOUR and hour < WorldTimeService.DUSK_END_HOUR:
|
||||
return 1.0 - smoothstep(
|
||||
WorldTimeService.DUSK_START_HOUR,
|
||||
WorldTimeService.DUSK_END_HOUR,
|
||||
hour,
|
||||
)
|
||||
return 0.0
|
||||
|
||||
|
||||
func _transition_warmth(hour: float) -> float:
|
||||
if hour >= WorldTimeService.DAWN_START_HOUR and hour < WorldTimeService.DAWN_END_HOUR:
|
||||
return 1.0 - absf(hour - WorldTimeService.DAY_START_HOUR) / WorldTimeService.TRANSITION_HALF_HOURS
|
||||
if hour >= WorldTimeService.DUSK_START_HOUR and hour < WorldTimeService.DUSK_END_HOUR:
|
||||
return 1.0 - absf(hour - WorldTimeService.NIGHT_START_HOUR) / WorldTimeService.TRANSITION_HALF_HOURS
|
||||
return 0.0
|
||||
|
||||
|
||||
func _blended_color(
|
||||
night_color: Color,
|
||||
day_color: Color,
|
||||
warm_color: Color,
|
||||
daylight: float,
|
||||
warmth: float,
|
||||
) -> Color:
|
||||
return night_color.lerp(day_color, daylight).lerp(warm_color, warmth)
|
||||
1
world/world_time_visual_controller.gd.uid
Normal file
1
world/world_time_visual_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://svirs8vbypad
|
||||
152
world/world_weather_service.gd
Normal file
152
world/world_weather_service.gd
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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)
|
||||
|
||||
var _weather: Weather = DEFAULT_WEATHER
|
||||
var _seconds_remaining: float = SUNNY_DURATION_RANGE.x
|
||||
var _running_authority: bool = false
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
|
||||
|
||||
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)
|
||||
_set_weather(DEFAULT_WEATHER, _roll_duration(DEFAULT_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
|
||||
_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
|
||||
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
static func is_valid_weather(value: int) -> bool:
|
||||
return value >= Weather.SUNNY and value <= Weather.FOGGY
|
||||
|
||||
|
||||
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 force_emit or changed:
|
||||
weather_changed.emit(_weather, _seconds_remaining)
|
||||
|
||||
|
||||
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
|
||||
1
world/world_weather_service.gd.uid
Normal file
1
world/world_weather_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://v4c54isq6w74
|
||||
Loading…
Add table
Add a link
Reference in a new issue