Compare commits
No commits in common. "26fbd411328fdb5dca9d370bfd5548db30f8c5e6" and "c806e7ca6f125725ff74d9cf57fb447be928a42e" have entirely different histories.
26fbd41132
...
c806e7ca6f
49 changed files with 220 additions and 3385 deletions
|
|
@ -16,10 +16,6 @@ const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
|
||||||
# multipliers later without changing catch serialization or tier identity.
|
# 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 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]
|
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 = [
|
const DISPLAY_NAMES: PackedStringArray = [
|
||||||
"boring",
|
"boring",
|
||||||
"average",
|
"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(
|
static func roll(
|
||||||
rng: RandomNumberGenerator,
|
rng: RandomNumberGenerator,
|
||||||
weight_multipliers: Array[float] = [],
|
weight_multipliers: Array[float] = [],
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,6 @@ barrier_health_max = 7
|
||||||
first_barrier_margin = 0.14
|
first_barrier_margin = 0.14
|
||||||
final_barrier_margin = 0.1
|
final_barrier_margin = 0.1
|
||||||
minimum_barrier_spacing = 0.14
|
minimum_barrier_spacing = 0.14
|
||||||
|
chase_start_delay = 1.0
|
||||||
|
chase_start_offset = 0.16
|
||||||
|
chase_speed = 0.145
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,6 @@ barrier_health_max = 8
|
||||||
first_barrier_margin = 0.13
|
first_barrier_margin = 0.13
|
||||||
final_barrier_margin = 0.09
|
final_barrier_margin = 0.09
|
||||||
minimum_barrier_spacing = 0.14
|
minimum_barrier_spacing = 0.14
|
||||||
|
chase_start_delay = 1.1
|
||||||
|
chase_start_offset = 0.18
|
||||||
|
chase_speed = 0.15
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ extends Node
|
||||||
const CatchDifficultyProfileType = preload(
|
const CatchDifficultyProfileType = preload(
|
||||||
"res://fishing/catch_difficulty_profile.gd"
|
"res://fishing/catch_difficulty_profile.gd"
|
||||||
)
|
)
|
||||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
|
||||||
|
|
||||||
signal encounter_updated(
|
signal encounter_updated(
|
||||||
progress: float,
|
progress: float,
|
||||||
|
|
@ -43,13 +42,6 @@ class Barrier:
|
||||||
maximum_health = barrier_health
|
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_category("Accessibility")
|
||||||
@export var auto_click_enabled: bool = false
|
@export var auto_click_enabled: bool = false
|
||||||
@export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20
|
@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_input_held: bool = false
|
||||||
var _reel_speed: float = 0.0
|
var _reel_speed: float = 0.0
|
||||||
var _click_power: int = 1
|
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_delay_remaining: float = 0.0
|
||||||
|
var _chase_speed: float = 0.0
|
||||||
var _failure_epsilon: float = 0.0001
|
var _failure_epsilon: float = 0.0001
|
||||||
var _auto_click_accumulator: float = 0.0
|
var _auto_click_accumulator: float = 0.0
|
||||||
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||||
|
|
@ -110,7 +103,6 @@ func start_encounter(
|
||||||
profile: CatchDifficultyProfileType,
|
profile: CatchDifficultyProfileType,
|
||||||
reel_speed: float,
|
reel_speed: float,
|
||||||
click_power: int,
|
click_power: int,
|
||||||
fish_quality: int = FishQualityType.Tier.BORING,
|
|
||||||
) -> void:
|
) -> void:
|
||||||
reset()
|
reset()
|
||||||
if profile == null:
|
if profile == null:
|
||||||
|
|
@ -119,13 +111,10 @@ func start_encounter(
|
||||||
|
|
||||||
_reel_speed = maxf(reel_speed, 0.0)
|
_reel_speed = maxf(reel_speed, 0.0)
|
||||||
_click_power = maxi(click_power, 1)
|
_click_power = maxi(click_power, 1)
|
||||||
_fish_quality = (
|
_chase_start_delay = maxf(profile.chase_start_delay, 0.0)
|
||||||
fish_quality
|
_chase_delay_remaining = _chase_start_delay
|
||||||
if FishQualityType.is_valid(fish_quality)
|
chase_progress = -maxf(profile.chase_start_offset, 0.01)
|
||||||
else FishQualityType.Tier.BORING
|
_chase_speed = maxf(profile.chase_speed, 0.0)
|
||||||
)
|
|
||||||
_chase_delay_remaining = CHASE_START_DELAY
|
|
||||||
chase_progress = -CHASE_START_OFFSET
|
|
||||||
_seed_encounter_rng()
|
_seed_encounter_rng()
|
||||||
_generate_barriers(profile)
|
_generate_barriers(profile)
|
||||||
progress = 0.0
|
progress = 0.0
|
||||||
|
|
@ -138,13 +127,12 @@ func start_authoritative_encounter(
|
||||||
reel_speed: float,
|
reel_speed: float,
|
||||||
click_power: int,
|
click_power: int,
|
||||||
seed: int,
|
seed: int,
|
||||||
fish_quality: int = FishQualityType.Tier.BORING,
|
|
||||||
) -> void:
|
) -> void:
|
||||||
var previous_test_mode: bool = use_deterministic_test_seed
|
var previous_test_mode: bool = use_deterministic_test_seed
|
||||||
var previous_seed: int = deterministic_test_seed
|
var previous_seed: int = deterministic_test_seed
|
||||||
use_deterministic_test_seed = true
|
use_deterministic_test_seed = true
|
||||||
deterministic_test_seed = seed
|
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
|
use_deterministic_test_seed = previous_test_mode
|
||||||
deterministic_test_seed = previous_seed
|
deterministic_test_seed = previous_seed
|
||||||
|
|
||||||
|
|
@ -180,8 +168,9 @@ func reset() -> void:
|
||||||
_reel_input_held = false
|
_reel_input_held = false
|
||||||
_reel_speed = 0.0
|
_reel_speed = 0.0
|
||||||
_click_power = 1
|
_click_power = 1
|
||||||
_fish_quality = FishQualityType.Tier.BORING
|
_chase_start_delay = 0.0
|
||||||
_chase_delay_remaining = 0.0
|
_chase_delay_remaining = 0.0
|
||||||
|
_chase_speed = 0.0
|
||||||
_auto_click_accumulator = 0.0
|
_auto_click_accumulator = 0.0
|
||||||
emit_signal(
|
emit_signal(
|
||||||
"encounter_updated",
|
"encounter_updated",
|
||||||
|
|
@ -226,7 +215,7 @@ func _update_chase(delta: float) -> void:
|
||||||
if active_delta <= 0.0:
|
if active_delta <= 0.0:
|
||||||
return
|
return
|
||||||
chase_progress = minf(
|
chase_progress = minf(
|
||||||
chase_progress + CHASE_SPEED * active_delta,
|
chase_progress + _chase_speed * active_delta,
|
||||||
1.0
|
1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -313,14 +302,7 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
|
||||||
+ spacing * float(barrier_index)
|
+ spacing * float(barrier_index)
|
||||||
+ random_offsets[barrier_index] * random_slack
|
+ random_offsets[barrier_index] * random_slack
|
||||||
)
|
)
|
||||||
var base_health: int = _rng.randi_range(
|
var health: int = _rng.randi_range(health_range.x, health_range.y)
|
||||||
health_range.x,
|
|
||||||
health_range.y,
|
|
||||||
)
|
|
||||||
var health: int = FishQualityType.apply_barrier_health(
|
|
||||||
base_health,
|
|
||||||
_fish_quality,
|
|
||||||
)
|
|
||||||
_barriers.append(Barrier.new(position, health))
|
_barriers.append(Barrier.new(position, health))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 first_barrier_margin: float = 0.15
|
||||||
@export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10
|
@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.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:
|
func get_barrier_count_range() -> Vector2i:
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,6 @@ barrier_health_max = 7
|
||||||
first_barrier_margin = 0.15
|
first_barrier_margin = 0.15
|
||||||
final_barrier_margin = 0.1
|
final_barrier_margin = 0.1
|
||||||
minimum_barrier_spacing = 0.15
|
minimum_barrier_spacing = 0.15
|
||||||
|
chase_start_delay = 1.0
|
||||||
|
chase_start_offset = 0.15
|
||||||
|
chase_speed = 0.14
|
||||||
|
|
|
||||||
|
|
@ -919,10 +919,6 @@ func _activate_bite() -> void:
|
||||||
state = FishingState.FIGHTING
|
state = FishingState.FIGHTING
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_withdrawal_input_held = false
|
_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()
|
bite_activated.emit()
|
||||||
status_changed.emit("fish on!")
|
status_changed.emit("fish on!")
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
|
|
@ -930,8 +926,7 @@ func _activate_bite() -> void:
|
||||||
_catch_controller.start_encounter(
|
_catch_controller.start_encounter(
|
||||||
_selected_fish.catch_profile,
|
_selected_fish.catch_profile,
|
||||||
_get_effective_reel_speed(),
|
_get_effective_reel_speed(),
|
||||||
_get_effective_barrier_damage(),
|
_get_effective_barrier_damage()
|
||||||
_pending_catch.quality,
|
|
||||||
)
|
)
|
||||||
_catch_controller.set_reel_input(
|
_catch_controller.set_reel_input(
|
||||||
Input.is_action_pressed("fish_primary")
|
Input.is_action_pressed("fish_primary")
|
||||||
|
|
@ -998,6 +993,7 @@ func _on_catch_completed() -> void:
|
||||||
_cancel_attempt()
|
_cancel_attempt()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_pending_catch = _fish_selector.create_catch(_selected_fish)
|
||||||
if _pending_catch == null or not _pending_catch.is_valid():
|
if _pending_catch == null or not _pending_catch.is_valid():
|
||||||
_cancel_attempt()
|
_cancel_attempt()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c52vqhdties3o
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://daqtje84gqgu0
|
|
||||||
26
main/main.gd
26
main/main.gd
|
|
@ -88,8 +88,6 @@ const WorldWeatherServiceType = preload("res://world/world_weather_service.gd")
|
||||||
const NetworkWorldWeatherServiceType = preload(
|
const NetworkWorldWeatherServiceType = preload(
|
||||||
"res://network/network_world_weather_service.gd"
|
"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 TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
const PLAYER_MENU_PATTERN_SCALE: float = 0.85
|
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 = (
|
@onready var _network_world_weather: NetworkWorldWeatherServiceType = (
|
||||||
%NetworkWorldWeatherService
|
%NetworkWorldWeatherService
|
||||||
)
|
)
|
||||||
@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService
|
|
||||||
@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService
|
|
||||||
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
|
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
|
||||||
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
|
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
|
||||||
@onready var _network_profile: NetworkProfilePreferencesType = (
|
@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_time.setup(_network_session, _world_time)
|
||||||
_network_world_weather.setup(_network_session, _world_weather)
|
_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)
|
_identity_backups.setup(_data_root, _player_identity, _host_identity)
|
||||||
_network_profile_service.setup(
|
_network_profile_service.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
|
|
@ -373,10 +359,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_player.art_unlocks,
|
_player.art_unlocks,
|
||||||
_player.experience,
|
_player.experience,
|
||||||
_world_time,
|
_world_time,
|
||||||
_world_weather,
|
|
||||||
_player_jobs,
|
|
||||||
)
|
)
|
||||||
_player_jobs.set_save_manager(_save_manager)
|
|
||||||
_save_manager.set_autosave_enabled(false)
|
_save_manager.set_autosave_enabled(false)
|
||||||
_asset_reservations.setup(
|
_asset_reservations.setup(
|
||||||
_player.wallet, _player.inventory, _player.bag, item_catalog
|
_player.wallet, _player.inventory, _player.bag, item_catalog
|
||||||
|
|
@ -453,7 +436,6 @@ func _initialize_after_data_root() -> void:
|
||||||
sale_buyers,
|
sale_buyers,
|
||||||
_asset_reservations
|
_asset_reservations
|
||||||
)
|
)
|
||||||
_player_jobs.bind_authoritative_services(_network_fishing, _network_sale)
|
|
||||||
_network_shop.setup(
|
_network_shop.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -518,7 +500,6 @@ func _initialize_after_data_root() -> void:
|
||||||
_player.art_unlocks,
|
_player.art_unlocks,
|
||||||
_world_time,
|
_world_time,
|
||||||
_world_weather,
|
_world_weather,
|
||||||
_player_jobs,
|
|
||||||
)
|
)
|
||||||
_game_ui.setup_data_and_identity(
|
_game_ui.setup_data_and_identity(
|
||||||
_data_root,
|
_data_root,
|
||||||
|
|
@ -1036,10 +1017,8 @@ func _set_gameplay_active(active: bool) -> void:
|
||||||
_game_ui.set_gameplay_ui_enabled(active)
|
_game_ui.set_gameplay_ui_enabled(active)
|
||||||
_save_manager.set_autosave_enabled(active)
|
_save_manager.set_autosave_enabled(active)
|
||||||
if active:
|
if active:
|
||||||
_player_jobs.begin_progression_session()
|
|
||||||
_refresh_active_hotbar_item()
|
_refresh_active_hotbar_item()
|
||||||
else:
|
else:
|
||||||
_player_jobs.end_progression_session()
|
|
||||||
_set_player_menu_backdrop_visible(false)
|
_set_player_menu_backdrop_visible(false)
|
||||||
_set_shop_backdrop_visible(false)
|
_set_shop_backdrop_visible(false)
|
||||||
|
|
||||||
|
|
@ -1162,10 +1141,7 @@ func _prepare_private_host() -> bool:
|
||||||
NetworkSessionType.State.SERVER_LOST,
|
NetworkSessionType.State.SERVER_LOST,
|
||||||
]:
|
]:
|
||||||
_network_session.reset_failure()
|
_network_session.reset_failure()
|
||||||
if not _network_session.start_private_host(
|
if not _network_session.start_private_host():
|
||||||
NetworkSessionType.DEFAULT_PORT,
|
|
||||||
NetworkSessionType.DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS,
|
|
||||||
):
|
|
||||||
_game_ui.get_title_screen().report_network_error(
|
_game_ui.get_title_screen().report_network_error(
|
||||||
"Could not start the private multiplayer session."
|
"Could not start the private multiplayer session."
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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://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://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://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"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||||
shader = ExtResource("40_title_water")
|
shader = ExtResource("40_title_water")
|
||||||
|
|
@ -140,14 +138,6 @@ script = ExtResource("50_world_weather")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("51_network_world_weather")
|
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]
|
[node name="PlayerDataRoot" type="Node" parent="." unique_id=281868360]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("37_data_root")
|
script = ExtResource("37_data_root")
|
||||||
|
|
|
||||||
|
|
@ -429,21 +429,11 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
||||||
return
|
return
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
||||||
attempt.encounter_seed = _new_seed()
|
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(
|
attempt.controller.start_authoritative_encounter(
|
||||||
fish.catch_profile,
|
fish.catch_profile,
|
||||||
attempt.reel_speed,
|
attempt.reel_speed,
|
||||||
attempt.barrier_damage,
|
attempt.barrier_damage,
|
||||||
attempt.encounter_seed,
|
attempt.encounter_seed
|
||||||
fish_catch.quality,
|
|
||||||
)
|
)
|
||||||
var data: Dictionary = {
|
var data: Dictionary = {
|
||||||
"attempt_id": attempt.attempt_id,
|
"attempt_id": attempt.attempt_id,
|
||||||
|
|
@ -627,10 +617,11 @@ func _on_attempt_caught(peer_id: int) -> void:
|
||||||
if fish == null:
|
if fish == null:
|
||||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||||
return
|
return
|
||||||
var fish_catch: FishCatch = FishCatchType.from_network_dict(
|
var selector := FishSelectorType.new()
|
||||||
attempt.catch_payload,
|
selector.use_deterministic_test_seed = true
|
||||||
fish,
|
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():
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
|
||||||
)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://drlyytr2o3uw8
|
|
||||||
|
|
@ -23,7 +23,6 @@ const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||||
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
||||||
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||||
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
||||||
const JOBS_CAPABILITY: String = "jobs_v1"
|
|
||||||
|
|
||||||
enum RejectionCode {
|
enum RejectionCode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -94,7 +93,6 @@ static func make_client_hello(
|
||||||
SURFACE_DRAWING_CAPABILITY,
|
SURFACE_DRAWING_CAPABILITY,
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
|
||||||
]),
|
]),
|
||||||
"cosmetic_snapshot": cosmetic_snapshot,
|
"cosmetic_snapshot": cosmetic_snapshot,
|
||||||
"identity_fingerprint": identity_fingerprint,
|
"identity_fingerprint": identity_fingerprint,
|
||||||
|
|
@ -215,7 +213,6 @@ static func make_server_hello(
|
||||||
SURFACE_DRAWING_CAPABILITY,
|
SURFACE_DRAWING_CAPABILITY,
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ class_name NetworkSession
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const DEFAULT_PORT: int = 7777
|
const DEFAULT_PORT: int = 7777
|
||||||
const DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS: int = 16
|
|
||||||
const DEFAULT_SESSION_MAX_PLAYERS: int = 8
|
const DEFAULT_SESSION_MAX_PLAYERS: int = 8
|
||||||
const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31
|
const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31
|
||||||
const CONNECTION_TIMEOUT_SECONDS: float = 10.0
|
const CONNECTION_TIMEOUT_SECONDS: float = 10.0
|
||||||
|
|
@ -98,7 +97,6 @@ var _local_appearance_snapshot: Dictionary = (
|
||||||
CharacterCustomizationCatalog.default_snapshot()
|
CharacterCustomizationCatalog.default_snapshot()
|
||||||
)
|
)
|
||||||
var _moderation_disconnect_message := ""
|
var _moderation_disconnect_message := ""
|
||||||
var _host_port: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -133,10 +131,7 @@ func setup(
|
||||||
_profile_ready = _profile_ready and _player_identity.load_or_create()
|
_profile_ready = _profile_ready and _player_identity.load_or_create()
|
||||||
|
|
||||||
|
|
||||||
func start_private_host(
|
func start_private_host(port: int = DEFAULT_PORT) -> bool:
|
||||||
port: int = DEFAULT_PORT,
|
|
||||||
port_attempts: int = 1,
|
|
||||||
) -> bool:
|
|
||||||
if (
|
if (
|
||||||
state != State.INACTIVE
|
state != State.INACTIVE
|
||||||
or not _profile_ready
|
or not _profile_ready
|
||||||
|
|
@ -148,31 +143,19 @@ func start_private_host(
|
||||||
if not _host_identity.load_or_create():
|
if not _host_identity.load_or_create():
|
||||||
_fail(_host_identity.error_message)
|
_fail(_host_identity.error_message)
|
||||||
return false
|
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.")
|
_fail("The hosting port must be from 1 to 65535.")
|
||||||
return false
|
return false
|
||||||
_operation_generation += 1
|
_operation_generation += 1
|
||||||
_set_state(State.STARTING_PRIVATE_HOST, "Starting private game...")
|
_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()
|
_replace_transport()
|
||||||
var error: Error = _transport.start_host(
|
var error: Error = _transport.start_host(
|
||||||
candidate_port,
|
port,
|
||||||
transport_max_clients,
|
transport_max_clients,
|
||||||
)
|
)
|
||||||
if error == OK:
|
if error != OK:
|
||||||
selected_port = candidate_port
|
_fail("Could not start a private game on UDP port %d." % port)
|
||||||
break
|
|
||||||
if selected_port == 0:
|
|
||||||
_fail(
|
|
||||||
"Could not start a private game on UDP ports %d–%d."
|
|
||||||
% [port, final_port]
|
|
||||||
)
|
|
||||||
return false
|
return false
|
||||||
_host_port = selected_port
|
|
||||||
var peer: MultiplayerPeer = _transport.get_multiplayer_peer()
|
var peer: MultiplayerPeer = _transport.get_multiplayer_peer()
|
||||||
peer.refuse_new_connections = true
|
peer.refuse_new_connections = true
|
||||||
multiplayer.multiplayer_peer = peer
|
multiplayer.multiplayer_peer = peer
|
||||||
|
|
@ -190,7 +173,6 @@ func start_private_host(
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
NetworkProtocol.JOBS_CAPABILITY,
|
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||||
|
|
@ -218,23 +200,12 @@ func start_private_host(
|
||||||
var host_avatar := _spawn_service.get_avatar(1)
|
var host_avatar := _spawn_service.get_avatar(1)
|
||||||
if host_avatar != null:
|
if host_avatar != null:
|
||||||
host_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
|
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)
|
host_openness_changed.emit(false)
|
||||||
_emit_peer_count()
|
_emit_peer_count()
|
||||||
return true
|
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:
|
func join_direct(endpoint_text: String) -> bool:
|
||||||
if (
|
if (
|
||||||
state != State.INACTIVE
|
state != State.INACTIVE
|
||||||
|
|
@ -312,10 +283,10 @@ func set_host_open(is_open: bool) -> bool:
|
||||||
_set_state(
|
_set_state(
|
||||||
State.OPEN_HOST if is_open else State.PRIVATE_HOST,
|
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
|
if is_open
|
||||||
else "Private game • UDP %d • %d / %d players"
|
else "Private game • %d / %d players"
|
||||||
) % [_host_port, _registry.size(), session_max_players]
|
) % [_registry.size(), session_max_players]
|
||||||
)
|
)
|
||||||
host_openness_changed.emit(is_open)
|
host_openness_changed.emit(is_open)
|
||||||
return true
|
return true
|
||||||
|
|
@ -1758,4 +1729,3 @@ func _teardown_peer() -> void:
|
||||||
_server_identity_fingerprint = ""
|
_server_identity_fingerprint = ""
|
||||||
_server_identity_public_key = ""
|
_server_identity_public_key = ""
|
||||||
_session_identity_keys.clear()
|
_session_identity_keys.clear()
|
||||||
_host_port = 0
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(true)
|
|
||||||
_world_weather.begin_authoritative_session(
|
_world_weather.begin_authoritative_session(
|
||||||
_active_session_id.hash()
|
_active_session_id.hash()
|
||||||
)
|
)
|
||||||
|
|
@ -53,7 +52,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_active_session_id = _session.get_session_id()
|
_active_session_id = _session.get_session_id()
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(false)
|
|
||||||
if _session.supports_server_capability(
|
if _session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
):
|
):
|
||||||
|
|
@ -71,7 +69,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(false)
|
|
||||||
_world_weather.end_session()
|
_world_weather.end_session()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ const FishingRodAttachmentScene = preload(
|
||||||
const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
||||||
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
||||||
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
||||||
const BASE_REEL_SPEED: float = 0.16
|
|
||||||
|
|
||||||
var appearance_snapshot: Dictionary = (
|
var appearance_snapshot: Dictionary = (
|
||||||
CharacterCustomizationCatalog.default_snapshot()
|
CharacterCustomizationCatalog.default_snapshot()
|
||||||
|
|
@ -70,7 +69,7 @@ class ShowcaseCameraSnapshot:
|
||||||
@export var local_control_enabled: bool = true
|
@export var local_control_enabled: bool = true
|
||||||
|
|
||||||
@export_category("Fishing Stats")
|
@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_range(1, 100, 1) var click_power: int = 1
|
||||||
|
|
||||||
@export_category("Showcase")
|
@export_category("Showcase")
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,8 @@ const PlayerExperienceType = preload(
|
||||||
"res://progression/player_experience.gd"
|
"res://progression/player_experience.gd"
|
||||||
)
|
)
|
||||||
const WorldTimeServiceType = preload("res://world/world_time_service.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 BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||||
const MAX_SAFE_BALANCE: int = 1000000000000
|
const MAX_SAFE_BALANCE: int = 1000000000000
|
||||||
|
|
||||||
|
|
@ -52,14 +48,6 @@ class LoadSnapshot:
|
||||||
var art_unlock_mask: int = 0
|
var art_unlock_mask: int = 0
|
||||||
var total_experience: int = 0
|
var total_experience: int = 0
|
||||||
var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
|
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
|
@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 _art_unlocks: PlayerArtUnlocksType
|
||||||
var _experience: PlayerExperienceType
|
var _experience: PlayerExperienceType
|
||||||
var _world_time: WorldTimeServiceType
|
var _world_time: WorldTimeServiceType
|
||||||
var _world_weather: WorldWeatherServiceType
|
|
||||||
var _jobs: PlayerJobServiceType
|
|
||||||
var _autosave_timer: Timer
|
var _autosave_timer: Timer
|
||||||
var _is_configured: bool = false
|
var _is_configured: bool = false
|
||||||
var _is_restoring: bool = false
|
var _is_restoring: bool = false
|
||||||
|
|
@ -122,8 +108,6 @@ func setup(
|
||||||
art_unlocks: PlayerArtUnlocksType,
|
art_unlocks: PlayerArtUnlocksType,
|
||||||
experience: PlayerExperienceType,
|
experience: PlayerExperienceType,
|
||||||
world_time: WorldTimeServiceType,
|
world_time: WorldTimeServiceType,
|
||||||
world_weather: WorldWeatherServiceType,
|
|
||||||
jobs: PlayerJobServiceType,
|
|
||||||
) -> void:
|
) -> void:
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
_collection_log = collection_log
|
_collection_log = collection_log
|
||||||
|
|
@ -137,8 +121,6 @@ func setup(
|
||||||
_art_unlocks = art_unlocks
|
_art_unlocks = art_unlocks
|
||||||
_experience = experience
|
_experience = experience
|
||||||
_world_time = world_time
|
_world_time = world_time
|
||||||
_world_weather = world_weather
|
|
||||||
_jobs = jobs
|
|
||||||
_is_configured = (
|
_is_configured = (
|
||||||
_inventory != null
|
_inventory != null
|
||||||
and _collection_log != null
|
and _collection_log != null
|
||||||
|
|
@ -152,8 +134,6 @@ func setup(
|
||||||
and _art_unlocks != null
|
and _art_unlocks != null
|
||||||
and _experience != null
|
and _experience != null
|
||||||
and _world_time != null
|
and _world_time != null
|
||||||
and _world_weather != null
|
|
||||||
and _jobs != null
|
|
||||||
)
|
)
|
||||||
if not _is_configured:
|
if not _is_configured:
|
||||||
push_error("PlayerSaveManager setup is missing required references.")
|
push_error("PlayerSaveManager setup is missing required references.")
|
||||||
|
|
@ -192,8 +172,6 @@ func setup(
|
||||||
_on_experience_changed
|
_on_experience_changed
|
||||||
):
|
):
|
||||||
_experience.experience_changed.connect(_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:
|
func load_player_data() -> bool:
|
||||||
|
|
@ -280,13 +258,6 @@ func load_player_data() -> bool:
|
||||||
var world_time_restored: bool = (
|
var world_time_restored: bool = (
|
||||||
_world_time.restore_persistent_time_hours(snapshot.world_time_hours)
|
_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
|
_is_restoring = false
|
||||||
if (
|
if (
|
||||||
not inventory_restored
|
not inventory_restored
|
||||||
|
|
@ -299,8 +270,6 @@ func load_player_data() -> bool:
|
||||||
or not art_restored
|
or not art_restored
|
||||||
or not experience_restored
|
or not experience_restored
|
||||||
or not world_time_restored
|
or not world_time_restored
|
||||||
or not world_weather_restored
|
|
||||||
or not jobs_restored
|
|
||||||
):
|
):
|
||||||
push_error("Validated player save could not be restored.")
|
push_error("Validated player save could not be restored.")
|
||||||
return false
|
return false
|
||||||
|
|
@ -536,12 +505,7 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
"experience": _experience.to_save_data(),
|
"experience": _experience.to_save_data(),
|
||||||
"world": {
|
"world": {
|
||||||
"time_hours": _world_time.get_persistent_time_hours(),
|
"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("bag")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
|
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
|
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var wallet_data: Dictionary = save_data["wallet"]
|
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 bag_data: Dictionary = save_data["bag"]
|
||||||
var hotbar_data: Dictionary = save_data["hotbar"]
|
var hotbar_data: Dictionary = save_data["hotbar"]
|
||||||
var experience_data: Dictionary = save_data["experience"]
|
var experience_data: Dictionary = save_data["experience"]
|
||||||
var jobs_data: Dictionary = save_data["jobs"]
|
|
||||||
var world_data: Dictionary = {}
|
var world_data: Dictionary = {}
|
||||||
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
||||||
world_data = save_data["world"]
|
world_data = save_data["world"]
|
||||||
|
|
@ -617,40 +579,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
)
|
)
|
||||||
if snapshot.total_experience < 0:
|
if snapshot.total_experience < 0:
|
||||||
return null
|
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"):
|
if world_data.has("time_hours"):
|
||||||
snapshot.world_time_hours = _read_world_time_hours(
|
snapshot.world_time_hours = _read_world_time_hours(
|
||||||
world_data["time_hours"]
|
world_data["time_hours"]
|
||||||
)
|
)
|
||||||
if snapshot.world_time_hours < 0.0:
|
if snapshot.world_time_hours < 0.0:
|
||||||
return null
|
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 discovered_values: Array = collection_data["discovered_fish_ids"]
|
||||||
var seen_discoveries: Dictionary[StringName, bool] = {}
|
var seen_discoveries: Dictionary[StringName, bool] = {}
|
||||||
for value: Variant in discovered_values:
|
for value: Variant in discovered_values:
|
||||||
|
|
@ -872,8 +806,6 @@ func _migrate_save(
|
||||||
migrated = _migrate_version_4_to_5(migrated)
|
migrated = _migrate_version_4_to_5(migrated)
|
||||||
5:
|
5:
|
||||||
migrated = _migrate_version_5_to_6(migrated)
|
migrated = _migrate_version_5_to_6(migrated)
|
||||||
6:
|
|
||||||
migrated = _migrate_version_6_to_7(migrated)
|
|
||||||
_:
|
_:
|
||||||
return {}
|
return {}
|
||||||
if migrated.is_empty():
|
if migrated.is_empty():
|
||||||
|
|
@ -983,15 +915,6 @@ func _migrate_version_5_to_6(data: Dictionary) -> Dictionary:
|
||||||
return migrated
|
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:
|
func _mark_dirty() -> void:
|
||||||
if (
|
if (
|
||||||
_is_restoring
|
_is_restoring
|
||||||
|
|
@ -1106,8 +1029,6 @@ func _restore_defaults() -> void:
|
||||||
_world_time.restore_persistent_time_hours(
|
_world_time.restore_persistent_time_hours(
|
||||||
WorldTimeServiceType.DEFAULT_START_HOUR
|
WorldTimeServiceType.DEFAULT_START_HOUR
|
||||||
)
|
)
|
||||||
_world_weather.reset_persistent_state()
|
|
||||||
_jobs.reset_to_defaults()
|
|
||||||
_is_restoring = false
|
_is_restoring = false
|
||||||
_is_dirty = false
|
_is_dirty = false
|
||||||
|
|
||||||
|
|
@ -1146,19 +1067,6 @@ func _read_world_time_hours(value: Variant) -> float:
|
||||||
return time_hours
|
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(
|
func _read_upgrade_level(
|
||||||
value: Variant,
|
value: Variant,
|
||||||
maximum_level: int,
|
maximum_level: int,
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ func _run() -> void:
|
||||||
|
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||||
var world_weather := (
|
|
||||||
main.get_node("%WorldWeatherService") as WorldWeatherService
|
|
||||||
)
|
|
||||||
world_time.synchronize_time(19.75)
|
world_time.synchronize_time(19.75)
|
||||||
assert(player.experience.award_experience(125))
|
assert(player.experience.award_experience(125))
|
||||||
var fish_catalog := main.get("fish_catalog") as FishPool
|
var fish_catalog := main.get("fish_catalog") as FishPool
|
||||||
|
|
@ -95,10 +92,6 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not service.is_local_showcase_visible())
|
assert(not service.is_local_showcase_visible())
|
||||||
assert(not (player.get_node("%HeldFishDisplay") as Node3D).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())
|
assert(save_manager.save_now())
|
||||||
var save_path: String = str(save_manager.get("_save_path"))
|
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"]
|
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
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(
|
assert(
|
||||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||||
== 125
|
== 125
|
||||||
)
|
)
|
||||||
assert(absf(
|
|
||||||
float((parsed as Dictionary)["world"]["time_hours"]) - 19.75
|
|
||||||
) < 0.01)
|
|
||||||
assert(
|
assert(
|
||||||
int((parsed as Dictionary)["world"]["weather"])
|
is_equal_approx(
|
||||||
== int(saved_weather)
|
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"]
|
var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"]
|
||||||
assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
|
assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
|
||||||
var saved_masks: Dictionary = (
|
var saved_masks: Dictionary = (
|
||||||
|
|
@ -142,18 +127,9 @@ func _run() -> void:
|
||||||
assert(player.hotbar.clear_slot(1))
|
assert(player.hotbar.clear_slot(1))
|
||||||
assert(player.experience.restore_total_experience(0))
|
assert(player.experience.restore_total_experience(0))
|
||||||
world_time.synchronize_time(8.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(save_manager.load_player_data())
|
||||||
assert(player.experience.get_total_experience() == 125)
|
assert(player.experience.get_total_experience() == 125)
|
||||||
assert(absf(world_time.get_time_hours() - 19.75) < 0.01)
|
assert(is_equal_approx(world_time.get_time_hours(), 19.75))
|
||||||
assert(world_weather.get_weather() == saved_weather)
|
|
||||||
assert(absf(
|
|
||||||
world_weather.get_seconds_remaining() - saved_weather_seconds
|
|
||||||
) < 2.0)
|
|
||||||
assert(player.experience.get_level() == 2)
|
assert(player.experience.get_level() == 2)
|
||||||
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||||
assert(player.hotbar.get_selected_slot() == 1)
|
assert(player.hotbar.get_selected_slot() == 1)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ func _initialize() -> void:
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
_validate_tiers_and_distribution()
|
_validate_tiers_and_distribution()
|
||||||
_validate_barrier_challenge_curve()
|
|
||||||
_validate_fight_pacing_and_reel_upgrades()
|
|
||||||
_validate_catch_round_trip_and_sale()
|
_validate_catch_round_trip_and_sale()
|
||||||
_validate_mail_round_trip()
|
_validate_mail_round_trip()
|
||||||
_validate_collection_mastery()
|
_validate_collection_mastery()
|
||||||
|
|
@ -74,115 +72,6 @@ func _validate_tiers_and_distribution() -> void:
|
||||||
assert(absf(observed - expected) < 0.015)
|
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:
|
func _validate_catch_round_trip_and_sale() -> void:
|
||||||
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||||
assert(fish != null)
|
assert(fish != null)
|
||||||
|
|
@ -353,7 +242,7 @@ func _validate_version_four_migration() -> void:
|
||||||
version_four,
|
version_four,
|
||||||
4,
|
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(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||||
assert(
|
assert(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
|
|
@ -370,7 +259,4 @@ func _validate_version_four_migration() -> void:
|
||||||
var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
|
var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
|
||||||
assert(int(masks["bluegill"]) == boring_bit)
|
assert(int(masks["bluegill"]) == boring_bit)
|
||||||
assert(int(masks["carp"]) == boring_bit)
|
assert(int(masks["carp"]) == boring_bit)
|
||||||
assert(
|
|
||||||
PlayerJobService.validate_save_data(migrated.get("jobs", {}))
|
|
||||||
)
|
|
||||||
manager.queue_free()
|
manager.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -97,60 +97,6 @@ func _run() -> void:
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
assert(not bobber.visible)
|
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")
|
print("Fishing authority validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://c4y0pmxk2bhmu
|
|
||||||
|
|
@ -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 {}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://b2grposd30lkw
|
|
||||||
|
|
@ -143,14 +143,11 @@ func _validate_save_migration() -> void:
|
||||||
version_five,
|
version_five,
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
assert(int(migrated.get("save_version", -1)) == 7)
|
assert(int(migrated.get("save_version", -1)) == 6)
|
||||||
var experience_data: Dictionary = migrated.get("experience", {})
|
var experience_data: Dictionary = migrated.get("experience", {})
|
||||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||||
var world_data: Dictionary = migrated.get("world", {})
|
var world_data: Dictionary = migrated.get("world", {})
|
||||||
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
|
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()
|
manager.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,6 @@ func _run_host() -> void:
|
||||||
world_weather.apply_authoritative_snapshot(
|
world_weather.apply_authoritative_snapshot(
|
||||||
WorldWeatherService.Weather.RAINY, 300.0
|
WorldWeatherService.Weather.RAINY, 300.0
|
||||||
)
|
)
|
||||||
assert(
|
|
||||||
world_weather.get_persistent_weather()
|
|
||||||
== WorldWeatherService.Weather.RAINY
|
|
||||||
)
|
|
||||||
assert(session.set_host_open(true))
|
assert(session.set_host_open(true))
|
||||||
|
|
||||||
var remote_peer_id: int = 0
|
var remote_peer_id: int = 0
|
||||||
|
|
@ -69,10 +65,6 @@ func _run_host() -> void:
|
||||||
world_weather.apply_authoritative_snapshot(
|
world_weather.apply_authoritative_snapshot(
|
||||||
WorldWeatherService.Weather.FOGGY, 300.0
|
WorldWeatherService.Weather.FOGGY, 300.0
|
||||||
)
|
)
|
||||||
assert(
|
|
||||||
world_weather.get_persistent_weather()
|
|
||||||
== WorldWeatherService.Weather.FOGGY
|
|
||||||
)
|
|
||||||
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < disconnect_deadline
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
|
@ -113,10 +105,6 @@ func _run_client() -> void:
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
))
|
))
|
||||||
assert(world_time.restore_persistent_time_hours(15.25))
|
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
|
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
|
|
@ -174,14 +162,6 @@ func _run_client() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(world_weather.is_foggy())
|
assert(world_weather.is_foggy())
|
||||||
assert(weather_icon.get_weather() == WorldWeatherService.Weather.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)
|
chat_ui.call("set_dock_right", true)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(clock_panel.position.x > 1000.0)
|
assert(clock_panel.position.x > 1000.0)
|
||||||
|
|
|
||||||
|
|
@ -76,20 +76,6 @@ func _validate_clock_boundaries_and_duration() -> void:
|
||||||
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
||||||
assert(not clock.is_night_period())
|
assert(not clock.is_night_period())
|
||||||
assert(clock.is_transition())
|
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()
|
clock.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ func _initialize() -> void:
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
_validate_weather_scheduler()
|
_validate_weather_scheduler()
|
||||||
_validate_weather_persistence()
|
|
||||||
_validate_snapshot_bounds()
|
_validate_snapshot_bounds()
|
||||||
_validate_fishing_weather_seams()
|
_validate_fishing_weather_seams()
|
||||||
_validate_fishing_spot_context()
|
_validate_fishing_spot_context()
|
||||||
|
|
@ -74,51 +73,6 @@ func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
|
||||||
return false
|
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:
|
func _validate_snapshot_bounds() -> void:
|
||||||
assert(NetworkWorldWeatherServiceType.validate_snapshot({
|
assert(NetworkWorldWeatherServiceType.validate_snapshot({
|
||||||
"session_id": "session",
|
"session_id": "session",
|
||||||
|
|
@ -233,22 +187,6 @@ func _validate_weather_presentation() -> void:
|
||||||
assert(rain != null)
|
assert(rain != null)
|
||||||
assert(rain.emitting)
|
assert(rain.emitting)
|
||||||
assert(rain.amount_ratio > 0.99)
|
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(
|
visuals.apply_weather_immediately(
|
||||||
WorldWeatherServiceType.Weather.SUNNY
|
WorldWeatherServiceType.Weather.SUNNY
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
||||||
|
|
||||||
@onready var _status_label: Label = %StatusLabel
|
@onready var _status_label: Label = %StatusLabel
|
||||||
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
|
@onready var _gameplay_transient_hud: Control = %GameplayTransientHUD
|
||||||
@onready var _experience_presentation: Control = %ExperiencePresentation
|
|
||||||
@onready var _catch_track: Control = %CatchTrack
|
@onready var _catch_track: Control = %CatchTrack
|
||||||
@onready var _green_catch_progress: ProgressBar = %GreenCatchProgress
|
@onready var _green_catch_progress: ProgressBar = %GreenCatchProgress
|
||||||
@onready var _red_chase_progress: ProgressBar = %RedChaseProgress
|
@onready var _red_chase_progress: ProgressBar = %RedChaseProgress
|
||||||
|
|
@ -118,15 +117,6 @@ var _experience_panel_rest_y: float = 18.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
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)
|
_emote_radial_menu.emote_selected.connect(_on_emote_selected)
|
||||||
_chat_ui.text_entry_ownership_changed.connect(
|
_chat_ui.text_entry_ownership_changed.connect(
|
||||||
_on_chat_text_entry_ownership_changed
|
_on_chat_text_entry_ownership_changed
|
||||||
|
|
@ -181,7 +171,6 @@ func setup(
|
||||||
art_unlocks: PlayerArtUnlocks,
|
art_unlocks: PlayerArtUnlocks,
|
||||||
world_time: WorldTimeServiceType,
|
world_time: WorldTimeServiceType,
|
||||||
world_weather: WorldWeatherServiceType,
|
world_weather: WorldWeatherServiceType,
|
||||||
player_jobs: PlayerJobService,
|
|
||||||
) -> void:
|
) -> void:
|
||||||
_player = player
|
_player = player
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
|
|
@ -222,7 +211,6 @@ func setup(
|
||||||
player,
|
player,
|
||||||
inventory,
|
inventory,
|
||||||
collection_log,
|
collection_log,
|
||||||
experience,
|
|
||||||
wallet,
|
wallet,
|
||||||
sale_service,
|
sale_service,
|
||||||
default_buyer,
|
default_buyer,
|
||||||
|
|
@ -238,8 +226,6 @@ func setup(
|
||||||
reservations,
|
reservations,
|
||||||
network_profile_service,
|
network_profile_service,
|
||||||
network_player_list,
|
network_player_list,
|
||||||
player_jobs,
|
|
||||||
world_time,
|
|
||||||
)
|
)
|
||||||
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
|
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot, inventory)
|
||||||
_fishing_shop.setup(
|
_fishing_shop.setup(
|
||||||
|
|
@ -400,7 +386,6 @@ func get_pause_menu() -> PauseMenuType:
|
||||||
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_gameplay_ui_enabled = enabled
|
_gameplay_ui_enabled = enabled
|
||||||
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
_gameplay_transient_hud.visible = enabled and not _player_menu_open
|
||||||
_experience_presentation.visible = enabled
|
|
||||||
_refresh_chat_availability()
|
_refresh_chat_availability()
|
||||||
if not enabled:
|
if not enabled:
|
||||||
if _surface_drawing != null:
|
if _surface_drawing != null:
|
||||||
|
|
@ -657,7 +642,6 @@ func _on_experience_awarded(
|
||||||
"previous_level": previous_level,
|
"previous_level": previous_level,
|
||||||
"new_level": new_level,
|
"new_level": new_level,
|
||||||
})
|
})
|
||||||
_start_next_experience_animation()
|
|
||||||
|
|
||||||
|
|
||||||
func _start_next_experience_animation() -> void:
|
func _start_next_experience_animation() -> void:
|
||||||
|
|
@ -801,13 +785,6 @@ func _update_experience_progress(total_experience: int) -> void:
|
||||||
func _update_experience_bubble_position() -> void:
|
func _update_experience_bubble_position() -> void:
|
||||||
if not _experience_animation_active or _player == null:
|
if not _experience_animation_active or _player == null:
|
||||||
return
|
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 camera: Camera3D = _player.get_gameplay_camera()
|
||||||
var anchor_position: Vector3 = _player.get_chat_anchor_position()
|
var anchor_position: Vector3 = _player.get_chat_anchor_position()
|
||||||
if camera == null or camera.is_position_behind(anchor_position):
|
if camera == null or camera.is_position_behind(anchor_position):
|
||||||
|
|
|
||||||
|
|
@ -80,18 +80,7 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
||||||
[node name="ExperiencePresentation" type="Control" parent="UIRoot/CanonicalStage"]
|
[node name="ExperienceProgressPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
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"]
|
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
z_index = 63
|
z_index = 63
|
||||||
|
|
@ -107,40 +96,40 @@ mouse_filter = 2
|
||||||
theme = ExtResource("3_theme")
|
theme = ExtResource("3_theme")
|
||||||
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
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
|
layout_mode = 2
|
||||||
theme_override_constants/margin_left = 14
|
theme_override_constants/margin_left = 14
|
||||||
theme_override_constants/margin_top = 7
|
theme_override_constants/margin_top = 7
|
||||||
theme_override_constants/margin_right = 14
|
theme_override_constants/margin_right = 14
|
||||||
theme_override_constants/margin_bottom = 9
|
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
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 4
|
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
|
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
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "level 1"
|
text = "level 1"
|
||||||
theme_override_colors/font_color = Color(0.95, 0.98, 1, 1)
|
theme_override_colors/font_color = Color(0.95, 0.98, 1, 1)
|
||||||
theme_override_font_sizes/font_size = 16
|
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
|
layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
mouse_filter = 2
|
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
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "+0 xp"
|
text = "+0 xp"
|
||||||
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
||||||
theme_override_font_sizes/font_size = 16
|
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
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 20)
|
custom_minimum_size = Vector2(0, 20)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
@ -150,7 +139,7 @@ theme_override_styles/fill = SubResource("StyleBox_experience_fill")
|
||||||
value = 0.0
|
value = 0.0
|
||||||
show_percentage = false
|
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
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
z_index = 63
|
z_index = 63
|
||||||
|
|
@ -160,14 +149,14 @@ mouse_filter = 2
|
||||||
theme = ExtResource("3_theme")
|
theme = ExtResource("3_theme")
|
||||||
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
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
|
layout_mode = 2
|
||||||
theme_override_constants/margin_left = 10
|
theme_override_constants/margin_left = 10
|
||||||
theme_override_constants/margin_top = 5
|
theme_override_constants/margin_top = 5
|
||||||
theme_override_constants/margin_right = 10
|
theme_override_constants/margin_right = 10
|
||||||
theme_override_constants/margin_bottom = 5
|
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
|
unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "+0 xp!"
|
text = "+0 xp!"
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,17 @@ func set_interactive(value: bool) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> 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()
|
var root := Control.new()
|
||||||
margin.add_child(root)
|
margin.add_child(root)
|
||||||
_inbox = _build_inbox()
|
_inbox = _build_inbox()
|
||||||
|
|
@ -123,8 +133,8 @@ func _build_ui() -> void:
|
||||||
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
root.add_child(page)
|
root.add_child(page)
|
||||||
_status = Label.new()
|
_status = Label.new()
|
||||||
_status.position = Vector2(18, 408)
|
_status.position = Vector2(18, 448)
|
||||||
_status.size = Vector2(1024, 28)
|
_status.size = Vector2(900, 28)
|
||||||
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_status.add_theme_color_override(
|
_status.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
|
|
@ -137,7 +147,7 @@ func _build_ui() -> void:
|
||||||
func _build_inbox() -> Control:
|
func _build_inbox() -> Control:
|
||||||
var page := Control.new()
|
var page := Control.new()
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "mail"
|
title.text = "Mail"
|
||||||
title.position = Vector2(12, 0)
|
title.position = Vector2(12, 0)
|
||||||
title.size = Vector2(500, 42)
|
title.size = Vector2(500, 42)
|
||||||
title.add_theme_font_size_override("font_size", 30)
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
|
|
@ -160,16 +170,16 @@ func _build_inbox() -> Control:
|
||||||
page.add_child(archive_view)
|
page.add_child(archive_view)
|
||||||
var scroll := ScrollContainer.new()
|
var scroll := ScrollContainer.new()
|
||||||
scroll.position = Vector2(12, 62)
|
scroll.position = Vector2(12, 62)
|
||||||
scroll.size = Vector2(1036, 334)
|
scroll.size = Vector2(958, 374)
|
||||||
page.add_child(scroll)
|
page.add_child(scroll)
|
||||||
_inbox_list = VBoxContainer.new()
|
_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)
|
_inbox_list.add_theme_constant_override("separation", 8)
|
||||||
scroll.add_child(_inbox_list)
|
scroll.add_child(_inbox_list)
|
||||||
_empty_label = Label.new()
|
_empty_label = Label.new()
|
||||||
_empty_label.text = "No letters yet."
|
_empty_label.text = "No letters yet."
|
||||||
_empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
_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)
|
_inbox_list.add_child(_empty_label)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
@ -412,7 +422,7 @@ func _refresh_inbox() -> void:
|
||||||
empty.text = (
|
empty.text = (
|
||||||
"No archived letters." if _showing_archive else "No letters yet."
|
"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
|
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_inbox_list.add_child(empty)
|
_inbox_list.add_child(empty)
|
||||||
return
|
return
|
||||||
|
|
@ -439,7 +449,7 @@ func _refresh_inbox() -> void:
|
||||||
first_line.left(72),
|
first_line.left(72),
|
||||||
" · gift enclosed" if gift else "",
|
" · 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.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
|
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
|
||||||
UtilityPageStyle.apply_ocean_button(button)
|
UtilityPageStyle.apply_ocean_button(button)
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ enum Mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADDRESS_FORMAT_HELP: String = (
|
const ADDRESS_FORMAT_HELP: String = (
|
||||||
"Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; "
|
"Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted."
|
||||||
+ "include the port shown by a host when it differs."
|
|
||||||
)
|
)
|
||||||
const DIRECT_WORKFLOW_HELP: String = (
|
const DIRECT_WORKFLOW_HELP: String = (
|
||||||
"%s\nJoin Now connects once; Save Server stores this address locally."
|
"%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
|
and _network_session.state != NetworkSession.State.INACTIVE
|
||||||
)
|
)
|
||||||
if _session_summary.visible:
|
if _session_summary.visible:
|
||||||
_session_summary.text = (
|
_session_summary.text = "%d / %d players" % [
|
||||||
"UDP %d • %d / %d players"
|
|
||||||
% [
|
|
||||||
_network_session.get_host_port(),
|
|
||||||
_network_session.get_player_count(),
|
_network_session.get_player_count(),
|
||||||
_network_session.get_session_max_players(),
|
_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(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if not direct:
|
if not direct:
|
||||||
if selected:
|
if selected:
|
||||||
_details.text = _format_entry_details(_selected_entry)
|
_details.text = _format_entry_details(_selected_entry)
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
theme_override_colors/font_color = Color(0.624, 0.812, 0.824, 1)
|
||||||
theme_override_font_sizes/font_size = 13
|
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
|
horizontal_alignment = 1
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,6 @@ const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
const PlayerCoolerCapacityType = preload(
|
const PlayerCoolerCapacityType = preload(
|
||||||
"res://progression/player_cooler_capacity.gd"
|
"res://progression/player_cooler_capacity.gd"
|
||||||
)
|
)
|
||||||
const PlayerExperienceType = preload(
|
|
||||||
"res://progression/player_experience.gd"
|
|
||||||
)
|
|
||||||
const FishBatchSelectionType = preload(
|
const FishBatchSelectionType = preload(
|
||||||
"res://ui/fish_batch_selection.gd"
|
"res://ui/fish_batch_selection.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -90,7 +87,6 @@ enum Section {
|
||||||
BAG,
|
BAG,
|
||||||
TACKLE_BOX,
|
TACKLE_BOX,
|
||||||
LOGBOOK,
|
LOGBOOK,
|
||||||
NET,
|
|
||||||
MAIL,
|
MAIL,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
PLAYERS,
|
PLAYERS,
|
||||||
|
|
@ -201,7 +197,6 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
|
||||||
@onready var _bag_sprite_detail_data: Label = %BagSpriteDetailData
|
@onready var _bag_sprite_detail_data: Label = %BagSpriteDetailData
|
||||||
@onready var _logbook_page: Control = %LogbookPage
|
@onready var _logbook_page: Control = %LogbookPage
|
||||||
@onready var _catalog_logbook: LogbookPage = %CatalogLogbook
|
@onready var _catalog_logbook: LogbookPage = %CatalogLogbook
|
||||||
@onready var _the_net_page: TheNetPage = %TheNetPage
|
|
||||||
@onready var _mail_page: MailPage = %MailPage
|
@onready var _mail_page: MailPage = %MailPage
|
||||||
@onready var _profile_page: ProfilePage = %ProfilePage
|
@onready var _profile_page: ProfilePage = %ProfilePage
|
||||||
@onready var _players_page: PlayersPage = %PlayersPage
|
@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 _logbook_page_status: BubbleStatusBubbleType = %LogbookPageStatus
|
||||||
@onready var _inventory_tab: BubbleButtonType = %InventoryTab
|
@onready var _inventory_tab: BubbleButtonType = %InventoryTab
|
||||||
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
|
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
|
||||||
@onready var _the_net_tab: BubbleButtonType = %TheNetTab
|
|
||||||
@onready var _mail_tab: BubbleButtonType = %MailTab
|
@onready var _mail_tab: BubbleButtonType = %MailTab
|
||||||
@onready var _profile_tab: BubbleButtonType = %ProfileTab
|
@onready var _profile_tab: BubbleButtonType = %ProfileTab
|
||||||
@onready var _players_tab: BubbleButtonType = %PlayersTab
|
@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 _bag_rest_position: Vector2 = Vector2.ZERO
|
||||||
var _tackle_rest_position: Vector2 = Vector2.ZERO
|
var _tackle_rest_position: Vector2 = Vector2.ZERO
|
||||||
var _logbook_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 _mail_rest_position: Vector2 = Vector2.ZERO
|
||||||
var _profile_rest_position: Vector2 = Vector2.ZERO
|
var _profile_rest_position: Vector2 = Vector2.ZERO
|
||||||
var _page_outgoing_root: Control
|
var _page_outgoing_root: Control
|
||||||
|
|
@ -367,7 +360,6 @@ func _ready() -> void:
|
||||||
_logbook_tab.pressed.connect(
|
_logbook_tab.pressed.connect(
|
||||||
_show_section.bind(Section.LOGBOOK)
|
_show_section.bind(Section.LOGBOOK)
|
||||||
)
|
)
|
||||||
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
|
|
||||||
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
||||||
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
||||||
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
|
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
|
||||||
|
|
@ -395,7 +387,6 @@ func _ready() -> void:
|
||||||
_navigation_cluster.configure([
|
_navigation_cluster.configure([
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_the_net_tab,
|
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
_players_tab,
|
_players_tab,
|
||||||
|
|
@ -486,7 +477,6 @@ func setup(
|
||||||
player: PlayerType,
|
player: PlayerType,
|
||||||
inventory: FishInventoryType,
|
inventory: FishInventoryType,
|
||||||
collection_log: CollectionLogType,
|
collection_log: CollectionLogType,
|
||||||
experience: PlayerExperienceType,
|
|
||||||
wallet: PlayerWalletType,
|
wallet: PlayerWalletType,
|
||||||
sale_service: FishSaleServiceType,
|
sale_service: FishSaleServiceType,
|
||||||
default_buyer: FishBuyerProfileType,
|
default_buyer: FishBuyerProfileType,
|
||||||
|
|
@ -502,8 +492,6 @@ func setup(
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
network_profile_service: NetworkProfileService,
|
network_profile_service: NetworkProfileService,
|
||||||
network_player_list: NetworkPlayerListService,
|
network_player_list: NetworkPlayerListService,
|
||||||
player_jobs: PlayerJobService,
|
|
||||||
world_time: WorldTimeService,
|
|
||||||
) -> void:
|
) -> void:
|
||||||
_player = player
|
_player = player
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
|
|
@ -526,10 +514,9 @@ func setup(
|
||||||
_mail_page.setup(
|
_mail_page.setup(
|
||||||
network_mail_service, reservations, inventory, wallet, bag, item_catalog
|
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)
|
_players_page.setup(network_player_list)
|
||||||
_catalog_logbook.setup(collection_log, inventory, catalog)
|
_catalog_logbook.setup(collection_log, inventory, catalog)
|
||||||
_the_net_page.setup(player_jobs, world_time)
|
|
||||||
_network_mail_service.unread_count_changed.connect(
|
_network_mail_service.unread_count_changed.connect(
|
||||||
_on_mail_unread_count_changed
|
_on_mail_unread_count_changed
|
||||||
)
|
)
|
||||||
|
|
@ -911,7 +898,6 @@ func _show_section_immediate(section: Section) -> void:
|
||||||
_tackle_box_page.visible = section == Section.TACKLE_BOX
|
_tackle_box_page.visible = section == Section.TACKLE_BOX
|
||||||
_inventory_sub_tabs.visible = _is_inventory_section(section)
|
_inventory_sub_tabs.visible = _is_inventory_section(section)
|
||||||
_logbook_page.visible = section == Section.LOGBOOK
|
_logbook_page.visible = section == Section.LOGBOOK
|
||||||
_the_net_page.visible = section == Section.NET
|
|
||||||
_mail_page.visible = section == Section.MAIL
|
_mail_page.visible = section == Section.MAIL
|
||||||
_profile_page.visible = section == Section.PROFILE
|
_profile_page.visible = section == Section.PROFILE
|
||||||
_players_page.visible = section == Section.PLAYERS
|
_players_page.visible = section == Section.PLAYERS
|
||||||
|
|
@ -936,10 +922,6 @@ func _show_section_immediate(section: Section) -> void:
|
||||||
_catalog_logbook.deactivate()
|
_catalog_logbook.deactivate()
|
||||||
else:
|
else:
|
||||||
_catalog_logbook.activate()
|
_catalog_logbook.activate()
|
||||||
if section == Section.NET:
|
|
||||||
_the_net_page.activate()
|
|
||||||
else:
|
|
||||||
_the_net_page.deactivate()
|
|
||||||
if section == Section.MAIL:
|
if section == Section.MAIL:
|
||||||
_mail_page.activate()
|
_mail_page.activate()
|
||||||
else:
|
else:
|
||||||
|
|
@ -958,7 +940,6 @@ func _show_section_immediate(section: Section) -> void:
|
||||||
_tackle_sub_tab.set_selected(section == Section.TACKLE_BOX)
|
_tackle_sub_tab.set_selected(section == Section.TACKLE_BOX)
|
||||||
_refresh_inventory_organizer_tabs()
|
_refresh_inventory_organizer_tabs()
|
||||||
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
||||||
_the_net_tab.button_pressed = section == Section.NET
|
|
||||||
_mail_tab.button_pressed = section == Section.MAIL
|
_mail_tab.button_pressed = section == Section.MAIL
|
||||||
_profile_tab.button_pressed = section == Section.PROFILE
|
_profile_tab.button_pressed = section == Section.PROFILE
|
||||||
_players_tab.button_pressed = section == Section.PLAYERS
|
_players_tab.button_pressed = section == Section.PLAYERS
|
||||||
|
|
@ -1019,8 +1000,6 @@ func _focus_current_section() -> void:
|
||||||
_tackle_sub_tab.grab_focus()
|
_tackle_sub_tab.grab_focus()
|
||||||
elif _current_section == Section.LOGBOOK:
|
elif _current_section == Section.LOGBOOK:
|
||||||
_catalog_logbook.focus_initial()
|
_catalog_logbook.focus_initial()
|
||||||
elif _current_section == Section.NET:
|
|
||||||
_the_net_page.focus_initial()
|
|
||||||
elif _current_section == Section.MAIL:
|
elif _current_section == Section.MAIL:
|
||||||
_mail_tab.grab_focus()
|
_mail_tab.grab_focus()
|
||||||
elif _current_section == Section.PLAYERS:
|
elif _current_section == Section.PLAYERS:
|
||||||
|
|
@ -1053,7 +1032,6 @@ func _configure_navigation_focus() -> void:
|
||||||
var navigation: Array[BubbleButtonType] = [
|
var navigation: Array[BubbleButtonType] = [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_the_net_tab,
|
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
_players_tab,
|
_players_tab,
|
||||||
|
|
@ -1144,8 +1122,6 @@ func _configure_active_page_focus() -> void:
|
||||||
_bait_filter.grab_focus()
|
_bait_filter.grab_focus()
|
||||||
Section.LOGBOOK:
|
Section.LOGBOOK:
|
||||||
_catalog_logbook.focus_initial()
|
_catalog_logbook.focus_initial()
|
||||||
Section.NET:
|
|
||||||
_the_net_page.focus_initial()
|
|
||||||
Section.MAIL:
|
Section.MAIL:
|
||||||
_mail_page.activate()
|
_mail_page.activate()
|
||||||
Section.PROFILE:
|
Section.PROFILE:
|
||||||
|
|
@ -1373,7 +1349,6 @@ func _apply_navigation_styles() -> void:
|
||||||
for bubble: BubbleButtonType in [
|
for bubble: BubbleButtonType in [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_the_net_tab,
|
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
_players_tab,
|
_players_tab,
|
||||||
|
|
@ -1405,7 +1380,6 @@ func _apply_navigation_selection_presentation() -> void:
|
||||||
for bubble: BubbleButtonType in [
|
for bubble: BubbleButtonType in [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_the_net_tab,
|
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
_players_tab,
|
_players_tab,
|
||||||
|
|
@ -1491,7 +1465,6 @@ func _on_mail_unread_count_changed(count: int) -> void:
|
||||||
func _set_navigation_target(section: Section) -> void:
|
func _set_navigation_target(section: Section) -> void:
|
||||||
_inventory_tab.button_pressed = _is_inventory_section(section)
|
_inventory_tab.button_pressed = _is_inventory_section(section)
|
||||||
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
||||||
_the_net_tab.button_pressed = section == Section.NET
|
|
||||||
_mail_tab.button_pressed = section == Section.MAIL
|
_mail_tab.button_pressed = section == Section.MAIL
|
||||||
_profile_tab.button_pressed = section == Section.PROFILE
|
_profile_tab.button_pressed = section == Section.PROFILE
|
||||||
_players_tab.button_pressed = section == Section.PLAYERS
|
_players_tab.button_pressed = section == Section.PLAYERS
|
||||||
|
|
@ -1670,18 +1643,14 @@ func _update_shell_layout() -> void:
|
||||||
_logbook_page.size = reference_size
|
_logbook_page.size = reference_size
|
||||||
_logbook_page.position = Vector2.ZERO
|
_logbook_page.position = Vector2.ZERO
|
||||||
_logbook_rest_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.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
_mail_page.size = reference_size
|
_mail_page.size = reference_size
|
||||||
_mail_page.position = Vector2.ZERO
|
_mail_page.position = Vector2.ZERO
|
||||||
_mail_rest_position = Vector2.ZERO
|
_mail_rest_position = Vector2.ZERO
|
||||||
_profile_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
_profile_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
_profile_page.position = Vector2.ZERO
|
_profile_page.position = Vector2(42.0, 104.0)
|
||||||
_profile_page.size = reference_size
|
_profile_page.size = Vector2(1196.0, 608.0)
|
||||||
_profile_rest_position = Vector2.ZERO
|
_profile_rest_position = _profile_page.position
|
||||||
_players_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
_players_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
_players_page.position = Vector2.ZERO
|
_players_page.position = Vector2.ZERO
|
||||||
_players_page.size = reference_size
|
_players_page.size = reference_size
|
||||||
|
|
@ -1704,7 +1673,6 @@ func _update_shell_layout() -> void:
|
||||||
_bag_page.position = _bag_rest_position
|
_bag_page.position = _bag_rest_position
|
||||||
_tackle_box_page.position = _tackle_rest_position
|
_tackle_box_page.position = _tackle_rest_position
|
||||||
_logbook_page.position = _logbook_rest_position
|
_logbook_page.position = _logbook_rest_position
|
||||||
_the_net_page.position = _the_net_rest_position
|
|
||||||
_mail_page.position = _mail_rest_position
|
_mail_page.position = _mail_rest_position
|
||||||
_profile_page.position = _profile_rest_position
|
_profile_page.position = _profile_rest_position
|
||||||
_players_page.position = Vector2.ZERO
|
_players_page.position = Vector2.ZERO
|
||||||
|
|
@ -1712,7 +1680,6 @@ func _update_shell_layout() -> void:
|
||||||
_bag_page.modulate.a = 1.0
|
_bag_page.modulate.a = 1.0
|
||||||
_tackle_box_page.modulate.a = 1.0
|
_tackle_box_page.modulate.a = 1.0
|
||||||
_logbook_page.modulate.a = 1.0
|
_logbook_page.modulate.a = 1.0
|
||||||
_the_net_page.modulate.a = 1.0
|
|
||||||
_mail_page.modulate.a = 1.0
|
_mail_page.modulate.a = 1.0
|
||||||
_profile_page.modulate.a = 1.0
|
_profile_page.modulate.a = 1.0
|
||||||
_players_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()
|
_settle_inventory_tabs_for_close()
|
||||||
if _current_section == Section.LOGBOOK:
|
if _current_section == Section.LOGBOOK:
|
||||||
_catalog_logbook.deactivate()
|
_catalog_logbook.deactivate()
|
||||||
elif _current_section == Section.NET:
|
|
||||||
_the_net_page.deactivate()
|
|
||||||
get_viewport().gui_release_focus()
|
get_viewport().gui_release_focus()
|
||||||
var generation: int = _transition_generation
|
var generation: int = _transition_generation
|
||||||
var closing_generation: int = _menu_generation
|
var closing_generation: int = _menu_generation
|
||||||
|
|
@ -1886,7 +1851,6 @@ func _finish_close(
|
||||||
_cancel_presentation_tween()
|
_cancel_presentation_tween()
|
||||||
_cancel_page_tween()
|
_cancel_page_tween()
|
||||||
_cancel_logbook_page_transition(true)
|
_cancel_logbook_page_transition(true)
|
||||||
_the_net_page.deactivate()
|
|
||||||
_transitioning = false
|
_transitioning = false
|
||||||
_page_transitioning = false
|
_page_transitioning = false
|
||||||
_bag_drag_active = false
|
_bag_drag_active = false
|
||||||
|
|
@ -1918,8 +1882,6 @@ func _get_section_root(section: Section) -> Control:
|
||||||
return _tackle_box_page
|
return _tackle_box_page
|
||||||
Section.LOGBOOK:
|
Section.LOGBOOK:
|
||||||
return _logbook_page
|
return _logbook_page
|
||||||
Section.NET:
|
|
||||||
return _the_net_page
|
|
||||||
Section.MAIL:
|
Section.MAIL:
|
||||||
return _mail_page
|
return _mail_page
|
||||||
Section.PROFILE:
|
Section.PROFILE:
|
||||||
|
|
@ -1938,8 +1900,6 @@ func _get_section_rest_position(section: Section) -> Vector2:
|
||||||
return _tackle_rest_position
|
return _tackle_rest_position
|
||||||
Section.LOGBOOK:
|
Section.LOGBOOK:
|
||||||
return _logbook_rest_position
|
return _logbook_rest_position
|
||||||
Section.NET:
|
|
||||||
return _the_net_rest_position
|
|
||||||
Section.MAIL:
|
Section.MAIL:
|
||||||
return _mail_rest_position
|
return _mail_rest_position
|
||||||
Section.PROFILE:
|
Section.PROFILE:
|
||||||
|
|
@ -2042,7 +2002,6 @@ func _set_shell_interactive(interactive: bool) -> void:
|
||||||
for bubble: BubbleButtonType in [
|
for bubble: BubbleButtonType in [
|
||||||
_inventory_tab,
|
_inventory_tab,
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_the_net_tab,
|
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
_players_tab,
|
_players_tab,
|
||||||
|
|
@ -2099,9 +2058,6 @@ func _set_content_interactive(interactive: bool) -> void:
|
||||||
_catalog_logbook.set_interactive(
|
_catalog_logbook.set_interactive(
|
||||||
interactive and _current_section == Section.LOGBOOK
|
interactive and _current_section == Section.LOGBOOK
|
||||||
)
|
)
|
||||||
_the_net_page.set_interactive(
|
|
||||||
interactive and _current_section == Section.NET
|
|
||||||
)
|
|
||||||
_mail_page.set_interactive(
|
_mail_page.set_interactive(
|
||||||
interactive and _current_section == Section.MAIL
|
interactive and _current_section == Section.MAIL
|
||||||
)
|
)
|
||||||
|
|
@ -2177,7 +2133,6 @@ func _reset_page_transition_visuals() -> void:
|
||||||
Section.BAG,
|
Section.BAG,
|
||||||
Section.TACKLE_BOX,
|
Section.TACKLE_BOX,
|
||||||
Section.LOGBOOK,
|
Section.LOGBOOK,
|
||||||
Section.NET,
|
|
||||||
Section.MAIL,
|
Section.MAIL,
|
||||||
Section.PROFILE,
|
Section.PROFILE,
|
||||||
Section.PLAYERS,
|
Section.PLAYERS,
|
||||||
|
|
|
||||||
|
|
@ -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="Script" path="res://ui/player_menu.gd" id="1_menu"]
|
||||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
[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="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/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="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"]
|
[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")]
|
[node name="CatalogLogbook" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage" instance=ExtResource("21_logbook_page")]
|
||||||
unique_name_in_owner = true
|
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"]
|
[node name="MailPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
|
|
@ -739,8 +734,9 @@ script = ExtResource("18_mail_page")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
|
offset_top = 112.0
|
||||||
offset_right = 1280.0
|
offset_right = 1280.0
|
||||||
offset_bottom = 720.0
|
offset_bottom = 710.0
|
||||||
|
|
||||||
[node name="PlayersPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
[node name="PlayersPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
||||||
visible = false
|
visible = false
|
||||||
|
|
@ -1394,16 +1390,15 @@ compact_reference_size = Vector2(840, 100)
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
toggle_mode = true
|
toggle_mode = true
|
||||||
text = "stuff"
|
text = "inventory"
|
||||||
accessibility_name = "stuff"
|
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(112, 108)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(60, 60)
|
desktop_anchor = Vector2(70, 60)
|
||||||
compact_anchor = Vector2(60, 73.3333)
|
compact_anchor = Vector2(70, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 20
|
||||||
maximum_font_size = 25
|
maximum_font_size = 22
|
||||||
horizontal_amplitude = 1.2
|
horizontal_amplitude = 1.2
|
||||||
vertical_amplitude = 2.4
|
vertical_amplitude = 2.4
|
||||||
motion_period = 5.6
|
motion_period = 5.6
|
||||||
|
|
@ -1419,8 +1414,8 @@ text = "logbook"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(112, 108)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(180, 60)
|
desktop_anchor = Vector2(210, 60)
|
||||||
compact_anchor = Vector2(180, 73.3333)
|
compact_anchor = Vector2(210, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1431,27 +1426,6 @@ motion_phase = 2.85
|
||||||
deformation_amplitude = 0.012
|
deformation_amplitude = 0.012
|
||||||
deformation_period = 6.7
|
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"]
|
[node name="MailTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
|
|
@ -1460,8 +1434,8 @@ text = "mail"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(112, 108)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(420, 60)
|
desktop_anchor = Vector2(350, 60)
|
||||||
compact_anchor = Vector2(420, 73.3333)
|
compact_anchor = Vector2(350, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1496,8 +1470,8 @@ text = "profile"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(112, 108)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(540, 60)
|
desktop_anchor = Vector2(490, 60)
|
||||||
compact_anchor = Vector2(540, 73.3333)
|
compact_anchor = Vector2(490, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 23
|
minimum_font_size = 23
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1512,13 +1486,12 @@ deformation_period = 6.5
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
toggle_mode = true
|
toggle_mode = true
|
||||||
text = "online"
|
text = "players"
|
||||||
accessibility_name = "online"
|
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(112, 108)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(660, 60)
|
desktop_anchor = Vector2(630, 60)
|
||||||
compact_anchor = Vector2(660, 73.3333)
|
compact_anchor = Vector2(630, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 22
|
minimum_font_size = 22
|
||||||
maximum_font_size = 24
|
maximum_font_size = 24
|
||||||
|
|
@ -1536,8 +1509,8 @@ text = "close"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(96, 94)
|
neutral_size = Vector2(96, 94)
|
||||||
desktop_anchor = Vector2(780, 57)
|
desktop_anchor = Vector2(770, 57)
|
||||||
compact_anchor = Vector2(780, 73.3333)
|
compact_anchor = Vector2(770, 73.3333)
|
||||||
compact_minimum_size = Vector2(76, 74)
|
compact_minimum_size = Vector2(76, 74)
|
||||||
minimum_font_size = 23
|
minimum_font_size = 23
|
||||||
maximum_font_size = 23
|
maximum_font_size = 23
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ class_name PlayersPage
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
var _service: NetworkPlayerListService
|
var _service: NetworkPlayerListService
|
||||||
var _count_label: Label
|
var _header: Label
|
||||||
var _tabs: HBoxContainer
|
var _tabs: HBoxContainer
|
||||||
var _list: VBoxContainer
|
var _list: VBoxContainer
|
||||||
var _status: Label
|
var _status: Label
|
||||||
|
|
@ -36,17 +36,23 @@ func deactivate() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _build() -> 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()
|
var root := VBoxContainer.new()
|
||||||
root.add_theme_constant_override("separation", 9)
|
root.add_theme_constant_override("separation", 10)
|
||||||
margin.add_child(root)
|
paper.add_child(root)
|
||||||
var tab_row := HBoxContainer.new()
|
_header = Label.new()
|
||||||
tab_row.add_theme_constant_override("separation", 16)
|
_header.add_theme_font_size_override("font_size", 27)
|
||||||
root.add_child(tab_row)
|
_header.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
root.add_child(_header)
|
||||||
_tabs = HBoxContainer.new()
|
_tabs = HBoxContainer.new()
|
||||||
_tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
_tabs.add_theme_constant_override("separation", 10)
|
_tabs.add_theme_constant_override("separation", 10)
|
||||||
tab_row.add_child(_tabs)
|
root.add_child(_tabs)
|
||||||
for index: int in 3:
|
for index: int in 3:
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.text = ["players", "relationships", "banned"][index]
|
button.text = ["players", "relationships", "banned"][index]
|
||||||
|
|
@ -54,20 +60,12 @@ func _build() -> void:
|
||||||
button.pressed.connect(_select_tab.bind(index))
|
button.pressed.connect(_select_tab.bind(index))
|
||||||
UtilityPageStyle.apply_ocean_button(button)
|
UtilityPageStyle.apply_ocean_button(button)
|
||||||
_tabs.add_child(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()
|
var scroll := ScrollContainer.new()
|
||||||
scroll.custom_minimum_size = Vector2(0, 310)
|
scroll.custom_minimum_size = Vector2(0, 392)
|
||||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
||||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
root.add_child(scroll)
|
root.add_child(scroll)
|
||||||
_list = VBoxContainer.new()
|
_list = VBoxContainer.new()
|
||||||
_list.custom_minimum_size = Vector2(1032, 0)
|
_list.custom_minimum_size = Vector2(1080, 0)
|
||||||
_list.add_theme_constant_override("separation", 7)
|
_list.add_theme_constant_override("separation", 7)
|
||||||
scroll.add_child(_list)
|
scroll.add_child(_list)
|
||||||
_status = Label.new()
|
_status = Label.new()
|
||||||
|
|
@ -88,7 +86,7 @@ func _select_tab(index: int) -> void:
|
||||||
func _refresh() -> void:
|
func _refresh() -> void:
|
||||||
if _service == null or _list == null:
|
if _service == null or _list == null:
|
||||||
return
|
return
|
||||||
_count_label.text = "%d / %d connected" % [
|
_header.text = "Players\n%d / %d connected" % [
|
||||||
_service.get_connected_count(), _service.get_max_players(),
|
_service.get_connected_count(), _service.get_max_players(),
|
||||||
]
|
]
|
||||||
for index: int in _tabs.get_child_count():
|
for index: int in _tabs.get_child_count():
|
||||||
|
|
@ -170,6 +168,7 @@ func _build_active_rows() -> void:
|
||||||
ban.pressed.connect(_confirm_ban.bind(entry))
|
ban.pressed.connect(_confirm_ban.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(ban)
|
UtilityPageStyle.apply_ocean_button(ban)
|
||||||
row.add_child(ban)
|
row.add_child(ban)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
func _build_session_artwork_controls() -> void:
|
func _build_session_artwork_controls() -> void:
|
||||||
|
|
@ -196,6 +195,7 @@ func _build_session_artwork_controls() -> void:
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(reset)
|
UtilityPageStyle.apply_ocean_button(reset)
|
||||||
row.add_child(reset)
|
row.add_child(reset)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
func _build_relationship_rows() -> void:
|
func _build_relationship_rows() -> void:
|
||||||
|
|
@ -234,6 +234,7 @@ func _build_relationship_rows() -> void:
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(unmute)
|
UtilityPageStyle.apply_ocean_button(unmute)
|
||||||
row.add_child(unmute)
|
row.add_child(unmute)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
func _build_ban_rows() -> void:
|
func _build_ban_rows() -> void:
|
||||||
|
|
@ -261,18 +262,14 @@ func _build_ban_rows() -> void:
|
||||||
unban.pressed.connect(_confirm_unban.bind(fingerprint))
|
unban.pressed.connect(_confirm_unban.bind(fingerprint))
|
||||||
UtilityPageStyle.apply_ocean_button(unban)
|
UtilityPageStyle.apply_ocean_button(unban)
|
||||||
row.add_child(unban)
|
row.add_child(unban)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
func _make_row() -> HBoxContainer:
|
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()
|
var row := HBoxContainer.new()
|
||||||
row.custom_minimum_size.y = 58
|
row.custom_minimum_size.y = 58
|
||||||
row.add_theme_constant_override("separation", 8)
|
row.add_theme_constant_override("separation", 8)
|
||||||
panel.add_child(row)
|
row.add_theme_constant_override("outline_size", 1)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ extends Control
|
||||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||||
|
|
||||||
var _service: NetworkProfileService
|
var _service: NetworkProfileService
|
||||||
var _experience: PlayerExperience
|
|
||||||
var _draft_name: String = ""
|
var _draft_name: String = ""
|
||||||
var _draft_appearance: Dictionary = {}
|
var _draft_appearance: Dictionary = {}
|
||||||
var _persisted_name: String = ""
|
var _persisted_name: String = ""
|
||||||
|
|
@ -26,9 +25,6 @@ var _confirmation_label: Label
|
||||||
var _confirmation_confirm: Button
|
var _confirmation_confirm: Button
|
||||||
var _confirmation_action: String = ""
|
var _confirmation_action: String = ""
|
||||||
var _debounce: Timer
|
var _debounce: Timer
|
||||||
var _experience_level: Label
|
|
||||||
var _experience_progress: ProgressBar
|
|
||||||
var _experience_value: Label
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -41,24 +37,12 @@ func _ready() -> void:
|
||||||
add_child(_debounce)
|
add_child(_debounce)
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(service: NetworkProfileService) -> void:
|
||||||
service: NetworkProfileService,
|
|
||||||
experience: PlayerExperience,
|
|
||||||
) -> void:
|
|
||||||
_service = service
|
_service = service
|
||||||
_experience = experience
|
|
||||||
if not _service.conflict_result.is_connected(_on_conflict_result):
|
if not _service.conflict_result.is_connected(_on_conflict_result):
|
||||||
_service.conflict_result.connect(_on_conflict_result)
|
_service.conflict_result.connect(_on_conflict_result)
|
||||||
_service.apply_finished.connect(_on_apply_finished)
|
_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()
|
_load_persisted()
|
||||||
_refresh_experience()
|
|
||||||
var identity_value := find_child("IdentityFingerprint", true, false) as Label
|
var identity_value := find_child("IdentityFingerprint", true, false) as Label
|
||||||
if identity_value != null:
|
if identity_value != null:
|
||||||
var fingerprint := _service.get_identity_fingerprint()
|
var fingerprint := _service.get_identity_fingerprint()
|
||||||
|
|
@ -124,200 +108,134 @@ func has_modal_confirmation() -> bool:
|
||||||
|
|
||||||
|
|
||||||
func _build_ui() -> void:
|
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)
|
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()
|
var layout := VBoxContainer.new()
|
||||||
layout.add_theme_constant_override("separation", 8)
|
layout.add_theme_constant_override("separation", 5)
|
||||||
margin.add_child(layout)
|
margin.add_child(layout)
|
||||||
|
|
||||||
var account_row := HBoxContainer.new()
|
var heading := Label.new()
|
||||||
account_row.add_theme_constant_override("separation", 14)
|
heading.text = "Player Profile"
|
||||||
layout.add_child(account_row)
|
heading.add_theme_font_size_override("font_size", 25)
|
||||||
var account_stack := VBoxContainer.new()
|
heading.add_theme_color_override(
|
||||||
account_stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||||
account_stack.add_theme_constant_override("separation", 2)
|
)
|
||||||
account_row.add_child(account_stack)
|
layout.add_child(heading)
|
||||||
var name_line := HBoxContainer.new()
|
|
||||||
name_line.add_theme_constant_override("separation", 10)
|
var name_row := HBoxContainer.new()
|
||||||
account_stack.add_child(name_line)
|
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()
|
var name_label := Label.new()
|
||||||
name_label.text = "player name"
|
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(
|
name_label.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||||
)
|
)
|
||||||
name_line.add_child(name_label)
|
name_stack.add_child(name_label)
|
||||||
_name_edit = LineEdit.new()
|
_name_edit = LineEdit.new()
|
||||||
_name_edit.max_length = NetworkProtocol.MAX_DISPLAY_NAME_LENGTH
|
_name_edit.max_length = NetworkProtocol.MAX_DISPLAY_NAME_LENGTH
|
||||||
_name_edit.placeholder_text = "Player"
|
_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)
|
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_edit.text_changed.connect(_on_name_changed)
|
||||||
name_line.add_child(_name_edit)
|
name_stack.add_child(_name_edit)
|
||||||
var helper := Label.new()
|
var helper := Label.new()
|
||||||
helper.text = "Shown to other players in multiplayer."
|
helper.text = "Shown to other players in multiplayer."
|
||||||
helper.add_theme_font_size_override("font_size", 12)
|
|
||||||
helper.add_theme_color_override(
|
helper.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"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 = Label.new()
|
||||||
_name_status.add_theme_font_size_override("font_size", 12)
|
|
||||||
_name_status.add_theme_color_override(
|
_name_status.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
account_stack.add_child(_name_status)
|
layout.add_child(_name_status)
|
||||||
_suggestions = HBoxContainer.new()
|
_suggestions = HBoxContainer.new()
|
||||||
_suggestions.add_theme_constant_override("separation", 6)
|
_suggestions.add_theme_constant_override("separation", 8)
|
||||||
account_stack.add_child(_suggestions)
|
layout.add_child(_suggestions)
|
||||||
|
|
||||||
var identity_value := Label.new()
|
var identity_value := Label.new()
|
||||||
identity_value.name = "IdentityFingerprint"
|
identity_value.name = "IdentityFingerprint"
|
||||||
identity_value.text = "identity • stored on this device"
|
identity_value.text = "Identity • Stored on this device"
|
||||||
identity_value.tooltip_text = (
|
identity_value.tooltip_text = (
|
||||||
"This identity helps other players recognize you between sessions."
|
"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(
|
identity_value.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"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()
|
var body := HBoxContainer.new()
|
||||||
body.alignment = BoxContainer.ALIGNMENT_BEGIN
|
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
body.add_theme_constant_override("separation", 16)
|
body.add_theme_constant_override("separation", 12)
|
||||||
body_margin.add_child(body)
|
layout.add_child(body)
|
||||||
_category_list = VBoxContainer.new()
|
_category_list = VBoxContainer.new()
|
||||||
_category_list.custom_minimum_size = Vector2(120, 0)
|
_category_list.custom_minimum_size = Vector2(140, 0)
|
||||||
_category_list.add_theme_constant_override("separation", 5)
|
|
||||||
body.add_child(_category_list)
|
body.add_child(_category_list)
|
||||||
_option_list = VBoxContainer.new()
|
_option_list = VBoxContainer.new()
|
||||||
_option_list.custom_minimum_size = Vector2(170, 0)
|
_option_list.custom_minimum_size = Vector2(190, 0)
|
||||||
_option_list.add_theme_constant_override("separation", 5)
|
|
||||||
body.add_child(_option_list)
|
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()
|
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.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
preview_stack.add_theme_constant_override("separation", 7)
|
|
||||||
body.add_child(preview_stack)
|
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 = preload("res://ui/profile_preview.tscn").instantiate()
|
||||||
_preview.custom_minimum_size = Vector2(180.0, 220.0)
|
preview_stack.add_child(_preview)
|
||||||
preview_frame.add_child(_preview)
|
var preview_actions := HBoxContainer.new()
|
||||||
var preview_actions := VBoxContainer.new()
|
|
||||||
preview_actions.alignment = BoxContainer.ALIGNMENT_CENTER
|
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)
|
preview_stack.add_child(preview_actions)
|
||||||
var preview_note := Label.new()
|
var preview_note := Label.new()
|
||||||
preview_note.text = "drag or use left / right"
|
preview_note.text = "Current player • 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.add_theme_color_override(
|
preview_note.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
preview_actions.add_child(preview_note)
|
preview_actions.add_child(preview_note)
|
||||||
var reset_view := Button.new()
|
var reset_view := Button.new()
|
||||||
reset_view.text = "reset view"
|
reset_view.text = "reset view"
|
||||||
reset_view.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
|
||||||
reset_view.pressed.connect(_preview.reset_view)
|
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)
|
preview_actions.add_child(reset_view)
|
||||||
|
|
||||||
_discard_confirmation = PanelContainer.new()
|
_discard_confirmation = PanelContainer.new()
|
||||||
_discard_confirmation.visible = false
|
_discard_confirmation.visible = false
|
||||||
_discard_confirmation.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
_discard_confirmation.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||||
_discard_confirmation.custom_minimum_size = Vector2(430, 190)
|
_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)
|
add_child(_discard_confirmation)
|
||||||
var confirm_stack := VBoxContainer.new()
|
var confirm_stack := VBoxContainer.new()
|
||||||
confirm_stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
confirm_stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
|
@ -331,7 +249,6 @@ func _build_ui() -> void:
|
||||||
confirm_stack.add_child(confirm_buttons)
|
confirm_stack.add_child(confirm_buttons)
|
||||||
_confirmation_confirm = Button.new()
|
_confirmation_confirm = Button.new()
|
||||||
_confirmation_confirm.pressed.connect(_confirm_pending_action)
|
_confirmation_confirm.pressed.connect(_confirm_pending_action)
|
||||||
UtilityPageStyle.apply_ocean_button(_confirmation_confirm)
|
|
||||||
confirm_buttons.add_child(_confirmation_confirm)
|
confirm_buttons.add_child(_confirmation_confirm)
|
||||||
var keep := Button.new()
|
var keep := Button.new()
|
||||||
keep.name = "KeepEditing"
|
keep.name = "KeepEditing"
|
||||||
|
|
@ -341,7 +258,6 @@ func _build_ui() -> void:
|
||||||
_discard_confirmation.visible = false
|
_discard_confirmation.visible = false
|
||||||
_name_edit.grab_focus()
|
_name_edit.grab_focus()
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(keep)
|
|
||||||
confirm_buttons.add_child(keep)
|
confirm_buttons.add_child(keep)
|
||||||
|
|
||||||
_build_categories()
|
_build_categories()
|
||||||
|
|
@ -354,10 +270,10 @@ func _build_categories() -> void:
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.text = CharacterCustomizationCatalog.category_label(category_id)
|
button.text = CharacterCustomizationCatalog.category_label(category_id)
|
||||||
button.toggle_mode = true
|
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.button_pressed = category_id == _category_id
|
||||||
button.pressed.connect(_select_category.bind(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)
|
_category_list.add_child(button)
|
||||||
_refresh_options()
|
_refresh_options()
|
||||||
|
|
||||||
|
|
@ -378,7 +294,7 @@ func _refresh_options() -> void:
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = CharacterCustomizationCatalog.category_label(_category_id)
|
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(
|
title.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||||
)
|
)
|
||||||
|
|
@ -395,10 +311,10 @@ func _refresh_options() -> void:
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.text = str(option["label"])
|
button.text = str(option["label"])
|
||||||
button.toggle_mode = true
|
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.button_pressed = _draft_appearance.get(_category_id) == option_id
|
||||||
button.pressed.connect(_select_option.bind(_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)
|
_option_list.add_child(button)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -442,7 +358,6 @@ func _on_conflict_result(
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.text = suggestion
|
button.text = suggestion
|
||||||
button.pressed.connect(_use_suggestion.bind(suggestion))
|
button.pressed.connect(_use_suggestion.bind(suggestion))
|
||||||
UtilityPageStyle.apply_compact_ocean_button(button)
|
|
||||||
_suggestions.add_child(button)
|
_suggestions.add_child(button)
|
||||||
var anyway := Button.new()
|
var anyway := Button.new()
|
||||||
anyway.text = "use anyway"
|
anyway.text = "use anyway"
|
||||||
|
|
@ -450,33 +365,9 @@ func _on_conflict_result(
|
||||||
_allow_duplicate = true
|
_allow_duplicate = true
|
||||||
_name_status.text = "Duplicate name allowed for this apply."
|
_name_status.text = "Duplicate name allowed for this apply."
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_compact_ocean_button(anyway)
|
|
||||||
_suggestions.add_child(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:
|
func _use_suggestion(value: String) -> void:
|
||||||
_name_edit.text = value
|
_name_edit.text = value
|
||||||
_draft_name = value
|
_draft_name = value
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ stretch = true
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
[node name="Viewport" type="SubViewport" parent="."]
|
[node name="Viewport" type="SubViewport" parent="."]
|
||||||
transparent_bg = true
|
transparent_bg = false
|
||||||
size = Vector2i(320, 330)
|
size = Vector2i(320, 330)
|
||||||
render_target_update_mode = 4
|
render_target_update_mode = 4
|
||||||
world_3d = SubResource("PreviewWorld")
|
world_3d = SubResource("PreviewWorld")
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://couoolj844sj3
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -26,65 +26,12 @@ const GREEN: Color = Color("31594d")
|
||||||
const LIGHT_TEXT: Color = Color("f5eed9")
|
const LIGHT_TEXT: Color = Color("f5eed9")
|
||||||
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
|
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
|
||||||
const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween"
|
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:
|
static func apply_page(root: Control) -> void:
|
||||||
root.add_theme_font_override("font", TuffyFont)
|
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:
|
static func panel_style() -> StyleBoxFlat:
|
||||||
var style := StyleBoxFlat.new()
|
var style := StyleBoxFlat.new()
|
||||||
style.bg_color = OCEAN_PANEL_DEEP
|
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)
|
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:
|
static func apply_line_edit(edit: LineEdit) -> void:
|
||||||
edit.add_theme_font_override("font", TuffyFont)
|
edit.add_theme_font_override("font", TuffyFont)
|
||||||
edit.add_theme_color_override("font_color", LIGHT_TEXT)
|
edit.add_theme_color_override("font_color", LIGHT_TEXT)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ var _persistent_time_hours: float = DEFAULT_START_HOUR
|
||||||
var _persistence_tracking_enabled: bool = false
|
var _persistence_tracking_enabled: bool = false
|
||||||
var _running: bool = false
|
var _running: bool = false
|
||||||
var _phase: Phase = Phase.DAWN
|
var _phase: Phase = Phase.DAWN
|
||||||
var _last_emitted_clock_minute: int = floori(DEFAULT_START_HOUR * 60.0)
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
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 normalized: float = _normalized_hour(time_hours)
|
||||||
var next_phase: Phase = phase_for_hour(normalized)
|
var next_phase: Phase = phase_for_hour(normalized)
|
||||||
var phase_was_changed: bool = next_phase != _phase
|
var phase_was_changed: bool = next_phase != _phase
|
||||||
var clock_minute: int = floori(normalized * 60.0)
|
var time_was_changed: bool = not is_equal_approx(normalized, _time_hours)
|
||||||
var clock_minute_changed: bool = (
|
|
||||||
clock_minute != _last_emitted_clock_minute
|
|
||||||
)
|
|
||||||
_time_hours = normalized
|
_time_hours = normalized
|
||||||
_phase = next_phase
|
_phase = next_phase
|
||||||
if _persistence_tracking_enabled:
|
if _persistence_tracking_enabled:
|
||||||
_persistent_time_hours = normalized
|
_persistent_time_hours = normalized
|
||||||
if phase_was_changed:
|
if phase_was_changed:
|
||||||
phase_changed.emit(_phase)
|
phase_changed.emit(_phase)
|
||||||
if force_emit or clock_minute_changed or phase_was_changed:
|
if force_emit or time_was_changed or phase_was_changed:
|
||||||
_last_emitted_clock_minute = clock_minute
|
|
||||||
time_changed.emit(_time_hours, _phase)
|
time_changed.emit(_time_hours, _phase)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,6 @@ const UPDATE_INTERVAL_SECONDS: float = 0.1
|
||||||
const SUN_YAW_DEGREES: float = -32.0
|
const SUN_YAW_DEGREES: float = -32.0
|
||||||
const WEATHER_TRANSITION_SECONDS: float = 10.0
|
const WEATHER_TRANSITION_SECONDS: float = 10.0
|
||||||
const RAIN_EMITTER_OFFSET := Vector3(0.0, 7.0, 0.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_TOP := Color(0.204, 0.498, 0.643)
|
||||||
const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847)
|
const DAY_SKY_HORIZON := Color(0.663, 0.843, 0.847)
|
||||||
|
|
@ -149,7 +145,7 @@ func _prepare_runtime_environment() -> bool:
|
||||||
func _prepare_rain() -> void:
|
func _prepare_rain() -> void:
|
||||||
_rain = GPUParticles3D.new()
|
_rain = GPUParticles3D.new()
|
||||||
_rain.name = "LocalRain"
|
_rain.name = "LocalRain"
|
||||||
_rain.amount = RAIN_PARTICLE_AMOUNT
|
_rain.amount = 480
|
||||||
_rain.amount_ratio = 0.0
|
_rain.amount_ratio = 0.0
|
||||||
_rain.lifetime = 1.25
|
_rain.lifetime = 1.25
|
||||||
_rain.fixed_fps = 30
|
_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.emission_box_extents = Vector3(6.5, 1.0, 6.5)
|
||||||
process_material.direction = Vector3.DOWN
|
process_material.direction = Vector3.DOWN
|
||||||
process_material.spread = 5.0
|
process_material.spread = 5.0
|
||||||
process_material.initial_velocity_min = RAIN_VELOCITY_MIN
|
process_material.initial_velocity_min = 11.0
|
||||||
process_material.initial_velocity_max = RAIN_VELOCITY_MAX
|
process_material.initial_velocity_max = 15.0
|
||||||
process_material.gravity = Vector3(0.0, -2.0, 0.0)
|
process_material.gravity = Vector3(0.0, -2.0, 0.0)
|
||||||
_rain.process_material = process_material
|
_rain.process_material = process_material
|
||||||
var drop_mesh := BoxMesh.new()
|
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()
|
var drop_material := StandardMaterial3D.new()
|
||||||
drop_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
drop_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
drop_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
drop_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
|
|
||||||
|
|
@ -15,21 +15,11 @@ const SUNNY_DURATION_RANGE := Vector2(480.0, 900.0)
|
||||||
const CLOUDY_DURATION_RANGE := Vector2(300.0, 720.0)
|
const CLOUDY_DURATION_RANGE := Vector2(300.0, 720.0)
|
||||||
const RAINY_DURATION_RANGE := Vector2(300.0, 600.0)
|
const RAINY_DURATION_RANGE := Vector2(300.0, 600.0)
|
||||||
const FOGGY_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 _weather: Weather = DEFAULT_WEATHER
|
||||||
var _seconds_remaining: float = SUNNY_DURATION_RANGE.x
|
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 _running_authority: bool = false
|
||||||
var _rng := RandomNumberGenerator.new()
|
var _rng := RandomNumberGenerator.new()
|
||||||
var _daily_plan_id: String = ""
|
|
||||||
var _daily_schedule: Array[Dictionary] = []
|
|
||||||
var _world_time: WorldTimeService
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -44,21 +34,7 @@ func begin_authoritative_session(seed_value: int) -> void:
|
||||||
_rng.seed = seed_value
|
_rng.seed = seed_value
|
||||||
_running_authority = true
|
_running_authority = true
|
||||||
set_process(true)
|
set_process(true)
|
||||||
if _daily_schedule.is_empty() or _world_time == null:
|
_set_weather(DEFAULT_WEATHER, _roll_duration(DEFAULT_WEATHER), true)
|
||||||
if _has_persistent_state:
|
|
||||||
_set_weather(
|
|
||||||
_persistent_weather,
|
|
||||||
_persistent_seconds_remaining,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_set_weather(
|
|
||||||
DEFAULT_WEATHER,
|
|
||||||
_roll_duration(DEFAULT_WEATHER),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_update_scheduled_weather(true)
|
|
||||||
|
|
||||||
|
|
||||||
func begin_remote_session() -> void:
|
func begin_remote_session() -> void:
|
||||||
|
|
@ -76,17 +52,12 @@ func end_session() -> void:
|
||||||
func advance_weather(real_seconds: float) -> void:
|
func advance_weather(real_seconds: float) -> void:
|
||||||
if not _running_authority or real_seconds <= 0.0:
|
if not _running_authority or real_seconds <= 0.0:
|
||||||
return
|
return
|
||||||
if not _daily_schedule.is_empty() and _world_time != null:
|
|
||||||
_update_scheduled_weather(false)
|
|
||||||
return
|
|
||||||
_seconds_remaining -= real_seconds
|
_seconds_remaining -= real_seconds
|
||||||
while _seconds_remaining <= 0.0:
|
while _seconds_remaining <= 0.0:
|
||||||
var overrun: float = -_seconds_remaining
|
var overrun: float = -_seconds_remaining
|
||||||
var next_weather: Weather = _choose_next_weather()
|
var next_weather: Weather = _choose_next_weather()
|
||||||
_set_weather(next_weather, _roll_duration(next_weather), true)
|
_set_weather(next_weather, _roll_duration(next_weather), true)
|
||||||
_seconds_remaining -= overrun
|
_seconds_remaining -= overrun
|
||||||
if _persistence_tracking_enabled:
|
|
||||||
_store_persistent_state()
|
|
||||||
|
|
||||||
|
|
||||||
func apply_authoritative_snapshot(
|
func apply_authoritative_snapshot(
|
||||||
|
|
@ -98,56 +69,6 @@ func apply_authoritative_snapshot(
|
||||||
_set_weather(weather, maxf(seconds_remaining, 0.0), false)
|
_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:
|
func get_weather() -> Weather:
|
||||||
return _weather
|
return _weather
|
||||||
|
|
||||||
|
|
@ -168,95 +89,10 @@ func get_weather_name() -> String:
|
||||||
return weather_name(_weather)
|
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:
|
static func is_valid_weather(value: int) -> bool:
|
||||||
return value >= Weather.SUNNY and value <= Weather.FOGGY
|
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:
|
static func weather_name(weather: Weather) -> String:
|
||||||
match weather:
|
match weather:
|
||||||
Weather.SUNNY:
|
Weather.SUNNY:
|
||||||
|
|
@ -278,18 +114,10 @@ func _set_weather(
|
||||||
var changed: bool = weather != _weather
|
var changed: bool = weather != _weather
|
||||||
_weather = weather
|
_weather = weather
|
||||||
_seconds_remaining = maxf(seconds_remaining, 0.0)
|
_seconds_remaining = maxf(seconds_remaining, 0.0)
|
||||||
if _persistence_tracking_enabled:
|
|
||||||
_store_persistent_state()
|
|
||||||
if force_emit or changed:
|
if force_emit or changed:
|
||||||
weather_changed.emit(_weather, _seconds_remaining)
|
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:
|
func _choose_next_weather() -> Weather:
|
||||||
match _weather:
|
match _weather:
|
||||||
Weather.SUNNY:
|
Weather.SUNNY:
|
||||||
|
|
@ -322,26 +150,3 @@ func _duration_range(weather: Weather) -> Vector2:
|
||||||
Weather.FOGGY:
|
Weather.FOGGY:
|
||||||
return FOGGY_DURATION_RANGE
|
return FOGGY_DURATION_RANGE
|
||||||
return SUNNY_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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue