From 3dfa6b221e498f67010a067c6d9450f2b21f9071 Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 00:03:47 -0400 Subject: [PATCH] Add fish silhouettes to Logbook --- tests/logbook_runtime_validation.gd | 6 +- tests/logbook_validation.gd | 114 ++++++++++++++++++++++------ ui/logbook_page.gd | 77 +++++++++++++++++-- ui/logbook_silhouette.gdshader | 8 ++ ui/logbook_silhouette.gdshader.uid | 1 + 5 files changed, 173 insertions(+), 33 deletions(-) create mode 100644 ui/logbook_silhouette.gdshader create mode 100644 ui/logbook_silhouette.gdshader.uid diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd index c4802e7..1b27a33 100644 --- a/tests/logbook_runtime_validation.gd +++ b/tests/logbook_runtime_validation.gd @@ -21,6 +21,11 @@ func _run() -> void: await process_frame assert(bool(main.get("_gameplay_started"))) _validate_save_round_trip(main, save_manager) + var player := main.get("_player") as Player + var no_catches: Array[FishCatch] = [] + var no_discoveries: Array[StringName] = [] + assert(player.inventory.replace_all_catches(no_catches, 1)) + assert(player.collection_log.replace_discovered_ids(no_discoveries)) var game_ui := main.get_node("%GameUI") as GameUI var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu @@ -54,7 +59,6 @@ func _run() -> void: logbook.call("_select_category", LogbookCatalog.Category.OTHER) await create_timer(0.25).timeout assert((logbook.get("_entry_buttons") as Dictionary).size() == 4) - var player := main.get("_player") as Player player.collection_log.mark_discovered(&"bluegill") await process_frame logbook.call("_select_entry", &"bluegill", &"bluegill") diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index 98fa3b0..f569215 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -4,6 +4,7 @@ const FishCatchType = preload("res://fish/fish_catch.gd") 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 LogbookPageScene = preload("res://ui/logbook_page.tscn") const CatalogResource: FishPoolType = preload( "res://fish/pools/fish_catalog.tres" @@ -75,30 +76,63 @@ func _validate_page() -> void: page.activate() await process_frame - var entries: Dictionary = page.get("_entry_buttons") - assert(entries.size() == 4) - for entry_value: Variant in entries.values(): - var entry := entry_value as Button - assert(entry != null) - assert(entry.text.contains("???")) - assert(entry.tooltip_text.is_empty()) - assert(entry.icon == null) - assert(entry.accessibility_name == "Unknown catalog entry") - for fish in CatalogResource.candidates: - assert(not entry.text.contains(fish.display_name)) + var shared_material: Material + var silhouette_count: int = 0 + for category: LogbookCatalog.Category in [ + LogbookCatalog.Category.FRESH_WATER, + LogbookCatalog.Category.OTHER, + ]: + page.call("_select_category", category) + await create_timer(0.25).timeout + var entries: Dictionary = page.get("_entry_buttons") + assert(entries.size() == 4) + for fish: FishDataType in LogbookCatalog.ordered_species( + CatalogResource.candidates + ): + if LogbookCatalog.category_for(fish) != category: + continue + var unknown_key := StringName( + "unknown_%d" % LogbookCatalog.catalog_number(fish.id) + ) + var entry := entries.get(unknown_key) as Button + assert(entry != null) + assert(entry.text.is_empty()) + assert(entry.tooltip_text.is_empty()) + assert(entry.icon == null) + assert(entry.accessibility_name == "Unknown catalog entry") + assert(_has_visible_unknown_name(entry)) + var portrait := _find_unknown_portrait(entry) + assert(portrait != null) + assert(portrait.texture == fish.display_texture) + assert( + portrait.expand_mode + == TextureRect.EXPAND_IGNORE_SIZE + ) + assert( + portrait.stretch_mode + == TextureRect.STRETCH_KEEP_ASPECT_CENTERED + ) + assert(portrait.material is ShaderMaterial) + if shared_material == null: + shared_material = portrait.material + else: + assert(portrait.material == shared_material) + assert(_texture_has_transparency(portrait.texture)) + silhouette_count += 1 + for candidate: FishDataType in CatalogResource.candidates: + assert(not entry.text.contains(candidate.display_name)) + assert( + not entry.tooltip_text.contains( + candidate.display_name + ) + ) + assert( + not entry.accessibility_name.contains( + candidate.display_name + ) + ) + assert(silhouette_count == 8) - page.call( - "_select_category", LogbookCatalog.Category.FRESH_WATER - ) - await create_timer(0.25).timeout - assert((page.get("_entry_buttons") as Dictionary).size() == 4) - for entry_value: Variant in ( - page.get("_entry_buttons") as Dictionary - ).values(): - var unknown_catfish := entry_value as Button - assert(unknown_catfish != null) - assert(unknown_catfish.text.contains("???")) - assert(unknown_catfish.icon == null) page.call("_select_category", LogbookCatalog.Category.SALT_WATER) await create_timer(0.25).timeout assert((page.get("_entry_buttons") as Dictionary).is_empty()) @@ -110,13 +144,20 @@ func _validate_page() -> void: await create_timer(0.25).timeout var bluegill = CatalogResource.get_fish_by_id(&"bluegill") + page.call("_select_entry", &"unknown_1", StringName()) + assert( + (page.get("_entry_buttons") as Dictionary) + .get(&"unknown_1") != null + ) collection.mark_discovered(&"bluegill") await process_frame - entries = page.get("_entry_buttons") + 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.button_pressed) var fish_catch := FishCatchType.new() fish_catch.fish = bluegill @@ -151,3 +192,28 @@ func _detail_text(page: LogbookPage) -> String: for node: Node in detail_body.find_children("*", "Label", true, false): values.append((node as Label).text) return "\n".join(values) + + +func _find_unknown_portrait(entry: Button) -> TextureRect: + var pending: Array[Node] = [entry] + while not pending.is_empty(): + var node: Node = pending.pop_back() + if node is TextureRect: + return node as TextureRect + pending.append_array(node.get_children()) + return null + + +func _has_visible_unknown_name(entry: Button) -> 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 == "???": + return true + pending.append_array(node.get_children()) + return false + + +func _texture_has_transparency(texture: Texture2D) -> bool: + var image: Image = texture.get_image() + return image != null and image.detect_alpha() != Image.ALPHA_NONE diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index be1dc00..6c91cb0 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -5,6 +5,9 @@ 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") +const SILHOUETTE_SHADER: Shader = preload( + "res://ui/logbook_silhouette.gdshader" +) const DETAIL_FADE_DURATION: float = 0.12 const CATEGORY_FADE_DURATION: float = UIMotion.UTILITY_EXIT_DURATION @@ -26,6 +29,7 @@ var _detail_generation: int = 0 var _detail_tween: Tween var _category_generation: int = 0 var _category_tween: Tween +var _silhouette_material: ShaderMaterial var _category_tabs: Array[Button] = [] var _catalog_scroll: ScrollContainer @@ -35,6 +39,8 @@ var _detail_body: VBoxContainer func _ready() -> void: + _silhouette_material = ShaderMaterial.new() + _silhouette_material.shader = SILHOUETTE_SHADER _build_interface() resized.connect(_update_responsive_layout) _update_responsive_layout() @@ -225,7 +231,7 @@ func _refresh_catalog() -> void: if LogbookCatalog.category_for(fish) != _category: continue var discovered: bool = _collection_log.has_discovered(fish.id) - var entry: Button = _make_entry(fish if discovered else null) + var entry: Button = _make_entry(fish, discovered) var selection_key: StringName = ( fish.id if discovered else _unknown_selection_key(fish) ) @@ -263,9 +269,9 @@ func _refresh_catalog() -> void: set_interactive(_interactive) -func _make_entry(fish: FishDataType) -> Button: +func _make_entry(fish: FishDataType, discovered: bool) -> Button: var entry := Button.new() - entry.custom_minimum_size = Vector2(220, 122) + entry.custom_minimum_size = Vector2(220, 138) entry.toggle_mode = true entry.clip_text = true entry.add_theme_font_override("font", UtilityPageStyle.TuffyFont) @@ -277,10 +283,11 @@ func _make_entry(fish: FishDataType) -> Button: 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)) - if fish == null: - entry.text = "?\n???\nCatch this creature\nto learn more." + if not discovered: + entry.text = "" entry.tooltip_text = "" entry.accessibility_name = "Unknown catalog entry" + _add_unknown_entry_content(entry, fish.display_texture) else: entry.text = fish.display_name entry.icon = fish.display_texture @@ -292,6 +299,56 @@ func _make_entry(fish: FishDataType) -> Button: return entry +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) + + func _select_category(category: LogbookCatalog.Category) -> void: if category == _category: return @@ -544,10 +601,14 @@ func _refresh_selection_styles() -> void: func _on_fish_discovered(fish_id: StringName) -> void: - if _catalog == null or _catalog.get_fish_by_id(fish_id) == null: + if _catalog == null: return - _selected_id = fish_id - _selected_entry_key = fish_id + 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() diff --git a/ui/logbook_silhouette.gdshader b/ui/logbook_silhouette.gdshader new file mode 100644 index 0000000..89ac208 --- /dev/null +++ b/ui/logbook_silhouette.gdshader @@ -0,0 +1,8 @@ +shader_type canvas_item; + +uniform vec4 silhouette_color : source_color = vec4(0.22, 0.23, 0.24, 1.0); + +void fragment() { + vec4 source = texture(TEXTURE, UV); + COLOR = vec4(silhouette_color.rgb, source.a * silhouette_color.a); +} diff --git a/ui/logbook_silhouette.gdshader.uid b/ui/logbook_silhouette.gdshader.uid new file mode 100644 index 0000000..2dd5442 --- /dev/null +++ b/ui/logbook_silhouette.gdshader.uid @@ -0,0 +1 @@ +uid://1lqrwon78hwm