netfishing/ui/logbook_page.gd

679 lines
21 KiB
GDScript3
Raw Normal View History

2026-07-30 23:16:28 -04:00
class_name LogbookPage
extends Control
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
2026-07-31 00:03:47 -04:00
const SILHOUETTE_SHADER: Shader = preload(
"res://ui/logbook_silhouette.gdshader"
)
2026-07-30 23:16:28 -04:00
const DETAIL_FADE_DURATION: float = 0.12
const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION
const INK := Color("251b10")
const MUTED_INK := Color("6d5b45")
const PAPER := Color("f2e6c9")
const PAPER_DARK := Color("e8d7b4")
var _collection_log: CollectionLogType
var _inventory: FishInventoryType
var _catalog: FishPoolType
var _category: LogbookCatalog.Category = LogbookCatalog.Category.OTHER
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
2026-07-31 00:03:47 -04:00
var _silhouette_material: ShaderMaterial
2026-07-30 23:16:28 -04:00
var _category_tabs: Array[Button] = []
var _catalog_scroll: ScrollContainer
var _catalog_grid: GridContainer
var _empty_state: Label
var _detail_body: VBoxContainer
func _ready() -> void:
2026-07-31 00:03:47 -04:00
_silhouette_material = ShaderMaterial.new()
_silhouette_material.shader = SILHOUETTE_SHADER
2026-07-30 23:16:28 -04:00
_build_interface()
resized.connect(_update_responsive_layout)
_update_responsive_layout()
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()
func deactivate() -> void:
_active = false
set_interactive(false)
_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
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
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", 116)
outer.add_theme_constant_override("margin_right", 52)
outer.add_theme_constant_override("margin_bottom", 18)
add_child(outer)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 8)
outer.add_child(layout)
var tabs := HBoxContainer.new()
tabs.add_theme_constant_override("separation", 8)
layout.add_child(tabs)
for category: LogbookCatalog.Category in [
LogbookCatalog.Category.FRESH_WATER,
LogbookCatalog.Category.SALT_WATER,
LogbookCatalog.Category.OTHER,
]:
var tab := Button.new()
tab.custom_minimum_size = Vector2(168, 42)
tab.toggle_mode = true
tab.text = LogbookCatalog.category_label(category)
tab.button_pressed = category == _category
tab.pressed.connect(_select_category.bind(category))
UtilityPageStyle.apply_button(tab)
tabs.add_child(tab)
_category_tabs.append(tab)
var book := HBoxContainer.new()
book.size_flags_vertical = Control.SIZE_EXPAND_FILL
book.add_theme_constant_override("separation", 12)
layout.add_child(book)
var left_page := PanelContainer.new()
left_page.size_flags_horizontal = Control.SIZE_EXPAND_FILL
left_page.size_flags_stretch_ratio = 1.12
left_page.add_theme_stylebox_override(
"panel", _paper_style(PAPER, true)
)
book.add_child(left_page)
var left_margin := MarginContainer.new()
_set_margins(left_margin, 18, 16, 18, 16)
left_page.add_child(left_margin)
var left_layout := VBoxContainer.new()
left_layout.add_theme_constant_override("separation", 8)
left_margin.add_child(left_layout)
var heading := _label("catch catalog", 25)
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
left_layout.add_child(heading)
_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)
_catalog_scroll = ScrollContainer.new()
_catalog_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_catalog_scroll.horizontal_scroll_mode = (
ScrollContainer.SCROLL_MODE_DISABLED
)
left_layout.add_child(_catalog_scroll)
_catalog_grid = GridContainer.new()
_catalog_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_catalog_grid.add_theme_constant_override("h_separation", 8)
_catalog_grid.add_theme_constant_override("v_separation", 8)
_catalog_scroll.add_child(_catalog_grid)
var gutter := ColorRect.new()
gutter.custom_minimum_size.x = 10
gutter.color = Color("795f3f")
gutter.mouse_filter = Control.MOUSE_FILTER_IGNORE
book.add_child(gutter)
var right_page := PanelContainer.new()
right_page.size_flags_horizontal = Control.SIZE_EXPAND_FILL
right_page.add_theme_stylebox_override(
"panel", _paper_style(PAPER_DARK, false)
)
book.add_child(right_page)
var right_margin := MarginContainer.new()
_set_margins(right_margin, 22, 16, 22, 16)
right_page.add_child(right_margin)
var details_scroll := ScrollContainer.new()
details_scroll.horizontal_scroll_mode = (
ScrollContainer.SCROLL_MODE_DISABLED
)
right_margin.add_child(details_scroll)
_detail_body = VBoxContainer.new()
_detail_body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_detail_body.add_theme_constant_override("separation", 8)
details_scroll.add_child(_detail_body)
_show_no_selection()
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)
2026-07-31 00:03:47 -04:00
var entry: Button = _make_entry(fish, discovered)
2026-07-30 23:16:28 -04:00
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)
2026-07-31 00:03:47 -04:00
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
2026-07-30 23:16:28 -04:00
var entry := Button.new()
2026-07-31 00:03:47 -04:00
entry.custom_minimum_size = Vector2(220, 138)
2026-07-30 23:16:28 -04:00
entry.toggle_mode = true
entry.clip_text = true
entry.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
entry.add_theme_font_size_override("font_size", 17)
entry.add_theme_color_override("font_color", INK)
entry.add_theme_color_override("font_focus_color", INK)
entry.add_theme_color_override("font_hover_color", INK)
entry.add_theme_stylebox_override("normal", _entry_style(false))
entry.add_theme_stylebox_override("hover", _entry_style(true))
entry.add_theme_stylebox_override("focus", _entry_style(true))
entry.add_theme_stylebox_override("pressed", _entry_style(true))
2026-07-31 00:03:47 -04:00
if not discovered:
entry.text = ""
2026-07-30 23:16:28 -04:00
entry.tooltip_text = ""
entry.accessibility_name = "Unknown catalog entry"
2026-07-31 00:03:47 -04:00
_add_unknown_entry_content(entry, fish.display_texture)
2026-07-30 23:16:28 -04:00
else:
entry.text = fish.display_name
entry.icon = fish.display_texture
entry.add_theme_constant_override("icon_max_width", 118)
entry.expand_icon = true
entry.vertical_icon_alignment = VERTICAL_ALIGNMENT_TOP
entry.tooltip_text = fish.display_name
entry.accessibility_name = fish.display_name
return entry
2026-07-31 00:03:47 -04:00
func _add_unknown_entry_content(
entry: Button,
portrait: Texture2D,
) -> 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", 7)
content_margin.add_theme_constant_override("margin_top", 7)
content_margin.add_theme_constant_override("margin_right", 7)
content_margin.add_theme_constant_override("margin_bottom", 7)
entry.add_child(content_margin)
var content := VBoxContainer.new()
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_theme_constant_override("separation", 2)
content_margin.add_child(content)
var portrait_view := TextureRect.new()
portrait_view.custom_minimum_size = Vector2(0.0, 64.0)
portrait_view.mouse_filter = Control.MOUSE_FILTER_IGNORE
portrait_view.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
portrait_view.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
portrait_view.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
portrait_view.texture = portrait
portrait_view.material = _silhouette_material
content.add_child(portrait_view)
var unknown_name := Label.new()
unknown_name.mouse_filter = Control.MOUSE_FILTER_IGNORE
unknown_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
unknown_name.text = "???"
unknown_name.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
unknown_name.add_theme_font_size_override("font_size", 18)
unknown_name.add_theme_color_override("font_color", INK)
content.add_child(unknown_name)
var discovery_hint := Label.new()
discovery_hint.mouse_filter = Control.MOUSE_FILTER_IGNORE
discovery_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
discovery_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
discovery_hint.text = "Catch this creature to learn more."
discovery_hint.add_theme_font_override(
"font", UtilityPageStyle.TuffyFont
)
discovery_hint.add_theme_font_size_override("font_size", 12)
discovery_hint.add_theme_color_override("font_color", MUTED_INK)
content.add_child(discovery_hint)
2026-07-30 23:16:28 -04:00
func _select_category(category: LogbookCatalog.Category) -> void:
if category == _category:
return
_category = category
_selected_id = StringName()
_selected_entry_key = StringName()
for index: int in _category_tabs.size():
_category_tabs[index].button_pressed = index == int(_category)
_begin_category_transition()
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 title := _label(fish.display_name, 30)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_detail_body.add_child(title)
if fish.display_texture != null:
var artwork := TextureRect.new()
artwork.custom_minimum_size = Vector2(0, 150)
artwork.texture = fish.display_texture
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
artwork.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
artwork.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
_detail_body.add_child(artwork)
_add_detail_row(
"catalog number",
"#%03d" % catalog_number if catalog_number > 0 else "Unknown",
)
_add_detail_row("rarity", fish.get_rarity_name())
_add_detail_row("body of water", "Unknown")
_add_detail_row("time of day", _availability_text(fish))
_add_detail_row(
"weight range",
"%.2f%.2f lb" % [
fish.get_minimum_weight(),
fish.get_maximum_weight(),
],
)
_add_detail_row(
"value range",
"%d%d fish coin" % [
fish.sell_value_min,
fish.sell_value_max,
],
)
_add_detail_row("number caught", "Unknown")
_add_detail_row(
"number owned",
str(_inventory.get_count(fish.id) if _inventory != null else 0),
)
_add_detail_row("facts", "Unknown")
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(row_name: String, value: String) -> void:
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 1)
var heading := _label(row_name, 15)
heading.add_theme_color_override("font_color", MUTED_INK)
var content := _label(value, 20)
content.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
row.add_child(heading)
row.add_child(content)
_detail_body.add_child(row)
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:
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:
2026-07-31 00:03:47 -04:00
if _catalog == null:
2026-07-30 23:16:28 -04:00
return
2026-07-31 00:03:47 -04:00
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
2026-07-30 23:16:28 -04:00
_refresh_catalog()
func _on_collection_changed() -> void:
_refresh_catalog()
func _on_inventory_changed() -> void:
_refresh_details(false)
func _update_responsive_layout() -> void:
if _catalog_grid == null:
return
_catalog_grid.columns = 1 if size.x < 900.0 else 2
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", UtilityPageStyle.TuffyFont)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", INK)
return label
func _paper_style(color: Color, left: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = Color("9b7a4f")
style.set_border_width_all(3)
style.corner_radius_top_left = 26 if left else 6
style.corner_radius_bottom_left = 32 if left else 6
style.corner_radius_top_right = 6 if left else 28
style.corner_radius_bottom_right = 6 if left else 34
return style
func _entry_style(selected: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color("fff5dc") if selected else Color("eadcbd")
style.border_color = Color("247a8b") if selected else Color("9b7a4f")
style.set_border_width_all(3 if selected else 2)
style.set_corner_radius_all(8)
style.content_margin_left = 8
style.content_margin_top = 8
style.content_margin_right = 8
style.content_margin_bottom = 8
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)