Add logbook catalog index filters

This commit is contained in:
Alexander Sellite 2026-08-29 20:19:47 -04:00
parent 4575830857
commit 81decf2b71
9 changed files with 1540 additions and 10 deletions

View file

@ -150,6 +150,7 @@ var _network_session: NetworkSessionType
var _network_fishing: NetworkFishingServiceType
var _world_time: WorldTimeServiceType
var _world_weather: WorldWeatherServiceType
var _fishable_water_regions_resolver := Callable()
var _active_player: PlayerType
var _state_time_remaining: float = 0.0
var _cast_charge: float = 0.0
@ -222,6 +223,7 @@ func setup(
network_fishing: NetworkFishingServiceType = null,
world_time: WorldTimeServiceType = null,
world_weather: WorldWeatherServiceType = null,
fishable_water_regions_resolver: Callable = Callable(),
) -> void:
_local_player = local_player
_local_inventory = local_inventory
@ -242,6 +244,7 @@ func setup(
_network_fishing = network_fishing
_world_time = world_time
_world_weather = world_weather
_fishable_water_regions_resolver = fishable_water_regions_resolver
if (
_network_session != null
and not _network_session.state_changed.is_connected(
@ -1664,6 +1667,47 @@ func build_network_context(
return _build_fishing_context(region)
func is_species_available_now(fish: FishDataType) -> bool:
if fish == null or not fish.is_selectable():
return false
var season := (
_world_time.get_season()
if _world_time != null
else CalendarSeason.UNKNOWN
)
if not fish.is_available_in_season(season):
return false
if fish.collection_method != FishDataType.CollectionMethod.FISHING:
return true
for region: FishableWaterRegionType in _get_logbook_water_regions():
if (
region == null
or region.fish_pool == null
or region.fish_pool.get_fish_by_id(fish.id) == null
):
continue
var context := _build_fishing_context(region)
if not fish.is_allowed_in_water(context.water_type):
continue
if fish.availability == null or fish.availability.is_available(context):
return true
return false
func _get_logbook_water_regions() -> Array[FishableWaterRegionType]:
var regions: Array[FishableWaterRegionType] = []
if not _fishable_water_regions_resolver.is_valid():
return regions
var resolved: Variant = _fishable_water_regions_resolver.call()
if resolved is not Array:
return regions
for value: Variant in resolved:
var region := value as FishableWaterRegionType
if region != null and is_instance_valid(region):
regions.append(region)
return regions
func _get_active_bait_tags() -> Array[StringName]:
if _local_player == null or _item_catalog == null:
return []

View file

@ -920,6 +920,7 @@ func _initialize_application(dedicated: bool) -> void:
_network_fishing,
_world_time,
_world_weather,
Callable(_test_world, "get_fishable_water_regions"),
)
if dedicated:
return

View file

@ -1356,6 +1356,24 @@ func _validate_player_menu_nested_controller_back() -> void:
== LogbookPageType.ControllerZone.ENTRIES,
"Global player-menu Back did not return Logbook to creature selection.",
)
logbook_page.call("_set_index_open", true, false)
logbook_page.set(
"_controller_zone",
LogbookPageType.ControllerZone.INDEX,
)
_expect(
bool(menu.call("consume_escape")),
"Global player-menu Back did not consume the Logbook index zone.",
)
_expect(
menu.visible and not bool(logbook_page.get("_index_open")),
"Global player-menu Back did not return from the Logbook index page.",
)
_expect(
int(logbook_page.get("_controller_zone"))
== LogbookPageType.ControllerZone.TABS,
"Global player-menu Back did not return Logbook index focus to tabs.",
)
var net_page := menu.get("_the_net_page") as Control
net_page.set("_active", true)

View file

@ -2,6 +2,7 @@ extends SceneTree
const MainScene = preload("res://main/main.tscn")
const FishCatchType = preload("res://fish/fish_catch.gd")
const MENU_TRANSITION_SETTLE_SECONDS := 3.0
func _initialize() -> void:
@ -36,12 +37,16 @@ func _run() -> void:
var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage
var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect
var hotbar := game_ui.get_node("%Hotbar") as Control
var fishing_spot := main.get("_fishing_spot") as FishingSpot
assert(logbook.get("_fishing_spot") == fishing_spot)
assert(logbook.get("_world_time") == main.get("_world_time"))
assert(logbook.get("_world_weather") == main.get("_world_weather"))
var shortcut := InputEventKey.new()
shortcut.pressed = true
shortcut.physical_keycode = KEY_L
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
await create_timer(2.2).timeout
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
assert(player_menu.visible)
assert(logbook.visible)
assert(backdrop.visible)
@ -51,6 +56,18 @@ func _run() -> void:
var entry_buttons: Dictionary = logbook.get("_entry_buttons")
assert(entry_buttons.size() == 108)
await _capture_if_requested("-unknown")
logbook.call("_set_index_open", true, false)
await process_frame
assert(bool(logbook.get("_index_open")))
assert((logbook.get("_index_body") as Control).visible)
await _capture_if_requested("-index")
logbook.set_compact_presentation(true)
await process_frame
await _capture_if_requested("-index-compact")
logbook.set_compact_presentation(false)
await process_frame
logbook.call("_set_index_open", false, false)
await process_frame
logbook.call(
"_select_category", WaterType.Type.FRESH_WATER
)
@ -68,13 +85,29 @@ func _run() -> void:
logbook.call("_select_entry", &"bluegill", &"bluegill")
await create_timer(0.2).timeout
await _capture_if_requested("-known")
var bluegill := (
main.get("fish_catalog") as FishPool
).get_fish_by_id(&"bluegill")
var bluegill_available_now := fishing_spot.is_species_available_now(bluegill)
var index_options := logbook.get("_index_options") as Dictionary
index_options["availability"] = (
LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
)
logbook.notify_availability_context_changed()
await process_frame
assert(
(logbook.get("_entry_buttons") as Dictionary).has(&"bluegill")
== bluegill_available_now
)
logbook.call("_reset_index")
await process_frame
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
await create_timer(2.2).timeout
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
assert(not player_menu.visible)
assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
await create_timer(2.2).timeout
await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
player_menu.call(
"_show_section_immediate", PlayerMenu.Section.PROFILE
)

View file

@ -17,6 +17,8 @@ const CatalogResource: FishPoolType = preload(
"res://fish/pools/fish_catalog.tres"
)
var _query_available_id: StringName
func _initialize() -> void:
call_deferred("_run")
@ -35,6 +37,7 @@ func _run() -> void:
quit()
return
_validate_catalog()
_validate_catalog_query()
await _validate_page()
print("Logbook validation: PASS")
quit()
@ -68,6 +71,348 @@ func _validate_catalog() -> void:
)
func _validate_catalog_query() -> void:
var collection := CollectionLogType.new()
var inventory := FishInventoryType.new()
var fresh_species: Array[FishDataType] = []
for fish: FishDataType in LogbookCatalog.ordered_species(
CatalogResource.candidates
):
if LogbookCatalog.category_for(fish) == LogbookCatalog.Category.FRESH_WATER:
fresh_species.append(fish)
assert(fresh_species.size() == 108)
var first := fresh_species[0]
var second := fresh_species[1]
var unknown := fresh_species[2]
for quality: int in FishQualityType.TIER_COUNT:
collection.record_catch(first.id, quality)
collection.record_catch(second.id, FishQualityType.Tier.BORING)
var held := _make_test_catch(first, true)
assert(inventory.add_catch(held))
var options := _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
var filtered := LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered.size() == 2)
assert(first in filtered and second in filtered)
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered.size() == fresh_species.size() - 2)
options["sort"] = LogbookCatalog.SortMode.CATALOG_NUMBER
options["descending"] = true
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered.front() == fresh_species.back())
assert(filtered.back() == unknown)
options["show"] = LogbookCatalog.DiscoveryFilter.INCOMPLETE
options["descending"] = false
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [second])
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["sort"] = LogbookCatalog.SortMode.CATCH_COUNT
options["descending"] = true
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [first, second])
options = _default_query_options()
options["search"] = unknown.display_name
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered.is_empty())
options["search"] = str(unknown.catalog_number)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [unknown])
options = _default_query_options()
options["holding"] = LogbookCatalog.HoldingFilter.FAVORITED
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [first])
options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [first])
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["group"] = String(first.collection_group)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(first in filtered)
for fish: FishDataType in filtered:
assert(fish.collection_group == first.collection_group)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["rarity"] = (
int(first.rarity) + LogbookCatalog.RarityFilter.COMMON
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(first in filtered)
for fish: FishDataType in filtered:
assert(fish.rarity == first.rarity)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["method"] = (
int(first.collection_method) + LogbookCatalog.MethodFilter.FISHING
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(first in filtered)
for fish: FishDataType in filtered:
assert(fish.collection_method == first.collection_method)
var first_season := -1
for season: int in 4:
if first.is_available_in_season(season):
first_season = season
break
assert(first_season >= 0)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["season"] = (
first_season + LogbookCatalog.SeasonFilter.SPRING
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(first in filtered)
for fish: FishDataType in filtered:
assert(fish.is_available_in_season(first_season))
var first_is_available_by_day := (
first.availability == null or first.availability.allow_day
)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["time"] = (
LogbookCatalog.TimeFilter.DAY
if first_is_available_by_day
else LogbookCatalog.TimeFilter.NIGHT
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(first in filtered)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
options["sort"] = LogbookCatalog.SortMode.QUALITY_PROGRESS
options["descending"] = true
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered == [first, second])
for sort_mode: LogbookCatalog.SortMode in [
LogbookCatalog.SortMode.NAME,
LogbookCatalog.SortMode.RARITY,
LogbookCatalog.SortMode.VALUE,
LogbookCatalog.SortMode.WEIGHT,
]:
options = _default_query_options()
options["sort"] = sort_mode
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(filtered[0] in [first, second])
assert(filtered[1] in [first, second])
assert(filtered[0] != filtered[1])
assert(filtered[2] == unknown)
_query_available_id = second.id
options = _default_query_options()
options["availability"] = (
LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
Callable(self, "_query_species_available_now"),
)
assert(filtered == [second])
_query_available_id = unknown.id
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
options["rarity"] = (
int(unknown.rarity) + LogbookCatalog.RarityFilter.COMMON
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
Callable(self, "_query_species_available_now"),
)
assert(filtered == [unknown])
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
options["group"] = String(unknown.collection_group)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(unknown in filtered)
assert(
String(unknown.collection_group)
in LogbookCatalog.available_groups(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
)
)
var unknown_is_available_by_day := (
unknown.availability == null or unknown.availability.allow_day
)
options = _default_query_options()
options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
options["time"] = (
LogbookCatalog.TimeFilter.DAY
if unknown_is_available_by_day
else LogbookCatalog.TimeFilter.NIGHT
)
filtered = LogbookCatalog.filtered_species(
CatalogResource.candidates,
LogbookCatalog.Category.FRESH_WATER,
collection,
inventory,
options,
)
assert(unknown in filtered)
inventory.free()
collection.free()
func _default_query_options() -> Dictionary:
return {
"search": "",
"show": LogbookCatalog.DiscoveryFilter.ALL,
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
"descending": false,
"availability": LogbookCatalog.AvailabilityFilter.ANY,
"season": LogbookCatalog.SeasonFilter.ANY,
"time": LogbookCatalog.TimeFilter.ANY,
"rarity": LogbookCatalog.RarityFilter.ANY,
"method": LogbookCatalog.MethodFilter.ANY,
"holding": LogbookCatalog.HoldingFilter.ANY,
"group": "",
}
func _make_test_catch(fish: FishDataType, favorited: bool) -> FishCatchType:
var fish_catch := FishCatchType.new()
fish_catch.fish = fish
fish_catch.fish_id = fish.id
fish_catch.weight_lb = fish.get_minimum_weight()
fish_catch.display_scale = fish.get_display_scale_for_weight(
fish_catch.weight_lb
)
fish_catch.quality = FishQualityType.Tier.BORING
fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
fish_catch.is_favorited = favorited
fish_catch.ensure_identity()
return fish_catch
func _query_species_available_now(fish: FishDataType) -> bool:
return fish != null and fish.id == _query_available_id
func _validate_page() -> void:
var collection := CollectionLogType.new()
var inventory := FishInventoryType.new()
@ -91,6 +436,10 @@ func _validate_page() -> void:
assert(outer_margin.get_theme_constant("margin_right") == 160)
assert(outer_margin.get_theme_constant("margin_bottom") == 106)
assert(is_equal_approx(book.offset_top, 65.0))
page.call("_set_index_open", true, false)
await process_frame
_validate_index_content_bounds(page)
page.call("_set_index_open", false, false)
page.set_compact_presentation(false)
await process_frame
assert(outer_margin.get_theme_constant("margin_left") == 52)
@ -158,6 +507,39 @@ func _validate_page() -> void:
)
var initial_detail_body := page.get("_detail_body") as VBoxContainer
assert(not initial_detail_body.get_parent() is ScrollContainer)
var index_tab := page.get("_index_tab") as Button
var index_body := page.get("_index_body") as VBoxContainer
assert(index_tab != null and index_body != null)
assert(index_tab.size == LogbookPage.INDEX_TAB_SIZE)
assert(
not index_tab.get_global_rect().intersects(
(category_tabs.back() as Button).get_global_rect()
)
)
page.call("_set_index_open", true, false)
await process_frame
assert(index_body.visible)
assert(not initial_detail_body.visible)
assert((page.get("_index_option_buttons") as Dictionary).size() == 10)
_validate_index_content_bounds(page)
page.call(
"_cycle_index_option",
&"show",
1,
)
await process_frame
assert((page.get("_entry_buttons") as Dictionary).is_empty())
assert(
(page.get("_empty_state") as Label).text
== "No entries match this index."
)
page.call("_reset_index")
await process_frame
assert((page.get("_entry_buttons") as Dictionary).size() == 108)
page.call("_set_index_open", false, false)
await process_frame
assert(not index_body.visible)
assert(initial_detail_body.visible)
assert(
page.call("_entry_label_text", "flathead catfish")
== "flathead\ncatfish"
@ -458,8 +840,19 @@ func _validate_page() -> void:
(page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
await process_frame
var live_index_options := page.get("_index_options") as Dictionary
live_index_options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
page.call("_refresh_catalog")
await process_frame
assert(
(page.get("_entry_buttons") as Dictionary).keys()
== [&"bluegill"]
)
inventory.remove_catch_by_id(fish_catch.catch_id)
await process_frame
assert((page.get("_entry_buttons") as Dictionary).is_empty())
page.call("_reset_index")
await process_frame
assert(not _detail_text(page).contains("number owned"))
page.queue_free()
collection.queue_free()
@ -474,6 +867,54 @@ func _detail_text(page: LogbookPage) -> String:
return "\n".join(values)
func _validate_index_content_bounds(page: LogbookPage) -> void:
var body := page.get("_index_body") as VBoxContainer
assert(body != null and body.visible)
var right_page := body.get_parent() as Control
assert(right_page != null)
var page_rect := right_page.get_global_rect()
var body_rect := body.get_global_rect()
var protected_rect := Rect2(
page_rect.position + Vector2(
LogbookPage.INDEX_CONTENT_LEFT_INSET,
LogbookPage.INDEX_CONTENT_TOP_INSET,
),
page_rect.size - Vector2(
LogbookPage.INDEX_CONTENT_LEFT_INSET
+ LogbookPage.INDEX_CONTENT_RIGHT_INSET,
LogbookPage.INDEX_CONTENT_TOP_INSET
+ LogbookPage.INDEX_CONTENT_BOTTOM_INSET,
),
)
assert(
body_rect.position.x
>= page_rect.position.x + LogbookPage.INDEX_CONTENT_LEFT_INSET - 1.0
)
assert(
body_rect.end.x
<= page_rect.end.x - LogbookPage.INDEX_CONTENT_RIGHT_INSET + 1.0
)
assert(
body_rect.position.y >= page_rect.position.y,
"Index starts above the authored page: body=%s page=%s"
% [body_rect, page_rect],
)
assert(
body_rect.end.y <= page_rect.end.y,
"Index extends below the authored page: body=%s page=%s"
% [body_rect, page_rect],
)
var controls: Array = page.get("_index_controls") as Array
for value: Variant in controls:
var control := value as Control
if control != null and control.visible:
assert(
protected_rect.grow(2.0).encloses(control.get_global_rect()),
"Index control crossed the authored right-page boundary: %s"
% control.name,
)
func _descendant_label_text(parent: Node) -> String:
var values: PackedStringArray = []
for node: Node in parent.find_children("*", "Label", true, false):

View file

@ -260,6 +260,9 @@ func _ready() -> void:
_player_menu.controller_hotbar_management_ended.connect(
_on_controller_hotbar_management_ended
)
_player_menu.controller_text_entry_requested.connect(
request_controller_text_entry_for
)
_title_settings_panel.panel_visibility_changed.connect(
_on_settings_visibility_changed
)
@ -459,6 +462,7 @@ func setup(
discovery,
player_jobs,
world_time,
world_weather,
world_environment,
world_sun,
)

View file

@ -2,6 +2,9 @@ class_name LogbookCatalog
extends RefCounted
const FishDataType = preload("res://fish/fish_data.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const CalendarSeasonType = preload("res://world/calendar_season.gd")
enum Category {
FRESH_WATER,
@ -10,6 +13,66 @@ enum Category {
SHELLFISH,
}
enum DiscoveryFilter {
ALL,
CAUGHT,
MISSING,
INCOMPLETE,
}
enum SortMode {
CATALOG_NUMBER,
NAME,
RARITY,
CATCH_COUNT,
QUALITY_PROGRESS,
VALUE,
WEIGHT,
}
enum AvailabilityFilter {
ANY,
AVAILABLE_NOW,
}
enum SeasonFilter {
ANY,
CURRENT,
SPRING,
SUMMER,
FALL,
WINTER,
}
enum TimeFilter {
ANY,
CURRENT,
DAY,
NIGHT,
}
enum RarityFilter {
ANY,
COMMON,
UNCOMMON,
RARE,
EPIC,
LEGENDARY,
}
enum MethodFilter {
ANY,
FISHING,
NET,
DIGGING,
}
enum HoldingFilter {
ANY,
IN_COOLER,
FAVORITED,
}
static func category_for(fish: FishDataType) -> Category:
if fish == null:
@ -73,3 +136,278 @@ static func ordered_species(
return a.catalog_number < b.catalog_number
)
return ordered
static func filtered_species(
candidates: Array[FishDataType],
category: Category,
collection_log: CollectionLogType,
inventory: FishInventoryType,
options: Dictionary,
available_now_resolver: Callable = Callable(),
current_season: int = CalendarSeasonType.UNKNOWN,
current_is_night: bool = false,
) -> Array[FishDataType]:
var filtered: Array[FishDataType] = []
for fish: FishDataType in ordered_species(candidates):
if category_for(fish) != category:
continue
if not _matches_filters(
fish,
collection_log,
inventory,
options,
available_now_resolver,
current_season,
current_is_night,
):
continue
filtered.append(fish)
var sort_mode := int(options.get("sort", SortMode.CATALOG_NUMBER))
var descending := bool(options.get("descending", false))
filtered.sort_custom(
func(first: FishDataType, second: FishDataType) -> bool:
return _species_less_than(
first,
second,
collection_log,
sort_mode,
descending,
)
)
return filtered
static func available_groups(
candidates: Array[FishDataType],
category: Category,
_collection_log: CollectionLogType,
) -> Array[String]:
var groups: Array[String] = []
for fish: FishDataType in ordered_species(candidates):
if category_for(fish) != category:
continue
var group := _group_label(fish)
if not group.is_empty() and group not in groups:
groups.append(group)
groups.sort_custom(
func(first: String, second: String) -> bool:
return first.naturalnocasecmp_to(second) < 0
)
return groups
static func quality_progress(
collection_log: CollectionLogType,
fish_id: StringName,
) -> int:
if collection_log == null:
return 0
var mask: int = collection_log.get_quality_mask(fish_id)
var count: int = 0
while mask > 0:
count += mask & 1
mask >>= 1
return count
static func _matches_filters(
fish: FishDataType,
collection_log: CollectionLogType,
inventory: FishInventoryType,
options: Dictionary,
available_now_resolver: Callable,
current_season: int,
current_is_night: bool,
) -> bool:
var discovered := (
collection_log != null and collection_log.has_discovered(fish.id)
)
match int(options.get("show", DiscoveryFilter.ALL)):
DiscoveryFilter.CAUGHT:
if not discovered:
return false
DiscoveryFilter.MISSING:
if discovered:
return false
DiscoveryFilter.INCOMPLETE:
if (
not discovered
or collection_log.has_mastered(fish.id)
):
return false
var search_text := str(options.get("search", "")).strip_edges().to_lower()
if not search_text.is_empty():
var number_text := str(catalog_number(fish))
var number_query := search_text.trim_prefix("#")
var number_matches := (
number_query.is_valid_int()
and number_text == number_query
)
var identity_matches := false
if discovered:
identity_matches = " ".join([
fish.display_name,
_group_label(fish),
fish.get_habitat_label(),
]).to_lower().contains(search_text)
if not number_matches and not identity_matches:
return false
if (
int(options.get("availability", AvailabilityFilter.ANY))
== AvailabilityFilter.AVAILABLE_NOW
):
if available_now_resolver.is_valid():
var resolved: Variant = available_now_resolver.call(fish)
if typeof(resolved) != TYPE_BOOL or not bool(resolved):
return false
elif not _matches_season(fish, current_season):
return false
var season_filter := int(options.get("season", SeasonFilter.ANY))
if season_filter != SeasonFilter.ANY:
var requested_season := current_season
if season_filter >= SeasonFilter.SPRING:
requested_season = season_filter - SeasonFilter.SPRING
if not _matches_season(fish, requested_season):
return false
var time_filter := int(options.get("time", TimeFilter.ANY))
if time_filter != TimeFilter.ANY:
var requested_night := current_is_night
if time_filter == TimeFilter.DAY:
requested_night = false
elif time_filter == TimeFilter.NIGHT:
requested_night = true
if not _matches_time(fish, requested_night):
return false
var rarity_filter := int(options.get("rarity", RarityFilter.ANY))
if (
rarity_filter != RarityFilter.ANY
and fish.rarity != rarity_filter - RarityFilter.COMMON
):
return false
var method_filter := int(options.get("method", MethodFilter.ANY))
if (
method_filter != MethodFilter.ANY
and fish.collection_method != method_filter - MethodFilter.FISHING
):
return false
var holding_filter := int(options.get("holding", HoldingFilter.ANY))
if holding_filter != HoldingFilter.ANY:
var favorites_only := holding_filter == HoldingFilter.FAVORITED
if not _has_current_catch(inventory, fish.id, favorites_only):
return false
var requested_group := str(options.get("group", ""))
if (
not requested_group.is_empty()
and _group_label(fish).nocasecmp_to(requested_group) != 0
):
return false
return true
static func _species_less_than(
first: FishDataType,
second: FishDataType,
collection_log: CollectionLogType,
sort_mode: int,
descending: bool,
) -> bool:
var first_discovered := (
collection_log != null and collection_log.has_discovered(first.id)
)
var second_discovered := (
collection_log != null and collection_log.has_discovered(second.id)
)
if sort_mode != SortMode.CATALOG_NUMBER:
if first_discovered != second_discovered:
return first_discovered
if not first_discovered and not second_discovered:
return _catalog_less_than(first, second)
var comparison: int = 0
match sort_mode:
SortMode.NAME:
comparison = first.display_name.naturalnocasecmp_to(
second.display_name
)
SortMode.RARITY:
comparison = first.rarity - second.rarity
SortMode.CATCH_COUNT:
comparison = (
collection_log.get_catch_count(first.id)
- collection_log.get_catch_count(second.id)
)
SortMode.QUALITY_PROGRESS:
comparison = (
quality_progress(collection_log, first.id)
- quality_progress(collection_log, second.id)
)
SortMode.VALUE:
comparison = first.sell_value_max - second.sell_value_max
SortMode.WEIGHT:
comparison = _compare_floats(
first.get_maximum_weight(),
second.get_maximum_weight(),
)
_:
comparison = first.catalog_number - second.catalog_number
if comparison != 0:
return comparison > 0 if descending else comparison < 0
return _catalog_less_than(first, second)
static func _catalog_less_than(first: FishDataType, second: FishDataType) -> bool:
if first.catalog_number == second.catalog_number:
return String(first.id) < String(second.id)
return first.catalog_number < second.catalog_number
static func _compare_floats(first: float, second: float) -> int:
if is_equal_approx(first, second):
return 0
return -1 if first < second else 1
static func _matches_season(fish: FishDataType, season: int) -> bool:
return fish.is_available_in_season(season)
static func _matches_time(fish: FishDataType, requested_night: bool) -> bool:
if (
fish.collection_method != FishDataType.CollectionMethod.FISHING
or fish.availability == null
):
return true
return (
fish.availability.allow_night
if requested_night
else fish.availability.allow_day
)
static func _has_current_catch(
inventory: FishInventoryType,
fish_id: StringName,
favorites_only: bool,
) -> bool:
if inventory == null:
return false
for fish_catch in inventory.get_catches_by_fish_id(fish_id):
if fish_catch != null and (not favorites_only or fish_catch.is_favorited):
return true
return false
static func _group_label(fish: FishDataType) -> String:
if fish == null:
return ""
var group := String(fish.collection_group).strip_edges()
return group if not group.is_empty() else fish.get_habitat_label()

View file

@ -6,10 +6,21 @@ const FishDataType = preload("res://fish/fish_data.gd")
const FishQualityType = preload("res://fish/fish_quality.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
const WorldWeatherServiceType = preload(
"res://world/world_weather_service.gd"
)
const SILHOUETTE_SHADER: Shader = preload(
"res://ui/logbook_silhouette.gdshader"
)
const OrganizerTabType = preload("res://ui/components/organizer_tab.gd")
const ControllerFocusNavigationType = preload(
"res://ui/controller_focus_navigation.gd"
)
const NOTEPAD_INK_ACTION_SCENE: PackedScene = preload(
"res://ui/components/bubble_menu/notepad_ink_action.tscn"
)
const InventoryNotepadType = preload(
"res://ui/components/inventory_notepad.gd"
)
@ -48,6 +59,8 @@ const LOGBOOK_TAB_LEFT_INSET: float = 92.0
const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0)
const LOGBOOK_TAB_SEPARATION: float = 6.0
const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0
const INDEX_TAB_SIZE := Vector2(88.0, 38.0)
const INDEX_TAB_RIGHT_INSET: float = 72.0
const PAGE_CONTENT_SCALE: float = 0.97
const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0)
const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0)
@ -61,17 +74,53 @@ const DETAIL_SECTION_SEPARATION: int = 10
const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0)
const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0)
const DETAIL_STATS_ROW_SEPARATION: int = 8
const INDEX_OPTION_HEIGHT: float = 34.0
const INDEX_CONTENT_LEFT_INSET: float = 44.0
const INDEX_CONTENT_TOP_INSET: float = 4.0
const INDEX_CONTENT_RIGHT_INSET: float = 22.0
const INDEX_CONTENT_BOTTOM_INSET: float = 10.0
const INDEX_SHOW_LABELS := [
"all entries", "caught", "missing", "needs quality",
]
const INDEX_SORT_LABELS := [
"catalog no.", "name", "rarity", "number caught",
"quality progress", "value", "weight",
]
const INDEX_AVAILABILITY_LABELS := [
"any time", "available now",
]
const INDEX_SEASON_LABELS := [
"any season", "current season", "spring", "summer", "fall", "winter",
]
const INDEX_TIME_LABELS := [
"any time", "current time", "day", "night",
]
const INDEX_RARITY_LABELS := [
"any rarity", "common", "uncommon", "rare", "epic", "legendary",
]
const INDEX_METHOD_LABELS := [
"any method", "fishing", "netting", "digging",
]
const INDEX_HOLDING_LABELS := [
"any ownership", "in cooler", "favorited",
]
signal controller_text_entry_requested(control: Control)
enum ControllerZone {
TABS,
ENTRIES,
DETAILS,
INDEX,
OVERLAY,
}
var _collection_log: CollectionLogType
var _inventory: FishInventoryType
var _catalog: FishPoolType
var _fishing_spot: FishingSpotType
var _world_time: WorldTimeServiceType
var _world_weather: WorldWeatherServiceType
var _category: LogbookCatalog.Category = (
LogbookCatalog.Category.FRESH_WATER
)
@ -111,6 +160,31 @@ var _portrait_overlay_stats_view: Control
var _portrait_overlay_stats: GridContainer
var _overlay_return_focus: Control
var _controller_zone: ControllerZone = ControllerZone.TABS
var _index_tab: OrganizerTabType
var _index_body: VBoxContainer
var _index_search: LineEdit
var _index_summary: Label
var _index_note: Label
var _index_reset: Button
var _index_option_buttons: Dictionary[StringName, Button] = {}
var _index_controls: Array[Control] = []
var _index_open: bool = false
var _index_update_in_progress: bool = false
var _index_visible_count: int = 0
var _index_category_total: int = 0
var _index_options: Dictionary = {
"search": "",
"show": LogbookCatalog.DiscoveryFilter.ALL,
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
"descending": false,
"availability": LogbookCatalog.AvailabilityFilter.ANY,
"season": LogbookCatalog.SeasonFilter.ANY,
"time": LogbookCatalog.TimeFilter.ANY,
"rarity": LogbookCatalog.RarityFilter.ANY,
"method": LogbookCatalog.MethodFilter.ANY,
"holding": LogbookCatalog.HoldingFilter.ANY,
"group": "",
}
func _ready() -> void:
@ -124,10 +198,16 @@ func setup(
collection_log: CollectionLogType,
inventory: FishInventoryType,
catalog: FishPoolType,
fishing_spot: FishingSpotType = null,
world_time: WorldTimeServiceType = null,
world_weather: WorldWeatherServiceType = null,
) -> void:
_collection_log = collection_log
_inventory = inventory
_catalog = catalog
_fishing_spot = fishing_spot
_world_time = world_time
_world_weather = world_weather
if not _collection_log.fish_discovered.is_connected(
_on_fish_discovered
):
@ -138,12 +218,28 @@ func setup(
_collection_log.collection_changed.connect(_on_collection_changed)
if not _inventory.catches_changed.is_connected(_on_inventory_changed):
_inventory.catches_changed.connect(_on_inventory_changed)
if (
_world_time != null
and not _world_time.time_changed.is_connected(
_on_logbook_context_changed
)
):
_world_time.time_changed.connect(_on_logbook_context_changed)
_world_time.calendar_date_changed.connect(_on_logbook_context_changed)
if (
_world_weather != null
and not _world_weather.weather_changed.is_connected(
_on_logbook_context_changed
)
):
_world_weather.weather_changed.connect(_on_logbook_context_changed)
_refresh_catalog()
func set_compact_presentation(compact: bool) -> void:
_compact_presentation = compact
_apply_presentation_margins()
_apply_index_presentation()
func activate() -> void:
@ -180,6 +276,17 @@ func set_interactive(value: bool) -> void:
if _interactive and not tab.button_pressed
else Control.MOUSE_FILTER_IGNORE
)
if _index_tab != null:
_index_tab.focus_mode = (
Control.FOCUS_ALL
if _interactive and _controller_zone == ControllerZone.TABS
else Control.FOCUS_NONE
)
_index_tab.mouse_filter = (
Control.MOUSE_FILTER_STOP
if _interactive
else Control.MOUSE_FILTER_IGNORE
)
for entry: Button in _entry_buttons.values():
entry.focus_mode = (
Control.FOCUS_ALL
@ -199,6 +306,19 @@ func set_interactive(value: bool) -> void:
if _interactive and _controller_zone == ControllerZone.DETAILS
else Control.FOCUS_NONE
)
for index_control: Control in _index_controls:
if not is_instance_valid(index_control):
continue
index_control.focus_mode = (
Control.FOCUS_ALL
if _interactive and _controller_zone == ControllerZone.INDEX
else Control.FOCUS_NONE
)
index_control.mouse_filter = (
Control.MOUSE_FILTER_STOP
if _interactive and _index_open
else Control.MOUSE_FILTER_IGNORE
)
if _portrait_overlay_backdrop != null:
_portrait_overlay_backdrop.focus_mode = (
Control.FOCUS_ALL
@ -214,6 +334,9 @@ func focus_initial() -> void:
_portrait_overlay_backdrop.grab_focus()
return
if _controller_zone == ControllerZone.TABS:
if _index_open and _index_tab != null:
_index_tab.grab_focus()
return
var category_tab_index: int = _category_tab_categories.find(_category)
if category_tab_index >= 0:
_category_tabs[category_tab_index].grab_focus()
@ -222,6 +345,10 @@ func focus_initial() -> void:
if not _detail_buttons.is_empty():
_detail_buttons.front().grab_focus()
return
if _controller_zone == ControllerZone.INDEX:
if _index_search != null:
_index_search.grab_focus()
return
var selected := _entry_buttons.get(_selected_entry_key) as Button
if selected != null:
selected.grab_focus()
@ -247,6 +374,8 @@ func handle_controller_input(event: InputEvent) -> bool:
_hide_portrait_overlay()
ControllerZone.DETAILS:
_set_controller_zone(ControllerZone.ENTRIES)
ControllerZone.INDEX:
_set_index_open(false)
ControllerZone.ENTRIES:
_set_controller_zone(ControllerZone.TABS)
ControllerZone.TABS:
@ -254,15 +383,43 @@ func handle_controller_input(event: InputEvent) -> bool:
return true
if _controller_zone == ControllerZone.TABS:
if event.is_action_pressed("ui_left"):
if _index_open:
_set_index_open(false, false)
_category_tabs.back().grab_focus()
return true
_select_adjacent_controller_category(-1)
return true
if event.is_action_pressed("ui_right"):
if (
_category_tab_categories.find(_category)
== _category_tab_categories.size() - 1
):
_set_index_open(true, false)
_index_tab.grab_focus()
return true
_select_adjacent_controller_category(1)
return true
if accept_pressed:
if not _entry_buttons.is_empty():
if _index_open:
_set_controller_zone(ControllerZone.INDEX)
elif not _entry_buttons.is_empty():
_set_controller_zone(ControllerZone.ENTRIES)
return true
if _controller_zone == ControllerZone.INDEX:
var focus_owner := get_viewport().gui_get_focus_owner()
if accept_pressed and focus_owner == _index_search:
controller_text_entry_requested.emit(_index_search)
return true
if event.is_action_pressed("ui_left"):
var previous_key := _index_key_for_control(focus_owner)
if not previous_key.is_empty():
_cycle_index_option(previous_key, -1)
return true
if event.is_action_pressed("ui_right") or accept_pressed:
var next_key := _index_key_for_control(focus_owner)
if not next_key.is_empty():
_cycle_index_option(next_key, 1)
return true
if _controller_zone == ControllerZone.ENTRIES and accept_pressed:
if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty():
_set_controller_zone(ControllerZone.DETAILS)
@ -329,6 +486,20 @@ func _build_interface() -> void:
tabs.add_child(tab)
_category_tabs.append(tab)
_category_tab_categories.append(category)
_index_tab = OrganizerTabType.new()
_index_tab.name = "CatalogIndexTab"
_index_tab.anchor_left = 1.0
_index_tab.anchor_right = 1.0
_index_tab.offset_left = -INDEX_TAB_RIGHT_INSET - INDEX_TAB_SIZE.x
_index_tab.offset_top = 37.0
_index_tab.offset_right = -INDEX_TAB_RIGHT_INSET
_index_tab.offset_bottom = 37.0 + INDEX_TAB_SIZE.y
_index_tab.text = "index"
_index_tab.palette_index = 0
_index_tab.tooltip_text = "Open catalog index"
_index_tab.accessibility_name = "Catalog index"
_index_tab.pressed.connect(_toggle_index_page)
stack.add_child(_index_tab)
_configure_category_focus()
var book := Control.new()
@ -418,9 +589,176 @@ func _build_interface() -> void:
right_page.add_child(_detail_body)
_apply_page_content_scale(_detail_body)
_show_no_selection()
_build_index_page(right_page)
_build_portrait_overlay()
func _build_index_page(right_page: Control) -> void:
_index_body = VBoxContainer.new()
_index_body.name = "CatalogIndex"
_index_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_index_body.offset_left = INDEX_CONTENT_LEFT_INSET
_index_body.offset_top = INDEX_CONTENT_TOP_INSET
_index_body.offset_right = -INDEX_CONTENT_RIGHT_INSET
_index_body.offset_bottom = -INDEX_CONTENT_BOTTOM_INSET
_index_body.add_theme_constant_override("separation", 4)
_index_body.visible = false
right_page.add_child(_index_body)
_apply_page_content_scale(_index_body)
_apply_index_presentation()
var heading := _label("catalog index", 28)
heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_index_body.add_child(heading)
_index_summary = _field_label("", 14)
_index_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_index_summary.add_theme_color_override("font_color", MUTED_INK)
_index_body.add_child(_index_summary)
var search_stack := VBoxContainer.new()
search_stack.add_theme_constant_override("separation", 0)
_index_body.add_child(search_stack)
var search_label := _field_label("find by name, group, or catalog no.", 13)
search_label.add_theme_color_override("font_color", MUTED_INK)
search_stack.add_child(search_label)
_index_search = LineEdit.new()
_index_search.name = "CatalogIndexSearch"
_index_search.custom_minimum_size.y = 34.0
_index_search.placeholder_text = "write here..."
_index_search.max_length = 48
_index_search.add_theme_font_override(
"font", InventoryNotepadType.HANDWRITTEN_FONT
)
_index_search.add_theme_font_size_override("font_size", 18)
_index_search.add_theme_color_override("font_color", INK)
_index_search.add_theme_color_override("caret_color", INK)
_index_search.add_theme_color_override(
"font_placeholder_color", Color(MUTED_INK, 0.72)
)
for style_name: StringName in [
&"normal", &"focus", &"read_only",
]:
_index_search.add_theme_stylebox_override(
style_name,
_index_search_style(style_name == &"focus"),
)
_index_search.text_changed.connect(_on_index_search_changed)
search_stack.add_child(_index_search)
_index_controls.append(_index_search)
var options_grid := GridContainer.new()
options_grid.columns = 2
options_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
options_grid.add_theme_constant_override("h_separation", 18)
options_grid.add_theme_constant_override("v_separation", 2)
_index_body.add_child(options_grid)
for descriptor: Dictionary in [
{"key": &"show", "label": "show"},
{"key": &"rarity", "label": "rarity"},
{"key": &"sort", "label": "sort by"},
{"key": &"availability", "label": "availability"},
{"key": &"descending", "label": "order"},
{"key": &"season", "label": "season"},
{"key": &"method", "label": "method"},
{"key": &"time", "label": "time of day"},
{"key": &"holding", "label": "ownership"},
{"key": &"group", "label": "collection group"},
]:
options_grid.add_child(_make_index_option(
descriptor["key"],
str(descriptor["label"]),
))
var footer := HBoxContainer.new()
footer.alignment = BoxContainer.ALIGNMENT_CENTER
_index_body.add_child(footer)
_index_reset = NOTEPAD_INK_ACTION_SCENE.instantiate() as Button
_index_reset.name = "CatalogIndexReset"
_index_reset.custom_minimum_size = Vector2(126.0, 34.0)
_index_reset.text = "clear index"
_apply_index_value_font(_index_reset)
_index_reset.tooltip_text = "Clear every catalog filter"
_index_reset.accessibility_name = "Clear catalog index filters"
_index_reset.pressed.connect(_reset_index)
footer.add_child(_index_reset)
_index_controls.append(_index_reset)
_index_note = _field_label(
"silhouettes keep names hidden in filtered results",
12,
)
_index_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_index_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_index_note.add_theme_color_override("font_color", Color(MUTED_INK, 0.82))
_index_body.add_child(_index_note)
_update_index_controls()
_configure_index_focus.call_deferred()
func _make_index_option(key: StringName, label_text: String) -> Control:
var stack := VBoxContainer.new()
stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL
stack.add_theme_constant_override("separation", -3)
var label := _field_label(label_text, 12)
label.add_theme_color_override("font_color", MUTED_INK)
stack.add_child(label)
var button := NOTEPAD_INK_ACTION_SCENE.instantiate() as Button
button.name = "CatalogIndex%s" % String(key).capitalize().replace(" ", "")
button.custom_minimum_size = Vector2(0.0, INDEX_OPTION_HEIGHT)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.clip_text = true
_apply_index_value_font(button)
button.tooltip_text = "Change %s; use left or right on a controller" % label_text
button.pressed.connect(_cycle_index_option.bind(key, 1))
button.gui_input.connect(_on_index_option_gui_input.bind(key))
stack.add_child(button)
_index_option_buttons[key] = button
_index_controls.append(button)
return stack
func _apply_index_value_font(button: Button) -> void:
button.add_theme_font_override(
"font", InventoryNotepadType.HANDWRITTEN_FONT
)
button.add_theme_font_size_override("font_size", 18)
for color_name: StringName in [
&"font_color",
&"font_hover_color",
&"font_focus_color",
&"font_pressed_color",
]:
button.add_theme_color_override(color_name, INK)
func _index_search_style(focused: bool) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(1.0, 0.97, 0.82, 0.12 if focused else 0.0)
style.border_color = Color(INK, 0.72 if focused else 0.42)
style.border_width_bottom = 2
style.content_margin_left = 6
style.content_margin_right = 22
style.content_margin_top = 2
style.content_margin_bottom = 3
return style
func _configure_index_focus() -> void:
var controls: Array[Control] = []
for control: Control in _index_controls:
if control != null and is_instance_valid(control):
controls.append(control)
if not controls.is_empty():
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
func _apply_index_presentation() -> void:
if _index_body == null:
return
var scale_value := 0.82 if _compact_presentation else PAGE_CONTENT_SCALE
_index_body.scale = Vector2.ONE * scale_value
func _apply_presentation_margins() -> void:
if _outer_margin == null:
return
@ -548,11 +886,30 @@ func _refresh_catalog() -> void:
var preserved_entry_key: StringName = _selected_entry_key
_clear_entries()
var species: Array[FishDataType] = []
var category_total: int = 0
if _catalog != null:
species = LogbookCatalog.ordered_species(_catalog.candidates)
for fish: FishDataType in LogbookCatalog.ordered_species(
_catalog.candidates
):
if LogbookCatalog.category_for(fish) == _category:
category_total += 1
var available_now_resolver := Callable()
if _fishing_spot != null and is_instance_valid(_fishing_spot):
available_now_resolver = Callable(
_fishing_spot,
"is_species_available_now",
)
species = LogbookCatalog.filtered_species(
_catalog.candidates,
_category,
_collection_log,
_inventory,
_index_options,
available_now_resolver,
_current_season(),
_current_is_night(),
)
for fish: FishDataType in species:
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 = (
@ -572,7 +929,11 @@ func _refresh_catalog() -> void:
)
)
_catalog_grid.add_child(entry)
_empty_state.text = LogbookCatalog.empty_state(_category)
_empty_state.text = (
"No entries match this index."
if _index_has_active_criteria()
else LogbookCatalog.empty_state(_category)
)
_empty_state.visible = _entry_buttons.is_empty()
_catalog_scroll.visible = not _entry_buttons.is_empty()
if (
@ -589,10 +950,269 @@ func _refresh_catalog() -> void:
_selected_entry_key = StringName()
_refresh_selection_styles()
_refresh_details(false)
_update_index_controls(species.size(), category_total)
set_interactive(_interactive)
_refresh_catalog_scroll_indicators.call_deferred()
func _toggle_index_page() -> void:
_set_index_open(not _index_open)
func _set_index_open(open: bool, restore_focus: bool = true) -> void:
_index_open = open
if _index_tab != null:
_index_tab.set_selected(open)
_index_tab.tooltip_text = (
"Return to catch record" if open else "Open catalog index"
)
if _index_body != null:
_index_body.visible = open
if _detail_body != null:
_detail_body.visible = not open
if not open:
_refresh_details(false)
_update_index_controls()
if restore_focus and _active and _interactive:
if open:
_controller_zone = ControllerZone.INDEX
set_interactive(true)
_index_search.grab_focus()
elif _controller_zone == ControllerZone.INDEX:
_controller_zone = ControllerZone.TABS
set_interactive(true)
_index_tab.grab_focus()
func _on_index_search_changed(value: String) -> void:
if _index_update_in_progress:
return
_index_options["search"] = value
_refresh_catalog()
func _cycle_index_option(key: StringName, direction: int = 1) -> void:
if not _index_options.has(key):
return
if key == &"descending":
_index_options[key] = not bool(_index_options[key])
elif key == &"group":
var groups: Array[String] = [""]
if _catalog != null:
groups.append_array(LogbookCatalog.available_groups(
_catalog.candidates,
_category,
_collection_log,
))
var current_index := groups.find(str(_index_options[key]))
if current_index < 0:
current_index = 0
_index_options[key] = groups[wrapi(
current_index + direction,
0,
groups.size(),
)]
else:
var option_count := _index_option_count(key)
if option_count <= 0:
return
_index_options[key] = wrapi(
int(_index_options[key]) + direction,
0,
option_count,
)
_refresh_catalog()
func _on_index_option_gui_input(
event: InputEvent,
key: StringName,
) -> void:
var mouse_event := event as InputEventMouseButton
if mouse_event == null or not mouse_event.pressed:
return
if mouse_event.button_index in [MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_UP]:
_cycle_index_option(key, -1)
get_viewport().set_input_as_handled()
elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_cycle_index_option(key, 1)
get_viewport().set_input_as_handled()
func _index_key_for_control(control: Control) -> StringName:
for key: StringName in _index_option_buttons:
if _index_option_buttons[key] == control:
return key
return StringName()
func _reset_index() -> void:
_index_options = {
"search": "",
"show": LogbookCatalog.DiscoveryFilter.ALL,
"sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
"descending": false,
"availability": LogbookCatalog.AvailabilityFilter.ANY,
"season": LogbookCatalog.SeasonFilter.ANY,
"time": LogbookCatalog.TimeFilter.ANY,
"rarity": LogbookCatalog.RarityFilter.ANY,
"method": LogbookCatalog.MethodFilter.ANY,
"holding": LogbookCatalog.HoldingFilter.ANY,
"group": "",
}
_index_update_in_progress = true
_index_search.text = ""
_index_update_in_progress = false
_refresh_catalog()
func _update_index_controls(
visible_count: int = -1,
category_total: int = -1,
) -> void:
if visible_count >= 0:
_index_visible_count = visible_count
if category_total >= 0:
_index_category_total = category_total
if _index_summary != null:
_index_summary.text = "%d shown of %d" % [
_index_visible_count,
_index_category_total,
]
for key: StringName in _index_option_buttons:
var button := _index_option_buttons[key]
if button == null:
continue
button.text = _index_option_label(key)
button.accessibility_name = "%s: %s" % [
String(key).replace("_", " "),
button.text,
]
if _index_tab != null:
var marked := _index_has_active_criteria()
_index_tab.text = "index •" if marked else "index"
_index_tab.accessibility_name = (
"Catalog index, filters active" if marked else "Catalog index"
)
if _index_reset != null:
_index_reset.disabled = not _index_has_active_criteria()
func _index_option_label(key: StringName) -> String:
match key:
&"show":
return INDEX_SHOW_LABELS[int(_index_options[key])]
&"sort":
return INDEX_SORT_LABELS[int(_index_options[key])]
&"availability":
return INDEX_AVAILABILITY_LABELS[int(_index_options[key])]
&"season":
return INDEX_SEASON_LABELS[int(_index_options[key])]
&"time":
return INDEX_TIME_LABELS[int(_index_options[key])]
&"rarity":
return INDEX_RARITY_LABELS[int(_index_options[key])]
&"method":
return INDEX_METHOD_LABELS[int(_index_options[key])]
&"holding":
return INDEX_HOLDING_LABELS[int(_index_options[key])]
&"group":
var group := str(_index_options[key])
return group if not group.is_empty() else "any group"
&"descending":
var descending := bool(_index_options[key])
match int(_index_options["sort"]):
LogbookCatalog.SortMode.NAME:
return "za" if descending else "az"
LogbookCatalog.SortMode.CATALOG_NUMBER:
return "highlow" if descending else "lowhigh"
LogbookCatalog.SortMode.RARITY:
return (
"rarecommon" if descending else "commonrare"
)
LogbookCatalog.SortMode.VALUE, LogbookCatalog.SortMode.WEIGHT:
return "highlow" if descending else "lowhigh"
_:
return "most first" if descending else "least first"
return "any"
func _index_option_count(key: StringName) -> int:
match key:
&"show":
return INDEX_SHOW_LABELS.size()
&"sort":
return INDEX_SORT_LABELS.size()
&"availability":
return INDEX_AVAILABILITY_LABELS.size()
&"season":
return INDEX_SEASON_LABELS.size()
&"time":
return INDEX_TIME_LABELS.size()
&"rarity":
return INDEX_RARITY_LABELS.size()
&"method":
return INDEX_METHOD_LABELS.size()
&"holding":
return INDEX_HOLDING_LABELS.size()
return 0
func _index_has_active_criteria() -> bool:
return (
not str(_index_options["search"]).is_empty()
or int(_index_options["show"])
!= LogbookCatalog.DiscoveryFilter.ALL
or int(_index_options["sort"])
!= LogbookCatalog.SortMode.CATALOG_NUMBER
or bool(_index_options["descending"])
or int(_index_options["availability"])
!= LogbookCatalog.AvailabilityFilter.ANY
or int(_index_options["season"])
!= LogbookCatalog.SeasonFilter.ANY
or int(_index_options["time"])
!= LogbookCatalog.TimeFilter.ANY
or int(_index_options["rarity"])
!= LogbookCatalog.RarityFilter.ANY
or int(_index_options["method"])
!= LogbookCatalog.MethodFilter.ANY
or int(_index_options["holding"])
!= LogbookCatalog.HoldingFilter.ANY
or not str(_index_options["group"]).is_empty()
)
func _current_season() -> int:
return (
_world_time.get_season()
if _world_time != null
else CalendarSeason.UNKNOWN
)
func _current_is_night() -> bool:
return _world_time != null and _world_time.is_night_period()
func notify_availability_context_changed() -> void:
_on_logbook_context_changed()
func _on_logbook_context_changed(
_first: Variant = null,
_second: Variant = null,
) -> void:
if (
int(_index_options["availability"])
== LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
or int(_index_options["season"])
== LogbookCatalog.SeasonFilter.CURRENT
or int(_index_options["time"])
== LogbookCatalog.TimeFilter.CURRENT
):
_refresh_catalog()
func _make_entry(fish: FishDataType, discovered: bool) -> Button:
var entry := Button.new()
entry.custom_minimum_size = CATALOG_ENTRY_SIZE
@ -663,8 +1283,11 @@ func _add_entry_content(
func _select_category(category: LogbookCatalog.Category) -> void:
if category == _category:
if _index_open:
_set_index_open(false)
return
_category = category
_set_index_open(false, false)
_selected_id = StringName()
_selected_entry_key = StringName()
for index: int in _category_tabs.size():
@ -683,6 +1306,11 @@ func _configure_category_focus() -> void:
tab.focus_neighbor_right = tab.get_path_to(
_category_tabs[mini(index + 1, _category_tabs.size() - 1)]
)
if not _category_tabs.is_empty() and _index_tab != null:
var final_tab: Button = _category_tabs.back()
final_tab.focus_neighbor_right = final_tab.get_path_to(_index_tab)
_index_tab.focus_neighbor_left = _index_tab.get_path_to(final_tab)
_index_tab.focus_neighbor_right = _index_tab.get_path_to(_index_tab)
func _animate_tab_entry() -> void:
@ -690,11 +1318,15 @@ func _animate_tab_entry() -> void:
(_category_tabs[index] as OrganizerTabType).animate_entrance(
float(index) * 0.025
)
if _index_tab != null:
_index_tab.animate_entrance(float(_category_tabs.size()) * 0.025)
func _settle_tabs_for_close() -> void:
for tab: OrganizerTabType in _category_tabs:
tab.settle_for_close()
if _index_tab != null:
_index_tab.settle_for_close()
func _begin_category_transition() -> void:
@ -750,9 +1382,12 @@ func _set_category_content_alpha(alpha: float) -> void:
_empty_state.modulate.a = alpha
if _detail_body != null:
_detail_body.modulate.a = alpha
if _index_body != null:
_index_body.modulate.a = alpha
func _select_entry(entry_key: StringName, fish_id: StringName) -> void:
_set_index_open(false, false)
if fish_id.is_empty():
_selected_id = StringName()
_selected_entry_key = entry_key
@ -1284,7 +1919,9 @@ func _on_collection_changed() -> void:
func _on_inventory_changed() -> void:
_refresh_details(false)
# Ownership filters and favorite-backed views depend on the live cooler,
# while rebuilding the catalog also keeps the selected detail current.
_refresh_catalog()
func _unknown_selection_key(fish: FishDataType) -> StringName:

View file

@ -105,6 +105,7 @@ signal controller_hotbar_management_requested(initial_slot: int)
signal controller_hotbar_management_ended
signal menu_exit_started
signal shop_cooler_modal_changed(is_open: bool)
signal controller_text_entry_requested(control: Control)
enum Section {
COOLER,
@ -409,6 +410,9 @@ func _ready() -> void:
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
)
_catalog_logbook.controller_text_entry_requested.connect(
controller_text_entry_requested.emit
)
_the_net_tab.pressed.connect(_show_section.bind(Section.NET))
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
@ -556,6 +560,7 @@ func setup(
discovery: DiscoveryClient,
player_jobs: PlayerJobService,
world_time: WorldTimeService,
world_weather: WorldWeatherService,
world_environment: WorldEnvironment,
world_sun: DirectionalLight3D,
) -> void:
@ -589,7 +594,14 @@ func setup(
world_sun,
)
_players_page.setup(network_player_list, discovery)
_catalog_logbook.setup(collection_log, inventory, catalog)
_catalog_logbook.setup(
collection_log,
inventory,
catalog,
fishing_spot,
world_time,
world_weather,
)
_the_net_page.setup(player_jobs, world_time)
_network_mail_service.unread_count_changed.connect(
_on_mail_unread_count_changed
@ -2779,6 +2791,7 @@ func _toggle_active_tackle() -> void:
func _on_active_bait_changed(_item_id: StringName) -> void:
_update_tackle_detail()
_catalog_logbook.notify_availability_context_changed()
func _on_active_lure_changed(_item_id: StringName) -> void:
@ -3733,6 +3746,7 @@ func _refresh_all() -> void:
func _on_bag_changed() -> void:
_refresh_bag()
_refresh_tackle_box()
_catalog_logbook.notify_availability_context_changed()
func _on_hotbar_changed() -> void: