413 lines
10 KiB
GDScript
413 lines
10 KiB
GDScript
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()
|