class_name LogbookPage extends Control const CollectionLogType = preload("res://collection/collection_log.gd") const FishDataType = preload("res://fish/fish_data.gd") const FishQualityType = preload("res://fish/fish_quality.gd") const FishInventoryType = preload("res://inventory/fish_inventory.gd") const FishPoolType = preload("res://fish/fish_pool.gd") const FishingSpotType = preload("res://fishing/fishing_spot.gd") const WorldTimeServiceType = preload("res://world/world_time_service.gd") const WorldWeatherServiceType = preload( "res://world/world_weather_service.gd" ) const SILHOUETTE_SHADER: Shader = preload( "res://ui/logbook_silhouette.gdshader" ) const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") const ControllerFocusNavigationType = preload( "res://ui/controller_focus_navigation.gd" ) const NOTEPAD_INK_ACTION_SCENE: PackedScene = preload( "res://ui/components/bubble_menu/notepad_ink_action.tscn" ) const InventoryNotepadType = preload( "res://ui/components/inventory_notepad.gd" ) const LogbookPortraitType = preload( "res://ui/components/logbook_portrait.gd" ) const CurrencyPresentationType = preload( "res://ui/currency_presentation.gd" ) const DETAIL_FADE_DURATION: float = 0.12 const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION const HANDWRITTEN_NUMERIC_SCALE: float = 0.8 const PORTRAIT_VIEW_MAX_SIZE := Vector2(680.0, 420.0) const DETAIL_OVERLAY_CARD_SIZE := Vector2(800.0, 600.0) const DETAIL_OVERLAY_CONTENT_MARGIN: int = 34 const DETAIL_OVERLAY_TITLE_FONT_SIZE: int = 36 const DETAIL_OVERLAY_TEXT_FONT_SIZE: int = 28 const DETAIL_OVERLAY_FIELD_FONT_SIZE: int = 22 const DETAIL_OVERLAY_VALUE_FONT_SIZE: int = 26 const INK := Color("251b10") const MUTED_INK := Color("6d5b45") const LOGBOOK_ARTWORK: Texture2D = preload( "res://ui/assets/logbook/ui_logbook.png" ) const SCROLL_UP_TEXTURE: Texture2D = preload( "res://ui/icons/pictograms/arrow_dark_up_full.png" ) const SCROLL_DOWN_TEXTURE: Texture2D = preload( "res://ui/icons/pictograms/arrow_dark_down_full.png" ) 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 = 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 INDEX_TAB_SIZE := Vector2(88.0, 38.0) const INDEX_TAB_RIGHT_INSET: float = 72.0 const PAGE_CONTENT_SCALE: float = 0.97 const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0) const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0) const WIDE_BOOK_TOP: float = 50.0 const COMPACT_BOOK_TOP: float = 65.0 const CATALOG_PORTRAIT_SIZE := Vector2(78.0, 36.0) const CATALOG_ENTRY_SIZE := Vector2(92.0, 92.0) const CATALOG_ROW_STEP: float = 98.0 const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0) const DETAIL_SECTION_SEPARATION: int = 10 const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0) const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0) const DETAIL_STATS_ROW_SEPARATION: int = 8 const INDEX_OPTION_HEIGHT: float = 34.0 const INDEX_CONTENT_LEFT_INSET: float = 44.0 const INDEX_CONTENT_TOP_INSET: float = 4.0 const INDEX_CONTENT_RIGHT_INSET: float = 22.0 const INDEX_CONTENT_BOTTOM_INSET: float = 10.0 const INDEX_SHOW_LABELS := [ "all entries", "caught", "missing", "needs quality", ] const INDEX_SORT_LABELS := [ "catalog no.", "name", "rarity", "number caught", "quality progress", "value", "weight", ] const INDEX_AVAILABILITY_LABELS := [ "any time", "available now", ] const INDEX_SEASON_LABELS := [ "any season", "current season", "spring", "summer", "fall", "winter", ] const INDEX_TIME_LABELS := [ "any time", "current time", "day", "night", ] const INDEX_RARITY_LABELS := [ "any rarity", "common", "uncommon", "rare", "epic", "legendary", ] const INDEX_METHOD_LABELS := [ "any method", "fishing", "netting", "digging", ] const INDEX_HOLDING_LABELS := [ "any ownership", "in cooler", "favorited", ] signal controller_text_entry_requested(control: Control) enum ControllerZone { TABS, ENTRIES, DETAILS, INDEX, OVERLAY, } var _collection_log: CollectionLogType var _inventory: FishInventoryType var _catalog: FishPoolType var _fishing_spot: FishingSpotType var _world_time: WorldTimeServiceType var _world_weather: WorldWeatherServiceType var _category: LogbookCatalog.Category = ( LogbookCatalog.Category.FRESH_WATER ) var _selected_id: StringName var _selected_entry_key: StringName var _active: bool = false var _interactive: bool = false var _entry_buttons: Dictionary[StringName, Button] = {} var _detail_generation: int = 0 var _detail_tween: Tween var _category_generation: int = 0 var _category_tween: Tween var _silhouette_material: ShaderMaterial var _outer_margin: MarginContainer var _book: Control var _compact_presentation: bool = false 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 var _catalog_scroll_down_indicator: TextureRect var _empty_state: Label var _detail_body: VBoxContainer var _detail_portrait_button: Button var _detail_buttons: Array[Button] = [] var _portrait_overlay: Control var _portrait_overlay_backdrop: Button var _portrait_overlay_artwork: LogbookPortraitType var _portrait_overlay_artwork_view: Control var _portrait_overlay_title: Label var _portrait_overlay_text: Label var _portrait_overlay_quality_view: Control var _portrait_overlay_quality: VBoxContainer var _portrait_overlay_stats_view: Control var _portrait_overlay_stats: GridContainer var _overlay_return_focus: Control var _controller_zone: ControllerZone = ControllerZone.TABS var _index_tab: OrganizerTabType var _index_body: VBoxContainer var _index_search: LineEdit var _index_summary: Label var _index_note: Label var _index_reset: Button var _index_option_buttons: Dictionary[StringName, Button] = {} var _index_controls: Array[Control] = [] var _index_open: bool = false var _index_update_in_progress: bool = false var _index_visible_count: int = 0 var _index_category_total: int = 0 var _index_options: Dictionary = { "search": "", "show": LogbookCatalog.DiscoveryFilter.ALL, "sort": LogbookCatalog.SortMode.CATALOG_NUMBER, "descending": false, "availability": LogbookCatalog.AvailabilityFilter.ANY, "season": LogbookCatalog.SeasonFilter.ANY, "time": LogbookCatalog.TimeFilter.ANY, "rarity": LogbookCatalog.RarityFilter.ANY, "method": LogbookCatalog.MethodFilter.ANY, "holding": LogbookCatalog.HoldingFilter.ANY, "group": "", } func _ready() -> void: _silhouette_material = ShaderMaterial.new() _silhouette_material.shader = SILHOUETTE_SHADER _build_interface() set_interactive(false) func setup( collection_log: CollectionLogType, inventory: FishInventoryType, catalog: FishPoolType, fishing_spot: FishingSpotType = null, world_time: WorldTimeServiceType = null, world_weather: WorldWeatherServiceType = null, ) -> void: _collection_log = collection_log _inventory = inventory _catalog = catalog _fishing_spot = fishing_spot _world_time = world_time _world_weather = world_weather if not _collection_log.fish_discovered.is_connected( _on_fish_discovered ): _collection_log.fish_discovered.connect(_on_fish_discovered) if not _collection_log.collection_changed.is_connected( _on_collection_changed ): _collection_log.collection_changed.connect(_on_collection_changed) if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) if ( _world_time != null and not _world_time.time_changed.is_connected( _on_logbook_context_changed ) ): _world_time.time_changed.connect(_on_logbook_context_changed) _world_time.calendar_date_changed.connect(_on_logbook_context_changed) if ( _world_weather != null and not _world_weather.weather_changed.is_connected( _on_logbook_context_changed ) ): _world_weather.weather_changed.connect(_on_logbook_context_changed) _refresh_catalog() func set_compact_presentation(compact: bool) -> void: _compact_presentation = compact _apply_presentation_margins() _apply_index_presentation() func activate() -> void: _active = true set_interactive(true) _refresh_catalog() _animate_tab_entry() func deactivate() -> void: _active = false _hide_portrait_overlay(false) set_interactive(false) _settle_tabs_for_close() _cancel_detail_tween() _cancel_category_tween() func set_interactive(value: bool) -> void: _interactive = value and _active mouse_filter = ( Control.MOUSE_FILTER_PASS if _interactive else Control.MOUSE_FILTER_IGNORE ) for tab: Button in _category_tabs: tab.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.TABS else Control.FOCUS_NONE ) tab.mouse_filter = ( Control.MOUSE_FILTER_STOP if _interactive and not tab.button_pressed else Control.MOUSE_FILTER_IGNORE ) if _index_tab != null: _index_tab.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.TABS else Control.FOCUS_NONE ) _index_tab.mouse_filter = ( Control.MOUSE_FILTER_STOP if _interactive else Control.MOUSE_FILTER_IGNORE ) for entry: Button in _entry_buttons.values(): entry.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.ENTRIES else Control.FOCUS_NONE ) entry.mouse_filter = ( Control.MOUSE_FILTER_STOP if _interactive else Control.MOUSE_FILTER_IGNORE ) for detail_button: Button in _detail_buttons: if not is_instance_valid(detail_button): continue detail_button.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.DETAILS else Control.FOCUS_NONE ) for index_control: Control in _index_controls: if not is_instance_valid(index_control): continue index_control.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.INDEX else Control.FOCUS_NONE ) index_control.mouse_filter = ( Control.MOUSE_FILTER_STOP if _interactive and _index_open else Control.MOUSE_FILTER_IGNORE ) if _portrait_overlay_backdrop != null: _portrait_overlay_backdrop.focus_mode = ( Control.FOCUS_ALL if _interactive and _controller_zone == ControllerZone.OVERLAY else Control.FOCUS_NONE ) func focus_initial() -> void: if not _active or not _interactive: return if _controller_zone == ControllerZone.OVERLAY: _portrait_overlay_backdrop.grab_focus() return if _controller_zone == ControllerZone.TABS: if _index_open and _index_tab != null: _index_tab.grab_focus() return var category_tab_index: int = _category_tab_categories.find(_category) if category_tab_index >= 0: _category_tabs[category_tab_index].grab_focus() return if _controller_zone == ControllerZone.DETAILS: if not _detail_buttons.is_empty(): _detail_buttons.front().grab_focus() return if _controller_zone == ControllerZone.INDEX: if _index_search != null: _index_search.grab_focus() return var selected := _entry_buttons.get(_selected_entry_key) as Button if selected != null: selected.grab_focus() elif not _entry_buttons.is_empty(): var first := _entry_buttons.values().front() as Button first.grab_focus() func reset_controller_zone() -> void: _controller_zone = ControllerZone.TABS set_interactive(_interactive) call_deferred("focus_initial") func handle_controller_input(event: InputEvent) -> bool: if not _active or not _interactive: return false var accept_pressed: bool = event.is_action_pressed("ui_accept") var cancel_pressed: bool = event.is_action_pressed("ui_cancel") if cancel_pressed: match _controller_zone: ControllerZone.OVERLAY: _hide_portrait_overlay() ControllerZone.DETAILS: _set_controller_zone(ControllerZone.ENTRIES) ControllerZone.INDEX: _set_index_open(false) ControllerZone.ENTRIES: _set_controller_zone(ControllerZone.TABS) ControllerZone.TABS: return false return true if _controller_zone == ControllerZone.TABS: if event.is_action_pressed("ui_left"): if _index_open: _set_index_open(false, false) _category_tabs.back().grab_focus() return true _select_adjacent_controller_category(-1) return true if event.is_action_pressed("ui_right"): if ( _category_tab_categories.find(_category) == _category_tab_categories.size() - 1 ): _set_index_open(true, false) _index_tab.grab_focus() return true _select_adjacent_controller_category(1) return true if accept_pressed: if _index_open: _set_controller_zone(ControllerZone.INDEX) elif not _entry_buttons.is_empty(): _set_controller_zone(ControllerZone.ENTRIES) return true if _controller_zone == ControllerZone.INDEX: var focus_owner := get_viewport().gui_get_focus_owner() if accept_pressed and focus_owner == _index_search: controller_text_entry_requested.emit(_index_search) return true if event.is_action_pressed("ui_left"): var previous_key := _index_key_for_control(focus_owner) if not previous_key.is_empty(): _cycle_index_option(previous_key, -1) return true if event.is_action_pressed("ui_right") or accept_pressed: var next_key := _index_key_for_control(focus_owner) if not next_key.is_empty(): _cycle_index_option(next_key, 1) return true if _controller_zone == ControllerZone.ENTRIES and accept_pressed: if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty(): _set_controller_zone(ControllerZone.DETAILS) return true return false func _set_controller_zone(zone: ControllerZone) -> void: _controller_zone = zone set_interactive(_interactive) call_deferred("focus_initial") func _select_adjacent_controller_category(direction: int) -> void: var current_index: int = _category_tab_categories.find(_category) var target_index: int = clampi( current_index + direction, 0, _category_tab_categories.size() - 1, ) if target_index != current_index: _select_category(_category_tab_categories[target_index]) func _build_interface() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _outer_margin = MarginContainer.new() _outer_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(_outer_margin) _apply_presentation_margins() var stack := Control.new() stack.mouse_filter = Control.MOUSE_FILTER_IGNORE _outer_margin.add_child(stack) var tabs := HBoxContainer.new() # Align the first flange with the authored left paper edge. tabs.position = Vector2(LOGBOOK_TAB_LEFT_INSET, 37.0) var category_order: Array[LogbookCatalog.Category] = [ LogbookCatalog.Category.FRESH_WATER, LogbookCatalog.Category.SALT_WATER, 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 = LOGBOOK_TAB_SIZE tab.toggle_mode = true tab.text = LogbookCatalog.category_label(category) tab.palette_index = int(category) tab.set_selected(category == _category, false) tab.pressed.connect(_select_category.bind(category)) tabs.add_child(tab) _category_tabs.append(tab) _category_tab_categories.append(category) _index_tab = OrganizerTabType.new() _index_tab.name = "CatalogIndexTab" _index_tab.anchor_left = 1.0 _index_tab.anchor_right = 1.0 _index_tab.offset_left = -INDEX_TAB_RIGHT_INSET - INDEX_TAB_SIZE.x _index_tab.offset_top = 37.0 _index_tab.offset_right = -INDEX_TAB_RIGHT_INSET _index_tab.offset_bottom = 37.0 + INDEX_TAB_SIZE.y _index_tab.text = "index" _index_tab.palette_index = 0 _index_tab.tooltip_text = "Open catalog index" _index_tab.accessibility_name = "Catalog index" _index_tab.pressed.connect(_toggle_index_page) stack.add_child(_index_tab) _configure_category_focus() var book := Control.new() _book = book book.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) book.offset_top = ( COMPACT_BOOK_TOP if _compact_presentation else WIDE_BOOK_TOP ) book.z_index = 20 book.mouse_filter = Control.MOUSE_FILTER_IGNORE stack.add_child(book) var artwork := TextureRect.new() artwork.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) artwork.texture = LOGBOOK_ARTWORK artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE artwork.stretch_mode = TextureRect.STRETCH_SCALE artwork.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE book.add_child(artwork) var left_page := Control.new() left_page.mouse_filter = Control.MOUSE_FILTER_IGNORE _apply_artwork_rect(left_page, LEFT_PAGE_CONTENT_RECT) book.add_child(left_page) var left_layout := VBoxContainer.new() left_layout.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) left_layout.add_theme_constant_override("separation", 8) left_page.add_child(left_layout) _apply_page_content_scale(left_layout) var scroll_indicator_top_lane := Control.new() scroll_indicator_top_lane.custom_minimum_size.y = 20.0 scroll_indicator_top_lane.mouse_filter = Control.MOUSE_FILTER_IGNORE left_layout.add_child(scroll_indicator_top_lane) _empty_state = _label("", 18) _empty_state.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _empty_state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _empty_state.size_flags_vertical = Control.SIZE_EXPAND_FILL left_layout.add_child(_empty_state) var catalog_scroll_margin := MarginContainer.new() catalog_scroll_margin.size_flags_vertical = Control.SIZE_EXPAND_FILL catalog_scroll_margin.add_theme_constant_override("margin_left", 20) left_layout.add_child(catalog_scroll_margin) _catalog_scroll = ScrollContainer.new() _catalog_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _catalog_scroll.horizontal_scroll_mode = ( ScrollContainer.SCROLL_MODE_DISABLED ) _catalog_scroll.follow_focus = true _catalog_scroll.scroll_vertical_custom_step = CATALOG_ROW_STEP catalog_scroll_margin.add_child(_catalog_scroll) _catalog_grid = GridContainer.new() _catalog_grid.columns = 4 _catalog_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL _catalog_grid.add_theme_constant_override("h_separation", 6) _catalog_grid.add_theme_constant_override("v_separation", 6) _catalog_scroll.add_child(_catalog_grid) var scroll_indicator_bottom_lane := Control.new() scroll_indicator_bottom_lane.custom_minimum_size.y = 32.0 scroll_indicator_bottom_lane.mouse_filter = Control.MOUSE_FILTER_IGNORE left_layout.add_child(scroll_indicator_bottom_lane) _configure_catalog_scroll_bar() _catalog_scroll_up_indicator = _make_scroll_indicator( "CatalogScrollUpIndicator", SCROLL_UP_TEXTURE, true, ) left_page.add_child(_catalog_scroll_up_indicator) _catalog_scroll_down_indicator = _make_scroll_indicator( "CatalogScrollDownIndicator", SCROLL_DOWN_TEXTURE, false, ) left_page.add_child(_catalog_scroll_down_indicator) var right_page := Control.new() right_page.mouse_filter = Control.MOUSE_FILTER_IGNORE _apply_artwork_rect(right_page, RIGHT_PAGE_CONTENT_RECT) book.add_child(right_page) _detail_body = VBoxContainer.new() _detail_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _detail_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL _detail_body.size_flags_vertical = Control.SIZE_EXPAND_FILL _detail_body.add_theme_constant_override( "separation", DETAIL_SECTION_SEPARATION ) right_page.add_child(_detail_body) _apply_page_content_scale(_detail_body) _show_no_selection() _build_index_page(right_page) _build_portrait_overlay() func _build_index_page(right_page: Control) -> void: _index_body = VBoxContainer.new() _index_body.name = "CatalogIndex" _index_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _index_body.offset_left = INDEX_CONTENT_LEFT_INSET _index_body.offset_top = INDEX_CONTENT_TOP_INSET _index_body.offset_right = -INDEX_CONTENT_RIGHT_INSET _index_body.offset_bottom = -INDEX_CONTENT_BOTTOM_INSET _index_body.add_theme_constant_override("separation", 4) _index_body.visible = false right_page.add_child(_index_body) _apply_page_content_scale(_index_body) _apply_index_presentation() var heading := _label("catalog index", 28) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _index_body.add_child(heading) _index_summary = _field_label("", 14) _index_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _index_summary.add_theme_color_override("font_color", MUTED_INK) _index_body.add_child(_index_summary) var search_stack := VBoxContainer.new() search_stack.add_theme_constant_override("separation", 0) _index_body.add_child(search_stack) var search_label := _field_label("find by name, group, or catalog no.", 13) search_label.add_theme_color_override("font_color", MUTED_INK) search_stack.add_child(search_label) _index_search = LineEdit.new() _index_search.name = "CatalogIndexSearch" _index_search.custom_minimum_size.y = 34.0 _index_search.placeholder_text = "write here..." _index_search.max_length = 48 _index_search.add_theme_font_override( "font", InventoryNotepadType.HANDWRITTEN_FONT ) _index_search.add_theme_font_size_override("font_size", 18) _index_search.add_theme_color_override("font_color", INK) _index_search.add_theme_color_override("caret_color", INK) _index_search.add_theme_color_override( "font_placeholder_color", Color(MUTED_INK, 0.72) ) for style_name: StringName in [ &"normal", &"focus", &"read_only", ]: _index_search.add_theme_stylebox_override( style_name, _index_search_style(style_name == &"focus"), ) _index_search.text_changed.connect(_on_index_search_changed) search_stack.add_child(_index_search) _index_controls.append(_index_search) var options_grid := GridContainer.new() options_grid.columns = 2 options_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL options_grid.add_theme_constant_override("h_separation", 18) options_grid.add_theme_constant_override("v_separation", 2) _index_body.add_child(options_grid) for descriptor: Dictionary in [ {"key": &"show", "label": "show"}, {"key": &"rarity", "label": "rarity"}, {"key": &"sort", "label": "sort by"}, {"key": &"availability", "label": "availability"}, {"key": &"descending", "label": "order"}, {"key": &"season", "label": "season"}, {"key": &"method", "label": "method"}, {"key": &"time", "label": "time of day"}, {"key": &"holding", "label": "ownership"}, {"key": &"group", "label": "collection group"}, ]: options_grid.add_child(_make_index_option( descriptor["key"], str(descriptor["label"]), )) var footer := HBoxContainer.new() footer.alignment = BoxContainer.ALIGNMENT_CENTER _index_body.add_child(footer) _index_reset = NOTEPAD_INK_ACTION_SCENE.instantiate() as Button _index_reset.name = "CatalogIndexReset" _index_reset.custom_minimum_size = Vector2(126.0, 34.0) _index_reset.text = "clear index" _apply_index_value_font(_index_reset) _index_reset.tooltip_text = "Clear every catalog filter" _index_reset.accessibility_name = "Clear catalog index filters" _index_reset.pressed.connect(_reset_index) footer.add_child(_index_reset) _index_controls.append(_index_reset) _index_note = _field_label( "silhouettes keep names hidden in filtered results", 12, ) _index_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _index_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _index_note.add_theme_color_override("font_color", Color(MUTED_INK, 0.82)) _index_body.add_child(_index_note) _update_index_controls() _configure_index_focus.call_deferred() func _make_index_option(key: StringName, label_text: String) -> Control: var stack := VBoxContainer.new() stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL stack.add_theme_constant_override("separation", -3) var label := _field_label(label_text, 12) label.add_theme_color_override("font_color", MUTED_INK) stack.add_child(label) var button := NOTEPAD_INK_ACTION_SCENE.instantiate() as Button button.name = "CatalogIndex%s" % String(key).capitalize().replace(" ", "") button.custom_minimum_size = Vector2(0.0, INDEX_OPTION_HEIGHT) button.size_flags_horizontal = Control.SIZE_EXPAND_FILL button.clip_text = true _apply_index_value_font(button) button.tooltip_text = "Change %s; use left or right on a controller" % label_text button.pressed.connect(_cycle_index_option.bind(key, 1)) button.gui_input.connect(_on_index_option_gui_input.bind(key)) stack.add_child(button) _index_option_buttons[key] = button _index_controls.append(button) return stack func _apply_index_value_font(button: Button) -> void: button.add_theme_font_override( "font", InventoryNotepadType.HANDWRITTEN_FONT ) button.add_theme_font_size_override("font_size", 18) for color_name: StringName in [ &"font_color", &"font_hover_color", &"font_focus_color", &"font_pressed_color", ]: button.add_theme_color_override(color_name, INK) func _index_search_style(focused: bool) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(1.0, 0.97, 0.82, 0.12 if focused else 0.0) style.border_color = Color(INK, 0.72 if focused else 0.42) style.border_width_bottom = 2 style.content_margin_left = 6 style.content_margin_right = 22 style.content_margin_top = 2 style.content_margin_bottom = 3 return style func _configure_index_focus() -> void: var controls: Array[Control] = [] for control: Control in _index_controls: if control != null and is_instance_valid(control): controls.append(control) if not controls.is_empty(): ControllerFocusNavigationType.configure_spatial_neighbors(controls) func _apply_index_presentation() -> void: if _index_body == null: return var scale_value := 0.82 if _compact_presentation else PAGE_CONTENT_SCALE _index_body.scale = Vector2.ONE * scale_value func _apply_presentation_margins() -> void: if _outer_margin == null: return var margins: Rect2 = ( COMPACT_OUTER_MARGINS if _compact_presentation else WIDE_OUTER_MARGINS ) _outer_margin.add_theme_constant_override( "margin_left", roundi(margins.position.x) ) _outer_margin.add_theme_constant_override( "margin_top", roundi(margins.position.y) ) _outer_margin.add_theme_constant_override( "margin_right", roundi(margins.size.x) ) _outer_margin.add_theme_constant_override( "margin_bottom", roundi(margins.size.y) ) if _book != null: _book.offset_top = ( COMPACT_BOOK_TOP if _compact_presentation else WIDE_BOOK_TOP ) func _apply_artwork_rect(control: Control, source_rect: Rect2) -> void: control.anchor_left = source_rect.position.x / LOGBOOK_ARTWORK_SOURCE_SIZE.x control.anchor_top = source_rect.position.y / LOGBOOK_ARTWORK_SOURCE_SIZE.y control.anchor_right = source_rect.end.x / LOGBOOK_ARTWORK_SOURCE_SIZE.x control.anchor_bottom = source_rect.end.y / LOGBOOK_ARTWORK_SOURCE_SIZE.y control.offset_left = 0.0 control.offset_top = 0.0 control.offset_right = 0.0 control.offset_bottom = 0.0 func _apply_page_content_scale(control: Control) -> void: control.scale = Vector2.ONE * PAGE_CONTENT_SCALE control.resized.connect(_center_scaled_content.bind(control)) _center_scaled_content.call_deferred(control) func _center_scaled_content(control: Control) -> void: if is_instance_valid(control): control.pivot_offset = control.size * 0.5 func _configure_catalog_scroll_bar() -> void: var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() scroll_bar.mouse_filter = Control.MOUSE_FILTER_IGNORE scroll_bar.focus_mode = Control.FOCUS_NONE scroll_bar.self_modulate = Color.TRANSPARENT scroll_bar.custom_minimum_size.x = 0.0 scroll_bar.add_theme_constant_override("scroll_size", 0) var empty_style := StyleBoxEmpty.new() for style_name: StringName in [ &"scroll", &"scroll_focus", &"grabber", &"grabber_highlight", &"grabber_pressed", ]: scroll_bar.add_theme_stylebox_override(style_name, empty_style) scroll_bar.changed.connect(_refresh_catalog_scroll_indicators) scroll_bar.value_changed.connect(_on_catalog_scroll_value_changed) func _make_scroll_indicator( indicator_name: String, indicator_texture: Texture2D, at_top: bool, ) -> TextureRect: var indicator := TextureRect.new() indicator.name = indicator_name indicator.anchor_left = 0.5 indicator.anchor_right = 0.5 indicator.anchor_top = 0.0 if at_top else 1.0 indicator.anchor_bottom = indicator.anchor_top indicator.offset_left = -14.0 indicator.offset_right = 14.0 indicator.offset_top = -8.0 if at_top else -35.0 indicator.offset_bottom = 20.0 if at_top else -7.0 indicator.z_index = 30 indicator.texture = indicator_texture indicator.expand_mode = TextureRect.EXPAND_IGNORE_SIZE indicator.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED indicator.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST indicator.mouse_filter = Control.MOUSE_FILTER_IGNORE indicator.visible = false return indicator func _on_catalog_scroll_value_changed(_value: float) -> void: _refresh_catalog_scroll_indicators() func _refresh_catalog_scroll_indicators() -> void: if ( _catalog_scroll == null or _catalog_scroll_up_indicator == null or _catalog_scroll_down_indicator == null ): return var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar() var maximum_scroll: float = scroll_bar.max_value - scroll_bar.page var has_overflow: bool = ( _catalog_scroll.visible and maximum_scroll > scroll_bar.min_value + 0.5 ) _catalog_scroll_up_indicator.visible = ( has_overflow and scroll_bar.value > scroll_bar.min_value + 0.5 ) _catalog_scroll_down_indicator.visible = ( has_overflow and scroll_bar.value < maximum_scroll - 0.5 ) func _refresh_catalog() -> void: if not is_node_ready(): return var preserved_selection: StringName = _selected_id var preserved_entry_key: StringName = _selected_entry_key _clear_entries() var species: Array[FishDataType] = [] var category_total: int = 0 if _catalog != null: for fish: FishDataType in LogbookCatalog.ordered_species( _catalog.candidates ): if LogbookCatalog.category_for(fish) == _category: category_total += 1 var available_now_resolver := Callable() if _fishing_spot != null and is_instance_valid(_fishing_spot): available_now_resolver = Callable( _fishing_spot, "is_species_available_now", ) species = LogbookCatalog.filtered_species( _catalog.candidates, _category, _collection_log, _inventory, _index_options, available_now_resolver, _current_season(), _current_is_night(), ) for fish: FishDataType in species: var discovered: bool = _collection_log.has_discovered(fish.id) var entry: Button = _make_entry(fish, discovered) var selection_key: StringName = ( fish.id if discovered else _unknown_selection_key(fish) ) _entry_buttons[selection_key] = entry entry.pressed.connect( _select_entry.bind( selection_key, fish.id if discovered else StringName(), ) ) entry.focus_entered.connect( _select_entry.bind( selection_key, fish.id if discovered else StringName(), ) ) _catalog_grid.add_child(entry) _empty_state.text = ( "No entries match this index." if _index_has_active_criteria() else LogbookCatalog.empty_state(_category) ) _empty_state.visible = _entry_buttons.is_empty() _catalog_scroll.visible = not _entry_buttons.is_empty() if ( not preserved_selection.is_empty() and _entry_buttons.has(preserved_selection) ): _selected_id = preserved_selection _selected_entry_key = preserved_selection elif _entry_buttons.has(preserved_entry_key): _selected_id = StringName() _selected_entry_key = preserved_entry_key else: _selected_id = StringName() _selected_entry_key = StringName() _refresh_selection_styles() _refresh_details(false) _update_index_controls(species.size(), category_total) set_interactive(_interactive) _refresh_catalog_scroll_indicators.call_deferred() func _toggle_index_page() -> void: _set_index_open(not _index_open) func _set_index_open(open: bool, restore_focus: bool = true) -> void: _index_open = open if _index_tab != null: _index_tab.set_selected(open) _index_tab.tooltip_text = ( "Return to catch record" if open else "Open catalog index" ) if _index_body != null: _index_body.visible = open if _detail_body != null: _detail_body.visible = not open if not open: _refresh_details(false) _update_index_controls() if restore_focus and _active and _interactive: if open: _controller_zone = ControllerZone.INDEX set_interactive(true) _index_search.grab_focus() elif _controller_zone == ControllerZone.INDEX: _controller_zone = ControllerZone.TABS set_interactive(true) _index_tab.grab_focus() func _on_index_search_changed(value: String) -> void: if _index_update_in_progress: return _index_options["search"] = value _refresh_catalog() func _cycle_index_option(key: StringName, direction: int = 1) -> void: if not _index_options.has(key): return if key == &"descending": _index_options[key] = not bool(_index_options[key]) elif key == &"group": var groups: Array[String] = [""] if _catalog != null: groups.append_array(LogbookCatalog.available_groups( _catalog.candidates, _category, _collection_log, )) var current_index := groups.find(str(_index_options[key])) if current_index < 0: current_index = 0 _index_options[key] = groups[wrapi( current_index + direction, 0, groups.size(), )] else: var option_count := _index_option_count(key) if option_count <= 0: return _index_options[key] = wrapi( int(_index_options[key]) + direction, 0, option_count, ) _refresh_catalog() func _on_index_option_gui_input( event: InputEvent, key: StringName, ) -> void: var mouse_event := event as InputEventMouseButton if mouse_event == null or not mouse_event.pressed: return if mouse_event.button_index in [MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_UP]: _cycle_index_option(key, -1) get_viewport().set_input_as_handled() elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: _cycle_index_option(key, 1) get_viewport().set_input_as_handled() func _index_key_for_control(control: Control) -> StringName: for key: StringName in _index_option_buttons: if _index_option_buttons[key] == control: return key return StringName() func _reset_index() -> void: _index_options = { "search": "", "show": LogbookCatalog.DiscoveryFilter.ALL, "sort": LogbookCatalog.SortMode.CATALOG_NUMBER, "descending": false, "availability": LogbookCatalog.AvailabilityFilter.ANY, "season": LogbookCatalog.SeasonFilter.ANY, "time": LogbookCatalog.TimeFilter.ANY, "rarity": LogbookCatalog.RarityFilter.ANY, "method": LogbookCatalog.MethodFilter.ANY, "holding": LogbookCatalog.HoldingFilter.ANY, "group": "", } _index_update_in_progress = true _index_search.text = "" _index_update_in_progress = false _refresh_catalog() func _update_index_controls( visible_count: int = -1, category_total: int = -1, ) -> void: if visible_count >= 0: _index_visible_count = visible_count if category_total >= 0: _index_category_total = category_total if _index_summary != null: _index_summary.text = "%d shown of %d" % [ _index_visible_count, _index_category_total, ] for key: StringName in _index_option_buttons: var button := _index_option_buttons[key] if button == null: continue button.text = _index_option_label(key) button.accessibility_name = "%s: %s" % [ String(key).replace("_", " "), button.text, ] if _index_tab != null: var marked := _index_has_active_criteria() _index_tab.text = "index •" if marked else "index" _index_tab.accessibility_name = ( "Catalog index, filters active" if marked else "Catalog index" ) if _index_reset != null: _index_reset.disabled = not _index_has_active_criteria() func _index_option_label(key: StringName) -> String: match key: &"show": return INDEX_SHOW_LABELS[int(_index_options[key])] &"sort": return INDEX_SORT_LABELS[int(_index_options[key])] &"availability": return INDEX_AVAILABILITY_LABELS[int(_index_options[key])] &"season": return INDEX_SEASON_LABELS[int(_index_options[key])] &"time": return INDEX_TIME_LABELS[int(_index_options[key])] &"rarity": return INDEX_RARITY_LABELS[int(_index_options[key])] &"method": return INDEX_METHOD_LABELS[int(_index_options[key])] &"holding": return INDEX_HOLDING_LABELS[int(_index_options[key])] &"group": var group := str(_index_options[key]) return group if not group.is_empty() else "any group" &"descending": var descending := bool(_index_options[key]) match int(_index_options["sort"]): LogbookCatalog.SortMode.NAME: return "z–a" if descending else "a–z" LogbookCatalog.SortMode.CATALOG_NUMBER: return "high–low" if descending else "low–high" LogbookCatalog.SortMode.RARITY: return ( "rare–common" if descending else "common–rare" ) LogbookCatalog.SortMode.VALUE, LogbookCatalog.SortMode.WEIGHT: return "high–low" if descending else "low–high" _: return "most first" if descending else "least first" return "any" func _index_option_count(key: StringName) -> int: match key: &"show": return INDEX_SHOW_LABELS.size() &"sort": return INDEX_SORT_LABELS.size() &"availability": return INDEX_AVAILABILITY_LABELS.size() &"season": return INDEX_SEASON_LABELS.size() &"time": return INDEX_TIME_LABELS.size() &"rarity": return INDEX_RARITY_LABELS.size() &"method": return INDEX_METHOD_LABELS.size() &"holding": return INDEX_HOLDING_LABELS.size() return 0 func _index_has_active_criteria() -> bool: return ( not str(_index_options["search"]).is_empty() or int(_index_options["show"]) != LogbookCatalog.DiscoveryFilter.ALL or int(_index_options["sort"]) != LogbookCatalog.SortMode.CATALOG_NUMBER or bool(_index_options["descending"]) or int(_index_options["availability"]) != LogbookCatalog.AvailabilityFilter.ANY or int(_index_options["season"]) != LogbookCatalog.SeasonFilter.ANY or int(_index_options["time"]) != LogbookCatalog.TimeFilter.ANY or int(_index_options["rarity"]) != LogbookCatalog.RarityFilter.ANY or int(_index_options["method"]) != LogbookCatalog.MethodFilter.ANY or int(_index_options["holding"]) != LogbookCatalog.HoldingFilter.ANY or not str(_index_options["group"]).is_empty() ) func _current_season() -> int: return ( _world_time.get_season() if _world_time != null else CalendarSeason.UNKNOWN ) func _current_is_night() -> bool: return _world_time != null and _world_time.is_night_period() func notify_availability_context_changed() -> void: _on_logbook_context_changed() func _on_logbook_context_changed( _first: Variant = null, _second: Variant = null, ) -> void: if ( int(_index_options["availability"]) == LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW or int(_index_options["season"]) == LogbookCatalog.SeasonFilter.CURRENT or int(_index_options["time"]) == LogbookCatalog.TimeFilter.CURRENT ): _refresh_catalog() func _make_entry(fish: FishDataType, discovered: bool) -> Button: var entry := Button.new() entry.custom_minimum_size = CATALOG_ENTRY_SIZE entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER entry.toggle_mode = true entry.add_theme_stylebox_override("normal", _entry_style(false)) entry.add_theme_stylebox_override("hover", _entry_style(true)) entry.add_theme_stylebox_override("hover_pressed", _entry_style(true)) entry.add_theme_stylebox_override("focus", _entry_style(true)) entry.add_theme_stylebox_override("pressed", _entry_style(true)) entry.text = "" if not discovered: entry.tooltip_text = "" entry.accessibility_name = "Unknown catalog entry" _add_entry_content(entry, fish.display_texture, "???", true) else: entry.tooltip_text = fish.display_name entry.accessibility_name = fish.display_name _add_entry_content( entry, fish.display_texture, fish.display_name, false ) return entry func _add_entry_content( entry: Button, portrait: Texture2D, entry_name: String, unknown: bool, ) -> void: var content_margin := MarginContainer.new() content_margin.mouse_filter = Control.MOUSE_FILTER_IGNORE content_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) content_margin.add_theme_constant_override("margin_left", 5) content_margin.add_theme_constant_override("margin_top", 8) content_margin.add_theme_constant_override("margin_right", 5) content_margin.add_theme_constant_override("margin_bottom", 2) entry.add_child(content_margin) var content := VBoxContainer.new() content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.alignment = BoxContainer.ALIGNMENT_CENTER content.add_theme_constant_override("separation", 4) content_margin.add_child(content) var portrait_frame := CenterContainer.new() portrait_frame.custom_minimum_size = CATALOG_PORTRAIT_SIZE portrait_frame.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_child(portrait_frame) var portrait_view := LogbookPortraitType.new() portrait_view.configure_uniform_footprint( portrait, CATALOG_PORTRAIT_SIZE, _silhouette_material if unknown else null, ) portrait_frame.add_child(portrait_view) var name_label := _label(_entry_label_text(entry_name), 16) name_label.custom_minimum_size.y = 38.0 name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART content.add_child(name_label) func _select_category(category: LogbookCatalog.Category) -> void: if category == _category: if _index_open: _set_index_open(false) return _category = category _set_index_open(false, false) _selected_id = StringName() _selected_entry_key = StringName() for index: int in _category_tabs.size(): (_category_tabs[index] as OrganizerTabType).set_selected( _category_tab_categories[index] == _category ) _begin_category_transition() func _configure_category_focus() -> void: for index: int in _category_tabs.size(): var tab: Button = _category_tabs[index] tab.focus_neighbor_left = tab.get_path_to( _category_tabs[maxi(index - 1, 0)] ) tab.focus_neighbor_right = tab.get_path_to( _category_tabs[mini(index + 1, _category_tabs.size() - 1)] ) if not _category_tabs.is_empty() and _index_tab != null: var final_tab: Button = _category_tabs.back() final_tab.focus_neighbor_right = final_tab.get_path_to(_index_tab) _index_tab.focus_neighbor_left = _index_tab.get_path_to(final_tab) _index_tab.focus_neighbor_right = _index_tab.get_path_to(_index_tab) func _animate_tab_entry() -> void: for index: int in _category_tabs.size(): (_category_tabs[index] as OrganizerTabType).animate_entrance( float(index) * 0.025 ) if _index_tab != null: _index_tab.animate_entrance(float(_category_tabs.size()) * 0.025) func _settle_tabs_for_close() -> void: for tab: OrganizerTabType in _category_tabs: tab.settle_for_close() if _index_tab != null: _index_tab.settle_for_close() func _begin_category_transition() -> void: _cancel_category_tween() _category_generation += 1 var generation: int = _category_generation var restore_interactive: bool = _interactive set_interactive(false) _category_tween = create_tween() _category_tween.tween_method( _set_category_content_alpha, 1.0, 0.0, CATEGORY_FADE_DURATION * 0.5, ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) _category_tween.tween_callback( func() -> void: if generation != _category_generation: return _refresh_catalog() _set_category_content_alpha(0.0) ) _category_tween.tween_method( _set_category_content_alpha, 0.0, 1.0, CATEGORY_FADE_DURATION * 0.5, ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) _category_tween.finished.connect( func() -> void: if generation != _category_generation: return _category_tween = null _set_category_content_alpha(1.0) set_interactive(restore_interactive) focus_initial(), CONNECT_ONE_SHOT, ) func _cancel_category_tween() -> void: _category_generation += 1 if _category_tween != null: _category_tween.kill() _category_tween = null _set_category_content_alpha(1.0) func _set_category_content_alpha(alpha: float) -> void: if _catalog_scroll != null: _catalog_scroll.modulate.a = alpha if _empty_state != null: _empty_state.modulate.a = alpha if _detail_body != null: _detail_body.modulate.a = alpha if _index_body != null: _index_body.modulate.a = alpha func _select_entry(entry_key: StringName, fish_id: StringName) -> void: _set_index_open(false, false) if fish_id.is_empty(): _selected_id = StringName() _selected_entry_key = entry_key _refresh_selection_styles() _show_unknown() return if ( _collection_log == null or not _collection_log.has_discovered(fish_id) ): return _selected_id = fish_id _selected_entry_key = entry_key _refresh_selection_styles() _refresh_details(true) func _refresh_details(animate: bool) -> void: if _selected_id.is_empty(): _show_no_selection() return if ( _collection_log == null or not _collection_log.has_discovered(_selected_id) ): _show_unknown() return var fish: FishDataType = ( _catalog.get_fish_by_id(_selected_id) if _catalog != null else null ) if fish == null: _show_no_selection() return var rebuild: Callable = _build_known_details.bind(fish) if animate: _crossfade_details(rebuild) else: rebuild.call() func _build_known_details(fish: FishDataType) -> void: _clear_details() var catalog_number: int = LogbookCatalog.catalog_number(fish) var summary_margin := MarginContainer.new() summary_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL _set_margins(summary_margin, 40, 0, 40, 0) _detail_body.add_child(summary_margin) var summary_columns := HBoxContainer.new() summary_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL summary_columns.add_theme_constant_override("separation", 24) summary_margin.add_child(summary_columns) var portrait_column := VBoxContainer.new() portrait_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL portrait_column.size_flags_stretch_ratio = 1.0 portrait_column.add_theme_constant_override("separation", 4) summary_columns.add_child(portrait_column) var title := _label(fish.display_name, 26) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER portrait_column.add_child(title) if fish.display_texture != null: _detail_portrait_button = Button.new() _detail_portrait_button.custom_minimum_size = DETAIL_PORTRAIT_SIZE _detail_portrait_button.size_flags_horizontal = ( Control.SIZE_SHRINK_CENTER ) _detail_portrait_button.clip_contents = true _detail_portrait_button.tooltip_text = "View larger artwork" _detail_portrait_button.accessibility_name = ( "View larger artwork for %s" % fish.display_name ) _detail_portrait_button.add_theme_stylebox_override( "normal", _portrait_button_style(false) ) _detail_portrait_button.add_theme_stylebox_override( "hover", _portrait_button_style(true) ) _detail_portrait_button.add_theme_stylebox_override( "focus", _portrait_button_style(true) ) _detail_portrait_button.add_theme_stylebox_override( "pressed", _portrait_button_style(true) ) _detail_portrait_button.pressed.connect( _show_portrait_overlay.bind(fish.display_texture) ) _detail_buttons.append(_detail_portrait_button) portrait_column.add_child(_detail_portrait_button) var artwork_center := CenterContainer.new() artwork_center.mouse_filter = Control.MOUSE_FILTER_IGNORE artwork_center.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) _detail_portrait_button.add_child(artwork_center) var artwork := LogbookPortraitType.new() artwork.configure( fish.display_texture, DETAIL_PORTRAIT_SIZE, ) artwork_center.add_child(artwork) var facts_text: String = LogbookCatalog.facts_for(fish) var facts_section_title: String = ( "shellfish facts" if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH else "fish facts" ) var facts_button := _make_detail_section_button( facts_section_title, facts_text, ) facts_button.custom_minimum_size = Vector2(190.0, 132.0) facts_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL facts_button.size_flags_stretch_ratio = 1.0 summary_columns.add_child(facts_button) var facts_column := VBoxContainer.new() facts_column.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) facts_column.mouse_filter = Control.MOUSE_FILTER_IGNORE facts_column.add_theme_constant_override("separation", 6) facts_button.add_child(facts_column) var facts_heading := _field_label( facts_section_title, 16, ) facts_column.add_child(facts_heading) var facts := _label(facts_text, 16) facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL facts_column.add_child(facts) var quality_button := _make_detail_section_button( "quality collection", _quality_overlay_text(fish.id), ) quality_button.custom_minimum_size = DETAIL_QUALITY_SECTION_SIZE quality_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _detail_body.add_child(quality_button) var quality_progress := _build_quality_progress(fish.id) quality_progress.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) quality_progress.mouse_filter = Control.MOUSE_FILTER_IGNORE quality_button.add_child(quality_progress) var stats_button := _make_detail_section_button( "fish stats", _stats_overlay_text(fish, catalog_number), ) stats_button.custom_minimum_size = DETAIL_STATS_SECTION_SIZE stats_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _detail_body.add_child(stats_button) var stats_anchor := VBoxContainer.new() stats_anchor.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) stats_anchor.mouse_filter = Control.MOUSE_FILTER_IGNORE stats_anchor.size_flags_horizontal = Control.SIZE_EXPAND_FILL stats_anchor.size_flags_vertical = Control.SIZE_EXPAND_FILL stats_anchor.alignment = BoxContainer.ALIGNMENT_BEGIN stats_button.add_child(stats_anchor) var stats_columns := HBoxContainer.new() stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL stats_columns.add_theme_constant_override("separation", 28) stats_anchor.add_child(stats_columns) var left_stats := _make_stats_column() var right_stats := _make_stats_column() stats_columns.add_child(left_stats) stats_columns.add_child(right_stats) _add_detail_row( left_stats, "catalog number", "#%03d" % catalog_number if catalog_number > 0 else "unknown", ) _add_detail_row( left_stats, ( "habitat" if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH else "body of water" ), fish.get_habitat_label(), ) _add_detail_row( left_stats, "weight range", "%.2f–%.2f lb" % [ fish.get_minimum_weight(), fish.get_maximum_weight(), ], ) _add_detail_row( left_stats, "number caught", str(_collection_log.get_catch_count(fish.id)), ) _add_detail_row(right_stats, "rarity", fish.get_rarity_name().to_lower()) _add_detail_row(right_stats, "time of day", _availability_text(fish)) _add_detail_row(right_stats, "seasons", fish.get_season_text()) _add_currency_detail_row( right_stats, "value range", FishQualityType.apply_sale_value( fish.sell_value_min, FishQualityType.Tier.BORING, ), FishQualityType.apply_sale_value( fish.sell_value_max, FishQualityType.Tier.SHINY, ), ) _configure_detail_focus( _detail_portrait_button, facts_button, quality_button, stats_button, ) set_interactive(_interactive) func _configure_detail_focus( portrait: Button, facts: Button, quality: Button, stats: Button, ) -> void: var top_left: Button = portrait if portrait != null else facts if portrait != null: portrait.focus_neighbor_left = portrait.get_path_to(portrait) portrait.focus_neighbor_right = portrait.get_path_to(facts) portrait.focus_neighbor_top = portrait.get_path_to(portrait) portrait.focus_neighbor_bottom = portrait.get_path_to(quality) portrait.focus_previous = portrait.get_path_to(stats) portrait.focus_next = portrait.get_path_to(facts) facts.focus_neighbor_left = facts.get_path_to(top_left) facts.focus_neighbor_right = facts.get_path_to(facts) facts.focus_neighbor_top = facts.get_path_to(facts) facts.focus_neighbor_bottom = facts.get_path_to(quality) facts.focus_previous = facts.get_path_to(top_left) facts.focus_next = facts.get_path_to(quality) quality.focus_neighbor_left = quality.get_path_to(quality) quality.focus_neighbor_right = quality.get_path_to(quality) quality.focus_neighbor_top = quality.get_path_to(facts) quality.focus_neighbor_bottom = quality.get_path_to(stats) quality.focus_previous = quality.get_path_to(facts) quality.focus_next = quality.get_path_to(stats) stats.focus_neighbor_left = stats.get_path_to(stats) stats.focus_neighbor_right = stats.get_path_to(stats) stats.focus_neighbor_top = stats.get_path_to(quality) stats.focus_neighbor_bottom = stats.get_path_to(stats) stats.focus_previous = stats.get_path_to(quality) stats.focus_next = stats.get_path_to(top_left) func _build_quality_progress(fish_id: StringName) -> VBoxContainer: var quality_section := VBoxContainer.new() quality_section.size_flags_horizontal = Control.SIZE_EXPAND_FILL quality_section.add_theme_constant_override("separation", 6) var discovered_count: int = 0 for quality: int in FishQualityType.TIER_COUNT: if _collection_log.has_discovered_quality(fish_id, quality): discovered_count += 1 var heading := _field_label( "quality collection • %d / %d" % [ discovered_count, FishQualityType.TIER_COUNT, ], 17, ) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER heading.add_theme_color_override("font_color", MUTED_INK) quality_section.add_child(heading) var tiers := VBoxContainer.new() tiers.add_theme_constant_override("separation", 6) quality_section.add_child(tiers) for quality: int in FishQualityType.TIER_COUNT: var row: HBoxContainer if quality % 2 == 0: row = HBoxContainer.new() row.alignment = BoxContainer.ALIGNMENT_CENTER row.add_theme_constant_override("separation", 28) tiers.add_child(row) else: row = tiers.get_child(tiers.get_child_count() - 1) as HBoxContainer var discovered: bool = _collection_log.has_discovered_quality( fish_id, quality, ) var tier := HBoxContainer.new() tier.alignment = BoxContainer.ALIGNMENT_CENTER tier.add_theme_constant_override("separation", 5) var quality_color: Color = UIPalette.get_quality_color(quality) var dot := _label("●" if discovered else "○", 17) dot.add_theme_color_override("font_color", quality_color) dot.modulate.a = 1.0 if discovered else 0.48 tier.add_child(dot) var tier_label := _label(FishQualityType.display_name(quality), 16) tier_label.add_theme_color_override( "font_color", INK if discovered else MUTED_INK, ) tier_label.modulate.a = 1.0 if discovered else 0.58 tier.tooltip_text = ( "%s quality collected" if discovered else "%s quality not yet collected" ) % FishQualityType.display_name(quality) tier.add_child(tier_label) row.add_child(tier) return quality_section func _make_detail_section_button( section_title: String, section_text: String, ) -> Button: var button := Button.new() button.text = "" button.tooltip_text = "View %s larger" % section_title button.accessibility_name = "View %s larger" % section_title button.add_theme_stylebox_override("normal", _portrait_button_style(false)) button.add_theme_stylebox_override("hover", _portrait_button_style(true)) button.add_theme_stylebox_override("focus", _portrait_button_style(true)) button.add_theme_stylebox_override("pressed", _portrait_button_style(true)) button.pressed.connect( _show_detail_text_overlay.bind(section_title, section_text, button) ) _detail_buttons.append(button) return button func _quality_overlay_text(fish_id: StringName) -> String: var lines: Array[String] = [] for quality: int in FishQualityType.TIER_COUNT: var collected: bool = _collection_log.has_discovered_quality( fish_id, quality, ) lines.append("%s %s" % [ "collected" if collected else "not collected", FishQualityType.display_name(quality), ]) return "\n".join(lines) func _stats_overlay_text(fish: FishDataType, catalog_number: int) -> String: var habitat_label: String = ( "habitat" if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH else "body of water" ) return "\n".join([ "catalog number: %s" % ( "#%03d" % catalog_number if catalog_number > 0 else "unknown" ), "%s: %s" % [habitat_label, fish.get_habitat_label()], "weight range: %.2f–%.2f lb" % [ fish.get_minimum_weight(), fish.get_maximum_weight(), ], "number caught: %d" % _collection_log.get_catch_count(fish.id), "rarity: %s" % fish.get_rarity_name().to_lower(), "time of day: %s" % _availability_text(fish), "seasons: %s" % fish.get_season_text(), "value range: %d–%d" % [ FishQualityType.apply_sale_value( fish.sell_value_min, FishQualityType.Tier.BORING, ), FishQualityType.apply_sale_value( fish.sell_value_max, FishQualityType.Tier.SHINY, ), ], ]) func _show_no_selection() -> void: _clear_details() var instruction := _label("Select an entry to read its catch record.", 21) instruction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART instruction.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER instruction.vertical_alignment = VERTICAL_ALIGNMENT_CENTER instruction.size_flags_vertical = Control.SIZE_EXPAND_FILL _detail_body.add_child(instruction) func _show_unknown() -> void: _clear_details() var unknown := _label("???", 34) unknown.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _detail_body.add_child(unknown) var instruction := _label("Catch this creature to learn more.", 20) instruction.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART instruction.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _detail_body.add_child(instruction) func _add_detail_row( parent: VBoxContainer, row_name: String, value: String, ) -> void: var row := VBoxContainer.new() row.add_theme_constant_override("separation", 2) var heading := _field_label(row_name, 14) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER heading.add_theme_color_override("font_color", MUTED_INK) var content := _handwritten_value_label(value.to_lower(), 16) content.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER content.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART row.add_child(heading) row.add_child(content) parent.add_child(row) func _add_currency_detail_row( parent: VBoxContainer, row_name: String, minimum_value: int, maximum_value: int, ) -> void: var row := VBoxContainer.new() row.add_theme_constant_override("separation", 2) var heading := _field_label(row_name, 14) heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER heading.add_theme_color_override("font_color", MUTED_INK) row.add_child(heading) var value: CurrencyAmount = ( CurrencyPresentationType.instantiate_amount(minimum_value, 16.0) ) value.set_amount_text("%d–%d" % [minimum_value, maximum_value]) value.get_amount_label().add_theme_font_override( "font", InventoryNotepadType.HANDWRITTEN_FONT ) value.get_amount_label().add_theme_font_size_override("font_size", 16) value.get_amount_label().add_theme_color_override("font_color", INK) row.add_child(value) parent.add_child(row) func _make_stats_column() -> VBoxContainer: var column := VBoxContainer.new() column.size_flags_horizontal = Control.SIZE_EXPAND_FILL column.size_flags_stretch_ratio = 1.0 column.add_theme_constant_override( "separation", DETAIL_STATS_ROW_SEPARATION ) return column func _availability_text(fish: FishDataType) -> String: if fish.availability == null: return "unknown" if fish.availability.allow_day and fish.availability.allow_night: return "day and night" if fish.availability.allow_day: return "day" if fish.availability.allow_night: return "night" return "unknown" func _crossfade_details(rebuild: Callable) -> void: _cancel_detail_tween() _detail_generation += 1 var generation: int = _detail_generation _detail_tween = create_tween() _detail_tween.tween_property( _detail_body, "modulate:a", 0.0, DETAIL_FADE_DURATION * 0.5 ) _detail_tween.tween_callback( func() -> void: if generation == _detail_generation: rebuild.call() ) _detail_tween.tween_property( _detail_body, "modulate:a", 1.0, DETAIL_FADE_DURATION * 0.5 ) _detail_tween.finished.connect( func() -> void: if generation == _detail_generation: _detail_tween = null, CONNECT_ONE_SHOT, ) func _cancel_detail_tween() -> void: _detail_generation += 1 if _detail_tween != null: _detail_tween.kill() _detail_tween = null if _detail_body != null: _detail_body.modulate.a = 1.0 func _clear_entries() -> void: for child: Node in _catalog_grid.get_children(): _catalog_grid.remove_child(child) child.queue_free() _entry_buttons.clear() func _clear_details() -> void: _detail_portrait_button = null _detail_buttons.clear() for child: Node in _detail_body.get_children(): _detail_body.remove_child(child) child.queue_free() func _refresh_selection_styles() -> void: for fish_id: StringName in _entry_buttons: _entry_buttons[fish_id].button_pressed = ( fish_id == _selected_entry_key ) func _on_fish_discovered(fish_id: StringName) -> void: if _catalog == null: return var fish: FishDataType = _catalog.get_fish_by_id(fish_id) if fish == null: return if _selected_entry_key == _unknown_selection_key(fish): _selected_id = fish_id _selected_entry_key = fish_id _refresh_catalog() func _on_collection_changed() -> void: _refresh_catalog() func _on_inventory_changed() -> void: # Ownership filters and favorite-backed views depend on the live cooler, # while rebuilding the catalog also keeps the selected detail current. _refresh_catalog() func _unknown_selection_key(fish: FishDataType) -> StringName: var number: int = LogbookCatalog.catalog_number(fish) return StringName("unknown_%d" % number) func _label(text: String, font_size: int) -> Label: var label := Label.new() label.text = text label.add_theme_font_override( "font", InventoryNotepadType.HANDWRITTEN_FONT ) label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", INK) return label func _field_label(text: String, font_size: int) -> Label: var label := Label.new() label.text = text label.add_theme_font_override("font", UtilityPageStyle.TuffyFont) label.add_theme_font_size_override("font_size", font_size) label.add_theme_color_override("font_color", INK) return label func _handwritten_value_label(text: String, font_size: int) -> Label: var adjusted_size: int = font_size if _contains_ascii_digit(text): adjusted_size = roundi( float(font_size) * HANDWRITTEN_NUMERIC_SCALE ) return _label(text, adjusted_size) func _contains_ascii_digit(text: String) -> bool: for codepoint: int in text.to_utf32_buffer(): if codepoint >= 48 and codepoint <= 57: return true return false func _entry_label_text(entry_name: String) -> String: var split_index: int = entry_name.find(" ") if split_index < 0: return entry_name return ( entry_name.substr(0, split_index) + "\n" + entry_name.substr(split_index + 1) ) func _build_portrait_overlay() -> void: _portrait_overlay = Control.new() _portrait_overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _portrait_overlay.z_index = 500 _portrait_overlay.visible = false _portrait_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(_portrait_overlay) _portrait_overlay_backdrop = Button.new() _portrait_overlay_backdrop.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) _portrait_overlay_backdrop.text = "" _portrait_overlay_backdrop.tooltip_text = "Return to logbook" _portrait_overlay_backdrop.accessibility_name = "Return to logbook" _portrait_overlay_backdrop.focus_neighbor_top = NodePath(".") _portrait_overlay_backdrop.focus_neighbor_bottom = NodePath(".") _portrait_overlay_backdrop.focus_neighbor_left = NodePath(".") _portrait_overlay_backdrop.focus_neighbor_right = NodePath(".") _portrait_overlay_backdrop.focus_next = NodePath(".") _portrait_overlay_backdrop.focus_previous = NodePath(".") var backdrop_style := _portrait_overlay_backdrop_style() for state: StringName in [ &"normal", &"hover", &"focus", &"pressed", &"disabled", ]: _portrait_overlay_backdrop.add_theme_stylebox_override( state, backdrop_style ) _portrait_overlay_backdrop.pressed.connect(_hide_portrait_overlay) _portrait_overlay_backdrop.gui_input.connect( _on_portrait_overlay_backdrop_input ) _portrait_overlay.add_child(_portrait_overlay_backdrop) var overlay_center := CenterContainer.new() overlay_center.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay_center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _portrait_overlay.add_child(overlay_center) var card := PanelContainer.new() card.mouse_filter = Control.MOUSE_FILTER_STOP card.custom_minimum_size = DETAIL_OVERLAY_CARD_SIZE card.size_flags_horizontal = Control.SIZE_SHRINK_CENTER card.size_flags_vertical = Control.SIZE_SHRINK_CENTER card.add_theme_stylebox_override("panel", _portrait_view_style()) overlay_center.add_child(card) var margin := MarginContainer.new() _set_margins( margin, DETAIL_OVERLAY_CONTENT_MARGIN, DETAIL_OVERLAY_CONTENT_MARGIN, DETAIL_OVERLAY_CONTENT_MARGIN, DETAIL_OVERLAY_CONTENT_MARGIN, ) card.add_child(margin) var overlay_stack := VBoxContainer.new() overlay_stack.add_theme_constant_override("separation", 20) margin.add_child(overlay_stack) _portrait_overlay_title = _field_label( "", DETAIL_OVERLAY_TITLE_FONT_SIZE ) _portrait_overlay_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _portrait_overlay_title.add_theme_color_override( "font_color", UIPalette.TEXT ) overlay_stack.add_child(_portrait_overlay_title) var content_panel := PanelContainer.new() content_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL content_panel.add_theme_stylebox_override( "panel", _portrait_overlay_content_style() ) overlay_stack.add_child(content_panel) var content_margin := MarginContainer.new() _set_margins(content_margin, 28, 24, 28, 24) content_panel.add_child(content_margin) var content_holder := Control.new() content_holder.size_flags_horizontal = Control.SIZE_EXPAND_FILL content_holder.size_flags_vertical = Control.SIZE_EXPAND_FILL content_margin.add_child(content_holder) _portrait_overlay_artwork_view = CenterContainer.new() _portrait_overlay_artwork_view.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) content_holder.add_child(_portrait_overlay_artwork_view) _portrait_overlay_artwork = LogbookPortraitType.new() _portrait_overlay_artwork.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _portrait_overlay_artwork_view.add_child(_portrait_overlay_artwork) _portrait_overlay_text = _field_label("", DETAIL_OVERLAY_TEXT_FONT_SIZE) _portrait_overlay_text.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) _portrait_overlay_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT _portrait_overlay_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _portrait_overlay_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _portrait_overlay_text.add_theme_color_override( "font_color", UIPalette.TEXT ) content_holder.add_child(_portrait_overlay_text) _portrait_overlay_quality_view = CenterContainer.new() _portrait_overlay_quality_view.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) content_holder.add_child(_portrait_overlay_quality_view) _portrait_overlay_quality = VBoxContainer.new() _portrait_overlay_quality.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _portrait_overlay_quality.size_flags_vertical = Control.SIZE_SHRINK_CENTER _portrait_overlay_quality.add_theme_constant_override("separation", 22) _portrait_overlay_quality_view.add_child(_portrait_overlay_quality) _portrait_overlay_stats_view = CenterContainer.new() _portrait_overlay_stats_view.set_anchors_and_offsets_preset( Control.PRESET_FULL_RECT ) content_holder.add_child(_portrait_overlay_stats_view) _portrait_overlay_stats = GridContainer.new() _portrait_overlay_stats.columns = 2 _portrait_overlay_stats.size_flags_horizontal = Control.SIZE_SHRINK_CENTER _portrait_overlay_stats.size_flags_vertical = Control.SIZE_SHRINK_CENTER _portrait_overlay_stats.add_theme_constant_override("h_separation", 48) _portrait_overlay_stats.add_theme_constant_override("v_separation", 12) _portrait_overlay_stats_view.add_child(_portrait_overlay_stats) _set_detail_overlay_view(&"text") func _show_portrait_overlay(portrait_texture: Texture2D) -> void: if portrait_texture == null or _portrait_overlay == null: return _portrait_overlay_artwork.configure_fitted( portrait_texture, PORTRAIT_VIEW_MAX_SIZE, ) _portrait_overlay_title.text = "artwork" _set_detail_overlay_view(&"artwork") _overlay_return_focus = _detail_portrait_button _controller_zone = ControllerZone.OVERLAY set_interactive(_interactive) _portrait_overlay.visible = true _portrait_overlay.mouse_filter = Control.MOUSE_FILTER_STOP _portrait_overlay_backdrop.grab_focus() func _show_detail_text_overlay( section_title: String, section_text: String, return_focus: Control, ) -> void: if _portrait_overlay == null: return _portrait_overlay_title.text = section_title match section_title: "quality collection": _populate_quality_overlay(_selected_id) _set_detail_overlay_view(&"quality") "fish stats": _populate_stats_overlay(section_text) _set_detail_overlay_view(&"stats") _: _portrait_overlay_text.text = section_text _set_detail_overlay_view(&"text") _overlay_return_focus = return_focus _controller_zone = ControllerZone.OVERLAY set_interactive(_interactive) _portrait_overlay.visible = true _portrait_overlay.mouse_filter = Control.MOUSE_FILTER_STOP _portrait_overlay_backdrop.grab_focus() func _set_detail_overlay_view(view: StringName) -> void: _portrait_overlay_artwork_view.visible = view == &"artwork" _portrait_overlay_text.visible = view == &"text" _portrait_overlay_quality_view.visible = view == &"quality" _portrait_overlay_stats_view.visible = view == &"stats" func _populate_quality_overlay(fish_id: StringName) -> void: _clear_overlay_container(_portrait_overlay_quality) var discovered_count: int = 0 for quality: int in FishQualityType.TIER_COUNT: if _collection_log.has_discovered_quality(fish_id, quality): discovered_count += 1 var summary := _field_label( "%d / %d collected" % [ discovered_count, FishQualityType.TIER_COUNT, ], DETAIL_OVERLAY_VALUE_FONT_SIZE, ) summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER summary.add_theme_color_override("font_color", UIPalette.MUTED_TEXT) _portrait_overlay_quality.add_child(summary) var quality_grid := GridContainer.new() quality_grid.columns = 2 quality_grid.add_theme_constant_override("h_separation", 44) quality_grid.add_theme_constant_override("v_separation", 20) _portrait_overlay_quality.add_child(quality_grid) for quality: int in FishQualityType.TIER_COUNT: var collected: bool = _collection_log.has_discovered_quality( fish_id, quality, ) var tier := HBoxContainer.new() tier.custom_minimum_size.x = 250.0 tier.add_theme_constant_override("separation", 12) quality_grid.add_child(tier) var quality_color: Color = UIPalette.get_quality_color(quality) var dot := _field_label("●" if collected else "○", 34) dot.add_theme_color_override("font_color", quality_color) dot.modulate.a = 1.0 if collected else 0.55 tier.add_child(dot) var tier_label := _field_label( FishQualityType.display_name(quality), DETAIL_OVERLAY_VALUE_FONT_SIZE, ) tier_label.add_theme_color_override( "font_color", UIPalette.TEXT if collected else UIPalette.MUTED_TEXT, ) tier.add_child(tier_label) func _populate_stats_overlay(section_text: String) -> void: _clear_overlay_container(_portrait_overlay_stats) for line: String in section_text.split("\n", false): var separator_index: int = line.find(":") if separator_index < 0: continue var field_name: String = line.left(separator_index).strip_edges() var field_value: String = line.substr(separator_index + 1).strip_edges() var field_label := _field_label( field_name, DETAIL_OVERLAY_FIELD_FONT_SIZE, ) field_label.custom_minimum_size.x = 210.0 field_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT field_label.add_theme_color_override( "font_color", UIPalette.MUTED_TEXT ) _portrait_overlay_stats.add_child(field_label) var value_label := _field_label( field_value, DETAIL_OVERLAY_VALUE_FONT_SIZE, ) value_label.custom_minimum_size.x = 280.0 value_label.add_theme_color_override("font_color", UIPalette.TEXT) _portrait_overlay_stats.add_child(value_label) func _clear_overlay_container(container: Container) -> void: for child: Node in container.get_children(): container.remove_child(child) child.queue_free() func _hide_portrait_overlay(restore_focus: bool = true) -> void: if _portrait_overlay == null or not _portrait_overlay.visible: return _portrait_overlay.visible = false _portrait_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE _controller_zone = ControllerZone.DETAILS set_interactive(_interactive) if ( restore_focus and _overlay_return_focus != null and is_instance_valid(_overlay_return_focus) and _active and _interactive ): _overlay_return_focus.grab_focus() func _on_portrait_overlay_backdrop_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): _hide_portrait_overlay() get_viewport().set_input_as_handled() func _portrait_button_style(highlighted: bool) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = ( Color(1.0, 0.96, 0.86, 0.48) if highlighted else Color.TRANSPARENT ) style.set_border_width_all(0) style.set_corner_radius_all(12) return style func _portrait_view_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = UIPalette.PANEL style.border_color = UIPalette.PRIMARY style.set_border_width_all(4) style.set_corner_radius_all(24) style.shadow_color = Color(0.0, 0.0, 0.0, 0.42) style.shadow_size = 12 return style func _portrait_overlay_backdrop_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color(0.01, 0.045, 0.065, 0.82) style.set_border_width_all(0) return style func _portrait_overlay_content_style() -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = UIPalette.ELEVATED_PANEL style.set_border_width_all(0) style.set_corner_radius_all(16) return style func _entry_style(highlighted: bool) -> StyleBoxFlat: var style := StyleBoxFlat.new() style.bg_color = Color("fff5dc") if highlighted else Color.TRANSPARENT style.set_border_width_all(0) style.set_corner_radius_all(51) style.content_margin_left = 5 style.content_margin_top = 5 style.content_margin_right = 5 style.content_margin_bottom = 5 return style func _set_margins( container: MarginContainer, left: int, top: int, right: int, bottom: int, ) -> void: container.add_theme_constant_override("margin_left", left) container.add_theme_constant_override("margin_top", top) container.add_theme_constant_override("margin_right", right) container.add_theme_constant_override("margin_bottom", bottom)