From a60925bf7b7835fda93a36a4178609dfd989f984 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 15 Aug 2026 14:28:13 -0400 Subject: [PATCH] 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. --- gathering/gathering_controller.gd | 3 - progression/player_item_effects.gd | 10 + tests/art_tools_validation.gd | 27 ++ tests/economy_regression_validation.gd | 180 +++++++- tests/equipment_catalog_validation.gd | 25 ++ tests/inventory_notepad_art_validation.gd | 49 ++- tests/logbook_validation.gd | 28 ++ tests/ui_scaling_runtime_validation.gd | 18 + ui/chat_ui.gd | 128 ++++-- ui/fishing_shop.gd | 153 ++++++- ui/fishing_shop.tscn | 4 +- ui/game_ui.gd | 51 +-- ui/game_ui.tscn | 22 +- ui/logbook_page.gd | 38 +- ui/mail_page.gd | 5 +- ui/player_menu.gd | 494 +++++++++++++--------- ui/player_menu.tscn | 153 ++++--- ui/players_page.gd | 2 - ui/surface_drawing_toolbar.gd | 38 +- ui/surface_drawing_toolbar.tscn | 10 +- ui/utility_page_style.gd | 97 ++++- 21 files changed, 1150 insertions(+), 385 deletions(-) diff --git a/gathering/gathering_controller.gd b/gathering/gathering_controller.gd index a0b6602..1a7649e 100644 --- a/gathering/gathering_controller.gd +++ b/gathering/gathering_controller.gd @@ -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() diff --git a/progression/player_item_effects.gd b/progression/player_item_effects.gd index 1a73c43..2d4c23d 100644 --- a/progression/player_item_effects.gd +++ b/progression/player_item_effects.gd @@ -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) diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 9dce65d..1c6c7bc 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -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)) diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index 42c56e3..debe48c 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -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 ) diff --git a/tests/equipment_catalog_validation.gd b/tests/equipment_catalog_validation.gd index 3e562b4..498c2ba 100644 --- a/tests/equipment_catalog_validation.gd +++ b/tests/equipment_catalog_validation.gd @@ -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 diff --git a/tests/inventory_notepad_art_validation.gd b/tests/inventory_notepad_art_validation.gd index de76221..2a526e4 100644 --- a/tests/inventory_notepad_art_validation.gd +++ b/tests/inventory_notepad_art_validation.gd @@ -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 diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index b8ed8be..3e152ac 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -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( diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd index f7144eb..f9ddcae 100644 --- a/tests/ui_scaling_runtime_validation.gd +++ b/tests/ui_scaling_runtime_validation.gd @@ -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 )) diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index c080a04..7c8ba5c 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -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( diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index ff1a44e..e3fa3c7 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -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 diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 5d0b9ec..271ad18 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -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"] diff --git a/ui/game_ui.gd b/ui/game_ui.gd index d368d8f..d9e6f77 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -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, ] diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 1cbbc4a..2354311 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -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 diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index efa7ff4..d27e450 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -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() diff --git a/ui/mail_page.gd b/ui/mail_page.gd index f1bd37d..f34c300 100644 --- a/ui/mail_page.gd +++ b/ui/mail_page.gd @@ -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." diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 6c5d2f2..62cf3b9 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -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 diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index e20077c..049107b 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -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 diff --git a/ui/players_page.gd b/ui/players_page.gd index 380299e..d36d60f 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -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) diff --git a/ui/surface_drawing_toolbar.gd b/ui/surface_drawing_toolbar.gd index e6c8efa..5ede917 100644 --- a/ui/surface_drawing_toolbar.gd +++ b/ui/surface_drawing_toolbar.gd @@ -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() diff --git a/ui/surface_drawing_toolbar.tscn b/ui/surface_drawing_toolbar.tscn index 448cae2..349b209 100644 --- a/ui/surface_drawing_toolbar.tscn +++ b/ui/surface_drawing_toolbar.tscn @@ -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 diff --git a/ui/utility_page_style.gd b/ui/utility_page_style.gd index 9045cbc..67e96b7 100644 --- a/ui/utility_page_style.gd +++ b/ui/utility_page_style.gd @@ -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(