Polish inventory shop and utility interfaces

Split inventory items and tackle views, standardize bait supply presentation, add status-effect icons, and align shop, logbook, art-kit, and shared utility layouts.
This commit is contained in:
Alexander Sellite 2026-08-15 14:28:13 -04:00
parent 84695c473b
commit a60925bf7b
21 changed files with 1150 additions and 385 deletions

View file

@ -151,9 +151,6 @@ func _unhandled_input(event: InputEvent) -> void:
)
else:
_service.cancel_local_interaction(_request_id)
status_changed.emit(
"Sneak close, pull the net all the way back, and line up the marker."
)
_reset_charge_state()
get_viewport().set_input_as_handled()

View file

@ -11,6 +11,12 @@ const ENERGY_DRINK_ID: StringName = &"energy_drink"
const SNACK_ID: StringName = &"snack"
const FISH_FINDER_ID: StringName = &"fish_finder"
const BATTERIES_ID: StringName = &"batteries"
const EFFECT_IDS: Array[StringName] = [
COFFEE_ID,
ENERGY_DRINK_ID,
SNACK_ID,
FISH_FINDER_ID,
]
const FISH_FINDER_DEAD_MESSAGE: String = "hmm... batteries are dead..."
const COFFEE_DURATION: float = 90.0
const ENERGY_DRINK_DURATION: float = 90.0
@ -100,6 +106,10 @@ func get_remaining_snapshot() -> Dictionary[StringName, float]:
return _remaining.duplicate()
func get_registered_effect_ids() -> Array[StringName]:
return EFFECT_IDS.duplicate()
func get_effect_duration(item_id: StringName) -> float:
return _get_duration(item_id)

View file

@ -399,6 +399,33 @@ func _run() -> void:
assert(hide_button.button_group.allow_unpress)
assert(brush_option.item_count == 4)
assert(grid_option.item_count == 4)
var expected_brush_icons: Array[String] = [
"art_kit_marker_tip_fine.png",
"art_kit_marker_tip_thin.png",
"art_kit_marker_tip_mid.png",
"art_kit_marker_tip_thick.png",
]
var expected_grid_icons: Array[String] = [
"art_kit_grid_small_light.png",
"art_kit_grid_medium_light.png",
"art_kit_grid_large_light.png",
"art_kit_grid_xl_light.png",
]
for index: int in 4:
assert(brush_option.get_item_text(index).is_empty())
assert(grid_option.get_item_text(index).is_empty())
assert(brush_option.get_item_icon(index) != null)
assert(grid_option.get_item_icon(index) != null)
assert(
brush_option.get_item_icon(index).resource_path.ends_with(
expected_brush_icons[index]
)
)
assert(
grid_option.get_item_icon(index).resource_path.ends_with(
expected_grid_icons[index]
)
)
assert(not brush_option.get_popup().is_item_disabled(0))
assert(brush_option.get_popup().is_item_disabled(1))
assert(not grid_option.get_popup().is_item_disabled(0))

View file

@ -570,6 +570,53 @@ func _test_fishing_shop_sale_ui(
and shop_backdrop != null
and ui_viewport != null
)
var item_catalog := main.get("item_catalog") as ItemCatalog
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
assert(worms != null and worms.max_stack == 10)
if player.bag.get_quantity(worms.item_id) == 0:
assert(player.bag.add_item(worms.item_id, 4))
assert(player.equip_bait(worms))
await process_frame
var expected_bait_supply := UtilityPageStyle.supply_quantity_text(
player.bag.get_quantity(worms.item_id),
worms.max_stack,
)
var hud_bait_quantity := game_ui.get_node(
"%ActiveBaitQuantity"
) as Label
assert(hud_bait_quantity.text == expected_bait_supply)
var hud_bait_badge := hud_bait_quantity.get_parent() as Panel
assert(
is_equal_approx(
hud_bait_badge.rotation_degrees,
UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
)
)
player_menu.call("_refresh_tackle_box")
await process_frame
var tackle_bait_button: Button
for child: Node in player_menu.get_node("%BaitItemList").get_children():
var candidate := child as Button
if (
candidate != null
and candidate.get_meta(&"controller_tackle_item_id", StringName())
== worms.item_id
):
tackle_bait_button = candidate
break
assert(tackle_bait_button != null)
var tackle_quantity := tackle_bait_button.get_node(
"QuantityBadge/Quantity"
) as Label
assert(tackle_quantity.text == expected_bait_supply)
assert(tackle_bait_button.tooltip_text.contains(expected_bait_supply))
var tackle_badge := tackle_quantity.get_parent() as Panel
assert(
is_equal_approx(
tackle_badge.rotation_degrees,
UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
)
)
player.global_position = interaction.global_position
for _frame: int in 4:
await physics_frame
@ -596,6 +643,14 @@ func _test_fishing_shop_sale_ui(
assert(shop_panel_style.corner_radius_top_left == 24)
var upgrade_grid := shop.get_node("%UpgradeGrid") as GridContainer
assert(upgrade_grid != null and upgrade_grid.columns == 3)
assert(
upgrade_grid.get_theme_constant("h_separation")
== FishingShop.SHOP_SLOT_SEPARATION
)
assert(
upgrade_grid.get_theme_constant("v_separation")
== FishingShop.SHOP_SLOT_SEPARATION
)
assert((shop.get_node("%Upgrades") as Control).visible)
assert(not (shop.get_node("%Supplies") as Control).visible)
for upgrade_name: String in [
@ -638,6 +693,89 @@ func _test_fishing_shop_sale_ui(
assert(sell_mode != null and sell_mode.text == "Sell Fish")
var equipment_tab := shop_tabs[3] as Button
assert(equipment_tab != null and equipment_tab.text == "Equipment")
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
var bait_tab := shop_tabs[1] as Button
assert(bait_tab != null and bait_tab.text == "Bait and Lures")
await _activate_pointer_control(bait_tab, ui_viewport)
await process_frame
var worms_button := supplies_list.find_child(
"Supply_worms", true, false
) as Button
var snails_button := supplies_list.find_child(
"Supply_snails", true, false
) as Button
assert(worms_button != null and snails_button != null)
var bait_grid := worms_button.get_parent().get_parent() as GridContainer
assert(bait_grid != null)
assert(
bait_grid.get_theme_constant("h_separation")
== FishingShop.SHOP_SLOT_SEPARATION
)
assert(
bait_grid.get_theme_constant("v_separation")
== FishingShop.SHOP_SLOT_SEPARATION
)
var shop_bait_quantity := worms_button.get_node(
"SupplyQuantityBadge/Quantity"
) as Label
assert(shop_bait_quantity.text == expected_bait_supply)
var shop_bait_badge := shop_bait_quantity.get_parent() as Panel
assert(shop_bait_badge.position.y == FishingShop.BAIT_SUPPLY_BADGE_Y)
assert(
is_equal_approx(
shop_bait_badge.rotation_degrees,
UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
)
)
assert(
shop_bait_badge.position.y + shop_bait_badge.size.y
<= FishingShop.SUPPLY_PRICE_Y
)
var worms_unit_price: int = FishingShopStock.get_price(worms.item_id)
assert(
worms_button.tooltip_text.contains(
"%d fish coin%s per bait piece" % [
worms_unit_price,
"" if worms_unit_price == 1 else "s",
]
)
)
assert(not worms_button.tooltip_text.contains("owned"))
var worms_restock_quantity: int = FishingShopStock.get_purchase_quantity(
worms.item_id,
player.bag.get_quantity(worms.item_id),
worms.max_stack,
)
var worms_restock_cost: int = FishingShopStock.get_purchase_cost(
worms.item_id,
worms_restock_quantity,
player.bag.is_bait_unlocked(worms.item_id),
)
var worms_price := worms_button.get_parent().get_node(
"PriceBubble/CurrencyAmount/Price"
) as Label
assert(worms_price.text == str(maxi(worms_restock_cost, 0)))
var bait_receipt: String = shop.call(
"_purchase_success_feedback",
worms.item_id,
NetworkShopProtocol.ProductCategory.SUPPLY,
maxi(worms_restock_quantity, 1),
maxi(worms_restock_cost, worms_unit_price),
)
assert(bait_receipt.begins_with("Bait Restocked\n"))
assert(bait_receipt.contains(CurrencyPresentation.ICON_PATH))
assert(bait_receipt.contains(" spent on "))
assert(bait_receipt.ends_with(" worms"))
assert(snails_button.get_node_or_null("SupplyQuantityBadge") == null)
assert(worms_button.get_node_or_null("UnlockStateIcon") == null)
var snails_lock_icon := snails_button.get_node(
"UnlockStateIcon"
) as TextureRect
assert(not bool(snails_lock_icon.get_meta(&"unlocked")))
assert(snails_lock_icon.texture.resource_path.ends_with(
"/lock_light.png"
))
assert(is_equal_approx(snails_lock_icon.modulate.a, 0.75))
await _activate_pointer_control(equipment_tab, ui_viewport)
await process_frame
var equipment_sections: Array[String] = []
@ -645,17 +783,21 @@ func _test_fishing_shop_sale_ui(
if child is Label:
equipment_sections.append((child as Label).text)
assert(equipment_sections == ["rods", "equipment"])
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
var rod_carousel := supplies_list.get_node("RodCarousel") as HBoxContainer
assert(rod_carousel != null)
var rod_cards := rod_carousel.get_node("RodScroll/RodCards") as HBoxContainer
assert(rod_cards != null and rod_cards.get_child_count() == 1)
assert(
rod_cards.get_theme_constant("separation")
== FishingShop.SHOP_SLOT_SEPARATION
)
var basic_rod_button := rod_cards.find_child(
"Rod_basic_fishing_rod", true, false
) as Button
assert(basic_rod_button != null)
assert(basic_rod_button.custom_minimum_size == Vector2(144.0, 144.0))
assert(basic_rod_button.icon != null and basic_rod_button.disabled)
assert(basic_rod_button.get_node_or_null("UnlockStateIcon") == null)
assert(rod_cards.find_child("Rod_aurora_rod", true, false) == null)
await _activate_pointer_control(art_supplies_tab, ui_viewport)
await process_frame
@ -681,6 +823,42 @@ func _test_fishing_shop_sale_ui(
marker_material.get_shader_parameter("marker_color")
== SurfaceDrawingPalette.get_color(color_id)
)
var expected_art_upgrade_icons: Dictionary[StringName, String] = {
&"brush_2x": "art_kit_marker_tip_thin.png",
&"brush_3x": "art_kit_marker_tip_mid.png",
&"brush_4x": "art_kit_marker_tip_thick.png",
&"grid_32x": "art_kit_grid_medium_light.png",
&"grid_64x": "art_kit_grid_large_light.png",
&"grid_128x": "art_kit_grid_xl_light.png",
}
var found_art_upgrade_icons: int = 0
for upgrade_node: Node in shop.find_children("*", "Button", true, false):
var upgrade_button := upgrade_node as Button
if not upgrade_button.has_meta(&"art_product_id"):
continue
var product_id := StringName(
str(upgrade_button.get_meta(&"art_product_id"))
)
if not expected_art_upgrade_icons.has(product_id):
continue
found_art_upgrade_icons += 1
assert(upgrade_button.text.is_empty())
assert(upgrade_button.icon != null)
var product_unlocked: bool = (
player.art_unlocks.owns_product(product_id)
)
var state_icon := upgrade_button.get_node_or_null(
"UnlockStateIcon"
) as TextureRect
assert((state_icon == null) == product_unlocked)
if state_icon != null:
assert(state_icon.texture.resource_path.ends_with("/lock_light.png"))
assert(
upgrade_button.icon.resource_path.ends_with(
expected_art_upgrade_icons[product_id]
)
)
assert(found_art_upgrade_icons == expected_art_upgrade_icons.size())
var price_bubbles := shop.find_children(
"PriceBubble", "PanelContainer", true, false
)

View file

@ -16,6 +16,25 @@ func _initialize() -> void:
var shop := FishingShopType.new()
assert(shop.call("_unlock_status", false) == "locked")
assert(shop.call("_unlock_status", true) == "unlocked")
var locked_button := Button.new()
shop.call(
"_add_unlock_state_icon", locked_button, false, Vector2(72.0, 72.0)
)
var locked_icon := locked_button.get_node(
"UnlockStateIcon"
) as TextureRect
assert(locked_icon.texture.resource_path.ends_with("/lock_light.png"))
assert(locked_icon.size == Vector2(48.0, 48.0))
assert(locked_icon.position == Vector2(12.0, 6.0))
assert(is_equal_approx(locked_icon.modulate.a, 0.75))
assert(not bool(locked_icon.get_meta(&"unlocked")))
locked_button.free()
var unlocked_button := Button.new()
shop.call(
"_add_unlock_state_icon", unlocked_button, true, Vector2(72.0, 72.0)
)
assert(unlocked_button.get_node_or_null("UnlockStateIcon") == null)
unlocked_button.free()
shop.free()
var magnet: ItemDataType = ItemCatalogResource.get_available_item_by_id(
@ -30,6 +49,12 @@ func _initialize() -> void:
assert(FishingShopStockType.get_price(&"magnet") == 250)
assert(FishingShopStockType.get_stock_item_ids().has(&"magnet"))
assert(FishingShopStockType.is_permanent_unlock(&"magnet", magnet))
var crab_net: ItemDataType = ItemCatalogResource.get_available_item_by_id(
&"crab_net"
)
assert(crab_net != null)
assert(crab_net.icon != null)
assert(crab_net.icon.resource_path.ends_with("/equipment/temp_net.png"))
var wallet := PlayerWalletType.new()
wallet.current_balance = 250

View file

@ -58,6 +58,7 @@ func _run() -> void:
))
_validate_shared_inventory_geometry(player_menu)
_validate_inventory_layering(player_menu)
_validate_tackle_to_items_transition(player_menu)
_validate_cooler_notepad_typography(player_menu)
_validate_resolution_matrix()
await _capture_inventory_pages(player_menu)
@ -101,6 +102,15 @@ func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
for node_name: StringName in [
&"CoolerOuterWall",
&"BagOuterWall",
&"TackleMainPanel",
]:
var main_panel := player_menu.get_node("%%%s" % node_name) as Control
assert(main_panel != null)
assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
assert(main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size))
for node_name: StringName in [
&"DetailConstellation",
&"BagDetailConstellation",
@ -113,17 +123,24 @@ func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
var bag_filters := player_menu.get_node("%BagFilterTabs") as Control
var bag_panel := player_menu.get_node("%BagOuterWall") as Control
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
var tackle_panel := player_menu.get_node("%TackleMainPanel") as Control
assert(bag_filters != null)
var inventory_tabs := player_menu.get_node("%InventorySubTabs") as Control
var items_tab := player_menu.get_node("%ItemsSubTab") as Button
var bait_list := player_menu.get_node("%BaitItemList") as GridContainer
var lure_list := player_menu.get_node("%LureItemList") as GridContainer
assert(bag_panel != null)
assert(sale_confirmation != null)
assert(cooler_panel != null)
assert(tackle_panel != null)
assert(bag_filters.z_index > bag_panel.z_index)
assert(inventory_tabs != null)
assert(items_tab != null and items_tab.text == "Items")
assert(bait_list != null and bait_list.columns == 3)
assert(lure_list != null and lure_list.columns == 3)
assert(bait_list.get_theme_constant("h_separation") == 28)
assert(lure_list.get_theme_constant("h_separation") == 28)
assert(sale_confirmation.z_index > cooler_panel.z_index)
assert(sale_confirmation.z_index > tackle_panel.z_index)
# The contextual Hotbar uses z=90 while the Player Menu is open.
@ -141,6 +158,28 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
)
func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
var empty_state := player_menu.get_node("%BagEmptyState") as Label
player_menu.set("_bag_view", PlayerMenu.BagView.EQUIPMENT)
player_menu.call("_refresh_bag")
assert(empty_state.text == "No equipment in your Bag.")
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
)
player_menu.call("_show_bag_view", PlayerMenu.BagView.CONSUMABLES)
assert(
player_menu.get("_current_section") == PlayerMenu.Section.BAG
)
assert(
player_menu.get("_bag_view") == PlayerMenu.BagView.CONSUMABLES
)
assert(empty_state.text == "No items in your Bag.")
player_menu.call("_cancel_page_tween")
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.COOLER
)
func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
var sort_choice := player_menu.get_node("%CoolerSortOption") as Control
var sort_direction := player_menu.get_node(
@ -276,10 +315,10 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
player_menu.visible = true
var sections: Array[PlayerMenu.Section] = [
PlayerMenu.Section.COOLER,
PlayerMenu.Section.TACKLE_BOX,
PlayerMenu.Section.BAG,
PlayerMenu.Section.TACKLE_BOX,
]
var suffixes: Array[String] = ["cooler", "tackle", "equipment"]
var suffixes: Array[String] = ["cooler", "equipment", "tackle"]
for index: int in sections.size():
player_menu.call("_show_section_immediate", sections[index])
await process_frame

View file

@ -82,6 +82,34 @@ func _validate_page() -> void:
page.get("_category") == LogbookCatalog.Category.FRESH_WATER
)
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
var category_tabs: Array = page.get("_category_tabs") as Array
var category_tab_categories: Array = (
page.get("_category_tab_categories") as Array
)
assert(category_tab_categories == [
LogbookCatalog.Category.FRESH_WATER,
LogbookCatalog.Category.SALT_WATER,
LogbookCatalog.Category.SHELLFISH,
LogbookCatalog.Category.OTHER,
])
assert(category_tabs.size() == category_tab_categories.size())
for tab_node: Variant in category_tabs:
var category_tab := tab_node as Button
assert(category_tab.size == LogbookPage.LOGBOOK_TAB_SIZE)
var text_width: float = UtilityPageStyle.TuffyFont.get_string_size(
category_tab.text,
HORIZONTAL_ALIGNMENT_LEFT,
-1.0,
OrganizerTab.FONT_SIZE,
).x
assert(
text_width + LogbookPage.LOGBOOK_TAB_TEXT_SIDE_INSET * 2.0
<= category_tab.size.x
)
assert(
(category_tabs[0] as Button).get_parent().position.x
== LogbookPage.LOGBOOK_TAB_LEFT_INSET
)
var initial_detail_body := page.get("_detail_body") as VBoxContainer
assert(not initial_detail_body.get_parent() is ScrollContainer)
assert(

View file

@ -43,6 +43,9 @@ func _run() -> void:
var hotbar := game_ui.get_node(
"UIRoot/CanonicalStage/Hotbar"
) as HotbarUI
assert(game_ui.get_node_or_null(
"UIRoot/CanonicalStage/GameplayTransientHUD/EffectStatus"
) == null)
var screen_fade := game_ui.get_node("UIRoot/ScreenFade") as Control
var title_screen := game_ui.get_node("UIRoot/TitleScreen") as TitleScreen
var title_content_stage := title_screen.get_node(
@ -154,9 +157,13 @@ func _run() -> void:
assert(presenter.size.is_equal_approx(Vector2(expected_viewport_size)))
var chat_panel := chat_ui.get_node("ChatPanel") as Control
var clock_panel := chat_ui.get_node("WorldClockPanel") as Control
var status_effect_column := chat_ui.get_node(
"StatusEffectColumn"
) as Control
var weather_icon := chat_ui.get_node("WorldWeatherIcon") as Control
assert(chat_panel != null)
assert(clock_panel != null)
assert(status_effect_column != null)
assert(weather_icon != null)
assert(chat_panel.visible)
assert(is_equal_approx(chat_panel.position.x, 0.0))
@ -168,6 +175,17 @@ func _run() -> void:
ChatUI.CLOCK_EDGE_MARGIN,
ChatUI.CLOCK_EDGE_MARGIN,
)))
assert(is_equal_approx(
status_effect_column.position.x,
clock_panel.position.x
+ (ChatUI.CLOCK_SIZE.x - ChatUI.STATUS_EFFECT_ICON_SIZE.x) * 0.5,
))
assert(is_equal_approx(
status_effect_column.position.y,
clock_panel.position.y
+ ChatUI.CLOCK_SIZE.y
+ ChatUI.STATUS_EFFECT_TOP_GAP,
))
assert(is_equal_approx(
weather_icon.position.y, ChatUI.CLOCK_EDGE_MARGIN
))

View file

@ -36,10 +36,12 @@ const CLOCK_FONT_SIZE: int = 27
const CLOCK_EDGE_MARGIN: float = 10.0
const WEATHER_ICON_SIZE := Vector2(51.0, 51.0)
const WEATHER_ICON_GAP: float = 6.0
const FISH_FINDER_EFFECT_ICON_SIZE := Vector2(18.0, 18.0)
const FISH_FINDER_EFFECT_ICON: Texture2D = preload(
"res://items/icons/consumables/64_consumable_fishfinder.png"
)
const STATUS_EFFECT_ICON_SIZE := Vector2(40.0, 40.0)
const STATUS_EFFECT_TOP_GAP: float = 6.0
const STATUS_EFFECT_ICON_GAP: int = 4
const STATUS_EFFECT_WARNING_RATIO: float = 0.10
const STATUS_EFFECT_PULSE_PERIOD: float = 1.0
const STATUS_EFFECT_PULSE_MIN_ALPHA: float = 0.25
const CHAT_SHOW_ICON: Texture2D = preload(
"res://ui/icons/pictograms/arrow_light_right_more.png"
)
@ -57,6 +59,8 @@ const WorldWeatherServiceType = preload(
"res://world/world_weather_service.gd"
)
const WeatherIconType = preload("res://ui/weather_icon.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const TypewriterRevealType = preload("res://ui/typewriter_reveal.gd")
const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
@ -92,7 +96,8 @@ var _animalese_voice: AnimaleseVoiceType
var _clock_panel: PanelContainer
var _clock_label: Label
var _weather_icon: WeatherIconType
var _fish_finder_effect_icon: TextureRect
var _status_effect_column: VBoxContainer
var _status_effect_icons: Dictionary[StringName, TextureRect] = {}
var _speech: Dictionary[int, Dictionary] = {}
var _draft_save_timer: Timer
var _opacity_tween: Tween
@ -117,6 +122,7 @@ var _mobile_mode: bool = false
var _world_time: WorldTimeServiceType
var _world_weather: WorldWeatherServiceType
var _item_effects: PlayerItemEffects
var _item_catalog: ItemCatalogType
func _ready() -> void:
@ -137,6 +143,7 @@ func setup(
world_time: WorldTimeServiceType,
world_weather: WorldWeatherServiceType,
item_effects: PlayerItemEffects,
item_catalog: ItemCatalogType,
) -> void:
_service = service
_session = session
@ -147,6 +154,8 @@ func setup(
_world_time = world_time
_world_weather = world_weather
_item_effects = item_effects
_item_catalog = item_catalog
_rebuild_status_effect_icons()
if (
_fishing_spot != null
and not _fishing_spot.local_speech_requested.is_connected(
@ -195,6 +204,77 @@ func setup(
_refresh_history()
func _rebuild_status_effect_icons() -> void:
for child: Node in _status_effect_column.get_children():
_status_effect_column.remove_child(child)
child.queue_free()
_status_effect_icons.clear()
if _item_effects == null or _item_catalog == null:
_status_effect_column.hide()
return
for item_id: StringName in _item_effects.get_registered_effect_ids():
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
if item == null or item.icon == null:
continue
var icon := TextureRect.new()
icon.name = "StatusEffect_%s" % String(item_id)
icon.texture = item.icon
icon.custom_minimum_size = STATUS_EFFECT_ICON_SIZE
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.hide()
_status_effect_column.add_child(icon)
_status_effect_icons[item_id] = icon
var icon_count: int = _status_effect_icons.size()
_status_effect_column.size = Vector2(
STATUS_EFFECT_ICON_SIZE.x,
STATUS_EFFECT_ICON_SIZE.y * icon_count
+ STATUS_EFFECT_ICON_GAP * maxi(icon_count - 1, 0),
)
func _update_status_effect_icons() -> void:
if _status_effect_column == null:
return
var has_active_effect: bool = false
for item_id: StringName in _status_effect_icons:
var icon: TextureRect = _status_effect_icons[item_id]
var remaining: float = (
_item_effects.get_remaining(item_id)
if _item_effects != null
else 0.0
)
icon.visible = remaining > 0.0
icon.modulate = Color.WHITE
if remaining <= 0.0:
continue
has_active_effect = true
var warning_duration: float = (
_item_effects.get_effect_duration(item_id)
* STATUS_EFFECT_WARNING_RATIO
)
if warning_duration <= 0.0 or remaining > warning_duration:
continue
var warning_elapsed: float = maxf(
warning_duration - remaining, 0.0
)
var pulse_phase: float = (
fmod(warning_elapsed, STATUS_EFFECT_PULSE_PERIOD)
/ STATUS_EFFECT_PULSE_PERIOD
)
var pulse_weight: float = 0.5 + 0.5 * cos(pulse_phase * TAU)
icon.modulate.a = lerpf(
STATUS_EFFECT_PULSE_MIN_ALPHA, 1.0, pulse_weight
)
_status_effect_column.visible = (
has_active_effect
and _available
and (not _hud_hidden or _opened)
)
func open_chat() -> void:
if (
_opened or not _available or _service == null
@ -359,6 +439,7 @@ func _unhandled_input(event: InputEvent) -> void:
func _process(_delta: float) -> void:
_refresh_visibility()
_update_status_effect_icons()
_update_panel_opacity()
_update_speech()
@ -391,15 +472,13 @@ func _build_ui() -> void:
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_clock_panel.add_child(_clock_label)
_fish_finder_effect_icon = TextureRect.new()
_fish_finder_effect_icon.name = "FishFinderEffectIcon"
_fish_finder_effect_icon.texture = FISH_FINDER_EFFECT_ICON
_fish_finder_effect_icon.size = FISH_FINDER_EFFECT_ICON_SIZE
_fish_finder_effect_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_fish_finder_effect_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_fish_finder_effect_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_fish_finder_effect_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_fish_finder_effect_icon)
_status_effect_column = VBoxContainer.new()
_status_effect_column.name = "StatusEffectColumn"
_status_effect_column.mouse_filter = Control.MOUSE_FILTER_IGNORE
_status_effect_column.add_theme_constant_override(
"separation", STATUS_EFFECT_ICON_GAP
)
add_child(_status_effect_column)
_weather_icon = WeatherIconType.new()
_weather_icon.name = "WorldWeatherIcon"
_weather_icon.size = WEATHER_ICON_SIZE
@ -968,7 +1047,7 @@ func _refresh_visibility() -> void:
_panel.hide()
_clock_panel.hide()
_weather_icon.hide()
_fish_finder_effect_icon.hide()
_status_effect_column.hide()
_collapse_button.hide()
_height_button.hide()
_unread_indicator.hide()
@ -978,7 +1057,7 @@ func _refresh_visibility() -> void:
_panel.hide()
_clock_panel.hide()
_weather_icon.hide()
_fish_finder_effect_icon.hide()
_status_effect_column.hide()
_collapse_button.hide()
_height_button.hide()
_unread_indicator.hide()
@ -986,10 +1065,6 @@ func _refresh_visibility() -> void:
return
_clock_panel.show()
_weather_icon.show()
_fish_finder_effect_icon.visible = (
_item_effects != null
and _item_effects.is_active(PlayerItemEffects.FISH_FINDER_ID)
)
_collapse_button.show()
var collapsed := _presentation_state == PresentationState.COLLAPSED
_panel.show()
@ -1365,10 +1440,10 @@ func _layout_presentation(animate: bool) -> void:
clock_x,
CLOCK_EDGE_MARGIN,
)
var fish_finder_effect_position := Vector2(
var status_effect_column_position := Vector2(
clock_position.x
+ (CLOCK_SIZE.x - FISH_FINDER_EFFECT_ICON_SIZE.x) * 0.5,
clock_position.y + CLOCK_SIZE.y + 2.0,
+ (CLOCK_SIZE.x - STATUS_EFFECT_ICON_SIZE.x) * 0.5,
clock_position.y + CLOCK_SIZE.y + STATUS_EFFECT_TOP_GAP,
)
var weather_icon_x: float = (
clock_position.x - WEATHER_ICON_GAP - WEATHER_ICON_SIZE.x
@ -1393,8 +1468,7 @@ func _layout_presentation(animate: bool) -> void:
_panel.size = target_size
_clock_panel.position = clock_position
_clock_panel.size = CLOCK_SIZE
_fish_finder_effect_icon.position = fish_finder_effect_position
_fish_finder_effect_icon.size = FISH_FINDER_EFFECT_ICON_SIZE
_status_effect_column.position = status_effect_column_position
_weather_icon.position = weather_icon_position
_weather_icon.size = WEATHER_ICON_SIZE
_collapse_button.position = collapse_position
@ -1416,9 +1490,9 @@ func _layout_presentation(animate: bool) -> void:
UIMotion.CHAT_RESIZE_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
_height_tween.tween_property(
_fish_finder_effect_icon,
_status_effect_column,
"position",
fish_finder_effect_position,
status_effect_column_position,
UIMotion.CHAT_RESIZE_DURATION,
).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
_height_tween.tween_property(

View file

@ -37,9 +37,32 @@ const ART_KIT_MARKER_ICON: Texture2D = preload(
const ART_KIT_MARKER_SHADER: Shader = preload(
"res://ui/art_kit_marker_icon.gdshader"
)
const ART_UPGRADE_ICONS: Dictionary[StringName, Texture2D] = {
&"brush_2x": preload(
"res://items/icons/art/art_kit_marker_tip_thin.png"
),
&"brush_3x": preload(
"res://items/icons/art/art_kit_marker_tip_mid.png"
),
&"brush_4x": preload(
"res://items/icons/art/art_kit_marker_tip_thick.png"
),
&"grid_32x": preload(
"res://items/icons/art/art_kit_grid_medium_light.png"
),
&"grid_64x": preload(
"res://items/icons/art/art_kit_grid_large_light.png"
),
&"grid_128x": preload(
"res://items/icons/art/art_kit_grid_xl_light.png"
),
}
const CURRENCY_ICON: Texture2D = preload(
"res://items/icons/shop/32_currency.png"
)
const LOCKED_ITEM_ICON: Texture2D = preload(
"res://ui/icons/pictograms/lock_light.png"
)
signal menu_visibility_changed(is_open: bool)
signal menu_exit_started
@ -75,7 +98,7 @@ const SHOP_SECTION_LABELS: Array[String] = [
const SUPPLY_ICON_GRID_COLUMNS: int = 9
const SUPPLY_ICON_TILE_SIZE := Vector2(72.0, 72.0)
const SUPPLY_ICON_HOST_SIZE := Vector2(72.0, 84.0)
const SUPPLY_ICON_GRID_SEPARATION: int = 8
const SHOP_SLOT_SEPARATION: int = 28
const SUPPLY_PRICE_COLOR := Color("c3dfe6")
const SUPPLY_PRICE_FONT_SIZE: int = 15
const SUPPLY_PRICE_Y: float = 60.0
@ -83,10 +106,15 @@ const SUPPLY_PRICE_HEIGHT: float = 24.0
const SUPPLY_PRICE_HORIZONTAL_PADDING: float = 12.0
const SUPPLY_PRICE_ICON_SIZE: float = 18.0
const SUPPLY_PRICE_ICON_GAP: float = 3.0
const BAIT_SUPPLY_BADGE_Y: float = (
UtilityPageStyleType.SUPPLY_BADGE_EDGE_MARGIN
)
const LOCKED_ITEM_ICON_SIZE := Vector2(48.0, 48.0)
const LOCKED_ITEM_ICON_ALPHA: float = 0.75
const ROD_CARD_TILE_SIZE := Vector2(144.0, 144.0)
const ROD_CARD_HOST_SIZE := Vector2(152.0, 198.0)
const ROD_CAROUSEL_HEIGHT: float = 206.0
const ROD_CAROUSEL_SEPARATION: int = 12
const ROD_CAROUSEL_SEPARATION: int = SHOP_SLOT_SEPARATION
const ROD_CAROUSEL_STEP: float = 328.0
const ROD_PRICE_Y: float = 128.0
const ROD_PRICE_HEIGHT: float = 30.0
@ -680,6 +708,8 @@ func _refresh_supplies() -> void:
current_stock_grid = _add_stock_icon_grid()
current_stock_group = stock_group
var button := Button.new()
button.name = "Supply_%s" % str(item_id)
button.set_meta(&"item_id", item_id)
button.custom_minimum_size = Vector2(195, 54)
button.icon = item.icon if item.icon != null else FALLBACK_SUPPLY_ICON
button.expand_icon = true
@ -709,6 +739,11 @@ func _refresh_supplies() -> void:
)
var item_tooltip_text: String = item.description
if bait_topoff:
var unit_price: int = FishingShopStockType.get_price(item_id)
var unit_price_description: String = (
"%d fish coin%s per bait piece"
% [unit_price, "" if unit_price == 1 else "s"]
)
if use_icon_tile:
button.custom_minimum_size = SUPPLY_ICON_TILE_SIZE
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
@ -725,17 +760,15 @@ func _refresh_supplies() -> void:
]
)
item_tooltip_text = (
"%s\nunlock and fill to %d/%d\n%s" % [
"%s\nunlock and fill\n%s\n%s" % [
item.display_name,
item.max_stack,
item.max_stack,
unit_price_description,
item.description,
]
if not bait_unlocked
else "%s\n%d/%d owned\n%s" % [
else "%s\n%s\n%s" % [
item.display_name,
owned,
item.max_stack,
unit_price_description,
item.description,
]
)
@ -773,14 +806,32 @@ func _refresh_supplies() -> void:
else item_tooltip_text
)
UtilityPageStyleType.apply_ocean_button(button)
if permanent_unlock or bait_topoff:
_add_unlock_state_icon(
button,
bait_unlocked if bait_topoff else owned > 0,
(
SUPPLY_ICON_TILE_SIZE
if use_icon_tile
else button.custom_minimum_size
),
)
if bait_topoff and bait_unlocked:
UtilityPageStyleType.add_supply_quantity_badge(
button,
owned,
item.max_stack,
&"SupplyQuantityBadge",
BAIT_SUPPLY_BADGE_Y,
)
button.pressed.connect(_purchase_supply.bind(item_id))
if current_stock_grid != null:
_add_supply_icon_tile(
current_stock_grid,
button,
(
total_cost
if bait_topoff and not bait_unlocked
maxi(total_cost, 0)
if bait_topoff
else FishingShopStockType.get_price(item_id)
),
)
@ -836,10 +887,10 @@ func _add_stock_icon_grid() -> GridContainer:
grid.columns = SUPPLY_ICON_GRID_COLUMNS
grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
grid.add_theme_constant_override(
"h_separation", SUPPLY_ICON_GRID_SEPARATION
"h_separation", SHOP_SLOT_SEPARATION
)
grid.add_theme_constant_override(
"v_separation", SUPPLY_ICON_GRID_SEPARATION
"v_separation", SHOP_SLOT_SEPARATION
)
_supplies_list.add_child(grid)
return grid
@ -919,6 +970,9 @@ func _add_rod_card(parent: HBoxContainer, rod: FishingRodDataType) -> void:
button.expand_icon = true
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_add_unlock_state_icon(
button, owned, ROD_CARD_TILE_SIZE, ROD_PRICE_Y
)
button.disabled = (
owned
or level_locked
@ -999,6 +1053,7 @@ func _add_art_kit_button(parent: GridContainer) -> void:
],
)
_configure_supply_icon_tile(button, item.icon)
_add_unlock_state_icon(button, owned, SUPPLY_ICON_TILE_SIZE)
_add_supply_icon_tile(
parent,
button,
@ -1035,16 +1090,23 @@ func _add_art_upgrade_button(
ArtShopStockType.get_description(product_id),
],
)
button.accessibility_name = ArtShopStockType.get_display_name(product_id)
button.set_meta(&"art_product_id", product_id)
var marker_color_id: StringName = (
PlayerArtUnlocksType.color_id_for_product(product_id)
)
if marker_color_id.is_empty():
_configure_supply_icon_tile(button, FALLBACK_SUPPLY_ICON)
var upgrade_icon: Texture2D = ART_UPGRADE_ICONS.get(
product_id,
FALLBACK_SUPPLY_ICON,
)
_configure_supply_icon_tile(button, upgrade_icon)
else:
_configure_marker_icon_tile(
button,
SurfaceDrawingPalette.get_color(marker_color_id),
)
_add_unlock_state_icon(button, unlocked, SUPPLY_ICON_TILE_SIZE)
_add_supply_icon_tile(
parent,
button,
@ -1110,6 +1172,32 @@ func _configure_marker_icon_tile(
button.add_child(icon)
func _add_unlock_state_icon(
button: Button,
unlocked: bool,
tile_size: Vector2,
price_bubble_y: float = SUPPLY_PRICE_Y,
) -> void:
if unlocked:
return
var icon := TextureRect.new()
icon.name = "UnlockStateIcon"
icon.texture = LOCKED_ITEM_ICON
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.z_index = 3
icon.set_meta(&"unlocked", false)
icon.size = LOCKED_ITEM_ICON_SIZE
icon.position = Vector2(
(tile_size.x - LOCKED_ITEM_ICON_SIZE.x) * 0.5,
(price_bubble_y - LOCKED_ITEM_ICON_SIZE.y) * 0.5,
)
icon.modulate.a = LOCKED_ITEM_ICON_ALPHA
button.add_child(icon)
func _add_supply_icon_tile(
parent: Container,
button: Button,
@ -1353,18 +1441,20 @@ func _on_network_purchase_finished(
_request_id: String,
accepted: bool,
message: String,
_product_id: StringName,
_category: int,
_quantity: int,
product_id: StringName,
category: int,
quantity: int,
total_cost: int,
) -> void:
_transaction_in_progress = false
if not visible:
return
_set_feedback(
(
"Purchase complete • %s spent."
% CurrencyPresentation.bbcode_amount(total_cost, 18)
_purchase_success_feedback(
product_id,
category,
quantity,
total_cost,
)
if accepted
else message
@ -1372,6 +1462,31 @@ func _on_network_purchase_finished(
_refresh_all()
func _purchase_success_feedback(
product_id: StringName,
category: int,
quantity: int,
total_cost: int,
) -> String:
if (
category == NetworkShopProtocol.ProductCategory.SUPPLY
and FishingShopStockType.is_bait_topoff(product_id)
):
var item: ItemDataType = _item_catalog.get_item_by_id(product_id)
var item_name: String = (
item.display_name if item != null else str(product_id).capitalize()
)
return "Bait Restocked\n%s spent on %d %s" % [
CurrencyPresentation.bbcode_amount(total_cost, 18),
quantity,
item_name,
]
return (
"Purchase complete • %s spent."
% CurrencyPresentation.bbcode_amount(total_cost, 18)
)
func _set_feedback(message: String) -> void:
_feedback.text = "[center]%s[/center]" % message

View file

@ -198,8 +198,8 @@ horizontal_scroll_mode = 0
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 20
theme_override_constants/v_separation = 20
theme_override_constants/h_separation = 28
theme_override_constants/v_separation = 28
columns = 3
[node name="ReelTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]

View file

@ -141,7 +141,6 @@ var _shop_animalese_voice: AnimaleseVoiceType
var _shop_npc_player_in_range: bool = false
var _shop_npc_spoken_for_current_visit: bool = false
var _shop_npc_next_speech_msec: int = 0
@onready var _effect_status: Label = %EffectStatus
@onready var _chat_ui: ChatUIType = %ChatUI
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
@onready var _quick_radial_menu: QuickRadialMenuType = %QuickRadialMenu
@ -174,7 +173,6 @@ var _system_menu_open: bool = false
var _shop_open: bool = false
var _chat_input_open: bool = false
var _player_menu_hotbar_visible: bool = false
var _item_effects: PlayerItemEffectsType
var _main_shop_buyer: FishBuyerProfileType
var _shop_interaction: ShopInteractionType
var _surface_drawing: NetworkSurfaceDrawingService
@ -304,7 +302,6 @@ func setup(
_item_catalog = item_catalog
_settings_manager = settings_manager
_fishing_spot = fishing_spot
_item_effects = item_effects
_experience = experience
if (
_player != null
@ -339,7 +336,8 @@ func setup(
_experience.experience_awarded.connect(_on_experience_awarded)
_chat_ui.setup(
network_chat_service, network_session, spawn_service, player,
fishing_spot, settings_manager, world_time, world_weather, item_effects
fishing_spot, settings_manager, world_time, world_weather, item_effects,
item_catalog,
)
_title_settings_panel.setup_network_profile(
network_profile, network_session
@ -1195,31 +1193,6 @@ func _process(delta: float) -> void:
_update_virtual_mouse(delta)
_update_controller_menu_scroll(delta)
_update_experience_bubble_position()
if _item_effects == null or not _gameplay_ui_enabled:
_effect_status.hide()
return
var parts: Array[String] = []
for item_id: StringName in [
PlayerItemEffectsType.COFFEE_ID,
PlayerItemEffectsType.ENERGY_DRINK_ID,
PlayerItemEffectsType.SNACK_ID,
PlayerItemEffectsType.FISH_FINDER_ID,
]:
var remaining: float = _item_effects.get_remaining(item_id)
if remaining <= 0.0:
continue
var label: String = {
PlayerItemEffectsType.COFFEE_ID: "coffee",
PlayerItemEffectsType.ENERGY_DRINK_ID: "energy",
PlayerItemEffectsType.SNACK_ID: "snack",
PlayerItemEffectsType.FISH_FINDER_ID: "finder",
}.get(item_id, "")
parts.append(
"%s %d:%02d"
% [label, floori(remaining / 60.0), int(remaining) % 60]
)
_effect_status.text = " ".join(parts)
_effect_status.visible = not parts.is_empty()
func _update_controller_menu_scroll(delta: float) -> void:
@ -1363,14 +1336,6 @@ func _apply_active_bait_indicator_style() -> void:
&"icon_pressed_color",
]:
_active_bait_button.add_theme_color_override(state, Color.WHITE)
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
_active_bait_quantity_badge.add_theme_stylebox_override(
"panel",
badge_style,
)
func _refresh_active_bait_indicator() -> void:
@ -1399,14 +1364,16 @@ func _refresh_active_bait_indicator() -> void:
if _bag != null
else 0
)
_active_bait_quantity.text = str(quantity)
_active_bait_quantity.add_theme_font_size_override(
"font_size",
13 if quantity >= 100 else 15,
UtilityPageStyle.configure_supply_quantity_badge(
_active_bait_quantity_badge,
_active_bait_quantity,
quantity,
item.max_stack,
)
_active_bait_button.tooltip_text = "%s ×%d" % [
_active_bait_button.tooltip_text = "%s %d/%d" % [
item.display_name,
quantity,
item.max_stack,
]

View file

@ -123,7 +123,7 @@ layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -27.0
offset_left = -49.0
offset_top = 3.0
offset_right = -3.0
offset_bottom = 27.0
@ -136,14 +136,12 @@ layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_right = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
theme_override_colors/font_color = Color(0.905882, 0.960784, 0.956863, 1)
theme_override_font_sizes/font_size = 15
text = "0"
theme_override_font_sizes/font_size = 13
text = "0/10"
horizontal_alignment = 1
vertical_alignment = 1
@ -473,20 +471,6 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10)
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
unique_name_in_owner = true
[node name="EffectStatus" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
visible = false
z_index = 54
offset_left = 18.0
offset_top = 18.0
offset_right = 500.0
offset_bottom = 44.0
mouse_filter = 2
theme = ExtResource("3_theme")
theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9)
theme_override_constants/shadow_offset_x = 2
theme_override_constants/shadow_offset_y = 2
[node name="ChatUI" type="Control" parent="UIRoot"]
unique_name_in_owner = true
layout_mode = 1

View file

@ -38,7 +38,10 @@ const SCROLL_DOWN_TEXTURE: Texture2D = preload(
const LOGBOOK_ARTWORK_SOURCE_SIZE := Vector2(512.0, 247.0)
const LEFT_PAGE_CONTENT_RECT := Rect2(56.0, 26.0, 190.0, 198.0)
const RIGHT_PAGE_CONTENT_RECT := Rect2(267.0, 26.0, 210.0, 198.0)
const LOGBOOK_TAB_LEFT_INSET: float = 122.0
const LOGBOOK_TAB_LEFT_INSET: float = 92.0
const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0)
const LOGBOOK_TAB_SEPARATION: float = 6.0
const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0
const PAGE_CONTENT_SCALE: float = 0.97
const CATALOG_PORTRAIT_SIZE := Vector2(78.0, 36.0)
const CATALOG_ENTRY_SIZE := Vector2(92.0, 92.0)
@ -67,6 +70,7 @@ var _snapping_catalog_scroll: bool = false
var _catalog_scroll_snap_timer: Timer
var _category_tabs: Array[Button] = []
var _category_tab_categories: Array[LogbookCatalog.Category] = []
var _catalog_scroll: ScrollContainer
var _catalog_grid: GridContainer
var _catalog_scroll_up_indicator: TextureRect
@ -163,7 +167,9 @@ func focus_initial() -> void:
var first := _entry_buttons.values().front() as Button
first.grab_focus()
else:
_category_tabs[int(_category)].grab_focus()
var category_tab_index: int = _category_tab_categories.find(_category)
if category_tab_index >= 0:
_category_tabs[category_tab_index].grab_focus()
func _build_interface() -> void:
@ -183,19 +189,26 @@ func _build_interface() -> void:
var tabs := HBoxContainer.new()
# Align the first flange with the authored left paper edge.
tabs.position = Vector2(LOGBOOK_TAB_LEFT_INSET, 37.0)
tabs.size = Vector2(384.0, 38.0)
tabs.z_index = 10
tabs.mouse_filter = Control.MOUSE_FILTER_PASS
tabs.add_theme_constant_override("separation", 6)
stack.add_child(tabs)
for category: LogbookCatalog.Category in [
var category_order: Array[LogbookCatalog.Category] = [
LogbookCatalog.Category.FRESH_WATER,
LogbookCatalog.Category.SALT_WATER,
LogbookCatalog.Category.OTHER,
LogbookCatalog.Category.SHELLFISH,
]:
LogbookCatalog.Category.OTHER,
]
tabs.size = Vector2(
LOGBOOK_TAB_SIZE.x * float(category_order.size())
+ LOGBOOK_TAB_SEPARATION * float(category_order.size() - 1),
LOGBOOK_TAB_SIZE.y,
)
tabs.z_index = 10
tabs.mouse_filter = Control.MOUSE_FILTER_PASS
tabs.add_theme_constant_override(
"separation", roundi(LOGBOOK_TAB_SEPARATION)
)
stack.add_child(tabs)
for category: LogbookCatalog.Category in category_order:
var tab := OrganizerTabType.new()
tab.custom_minimum_size = Vector2(90, 38)
tab.custom_minimum_size = LOGBOOK_TAB_SIZE
tab.toggle_mode = true
tab.text = LogbookCatalog.category_label(category)
tab.palette_index = int(category)
@ -203,6 +216,7 @@ func _build_interface() -> void:
tab.pressed.connect(_select_category.bind(category))
tabs.add_child(tab)
_category_tabs.append(tab)
_category_tab_categories.append(category)
_configure_category_focus()
var book := Control.new()
@ -529,7 +543,7 @@ func _select_category(category: LogbookCatalog.Category) -> void:
_selected_entry_key = StringName()
for index: int in _category_tabs.size():
(_category_tabs[index] as OrganizerTabType).set_selected(
index == int(_category)
_category_tab_categories[index] == _category
)
_begin_category_transition()

View file

@ -86,7 +86,6 @@ func setup(
func activate() -> void:
_show_inbox()
UtilityPageStyle.animate_in(self)
call_deferred("_focus_first")
@ -217,7 +216,7 @@ func _build_compose() -> Control:
_attachment_kind = OptionButton.new()
_attachment_kind.position = Vector2(688, 48)
_attachment_kind.size = Vector2(280, 46)
for label: String in ["No gift", "Currency", "Fish", "Consumable"]:
for label: String in ["No gift", "Currency", "Fish", "Item"]:
_attachment_kind.add_item(label)
_attachment_kind.item_selected.connect(_refresh_attachment_choices)
page.add_child(_attachment_kind)
@ -770,7 +769,7 @@ func _attachment_text(attachment: Dictionary) -> String:
StringName(str(attachment.get("item_id", "")))
)
return "%s ×%d" % [
item.display_name if item != null else "Consumable",
item.display_name if item != null else "Item",
int(attachment.get("quantity", 0)),
]
return "Invalid gift."

View file

@ -115,11 +115,6 @@ enum BagView {
CONSUMABLES,
}
enum TackleView {
BAIT,
LURES,
}
enum SortMode {
CATCH_ORDER,
NAME,
@ -150,10 +145,9 @@ const INVENTORY_NOTEPAD_POSITION := Vector2(
INVENTORY_MAIN_POSITION.y,
)
const INVENTORY_NOTEPAD_SIZE := Vector2(278.0, 484.0)
const INVENTORY_HEADER_INSET := Vector2(24.0, 20.0)
const INVENTORY_HEADER_HEIGHT := 40.0
const INVENTORY_MAIN_CORNER_RADIUS: int = 58
const INVENTORY_INNER_CORNER_RADIUS: int = 45
const TACKLE_GRID_COLUMNS: int = 3
const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _navigation_cluster: BubbleClusterType = %NavigationCluster
@ -198,18 +192,18 @@ const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
@onready var _inventory_sub_tabs: HBoxContainer = %InventorySubTabs
@onready var _cooler_sub_tab: Button = %CoolerSubTab
@onready var _bag_sub_tab: Button = %BagSubTab
@onready var _items_sub_tab: Button = %ItemsSubTab
@onready var _tackle_sub_tab: Button = %TackleSubTab
@onready var _bag_filter_tabs: HBoxContainer = %BagFilterTabs
@onready var _equipment_filter: Button = %EquipmentFilter
@onready var _consumables_filter: Button = %ConsumablesFilter
@onready var _tackle_main_panel: PanelContainer = %TackleMainPanel
@onready var _tackle_detail_panel: PanelContainer = %TackleDetailPanel
@onready var _bait_filter: Button = %BaitFilter
@onready var _lures_filter: Button = %LuresFilter
@onready var _tackle_empty: Label = %TackleEmpty
@onready var _bait_heading: Label = %BaitHeading
@onready var _lure_heading: Label = %LureHeading
@onready var _bait_empty: Label = %BaitEmpty
@onready var _lure_empty: Label = %LureEmpty
@onready var _tackle_detail_text: Label = %TackleDetailText
@onready var _tackle_equip_button: NotepadInkActionType = %TackleEquipButton
@onready var _tackle_item_list: VBoxContainer = %TackleItemList
@onready var _bait_item_list: GridContainer = %BaitItemList
@onready var _lure_item_list: GridContainer = %LureItemList
@onready var _bag_outer_wall: PanelContainer = %BagOuterWall
@onready var _bag_inner_liner: PanelContainer = %BagInnerLiner
@onready var _bag_scroll: ScrollContainer = %BagScroll
@ -312,8 +306,8 @@ var _cooler_capacity: PlayerCoolerCapacityType
var _current_section: Section = Section.COOLER
var _last_inventory_section: Section = Section.COOLER
var _bag_view: BagView = BagView.EQUIPMENT
var _tackle_view: TackleView = TackleView.BAIT
var _selected_tackle_item_id: StringName
var _tackle_item_buttons: Dictionary[StringName, Button] = {}
var _controller_ownership: ControllerOwnership = ControllerOwnership.ITEM_LIST
var _controller_source_section: Section = Section.COOLER
var _controller_source_identity: StringName
@ -377,18 +371,11 @@ func _ready() -> void:
_cooler_original_index = _cooler_page.get_index()
_confirmation_original_parent = _sale_confirmation.get_parent()
_confirmation_original_index = _sale_confirmation.get_index()
_inventory_sub_tabs.move_child(_tackle_sub_tab, 1)
_inventory_sub_tabs.move_child(_bag_sub_tab, 2)
_inventory_tab.pressed.connect(_show_last_inventory_section)
_cooler_sub_tab.pressed.connect(_show_section.bind(Section.COOLER))
_bag_sub_tab.pressed.connect(_show_section.bind(Section.BAG))
_bag_sub_tab.pressed.connect(_show_bag_view.bind(BagView.EQUIPMENT))
_items_sub_tab.pressed.connect(_show_bag_view.bind(BagView.CONSUMABLES))
_tackle_sub_tab.pressed.connect(_show_section.bind(Section.TACKLE_BOX))
_equipment_filter.pressed.connect(_set_bag_view.bind(BagView.EQUIPMENT))
_consumables_filter.pressed.connect(
_set_bag_view.bind(BagView.CONSUMABLES)
)
_bait_filter.pressed.connect(_set_tackle_view.bind(TackleView.BAIT))
_lures_filter.pressed.connect(_set_tackle_view.bind(TackleView.LURES))
_tackle_equip_button.pressed.connect(_toggle_active_tackle)
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
@ -434,7 +421,6 @@ func _ready() -> void:
# Keep the exposed tab hitboxes above the full-page roots. The individual
# main panels retain a higher z-index and cover the tab bodies below y=166.
_inventory_sub_tabs.move_to_front()
_bag_filter_tabs.move_to_front()
_apply_cooler_control_styles()
_apply_cooler_wall_styles()
_apply_cooler_notepad_style()
@ -947,17 +933,7 @@ func _restore_controller_item_focus(
elif section == Section.BAG:
target = _bag_item_nodes.get(identity) as BagItemSpriteType
elif section == Section.TACKLE_BOX:
for child: Node in _tackle_item_list.get_children():
var button := child as BaseButton
if (
button != null
and button.has_meta(&"controller_tackle_item_id")
and StringName(str(button.get_meta(
&"controller_tackle_item_id"
))) == identity
):
target = button
break
target = _tackle_item_buttons.get(identity) as Button
if (
target != null
and is_instance_valid(target)
@ -1087,23 +1063,14 @@ func _handle_controller_secondary_switch(event: InputEvent) -> bool:
):
return true
if _is_inventory_section(_current_section):
var inventory_index: int = _get_inventory_tab_index()
if (
(direction > 0 and _current_section == Section.BAG)
or (direction < 0 and _current_section == Section.COOLER)
(direction > 0 and inventory_index == 3)
or (direction < 0 and inventory_index == 0)
):
_begin_controller_hotbar_management()
return true
var inventory_sections: Array[Section] = [
Section.COOLER,
Section.TACKLE_BOX,
Section.BAG,
]
var inventory_index: int = inventory_sections.find(_current_section)
_show_section(inventory_sections[wrapi(
inventory_index + direction,
0,
inventory_sections.size(),
)])
_show_inventory_tab(inventory_index + direction)
return true
_cycle_visible_secondary_tabs(direction)
return true
@ -1129,13 +1096,31 @@ func _begin_controller_hotbar_management() -> void:
func _cycle_from_controller_hotbar_management(direction: int) -> void:
_release_controller_ownership(false, false)
var target_section: Section = (
Section.COOLER if direction > 0 else Section.BAG
)
if target_section == _current_section:
var target_index: int = 0 if direction > 0 else 3
if target_index == _get_inventory_tab_index():
call_deferred("_focus_current_section")
else:
_show_section(target_section)
_show_inventory_tab(target_index)
func _get_inventory_tab_index() -> int:
if _current_section == Section.COOLER:
return 0
if _current_section == Section.BAG:
return 2 if _bag_view == BagView.CONSUMABLES else 1
return 3
func _show_inventory_tab(index: int) -> void:
match clampi(index, 0, 3):
0:
_show_section(Section.COOLER)
1:
_show_bag_view(BagView.EQUIPMENT)
2:
_show_bag_view(BagView.CONSUMABLES)
_:
_show_section(Section.TACKLE_BOX)
func _cycle_visible_secondary_tabs(direction: int) -> bool:
@ -1575,7 +1560,9 @@ func _show_section_immediate(section: Section) -> void:
if section != Section.BAG:
_close_bag_detail()
else:
_update_bag_detail()
# BagView can change while entering from another Inventory page. Rebuild
# the filtered contents here so the visible list always matches its tab.
_refresh_bag()
_refresh_tackle_box()
if section != Section.LOGBOOK:
_cancel_logbook_page_transition(true)
@ -1600,7 +1587,12 @@ func _show_section_immediate(section: Section) -> void:
_players_page.deactivate()
_inventory_tab.button_pressed = _is_inventory_section(section)
_cooler_sub_tab.set_selected(section == Section.COOLER)
_bag_sub_tab.set_selected(section == Section.BAG)
_bag_sub_tab.set_selected(
section == Section.BAG and _bag_view == BagView.EQUIPMENT
)
_items_sub_tab.set_selected(
section == Section.BAG and _bag_view == BagView.CONSUMABLES
)
_tackle_sub_tab.set_selected(section == Section.TACKLE_BOX)
_refresh_inventory_organizer_tabs()
_logbook_tab.button_pressed = section == Section.LOGBOOK
@ -1622,8 +1614,9 @@ func _is_inventory_section(section: Section) -> bool:
func _refresh_inventory_organizer_tabs() -> void:
for tab: OrganizerTabType in [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
_items_sub_tab,
_tackle_sub_tab,
]:
tab.refresh_state()
@ -1633,8 +1626,9 @@ func _animate_inventory_tab_entry() -> void:
return
var tabs: Array[OrganizerTabType] = [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
_items_sub_tab,
_tackle_sub_tab,
]
for index: int in tabs.size():
tabs[index].animate_entrance(float(index) * 0.025)
@ -1643,8 +1637,9 @@ func _animate_inventory_tab_entry() -> void:
func _settle_inventory_tabs_for_close() -> void:
for tab: OrganizerTabType in [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
_items_sub_tab,
_tackle_sub_tab,
]:
tab.settle_for_close()
@ -1779,8 +1774,9 @@ func _reserve_visible_secondary_navigation() -> void:
call_deferred("_focus_current_section")
var inventory_tabs: Array[Button] = [
_cooler_sub_tab,
_tackle_sub_tab,
_bag_sub_tab,
_items_sub_tab,
_tackle_sub_tab,
]
for index: int in inventory_tabs.size():
var tab: Button = inventory_tabs[index]
@ -1794,9 +1790,7 @@ func _reserve_visible_secondary_navigation() -> void:
_inventory_tab.focus_neighbor_bottom = _inventory_tab.get_path_to(
_inventory_tab_for_section(_last_inventory_section)
)
_tackle_sub_tab.focus_neighbor_bottom = _tackle_sub_tab.get_path_to(
_bait_filter
)
_configure_tackle_item_focus()
func _inventory_tab_for_section(section: Section) -> Button:
@ -1804,7 +1798,11 @@ func _inventory_tab_for_section(section: Section) -> Button:
Section.COOLER:
return _cooler_sub_tab
Section.BAG:
return _bag_sub_tab
return (
_items_sub_tab
if _bag_view == BagView.CONSUMABLES
else _bag_sub_tab
)
_:
return _tackle_sub_tab
@ -1847,7 +1845,7 @@ func _configure_active_page_focus() -> void:
Section.BAG:
_configure_bag_item_focus()
Section.TACKLE_BOX:
_bait_filter.grab_focus()
_configure_tackle_item_focus()
Section.LOGBOOK:
_catalog_logbook.focus_initial()
Section.NET:
@ -1859,13 +1857,6 @@ func _configure_active_page_focus() -> void:
func _apply_inventory_styles() -> void:
for button: Button in [
_equipment_filter,
_consumables_filter,
_bait_filter,
_lures_filter,
]:
UtilityPageStyle.apply_ocean_button(button)
for panel: PanelContainer in [_tackle_main_panel]:
var panel_style := UtilityPageStyle.panel_style()
panel_style.set_corner_radius_all(INVENTORY_MAIN_CORNER_RADIUS)
@ -1873,7 +1864,12 @@ func _apply_inventory_styles() -> void:
"panel",
panel_style,
)
for label: Label in [_tackle_empty]:
for label: Label in [
_bait_heading,
_lure_heading,
_bait_empty,
_lure_empty,
]:
label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
label.add_theme_color_override(
"font_color",
@ -1895,124 +1891,180 @@ func _apply_inventory_styles() -> void:
UtilityPageStyle.apply_ocean_button(_cancel_sale_button)
func _set_bag_view(view: BagView) -> void:
func _show_bag_view(view: BagView) -> void:
if (
_transitioning
or _page_transitioning
or _sale_confirmation.visible
or get_viewport().gui_is_dragging()
):
return
if _current_section != Section.BAG:
_bag_view = view
_selected_bag_item_id = StringName()
_show_section(Section.BAG)
return
if _bag_view == view:
return
_release_controller_ownership(false, true)
_bag_view = view
_selected_bag_item_id = StringName()
_refresh_bag()
func _set_tackle_view(view: TackleView) -> void:
_tackle_view = view
_selected_tackle_item_id = StringName()
_refresh_tackle_box()
_bag_sub_tab.set_selected(view == BagView.EQUIPMENT)
_items_sub_tab.set_selected(view == BagView.CONSUMABLES)
_refresh_inventory_organizer_tabs()
_reserve_visible_secondary_navigation()
_set_content_interactive(true)
call_deferred("_focus_current_section")
func _refresh_tackle_box() -> void:
if not is_node_ready():
return
_bait_filter.button_pressed = _tackle_view == TackleView.BAIT
_lures_filter.button_pressed = _tackle_view == TackleView.LURES
for child: Node in _tackle_item_list.get_children():
child.queue_free()
var matching_items: Array[OwnedItemType] = []
for item_list: GridContainer in [_bait_item_list, _lure_item_list]:
for child: Node in item_list.get_children():
child.queue_free()
_tackle_item_buttons.clear()
var bait_items: Array[OwnedItemType] = []
var lure_items: Array[OwnedItemType] = []
if _bag != null and _item_catalog != null:
var available_items: Array[OwnedItemType] = (
_bag.get_unlocked_bait_items()
if _tackle_view == TackleView.BAIT
else _bag.get_all_items()
)
for owned: OwnedItemType in available_items:
for owned: OwnedItemType in _bag.get_unlocked_bait_items():
var item: ItemDataType = _item_catalog.get_item_by_id(
owned.item_id
)
if item == null:
continue
if (
_tackle_view == TackleView.BAIT
and item.category == ItemDataType.Category.BAIT
):
matching_items.append(owned)
elif (
_tackle_view == TackleView.LURES
and item.category == ItemDataType.Category.LURE
):
matching_items.append(owned)
matching_items.sort_custom(_sort_bag_items)
for owned: OwnedItemType in matching_items:
if item != null and item.category == ItemDataType.Category.BAIT:
bait_items.append(owned)
for owned: OwnedItemType in _bag.get_all_items():
var item: ItemDataType = _item_catalog.get_item_by_id(
owned.item_id
)
if item != null and item.category == ItemDataType.Category.LURE:
lure_items.append(owned)
bait_items.sort_custom(_sort_bag_items)
lure_items.sort_custom(_sort_bag_items)
_populate_tackle_column(bait_items, _bait_item_list)
_populate_tackle_column(lure_items, _lure_item_list)
if (
not _selected_tackle_item_id.is_empty()
and not _tackle_item_buttons.has(_selected_tackle_item_id)
):
_selected_tackle_item_id = StringName()
_bait_empty.visible = bait_items.is_empty()
_lure_empty.visible = lure_items.is_empty()
_configure_tackle_item_focus()
_update_tackle_detail()
_apply_tackle_interactivity(_tackle_is_interactive())
func _populate_tackle_column(
owned_items: Array[OwnedItemType],
item_list: GridContainer,
) -> void:
for owned: OwnedItemType in owned_items:
var item: ItemDataType = _item_catalog.get_item_by_id(owned.item_id)
var row := Button.new()
row.icon = item.icon
row.tooltip_text = (
"%s ×%d" % [item.display_name, owned.quantity]
if item == null:
continue
var button := Button.new()
button.icon = item.icon
button.tooltip_text = (
"%s%d/%d" % [
item.display_name,
owned.quantity,
item.max_stack,
]
if item.is_bait()
else item.display_name
)
var compact_tackle_item: bool = item.is_bait() or item.is_lure()
if compact_tackle_item:
row.custom_minimum_size = Vector2(72, 72)
row.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
row.expand_icon = item.icon != null
row.alignment = HORIZONTAL_ALIGNMENT_CENTER
row.text = "" if item.icon != null else item.display_name
if item.icon == null:
row.add_theme_font_size_override("font_size", 12)
else:
row.text = "%s ×%d" % [item.display_name, owned.quantity]
row.alignment = HORIZONTAL_ALIGNMENT_LEFT
row.toggle_mode = true
row.set_meta(&"controller_tackle_item_id", owned.item_id)
row.button_pressed = owned.item_id == _selected_tackle_item_id
if compact_tackle_item:
_apply_tackle_bait_button_style(row)
if item.is_bait():
_add_tackle_quantity_badge(row, owned.quantity)
else:
UtilityPageStyle.apply_ocean_button(row)
row.pressed.connect(_select_tackle_item.bind(owned.item_id))
_tackle_item_list.add_child(row)
_tackle_empty.text = (
"No bait collected."
if _tackle_view == TackleView.BAIT
else "No lures collected."
)
_tackle_empty.visible = matching_items.is_empty()
_update_tackle_detail()
button.custom_minimum_size = Vector2(72, 72)
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
button.expand_icon = item.icon != null
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.text = "" if item.icon != null else item.display_name
if item.icon == null:
button.add_theme_font_size_override("font_size", 12)
button.toggle_mode = true
button.set_meta(&"controller_tackle_item_id", owned.item_id)
button.button_pressed = owned.item_id == _selected_tackle_item_id
_apply_tackle_bait_button_style(button)
if item.is_bait():
UtilityPageStyle.add_supply_quantity_badge(
button,
owned.quantity,
item.max_stack,
&"QuantityBadge",
)
button.pressed.connect(_select_tackle_item.bind(owned.item_id))
item_list.add_child(button)
_tackle_item_buttons[owned.item_id] = button
func _add_tackle_quantity_badge(row: Button, quantity: int) -> void:
var badge := Panel.new()
badge.name = "QuantityBadge"
badge.anchor_left = 1.0
badge.anchor_right = 1.0
badge.offset_left = -27.0
badge.offset_top = 3.0
badge.offset_right = -3.0
badge.offset_bottom = 27.0
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.z_index = 2
var badge_style := StyleBoxFlat.new()
badge_style.bg_color = Color("0b5558")
badge_style.set_corner_radius_all(12)
badge_style.anti_aliasing = false
badge.add_theme_stylebox_override("panel", badge_style)
row.add_child(badge)
var quantity_label := Label.new()
quantity_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
quantity_label.offset_left = 1.0
quantity_label.offset_right = 1.0
quantity_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
quantity_label.text = str(quantity)
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
quantity_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
quantity_label.add_theme_font_size_override(
"font_size",
13 if quantity >= 100 else 15,
func _configure_tackle_item_focus() -> void:
if not is_node_ready():
return
var bait_buttons: Array[Button] = _get_tackle_column_buttons(
_bait_item_list
)
quantity_label.add_theme_color_override("font_color", Color("e7f5f4"))
badge.add_child(quantity_label)
var lure_buttons: Array[Button] = _get_tackle_column_buttons(
_lure_item_list
)
var first_button: Button = (
bait_buttons.front()
if not bait_buttons.is_empty()
else (lure_buttons.front() if not lure_buttons.is_empty() else null)
)
_tackle_sub_tab.focus_neighbor_bottom = (
_tackle_sub_tab.get_path_to(first_button)
if first_button != null
else NodePath()
)
_configure_tackle_column_focus(bait_buttons, lure_buttons, true)
_configure_tackle_column_focus(lure_buttons, bait_buttons, false)
func _get_tackle_column_buttons(item_list: GridContainer) -> Array[Button]:
var buttons: Array[Button] = []
for child: Node in item_list.get_children():
var button := child as Button
if button != null and not button.is_queued_for_deletion():
buttons.append(button)
return buttons
func _configure_tackle_column_focus(
buttons: Array[Button],
other_buttons: Array[Button],
is_left_column: bool,
) -> void:
for index: int in buttons.size():
var button: Button = buttons[index]
var column: int = index % TACKLE_GRID_COLUMNS
var row: int = floori(float(index) / float(TACKLE_GRID_COLUMNS))
var left_target: Button = button
var right_target: Button = button
if column > 0:
left_target = buttons[index - 1]
elif not is_left_column and not other_buttons.is_empty():
left_target = other_buttons[mini(
row * TACKLE_GRID_COLUMNS + TACKLE_GRID_COLUMNS - 1,
other_buttons.size() - 1,
)]
if column < TACKLE_GRID_COLUMNS - 1 and index + 1 < buttons.size():
right_target = buttons[index + 1]
elif is_left_column and not other_buttons.is_empty():
right_target = other_buttons[mini(
row * TACKLE_GRID_COLUMNS,
other_buttons.size() - 1,
)]
button.focus_neighbor_left = button.get_path_to(left_target)
button.focus_neighbor_right = button.get_path_to(right_target)
button.focus_neighbor_top = (
button.get_path_to(_tackle_sub_tab)
if index < TACKLE_GRID_COLUMNS
else button.get_path_to(buttons[index - TACKLE_GRID_COLUMNS])
)
button.focus_neighbor_bottom = button.get_path_to(
buttons[mini(index + TACKLE_GRID_COLUMNS, buttons.size() - 1)]
)
func _apply_tackle_bait_button_style(button: Button) -> void:
@ -2045,7 +2097,10 @@ func _apply_tackle_bait_button_style(button: Button) -> void:
func _select_tackle_item(item_id: StringName) -> void:
_selected_tackle_item_id = item_id
_refresh_tackle_box()
for candidate_id: StringName in _tackle_item_buttons:
var button: Button = _tackle_item_buttons[candidate_id]
button.button_pressed = candidate_id == item_id
_update_tackle_detail()
func _update_tackle_detail() -> void:
@ -2057,11 +2112,8 @@ func _update_tackle_detail() -> void:
if item == null:
_tackle_equip_button.visible = false
_tackle_equip_button.persistent_mark = false
_tackle_detail_text.text = (
"Select bait for details."
if _tackle_view == TackleView.BAIT
else "Select a lure for details."
)
_tackle_detail_text.text = "Select bait or a lure for details."
_apply_tackle_interactivity(_tackle_is_interactive())
return
_tackle_equip_button.visible = item.is_bait() or item.is_lure()
var quantity: int = _bag.get_quantity(item.item_id) if _bag != null else 0
@ -2073,7 +2125,7 @@ func _update_tackle_detail() -> void:
break
var detail_lines: Array[String] = [item.display_name, ""]
if item.is_bait():
detail_lines.append("quantity: %d" % quantity)
detail_lines.append("quantity: %d/%d" % [quantity, item.max_stack])
if assigned_slot >= 0:
detail_lines.append("hotbar slot %d" % (assigned_slot + 1))
detail_lines.append("")
@ -2096,6 +2148,42 @@ func _update_tackle_detail() -> void:
_tackle_equip_button.persistent_mark = is_equipped
_tackle_equip_button.disabled = quantity <= 0
_tackle_equip_button.refresh_ink_state()
_apply_tackle_interactivity(_tackle_is_interactive())
func _tackle_is_interactive() -> bool:
return (
visible
and _current_section == Section.TACKLE_BOX
and not _transitioning
and not _page_transitioning
and not _sale_confirmation.visible
)
func _apply_tackle_interactivity(interactive: bool) -> void:
for button: Button in _tackle_item_buttons.values():
button.focus_mode = (
Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
)
button.mouse_filter = (
Control.MOUSE_FILTER_STOP
if interactive
else Control.MOUSE_FILTER_IGNORE
)
var action_interactive: bool = (
interactive
and _tackle_equip_button.visible
and not _tackle_equip_button.disabled
)
_tackle_equip_button.focus_mode = (
Control.FOCUS_ALL if action_interactive else Control.FOCUS_NONE
)
_tackle_equip_button.mouse_filter = (
Control.MOUSE_FILTER_STOP
if action_interactive
else Control.MOUSE_FILTER_IGNORE
)
func _toggle_active_tackle() -> void:
@ -2489,10 +2577,6 @@ func _update_shell_layout() -> void:
_tackle_rest_position = Vector2.ZERO
_bag_outer_wall.position = INVENTORY_MAIN_POSITION
_bag_outer_wall.size = INVENTORY_MAIN_SIZE
_bag_filter_tabs.position = (
INVENTORY_MAIN_POSITION + INVENTORY_HEADER_INSET
)
_bag_filter_tabs.size = Vector2(320.0, INVENTORY_HEADER_HEIGHT)
_bag_host.custom_minimum_size = (
Vector2(520.0, 152.0) if compact else Vector2(788.0, 358.0)
)
@ -2932,7 +3016,10 @@ func _set_content_interactive(interactive: bool) -> void:
else Control.MOUSE_FILTER_IGNORE
)
for tab: Button in [
_cooler_sub_tab, _bag_sub_tab, _tackle_sub_tab,
_cooler_sub_tab,
_bag_sub_tab,
_items_sub_tab,
_tackle_sub_tab,
]:
var tab_interactive: bool = (
interactive and _is_inventory_section(_current_section)
@ -3014,6 +3101,10 @@ func _set_content_interactive(interactive: bool) -> void:
if bag_interactive
else Control.MOUSE_FILTER_IGNORE
)
var tackle_interactive: bool = (
interactive and _current_section == Section.TACKLE_BOX
)
_apply_tackle_interactivity(tackle_interactive)
func _cancel_presentation_tween() -> void:
@ -3117,9 +3208,9 @@ func _on_hotbar_changed() -> void:
func _refresh_bag() -> void:
if not is_node_ready():
return
var owned_items: Array[OwnedItemType] = (
_bag.get_all_items() if _bag != null else []
)
var owned_items: Array[OwnedItemType] = []
if _bag != null:
owned_items = _bag.get_all_items()
var filtered_items: Array[OwnedItemType] = []
for owned: OwnedItemType in owned_items:
var item: ItemDataType = (
@ -3145,16 +3236,12 @@ func _refresh_bag() -> void:
owned_items = filtered_items
owned_items.sort_custom(_sort_bag_items)
_sorted_bag_items = owned_items
_equipment_filter.button_pressed = _bag_view == BagView.EQUIPMENT
_consumables_filter.button_pressed = (
_bag_view == BagView.CONSUMABLES
)
_bag_empty.visible = owned_items.is_empty()
_bag_empty_state.visible = owned_items.is_empty()
_bag_empty_state.text = (
"No equipment in your Bag."
if _bag_view == BagView.EQUIPMENT
else "No consumables in your Bag."
else "No items in your Bag."
)
if (
not _selected_bag_item_id.is_empty()
@ -3209,7 +3296,7 @@ func _sync_bag_item_nodes(owned_items: Array[OwnedItemType]) -> void:
continue
var removed := _bag_item_nodes[item_id] as BagItemSpriteType
_bag_item_nodes.erase(item_id)
_release_focus_from(removed, _bag_sub_tab)
_release_focus_from(removed, _active_bag_tab())
removed.queue_free()
_layout_bag_items()
_configure_bag_item_focus()
@ -3270,6 +3357,7 @@ func _layout_bag_items() -> void:
func _configure_bag_item_focus() -> void:
if _current_section != Section.BAG:
return
var active_tab: Button = _active_bag_tab()
var controls: Array[BagItemSpriteType] = []
for owned: OwnedItemType in _sorted_bag_items:
var item_node := _bag_item_nodes.get(
@ -3278,9 +3366,9 @@ func _configure_bag_item_focus() -> void:
if item_node != null:
controls.append(item_node)
if controls.is_empty():
_bag_sub_tab.focus_neighbor_bottom = NodePath()
active_tab.focus_neighbor_bottom = NodePath()
return
_bag_sub_tab.focus_neighbor_bottom = _bag_sub_tab.get_path_to(
active_tab.focus_neighbor_bottom = active_tab.get_path_to(
controls.front()
)
for index: int in controls.size():
@ -3296,7 +3384,7 @@ func _configure_bag_item_focus() -> void:
controls[right_index]
)
control.focus_neighbor_top = (
control.get_path_to(_bag_sub_tab)
control.get_path_to(active_tab)
if index < 3
else control.get_path_to(controls[index - 3])
)
@ -3305,6 +3393,14 @@ func _configure_bag_item_focus() -> void:
)
func _active_bag_tab() -> Button:
return (
_items_sub_tab
if _bag_view == BagView.CONSUMABLES
else _bag_sub_tab
)
func _sort_bag_items(a: OwnedItemType, b: OwnedItemType) -> bool:
var item_a: ItemDataType = _item_catalog.get_item_by_id(a.item_id)
var item_b: ItemDataType = _item_catalog.get_item_by_id(b.item_id)
@ -3355,7 +3451,7 @@ func _update_bag_detail() -> void:
_bag_detail_data.text = (
"%s\nquantity: %d\n%s\n%s\n%s"
% [
item.get_category_name(),
_item_category_display_name(item),
quantity,
item_state,
hotbar_assignment,
@ -3374,7 +3470,7 @@ func _update_bag_detail() -> void:
_bag_sprite_detail_data.text = (
"%s • quantity: %d\n%s%s\n%s"
% [
item.get_category_name(),
_item_category_display_name(item),
quantity,
item_state,
hotbar_assignment,
@ -3388,6 +3484,12 @@ func _update_bag_detail() -> void:
)
func _item_category_display_name(item: ItemDataType) -> String:
if item.category == ItemDataType.Category.CONSUMABLE:
return "item"
return item.get_category_name()
func _close_bag_detail() -> void:
_bag_detail_constellation.visible = false

View file

@ -79,7 +79,7 @@ z_index = 40
layout_mode = 0
offset_left = 150.0
offset_top = 136.0
offset_right = 534.0
offset_right = 664.0
offset_bottom = 174.0
mouse_filter = 1
theme_override_constants/separation = 6
@ -103,6 +103,16 @@ text = "Equipment"
script = ExtResource("23_organizer_tab")
palette_index = 2
[node name="ItemsSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(124, 38)
layout_mode = 2
toggle_mode = true
button_group = null
text = "Items"
script = ExtResource("23_organizer_tab")
palette_index = 2
[node name="TackleSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(124, 38)
@ -485,30 +495,6 @@ offset_right = 1280.0
offset_bottom = 720.0
mouse_filter = 1
[node name="BagFilterTabs" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
unique_name_in_owner = true
z_index = 55
layout_mode = 0
offset_left = 122.0
offset_top = 166.0
offset_right = 442.0
offset_bottom = 206.0
theme_override_constants/separation = 8
[node name="EquipmentFilter" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagFilterTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
toggle_mode = true
text = "Equipment"
[node name="ConsumablesFilter" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagFilterTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
toggle_mode = true
text = "Consumables"
[node name="BagOuterWall" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
unique_name_in_owner = true
z_index = 50
@ -532,7 +518,7 @@ layout_mode = 2
[node name="BagInnerMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall/BagOuterMargin/BagInnerLiner"]
layout_mode = 2
theme_override_constants/margin_left = 18
theme_override_constants/margin_top = 58
theme_override_constants/margin_top = 18
theme_override_constants/margin_right = 18
theme_override_constants/margin_bottom = 18
@ -646,47 +632,114 @@ theme_override_constants/margin_bottom = 20
[node name="Layout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin"]
layout_mode = 2
theme_override_constants/separation = 14
[node name="TackleFilterTabs" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 8
[node name="BaitFilter" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleFilterTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
toggle_mode = true
text = "Bait"
[node name="LuresFilter" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleFilterTabs"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 40)
layout_mode = 2
toggle_mode = true
text = "Lures"
[node name="TackleScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout"]
[node name="TackleColumns" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
theme_override_constants/separation = 18
[node name="TackleItemList" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleScroll"]
unique_name_in_owner = true
[node name="BaitColumn" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="TackleEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout"]
[node name="BaitHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 26
text = "bait"
horizontal_alignment = 1
[node name="BaitBody" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn"]
layout_mode = 2
size_flags_vertical = 3
[node name="BaitScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
horizontal_scroll_mode = 0
[node name="BaitItemList" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody/BaitScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 28
theme_override_constants/v_separation = 12
columns = 3
[node name="BaitEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
text = "No bait collected."
horizontal_alignment = 1
vertical_alignment = 1
[node name="TackleDivider" type="ColorRect" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
mouse_filter = 2
color = Color(0.204, 0.482, 0.553, 0.65)
[node name="LureColumn" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="LureHeading" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 26
text = "lures"
horizontal_alignment = 1
[node name="LureBody" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn"]
layout_mode = 2
size_flags_vertical = 3
[node name="LureScroll" type="ScrollContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
horizontal_scroll_mode = 0
[node name="LureItemList" type="GridContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody/LureScroll"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 28
theme_override_constants/v_separation = 12
columns = 3
[node name="LureEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
text = "No lures collected."
horizontal_alignment = 1
vertical_alignment = 1
[node name="TackleDetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
unique_name_in_owner = true
layout_mode = 0

View file

@ -46,7 +46,6 @@ func setup(
func activate() -> void:
_refresh()
UtilityPageStyle.animate_in(self)
_focus_first()
@ -82,7 +81,6 @@ func _build() -> void:
tab_row.add_child(_count_label)
_build_host_settings(root)
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(0, 310)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
root.add_child(scroll)

View file

@ -11,6 +11,18 @@ const MARKER_MODE_SHADER: Shader = preload(
const GRID_MODE_ICON: Texture2D = preload(
"res://items/icons/art/art_kit_grid_light.png"
)
const BRUSH_SIZE_ICONS: Dictionary[int, Texture2D] = {
1: preload("res://items/icons/art/art_kit_marker_tip_fine.png"),
2: preload("res://items/icons/art/art_kit_marker_tip_thin.png"),
3: preload("res://items/icons/art/art_kit_marker_tip_mid.png"),
4: preload("res://items/icons/art/art_kit_marker_tip_thick.png"),
}
const GRID_SIZE_ICONS: Dictionary[int, Texture2D] = {
16: preload("res://items/icons/art/art_kit_grid_small_light.png"),
32: preload("res://items/icons/art/art_kit_grid_medium_light.png"),
64: preload("res://items/icons/art/art_kit_grid_large_light.png"),
128: preload("res://items/icons/art/art_kit_grid_xl_light.png"),
}
const TOOLBAR_WIDTH: float = 510.0
const TOOLBAR_HEIGHT: float = 373.0
const COLOR_RAIL_WIDTH: float = 46.0
@ -41,6 +53,12 @@ func _ready() -> void:
UtilityPageStyleType.apply_ocean_button(_mode_button)
UtilityPageStyleType.apply_ocean_button(_brush_option)
UtilityPageStyleType.apply_ocean_button(_grid_option)
for size_option: OptionButton in [_brush_option, _grid_option]:
size_option.expand_icon = true
size_option.add_theme_constant_override("icon_max_width", 40)
size_option.get_popup().add_theme_constant_override(
"icon_max_width", 40
)
for button: Button in _action_buttons():
UtilityPageStyleType.apply_ocean_button(button)
button.custom_minimum_size = Vector2(48, 44)
@ -188,10 +206,26 @@ func owns_pointer_event(event: InputEvent) -> bool:
func _build_options() -> void:
_brush_option.clear()
for brush_size: int in PlayerArtUnlocks.BRUSH_SIZES:
_brush_option.add_item("%d×" % brush_size, brush_size)
_brush_option.add_icon_item(
BRUSH_SIZE_ICONS[brush_size],
"",
brush_size,
)
_brush_option.set_item_tooltip(
_brush_option.item_count - 1,
"%d× marker size" % brush_size,
)
_grid_option.clear()
for grid_size: int in PlayerArtUnlocks.GRID_SIZES:
_grid_option.add_item("%d×" % grid_size, grid_size)
_grid_option.add_icon_item(
GRID_SIZE_ICONS[grid_size],
"",
grid_size,
)
_grid_option.set_item_tooltip(
_grid_option.item_count - 1,
"%d×%d grid" % [grid_size, grid_size],
)
for child: Node in _color_list.get_children():
child.queue_free()
_color_buttons.clear()

View file

@ -41,13 +41,21 @@ theme_override_constants/separation = 5
unique_name_in_owner = true
custom_minimum_size = Vector2(80, 48)
layout_mode = 2
tooltip_text = "brush size"
texture_filter = 1
tooltip_text = "marker size"
accessibility_name = "marker size"
expand_icon = true
fit_to_longest_item = false
[node name="GridOption" type="OptionButton" parent="TopPanel/Top"]
unique_name_in_owner = true
custom_minimum_size = Vector2(92, 48)
layout_mode = 2
texture_filter = 1
tooltip_text = "grid size"
accessibility_name = "grid size"
expand_icon = true
fit_to_longest_item = false
[node name="EraserButton" type="Button" parent="TopPanel/Top"]
unique_name_in_owner = true

View file

@ -27,12 +27,97 @@ const LIGHT_TEXT: Color = Color("f5eed9")
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween"
const LAPTOP_RECT: Rect2 = Rect2(66.0, 132.0, 1148.0, 520.0)
const SUPPLY_BADGE_MIN_WIDTH: float = 46.0
const SUPPLY_BADGE_HEIGHT: float = 24.0
const SUPPLY_BADGE_EDGE_MARGIN: float = 3.0
const SUPPLY_BADGE_X_OFFSET: float = 18.0
const SUPPLY_BADGE_ROTATION_DEGREES: float = 15.0
const SUPPLY_BADGE_HORIZONTAL_PADDING: float = 10.0
const SUPPLY_BADGE_FONT_SIZE: int = 13
const SUPPLY_BADGE_COLOR: Color = Color("0b5558")
const SUPPLY_BADGE_TEXT_COLOR: Color = Color("e7f5f4")
static func apply_page(root: Control) -> void:
root.add_theme_font_override("font", TuffyFont)
static func supply_quantity_text(current: int, total: int) -> String:
return "%d/%d" % [maxi(current, 0), maxi(total, 0)]
static func add_supply_quantity_badge(
parent: Control,
current: int,
total: int,
badge_name: StringName = &"SupplyQuantityBadge",
top_margin: float = SUPPLY_BADGE_EDGE_MARGIN,
) -> Panel:
var badge := Panel.new()
badge.name = badge_name
badge.set_anchors_preset(Control.PRESET_TOP_RIGHT)
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.z_index = 3
parent.add_child(badge)
var quantity_label := Label.new()
quantity_label.name = "Quantity"
quantity_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
quantity_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
badge.add_child(quantity_label)
configure_supply_quantity_badge(
badge, quantity_label, current, total, top_margin
)
return badge
static func configure_supply_quantity_badge(
badge: Panel,
quantity_label: Label,
current: int,
total: int,
top_margin: float = SUPPLY_BADGE_EDGE_MARGIN,
) -> void:
var supply_text := supply_quantity_text(current, total)
var text_width: float = TuffyFont.get_string_size(
supply_text,
HORIZONTAL_ALIGNMENT_LEFT,
-1.0,
SUPPLY_BADGE_FONT_SIZE,
).x
var badge_width: float = maxf(
SUPPLY_BADGE_MIN_WIDTH,
ceilf(text_width + SUPPLY_BADGE_HORIZONTAL_PADDING),
)
badge.offset_left = (
-badge_width - SUPPLY_BADGE_EDGE_MARGIN + SUPPLY_BADGE_X_OFFSET
)
badge.offset_top = top_margin
badge.offset_right = -SUPPLY_BADGE_EDGE_MARGIN + SUPPLY_BADGE_X_OFFSET
badge.offset_bottom = top_margin + SUPPLY_BADGE_HEIGHT
badge.pivot_offset = Vector2(
badge_width * 0.5,
SUPPLY_BADGE_HEIGHT * 0.5,
)
badge.rotation_degrees = SUPPLY_BADGE_ROTATION_DEGREES
var badge_style := rounded_style(
SUPPLY_BADGE_COLOR,
roundi(SUPPLY_BADGE_HEIGHT * 0.5),
)
badge_style.anti_aliasing = false
badge.add_theme_stylebox_override("panel", badge_style)
quantity_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
quantity_label.text = supply_text
quantity_label.add_theme_font_override("font", TuffyFont)
quantity_label.add_theme_font_size_override(
"font_size", SUPPLY_BADGE_FONT_SIZE
)
quantity_label.add_theme_color_override(
"font_color", SUPPLY_BADGE_TEXT_COLOR
)
static func rounded_style(color: Color, radius: int) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
@ -46,6 +131,7 @@ static func build_laptop_screen(
laptop_rect: Rect2 = LAPTOP_RECT,
) -> MarginContainer:
var laptop := PanelContainer.new()
laptop.name = "UtilityMainBox"
laptop.position = laptop_rect.position
laptop.size = laptop_rect.size
laptop.add_theme_stylebox_override(
@ -53,12 +139,21 @@ static func build_laptop_screen(
)
root.add_child(laptop)
# Page-specific minimum sizes must not resize the shared outer artwork.
# Keep the shell in a fixed Control so every utility page retains the same
# canonical rectangle even as its internal controls evolve.
var fixed_layout := Control.new()
fixed_layout.name = "UtilityMainBoxLayout"
fixed_layout.clip_contents = true
laptop.add_child(fixed_layout)
var shell_margin := MarginContainer.new()
shell_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
shell_margin.add_theme_constant_override("margin_left", 20)
shell_margin.add_theme_constant_override("margin_top", 18)
shell_margin.add_theme_constant_override("margin_right", 20)
shell_margin.add_theme_constant_override("margin_bottom", 24)
laptop.add_child(shell_margin)
fixed_layout.add_child(shell_margin)
var screen := PanelContainer.new()
screen.add_theme_stylebox_override(