Compare commits

..

No commits in common. "291cc4a713f47d4fa6a18f4c6702f7d2cf3f53ba" and "7661b057bc0a439cf2342f5aafbf16e0c8f7f78b" have entirely different histories.

47 changed files with 83 additions and 1844 deletions

View file

@ -2,13 +2,9 @@ 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:
@ -23,24 +19,6 @@ 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:
@ -49,53 +27,12 @@ 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

View file

@ -1,8 +1,6 @@
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
@ -27,18 +25,6 @@ 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,

View file

@ -128,15 +128,7 @@ 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 ordinary_value: int = ( var offer: int = buyer.get_offer(fish_catch.sale_value)
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

View file

@ -2,7 +2,6 @@ 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
@ -15,7 +14,6 @@ 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
@ -50,7 +48,6 @@ 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
) )
@ -63,7 +60,6 @@ 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,
} }
@ -75,7 +71,6 @@ 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,
} }
@ -93,7 +88,6 @@ 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",
]: ]:
@ -117,11 +111,6 @@ 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,
@ -139,7 +128,6 @@ 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
@ -153,7 +141,6 @@ 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
@ -170,7 +157,6 @@ 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):
@ -186,11 +172,6 @@ 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
@ -198,7 +179,6 @@ 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()
@ -208,7 +188,6 @@ 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

View file

@ -1,73 +0,0 @@
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,
)

View file

@ -1 +0,0 @@
uid://d05mwat7tju1b

View file

@ -1,91 +0,0 @@
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]

View file

@ -1 +0,0 @@
uid://csmvl1ciuux6a

View file

@ -5,12 +5,10 @@ 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
@ -83,12 +81,7 @@ 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.quality = FishQualityType.roll( caught_fish.sale_value = fish.get_sale_value_for_weight(
_rng, caught_fish.weight_lb
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

View file

@ -9,10 +9,6 @@ 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")
@ -60,7 +56,6 @@ 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
@ -105,9 +100,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.25 const BITE_QUICK_PROBABILITY: float = 0.15
const BITE_TYPICAL_PROBABILITY: float = 0.65 const BITE_TYPICAL_PROBABILITY: float = 0.55
const BITE_LONG_PROBABILITY: float = 0.08 const BITE_LONG_PROBABILITY: float = 0.25
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
@ -130,7 +125,6 @@ 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
@ -191,7 +185,6 @@ 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,
@ -207,7 +200,6 @@ 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
@ -389,11 +381,12 @@ 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
): ):
_store_catch_progression(_pending_catch) _local_inventory.add_catch(_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, 0, false) showcase_changed.emit("", "", 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()
@ -409,7 +402,8 @@ 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)
): ):
_store_catch_progression(_pending_catch) _local_inventory.add_catch(_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)
@ -1037,7 +1031,6 @@ 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
) )
@ -1060,12 +1053,13 @@ func _put_away_catch() -> void:
or _pending_catch == null or _pending_catch == null
): ):
return return
_store_catch_progression(_pending_catch) _local_inventory.add_catch(_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, 0, false) showcase_changed.emit("", "", 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(
@ -1076,29 +1070,6 @@ 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,
@ -1124,7 +1095,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, 0, false) showcase_changed.emit("", "", 0.0, false)
_catch_controller.reset() _catch_controller.reset()
state = FishingState.RETURNING state = FishingState.RETURNING
status_changed.emit("") status_changed.emit("")
@ -1167,7 +1138,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, 0, false) showcase_changed.emit("", "", 0.0, false)
_catch_controller.reset() _catch_controller.reset()
_presentation.cleanup() _presentation.cleanup()
_pending_cleanup_message = "" _pending_cleanup_message = ""

View file

@ -357,8 +357,6 @@ 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(
@ -413,7 +411,6 @@ 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,
@ -454,7 +451,6 @@ 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,
@ -471,7 +467,6 @@ 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,
@ -1159,11 +1154,6 @@ 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)
@ -1186,7 +1176,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_world_time_checkpoint(): if not _save_manager.save_if_dirty():
_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."
) )
@ -1419,7 +1409,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_world_time_checkpoint() _save_manager.save_if_dirty()
_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()

View file

@ -7,7 +7,6 @@ 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:
@ -21,7 +20,6 @@ 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):
@ -39,8 +37,6 @@ 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
): ):
@ -57,5 +53,4 @@ 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
) )

View file

@ -4,7 +4,6 @@ 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")
@ -81,11 +80,6 @@ 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"])

View file

@ -7,10 +7,6 @@ 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"
) )
@ -33,7 +29,6 @@ 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
@ -56,7 +51,6 @@ 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,
@ -68,7 +62,6 @@ 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
@ -767,18 +760,8 @@ 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_quality_discovered( _local_collection.mark_discovered(fish_id)
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

View file

@ -144,7 +144,6 @@ 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:

View file

@ -7,7 +7,6 @@ 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
@ -951,10 +950,7 @@ 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_quality_discovered( _collection_log.mark_discovered(fish_catch.fish_id)
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(
@ -966,26 +962,10 @@ 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: FishData = _fish_catalog.get_fish_by_id( var fish := _fish_catalog.get_fish_by_id(
StringName(str(data.get("fish_id", ""))) StringName(str(data.get("fish_id", "")))
) )
var fish_catch: FishCatch = FishCatch.from_network_dict(data, fish) return 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:
@ -995,7 +975,6 @@ 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(),
} }
@ -1005,10 +984,7 @@ 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_discovery_state( _collection_log.replace_discovered_ids(snapshot["discovered"])
snapshot["discovered"],
snapshot["quality_masks"],
)
func _emit_mailbox() -> void: func _emit_mailbox() -> void:

View file

@ -22,7 +22,6 @@ 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,
@ -89,7 +88,6 @@ 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,
@ -209,7 +207,6 @@ 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,

View file

@ -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",
"quality", "sale_value", "is_favorited", "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."

View file

@ -2,7 +2,6 @@ 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")
@ -253,10 +252,8 @@ 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 != FishQualityType.apply_sale_value( or decoded.sale_value < fish.sell_value_min
fish.get_sale_value_for_weight(decoded.weight_lb), or decoded.sale_value > fish.sell_value_max
decoded.quality,
)
): ):
return _rejected_result( return _rejected_result(
request_id, "Sale could not be completed." request_id, "Sale could not be completed."
@ -265,13 +262,7 @@ 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 ordinary_value: int = fish.get_sale_value_for_weight( var offer: int = buyer.get_offer(decoded.sale_value)
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

View file

@ -169,7 +169,6 @@ 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,
@ -368,7 +367,6 @@ 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,
@ -917,15 +915,6 @@ 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)
@ -959,7 +948,7 @@ func submit_client_hello(data: Dictionary) -> void:
NetworkProtocol.PROTOCOL_VERSION, NetworkProtocol.PROTOCOL_VERSION,
identity["fingerprint"], identity["fingerprint"],
identity["public_key"], identity["public_key"],
client_capabilities, _sanitized_capabilities(data.get("capability_flags", [])),
): ):
_reject_peer( _reject_peer(
sender_id, sender_id,
@ -1082,10 +1071,6 @@ 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(
@ -1096,7 +1081,6 @@ 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,

View file

@ -39,16 +39,12 @@ 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(true) _world_time.begin_session()
_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
): ):
@ -66,7 +62,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
_sequence = 0 _sequence = 0
_last_received_sequence = -1 _last_received_sequence = -1
_sync_elapsed = 0.0 _sync_elapsed = 0.0
_world_time.set_persistence_tracking_enabled(false)
_world_time.end_session() _world_time.end_session()

View file

@ -21,9 +21,6 @@ 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"
) )
@ -112,7 +109,6 @@ 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

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=18 format=3] [gd_scene load_steps=17 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,7 +14,6 @@
[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
@ -105,10 +104,6 @@ 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)

View file

@ -1,130 +0,0 @@
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,
)

View file

@ -1 +0,0 @@
uid://eml3k7a43e1d

View file

@ -2,7 +2,6 @@ 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")
@ -21,12 +20,8 @@ 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 = 6 const SAVE_VERSION: int = 4
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
@ -35,7 +30,6 @@ 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] = []
@ -46,8 +40,6 @@ 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
@ -62,8 +54,6 @@ 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
@ -106,8 +96,6 @@ 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
@ -119,8 +107,6 @@ 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
@ -132,18 +118,16 @@ 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.collection_changed.is_connected( if not _collection_log.fish_discovered.is_connected(
_on_collection_changed _on_fish_discovered
): ):
_collection_log.collection_changed.connect(_on_collection_changed) _collection_log.fish_discovered.connect(_on_fish_discovered)
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):
@ -168,10 +152,6 @@ 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:
@ -228,10 +208,7 @@ func load_player_data() -> bool:
snapshot.next_catch_sequence snapshot.next_catch_sequence
) )
var collection_restored: bool = ( var collection_restored: bool = (
_collection_log.replace_discovery_state( _collection_log.replace_discovered_ids(snapshot.discovered_ids)
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
@ -252,12 +229,6 @@ 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
@ -268,8 +239,6 @@ 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
@ -363,17 +332,6 @@ 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
@ -443,19 +401,6 @@ 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():
@ -485,7 +430,6 @@ 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(),
@ -502,10 +446,6 @@ 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(),
},
} }
@ -516,7 +456,6 @@ 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"]
@ -524,10 +463,6 @@ 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 = {}
@ -546,14 +481,11 @@ 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
@ -572,19 +504,6 @@ 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:
@ -595,26 +514,6 @@ 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] = {}
@ -802,10 +701,6 @@ 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():
@ -853,68 +748,6 @@ 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
@ -927,7 +760,7 @@ func _mark_dirty() -> void:
_autosave_timer.start(maxf(autosave_delay, 0.05)) _autosave_timer.start(maxf(autosave_delay, 0.05))
func _on_collection_changed() -> void: func _on_fish_discovered(_fish_id: StringName) -> void:
_mark_dirty() _mark_dirty()
@ -960,10 +793,6 @@ 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()
@ -1004,7 +833,6 @@ 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
@ -1015,20 +843,13 @@ 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_discovery_state( _collection_log.replace_discovered_ids(empty_discoveries)
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
@ -1054,19 +875,6 @@ 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,

View file

@ -30,9 +30,6 @@ 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"
@ -48,17 +45,11 @@ 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.quality = FishQuality.Tier.EXCEPTIONAL fish_catch.sale_value = fish.get_sale_value_for_weight(
fish_catch.sale_value = FishQuality.apply_sale_value( fish_catch.weight_lb
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)
@ -103,42 +94,12 @@ 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"]) == 6) assert(int((parsed as Dictionary)["save_version"]) == 4)
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())
@ -157,7 +118,6 @@ 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))
@ -166,10 +126,6 @@ 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"

View file

@ -1,262 +0,0 @@
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()

View file

@ -1 +0,0 @@
uid://bffg3t4du01cu

View file

@ -239,29 +239,18 @@ 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
if wait_seconds >= 180.0: assert(quick_count > 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

View file

@ -274,8 +274,6 @@ 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",
@ -309,9 +307,6 @@ 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)

View file

@ -1,47 +0,0 @@
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()

View file

@ -1 +0,0 @@
uid://c03tvq1ydhdqj

View file

@ -1,178 +0,0 @@
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

View file

@ -1 +0,0 @@
uid://djkr2iaa5taks

View file

@ -32,9 +32,6 @@ 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
) )
@ -59,9 +56,6 @@ 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
) )
@ -104,7 +98,6 @@ 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 (
@ -152,7 +145,6 @@ 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 (

View file

@ -25,7 +25,6 @@ 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()
@ -79,24 +78,6 @@ 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"]

View file

@ -12,7 +12,6 @@ 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
@ -180,10 +179,7 @@ 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

View file

@ -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 _quality_color := Color.WHITE var _rarity_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,
quality_color: Color, rarity_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
_quality_color = quality_color _rarity_color = rarity_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,16 +136,14 @@ func _update_visual_pivot() -> void:
func _refresh_style() -> void: func _refresh_style() -> void:
var idle := StyleBoxEmpty.new() var idle := StyleBoxEmpty.new()
var normal := _make_quality_style(_quality_color, 2, 0.25) var normal := _make_rarity_style(_rarity_color, 2, 0.25)
var hover := _make_quality_style( var hover := _make_rarity_style(_rarity_color.lightened(0.08), 4, 0.34)
_quality_color.lightened(0.08), 4, 0.34
)
var selected_fill: Color = ( var selected_fill: Color = (
_quality_color.lightened(0.12) _rarity_color.lightened(0.12)
if _batch_selected if _batch_selected
else _quality_color else _rarity_color
) )
var selected := _make_quality_style( var selected := _make_rarity_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,
@ -171,7 +169,7 @@ func _refresh_style() -> void:
add_theme_stylebox_override("pressed", selected) add_theme_stylebox_override("pressed", selected)
func _make_quality_style( func _make_rarity_style(
fill: Color, fill: Color,
shadow_size: int, shadow_size: int,
shadow_alpha: float, shadow_alpha: float,

View file

@ -5,7 +5,6 @@ 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")
@ -44,12 +43,6 @@ 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
@ -69,13 +62,6 @@ 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
@ -109,11 +95,6 @@ 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:
@ -142,7 +123,6 @@ func setup(
player: PlayerType, player: PlayerType,
inventory: FishInventoryType, inventory: FishInventoryType,
collection_log: CollectionLogType, collection_log: CollectionLogType,
experience: PlayerExperienceType,
wallet: PlayerWalletType, wallet: PlayerWalletType,
sale_service: FishSaleServiceType, sale_service: FishSaleServiceType,
default_buyer: FishBuyerProfileType, default_buyer: FishBuyerProfileType,
@ -175,14 +155,6 @@ 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
@ -331,7 +303,6 @@ 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
@ -400,7 +371,6 @@ 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:
@ -599,7 +569,6 @@ 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
@ -607,7 +576,6 @@ 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
@ -615,208 +583,15 @@ func _on_showcase_changed(
_barrier_health.visible = false _barrier_health.visible = false
_clear_barrier_markers() _clear_barrier_markers()
_showcase_details.text = ( _showcase_details.text = (
"%s%.1f lb" "%.1f lb%s"
% [rarity_name.to_lower(), weight_lb] % [weight_lb, rarity_name]
) )
_showcase_details.visible = true _showcase_details.visible = true
_status_label.text = "You caught %s!" % ( _status_label.text = "You caught a %s!" % fish_name
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:

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=19 format=3] [gd_scene load_steps=16 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,27 +35,6 @@ 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")
@ -80,91 +59,6 @@ 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

View file

@ -8,7 +8,6 @@ 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"
) )
@ -322,10 +321,7 @@ 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 = FishQualityType.qualified_name( _selected_item_label.text = fish_catch.fish.display_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 = (

View file

@ -3,7 +3,6 @@ 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(
@ -560,7 +559,6 @@ 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
@ -596,14 +594,8 @@ 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, fish.sell_value_max,
FishQualityType.Tier.BORING,
),
FishQualityType.apply_sale_value(
fish.sell_value_max,
FishQualityType.Tier.SHINY,
),
], ],
) )
_add_detail_row( _add_detail_row(
@ -613,56 +605,6 @@ 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)

View file

@ -16,7 +16,6 @@ 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
@ -508,11 +507,7 @@ func _refresh_attachment_choices(_index: int) -> void:
): ):
_attachment_choice.add_item( _attachment_choice.add_item(
"%s%.1f lb" % [ "%s%.1f lb" % [
FishQualityType.qualified_name( fish_catch.fish.display_name, fish_catch.weight_lb,
fish_catch.fish.display_name,
fish_catch.quality,
),
fish_catch.weight_lb,
] ]
) )
_attachment_choice.set_item_metadata( _attachment_choice.set_item_metadata(
@ -724,18 +719,11 @@ func _attachment_text(attachment: Dictionary) -> String:
) )
if fish_catch != null: if fish_catch != null:
return "%s%.1f lb" % [ return "%s%.1f lb" % [
FishQualityType.qualified_name( fish_catch.fish.display_name, fish_catch.weight_lb,
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:

View file

@ -5,7 +5,6 @@ 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")
@ -75,6 +74,11 @@ 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)
@ -2605,14 +2609,11 @@ 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,
UIPalette.get_quality_color(fish_catch.quality), _get_cooler_rarity_color(fish_catch.fish.rarity),
) )
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),
@ -2630,6 +2631,20 @@ 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
@ -2911,10 +2926,7 @@ 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%s" % [ _cooler_weight_unit.text = "lb • %s" % fish_catch.fish.get_rarity_name()
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

View file

@ -18,12 +18,6 @@ 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:
@ -37,17 +31,3 @@ 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

View file

@ -24,8 +24,6 @@ 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
@ -65,31 +63,6 @@ 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
@ -143,8 +116,6 @@ 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: