Add catch experience and persistent world time
This commit is contained in:
parent
35d00e8035
commit
291cc4a713
23 changed files with 1027 additions and 27 deletions
73
fish/fish_experience.gd
Normal file
73
fish/fish_experience.gd
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
class_name FishExperience
|
||||
extends RefCounted
|
||||
|
||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||
|
||||
const RARITY_BASE_EXPERIENCE: Array[int] = [10, 18, 32, 55, 90]
|
||||
const QUALITY_MULTIPLIERS: Array[float] = [1.0, 1.15, 1.4, 1.8, 2.5]
|
||||
const MAXIMUM_WEIGHT_BONUS: float = 0.25
|
||||
const FIRST_SPECIES_BONUS: int = 25
|
||||
const FIRST_QUALITY_BONUS: int = 15
|
||||
const SPECIES_MASTERY_BONUS: int = 100
|
||||
|
||||
|
||||
static func calculate_for_collection(
|
||||
fish_catch: FishCatchType,
|
||||
collection_log: CollectionLogType,
|
||||
) -> int:
|
||||
if fish_catch == null or collection_log == null:
|
||||
return 0
|
||||
return calculate_catch_experience(
|
||||
fish_catch,
|
||||
collection_log.has_discovered(fish_catch.fish_id),
|
||||
collection_log.get_quality_mask(fish_catch.fish_id),
|
||||
)
|
||||
|
||||
|
||||
static func calculate_catch_experience(
|
||||
fish_catch: FishCatchType,
|
||||
was_species_discovered: bool,
|
||||
previous_quality_mask: int,
|
||||
) -> int:
|
||||
if fish_catch == null or not fish_catch.is_valid():
|
||||
return 0
|
||||
var rarity: int = int(fish_catch.fish.rarity)
|
||||
if rarity < 0 or rarity >= RARITY_BASE_EXPERIENCE.size():
|
||||
return 0
|
||||
if not FishQualityType.is_valid(fish_catch.quality):
|
||||
return 0
|
||||
var weight_percentile: float = _get_weight_percentile(fish_catch)
|
||||
var weight_multiplier: float = (
|
||||
1.0 + MAXIMUM_WEIGHT_BONUS * weight_percentile * weight_percentile
|
||||
)
|
||||
var catch_experience: int = roundi(
|
||||
float(RARITY_BASE_EXPERIENCE[rarity])
|
||||
* QUALITY_MULTIPLIERS[fish_catch.quality]
|
||||
* weight_multiplier
|
||||
)
|
||||
var quality_bit: int = FishQualityType.bit_for(fish_catch.quality)
|
||||
if not was_species_discovered:
|
||||
catch_experience += FIRST_SPECIES_BONUS
|
||||
if (previous_quality_mask & quality_bit) == 0:
|
||||
catch_experience += FIRST_QUALITY_BONUS
|
||||
var next_quality_mask: int = previous_quality_mask | quality_bit
|
||||
if (
|
||||
previous_quality_mask != FishQualityType.ALL_TIERS_MASK
|
||||
and next_quality_mask == FishQualityType.ALL_TIERS_MASK
|
||||
):
|
||||
catch_experience += SPECIES_MASTERY_BONUS
|
||||
return maxi(catch_experience, 0)
|
||||
|
||||
|
||||
static func _get_weight_percentile(fish_catch: FishCatchType) -> float:
|
||||
var minimum_weight: float = fish_catch.fish.get_minimum_weight()
|
||||
var maximum_weight: float = fish_catch.fish.get_maximum_weight()
|
||||
if maximum_weight <= minimum_weight:
|
||||
return 0.0
|
||||
return clampf(
|
||||
inverse_lerp(minimum_weight, maximum_weight, fish_catch.weight_lb),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
1
fish/fish_experience.gd.uid
Normal file
1
fish/fish_experience.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://d05mwat7tju1b
|
||||
|
|
@ -9,6 +9,10 @@ const CatchControllerType = preload("res://fishing/catch_controller.gd")
|
|||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||
const ItemDataType = preload("res://items/item_data.gd")
|
||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||
|
|
@ -101,9 +105,9 @@ const BITE_QUICK_MAX_SECONDS: float = 30.0
|
|||
const BITE_TYPICAL_MAX_SECONDS: float = 90.0
|
||||
const BITE_LONG_MAX_SECONDS: float = 180.0
|
||||
const BITE_MAX_SECONDS: float = 240.0
|
||||
const BITE_QUICK_PROBABILITY: float = 0.15
|
||||
const BITE_TYPICAL_PROBABILITY: float = 0.55
|
||||
const BITE_LONG_PROBABILITY: float = 0.25
|
||||
const BITE_QUICK_PROBABILITY: float = 0.25
|
||||
const BITE_TYPICAL_PROBABILITY: float = 0.65
|
||||
const BITE_LONG_PROBABILITY: float = 0.08
|
||||
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
||||
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
|
||||
|
||||
|
|
@ -126,6 +130,7 @@ var _local_menu_input_owners: Dictionary[StringName, bool] = {}
|
|||
var _local_player: PlayerType
|
||||
var _local_inventory: FishInventoryType
|
||||
var _local_collection_log: CollectionLogType
|
||||
var _local_experience: PlayerExperienceType
|
||||
var _local_bag: PlayerBagType
|
||||
var _local_hotbar: PlayerHotbarType
|
||||
var _item_catalog: ItemCatalogType
|
||||
|
|
@ -186,6 +191,7 @@ func setup(
|
|||
local_player: PlayerType,
|
||||
local_inventory: FishInventoryType,
|
||||
local_collection_log: CollectionLogType,
|
||||
local_experience: PlayerExperienceType,
|
||||
local_bag: PlayerBagType,
|
||||
local_hotbar: PlayerHotbarType,
|
||||
item_catalog: ItemCatalogType,
|
||||
|
|
@ -201,6 +207,7 @@ func setup(
|
|||
_local_player = local_player
|
||||
_local_inventory = local_inventory
|
||||
_local_collection_log = local_collection_log
|
||||
_local_experience = local_experience
|
||||
_local_bag = local_bag
|
||||
_local_hotbar = local_hotbar
|
||||
_item_catalog = item_catalog
|
||||
|
|
@ -382,11 +389,7 @@ func _secure_showcase_catch_for_recovery() -> void:
|
|||
and _local_inventory != null
|
||||
and _local_collection_log != null
|
||||
):
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_store_catch_progression(_pending_catch)
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_put_away_press_armed = false
|
||||
|
|
@ -406,11 +409,7 @@ func _exit_tree() -> void:
|
|||
and _local_collection_log != null
|
||||
and is_instance_valid(_local_collection_log)
|
||||
):
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_store_catch_progression(_pending_catch)
|
||||
_pending_catch = null
|
||||
if _active_player != null and is_instance_valid(_active_player):
|
||||
_active_player.end_catch_showcase(Callable(), true)
|
||||
|
|
@ -1061,11 +1060,7 @@ func _put_away_catch() -> void:
|
|||
or _pending_catch == null
|
||||
):
|
||||
return
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_store_catch_progression(_pending_catch)
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
|
|
@ -1081,6 +1076,29 @@ func _put_away_catch() -> void:
|
|||
)
|
||||
|
||||
|
||||
func _store_catch_progression(fish_catch: FishCatchType) -> void:
|
||||
if (
|
||||
fish_catch == null
|
||||
or _local_inventory == null
|
||||
or _local_collection_log == null
|
||||
or _local_experience == null
|
||||
or _local_inventory.contains_catch_id(fish_catch.catch_id)
|
||||
):
|
||||
return
|
||||
var experience_award: int = (
|
||||
FishExperienceType.calculate_for_collection(
|
||||
fish_catch,
|
||||
_local_collection_log,
|
||||
)
|
||||
)
|
||||
_local_inventory.add_catch(fish_catch)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
fish_catch.fish_id,
|
||||
fish_catch.quality,
|
||||
)
|
||||
_local_experience.award_experience(experience_award)
|
||||
|
||||
|
||||
func _finish_showcase_put_away(
|
||||
restore_generation: int,
|
||||
cooldown_message: String,
|
||||
|
|
|
|||
14
main/main.gd
14
main/main.gd
|
|
@ -357,6 +357,8 @@ func _initialize_after_data_root() -> void:
|
|||
_player.fishing_upgrades,
|
||||
_player.cooler_capacity,
|
||||
_player.art_unlocks,
|
||||
_player.experience,
|
||||
_world_time,
|
||||
)
|
||||
_save_manager.set_autosave_enabled(false)
|
||||
_asset_reservations.setup(
|
||||
|
|
@ -411,6 +413,7 @@ func _initialize_after_data_root() -> void:
|
|||
_player.inventory,
|
||||
_player.collection_log,
|
||||
_player.cooler_capacity,
|
||||
_player.experience,
|
||||
_save_manager,
|
||||
item_catalog,
|
||||
fish_catalog,
|
||||
|
|
@ -451,6 +454,7 @@ func _initialize_after_data_root() -> void:
|
|||
_player,
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
_player.experience,
|
||||
_player.bag,
|
||||
_player.hotbar,
|
||||
item_catalog,
|
||||
|
|
@ -467,6 +471,7 @@ func _initialize_after_data_root() -> void:
|
|||
_player,
|
||||
_player.inventory,
|
||||
_player.collection_log,
|
||||
_player.experience,
|
||||
_player.wallet,
|
||||
_player.fish_sale_service,
|
||||
pelican_buyer_profile,
|
||||
|
|
@ -1154,6 +1159,11 @@ func _on_return_to_title_requested() -> void:
|
|||
if _quit_in_progress:
|
||||
return
|
||||
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
|
||||
if not _save_manager.save_world_time_checkpoint():
|
||||
pause_menu.report_network_error(
|
||||
"Could not save progression before returning to title."
|
||||
)
|
||||
return
|
||||
pause_menu.close_for_title_transition()
|
||||
_network_session.disconnect_session("Returned to title.")
|
||||
_set_gameplay_active(false)
|
||||
|
|
@ -1176,7 +1186,7 @@ func _on_title_join_game_requested(endpoint: String) -> void:
|
|||
func _on_pause_join_game_requested(endpoint: String) -> void:
|
||||
if _quit_in_progress or not _gameplay_started:
|
||||
return
|
||||
if not _save_manager.save_if_dirty():
|
||||
if not _save_manager.save_world_time_checkpoint():
|
||||
_game_ui.get_pause_menu().report_network_error(
|
||||
"Could not save progression before leaving this session."
|
||||
)
|
||||
|
|
@ -1409,7 +1419,7 @@ func _on_quit_requested() -> void:
|
|||
_quit_in_progress = true
|
||||
_settings_manager.save_if_dirty()
|
||||
if _gameplay_started:
|
||||
_save_manager.save_if_dirty()
|
||||
_save_manager.save_world_time_checkpoint()
|
||||
_network_session.disconnect_session("Application closing.")
|
||||
_title_music_requested = false
|
||||
_replace_title_music_transition()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
|
|||
const FishSelectorType = preload("res://fish/fish_selector.gd")
|
||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const RemotePresentationType = preload(
|
||||
"res://fishing/remote_fishing_presentation.gd"
|
||||
)
|
||||
|
|
@ -29,6 +33,7 @@ var _fishing_spot: FishingSpot
|
|||
var _local_inventory: FishInventory
|
||||
var _local_collection: CollectionLog
|
||||
var _local_capacity: PlayerCoolerCapacity
|
||||
var _local_experience: PlayerExperienceType
|
||||
var _save_manager: PlayerSaveManager
|
||||
var _item_catalog: ItemCatalog
|
||||
var _fish_catalog: FishPoolType
|
||||
|
|
@ -51,6 +56,7 @@ func setup(
|
|||
local_inventory: FishInventory,
|
||||
local_collection: CollectionLog,
|
||||
local_capacity: PlayerCoolerCapacity,
|
||||
local_experience: PlayerExperienceType,
|
||||
save_manager: PlayerSaveManager,
|
||||
item_catalog: ItemCatalog,
|
||||
fish_catalog: FishPoolType,
|
||||
|
|
@ -62,6 +68,7 @@ func setup(
|
|||
_local_inventory = local_inventory
|
||||
_local_collection = local_collection
|
||||
_local_capacity = local_capacity
|
||||
_local_experience = local_experience
|
||||
_save_manager = save_manager
|
||||
_item_catalog = item_catalog
|
||||
_fish_catalog = fish_catalog
|
||||
|
|
@ -760,11 +767,18 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
|||
):
|
||||
return
|
||||
if not already_owned:
|
||||
var experience_award: int = (
|
||||
FishExperienceType.calculate_for_collection(
|
||||
fish_catch,
|
||||
_local_collection,
|
||||
)
|
||||
)
|
||||
_local_inventory.add_catch(fish_catch)
|
||||
_local_collection.mark_quality_discovered(
|
||||
fish_id,
|
||||
fish_catch.quality,
|
||||
)
|
||||
_local_experience.award_experience(experience_award)
|
||||
if not _save_manager.save_if_dirty():
|
||||
return
|
||||
_result_ledgers[result_id] = true
|
||||
|
|
|
|||
|
|
@ -39,12 +39,16 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.begin_session()
|
||||
_world_time.set_persistence_tracking_enabled(true)
|
||||
_world_time.begin_session(
|
||||
_world_time.get_persistent_time_hours()
|
||||
)
|
||||
return
|
||||
if state == NetworkSession.State.JOINED_CLIENT:
|
||||
_active_session_id = _session.get_session_id()
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.set_persistence_tracking_enabled(false)
|
||||
if _session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||
):
|
||||
|
|
@ -62,6 +66,7 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_sequence = 0
|
||||
_last_received_sequence = -1
|
||||
_sync_elapsed = 0.0
|
||||
_world_time.set_persistence_tracking_enabled(false)
|
||||
_world_time.end_session()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ const PlayerCoolerCapacityType = preload(
|
|||
const PlayerArtUnlocksType = preload(
|
||||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const FishingRodAttachmentScene = preload(
|
||||
"res://player/fishing_rod_attachment.tscn"
|
||||
)
|
||||
|
|
@ -109,6 +112,7 @@ class ShowcaseCameraSnapshot:
|
|||
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
||||
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
||||
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
||||
@onready var experience: PlayerExperienceType = %Experience
|
||||
@onready var _cast_origin: Marker3D = %CastOrigin
|
||||
@onready var _catch_display: Node3D = %CatchDisplay
|
||||
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=17 format=3]
|
||||
[gd_scene load_steps=18 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
[ext_resource type="Material" path="res://player/materials/player_blob_shadow.tres" id="12_blob_shadow"]
|
||||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||
radius = 0.45
|
||||
|
|
@ -104,6 +105,10 @@ script = ExtResource("10_capacity")
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("14_art_unlocks")
|
||||
|
||||
[node name="Experience" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("15_experience")
|
||||
|
||||
[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 1.45, -1.15)
|
||||
|
|
|
|||
130
progression/player_experience.gd
Normal file
130
progression/player_experience.gd
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
class_name PlayerExperience
|
||||
extends Node
|
||||
|
||||
signal experience_changed(total_experience: int, level: int)
|
||||
signal experience_awarded(
|
||||
amount: int,
|
||||
previous_total: int,
|
||||
new_total: int,
|
||||
previous_level: int,
|
||||
new_level: int,
|
||||
)
|
||||
|
||||
const MAX_TOTAL_EXPERIENCE: int = 1000000000000
|
||||
const MAX_LEVEL: int = 100000
|
||||
|
||||
var _total_experience: int = 0
|
||||
|
||||
|
||||
func get_total_experience() -> int:
|
||||
return _total_experience
|
||||
|
||||
|
||||
func get_level() -> int:
|
||||
return level_for_total_experience(_total_experience)
|
||||
|
||||
|
||||
func get_experience_in_level() -> int:
|
||||
return _total_experience - total_experience_for_level(get_level())
|
||||
|
||||
|
||||
func get_experience_for_next_level() -> int:
|
||||
return experience_required_for_next_level(get_level())
|
||||
|
||||
|
||||
func get_level_progress() -> float:
|
||||
return progress_for_total_experience(_total_experience)
|
||||
|
||||
|
||||
func award_experience(amount: int) -> bool:
|
||||
if amount <= 0 or _total_experience >= MAX_TOTAL_EXPERIENCE:
|
||||
return false
|
||||
var previous_total: int = _total_experience
|
||||
var previous_level: int = level_for_total_experience(previous_total)
|
||||
_total_experience = mini(
|
||||
_total_experience + amount,
|
||||
MAX_TOTAL_EXPERIENCE,
|
||||
)
|
||||
var awarded_amount: int = _total_experience - previous_total
|
||||
if awarded_amount <= 0:
|
||||
return false
|
||||
var new_level: int = level_for_total_experience(_total_experience)
|
||||
experience_changed.emit(_total_experience, new_level)
|
||||
experience_awarded.emit(
|
||||
awarded_amount,
|
||||
previous_total,
|
||||
_total_experience,
|
||||
previous_level,
|
||||
new_level,
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func restore_total_experience(total_experience: int) -> bool:
|
||||
if total_experience < 0 or total_experience > MAX_TOTAL_EXPERIENCE:
|
||||
return false
|
||||
var changed: bool = _total_experience != total_experience
|
||||
_total_experience = total_experience
|
||||
if changed:
|
||||
experience_changed.emit(_total_experience, get_level())
|
||||
return true
|
||||
|
||||
|
||||
func reset_to_defaults() -> void:
|
||||
restore_total_experience(0)
|
||||
|
||||
|
||||
func to_save_data() -> Dictionary:
|
||||
return {"total_experience": _total_experience}
|
||||
|
||||
|
||||
static func experience_required_for_next_level(level: int) -> int:
|
||||
var safe_level: int = maxi(level, 1)
|
||||
var level_index: int = safe_level - 1
|
||||
return 100 + level_index * 25 + level_index * level_index * 5
|
||||
|
||||
|
||||
static func total_experience_for_level(level: int) -> int:
|
||||
var completed_levels: int = clampi(level - 1, 0, MAX_LEVEL - 1)
|
||||
if completed_levels == 0:
|
||||
return 0
|
||||
var linear_sum: int = floori(
|
||||
float(completed_levels) * float(completed_levels - 1) / 2.0
|
||||
)
|
||||
var square_sum: int = floori(
|
||||
float(completed_levels - 1)
|
||||
* float(completed_levels)
|
||||
* float(2 * completed_levels - 1)
|
||||
/ 6.0
|
||||
)
|
||||
return completed_levels * 100 + linear_sum * 25 + square_sum * 5
|
||||
|
||||
|
||||
static func level_for_total_experience(total_experience: int) -> int:
|
||||
var safe_total: int = clampi(
|
||||
total_experience,
|
||||
0,
|
||||
MAX_TOTAL_EXPERIENCE,
|
||||
)
|
||||
var low: int = 1
|
||||
var high: int = MAX_LEVEL
|
||||
while low < high:
|
||||
var middle: int = low + floori(float(high - low + 1) / 2.0)
|
||||
if total_experience_for_level(middle) <= safe_total:
|
||||
low = middle
|
||||
else:
|
||||
high = middle - 1
|
||||
return low
|
||||
|
||||
|
||||
static func progress_for_total_experience(total_experience: int) -> float:
|
||||
var level: int = level_for_total_experience(total_experience)
|
||||
var level_start: int = total_experience_for_level(level)
|
||||
var required: int = experience_required_for_next_level(level)
|
||||
if required <= 0:
|
||||
return 0.0
|
||||
return clampf(
|
||||
float(total_experience - level_start) / float(required),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
1
progression/player_experience.gd.uid
Normal file
1
progression/player_experience.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://eml3k7a43e1d
|
||||
|
|
@ -21,8 +21,12 @@ const PlayerCoolerCapacityType = preload(
|
|||
const PlayerArtUnlocksType = preload(
|
||||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||
|
||||
const SAVE_VERSION: int = 5
|
||||
const SAVE_VERSION: int = 6
|
||||
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||
const MAX_SAFE_BALANCE: int = 1000000000000
|
||||
|
||||
|
|
@ -42,6 +46,8 @@ class LoadSnapshot:
|
|||
var barrier_power_level: int = 0
|
||||
var cooler_capacity_level: int = 0
|
||||
var art_unlock_mask: int = 0
|
||||
var total_experience: int = 0
|
||||
var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
|
||||
|
||||
|
||||
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
||||
|
|
@ -56,6 +62,8 @@ var _item_catalog: ItemCatalogType
|
|||
var _fishing_upgrades: PlayerFishingUpgradesType
|
||||
var _cooler_capacity: PlayerCoolerCapacityType
|
||||
var _art_unlocks: PlayerArtUnlocksType
|
||||
var _experience: PlayerExperienceType
|
||||
var _world_time: WorldTimeServiceType
|
||||
var _autosave_timer: Timer
|
||||
var _is_configured: bool = false
|
||||
var _is_restoring: bool = false
|
||||
|
|
@ -98,6 +106,8 @@ func setup(
|
|||
fishing_upgrades: PlayerFishingUpgradesType,
|
||||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
art_unlocks: PlayerArtUnlocksType,
|
||||
experience: PlayerExperienceType,
|
||||
world_time: WorldTimeServiceType,
|
||||
) -> void:
|
||||
_inventory = inventory
|
||||
_collection_log = collection_log
|
||||
|
|
@ -109,6 +119,8 @@ func setup(
|
|||
_fishing_upgrades = fishing_upgrades
|
||||
_cooler_capacity = cooler_capacity
|
||||
_art_unlocks = art_unlocks
|
||||
_experience = experience
|
||||
_world_time = world_time
|
||||
_is_configured = (
|
||||
_inventory != null
|
||||
and _collection_log != null
|
||||
|
|
@ -120,6 +132,8 @@ func setup(
|
|||
and _fishing_upgrades != null
|
||||
and _cooler_capacity != null
|
||||
and _art_unlocks != null
|
||||
and _experience != null
|
||||
and _world_time != null
|
||||
)
|
||||
if not _is_configured:
|
||||
push_error("PlayerSaveManager setup is missing required references.")
|
||||
|
|
@ -154,6 +168,10 @@ func setup(
|
|||
)
|
||||
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
|
||||
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
|
||||
if not _experience.experience_changed.is_connected(
|
||||
_on_experience_changed
|
||||
):
|
||||
_experience.experience_changed.connect(_on_experience_changed)
|
||||
|
||||
|
||||
func load_player_data() -> bool:
|
||||
|
|
@ -234,6 +252,12 @@ func load_player_data() -> bool:
|
|||
var art_restored: bool = _art_unlocks.restore_mask(
|
||||
snapshot.art_unlock_mask
|
||||
)
|
||||
var experience_restored: bool = _experience.restore_total_experience(
|
||||
snapshot.total_experience
|
||||
)
|
||||
var world_time_restored: bool = (
|
||||
_world_time.restore_persistent_time_hours(snapshot.world_time_hours)
|
||||
)
|
||||
_is_restoring = false
|
||||
if (
|
||||
not inventory_restored
|
||||
|
|
@ -244,6 +268,8 @@ func load_player_data() -> bool:
|
|||
or not upgrades_restored
|
||||
or not cooler_restored
|
||||
or not art_restored
|
||||
or not experience_restored
|
||||
or not world_time_restored
|
||||
):
|
||||
push_error("Validated player save could not be restored.")
|
||||
return false
|
||||
|
|
@ -337,6 +363,17 @@ func save_if_dirty() -> bool:
|
|||
return not _is_dirty or save_now()
|
||||
|
||||
|
||||
func save_world_time_checkpoint() -> bool:
|
||||
if (
|
||||
not _is_configured
|
||||
or _automatic_saving_blocked
|
||||
or not _autosave_enabled
|
||||
):
|
||||
return false
|
||||
_is_dirty = true
|
||||
return save_now()
|
||||
|
||||
|
||||
func is_dirty() -> bool:
|
||||
return _is_dirty
|
||||
|
||||
|
|
@ -465,6 +502,10 @@ func _build_save_dictionary() -> Dictionary:
|
|||
"upgrades": _fishing_upgrades.to_save_data(),
|
||||
"cooler": _cooler_capacity.to_save_data(),
|
||||
"art": _art_unlocks.to_save_data(),
|
||||
"experience": _experience.to_save_data(),
|
||||
"world": {
|
||||
"time_hours": _world_time.get_persistent_time_hours(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -475,6 +516,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
||||
or typeof(save_data.get("bag")) != TYPE_DICTIONARY
|
||||
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
|
||||
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
||||
):
|
||||
return null
|
||||
var wallet_data: Dictionary = save_data["wallet"]
|
||||
|
|
@ -482,6 +524,10 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
var inventory_data: Dictionary = save_data["inventory"]
|
||||
var bag_data: Dictionary = save_data["bag"]
|
||||
var hotbar_data: Dictionary = save_data["hotbar"]
|
||||
var experience_data: Dictionary = save_data["experience"]
|
||||
var world_data: Dictionary = {}
|
||||
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
||||
world_data = save_data["world"]
|
||||
var upgrades_data: Dictionary = {}
|
||||
var cooler_data: Dictionary = {}
|
||||
var art_data: Dictionary = {}
|
||||
|
|
@ -507,6 +553,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
or typeof(bag_data.get("items")) != TYPE_ARRAY
|
||||
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
|
||||
or not hotbar_data.has("selected_slot")
|
||||
or not experience_data.has("total_experience")
|
||||
):
|
||||
return null
|
||||
|
||||
|
|
@ -525,6 +572,19 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
|
||||
var snapshot := LoadSnapshot.new()
|
||||
snapshot.wallet_balance = balance
|
||||
snapshot.total_experience = _read_integer(
|
||||
experience_data["total_experience"],
|
||||
-1,
|
||||
PlayerExperienceType.MAX_TOTAL_EXPERIENCE,
|
||||
)
|
||||
if snapshot.total_experience < 0:
|
||||
return null
|
||||
if world_data.has("time_hours"):
|
||||
snapshot.world_time_hours = _read_world_time_hours(
|
||||
world_data["time_hours"]
|
||||
)
|
||||
if snapshot.world_time_hours < 0.0:
|
||||
return null
|
||||
var discovered_values: Array = collection_data["discovered_fish_ids"]
|
||||
var seen_discoveries: Dictionary[StringName, bool] = {}
|
||||
for value: Variant in discovered_values:
|
||||
|
|
@ -744,6 +804,8 @@ func _migrate_save(
|
|||
migrated = _migrate_version_3_to_4(migrated)
|
||||
4:
|
||||
migrated = _migrate_version_4_to_5(migrated)
|
||||
5:
|
||||
migrated = _migrate_version_5_to_6(migrated)
|
||||
_:
|
||||
return {}
|
||||
if migrated.is_empty():
|
||||
|
|
@ -843,6 +905,16 @@ func _migrate_version_4_to_5(data: Dictionary) -> Dictionary:
|
|||
return migrated
|
||||
|
||||
|
||||
func _migrate_version_5_to_6(data: Dictionary) -> Dictionary:
|
||||
var migrated: Dictionary = data.duplicate(true)
|
||||
migrated["save_version"] = 6
|
||||
migrated["experience"] = {"total_experience": 0}
|
||||
migrated["world"] = {
|
||||
"time_hours": WorldTimeServiceType.DEFAULT_START_HOUR,
|
||||
}
|
||||
return migrated
|
||||
|
||||
|
||||
func _mark_dirty() -> void:
|
||||
if (
|
||||
_is_restoring
|
||||
|
|
@ -888,6 +960,10 @@ func _on_art_unlocks_changed(_unlock_mask: int) -> void:
|
|||
_mark_dirty()
|
||||
|
||||
|
||||
func _on_experience_changed(_total_experience: int, _level: int) -> void:
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _on_autosave_timeout() -> void:
|
||||
if _is_dirty:
|
||||
save_now()
|
||||
|
|
@ -949,6 +1025,10 @@ func _restore_defaults() -> void:
|
|||
_fishing_upgrades.reset_to_defaults()
|
||||
_cooler_capacity.reset_to_defaults()
|
||||
_art_unlocks.reset_to_defaults()
|
||||
_experience.reset_to_defaults()
|
||||
_world_time.restore_persistent_time_hours(
|
||||
WorldTimeServiceType.DEFAULT_START_HOUR
|
||||
)
|
||||
_is_restoring = false
|
||||
_is_dirty = false
|
||||
|
||||
|
|
@ -974,6 +1054,19 @@ func _read_integer(
|
|||
return invalid_value
|
||||
|
||||
|
||||
func _read_world_time_hours(value: Variant) -> float:
|
||||
if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
|
||||
return -1.0
|
||||
var time_hours: float = float(value)
|
||||
if (
|
||||
not is_finite(time_hours)
|
||||
or time_hours < 0.0
|
||||
or time_hours >= WorldTimeServiceType.HOURS_PER_DAY
|
||||
):
|
||||
return -1.0
|
||||
return time_hours
|
||||
|
||||
|
||||
func _read_upgrade_level(
|
||||
value: Variant,
|
||||
maximum_level: int,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ func _run() -> void:
|
|||
await process_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||
world_time.synchronize_time(19.75)
|
||||
assert(player.experience.award_experience(125))
|
||||
var fish_catalog := main.get("fish_catalog") as FishPool
|
||||
var service := main.get_node(
|
||||
"%NetworkFishShowcaseService"
|
||||
|
|
@ -100,7 +103,17 @@ func _run() -> void:
|
|||
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 5)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 6)
|
||||
assert(
|
||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||
== 125
|
||||
)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
float((parsed as Dictionary)["world"]["time_hours"]),
|
||||
19.75,
|
||||
)
|
||||
)
|
||||
var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"]
|
||||
assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
|
||||
var saved_masks: Dictionary = (
|
||||
|
|
@ -112,7 +125,12 @@ func _run() -> void:
|
|||
)
|
||||
|
||||
assert(player.hotbar.clear_slot(1))
|
||||
assert(player.experience.restore_total_experience(0))
|
||||
world_time.synchronize_time(8.0)
|
||||
assert(save_manager.load_player_data())
|
||||
assert(player.experience.get_total_experience() == 125)
|
||||
assert(is_equal_approx(world_time.get_time_hours(), 19.75))
|
||||
assert(player.experience.get_level() == 2)
|
||||
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||
assert(player.hotbar.get_selected_slot() == 1)
|
||||
assert(
|
||||
|
|
|
|||
|
|
@ -242,7 +242,14 @@ func _validate_version_four_migration() -> void:
|
|||
version_four,
|
||||
4,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 5)
|
||||
assert(int(migrated.get("save_version", -1)) == 6)
|
||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
float((migrated["world"] as Dictionary)["time_hours"]),
|
||||
8.0,
|
||||
)
|
||||
)
|
||||
var catches: Array = migrated["inventory"]["catches"]
|
||||
assert(int((catches[0] as Dictionary)["quality"]) == 0)
|
||||
var collection: Dictionary = migrated["collection"]
|
||||
|
|
|
|||
|
|
@ -239,18 +239,29 @@ func _validate_bite_wait_distribution() -> void:
|
|||
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||
root.add_child(fishing_spot)
|
||||
await process_frame
|
||||
var bite_rng := fishing_spot.get("_bite_rng") as RandomNumberGenerator
|
||||
bite_rng.seed = 84219
|
||||
var quick_count: int = 0
|
||||
var typical_or_long_count: int = 0
|
||||
var very_long_count: int = 0
|
||||
var total_wait_seconds: float = 0.0
|
||||
for _sample_index: int in 10000:
|
||||
var wait_seconds: float = fishing_spot.roll_bite_wait_time()
|
||||
assert(wait_seconds >= 10.0)
|
||||
assert(wait_seconds <= 240.0)
|
||||
total_wait_seconds += wait_seconds
|
||||
if wait_seconds < 30.0:
|
||||
quick_count += 1
|
||||
else:
|
||||
typical_or_long_count += 1
|
||||
assert(quick_count > 0)
|
||||
if wait_seconds >= 180.0:
|
||||
very_long_count += 1
|
||||
var average_wait_seconds: float = total_wait_seconds / 10000.0
|
||||
assert(quick_count >= 2300 and quick_count <= 2700)
|
||||
assert(typical_or_long_count > quick_count)
|
||||
assert(very_long_count >= 100 and very_long_count <= 300)
|
||||
assert(average_wait_seconds >= 57.0)
|
||||
assert(average_wait_seconds <= 61.0)
|
||||
fishing_spot.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
|
|
|||
47
tests/player_experience_ui_validation.gd
Normal file
47
tests/player_experience_ui_validation.gd
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
extends SceneTree
|
||||
|
||||
const GameUIScene: PackedScene = preload("res://ui/game_ui.tscn")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var game_ui := GameUIScene.instantiate() as GameUI
|
||||
root.add_child(game_ui)
|
||||
await process_frame
|
||||
game_ui.set("_gameplay_ui_enabled", true)
|
||||
game_ui.call(
|
||||
"_on_experience_awarded",
|
||||
50,
|
||||
0,
|
||||
50,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
var panel := game_ui.get_node("%ExperienceProgressPanel") as PanelContainer
|
||||
var bubble := game_ui.get_node("%ExperienceBubble") as PanelContainer
|
||||
var bubble_label := game_ui.get_node("%ExperienceBubbleLabel") as Label
|
||||
var award_label := game_ui.get_node("%ExperienceAwardLabel") as Label
|
||||
assert(panel != null and bubble != null)
|
||||
assert(not panel.visible and not bubble.visible)
|
||||
game_ui.call("_on_showcase_changed", "bluegill", "common", 1.0, 0, true)
|
||||
await process_frame
|
||||
assert(not panel.visible and not bubble.visible)
|
||||
game_ui.call("_on_showcase_changed", "", "", 0.0, 0, false)
|
||||
await process_frame
|
||||
await process_frame
|
||||
assert(panel.visible and bubble.visible)
|
||||
assert(award_label.text == "+50 xp")
|
||||
assert(bubble_label.text == "+50 xp!")
|
||||
await create_timer(1.6).timeout
|
||||
var progress := game_ui.get_node("%ExperienceProgress") as ProgressBar
|
||||
var level_label := game_ui.get_node("%ExperienceLevelLabel") as Label
|
||||
assert(is_equal_approx(progress.value, 50.0))
|
||||
assert(level_label.text == "level 1")
|
||||
await create_timer(1.2).timeout
|
||||
assert(not panel.visible and not bubble.visible)
|
||||
game_ui.queue_free()
|
||||
print("Player experience UI validation: PASS")
|
||||
quit()
|
||||
1
tests/player_experience_ui_validation.gd.uid
Normal file
1
tests/player_experience_ui_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c03tvq1ydhdqj
|
||||
178
tests/player_experience_validation.gd
Normal file
178
tests/player_experience_validation.gd
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
extends SceneTree
|
||||
|
||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_validate_level_curve()
|
||||
_validate_catch_awards()
|
||||
_validate_player_experience_state()
|
||||
_validate_save_migration()
|
||||
print("Player experience validation: PASS")
|
||||
quit()
|
||||
|
||||
|
||||
func _validate_level_curve() -> void:
|
||||
assert(PlayerExperienceType.experience_required_for_next_level(1) == 100)
|
||||
assert(PlayerExperienceType.experience_required_for_next_level(2) == 130)
|
||||
assert(PlayerExperienceType.experience_required_for_next_level(3) == 170)
|
||||
assert(PlayerExperienceType.total_experience_for_level(1) == 0)
|
||||
assert(PlayerExperienceType.total_experience_for_level(2) == 100)
|
||||
assert(PlayerExperienceType.total_experience_for_level(3) == 230)
|
||||
assert(PlayerExperienceType.total_experience_for_level(4) == 400)
|
||||
assert(PlayerExperienceType.level_for_total_experience(99) == 1)
|
||||
assert(PlayerExperienceType.level_for_total_experience(100) == 2)
|
||||
assert(PlayerExperienceType.level_for_total_experience(399) == 3)
|
||||
assert(PlayerExperienceType.level_for_total_experience(400) == 4)
|
||||
assert(
|
||||
is_equal_approx(
|
||||
PlayerExperienceType.progress_for_total_experience(50),
|
||||
0.5,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _validate_catch_awards() -> void:
|
||||
var minimum_boring: FishCatch = _make_catch(
|
||||
FishQualityType.Tier.BORING,
|
||||
false,
|
||||
)
|
||||
assert(
|
||||
FishExperienceType.calculate_catch_experience(
|
||||
minimum_boring,
|
||||
false,
|
||||
0,
|
||||
) == 50
|
||||
)
|
||||
var maximum_shiny: FishCatch = _make_catch(
|
||||
FishQualityType.Tier.SHINY,
|
||||
true,
|
||||
)
|
||||
var boring_mask: int = FishQualityType.bit_for(
|
||||
FishQualityType.Tier.BORING
|
||||
)
|
||||
assert(
|
||||
FishExperienceType.calculate_catch_experience(
|
||||
maximum_shiny,
|
||||
true,
|
||||
boring_mask,
|
||||
) == 46
|
||||
)
|
||||
var almost_mastered: int = (
|
||||
FishQualityType.ALL_TIERS_MASK
|
||||
& ~FishQualityType.bit_for(FishQualityType.Tier.SHINY)
|
||||
)
|
||||
assert(
|
||||
FishExperienceType.calculate_catch_experience(
|
||||
maximum_shiny,
|
||||
true,
|
||||
almost_mastered,
|
||||
) == 146
|
||||
)
|
||||
assert(
|
||||
FishExperienceType.calculate_catch_experience(
|
||||
maximum_shiny,
|
||||
true,
|
||||
FishQualityType.ALL_TIERS_MASK,
|
||||
) == 31
|
||||
)
|
||||
|
||||
|
||||
func _validate_player_experience_state() -> void:
|
||||
var experience := PlayerExperienceType.new()
|
||||
root.add_child(experience)
|
||||
var awards: Array[Array] = []
|
||||
experience.experience_awarded.connect(
|
||||
func(
|
||||
amount: int,
|
||||
previous_total: int,
|
||||
new_total: int,
|
||||
previous_level: int,
|
||||
new_level: int,
|
||||
) -> void:
|
||||
awards.append([
|
||||
amount,
|
||||
previous_total,
|
||||
new_total,
|
||||
previous_level,
|
||||
new_level,
|
||||
])
|
||||
)
|
||||
assert(experience.get_level() == 1)
|
||||
assert(experience.award_experience(125))
|
||||
assert(experience.get_total_experience() == 125)
|
||||
assert(experience.get_level() == 2)
|
||||
assert(experience.get_experience_in_level() == 25)
|
||||
assert(awards.size() == 1)
|
||||
assert(awards[0] == [125, 0, 125, 1, 2])
|
||||
assert(experience.restore_total_experience(400))
|
||||
assert(experience.get_level() == 4)
|
||||
assert(not experience.restore_total_experience(-1))
|
||||
experience.queue_free()
|
||||
|
||||
|
||||
func _validate_save_migration() -> void:
|
||||
var manager := PlayerSaveManager.new()
|
||||
root.add_child(manager)
|
||||
var version_five: Dictionary = {
|
||||
"save_version": 5,
|
||||
"wallet": {"balance": 0},
|
||||
"collection": {
|
||||
"discovered_fish_ids": [],
|
||||
"discovered_quality_masks": {},
|
||||
},
|
||||
"inventory": {"next_catch_sequence": 1, "catches": []},
|
||||
"bag": {"items": []},
|
||||
"hotbar": {"selected_slot": 0, "slots": []},
|
||||
"upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
|
||||
"cooler": {"capacity_level": 0},
|
||||
"art": {"unlock_mask": 0},
|
||||
}
|
||||
var migrated: Dictionary = manager.call(
|
||||
"_migrate_save",
|
||||
version_five,
|
||||
5,
|
||||
)
|
||||
assert(int(migrated.get("save_version", -1)) == 6)
|
||||
var experience_data: Dictionary = migrated.get("experience", {})
|
||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||
var world_data: Dictionary = migrated.get("world", {})
|
||||
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
|
||||
manager.queue_free()
|
||||
|
||||
|
||||
func _make_catch(quality: int, maximum_weight: bool) -> FishCatch:
|
||||
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||
assert(fish != null)
|
||||
var fish_catch := FishCatchType.new()
|
||||
fish_catch.fish = fish
|
||||
fish_catch.fish_id = fish.id
|
||||
fish_catch.catch_id = StringName("bluegill:xp-%d-%s" % [
|
||||
quality,
|
||||
"max" if maximum_weight else "min",
|
||||
])
|
||||
fish_catch.catch_sequence = 1
|
||||
fish_catch.weight_lb = (
|
||||
fish.get_maximum_weight()
|
||||
if maximum_weight
|
||||
else fish.get_minimum_weight()
|
||||
)
|
||||
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||
fish_catch.weight_lb
|
||||
)
|
||||
fish_catch.quality = quality
|
||||
fish_catch.sale_value = FishQualityType.apply_sale_value(
|
||||
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||
quality,
|
||||
)
|
||||
return fish_catch
|
||||
1
tests/player_experience_validation.gd.uid
Normal file
1
tests/player_experience_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://djkr2iaa5taks
|
||||
|
|
@ -32,6 +32,9 @@ func _run_host() -> void:
|
|||
)
|
||||
assert(session.start_private_host(TEST_PORT))
|
||||
world_time.synchronize_time(INITIAL_HOST_TIME)
|
||||
assert(is_equal_approx(
|
||||
world_time.get_persistent_time_hours(), INITIAL_HOST_TIME
|
||||
))
|
||||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.RAINY, 300.0
|
||||
)
|
||||
|
|
@ -56,6 +59,9 @@ func _run_host() -> void:
|
|||
|
||||
await create_timer(1.0).timeout
|
||||
world_time.synchronize_time(UPDATED_HOST_TIME)
|
||||
assert(is_equal_approx(
|
||||
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
|
||||
))
|
||||
world_weather.apply_authoritative_snapshot(
|
||||
WorldWeatherService.Weather.FOGGY, 300.0
|
||||
)
|
||||
|
|
@ -98,6 +104,7 @@ func _run_client() -> void:
|
|||
assert(session.supports_server_capability(
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||
))
|
||||
assert(world_time.restore_persistent_time_hours(15.25))
|
||||
|
||||
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
|
|
@ -145,6 +152,7 @@ func _run_client() -> void:
|
|||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||
) <= TIME_TOLERANCE_HOURS)
|
||||
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
||||
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
|
||||
assert(clock_label.text == world_time.get_clock_text())
|
||||
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
||||
while (
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_clock_boundaries_and_duration()
|
||||
_validate_persistent_host_clock()
|
||||
_validate_fishing_availability()
|
||||
_validate_fishing_spot_context()
|
||||
_validate_network_snapshot_bounds()
|
||||
|
|
@ -78,6 +79,24 @@ func _validate_clock_boundaries_and_duration() -> void:
|
|||
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.set_persistence_tracking_enabled(true)
|
||||
clock.begin_session(clock.get_persistent_time_hours())
|
||||
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.set_persistence_tracking_enabled(false)
|
||||
clock.synchronize_time(6.5)
|
||||
assert(is_equal_approx(clock.get_time_hours(), 6.5))
|
||||
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
||||
assert(not clock.restore_persistent_time_hours(-1.0))
|
||||
assert(not clock.restore_persistent_time_hours(24.0))
|
||||
clock.queue_free()
|
||||
|
||||
|
||||
func _validate_fishing_availability() -> void:
|
||||
var day_context := FishingContextType.new()
|
||||
day_context.location_tags = [&"starter_pond"]
|
||||
|
|
|
|||
221
ui/game_ui.gd
221
ui/game_ui.gd
|
|
@ -44,6 +44,12 @@ const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
|||
const WorldWeatherServiceType = preload(
|
||||
"res://world/world_weather_service.gd"
|
||||
)
|
||||
const PlayerExperienceType = preload(
|
||||
"res://progression/player_experience.gd"
|
||||
)
|
||||
const UIReferencePresentationType = preload(
|
||||
"res://ui/ui_reference_presentation.gd"
|
||||
)
|
||||
|
||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||
signal crisp_reset_focus_requested
|
||||
|
|
@ -63,6 +69,13 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
|||
@onready var _barrier_health: Label = %BarrierHealth
|
||||
@onready var _showcase_details: Label = %ShowcaseDetails
|
||||
@onready var _fishing_panel: PanelContainer = %FishingPanel
|
||||
@onready var _experience_panel: PanelContainer = %ExperienceProgressPanel
|
||||
@onready var _experience_level_label: Label = %ExperienceLevelLabel
|
||||
@onready var _experience_award_label: Label = %ExperienceAwardLabel
|
||||
@onready var _experience_progress: ProgressBar = %ExperienceProgress
|
||||
@onready var _experience_bubble: PanelContainer = %ExperienceBubble
|
||||
@onready var _experience_bubble_label: Label = %ExperienceBubbleLabel
|
||||
@onready var _canonical_stage: Control = %CanonicalStage
|
||||
@onready var _player_menu: PlayerMenuType = %PlayerMenu
|
||||
@onready var _screen_fade: ScreenFade = %ScreenFade
|
||||
@onready var _title_screen: TitleScreenType = %TitleScreen
|
||||
|
|
@ -96,6 +109,11 @@ var _item_effects: PlayerItemEffectsType
|
|||
var _main_shop_buyer: FishBuyerProfileType
|
||||
var _shop_interaction: ShopInteractionType
|
||||
var _surface_drawing: NetworkSurfaceDrawingService
|
||||
var _experience: PlayerExperienceType
|
||||
var _experience_award_queue: Array[Dictionary] = []
|
||||
var _experience_animation_active: bool = false
|
||||
var _experience_animation_generation: int = 0
|
||||
var _experience_panel_rest_y: float = 18.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -124,6 +142,7 @@ func setup(
|
|||
player: PlayerType,
|
||||
inventory: FishInventoryType,
|
||||
collection_log: CollectionLogType,
|
||||
experience: PlayerExperienceType,
|
||||
wallet: PlayerWalletType,
|
||||
sale_service: FishSaleServiceType,
|
||||
default_buyer: FishBuyerProfileType,
|
||||
|
|
@ -156,6 +175,14 @@ func setup(
|
|||
_player = player
|
||||
_fishing_spot = fishing_spot
|
||||
_item_effects = item_effects
|
||||
_experience = experience
|
||||
if (
|
||||
_experience != null
|
||||
and not _experience.experience_awarded.is_connected(
|
||||
_on_experience_awarded
|
||||
)
|
||||
):
|
||||
_experience.experience_awarded.connect(_on_experience_awarded)
|
||||
_chat_ui.setup(
|
||||
network_chat_service, network_session, spawn_service, player,
|
||||
fishing_spot, settings_manager, world_time, world_weather
|
||||
|
|
@ -304,6 +331,7 @@ func setup_data_and_identity(
|
|||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_update_experience_bubble_position()
|
||||
if _item_effects == null or not _gameplay_ui_enabled:
|
||||
_effect_status.hide()
|
||||
return
|
||||
|
|
@ -372,6 +400,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
|||
_refresh_hotbar_visibility()
|
||||
_hotbar_ui.set_gameplay_input_enabled(true)
|
||||
_refresh_fishing_panel_visibility()
|
||||
call_deferred("_start_next_experience_animation")
|
||||
|
||||
|
||||
func set_system_menu_open(is_open: bool) -> void:
|
||||
|
|
@ -578,6 +607,7 @@ func _on_showcase_changed(
|
|||
_showcase_details.text = ""
|
||||
_showcase_details.visible = false
|
||||
_set_fishing_status("")
|
||||
call_deferred("_start_next_experience_animation")
|
||||
return
|
||||
_catch_track.visible = false
|
||||
_barrier_prompt_panel.visible = false
|
||||
|
|
@ -596,6 +626,197 @@ func _on_showcase_changed(
|
|||
_refresh_fishing_panel_visibility()
|
||||
|
||||
|
||||
func _on_experience_awarded(
|
||||
amount: int,
|
||||
previous_total: int,
|
||||
new_total: int,
|
||||
previous_level: int,
|
||||
new_level: int,
|
||||
) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
_experience_award_queue.append({
|
||||
"amount": amount,
|
||||
"previous_total": previous_total,
|
||||
"new_total": new_total,
|
||||
"previous_level": previous_level,
|
||||
"new_level": new_level,
|
||||
})
|
||||
|
||||
|
||||
func _start_next_experience_animation() -> void:
|
||||
if (
|
||||
_experience_animation_active
|
||||
or _showcase_active
|
||||
or _experience_award_queue.is_empty()
|
||||
or not _gameplay_ui_enabled
|
||||
):
|
||||
return
|
||||
var award: Dictionary = _experience_award_queue.pop_front()
|
||||
_experience_animation_active = true
|
||||
_experience_animation_generation += 1
|
||||
var generation: int = _experience_animation_generation
|
||||
_play_experience_animation(award, generation)
|
||||
|
||||
|
||||
func _play_experience_animation(
|
||||
award: Dictionary,
|
||||
generation: int,
|
||||
) -> void:
|
||||
var amount: int = int(award.get("amount", 0))
|
||||
var previous_total: int = int(award.get("previous_total", 0))
|
||||
var new_total: int = int(award.get("new_total", previous_total))
|
||||
_experience_award_label.text = "+%d xp" % amount
|
||||
_experience_bubble_label.text = "+%d xp!" % amount
|
||||
_update_experience_progress(previous_total)
|
||||
_experience_panel.position.y = -_experience_panel.size.y - 8.0
|
||||
_experience_panel.modulate.a = 0.0
|
||||
_experience_panel.show()
|
||||
_experience_bubble.modulate.a = 0.0
|
||||
_experience_bubble.scale = Vector2(0.72, 0.72)
|
||||
_experience_bubble.pivot_offset = _experience_bubble.size * 0.5
|
||||
_experience_bubble.show()
|
||||
|
||||
var entry_tween: Tween = create_tween()
|
||||
entry_tween.set_parallel(true)
|
||||
entry_tween.tween_property(
|
||||
_experience_panel,
|
||||
"position:y",
|
||||
_experience_panel_rest_y,
|
||||
0.26,
|
||||
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
entry_tween.tween_property(
|
||||
_experience_panel,
|
||||
"modulate:a",
|
||||
1.0,
|
||||
0.18,
|
||||
)
|
||||
entry_tween.tween_property(
|
||||
_experience_bubble,
|
||||
"modulate:a",
|
||||
1.0,
|
||||
0.15,
|
||||
)
|
||||
entry_tween.tween_property(
|
||||
_experience_bubble,
|
||||
"scale",
|
||||
Vector2.ONE,
|
||||
0.28,
|
||||
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
await entry_tween.finished
|
||||
if generation != _experience_animation_generation:
|
||||
return
|
||||
|
||||
var fill_duration: float = clampf(
|
||||
0.8 + float(amount) * 0.006,
|
||||
0.9,
|
||||
1.65,
|
||||
)
|
||||
var fill_tween: Tween = create_tween()
|
||||
fill_tween.tween_method(
|
||||
Callable(self, "_set_experience_animation_progress").bind(
|
||||
previous_total,
|
||||
new_total,
|
||||
),
|
||||
0.0,
|
||||
1.0,
|
||||
fill_duration,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
await fill_tween.finished
|
||||
if generation != _experience_animation_generation:
|
||||
return
|
||||
await get_tree().create_timer(0.75).timeout
|
||||
if generation != _experience_animation_generation:
|
||||
return
|
||||
|
||||
var exit_tween: Tween = create_tween()
|
||||
exit_tween.set_parallel(true)
|
||||
exit_tween.tween_property(
|
||||
_experience_panel,
|
||||
"modulate:a",
|
||||
0.0,
|
||||
0.24,
|
||||
)
|
||||
exit_tween.tween_property(
|
||||
_experience_bubble,
|
||||
"modulate:a",
|
||||
0.0,
|
||||
0.2,
|
||||
)
|
||||
exit_tween.tween_property(
|
||||
_experience_bubble,
|
||||
"scale",
|
||||
Vector2(0.82, 0.82),
|
||||
0.24,
|
||||
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||
await exit_tween.finished
|
||||
if generation != _experience_animation_generation:
|
||||
return
|
||||
_experience_panel.hide()
|
||||
_experience_bubble.hide()
|
||||
_experience_animation_active = false
|
||||
call_deferred("_start_next_experience_animation")
|
||||
|
||||
|
||||
func _set_experience_animation_progress(
|
||||
progress: float,
|
||||
previous_total: int,
|
||||
new_total: int,
|
||||
) -> void:
|
||||
var displayed_total: int = roundi(lerpf(
|
||||
float(previous_total),
|
||||
float(new_total),
|
||||
clampf(progress, 0.0, 1.0),
|
||||
))
|
||||
_update_experience_progress(displayed_total)
|
||||
|
||||
|
||||
func _update_experience_progress(total_experience: int) -> void:
|
||||
var level: int = PlayerExperienceType.level_for_total_experience(
|
||||
total_experience
|
||||
)
|
||||
_experience_level_label.text = "level %d" % level
|
||||
_experience_progress.value = (
|
||||
PlayerExperienceType.progress_for_total_experience(total_experience)
|
||||
* 100.0
|
||||
)
|
||||
|
||||
|
||||
func _update_experience_bubble_position() -> void:
|
||||
if not _experience_animation_active or _player == null:
|
||||
return
|
||||
var camera: Camera3D = _player.get_gameplay_camera()
|
||||
var anchor_position: Vector3 = _player.get_chat_anchor_position()
|
||||
if camera == null or camera.is_position_behind(anchor_position):
|
||||
_experience_bubble.hide()
|
||||
return
|
||||
_experience_bubble.show()
|
||||
var window_size := Vector2(get_window().size)
|
||||
var output_scale: float = UIReferencePresentationType.get_scale(
|
||||
window_size
|
||||
)
|
||||
var stage_position: Vector2 = (
|
||||
camera.unproject_position(anchor_position) / output_scale
|
||||
- _canonical_stage.position
|
||||
)
|
||||
var desired: Vector2 = stage_position - Vector2(
|
||||
_experience_bubble.size.x * 0.5,
|
||||
_experience_bubble.size.y + 10.0,
|
||||
)
|
||||
_experience_bubble.position = Vector2(
|
||||
clampf(
|
||||
desired.x,
|
||||
8.0,
|
||||
_canonical_stage.size.x - _experience_bubble.size.x - 8.0,
|
||||
),
|
||||
clampf(
|
||||
desired.y,
|
||||
8.0,
|
||||
_canonical_stage.size.y - _experience_bubble.size.y - 8.0,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||
_player_menu_open = is_open
|
||||
if is_open and _surface_drawing != null:
|
||||
|
|
|
|||
108
ui/game_ui.tscn
108
ui/game_ui.tscn
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=16 format=3]
|
||||
[gd_scene load_steps=19 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||
|
|
@ -35,6 +35,27 @@ corner_radius_bottom_left = 12
|
|||
[sub_resource type="StyleBoxFlat" id="StyleBox_transparent"]
|
||||
bg_color = Color(0, 0, 0, 0)
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_panel"]
|
||||
bg_color = Color(0.051, 0.173, 0.227, 0.97)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_background"]
|
||||
bg_color = Color(0.025, 0.102, 0.137, 1)
|
||||
corner_radius_top_left = 7
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 7
|
||||
corner_radius_bottom_left = 7
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_fill"]
|
||||
bg_color = Color(1, 0.82, 0.4, 1)
|
||||
corner_radius_top_left = 7
|
||||
corner_radius_top_right = 7
|
||||
corner_radius_bottom_right = 7
|
||||
corner_radius_bottom_left = 7
|
||||
|
||||
[node name="GameUI" type="CanvasLayer"]
|
||||
script = ExtResource("1_ui")
|
||||
|
||||
|
|
@ -59,6 +80,91 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ExperienceProgressPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
z_index = 63
|
||||
anchors_preset = 10
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -240.0
|
||||
offset_top = 18.0
|
||||
offset_right = 240.0
|
||||
offset_bottom = 82.0
|
||||
grow_horizontal = 2
|
||||
mouse_filter = 2
|
||||
theme = ExtResource("3_theme")
|
||||
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 14
|
||||
theme_override_constants/margin_top = 7
|
||||
theme_override_constants/margin_right = 14
|
||||
theme_override_constants/margin_bottom = 9
|
||||
|
||||
[node name="Layout" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="Header" type="HBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ExperienceLevelLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "level 1"
|
||||
theme_override_colors/font_color = Color(0.95, 0.98, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
|
||||
[node name="Spacer" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ExperienceAwardLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "+0 xp"
|
||||
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
|
||||
[node name="ExperienceProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 20)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_styles/background = SubResource("StyleBox_experience_background")
|
||||
theme_override_styles/fill = SubResource("StyleBox_experience_fill")
|
||||
value = 0.0
|
||||
show_percentage = false
|
||||
|
||||
[node name="ExperienceBubble" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
z_index = 63
|
||||
offset_right = 122.0
|
||||
offset_bottom = 46.0
|
||||
mouse_filter = 2
|
||||
theme = ExtResource("3_theme")
|
||||
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceBubble"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 10
|
||||
theme_override_constants/margin_top = 5
|
||||
theme_override_constants/margin_right = 10
|
||||
theme_override_constants/margin_bottom = 5
|
||||
|
||||
[node name="ExperienceBubbleLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceBubble/Margin"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "+0 xp!"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
|
||||
[node name="FishingPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ 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 _persistent_time_hours: float = DEFAULT_START_HOUR
|
||||
var _persistence_tracking_enabled: bool = false
|
||||
var _running: bool = false
|
||||
var _phase: Phase = Phase.DAWN
|
||||
|
||||
|
|
@ -63,6 +65,31 @@ func synchronize_time(authoritative_time_hours: float) -> void:
|
|||
_set_time_hours(authoritative_time_hours, true)
|
||||
|
||||
|
||||
func set_persistence_tracking_enabled(enabled: bool) -> void:
|
||||
if _persistence_tracking_enabled == enabled:
|
||||
return
|
||||
if _persistence_tracking_enabled:
|
||||
_persistent_time_hours = _time_hours
|
||||
_persistence_tracking_enabled = enabled
|
||||
|
||||
|
||||
func restore_persistent_time_hours(time_hours: float) -> bool:
|
||||
if (
|
||||
not is_finite(time_hours)
|
||||
or time_hours < 0.0
|
||||
or time_hours >= HOURS_PER_DAY
|
||||
):
|
||||
return false
|
||||
_persistent_time_hours = time_hours
|
||||
if _persistence_tracking_enabled:
|
||||
_set_time_hours(_persistent_time_hours, true)
|
||||
return true
|
||||
|
||||
|
||||
func get_persistent_time_hours() -> float:
|
||||
return _persistent_time_hours
|
||||
|
||||
|
||||
func get_time_hours() -> float:
|
||||
return _time_hours
|
||||
|
||||
|
|
@ -116,6 +143,8 @@ func _set_time_hours(time_hours: float, force_emit: bool) -> void:
|
|||
var time_was_changed: bool = not is_equal_approx(normalized, _time_hours)
|
||||
_time_hours = normalized
|
||||
_phase = next_phase
|
||||
if _persistence_tracking_enabled:
|
||||
_persistent_time_hours = normalized
|
||||
if phase_was_changed:
|
||||
phase_changed.emit(_phase)
|
||||
if force_emit or time_was_changed or phase_was_changed:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue