Add collectible fish quality tiers
This commit is contained in:
parent
7661b057bc
commit
35d00e8035
30 changed files with 834 additions and 73 deletions
|
|
@ -2,9 +2,13 @@ class_name CollectionLog
|
|||
extends Node
|
||||
|
||||
signal fish_discovered(fish_id: StringName)
|
||||
signal fish_quality_discovered(fish_id: StringName, quality: int)
|
||||
signal collection_changed
|
||||
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
var _discovered: Dictionary[StringName, bool] = {}
|
||||
var _quality_masks: Dictionary[StringName, int] = {}
|
||||
|
||||
|
||||
func has_discovered(fish_id: StringName) -> bool:
|
||||
|
|
@ -19,6 +23,24 @@ func mark_discovered(fish_id: StringName) -> void:
|
|||
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]:
|
||||
var discovered_ids: Array[StringName] = []
|
||||
for fish_id: StringName in _discovered:
|
||||
|
|
@ -27,12 +49,53 @@ func get_discovered_ids() -> Array[StringName]:
|
|||
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:
|
||||
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] = {}
|
||||
for fish_id: StringName in fish_ids:
|
||||
if fish_id.is_empty():
|
||||
return false
|
||||
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
|
||||
_quality_masks = replacement_masks
|
||||
collection_changed.emit()
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
class_name FishBuyerProfile
|
||||
extends Resource
|
||||
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
@export var id: StringName
|
||||
@export var display_name: String
|
||||
@export var animal_name_singular: String
|
||||
|
|
@ -25,6 +27,18 @@ func get_offer(base_value: int) -> int:
|
|||
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(
|
||||
fish_name: String,
|
||||
payout: int,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,15 @@ func _validate_batch(
|
|||
if fish_catch.sale_value < 0:
|
||||
result.status = FishSaleResultType.Status.INVALID_VALUE
|
||||
return result
|
||||
var offer: int = buyer.get_offer(fish_catch.sale_value)
|
||||
var ordinary_value: int = (
|
||||
fish_catch.fish.get_sale_value_for_weight(
|
||||
fish_catch.weight_lb
|
||||
)
|
||||
)
|
||||
var offer: int = buyer.get_quality_offer(
|
||||
ordinary_value,
|
||||
fish_catch.quality,
|
||||
)
|
||||
if offer < 0:
|
||||
result.status = FishSaleResultType.Status.INVALID_OFFER
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name FishCatch
|
|||
extends Resource
|
||||
|
||||
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_DISPLAY_SCALE: float = 10000.0
|
||||
|
|
@ -14,6 +15,7 @@ const MAX_SAFE_SEQUENCE: int = 2147483647
|
|||
@export var catch_sequence: int = 0
|
||||
@export var weight_lb: float = 0.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 is_favorited: bool = false
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ func is_valid() -> bool:
|
|||
and is_finite(display_scale)
|
||||
and display_scale > 0.0
|
||||
and display_scale <= MAX_SAFE_DISPLAY_SCALE
|
||||
and FishQualityType.is_valid(quality)
|
||||
and sale_value >= 0
|
||||
and sale_value <= MAX_SAFE_SALE_VALUE
|
||||
)
|
||||
|
|
@ -60,6 +63,7 @@ func to_save_dict() -> Dictionary:
|
|||
"fish_id": String(fish_id),
|
||||
"weight_lb": weight_lb,
|
||||
"display_scale": display_scale,
|
||||
"quality": quality,
|
||||
"sale_value": sale_value,
|
||||
"is_favorited": is_favorited,
|
||||
}
|
||||
|
|
@ -71,6 +75,7 @@ func to_network_dict() -> Dictionary:
|
|||
"fish_id": String(fish_id),
|
||||
"weight_lb": weight_lb,
|
||||
"display_scale": display_scale,
|
||||
"quality": quality,
|
||||
"sale_value": sale_value,
|
||||
"is_favorited": is_favorited,
|
||||
}
|
||||
|
|
@ -88,6 +93,7 @@ static func from_save_dict(
|
|||
"fish_id",
|
||||
"weight_lb",
|
||||
"display_scale",
|
||||
"quality",
|
||||
"sale_value",
|
||||
"is_favorited",
|
||||
]:
|
||||
|
|
@ -111,6 +117,11 @@ static func from_save_dict(
|
|||
-1,
|
||||
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(
|
||||
data["weight_lb"],
|
||||
0.0,
|
||||
|
|
@ -128,6 +139,7 @@ static func from_save_dict(
|
|||
or loaded_sequence <= 0
|
||||
or loaded_sequence >= MAX_SAFE_SEQUENCE
|
||||
or loaded_sale_value < 0
|
||||
or not FishQualityType.is_valid(loaded_quality)
|
||||
or loaded_weight <= 0.0
|
||||
or loaded_scale <= 0.0
|
||||
or typeof(loaded_favorite) != TYPE_BOOL
|
||||
|
|
@ -141,6 +153,7 @@ static func from_save_dict(
|
|||
fish_catch.catch_sequence = loaded_sequence
|
||||
fish_catch.weight_lb = loaded_weight
|
||||
fish_catch.display_scale = loaded_scale
|
||||
fish_catch.quality = loaded_quality
|
||||
fish_catch.sale_value = loaded_sale_value
|
||||
fish_catch.is_favorited = bool(loaded_favorite)
|
||||
return fish_catch if fish_catch.is_valid() else null
|
||||
|
|
@ -157,6 +170,7 @@ static func from_network_dict(
|
|||
"fish_id",
|
||||
"weight_lb",
|
||||
"display_scale",
|
||||
"quality",
|
||||
"sale_value",
|
||||
]:
|
||||
if not data.has(required_key):
|
||||
|
|
@ -172,6 +186,11 @@ static func from_network_dict(
|
|||
var loaded_value: int = _read_safe_integer(
|
||||
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 (
|
||||
loaded_id.is_empty()
|
||||
or loaded_id.length() > 160
|
||||
|
|
@ -179,6 +198,7 @@ static func from_network_dict(
|
|||
or loaded_weight <= 0.0
|
||||
or loaded_scale <= 0.0
|
||||
or loaded_value < 0
|
||||
or not FishQualityType.is_valid(loaded_quality)
|
||||
):
|
||||
return null
|
||||
var fish_catch := FishCatch.new()
|
||||
|
|
@ -188,6 +208,7 @@ static func from_network_dict(
|
|||
fish_catch.catch_sequence = 0
|
||||
fish_catch.weight_lb = loaded_weight
|
||||
fish_catch.display_scale = loaded_scale
|
||||
fish_catch.quality = loaded_quality
|
||||
fish_catch.sale_value = loaded_value
|
||||
fish_catch.is_favorited = false
|
||||
return fish_catch if fish_catch.is_valid() else null
|
||||
|
|
|
|||
91
fish/fish_quality.gd
Normal file
91
fish/fish_quality.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
class_name FishQuality
|
||||
extends RefCounted
|
||||
|
||||
enum Tier {
|
||||
BORING,
|
||||
AVERAGE,
|
||||
IMPRESSIVE,
|
||||
EXCEPTIONAL,
|
||||
SHINY,
|
||||
}
|
||||
|
||||
const TIER_COUNT: int = 5
|
||||
const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
|
||||
|
||||
# Starting distribution. Equipment and tackle can supply per-tier
|
||||
# multipliers later without changing catch serialization or tier identity.
|
||||
const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
|
||||
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
|
||||
const DISPLAY_NAMES: PackedStringArray = [
|
||||
"boring",
|
||||
"average",
|
||||
"impressive",
|
||||
"exceptional",
|
||||
"shiny",
|
||||
]
|
||||
|
||||
|
||||
static func is_valid(quality: int) -> bool:
|
||||
return quality >= 0 and quality < TIER_COUNT
|
||||
|
||||
|
||||
static func display_name(quality: int) -> String:
|
||||
return DISPLAY_NAMES[quality] if is_valid(quality) else "unknown"
|
||||
|
||||
|
||||
static func bit_for(quality: int) -> int:
|
||||
return 1 << quality if is_valid(quality) else 0
|
||||
|
||||
|
||||
static func sale_multiplier(quality: int) -> float:
|
||||
return SALE_MULTIPLIERS[quality] if is_valid(quality) else 1.0
|
||||
|
||||
|
||||
static func apply_sale_value(base_value: int, quality: int) -> int:
|
||||
if base_value <= 0 or not is_valid(quality):
|
||||
return maxi(base_value, 0)
|
||||
if quality == Tier.BORING:
|
||||
return base_value
|
||||
return maxi(
|
||||
base_value + quality,
|
||||
ceili(float(base_value) * sale_multiplier(quality)),
|
||||
)
|
||||
|
||||
|
||||
static func roll(
|
||||
rng: RandomNumberGenerator,
|
||||
weight_multipliers: Array[float] = [],
|
||||
) -> int:
|
||||
if rng == null:
|
||||
return Tier.BORING
|
||||
var weights: Array[float] = []
|
||||
var total_weight: float = 0.0
|
||||
for quality: int in TIER_COUNT:
|
||||
var multiplier: float = 1.0
|
||||
if quality < weight_multipliers.size():
|
||||
multiplier = maxf(weight_multipliers[quality], 0.0)
|
||||
var weight: float = BASE_ROLL_WEIGHTS[quality] * multiplier
|
||||
weights.append(weight)
|
||||
total_weight += weight
|
||||
if total_weight <= 0.0:
|
||||
return Tier.BORING
|
||||
var rolled_weight: float = rng.randf() * total_weight
|
||||
var accumulated_weight: float = 0.0
|
||||
for quality: int in TIER_COUNT:
|
||||
accumulated_weight += weights[quality]
|
||||
if rolled_weight <= accumulated_weight:
|
||||
return quality
|
||||
return Tier.SHINY
|
||||
|
||||
|
||||
static func qualified_name(fish_name: String, quality: int) -> String:
|
||||
return "%s %s" % [display_name(quality), fish_name]
|
||||
|
||||
|
||||
static func qualified_name_with_article(
|
||||
fish_name: String,
|
||||
quality: int,
|
||||
) -> String:
|
||||
var quality_name: String = display_name(quality)
|
||||
var article: String = "an" if quality_name[0] in ["a", "e", "i", "o", "u"] else "a"
|
||||
return "%s %s %s" % [article, quality_name, fish_name]
|
||||
1
fish/fish_quality.gd.uid
Normal file
1
fish/fish_quality.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://csmvl1ciuux6a
|
||||
|
|
@ -5,10 +5,12 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
|
|||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishDataType = preload("res://fish/fish_data.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")
|
||||
|
||||
var undiscovered_weight_multiplier: float = 1.5
|
||||
var rarity_weight_multipliers: Array[float] = []
|
||||
var quality_weight_multipliers: Array[float] = []
|
||||
var use_deterministic_test_seed: bool = false
|
||||
var deterministic_test_seed: int = 24680
|
||||
var selection_seed: int = 0
|
||||
|
|
@ -81,7 +83,12 @@ func create_catch(fish: FishDataType) -> FishCatchType:
|
|||
caught_fish.display_scale = fish.get_display_scale_for_weight(
|
||||
caught_fish.weight_lb
|
||||
)
|
||||
caught_fish.sale_value = fish.get_sale_value_for_weight(
|
||||
caught_fish.weight_lb
|
||||
caught_fish.quality = FishQualityType.roll(
|
||||
_rng,
|
||||
quality_weight_multipliers,
|
||||
)
|
||||
caught_fish.sale_value = FishQualityType.apply_sale_value(
|
||||
fish.get_sale_value_for_weight(caught_fish.weight_lb),
|
||||
caught_fish.quality,
|
||||
)
|
||||
return caught_fish
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ signal showcase_changed(
|
|||
fish_name: String,
|
||||
rarity_name: String,
|
||||
weight_lb: float,
|
||||
quality: int,
|
||||
visible: bool,
|
||||
)
|
||||
signal bite_activated
|
||||
|
|
@ -382,11 +383,14 @@ func _secure_showcase_catch_for_recovery() -> void:
|
|||
and _local_collection_log != null
|
||||
):
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
showcase_changed.emit("", "", 0.0, 0, false)
|
||||
if _active_player != null:
|
||||
_active_player.end_catch_showcase(Callable(), true)
|
||||
_cleanup_attempt()
|
||||
|
|
@ -403,7 +407,10 @@ func _exit_tree() -> void:
|
|||
and is_instance_valid(_local_collection_log)
|
||||
):
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_pending_catch = null
|
||||
if _active_player != null and is_instance_valid(_active_player):
|
||||
_active_player.end_catch_showcase(Callable(), true)
|
||||
|
|
@ -1031,6 +1038,7 @@ func _on_outcome_completed(outcome: StringName) -> void:
|
|||
_pending_catch.fish.display_name,
|
||||
_pending_catch.fish.get_rarity_name(),
|
||||
_pending_catch.weight_lb,
|
||||
_pending_catch.quality,
|
||||
true
|
||||
)
|
||||
|
||||
|
|
@ -1054,12 +1062,15 @@ func _put_away_catch() -> void:
|
|||
):
|
||||
return
|
||||
_local_inventory.add_catch(_pending_catch)
|
||||
_local_collection_log.mark_discovered(_pending_catch.fish_id)
|
||||
_local_collection_log.mark_quality_discovered(
|
||||
_pending_catch.fish_id,
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_pending_catch = null
|
||||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
showcase_changed.emit("", "", 0.0, 0, false)
|
||||
_showcase_restore_generation += 1
|
||||
var restore_generation: int = _showcase_restore_generation
|
||||
_active_player.end_catch_showcase(
|
||||
|
|
@ -1095,7 +1106,7 @@ func _cleanup_attempt(
|
|||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
showcase_changed.emit("", "", 0.0, 0, false)
|
||||
_catch_controller.reset()
|
||||
state = FishingState.RETURNING
|
||||
status_changed.emit("")
|
||||
|
|
@ -1138,7 +1149,7 @@ func _finalize_attempt_cleanup(cooldown_message: String) -> void:
|
|||
_showcase_ready = false
|
||||
_showcase_outcome_completed = false
|
||||
_put_away_press_armed = false
|
||||
showcase_changed.emit("", "", 0.0, false)
|
||||
showcase_changed.emit("", "", 0.0, 0, false)
|
||||
_catch_controller.reset()
|
||||
_presentation.cleanup()
|
||||
_pending_cleanup_message = ""
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const MAX_SESSION_ID_LENGTH: int = 96
|
|||
const MAX_FISH_ID_LENGTH: int = 96
|
||||
const MAX_WEIGHT_LB: float = 1000.0
|
||||
const MAX_DISPLAY_SCALE: float = 20.0
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
|
||||
static func validate_state(data: Variant) -> bool:
|
||||
|
|
@ -20,6 +21,7 @@ static func validate_state(data: Variant) -> bool:
|
|||
"fish_id",
|
||||
"weight_lb",
|
||||
"display_scale",
|
||||
"quality",
|
||||
"revision",
|
||||
]:
|
||||
if not value.has(key):
|
||||
|
|
@ -37,6 +39,8 @@ static func validate_state(data: Variant) -> bool:
|
|||
or not is_finite(float(value["weight_lb"]))
|
||||
or typeof(value["display_scale"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
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 int(value["revision"]) < 0
|
||||
):
|
||||
|
|
@ -53,4 +57,5 @@ static func validate_state(data: Variant) -> bool:
|
|||
str(value["fish_id"]).is_empty()
|
||||
and is_zero_approx(float(value["weight_lb"]))
|
||||
and is_zero_approx(float(value["display_scale"]))
|
||||
and int(value["quality"]) == FishQualityType.Tier.BORING
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends Node
|
|||
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
const FishDataType = preload("res://fish/fish_data.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 PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
|
||||
|
|
@ -80,6 +81,11 @@ func _submit_local_state(fish_catch: FishCatchType, should_show: bool) -> void:
|
|||
"display_scale": (
|
||||
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,
|
||||
}
|
||||
_local_visible = bool(data["visible"])
|
||||
|
|
|
|||
|
|
@ -761,7 +761,10 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
|||
return
|
||||
if not already_owned:
|
||||
_local_inventory.add_catch(fish_catch)
|
||||
_local_collection.mark_discovered(fish_id)
|
||||
_local_collection.mark_quality_discovered(
|
||||
fish_id,
|
||||
fish_catch.quality,
|
||||
)
|
||||
if not _save_manager.save_if_dirty():
|
||||
return
|
||||
_result_ledgers[result_id] = true
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ static func attachment_signature_fields(value: Variant) -> Array:
|
|||
result.append(str(fish.get("fish_id", "")))
|
||||
result.append(str(fish.get("weight_lb", "")))
|
||||
result.append(str(fish.get("display_scale", "")))
|
||||
result.append(int(fish.get("quality", -1)))
|
||||
result.append(int(fish.get("sale_value", 0)))
|
||||
result.append(bool(fish.get("is_favorited", false)))
|
||||
3:
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ signal operation_finished(success: bool, message: String)
|
|||
signal peers_changed
|
||||
|
||||
const MAX_LEDGER: int = 256
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
var _session: NetworkSession
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
|
@ -950,7 +951,10 @@ func _apply_award(attachment: Dictionary) -> bool:
|
|||
PlayerAssetReservationService.AttachmentType.FISH:
|
||||
var fish_catch := _decode_catch(attachment)
|
||||
_inventory.add_catch(fish_catch)
|
||||
_collection_log.mark_discovered(fish_catch.fish_id)
|
||||
_collection_log.mark_quality_discovered(
|
||||
fish_catch.fish_id,
|
||||
fish_catch.quality,
|
||||
)
|
||||
return _inventory.contains_catch_id(fish_catch.catch_id)
|
||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||
return _bag.add_item(
|
||||
|
|
@ -962,10 +966,26 @@ func _apply_award(attachment: Dictionary) -> bool:
|
|||
|
||||
func _decode_catch(attachment: Dictionary) -> FishCatch:
|
||||
var data: Dictionary = attachment.get("catch", {})
|
||||
var fish := _fish_catalog.get_fish_by_id(
|
||||
var fish: FishData = _fish_catalog.get_fish_by_id(
|
||||
StringName(str(data.get("fish_id", "")))
|
||||
)
|
||||
return FishCatch.from_network_dict(data, fish)
|
||||
var fish_catch: FishCatch = FishCatch.from_network_dict(data, fish)
|
||||
if fish_catch == null or fish == null:
|
||||
return null
|
||||
if (
|
||||
fish_catch.weight_lb < fish.get_minimum_weight()
|
||||
or fish_catch.weight_lb > fish.get_maximum_weight()
|
||||
or not is_equal_approx(
|
||||
fish_catch.display_scale,
|
||||
fish.get_display_scale_for_weight(fish_catch.weight_lb),
|
||||
)
|
||||
or fish_catch.sale_value != FishQualityType.apply_sale_value(
|
||||
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||
fish_catch.quality,
|
||||
)
|
||||
):
|
||||
return null
|
||||
return fish_catch
|
||||
|
||||
|
||||
func _capture_assets() -> Dictionary:
|
||||
|
|
@ -975,6 +995,7 @@ func _capture_assets() -> Dictionary:
|
|||
"catches": _inventory.get_all_catches(),
|
||||
"next_sequence": _inventory.get_next_catch_sequence(),
|
||||
"discovered": _collection_log.get_discovered_ids(),
|
||||
"quality_masks": _collection_log.get_discovered_quality_masks(),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -984,7 +1005,10 @@ func _restore_assets(snapshot: Dictionary) -> void:
|
|||
_inventory.replace_all_catches(
|
||||
snapshot["catches"], int(snapshot["next_sequence"])
|
||||
)
|
||||
_collection_log.replace_discovered_ids(snapshot["discovered"])
|
||||
_collection_log.replace_discovery_state(
|
||||
snapshot["discovered"],
|
||||
snapshot["quality_masks"],
|
||||
)
|
||||
|
||||
|
||||
func _emit_mailbox() -> void:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
|||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
||||
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
||||
|
||||
enum RejectionCode {
|
||||
NONE,
|
||||
|
|
@ -88,6 +89,7 @@ static func make_client_hello(
|
|||
"display_name": display_name,
|
||||
"client_nonce": client_nonce,
|
||||
"capability_flags": PackedStringArray([
|
||||
FISH_QUALITY_CAPABILITY,
|
||||
SURFACE_DRAWING_CAPABILITY,
|
||||
WORLD_TIME_CAPABILITY,
|
||||
WORLD_WEATHER_CAPABILITY,
|
||||
|
|
@ -207,6 +209,7 @@ static func make_server_hello(
|
|||
"item_use_v1",
|
||||
"equipment_v1",
|
||||
"fish_showcase_v1",
|
||||
FISH_QUALITY_CAPABILITY,
|
||||
SURFACE_DRAWING_CAPABILITY,
|
||||
WORLD_TIME_CAPABILITY,
|
||||
WORLD_WEATHER_CAPABILITY,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ static func validate_request(data: Variant) -> String:
|
|||
var evidence: Dictionary = value
|
||||
for key: String in [
|
||||
"catch_id", "fish_id", "weight_lb", "display_scale",
|
||||
"sale_value", "is_favorited",
|
||||
"quality", "sale_value", "is_favorited",
|
||||
]:
|
||||
if not evidence.has(key):
|
||||
return "Sale could not be completed."
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name NetworkSaleService
|
|||
extends Node
|
||||
|
||||
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 FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||
|
|
@ -252,8 +253,10 @@ func _build_authoritative_result(
|
|||
not str(decoded.catch_id).begins_with("%s:" % fish_id)
|
||||
or decoded.weight_lb < fish.get_minimum_weight()
|
||||
or decoded.weight_lb > fish.get_maximum_weight()
|
||||
or decoded.sale_value < fish.sell_value_min
|
||||
or decoded.sale_value > fish.sell_value_max
|
||||
or decoded.sale_value != FishQualityType.apply_sale_value(
|
||||
fish.get_sale_value_for_weight(decoded.weight_lb),
|
||||
decoded.quality,
|
||||
)
|
||||
):
|
||||
return _rejected_result(
|
||||
request_id, "Sale could not be completed."
|
||||
|
|
@ -262,7 +265,13 @@ func _build_authoritative_result(
|
|||
return _rejected_result(
|
||||
request_id, "Favorite catches cannot be sold."
|
||||
)
|
||||
var offer: int = buyer.get_offer(decoded.sale_value)
|
||||
var ordinary_value: int = fish.get_sale_value_for_weight(
|
||||
decoded.weight_lb
|
||||
)
|
||||
var offer: int = buyer.get_quality_offer(
|
||||
ordinary_value,
|
||||
decoded.quality,
|
||||
)
|
||||
if (
|
||||
offer < 0
|
||||
or base_value > 9223372036854775807 - decoded.sale_value
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ func start_private_host(port: int = DEFAULT_PORT) -> bool:
|
|||
_player_identity.fingerprint,
|
||||
_player_identity.public_pem,
|
||||
PackedStringArray([
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
|
|
@ -367,6 +368,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
|
|
@ -915,6 +917,15 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
if not validation_error.is_empty():
|
||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE)
|
||||
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, {})
|
||||
if identity.is_empty():
|
||||
_reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
|
||||
|
|
@ -948,7 +959,7 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.PROTOCOL_VERSION,
|
||||
identity["fingerprint"],
|
||||
identity["public_key"],
|
||||
_sanitized_capabilities(data.get("capability_flags", [])),
|
||||
client_capabilities,
|
||||
):
|
||||
_reject_peer(
|
||||
sender_id,
|
||||
|
|
@ -1071,6 +1082,10 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
for value: Variant in advertised_capabilities:
|
||||
if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
_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()
|
||||
_registry.clear()
|
||||
_registry.add_peer(
|
||||
|
|
@ -1081,6 +1096,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_player_identity.fingerprint,
|
||||
_player_identity.public_pem,
|
||||
PackedStringArray([
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name PlayerSaveManager
|
|||
extends Node
|
||||
|
||||
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 CollectionLogType = preload("res://collection/collection_log.gd")
|
||||
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||
|
|
@ -21,7 +22,7 @@ const PlayerArtUnlocksType = preload(
|
|||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
|
||||
const SAVE_VERSION: int = 4
|
||||
const SAVE_VERSION: int = 5
|
||||
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||
const MAX_SAFE_BALANCE: int = 1000000000000
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ class LoadSnapshot:
|
|||
|
||||
var catches: Array[FishCatchType] = []
|
||||
var discovered_ids: Array[StringName] = []
|
||||
var discovered_quality_masks: Dictionary[StringName, int] = {}
|
||||
var wallet_balance: int = 0
|
||||
var next_catch_sequence: int = 1
|
||||
var bag_items: Array[OwnedItemType] = []
|
||||
|
|
@ -124,10 +126,10 @@ func setup(
|
|||
return
|
||||
if not _inventory.catches_changed.is_connected(_mark_dirty):
|
||||
_inventory.catches_changed.connect(_mark_dirty)
|
||||
if not _collection_log.fish_discovered.is_connected(
|
||||
_on_fish_discovered
|
||||
if not _collection_log.collection_changed.is_connected(
|
||||
_on_collection_changed
|
||||
):
|
||||
_collection_log.fish_discovered.connect(_on_fish_discovered)
|
||||
_collection_log.collection_changed.connect(_on_collection_changed)
|
||||
if not _wallet.balance_changed.is_connected(_on_balance_changed):
|
||||
_wallet.balance_changed.connect(_on_balance_changed)
|
||||
if not _bag.contents_changed.is_connected(_mark_dirty):
|
||||
|
|
@ -208,7 +210,10 @@ func load_player_data() -> bool:
|
|||
snapshot.next_catch_sequence
|
||||
)
|
||||
var collection_restored: bool = (
|
||||
_collection_log.replace_discovered_ids(snapshot.discovered_ids)
|
||||
_collection_log.replace_discovery_state(
|
||||
snapshot.discovered_ids,
|
||||
snapshot.discovered_quality_masks,
|
||||
)
|
||||
)
|
||||
var wallet_restored: bool = _wallet.restore_balance(
|
||||
snapshot.wallet_balance
|
||||
|
|
@ -401,6 +406,19 @@ func _build_save_dictionary() -> Dictionary:
|
|||
if fish_id.is_empty():
|
||||
return {}
|
||||
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] = []
|
||||
for owned: OwnedItemType in _bag.get_all_items():
|
||||
if owned == null or not owned.is_valid():
|
||||
|
|
@ -430,6 +448,7 @@ func _build_save_dictionary() -> Dictionary:
|
|||
},
|
||||
"collection": {
|
||||
"discovered_fish_ids": discovered_strings,
|
||||
"discovered_quality_masks": serialized_quality_masks,
|
||||
},
|
||||
"inventory": {
|
||||
"next_catch_sequence": _inventory.get_next_catch_sequence(),
|
||||
|
|
@ -481,6 +500,8 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
if (
|
||||
not wallet_data.has("balance")
|
||||
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 not inventory_data.has("next_catch_sequence")
|
||||
or typeof(bag_data.get("items")) != TYPE_ARRAY
|
||||
|
|
@ -514,6 +535,26 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
continue
|
||||
seen_discoveries[fish_id] = true
|
||||
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_sequences: Dictionary[int, bool] = {}
|
||||
|
|
@ -701,6 +742,8 @@ func _migrate_save(
|
|||
migrated = _migrate_version_2_to_3(migrated)
|
||||
3:
|
||||
migrated = _migrate_version_3_to_4(migrated)
|
||||
4:
|
||||
migrated = _migrate_version_4_to_5(migrated)
|
||||
_:
|
||||
return {}
|
||||
if migrated.is_empty():
|
||||
|
|
@ -748,6 +791,58 @@ func _migrate_version_3_to_4(data: Dictionary) -> Dictionary:
|
|||
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 _mark_dirty() -> void:
|
||||
if (
|
||||
_is_restoring
|
||||
|
|
@ -760,7 +855,7 @@ func _mark_dirty() -> void:
|
|||
_autosave_timer.start(maxf(autosave_delay, 0.05))
|
||||
|
||||
|
||||
func _on_fish_discovered(_fish_id: StringName) -> void:
|
||||
func _on_collection_changed() -> void:
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
|
|
@ -833,6 +928,7 @@ func _restore_defaults() -> void:
|
|||
_is_restoring = true
|
||||
var empty_catches: Array[FishCatchType] = []
|
||||
var empty_discoveries: Array[StringName] = []
|
||||
var empty_quality_masks: Dictionary[StringName, int] = {}
|
||||
var default_items: Array[OwnedItemType] = []
|
||||
var basic_rod := OwnedItemType.new()
|
||||
basic_rod.item_id = BASIC_ROD_ID
|
||||
|
|
@ -843,7 +939,10 @@ func _restore_defaults() -> void:
|
|||
default_slots.fill(StringName())
|
||||
default_slots[0] = BASIC_ROD_ID
|
||||
_inventory.replace_all_catches(empty_catches, 1)
|
||||
_collection_log.replace_discovered_ids(empty_discoveries)
|
||||
_collection_log.replace_discovery_state(
|
||||
empty_discoveries,
|
||||
empty_quality_masks,
|
||||
)
|
||||
_wallet.restore_balance(0)
|
||||
_bag.replace_all_items(default_items)
|
||||
_hotbar.replace_state(default_slots, 0)
|
||||
|
|
|
|||
|
|
@ -45,11 +45,17 @@ func _run() -> void:
|
|||
fish_catch.display_scale = fish.get_display_scale_for_weight(
|
||||
fish_catch.weight_lb
|
||||
)
|
||||
fish_catch.sale_value = fish.get_sale_value_for_weight(
|
||||
fish_catch.weight_lb
|
||||
fish_catch.quality = FishQuality.Tier.EXCEPTIONAL
|
||||
fish_catch.sale_value = FishQuality.apply_sale_value(
|
||||
fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||
fish_catch.quality,
|
||||
)
|
||||
fish_catch.ensure_identity()
|
||||
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 hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
|
||||
assert(hotbar_ui != null)
|
||||
|
|
@ -94,12 +100,27 @@ func _run() -> void:
|
|||
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 4)
|
||||
assert(int((parsed as Dictionary)["save_version"]) == 5)
|
||||
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(save_manager.load_player_data())
|
||||
assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
|
||||
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(service.toggle_selected_fish())
|
||||
assert(service.is_local_showcase_visible())
|
||||
|
|
@ -118,6 +139,7 @@ func _run() -> void:
|
|||
"display_scale": fish.get_display_scale_for_weight(
|
||||
fish.get_minimum_weight()
|
||||
),
|
||||
"quality": FishQuality.Tier.BORING,
|
||||
"revision": 1,
|
||||
}
|
||||
assert(NetworkFishShowcaseProtocol.validate_state(valid_state))
|
||||
|
|
@ -126,6 +148,10 @@ func _run() -> void:
|
|||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||
assert(NetworkProtocol.PROTOCOL_VERSION == 3)
|
||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||
assert(
|
||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||
== "fish_quality_v1"
|
||||
)
|
||||
assert(
|
||||
NetworkFishShowcaseProtocol.CAPABILITY
|
||||
== &"fish_showcase_v1"
|
||||
|
|
|
|||
255
tests/fish_quality_validation.gd
Normal file
255
tests/fish_quality_validation.gd
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
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)) == 5)
|
||||
var catches: Array = migrated["inventory"]["catches"]
|
||||
assert(int((catches[0] as Dictionary)["quality"]) == 0)
|
||||
var collection: Dictionary = migrated["collection"]
|
||||
var ids: Array = collection["discovered_fish_ids"]
|
||||
assert("bluegill" in ids and "carp" in ids)
|
||||
var masks: Dictionary = collection["discovered_quality_masks"]
|
||||
var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
|
||||
assert(int(masks["bluegill"]) == boring_bit)
|
||||
assert(int(masks["carp"]) == boring_bit)
|
||||
manager.queue_free()
|
||||
1
tests/fish_quality_validation.gd.uid
Normal file
1
tests/fish_quality_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bffg3t4du01cu
|
||||
|
|
@ -274,6 +274,8 @@ func _validate_handwritten_logbook_font(page: LogbookPage) -> void:
|
|||
var detail_body := page.get("_detail_body") as VBoxContainer
|
||||
for node: Node in detail_body.find_children("*", "Label", true, false):
|
||||
var label := node as Label
|
||||
if label.text.begins_with("quality collection"):
|
||||
continue
|
||||
if label.text in [
|
||||
"fish facts",
|
||||
"catalog number",
|
||||
|
|
@ -307,6 +309,9 @@ func _validate_detail_field_fonts(page: LogbookPage) -> void:
|
|||
]
|
||||
for node: Node in detail_body.find_children("*", "Label", true, false):
|
||||
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:
|
||||
continue
|
||||
assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
const SELECTED_SCALE: float = 1.12
|
||||
const HOVER_SCALE: float = 1.025
|
||||
|
|
@ -179,7 +180,10 @@ func refresh() -> void:
|
|||
_quantity_label.visible = not quantity_text.is_empty()
|
||||
tooltip_text = (
|
||||
"%s · %.1f lb" % [
|
||||
FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
),
|
||||
fish_catch.weight_lb,
|
||||
]
|
||||
if fish_catch != null
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ var _depth_scale: float = 1.0
|
|||
var _batch_selected: bool = false
|
||||
var _focused_catch: bool = false
|
||||
var _hovered: bool = false
|
||||
var _rarity_color := Color.WHITE
|
||||
var _quality_color := Color.WHITE
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -40,7 +40,7 @@ func configure(
|
|||
texture: Texture2D,
|
||||
phase: float,
|
||||
depth_scale: float,
|
||||
rarity_color: Color,
|
||||
quality_color: Color,
|
||||
) -> void:
|
||||
catch_id = identity
|
||||
_display_name = display_name
|
||||
|
|
@ -48,7 +48,7 @@ func configure(
|
|||
_fish_shadow.texture = texture
|
||||
_motion_phase = phase
|
||||
_depth_scale = depth_scale
|
||||
_rarity_color = rarity_color
|
||||
_quality_color = quality_color
|
||||
_refresh_style()
|
||||
tooltip_text = "%s · drag to a hotbar slot" % _display_name
|
||||
|
||||
|
|
@ -136,14 +136,16 @@ func _update_visual_pivot() -> void:
|
|||
|
||||
func _refresh_style() -> void:
|
||||
var idle := StyleBoxEmpty.new()
|
||||
var normal := _make_rarity_style(_rarity_color, 2, 0.25)
|
||||
var hover := _make_rarity_style(_rarity_color.lightened(0.08), 4, 0.34)
|
||||
var selected_fill: Color = (
|
||||
_rarity_color.lightened(0.12)
|
||||
if _batch_selected
|
||||
else _rarity_color
|
||||
var normal := _make_quality_style(_quality_color, 2, 0.25)
|
||||
var hover := _make_quality_style(
|
||||
_quality_color.lightened(0.08), 4, 0.34
|
||||
)
|
||||
var selected := _make_rarity_style(
|
||||
var selected_fill: Color = (
|
||||
_quality_color.lightened(0.12)
|
||||
if _batch_selected
|
||||
else _quality_color
|
||||
)
|
||||
var selected := _make_quality_style(
|
||||
selected_fill,
|
||||
6 if _batch_selected else 4,
|
||||
0.46 if _batch_selected else 0.34,
|
||||
|
|
@ -169,7 +171,7 @@ func _refresh_style() -> void:
|
|||
add_theme_stylebox_override("pressed", selected)
|
||||
|
||||
|
||||
func _make_rarity_style(
|
||||
func _make_quality_style(
|
||||
fill: Color,
|
||||
shadow_size: int,
|
||||
shadow_alpha: float,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
|
|||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.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 FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||
|
|
@ -569,6 +570,7 @@ func _on_showcase_changed(
|
|||
fish_name: String,
|
||||
rarity_name: String,
|
||||
weight_lb: float,
|
||||
quality: int,
|
||||
visible: bool,
|
||||
) -> void:
|
||||
_showcase_active = visible
|
||||
|
|
@ -583,11 +585,13 @@ func _on_showcase_changed(
|
|||
_barrier_health.visible = false
|
||||
_clear_barrier_markers()
|
||||
_showcase_details.text = (
|
||||
"%.1f lb • %s"
|
||||
% [weight_lb, rarity_name]
|
||||
"%s • %.1f lb"
|
||||
% [rarity_name.to_lower(), weight_lb]
|
||||
)
|
||||
_showcase_details.visible = true
|
||||
_status_label.text = "You caught a %s!" % fish_name
|
||||
_status_label.text = "You caught %s!" % (
|
||||
FishQualityType.qualified_name_with_article(fish_name, quality)
|
||||
)
|
||||
_status_label.visible = true
|
||||
_refresh_fishing_panel_visibility()
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
const BubbleHotbarSlotType = preload(
|
||||
"res://ui/components/bubble_hotbar/bubble_hotbar_slot.gd"
|
||||
)
|
||||
|
|
@ -321,7 +322,10 @@ func _show_assignment_name(slot_index: int, identity: StringName) -> void:
|
|||
if not catch_id.is_empty() and _fish_inventory != null:
|
||||
var fish_catch: FishCatchType = _fish_inventory.get_catch_by_id(catch_id)
|
||||
if fish_catch != null:
|
||||
_selected_item_label.text = fish_catch.fish.display_name
|
||||
_selected_item_label.text = FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
)
|
||||
_selected_item_label.visible = true
|
||||
return
|
||||
var item = (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ extends Control
|
|||
|
||||
const CollectionLogType = preload("res://collection/collection_log.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 FishPoolType = preload("res://fish/fish_pool.gd")
|
||||
const SILHOUETTE_SHADER: Shader = preload(
|
||||
|
|
@ -559,6 +560,7 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
facts_column.add_child(facts)
|
||||
_detail_body.add_child(_build_quality_progress(fish.id))
|
||||
|
||||
var stats_columns := HBoxContainer.new()
|
||||
stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
|
@ -594,8 +596,14 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
right_stats,
|
||||
"value range",
|
||||
"%d–%d fish coin" % [
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_min,
|
||||
FishQualityType.Tier.BORING,
|
||||
),
|
||||
FishQualityType.apply_sale_value(
|
||||
fish.sell_value_max,
|
||||
FishQualityType.Tier.SHINY,
|
||||
),
|
||||
],
|
||||
)
|
||||
_add_detail_row(
|
||||
|
|
@ -605,6 +613,56 @@ func _build_known_details(fish: FishDataType) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _build_quality_progress(fish_id: StringName) -> VBoxContainer:
|
||||
var quality_section := VBoxContainer.new()
|
||||
quality_section.add_theme_constant_override("separation", 3)
|
||||
var discovered_count: int = 0
|
||||
for quality: int in FishQualityType.TIER_COUNT:
|
||||
if _collection_log.has_discovered_quality(fish_id, quality):
|
||||
discovered_count += 1
|
||||
var heading := _field_label(
|
||||
"quality collection • %d / %d" % [
|
||||
discovered_count,
|
||||
FishQualityType.TIER_COUNT,
|
||||
],
|
||||
14,
|
||||
)
|
||||
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
heading.add_theme_color_override("font_color", MUTED_INK)
|
||||
quality_section.add_child(heading)
|
||||
var tiers := HBoxContainer.new()
|
||||
tiers.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
tiers.add_theme_constant_override("separation", 10)
|
||||
quality_section.add_child(tiers)
|
||||
for quality: int in FishQualityType.TIER_COUNT:
|
||||
var discovered: bool = _collection_log.has_discovered_quality(
|
||||
fish_id,
|
||||
quality,
|
||||
)
|
||||
var tier := HBoxContainer.new()
|
||||
tier.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
tier.add_theme_constant_override("separation", 3)
|
||||
var quality_color: Color = UIPalette.get_quality_color(quality)
|
||||
var dot := _label("●" if discovered else "○", 13)
|
||||
dot.add_theme_color_override("font_color", quality_color)
|
||||
dot.modulate.a = 1.0 if discovered else 0.48
|
||||
tier.add_child(dot)
|
||||
var tier_label := _label(FishQualityType.display_name(quality), 13)
|
||||
tier_label.add_theme_color_override(
|
||||
"font_color",
|
||||
INK if discovered else MUTED_INK,
|
||||
)
|
||||
tier_label.modulate.a = 1.0 if discovered else 0.58
|
||||
tier.tooltip_text = (
|
||||
"%s quality collected"
|
||||
if discovered
|
||||
else "%s quality not yet collected"
|
||||
) % FishQualityType.display_name(quality)
|
||||
tier.add_child(tier_label)
|
||||
tiers.add_child(tier)
|
||||
return quality_section
|
||||
|
||||
|
||||
func _show_no_selection() -> void:
|
||||
_clear_details()
|
||||
var instruction := _label("Select an entry to read its catch record.", 21)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const SALUTATION_LABELS := {
|
|||
"salutations": "Salutations",
|
||||
"good_luck_have_fun": "Good Luck Have Fun",
|
||||
}
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
var _service: NetworkMailService
|
||||
var _reservations: PlayerAssetReservationService
|
||||
|
|
@ -507,7 +508,11 @@ func _refresh_attachment_choices(_index: int) -> void:
|
|||
):
|
||||
_attachment_choice.add_item(
|
||||
"%s — %.1f lb" % [
|
||||
fish_catch.fish.display_name, fish_catch.weight_lb,
|
||||
FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
),
|
||||
fish_catch.weight_lb,
|
||||
]
|
||||
)
|
||||
_attachment_choice.set_item_metadata(
|
||||
|
|
@ -719,11 +724,18 @@ func _attachment_text(attachment: Dictionary) -> String:
|
|||
)
|
||||
if fish_catch != null:
|
||||
return "%s — %.1f lb" % [
|
||||
fish_catch.fish.display_name, fish_catch.weight_lb,
|
||||
FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
),
|
||||
fish_catch.weight_lb,
|
||||
]
|
||||
var data: Dictionary = attachment.get("catch", {})
|
||||
return "%s — %.1f lb" % [
|
||||
FishQualityType.qualified_name(
|
||||
str(data.get("fish_id", "fish")).capitalize(),
|
||||
int(data.get("quality", FishQualityType.Tier.BORING)),
|
||||
),
|
||||
float(data.get("weight_lb", 0.0)),
|
||||
]
|
||||
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const INPUT_OWNER: StringName = &"player_menu"
|
|||
const CollectionLogType = preload("res://collection/collection_log.gd")
|
||||
const FishCatchType = preload("res://fish/fish_catch.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 FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||
|
|
@ -74,11 +75,6 @@ const NAVIGATION_PRESENTATION_SCALE: float = 0.60
|
|||
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
|
||||
const NAVIGATION_SELECTED_SCALE: float = 1.02
|
||||
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"
|
||||
|
||||
signal menu_visibility_changed(is_open: bool)
|
||||
|
|
@ -2609,11 +2605,14 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
|||
var identity_hash: int = absi(String(fish_catch.catch_id).hash())
|
||||
fish_node.configure(
|
||||
fish_catch.catch_id,
|
||||
FishQualityType.qualified_name(
|
||||
fish_catch.fish.display_name,
|
||||
fish_catch.quality,
|
||||
),
|
||||
fish_catch.fish.display_texture,
|
||||
float(identity_hash % 628) / 100.0,
|
||||
0.96 + float(identity_hash % 9) * 0.01,
|
||||
_get_cooler_rarity_color(fish_catch.fish.rarity),
|
||||
UIPalette.get_quality_color(fish_catch.quality),
|
||||
)
|
||||
fish_node.set_item_state(
|
||||
_fish_selection.is_selected(fish_catch.catch_id),
|
||||
|
|
@ -2631,20 +2630,6 @@ func _sync_cooler_fish_nodes(catches: Array[FishCatchType]) -> void:
|
|||
_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:
|
||||
if not is_node_ready():
|
||||
return
|
||||
|
|
@ -2926,7 +2911,10 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void:
|
|||
and _reservations.is_fish_reserved(fish_catch.catch_id)
|
||||
):
|
||||
_cooler_detail_name.text += " • reserved in mail"
|
||||
_cooler_weight_unit.text = "lb • %s" % fish_catch.fish.get_rarity_name()
|
||||
_cooler_weight_unit.text = "lb • %s • %s" % [
|
||||
FishQualityType.display_name(fish_catch.quality),
|
||||
fish_catch.fish.get_rarity_name(),
|
||||
]
|
||||
if buyer_offer >= 0 and active_buyer != null:
|
||||
_cooler_offer_label.text = _get_offer_label(active_buyer)
|
||||
_cooler_offer_value.text = "$%d" % buyer_offer
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ const RARITY_RARE := Color("5596f6")
|
|||
const RARITY_EPIC := Color("b176e8")
|
||||
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:
|
||||
match rarity:
|
||||
|
|
@ -31,3 +37,17 @@ static func get_rarity_color(rarity: int) -> Color:
|
|||
return RARITY_LEGENDARY
|
||||
_:
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue