diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd index d43039b..f851e66 100644 --- a/fish/fish_quality.gd +++ b/fish/fish_quality.gd @@ -16,10 +16,6 @@ const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1 # multipliers later without changing catch serialization or tier identity. const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] -# Provisional challenge curve. Fish profiles continue to own their baseline -# barrier health; quality scales that authored baseline before player upgrades -# apply damage. This keeps future rods, bait, and lures on one shared seam. -const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25] const DISPLAY_NAMES: PackedStringArray = [ "boring", "average", @@ -56,24 +52,6 @@ static func apply_sale_value(base_value: int, quality: int) -> int: ) -static func barrier_health_multiplier(quality: int) -> float: - return ( - BARRIER_HEALTH_MULTIPLIERS[quality] - if is_valid(quality) - else BARRIER_HEALTH_MULTIPLIERS[Tier.BORING] - ) - - -static func apply_barrier_health(base_health: int, quality: int) -> int: - return maxi( - ceili( - float(maxi(base_health, 1)) - * barrier_health_multiplier(quality) - ), - 1, - ) - - static func roll( rng: RandomNumberGenerator, weight_multipliers: Array[float] = [], diff --git a/fishing/bass_catch_profile.tres b/fishing/bass_catch_profile.tres index 6385eec..077b229 100644 --- a/fishing/bass_catch_profile.tres +++ b/fishing/bass_catch_profile.tres @@ -11,3 +11,6 @@ barrier_health_max = 7 first_barrier_margin = 0.14 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.14 +chase_start_delay = 1.0 +chase_start_offset = 0.16 +chase_speed = 0.145 diff --git a/fishing/carp_catch_profile.tres b/fishing/carp_catch_profile.tres index 94a8470..bc3f5dc 100644 --- a/fishing/carp_catch_profile.tres +++ b/fishing/carp_catch_profile.tres @@ -11,3 +11,6 @@ barrier_health_max = 8 first_barrier_margin = 0.13 final_barrier_margin = 0.09 minimum_barrier_spacing = 0.14 +chase_start_delay = 1.1 +chase_start_offset = 0.18 +chase_speed = 0.15 diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd index 6eb058e..53617df 100644 --- a/fishing/catch_controller.gd +++ b/fishing/catch_controller.gd @@ -4,7 +4,6 @@ extends Node const CatchDifficultyProfileType = preload( "res://fishing/catch_difficulty_profile.gd" ) -const FishQualityType = preload("res://fish/fish_quality.gd") signal encounter_updated( progress: float, @@ -43,13 +42,6 @@ class Barrier: maximum_health = barrier_health -# Fight movement uses one shared baseline. Species and quality difficulty comes -# from barrier placement and health, while reel upgrades affect only the -# player's progress speed. -const CHASE_START_DELAY: float = 0.5 -const CHASE_START_OFFSET: float = 0.04 -const CHASE_SPEED: float = 0.07 - @export_category("Accessibility") @export var auto_click_enabled: bool = false @export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20 @@ -68,8 +60,9 @@ var _active_barrier_index: int = -1 var _reel_input_held: bool = false var _reel_speed: float = 0.0 var _click_power: int = 1 -var _fish_quality: int = FishQualityType.Tier.BORING +var _chase_start_delay: float = 0.0 var _chase_delay_remaining: float = 0.0 +var _chase_speed: float = 0.0 var _failure_epsilon: float = 0.0001 var _auto_click_accumulator: float = 0.0 var _rng: RandomNumberGenerator = RandomNumberGenerator.new() @@ -110,7 +103,6 @@ func start_encounter( profile: CatchDifficultyProfileType, reel_speed: float, click_power: int, - fish_quality: int = FishQualityType.Tier.BORING, ) -> void: reset() if profile == null: @@ -119,13 +111,10 @@ func start_encounter( _reel_speed = maxf(reel_speed, 0.0) _click_power = maxi(click_power, 1) - _fish_quality = ( - fish_quality - if FishQualityType.is_valid(fish_quality) - else FishQualityType.Tier.BORING - ) - _chase_delay_remaining = CHASE_START_DELAY - chase_progress = -CHASE_START_OFFSET + _chase_start_delay = maxf(profile.chase_start_delay, 0.0) + _chase_delay_remaining = _chase_start_delay + chase_progress = -maxf(profile.chase_start_offset, 0.01) + _chase_speed = maxf(profile.chase_speed, 0.0) _seed_encounter_rng() _generate_barriers(profile) progress = 0.0 @@ -138,13 +127,12 @@ func start_authoritative_encounter( reel_speed: float, click_power: int, seed: int, - fish_quality: int = FishQualityType.Tier.BORING, ) -> void: var previous_test_mode: bool = use_deterministic_test_seed var previous_seed: int = deterministic_test_seed use_deterministic_test_seed = true deterministic_test_seed = seed - start_encounter(profile, reel_speed, click_power, fish_quality) + start_encounter(profile, reel_speed, click_power) use_deterministic_test_seed = previous_test_mode deterministic_test_seed = previous_seed @@ -180,8 +168,9 @@ func reset() -> void: _reel_input_held = false _reel_speed = 0.0 _click_power = 1 - _fish_quality = FishQualityType.Tier.BORING + _chase_start_delay = 0.0 _chase_delay_remaining = 0.0 + _chase_speed = 0.0 _auto_click_accumulator = 0.0 emit_signal( "encounter_updated", @@ -226,7 +215,7 @@ func _update_chase(delta: float) -> void: if active_delta <= 0.0: return chase_progress = minf( - chase_progress + CHASE_SPEED * active_delta, + chase_progress + _chase_speed * active_delta, 1.0 ) @@ -313,14 +302,7 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void: + spacing * float(barrier_index) + random_offsets[barrier_index] * random_slack ) - var base_health: int = _rng.randi_range( - health_range.x, - health_range.y, - ) - var health: int = FishQualityType.apply_barrier_health( - base_health, - _fish_quality, - ) + var health: int = _rng.randi_range(health_range.x, health_range.y) _barriers.append(Barrier.new(position, health)) diff --git a/fishing/catch_difficulty_profile.gd b/fishing/catch_difficulty_profile.gd index bd2508c..3ad20ad 100644 --- a/fishing/catch_difficulty_profile.gd +++ b/fishing/catch_difficulty_profile.gd @@ -8,6 +8,9 @@ extends Resource @export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15 @export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10 @export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15 +@export_range(0.0, 5.0, 0.05) var chase_start_delay: float = 1.0 +@export_range(0.01, 0.5, 0.01) var chase_start_offset: float = 0.15 +@export_range(0.0, 2.0, 0.01) var chase_speed: float = 0.14 func get_barrier_count_range() -> Vector2i: diff --git a/fishing/common_catch_profile.tres b/fishing/common_catch_profile.tres index 4af3284..1169e2e 100644 --- a/fishing/common_catch_profile.tres +++ b/fishing/common_catch_profile.tres @@ -11,3 +11,6 @@ barrier_health_max = 7 first_barrier_margin = 0.15 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.15 +chase_start_delay = 1.0 +chase_start_offset = 0.15 +chase_speed = 0.14 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index a12427b..c694461 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -919,10 +919,6 @@ func _activate_bite() -> void: state = FishingState.FIGHTING _state_time_remaining = 0.0 _withdrawal_input_held = false - _pending_catch = _fish_selector.create_catch(_selected_fish) - if _pending_catch == null or not _pending_catch.is_valid(): - _cancel_attempt() - return bite_activated.emit() status_changed.emit("fish on!") _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) @@ -930,8 +926,7 @@ func _activate_bite() -> void: _catch_controller.start_encounter( _selected_fish.catch_profile, _get_effective_reel_speed(), - _get_effective_barrier_damage(), - _pending_catch.quality, + _get_effective_barrier_damage() ) _catch_controller.set_reel_input( Input.is_action_pressed("fish_primary") @@ -998,6 +993,7 @@ func _on_catch_completed() -> void: _cancel_attempt() return + _pending_catch = _fish_selector.create_catch(_selected_fish) if _pending_catch == null or not _pending_catch.is_valid(): _cancel_attempt() return diff --git a/jobs/job_catalog.gd b/jobs/job_catalog.gd deleted file mode 100644 index 4429e89..0000000 --- a/jobs/job_catalog.gd +++ /dev/null @@ -1,416 +0,0 @@ -class_name JobCatalog -extends RefCounted - -const FishDataType = preload("res://fish/fish_data.gd") - -enum Kind { - CATCH_TOTAL, - CATCH_WATER, - SELL_TOTAL, - CATCH_QUALITY, - CATCH_PHASE, - CATCH_WEATHER, - CATCH_SPECIES, - REACH_LEVEL, - DISCOVER_SPECIES, - MASTER_QUALITIES, -} - -const DAILY_JOB_COUNT: int = 4 -const WEATHER_SEGMENT_HOURS: float = ( - WorldWeatherService.DAILY_PLAN_SEGMENT_HOURS -) -const WEATHER_SEGMENT_COUNT: int = ( - WorldWeatherService.DAILY_PLAN_SEGMENT_COUNT -) -const MAX_JOB_REWARD_COINS: int = 100000 -const MAX_JOB_REWARD_EXPERIENCE: int = 10000 - - -static func generate_daily_jobs( - plan_id: String, - candidates: Array[FishDataType], - allow_weather_jobs: bool = true, -) -> Array[Dictionary]: - var rng := RandomNumberGenerator.new() - rng.seed = plan_id.hash() - var jobs: Array[Dictionary] = [ - _job( - "fresh_catches", - "fresh catch delivery", - "catch 5 fresh water fish", - Kind.CATCH_WATER, - 5, - 35, - 50, - {"water_type": int(WaterType.Type.FRESH_WATER)}, - ), - _job( - "sell_fish", - "market supply", - "sell 5 fish", - Kind.SELL_TOTAL, - 5, - 40, - 50, - ), - ] - var optional: Array[Dictionary] = [ - _job( - "quality_catch", - "quality check", - "catch an impressive fish or better", - Kind.CATCH_QUALITY, - 1, - 55, - 75, - {"minimum_quality": int(FishQuality.Tier.IMPRESSIVE)}, - ), - _job( - "night_catch", - "night shift", - "catch 3 fish at night", - Kind.CATCH_PHASE, - 3, - 35, - 50, - {"phase": int(WorldTimeService.Phase.NIGHT)}, - ), - ] - if allow_weather_jobs: - optional.append(_job( - "fog_catch", - "through the fog", - "catch 2 fish in foggy weather", - Kind.CATCH_WEATHER, - 2, - 65, - 75, - {"weather": int(WorldWeatherService.Weather.FOGGY)}, - )) - var species: Array[FishDataType] = [] - for fish: FishDataType in candidates: - if fish != null and fish.is_selectable(): - species.append(fish) - if not species.is_empty(): - species.sort_custom( - func(a: FishDataType, b: FishDataType) -> bool: - return String(a.id) < String(b.id) - ) - var species_index: int = rng.randi_range(0, species.size() - 1) - var selected: FishDataType = species[species_index] - optional.append(_job( - "species_%s" % String(selected.id), - "specific request", - "catch 3 %s" % selected.display_name.to_lower(), - Kind.CATCH_SPECIES, - 3, - 45, - 50, - {"fish_id": String(selected.id)}, - )) - _shuffle(optional, rng) - while jobs.size() < DAILY_JOB_COUNT and not optional.is_empty(): - jobs.append(optional.pop_back()) - return jobs - - -static func generate_weather_schedule( - plan_id: String, - jobs: Array[Dictionary], - anchor_index: int = 0, -) -> Array[Dictionary]: - anchor_index = clampi(anchor_index, 0, WEATHER_SEGMENT_COUNT - 1) - var required: Array[int] = [] - for job: Dictionary in jobs: - if int(job.get("kind", -1)) != Kind.CATCH_WEATHER: - continue - var weather: int = int(job.get("weather", -1)) - if WorldWeatherService.is_valid_weather(weather) and weather not in required: - required.append(weather) - var rng := RandomNumberGenerator.new() - rng.seed = plan_id.hash() ^ 0x57454154 - var weather_values: Array[int] = [] - for index: int in WEATHER_SEGMENT_COUNT: - var roll: float = rng.randf() - var weather: int = int(WorldWeatherService.Weather.SUNNY) - if roll >= 0.42 and roll < 0.70: - weather = int(WorldWeatherService.Weather.CLOUDY) - elif roll >= 0.70 and roll < 0.88: - weather = int(WorldWeatherService.Weather.RAINY) - elif roll >= 0.88: - weather = int(WorldWeatherService.Weather.FOGGY) - weather_values.append(weather) - for requirement_index: int in required.size(): - var early_index: int = mini( - anchor_index + requirement_index, - WEATHER_SEGMENT_COUNT - 2, - ) - var remaining_segments: int = WEATHER_SEGMENT_COUNT - early_index - var later_index: int = mini( - early_index + maxi( - 1, floori(float(remaining_segments) / 2.0) - ), - WEATHER_SEGMENT_COUNT - 1, - ) - weather_values[early_index] = required[requirement_index] - weather_values[later_index] = required[requirement_index] - var schedule: Array[Dictionary] = [] - for index: int in WEATHER_SEGMENT_COUNT: - schedule.append({ - "start_hour": fposmod( - WorldTimeService.DAY_START_HOUR - + float(index) * WEATHER_SEGMENT_HOURS, - WorldTimeService.HOURS_PER_DAY, - ), - "weather": weather_values[index], - }) - return schedule - - -static func lifetime_chains( - registered_species_count: int, -) -> Array[Dictionary]: - var chains: Array[Dictionary] = [ - _chain("catch", Kind.CATCH_TOTAL, [100, 1000, 5000], [ - [150, 250], [700, 1000], [2500, 3000], - ]), - _chain("sell", Kind.SELL_TOTAL, [50, 500, 2500], [ - [125, 200], [600, 800], [2200, 2400], - ]), - _chain("level", Kind.REACH_LEVEL, [5, 10, 25, 50], [ - [100, 150], [250, 350], [1000, 1200], [3000, 3500], - ]), - ] - if registered_species_count > 0: - chains.append(_chain( - "discover", - Kind.DISCOVER_SPECIES, - [registered_species_count], - [[500, 750]], - )) - chains.append(_chain( - "master", - Kind.MASTER_QUALITIES, - [registered_species_count], - [[1500, 2000]], - )) - return chains - - -static func visible_lifetime_jobs( - registered_species_count: int, - claimed_ids: Array[String], -) -> Array[Dictionary]: - var visible: Array[Dictionary] = [] - for chain: Dictionary in lifetime_chains(registered_species_count): - var targets: Array = chain.get("targets", []) - var rewards: Array = chain.get("rewards", []) - for tier_index: int in targets.size(): - var job_id: String = "%s_%d" % [ - str(chain.get("id", "")), - int(targets[tier_index]), - ] - if job_id in claimed_ids: - continue - var reward: Array = rewards[tier_index] - visible.append(_job( - job_id, - _lifetime_title(str(chain.get("id", "")), int(targets[tier_index])), - _lifetime_description( - str(chain.get("id", "")), int(targets[tier_index]) - ), - int(chain.get("kind", Kind.CATCH_TOTAL)) as Kind, - int(targets[tier_index]), - int(reward[0]), - int(reward[1]), - )) - break - return visible - - -static func weather_requirements(jobs: Array[Dictionary]) -> Array[int]: - var result: Array[int] = [] - for job: Dictionary in jobs: - if int(job.get("kind", -1)) != Kind.CATCH_WEATHER: - continue - var weather: int = int(job.get("weather", -1)) - if WorldWeatherService.is_valid_weather(weather) and weather not in result: - result.append(weather) - return result - - -static func is_valid_job(value: Variant) -> bool: - if typeof(value) != TYPE_DICTIONARY: - return false - var job: Dictionary = value - var kind: int = int(job.get("kind", -1)) - if not ( - typeof(job.get("id")) == TYPE_STRING - and not str(job["id"]).is_empty() - and str(job["id"]).length() <= 96 - and typeof(job.get("title")) == TYPE_STRING - and str(job["title"]).length() <= 96 - and typeof(job.get("description")) == TYPE_STRING - and str(job["description"]).length() <= 160 - and is_bounded_integer( - job.get("kind"), Kind.CATCH_TOTAL, Kind.MASTER_QUALITIES - ) - and kind >= Kind.CATCH_TOTAL - and kind <= Kind.MASTER_QUALITIES - and is_bounded_integer(job.get("target"), 1, 1000000000) - and is_bounded_integer( - job.get("fish_coin"), 0, MAX_JOB_REWARD_COINS - ) - and is_bounded_integer( - job.get("experience"), 0, MAX_JOB_REWARD_EXPERIENCE - ) - ): - return false - match kind: - Kind.CATCH_WATER: - var water_type: int = int(job.get("water_type", -1)) - return ( - is_bounded_integer( - job.get("water_type"), - WaterType.Type.FRESH_WATER, - WaterType.Type.OTHER, - ) - and water_type >= WaterType.Type.FRESH_WATER - and water_type <= WaterType.Type.OTHER - ) - Kind.CATCH_QUALITY: - return ( - is_bounded_integer( - job.get("minimum_quality"), - FishQuality.Tier.BORING, - FishQuality.Tier.SHINY, - ) - and FishQuality.is_valid(int(job.get("minimum_quality", -1))) - ) - Kind.CATCH_PHASE: - var phase: int = int(job.get("phase", -1)) - return ( - is_bounded_integer( - job.get("phase"), - WorldTimeService.Phase.DAWN, - WorldTimeService.Phase.NIGHT, - ) - and phase >= WorldTimeService.Phase.DAWN - and phase <= WorldTimeService.Phase.NIGHT - ) - Kind.CATCH_WEATHER: - return ( - is_bounded_integer( - job.get("weather"), - WorldWeatherService.Weather.SUNNY, - WorldWeatherService.Weather.FOGGY, - ) - and WorldWeatherService.is_valid_weather( - int(job.get("weather", -1)) - ) - ) - Kind.CATCH_SPECIES: - return ( - typeof(job.get("fish_id")) == TYPE_STRING - and not str(job.get("fish_id", "")).is_empty() - and str(job.get("fish_id", "")).length() <= 96 - ) - return true - - -static func is_bounded_integer( - value: Variant, - minimum: int, - maximum: int, -) -> bool: - if typeof(value) == TYPE_INT: - return int(value) >= minimum and int(value) <= maximum - if typeof(value) != TYPE_FLOAT: - return false - var number: float = float(value) - return ( - is_finite(number) - and number >= float(minimum) - and number <= float(maximum) - and is_equal_approx(number, round(number)) - ) - - -static func is_valid_weather_schedule(value: Variant) -> bool: - return WorldWeatherService.is_valid_daily_plan_schedule(value) - - -static func _job( - id: String, - title: String, - description: String, - kind: Kind, - target: int, - fish_coin: int, - experience: int, - extra: Dictionary = {}, -) -> Dictionary: - var result: Dictionary = { - "id": id, - "title": title, - "description": description, - "kind": int(kind), - "target": target, - "fish_coin": fish_coin, - "experience": experience, - } - result.merge(extra, true) - return result - - -static func _chain( - id: String, - kind: Kind, - targets: Array[int], - rewards: Array, -) -> Dictionary: - return { - "id": id, - "kind": int(kind), - "targets": targets, - "rewards": rewards, - } - - -static func _lifetime_title(chain_id: String, target: int) -> String: - match chain_id: - "catch": - return "seasoned angler" - "sell": - return "fish coin regular" - "level": - return "reach level %d" % target - "discover": - return "complete the catalog" - "master": - return "quality master" - return "long-term job" - - -static func _lifetime_description(chain_id: String, target: int) -> String: - match chain_id: - "catch": - return "catch %d fish" % target - "sell": - return "sell %d fish" % target - "level": - return "reach player level %d" % target - "discover": - return "discover all %d cataloged fish" % target - "master": - return "collect every quality of all %d fish" % target - return "keep fishing" - - -static func _shuffle(values: Array[Dictionary], rng: RandomNumberGenerator) -> void: - for index: int in range(values.size() - 1, 0, -1): - var swap_index: int = rng.randi_range(0, index) - var value: Dictionary = values[index] - values[index] = values[swap_index] - values[swap_index] = value diff --git a/jobs/job_catalog.gd.uid b/jobs/job_catalog.gd.uid deleted file mode 100644 index f54ab48..0000000 --- a/jobs/job_catalog.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c52vqhdties3o diff --git a/jobs/player_job_service.gd b/jobs/player_job_service.gd deleted file mode 100644 index 5f764f8..0000000 --- a/jobs/player_job_service.gd +++ /dev/null @@ -1,786 +0,0 @@ -class_name PlayerJobService -extends Node - -const FishCatchType = preload("res://fish/fish_catch.gd") -const FishDataType = preload("res://fish/fish_data.gd") -const FishPoolType = preload("res://fish/fish_pool.gd") - -const FORMAT_VERSION: int = 1 -const MAX_PROGRESS_VALUE: int = 1000000000 -const MAX_PENDING_REWARDS: int = 64 -const DAILY_REFRESH_HOUR: float = WorldTimeService.DAY_START_HOUR - -signal changed -signal board_changed -signal reward_claimed(title: String, fish_coin: int, experience: int) -signal status_changed(message: String) - -var _wallet: PlayerWallet -var _experience: PlayerExperience -var _collection: CollectionLog -var _catalog: FishPoolType -var _world_time: WorldTimeService -var _world_weather: WorldWeatherService -var _session: NetworkSession -var _save_manager: PlayerSaveManager -var _network_fishing: NetworkFishingService -var _network_sale: NetworkSaleService - -var _progression_ready: bool = false -var _host_board: Dictionary = {} -var _active_board: Dictionary = {} -var _active_plan_id: String = "" -var _daily_progress: Dictionary[String, int] = {} -var _daily_completions: Dictionary[String, int] = {} -var _pending_rewards: Array[Dictionary] = [] -var _lifetime_claimed: Array[String] = [] -var _total_catches: int = 0 -var _total_sold: int = 0 -var _previous_hour: float = WorldTimeService.DEFAULT_START_HOUR - - -func setup( - wallet: PlayerWallet, - experience: PlayerExperience, - collection: CollectionLog, - catalog: FishPoolType, - world_time: WorldTimeService, - world_weather: WorldWeatherService, - session: NetworkSession, -) -> void: - _wallet = wallet - _experience = experience - _collection = collection - _catalog = catalog - _world_time = world_time - _world_weather = world_weather - _session = session - _previous_hour = _world_time.get_time_hours() - _world_time.time_changed.connect(_on_time_changed) - _session.state_changed.connect(_on_session_state_changed) - _collection.collection_changed.connect(_on_current_state_changed) - _experience.experience_changed.connect(_on_experience_changed) - - -func bind_authoritative_services( - network_fishing: NetworkFishingService, - network_sale: NetworkSaleService, -) -> void: - _network_fishing = network_fishing - _network_sale = network_sale - if not _network_fishing.local_catch_received.is_connected( - _on_authoritative_catch - ): - _network_fishing.local_catch_received.connect(_on_authoritative_catch) - if not _network_sale.local_sale_finished.is_connected( - _on_authoritative_sale_finished - ): - _network_sale.local_sale_finished.connect( - _on_authoritative_sale_finished - ) - - -func set_save_manager(save_manager: PlayerSaveManager) -> void: - _save_manager = save_manager - - -func begin_progression_session() -> void: - _progression_ready = true - _previous_hour = _world_time.get_time_hours() - if _session.is_host(): - _activate_host_board() - changed.emit() - - -func end_progression_session() -> void: - _progression_ready = false - _active_board = {} - changed.emit() - - -func get_daily_jobs() -> Array[Dictionary]: - var result: Array[Dictionary] = [] - var jobs: Array = _active_board.get("jobs", []) - for value: Variant in jobs: - if typeof(value) != TYPE_DICTIONARY: - continue - var job: Dictionary = (value as Dictionary).duplicate(true) - var job_id: String = str(job.get("id", "")) - job["progress"] = mini( - int(_daily_progress.get(job_id, 0)), int(job.get("target", 1)) - ) - job["completed_count"] = int(_daily_completions.get(job_id, 0)) - var pending: Array[Dictionary] = _pending_for_job( - _active_plan_id, job_id - ) - job["pending_count"] = pending.size() - job["claim_id"] = ( - str(pending[0].get("claim_id", "")) if not pending.is_empty() else "" - ) - job["claimable"] = not pending.is_empty() - result.append(job) - return result - - -func get_lifetime_jobs() -> Array[Dictionary]: - var result: Array[Dictionary] = JobCatalog.visible_lifetime_jobs( - _registered_species_count(), _lifetime_claimed - ) - for job: Dictionary in result: - var progress: int = _lifetime_progress(job) - job["progress"] = mini(progress, int(job.get("target", 1))) - job["complete"] = progress >= int(job.get("target", 1)) - job["claimable"] = bool(job["complete"]) - return result - - -func get_pending_rewards() -> Array[Dictionary]: - return _pending_rewards.duplicate(true) - - -func get_forecast() -> Array[Dictionary]: - var schedule: Array = _active_board.get("weather_schedule", []) - var result: Array[Dictionary] = [] - for value: Variant in schedule: - if typeof(value) == TYPE_DICTIONARY: - result.append((value as Dictionary).duplicate(true)) - return result - - -func get_plan_id() -> String: - return _active_plan_id - - -func get_time_until_refresh_text() -> String: - if _active_plan_id.is_empty() or _world_time == null: - return "daily jobs unavailable" - var elapsed: float = fposmod( - _world_time.get_time_hours() - DAILY_REFRESH_HOUR, - WorldTimeService.HOURS_PER_DAY, - ) - var hours_remaining: float = WorldTimeService.HOURS_PER_DAY - elapsed - var real_seconds: int = ceili( - hours_remaining / WorldTimeService.HOURS_PER_REAL_SECOND - ) - return "refreshes in %d:%02d" % [ - floori(float(real_seconds) / 60.0), real_seconds % 60, - ] - - -func claim(claim_id: String) -> bool: - if claim_id.is_empty() or _wallet == null or _experience == null: - return false - for index: int in _pending_rewards.size(): - var reward: Dictionary = _pending_rewards[index] - if str(reward.get("claim_id", "")) != claim_id: - continue - return _claim_pending_reward(index, reward) - for lifetime_job: Dictionary in get_lifetime_jobs(): - if str(lifetime_job.get("id", "")) != claim_id: - continue - if not bool(lifetime_job.get("claimable", false)): - return false - return _claim_lifetime_reward(lifetime_job) - return false - - -func get_host_board_network_data() -> Dictionary: - return _host_board.duplicate(true) - - -func apply_remote_board(board: Dictionary) -> bool: - if not validate_board(board) or not _matches_canonical_board(board): - return false - var plan_id: String = str(board.get("plan_id", "")) - if plan_id != _active_plan_id: - _expire_incomplete_daily_jobs() - _active_plan_id = plan_id - _daily_progress.clear() - _daily_completions.clear() - _active_board = board.duplicate(true) - changed.emit() - return true - - -func clear_remote_board() -> void: - if _session != null and _session.is_host(): - _activate_host_board() - return - _active_board = {} - changed.emit() - - -func to_save_data() -> Dictionary: - var progress: Dictionary = {} - for job_id: String in _daily_progress: - progress[job_id] = _daily_progress[job_id] - var completions: Dictionary = {} - for job_id: String in _daily_completions: - completions[job_id] = _daily_completions[job_id] - return { - "format_version": FORMAT_VERSION, - "host_board": _host_board.duplicate(true), - "active_plan_id": _active_plan_id, - "daily_progress": progress, - "daily_completions": completions, - "pending_rewards": _pending_rewards.duplicate(true), - "lifetime_claimed": _lifetime_claimed.duplicate(), - "statistics": { - "fish_caught": _total_catches, - "fish_sold": _total_sold, - }, - } - - -func restore_from_save_data(data: Dictionary) -> bool: - if not validate_save_data(data): - return false - var received_board: Dictionary = _active_board.duplicate(true) - _host_board = (data.get("host_board", {}) as Dictionary).duplicate(true) - _active_plan_id = str(data.get("active_plan_id", "")) - _daily_progress.clear() - var progress: Dictionary = data.get("daily_progress", {}) - for key: Variant in progress: - _daily_progress[str(key)] = int(progress[key]) - _daily_completions.clear() - var completions: Dictionary = data.get("daily_completions", {}) - for key: Variant in completions: - _daily_completions[str(key)] = mini(int(completions[key]), 1) - _pending_rewards.clear() - var rewards: Array = data.get("pending_rewards", []) - var restored_daily_rewards: Dictionary[String, bool] = {} - for value: Variant in rewards: - var reward: Dictionary = value - var reward_key: String = "%s/%s" % [ - str(reward.get("source_plan_id", "")), - str(reward.get("source_job_id", "")), - ] - if restored_daily_rewards.has(reward_key): - continue - restored_daily_rewards[reward_key] = true - _pending_rewards.append(reward.duplicate(true)) - _lifetime_claimed.clear() - var claimed: Array = data.get("lifetime_claimed", []) - for value: Variant in claimed: - _lifetime_claimed.append(str(value)) - var statistics: Dictionary = data.get("statistics", {}) - _total_catches = int(statistics.get("fish_caught", 0)) - _total_sold = int(statistics.get("fish_sold", 0)) - if _session != null and _session.is_host(): - _active_board = _host_board.duplicate(true) - _active_plan_id = str(_host_board.get("plan_id", "")) - elif ( - _session != null - and _session.is_joined_client() - and not received_board.is_empty() - ): - apply_remote_board(received_board) - _apply_host_weather_plan() - changed.emit() - return true - - -func reset_to_defaults() -> void: - _host_board = {} - _active_board = {} - _active_plan_id = "" - _daily_progress.clear() - _daily_completions.clear() - _pending_rewards.clear() - _lifetime_claimed.clear() - _total_catches = 0 - _total_sold = 0 - changed.emit() - - -static func default_save_data() -> Dictionary: - return { - "format_version": FORMAT_VERSION, - "host_board": {}, - "active_plan_id": "", - "daily_progress": {}, - "daily_completions": {}, - "pending_rewards": [], - "lifetime_claimed": [], - "statistics": {"fish_caught": 0, "fish_sold": 0}, - } - - -static func validate_board(value: Variant) -> bool: - if typeof(value) != TYPE_DICTIONARY: - return false - var board: Dictionary = value - if ( - typeof(board.get("plan_id")) != TYPE_STRING - or str(board.get("plan_id", "")).is_empty() - or str(board.get("plan_id", "")).length() > 96 - or not JobCatalog.is_bounded_integer( - board.get("cycle"), 0, MAX_PROGRESS_VALUE - ) - or not JobCatalog.is_bounded_integer( - board.get("schedule_anchor_index"), - 0, - JobCatalog.WEATHER_SEGMENT_COUNT - 1, - ) - or typeof(board.get("jobs")) != TYPE_ARRAY - or not JobCatalog.is_valid_weather_schedule( - board.get("weather_schedule", []) - ) - ): - return false - var jobs: Array = board.get("jobs", []) - if jobs.is_empty() or jobs.size() > 8: - return false - var seen: Dictionary[String, bool] = {} - for job_value: Variant in jobs: - if not JobCatalog.is_valid_job(job_value): - return false - var job: Dictionary = job_value - var job_id: String = str(job.get("id", "")) - if seen.has(job_id): - return false - seen[job_id] = true - return true - - -static func validate_save_data(value: Variant) -> bool: - if typeof(value) != TYPE_DICTIONARY: - return false - var data: Dictionary = value - if ( - not JobCatalog.is_bounded_integer( - data.get("format_version"), FORMAT_VERSION, FORMAT_VERSION - ) - or typeof(data.get("host_board")) != TYPE_DICTIONARY - or typeof(data.get("active_plan_id")) != TYPE_STRING - or typeof(data.get("daily_progress")) != TYPE_DICTIONARY - or typeof(data.get("daily_completions")) != TYPE_DICTIONARY - or typeof(data.get("pending_rewards")) != TYPE_ARRAY - or typeof(data.get("lifetime_claimed")) != TYPE_ARRAY - or typeof(data.get("statistics")) != TYPE_DICTIONARY - ): - return false - var host_board: Dictionary = data.get("host_board", {}) - if not host_board.is_empty() and not validate_board(host_board): - return false - var progress: Dictionary = data.get("daily_progress", {}) - if progress.size() > 8: - return false - for key: Variant in progress: - if ( - typeof(key) != TYPE_STRING - or str(key).is_empty() - or str(key).length() > 96 - or not JobCatalog.is_bounded_integer( - progress[key], 0, MAX_PROGRESS_VALUE - ) - ): - return false - var completions: Dictionary = data.get("daily_completions", {}) - if completions.size() > 8: - return false - for key: Variant in completions: - if ( - typeof(key) != TYPE_STRING - or str(key).is_empty() - or str(key).length() > 96 - or not JobCatalog.is_bounded_integer( - completions[key], 0, MAX_PROGRESS_VALUE - ) - ): - return false - var rewards: Array = data.get("pending_rewards", []) - if rewards.size() > MAX_PENDING_REWARDS: - return false - for reward: Variant in rewards: - if not _valid_reward(reward): - return false - var claimed: Array = data.get("lifetime_claimed", []) - if claimed.size() > 64 or not _valid_string_array(claimed, 96): - return false - var statistics: Dictionary = data.get("statistics", {}) - return ( - JobCatalog.is_bounded_integer( - statistics.get("fish_caught"), 0, MAX_PROGRESS_VALUE - ) - and JobCatalog.is_bounded_integer( - statistics.get("fish_sold"), 0, MAX_PROGRESS_VALUE - ) - ) - - -func _activate_host_board() -> void: - if _host_board.is_empty(): - _generate_host_board(0) - else: - _active_board = _host_board.duplicate(true) - _switch_plan(str(_host_board.get("plan_id", ""))) - _apply_host_weather_plan() - board_changed.emit() - changed.emit() - - -func _generate_host_board(cycle: int) -> void: - _expire_incomplete_daily_jobs() - var random_suffix: String = Crypto.new().generate_random_bytes(12).hex_encode() - var plan_id: String = "daily:%d:%s" % [cycle, random_suffix] - var candidates: Array[FishDataType] = [] - if _catalog != null: - for fish: FishDataType in _catalog.candidates: - if fish != null: - candidates.append(fish) - var schedule_anchor_index: int = _current_weather_segment() - var allow_weather_jobs: bool = ( - JobCatalog.WEATHER_SEGMENT_COUNT - schedule_anchor_index >= 2 - ) - var jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( - plan_id, candidates, allow_weather_jobs - ) - _host_board = { - "plan_id": plan_id, - "cycle": cycle, - "schedule_anchor_index": schedule_anchor_index, - "jobs": jobs, - "weather_schedule": JobCatalog.generate_weather_schedule( - plan_id, jobs, schedule_anchor_index - ), - } - _active_board = _host_board.duplicate(true) - _switch_plan(plan_id) - _apply_host_weather_plan() - board_changed.emit() - changed.emit() - - -func _switch_plan(plan_id: String) -> void: - if plan_id == _active_plan_id: - return - _active_plan_id = plan_id - _daily_progress.clear() - _daily_completions.clear() - - -func _apply_host_weather_plan() -> void: - if ( - _world_weather == null - or _world_time == null - or _session == null - or not _session.is_host() - or _host_board.is_empty() - ): - return - var schedule: Array = _host_board.get("weather_schedule", []) - _world_weather.configure_daily_plan( - str(_host_board.get("plan_id", "")), schedule, _world_time - ) - - -func _on_time_changed( - time_hours: float, - _phase: WorldTimeService.Phase, -) -> void: - if ( - _progression_ready - and _session != null - and _session.is_host() - and _crossed_daily_refresh(_previous_hour, time_hours) - ): - var cycle: int = int(_host_board.get("cycle", -1)) + 1 - _generate_host_board(maxi(cycle, 0)) - _previous_hour = time_hours - - -func _on_session_state_changed(state: NetworkSession.State) -> void: - if not _progression_ready: - return - if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]: - _activate_host_board() - elif state == NetworkSession.State.JOINED_CLIENT: - _active_board = {} - changed.emit() - elif state in [ - NetworkSession.State.INACTIVE, - NetworkSession.State.DISCONNECTING, - NetworkSession.State.CONNECTION_FAILED, - NetworkSession.State.SERVER_LOST, - ]: - clear_remote_board() - - -func _on_authoritative_catch(fish_catch: FishCatchType) -> void: - if not _progression_ready or fish_catch == null or not fish_catch.is_valid(): - return - _total_catches = mini(_total_catches + 1, MAX_PROGRESS_VALUE) - _update_daily_for_catch(fish_catch) - changed.emit() - - -func _on_authoritative_sale_finished( - _request_id: String, - accepted: bool, - _message: String, - catch_ids: Array[StringName], - _payout: int, -) -> void: - if not _progression_ready or not accepted or catch_ids.is_empty(): - return - _total_sold = mini(_total_sold + catch_ids.size(), MAX_PROGRESS_VALUE) - for job: Dictionary in get_daily_jobs(): - if int(job.get("kind", -1)) == JobCatalog.Kind.SELL_TOTAL: - _advance_daily(job, catch_ids.size()) - changed.emit() - - -func _update_daily_for_catch(fish_catch: FishCatchType) -> void: - for job: Dictionary in get_daily_jobs(): - var matches: bool = false - match int(job.get("kind", -1)): - JobCatalog.Kind.CATCH_TOTAL: - matches = true - JobCatalog.Kind.CATCH_WATER: - matches = fish_catch.fish.is_allowed_in_water( - int(job.get("water_type", WaterType.Type.OTHER)) as WaterType.Type - ) - JobCatalog.Kind.CATCH_QUALITY: - matches = fish_catch.quality >= int(job.get("minimum_quality", 0)) - JobCatalog.Kind.CATCH_PHASE: - matches = int(_world_time.get_phase()) == int(job.get("phase", -1)) - JobCatalog.Kind.CATCH_WEATHER: - matches = int(_world_weather.get_weather()) == int(job.get("weather", -1)) - JobCatalog.Kind.CATCH_SPECIES: - matches = String(fish_catch.fish_id) == str(job.get("fish_id", "")) - if matches: - _advance_daily(job, 1) - - -func _advance_daily(job: Dictionary, amount: int) -> void: - var job_id: String = str(job.get("id", "")) - if job_id.is_empty() or amount <= 0: - return - if int(_daily_completions.get(job_id, 0)) > 0: - return - var target: int = int(job.get("target", 1)) - var progress: int = mini( - int(_daily_progress.get(job_id, 0)) + amount, - target, - ) - if progress >= target and _pending_rewards.size() < MAX_PENDING_REWARDS: - _daily_completions[job_id] = 1 - var reward: Dictionary = { - "claim_id": _daily_claim_id(_active_plan_id, job_id), - "source_plan_id": _active_plan_id, - "source_job_id": job_id, - "title": str(job.get("title", "daily job")), - "fish_coin": int(job.get("fish_coin", 0)), - "experience": int(job.get("experience", 0)), - } - _pending_rewards.append(reward) - status_changed.emit("job complete — payment ready on the net") - _daily_progress[job_id] = progress - - -func _claim_pending_reward(index: int, reward: Dictionary) -> bool: - var state_snapshot: Dictionary = to_save_data() - var wallet_snapshot: int = _wallet.get_balance() - var experience_snapshot: int = _experience.get_total_experience() - _pending_rewards.remove_at(index) - if not _apply_reward(reward) or not _save_claim_transaction(): - _wallet.restore_balance(wallet_snapshot) - _experience.restore_total_experience(experience_snapshot) - restore_from_save_data(state_snapshot) - return false - _reward_claimed(reward) - return true - - -func _claim_lifetime_reward(job: Dictionary) -> bool: - var job_id: String = str(job.get("id", "")) - if job_id.is_empty() or job_id in _lifetime_claimed: - return false - var state_snapshot: Dictionary = to_save_data() - var wallet_snapshot: int = _wallet.get_balance() - var experience_snapshot: int = _experience.get_total_experience() - _lifetime_claimed.append(job_id) - if not _apply_reward(job) or not _save_claim_transaction(): - _wallet.restore_balance(wallet_snapshot) - _experience.restore_total_experience(experience_snapshot) - restore_from_save_data(state_snapshot) - return false - _reward_claimed(job) - return true - - -func _apply_reward(reward: Dictionary) -> bool: - var fish_coin: int = int(reward.get("fish_coin", 0)) - var experience: int = int(reward.get("experience", 0)) - if fish_coin > 0 and not _wallet.credit(fish_coin): - return false - if experience > 0 and not _experience.award_experience(experience): - return false - changed.emit() - return true - - -func _save_claim_transaction() -> bool: - return _save_manager != null and _save_manager.save_if_dirty() - - -func _reward_claimed(reward: Dictionary) -> void: - var title: String = str(reward.get("title", "job")) - var fish_coin: int = int(reward.get("fish_coin", 0)) - var experience: int = int(reward.get("experience", 0)) - reward_claimed.emit(title, fish_coin, experience) - status_changed.emit("payment received — $%d and %d xp" % [ - fish_coin, experience, - ]) - changed.emit() - - -func _lifetime_progress(job: Dictionary) -> int: - match int(job.get("kind", -1)): - JobCatalog.Kind.CATCH_TOTAL: - return _total_catches - JobCatalog.Kind.SELL_TOTAL: - return _total_sold - JobCatalog.Kind.REACH_LEVEL: - return _experience.get_level() if _experience != null else 0 - JobCatalog.Kind.DISCOVER_SPECIES: - return _collection.get_discovered_ids().size() if _collection != null else 0 - JobCatalog.Kind.MASTER_QUALITIES: - var mastered: int = 0 - if _collection != null and _catalog != null: - for fish: FishDataType in _catalog.candidates: - if fish != null and _collection.has_mastered(fish.id): - mastered += 1 - return mastered - return 0 - - -func _registered_species_count() -> int: - var count: int = 0 - if _catalog != null: - for fish: FishDataType in _catalog.candidates: - if fish != null and fish.is_selectable(): - count += 1 - return count - - -func _expire_incomplete_daily_jobs() -> void: - _daily_progress.clear() - _daily_completions.clear() - - -func _on_current_state_changed() -> void: - if _progression_ready: - changed.emit() - - -func _on_experience_changed(_total: int, _level: int) -> void: - if _progression_ready: - changed.emit() - - -func _pending_for_job( - plan_id: String, - job_id: String, -) -> Array[Dictionary]: - var result: Array[Dictionary] = [] - for reward: Dictionary in _pending_rewards: - if ( - str(reward.get("source_plan_id", "")) == plan_id - and str(reward.get("source_job_id", "")) == job_id - ): - result.append(reward) - return result - - -func _matches_canonical_board(board: Dictionary) -> bool: - var plan_id: String = str(board.get("plan_id", "")) - var candidates: Array[FishDataType] = [] - if _catalog != null: - for fish: FishDataType in _catalog.candidates: - if fish != null: - candidates.append(fish) - var anchor_index: int = int(board.get("schedule_anchor_index", -1)) - var allow_weather_jobs: bool = ( - JobCatalog.WEATHER_SEGMENT_COUNT - anchor_index >= 2 - ) - var jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( - plan_id, candidates, allow_weather_jobs - ) - var schedule: Array[Dictionary] = JobCatalog.generate_weather_schedule( - plan_id, jobs, anchor_index - ) - return ( - board.get("jobs", []) == jobs - and board.get("weather_schedule", []) == schedule - ) - - -func _current_weather_segment() -> int: - if _world_time == null: - return 0 - var elapsed_hours: float = fposmod( - _world_time.get_time_hours() - WorldTimeService.DAY_START_HOUR, - WorldTimeService.HOURS_PER_DAY, - ) - return clampi( - floori(elapsed_hours / JobCatalog.WEATHER_SEGMENT_HOURS), - 0, - JobCatalog.WEATHER_SEGMENT_COUNT - 1, - ) - - -static func _daily_claim_id( - plan_id: String, - job_id: String, -) -> String: - return "%s/%s" % [plan_id, job_id] - - -static func _crossed_daily_refresh(previous: float, current: float) -> bool: - if current >= previous: - return previous < DAILY_REFRESH_HOUR and current >= DAILY_REFRESH_HOUR - return ( - previous < DAILY_REFRESH_HOUR - or current >= DAILY_REFRESH_HOUR - ) - - -static func _valid_string_array(values: Array, maximum_length: int) -> bool: - var seen: Dictionary[String, bool] = {} - for value: Variant in values: - if typeof(value) != TYPE_STRING: - return false - var text: String = str(value) - if text.is_empty() or text.length() > maximum_length or seen.has(text): - return false - seen[text] = true - return true - - -static func _valid_reward(value: Variant) -> bool: - if typeof(value) != TYPE_DICTIONARY: - return false - var reward: Dictionary = value - return ( - typeof(reward.get("claim_id")) == TYPE_STRING - and not str(reward.get("claim_id", "")).is_empty() - and str(reward.get("claim_id", "")).length() <= 196 - and typeof(reward.get("source_plan_id")) == TYPE_STRING - and not str(reward.get("source_plan_id", "")).is_empty() - and str(reward.get("source_plan_id", "")).length() <= 96 - and typeof(reward.get("source_job_id")) == TYPE_STRING - and not str(reward.get("source_job_id", "")).is_empty() - and str(reward.get("source_job_id", "")).length() <= 96 - and typeof(reward.get("title")) == TYPE_STRING - and str(reward.get("title", "")).length() <= 96 - and JobCatalog.is_bounded_integer( - reward.get("fish_coin"), 0, JobCatalog.MAX_JOB_REWARD_COINS - ) - and JobCatalog.is_bounded_integer( - reward.get("experience"), - 0, - JobCatalog.MAX_JOB_REWARD_EXPERIENCE, - ) - ) diff --git a/jobs/player_job_service.gd.uid b/jobs/player_job_service.gd.uid deleted file mode 100644 index 110c6e2..0000000 --- a/jobs/player_job_service.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://daqtje84gqgu0 diff --git a/main/main.gd b/main/main.gd index cda5c8f..7a074a3 100644 --- a/main/main.gd +++ b/main/main.gd @@ -88,8 +88,6 @@ const WorldWeatherServiceType = preload("res://world/world_weather_service.gd") const NetworkWorldWeatherServiceType = preload( "res://network/network_world_weather_service.gd" ) -const PlayerJobServiceType = preload("res://jobs/player_job_service.gd") -const NetworkJobServiceType = preload("res://network/network_job_service.gd") const TITLE_MUSIC_SILENCE_DB: float = -80.0 const PLAYER_MENU_PATTERN_SCALE: float = 0.85 @@ -132,8 +130,6 @@ const SHOP_PATTERN_SCALE: float = 1.75 @onready var _network_world_weather: NetworkWorldWeatherServiceType = ( %NetworkWorldWeatherService ) -@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService -@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService @onready var _data_root: PlayerDataRoot = %PlayerDataRoot @onready var _identity_backups: IdentityBackupService = %IdentityBackupService @onready var _network_profile: NetworkProfilePreferencesType = ( @@ -298,16 +294,6 @@ func _initialize_after_data_root() -> void: ) _network_world_time.setup(_network_session, _world_time) _network_world_weather.setup(_network_session, _world_weather) - _player_jobs.setup( - _player.wallet, - _player.experience, - _player.collection_log, - fish_catalog, - _world_time, - _world_weather, - _network_session, - ) - _network_jobs.setup(_network_session, _player_jobs) _identity_backups.setup(_data_root, _player_identity, _host_identity) _network_profile_service.setup( _network_session, @@ -373,10 +359,7 @@ func _initialize_after_data_root() -> void: _player.art_unlocks, _player.experience, _world_time, - _world_weather, - _player_jobs, ) - _player_jobs.set_save_manager(_save_manager) _save_manager.set_autosave_enabled(false) _asset_reservations.setup( _player.wallet, _player.inventory, _player.bag, item_catalog @@ -453,7 +436,6 @@ func _initialize_after_data_root() -> void: sale_buyers, _asset_reservations ) - _player_jobs.bind_authoritative_services(_network_fishing, _network_sale) _network_shop.setup( _network_session, _player_spawn_service, @@ -518,7 +500,6 @@ func _initialize_after_data_root() -> void: _player.art_unlocks, _world_time, _world_weather, - _player_jobs, ) _game_ui.setup_data_and_identity( _data_root, @@ -1036,10 +1017,8 @@ func _set_gameplay_active(active: bool) -> void: _game_ui.set_gameplay_ui_enabled(active) _save_manager.set_autosave_enabled(active) if active: - _player_jobs.begin_progression_session() _refresh_active_hotbar_item() else: - _player_jobs.end_progression_session() _set_player_menu_backdrop_visible(false) _set_shop_backdrop_visible(false) @@ -1162,10 +1141,7 @@ func _prepare_private_host() -> bool: NetworkSessionType.State.SERVER_LOST, ]: _network_session.reset_failure() - if not _network_session.start_private_host( - NetworkSessionType.DEFAULT_PORT, - NetworkSessionType.DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS, - ): + if not _network_session.start_private_host(): _game_ui.get_title_screen().report_network_error( "Could not start the private multiplayer session." ) diff --git a/main/main.tscn b/main/main.tscn index 03a38ab..1e60b8d 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -51,8 +51,6 @@ [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"] -[ext_resource type="Script" path="res://jobs/player_job_service.gd" id="52_player_jobs"] -[ext_resource type="Script" path="res://network/network_job_service.gd" id="53_network_jobs"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"] shader = ExtResource("40_title_water") @@ -140,14 +138,6 @@ script = ExtResource("50_world_weather") unique_name_in_owner = true script = ExtResource("51_network_world_weather") -[node name="PlayerJobService" type="Node" parent="."] -unique_name_in_owner = true -script = ExtResource("52_player_jobs") - -[node name="NetworkJobService" type="Node" parent="."] -unique_name_in_owner = true -script = ExtResource("53_network_jobs") - [node name="PlayerDataRoot" type="Node" parent="." unique_id=281868360] unique_name_in_owner = true script = ExtResource("37_data_root") diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index 18b68bc..be214e2 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -429,21 +429,11 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void: return attempt.phase = NetworkFishingAttempt.Phase.FIGHTING attempt.encounter_seed = _new_seed() - var selector := FishSelectorType.new() - selector.use_deterministic_test_seed = true - selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF - selector.begin_roll() - var fish_catch: FishCatch = selector.create_catch(fish) - if fish_catch == null or not fish_catch.is_valid(): - _cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.") - return - attempt.catch_payload = fish_catch.to_network_dict() attempt.controller.start_authoritative_encounter( fish.catch_profile, attempt.reel_speed, attempt.barrier_damage, - attempt.encounter_seed, - fish_catch.quality, + attempt.encounter_seed ) var data: Dictionary = { "attempt_id": attempt.attempt_id, @@ -627,10 +617,11 @@ func _on_attempt_caught(peer_id: int) -> void: if fish == null: _cancel_attempt(peer_id, "Fishing attempt ended.") return - var fish_catch: FishCatch = FishCatchType.from_network_dict( - attempt.catch_payload, - fish, - ) + var selector := FishSelectorType.new() + selector.use_deterministic_test_seed = true + selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF + selector.begin_roll() + var fish_catch: FishCatch = selector.create_catch(fish) if fish_catch == null or not fish_catch.is_valid(): _cancel_attempt(peer_id, "Fishing attempt ended.") return diff --git a/network/network_job_service.gd b/network/network_job_service.gd deleted file mode 100644 index 7745903..0000000 --- a/network/network_job_service.gd +++ /dev/null @@ -1,102 +0,0 @@ -class_name NetworkJobService -extends Node - -const MAX_SESSION_ID_LENGTH: int = 96 - -var _session: NetworkSession -var _jobs: PlayerJobService -var _sequence: int = 0 -var _last_received_sequence: int = -1 - - -func setup(session: NetworkSession, jobs: PlayerJobService) -> void: - _session = session - _jobs = jobs - _session.state_changed.connect(_on_session_state_changed) - _session.peer_authenticated.connect(_on_peer_authenticated) - _jobs.board_changed.connect(_on_board_changed) - - -func _on_session_state_changed(state: NetworkSession.State) -> void: - _sequence = 0 - _last_received_sequence = -1 - if state == NetworkSession.State.JOINED_CLIENT: - if not _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY): - _jobs.clear_remote_board() - elif state in [ - NetworkSession.State.INACTIVE, - NetworkSession.State.DISCONNECTING, - NetworkSession.State.CONNECTION_FAILED, - NetworkSession.State.SERVER_LOST, - ]: - _jobs.clear_remote_board() - - -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.JOBS_CAPABILITY - ) - ): - return - _send_board(peer_id) - - -func _on_board_changed() -> 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() - and _session.peer_supports_capability( - peer_id, NetworkProtocol.JOBS_CAPABILITY - ) - ): - _send_board(peer_id) - - -func _send_board(peer_id: int) -> void: - var board: Dictionary = _jobs.get_host_board_network_data() - if board.is_empty() or not PlayerJobService.validate_board(board): - return - _sequence += 1 - receive_job_board.rpc_id(peer_id, { - "session_id": _session.get_session_id(), - "sequence": _sequence, - "board": board, - }) - - -@rpc("authority", "call_remote", "reliable", 0) -func receive_job_board(data: Dictionary) -> void: - if ( - _session == null - or _jobs == null - or not _session.is_joined_client() - or not _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY) - or not validate_snapshot(data) - or str(data.get("session_id", "")) != _session.get_session_id() - ): - return - var sequence: int = int(data.get("sequence", -1)) - if sequence <= _last_received_sequence: - return - var board: Dictionary = data.get("board", {}) - if _jobs.apply_remote_board(board): - _last_received_sequence = sequence - - -static func validate_snapshot(value: Variant) -> bool: - if typeof(value) != TYPE_DICTIONARY: - return false - var data: Dictionary = value - return ( - typeof(data.get("session_id")) == TYPE_STRING - and not str(data.get("session_id", "")).is_empty() - and str(data.get("session_id", "")).length() <= MAX_SESSION_ID_LENGTH - and typeof(data.get("sequence")) == TYPE_INT - and int(data.get("sequence", -1)) >= 0 - and PlayerJobService.validate_board(data.get("board")) - ) diff --git a/network/network_job_service.gd.uid b/network/network_job_service.gd.uid deleted file mode 100644 index d50e8dc..0000000 --- a/network/network_job_service.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://drlyytr2o3uw8 diff --git a/network/network_protocol.gd b/network/network_protocol.gd index 9ba7353..94236b7 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -23,7 +23,6 @@ const ART_SHOP_CAPABILITY: String = "art_shop_v1" const WORLD_TIME_CAPABILITY: String = "world_time_v1" const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1" const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1" -const JOBS_CAPABILITY: String = "jobs_v1" enum RejectionCode { NONE, @@ -94,7 +93,6 @@ static func make_client_hello( SURFACE_DRAWING_CAPABILITY, WORLD_TIME_CAPABILITY, WORLD_WEATHER_CAPABILITY, - JOBS_CAPABILITY, ]), "cosmetic_snapshot": cosmetic_snapshot, "identity_fingerprint": identity_fingerprint, @@ -215,7 +213,6 @@ static func make_server_hello( SURFACE_DRAWING_CAPABILITY, WORLD_TIME_CAPABILITY, WORLD_WEATHER_CAPABILITY, - JOBS_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", diff --git a/network/network_session.gd b/network/network_session.gd index 1083c24..8c948af 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -2,7 +2,6 @@ class_name NetworkSession extends Node const DEFAULT_PORT: int = 7777 -const DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS: int = 16 const DEFAULT_SESSION_MAX_PLAYERS: int = 8 const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31 const CONNECTION_TIMEOUT_SECONDS: float = 10.0 @@ -98,7 +97,6 @@ var _local_appearance_snapshot: Dictionary = ( CharacterCustomizationCatalog.default_snapshot() ) var _moderation_disconnect_message := "" -var _host_port: int = 0 func _ready() -> void: @@ -133,10 +131,7 @@ func setup( _profile_ready = _profile_ready and _player_identity.load_or_create() -func start_private_host( - port: int = DEFAULT_PORT, - port_attempts: int = 1, -) -> bool: +func start_private_host(port: int = DEFAULT_PORT) -> bool: if ( state != State.INACTIVE or not _profile_ready @@ -148,31 +143,19 @@ func start_private_host( if not _host_identity.load_or_create(): _fail(_host_identity.error_message) return false - if port < 1 or port > 65535 or port_attempts < 1: + if port < 1 or port > 65535: _fail("The hosting port must be from 1 to 65535.") return false _operation_generation += 1 _set_state(State.STARTING_PRIVATE_HOST, "Starting private game...") - var selected_port: int = 0 - var final_port: int = mini(port + port_attempts - 1, 65535) - for candidate_port: int in range(port, final_port + 1): - if not _can_bind_udp_port(candidate_port): - continue - _replace_transport() - var error: Error = _transport.start_host( - candidate_port, - transport_max_clients, - ) - if error == OK: - selected_port = candidate_port - break - if selected_port == 0: - _fail( - "Could not start a private game on UDP ports %d–%d." - % [port, final_port] - ) + _replace_transport() + var error: Error = _transport.start_host( + port, + transport_max_clients, + ) + if error != OK: + _fail("Could not start a private game on UDP port %d." % port) return false - _host_port = selected_port var peer: MultiplayerPeer = _transport.get_multiplayer_peer() peer.refuse_new_connections = true multiplayer.multiplayer_peer = peer @@ -190,7 +173,6 @@ func start_private_host( NetworkProtocol.SURFACE_DRAWING_CAPABILITY, NetworkProtocol.WORLD_TIME_CAPABILITY, NetworkProtocol.WORLD_WEATHER_CAPABILITY, - NetworkProtocol.JOBS_CAPABILITY, ]), ) _registry.update_appearance(1, _local_appearance_snapshot) @@ -218,23 +200,12 @@ func start_private_host( var host_avatar := _spawn_service.get_avatar(1) if host_avatar != null: host_avatar.apply_appearance_snapshot(_local_appearance_snapshot) - _set_state(State.PRIVATE_HOST, "Private game • UDP %d" % selected_port) + _set_state(State.PRIVATE_HOST, "Private game • UDP %d" % port) host_openness_changed.emit(false) _emit_peer_count() return true -func get_host_port() -> int: - return _host_port if is_host() else 0 - - -static func _can_bind_udp_port(port: int) -> bool: - var probe := PacketPeerUDP.new() - var error: Error = probe.bind(port) - probe.close() - return error == OK - - func join_direct(endpoint_text: String) -> bool: if ( state != State.INACTIVE @@ -312,10 +283,10 @@ func set_host_open(is_open: bool) -> bool: _set_state( State.OPEN_HOST if is_open else State.PRIVATE_HOST, ( - "Open game • UDP %d • %d / %d players" + "Open game • %d / %d players" if is_open - else "Private game • UDP %d • %d / %d players" - ) % [_host_port, _registry.size(), session_max_players] + else "Private game • %d / %d players" + ) % [_registry.size(), session_max_players] ) host_openness_changed.emit(is_open) return true @@ -1758,4 +1729,3 @@ func _teardown_peer() -> void: _server_identity_fingerprint = "" _server_identity_public_key = "" _session_identity_keys.clear() - _host_port = 0 diff --git a/network/network_world_weather_service.gd b/network/network_world_weather_service.gd index 43d5762..ec0cab7 100644 --- a/network/network_world_weather_service.gd +++ b/network/network_world_weather_service.gd @@ -44,7 +44,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _sequence = 0 _last_received_sequence = -1 _sync_elapsed = 0.0 - _world_weather.set_persistence_tracking_enabled(true) _world_weather.begin_authoritative_session( _active_session_id.hash() ) @@ -53,7 +52,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _active_session_id = _session.get_session_id() _last_received_sequence = -1 _sync_elapsed = 0.0 - _world_weather.set_persistence_tracking_enabled(false) if _session.supports_server_capability( NetworkProtocol.WORLD_WEATHER_CAPABILITY ): @@ -71,7 +69,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _sequence = 0 _last_received_sequence = -1 _sync_elapsed = 0.0 - _world_weather.set_persistence_tracking_enabled(false) _world_weather.end_session() diff --git a/player/player.gd b/player/player.gd index 8933247..2f0f134 100644 --- a/player/player.gd +++ b/player/player.gd @@ -31,7 +31,6 @@ const FishingRodAttachmentScene = preload( const CHARACTER_IDLE_ANIMATION: StringName = &"idle" const CHARACTER_WALKING_ANIMATION: StringName = &"walking" const CHARACTER_SITTING_ANIMATION: StringName = &"sitting" -const BASE_REEL_SPEED: float = 0.16 var appearance_snapshot: Dictionary = ( CharacterCustomizationCatalog.default_snapshot() @@ -70,7 +69,7 @@ class ShowcaseCameraSnapshot: @export var local_control_enabled: bool = true @export_category("Fishing Stats") -@export_range(0.01, 2.0, 0.01) var reel_speed: float = BASE_REEL_SPEED +@export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32 @export_range(1, 100, 1) var click_power: int = 1 @export_category("Showcase") diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 9a9a3be..72f7676 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -25,12 +25,8 @@ const PlayerExperienceType = preload( "res://progression/player_experience.gd" ) const WorldTimeServiceType = preload("res://world/world_time_service.gd") -const WorldWeatherServiceType = preload( - "res://world/world_weather_service.gd" -) -const PlayerJobServiceType = preload("res://jobs/player_job_service.gd") -const SAVE_VERSION: int = 7 +const SAVE_VERSION: int = 6 const BASIC_ROD_ID: StringName = &"basic_fishing_rod" const MAX_SAFE_BALANCE: int = 1000000000000 @@ -52,14 +48,6 @@ class LoadSnapshot: var art_unlock_mask: int = 0 var total_experience: int = 0 var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR - var has_world_weather_state: bool = false - var world_weather: WorldWeatherServiceType.Weather = ( - WorldWeatherServiceType.DEFAULT_WEATHER - ) - var world_weather_seconds_remaining: float = ( - WorldWeatherServiceType.SUNNY_DURATION_RANGE.x - ) - var jobs_data: Dictionary = PlayerJobServiceType.default_save_data() @export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5 @@ -76,8 +64,6 @@ var _cooler_capacity: PlayerCoolerCapacityType var _art_unlocks: PlayerArtUnlocksType var _experience: PlayerExperienceType var _world_time: WorldTimeServiceType -var _world_weather: WorldWeatherServiceType -var _jobs: PlayerJobServiceType var _autosave_timer: Timer var _is_configured: bool = false var _is_restoring: bool = false @@ -122,8 +108,6 @@ func setup( art_unlocks: PlayerArtUnlocksType, experience: PlayerExperienceType, world_time: WorldTimeServiceType, - world_weather: WorldWeatherServiceType, - jobs: PlayerJobServiceType, ) -> void: _inventory = inventory _collection_log = collection_log @@ -137,8 +121,6 @@ func setup( _art_unlocks = art_unlocks _experience = experience _world_time = world_time - _world_weather = world_weather - _jobs = jobs _is_configured = ( _inventory != null and _collection_log != null @@ -152,8 +134,6 @@ func setup( and _art_unlocks != null and _experience != null and _world_time != null - and _world_weather != null - and _jobs != null ) if not _is_configured: push_error("PlayerSaveManager setup is missing required references.") @@ -192,8 +172,6 @@ func setup( _on_experience_changed ): _experience.experience_changed.connect(_on_experience_changed) - if not _jobs.changed.is_connected(_mark_dirty): - _jobs.changed.connect(_mark_dirty) func load_player_data() -> bool: @@ -280,13 +258,6 @@ func load_player_data() -> bool: var world_time_restored: bool = ( _world_time.restore_persistent_time_hours(snapshot.world_time_hours) ) - var world_weather_restored: bool = true - if snapshot.has_world_weather_state: - world_weather_restored = _world_weather.restore_persistent_state( - snapshot.world_weather, - snapshot.world_weather_seconds_remaining, - ) - var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data) _is_restoring = false if ( not inventory_restored @@ -299,8 +270,6 @@ func load_player_data() -> bool: or not art_restored or not experience_restored or not world_time_restored - or not world_weather_restored - or not jobs_restored ): push_error("Validated player save could not be restored.") return false @@ -536,12 +505,7 @@ func _build_save_dictionary() -> Dictionary: "experience": _experience.to_save_data(), "world": { "time_hours": _world_time.get_persistent_time_hours(), - "weather": int(_world_weather.get_persistent_weather()), - "weather_seconds_remaining": ( - _world_weather.get_persistent_seconds_remaining() - ), }, - "jobs": _jobs.to_save_data(), } @@ -553,7 +517,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: or typeof(save_data.get("bag")) != TYPE_DICTIONARY or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY or typeof(save_data.get("experience")) != TYPE_DICTIONARY - or typeof(save_data.get("jobs")) != TYPE_DICTIONARY ): return null var wallet_data: Dictionary = save_data["wallet"] @@ -562,7 +525,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: var bag_data: Dictionary = save_data["bag"] var hotbar_data: Dictionary = save_data["hotbar"] var experience_data: Dictionary = save_data["experience"] - var jobs_data: Dictionary = save_data["jobs"] var world_data: Dictionary = {} if typeof(save_data.get("world")) == TYPE_DICTIONARY: world_data = save_data["world"] @@ -617,40 +579,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: ) if snapshot.total_experience < 0: return null - if not PlayerJobServiceType.validate_save_data(jobs_data): - return null - snapshot.jobs_data = jobs_data.duplicate(true) 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 has_weather: bool = world_data.has("weather") - var has_weather_seconds: bool = world_data.has( - "weather_seconds_remaining" - ) - if has_weather != has_weather_seconds: - return null - if has_weather: - var weather_value: int = _read_integer( - world_data["weather"], - -1, - WorldWeatherServiceType.Weather.FOGGY, - ) - var weather_seconds: float = _read_world_weather_seconds( - world_data["weather_seconds_remaining"] - ) - if ( - not WorldWeatherServiceType.is_valid_weather(weather_value) - or weather_seconds < 0.0 - ): - return null - snapshot.has_world_weather_state = true - snapshot.world_weather = ( - weather_value as WorldWeatherServiceType.Weather - ) - snapshot.world_weather_seconds_remaining = weather_seconds var discovered_values: Array = collection_data["discovered_fish_ids"] var seen_discoveries: Dictionary[StringName, bool] = {} for value: Variant in discovered_values: @@ -872,8 +806,6 @@ func _migrate_save( migrated = _migrate_version_4_to_5(migrated) 5: migrated = _migrate_version_5_to_6(migrated) - 6: - migrated = _migrate_version_6_to_7(migrated) _: return {} if migrated.is_empty(): @@ -983,15 +915,6 @@ func _migrate_version_5_to_6(data: Dictionary) -> Dictionary: return migrated -func _migrate_version_6_to_7(data: Dictionary) -> Dictionary: - var migrated: Dictionary = data.duplicate(true) - migrated["save_version"] = 7 - # Historical catch and sale totals cannot be reconstructed honestly from - # current inventory. New cumulative counters begin at feature introduction. - migrated["jobs"] = PlayerJobServiceType.default_save_data() - return migrated - - func _mark_dirty() -> void: if ( _is_restoring @@ -1106,8 +1029,6 @@ func _restore_defaults() -> void: _world_time.restore_persistent_time_hours( WorldTimeServiceType.DEFAULT_START_HOUR ) - _world_weather.reset_persistent_state() - _jobs.reset_to_defaults() _is_restoring = false _is_dirty = false @@ -1146,19 +1067,6 @@ func _read_world_time_hours(value: Variant) -> float: return time_hours -func _read_world_weather_seconds(value: Variant) -> float: - if typeof(value) not in [TYPE_FLOAT, TYPE_INT]: - return -1.0 - var seconds_remaining: float = float(value) - if ( - not is_finite(seconds_remaining) - or seconds_remaining < 0.0 - or seconds_remaining > WorldWeatherServiceType.MAX_PERSISTED_SECONDS - ): - return -1.0 - return seconds_remaining - - func _read_upgrade_level( value: Variant, maximum_level: int, diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index ec802b0..bd1df0f 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -31,9 +31,6 @@ func _run() -> void: var player := main.get("_player") as Player var world_time := main.get_node("%WorldTimeService") as WorldTimeService - var world_weather := ( - main.get_node("%WorldWeatherService") as WorldWeatherService - ) world_time.synchronize_time(19.75) assert(player.experience.award_experience(125)) var fish_catalog := main.get("fish_catalog") as FishPool @@ -95,10 +92,6 @@ func _run() -> void: await process_frame assert(not service.is_local_showcase_visible()) assert(not (player.get_node("%HeldFishDisplay") as Node3D).visible) - var saved_weather: WorldWeatherService.Weather = ( - world_weather.get_weather() - ) - var saved_weather_seconds: float = world_weather.get_seconds_remaining() assert(save_manager.save_now()) var save_path: String = str(save_manager.get("_save_path")) @@ -110,25 +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"]) == 7) + assert(int((parsed as Dictionary)["save_version"]) == 6) assert( int((parsed as Dictionary)["experience"]["total_experience"]) == 125 ) - assert(absf( - float((parsed as Dictionary)["world"]["time_hours"]) - 19.75 - ) < 0.01) assert( - int((parsed as Dictionary)["world"]["weather"]) - == int(saved_weather) + is_equal_approx( + float((parsed as Dictionary)["world"]["time_hours"]), + 19.75, + ) ) - assert(absf( - float( - (parsed as Dictionary)["world"][ - "weather_seconds_remaining" - ] - ) - saved_weather_seconds - ) < 0.1) var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"] assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality) var saved_masks: Dictionary = ( @@ -142,18 +127,9 @@ func _run() -> void: assert(player.hotbar.clear_slot(1)) assert(player.experience.restore_total_experience(0)) world_time.synchronize_time(8.0) - world_weather.clear_daily_plan() - world_weather.apply_authoritative_snapshot( - WorldWeatherService.Weather.FOGGY, - 10.0, - ) assert(save_manager.load_player_data()) assert(player.experience.get_total_experience() == 125) - assert(absf(world_time.get_time_hours() - 19.75) < 0.01) - assert(world_weather.get_weather() == saved_weather) - assert(absf( - world_weather.get_seconds_remaining() - saved_weather_seconds - ) < 2.0) + 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) diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 2ecaf66..e4f3288 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -19,8 +19,6 @@ func _initialize() -> void: func _run() -> void: _validate_tiers_and_distribution() - _validate_barrier_challenge_curve() - _validate_fight_pacing_and_reel_upgrades() _validate_catch_round_trip_and_sale() _validate_mail_round_trip() _validate_collection_mastery() @@ -74,115 +72,6 @@ func _validate_tiers_and_distribution() -> void: assert(absf(observed - expected) < 0.015) -func _validate_barrier_challenge_curve() -> void: - assert( - FishQualityType.BARRIER_HEALTH_MULTIPLIERS.size() - == FishQualityType.TIER_COUNT - ) - var expected_health: Array[int] = [8, 10, 13, 18, 26] - var previous_health: int = 0 - for quality: int in FishQualityType.TIER_COUNT: - var health: int = FishQualityType.apply_barrier_health(8, quality) - assert(health == expected_health[quality]) - assert(health > previous_health) - previous_health = health - assert(FishQualityType.apply_barrier_health(8, -1) == 8) - assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4) - - var profile := CatchDifficultyProfile.new() - profile.barrier_count_min = 1 - profile.barrier_count_max = 1 - profile.barrier_health_min = 8 - profile.barrier_health_max = 8 - profile.first_barrier_margin = 0.2 - profile.final_barrier_margin = 0.2 - profile.minimum_barrier_spacing = 0.1 - var controller := CatchController.new() - root.add_child(controller) - for quality: int in FishQualityType.TIER_COUNT: - controller.start_authoritative_encounter( - profile, - 0.1, - 1, - 818181, - quality, - ) - var barriers_value: Variant = controller.get("_barriers") - assert(barriers_value is Array) - var barriers: Array = barriers_value as Array - assert(barriers.size() == 1) - var barrier := barriers[0] as RefCounted - assert(barrier != null) - assert(int(barrier.get("maximum_health")) == expected_health[quality]) - controller.queue_free() - - var shiny_health: int = FishQualityType.apply_barrier_health( - 8, - FishQualityType.Tier.SHINY, - ) - var base_power_clicks: int = ceili(float(shiny_health) / 1.0) - var max_power_clicks: int = ceili( - float(shiny_health) - / float(PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL + 1) - ) - assert(base_power_clicks == 26) - assert(max_power_clicks == 7) - assert(max_power_clicks < base_power_clicks) - - -func _validate_fight_pacing_and_reel_upgrades() -> void: - assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07)) - assert(is_equal_approx(CatchController.CHASE_START_DELAY, 0.5)) - assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04)) - assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16)) - - var upgrades := PlayerFishingUpgrades.new() - assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0)) - assert( - upgrades.restore_levels( - PlayerFishingUpgrades.MAX_REEL_SPEED_LEVEL, - 0, - ) - ) - var upgraded_multiplier: float = upgrades.get_reel_speed_multiplier() - assert(is_equal_approx(upgraded_multiplier, 1.5)) - - var profile := CatchDifficultyProfile.new() - profile.barrier_count_min = 0 - profile.barrier_count_max = 0 - var controller := CatchController.new() - root.add_child(controller) - controller.start_authoritative_encounter( - profile, - Player.BASE_REEL_SPEED, - 1, - 919191, - ) - controller.set_reel_input(true) - controller.call("_update_free_reeling", 1.0) - var base_progress: float = controller.progress - assert(is_equal_approx(base_progress, Player.BASE_REEL_SPEED)) - - controller.reset() - controller.start_authoritative_encounter( - profile, - Player.BASE_REEL_SPEED * upgraded_multiplier, - 1, - 919191, - ) - controller.set_reel_input(true) - controller.call("_update_free_reeling", 1.0) - assert( - is_equal_approx( - controller.progress, - Player.BASE_REEL_SPEED * upgraded_multiplier, - ) - ) - assert(controller.progress > base_progress) - controller.queue_free() - upgrades.queue_free() - - func _validate_catch_round_trip_and_sale() -> void: var fish: FishData = Catalog.get_fish_by_id(&"bluegill") assert(fish != null) @@ -353,7 +242,7 @@ func _validate_version_four_migration() -> void: version_four, 4, ) - assert(int(migrated.get("save_version", -1)) == 7) + assert(int(migrated.get("save_version", -1)) == 6) assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0) assert( is_equal_approx( @@ -370,7 +259,4 @@ func _validate_version_four_migration() -> void: var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING) assert(int(masks["bluegill"]) == boring_bit) assert(int(masks["carp"]) == boring_bit) - assert( - PlayerJobService.validate_save_data(migrated.get("jobs", {})) - ) manager.queue_free() diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index dc79c44..2a98968 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -97,60 +97,6 @@ func _run() -> void: assert(player.is_movement_enabled()) assert(not bobber.visible) - # A private host rolls and retains the authoritative catch before the fight - # so its quality can scale the same barriers clients receive in snapshots. - fishing_spot.call("_begin_aiming", player) - fishing_spot.set("_cast_charge", 0.32) - fishing_spot.call("_update_cast_charge", 0.0) - fishing_spot.call("_confirm_cast") - var second_wait_deadline: int = Time.get_ticks_msec() + 4000 - while ( - Time.get_ticks_msec() < second_wait_deadline - and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE - ): - await process_frame - assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE) - attempts = service.get("_attempts") - attempt = attempts.get(session.get_local_peer_id()) - assert(attempt != null) - service.call("_start_bite", attempt) - await process_frame - assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING) - var catalog: FishPool = main.get("fish_catalog") as FishPool - assert(catalog != null) - var fish: FishData = catalog.get_fish_by_id(attempt.fish_id) - var fish_catch := FishCatch.from_network_dict( - attempt.catch_payload, - fish, - ) - assert(fish_catch != null and fish_catch.is_valid()) - var baseline_controller := CatchController.new() - root.add_child(baseline_controller) - baseline_controller.start_authoritative_encounter( - fish.catch_profile, - attempt.reel_speed, - attempt.barrier_damage, - attempt.encounter_seed, - FishQuality.Tier.BORING, - ) - var quality_barriers: Array = attempt.controller.get("_barriers") - var baseline_barriers: Array = baseline_controller.get("_barriers") - assert(quality_barriers.size() == baseline_barriers.size()) - for barrier_index: int in quality_barriers.size(): - var quality_barrier: RefCounted = quality_barriers[barrier_index] - var baseline_barrier: RefCounted = baseline_barriers[barrier_index] - assert( - int(quality_barrier.get("maximum_health")) - == FishQuality.apply_barrier_health( - int(baseline_barrier.get("maximum_health")), - fish_catch.quality, - ) - ) - baseline_controller.queue_free() - service.call("_cancel_attempt", session.get_local_peer_id(), "") - await process_frame - assert(not service.has_local_attempt()) - print("Fishing authority validation: PASS") session.disconnect_session("") main.queue_free() diff --git a/tests/job_multiplayer_validation.gd b/tests/job_multiplayer_validation.gd deleted file mode 100644 index 5994eae..0000000 --- a/tests/job_multiplayer_validation.gd +++ /dev/null @@ -1,111 +0,0 @@ -extends SceneTree - -const MainScene: PackedScene = preload("res://main/main.tscn") -const TEST_PORT: int = 18139 - - -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("Job 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 save_manager := main.get("_save_manager") as PlayerSaveManager - var jobs := main.get_node("%PlayerJobService") as PlayerJobService - assert(session.start_private_host(TEST_PORT)) - assert(save_manager.initialize_new_game()) - main.call("_enter_gameplay") - await process_frame - assert(session.set_host_open(true)) - var host_board: Dictionary = jobs.get_host_board_network_data() - assert(PlayerJobService.validate_board(host_board)) - - 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.JOBS_CAPABILITY - )) - - 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("Job 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 jobs := main.get_node("%PlayerJobService") as PlayerJobService - var 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")) - and not jobs.get_plan_id().is_empty() - ): - break - assert(session.is_joined_client()) - assert(session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY)) - assert(not jobs.get_plan_id().is_empty()) - assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT) - assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT) - assert(weather.get_daily_plan_id().is_empty()) - var game_ui := main.get_node("%GameUI") as GameUI - var player_menu := game_ui.get("_player_menu") as PlayerMenu - player_menu.call("_show_section_immediate", PlayerMenu.Section.NET) - assert((player_menu.get_node("%TheNetPage") as TheNetPage).visible) - print("Job 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: Node = 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 diff --git a/tests/job_multiplayer_validation.gd.uid b/tests/job_multiplayer_validation.gd.uid deleted file mode 100644 index 1b526ba..0000000 --- a/tests/job_multiplayer_validation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c4y0pmxk2bhmu diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd deleted file mode 100644 index 24cad9d..0000000 --- a/tests/job_system_validation.gd +++ /dev/null @@ -1,256 +0,0 @@ -extends SceneTree - -const MainScene: PackedScene = preload("res://main/main.tscn") -const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres") - - -func _initialize() -> void: - call_deferred("_run") - - -func _run() -> void: - root.size = Vector2i(1280, 720) - var main: Node = 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"))) - var session := main.get_node("%NetworkSession") as NetworkSession - assert(session != null) - var occupied_port := PacketPeerUDP.new() - assert(occupied_port.bind(18137, "127.0.0.1") == OK) - assert(session.start_private_host(18137, 3)) - assert(session.get_host_port() == 18138) - occupied_port.close() - var save_manager := main.get("_save_manager") as PlayerSaveManager - assert(save_manager.initialize_new_game()) - main.call("_enter_gameplay") - await physics_frame - await physics_frame - - var jobs := main.get_node("%PlayerJobService") as PlayerJobService - var weather := main.get_node("%WorldWeatherService") as WorldWeatherService - var network_fishing := main.get( - "_network_fishing" - ) as NetworkFishingService - var network_sale := main.get("_network_sale") as NetworkSaleService - var player := main.get("_player") as Player - assert(jobs != null and weather != null and player != null) - assert(network_fishing != null and network_sale != null) - - var board: Dictionary = jobs.get_host_board_network_data() - assert(PlayerJobService.validate_board(board)) - assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT) - assert(weather.get_daily_plan_id() == jobs.get_plan_id()) - _validate_weather_guarantees(board) - _validate_late_board_weather_fallback() - _validate_remote_tamper_rejection(jobs, board) - - var sell_job: Dictionary = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL - ) - assert(not sell_job.is_empty()) - var sell_target: int = int(sell_job.get("target", 0)) - var sold_ids: Array[StringName] = [] - for index: int in sell_target * 2: - sold_ids.append(StringName("job-sale-%d" % index)) - network_sale.local_sale_finished.emit( - "job-test", true, "sold", sold_ids, 1 - ) - await process_frame - var pending: Array[Dictionary] = jobs.get_pending_rewards() - assert(pending.size() == 1) - var refreshed_sell: Dictionary = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL - ) - assert(int(refreshed_sell.get("progress", -1)) == sell_target) - assert(int(refreshed_sell.get("completed_count", 0)) == 1) - - var game_ui := main.get_node("%GameUI") as GameUI - var player_menu := game_ui.get("_player_menu") as PlayerMenu - assert(player_menu != null) - player_menu.open_menu() - for _frame: int in 24: - await process_frame - assert(player_menu.visible) - player_menu.call("_show_section_immediate", PlayerMenu.Section.NET) - var net_page := player_menu.get_node("%TheNetPage") as TheNetPage - assert(net_page.visible) - assert((player_menu.get_node("%TheNetTab") as Button).button_pressed) - assert( - (player_menu.get_node("%NavigationCluster") as Control).get_child_count() - == 7 - ) - - var wallet_before: int = player.wallet.get_balance() - var experience_before: int = player.experience.get_total_experience() - var first_claim_id: String = str(pending[0].get("claim_id", "")) - net_page.call("_claim", first_claim_id) - await process_frame - await process_frame - assert(bool(game_ui.get("_experience_animation_active"))) - var experience_presentation := ( - game_ui.get_node("%ExperiencePresentation") as Control - ) - assert(experience_presentation.visible) - assert((game_ui.get_node("%ExperienceProgressPanel") as Control).visible) - assert((game_ui.get_node("%ExperienceBubble") as Control).visible) - assert( - experience_presentation.get_index() - == experience_presentation.get_parent().get_child_count() - 1 - ) - assert(not (game_ui.get_node("%GameplayTransientHUD") as Control).visible) - assert(player_menu.visible) - assert(not jobs.claim(first_claim_id)) - assert( - player.wallet.get_balance() - == wallet_before + int(pending[0].get("fish_coin", 0)) - ) - assert( - player.experience.get_total_experience() - == experience_before + int(pending[0].get("experience", 0)) - ) - refreshed_sell = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL - ) - assert(int(refreshed_sell.get("progress", -1)) == sell_target) - assert(int(refreshed_sell.get("completed_count", 0)) == 1) - assert(not bool(refreshed_sell.get("claimable", true))) - network_sale.local_sale_finished.emit( - "job-test-repeat", true, "sold", sold_ids, 1 - ) - await process_frame - assert(jobs.get_pending_rewards().is_empty()) - refreshed_sell = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL - ) - assert(int(refreshed_sell.get("progress", -1)) == sell_target) - assert(int(refreshed_sell.get("completed_count", 0)) == 1) - - var fresh_job: Dictionary = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER - ) - assert(not fresh_job.is_empty()) - var bluegill: FishData = Catalog.get_fish_by_id(&"bluegill") - assert(bluegill != null) - for index: int in int(fresh_job.get("target", 0)): - var fish_catch := FishCatch.new() - fish_catch.fish = bluegill - fish_catch.fish_id = bluegill.id - fish_catch.catch_id = StringName("job-catch-%d" % index) - fish_catch.catch_sequence = index + 1 - fish_catch.weight_lb = bluegill.get_minimum_weight() - fish_catch.display_scale = bluegill.get_display_scale_for_weight( - fish_catch.weight_lb - ) - fish_catch.quality = FishQuality.Tier.BORING - fish_catch.sale_value = bluegill.get_sale_value_for_weight( - fish_catch.weight_lb - ) - assert(fish_catch.is_valid()) - network_fishing.local_catch_received.emit(fish_catch) - await process_frame - var refreshed_fresh: Dictionary = _find_job( - jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER - ) - assert(int(refreshed_fresh.get("completed_count", 0)) == 1) - - var pause_menu := game_ui.get_pause_menu() - var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage - assert(join_page != null) - join_page.call("_refresh") - var session_summary := ( - join_page.get_node("%SessionSummary") as Label - ) - assert("UDP 18138" in session_summary.text) - assert(session.set_host_open(true)) - await process_frame - assert("UDP 18138" in session_summary.text) - assert(session.set_host_open(false)) - - assert(save_manager.save_now()) - var save_file := FileAccess.open( - str(save_manager.get("_save_path")), FileAccess.READ - ) - assert(save_file != null) - var parsed: Variant = JSON.parse_string(save_file.get_as_text()) - save_file.close() - assert(typeof(parsed) == TYPE_DICTIONARY) - var save_data: Dictionary = parsed - assert(int(save_data.get("save_version", -1)) == 7) - assert(PlayerJobService.validate_save_data(save_data.get("jobs", {}))) - - main.queue_free() - await process_frame - print("Job system validation: PASS") - quit() - - -func _validate_weather_guarantees(board: Dictionary) -> void: - var jobs: Array = board.get("jobs", []) - var schedule: Array = board.get("weather_schedule", []) - var anchor_index: int = int(board.get("schedule_anchor_index", 0)) - for required_weather: int in JobCatalog.weather_requirements(jobs): - var occurrences: int = 0 - var has_later_window: bool = false - for index: int in schedule.size(): - var entry: Dictionary = schedule[index] - if int(entry.get("weather", -1)) != required_weather: - continue - occurrences += 1 - if index > anchor_index: - has_later_window = true - assert(occurrences >= 2) - assert(has_later_window) - - -func _validate_late_board_weather_fallback() -> void: - var late_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( - "late-board-validation", [], false - ) - assert(JobCatalog.weather_requirements(late_jobs).is_empty()) - var anchored_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs( - "anchored-board-validation", [], true - ) - var anchored_schedule: Array[Dictionary] = ( - JobCatalog.generate_weather_schedule( - "anchored-board-validation", anchored_jobs, 9 - ) - ) - for required_weather: int in JobCatalog.weather_requirements(anchored_jobs): - var occurrence_indices: Array[int] = [] - for index: int in anchored_schedule.size(): - var entry: Dictionary = anchored_schedule[index] - if ( - index >= 9 - and int(entry.get("weather", -1)) == required_weather - ): - occurrence_indices.append(index) - assert(occurrence_indices.size() >= 2) - assert(occurrence_indices[0] >= 9) - assert(occurrence_indices[-1] > 9) - - -func _validate_remote_tamper_rejection( - jobs: PlayerJobService, - board: Dictionary, -) -> void: - var tampered: Dictionary = board.duplicate(true) - var tampered_jobs: Array = tampered.get("jobs", []) - var first_job: Dictionary = tampered_jobs[0] - first_job["fish_coin"] = int(first_job.get("fish_coin", 0)) + 1 - tampered_jobs[0] = first_job - tampered["jobs"] = tampered_jobs - assert(PlayerJobService.validate_board(tampered)) - assert(not jobs.apply_remote_board(tampered)) - - -func _find_job(jobs: Array[Dictionary], kind: JobCatalog.Kind) -> Dictionary: - for job: Dictionary in jobs: - if int(job.get("kind", -1)) == int(kind): - return job - return {} diff --git a/tests/job_system_validation.gd.uid b/tests/job_system_validation.gd.uid deleted file mode 100644 index d37784b..0000000 --- a/tests/job_system_validation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b2grposd30lkw diff --git a/tests/player_experience_validation.gd b/tests/player_experience_validation.gd index c9b36cd..e7b8c18 100644 --- a/tests/player_experience_validation.gd +++ b/tests/player_experience_validation.gd @@ -143,14 +143,11 @@ func _validate_save_migration() -> void: version_five, 5, ) - assert(int(migrated.get("save_version", -1)) == 7) + 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)) - assert( - PlayerJobService.validate_save_data(migrated.get("jobs", {})) - ) manager.queue_free() diff --git a/tests/world_time_multiplayer_validation.gd b/tests/world_time_multiplayer_validation.gd index 2d82aea..6aa89aa 100644 --- a/tests/world_time_multiplayer_validation.gd +++ b/tests/world_time_multiplayer_validation.gd @@ -38,10 +38,6 @@ func _run_host() -> void: world_weather.apply_authoritative_snapshot( WorldWeatherService.Weather.RAINY, 300.0 ) - assert( - world_weather.get_persistent_weather() - == WorldWeatherService.Weather.RAINY - ) assert(session.set_host_open(true)) var remote_peer_id: int = 0 @@ -69,10 +65,6 @@ func _run_host() -> void: world_weather.apply_authoritative_snapshot( WorldWeatherService.Weather.FOGGY, 300.0 ) - assert( - world_weather.get_persistent_weather() - == WorldWeatherService.Weather.FOGGY - ) var disconnect_deadline: int = Time.get_ticks_msec() + 12000 while ( Time.get_ticks_msec() < disconnect_deadline @@ -113,10 +105,6 @@ func _run_client() -> void: NetworkProtocol.WORLD_WEATHER_CAPABILITY )) assert(world_time.restore_persistent_time_hours(15.25)) - assert(world_weather.restore_persistent_state( - WorldWeatherService.Weather.CLOUDY, - 222.0, - )) var initial_deadline: int = Time.get_ticks_msec() + 8000 while ( @@ -174,14 +162,6 @@ func _run_client() -> void: await process_frame assert(world_weather.is_foggy()) assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY) - assert( - world_weather.get_persistent_weather() - == WorldWeatherService.Weather.CLOUDY - ) - assert(is_equal_approx( - world_weather.get_persistent_seconds_remaining(), - 222.0, - )) chat_ui.call("set_dock_right", true) await process_frame assert(clock_panel.position.x > 1000.0) diff --git a/tests/world_time_validation.gd b/tests/world_time_validation.gd index a5fde90..1908951 100644 --- a/tests/world_time_validation.gd +++ b/tests/world_time_validation.gd @@ -76,20 +76,6 @@ func _validate_clock_boundaries_and_duration() -> void: assert(is_equal_approx(clock.get_time_hours(), 8.0)) assert(not clock.is_night_period()) assert(clock.is_transition()) - - # Small high-refresh deltas must still advance the displayed minute. Using - # is_equal_approx() per frame used to suppress every clock notification. - var emitted_times: Array[float] = [] - clock.time_changed.connect( - func(time_hours: float, _phase: WorldTimeService.Phase) -> void: - emitted_times.append(time_hours) - ) - clock.begin_session(12.0 + 31.0 / 60.0) - emitted_times.clear() - for _frame: int in 1201: - clock.advance_time(1.0 / 240.0) - assert(clock.get_clock_text() == "12:33 pm") - assert(emitted_times.size() >= 2) clock.queue_free() diff --git a/tests/world_weather_validation.gd b/tests/world_weather_validation.gd index a5838f8..1238fc4 100644 --- a/tests/world_weather_validation.gd +++ b/tests/world_weather_validation.gd @@ -24,7 +24,6 @@ func _initialize() -> void: func _run() -> void: _validate_weather_scheduler() - _validate_weather_persistence() _validate_snapshot_bounds() _validate_fishing_weather_seams() _validate_fishing_spot_context() @@ -74,51 +73,6 @@ func _duration_is_valid(weather: WorldWeatherServiceType) -> bool: return false -func _validate_weather_persistence() -> void: - var weather := WorldWeatherServiceType.new() - root.add_child(weather) - weather.set_persistence_tracking_enabled(true) - weather.begin_authoritative_session(20260802) - weather.apply_authoritative_snapshot( - WorldWeatherServiceType.Weather.RAINY, - 200.0, - ) - weather.advance_weather(12.5) - assert(weather.has_persistent_state()) - assert( - weather.get_persistent_weather() - == WorldWeatherServiceType.Weather.RAINY - ) - assert(is_equal_approx( - weather.get_persistent_seconds_remaining(), - 187.5, - )) - weather.set_persistence_tracking_enabled(false) - weather.begin_remote_session() - weather.apply_authoritative_snapshot( - WorldWeatherServiceType.Weather.FOGGY, - 45.0, - ) - assert(weather.is_foggy()) - assert( - weather.get_persistent_weather() - == WorldWeatherServiceType.Weather.RAINY - ) - assert(is_equal_approx( - weather.get_persistent_seconds_remaining(), - 187.5, - )) - weather.set_persistence_tracking_enabled(true) - weather.begin_authoritative_session(20260803) - assert(weather.is_raining()) - assert(is_equal_approx(weather.get_seconds_remaining(), 187.5)) - assert(not weather.restore_persistent_state( - WorldWeatherServiceType.Weather.RAINY, - WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0, - )) - weather.queue_free() - - func _validate_snapshot_bounds() -> void: assert(NetworkWorldWeatherServiceType.validate_snapshot({ "session_id": "session", @@ -233,22 +187,6 @@ func _validate_weather_presentation() -> void: assert(rain != null) assert(rain.emitting) assert(rain.amount_ratio > 0.99) - assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT) - var rain_material := rain.process_material as ParticleProcessMaterial - assert(rain_material != null) - assert(is_equal_approx( - rain_material.initial_velocity_min, - WorldTimeVisualControllerType.RAIN_VELOCITY_MIN, - )) - assert(is_equal_approx( - rain_material.initial_velocity_max, - WorldTimeVisualControllerType.RAIN_VELOCITY_MAX, - )) - var rain_mesh := rain.draw_pass_1 as BoxMesh - assert(rain_mesh != null) - assert(rain_mesh.size.is_equal_approx( - WorldTimeVisualControllerType.RAIN_DROP_SIZE - )) visuals.apply_weather_immediately( WorldWeatherServiceType.Weather.SUNNY ) diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 16ad69d..0bcaa53 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -60,7 +60,6 @@ signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _status_label: Label = %StatusLabel @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD -@onready var _experience_presentation: Control = %ExperiencePresentation @onready var _catch_track: Control = %CatchTrack @onready var _green_catch_progress: ProgressBar = %GreenCatchProgress @onready var _red_chase_progress: ProgressBar = %RedChaseProgress @@ -118,15 +117,6 @@ var _experience_panel_rest_y: float = 18.0 func _ready() -> void: - # Reward feedback must remain above full-screen canonical menus. Keeping the - # overlay as the final stage child makes that ownership explicit instead of - # relying on scene declaration order when another menu adds high-z children. - var presentation_parent: Node = _experience_presentation.get_parent() - if presentation_parent != null: - presentation_parent.move_child( - _experience_presentation, - presentation_parent.get_child_count() - 1, - ) _emote_radial_menu.emote_selected.connect(_on_emote_selected) _chat_ui.text_entry_ownership_changed.connect( _on_chat_text_entry_ownership_changed @@ -181,7 +171,6 @@ func setup( art_unlocks: PlayerArtUnlocks, world_time: WorldTimeServiceType, world_weather: WorldWeatherServiceType, - player_jobs: PlayerJobService, ) -> void: _player = player _fishing_spot = fishing_spot @@ -222,7 +211,6 @@ func setup( player, inventory, collection_log, - experience, wallet, sale_service, default_buyer, @@ -238,8 +226,6 @@ func setup( reservations, network_profile_service, network_player_list, - player_jobs, - world_time, ) _hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory) _fishing_shop.setup( @@ -400,7 +386,6 @@ func get_pause_menu() -> PauseMenuType: func set_gameplay_ui_enabled(enabled: bool) -> void: _gameplay_ui_enabled = enabled _gameplay_transient_hud.visible = enabled and not _player_menu_open - _experience_presentation.visible = enabled _refresh_chat_availability() if not enabled: if _surface_drawing != null: @@ -657,7 +642,6 @@ func _on_experience_awarded( "previous_level": previous_level, "new_level": new_level, }) - _start_next_experience_animation() func _start_next_experience_animation() -> void: @@ -801,13 +785,6 @@ func _update_experience_progress(total_experience: int) -> void: func _update_experience_bubble_position() -> void: if not _experience_animation_active or _player == null: return - if _player_menu_open: - _experience_bubble.show() - _experience_bubble.position = Vector2( - (_canonical_stage.size.x - _experience_bubble.size.x) * 0.5, - _experience_panel_rest_y + _experience_panel.size.y + 10.0, - ) - 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): diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 9d63e54..3d59a55 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -80,18 +80,7 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="ExperiencePresentation" type="Control" parent="UIRoot/CanonicalStage"] -unique_name_in_owner = true -z_index = 200 -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 - -[node name="ExperienceProgressPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/ExperiencePresentation"] +[node name="ExperienceProgressPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 63 @@ -107,40 +96,40 @@ mouse_filter = 2 theme = ExtResource("3_theme") theme_override_styles/panel = SubResource("StyleBox_experience_panel") -[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/ExperiencePresentation/ExperienceProgressPanel"] +[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/ExperiencePresentation/ExperienceProgressPanel/Margin"] +[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/ExperiencePresentation/ExperienceProgressPanel/Margin/Layout"] +[node name="Header" type="HBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout"] layout_mode = 2 -[node name="ExperienceLevelLabel" type="Label" parent="UIRoot/CanonicalStage/ExperiencePresentation/ExperienceProgressPanel/Margin/Layout/Header"] +[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/ExperiencePresentation/ExperienceProgressPanel/Margin/Layout/Header"] +[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/ExperiencePresentation/ExperienceProgressPanel/Margin/Layout/Header"] +[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/ExperiencePresentation/ExperienceProgressPanel/Margin/Layout"] +[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 @@ -150,7 +139,7 @@ theme_override_styles/fill = SubResource("StyleBox_experience_fill") value = 0.0 show_percentage = false -[node name="ExperienceBubble" type="PanelContainer" parent="UIRoot/CanonicalStage/ExperiencePresentation"] +[node name="ExperienceBubble" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 63 @@ -160,14 +149,14 @@ mouse_filter = 2 theme = ExtResource("3_theme") theme_override_styles/panel = SubResource("StyleBox_experience_panel") -[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/ExperiencePresentation/ExperienceBubble"] +[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/ExperiencePresentation/ExperienceBubble/Margin"] +[node name="ExperienceBubbleLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceBubble/Margin"] unique_name_in_owner = true layout_mode = 2 text = "+0 xp!" diff --git a/ui/mail_page.gd b/ui/mail_page.gd index c89d618..746835a 100644 --- a/ui/mail_page.gd +++ b/ui/mail_page.gd @@ -113,7 +113,17 @@ func set_interactive(value: bool) -> void: func _build_ui() -> void: - var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) + var paper := PanelContainer.new() + paper.position = Vector2(116, 126) + paper.size = Vector2(1048, 540) + paper.add_theme_stylebox_override("panel", UtilityPageStyle.panel_style()) + add_child(paper) + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_left", 34) + margin.add_theme_constant_override("margin_right", 34) + margin.add_theme_constant_override("margin_top", 28) + margin.add_theme_constant_override("margin_bottom", 28) + paper.add_child(margin) var root := Control.new() margin.add_child(root) _inbox = _build_inbox() @@ -123,8 +133,8 @@ func _build_ui() -> void: page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) root.add_child(page) _status = Label.new() - _status.position = Vector2(18, 408) - _status.size = Vector2(1024, 28) + _status.position = Vector2(18, 448) + _status.size = Vector2(900, 28) _status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY @@ -137,7 +147,7 @@ func _build_ui() -> void: func _build_inbox() -> Control: var page := Control.new() var title := Label.new() - title.text = "mail" + title.text = "Mail" title.position = Vector2(12, 0) title.size = Vector2(500, 42) title.add_theme_font_size_override("font_size", 30) @@ -160,16 +170,16 @@ func _build_inbox() -> Control: page.add_child(archive_view) var scroll := ScrollContainer.new() scroll.position = Vector2(12, 62) - scroll.size = Vector2(1036, 334) + scroll.size = Vector2(958, 374) page.add_child(scroll) _inbox_list = VBoxContainer.new() - _inbox_list.custom_minimum_size = Vector2(1008, 0) + _inbox_list.custom_minimum_size = Vector2(930, 0) _inbox_list.add_theme_constant_override("separation", 8) scroll.add_child(_inbox_list) _empty_label = Label.new() _empty_label.text = "No letters yet." _empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - _empty_label.custom_minimum_size = Vector2(1008, 80) + _empty_label.custom_minimum_size = Vector2(930, 80) _inbox_list.add_child(_empty_label) return page @@ -412,7 +422,7 @@ func _refresh_inbox() -> void: empty.text = ( "No archived letters." if _showing_archive else "No letters yet." ) - empty.custom_minimum_size = Vector2(1008, 80) + empty.custom_minimum_size = Vector2(930, 80) empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _inbox_list.add_child(empty) return @@ -439,7 +449,7 @@ func _refresh_inbox() -> void: first_line.left(72), " · gift enclosed" if gift else "", ] - button.custom_minimum_size = Vector2(1008, 54) + button.custom_minimum_size = Vector2(930, 54) button.alignment = HORIZONTAL_ALIGNMENT_LEFT button.pressed.connect(_open_letter.bind(str(letter["mail_id"]))) UtilityPageStyle.apply_ocean_button(button) diff --git a/ui/network/join_game_page.gd b/ui/network/join_game_page.gd index e096f50..81dec27 100644 --- a/ui/network/join_game_page.gd +++ b/ui/network/join_game_page.gd @@ -11,8 +11,7 @@ enum Mode { } const ADDRESS_FORMAT_HELP: String = ( - "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; " - + "include the port shown by a host when it differs." + "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted." ) const DIRECT_WORKFLOW_HELP: String = ( "%s\nJoin Now connects once; Save Server stores this address locally." @@ -436,19 +435,10 @@ func _refresh() -> void: and _network_session.state != NetworkSession.State.INACTIVE ) if _session_summary.visible: - _session_summary.text = ( - "UDP %d • %d / %d players" - % [ - _network_session.get_host_port(), - _network_session.get_player_count(), - _network_session.get_session_max_players(), - ] - if _network_session.is_host() - else "%d / %d players" % [ - _network_session.get_player_count(), - _network_session.get_session_max_players(), - ] - ) + _session_summary.text = "%d / %d players" % [ + _network_session.get_player_count(), + _network_session.get_session_max_players(), + ] if not direct: if selected: _details.text = _format_entry_details(_selected_entry) diff --git a/ui/network/join_game_page.tscn b/ui/network/join_game_page.tscn index b2597a6..0337d81 100644 --- a/ui/network/join_game_page.tscn +++ b/ui/network/join_game_page.tscn @@ -163,7 +163,7 @@ unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1) theme_override_font_sizes/font_size = 13 -text = "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; include the port shown by a host when it differs.\nJoin Now connects once; Save Server stores this address locally." +text = "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted.\nJoin Now connects once; Save Server stores this address locally." horizontal_alignment = 1 autowrap_mode = 2 diff --git a/ui/player_menu.gd b/ui/player_menu.gd index af8148d..dd13792 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -27,9 +27,6 @@ const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") const PlayerCoolerCapacityType = preload( "res://progression/player_cooler_capacity.gd" ) -const PlayerExperienceType = preload( - "res://progression/player_experience.gd" -) const FishBatchSelectionType = preload( "res://ui/fish_batch_selection.gd" ) @@ -90,7 +87,6 @@ enum Section { BAG, TACKLE_BOX, LOGBOOK, - NET, MAIL, PROFILE, PLAYERS, @@ -201,7 +197,6 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0) @onready var _bag_sprite_detail_data: Label = %BagSpriteDetailData @onready var _logbook_page: Control = %LogbookPage @onready var _catalog_logbook: LogbookPage = %CatalogLogbook -@onready var _the_net_page: TheNetPage = %TheNetPage @onready var _mail_page: MailPage = %MailPage @onready var _profile_page: ProfilePage = %ProfilePage @onready var _players_page: PlayersPage = %PlayersPage @@ -220,7 +215,6 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0) @onready var _logbook_page_status: BubbleStatusBubbleType = %LogbookPageStatus @onready var _inventory_tab: BubbleButtonType = %InventoryTab @onready var _logbook_tab: BubbleButtonType = %LogbookTab -@onready var _the_net_tab: BubbleButtonType = %TheNetTab @onready var _mail_tab: BubbleButtonType = %MailTab @onready var _profile_tab: BubbleButtonType = %ProfileTab @onready var _players_tab: BubbleButtonType = %PlayersTab @@ -325,7 +319,6 @@ var _cooler_rest_position: Vector2 = Vector2.ZERO var _bag_rest_position: Vector2 = Vector2.ZERO var _tackle_rest_position: Vector2 = Vector2.ZERO var _logbook_rest_position: Vector2 = Vector2.ZERO -var _the_net_rest_position: Vector2 = Vector2.ZERO var _mail_rest_position: Vector2 = Vector2.ZERO var _profile_rest_position: Vector2 = Vector2.ZERO var _page_outgoing_root: Control @@ -367,7 +360,6 @@ func _ready() -> void: _logbook_tab.pressed.connect( _show_section.bind(Section.LOGBOOK) ) - _the_net_tab.pressed.connect(_show_section.bind(Section.NET)) _mail_tab.pressed.connect(_show_section.bind(Section.MAIL)) _profile_tab.pressed.connect(_show_section.bind(Section.PROFILE)) _players_tab.pressed.connect(_show_section.bind(Section.PLAYERS)) @@ -395,7 +387,6 @@ func _ready() -> void: _navigation_cluster.configure([ _inventory_tab, _logbook_tab, - _the_net_tab, _mail_tab, _profile_tab, _players_tab, @@ -486,7 +477,6 @@ func setup( player: PlayerType, inventory: FishInventoryType, collection_log: CollectionLogType, - experience: PlayerExperienceType, wallet: PlayerWalletType, sale_service: FishSaleServiceType, default_buyer: FishBuyerProfileType, @@ -502,8 +492,6 @@ func setup( reservations: PlayerAssetReservationService, network_profile_service: NetworkProfileService, network_player_list: NetworkPlayerListService, - player_jobs: PlayerJobService, - world_time: WorldTimeService, ) -> void: _player = player _inventory = inventory @@ -526,10 +514,9 @@ func setup( _mail_page.setup( network_mail_service, reservations, inventory, wallet, bag, item_catalog ) - _profile_page.setup(network_profile_service, experience) + _profile_page.setup(network_profile_service) _players_page.setup(network_player_list) _catalog_logbook.setup(collection_log, inventory, catalog) - _the_net_page.setup(player_jobs, world_time) _network_mail_service.unread_count_changed.connect( _on_mail_unread_count_changed ) @@ -911,7 +898,6 @@ func _show_section_immediate(section: Section) -> void: _tackle_box_page.visible = section == Section.TACKLE_BOX _inventory_sub_tabs.visible = _is_inventory_section(section) _logbook_page.visible = section == Section.LOGBOOK - _the_net_page.visible = section == Section.NET _mail_page.visible = section == Section.MAIL _profile_page.visible = section == Section.PROFILE _players_page.visible = section == Section.PLAYERS @@ -936,10 +922,6 @@ func _show_section_immediate(section: Section) -> void: _catalog_logbook.deactivate() else: _catalog_logbook.activate() - if section == Section.NET: - _the_net_page.activate() - else: - _the_net_page.deactivate() if section == Section.MAIL: _mail_page.activate() else: @@ -958,7 +940,6 @@ func _show_section_immediate(section: Section) -> void: _tackle_sub_tab.set_selected(section == Section.TACKLE_BOX) _refresh_inventory_organizer_tabs() _logbook_tab.button_pressed = section == Section.LOGBOOK - _the_net_tab.button_pressed = section == Section.NET _mail_tab.button_pressed = section == Section.MAIL _profile_tab.button_pressed = section == Section.PROFILE _players_tab.button_pressed = section == Section.PLAYERS @@ -1019,8 +1000,6 @@ func _focus_current_section() -> void: _tackle_sub_tab.grab_focus() elif _current_section == Section.LOGBOOK: _catalog_logbook.focus_initial() - elif _current_section == Section.NET: - _the_net_page.focus_initial() elif _current_section == Section.MAIL: _mail_tab.grab_focus() elif _current_section == Section.PLAYERS: @@ -1053,7 +1032,6 @@ func _configure_navigation_focus() -> void: var navigation: Array[BubbleButtonType] = [ _inventory_tab, _logbook_tab, - _the_net_tab, _mail_tab, _profile_tab, _players_tab, @@ -1144,8 +1122,6 @@ func _configure_active_page_focus() -> void: _bait_filter.grab_focus() Section.LOGBOOK: _catalog_logbook.focus_initial() - Section.NET: - _the_net_page.focus_initial() Section.MAIL: _mail_page.activate() Section.PROFILE: @@ -1373,7 +1349,6 @@ func _apply_navigation_styles() -> void: for bubble: BubbleButtonType in [ _inventory_tab, _logbook_tab, - _the_net_tab, _mail_tab, _profile_tab, _players_tab, @@ -1405,7 +1380,6 @@ func _apply_navigation_selection_presentation() -> void: for bubble: BubbleButtonType in [ _inventory_tab, _logbook_tab, - _the_net_tab, _mail_tab, _profile_tab, _players_tab, @@ -1491,7 +1465,6 @@ func _on_mail_unread_count_changed(count: int) -> void: func _set_navigation_target(section: Section) -> void: _inventory_tab.button_pressed = _is_inventory_section(section) _logbook_tab.button_pressed = section == Section.LOGBOOK - _the_net_tab.button_pressed = section == Section.NET _mail_tab.button_pressed = section == Section.MAIL _profile_tab.button_pressed = section == Section.PROFILE _players_tab.button_pressed = section == Section.PLAYERS @@ -1670,18 +1643,14 @@ func _update_shell_layout() -> void: _logbook_page.size = reference_size _logbook_page.position = Vector2.ZERO _logbook_rest_position = Vector2.ZERO - _the_net_page.set_anchors_preset(Control.PRESET_TOP_LEFT) - _the_net_page.size = reference_size - _the_net_page.position = Vector2.ZERO - _the_net_rest_position = Vector2.ZERO _mail_page.set_anchors_preset(Control.PRESET_TOP_LEFT) _mail_page.size = reference_size _mail_page.position = Vector2.ZERO _mail_rest_position = Vector2.ZERO _profile_page.set_anchors_preset(Control.PRESET_TOP_LEFT) - _profile_page.position = Vector2.ZERO - _profile_page.size = reference_size - _profile_rest_position = Vector2.ZERO + _profile_page.position = Vector2(42.0, 104.0) + _profile_page.size = Vector2(1196.0, 608.0) + _profile_rest_position = _profile_page.position _players_page.set_anchors_preset(Control.PRESET_TOP_LEFT) _players_page.position = Vector2.ZERO _players_page.size = reference_size @@ -1704,7 +1673,6 @@ func _update_shell_layout() -> void: _bag_page.position = _bag_rest_position _tackle_box_page.position = _tackle_rest_position _logbook_page.position = _logbook_rest_position - _the_net_page.position = _the_net_rest_position _mail_page.position = _mail_rest_position _profile_page.position = _profile_rest_position _players_page.position = Vector2.ZERO @@ -1712,7 +1680,6 @@ func _update_shell_layout() -> void: _bag_page.modulate.a = 1.0 _tackle_box_page.modulate.a = 1.0 _logbook_page.modulate.a = 1.0 - _the_net_page.modulate.a = 1.0 _mail_page.modulate.a = 1.0 _profile_page.modulate.a = 1.0 _players_page.modulate.a = 1.0 @@ -1838,8 +1805,6 @@ func _begin_menu_exit(reason: CloseReason, restore_controls: bool) -> void: _settle_inventory_tabs_for_close() if _current_section == Section.LOGBOOK: _catalog_logbook.deactivate() - elif _current_section == Section.NET: - _the_net_page.deactivate() get_viewport().gui_release_focus() var generation: int = _transition_generation var closing_generation: int = _menu_generation @@ -1886,7 +1851,6 @@ func _finish_close( _cancel_presentation_tween() _cancel_page_tween() _cancel_logbook_page_transition(true) - _the_net_page.deactivate() _transitioning = false _page_transitioning = false _bag_drag_active = false @@ -1918,8 +1882,6 @@ func _get_section_root(section: Section) -> Control: return _tackle_box_page Section.LOGBOOK: return _logbook_page - Section.NET: - return _the_net_page Section.MAIL: return _mail_page Section.PROFILE: @@ -1938,8 +1900,6 @@ func _get_section_rest_position(section: Section) -> Vector2: return _tackle_rest_position Section.LOGBOOK: return _logbook_rest_position - Section.NET: - return _the_net_rest_position Section.MAIL: return _mail_rest_position Section.PROFILE: @@ -2042,7 +2002,6 @@ func _set_shell_interactive(interactive: bool) -> void: for bubble: BubbleButtonType in [ _inventory_tab, _logbook_tab, - _the_net_tab, _mail_tab, _profile_tab, _players_tab, @@ -2099,9 +2058,6 @@ func _set_content_interactive(interactive: bool) -> void: _catalog_logbook.set_interactive( interactive and _current_section == Section.LOGBOOK ) - _the_net_page.set_interactive( - interactive and _current_section == Section.NET - ) _mail_page.set_interactive( interactive and _current_section == Section.MAIL ) @@ -2177,7 +2133,6 @@ func _reset_page_transition_visuals() -> void: Section.BAG, Section.TACKLE_BOX, Section.LOGBOOK, - Section.NET, Section.MAIL, Section.PROFILE, Section.PLAYERS, diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index e9693fd..aac0884 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=26 format=3] +[gd_scene load_steps=25 format=3] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] @@ -23,7 +23,6 @@ [ext_resource type="PackedScene" path="res://ui/logbook_page.tscn" id="21_logbook_page"] [ext_resource type="Script" path="res://ui/components/inventory_notepad.gd" id="22_inventory_notepad"] [ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="23_organizer_tab"] -[ext_resource type="PackedScene" path="res://ui/the_net_page.tscn" id="24_the_net_page"] [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"] @@ -723,10 +722,6 @@ mouse_filter = 1 [node name="CatalogLogbook" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage" instance=ExtResource("21_logbook_page")] unique_name_in_owner = true -[node name="TheNetPage" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot" instance=ExtResource("24_the_net_page")] -unique_name_in_owner = true -visible = false - [node name="MailPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true layout_mode = 0 @@ -739,8 +734,9 @@ script = ExtResource("18_mail_page") unique_name_in_owner = true visible = false layout_mode = 0 +offset_top = 112.0 offset_right = 1280.0 -offset_bottom = 720.0 +offset_bottom = 710.0 [node name="PlayersPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] visible = false @@ -1394,16 +1390,15 @@ compact_reference_size = Vector2(840, 100) unique_name_in_owner = true layout_mode = 0 toggle_mode = true -text = "stuff" -accessibility_name = "stuff" +text = "inventory" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(60, 60) -compact_anchor = Vector2(60, 73.3333) +desktop_anchor = Vector2(70, 60) +compact_anchor = Vector2(70, 73.3333) compact_minimum_size = Vector2(84, 82) -minimum_font_size = 25 -maximum_font_size = 25 +minimum_font_size = 20 +maximum_font_size = 22 horizontal_amplitude = 1.2 vertical_amplitude = 2.4 motion_period = 5.6 @@ -1419,8 +1414,8 @@ text = "logbook" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(180, 60) -compact_anchor = Vector2(180, 73.3333) +desktop_anchor = Vector2(210, 60) +compact_anchor = Vector2(210, 73.3333) compact_minimum_size = Vector2(84, 82) minimum_font_size = 25 maximum_font_size = 25 @@ -1431,27 +1426,6 @@ motion_phase = 2.85 deformation_amplitude = 0.012 deformation_period = 6.7 -[node name="TheNetTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"] -unique_name_in_owner = true -layout_mode = 0 -toggle_mode = true -text = "fishnet" -accessibility_name = "net" -script = ExtResource("6_bubble") -profile = ExtResource("4_profile") -neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(300, 60) -compact_anchor = Vector2(300, 73.3333) -compact_minimum_size = Vector2(84, 82) -minimum_font_size = 25 -maximum_font_size = 25 -horizontal_amplitude = 1.2 -vertical_amplitude = 2.3 -motion_period = 5.75 -motion_phase = 3.2 -deformation_amplitude = 0.011 -deformation_period = 6.45 - [node name="MailTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"] unique_name_in_owner = true layout_mode = 0 @@ -1460,8 +1434,8 @@ text = "mail" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(420, 60) -compact_anchor = Vector2(420, 73.3333) +desktop_anchor = Vector2(350, 60) +compact_anchor = Vector2(350, 73.3333) compact_minimum_size = Vector2(84, 82) minimum_font_size = 25 maximum_font_size = 25 @@ -1496,8 +1470,8 @@ text = "profile" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(540, 60) -compact_anchor = Vector2(540, 73.3333) +desktop_anchor = Vector2(490, 60) +compact_anchor = Vector2(490, 73.3333) compact_minimum_size = Vector2(84, 82) minimum_font_size = 23 maximum_font_size = 25 @@ -1512,13 +1486,12 @@ deformation_period = 6.5 unique_name_in_owner = true layout_mode = 0 toggle_mode = true -text = "online" -accessibility_name = "online" +text = "players" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(112, 108) -desktop_anchor = Vector2(660, 60) -compact_anchor = Vector2(660, 73.3333) +desktop_anchor = Vector2(630, 60) +compact_anchor = Vector2(630, 73.3333) compact_minimum_size = Vector2(84, 82) minimum_font_size = 22 maximum_font_size = 24 @@ -1536,8 +1509,8 @@ text = "close" script = ExtResource("6_bubble") profile = ExtResource("4_profile") neutral_size = Vector2(96, 94) -desktop_anchor = Vector2(780, 57) -compact_anchor = Vector2(780, 73.3333) +desktop_anchor = Vector2(770, 57) +compact_anchor = Vector2(770, 73.3333) compact_minimum_size = Vector2(76, 74) minimum_font_size = 23 maximum_font_size = 23 diff --git a/ui/players_page.gd b/ui/players_page.gd index b5a62ee..d834176 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -2,7 +2,7 @@ class_name PlayersPage extends Control var _service: NetworkPlayerListService -var _count_label: Label +var _header: Label var _tabs: HBoxContainer var _list: VBoxContainer var _status: Label @@ -36,17 +36,23 @@ func deactivate() -> void: func _build() -> void: - var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) + var paper := PanelContainer.new() + paper.position = Vector2(58, 128) + paper.size = Vector2(1164, 538) + add_child(paper) + paper.add_theme_stylebox_override("panel", UtilityPageStyle.panel_style()) var root := VBoxContainer.new() - root.add_theme_constant_override("separation", 9) - margin.add_child(root) - var tab_row := HBoxContainer.new() - tab_row.add_theme_constant_override("separation", 16) - root.add_child(tab_row) + root.add_theme_constant_override("separation", 10) + paper.add_child(root) + _header = Label.new() + _header.add_theme_font_size_override("font_size", 27) + _header.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + root.add_child(_header) _tabs = HBoxContainer.new() - _tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL _tabs.add_theme_constant_override("separation", 10) - tab_row.add_child(_tabs) + root.add_child(_tabs) for index: int in 3: var button := Button.new() button.text = ["players", "relationships", "banned"][index] @@ -54,20 +60,12 @@ func _build() -> void: button.pressed.connect(_select_tab.bind(index)) UtilityPageStyle.apply_ocean_button(button) _tabs.add_child(button) - _count_label = Label.new() - _count_label.add_theme_font_size_override("font_size", 17) - _count_label.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - _count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - tab_row.add_child(_count_label) var scroll := ScrollContainer.new() - scroll.custom_minimum_size = Vector2(0, 310) - scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + scroll.custom_minimum_size = Vector2(0, 392) scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED root.add_child(scroll) _list = VBoxContainer.new() - _list.custom_minimum_size = Vector2(1032, 0) + _list.custom_minimum_size = Vector2(1080, 0) _list.add_theme_constant_override("separation", 7) scroll.add_child(_list) _status = Label.new() @@ -88,7 +86,7 @@ func _select_tab(index: int) -> void: func _refresh() -> void: if _service == null or _list == null: return - _count_label.text = "%d / %d connected" % [ + _header.text = "Players\n%d / %d connected" % [ _service.get_connected_count(), _service.get_max_players(), ] for index: int in _tabs.get_child_count(): @@ -170,6 +168,7 @@ func _build_active_rows() -> void: ban.pressed.connect(_confirm_ban.bind(entry)) UtilityPageStyle.apply_ocean_button(ban) row.add_child(ban) + _list.add_child(row) func _build_session_artwork_controls() -> void: @@ -196,6 +195,7 @@ func _build_session_artwork_controls() -> void: ) UtilityPageStyle.apply_ocean_button(reset) row.add_child(reset) + _list.add_child(row) func _build_relationship_rows() -> void: @@ -234,6 +234,7 @@ func _build_relationship_rows() -> void: ) UtilityPageStyle.apply_ocean_button(unmute) row.add_child(unmute) + _list.add_child(row) func _build_ban_rows() -> void: @@ -261,18 +262,14 @@ func _build_ban_rows() -> void: unban.pressed.connect(_confirm_unban.bind(fingerprint)) UtilityPageStyle.apply_ocean_button(unban) row.add_child(unban) + _list.add_child(row) func _make_row() -> HBoxContainer: - var panel := PanelContainer.new() - panel.add_theme_stylebox_override( - "panel", UtilityPageStyle.row_style(false) - ) - _list.add_child(panel) var row := HBoxContainer.new() row.custom_minimum_size.y = 58 row.add_theme_constant_override("separation", 8) - panel.add_child(row) + row.add_theme_constant_override("outline_size", 1) return row diff --git a/ui/profile_page.gd b/ui/profile_page.gd index dac52b3..eb434bc 100644 --- a/ui/profile_page.gd +++ b/ui/profile_page.gd @@ -4,7 +4,6 @@ extends Control const CHECK_DEBOUNCE_SECONDS: float = 0.4 var _service: NetworkProfileService -var _experience: PlayerExperience var _draft_name: String = "" var _draft_appearance: Dictionary = {} var _persisted_name: String = "" @@ -26,9 +25,6 @@ var _confirmation_label: Label var _confirmation_confirm: Button var _confirmation_action: String = "" var _debounce: Timer -var _experience_level: Label -var _experience_progress: ProgressBar -var _experience_value: Label func _ready() -> void: @@ -41,24 +37,12 @@ func _ready() -> void: add_child(_debounce) -func setup( - service: NetworkProfileService, - experience: PlayerExperience, -) -> void: +func setup(service: NetworkProfileService) -> void: _service = service - _experience = experience if not _service.conflict_result.is_connected(_on_conflict_result): _service.conflict_result.connect(_on_conflict_result) _service.apply_finished.connect(_on_apply_finished) - if ( - _experience != null - and not _experience.experience_changed.is_connected( - _on_experience_changed - ) - ): - _experience.experience_changed.connect(_on_experience_changed) _load_persisted() - _refresh_experience() var identity_value := find_child("IdentityFingerprint", true, false) as Label if identity_value != null: var fingerprint := _service.get_identity_fingerprint() @@ -124,200 +108,134 @@ func has_modal_confirmation() -> bool: func _build_ui() -> void: + var paper := PanelContainer.new() + paper.set_anchors_preset(Control.PRESET_FULL_RECT) + paper.offset_left = 64.0 + paper.offset_top = 28.0 + paper.offset_right = -64.0 + paper.offset_bottom = -28.0 + paper.add_theme_stylebox_override( + "panel", UtilityPageStyle.panel_style() + ) + add_child(paper) UtilityPageStyle.apply_page(self) - var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) + + var margin := MarginContainer.new() + for side: String in ["left", "right", "top", "bottom"]: + margin.add_theme_constant_override("margin_%s" % side, 18) + paper.add_child(margin) var layout := VBoxContainer.new() - layout.add_theme_constant_override("separation", 8) + layout.add_theme_constant_override("separation", 5) margin.add_child(layout) - var account_row := HBoxContainer.new() - account_row.add_theme_constant_override("separation", 14) - layout.add_child(account_row) - var account_stack := VBoxContainer.new() - account_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL - account_stack.add_theme_constant_override("separation", 2) - account_row.add_child(account_stack) - var name_line := HBoxContainer.new() - name_line.add_theme_constant_override("separation", 10) - account_stack.add_child(name_line) + var heading := Label.new() + heading.text = "Player Profile" + heading.add_theme_font_size_override("font_size", 25) + heading.add_theme_color_override( + "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY + ) + layout.add_child(heading) + + var name_row := HBoxContainer.new() + name_row.add_theme_constant_override("separation", 10) + layout.add_child(name_row) + var name_stack := VBoxContainer.new() + name_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL + name_row.add_child(name_stack) var name_label := Label.new() name_label.text = "player name" - name_label.custom_minimum_size.x = 98.0 - name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) - name_line.add_child(name_label) + name_stack.add_child(name_label) _name_edit = LineEdit.new() _name_edit.max_length = NetworkProtocol.MAX_DISPLAY_NAME_LENGTH _name_edit.placeholder_text = "Player" - _name_edit.custom_minimum_size = Vector2(280, 34) + _name_edit.custom_minimum_size = Vector2(300, 40) UtilityPageStyle.apply_ocean_line_edit(_name_edit) - _name_edit.add_theme_stylebox_override( - "normal", UtilityPageStyle.ocean_button_style( - UtilityPageStyle.OCEAN_PANEL_MID - ) - ) _name_edit.text_changed.connect(_on_name_changed) - name_line.add_child(_name_edit) + name_stack.add_child(_name_edit) var helper := Label.new() helper.text = "Shown to other players in multiplayer." - helper.add_theme_font_size_override("font_size", 12) helper.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) - account_stack.add_child(helper) + name_stack.add_child(helper) + _apply_button = Button.new() + _apply_button.text = "apply" + _apply_button.custom_minimum_size = Vector2(92, 40) + UtilityPageStyle.apply_ocean_button(_apply_button) + _apply_button.pressed.connect(_apply) + name_row.add_child(_apply_button) + _revert_button = Button.new() + _revert_button.text = "revert" + _revert_button.custom_minimum_size = Vector2(88, 40) + UtilityPageStyle.apply_ocean_button(_revert_button) + _revert_button.pressed.connect(_revert) + name_row.add_child(_revert_button) + var defaults_button := Button.new() + defaults_button.text = "defaults" + defaults_button.custom_minimum_size = Vector2(88, 40) + UtilityPageStyle.apply_ocean_button(defaults_button) + defaults_button.pressed.connect(_show_confirmation.bind("defaults")) + name_row.add_child(defaults_button) + _name_status = Label.new() - _name_status.add_theme_font_size_override("font_size", 12) _name_status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) - account_stack.add_child(_name_status) + layout.add_child(_name_status) _suggestions = HBoxContainer.new() - _suggestions.add_theme_constant_override("separation", 6) - account_stack.add_child(_suggestions) + _suggestions.add_theme_constant_override("separation", 8) + layout.add_child(_suggestions) var identity_value := Label.new() identity_value.name = "IdentityFingerprint" - identity_value.text = "identity • stored on this device" + identity_value.text = "Identity • Stored on this device" identity_value.tooltip_text = ( "This identity helps other players recognize you between sessions." ) - identity_value.add_theme_font_size_override("font_size", 12) identity_value.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) - account_stack.add_child(identity_value) + layout.add_child(identity_value) - var experience_row := HBoxContainer.new() - experience_row.add_theme_constant_override("separation", 9) - account_stack.add_child(experience_row) - _experience_level = Label.new() - _experience_level.custom_minimum_size.x = 64.0 - _experience_level.add_theme_font_size_override("font_size", 14) - _experience_level.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - experience_row.add_child(_experience_level) - _experience_progress = ProgressBar.new() - _experience_progress.custom_minimum_size = Vector2(210.0, 14.0) - _experience_progress.max_value = 1.0 - _experience_progress.show_percentage = false - _experience_progress.add_theme_stylebox_override( - "background", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_PANEL_MID, 7 - ) - ) - _experience_progress.add_theme_stylebox_override( - "fill", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_SELECTED, 7 - ) - ) - experience_row.add_child(_experience_progress) - _experience_value = Label.new() - _experience_value.add_theme_font_size_override("font_size", 14) - _experience_value.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - experience_row.add_child(_experience_value) - - var actions := HBoxContainer.new() - actions.alignment = BoxContainer.ALIGNMENT_END - actions.size_flags_vertical = Control.SIZE_SHRINK_BEGIN - actions.add_theme_constant_override("separation", 7) - account_row.add_child(actions) - _apply_button = Button.new() - _apply_button.text = "apply" - _apply_button.custom_minimum_size.x = 72.0 - UtilityPageStyle.apply_compact_ocean_button(_apply_button) - _apply_button.pressed.connect(_apply) - actions.add_child(_apply_button) - _revert_button = Button.new() - _revert_button.text = "revert" - _revert_button.custom_minimum_size.x = 76.0 - UtilityPageStyle.apply_compact_ocean_button(_revert_button) - _revert_button.pressed.connect(_revert) - actions.add_child(_revert_button) - var defaults_button := Button.new() - defaults_button.text = "defaults" - defaults_button.custom_minimum_size.x = 82.0 - UtilityPageStyle.apply_compact_ocean_button(defaults_button) - defaults_button.pressed.connect(_show_confirmation.bind("defaults")) - actions.add_child(defaults_button) - - var body_panel := PanelContainer.new() - body_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL - body_panel.add_theme_stylebox_override( - "panel", UtilityPageStyle.row_style(false) - ) - layout.add_child(body_panel) - var body_margin := MarginContainer.new() - body_margin.add_theme_constant_override("margin_left", 14) - body_margin.add_theme_constant_override("margin_top", 10) - body_margin.add_theme_constant_override("margin_right", 14) - body_margin.add_theme_constant_override("margin_bottom", 10) - body_panel.add_child(body_margin) var body := HBoxContainer.new() - body.alignment = BoxContainer.ALIGNMENT_BEGIN - body.add_theme_constant_override("separation", 16) - body_margin.add_child(body) + body.size_flags_vertical = Control.SIZE_EXPAND_FILL + body.add_theme_constant_override("separation", 12) + layout.add_child(body) _category_list = VBoxContainer.new() - _category_list.custom_minimum_size = Vector2(120, 0) - _category_list.add_theme_constant_override("separation", 5) + _category_list.custom_minimum_size = Vector2(140, 0) body.add_child(_category_list) _option_list = VBoxContainer.new() - _option_list.custom_minimum_size = Vector2(170, 0) - _option_list.add_theme_constant_override("separation", 5) + _option_list.custom_minimum_size = Vector2(190, 0) body.add_child(_option_list) - var body_spacer := Control.new() - body_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL - body.add_child(body_spacer) var preview_stack := VBoxContainer.new() - preview_stack.custom_minimum_size.x = 180.0 + preview_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER - preview_stack.add_theme_constant_override("separation", 7) body.add_child(preview_stack) - var preview_frame := PanelContainer.new() - preview_frame.custom_minimum_size = Vector2(180.0, 220.0) - preview_frame.add_theme_stylebox_override( - "panel", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_FIELD, 18 - ) - ) - preview_stack.add_child(preview_frame) _preview = preload("res://ui/profile_preview.tscn").instantiate() - _preview.custom_minimum_size = Vector2(180.0, 220.0) - preview_frame.add_child(_preview) - var preview_actions := VBoxContainer.new() + preview_stack.add_child(_preview) + var preview_actions := HBoxContainer.new() preview_actions.alignment = BoxContainer.ALIGNMENT_CENTER - preview_actions.add_theme_constant_override("separation", 4) + preview_actions.add_theme_constant_override("separation", 10) preview_stack.add_child(preview_actions) var preview_note := Label.new() - preview_note.text = "drag or use left / right" - preview_note.custom_minimum_size.x = 180.0 - preview_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - preview_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - preview_note.add_theme_font_size_override("font_size", 12) + preview_note.text = "Current player • drag or use left / right" preview_note.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) preview_actions.add_child(preview_note) var reset_view := Button.new() reset_view.text = "reset view" - reset_view.size_flags_horizontal = Control.SIZE_SHRINK_CENTER reset_view.pressed.connect(_preview.reset_view) - UtilityPageStyle.apply_compact_ocean_button(reset_view) + UtilityPageStyle.apply_ocean_button(reset_view) preview_actions.add_child(reset_view) _discard_confirmation = PanelContainer.new() _discard_confirmation.visible = false _discard_confirmation.set_anchors_and_offsets_preset(Control.PRESET_CENTER) _discard_confirmation.custom_minimum_size = Vector2(430, 190) - _discard_confirmation.add_theme_stylebox_override( - "panel", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_PANEL_MID, 14 - ) - ) add_child(_discard_confirmation) var confirm_stack := VBoxContainer.new() confirm_stack.alignment = BoxContainer.ALIGNMENT_CENTER @@ -331,7 +249,6 @@ func _build_ui() -> void: confirm_stack.add_child(confirm_buttons) _confirmation_confirm = Button.new() _confirmation_confirm.pressed.connect(_confirm_pending_action) - UtilityPageStyle.apply_ocean_button(_confirmation_confirm) confirm_buttons.add_child(_confirmation_confirm) var keep := Button.new() keep.name = "KeepEditing" @@ -341,7 +258,6 @@ func _build_ui() -> void: _discard_confirmation.visible = false _name_edit.grab_focus() ) - UtilityPageStyle.apply_ocean_button(keep) confirm_buttons.add_child(keep) _build_categories() @@ -354,10 +270,10 @@ func _build_categories() -> void: var button := Button.new() button.text = CharacterCustomizationCatalog.category_label(category_id) button.toggle_mode = true - button.custom_minimum_size.x = 120.0 + button.custom_minimum_size = Vector2(0, 34) button.button_pressed = category_id == _category_id button.pressed.connect(_select_category.bind(category_id)) - UtilityPageStyle.apply_compact_ocean_button(button) + UtilityPageStyle.apply_ocean_button(button) _category_list.add_child(button) _refresh_options() @@ -378,7 +294,7 @@ func _refresh_options() -> void: child.queue_free() var title := Label.new() title.text = CharacterCustomizationCatalog.category_label(_category_id) - title.add_theme_font_size_override("font_size", 18) + title.add_theme_font_size_override("font_size", 22) title.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) @@ -395,10 +311,10 @@ func _refresh_options() -> void: var button := Button.new() button.text = str(option["label"]) button.toggle_mode = true - button.custom_minimum_size.x = 170.0 + button.custom_minimum_size = Vector2(0, 34) button.button_pressed = _draft_appearance.get(_category_id) == option_id button.pressed.connect(_select_option.bind(_category_id, option_id)) - UtilityPageStyle.apply_compact_ocean_button(button) + UtilityPageStyle.apply_ocean_button(button) _option_list.add_child(button) @@ -442,7 +358,6 @@ func _on_conflict_result( var button := Button.new() button.text = suggestion button.pressed.connect(_use_suggestion.bind(suggestion)) - UtilityPageStyle.apply_compact_ocean_button(button) _suggestions.add_child(button) var anyway := Button.new() anyway.text = "use anyway" @@ -450,33 +365,9 @@ func _on_conflict_result( _allow_duplicate = true _name_status.text = "Duplicate name allowed for this apply." ) - UtilityPageStyle.apply_compact_ocean_button(anyway) _suggestions.add_child(anyway) -func _on_experience_changed(_total_experience: int, _level: int) -> void: - _refresh_experience() - - -func _refresh_experience() -> void: - if _experience_level == null: - return - if _experience == null: - _experience_level.text = "level 1" - _experience_progress.value = 0.0 - _experience_value.text = "0 / 100 xp" - return - var level: int = _experience.get_level() - var current: int = _experience.get_experience_in_level() - var required: int = _experience.get_experience_for_next_level() - _experience_level.text = "level %d" % level - _experience_progress.value = _experience.get_level_progress() - _experience_value.text = "%d / %d xp" % [current, required] - _experience_value.tooltip_text = "%d total xp" % ( - _experience.get_total_experience() - ) - - func _use_suggestion(value: String) -> void: _name_edit.text = value _draft_name = value diff --git a/ui/profile_preview.tscn b/ui/profile_preview.tscn index 79cd8fe..37be6fa 100644 --- a/ui/profile_preview.tscn +++ b/ui/profile_preview.tscn @@ -18,7 +18,7 @@ stretch = true script = ExtResource("1") [node name="Viewport" type="SubViewport" parent="."] -transparent_bg = true +transparent_bg = false size = Vector2i(320, 330) render_target_update_mode = 4 world_3d = SubResource("PreviewWorld") diff --git a/ui/the_net_page.gd b/ui/the_net_page.gd deleted file mode 100644 index 3ee735a..0000000 --- a/ui/the_net_page.gd +++ /dev/null @@ -1,489 +0,0 @@ -class_name TheNetPage -extends Control - -enum View { - DAILY, - LIFETIME, - PAYMENTS, -} - -var _jobs: PlayerJobService -var _world_time: WorldTimeService -var _header: Label -var _refresh_label: Label -var _forecast_list: HBoxContainer -var _tabs: HBoxContainer -var _list: VBoxContainer -var _status: Label -var _current_view: View = View.DAILY -var _forecast_start_index: int = -1 -var _active: bool = false -var _interactive: bool = false - - -func _ready() -> void: - set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - UtilityPageStyle.apply_page(self) - _build_laptop() - set_process(false) - - -func setup(jobs: PlayerJobService, world_time: WorldTimeService) -> void: - _jobs = jobs - _world_time = world_time - _jobs.changed.connect(_refresh) - _jobs.status_changed.connect(_on_status_changed) - _refresh() - - -func activate() -> void: - _active = true - set_process(true) - _refresh() - focus_initial() - - -func deactivate() -> void: - _active = false - set_process(false) - set_interactive(false) - _status.text = "" - - -func set_interactive(interactive: bool) -> void: - _interactive = interactive - mouse_filter = ( - Control.MOUSE_FILTER_PASS - if interactive - else Control.MOUSE_FILTER_IGNORE - ) - if _tabs != null: - for child: Node in _tabs.get_children(): - var button := child as Button - if button != null: - button.focus_mode = ( - Control.FOCUS_ALL - if interactive - else Control.FOCUS_NONE - ) - button.mouse_filter = ( - Control.MOUSE_FILTER_STOP - if interactive - else Control.MOUSE_FILTER_IGNORE - ) - _refresh() - - -func focus_initial() -> void: - if _interactive and _tabs != null and _tabs.get_child_count() > 0: - var button := _tabs.get_child(int(_current_view)) as Button - if button != null: - button.grab_focus() - - -func _process(_delta: float) -> void: - if _active and _jobs != null: - _refresh_label.text = _daily_refresh_text() - _refresh_forecast(false) - - -func _build_laptop() -> void: - var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) - - var layout := VBoxContainer.new() - layout.add_theme_constant_override("separation", 9) - margin.add_child(layout) - - var title_row := HBoxContainer.new() - title_row.add_theme_constant_override("separation", 16) - layout.add_child(title_row) - var header_left := VBoxContainer.new() - header_left.size_flags_horizontal = Control.SIZE_EXPAND_FILL - header_left.add_theme_constant_override("separation", 0) - title_row.add_child(header_left) - _header = Label.new() - _header.text = "fishnet" - _header.add_theme_font_size_override("font_size", 32) - _header.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - header_left.add_child(_header) - var header_spacer := Control.new() - header_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL - header_spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE - header_left.add_child(header_spacer) - _tabs = HBoxContainer.new() - _tabs.add_theme_constant_override("separation", 8) - header_left.add_child(_tabs) - for view_index: int in 3: - var tab := Button.new() - tab.text = ["daily jobs", "lifetime jobs", "payments"][view_index] - tab.toggle_mode = true - tab.pressed.connect(_select_view.bind(view_index as View)) - UtilityPageStyle.apply_ocean_button(tab) - _tabs.add_child(tab) - var refresh_alignment_spacer := Control.new() - refresh_alignment_spacer.custom_minimum_size.y = 20.0 - refresh_alignment_spacer.mouse_filter = Control.MOUSE_FILTER_IGNORE - header_left.add_child(refresh_alignment_spacer) - var forecast_column := VBoxContainer.new() - forecast_column.custom_minimum_size.x = 292.0 - forecast_column.size_flags_horizontal = Control.SIZE_SHRINK_END - forecast_column.add_theme_constant_override("separation", 3) - title_row.add_child(forecast_column) - var forecast_panel := PanelContainer.new() - forecast_panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN - forecast_panel.add_theme_stylebox_override( - "panel", UtilityPageStyle.row_style(false) - ) - forecast_column.add_child(forecast_panel) - var forecast_margin := MarginContainer.new() - forecast_margin.add_theme_constant_override("margin_left", 10) - forecast_margin.add_theme_constant_override("margin_top", 8) - forecast_margin.add_theme_constant_override("margin_right", 10) - forecast_margin.add_theme_constant_override("margin_bottom", 8) - forecast_panel.add_child(forecast_margin) - var forecast_stack := VBoxContainer.new() - forecast_stack.add_theme_constant_override("separation", 7) - forecast_margin.add_child(forecast_stack) - var forecast_heading := Label.new() - forecast_heading.text = "weather forecast" - forecast_heading.add_theme_font_size_override("font_size", 20) - forecast_heading.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - forecast_stack.add_child(forecast_heading) - _forecast_list = HBoxContainer.new() - _forecast_list.alignment = BoxContainer.ALIGNMENT_CENTER - _forecast_list.add_theme_constant_override("separation", 6) - forecast_stack.add_child(_forecast_list) - _refresh_label = Label.new() - _refresh_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT - _refresh_label.add_theme_font_size_override("font_size", 14) - _refresh_label.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - forecast_column.add_child(_refresh_label) - - var jobs_column := VBoxContainer.new() - jobs_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL - jobs_column.size_flags_vertical = Control.SIZE_EXPAND_FILL - jobs_column.add_theme_constant_override("separation", 8) - layout.add_child(jobs_column) - - var scroll := ScrollContainer.new() - scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL - scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL - scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED - jobs_column.add_child(scroll) - _list = VBoxContainer.new() - _list.size_flags_horizontal = Control.SIZE_EXPAND_FILL - _list.add_theme_constant_override("separation", 7) - scroll.add_child(_list) - - _status = Label.new() - _status.add_theme_font_size_override("font_size", 16) - _status.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - jobs_column.add_child(_status) - -func _select_view(view: View) -> void: - _current_view = view - _refresh() - focus_initial() - - -func _refresh() -> void: - if _jobs == null or _list == null: - return - _refresh_label.text = _daily_refresh_text() - _refresh_forecast(true) - for index: int in _tabs.get_child_count(): - var tab := _tabs.get_child(index) as Button - if tab != null: - tab.button_pressed = index == int(_current_view) - for child: Node in _list.get_children(): - _list.remove_child(child) - child.queue_free() - match _current_view: - View.DAILY: - _build_job_rows(_jobs.get_daily_jobs(), "no daily jobs available") - View.LIFETIME: - _build_job_rows( - _jobs.get_lifetime_jobs(), "all lifetime jobs complete" - ) - View.PAYMENTS: - _build_payment_rows() - - -func _build_job_rows(jobs: Array[Dictionary], empty_text: String) -> void: - if jobs.is_empty(): - _add_empty(empty_text) - return - for job: Dictionary in jobs: - var row := PanelContainer.new() - row.add_theme_stylebox_override( - "panel", UtilityPageStyle.row_style(false) - ) - var content := HBoxContainer.new() - content.add_theme_constant_override("separation", 14) - row.add_child(content) - - var text_column := VBoxContainer.new() - text_column.custom_minimum_size.x = 245.0 - text_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL - content.add_child(text_column) - var title := Label.new() - title.text = str(job.get("title", "job")) - title.add_theme_font_size_override("font_size", 20) - title.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - text_column.add_child(title) - var description := Label.new() - description.text = str(job.get("description", "")) - description.add_theme_font_size_override("font_size", 16) - description.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - text_column.add_child(description) - - var target: int = int(job.get("target", 1)) - var progress: int = int(job.get("progress", 0)) - var progress_bar := ProgressBar.new() - progress_bar.custom_minimum_size = Vector2(100.0, 24.0) - progress_bar.max_value = float(target) - progress_bar.value = float(progress) - progress_bar.show_percentage = false - progress_bar.add_theme_stylebox_override( - "background", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_PANEL_DEEP, 9 - ) - ) - progress_bar.add_theme_stylebox_override( - "fill", UtilityPageStyle.rounded_style( - UtilityPageStyle.OCEAN_SELECTED, 9 - ) - ) - content.add_child(progress_bar) - - var count := Label.new() - count.custom_minimum_size.x = 54.0 - count.text = "%d / %d" % [progress, target] - count.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - count.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - count.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - content.add_child(count) - - var reward := Label.new() - reward.custom_minimum_size.x = 98.0 - reward.text = "$%d • %d xp" % [ - int(job.get("fish_coin", 0)), int(job.get("experience", 0)), - ] - reward.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - reward.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - content.add_child(reward) - - var claim := Button.new() - claim.custom_minimum_size = Vector2(76.0, 36.0) - var claimable: bool = bool(job.get("claimable", false)) - claim.text = "claim" if claimable else ( - "done" if int(job.get("completed_count", 0)) > 0 else "working" - ) - claim.disabled = not claimable or not _interactive - claim.focus_mode = ( - Control.FOCUS_ALL if not claim.disabled else Control.FOCUS_NONE - ) - claim.mouse_filter = ( - Control.MOUSE_FILTER_STOP - if not claim.disabled - else Control.MOUSE_FILTER_IGNORE - ) - var claim_id: String = str( - job.get("claim_id", job.get("id", "")) - ) - claim.pressed.connect(_claim.bind(claim_id)) - UtilityPageStyle.apply_compact_ocean_button(claim) - content.add_child(claim) - _list.add_child(row) - - -func _build_payment_rows() -> void: - var rewards: Array[Dictionary] = _jobs.get_pending_rewards() - if rewards.is_empty(): - _add_empty("no completed payments waiting") - return - for reward: Dictionary in rewards: - var row := PanelContainer.new() - row.add_theme_stylebox_override( - "panel", UtilityPageStyle.row_style(false) - ) - var content := HBoxContainer.new() - content.add_theme_constant_override("separation", 14) - row.add_child(content) - var label := Label.new() - label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - label.text = "%s\n$%d • %d xp" % [ - str(reward.get("title", "completed job")), - int(reward.get("fish_coin", 0)), - int(reward.get("experience", 0)), - ] - label.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - content.add_child(label) - var claim := Button.new() - claim.text = "claim" - claim.custom_minimum_size = Vector2(110.0, 42.0) - claim.disabled = not _interactive - claim.focus_mode = ( - Control.FOCUS_ALL if _interactive else Control.FOCUS_NONE - ) - claim.mouse_filter = ( - Control.MOUSE_FILTER_STOP - if _interactive - else Control.MOUSE_FILTER_IGNORE - ) - claim.pressed.connect( - _claim.bind(str(reward.get("claim_id", ""))) - ) - UtilityPageStyle.apply_ocean_button(claim) - content.add_child(claim) - _list.add_child(row) - - -func _claim(claim_id: String) -> void: - if not _jobs.claim(claim_id): - _status.text = "payment could not be completed" - _refresh() - - -func _add_empty(message: String) -> void: - var empty := Label.new() - empty.text = message - empty.custom_minimum_size = Vector2(0.0, 92.0) - empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - empty.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - empty.add_theme_font_size_override("font_size", 20) - empty.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - _list.add_child(empty) - - -func _refresh_forecast(force: bool) -> void: - if _forecast_list == null: - return - var current_index: int = _current_forecast_index() - if not force and current_index == _forecast_start_index: - return - _forecast_start_index = current_index - for child: Node in _forecast_list.get_children(): - _forecast_list.remove_child(child) - child.queue_free() - if _jobs == null or _world_time == null: - _add_forecast_empty("forecast unavailable") - return - var forecast: Array[Dictionary] = _jobs.get_forecast() - if forecast.is_empty(): - _add_forecast_empty("forecast unavailable") - return - current_index = clampi(current_index, 0, forecast.size() - 1) - var visible_count: int = mini(4, forecast.size()) - for offset: int in visible_count: - var index: int = (current_index + offset) % forecast.size() - var entry: Dictionary = forecast[index] - var start_hour: float = float(entry.get("start_hour", 0.0)) - var weather: WorldWeatherService.Weather = ( - int(entry.get("weather", 0)) as WorldWeatherService.Weather - ) - var slot := Control.new() - slot.custom_minimum_size = Vector2(63.0, 66.0) - slot.mouse_filter = Control.MOUSE_FILTER_IGNORE - _forecast_list.add_child(slot) - var icon := WeatherIcon.new() - icon.position = Vector2(1.0, 7.0) - icon.size = Vector2(56.0, 56.0) - icon.custom_minimum_size = Vector2(56.0, 56.0) - icon.set_weather(weather) - icon.set_nighttime( - WorldTimeService.phase_for_hour(start_hour) - == WorldTimeService.Phase.NIGHT - ) - slot.add_child(icon) - var time_label := Label.new() - time_label.position = Vector2(34.0, 0.0) - time_label.size = Vector2(29.0, 18.0) - time_label.mouse_filter = Control.MOUSE_FILTER_IGNORE - time_label.text = _forecast_exponent_time(start_hour) - time_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - time_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER - time_label.add_theme_font_size_override("font_size", 10) - time_label.add_theme_constant_override("outline_size", 3) - time_label.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY - ) - time_label.add_theme_color_override( - "font_outline_color", UtilityPageStyle.OCEAN_PANEL_DEEP - ) - slot.add_child(time_label) - - -func _current_forecast_index() -> int: - if _world_time == null: - return -1 - var elapsed: float = fposmod( - _world_time.get_time_hours() - WorldTimeService.DAY_START_HOUR, - WorldTimeService.HOURS_PER_DAY, - ) - return floori(elapsed / JobCatalog.WEATHER_SEGMENT_HOURS) - - -func _compact_clock_time(time_hours: float) -> String: - var normalized: float = fposmod( - time_hours, WorldTimeService.HOURS_PER_DAY - ) - var total_minutes: int = floori(normalized * 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 - var suffix: String = "AM" if hour_24 < 12 else "PM" - if minute == 0: - return "%d%s" % [hour_12, suffix] - return "%d:%02d%s" % [hour_12, minute, suffix] - - -func _forecast_exponent_time(time_hours: float) -> String: - return _compact_clock_time(time_hours).to_lower() - - -func _daily_refresh_text() -> String: - if _jobs == null: - return "daily jobs unavailable" - var countdown: String = _jobs.get_time_until_refresh_text() - if countdown.begins_with("refreshes in "): - return "daily jobs refresh in %s" % countdown.trim_prefix( - "refreshes in " - ) - return countdown - - -func _add_forecast_empty(message: String) -> void: - var label := Label.new() - label.text = message - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - label.add_theme_color_override( - "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY - ) - _forecast_list.add_child(label) - - -func _on_status_changed(message: String) -> void: - _status.text = message diff --git a/ui/the_net_page.gd.uid b/ui/the_net_page.gd.uid deleted file mode 100644 index 614db5e..0000000 --- a/ui/the_net_page.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://couoolj844sj3 diff --git a/ui/the_net_page.tscn b/ui/the_net_page.tscn deleted file mode 100644 index db0bb43..0000000 --- a/ui/the_net_page.tscn +++ /dev/null @@ -1,13 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://ui/the_net_page.gd" id="1_net"] - -[node name="TheNetPage" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 1 -script = ExtResource("1_net") diff --git a/ui/utility_page_style.gd b/ui/utility_page_style.gd index 9045cbc..a0b40be 100644 --- a/ui/utility_page_style.gd +++ b/ui/utility_page_style.gd @@ -26,65 +26,12 @@ const GREEN: Color = Color("31594d") const LIGHT_TEXT: Color = Color("f5eed9") const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72) const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween" -const LAPTOP_RECT: Rect2 = Rect2(66.0, 132.0, 1148.0, 520.0) static func apply_page(root: Control) -> void: root.add_theme_font_override("font", TuffyFont) -static func rounded_style(color: Color, radius: int) -> StyleBoxFlat: - var style := StyleBoxFlat.new() - style.bg_color = color - style.set_border_width_all(0) - style.set_corner_radius_all(radius) - return style - - -static func build_laptop_screen( - root: Control, - laptop_rect: Rect2 = LAPTOP_RECT, -) -> MarginContainer: - var laptop := PanelContainer.new() - laptop.position = laptop_rect.position - laptop.size = laptop_rect.size - laptop.add_theme_stylebox_override( - "panel", rounded_style(OCEAN_PANEL_MID, 24) - ) - root.add_child(laptop) - - var shell_margin := MarginContainer.new() - shell_margin.add_theme_constant_override("margin_left", 20) - shell_margin.add_theme_constant_override("margin_top", 18) - shell_margin.add_theme_constant_override("margin_right", 20) - shell_margin.add_theme_constant_override("margin_bottom", 24) - laptop.add_child(shell_margin) - - var screen := PanelContainer.new() - screen.add_theme_stylebox_override( - "panel", rounded_style(OCEAN_FIELD, 16) - ) - shell_margin.add_child(screen) - - var content_margin := MarginContainer.new() - content_margin.add_theme_constant_override("margin_left", 24) - content_margin.add_theme_constant_override("margin_top", 18) - content_margin.add_theme_constant_override("margin_right", 24) - content_margin.add_theme_constant_override("margin_bottom", 18) - screen.add_child(content_margin) - - var base := ColorRect.new() - base.position = Vector2( - laptop_rect.position.x + 82.0, - laptop_rect.end.y - 6.0, - ) - base.size = Vector2(laptop_rect.size.x - 164.0, 14.0) - base.color = OCEAN_PANEL_MID - base.mouse_filter = Control.MOUSE_FILTER_IGNORE - root.add_child(base) - return content_margin - - static func panel_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = OCEAN_PANEL_DEEP @@ -173,22 +120,6 @@ static func apply_ocean_button(button: BaseButton) -> void: button.custom_minimum_size.y = maxf(button.custom_minimum_size.y, 40.0) -static func apply_compact_ocean_button(button: BaseButton) -> void: - apply_ocean_button(button) - for state: StringName in [ - &"normal", &"hover", &"pressed", &"focus", &"disabled", - ]: - var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat - if style == null: - continue - style.content_margin_left = 10.0 - style.content_margin_right = 10.0 - style.content_margin_top = 4.0 - style.content_margin_bottom = 4.0 - button.add_theme_stylebox_override(state, style) - button.custom_minimum_size.y = 34.0 - - static func apply_line_edit(edit: LineEdit) -> void: edit.add_theme_font_override("font", TuffyFont) edit.add_theme_color_override("font_color", LIGHT_TEXT) diff --git a/world/world_time_service.gd b/world/world_time_service.gd index 1285f3e..378fcd1 100644 --- a/world/world_time_service.gd +++ b/world/world_time_service.gd @@ -28,7 +28,6 @@ var _persistent_time_hours: float = DEFAULT_START_HOUR var _persistence_tracking_enabled: bool = false var _running: bool = false var _phase: Phase = Phase.DAWN -var _last_emitted_clock_minute: int = floori(DEFAULT_START_HOUR * 60.0) func _ready() -> void: @@ -141,18 +140,14 @@ 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 clock_minute: int = floori(normalized * 60.0) - var clock_minute_changed: bool = ( - clock_minute != _last_emitted_clock_minute - ) + 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 clock_minute_changed or phase_was_changed: - _last_emitted_clock_minute = clock_minute + if force_emit or time_was_changed or phase_was_changed: time_changed.emit(_time_hours, _phase) diff --git a/world/world_time_visual_controller.gd b/world/world_time_visual_controller.gd index b7b1839..611e60b 100644 --- a/world/world_time_visual_controller.gd +++ b/world/world_time_visual_controller.gd @@ -5,10 +5,6 @@ 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 RAIN_PARTICLE_AMOUNT: int = 560 -const RAIN_VELOCITY_MIN: float = 16.0 -const RAIN_VELOCITY_MAX: float = 20.0 -const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014) const DAY_SKY_TOP := Color(0.204, 0.498, 0.643) const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847) @@ -149,7 +145,7 @@ func _prepare_runtime_environment() -> bool: func _prepare_rain() -> void: _rain = GPUParticles3D.new() _rain.name = "LocalRain" - _rain.amount = RAIN_PARTICLE_AMOUNT + _rain.amount = 480 _rain.amount_ratio = 0.0 _rain.lifetime = 1.25 _rain.fixed_fps = 30 @@ -164,12 +160,12 @@ func _prepare_rain() -> void: 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 = RAIN_VELOCITY_MIN - process_material.initial_velocity_max = RAIN_VELOCITY_MAX + 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 = RAIN_DROP_SIZE + 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 diff --git a/world/world_weather_service.gd b/world/world_weather_service.gd index fd09d60..d01728c 100644 --- a/world/world_weather_service.gd +++ b/world/world_weather_service.gd @@ -15,21 +15,11 @@ const SUNNY_DURATION_RANGE := Vector2(480.0, 900.0) const CLOUDY_DURATION_RANGE := Vector2(300.0, 720.0) const RAINY_DURATION_RANGE := Vector2(300.0, 600.0) const FOGGY_DURATION_RANGE := Vector2(300.0, 600.0) -const MAX_PERSISTED_SECONDS: float = 1800.0 -const DAILY_PLAN_SEGMENT_HOURS: float = 2.0 -const DAILY_PLAN_SEGMENT_COUNT: int = 12 var _weather: Weather = DEFAULT_WEATHER var _seconds_remaining: float = SUNNY_DURATION_RANGE.x -var _persistent_weather: Weather = DEFAULT_WEATHER -var _persistent_seconds_remaining: float = SUNNY_DURATION_RANGE.x -var _has_persistent_state: bool = false -var _persistence_tracking_enabled: bool = false var _running_authority: bool = false var _rng := RandomNumberGenerator.new() -var _daily_plan_id: String = "" -var _daily_schedule: Array[Dictionary] = [] -var _world_time: WorldTimeService func _ready() -> void: @@ -44,21 +34,7 @@ func begin_authoritative_session(seed_value: int) -> void: _rng.seed = seed_value _running_authority = true set_process(true) - if _daily_schedule.is_empty() or _world_time == null: - if _has_persistent_state: - _set_weather( - _persistent_weather, - _persistent_seconds_remaining, - true, - ) - else: - _set_weather( - DEFAULT_WEATHER, - _roll_duration(DEFAULT_WEATHER), - true, - ) - else: - _update_scheduled_weather(true) + _set_weather(DEFAULT_WEATHER, _roll_duration(DEFAULT_WEATHER), true) func begin_remote_session() -> void: @@ -76,17 +52,12 @@ func end_session() -> void: func advance_weather(real_seconds: float) -> void: if not _running_authority or real_seconds <= 0.0: return - if not _daily_schedule.is_empty() and _world_time != null: - _update_scheduled_weather(false) - return _seconds_remaining -= real_seconds while _seconds_remaining <= 0.0: var overrun: float = -_seconds_remaining var next_weather: Weather = _choose_next_weather() _set_weather(next_weather, _roll_duration(next_weather), true) _seconds_remaining -= overrun - if _persistence_tracking_enabled: - _store_persistent_state() func apply_authoritative_snapshot( @@ -98,56 +69,6 @@ func apply_authoritative_snapshot( _set_weather(weather, maxf(seconds_remaining, 0.0), false) -func set_persistence_tracking_enabled(enabled: bool) -> void: - if _persistence_tracking_enabled == enabled: - return - if _persistence_tracking_enabled: - _store_persistent_state() - _persistence_tracking_enabled = enabled - - -func restore_persistent_state( - weather: Weather, - seconds_remaining: float, -) -> bool: - if not is_valid_persistent_state(weather, seconds_remaining): - return false - _persistent_weather = weather - _persistent_seconds_remaining = seconds_remaining - _has_persistent_state = true - if _persistence_tracking_enabled: - _set_weather( - _persistent_weather, - _persistent_seconds_remaining, - true, - ) - return true - - -func reset_persistent_state() -> void: - _has_persistent_state = false - _persistent_weather = DEFAULT_WEATHER - _persistent_seconds_remaining = SUNNY_DURATION_RANGE.x - if _persistence_tracking_enabled and _running_authority: - _set_weather( - DEFAULT_WEATHER, - _roll_duration(DEFAULT_WEATHER), - true, - ) - - -func has_persistent_state() -> bool: - return _has_persistent_state - - -func get_persistent_weather() -> Weather: - return _persistent_weather - - -func get_persistent_seconds_remaining() -> float: - return _persistent_seconds_remaining - - func get_weather() -> Weather: return _weather @@ -168,95 +89,10 @@ func get_weather_name() -> String: return weather_name(_weather) -func configure_daily_plan( - plan_id: String, - schedule: Array, - world_time: WorldTimeService, -) -> bool: - if ( - plan_id.is_empty() - or not is_valid_daily_plan_schedule(schedule) - or world_time == null - ): - return false - _daily_plan_id = plan_id - _daily_schedule.clear() - for value: Variant in schedule: - _daily_schedule.append((value as Dictionary).duplicate(true)) - _world_time = world_time - if _running_authority: - _update_scheduled_weather(true) - return true - - -func clear_daily_plan() -> void: - _daily_plan_id = "" - _daily_schedule.clear() - _world_time = null - - -func get_daily_plan_id() -> String: - return _daily_plan_id - - static func is_valid_weather(value: int) -> bool: return value >= Weather.SUNNY and value <= Weather.FOGGY -static func is_valid_persistent_state( - weather: Weather, - seconds_remaining: float, -) -> bool: - return ( - is_valid_weather(int(weather)) - and is_finite(seconds_remaining) - and seconds_remaining >= 0.0 - and seconds_remaining <= MAX_PERSISTED_SECONDS - ) - - -static func is_valid_daily_plan_schedule(value: Variant) -> bool: - if typeof(value) != TYPE_ARRAY: - return false - var schedule: Array = value - if schedule.size() != DAILY_PLAN_SEGMENT_COUNT: - return false - for entry_value: Variant in schedule: - if typeof(entry_value) != TYPE_DICTIONARY: - return false - var entry: Dictionary = entry_value - if ( - typeof(entry.get("start_hour")) not in [TYPE_FLOAT, TYPE_INT] - or not is_finite(float(entry.get("start_hour", -1.0))) - or float(entry.get("start_hour", -1.0)) < 0.0 - or float(entry.get("start_hour", -1.0)) >= 24.0 - or not _is_bounded_integer( - entry.get("weather"), Weather.SUNNY, Weather.FOGGY - ) - or not is_valid_weather(int(entry.get("weather", -1))) - ): - return false - return true - - -static func _is_bounded_integer( - value: Variant, - minimum: int, - maximum: int, -) -> bool: - if typeof(value) == TYPE_INT: - return int(value) >= minimum and int(value) <= maximum - if typeof(value) != TYPE_FLOAT: - return false - var number: float = float(value) - return ( - is_finite(number) - and number >= float(minimum) - and number <= float(maximum) - and is_equal_approx(number, round(number)) - ) - - static func weather_name(weather: Weather) -> String: match weather: Weather.SUNNY: @@ -278,18 +114,10 @@ func _set_weather( var changed: bool = weather != _weather _weather = weather _seconds_remaining = maxf(seconds_remaining, 0.0) - if _persistence_tracking_enabled: - _store_persistent_state() if force_emit or changed: weather_changed.emit(_weather, _seconds_remaining) -func _store_persistent_state() -> void: - _persistent_weather = _weather - _persistent_seconds_remaining = _seconds_remaining - _has_persistent_state = true - - func _choose_next_weather() -> Weather: match _weather: Weather.SUNNY: @@ -322,26 +150,3 @@ func _duration_range(weather: Weather) -> Vector2: Weather.FOGGY: return FOGGY_DURATION_RANGE return SUNNY_DURATION_RANGE - - -func _update_scheduled_weather(force_emit: bool) -> void: - if _daily_schedule.is_empty() or _world_time == null: - return - var elapsed_hours: float = fposmod( - _world_time.get_time_hours() - WorldTimeService.DAY_START_HOUR, - WorldTimeService.HOURS_PER_DAY, - ) - var segment_hours: float = DAILY_PLAN_SEGMENT_HOURS - var segment_index: int = clampi( - floori(elapsed_hours / segment_hours), - 0, - _daily_schedule.size() - 1, - ) - var entry: Dictionary = _daily_schedule[segment_index] - var weather: Weather = int(entry.get("weather", DEFAULT_WEATHER)) as Weather - var next_boundary: float = float(segment_index + 1) * segment_hours - var hours_remaining: float = maxf(next_boundary - elapsed_hours, 0.0) - var seconds_remaining: float = ( - hours_remaining / WorldTimeService.HOURS_PER_REAL_SECOND - ) - _set_weather(weather, seconds_remaining, force_emit)