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

@ -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" % [
fish_catch.fish.display_name,
FishQualityType.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
),
fish_catch.weight_lb,
]
if fish_catch != null

View file

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

View file

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

View file

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

View file

@ -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" % [
fish.sell_value_min,
fish.sell_value_max,
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)

View file

@ -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" % [
str(data.get("fish_id", "fish")).capitalize(),
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:

View file

@ -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,
fish_catch.fish.display_name,
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

View file

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