Add collectible fish quality tiers

This commit is contained in:
Alexander Sellite 2026-08-02 17:44:11 -04:00
parent 7661b057bc
commit 35d00e8035
30 changed files with 834 additions and 73 deletions

View file

@ -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
)

View file

@ -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"])

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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,

View file

@ -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."

View file

@ -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

View file

@ -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,