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, SALT_WATER, OTHER, 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: return Category.OTHER if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH: return Category.SHELLFISH match fish.get_primary_water_type(): WaterType.Type.FRESH_WATER: return Category.FRESH_WATER WaterType.Type.SALT_WATER: return Category.SALT_WATER _: return Category.OTHER static func category_label(category: Category) -> String: match category: Category.FRESH_WATER: return "Fresh Water" Category.SALT_WATER: return "Salt Water" Category.SHELLFISH: return "Shellfish" _: return "Insects" static func empty_state(category: Category) -> String: match category: Category.FRESH_WATER: return "No freshwater catches cataloged yet." Category.SALT_WATER: return "No saltwater catches cataloged yet." Category.SHELLFISH: return "No shellfish cataloged yet." _: return "No entries available." static func catalog_number(fish: FishDataType) -> int: return fish.catalog_number if fish != null else 0 static func facts_for(fish: FishDataType) -> String: if fish == null or fish.logbook_fact.strip_edges().is_empty(): return "unknown" return fish.logbook_fact static func ordered_species( candidates: Array[FishDataType], ) -> Array[FishDataType]: var ordered: Array[FishDataType] = [] for fish: FishDataType in candidates: if fish != null and fish.active and not fish.id.is_empty(): ordered.append(fish) ordered.sort_custom( func(a: FishDataType, b: FishDataType) -> bool: if a.catalog_number == b.catalog_number: return String(a.id) < String(b.id) 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()