Compare commits
2 commits
7661b057bc
...
291cc4a713
| Author | SHA1 | Date | |
|---|---|---|---|
| 291cc4a713 | |||
| 35d00e8035 |
47 changed files with 1846 additions and 85 deletions
|
|
@ -2,9 +2,13 @@ class_name CollectionLog
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
signal fish_discovered(fish_id: StringName)
|
signal fish_discovered(fish_id: StringName)
|
||||||
|
signal fish_quality_discovered(fish_id: StringName, quality: int)
|
||||||
signal collection_changed
|
signal collection_changed
|
||||||
|
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
var _discovered: Dictionary[StringName, bool] = {}
|
var _discovered: Dictionary[StringName, bool] = {}
|
||||||
|
var _quality_masks: Dictionary[StringName, int] = {}
|
||||||
|
|
||||||
|
|
||||||
func has_discovered(fish_id: StringName) -> bool:
|
func has_discovered(fish_id: StringName) -> bool:
|
||||||
|
|
@ -19,6 +23,24 @@ func mark_discovered(fish_id: StringName) -> void:
|
||||||
collection_changed.emit()
|
collection_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func mark_quality_discovered(fish_id: StringName, quality: int) -> void:
|
||||||
|
if fish_id.is_empty() or not FishQualityType.is_valid(quality):
|
||||||
|
return
|
||||||
|
var species_was_discovered: bool = has_discovered(fish_id)
|
||||||
|
if not species_was_discovered:
|
||||||
|
_discovered[fish_id] = true
|
||||||
|
var previous_mask: int = _quality_masks.get(fish_id, 0)
|
||||||
|
var next_mask: int = previous_mask | FishQualityType.bit_for(quality)
|
||||||
|
if species_was_discovered and next_mask == previous_mask:
|
||||||
|
return
|
||||||
|
_quality_masks[fish_id] = next_mask
|
||||||
|
if not species_was_discovered:
|
||||||
|
fish_discovered.emit(fish_id)
|
||||||
|
if next_mask != previous_mask:
|
||||||
|
fish_quality_discovered.emit(fish_id, quality)
|
||||||
|
collection_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
func get_discovered_ids() -> Array[StringName]:
|
func get_discovered_ids() -> Array[StringName]:
|
||||||
var discovered_ids: Array[StringName] = []
|
var discovered_ids: Array[StringName] = []
|
||||||
for fish_id: StringName in _discovered:
|
for fish_id: StringName in _discovered:
|
||||||
|
|
@ -27,12 +49,53 @@ func get_discovered_ids() -> Array[StringName]:
|
||||||
return discovered_ids
|
return discovered_ids
|
||||||
|
|
||||||
|
|
||||||
|
func has_discovered_quality(fish_id: StringName, quality: int) -> bool:
|
||||||
|
return (
|
||||||
|
not fish_id.is_empty()
|
||||||
|
and FishQualityType.is_valid(quality)
|
||||||
|
and (_quality_masks.get(fish_id, 0) & FishQualityType.bit_for(quality)) != 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_quality_mask(fish_id: StringName) -> int:
|
||||||
|
return _quality_masks.get(fish_id, 0) if has_discovered(fish_id) else 0
|
||||||
|
|
||||||
|
|
||||||
|
func get_discovered_quality_masks() -> Dictionary[StringName, int]:
|
||||||
|
return _quality_masks.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func has_mastered(fish_id: StringName) -> bool:
|
||||||
|
return get_quality_mask(fish_id) == FishQualityType.ALL_TIERS_MASK
|
||||||
|
|
||||||
|
|
||||||
func replace_discovered_ids(fish_ids: Array[StringName]) -> bool:
|
func replace_discovered_ids(fish_ids: Array[StringName]) -> bool:
|
||||||
|
var empty_masks: Dictionary[StringName, int] = {}
|
||||||
|
return replace_discovery_state(fish_ids, empty_masks)
|
||||||
|
|
||||||
|
|
||||||
|
func replace_discovery_state(
|
||||||
|
fish_ids: Array[StringName],
|
||||||
|
quality_masks: Dictionary[StringName, int],
|
||||||
|
) -> bool:
|
||||||
var replacement: Dictionary[StringName, bool] = {}
|
var replacement: Dictionary[StringName, bool] = {}
|
||||||
for fish_id: StringName in fish_ids:
|
for fish_id: StringName in fish_ids:
|
||||||
if fish_id.is_empty():
|
if fish_id.is_empty():
|
||||||
return false
|
return false
|
||||||
replacement[fish_id] = true
|
replacement[fish_id] = true
|
||||||
|
var replacement_masks: Dictionary[StringName, int] = {}
|
||||||
|
for fish_id: StringName in quality_masks:
|
||||||
|
var mask: int = quality_masks[fish_id]
|
||||||
|
if (
|
||||||
|
fish_id.is_empty()
|
||||||
|
or not replacement.has(fish_id)
|
||||||
|
or mask < 0
|
||||||
|
or (mask & ~FishQualityType.ALL_TIERS_MASK) != 0
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if mask != 0:
|
||||||
|
replacement_masks[fish_id] = mask
|
||||||
_discovered = replacement
|
_discovered = replacement
|
||||||
|
_quality_masks = replacement_masks
|
||||||
collection_changed.emit()
|
collection_changed.emit()
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
class_name FishBuyerProfile
|
class_name FishBuyerProfile
|
||||||
extends Resource
|
extends Resource
|
||||||
|
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
@export var id: StringName
|
@export var id: StringName
|
||||||
@export var display_name: String
|
@export var display_name: String
|
||||||
@export var animal_name_singular: String
|
@export var animal_name_singular: String
|
||||||
|
|
@ -25,6 +27,18 @@ func get_offer(base_value: int) -> int:
|
||||||
return maxi(offer, 0)
|
return maxi(offer, 0)
|
||||||
|
|
||||||
|
|
||||||
|
func get_quality_offer(base_value: int, quality: int) -> int:
|
||||||
|
if not FishQualityType.is_valid(quality):
|
||||||
|
return -1
|
||||||
|
var ordinary_offer: int = get_offer(base_value)
|
||||||
|
var adjusted_offer: int = get_offer(
|
||||||
|
FishQualityType.apply_sale_value(base_value, quality)
|
||||||
|
)
|
||||||
|
if ordinary_offer < 0 or adjusted_offer < 0:
|
||||||
|
return -1
|
||||||
|
return maxi(adjusted_offer, ordinary_offer + quality)
|
||||||
|
|
||||||
|
|
||||||
func get_sale_message(
|
func get_sale_message(
|
||||||
fish_name: String,
|
fish_name: String,
|
||||||
payout: int,
|
payout: int,
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,15 @@ func _validate_batch(
|
||||||
if fish_catch.sale_value < 0:
|
if fish_catch.sale_value < 0:
|
||||||
result.status = FishSaleResultType.Status.INVALID_VALUE
|
result.status = FishSaleResultType.Status.INVALID_VALUE
|
||||||
return result
|
return result
|
||||||
var offer: int = buyer.get_offer(fish_catch.sale_value)
|
var ordinary_value: int = (
|
||||||
|
fish_catch.fish.get_sale_value_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var offer: int = buyer.get_quality_offer(
|
||||||
|
ordinary_value,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
if offer < 0:
|
if offer < 0:
|
||||||
result.status = FishSaleResultType.Status.INVALID_OFFER
|
result.status = FishSaleResultType.Status.INVALID_OFFER
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ class_name FishCatch
|
||||||
extends Resource
|
extends Resource
|
||||||
|
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
const MAX_SAFE_WEIGHT_LB: float = 1000000.0
|
const MAX_SAFE_WEIGHT_LB: float = 1000000.0
|
||||||
const MAX_SAFE_DISPLAY_SCALE: float = 10000.0
|
const MAX_SAFE_DISPLAY_SCALE: float = 10000.0
|
||||||
|
|
@ -14,6 +15,7 @@ const MAX_SAFE_SEQUENCE: int = 2147483647
|
||||||
@export var catch_sequence: int = 0
|
@export var catch_sequence: int = 0
|
||||||
@export var weight_lb: float = 0.0
|
@export var weight_lb: float = 0.0
|
||||||
@export var display_scale: float = 1.0
|
@export var display_scale: float = 1.0
|
||||||
|
@export_range(0, 4, 1) var quality: int = FishQualityType.Tier.BORING
|
||||||
@export var sale_value: int = 0
|
@export var sale_value: int = 0
|
||||||
@export var is_favorited: bool = false
|
@export var is_favorited: bool = false
|
||||||
|
|
||||||
|
|
@ -48,6 +50,7 @@ func is_valid() -> bool:
|
||||||
and is_finite(display_scale)
|
and is_finite(display_scale)
|
||||||
and display_scale > 0.0
|
and display_scale > 0.0
|
||||||
and display_scale <= MAX_SAFE_DISPLAY_SCALE
|
and display_scale <= MAX_SAFE_DISPLAY_SCALE
|
||||||
|
and FishQualityType.is_valid(quality)
|
||||||
and sale_value >= 0
|
and sale_value >= 0
|
||||||
and sale_value <= MAX_SAFE_SALE_VALUE
|
and sale_value <= MAX_SAFE_SALE_VALUE
|
||||||
)
|
)
|
||||||
|
|
@ -60,6 +63,7 @@ func to_save_dict() -> Dictionary:
|
||||||
"fish_id": String(fish_id),
|
"fish_id": String(fish_id),
|
||||||
"weight_lb": weight_lb,
|
"weight_lb": weight_lb,
|
||||||
"display_scale": display_scale,
|
"display_scale": display_scale,
|
||||||
|
"quality": quality,
|
||||||
"sale_value": sale_value,
|
"sale_value": sale_value,
|
||||||
"is_favorited": is_favorited,
|
"is_favorited": is_favorited,
|
||||||
}
|
}
|
||||||
|
|
@ -71,6 +75,7 @@ func to_network_dict() -> Dictionary:
|
||||||
"fish_id": String(fish_id),
|
"fish_id": String(fish_id),
|
||||||
"weight_lb": weight_lb,
|
"weight_lb": weight_lb,
|
||||||
"display_scale": display_scale,
|
"display_scale": display_scale,
|
||||||
|
"quality": quality,
|
||||||
"sale_value": sale_value,
|
"sale_value": sale_value,
|
||||||
"is_favorited": is_favorited,
|
"is_favorited": is_favorited,
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +93,7 @@ static func from_save_dict(
|
||||||
"fish_id",
|
"fish_id",
|
||||||
"weight_lb",
|
"weight_lb",
|
||||||
"display_scale",
|
"display_scale",
|
||||||
|
"quality",
|
||||||
"sale_value",
|
"sale_value",
|
||||||
"is_favorited",
|
"is_favorited",
|
||||||
]:
|
]:
|
||||||
|
|
@ -111,6 +117,11 @@ static func from_save_dict(
|
||||||
-1,
|
-1,
|
||||||
MAX_SAFE_SALE_VALUE
|
MAX_SAFE_SALE_VALUE
|
||||||
)
|
)
|
||||||
|
var loaded_quality: int = _read_safe_integer(
|
||||||
|
data["quality"],
|
||||||
|
-1,
|
||||||
|
FishQualityType.TIER_COUNT - 1,
|
||||||
|
)
|
||||||
var loaded_weight: float = _read_safe_float(
|
var loaded_weight: float = _read_safe_float(
|
||||||
data["weight_lb"],
|
data["weight_lb"],
|
||||||
0.0,
|
0.0,
|
||||||
|
|
@ -128,6 +139,7 @@ static func from_save_dict(
|
||||||
or loaded_sequence <= 0
|
or loaded_sequence <= 0
|
||||||
or loaded_sequence >= MAX_SAFE_SEQUENCE
|
or loaded_sequence >= MAX_SAFE_SEQUENCE
|
||||||
or loaded_sale_value < 0
|
or loaded_sale_value < 0
|
||||||
|
or not FishQualityType.is_valid(loaded_quality)
|
||||||
or loaded_weight <= 0.0
|
or loaded_weight <= 0.0
|
||||||
or loaded_scale <= 0.0
|
or loaded_scale <= 0.0
|
||||||
or typeof(loaded_favorite) != TYPE_BOOL
|
or typeof(loaded_favorite) != TYPE_BOOL
|
||||||
|
|
@ -141,6 +153,7 @@ static func from_save_dict(
|
||||||
fish_catch.catch_sequence = loaded_sequence
|
fish_catch.catch_sequence = loaded_sequence
|
||||||
fish_catch.weight_lb = loaded_weight
|
fish_catch.weight_lb = loaded_weight
|
||||||
fish_catch.display_scale = loaded_scale
|
fish_catch.display_scale = loaded_scale
|
||||||
|
fish_catch.quality = loaded_quality
|
||||||
fish_catch.sale_value = loaded_sale_value
|
fish_catch.sale_value = loaded_sale_value
|
||||||
fish_catch.is_favorited = bool(loaded_favorite)
|
fish_catch.is_favorited = bool(loaded_favorite)
|
||||||
return fish_catch if fish_catch.is_valid() else null
|
return fish_catch if fish_catch.is_valid() else null
|
||||||
|
|
@ -157,6 +170,7 @@ static func from_network_dict(
|
||||||
"fish_id",
|
"fish_id",
|
||||||
"weight_lb",
|
"weight_lb",
|
||||||
"display_scale",
|
"display_scale",
|
||||||
|
"quality",
|
||||||
"sale_value",
|
"sale_value",
|
||||||
]:
|
]:
|
||||||
if not data.has(required_key):
|
if not data.has(required_key):
|
||||||
|
|
@ -172,6 +186,11 @@ static func from_network_dict(
|
||||||
var loaded_value: int = _read_safe_integer(
|
var loaded_value: int = _read_safe_integer(
|
||||||
data.get("sale_value"), -1, MAX_SAFE_SALE_VALUE
|
data.get("sale_value"), -1, MAX_SAFE_SALE_VALUE
|
||||||
)
|
)
|
||||||
|
var loaded_quality: int = _read_safe_integer(
|
||||||
|
data.get("quality"),
|
||||||
|
-1,
|
||||||
|
FishQualityType.TIER_COUNT - 1,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
loaded_id.is_empty()
|
loaded_id.is_empty()
|
||||||
or loaded_id.length() > 160
|
or loaded_id.length() > 160
|
||||||
|
|
@ -179,6 +198,7 @@ static func from_network_dict(
|
||||||
or loaded_weight <= 0.0
|
or loaded_weight <= 0.0
|
||||||
or loaded_scale <= 0.0
|
or loaded_scale <= 0.0
|
||||||
or loaded_value < 0
|
or loaded_value < 0
|
||||||
|
or not FishQualityType.is_valid(loaded_quality)
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var fish_catch := FishCatch.new()
|
var fish_catch := FishCatch.new()
|
||||||
|
|
@ -188,6 +208,7 @@ static func from_network_dict(
|
||||||
fish_catch.catch_sequence = 0
|
fish_catch.catch_sequence = 0
|
||||||
fish_catch.weight_lb = loaded_weight
|
fish_catch.weight_lb = loaded_weight
|
||||||
fish_catch.display_scale = loaded_scale
|
fish_catch.display_scale = loaded_scale
|
||||||
|
fish_catch.quality = loaded_quality
|
||||||
fish_catch.sale_value = loaded_value
|
fish_catch.sale_value = loaded_value
|
||||||
fish_catch.is_favorited = false
|
fish_catch.is_favorited = false
|
||||||
return fish_catch if fish_catch.is_valid() else null
|
return fish_catch if fish_catch.is_valid() else null
|
||||||
|
|
|
||||||
73
fish/fish_experience.gd
Normal file
73
fish/fish_experience.gd
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
class_name FishExperience
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
|
|
||||||
|
const RARITY_BASE_EXPERIENCE: Array[int] = [10, 18, 32, 55, 90]
|
||||||
|
const QUALITY_MULTIPLIERS: Array[float] = [1.0, 1.15, 1.4, 1.8, 2.5]
|
||||||
|
const MAXIMUM_WEIGHT_BONUS: float = 0.25
|
||||||
|
const FIRST_SPECIES_BONUS: int = 25
|
||||||
|
const FIRST_QUALITY_BONUS: int = 15
|
||||||
|
const SPECIES_MASTERY_BONUS: int = 100
|
||||||
|
|
||||||
|
|
||||||
|
static func calculate_for_collection(
|
||||||
|
fish_catch: FishCatchType,
|
||||||
|
collection_log: CollectionLogType,
|
||||||
|
) -> int:
|
||||||
|
if fish_catch == null or collection_log == null:
|
||||||
|
return 0
|
||||||
|
return calculate_catch_experience(
|
||||||
|
fish_catch,
|
||||||
|
collection_log.has_discovered(fish_catch.fish_id),
|
||||||
|
collection_log.get_quality_mask(fish_catch.fish_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func calculate_catch_experience(
|
||||||
|
fish_catch: FishCatchType,
|
||||||
|
was_species_discovered: bool,
|
||||||
|
previous_quality_mask: int,
|
||||||
|
) -> int:
|
||||||
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
|
return 0
|
||||||
|
var rarity: int = int(fish_catch.fish.rarity)
|
||||||
|
if rarity < 0 or rarity >= RARITY_BASE_EXPERIENCE.size():
|
||||||
|
return 0
|
||||||
|
if not FishQualityType.is_valid(fish_catch.quality):
|
||||||
|
return 0
|
||||||
|
var weight_percentile: float = _get_weight_percentile(fish_catch)
|
||||||
|
var weight_multiplier: float = (
|
||||||
|
1.0 + MAXIMUM_WEIGHT_BONUS * weight_percentile * weight_percentile
|
||||||
|
)
|
||||||
|
var catch_experience: int = roundi(
|
||||||
|
float(RARITY_BASE_EXPERIENCE[rarity])
|
||||||
|
* QUALITY_MULTIPLIERS[fish_catch.quality]
|
||||||
|
* weight_multiplier
|
||||||
|
)
|
||||||
|
var quality_bit: int = FishQualityType.bit_for(fish_catch.quality)
|
||||||
|
if not was_species_discovered:
|
||||||
|
catch_experience += FIRST_SPECIES_BONUS
|
||||||
|
if (previous_quality_mask & quality_bit) == 0:
|
||||||
|
catch_experience += FIRST_QUALITY_BONUS
|
||||||
|
var next_quality_mask: int = previous_quality_mask | quality_bit
|
||||||
|
if (
|
||||||
|
previous_quality_mask != FishQualityType.ALL_TIERS_MASK
|
||||||
|
and next_quality_mask == FishQualityType.ALL_TIERS_MASK
|
||||||
|
):
|
||||||
|
catch_experience += SPECIES_MASTERY_BONUS
|
||||||
|
return maxi(catch_experience, 0)
|
||||||
|
|
||||||
|
|
||||||
|
static func _get_weight_percentile(fish_catch: FishCatchType) -> float:
|
||||||
|
var minimum_weight: float = fish_catch.fish.get_minimum_weight()
|
||||||
|
var maximum_weight: float = fish_catch.fish.get_maximum_weight()
|
||||||
|
if maximum_weight <= minimum_weight:
|
||||||
|
return 0.0
|
||||||
|
return clampf(
|
||||||
|
inverse_lerp(minimum_weight, maximum_weight, fish_catch.weight_lb),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
1
fish/fish_experience.gd.uid
Normal file
1
fish/fish_experience.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d05mwat7tju1b
|
||||||
91
fish/fish_quality.gd
Normal file
91
fish/fish_quality.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
class_name FishQuality
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
enum Tier {
|
||||||
|
BORING,
|
||||||
|
AVERAGE,
|
||||||
|
IMPRESSIVE,
|
||||||
|
EXCEPTIONAL,
|
||||||
|
SHINY,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIER_COUNT: int = 5
|
||||||
|
const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
|
||||||
|
|
||||||
|
# Starting distribution. Equipment and tackle can supply per-tier
|
||||||
|
# multipliers later without changing catch serialization or tier identity.
|
||||||
|
const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
|
||||||
|
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
|
||||||
|
const DISPLAY_NAMES: PackedStringArray = [
|
||||||
|
"boring",
|
||||||
|
"average",
|
||||||
|
"impressive",
|
||||||
|
"exceptional",
|
||||||
|
"shiny",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid(quality: int) -> bool:
|
||||||
|
return quality >= 0 and quality < TIER_COUNT
|
||||||
|
|
||||||
|
|
||||||
|
static func display_name(quality: int) -> String:
|
||||||
|
return DISPLAY_NAMES[quality] if is_valid(quality) else "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
static func bit_for(quality: int) -> int:
|
||||||
|
return 1 << quality if is_valid(quality) else 0
|
||||||
|
|
||||||
|
|
||||||
|
static func sale_multiplier(quality: int) -> float:
|
||||||
|
return SALE_MULTIPLIERS[quality] if is_valid(quality) else 1.0
|
||||||
|
|
||||||
|
|
||||||
|
static func apply_sale_value(base_value: int, quality: int) -> int:
|
||||||
|
if base_value <= 0 or not is_valid(quality):
|
||||||
|
return maxi(base_value, 0)
|
||||||
|
if quality == Tier.BORING:
|
||||||
|
return base_value
|
||||||
|
return maxi(
|
||||||
|
base_value + quality,
|
||||||
|
ceili(float(base_value) * sale_multiplier(quality)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func roll(
|
||||||
|
rng: RandomNumberGenerator,
|
||||||
|
weight_multipliers: Array[float] = [],
|
||||||
|
) -> int:
|
||||||
|
if rng == null:
|
||||||
|
return Tier.BORING
|
||||||
|
var weights: Array[float] = []
|
||||||
|
var total_weight: float = 0.0
|
||||||
|
for quality: int in TIER_COUNT:
|
||||||
|
var multiplier: float = 1.0
|
||||||
|
if quality < weight_multipliers.size():
|
||||||
|
multiplier = maxf(weight_multipliers[quality], 0.0)
|
||||||
|
var weight: float = BASE_ROLL_WEIGHTS[quality] * multiplier
|
||||||
|
weights.append(weight)
|
||||||
|
total_weight += weight
|
||||||
|
if total_weight <= 0.0:
|
||||||
|
return Tier.BORING
|
||||||
|
var rolled_weight: float = rng.randf() * total_weight
|
||||||
|
var accumulated_weight: float = 0.0
|
||||||
|
for quality: int in TIER_COUNT:
|
||||||
|
accumulated_weight += weights[quality]
|
||||||
|
if rolled_weight <= accumulated_weight:
|
||||||
|
return quality
|
||||||
|
return Tier.SHINY
|
||||||
|
|
||||||
|
|
||||||
|
static func qualified_name(fish_name: String, quality: int) -> String:
|
||||||
|
return "%s %s" % [display_name(quality), fish_name]
|
||||||
|
|
||||||
|
|
||||||
|
static func qualified_name_with_article(
|
||||||
|
fish_name: String,
|
||||||
|
quality: int,
|
||||||
|
) -> String:
|
||||||
|
var quality_name: String = display_name(quality)
|
||||||
|
var article: String = "an" if quality_name[0] in ["a", "e", "i", "o", "u"] else "a"
|
||||||
|
return "%s %s %s" % [article, quality_name, fish_name]
|
||||||
1
fish/fish_quality.gd.uid
Normal file
1
fish/fish_quality.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://csmvl1ciuux6a
|
||||||
|
|
@ -5,10 +5,12 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||||
|
|
||||||
var undiscovered_weight_multiplier: float = 1.5
|
var undiscovered_weight_multiplier: float = 1.5
|
||||||
var rarity_weight_multipliers: Array[float] = []
|
var rarity_weight_multipliers: Array[float] = []
|
||||||
|
var quality_weight_multipliers: Array[float] = []
|
||||||
var use_deterministic_test_seed: bool = false
|
var use_deterministic_test_seed: bool = false
|
||||||
var deterministic_test_seed: int = 24680
|
var deterministic_test_seed: int = 24680
|
||||||
var selection_seed: int = 0
|
var selection_seed: int = 0
|
||||||
|
|
@ -81,7 +83,12 @@ func create_catch(fish: FishDataType) -> FishCatchType:
|
||||||
caught_fish.display_scale = fish.get_display_scale_for_weight(
|
caught_fish.display_scale = fish.get_display_scale_for_weight(
|
||||||
caught_fish.weight_lb
|
caught_fish.weight_lb
|
||||||
)
|
)
|
||||||
caught_fish.sale_value = fish.get_sale_value_for_weight(
|
caught_fish.quality = FishQualityType.roll(
|
||||||
caught_fish.weight_lb
|
_rng,
|
||||||
|
quality_weight_multipliers,
|
||||||
|
)
|
||||||
|
caught_fish.sale_value = FishQualityType.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(caught_fish.weight_lb),
|
||||||
|
caught_fish.quality,
|
||||||
)
|
)
|
||||||
return caught_fish
|
return caught_fish
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ const CatchControllerType = preload("res://fishing/catch_controller.gd")
|
||||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||||
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
|
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const ItemDataType = preload("res://items/item_data.gd")
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
|
|
@ -56,6 +60,7 @@ signal showcase_changed(
|
||||||
fish_name: String,
|
fish_name: String,
|
||||||
rarity_name: String,
|
rarity_name: String,
|
||||||
weight_lb: float,
|
weight_lb: float,
|
||||||
|
quality: int,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
)
|
)
|
||||||
signal bite_activated
|
signal bite_activated
|
||||||
|
|
@ -100,9 +105,9 @@ const BITE_QUICK_MAX_SECONDS: float = 30.0
|
||||||
const BITE_TYPICAL_MAX_SECONDS: float = 90.0
|
const BITE_TYPICAL_MAX_SECONDS: float = 90.0
|
||||||
const BITE_LONG_MAX_SECONDS: float = 180.0
|
const BITE_LONG_MAX_SECONDS: float = 180.0
|
||||||
const BITE_MAX_SECONDS: float = 240.0
|
const BITE_MAX_SECONDS: float = 240.0
|
||||||
const BITE_QUICK_PROBABILITY: float = 0.15
|
const BITE_QUICK_PROBABILITY: float = 0.25
|
||||||
const BITE_TYPICAL_PROBABILITY: float = 0.55
|
const BITE_TYPICAL_PROBABILITY: float = 0.65
|
||||||
const BITE_LONG_PROBABILITY: float = 0.25
|
const BITE_LONG_PROBABILITY: float = 0.08
|
||||||
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
||||||
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
|
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
|
||||||
|
|
||||||
|
|
@ -125,6 +130,7 @@ var _local_menu_input_owners: Dictionary[StringName, bool] = {}
|
||||||
var _local_player: PlayerType
|
var _local_player: PlayerType
|
||||||
var _local_inventory: FishInventoryType
|
var _local_inventory: FishInventoryType
|
||||||
var _local_collection_log: CollectionLogType
|
var _local_collection_log: CollectionLogType
|
||||||
|
var _local_experience: PlayerExperienceType
|
||||||
var _local_bag: PlayerBagType
|
var _local_bag: PlayerBagType
|
||||||
var _local_hotbar: PlayerHotbarType
|
var _local_hotbar: PlayerHotbarType
|
||||||
var _item_catalog: ItemCatalogType
|
var _item_catalog: ItemCatalogType
|
||||||
|
|
@ -185,6 +191,7 @@ func setup(
|
||||||
local_player: PlayerType,
|
local_player: PlayerType,
|
||||||
local_inventory: FishInventoryType,
|
local_inventory: FishInventoryType,
|
||||||
local_collection_log: CollectionLogType,
|
local_collection_log: CollectionLogType,
|
||||||
|
local_experience: PlayerExperienceType,
|
||||||
local_bag: PlayerBagType,
|
local_bag: PlayerBagType,
|
||||||
local_hotbar: PlayerHotbarType,
|
local_hotbar: PlayerHotbarType,
|
||||||
item_catalog: ItemCatalogType,
|
item_catalog: ItemCatalogType,
|
||||||
|
|
@ -200,6 +207,7 @@ func setup(
|
||||||
_local_player = local_player
|
_local_player = local_player
|
||||||
_local_inventory = local_inventory
|
_local_inventory = local_inventory
|
||||||
_local_collection_log = local_collection_log
|
_local_collection_log = local_collection_log
|
||||||
|
_local_experience = local_experience
|
||||||
_local_bag = local_bag
|
_local_bag = local_bag
|
||||||
_local_hotbar = local_hotbar
|
_local_hotbar = local_hotbar
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
|
|
@ -381,12 +389,11 @@ func _secure_showcase_catch_for_recovery() -> void:
|
||||||
and _local_inventory != null
|
and _local_inventory != null
|
||||||
and _local_collection_log != null
|
and _local_collection_log != null
|
||||||
):
|
):
|
||||||
_local_inventory.add_catch(_pending_catch)
|
_store_catch_progression(_pending_catch)
|
||||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
|
||||||
_pending_catch = null
|
_pending_catch = null
|
||||||
_showcase_ready = false
|
_showcase_ready = false
|
||||||
_put_away_press_armed = false
|
_put_away_press_armed = false
|
||||||
showcase_changed.emit("", "", 0.0, false)
|
showcase_changed.emit("", "", 0.0, 0, false)
|
||||||
if _active_player != null:
|
if _active_player != null:
|
||||||
_active_player.end_catch_showcase(Callable(), true)
|
_active_player.end_catch_showcase(Callable(), true)
|
||||||
_cleanup_attempt()
|
_cleanup_attempt()
|
||||||
|
|
@ -402,8 +409,7 @@ func _exit_tree() -> void:
|
||||||
and _local_collection_log != null
|
and _local_collection_log != null
|
||||||
and is_instance_valid(_local_collection_log)
|
and is_instance_valid(_local_collection_log)
|
||||||
):
|
):
|
||||||
_local_inventory.add_catch(_pending_catch)
|
_store_catch_progression(_pending_catch)
|
||||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
|
||||||
_pending_catch = null
|
_pending_catch = null
|
||||||
if _active_player != null and is_instance_valid(_active_player):
|
if _active_player != null and is_instance_valid(_active_player):
|
||||||
_active_player.end_catch_showcase(Callable(), true)
|
_active_player.end_catch_showcase(Callable(), true)
|
||||||
|
|
@ -1031,6 +1037,7 @@ func _on_outcome_completed(outcome: StringName) -> void:
|
||||||
_pending_catch.fish.display_name,
|
_pending_catch.fish.display_name,
|
||||||
_pending_catch.fish.get_rarity_name(),
|
_pending_catch.fish.get_rarity_name(),
|
||||||
_pending_catch.weight_lb,
|
_pending_catch.weight_lb,
|
||||||
|
_pending_catch.quality,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1053,13 +1060,12 @@ func _put_away_catch() -> void:
|
||||||
or _pending_catch == null
|
or _pending_catch == null
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
_local_inventory.add_catch(_pending_catch)
|
_store_catch_progression(_pending_catch)
|
||||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
|
||||||
_pending_catch = null
|
_pending_catch = null
|
||||||
_showcase_ready = false
|
_showcase_ready = false
|
||||||
_showcase_outcome_completed = false
|
_showcase_outcome_completed = false
|
||||||
_put_away_press_armed = false
|
_put_away_press_armed = false
|
||||||
showcase_changed.emit("", "", 0.0, false)
|
showcase_changed.emit("", "", 0.0, 0, false)
|
||||||
_showcase_restore_generation += 1
|
_showcase_restore_generation += 1
|
||||||
var restore_generation: int = _showcase_restore_generation
|
var restore_generation: int = _showcase_restore_generation
|
||||||
_active_player.end_catch_showcase(
|
_active_player.end_catch_showcase(
|
||||||
|
|
@ -1070,6 +1076,29 @@ func _put_away_catch() -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _store_catch_progression(fish_catch: FishCatchType) -> void:
|
||||||
|
if (
|
||||||
|
fish_catch == null
|
||||||
|
or _local_inventory == null
|
||||||
|
or _local_collection_log == null
|
||||||
|
or _local_experience == null
|
||||||
|
or _local_inventory.contains_catch_id(fish_catch.catch_id)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var experience_award: int = (
|
||||||
|
FishExperienceType.calculate_for_collection(
|
||||||
|
fish_catch,
|
||||||
|
_local_collection_log,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_local_inventory.add_catch(fish_catch)
|
||||||
|
_local_collection_log.mark_quality_discovered(
|
||||||
|
fish_catch.fish_id,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
_local_experience.award_experience(experience_award)
|
||||||
|
|
||||||
|
|
||||||
func _finish_showcase_put_away(
|
func _finish_showcase_put_away(
|
||||||
restore_generation: int,
|
restore_generation: int,
|
||||||
cooldown_message: String,
|
cooldown_message: String,
|
||||||
|
|
@ -1095,7 +1124,7 @@ func _cleanup_attempt(
|
||||||
_showcase_ready = false
|
_showcase_ready = false
|
||||||
_showcase_outcome_completed = false
|
_showcase_outcome_completed = false
|
||||||
_put_away_press_armed = false
|
_put_away_press_armed = false
|
||||||
showcase_changed.emit("", "", 0.0, false)
|
showcase_changed.emit("", "", 0.0, 0, false)
|
||||||
_catch_controller.reset()
|
_catch_controller.reset()
|
||||||
state = FishingState.RETURNING
|
state = FishingState.RETURNING
|
||||||
status_changed.emit("")
|
status_changed.emit("")
|
||||||
|
|
@ -1138,7 +1167,7 @@ func _finalize_attempt_cleanup(cooldown_message: String) -> void:
|
||||||
_showcase_ready = false
|
_showcase_ready = false
|
||||||
_showcase_outcome_completed = false
|
_showcase_outcome_completed = false
|
||||||
_put_away_press_armed = false
|
_put_away_press_armed = false
|
||||||
showcase_changed.emit("", "", 0.0, false)
|
showcase_changed.emit("", "", 0.0, 0, false)
|
||||||
_catch_controller.reset()
|
_catch_controller.reset()
|
||||||
_presentation.cleanup()
|
_presentation.cleanup()
|
||||||
_pending_cleanup_message = ""
|
_pending_cleanup_message = ""
|
||||||
|
|
|
||||||
14
main/main.gd
14
main/main.gd
|
|
@ -357,6 +357,8 @@ func _initialize_after_data_root() -> void:
|
||||||
_player.fishing_upgrades,
|
_player.fishing_upgrades,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_player.art_unlocks,
|
_player.art_unlocks,
|
||||||
|
_player.experience,
|
||||||
|
_world_time,
|
||||||
)
|
)
|
||||||
_save_manager.set_autosave_enabled(false)
|
_save_manager.set_autosave_enabled(false)
|
||||||
_asset_reservations.setup(
|
_asset_reservations.setup(
|
||||||
|
|
@ -411,6 +413,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
|
_player.experience,
|
||||||
_save_manager,
|
_save_manager,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
fish_catalog,
|
fish_catalog,
|
||||||
|
|
@ -451,6 +454,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_player,
|
_player,
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
|
_player.experience,
|
||||||
_player.bag,
|
_player.bag,
|
||||||
_player.hotbar,
|
_player.hotbar,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
|
|
@ -467,6 +471,7 @@ func _initialize_after_data_root() -> void:
|
||||||
_player,
|
_player,
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
|
_player.experience,
|
||||||
_player.wallet,
|
_player.wallet,
|
||||||
_player.fish_sale_service,
|
_player.fish_sale_service,
|
||||||
pelican_buyer_profile,
|
pelican_buyer_profile,
|
||||||
|
|
@ -1154,6 +1159,11 @@ func _on_return_to_title_requested() -> void:
|
||||||
if _quit_in_progress:
|
if _quit_in_progress:
|
||||||
return
|
return
|
||||||
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
|
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
|
||||||
|
if not _save_manager.save_world_time_checkpoint():
|
||||||
|
pause_menu.report_network_error(
|
||||||
|
"Could not save progression before returning to title."
|
||||||
|
)
|
||||||
|
return
|
||||||
pause_menu.close_for_title_transition()
|
pause_menu.close_for_title_transition()
|
||||||
_network_session.disconnect_session("Returned to title.")
|
_network_session.disconnect_session("Returned to title.")
|
||||||
_set_gameplay_active(false)
|
_set_gameplay_active(false)
|
||||||
|
|
@ -1176,7 +1186,7 @@ func _on_title_join_game_requested(endpoint: String) -> void:
|
||||||
func _on_pause_join_game_requested(endpoint: String) -> void:
|
func _on_pause_join_game_requested(endpoint: String) -> void:
|
||||||
if _quit_in_progress or not _gameplay_started:
|
if _quit_in_progress or not _gameplay_started:
|
||||||
return
|
return
|
||||||
if not _save_manager.save_if_dirty():
|
if not _save_manager.save_world_time_checkpoint():
|
||||||
_game_ui.get_pause_menu().report_network_error(
|
_game_ui.get_pause_menu().report_network_error(
|
||||||
"Could not save progression before leaving this session."
|
"Could not save progression before leaving this session."
|
||||||
)
|
)
|
||||||
|
|
@ -1409,7 +1419,7 @@ func _on_quit_requested() -> void:
|
||||||
_quit_in_progress = true
|
_quit_in_progress = true
|
||||||
_settings_manager.save_if_dirty()
|
_settings_manager.save_if_dirty()
|
||||||
if _gameplay_started:
|
if _gameplay_started:
|
||||||
_save_manager.save_if_dirty()
|
_save_manager.save_world_time_checkpoint()
|
||||||
_network_session.disconnect_session("Application closing.")
|
_network_session.disconnect_session("Application closing.")
|
||||||
_title_music_requested = false
|
_title_music_requested = false
|
||||||
_replace_title_music_transition()
|
_replace_title_music_transition()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const MAX_SESSION_ID_LENGTH: int = 96
|
||||||
const MAX_FISH_ID_LENGTH: int = 96
|
const MAX_FISH_ID_LENGTH: int = 96
|
||||||
const MAX_WEIGHT_LB: float = 1000.0
|
const MAX_WEIGHT_LB: float = 1000.0
|
||||||
const MAX_DISPLAY_SCALE: float = 20.0
|
const MAX_DISPLAY_SCALE: float = 20.0
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
|
|
||||||
static func validate_state(data: Variant) -> bool:
|
static func validate_state(data: Variant) -> bool:
|
||||||
|
|
@ -20,6 +21,7 @@ static func validate_state(data: Variant) -> bool:
|
||||||
"fish_id",
|
"fish_id",
|
||||||
"weight_lb",
|
"weight_lb",
|
||||||
"display_scale",
|
"display_scale",
|
||||||
|
"quality",
|
||||||
"revision",
|
"revision",
|
||||||
]:
|
]:
|
||||||
if not value.has(key):
|
if not value.has(key):
|
||||||
|
|
@ -37,6 +39,8 @@ static func validate_state(data: Variant) -> bool:
|
||||||
or not is_finite(float(value["weight_lb"]))
|
or not is_finite(float(value["weight_lb"]))
|
||||||
or typeof(value["display_scale"]) not in [TYPE_FLOAT, TYPE_INT]
|
or typeof(value["display_scale"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
or not is_finite(float(value["display_scale"]))
|
or not is_finite(float(value["display_scale"]))
|
||||||
|
or typeof(value["quality"]) != TYPE_INT
|
||||||
|
or not FishQualityType.is_valid(int(value["quality"]))
|
||||||
or typeof(value["revision"]) != TYPE_INT
|
or typeof(value["revision"]) != TYPE_INT
|
||||||
or int(value["revision"]) < 0
|
or int(value["revision"]) < 0
|
||||||
):
|
):
|
||||||
|
|
@ -53,4 +57,5 @@ static func validate_state(data: Variant) -> bool:
|
||||||
str(value["fish_id"]).is_empty()
|
str(value["fish_id"]).is_empty()
|
||||||
and is_zero_approx(float(value["weight_lb"]))
|
and is_zero_approx(float(value["weight_lb"]))
|
||||||
and is_zero_approx(float(value["display_scale"]))
|
and is_zero_approx(float(value["display_scale"]))
|
||||||
|
and int(value["quality"]) == FishQualityType.Tier.BORING
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends Node
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
|
||||||
|
|
@ -80,6 +81,11 @@ func _submit_local_state(fish_catch: FishCatchType, should_show: bool) -> void:
|
||||||
"display_scale": (
|
"display_scale": (
|
||||||
fish_catch.display_scale if fish_catch != null else 0.0
|
fish_catch.display_scale if fish_catch != null else 0.0
|
||||||
),
|
),
|
||||||
|
"quality": (
|
||||||
|
fish_catch.quality
|
||||||
|
if fish_catch != null
|
||||||
|
else FishQualityType.Tier.BORING
|
||||||
|
),
|
||||||
"revision": _local_revision,
|
"revision": _local_revision,
|
||||||
}
|
}
|
||||||
_local_visible = bool(data["visible"])
|
_local_visible = bool(data["visible"])
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,10 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
const FishSelectorType = preload("res://fish/fish_selector.gd")
|
const FishSelectorType = preload("res://fish/fish_selector.gd")
|
||||||
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
const FishingContextType = preload("res://fishing/fishing_context.gd")
|
||||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
|
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
const RemotePresentationType = preload(
|
const RemotePresentationType = preload(
|
||||||
"res://fishing/remote_fishing_presentation.gd"
|
"res://fishing/remote_fishing_presentation.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -29,6 +33,7 @@ var _fishing_spot: FishingSpot
|
||||||
var _local_inventory: FishInventory
|
var _local_inventory: FishInventory
|
||||||
var _local_collection: CollectionLog
|
var _local_collection: CollectionLog
|
||||||
var _local_capacity: PlayerCoolerCapacity
|
var _local_capacity: PlayerCoolerCapacity
|
||||||
|
var _local_experience: PlayerExperienceType
|
||||||
var _save_manager: PlayerSaveManager
|
var _save_manager: PlayerSaveManager
|
||||||
var _item_catalog: ItemCatalog
|
var _item_catalog: ItemCatalog
|
||||||
var _fish_catalog: FishPoolType
|
var _fish_catalog: FishPoolType
|
||||||
|
|
@ -51,6 +56,7 @@ func setup(
|
||||||
local_inventory: FishInventory,
|
local_inventory: FishInventory,
|
||||||
local_collection: CollectionLog,
|
local_collection: CollectionLog,
|
||||||
local_capacity: PlayerCoolerCapacity,
|
local_capacity: PlayerCoolerCapacity,
|
||||||
|
local_experience: PlayerExperienceType,
|
||||||
save_manager: PlayerSaveManager,
|
save_manager: PlayerSaveManager,
|
||||||
item_catalog: ItemCatalog,
|
item_catalog: ItemCatalog,
|
||||||
fish_catalog: FishPoolType,
|
fish_catalog: FishPoolType,
|
||||||
|
|
@ -62,6 +68,7 @@ func setup(
|
||||||
_local_inventory = local_inventory
|
_local_inventory = local_inventory
|
||||||
_local_collection = local_collection
|
_local_collection = local_collection
|
||||||
_local_capacity = local_capacity
|
_local_capacity = local_capacity
|
||||||
|
_local_experience = local_experience
|
||||||
_save_manager = save_manager
|
_save_manager = save_manager
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
_fish_catalog = fish_catalog
|
_fish_catalog = fish_catalog
|
||||||
|
|
@ -760,8 +767,18 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if not already_owned:
|
if not already_owned:
|
||||||
|
var experience_award: int = (
|
||||||
|
FishExperienceType.calculate_for_collection(
|
||||||
|
fish_catch,
|
||||||
|
_local_collection,
|
||||||
|
)
|
||||||
|
)
|
||||||
_local_inventory.add_catch(fish_catch)
|
_local_inventory.add_catch(fish_catch)
|
||||||
_local_collection.mark_discovered(fish_id)
|
_local_collection.mark_quality_discovered(
|
||||||
|
fish_id,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
_local_experience.award_experience(experience_award)
|
||||||
if not _save_manager.save_if_dirty():
|
if not _save_manager.save_if_dirty():
|
||||||
return
|
return
|
||||||
_result_ledgers[result_id] = true
|
_result_ledgers[result_id] = true
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ static func attachment_signature_fields(value: Variant) -> Array:
|
||||||
result.append(str(fish.get("fish_id", "")))
|
result.append(str(fish.get("fish_id", "")))
|
||||||
result.append(str(fish.get("weight_lb", "")))
|
result.append(str(fish.get("weight_lb", "")))
|
||||||
result.append(str(fish.get("display_scale", "")))
|
result.append(str(fish.get("display_scale", "")))
|
||||||
|
result.append(int(fish.get("quality", -1)))
|
||||||
result.append(int(fish.get("sale_value", 0)))
|
result.append(int(fish.get("sale_value", 0)))
|
||||||
result.append(bool(fish.get("is_favorited", false)))
|
result.append(bool(fish.get("is_favorited", false)))
|
||||||
3:
|
3:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ signal operation_finished(success: bool, message: String)
|
||||||
signal peers_changed
|
signal peers_changed
|
||||||
|
|
||||||
const MAX_LEDGER: int = 256
|
const MAX_LEDGER: int = 256
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
var _session: NetworkSession
|
var _session: NetworkSession
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
|
|
@ -950,7 +951,10 @@ func _apply_award(attachment: Dictionary) -> bool:
|
||||||
PlayerAssetReservationService.AttachmentType.FISH:
|
PlayerAssetReservationService.AttachmentType.FISH:
|
||||||
var fish_catch := _decode_catch(attachment)
|
var fish_catch := _decode_catch(attachment)
|
||||||
_inventory.add_catch(fish_catch)
|
_inventory.add_catch(fish_catch)
|
||||||
_collection_log.mark_discovered(fish_catch.fish_id)
|
_collection_log.mark_quality_discovered(
|
||||||
|
fish_catch.fish_id,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
return _inventory.contains_catch_id(fish_catch.catch_id)
|
return _inventory.contains_catch_id(fish_catch.catch_id)
|
||||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||||
return _bag.add_item(
|
return _bag.add_item(
|
||||||
|
|
@ -962,10 +966,26 @@ func _apply_award(attachment: Dictionary) -> bool:
|
||||||
|
|
||||||
func _decode_catch(attachment: Dictionary) -> FishCatch:
|
func _decode_catch(attachment: Dictionary) -> FishCatch:
|
||||||
var data: Dictionary = attachment.get("catch", {})
|
var data: Dictionary = attachment.get("catch", {})
|
||||||
var fish := _fish_catalog.get_fish_by_id(
|
var fish: FishData = _fish_catalog.get_fish_by_id(
|
||||||
StringName(str(data.get("fish_id", "")))
|
StringName(str(data.get("fish_id", "")))
|
||||||
)
|
)
|
||||||
return FishCatch.from_network_dict(data, fish)
|
var fish_catch: FishCatch = FishCatch.from_network_dict(data, fish)
|
||||||
|
if fish_catch == null or fish == null:
|
||||||
|
return null
|
||||||
|
if (
|
||||||
|
fish_catch.weight_lb < fish.get_minimum_weight()
|
||||||
|
or fish_catch.weight_lb > fish.get_maximum_weight()
|
||||||
|
or not is_equal_approx(
|
||||||
|
fish_catch.display_scale,
|
||||||
|
fish.get_display_scale_for_weight(fish_catch.weight_lb),
|
||||||
|
)
|
||||||
|
or fish_catch.sale_value != FishQualityType.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
return fish_catch
|
||||||
|
|
||||||
|
|
||||||
func _capture_assets() -> Dictionary:
|
func _capture_assets() -> Dictionary:
|
||||||
|
|
@ -975,6 +995,7 @@ func _capture_assets() -> Dictionary:
|
||||||
"catches": _inventory.get_all_catches(),
|
"catches": _inventory.get_all_catches(),
|
||||||
"next_sequence": _inventory.get_next_catch_sequence(),
|
"next_sequence": _inventory.get_next_catch_sequence(),
|
||||||
"discovered": _collection_log.get_discovered_ids(),
|
"discovered": _collection_log.get_discovered_ids(),
|
||||||
|
"quality_masks": _collection_log.get_discovered_quality_masks(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -984,7 +1005,10 @@ func _restore_assets(snapshot: Dictionary) -> void:
|
||||||
_inventory.replace_all_catches(
|
_inventory.replace_all_catches(
|
||||||
snapshot["catches"], int(snapshot["next_sequence"])
|
snapshot["catches"], int(snapshot["next_sequence"])
|
||||||
)
|
)
|
||||||
_collection_log.replace_discovered_ids(snapshot["discovered"])
|
_collection_log.replace_discovery_state(
|
||||||
|
snapshot["discovered"],
|
||||||
|
snapshot["quality_masks"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _emit_mailbox() -> void:
|
func _emit_mailbox() -> void:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
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"
|
||||||
|
|
||||||
enum RejectionCode {
|
enum RejectionCode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -88,6 +89,7 @@ static func make_client_hello(
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"client_nonce": client_nonce,
|
"client_nonce": client_nonce,
|
||||||
"capability_flags": PackedStringArray([
|
"capability_flags": PackedStringArray([
|
||||||
|
FISH_QUALITY_CAPABILITY,
|
||||||
SURFACE_DRAWING_CAPABILITY,
|
SURFACE_DRAWING_CAPABILITY,
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
|
|
@ -207,6 +209,7 @@ static func make_server_hello(
|
||||||
"item_use_v1",
|
"item_use_v1",
|
||||||
"equipment_v1",
|
"equipment_v1",
|
||||||
"fish_showcase_v1",
|
"fish_showcase_v1",
|
||||||
|
FISH_QUALITY_CAPABILITY,
|
||||||
SURFACE_DRAWING_CAPABILITY,
|
SURFACE_DRAWING_CAPABILITY,
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ static func validate_request(data: Variant) -> String:
|
||||||
var evidence: Dictionary = value
|
var evidence: Dictionary = value
|
||||||
for key: String in [
|
for key: String in [
|
||||||
"catch_id", "fish_id", "weight_lb", "display_scale",
|
"catch_id", "fish_id", "weight_lb", "display_scale",
|
||||||
"sale_value", "is_favorited",
|
"quality", "sale_value", "is_favorited",
|
||||||
]:
|
]:
|
||||||
if not evidence.has(key):
|
if not evidence.has(key):
|
||||||
return "Sale could not be completed."
|
return "Sale could not be completed."
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ class_name NetworkSaleService
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||||
|
|
@ -252,8 +253,10 @@ func _build_authoritative_result(
|
||||||
not str(decoded.catch_id).begins_with("%s:" % fish_id)
|
not str(decoded.catch_id).begins_with("%s:" % fish_id)
|
||||||
or decoded.weight_lb < fish.get_minimum_weight()
|
or decoded.weight_lb < fish.get_minimum_weight()
|
||||||
or decoded.weight_lb > fish.get_maximum_weight()
|
or decoded.weight_lb > fish.get_maximum_weight()
|
||||||
or decoded.sale_value < fish.sell_value_min
|
or decoded.sale_value != FishQualityType.apply_sale_value(
|
||||||
or decoded.sale_value > fish.sell_value_max
|
fish.get_sale_value_for_weight(decoded.weight_lb),
|
||||||
|
decoded.quality,
|
||||||
|
)
|
||||||
):
|
):
|
||||||
return _rejected_result(
|
return _rejected_result(
|
||||||
request_id, "Sale could not be completed."
|
request_id, "Sale could not be completed."
|
||||||
|
|
@ -262,7 +265,13 @@ func _build_authoritative_result(
|
||||||
return _rejected_result(
|
return _rejected_result(
|
||||||
request_id, "Favorite catches cannot be sold."
|
request_id, "Favorite catches cannot be sold."
|
||||||
)
|
)
|
||||||
var offer: int = buyer.get_offer(decoded.sale_value)
|
var ordinary_value: int = fish.get_sale_value_for_weight(
|
||||||
|
decoded.weight_lb
|
||||||
|
)
|
||||||
|
var offer: int = buyer.get_quality_offer(
|
||||||
|
ordinary_value,
|
||||||
|
decoded.quality,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
offer < 0
|
offer < 0
|
||||||
or base_value > 9223372036854775807 - decoded.sale_value
|
or base_value > 9223372036854775807 - decoded.sale_value
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ func start_private_host(port: int = DEFAULT_PORT) -> bool:
|
||||||
_player_identity.fingerprint,
|
_player_identity.fingerprint,
|
||||||
_player_identity.public_pem,
|
_player_identity.public_pem,
|
||||||
PackedStringArray([
|
PackedStringArray([
|
||||||
|
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
|
|
@ -367,6 +368,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||||
|
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
|
|
@ -915,6 +917,15 @@ func submit_client_hello(data: Dictionary) -> void:
|
||||||
if not validation_error.is_empty():
|
if not validation_error.is_empty():
|
||||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE)
|
_reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE)
|
||||||
return
|
return
|
||||||
|
var client_capabilities: PackedStringArray = _sanitized_capabilities(
|
||||||
|
data.get("capability_flags", [])
|
||||||
|
)
|
||||||
|
if NetworkProtocol.FISH_QUALITY_CAPABILITY not in client_capabilities:
|
||||||
|
_reject_peer(
|
||||||
|
sender_id,
|
||||||
|
NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
|
||||||
|
)
|
||||||
|
return
|
||||||
var identity: Dictionary = _authenticated_identity_cache.get(sender_id, {})
|
var identity: Dictionary = _authenticated_identity_cache.get(sender_id, {})
|
||||||
if identity.is_empty():
|
if identity.is_empty():
|
||||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
|
_reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
|
||||||
|
|
@ -948,7 +959,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
||||||
NetworkProtocol.PROTOCOL_VERSION,
|
NetworkProtocol.PROTOCOL_VERSION,
|
||||||
identity["fingerprint"],
|
identity["fingerprint"],
|
||||||
identity["public_key"],
|
identity["public_key"],
|
||||||
_sanitized_capabilities(data.get("capability_flags", [])),
|
client_capabilities,
|
||||||
):
|
):
|
||||||
_reject_peer(
|
_reject_peer(
|
||||||
sender_id,
|
sender_id,
|
||||||
|
|
@ -1071,6 +1082,10 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
for value: Variant in advertised_capabilities:
|
for value: Variant in advertised_capabilities:
|
||||||
if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
|
if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
_server_capabilities.append(str(value))
|
_server_capabilities.append(str(value))
|
||||||
|
if NetworkProtocol.FISH_QUALITY_CAPABILITY not in _server_capabilities:
|
||||||
|
_teardown_peer()
|
||||||
|
_fail("This server does not support fish quality data.")
|
||||||
|
return
|
||||||
var local_peer_id: int = multiplayer.get_unique_id()
|
var local_peer_id: int = multiplayer.get_unique_id()
|
||||||
_registry.clear()
|
_registry.clear()
|
||||||
_registry.add_peer(
|
_registry.add_peer(
|
||||||
|
|
@ -1081,6 +1096,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
_player_identity.fingerprint,
|
_player_identity.fingerprint,
|
||||||
_player_identity.public_pem,
|
_player_identity.public_pem,
|
||||||
PackedStringArray([
|
PackedStringArray([
|
||||||
|
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,16 @@ 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_time.begin_session()
|
_world_time.set_persistence_tracking_enabled(true)
|
||||||
|
_world_time.begin_session(
|
||||||
|
_world_time.get_persistent_time_hours()
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if state == NetworkSession.State.JOINED_CLIENT:
|
if state == NetworkSession.State.JOINED_CLIENT:
|
||||||
_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_time.set_persistence_tracking_enabled(false)
|
||||||
if _session.supports_server_capability(
|
if _session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||||
):
|
):
|
||||||
|
|
@ -62,6 +66,7 @@ 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_time.set_persistence_tracking_enabled(false)
|
||||||
_world_time.end_session()
|
_world_time.end_session()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ const PlayerCoolerCapacityType = preload(
|
||||||
const PlayerArtUnlocksType = preload(
|
const PlayerArtUnlocksType = preload(
|
||||||
"res://progression/player_art_unlocks.gd"
|
"res://progression/player_art_unlocks.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
const FishingRodAttachmentScene = preload(
|
const FishingRodAttachmentScene = preload(
|
||||||
"res://player/fishing_rod_attachment.tscn"
|
"res://player/fishing_rod_attachment.tscn"
|
||||||
)
|
)
|
||||||
|
|
@ -109,6 +112,7 @@ class ShowcaseCameraSnapshot:
|
||||||
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
||||||
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
||||||
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
||||||
|
@onready var experience: PlayerExperienceType = %Experience
|
||||||
@onready var _cast_origin: Marker3D = %CastOrigin
|
@onready var _cast_origin: Marker3D = %CastOrigin
|
||||||
@onready var _catch_display: Node3D = %CatchDisplay
|
@onready var _catch_display: Node3D = %CatchDisplay
|
||||||
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=17 format=3]
|
[gd_scene load_steps=18 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
[ext_resource type="Material" path="res://player/materials/player_blob_shadow.tres" id="12_blob_shadow"]
|
[ext_resource type="Material" path="res://player/materials/player_blob_shadow.tres" id="12_blob_shadow"]
|
||||||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
||||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||||
|
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||||
radius = 0.45
|
radius = 0.45
|
||||||
|
|
@ -104,6 +105,10 @@ script = ExtResource("10_capacity")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("14_art_unlocks")
|
script = ExtResource("14_art_unlocks")
|
||||||
|
|
||||||
|
[node name="Experience" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("15_experience")
|
||||||
|
|
||||||
[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
|
[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector3(0, 1.45, -1.15)
|
position = Vector3(0, 1.45, -1.15)
|
||||||
|
|
|
||||||
130
progression/player_experience.gd
Normal file
130
progression/player_experience.gd
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
class_name PlayerExperience
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal experience_changed(total_experience: int, level: int)
|
||||||
|
signal experience_awarded(
|
||||||
|
amount: int,
|
||||||
|
previous_total: int,
|
||||||
|
new_total: int,
|
||||||
|
previous_level: int,
|
||||||
|
new_level: int,
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_TOTAL_EXPERIENCE: int = 1000000000000
|
||||||
|
const MAX_LEVEL: int = 100000
|
||||||
|
|
||||||
|
var _total_experience: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func get_total_experience() -> int:
|
||||||
|
return _total_experience
|
||||||
|
|
||||||
|
|
||||||
|
func get_level() -> int:
|
||||||
|
return level_for_total_experience(_total_experience)
|
||||||
|
|
||||||
|
|
||||||
|
func get_experience_in_level() -> int:
|
||||||
|
return _total_experience - total_experience_for_level(get_level())
|
||||||
|
|
||||||
|
|
||||||
|
func get_experience_for_next_level() -> int:
|
||||||
|
return experience_required_for_next_level(get_level())
|
||||||
|
|
||||||
|
|
||||||
|
func get_level_progress() -> float:
|
||||||
|
return progress_for_total_experience(_total_experience)
|
||||||
|
|
||||||
|
|
||||||
|
func award_experience(amount: int) -> bool:
|
||||||
|
if amount <= 0 or _total_experience >= MAX_TOTAL_EXPERIENCE:
|
||||||
|
return false
|
||||||
|
var previous_total: int = _total_experience
|
||||||
|
var previous_level: int = level_for_total_experience(previous_total)
|
||||||
|
_total_experience = mini(
|
||||||
|
_total_experience + amount,
|
||||||
|
MAX_TOTAL_EXPERIENCE,
|
||||||
|
)
|
||||||
|
var awarded_amount: int = _total_experience - previous_total
|
||||||
|
if awarded_amount <= 0:
|
||||||
|
return false
|
||||||
|
var new_level: int = level_for_total_experience(_total_experience)
|
||||||
|
experience_changed.emit(_total_experience, new_level)
|
||||||
|
experience_awarded.emit(
|
||||||
|
awarded_amount,
|
||||||
|
previous_total,
|
||||||
|
_total_experience,
|
||||||
|
previous_level,
|
||||||
|
new_level,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func restore_total_experience(total_experience: int) -> bool:
|
||||||
|
if total_experience < 0 or total_experience > MAX_TOTAL_EXPERIENCE:
|
||||||
|
return false
|
||||||
|
var changed: bool = _total_experience != total_experience
|
||||||
|
_total_experience = total_experience
|
||||||
|
if changed:
|
||||||
|
experience_changed.emit(_total_experience, get_level())
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func reset_to_defaults() -> void:
|
||||||
|
restore_total_experience(0)
|
||||||
|
|
||||||
|
|
||||||
|
func to_save_data() -> Dictionary:
|
||||||
|
return {"total_experience": _total_experience}
|
||||||
|
|
||||||
|
|
||||||
|
static func experience_required_for_next_level(level: int) -> int:
|
||||||
|
var safe_level: int = maxi(level, 1)
|
||||||
|
var level_index: int = safe_level - 1
|
||||||
|
return 100 + level_index * 25 + level_index * level_index * 5
|
||||||
|
|
||||||
|
|
||||||
|
static func total_experience_for_level(level: int) -> int:
|
||||||
|
var completed_levels: int = clampi(level - 1, 0, MAX_LEVEL - 1)
|
||||||
|
if completed_levels == 0:
|
||||||
|
return 0
|
||||||
|
var linear_sum: int = floori(
|
||||||
|
float(completed_levels) * float(completed_levels - 1) / 2.0
|
||||||
|
)
|
||||||
|
var square_sum: int = floori(
|
||||||
|
float(completed_levels - 1)
|
||||||
|
* float(completed_levels)
|
||||||
|
* float(2 * completed_levels - 1)
|
||||||
|
/ 6.0
|
||||||
|
)
|
||||||
|
return completed_levels * 100 + linear_sum * 25 + square_sum * 5
|
||||||
|
|
||||||
|
|
||||||
|
static func level_for_total_experience(total_experience: int) -> int:
|
||||||
|
var safe_total: int = clampi(
|
||||||
|
total_experience,
|
||||||
|
0,
|
||||||
|
MAX_TOTAL_EXPERIENCE,
|
||||||
|
)
|
||||||
|
var low: int = 1
|
||||||
|
var high: int = MAX_LEVEL
|
||||||
|
while low < high:
|
||||||
|
var middle: int = low + floori(float(high - low + 1) / 2.0)
|
||||||
|
if total_experience_for_level(middle) <= safe_total:
|
||||||
|
low = middle
|
||||||
|
else:
|
||||||
|
high = middle - 1
|
||||||
|
return low
|
||||||
|
|
||||||
|
|
||||||
|
static func progress_for_total_experience(total_experience: int) -> float:
|
||||||
|
var level: int = level_for_total_experience(total_experience)
|
||||||
|
var level_start: int = total_experience_for_level(level)
|
||||||
|
var required: int = experience_required_for_next_level(level)
|
||||||
|
if required <= 0:
|
||||||
|
return 0.0
|
||||||
|
return clampf(
|
||||||
|
float(total_experience - level_start) / float(required),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
1
progression/player_experience.gd.uid
Normal file
1
progression/player_experience.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://eml3k7a43e1d
|
||||||
|
|
@ -2,6 +2,7 @@ class_name PlayerSaveManager
|
||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||||
|
|
@ -20,8 +21,12 @@ const PlayerCoolerCapacityType = preload(
|
||||||
const PlayerArtUnlocksType = preload(
|
const PlayerArtUnlocksType = preload(
|
||||||
"res://progression/player_art_unlocks.gd"
|
"res://progression/player_art_unlocks.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
|
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
|
|
||||||
const SAVE_VERSION: int = 4
|
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
|
||||||
|
|
||||||
|
|
@ -30,6 +35,7 @@ class LoadSnapshot:
|
||||||
|
|
||||||
var catches: Array[FishCatchType] = []
|
var catches: Array[FishCatchType] = []
|
||||||
var discovered_ids: Array[StringName] = []
|
var discovered_ids: Array[StringName] = []
|
||||||
|
var discovered_quality_masks: Dictionary[StringName, int] = {}
|
||||||
var wallet_balance: int = 0
|
var wallet_balance: int = 0
|
||||||
var next_catch_sequence: int = 1
|
var next_catch_sequence: int = 1
|
||||||
var bag_items: Array[OwnedItemType] = []
|
var bag_items: Array[OwnedItemType] = []
|
||||||
|
|
@ -40,6 +46,8 @@ class LoadSnapshot:
|
||||||
var barrier_power_level: int = 0
|
var barrier_power_level: int = 0
|
||||||
var cooler_capacity_level: int = 0
|
var cooler_capacity_level: int = 0
|
||||||
var art_unlock_mask: int = 0
|
var art_unlock_mask: int = 0
|
||||||
|
var total_experience: int = 0
|
||||||
|
var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
|
||||||
|
|
||||||
|
|
||||||
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
||||||
|
|
@ -54,6 +62,8 @@ var _item_catalog: ItemCatalogType
|
||||||
var _fishing_upgrades: PlayerFishingUpgradesType
|
var _fishing_upgrades: PlayerFishingUpgradesType
|
||||||
var _cooler_capacity: PlayerCoolerCapacityType
|
var _cooler_capacity: PlayerCoolerCapacityType
|
||||||
var _art_unlocks: PlayerArtUnlocksType
|
var _art_unlocks: PlayerArtUnlocksType
|
||||||
|
var _experience: PlayerExperienceType
|
||||||
|
var _world_time: WorldTimeServiceType
|
||||||
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
|
||||||
|
|
@ -96,6 +106,8 @@ func setup(
|
||||||
fishing_upgrades: PlayerFishingUpgradesType,
|
fishing_upgrades: PlayerFishingUpgradesType,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
art_unlocks: PlayerArtUnlocksType,
|
art_unlocks: PlayerArtUnlocksType,
|
||||||
|
experience: PlayerExperienceType,
|
||||||
|
world_time: WorldTimeServiceType,
|
||||||
) -> void:
|
) -> void:
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
_collection_log = collection_log
|
_collection_log = collection_log
|
||||||
|
|
@ -107,6 +119,8 @@ func setup(
|
||||||
_fishing_upgrades = fishing_upgrades
|
_fishing_upgrades = fishing_upgrades
|
||||||
_cooler_capacity = cooler_capacity
|
_cooler_capacity = cooler_capacity
|
||||||
_art_unlocks = art_unlocks
|
_art_unlocks = art_unlocks
|
||||||
|
_experience = experience
|
||||||
|
_world_time = world_time
|
||||||
_is_configured = (
|
_is_configured = (
|
||||||
_inventory != null
|
_inventory != null
|
||||||
and _collection_log != null
|
and _collection_log != null
|
||||||
|
|
@ -118,16 +132,18 @@ func setup(
|
||||||
and _fishing_upgrades != null
|
and _fishing_upgrades != null
|
||||||
and _cooler_capacity != null
|
and _cooler_capacity != null
|
||||||
and _art_unlocks != null
|
and _art_unlocks != null
|
||||||
|
and _experience != null
|
||||||
|
and _world_time != 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.")
|
||||||
return
|
return
|
||||||
if not _inventory.catches_changed.is_connected(_mark_dirty):
|
if not _inventory.catches_changed.is_connected(_mark_dirty):
|
||||||
_inventory.catches_changed.connect(_mark_dirty)
|
_inventory.catches_changed.connect(_mark_dirty)
|
||||||
if not _collection_log.fish_discovered.is_connected(
|
if not _collection_log.collection_changed.is_connected(
|
||||||
_on_fish_discovered
|
_on_collection_changed
|
||||||
):
|
):
|
||||||
_collection_log.fish_discovered.connect(_on_fish_discovered)
|
_collection_log.collection_changed.connect(_on_collection_changed)
|
||||||
if not _wallet.balance_changed.is_connected(_on_balance_changed):
|
if not _wallet.balance_changed.is_connected(_on_balance_changed):
|
||||||
_wallet.balance_changed.connect(_on_balance_changed)
|
_wallet.balance_changed.connect(_on_balance_changed)
|
||||||
if not _bag.contents_changed.is_connected(_mark_dirty):
|
if not _bag.contents_changed.is_connected(_mark_dirty):
|
||||||
|
|
@ -152,6 +168,10 @@ func setup(
|
||||||
)
|
)
|
||||||
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
|
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
|
||||||
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
|
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
|
||||||
|
if not _experience.experience_changed.is_connected(
|
||||||
|
_on_experience_changed
|
||||||
|
):
|
||||||
|
_experience.experience_changed.connect(_on_experience_changed)
|
||||||
|
|
||||||
|
|
||||||
func load_player_data() -> bool:
|
func load_player_data() -> bool:
|
||||||
|
|
@ -208,7 +228,10 @@ func load_player_data() -> bool:
|
||||||
snapshot.next_catch_sequence
|
snapshot.next_catch_sequence
|
||||||
)
|
)
|
||||||
var collection_restored: bool = (
|
var collection_restored: bool = (
|
||||||
_collection_log.replace_discovered_ids(snapshot.discovered_ids)
|
_collection_log.replace_discovery_state(
|
||||||
|
snapshot.discovered_ids,
|
||||||
|
snapshot.discovered_quality_masks,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
var wallet_restored: bool = _wallet.restore_balance(
|
var wallet_restored: bool = _wallet.restore_balance(
|
||||||
snapshot.wallet_balance
|
snapshot.wallet_balance
|
||||||
|
|
@ -229,6 +252,12 @@ func load_player_data() -> bool:
|
||||||
var art_restored: bool = _art_unlocks.restore_mask(
|
var art_restored: bool = _art_unlocks.restore_mask(
|
||||||
snapshot.art_unlock_mask
|
snapshot.art_unlock_mask
|
||||||
)
|
)
|
||||||
|
var experience_restored: bool = _experience.restore_total_experience(
|
||||||
|
snapshot.total_experience
|
||||||
|
)
|
||||||
|
var world_time_restored: bool = (
|
||||||
|
_world_time.restore_persistent_time_hours(snapshot.world_time_hours)
|
||||||
|
)
|
||||||
_is_restoring = false
|
_is_restoring = false
|
||||||
if (
|
if (
|
||||||
not inventory_restored
|
not inventory_restored
|
||||||
|
|
@ -239,6 +268,8 @@ func load_player_data() -> bool:
|
||||||
or not upgrades_restored
|
or not upgrades_restored
|
||||||
or not cooler_restored
|
or not cooler_restored
|
||||||
or not art_restored
|
or not art_restored
|
||||||
|
or not experience_restored
|
||||||
|
or not world_time_restored
|
||||||
):
|
):
|
||||||
push_error("Validated player save could not be restored.")
|
push_error("Validated player save could not be restored.")
|
||||||
return false
|
return false
|
||||||
|
|
@ -332,6 +363,17 @@ func save_if_dirty() -> bool:
|
||||||
return not _is_dirty or save_now()
|
return not _is_dirty or save_now()
|
||||||
|
|
||||||
|
|
||||||
|
func save_world_time_checkpoint() -> bool:
|
||||||
|
if (
|
||||||
|
not _is_configured
|
||||||
|
or _automatic_saving_blocked
|
||||||
|
or not _autosave_enabled
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_is_dirty = true
|
||||||
|
return save_now()
|
||||||
|
|
||||||
|
|
||||||
func is_dirty() -> bool:
|
func is_dirty() -> bool:
|
||||||
return _is_dirty
|
return _is_dirty
|
||||||
|
|
||||||
|
|
@ -401,6 +443,19 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
if fish_id.is_empty():
|
if fish_id.is_empty():
|
||||||
return {}
|
return {}
|
||||||
discovered_strings.append(String(fish_id))
|
discovered_strings.append(String(fish_id))
|
||||||
|
var serialized_quality_masks: Dictionary = {}
|
||||||
|
var quality_masks: Dictionary[StringName, int] = (
|
||||||
|
_collection_log.get_discovered_quality_masks()
|
||||||
|
)
|
||||||
|
for fish_id: StringName in quality_masks:
|
||||||
|
var quality_mask: int = quality_masks[fish_id]
|
||||||
|
if (
|
||||||
|
fish_id.is_empty()
|
||||||
|
or quality_mask <= 0
|
||||||
|
or (quality_mask & ~FishQualityType.ALL_TIERS_MASK) != 0
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
serialized_quality_masks[String(fish_id)] = quality_mask
|
||||||
var serialized_items: Array[Dictionary] = []
|
var serialized_items: Array[Dictionary] = []
|
||||||
for owned: OwnedItemType in _bag.get_all_items():
|
for owned: OwnedItemType in _bag.get_all_items():
|
||||||
if owned == null or not owned.is_valid():
|
if owned == null or not owned.is_valid():
|
||||||
|
|
@ -430,6 +485,7 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"discovered_fish_ids": discovered_strings,
|
"discovered_fish_ids": discovered_strings,
|
||||||
|
"discovered_quality_masks": serialized_quality_masks,
|
||||||
},
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"next_catch_sequence": _inventory.get_next_catch_sequence(),
|
"next_catch_sequence": _inventory.get_next_catch_sequence(),
|
||||||
|
|
@ -446,6 +502,10 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
"upgrades": _fishing_upgrades.to_save_data(),
|
"upgrades": _fishing_upgrades.to_save_data(),
|
||||||
"cooler": _cooler_capacity.to_save_data(),
|
"cooler": _cooler_capacity.to_save_data(),
|
||||||
"art": _art_unlocks.to_save_data(),
|
"art": _art_unlocks.to_save_data(),
|
||||||
|
"experience": _experience.to_save_data(),
|
||||||
|
"world": {
|
||||||
|
"time_hours": _world_time.get_persistent_time_hours(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -456,6 +516,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
||||||
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
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
var wallet_data: Dictionary = save_data["wallet"]
|
var wallet_data: Dictionary = save_data["wallet"]
|
||||||
|
|
@ -463,6 +524,10 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
var inventory_data: Dictionary = save_data["inventory"]
|
var inventory_data: Dictionary = save_data["inventory"]
|
||||||
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 world_data: Dictionary = {}
|
||||||
|
if typeof(save_data.get("world")) == TYPE_DICTIONARY:
|
||||||
|
world_data = save_data["world"]
|
||||||
var upgrades_data: Dictionary = {}
|
var upgrades_data: Dictionary = {}
|
||||||
var cooler_data: Dictionary = {}
|
var cooler_data: Dictionary = {}
|
||||||
var art_data: Dictionary = {}
|
var art_data: Dictionary = {}
|
||||||
|
|
@ -481,11 +546,14 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
if (
|
if (
|
||||||
not wallet_data.has("balance")
|
not wallet_data.has("balance")
|
||||||
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
|
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
|
||||||
|
or typeof(collection_data.get("discovered_quality_masks"))
|
||||||
|
!= TYPE_DICTIONARY
|
||||||
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
|
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
|
||||||
or not inventory_data.has("next_catch_sequence")
|
or not inventory_data.has("next_catch_sequence")
|
||||||
or typeof(bag_data.get("items")) != TYPE_ARRAY
|
or typeof(bag_data.get("items")) != TYPE_ARRAY
|
||||||
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
|
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
|
||||||
or not hotbar_data.has("selected_slot")
|
or not hotbar_data.has("selected_slot")
|
||||||
|
or not experience_data.has("total_experience")
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
@ -504,6 +572,19 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
|
|
||||||
var snapshot := LoadSnapshot.new()
|
var snapshot := LoadSnapshot.new()
|
||||||
snapshot.wallet_balance = balance
|
snapshot.wallet_balance = balance
|
||||||
|
snapshot.total_experience = _read_integer(
|
||||||
|
experience_data["total_experience"],
|
||||||
|
-1,
|
||||||
|
PlayerExperienceType.MAX_TOTAL_EXPERIENCE,
|
||||||
|
)
|
||||||
|
if snapshot.total_experience < 0:
|
||||||
|
return null
|
||||||
|
if world_data.has("time_hours"):
|
||||||
|
snapshot.world_time_hours = _read_world_time_hours(
|
||||||
|
world_data["time_hours"]
|
||||||
|
)
|
||||||
|
if snapshot.world_time_hours < 0.0:
|
||||||
|
return null
|
||||||
var discovered_values: Array = collection_data["discovered_fish_ids"]
|
var 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:
|
||||||
|
|
@ -514,6 +595,26 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
continue
|
continue
|
||||||
seen_discoveries[fish_id] = true
|
seen_discoveries[fish_id] = true
|
||||||
snapshot.discovered_ids.append(fish_id)
|
snapshot.discovered_ids.append(fish_id)
|
||||||
|
var quality_mask_values: Dictionary = (
|
||||||
|
collection_data["discovered_quality_masks"]
|
||||||
|
)
|
||||||
|
for key: Variant in quality_mask_values:
|
||||||
|
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
return null
|
||||||
|
var fish_id: StringName = StringName(str(key))
|
||||||
|
var mask: int = _read_integer(
|
||||||
|
quality_mask_values[key],
|
||||||
|
-1,
|
||||||
|
FishQualityType.ALL_TIERS_MASK,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
fish_id.is_empty()
|
||||||
|
or not seen_discoveries.has(fish_id)
|
||||||
|
or mask <= 0
|
||||||
|
or (mask & ~FishQualityType.ALL_TIERS_MASK) != 0
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
snapshot.discovered_quality_masks[fish_id] = mask
|
||||||
|
|
||||||
var seen_ids: Dictionary[StringName, bool] = {}
|
var seen_ids: Dictionary[StringName, bool] = {}
|
||||||
var seen_sequences: Dictionary[int, bool] = {}
|
var seen_sequences: Dictionary[int, bool] = {}
|
||||||
|
|
@ -701,6 +802,10 @@ func _migrate_save(
|
||||||
migrated = _migrate_version_2_to_3(migrated)
|
migrated = _migrate_version_2_to_3(migrated)
|
||||||
3:
|
3:
|
||||||
migrated = _migrate_version_3_to_4(migrated)
|
migrated = _migrate_version_3_to_4(migrated)
|
||||||
|
4:
|
||||||
|
migrated = _migrate_version_4_to_5(migrated)
|
||||||
|
5:
|
||||||
|
migrated = _migrate_version_5_to_6(migrated)
|
||||||
_:
|
_:
|
||||||
return {}
|
return {}
|
||||||
if migrated.is_empty():
|
if migrated.is_empty():
|
||||||
|
|
@ -748,6 +853,68 @@ func _migrate_version_3_to_4(data: Dictionary) -> Dictionary:
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
func _migrate_version_4_to_5(data: Dictionary) -> Dictionary:
|
||||||
|
var migrated: Dictionary = data.duplicate(true)
|
||||||
|
if (
|
||||||
|
typeof(migrated.get("inventory")) != TYPE_DICTIONARY
|
||||||
|
or typeof(migrated.get("collection")) != TYPE_DICTIONARY
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var inventory_data: Dictionary = migrated["inventory"]
|
||||||
|
var collection_data: Dictionary = migrated["collection"]
|
||||||
|
if (
|
||||||
|
typeof(inventory_data.get("catches")) != TYPE_ARRAY
|
||||||
|
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var catches: Array = inventory_data["catches"]
|
||||||
|
var discovered_values: Array = collection_data["discovered_fish_ids"]
|
||||||
|
var discovered_lookup: Dictionary[String, bool] = {}
|
||||||
|
for value: Variant in discovered_values:
|
||||||
|
if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
var existing_id: String = str(value)
|
||||||
|
if not existing_id.is_empty():
|
||||||
|
discovered_lookup[existing_id] = true
|
||||||
|
for index: int in catches.size():
|
||||||
|
if typeof(catches[index]) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var catch_data: Dictionary = catches[index]
|
||||||
|
catch_data["quality"] = FishQualityType.Tier.BORING
|
||||||
|
var catch_fish_id: String = str(catch_data.get("fish_id", ""))
|
||||||
|
if not catch_fish_id.is_empty() and not discovered_lookup.has(
|
||||||
|
catch_fish_id
|
||||||
|
):
|
||||||
|
discovered_values.append(catch_fish_id)
|
||||||
|
discovered_lookup[catch_fish_id] = true
|
||||||
|
catches[index] = catch_data
|
||||||
|
inventory_data["catches"] = catches
|
||||||
|
migrated["inventory"] = inventory_data
|
||||||
|
var quality_masks: Dictionary = {}
|
||||||
|
for value: Variant in discovered_values:
|
||||||
|
if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||||
|
continue
|
||||||
|
var fish_id: String = str(value)
|
||||||
|
if not fish_id.is_empty():
|
||||||
|
quality_masks[fish_id] = FishQualityType.bit_for(
|
||||||
|
FishQualityType.Tier.BORING
|
||||||
|
)
|
||||||
|
collection_data["discovered_quality_masks"] = quality_masks
|
||||||
|
collection_data["discovered_fish_ids"] = discovered_values
|
||||||
|
migrated["collection"] = collection_data
|
||||||
|
migrated["save_version"] = 5
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
func _migrate_version_5_to_6(data: Dictionary) -> Dictionary:
|
||||||
|
var migrated: Dictionary = data.duplicate(true)
|
||||||
|
migrated["save_version"] = 6
|
||||||
|
migrated["experience"] = {"total_experience": 0}
|
||||||
|
migrated["world"] = {
|
||||||
|
"time_hours": WorldTimeServiceType.DEFAULT_START_HOUR,
|
||||||
|
}
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
func _mark_dirty() -> void:
|
func _mark_dirty() -> void:
|
||||||
if (
|
if (
|
||||||
_is_restoring
|
_is_restoring
|
||||||
|
|
@ -760,7 +927,7 @@ func _mark_dirty() -> void:
|
||||||
_autosave_timer.start(maxf(autosave_delay, 0.05))
|
_autosave_timer.start(maxf(autosave_delay, 0.05))
|
||||||
|
|
||||||
|
|
||||||
func _on_fish_discovered(_fish_id: StringName) -> void:
|
func _on_collection_changed() -> void:
|
||||||
_mark_dirty()
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -793,6 +960,10 @@ func _on_art_unlocks_changed(_unlock_mask: int) -> void:
|
||||||
_mark_dirty()
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_experience_changed(_total_experience: int, _level: int) -> void:
|
||||||
|
_mark_dirty()
|
||||||
|
|
||||||
|
|
||||||
func _on_autosave_timeout() -> void:
|
func _on_autosave_timeout() -> void:
|
||||||
if _is_dirty:
|
if _is_dirty:
|
||||||
save_now()
|
save_now()
|
||||||
|
|
@ -833,6 +1004,7 @@ func _restore_defaults() -> void:
|
||||||
_is_restoring = true
|
_is_restoring = true
|
||||||
var empty_catches: Array[FishCatchType] = []
|
var empty_catches: Array[FishCatchType] = []
|
||||||
var empty_discoveries: Array[StringName] = []
|
var empty_discoveries: Array[StringName] = []
|
||||||
|
var empty_quality_masks: Dictionary[StringName, int] = {}
|
||||||
var default_items: Array[OwnedItemType] = []
|
var default_items: Array[OwnedItemType] = []
|
||||||
var basic_rod := OwnedItemType.new()
|
var basic_rod := OwnedItemType.new()
|
||||||
basic_rod.item_id = BASIC_ROD_ID
|
basic_rod.item_id = BASIC_ROD_ID
|
||||||
|
|
@ -843,13 +1015,20 @@ func _restore_defaults() -> void:
|
||||||
default_slots.fill(StringName())
|
default_slots.fill(StringName())
|
||||||
default_slots[0] = BASIC_ROD_ID
|
default_slots[0] = BASIC_ROD_ID
|
||||||
_inventory.replace_all_catches(empty_catches, 1)
|
_inventory.replace_all_catches(empty_catches, 1)
|
||||||
_collection_log.replace_discovered_ids(empty_discoveries)
|
_collection_log.replace_discovery_state(
|
||||||
|
empty_discoveries,
|
||||||
|
empty_quality_masks,
|
||||||
|
)
|
||||||
_wallet.restore_balance(0)
|
_wallet.restore_balance(0)
|
||||||
_bag.replace_all_items(default_items)
|
_bag.replace_all_items(default_items)
|
||||||
_hotbar.replace_state(default_slots, 0)
|
_hotbar.replace_state(default_slots, 0)
|
||||||
_fishing_upgrades.reset_to_defaults()
|
_fishing_upgrades.reset_to_defaults()
|
||||||
_cooler_capacity.reset_to_defaults()
|
_cooler_capacity.reset_to_defaults()
|
||||||
_art_unlocks.reset_to_defaults()
|
_art_unlocks.reset_to_defaults()
|
||||||
|
_experience.reset_to_defaults()
|
||||||
|
_world_time.restore_persistent_time_hours(
|
||||||
|
WorldTimeServiceType.DEFAULT_START_HOUR
|
||||||
|
)
|
||||||
_is_restoring = false
|
_is_restoring = false
|
||||||
_is_dirty = false
|
_is_dirty = false
|
||||||
|
|
||||||
|
|
@ -875,6 +1054,19 @@ func _read_integer(
|
||||||
return invalid_value
|
return invalid_value
|
||||||
|
|
||||||
|
|
||||||
|
func _read_world_time_hours(value: Variant) -> float:
|
||||||
|
if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
|
||||||
|
return -1.0
|
||||||
|
var time_hours: float = float(value)
|
||||||
|
if (
|
||||||
|
not is_finite(time_hours)
|
||||||
|
or time_hours < 0.0
|
||||||
|
or time_hours >= WorldTimeServiceType.HOURS_PER_DAY
|
||||||
|
):
|
||||||
|
return -1.0
|
||||||
|
return time_hours
|
||||||
|
|
||||||
|
|
||||||
func _read_upgrade_level(
|
func _read_upgrade_level(
|
||||||
value: Variant,
|
value: Variant,
|
||||||
maximum_level: int,
|
maximum_level: int,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var world_time := main.get_node("%WorldTimeService") as WorldTimeService
|
||||||
|
world_time.synchronize_time(19.75)
|
||||||
|
assert(player.experience.award_experience(125))
|
||||||
var fish_catalog := main.get("fish_catalog") as FishPool
|
var fish_catalog := main.get("fish_catalog") as FishPool
|
||||||
var service := main.get_node(
|
var service := main.get_node(
|
||||||
"%NetworkFishShowcaseService"
|
"%NetworkFishShowcaseService"
|
||||||
|
|
@ -45,11 +48,17 @@ func _run() -> void:
|
||||||
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
fish_catch.weight_lb
|
fish_catch.weight_lb
|
||||||
)
|
)
|
||||||
fish_catch.sale_value = fish.get_sale_value_for_weight(
|
fish_catch.quality = FishQuality.Tier.EXCEPTIONAL
|
||||||
fish_catch.weight_lb
|
fish_catch.sale_value = FishQuality.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
fish_catch.quality,
|
||||||
)
|
)
|
||||||
fish_catch.ensure_identity()
|
fish_catch.ensure_identity()
|
||||||
player.inventory.add_catch(fish_catch)
|
player.inventory.add_catch(fish_catch)
|
||||||
|
player.collection_log.mark_quality_discovered(
|
||||||
|
fish_catch.fish_id,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
var game_ui := main.get_node("%GameUI") as GameUI
|
var game_ui := main.get_node("%GameUI") as GameUI
|
||||||
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
||||||
assert(hotbar_ui != null)
|
assert(hotbar_ui != null)
|
||||||
|
|
@ -94,12 +103,42 @@ 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"]) == 4)
|
assert(int((parsed as Dictionary)["save_version"]) == 6)
|
||||||
|
assert(
|
||||||
|
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||||
|
== 125
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
is_equal_approx(
|
||||||
|
float((parsed as Dictionary)["world"]["time_hours"]),
|
||||||
|
19.75,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"]
|
||||||
|
assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
|
||||||
|
var saved_masks: Dictionary = (
|
||||||
|
(parsed as Dictionary)["collection"]["discovered_quality_masks"]
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
int(saved_masks[String(fish.id)])
|
||||||
|
== FishQuality.bit_for(FishQuality.Tier.EXCEPTIONAL)
|
||||||
|
)
|
||||||
|
|
||||||
assert(player.hotbar.clear_slot(1))
|
assert(player.hotbar.clear_slot(1))
|
||||||
|
assert(player.experience.restore_total_experience(0))
|
||||||
|
world_time.synchronize_time(8.0)
|
||||||
assert(save_manager.load_player_data())
|
assert(save_manager.load_player_data())
|
||||||
|
assert(player.experience.get_total_experience() == 125)
|
||||||
|
assert(is_equal_approx(world_time.get_time_hours(), 19.75))
|
||||||
|
assert(player.experience.get_level() == 2)
|
||||||
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||||
assert(player.hotbar.get_selected_slot() == 1)
|
assert(player.hotbar.get_selected_slot() == 1)
|
||||||
|
assert(
|
||||||
|
player.collection_log.has_discovered_quality(
|
||||||
|
fish.id,
|
||||||
|
FishQuality.Tier.EXCEPTIONAL,
|
||||||
|
)
|
||||||
|
)
|
||||||
assert(not service.is_local_showcase_visible())
|
assert(not service.is_local_showcase_visible())
|
||||||
assert(service.toggle_selected_fish())
|
assert(service.toggle_selected_fish())
|
||||||
assert(service.is_local_showcase_visible())
|
assert(service.is_local_showcase_visible())
|
||||||
|
|
@ -118,6 +157,7 @@ func _run() -> void:
|
||||||
"display_scale": fish.get_display_scale_for_weight(
|
"display_scale": fish.get_display_scale_for_weight(
|
||||||
fish.get_minimum_weight()
|
fish.get_minimum_weight()
|
||||||
),
|
),
|
||||||
|
"quality": FishQuality.Tier.BORING,
|
||||||
"revision": 1,
|
"revision": 1,
|
||||||
}
|
}
|
||||||
assert(NetworkFishShowcaseProtocol.validate_state(valid_state))
|
assert(NetworkFishShowcaseProtocol.validate_state(valid_state))
|
||||||
|
|
@ -126,6 +166,10 @@ func _run() -> void:
|
||||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
|
assert(
|
||||||
|
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||||
|
== "fish_quality_v1"
|
||||||
|
)
|
||||||
assert(
|
assert(
|
||||||
NetworkFishShowcaseProtocol.CAPABILITY
|
NetworkFishShowcaseProtocol.CAPABILITY
|
||||||
== &"fish_showcase_v1"
|
== &"fish_showcase_v1"
|
||||||
|
|
|
||||||
262
tests/fish_quality_validation.gd
Normal file
262
tests/fish_quality_validation.gd
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
const FishSelectorType = preload("res://fish/fish_selector.gd")
|
||||||
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
|
const NetworkSaleServiceType = preload(
|
||||||
|
"res://network/network_sale_service.gd"
|
||||||
|
)
|
||||||
|
const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||||
|
const PelicanBuyer: FishBuyerProfile = preload(
|
||||||
|
"res://economy/buyers/pelicans.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_validate_tiers_and_distribution()
|
||||||
|
_validate_catch_round_trip_and_sale()
|
||||||
|
_validate_mail_round_trip()
|
||||||
|
_validate_collection_mastery()
|
||||||
|
_validate_version_four_migration()
|
||||||
|
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
||||||
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
|
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
||||||
|
print("Fish quality validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_tiers_and_distribution() -> void:
|
||||||
|
assert(FishQualityType.TIER_COUNT == 5)
|
||||||
|
assert(FishQualityType.display_name(0) == "boring")
|
||||||
|
assert(FishQualityType.display_name(4) == "shiny")
|
||||||
|
assert(UIPalette.get_quality_color(0) == UIPalette.QUALITY_BORING)
|
||||||
|
assert(UIPalette.get_quality_color(4) == UIPalette.QUALITY_SHINY)
|
||||||
|
assert(
|
||||||
|
UIPalette.get_quality_color(0)
|
||||||
|
!= UIPalette.get_quality_color(1)
|
||||||
|
)
|
||||||
|
assert(FishQualityType.apply_sale_value(3, 0) == 3)
|
||||||
|
assert(FishQualityType.apply_sale_value(3, 1) == 4)
|
||||||
|
assert(FishQualityType.apply_sale_value(3, 2) == 5)
|
||||||
|
assert(FishQualityType.apply_sale_value(3, 3) == 6)
|
||||||
|
assert(FishQualityType.apply_sale_value(3, 4) == 7)
|
||||||
|
var previous_offer: int = -1
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
var offer: int = PelicanBuyer.get_quality_offer(3, quality)
|
||||||
|
assert(offer > previous_offer)
|
||||||
|
previous_offer = offer
|
||||||
|
assert(
|
||||||
|
FishQualityType.qualified_name_with_article("bluegill", 0)
|
||||||
|
== "a boring bluegill"
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
FishQualityType.qualified_name_with_article("bluegill", 1)
|
||||||
|
== "an average bluegill"
|
||||||
|
)
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 727272
|
||||||
|
var counts: Array[int] = [0, 0, 0, 0, 0]
|
||||||
|
const SAMPLE_COUNT: int = 50000
|
||||||
|
for _sample: int in SAMPLE_COUNT:
|
||||||
|
counts[FishQualityType.roll(rng)] += 1
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
var observed: float = float(counts[quality]) / float(SAMPLE_COUNT)
|
||||||
|
var expected: float = (
|
||||||
|
FishQualityType.BASE_ROLL_WEIGHTS[quality] / 100.0
|
||||||
|
)
|
||||||
|
assert(absf(observed - expected) < 0.015)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_catch_round_trip_and_sale() -> void:
|
||||||
|
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||||
|
assert(fish != null)
|
||||||
|
var selector := FishSelectorType.new()
|
||||||
|
selector.use_deterministic_test_seed = true
|
||||||
|
selector.deterministic_test_seed = 9911
|
||||||
|
selector.quality_weight_multipliers = [0.0, 0.0, 0.0, 0.0, 1.0]
|
||||||
|
selector.begin_roll()
|
||||||
|
var fish_catch: FishCatch = selector.create_catch(fish)
|
||||||
|
assert(fish_catch != null)
|
||||||
|
assert(fish_catch.quality == FishQualityType.Tier.SHINY)
|
||||||
|
assert(
|
||||||
|
fish_catch.sale_value
|
||||||
|
== FishQualityType.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
FishQualityType.Tier.SHINY,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var save_data: Dictionary = fish_catch.to_save_dict()
|
||||||
|
fish_catch.catch_sequence = 1
|
||||||
|
save_data = fish_catch.to_save_dict()
|
||||||
|
var restored: FishCatch = FishCatchType.from_save_dict(save_data, fish)
|
||||||
|
assert(restored != null and restored.quality == fish_catch.quality)
|
||||||
|
var network_data: Dictionary = fish_catch.to_network_dict()
|
||||||
|
var replicated: FishCatch = FishCatchType.from_network_dict(
|
||||||
|
network_data,
|
||||||
|
fish,
|
||||||
|
)
|
||||||
|
assert(replicated != null and replicated.quality == fish_catch.quality)
|
||||||
|
var missing_quality: Dictionary = network_data.duplicate(true)
|
||||||
|
missing_quality.erase("quality")
|
||||||
|
assert(FishCatchType.from_network_dict(missing_quality, fish) == null)
|
||||||
|
|
||||||
|
var session := NetworkSession.new()
|
||||||
|
var sale_service := NetworkSaleServiceType.new()
|
||||||
|
root.add_child(session)
|
||||||
|
root.add_child(sale_service)
|
||||||
|
sale_service.set("_session", session)
|
||||||
|
sale_service.set("_fish_catalog", Catalog)
|
||||||
|
var accepted: Dictionary = sale_service.call(
|
||||||
|
"_build_authoritative_result",
|
||||||
|
1,
|
||||||
|
"quality_sale",
|
||||||
|
[network_data],
|
||||||
|
PelicanBuyer,
|
||||||
|
)
|
||||||
|
assert(bool(accepted.get("accepted", false)))
|
||||||
|
assert(
|
||||||
|
int(accepted["payout"])
|
||||||
|
== PelicanBuyer.get_quality_offer(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var inventory := FishInventory.new()
|
||||||
|
var wallet := PlayerWallet.new()
|
||||||
|
var local_sale := FishSaleService.new()
|
||||||
|
root.add_child(inventory)
|
||||||
|
root.add_child(wallet)
|
||||||
|
root.add_child(local_sale)
|
||||||
|
local_sale.setup(inventory, wallet)
|
||||||
|
inventory.add_catch(fish_catch)
|
||||||
|
var local_preview: FishSaleResult = local_sale.preview_batch(
|
||||||
|
[fish_catch.catch_id],
|
||||||
|
PelicanBuyer,
|
||||||
|
)
|
||||||
|
assert(local_preview.is_success())
|
||||||
|
assert(local_preview.payout == int(accepted["payout"]))
|
||||||
|
var forged: Dictionary = network_data.duplicate(true)
|
||||||
|
forged["quality"] = FishQualityType.Tier.BORING
|
||||||
|
var rejected: Dictionary = sale_service.call(
|
||||||
|
"_build_authoritative_result",
|
||||||
|
1,
|
||||||
|
"quality_sale_forged",
|
||||||
|
[forged],
|
||||||
|
PelicanBuyer,
|
||||||
|
)
|
||||||
|
assert(not bool(rejected.get("accepted", false)))
|
||||||
|
local_sale.queue_free()
|
||||||
|
wallet.queue_free()
|
||||||
|
inventory.queue_free()
|
||||||
|
sale_service.queue_free()
|
||||||
|
session.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_mail_round_trip() -> void:
|
||||||
|
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.weight_lb = fish.get_minimum_weight()
|
||||||
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.quality = FishQualityType.Tier.IMPRESSIVE
|
||||||
|
fish_catch.sale_value = FishQualityType.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
fish_catch.ensure_identity()
|
||||||
|
var attachment: Dictionary = {
|
||||||
|
"type": PlayerAssetReservationService.AttachmentType.FISH,
|
||||||
|
"catch_id": String(fish_catch.catch_id),
|
||||||
|
"catch": fish_catch.to_network_dict(),
|
||||||
|
}
|
||||||
|
var service := NetworkMailService.new()
|
||||||
|
root.add_child(service)
|
||||||
|
service.set("_fish_catalog", Catalog)
|
||||||
|
var restored: FishCatch = service.call("_decode_catch", attachment)
|
||||||
|
assert(restored != null)
|
||||||
|
assert(restored.quality == FishQualityType.Tier.IMPRESSIVE)
|
||||||
|
var signature: Array = NetworkMailProtocol.attachment_signature_fields(
|
||||||
|
attachment
|
||||||
|
)
|
||||||
|
assert(int(signature[5]) == FishQualityType.Tier.IMPRESSIVE)
|
||||||
|
var altered: Dictionary = attachment.duplicate(true)
|
||||||
|
(altered["catch"] as Dictionary)["quality"] = (
|
||||||
|
FishQualityType.Tier.BORING
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
NetworkMailProtocol.attachment_signature_fields(altered)
|
||||||
|
!= signature
|
||||||
|
)
|
||||||
|
service.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_collection_mastery() -> void:
|
||||||
|
var collection := CollectionLogType.new()
|
||||||
|
root.add_child(collection)
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
collection.mark_quality_discovered(&"bluegill", quality)
|
||||||
|
assert(collection.has_discovered_quality(&"bluegill", quality))
|
||||||
|
assert(collection.has_discovered(&"bluegill"))
|
||||||
|
assert(collection.has_mastered(&"bluegill"))
|
||||||
|
assert(
|
||||||
|
collection.get_quality_mask(&"bluegill")
|
||||||
|
== FishQualityType.ALL_TIERS_MASK
|
||||||
|
)
|
||||||
|
collection.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_version_four_migration() -> void:
|
||||||
|
var manager := PlayerSaveManager.new()
|
||||||
|
root.add_child(manager)
|
||||||
|
var version_four: Dictionary = {
|
||||||
|
"save_version": 4,
|
||||||
|
"wallet": {"balance": 12},
|
||||||
|
"collection": {"discovered_fish_ids": ["bluegill"]},
|
||||||
|
"inventory": {
|
||||||
|
"next_catch_sequence": 2,
|
||||||
|
"catches": [{
|
||||||
|
"catch_id": "carp:legacy",
|
||||||
|
"catch_sequence": 1,
|
||||||
|
"fish_id": "carp",
|
||||||
|
"weight_lb": 2.0,
|
||||||
|
"display_scale": 1.0,
|
||||||
|
"sale_value": 4,
|
||||||
|
"is_favorited": false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
"bag": {"items": []},
|
||||||
|
"hotbar": {"selected_slot": 0, "slots": []},
|
||||||
|
"upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
|
||||||
|
"cooler": {"capacity_level": 0},
|
||||||
|
}
|
||||||
|
var migrated: Dictionary = manager.call(
|
||||||
|
"_migrate_save",
|
||||||
|
version_four,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
assert(int(migrated.get("save_version", -1)) == 6)
|
||||||
|
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||||
|
assert(
|
||||||
|
is_equal_approx(
|
||||||
|
float((migrated["world"] as Dictionary)["time_hours"]),
|
||||||
|
8.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
var catches: Array = migrated["inventory"]["catches"]
|
||||||
|
assert(int((catches[0] as Dictionary)["quality"]) == 0)
|
||||||
|
var collection: Dictionary = migrated["collection"]
|
||||||
|
var ids: Array = collection["discovered_fish_ids"]
|
||||||
|
assert("bluegill" in ids and "carp" in ids)
|
||||||
|
var masks: Dictionary = collection["discovered_quality_masks"]
|
||||||
|
var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
|
||||||
|
assert(int(masks["bluegill"]) == boring_bit)
|
||||||
|
assert(int(masks["carp"]) == boring_bit)
|
||||||
|
manager.queue_free()
|
||||||
1
tests/fish_quality_validation.gd.uid
Normal file
1
tests/fish_quality_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bffg3t4du01cu
|
||||||
|
|
@ -239,18 +239,29 @@ func _validate_bite_wait_distribution() -> void:
|
||||||
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||||
root.add_child(fishing_spot)
|
root.add_child(fishing_spot)
|
||||||
await process_frame
|
await process_frame
|
||||||
|
var bite_rng := fishing_spot.get("_bite_rng") as RandomNumberGenerator
|
||||||
|
bite_rng.seed = 84219
|
||||||
var quick_count: int = 0
|
var quick_count: int = 0
|
||||||
var typical_or_long_count: int = 0
|
var typical_or_long_count: int = 0
|
||||||
|
var very_long_count: int = 0
|
||||||
|
var total_wait_seconds: float = 0.0
|
||||||
for _sample_index: int in 10000:
|
for _sample_index: int in 10000:
|
||||||
var wait_seconds: float = fishing_spot.roll_bite_wait_time()
|
var wait_seconds: float = fishing_spot.roll_bite_wait_time()
|
||||||
assert(wait_seconds >= 10.0)
|
assert(wait_seconds >= 10.0)
|
||||||
assert(wait_seconds <= 240.0)
|
assert(wait_seconds <= 240.0)
|
||||||
|
total_wait_seconds += wait_seconds
|
||||||
if wait_seconds < 30.0:
|
if wait_seconds < 30.0:
|
||||||
quick_count += 1
|
quick_count += 1
|
||||||
else:
|
else:
|
||||||
typical_or_long_count += 1
|
typical_or_long_count += 1
|
||||||
assert(quick_count > 0)
|
if wait_seconds >= 180.0:
|
||||||
|
very_long_count += 1
|
||||||
|
var average_wait_seconds: float = total_wait_seconds / 10000.0
|
||||||
|
assert(quick_count >= 2300 and quick_count <= 2700)
|
||||||
assert(typical_or_long_count > quick_count)
|
assert(typical_or_long_count > quick_count)
|
||||||
|
assert(very_long_count >= 100 and very_long_count <= 300)
|
||||||
|
assert(average_wait_seconds >= 57.0)
|
||||||
|
assert(average_wait_seconds <= 61.0)
|
||||||
fishing_spot.queue_free()
|
fishing_spot.queue_free()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,8 @@ func _validate_handwritten_logbook_font(page: LogbookPage) -> void:
|
||||||
var detail_body := page.get("_detail_body") as VBoxContainer
|
var detail_body := page.get("_detail_body") as VBoxContainer
|
||||||
for node: Node in detail_body.find_children("*", "Label", true, false):
|
for node: Node in detail_body.find_children("*", "Label", true, false):
|
||||||
var label := node as Label
|
var label := node as Label
|
||||||
|
if label.text.begins_with("quality collection"):
|
||||||
|
continue
|
||||||
if label.text in [
|
if label.text in [
|
||||||
"fish facts",
|
"fish facts",
|
||||||
"catalog number",
|
"catalog number",
|
||||||
|
|
@ -307,6 +309,9 @@ func _validate_detail_field_fonts(page: LogbookPage) -> void:
|
||||||
]
|
]
|
||||||
for node: Node in detail_body.find_children("*", "Label", true, false):
|
for node: Node in detail_body.find_children("*", "Label", true, false):
|
||||||
var label := node as Label
|
var label := node as Label
|
||||||
|
if label.text.begins_with("quality collection"):
|
||||||
|
assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
|
||||||
|
continue
|
||||||
if label.text not in field_names:
|
if label.text not in field_names:
|
||||||
continue
|
continue
|
||||||
assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
|
assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
|
||||||
|
|
|
||||||
47
tests/player_experience_ui_validation.gd
Normal file
47
tests/player_experience_ui_validation.gd
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const GameUIScene: PackedScene = preload("res://ui/game_ui.tscn")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var game_ui := GameUIScene.instantiate() as GameUI
|
||||||
|
root.add_child(game_ui)
|
||||||
|
await process_frame
|
||||||
|
game_ui.set("_gameplay_ui_enabled", true)
|
||||||
|
game_ui.call(
|
||||||
|
"_on_experience_awarded",
|
||||||
|
50,
|
||||||
|
0,
|
||||||
|
50,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
var panel := game_ui.get_node("%ExperienceProgressPanel") as PanelContainer
|
||||||
|
var bubble := game_ui.get_node("%ExperienceBubble") as PanelContainer
|
||||||
|
var bubble_label := game_ui.get_node("%ExperienceBubbleLabel") as Label
|
||||||
|
var award_label := game_ui.get_node("%ExperienceAwardLabel") as Label
|
||||||
|
assert(panel != null and bubble != null)
|
||||||
|
assert(not panel.visible and not bubble.visible)
|
||||||
|
game_ui.call("_on_showcase_changed", "bluegill", "common", 1.0, 0, true)
|
||||||
|
await process_frame
|
||||||
|
assert(not panel.visible and not bubble.visible)
|
||||||
|
game_ui.call("_on_showcase_changed", "", "", 0.0, 0, false)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
assert(panel.visible and bubble.visible)
|
||||||
|
assert(award_label.text == "+50 xp")
|
||||||
|
assert(bubble_label.text == "+50 xp!")
|
||||||
|
await create_timer(1.6).timeout
|
||||||
|
var progress := game_ui.get_node("%ExperienceProgress") as ProgressBar
|
||||||
|
var level_label := game_ui.get_node("%ExperienceLevelLabel") as Label
|
||||||
|
assert(is_equal_approx(progress.value, 50.0))
|
||||||
|
assert(level_label.text == "level 1")
|
||||||
|
await create_timer(1.2).timeout
|
||||||
|
assert(not panel.visible and not bubble.visible)
|
||||||
|
game_ui.queue_free()
|
||||||
|
print("Player experience UI validation: PASS")
|
||||||
|
quit()
|
||||||
1
tests/player_experience_ui_validation.gd.uid
Normal file
1
tests/player_experience_ui_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c03tvq1ydhdqj
|
||||||
178
tests/player_experience_validation.gd
Normal file
178
tests/player_experience_validation.gd
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishExperienceType = preload("res://fish/fish_experience.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
|
const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_validate_level_curve()
|
||||||
|
_validate_catch_awards()
|
||||||
|
_validate_player_experience_state()
|
||||||
|
_validate_save_migration()
|
||||||
|
print("Player experience validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_level_curve() -> void:
|
||||||
|
assert(PlayerExperienceType.experience_required_for_next_level(1) == 100)
|
||||||
|
assert(PlayerExperienceType.experience_required_for_next_level(2) == 130)
|
||||||
|
assert(PlayerExperienceType.experience_required_for_next_level(3) == 170)
|
||||||
|
assert(PlayerExperienceType.total_experience_for_level(1) == 0)
|
||||||
|
assert(PlayerExperienceType.total_experience_for_level(2) == 100)
|
||||||
|
assert(PlayerExperienceType.total_experience_for_level(3) == 230)
|
||||||
|
assert(PlayerExperienceType.total_experience_for_level(4) == 400)
|
||||||
|
assert(PlayerExperienceType.level_for_total_experience(99) == 1)
|
||||||
|
assert(PlayerExperienceType.level_for_total_experience(100) == 2)
|
||||||
|
assert(PlayerExperienceType.level_for_total_experience(399) == 3)
|
||||||
|
assert(PlayerExperienceType.level_for_total_experience(400) == 4)
|
||||||
|
assert(
|
||||||
|
is_equal_approx(
|
||||||
|
PlayerExperienceType.progress_for_total_experience(50),
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_catch_awards() -> void:
|
||||||
|
var minimum_boring: FishCatch = _make_catch(
|
||||||
|
FishQualityType.Tier.BORING,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
FishExperienceType.calculate_catch_experience(
|
||||||
|
minimum_boring,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
) == 50
|
||||||
|
)
|
||||||
|
var maximum_shiny: FishCatch = _make_catch(
|
||||||
|
FishQualityType.Tier.SHINY,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
var boring_mask: int = FishQualityType.bit_for(
|
||||||
|
FishQualityType.Tier.BORING
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
FishExperienceType.calculate_catch_experience(
|
||||||
|
maximum_shiny,
|
||||||
|
true,
|
||||||
|
boring_mask,
|
||||||
|
) == 46
|
||||||
|
)
|
||||||
|
var almost_mastered: int = (
|
||||||
|
FishQualityType.ALL_TIERS_MASK
|
||||||
|
& ~FishQualityType.bit_for(FishQualityType.Tier.SHINY)
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
FishExperienceType.calculate_catch_experience(
|
||||||
|
maximum_shiny,
|
||||||
|
true,
|
||||||
|
almost_mastered,
|
||||||
|
) == 146
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
FishExperienceType.calculate_catch_experience(
|
||||||
|
maximum_shiny,
|
||||||
|
true,
|
||||||
|
FishQualityType.ALL_TIERS_MASK,
|
||||||
|
) == 31
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_player_experience_state() -> void:
|
||||||
|
var experience := PlayerExperienceType.new()
|
||||||
|
root.add_child(experience)
|
||||||
|
var awards: Array[Array] = []
|
||||||
|
experience.experience_awarded.connect(
|
||||||
|
func(
|
||||||
|
amount: int,
|
||||||
|
previous_total: int,
|
||||||
|
new_total: int,
|
||||||
|
previous_level: int,
|
||||||
|
new_level: int,
|
||||||
|
) -> void:
|
||||||
|
awards.append([
|
||||||
|
amount,
|
||||||
|
previous_total,
|
||||||
|
new_total,
|
||||||
|
previous_level,
|
||||||
|
new_level,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
assert(experience.get_level() == 1)
|
||||||
|
assert(experience.award_experience(125))
|
||||||
|
assert(experience.get_total_experience() == 125)
|
||||||
|
assert(experience.get_level() == 2)
|
||||||
|
assert(experience.get_experience_in_level() == 25)
|
||||||
|
assert(awards.size() == 1)
|
||||||
|
assert(awards[0] == [125, 0, 125, 1, 2])
|
||||||
|
assert(experience.restore_total_experience(400))
|
||||||
|
assert(experience.get_level() == 4)
|
||||||
|
assert(not experience.restore_total_experience(-1))
|
||||||
|
experience.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_save_migration() -> void:
|
||||||
|
var manager := PlayerSaveManager.new()
|
||||||
|
root.add_child(manager)
|
||||||
|
var version_five: Dictionary = {
|
||||||
|
"save_version": 5,
|
||||||
|
"wallet": {"balance": 0},
|
||||||
|
"collection": {
|
||||||
|
"discovered_fish_ids": [],
|
||||||
|
"discovered_quality_masks": {},
|
||||||
|
},
|
||||||
|
"inventory": {"next_catch_sequence": 1, "catches": []},
|
||||||
|
"bag": {"items": []},
|
||||||
|
"hotbar": {"selected_slot": 0, "slots": []},
|
||||||
|
"upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
|
||||||
|
"cooler": {"capacity_level": 0},
|
||||||
|
"art": {"unlock_mask": 0},
|
||||||
|
}
|
||||||
|
var migrated: Dictionary = manager.call(
|
||||||
|
"_migrate_save",
|
||||||
|
version_five,
|
||||||
|
5,
|
||||||
|
)
|
||||||
|
assert(int(migrated.get("save_version", -1)) == 6)
|
||||||
|
var experience_data: Dictionary = migrated.get("experience", {})
|
||||||
|
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||||
|
var world_data: Dictionary = migrated.get("world", {})
|
||||||
|
assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
|
||||||
|
manager.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _make_catch(quality: int, maximum_weight: bool) -> FishCatch:
|
||||||
|
var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
|
||||||
|
assert(fish != null)
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.catch_id = StringName("bluegill:xp-%d-%s" % [
|
||||||
|
quality,
|
||||||
|
"max" if maximum_weight else "min",
|
||||||
|
])
|
||||||
|
fish_catch.catch_sequence = 1
|
||||||
|
fish_catch.weight_lb = (
|
||||||
|
fish.get_maximum_weight()
|
||||||
|
if maximum_weight
|
||||||
|
else fish.get_minimum_weight()
|
||||||
|
)
|
||||||
|
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.quality = quality
|
||||||
|
fish_catch.sale_value = FishQualityType.apply_sale_value(
|
||||||
|
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
quality,
|
||||||
|
)
|
||||||
|
return fish_catch
|
||||||
1
tests/player_experience_validation.gd.uid
Normal file
1
tests/player_experience_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://djkr2iaa5taks
|
||||||
|
|
@ -32,6 +32,9 @@ func _run_host() -> void:
|
||||||
)
|
)
|
||||||
assert(session.start_private_host(TEST_PORT))
|
assert(session.start_private_host(TEST_PORT))
|
||||||
world_time.synchronize_time(INITIAL_HOST_TIME)
|
world_time.synchronize_time(INITIAL_HOST_TIME)
|
||||||
|
assert(is_equal_approx(
|
||||||
|
world_time.get_persistent_time_hours(), INITIAL_HOST_TIME
|
||||||
|
))
|
||||||
world_weather.apply_authoritative_snapshot(
|
world_weather.apply_authoritative_snapshot(
|
||||||
WorldWeatherService.Weather.RAINY, 300.0
|
WorldWeatherService.Weather.RAINY, 300.0
|
||||||
)
|
)
|
||||||
|
|
@ -56,6 +59,9 @@ func _run_host() -> void:
|
||||||
|
|
||||||
await create_timer(1.0).timeout
|
await create_timer(1.0).timeout
|
||||||
world_time.synchronize_time(UPDATED_HOST_TIME)
|
world_time.synchronize_time(UPDATED_HOST_TIME)
|
||||||
|
assert(is_equal_approx(
|
||||||
|
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
|
||||||
|
))
|
||||||
world_weather.apply_authoritative_snapshot(
|
world_weather.apply_authoritative_snapshot(
|
||||||
WorldWeatherService.Weather.FOGGY, 300.0
|
WorldWeatherService.Weather.FOGGY, 300.0
|
||||||
)
|
)
|
||||||
|
|
@ -98,6 +104,7 @@ func _run_client() -> void:
|
||||||
assert(session.supports_server_capability(
|
assert(session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
))
|
))
|
||||||
|
assert(world_time.restore_persistent_time_hours(15.25))
|
||||||
|
|
||||||
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
|
|
@ -145,6 +152,7 @@ func _run_client() -> void:
|
||||||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||||
) <= TIME_TOLERANCE_HOURS)
|
) <= TIME_TOLERANCE_HOURS)
|
||||||
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
||||||
|
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
|
||||||
assert(clock_label.text == world_time.get_clock_text())
|
assert(clock_label.text == world_time.get_clock_text())
|
||||||
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ func _initialize() -> void:
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
_validate_clock_boundaries_and_duration()
|
_validate_clock_boundaries_and_duration()
|
||||||
|
_validate_persistent_host_clock()
|
||||||
_validate_fishing_availability()
|
_validate_fishing_availability()
|
||||||
_validate_fishing_spot_context()
|
_validate_fishing_spot_context()
|
||||||
_validate_network_snapshot_bounds()
|
_validate_network_snapshot_bounds()
|
||||||
|
|
@ -78,6 +79,24 @@ func _validate_clock_boundaries_and_duration() -> void:
|
||||||
clock.queue_free()
|
clock.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_persistent_host_clock() -> void:
|
||||||
|
var clock := WorldTimeServiceType.new()
|
||||||
|
root.add_child(clock)
|
||||||
|
assert(clock.restore_persistent_time_hours(18.75))
|
||||||
|
clock.set_persistence_tracking_enabled(true)
|
||||||
|
clock.begin_session(clock.get_persistent_time_hours())
|
||||||
|
assert(is_equal_approx(clock.get_time_hours(), 18.75))
|
||||||
|
clock.synchronize_time(19.25)
|
||||||
|
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
||||||
|
clock.set_persistence_tracking_enabled(false)
|
||||||
|
clock.synchronize_time(6.5)
|
||||||
|
assert(is_equal_approx(clock.get_time_hours(), 6.5))
|
||||||
|
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
||||||
|
assert(not clock.restore_persistent_time_hours(-1.0))
|
||||||
|
assert(not clock.restore_persistent_time_hours(24.0))
|
||||||
|
clock.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_fishing_availability() -> void:
|
func _validate_fishing_availability() -> void:
|
||||||
var day_context := FishingContextType.new()
|
var day_context := FishingContextType.new()
|
||||||
day_context.location_tags = [&"starter_pond"]
|
day_context.location_tags = [&"starter_pond"]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
const SELECTED_SCALE: float = 1.12
|
const SELECTED_SCALE: float = 1.12
|
||||||
const HOVER_SCALE: float = 1.025
|
const HOVER_SCALE: float = 1.025
|
||||||
|
|
@ -179,7 +180,10 @@ func refresh() -> void:
|
||||||
_quantity_label.visible = not quantity_text.is_empty()
|
_quantity_label.visible = not quantity_text.is_empty()
|
||||||
tooltip_text = (
|
tooltip_text = (
|
||||||
"%s · %.1f lb" % [
|
"%s · %.1f lb" % [
|
||||||
|
FishQualityType.qualified_name(
|
||||||
fish_catch.fish.display_name,
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
),
|
||||||
fish_catch.weight_lb,
|
fish_catch.weight_lb,
|
||||||
]
|
]
|
||||||
if fish_catch != null
|
if fish_catch != null
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ var _depth_scale: float = 1.0
|
||||||
var _batch_selected: bool = false
|
var _batch_selected: bool = false
|
||||||
var _focused_catch: bool = false
|
var _focused_catch: bool = false
|
||||||
var _hovered: bool = false
|
var _hovered: bool = false
|
||||||
var _rarity_color := Color.WHITE
|
var _quality_color := Color.WHITE
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -40,7 +40,7 @@ func configure(
|
||||||
texture: Texture2D,
|
texture: Texture2D,
|
||||||
phase: float,
|
phase: float,
|
||||||
depth_scale: float,
|
depth_scale: float,
|
||||||
rarity_color: Color,
|
quality_color: Color,
|
||||||
) -> void:
|
) -> void:
|
||||||
catch_id = identity
|
catch_id = identity
|
||||||
_display_name = display_name
|
_display_name = display_name
|
||||||
|
|
@ -48,7 +48,7 @@ func configure(
|
||||||
_fish_shadow.texture = texture
|
_fish_shadow.texture = texture
|
||||||
_motion_phase = phase
|
_motion_phase = phase
|
||||||
_depth_scale = depth_scale
|
_depth_scale = depth_scale
|
||||||
_rarity_color = rarity_color
|
_quality_color = quality_color
|
||||||
_refresh_style()
|
_refresh_style()
|
||||||
tooltip_text = "%s · drag to a hotbar slot" % _display_name
|
tooltip_text = "%s · drag to a hotbar slot" % _display_name
|
||||||
|
|
||||||
|
|
@ -136,14 +136,16 @@ func _update_visual_pivot() -> void:
|
||||||
|
|
||||||
func _refresh_style() -> void:
|
func _refresh_style() -> void:
|
||||||
var idle := StyleBoxEmpty.new()
|
var idle := StyleBoxEmpty.new()
|
||||||
var normal := _make_rarity_style(_rarity_color, 2, 0.25)
|
var normal := _make_quality_style(_quality_color, 2, 0.25)
|
||||||
var hover := _make_rarity_style(_rarity_color.lightened(0.08), 4, 0.34)
|
var hover := _make_quality_style(
|
||||||
var selected_fill: Color = (
|
_quality_color.lightened(0.08), 4, 0.34
|
||||||
_rarity_color.lightened(0.12)
|
|
||||||
if _batch_selected
|
|
||||||
else _rarity_color
|
|
||||||
)
|
)
|
||||||
var selected := _make_rarity_style(
|
var selected_fill: Color = (
|
||||||
|
_quality_color.lightened(0.12)
|
||||||
|
if _batch_selected
|
||||||
|
else _quality_color
|
||||||
|
)
|
||||||
|
var selected := _make_quality_style(
|
||||||
selected_fill,
|
selected_fill,
|
||||||
6 if _batch_selected else 4,
|
6 if _batch_selected else 4,
|
||||||
0.46 if _batch_selected else 0.34,
|
0.46 if _batch_selected else 0.34,
|
||||||
|
|
@ -169,7 +171,7 @@ func _refresh_style() -> void:
|
||||||
add_theme_stylebox_override("pressed", selected)
|
add_theme_stylebox_override("pressed", selected)
|
||||||
|
|
||||||
|
|
||||||
func _make_rarity_style(
|
func _make_quality_style(
|
||||||
fill: Color,
|
fill: Color,
|
||||||
shadow_size: int,
|
shadow_size: int,
|
||||||
shadow_alpha: float,
|
shadow_alpha: float,
|
||||||
|
|
|
||||||
231
ui/game_ui.gd
231
ui/game_ui.gd
|
|
@ -5,6 +5,7 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||||
|
|
@ -43,6 +44,12 @@ const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
const WorldWeatherServiceType = preload(
|
const WorldWeatherServiceType = preload(
|
||||||
"res://world/world_weather_service.gd"
|
"res://world/world_weather_service.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerExperienceType = preload(
|
||||||
|
"res://progression/player_experience.gd"
|
||||||
|
)
|
||||||
|
const UIReferencePresentationType = preload(
|
||||||
|
"res://ui/ui_reference_presentation.gd"
|
||||||
|
)
|
||||||
|
|
||||||
signal pixelation_settings_visibility_changed(is_visible: bool)
|
signal pixelation_settings_visibility_changed(is_visible: bool)
|
||||||
signal crisp_reset_focus_requested
|
signal crisp_reset_focus_requested
|
||||||
|
|
@ -62,6 +69,13 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
|
||||||
@onready var _barrier_health: Label = %BarrierHealth
|
@onready var _barrier_health: Label = %BarrierHealth
|
||||||
@onready var _showcase_details: Label = %ShowcaseDetails
|
@onready var _showcase_details: Label = %ShowcaseDetails
|
||||||
@onready var _fishing_panel: PanelContainer = %FishingPanel
|
@onready var _fishing_panel: PanelContainer = %FishingPanel
|
||||||
|
@onready var _experience_panel: PanelContainer = %ExperienceProgressPanel
|
||||||
|
@onready var _experience_level_label: Label = %ExperienceLevelLabel
|
||||||
|
@onready var _experience_award_label: Label = %ExperienceAwardLabel
|
||||||
|
@onready var _experience_progress: ProgressBar = %ExperienceProgress
|
||||||
|
@onready var _experience_bubble: PanelContainer = %ExperienceBubble
|
||||||
|
@onready var _experience_bubble_label: Label = %ExperienceBubbleLabel
|
||||||
|
@onready var _canonical_stage: Control = %CanonicalStage
|
||||||
@onready var _player_menu: PlayerMenuType = %PlayerMenu
|
@onready var _player_menu: PlayerMenuType = %PlayerMenu
|
||||||
@onready var _screen_fade: ScreenFade = %ScreenFade
|
@onready var _screen_fade: ScreenFade = %ScreenFade
|
||||||
@onready var _title_screen: TitleScreenType = %TitleScreen
|
@onready var _title_screen: TitleScreenType = %TitleScreen
|
||||||
|
|
@ -95,6 +109,11 @@ var _item_effects: PlayerItemEffectsType
|
||||||
var _main_shop_buyer: FishBuyerProfileType
|
var _main_shop_buyer: FishBuyerProfileType
|
||||||
var _shop_interaction: ShopInteractionType
|
var _shop_interaction: ShopInteractionType
|
||||||
var _surface_drawing: NetworkSurfaceDrawingService
|
var _surface_drawing: NetworkSurfaceDrawingService
|
||||||
|
var _experience: PlayerExperienceType
|
||||||
|
var _experience_award_queue: Array[Dictionary] = []
|
||||||
|
var _experience_animation_active: bool = false
|
||||||
|
var _experience_animation_generation: int = 0
|
||||||
|
var _experience_panel_rest_y: float = 18.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -123,6 +142,7 @@ 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,
|
||||||
|
|
@ -155,6 +175,14 @@ func setup(
|
||||||
_player = player
|
_player = player
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_item_effects = item_effects
|
_item_effects = item_effects
|
||||||
|
_experience = experience
|
||||||
|
if (
|
||||||
|
_experience != null
|
||||||
|
and not _experience.experience_awarded.is_connected(
|
||||||
|
_on_experience_awarded
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_experience.experience_awarded.connect(_on_experience_awarded)
|
||||||
_chat_ui.setup(
|
_chat_ui.setup(
|
||||||
network_chat_service, network_session, spawn_service, player,
|
network_chat_service, network_session, spawn_service, player,
|
||||||
fishing_spot, settings_manager, world_time, world_weather
|
fishing_spot, settings_manager, world_time, world_weather
|
||||||
|
|
@ -303,6 +331,7 @@ func setup_data_and_identity(
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
|
_update_experience_bubble_position()
|
||||||
if _item_effects == null or not _gameplay_ui_enabled:
|
if _item_effects == null or not _gameplay_ui_enabled:
|
||||||
_effect_status.hide()
|
_effect_status.hide()
|
||||||
return
|
return
|
||||||
|
|
@ -371,6 +400,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_refresh_hotbar_visibility()
|
_refresh_hotbar_visibility()
|
||||||
_hotbar_ui.set_gameplay_input_enabled(true)
|
_hotbar_ui.set_gameplay_input_enabled(true)
|
||||||
_refresh_fishing_panel_visibility()
|
_refresh_fishing_panel_visibility()
|
||||||
|
call_deferred("_start_next_experience_animation")
|
||||||
|
|
||||||
|
|
||||||
func set_system_menu_open(is_open: bool) -> void:
|
func set_system_menu_open(is_open: bool) -> void:
|
||||||
|
|
@ -569,6 +599,7 @@ func _on_showcase_changed(
|
||||||
fish_name: String,
|
fish_name: String,
|
||||||
rarity_name: String,
|
rarity_name: String,
|
||||||
weight_lb: float,
|
weight_lb: float,
|
||||||
|
quality: int,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
) -> void:
|
) -> void:
|
||||||
_showcase_active = visible
|
_showcase_active = visible
|
||||||
|
|
@ -576,6 +607,7 @@ func _on_showcase_changed(
|
||||||
_showcase_details.text = ""
|
_showcase_details.text = ""
|
||||||
_showcase_details.visible = false
|
_showcase_details.visible = false
|
||||||
_set_fishing_status("")
|
_set_fishing_status("")
|
||||||
|
call_deferred("_start_next_experience_animation")
|
||||||
return
|
return
|
||||||
_catch_track.visible = false
|
_catch_track.visible = false
|
||||||
_barrier_prompt_panel.visible = false
|
_barrier_prompt_panel.visible = false
|
||||||
|
|
@ -583,15 +615,208 @@ func _on_showcase_changed(
|
||||||
_barrier_health.visible = false
|
_barrier_health.visible = false
|
||||||
_clear_barrier_markers()
|
_clear_barrier_markers()
|
||||||
_showcase_details.text = (
|
_showcase_details.text = (
|
||||||
"%.1f lb • %s"
|
"%s • %.1f lb"
|
||||||
% [weight_lb, rarity_name]
|
% [rarity_name.to_lower(), weight_lb]
|
||||||
)
|
)
|
||||||
_showcase_details.visible = true
|
_showcase_details.visible = true
|
||||||
_status_label.text = "You caught a %s!" % fish_name
|
_status_label.text = "You caught %s!" % (
|
||||||
|
FishQualityType.qualified_name_with_article(fish_name, quality)
|
||||||
|
)
|
||||||
_status_label.visible = true
|
_status_label.visible = true
|
||||||
_refresh_fishing_panel_visibility()
|
_refresh_fishing_panel_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_experience_awarded(
|
||||||
|
amount: int,
|
||||||
|
previous_total: int,
|
||||||
|
new_total: int,
|
||||||
|
previous_level: int,
|
||||||
|
new_level: int,
|
||||||
|
) -> void:
|
||||||
|
if amount <= 0:
|
||||||
|
return
|
||||||
|
_experience_award_queue.append({
|
||||||
|
"amount": amount,
|
||||||
|
"previous_total": previous_total,
|
||||||
|
"new_total": new_total,
|
||||||
|
"previous_level": previous_level,
|
||||||
|
"new_level": new_level,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func _start_next_experience_animation() -> void:
|
||||||
|
if (
|
||||||
|
_experience_animation_active
|
||||||
|
or _showcase_active
|
||||||
|
or _experience_award_queue.is_empty()
|
||||||
|
or not _gameplay_ui_enabled
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var award: Dictionary = _experience_award_queue.pop_front()
|
||||||
|
_experience_animation_active = true
|
||||||
|
_experience_animation_generation += 1
|
||||||
|
var generation: int = _experience_animation_generation
|
||||||
|
_play_experience_animation(award, generation)
|
||||||
|
|
||||||
|
|
||||||
|
func _play_experience_animation(
|
||||||
|
award: Dictionary,
|
||||||
|
generation: int,
|
||||||
|
) -> void:
|
||||||
|
var amount: int = int(award.get("amount", 0))
|
||||||
|
var previous_total: int = int(award.get("previous_total", 0))
|
||||||
|
var new_total: int = int(award.get("new_total", previous_total))
|
||||||
|
_experience_award_label.text = "+%d xp" % amount
|
||||||
|
_experience_bubble_label.text = "+%d xp!" % amount
|
||||||
|
_update_experience_progress(previous_total)
|
||||||
|
_experience_panel.position.y = -_experience_panel.size.y - 8.0
|
||||||
|
_experience_panel.modulate.a = 0.0
|
||||||
|
_experience_panel.show()
|
||||||
|
_experience_bubble.modulate.a = 0.0
|
||||||
|
_experience_bubble.scale = Vector2(0.72, 0.72)
|
||||||
|
_experience_bubble.pivot_offset = _experience_bubble.size * 0.5
|
||||||
|
_experience_bubble.show()
|
||||||
|
|
||||||
|
var entry_tween: Tween = create_tween()
|
||||||
|
entry_tween.set_parallel(true)
|
||||||
|
entry_tween.tween_property(
|
||||||
|
_experience_panel,
|
||||||
|
"position:y",
|
||||||
|
_experience_panel_rest_y,
|
||||||
|
0.26,
|
||||||
|
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
entry_tween.tween_property(
|
||||||
|
_experience_panel,
|
||||||
|
"modulate:a",
|
||||||
|
1.0,
|
||||||
|
0.18,
|
||||||
|
)
|
||||||
|
entry_tween.tween_property(
|
||||||
|
_experience_bubble,
|
||||||
|
"modulate:a",
|
||||||
|
1.0,
|
||||||
|
0.15,
|
||||||
|
)
|
||||||
|
entry_tween.tween_property(
|
||||||
|
_experience_bubble,
|
||||||
|
"scale",
|
||||||
|
Vector2.ONE,
|
||||||
|
0.28,
|
||||||
|
).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||||
|
await entry_tween.finished
|
||||||
|
if generation != _experience_animation_generation:
|
||||||
|
return
|
||||||
|
|
||||||
|
var fill_duration: float = clampf(
|
||||||
|
0.8 + float(amount) * 0.006,
|
||||||
|
0.9,
|
||||||
|
1.65,
|
||||||
|
)
|
||||||
|
var fill_tween: Tween = create_tween()
|
||||||
|
fill_tween.tween_method(
|
||||||
|
Callable(self, "_set_experience_animation_progress").bind(
|
||||||
|
previous_total,
|
||||||
|
new_total,
|
||||||
|
),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
fill_duration,
|
||||||
|
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||||
|
await fill_tween.finished
|
||||||
|
if generation != _experience_animation_generation:
|
||||||
|
return
|
||||||
|
await get_tree().create_timer(0.75).timeout
|
||||||
|
if generation != _experience_animation_generation:
|
||||||
|
return
|
||||||
|
|
||||||
|
var exit_tween: Tween = create_tween()
|
||||||
|
exit_tween.set_parallel(true)
|
||||||
|
exit_tween.tween_property(
|
||||||
|
_experience_panel,
|
||||||
|
"modulate:a",
|
||||||
|
0.0,
|
||||||
|
0.24,
|
||||||
|
)
|
||||||
|
exit_tween.tween_property(
|
||||||
|
_experience_bubble,
|
||||||
|
"modulate:a",
|
||||||
|
0.0,
|
||||||
|
0.2,
|
||||||
|
)
|
||||||
|
exit_tween.tween_property(
|
||||||
|
_experience_bubble,
|
||||||
|
"scale",
|
||||||
|
Vector2(0.82, 0.82),
|
||||||
|
0.24,
|
||||||
|
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
|
||||||
|
await exit_tween.finished
|
||||||
|
if generation != _experience_animation_generation:
|
||||||
|
return
|
||||||
|
_experience_panel.hide()
|
||||||
|
_experience_bubble.hide()
|
||||||
|
_experience_animation_active = false
|
||||||
|
call_deferred("_start_next_experience_animation")
|
||||||
|
|
||||||
|
|
||||||
|
func _set_experience_animation_progress(
|
||||||
|
progress: float,
|
||||||
|
previous_total: int,
|
||||||
|
new_total: int,
|
||||||
|
) -> void:
|
||||||
|
var displayed_total: int = roundi(lerpf(
|
||||||
|
float(previous_total),
|
||||||
|
float(new_total),
|
||||||
|
clampf(progress, 0.0, 1.0),
|
||||||
|
))
|
||||||
|
_update_experience_progress(displayed_total)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_experience_progress(total_experience: int) -> void:
|
||||||
|
var level: int = PlayerExperienceType.level_for_total_experience(
|
||||||
|
total_experience
|
||||||
|
)
|
||||||
|
_experience_level_label.text = "level %d" % level
|
||||||
|
_experience_progress.value = (
|
||||||
|
PlayerExperienceType.progress_for_total_experience(total_experience)
|
||||||
|
* 100.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_experience_bubble_position() -> void:
|
||||||
|
if not _experience_animation_active or _player == null:
|
||||||
|
return
|
||||||
|
var camera: Camera3D = _player.get_gameplay_camera()
|
||||||
|
var anchor_position: Vector3 = _player.get_chat_anchor_position()
|
||||||
|
if camera == null or camera.is_position_behind(anchor_position):
|
||||||
|
_experience_bubble.hide()
|
||||||
|
return
|
||||||
|
_experience_bubble.show()
|
||||||
|
var window_size := Vector2(get_window().size)
|
||||||
|
var output_scale: float = UIReferencePresentationType.get_scale(
|
||||||
|
window_size
|
||||||
|
)
|
||||||
|
var stage_position: Vector2 = (
|
||||||
|
camera.unproject_position(anchor_position) / output_scale
|
||||||
|
- _canonical_stage.position
|
||||||
|
)
|
||||||
|
var desired: Vector2 = stage_position - Vector2(
|
||||||
|
_experience_bubble.size.x * 0.5,
|
||||||
|
_experience_bubble.size.y + 10.0,
|
||||||
|
)
|
||||||
|
_experience_bubble.position = Vector2(
|
||||||
|
clampf(
|
||||||
|
desired.x,
|
||||||
|
8.0,
|
||||||
|
_canonical_stage.size.x - _experience_bubble.size.x - 8.0,
|
||||||
|
),
|
||||||
|
clampf(
|
||||||
|
desired.y,
|
||||||
|
8.0,
|
||||||
|
_canonical_stage.size.y - _experience_bubble.size.y - 8.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
func _on_player_menu_visibility_changed(is_open: bool) -> void:
|
||||||
_player_menu_open = is_open
|
_player_menu_open = is_open
|
||||||
if is_open and _surface_drawing != null:
|
if is_open and _surface_drawing != null:
|
||||||
|
|
|
||||||
108
ui/game_ui.tscn
108
ui/game_ui.tscn
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=16 format=3]
|
[gd_scene load_steps=19 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||||
|
|
@ -35,6 +35,27 @@ corner_radius_bottom_left = 12
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_transparent"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_transparent"]
|
||||||
bg_color = Color(0, 0, 0, 0)
|
bg_color = Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_panel"]
|
||||||
|
bg_color = Color(0.051, 0.173, 0.227, 0.97)
|
||||||
|
corner_radius_top_left = 14
|
||||||
|
corner_radius_top_right = 14
|
||||||
|
corner_radius_bottom_right = 14
|
||||||
|
corner_radius_bottom_left = 14
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_background"]
|
||||||
|
bg_color = Color(0.025, 0.102, 0.137, 1)
|
||||||
|
corner_radius_top_left = 7
|
||||||
|
corner_radius_top_right = 7
|
||||||
|
corner_radius_bottom_right = 7
|
||||||
|
corner_radius_bottom_left = 7
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBox_experience_fill"]
|
||||||
|
bg_color = Color(1, 0.82, 0.4, 1)
|
||||||
|
corner_radius_top_left = 7
|
||||||
|
corner_radius_top_right = 7
|
||||||
|
corner_radius_bottom_right = 7
|
||||||
|
corner_radius_bottom_left = 7
|
||||||
|
|
||||||
[node name="GameUI" type="CanvasLayer"]
|
[node name="GameUI" type="CanvasLayer"]
|
||||||
script = ExtResource("1_ui")
|
script = ExtResource("1_ui")
|
||||||
|
|
||||||
|
|
@ -59,6 +80,91 @@ grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="ExperienceProgressPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 63
|
||||||
|
anchors_preset = 10
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
offset_left = -240.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 240.0
|
||||||
|
offset_bottom = 82.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("3_theme")
|
||||||
|
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 14
|
||||||
|
theme_override_constants/margin_top = 7
|
||||||
|
theme_override_constants/margin_right = 14
|
||||||
|
theme_override_constants/margin_bottom = 9
|
||||||
|
|
||||||
|
[node name="Layout" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
[node name="Header" type="HBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ExperienceLevelLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "level 1"
|
||||||
|
theme_override_colors/font_color = Color(0.95, 0.98, 1, 1)
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="ExperienceAwardLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout/Header"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+0 xp"
|
||||||
|
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
||||||
|
theme_override_font_sizes/font_size = 16
|
||||||
|
|
||||||
|
[node name="ExperienceProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceProgressPanel/Margin/Layout"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(0, 20)
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
theme_override_styles/background = SubResource("StyleBox_experience_background")
|
||||||
|
theme_override_styles/fill = SubResource("StyleBox_experience_fill")
|
||||||
|
value = 0.0
|
||||||
|
show_percentage = false
|
||||||
|
|
||||||
|
[node name="ExperienceBubble" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 63
|
||||||
|
offset_right = 122.0
|
||||||
|
offset_bottom = 46.0
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("3_theme")
|
||||||
|
theme_override_styles/panel = SubResource("StyleBox_experience_panel")
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceBubble"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 10
|
||||||
|
theme_override_constants/margin_top = 5
|
||||||
|
theme_override_constants/margin_right = 10
|
||||||
|
theme_override_constants/margin_bottom = 5
|
||||||
|
|
||||||
|
[node name="ExperienceBubbleLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ExperienceBubble/Margin"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "+0 xp!"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
theme_override_colors/font_color = Color(1, 0.82, 0.4, 1)
|
||||||
|
theme_override_font_sizes/font_size = 22
|
||||||
|
|
||||||
[node name="FishingPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
[node name="FishingPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const BubbleHotbarSlotType = preload(
|
const BubbleHotbarSlotType = preload(
|
||||||
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -321,7 +322,10 @@ func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
||||||
if not catch_id.is_empty() and _fish_inventory != null:
|
if not catch_id.is_empty() and _fish_inventory != null:
|
||||||
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
||||||
if fish_catch != null:
|
if fish_catch != null:
|
||||||
_selected_item_label.text = fish_catch.fish.display_name
|
_selected_item_label.text = FishQualityType.qualified_name(
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
_selected_item_label.visible = true
|
_selected_item_label.visible = true
|
||||||
return
|
return
|
||||||
var item = (
|
var item = (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ extends Control
|
||||||
|
|
||||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
const SILHOUETTE_SHADER: Shader = preload(
|
const SILHOUETTE_SHADER: Shader = preload(
|
||||||
|
|
@ -559,6 +560,7 @@ func _build_known_details(fish: FishDataType) -> void:
|
||||||
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
facts_column.add_child(facts)
|
facts_column.add_child(facts)
|
||||||
|
_detail_body.add_child(_build_quality_progress(fish.id))
|
||||||
|
|
||||||
var stats_columns := HBoxContainer.new()
|
var stats_columns := HBoxContainer.new()
|
||||||
stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
@ -594,8 +596,14 @@ func _build_known_details(fish: FishDataType) -> void:
|
||||||
right_stats,
|
right_stats,
|
||||||
"value range",
|
"value range",
|
||||||
"%d–%d fish coin" % [
|
"%d–%d fish coin" % [
|
||||||
|
FishQualityType.apply_sale_value(
|
||||||
fish.sell_value_min,
|
fish.sell_value_min,
|
||||||
|
FishQualityType.Tier.BORING,
|
||||||
|
),
|
||||||
|
FishQualityType.apply_sale_value(
|
||||||
fish.sell_value_max,
|
fish.sell_value_max,
|
||||||
|
FishQualityType.Tier.SHINY,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
_add_detail_row(
|
_add_detail_row(
|
||||||
|
|
@ -605,6 +613,56 @@ func _build_known_details(fish: FishDataType) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_quality_progress(fish_id: StringName) -> VBoxContainer:
|
||||||
|
var quality_section := VBoxContainer.new()
|
||||||
|
quality_section.add_theme_constant_override("separation", 3)
|
||||||
|
var discovered_count: int = 0
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
if _collection_log.has_discovered_quality(fish_id, quality):
|
||||||
|
discovered_count += 1
|
||||||
|
var heading := _field_label(
|
||||||
|
"quality collection • %d / %d" % [
|
||||||
|
discovered_count,
|
||||||
|
FishQualityType.TIER_COUNT,
|
||||||
|
],
|
||||||
|
14,
|
||||||
|
)
|
||||||
|
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
heading.add_theme_color_override("font_color", MUTED_INK)
|
||||||
|
quality_section.add_child(heading)
|
||||||
|
var tiers := HBoxContainer.new()
|
||||||
|
tiers.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
tiers.add_theme_constant_override("separation", 10)
|
||||||
|
quality_section.add_child(tiers)
|
||||||
|
for quality: int in FishQualityType.TIER_COUNT:
|
||||||
|
var discovered: bool = _collection_log.has_discovered_quality(
|
||||||
|
fish_id,
|
||||||
|
quality,
|
||||||
|
)
|
||||||
|
var tier := HBoxContainer.new()
|
||||||
|
tier.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
|
tier.add_theme_constant_override("separation", 3)
|
||||||
|
var quality_color: Color = UIPalette.get_quality_color(quality)
|
||||||
|
var dot := _label("●" if discovered else "○", 13)
|
||||||
|
dot.add_theme_color_override("font_color", quality_color)
|
||||||
|
dot.modulate.a = 1.0 if discovered else 0.48
|
||||||
|
tier.add_child(dot)
|
||||||
|
var tier_label := _label(FishQualityType.display_name(quality), 13)
|
||||||
|
tier_label.add_theme_color_override(
|
||||||
|
"font_color",
|
||||||
|
INK if discovered else MUTED_INK,
|
||||||
|
)
|
||||||
|
tier_label.modulate.a = 1.0 if discovered else 0.58
|
||||||
|
tier.tooltip_text = (
|
||||||
|
"%s quality collected"
|
||||||
|
if discovered
|
||||||
|
else "%s quality not yet collected"
|
||||||
|
) % FishQualityType.display_name(quality)
|
||||||
|
tier.add_child(tier_label)
|
||||||
|
tiers.add_child(tier)
|
||||||
|
return quality_section
|
||||||
|
|
||||||
|
|
||||||
func _show_no_selection() -> void:
|
func _show_no_selection() -> void:
|
||||||
_clear_details()
|
_clear_details()
|
||||||
var instruction := _label("Select an entry to read its catch record.", 21)
|
var instruction := _label("Select an entry to read its catch record.", 21)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const SALUTATION_LABELS := {
|
||||||
"salutations": "Salutations",
|
"salutations": "Salutations",
|
||||||
"good_luck_have_fun": "Good Luck Have Fun",
|
"good_luck_have_fun": "Good Luck Have Fun",
|
||||||
}
|
}
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
var _service: NetworkMailService
|
var _service: NetworkMailService
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
|
|
@ -507,7 +508,11 @@ func _refresh_attachment_choices(_index: int) -> void:
|
||||||
):
|
):
|
||||||
_attachment_choice.add_item(
|
_attachment_choice.add_item(
|
||||||
"%s — %.1f lb" % [
|
"%s — %.1f lb" % [
|
||||||
fish_catch.fish.display_name, fish_catch.weight_lb,
|
FishQualityType.qualified_name(
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
),
|
||||||
|
fish_catch.weight_lb,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
_attachment_choice.set_item_metadata(
|
_attachment_choice.set_item_metadata(
|
||||||
|
|
@ -719,11 +724,18 @@ func _attachment_text(attachment: Dictionary) -> String:
|
||||||
)
|
)
|
||||||
if fish_catch != null:
|
if fish_catch != null:
|
||||||
return "%s — %.1f lb" % [
|
return "%s — %.1f lb" % [
|
||||||
fish_catch.fish.display_name, fish_catch.weight_lb,
|
FishQualityType.qualified_name(
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
),
|
||||||
|
fish_catch.weight_lb,
|
||||||
]
|
]
|
||||||
var data: Dictionary = attachment.get("catch", {})
|
var data: Dictionary = attachment.get("catch", {})
|
||||||
return "%s — %.1f lb" % [
|
return "%s — %.1f lb" % [
|
||||||
|
FishQualityType.qualified_name(
|
||||||
str(data.get("fish_id", "fish")).capitalize(),
|
str(data.get("fish_id", "fish")).capitalize(),
|
||||||
|
int(data.get("quality", FishQualityType.Tier.BORING)),
|
||||||
|
),
|
||||||
float(data.get("weight_lb", 0.0)),
|
float(data.get("weight_lb", 0.0)),
|
||||||
]
|
]
|
||||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const INPUT_OWNER: StringName = &"player_menu"
|
||||||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||||
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
||||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||||
|
|
@ -74,11 +75,6 @@ const NAVIGATION_PRESENTATION_SCALE: float = 0.60
|
||||||
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
|
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
|
||||||
const NAVIGATION_SELECTED_SCALE: float = 1.02
|
const NAVIGATION_SELECTED_SCALE: float = 1.02
|
||||||
const INVENTORY_TAB_LEFT_INSET: float = 96.0
|
const INVENTORY_TAB_LEFT_INSET: float = 96.0
|
||||||
const COOLER_RARITY_COMMON := Color("e8eef0")
|
|
||||||
const COOLER_RARITY_UNCOMMON := Color("64c87c")
|
|
||||||
const COOLER_RARITY_RARE := Color("6098dd")
|
|
||||||
const COOLER_RARITY_EPIC := Color("a979cf")
|
|
||||||
const COOLER_RARITY_LEGENDARY := Color("db78a7")
|
|
||||||
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
||||||
|
|
||||||
signal menu_visibility_changed(is_open: bool)
|
signal menu_visibility_changed(is_open: bool)
|
||||||
|
|
@ -2609,11 +2605,14 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
||||||
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
||||||
fish_node.configure(
|
fish_node.configure(
|
||||||
fish_catch.catch_id,
|
fish_catch.catch_id,
|
||||||
|
FishQualityType.qualified_name(
|
||||||
fish_catch.fish.display_name,
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
),
|
||||||
fish_catch.fish.display_texture,
|
fish_catch.fish.display_texture,
|
||||||
float(identity_hash % 628) / 100.0,
|
float(identity_hash % 628) / 100.0,
|
||||||
0.96 + float(identity_hash % 9) * 0.01,
|
0.96 + float(identity_hash % 9) * 0.01,
|
||||||
_get_cooler_rarity_color(fish_catch.fish.rarity),
|
UIPalette.get_quality_color(fish_catch.quality),
|
||||||
)
|
)
|
||||||
fish_node.set_item_state(
|
fish_node.set_item_state(
|
||||||
_fish_selection.is_selected(fish_catch.catch_id),
|
_fish_selection.is_selected(fish_catch.catch_id),
|
||||||
|
|
@ -2631,20 +2630,6 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
||||||
_configure_cooler_fish_focus()
|
_configure_cooler_fish_focus()
|
||||||
|
|
||||||
|
|
||||||
func _get_cooler_rarity_color(rarity: int) -> Color:
|
|
||||||
match rarity:
|
|
||||||
FishDataType.Rarity.UNCOMMON:
|
|
||||||
return COOLER_RARITY_UNCOMMON
|
|
||||||
FishDataType.Rarity.RARE:
|
|
||||||
return COOLER_RARITY_RARE
|
|
||||||
FishDataType.Rarity.EPIC:
|
|
||||||
return COOLER_RARITY_EPIC
|
|
||||||
FishDataType.Rarity.LEGENDARY:
|
|
||||||
return COOLER_RARITY_LEGENDARY
|
|
||||||
_:
|
|
||||||
return COOLER_RARITY_COMMON
|
|
||||||
|
|
||||||
|
|
||||||
func _layout_cooler_fish(animate: bool = true) -> void:
|
func _layout_cooler_fish(animate: bool = true) -> void:
|
||||||
if not is_node_ready():
|
if not is_node_ready():
|
||||||
return
|
return
|
||||||
|
|
@ -2926,7 +2911,10 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
|
||||||
and _reservations.is_fish_reserved(fish_catch.catch_id)
|
and _reservations.is_fish_reserved(fish_catch.catch_id)
|
||||||
):
|
):
|
||||||
_cooler_detail_name.text += " • reserved in mail"
|
_cooler_detail_name.text += " • reserved in mail"
|
||||||
_cooler_weight_unit.text = "lb • %s" % fish_catch.fish.get_rarity_name()
|
_cooler_weight_unit.text = "lb • %s • %s" % [
|
||||||
|
FishQualityType.display_name(fish_catch.quality),
|
||||||
|
fish_catch.fish.get_rarity_name(),
|
||||||
|
]
|
||||||
if buyer_offer >= 0 and active_buyer != null:
|
if buyer_offer >= 0 and active_buyer != null:
|
||||||
_cooler_offer_label.text = _get_offer_label(active_buyer)
|
_cooler_offer_label.text = _get_offer_label(active_buyer)
|
||||||
_cooler_offer_value.text = "$%d" % buyer_offer
|
_cooler_offer_value.text = "$%d" % buyer_offer
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,12 @@ const RARITY_RARE := Color("5596f6")
|
||||||
const RARITY_EPIC := Color("b176e8")
|
const RARITY_EPIC := Color("b176e8")
|
||||||
const RARITY_LEGENDARY := Color("f276b3")
|
const RARITY_LEGENDARY := Color("f276b3")
|
||||||
|
|
||||||
|
const QUALITY_BORING := Color("e8eef0")
|
||||||
|
const QUALITY_AVERAGE := Color("64c87c")
|
||||||
|
const QUALITY_IMPRESSIVE := Color("6098dd")
|
||||||
|
const QUALITY_EXCEPTIONAL := Color("a979cf")
|
||||||
|
const QUALITY_SHINY := Color("db78a7")
|
||||||
|
|
||||||
|
|
||||||
static func get_rarity_color(rarity: int) -> Color:
|
static func get_rarity_color(rarity: int) -> Color:
|
||||||
match rarity:
|
match rarity:
|
||||||
|
|
@ -31,3 +37,17 @@ static func get_rarity_color(rarity: int) -> Color:
|
||||||
return RARITY_LEGENDARY
|
return RARITY_LEGENDARY
|
||||||
_:
|
_:
|
||||||
return RARITY_COMMON
|
return RARITY_COMMON
|
||||||
|
|
||||||
|
|
||||||
|
static func get_quality_color(quality: int) -> Color:
|
||||||
|
match quality:
|
||||||
|
1:
|
||||||
|
return QUALITY_AVERAGE
|
||||||
|
2:
|
||||||
|
return QUALITY_IMPRESSIVE
|
||||||
|
3:
|
||||||
|
return QUALITY_EXCEPTIONAL
|
||||||
|
4:
|
||||||
|
return QUALITY_SHINY
|
||||||
|
_:
|
||||||
|
return QUALITY_BORING
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ const DUSK_END_HOUR: float = NIGHT_START_HOUR + TRANSITION_HALF_HOURS
|
||||||
const DEFAULT_START_HOUR: float = DAY_START_HOUR
|
const DEFAULT_START_HOUR: float = DAY_START_HOUR
|
||||||
|
|
||||||
var _time_hours: float = DEFAULT_START_HOUR
|
var _time_hours: float = DEFAULT_START_HOUR
|
||||||
|
var _persistent_time_hours: float = DEFAULT_START_HOUR
|
||||||
|
var _persistence_tracking_enabled: bool = false
|
||||||
var _running: bool = false
|
var _running: bool = false
|
||||||
var _phase: Phase = Phase.DAWN
|
var _phase: Phase = Phase.DAWN
|
||||||
|
|
||||||
|
|
@ -63,6 +65,31 @@ func synchronize_time(authoritative_time_hours: float) -> void:
|
||||||
_set_time_hours(authoritative_time_hours, true)
|
_set_time_hours(authoritative_time_hours, true)
|
||||||
|
|
||||||
|
|
||||||
|
func set_persistence_tracking_enabled(enabled: bool) -> void:
|
||||||
|
if _persistence_tracking_enabled == enabled:
|
||||||
|
return
|
||||||
|
if _persistence_tracking_enabled:
|
||||||
|
_persistent_time_hours = _time_hours
|
||||||
|
_persistence_tracking_enabled = enabled
|
||||||
|
|
||||||
|
|
||||||
|
func restore_persistent_time_hours(time_hours: float) -> bool:
|
||||||
|
if (
|
||||||
|
not is_finite(time_hours)
|
||||||
|
or time_hours < 0.0
|
||||||
|
or time_hours >= HOURS_PER_DAY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_persistent_time_hours = time_hours
|
||||||
|
if _persistence_tracking_enabled:
|
||||||
|
_set_time_hours(_persistent_time_hours, true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_persistent_time_hours() -> float:
|
||||||
|
return _persistent_time_hours
|
||||||
|
|
||||||
|
|
||||||
func get_time_hours() -> float:
|
func get_time_hours() -> float:
|
||||||
return _time_hours
|
return _time_hours
|
||||||
|
|
||||||
|
|
@ -116,6 +143,8 @@ func _set_time_hours(time_hours: float, force_emit: bool) -> void:
|
||||||
var time_was_changed: bool = not is_equal_approx(normalized, _time_hours)
|
var time_was_changed: bool = not is_equal_approx(normalized, _time_hours)
|
||||||
_time_hours = normalized
|
_time_hours = normalized
|
||||||
_phase = next_phase
|
_phase = next_phase
|
||||||
|
if _persistence_tracking_enabled:
|
||||||
|
_persistent_time_hours = normalized
|
||||||
if phase_was_changed:
|
if phase_was_changed:
|
||||||
phase_changed.emit(_phase)
|
phase_changed.emit(_phase)
|
||||||
if force_emit or time_was_changed or phase_was_changed:
|
if force_emit or time_was_changed or phase_was_changed:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue