Improve Logbook presentation

This commit is contained in:
Alexander Sellite 2026-08-02 09:57:05 -04:00
parent 984f7c641c
commit c956301194
5 changed files with 578 additions and 107 deletions

View file

@ -5,6 +5,12 @@ const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const LogbookPortraitType = preload(
"res://ui/components/logbook_portrait.gd"
)
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
const LogbookPageScene = preload("res://ui/logbook_page.tscn")
const CatalogResource: FishPoolType = preload(
"res://fish/pools/fish_catalog.tres"
@ -48,6 +54,12 @@ func _validate_catalog() -> void:
for index: int in expected_ids.size():
var fish := CatalogResource.get_fish_by_id(expected_ids[index])
assert(fish != null)
assert(LogbookCatalog.facts_for(fish.id) != "unknown")
assert(not LogbookCatalog.facts_for(fish.id).is_empty())
assert(
LogbookCatalog.facts_for(fish.id)
== LogbookCatalog.facts_for(fish.id).to_lower()
)
var expected_category: WaterType.Type = (
WaterType.Type.SALT_WATER
if expected_ids[index] in [&"bass", &"sunfish"]
@ -75,6 +87,14 @@ func _validate_page() -> void:
page.setup(collection, inventory, CatalogResource)
page.activate()
await process_frame
assert(page.get("_category") == WaterType.Type.FRESH_WATER)
assert((page.get("_catalog_grid") as GridContainer).columns == 5)
var initial_detail_body := page.get("_detail_body") as VBoxContainer
assert(not initial_detail_body.get_parent() is ScrollContainer)
assert(
page.call("_entry_label_text", "flathead catfish")
== "flathead\ncatfish"
)
var shared_material: Material
var silhouette_count: int = 0
@ -101,9 +121,13 @@ func _validate_page() -> void:
assert(entry.icon == null)
assert(entry.accessibility_name == "Unknown catalog entry")
assert(_has_visible_unknown_name(entry))
var portrait := _find_unknown_portrait(entry)
var portrait: LogbookPortraitType = _find_portrait(entry)
assert(portrait != null)
assert(portrait.texture == fish.display_texture)
assert(portrait.source_texture == fish.display_texture)
assert(
portrait.custom_minimum_size
== LogbookPortraitType.ENTRY_FRAME_SIZE
)
assert(
portrait.expand_mode
== TextureRect.EXPAND_IGNORE_SIZE
@ -154,10 +178,17 @@ func _validate_page() -> void:
var entries: Dictionary = page.get("_entry_buttons")
var known := entries.get(&"bluegill") as Button
assert(known != null)
assert(known.text == bluegill.display_name)
assert(known.icon == bluegill.display_texture)
assert(_find_unknown_portrait(known) == null)
assert(known.custom_minimum_size.x == known.custom_minimum_size.y)
_validate_entry_bubble_styles(known)
assert(known.text.is_empty())
assert(known.icon == null)
assert(_has_visible_name(known, bluegill.display_name))
var known_portrait: LogbookPortraitType = _find_portrait(known)
assert(known_portrait != null)
assert(known_portrait.source_texture == bluegill.display_texture)
assert(known_portrait.material == null)
assert(known.button_pressed)
_validate_handwritten_logbook_font(page)
var fish_catch := FishCatchType.new()
fish_catch.fish = bluegill
@ -174,9 +205,30 @@ func _validate_page() -> void:
page.call("_select_entry", &"bluegill", &"bluegill")
await process_frame
assert(_detail_text(page).contains("number owned\n1"))
assert(_detail_text(page).contains("number caught\nUnknown"))
assert(_detail_text(page).contains("body of water\nFresh Water"))
assert(_detail_text(page).contains("number caught\nunknown"))
assert(_detail_text(page).contains("body of water\nfresh water"))
assert(_detail_text(page).contains("catalog number\n#001"))
assert(_detail_text(page).contains("fish facts\n"))
assert(_detail_text(page).contains(LogbookCatalog.facts_for(&"bluegill")))
_validate_detail_field_fonts(page)
_validate_handwritten_numeric_scale(page)
var portrait_button := page.get("_detail_portrait_button") as Button
assert(portrait_button != null)
portrait_button.pressed.emit()
await process_frame
var portrait_overlay := page.get("_portrait_overlay") as Control
assert(portrait_overlay.visible)
var overlay_artwork := (
page.get("_portrait_overlay_artwork") as LogbookPortraitType
)
assert(overlay_artwork.source_texture == bluegill.display_texture)
assert(
overlay_artwork.custom_minimum_size.x <= 860.0
and overlay_artwork.custom_minimum_size.y <= 480.0
)
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
await process_frame
assert(not portrait_overlay.visible)
inventory.remove_catch_by_id(fish_catch.catch_id)
await process_frame
@ -194,26 +246,110 @@ func _detail_text(page: LogbookPage) -> String:
return "\n".join(values)
func _find_unknown_portrait(entry: Button) -> TextureRect:
func _find_portrait(entry: Button) -> LogbookPortraitType:
var pending: Array[Node] = [entry]
while not pending.is_empty():
var node: Node = pending.pop_back()
if node is TextureRect:
return node as TextureRect
if node is LogbookPortraitType:
return node as LogbookPortraitType
pending.append_array(node.get_children())
return null
func _has_visible_unknown_name(entry: Button) -> bool:
return _has_visible_name(entry, "???")
func _has_visible_name(entry: Button, expected_name: String) -> bool:
var pending: Array[Node] = [entry]
while not pending.is_empty():
var node: Node = pending.pop_back()
if node is Label and (node as Label).text == "???":
if node is Label and (node as Label).text == expected_name:
return true
pending.append_array(node.get_children())
return false
func _validate_handwritten_logbook_font(page: LogbookPage) -> void:
var detail_body := page.get("_detail_body") as VBoxContainer
for node: Node in detail_body.find_children("*", "Label", true, false):
var label := node as Label
if label.text in [
"fish facts",
"catalog number",
"rarity",
"body of water",
"time of day",
"weight range",
"value range",
"number caught",
"number owned",
]:
continue
assert(
label.get_theme_font("font")
== InventoryNotepadType.HANDWRITTEN_FONT
)
func _validate_detail_field_fonts(page: LogbookPage) -> void:
var detail_body := page.get("_detail_body") as VBoxContainer
var field_names: Array[String] = [
"fish facts",
"catalog number",
"rarity",
"body of water",
"time of day",
"weight range",
"value range",
"number caught",
"number owned",
]
for node: Node in detail_body.find_children("*", "Label", true, false):
var label := node as Label
if label.text not in field_names:
continue
assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
if label.text != "fish facts":
assert(
label.horizontal_alignment
== HORIZONTAL_ALIGNMENT_CENTER
)
func _validate_handwritten_numeric_scale(page: LogbookPage) -> void:
var detail_body := page.get("_detail_body") as VBoxContainer
var text_value: Label
var numeric_value: Label
for node: Node in detail_body.find_children("*", "Label", true, false):
var label := node as Label
if label.text == "common":
text_value = label
elif label.text == "#001":
numeric_value = label
assert(text_value != null)
assert(numeric_value != null)
assert(
numeric_value.get_theme_font_size("font_size")
< text_value.get_theme_font_size("font_size")
)
func _validate_entry_bubble_styles(entry: Button) -> void:
var normal := entry.get_theme_stylebox("normal") as StyleBoxFlat
var hover := entry.get_theme_stylebox("hover") as StyleBoxFlat
var pressed := entry.get_theme_stylebox("pressed") as StyleBoxFlat
assert(normal != null)
assert(hover != null)
assert(pressed != null)
assert(is_zero_approx(normal.bg_color.a))
assert(hover.bg_color.a > 0.0)
assert(pressed.bg_color.a > 0.0)
assert(hover.border_width_left == 0)
assert(pressed.border_width_left == 0)
assert(hover.corner_radius_top_left == 51)
func _texture_has_transparency(texture: Texture2D) -> bool:
var image: Image = texture.get_image()
return image != null and image.detect_alpha() != Image.ALPHA_NONE

View file

@ -0,0 +1,79 @@
class_name LogbookPortrait
extends TextureRect
const ENTRY_FRAME_SIZE := Vector2(86.0, 40.0)
const DETAIL_FRAME_SIZE := Vector2(240.0, 132.0)
static var _normalized_textures: Dictionary[String, Texture2D] = {}
var source_texture: Texture2D
func _init() -> void:
expand_mode = TextureRect.EXPAND_IGNORE_SIZE
stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
mouse_filter = Control.MOUSE_FILTER_IGNORE
focus_mode = Control.FOCUS_NONE
size_flags_horizontal = Control.SIZE_SHRINK_CENTER
size_flags_vertical = Control.SIZE_SHRINK_CENTER
func configure(
portrait_texture: Texture2D,
frame_size: Vector2,
portrait_material: Material = null,
) -> void:
source_texture = portrait_texture
custom_minimum_size = frame_size
material = portrait_material
texture = _normalize_visible_bounds(portrait_texture)
func configure_fitted(
portrait_texture: Texture2D,
maximum_size: Vector2,
portrait_material: Material = null,
) -> void:
source_texture = portrait_texture
material = portrait_material
texture = _normalize_visible_bounds(portrait_texture)
if texture == null:
custom_minimum_size = Vector2.ZERO
return
var texture_size: Vector2 = texture.get_size()
if texture_size.x <= 0.0 or texture_size.y <= 0.0:
custom_minimum_size = Vector2.ZERO
return
var fit_scale: float = minf(
maximum_size.x / texture_size.x,
maximum_size.y / texture_size.y,
)
custom_minimum_size = texture_size * fit_scale
static func _normalize_visible_bounds(
portrait_texture: Texture2D,
) -> Texture2D:
if portrait_texture == null:
return null
var cache_key: String = portrait_texture.resource_path
if cache_key.is_empty():
cache_key = "instance_%d" % portrait_texture.get_instance_id()
var cached: Texture2D = _normalized_textures.get(cache_key) as Texture2D
if cached != null:
return cached
var image: Image = portrait_texture.get_image()
if image == null or image.is_empty():
_normalized_textures[cache_key] = portrait_texture
return portrait_texture
var full_rect := Rect2i(Vector2i.ZERO, image.get_size())
var visible_rect: Rect2i = image.get_used_rect()
if visible_rect.size == Vector2i.ZERO or visible_rect == full_rect:
_normalized_textures[cache_key] = portrait_texture
return portrait_texture
var normalized := AtlasTexture.new()
normalized.atlas = portrait_texture
normalized.region = Rect2(visible_rect)
_normalized_textures[cache_key] = normalized
return normalized

View file

@ -0,0 +1 @@
uid://qkduawll2pm2

View file

@ -16,6 +16,44 @@ const CATALOG_ORDER: Array[StringName] = [
&"catfish_white",
]
# Short presentation notes are kept here with the catalog contract rather
# than mixed into gameplay balance resources. Add facts deliberately when a
# species joins the catalog.
const FISH_FACTS: Dictionary[StringName, String] = {
&"bluegill": (
"bluegill fathers sweep shallow bowls into the bottom for nests, "
+ "then stand guard. whole neighborhoods of nests can crowd together."
),
&"bass": (
"striped bass split their lives between salt and fresh water, "
+ "returning upriver to spawn. a long-lived striper may see three decades."
),
&"carp": (
"whisker-like barbels help a common carp investigate the bottom. "
+ "nosing through sediment can cloud the water and loosen plants."
),
&"sunfish": (
"ocean sunfish are built more like enormous swimming heads than "
+ "ordinary fish. they visit the surface and turn sideways to warm up."
),
&"catfish_blue": (
"a deep fork in the tail inspired the blue catfish's scientific "
+ "name. it rests deep by daylight and becomes more active after dark."
),
&"catfish_channel": (
"a channel catfish can taste through skin all over its body, "
+ "especially near its gills and whiskers. the male watches the eggs."
),
&"catfish_flathead": (
"flatheads favor hideouts made by logs, rocks, ledges, and other "
+ "underwater structure. as they grow, fish dominate their menu."
),
&"catfish_white": (
"white catfish began along the atlantic coast between new york "
+ "and florida. people later carried them far beyond that home range."
),
}
static func category_for(fish: FishDataType) -> WaterType.Type:
if fish == null:
return WaterType.Type.OTHER
@ -41,6 +79,10 @@ static func catalog_number(fish_id: StringName) -> int:
return index + 1 if index >= 0 else 0
static func facts_for(fish_id: StringName) -> String:
return str(FISH_FACTS.get(fish_id, "unknown"))
static func ordered_species(
candidates: Array[FishDataType],
) -> Array[FishDataType]:

View file

@ -9,9 +9,17 @@ 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 PAPER := Color("f2e6c9")
@ -21,7 +29,7 @@ const LOGBOOK_TAB_LEFT_INSET: float = 34.0
var _collection_log: CollectionLogType
var _inventory: FishInventoryType
var _catalog: FishPoolType
var _category: WaterType.Type = WaterType.Type.OTHER
var _category: WaterType.Type = WaterType.Type.FRESH_WATER
var _selected_id: StringName
var _selected_entry_key: StringName
var _active: bool = false
@ -38,14 +46,16 @@ var _catalog_scroll: ScrollContainer
var _catalog_grid: GridContainer
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()
resized.connect(_update_responsive_layout)
_update_responsive_layout()
set_interactive(false)
@ -79,6 +89,7 @@ func activate() -> void:
func deactivate() -> void:
_active = false
_hide_portrait_overlay(false)
set_interactive(false)
_settle_tabs_for_close()
_cancel_detail_tween()
@ -115,6 +126,9 @@ func set_interactive(value: bool) -> void:
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()
@ -199,6 +213,7 @@ func _build_interface() -> void:
)
left_layout.add_child(_catalog_scroll)
_catalog_grid = GridContainer.new()
_catalog_grid.columns = 5
_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)
@ -219,16 +234,13 @@ func _build_interface() -> void:
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)
_detail_body.size_flags_vertical = Control.SIZE_EXPAND_FILL
_detail_body.add_theme_constant_override("separation", 16)
right_margin.add_child(_detail_body)
_show_no_selection()
_build_portrait_overlay()
func _refresh_catalog() -> void:
@ -284,82 +296,65 @@ func _refresh_catalog() -> void:
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
var entry := Button.new()
entry.custom_minimum_size = Vector2(220, 138)
entry.custom_minimum_size = Vector2(102, 102)
entry.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
entry.size_flags_vertical = Control.SIZE_SHRINK_CENTER
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("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.text = ""
entry.tooltip_text = ""
entry.accessibility_name = "Unknown catalog entry"
_add_unknown_entry_content(entry, fish.display_texture)
_add_entry_content(entry, fish.display_texture, "???", true)
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
_add_entry_content(
entry, fish.display_texture, fish.display_name, false
)
return entry
func _add_unknown_entry_content(
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", 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)
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.add_theme_constant_override("separation", 2)
content.alignment = BoxContainer.ALIGNMENT_CENTER
content.add_theme_constant_override("separation", 4)
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
var portrait_view := LogbookPortraitType.new()
portrait_view.configure(
portrait,
LogbookPortraitType.ENTRY_FRAME_SIZE,
_silhouette_material if unknown else null,
)
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)
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:
@ -499,45 +494,115 @@ func _refresh_details(animate: bool) -> void:
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)
var summary_columns := HBoxContainer.new()
summary_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
summary_columns.add_theme_constant_override("separation", 24)
_detail_body.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
_detail_body.add_child(title)
portrait_column.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)
_detail_portrait_button = Button.new()
_detail_portrait_button.custom_minimum_size = (
LogbookPortraitType.DETAIL_FRAME_SIZE
)
_detail_portrait_button.size_flags_horizontal = (
Control.SIZE_SHRINK_CENTER
)
_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,
LogbookPortraitType.DETAIL_FRAME_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)
var stats_columns := HBoxContainer.new()
stats_columns.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stats_columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
stats_columns.add_theme_constant_override("separation", 28)
_detail_body.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",
"#%03d" % catalog_number if catalog_number > 0 else "unknown",
)
_add_detail_row("rarity", fish.get_rarity_name())
_add_detail_row("body of water", WaterType.label(fish.get_primary_water_type()))
_add_detail_row("time of day", _availability_text(fish))
_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" % [
fish.sell_value_min,
fish.sell_value_max,
],
)
_add_detail_row("number caught", "Unknown")
_add_detail_row(
right_stats,
"number owned",
str(_inventory.get_count(fish.id) if _inventory != null else 0),
)
_add_detail_row("facts", "Unknown")
func _show_no_selection() -> void:
@ -561,28 +626,42 @@ func _show_unknown() -> void:
_detail_body.add_child(instruction)
func _add_detail_row(row_name: String, value: String) -> void:
func _add_detail_row(
parent: VBoxContainer,
row_name: String,
value: String,
) -> void:
var row := VBoxContainer.new()
row.add_theme_constant_override("separation", 1)
var heading := _label(row_name, 15)
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 := _label(value, 20)
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)
_detail_body.add_child(row)
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"
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"
return "unknown"
func _crossfade_details(rebuild: Callable) -> void:
@ -626,6 +705,7 @@ func _clear_entries() -> void:
func _clear_details() -> void:
_detail_portrait_button = null
for child: Node in _detail_body.get_children():
_detail_body.remove_child(child)
child.queue_free()
@ -658,18 +738,23 @@ 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", 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)
@ -678,6 +763,33 @@ func _label(text: String, font_size: int) -> Label:
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 _paper_style(color: Color, left: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color
@ -690,16 +802,117 @@ func _paper_style(color: Color, left: bool) -> StyleBoxFlat:
return style
func _entry_style(selected: bool) -> StyleBoxFlat:
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("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
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