netfishing/ui/logbook_page.gd

1150 lines
36 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 SILHOUETTE_SHADER: Shader = preload(
"res://ui/logbook_silhouette.gdshader"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
const LogbookPortraitType = preload(
"res://ui/components/logbook_portrait.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(860.0, 480.0)
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 = 122.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)
const CATALOG_ROW_STEP: float = 98.0
const CATALOG_SNAP_DELAY: float = 0.12
const DETAIL_PORTRAIT_SIZE := Vector2(160.0, 88.0)
const DETAIL_BOTTOM_INSET: float = 35.0
var _collection_log: CollectionLogType
var _inventory: FishInventoryType
var _catalog: FishPoolType
var _category: WaterType.Type = WaterType.Type.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 _snapping_catalog_scroll: bool = false
var _catalog_scroll_snap_timer: Timer
var _category_tabs: Array[Button] = []
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 _portrait_overlay: Control
var _portrait_overlay_backdrop: Button
var _portrait_overlay_artwork: LogbookPortraitType
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,
) -> void:
_collection_log = collection_log
_inventory = inventory
_catalog = catalog
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)
_refresh_catalog()
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 else Control.FOCUS_NONE
)
tab.mouse_filter = (
Control.MOUSE_FILTER_STOP
if _interactive and not tab.button_pressed
else Control.MOUSE_FILTER_IGNORE
)
for entry: Button in _entry_buttons.values():
entry.focus_mode = (
Control.FOCUS_ALL if _interactive else Control.FOCUS_NONE
)
entry.mouse_filter = (
Control.MOUSE_FILTER_STOP
if _interactive
else Control.MOUSE_FILTER_IGNORE
)
func focus_initial() -> void:
if not _active or not _interactive:
return
if _portrait_overlay != null and _portrait_overlay.visible:
_portrait_overlay_backdrop.grab_focus()
return
var selected := _entry_buttons.get(_selected_id) 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()
else:
_category_tabs[int(_category)].grab_focus()
func _build_interface() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var outer := MarginContainer.new()
outer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
outer.add_theme_constant_override("margin_left", 52)
outer.add_theme_constant_override("margin_top", 85)
outer.add_theme_constant_override("margin_right", 52)
outer.add_theme_constant_override("margin_bottom", 18)
add_child(outer)
var stack := Control.new()
stack.mouse_filter = Control.MOUSE_FILTER_IGNORE
outer.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)
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: WaterType.Type in [
WaterType.Type.FRESH_WATER,
WaterType.Type.SALT_WATER,
WaterType.Type.OTHER,
]:
var tab := OrganizerTabType.new()
tab.custom_minimum_size = Vector2(124, 38)
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)
_configure_category_focus()
var book := Control.new()
book.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
book.offset_top = 50.0
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.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", 16)
right_page.add_child(_detail_body)
_apply_page_content_scale(_detail_body)
_show_no_selection()
_build_portrait_overlay()
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:
_catalog_scroll_snap_timer = Timer.new()
_catalog_scroll_snap_timer.one_shot = true
_catalog_scroll_snap_timer.wait_time = CATALOG_SNAP_DELAY
_catalog_scroll_snap_timer.timeout.connect(_snap_catalog_scroll_to_row)
_catalog_scroll.add_child(_catalog_scroll_snap_timer)
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:
if _snapping_catalog_scroll:
return
_catalog_scroll_snap_timer.start()
_refresh_catalog_scroll_indicators()
func _snap_catalog_scroll_to_row() -> void:
var scroll_bar: VScrollBar = _catalog_scroll.get_v_scroll_bar()
var value: float = scroll_bar.value
var maximum_scroll: float = scroll_bar.max_value - scroll_bar.page
var snapped_value: float = clampf(
roundf(value / CATALOG_ROW_STEP) * CATALOG_ROW_STEP,
scroll_bar.min_value,
maximum_scroll,
)
if not is_equal_approx(value, snapped_value):
_snapping_catalog_scroll = true
_catalog_scroll.scroll_vertical = roundi(snapped_value)
_snapping_catalog_scroll = false
_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] = []
if _catalog != null:
species = LogbookCatalog.ordered_species(_catalog.candidates)
for fish: FishDataType in species:
if LogbookCatalog.category_for(fish) != _category:
continue
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 = 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)
set_interactive(_interactive)
_refresh_catalog_scroll_indicators.call_deferred()
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_view := LogbookPortraitType.new()
portrait_view.configure(
portrait,
CATALOG_PORTRAIT_SIZE,
_silhouette_material if unknown else null,
)
content.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: WaterType.Type) -> void:
if category == _category:
return
_category = category
_selected_id = StringName()
_selected_entry_key = StringName()
for index: int in _category_tabs.size():
(_category_tabs[index] as OrganizerTabType).set_selected(
index == int(_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)]
)
func _animate_tab_entry() -> void:
for index: int in _category_tabs.size():
(_category_tabs[index] as OrganizerTabType).animate_entrance(
float(index) * 0.025
)
func _settle_tabs_for_close() -> void:
for tab: OrganizerTabType in _category_tabs:
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
func _select_entry(entry_key: StringName, fish_id: StringName) -> void:
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.id)
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)
)
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_column := VBoxContainer.new()
facts_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
facts_column.size_flags_stretch_ratio = 1.0
facts_column.add_theme_constant_override("separation", 6)
summary_columns.add_child(facts_column)
var facts_heading := _field_label("fish facts", 16)
facts_column.add_child(facts_heading)
var facts := _label(LogbookCatalog.facts_for(fish.id), 16)
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
facts.size_flags_horizontal = Control.SIZE_EXPAND_FILL
facts_column.add_child(facts)
_detail_body.add_child(_build_quality_progress(fish.id))
var stats_anchor := VBoxContainer.new()
stats_anchor.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stats_anchor.size_flags_vertical = Control.SIZE_EXPAND_FILL
stats_anchor.alignment = BoxContainer.ALIGNMENT_END
_detail_body.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,
"body of water",
WaterType.label(fish.get_primary_water_type()).to_lower(),
)
_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", "unknown")
_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,
"value range",
"%d%d fish coin" % [
FishQualityType.apply_sale_value(
fish.sell_value_min,
FishQualityType.Tier.BORING,
),
FishQualityType.apply_sale_value(
fish.sell_value_max,
FishQualityType.Tier.SHINY,
),
],
)
_add_detail_row(
right_stats,
"number owned",
str(_inventory.get_count(fish.id) if _inventory != null else 0),
)
var bottom_inset := Control.new()
bottom_inset.custom_minimum_size.y = DETAIL_BOTTOM_INSET
bottom_inset.mouse_filter = Control.MOUSE_FILTER_IGNORE
stats_anchor.add_child(bottom_inset)
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 _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 _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", 12)
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
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:
_refresh_details(false)
func _unknown_selection_key(fish: FishDataType) -> StringName:
var number: int = LogbookCatalog.catalog_number(fish.id)
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 transparent_style := StyleBoxEmpty.new()
for state: StringName in [
&"normal", &"hover", &"focus", &"pressed", &"disabled",
]:
_portrait_overlay_backdrop.add_theme_stylebox_override(
state, transparent_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 center := CenterContainer.new()
center.mouse_filter = Control.MOUSE_FILTER_IGNORE
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_portrait_overlay.add_child(center)
var card := PanelContainer.new()
card.mouse_filter = Control.MOUSE_FILTER_STOP
card.add_theme_stylebox_override("panel", _portrait_view_style())
center.add_child(card)
var margin := MarginContainer.new()
_set_margins(margin, 22, 22, 22, 22)
card.add_child(margin)
_portrait_overlay_artwork = LogbookPortraitType.new()
margin.add_child(_portrait_overlay_artwork)
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.visible = true
_portrait_overlay.mouse_filter = Control.MOUSE_FILTER_STOP
_portrait_overlay_backdrop.grab_focus()
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
if (
restore_focus
and _detail_portrait_button != null
and is_instance_valid(_detail_portrait_button)
and _active
and _interactive
):
_detail_portrait_button.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 = Color("fff5dc")
style.set_border_width_all(0)
style.set_corner_radius_all(18)
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)