Add synchronized Fishnet jobs and laptop UI
This commit is contained in:
parent
46df74361a
commit
26fbd41132
39 changed files with 3257 additions and 188 deletions
416
jobs/job_catalog.gd
Normal file
416
jobs/job_catalog.gd
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
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
jobs/job_catalog.gd.uid
Normal file
1
jobs/job_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c52vqhdties3o
|
||||
786
jobs/player_job_service.gd
Normal file
786
jobs/player_job_service.gd
Normal file
|
|
@ -0,0 +1,786 @@
|
|||
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
jobs/player_job_service.gd.uid
Normal file
1
jobs/player_job_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://daqtje84gqgu0
|
||||
Loading…
Add table
Add a link
Reference in a new issue