coh-content-db 2.0.0-rc.4 → 2.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +31 -7
  2. package/dist/coh-content-db.d.ts +353 -185
  3. package/dist/coh-content-db.js +460 -300
  4. package/dist/coh-content-db.js.map +1 -1
  5. package/dist/coh-content-db.mjs +448 -294
  6. package/dist/coh-content-db.mjs.map +1 -1
  7. package/package.json +1 -4
  8. package/src/main/api/alternate-data.ts +2 -2
  9. package/src/main/api/badge-data.ts +21 -19
  10. package/src/main/api/badge-requirement-data.ts +82 -0
  11. package/src/main/api/badge-requirement-type.ts +11 -0
  12. package/src/main/api/change.ts +5 -2
  13. package/src/main/api/contact-data.ts +46 -0
  14. package/src/main/api/content-bundle.ts +12 -7
  15. package/src/main/api/markdown-string.ts +4 -0
  16. package/src/main/api/zone-data.ts +20 -0
  17. package/src/main/changelog.ts +7 -2
  18. package/src/main/db/alignments.ts +17 -0
  19. package/src/main/db/alternates.ts +8 -14
  20. package/src/main/db/badge-index.ts +93 -0
  21. package/src/main/db/badge-requirement.ts +102 -0
  22. package/src/main/db/badge-search-options.ts +51 -0
  23. package/src/main/db/badge.ts +55 -48
  24. package/src/main/db/bundle-metadata.ts +5 -6
  25. package/src/main/db/coh-content-database.ts +65 -40
  26. package/src/main/db/contact.ts +59 -0
  27. package/src/main/db/paged.ts +7 -0
  28. package/src/main/db/zone.ts +28 -0
  29. package/src/main/index.ts +15 -11
  30. package/src/main/util.ts +68 -7
  31. package/src/test/api/alignments.test.ts +40 -0
  32. package/src/test/api/badge-data.fixture.ts +9 -7
  33. package/src/test/api/badge-requirement-data.fixture.ts +17 -0
  34. package/src/test/api/badge-requirement-type.test.ts +31 -0
  35. package/src/test/api/contact-data.fixture.ts +13 -0
  36. package/src/test/api/content-bundle.fixture.ts +2 -2
  37. package/src/test/api/content-bundle.test.ts +1 -1
  38. package/src/test/api/zone-data.fixture.ts +8 -0
  39. package/src/test/db/alternates.test.ts +16 -74
  40. package/src/test/db/badge-index.test.ts +520 -0
  41. package/src/test/db/badge-requirement.test.ts +180 -0
  42. package/src/test/db/badge.test.ts +190 -15
  43. package/src/test/db/coh-content-database.test.ts +125 -18
  44. package/src/test/db/contact.test.ts +96 -0
  45. package/src/test/db/zone.test.ts +36 -0
  46. package/src/test/index.test.ts +6 -2
  47. package/src/test/util.test.ts +91 -18
  48. package/src/main/api/badge-partial-data.ts +0 -65
  49. package/src/main/api/badge-partial-type.ts +0 -8
  50. package/src/main/api/game-map-data.ts +0 -26
  51. package/src/main/api/vidiot-map-data.ts +0 -18
  52. package/src/main/api/vidiot-map-point-of-interest-data.ts +0 -30
  53. package/src/main/db/badge-partial.ts +0 -35
  54. package/src/main/db/badge-search-document.ts +0 -16
  55. package/src/main/db/game-map.ts +0 -33
  56. package/src/main/db/vidiot-map-point-of-interest.ts +0 -40
  57. package/src/main/db/vidiot-map.ts +0 -25
  58. package/src/test/api/badge-partial-data.fixture.ts +0 -17
  59. package/src/test/api/badge-partial-type.test.ts +0 -31
  60. package/src/test/api/game-map-data.fixture.ts +0 -10
  61. package/src/test/api/vidiot-map-point-of-interest.fixture.ts +0 -10
  62. package/src/test/api/vidiot-map.fixture.ts +0 -9
  63. package/src/test/db/badge-search-document.test.ts +0 -35
  64. package/src/test/db/coh-content-database-search.test.ts +0 -119
@@ -13,7 +13,7 @@ export class Alternates<T> {
13
13
  this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
14
14
  }
15
15
 
16
- getValue(alignment?: Alignment | string, sex?: Sex | string): T | undefined {
16
+ getValue(alignment?: Alignment, sex?: Sex): T | undefined {
17
17
  for (let index = this.#sortedValues.length; index--;) {
18
18
  const entry = this.#sortedValues[index]
19
19
  if ((entry.alignment === undefined || entry.alignment === alignment)
@@ -21,14 +21,14 @@ export class Alternates<T> {
21
21
  ) return entry.value
22
22
  }
23
23
 
24
- return this.default
24
+ return this.default?.value
25
25
  }
26
26
 
27
27
  /**
28
28
  * Get the default value for this list of alternates, the value with the highest priority and lowest specificity.
29
29
  */
30
- get default(): T | undefined {
31
- return this.#sortedValues[0]?.value
30
+ get default(): AlternateData<T> | undefined {
31
+ return this.#sortedValues[0]
32
32
  }
33
33
 
34
34
  /**
@@ -60,7 +60,7 @@ export class Alternates<T> {
60
60
  return String(a.value).localeCompare(String(b.value))
61
61
  }
62
62
 
63
- #compareAlignment(a: Alignment | string | undefined, b: Alignment | string | undefined): number {
63
+ #compareAlignment(a?: Alignment, b?: Alignment): number {
64
64
  if (a === b) return 0
65
65
  if (a === undefined && b !== undefined) return -1
66
66
  if (b === undefined && a !== undefined) return 1
@@ -68,13 +68,10 @@ export class Alternates<T> {
68
68
  const aSort = a === undefined ? -1 : ALIGNMENT_SORT[a] ?? -1 // Unknown values get -1 priority
69
69
  const bSort = b === undefined ? -1 : ALIGNMENT_SORT[b] ?? -1
70
70
 
71
- if (aSort !== bSort) return bSort - aSort
72
-
73
- // Unknown values (not in ALIGNMENT_SORT) are sorted alphabetically
74
- return a?.localeCompare(b ?? '') ?? 0
71
+ return bSort - aSort
75
72
  }
76
73
 
77
- #compareSex(a?: Sex | string | undefined, b?: Sex | string | undefined): number {
74
+ #compareSex(a?: Sex, b?: Sex): number {
78
75
  if (a === b) return 0
79
76
  if (a === undefined && b !== undefined) return -1
80
77
  if (b === undefined && a !== undefined) return 1
@@ -82,9 +79,6 @@ export class Alternates<T> {
82
79
  const aSort = SEX_SORT[a ?? -1] ?? -1 // Unknown values get -1 priority
83
80
  const bSort = SEX_SORT[b ?? -1] ?? -1
84
81
 
85
- if (aSort !== bSort) return bSort - aSort
86
-
87
- // Unknown values (not in SEX_SORT) are sorted alphabetically
88
- return a?.localeCompare(b ?? '') ?? 0
82
+ return bSort - aSort
89
83
  }
90
84
  }
@@ -0,0 +1,93 @@
1
+ import { Badge } from './badge'
2
+ import { BadgeSearchOptions } from './badge-search-options'
3
+ import { Zone } from './zone'
4
+ import { Paged } from './paged'
5
+
6
+ export class BadgeIndex {
7
+ readonly #badges: Badge[] = []
8
+ readonly #badgeIndex: Record<string, Badge> = {}
9
+
10
+ readonly #zoneOrder: Record<string, number> = {}
11
+
12
+ constructor(badges: Badge[], zones?: Zone[]) {
13
+ this.#zoneOrder = Object.fromEntries(
14
+ zones
15
+ ?.sort((a, b) => a.name.localeCompare(b.name))
16
+ ?.map((x, index) => [x.key, index]) ?? [],
17
+ )
18
+
19
+ this.#badges = badges
20
+ for (const badge of badges) {
21
+ if (this.#badgeIndex[badge.key] !== undefined) throw new Error(`Duplicate badge key [${badge.key}]`)
22
+ this.#badgeIndex[badge.key] = badge
23
+ }
24
+ }
25
+
26
+ getBadge(key: string): Badge {
27
+ const result = this.#badgeIndex[key]
28
+ if (result === undefined) throw new Error(`Unknown badge key [${key}]`)
29
+ return result
30
+ }
31
+
32
+ badgeExists(key: string): boolean {
33
+ return !!this.#badgeIndex[key]
34
+ }
35
+
36
+ searchBadges(options?: BadgeSearchOptions): Paged<Badge> {
37
+ const filtered = (options?.query || options?.filter)
38
+ ? this.#badges.filter(badge => this.#satisfiesQueryPredicate(badge, options?.query) && this.#satisfiesFilterPredicate(badge, options?.filter))
39
+ : this.#badges
40
+
41
+ const totalPages = options?.pageSize ? Math.ceil(filtered.length / (options?.pageSize)) : 1
42
+ const page = Math.max(1, Math.min(totalPages, options?.page ?? 1))
43
+ const paged = options?.pageSize ? filtered.slice((page - 1) * options.pageSize, page * options?.pageSize) : filtered
44
+
45
+ const sorted = this.#sort(paged, options?.sort)
46
+
47
+ return {
48
+ items: sorted,
49
+ page: page,
50
+ pageSize: options?.pageSize,
51
+ totalItems: filtered.length,
52
+ totalPages: totalPages,
53
+ }
54
+ }
55
+
56
+ #satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
57
+ const queryString = query?.str?.toLowerCase() ?? ''
58
+ return !!(((query?.on?.name ?? true) && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
59
+ || (query?.on?.badgeText && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
60
+ || (query?.on?.acquisition && badge.acquisition?.toLowerCase().includes(queryString))
61
+ || (query?.on?.effect && badge.effect?.toLowerCase().includes(queryString))
62
+ || (query?.on?.notes && badge.notes?.toLowerCase().includes(queryString))
63
+ || (query?.on?.setTitle && (badge.setTitle?.id?.toString().includes(queryString) || badge.setTitle?.praetorianId?.toString().includes(queryString))))
64
+ }
65
+
66
+ #satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
67
+ return (!filter?.type || badge.type === filter.type)
68
+ && (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
69
+ && (!filter?.alignment || badge.alignment.items.includes(filter.alignment))
70
+ }
71
+
72
+ #sort(badges: Badge[], sort?: BadgeSearchOptions['sort']): Badge[] {
73
+ if (!sort) return badges
74
+ const ascending = sort.dir !== 'DESC'
75
+
76
+ if (!sort.by || sort.by === 'CANONICAL') return sort.dir === 'DESC' ? badges.reverse() : badges
77
+
78
+ if (sort.by === 'BADGE_NAME') return ascending
79
+ ? badges.sort((a, b) => a.name.default?.value.localeCompare(b.name.default?.value ?? '') ?? 0)
80
+ : badges.sort((a, b) => b.name.default?.value.localeCompare(a.name.default?.value ?? '') ?? 0)
81
+
82
+ return badges.sort((a, b) => {
83
+ const aIndex = this.#zoneOrder[a.zoneKey ?? '']
84
+ const bIndex = this.#zoneOrder[b.zoneKey ?? '']
85
+
86
+ if (aIndex === bIndex) return 0
87
+ if (aIndex === undefined) return ascending ? 1 : -1
88
+ if (bIndex === undefined) return ascending ? -1 : 1
89
+
90
+ return ascending ? aIndex - bIndex : bIndex - aIndex
91
+ })
92
+ }
93
+ }
@@ -0,0 +1,102 @@
1
+ import { BadgeRequirementData } from '../api/badge-requirement-data'
2
+ import { PlaqueType } from '../api/plaque-type'
3
+ import { BadgeRequirementType } from '../api/badge-requirement-type'
4
+ import { EnhancementCategory } from '../api/enhancement-category'
5
+ import { Key } from './key'
6
+ import { MarkdownString } from '../api/markdown-string'
7
+ import { Link } from '../api/link'
8
+
9
+ export class BadgeRequirement {
10
+ /**
11
+ * Key.
12
+ */
13
+ readonly key: string
14
+
15
+ /**
16
+ * Type of requirement.
17
+ */
18
+ readonly type: BadgeRequirementType
19
+
20
+ /**
21
+ * Zone the requirement is located in.
22
+ */
23
+ readonly zoneKey?: string
24
+
25
+ /**
26
+ * /loc coordinates.
27
+ */
28
+ readonly loc?: number[]
29
+
30
+ /**
31
+ * Is it a wall plaque or a physical monument?
32
+ */
33
+ readonly plaqueType?: PlaqueType
34
+
35
+ /**
36
+ * Plaque inscription.
37
+ */
38
+ readonly plaqueInscription?: string
39
+
40
+ /**
41
+ * The number or letter the plaque appears as on Vidiot Maps.
42
+ */
43
+ readonly vidiotMapKey?: string
44
+
45
+ /**
46
+ * The key of the badge for this requirement.
47
+ */
48
+ readonly badgeKey?: string
49
+
50
+ /**
51
+ * Mission name.
52
+ */
53
+ readonly missionName?: string
54
+
55
+ /**
56
+ * {@link Contact} key for the story arc.
57
+ */
58
+ readonly contactKey?: string
59
+
60
+ /**
61
+ * Level of the invention required.
62
+ */
63
+ readonly inventionLevel?: number
64
+
65
+ /**
66
+ * The types of enhancements required to be crafted.
67
+ */
68
+ readonly inventionTypes?: EnhancementCategory[]
69
+
70
+ /**
71
+ * Number of invention crafts required.
72
+ */
73
+ readonly inventionCount?: number
74
+
75
+ /**
76
+ * Any additional notes.
77
+ */
78
+ readonly notes?: MarkdownString
79
+
80
+ /**
81
+ * List of external links. Wiki, forums, etc.
82
+ */
83
+ readonly links: Link[]
84
+
85
+ constructor(data: BadgeRequirementData) {
86
+ this.key = new Key(data.key).value
87
+ this.type = data.type
88
+ this.zoneKey = data.zoneKey
89
+ this.loc = data.loc
90
+ this.plaqueType = data.plaqueType
91
+ this.plaqueInscription = data.plaqueInscription
92
+ this.vidiotMapKey = data.vidiotMapKey
93
+ this.badgeKey = data.badgeKey
94
+ this.missionName = data.missionName
95
+ this.contactKey = data.contactKey
96
+ this.inventionLevel = data.inventionLevel
97
+ this.inventionTypes = data.inventionTypes
98
+ this.inventionCount = data.inventionCount
99
+ this.notes = data.notes
100
+ this.links = data.links ?? []
101
+ }
102
+ }
@@ -0,0 +1,51 @@
1
+ import { BadgeType } from '../api/badge-type'
2
+ import { Alignment } from '../api/alignment'
3
+
4
+ export interface BadgeSearchOptions {
5
+
6
+ /**
7
+ * Text-based search.
8
+ *
9
+ * Case-insensitive. Defaults to searching on name only.
10
+ */
11
+ query?: {
12
+ str?: string
13
+ on?: {
14
+ name?: boolean
15
+ badgeText?: boolean
16
+ acquisition?: boolean
17
+ notes?: boolean
18
+ effect?: boolean
19
+ setTitle?: boolean
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Filter results matching the given values.
25
+ */
26
+ filter?: {
27
+ type?: BadgeType
28
+ zoneKey?: string
29
+ alignment?: Alignment
30
+ }
31
+
32
+ /**
33
+ * Sort results.
34
+ *
35
+ * Badges are assumed to be in canonical order in the content bundle, and should match the in-game display order.
36
+ */
37
+ sort?: {
38
+ by?: 'CANONICAL' | 'BADGE_NAME' | 'ZONE_NAME'
39
+ dir?: 'ASC' | 'DESC'
40
+ }
41
+
42
+ /**
43
+ * The page (1-based)
44
+ */
45
+ page?: number
46
+
47
+ /**
48
+ * How many results per page
49
+ */
50
+ pageSize?: number
51
+ }
@@ -1,13 +1,14 @@
1
1
  import { BadgeType } from '../api/badge-type'
2
- import { Alignment } from '../api/alignment'
3
2
  import { Link } from '../api/link'
4
3
  import { BadgeData } from '../api/badge-data'
5
- import { BadgePartial } from './badge-partial'
4
+ import { BadgeRequirement } from './badge-requirement'
6
5
  import { Key } from './key'
7
6
  import { Alternates } from './alternates'
7
+ import { Alignments } from './alignments'
8
+ import { MarkdownString } from '../api/markdown-string'
8
9
 
9
10
  export class Badge {
10
- readonly #partialsIndex: Record<string, BadgePartial> = {}
11
+ readonly #requirementsIndex: Record<string, BadgeRequirement> = {}
11
12
 
12
13
  /**
13
14
  * The database key for this badge.
@@ -17,7 +18,7 @@ export class Badge {
17
18
  /**
18
19
  * The type of badge.
19
20
  */
20
- readonly type: BadgeType | string
21
+ readonly type: BadgeType
21
22
 
22
23
  /**
23
24
  * The name of this badge.
@@ -29,7 +30,7 @@ export class Badge {
29
30
  /**
30
31
  * The character alignments that this badge is available to.
31
32
  */
32
- readonly alignment: Alignment[]
33
+ readonly alignment: Alignments
33
34
 
34
35
  /**
35
36
  * The badge text as it appears in-game. May vary by character sex or alignment.
@@ -37,11 +38,9 @@ export class Badge {
37
38
  readonly badgeText: Alternates<string>
38
39
 
39
40
  /**
40
- * Description of how to acquire the badge.
41
- *
42
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
41
+ * Short description of how to acquire the badge. Detailed instructions will be in the notes field.
43
42
  */
44
- readonly acquisition?: string
43
+ readonly acquisition?: MarkdownString
45
44
 
46
45
  /**
47
46
  * Absolute URL to this badge's icon.
@@ -52,28 +51,26 @@ export class Badge {
52
51
 
53
52
  /**
54
53
  * Freeform notes or tips about the badge.
55
- *
56
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
57
54
  */
58
- readonly notes?: string
55
+ readonly notes?: MarkdownString
59
56
 
60
57
  /**
61
- * List of external links for this Badge. Wiki, forums, etc.
58
+ * List of external links. Wiki, forums, etc.
62
59
  */
63
- readonly links?: Link[]
60
+ readonly links: Link[]
64
61
 
65
62
  /**
66
- * For exploration badges, the key of the {@link GameMap} that this badge is found on.
63
+ * For exploration badges, the key of the {@link Zone} that this badge is found on.
67
64
  */
68
- readonly mapKey?: string
65
+ readonly zoneKey?: string
69
66
 
70
67
  /**
71
- * For exploration badges, the `/loc` coordinates of the badge on the in-game map.
68
+ * For exploration badges, the `/loc` coordinates of the badge.
72
69
  */
73
70
  readonly loc?: [number, number, number]
74
71
 
75
72
  /**
76
- * For badges that appear on a Vidiot Map, the number or letter the badge appears as.
73
+ * For plaques that appear on a Vidiot Map, the number or letter the badge appears as.
77
74
  */
78
75
  readonly vidiotMapKey?: string
79
76
 
@@ -93,49 +90,59 @@ export class Badge {
93
90
 
94
91
  /**
95
92
  * A description of the effect the badge will have, such as a buff or granting a temporary power.
96
- *
97
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
98
93
  */
99
- readonly effect?: string
94
+ readonly effect?: MarkdownString
100
95
 
101
96
  /**
102
- * A list of requirements for badges that have partial fulfilment steps, such as visiting plaques for history badges, or collecting other badges for meta-badges like accolades.
97
+ * Represents the layered requirements for badges with multiple fulfillment steps,
98
+ * such as visiting plaques for history badges or collecting other badges.
99
+ *
100
+ * The outer array represents groups of requirements evaluated with OR logic —
101
+ * fulfilling any group satisfies the badge.
102
+ *
103
+ * Each inner array represents individual requirements evaluated with AND logic —
104
+ * all conditions in the group must be met.
103
105
  */
104
- readonly partials?: BadgePartial[]
106
+ readonly requirements?: BadgeRequirement[][]
105
107
 
106
108
  /**
107
109
  * Some badges are not included in the badge total count... such as Flames of Prometheus, which can be removed by redeeming it for a Notice of the Well.
108
110
  */
109
111
  readonly ignoreInTotals: boolean
110
112
 
111
- constructor(data: BadgeData) {
112
- this.key = new Key(data.key).value
113
- this.type = data.type
114
- this.name = new Alternates(data.name)
115
- this.alignment = data.alignment
116
- this.badgeText = new Alternates(data.badgeText ?? [])
117
- this.acquisition = data.acquisition
118
- this.icon = new Alternates(data.icon ?? [])
119
- this.notes = data.notes
120
- this.links = data.links
121
- this.mapKey = data.mapKey
122
- this.loc = data.loc
123
- this.effect = data.effect
124
- this.vidiotMapKey = data.vidiotMapKey
125
- this.setTitle = data.setTitle
126
- this.ignoreInTotals = data.ignoreInTotals ?? false
127
-
128
- this.partials = data.partials?.map((data) => {
129
- if (this.#partialsIndex[data.key] !== undefined) throw new Error(`Duplicate badge partial key [${data.key}]`)
130
- const badge = new BadgePartial(data)
131
- this.#partialsIndex[badge.key] = badge
132
- return badge
113
+ constructor(badgeData: BadgeData) {
114
+ this.key = new Key(badgeData.key).value
115
+ this.type = badgeData.type
116
+ this.name = new Alternates(badgeData.name)
117
+ this.alignment = new Alignments(badgeData.alignment)
118
+ this.badgeText = new Alternates(badgeData.badgeText ?? [])
119
+ this.acquisition = badgeData.acquisition
120
+ this.icon = new Alternates(badgeData.icon ?? [])
121
+ this.notes = badgeData.notes
122
+ this.links = badgeData.links ?? []
123
+ this.zoneKey = badgeData.zoneKey
124
+ this.loc = badgeData.loc
125
+ this.effect = badgeData.effect
126
+ this.vidiotMapKey = badgeData.vidiotMapKey
127
+ this.setTitle = badgeData.setTitle
128
+ this.ignoreInTotals = badgeData.ignoreInTotals ?? false
129
+
130
+ this.requirements = badgeData.requirements?.map((groups, index) => {
131
+ const existingKeysInGroup = new Set<string>()
132
+ return groups.map((requirementData) => {
133
+ if (existingKeysInGroup.has(requirementData.key)) throw new Error(`Duplicate badge requirement key [${badgeData.key}:${requirementData.key}] in group [${index + 1}]`)
134
+ existingKeysInGroup.add(requirementData.key)
135
+
136
+ const badge = new BadgeRequirement(requirementData)
137
+ this.#requirementsIndex[badge.key] = badge
138
+ return badge
139
+ })
133
140
  })
134
141
  }
135
142
 
136
- getPartial(key: string): BadgePartial {
137
- const result = this.#partialsIndex[key]
138
- if (result === undefined) throw new Error(`Unknown badge partial key [${key}]`)
143
+ getRequirement(key: string): BadgeRequirement {
144
+ const result = this.#requirementsIndex[key]
145
+ if (result === undefined) throw new Error(`Unknown badge requirement key [${key}]`)
139
146
  return result
140
147
  }
141
148
  }
@@ -1,19 +1,18 @@
1
1
  import { ContentBundle } from '../api/content-bundle'
2
2
  import { Change } from '../api/change'
3
3
  import { Link } from '../api/link'
4
+ import { MarkdownString } from '../api/markdown-string'
4
5
 
5
6
  export class BundleMetadata {
6
7
  /**
7
- * Name of the server group.
8
+ * Name of the content bundle.
8
9
  */
9
10
  readonly name: string
10
11
 
11
12
  /**
12
- * Description of the server group.
13
- *
14
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
13
+ * Description of the fork.
15
14
  */
16
- readonly description?: string
15
+ readonly description?: MarkdownString
17
16
 
18
17
  /**
19
18
  * Repository where the db content package is maintained.
@@ -21,7 +20,7 @@ export class BundleMetadata {
21
20
  readonly repository?: string
22
21
 
23
22
  /**
24
- * List of external links for this Server Group. Wiki, forums, etc.
23
+ * List of external links. Wiki, forums, etc.
25
24
  */
26
25
  readonly links: Link[]
27
26
 
@@ -1,16 +1,18 @@
1
1
  import { ContentBundle } from '../api/content-bundle'
2
2
  import { Archetype } from './archetype'
3
- import { GameMap } from './game-map'
3
+ import { Zone } from './zone'
4
4
  import { Badge } from './badge'
5
5
  import { BundleMetadata } from './bundle-metadata'
6
- import MiniSearch from 'minisearch'
7
- import { BadgeSearchDocument } from './badge-search-document'
6
+ import { BadgeIndex } from './badge-index'
7
+ import { BadgeSearchOptions } from './badge-search-options'
8
+ import { Paged } from './paged'
9
+ import { Contact } from './contact'
8
10
 
9
11
  export class CohContentDatabase {
10
12
  readonly #archetypeIndex: Record<string, Archetype> = {}
11
- readonly #mapIndex: Record<string, GameMap> = {}
12
- readonly #badgeIndex: Record<string, Badge> = {}
13
- readonly #badgeSearch: MiniSearch
13
+ readonly #zoneIndex: Record<string, Zone> = {}
14
+ readonly #contactIndex: Record<string, Contact> = {}
15
+ readonly #badgeIndex: BadgeIndex
14
16
 
15
17
  /**
16
18
  * Metadata about the content bundle.
@@ -18,23 +20,29 @@ export class CohContentDatabase {
18
20
  readonly metadata: BundleMetadata
19
21
 
20
22
  /**
21
- * List of the game server names in this server group.
23
+ * List of the game server names.
24
+ *
22
25
  * Torchbearer, Excelsior, etc.
23
26
  */
24
27
  readonly servers: string[]
25
28
 
26
29
  /**
27
- * List of archetypes available in this server group.
30
+ * List of archetypes.
28
31
  */
29
32
  readonly archetypes: Archetype[]
30
33
 
31
34
  /**
32
- * List of game maps supported by this server group.
35
+ * List of game zones.
33
36
  */
34
- readonly maps: GameMap[]
37
+ readonly zones: Zone[]
35
38
 
36
39
  /**
37
- * List of badges available on this server group.
40
+ * List of contacts.
41
+ */
42
+ readonly contacts: Contact[]
43
+
44
+ /**
45
+ * List of badges.
38
46
  */
39
47
  readonly badges: Badge[]
40
48
 
@@ -45,56 +53,73 @@ export class CohContentDatabase {
45
53
  constructor(bundle: ContentBundle) {
46
54
  this.metadata = new BundleMetadata(bundle)
47
55
  this.servers = bundle.servers ?? []
56
+
48
57
  this.archetypes = bundle.archetypes?.map((data) => {
49
- if (this.#archetypeIndex[data.key] !== undefined) throw new Error(`Duplicate archetype key [${data.key}]`)
58
+ if (this.#archetypeIndex[data.key] !== undefined) throw new Error(`Duplicate archetype key '${data.key}'`)
50
59
  const archetype = new Archetype(data)
51
60
  this.#archetypeIndex[archetype.key] = archetype
52
61
  return archetype
53
62
  }) ?? []
54
- this.maps = bundle.maps?.map((data) => {
55
- if (this.#mapIndex[data.key] !== undefined) throw new Error(`Duplicate map key [${data.key}]`)
56
- const map = new GameMap(data)
57
- this.#mapIndex[map.key] = map
58
- return map
63
+
64
+ this.zones = bundle.zones?.map((data) => {
65
+ if (this.#zoneIndex[data.key] !== undefined) throw new Error(`Duplicate zone key '${data.key}'`)
66
+ const zone = new Zone(data)
67
+ this.#zoneIndex[zone.key] = zone
68
+ return zone
59
69
  }) ?? []
60
- this.badges = bundle.badges?.map((data) => {
61
- if (this.#badgeIndex[data.key] !== undefined) throw new Error(`Duplicate badge key [${data.key}]`)
62
- const badge = new Badge(data)
63
- this.#badgeIndex[badge.key] = badge
64
- return badge
70
+
71
+ this.contacts = bundle.contacts?.map((data) => {
72
+ if (this.#contactIndex[data.key] !== undefined) throw new Error(`Duplicate contact key '${data.key}'`)
73
+ const contact = new Contact(data)
74
+ this.#contactIndex[contact.key] = contact
75
+ return contact
65
76
  }) ?? []
66
77
 
67
- this.#badgeSearch = new MiniSearch({
68
- fields: ['key', 'name', 'badgeText', 'acquisition'],
69
- storeFields: ['key'],
70
- })
71
- for (const badge of this.badges) {
72
- this.#badgeSearch.add(new BadgeSearchDocument(badge))
73
- }
78
+ this.badges = bundle.badges?.map(data => new Badge(data)) ?? []
79
+ this.#badgeIndex = new BadgeIndex(this.badges, this.zones)
74
80
  }
75
81
 
76
82
  getArchetype(key: string): Archetype {
77
83
  const result = this.#archetypeIndex[key]
78
- if (result === undefined) throw new Error(`Unknown archetype key [${key}]`)
84
+ if (result === undefined) throw new Error(`Unknown archetype key '${key}'`)
79
85
  return result
80
86
  }
81
87
 
82
- getMap(key: string): GameMap {
83
- const result = this.#mapIndex[key]
84
- if (result === undefined) throw new Error(`Unknown map key [${key}]`)
88
+ getZone(key: string): Zone {
89
+ const result = this.#zoneIndex[key]
90
+ if (result === undefined) throw new Error(`Unknown zone key '${key}'`)
85
91
  return result
86
92
  }
87
93
 
88
- getBadge(key: string): Badge {
89
- const result = this.#badgeIndex[key]
90
- if (result === undefined) throw new Error(`Unknown badge key [${key}]`)
94
+ getContact(key: string): Contact {
95
+ const result = this.#contactIndex[key]
96
+ if (result === undefined) throw new Error(`Unknown contact key '${key}'`)
91
97
  return result
92
98
  }
93
99
 
94
- searchBadges(query?: string): Badge[] {
95
- if (!query) return this.badges
96
- const keys = this.#badgeSearch.search(query, { prefix: true, fuzzy: true })
100
+ zoneExists(key: string): boolean {
101
+ return !!this.#zoneIndex[key]
102
+ }
97
103
 
98
- return keys.map(result => this.getBadge(result['key']))
104
+ contactExists(key: string): boolean {
105
+ return !!this.#contactIndex[key]
106
+ }
107
+
108
+ getBadge(key: string): Badge {
109
+ return this.#badgeIndex.getBadge(key)
110
+ }
111
+
112
+ badgeExists(key: string): boolean {
113
+ return this.#badgeIndex.badgeExists(key)
114
+ }
115
+
116
+ /**
117
+ * Search, sort and filter the badge list.
118
+ * This is a fairly brute-forced approach and will not be as performant as loading the badge data into a traditional
119
+ * database engine, but is sufficient for most operations.
120
+ * @param options {@link BadgeSearchOptions}
121
+ */
122
+ searchBadges(options?: BadgeSearchOptions): Paged<Badge> {
123
+ return this.#badgeIndex.searchBadges(options)
99
124
  }
100
125
  }