coh-content-db 2.0.0-rc.2 → 2.0.0-rc.21

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 (120) hide show
  1. package/.editorconfig +10 -11
  2. package/.github/workflows/build.yml +4 -2
  3. package/.github/workflows/pull-request.yml +1 -1
  4. package/.github/workflows/release.yml +1 -1
  5. package/CHANGELOG.md +47 -0
  6. package/README.md +77 -32
  7. package/dist/coh-content-db.d.ts +755 -290
  8. package/dist/coh-content-db.js +1028 -358
  9. package/dist/coh-content-db.js.map +1 -1
  10. package/dist/coh-content-db.mjs +998 -349
  11. package/dist/coh-content-db.mjs.map +1 -1
  12. package/eslint.config.mjs +1 -0
  13. package/jest.config.mjs +1 -0
  14. package/package.json +14 -14
  15. package/src/main/api/alignment.ts +18 -2
  16. package/src/main/api/badge-data.ts +29 -51
  17. package/src/main/api/badge-requirement-data.ts +64 -0
  18. package/src/main/api/badge-requirement-type.ts +32 -0
  19. package/src/main/api/badge-type.ts +15 -15
  20. package/src/main/api/bundle-data.ts +47 -0
  21. package/src/main/api/bundle-header-data.ts +44 -0
  22. package/src/main/api/contact-data.ts +49 -0
  23. package/src/main/api/enhancement-category.ts +26 -26
  24. package/src/main/api/level-range-data.ts +4 -0
  25. package/src/main/api/location-data.ts +28 -0
  26. package/src/main/api/markdown-string.ts +4 -0
  27. package/src/main/api/mission-data.ts +57 -0
  28. package/src/main/api/mission-flashback-data.ts +31 -0
  29. package/src/main/api/mission-type.ts +2 -0
  30. package/src/main/api/morality.ts +49 -0
  31. package/src/main/api/set-title-data.ts +4 -0
  32. package/src/main/api/sex.ts +8 -1
  33. package/src/main/api/variant-context.ts +11 -0
  34. package/src/main/api/variant-data.ts +22 -0
  35. package/src/main/api/zone-data.ts +44 -0
  36. package/src/main/api/zone-type.ts +59 -0
  37. package/src/main/db/abstract-index.ts +37 -0
  38. package/src/main/db/alignment-list.ts +54 -0
  39. package/src/main/db/badge-index.ts +83 -0
  40. package/src/main/db/badge-requirement.ts +81 -0
  41. package/src/main/db/badge-search-options.ts +52 -0
  42. package/src/main/db/badge.ts +97 -74
  43. package/src/main/db/bundle-header.ts +52 -0
  44. package/src/main/db/coh-content-database.ts +123 -14
  45. package/src/main/db/contact.ts +63 -0
  46. package/src/main/db/level-range.ts +15 -0
  47. package/src/main/db/location.ts +30 -0
  48. package/src/main/db/mission.ts +108 -0
  49. package/src/main/db/morality-list.ts +99 -0
  50. package/src/main/db/paged.ts +11 -0
  51. package/src/main/db/set-title-ids.ts +10 -0
  52. package/src/main/db/variants.ts +84 -0
  53. package/src/main/db/zone.ts +57 -0
  54. package/src/main/index.ts +33 -18
  55. package/src/main/util/coalesce-to-array.ts +13 -0
  56. package/src/main/util/links.ts +104 -0
  57. package/src/main/util/to-date.ts +9 -0
  58. package/src/test/api/alignment.test.ts +38 -4
  59. package/src/test/api/badge-data.fixture.ts +2 -15
  60. package/src/test/api/badge-data.test.ts +5 -4
  61. package/src/test/api/badge-requirement-data.fixture.ts +7 -0
  62. package/src/test/api/badge-requirement-type.test.ts +31 -0
  63. package/src/test/api/badge-type.test.ts +5 -5
  64. package/src/test/api/bundle-data.fixture.ts +7 -0
  65. package/src/test/api/bundle-header-data.fixture.ts +8 -0
  66. package/src/test/api/contact-data.fixture.ts +7 -0
  67. package/src/test/api/enhancement-category.test.ts +5 -5
  68. package/src/test/api/mission-data.fixture.ts +12 -0
  69. package/src/test/api/morality.test.ts +31 -0
  70. package/src/test/api/sex.test.ts +33 -1
  71. package/src/test/api/zone-data.fixture.ts +9 -0
  72. package/src/test/db/abstract-index.test.ts +55 -0
  73. package/src/test/db/alignment-list.test.ts +200 -0
  74. package/src/test/db/badge-index.test.ts +653 -0
  75. package/src/test/db/badge-requirement.test.ts +145 -0
  76. package/src/test/db/badge.test.ts +416 -14
  77. package/src/test/db/bundle-header.test.ts +89 -0
  78. package/src/test/db/coh-content-database.test.ts +265 -24
  79. package/src/test/db/contact.test.ts +98 -0
  80. package/src/test/db/level-range.test.ts +47 -0
  81. package/src/test/db/location.test.ts +51 -0
  82. package/src/test/db/mission.test.ts +173 -0
  83. package/src/test/db/morality-list.test.ts +457 -0
  84. package/src/test/db/set-title-ids.test.ts +19 -0
  85. package/src/test/db/variants.test.ts +188 -0
  86. package/src/test/db/zone.test.ts +81 -0
  87. package/src/test/integration.test.ts +16 -0
  88. package/src/test/util/coalese-to-array.test.ts +17 -0
  89. package/src/test/util/links.test.ts +149 -0
  90. package/src/test/util/to-date.test.ts +15 -0
  91. package/src/main/api/alternate-data.ts +0 -22
  92. package/src/main/api/badge-partial-data.ts +0 -65
  93. package/src/main/api/badge-partial-type.ts +0 -8
  94. package/src/main/api/change.ts +0 -14
  95. package/src/main/api/game-map-data.ts +0 -26
  96. package/src/main/api/plaque-type.ts +0 -6
  97. package/src/main/api/server-group-data.ts +0 -65
  98. package/src/main/api/vidiot-map-data.ts +0 -18
  99. package/src/main/api/vidiot-map-point-of-interest-data.ts +0 -30
  100. package/src/main/changelog.ts +0 -20
  101. package/src/main/db/alternates.ts +0 -81
  102. package/src/main/db/badge-partial.ts +0 -35
  103. package/src/main/db/game-map.ts +0 -33
  104. package/src/main/db/server-group.ts +0 -112
  105. package/src/main/db/vidiot-map-point-of-interest.ts +0 -40
  106. package/src/main/db/vidiot-map.ts +0 -25
  107. package/src/main/util.ts +0 -17
  108. package/src/test/api/badge-partial-data.fixture.ts +0 -17
  109. package/src/test/api/badge-partial-type.test.ts +0 -31
  110. package/src/test/api/game-map-data.fixture.ts +0 -10
  111. package/src/test/api/plaque-type.test.ts +0 -31
  112. package/src/test/api/server-group-data.fixture.ts +0 -23
  113. package/src/test/api/server-group-data.test.ts +0 -15
  114. package/src/test/api/vidiot-map-point-of-interest.fixture.ts +0 -10
  115. package/src/test/api/vidiot-map.fixture.ts +0 -9
  116. package/src/test/changelog.test.ts +0 -36
  117. package/src/test/db/alternates.test.ts +0 -223
  118. package/src/test/db/server-group.test.ts +0 -124
  119. package/src/test/index.test.ts +0 -10
  120. package/src/test/util.test.ts +0 -39
@@ -0,0 +1,83 @@
1
+ import { Badge, compareByName, compareByReleaseDate, compareByZoneKey } from './badge'
2
+ import { BadgeSearchOptions } from './badge-search-options'
3
+ import { Paged } from './paged'
4
+ import { AbstractIndex } from './abstract-index'
5
+
6
+ export class BadgeIndex extends AbstractIndex<Badge> {
7
+ constructor(values: Badge[] | undefined) {
8
+ super('key', values)
9
+ }
10
+
11
+ search(options?: BadgeSearchOptions): Paged<Badge> {
12
+ const matched = (options?.query || options?.filter)
13
+ ? this._values.filter(badge => this.#satisfiesQueryPredicate(badge, options?.query) && this.#satisfiesFilterPredicate(badge, options?.filter))
14
+ : this._values
15
+
16
+ const sorted = this.#sort(matched, options)
17
+
18
+ const totalPages = options?.pageSize ? Math.ceil(matched.length / (options?.pageSize)) : 1
19
+ const pageNumber = Math.max(1, Math.min(totalPages, options?.page ?? 1))
20
+ const items = options?.pageSize ? sorted.slice((pageNumber - 1) * options.pageSize, pageNumber * options?.pageSize) : sorted
21
+
22
+ return {
23
+ items: items,
24
+ pageIndex: pageNumber - 1,
25
+ pageNumber: pageNumber,
26
+ pageSize: options?.pageSize,
27
+ matchedItemCount: matched.length,
28
+ totalItemCount: this._values.length,
29
+ totalPageCount: totalPages,
30
+ }
31
+ }
32
+
33
+ #satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
34
+ const queryString = query?.str?.toLowerCase() ?? ''
35
+ const fields = query?.fields ? new Set(query?.fields) : new Set(['name']) // Default to name if not provided
36
+ if (fields.size === 0) return true
37
+
38
+ return !!((fields.has('name') && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
39
+ || (fields.has('badge-text') && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
40
+ || (fields.has('acquisition') && badge.acquisition?.toLowerCase().includes(queryString))
41
+ || (fields.has('effect') && badge.effect?.toLowerCase().includes(queryString))
42
+ || (fields.has('notes') && badge.notes?.toLowerCase().includes(queryString))
43
+ || (fields.has('set-title-id') && (badge.setTitleId?.primal.toString() === queryString))
44
+ || (fields.has('set-title-id') && (badge.setTitleId?.praetorian?.toString() === queryString))
45
+ )
46
+ }
47
+
48
+ #satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
49
+ return (!filter?.type || badge.type === filter.type)
50
+ && (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
51
+ && (!filter?.morality || badge.morality.has(filter.morality))
52
+ && (!filter?.predicate || filter.predicate(badge))
53
+ }
54
+
55
+ #sort(badges: Badge[], options?: BadgeSearchOptions): Badge[] {
56
+ switch (options?.sort) {
57
+ case 'name.asc': {
58
+ return badges.toSorted((a, b) => compareByName(a, b, options.variantContext))
59
+ }
60
+ case 'name.desc': {
61
+ return badges.toSorted((a, b) => compareByName(b, a, options.variantContext))
62
+ }
63
+ case 'zone-key.asc': {
64
+ return badges.toSorted(compareByZoneKey)
65
+ }
66
+ case 'zone-key.desc': {
67
+ return badges.toSorted((a, b) => compareByZoneKey(b, a))
68
+ }
69
+ case 'release-date.asc': {
70
+ return badges.toSorted(compareByReleaseDate)
71
+ }
72
+ case 'release-date.desc': {
73
+ return badges.toSorted((a, b) => compareByReleaseDate(b, a))
74
+ }
75
+ case 'canonical.desc': {
76
+ return badges.toReversed()
77
+ }
78
+ default: {
79
+ return [...badges]
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,81 @@
1
+ import { BadgeRequirementData } from '../api/badge-requirement-data'
2
+ import { BadgeRequirementType } from '../api/badge-requirement-type'
3
+ import { EnhancementCategory } from '../api/enhancement-category'
4
+ import { Key } from './key'
5
+ import { MarkdownString } from '../api/markdown-string'
6
+ import { Link } from '../api/link'
7
+ import { Location } from './location'
8
+ import { coalesceToArray } from '../util/coalesce-to-array'
9
+
10
+ export class BadgeRequirement {
11
+ /**
12
+ * Unique key used to reference this badge requirement.
13
+ *
14
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
15
+ */
16
+ readonly key: string
17
+
18
+ /**
19
+ * The requirement type.
20
+ */
21
+ readonly type: BadgeRequirementType
22
+
23
+ /**
24
+ * If the requirement involves a location, where it is.
25
+ */
26
+ readonly location?: Location[]
27
+
28
+ /**
29
+ * If the requirement involves a badge, the badge key.
30
+ */
31
+ readonly badgeKey?: string
32
+
33
+ /**
34
+ * If the requirement involves a mission, the mission key.
35
+ */
36
+ readonly missionKey?: string
37
+
38
+ /**
39
+ * If the requirement involves a monument, the text that is displayed thereon.
40
+ */
41
+ readonly monumentText?: string
42
+
43
+ /**
44
+ * If the requirement involves crafting an invention, the Level of the invention required.
45
+ */
46
+ readonly inventionLevel?: number
47
+
48
+ /**
49
+ * If the requirement involves crafting an invention, the types of enhancements that will qualify.
50
+ */
51
+ readonly inventionTypes?: EnhancementCategory[]
52
+
53
+ /**
54
+ * Number of times the task needs to be repeated.
55
+ */
56
+ readonly count?: number
57
+
58
+ /**
59
+ * Additional information about the requirement.
60
+ */
61
+ readonly notes?: MarkdownString
62
+
63
+ /**
64
+ * List of external links. Wiki, forums, etc.
65
+ */
66
+ readonly links: Link[]
67
+
68
+ constructor(data: BadgeRequirementData) {
69
+ this.key = new Key(data.key).value
70
+ this.type = data.type
71
+ this.location = coalesceToArray(data.location)
72
+ this.badgeKey = data.badgeKey
73
+ this.missionKey = data.missionKey
74
+ this.monumentText = data.monumentText
75
+ this.inventionLevel = data.inventionLevel
76
+ this.inventionTypes = data.inventionTypes
77
+ this.count = data.count
78
+ this.notes = data.notes
79
+ this.links = data.links ?? []
80
+ }
81
+ }
@@ -0,0 +1,52 @@
1
+ import { BadgeType } from '../api/badge-type'
2
+ import { Morality } from '../api/morality'
3
+ import { Badge } from './badge'
4
+ import { VariantContext } from '../api/variant-context'
5
+
6
+ export type BadgeQueryableField = 'name' | 'badge-text' | 'acquisition' | 'notes' | 'effect' | 'set-title-id'
7
+ export type BadgeSort = `${'canonical' | 'name' | 'zone-key' | 'release-date'}.${'asc' | 'desc'}`
8
+
9
+ export interface BadgeSearchOptions {
10
+
11
+ /**
12
+ * Text-based search.
13
+ *
14
+ * Case-insensitive. Defaults to searching on name only.
15
+ */
16
+ query?: {
17
+ str?: string
18
+ fields?: BadgeQueryableField[]
19
+ }
20
+
21
+ /**
22
+ * Filter results matching the given values.
23
+ */
24
+ filter?: {
25
+ type?: BadgeType
26
+ zoneKey?: string
27
+ morality?: Morality
28
+ predicate?: (badge: Badge) => boolean
29
+ }
30
+
31
+ /**
32
+ * Adjust search results based on a given variant context (morality or sex of a character).
33
+ */
34
+ variantContext?: VariantContext
35
+
36
+ /**
37
+ * Sort results.
38
+ *
39
+ * Badges are assumed to be in canonical order in the content bundle, and should match the in-game display order.
40
+ */
41
+ sort?: BadgeSort
42
+
43
+ /**
44
+ * The page (1-based)
45
+ */
46
+ page?: number
47
+
48
+ /**
49
+ * How many results per page
50
+ */
51
+ pageSize?: number
52
+ }
@@ -1,13 +1,20 @@
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
- import { Alternates } from './alternates'
6
+ import { Variants } from './variants'
7
+ import { MarkdownString } from '../api/markdown-string'
8
+ import { MoralityList } from './morality-list'
9
+ import { AbstractIndex } from './abstract-index'
10
+ import { toDate } from '../util/to-date'
11
+ import { coalesceToArray } from '../util/coalesce-to-array'
12
+ import { SetTitleIds } from './set-title-ids'
13
+ import { VariantContext } from '../api/variant-context'
8
14
 
9
15
  export class Badge {
10
- readonly #partialsIndex: Record<string, BadgePartial> = {}
16
+ readonly #requirementsIndex: AbstractIndex<BadgeRequirement>
17
+ readonly #zoneKeys = new Set<string>()
11
18
 
12
19
  /**
13
20
  * The database key for this badge.
@@ -17,125 +24,141 @@ export class Badge {
17
24
  /**
18
25
  * The type of badge.
19
26
  */
20
- readonly type: BadgeType | string
27
+ readonly type: BadgeType
21
28
 
22
29
  /**
23
30
  * The name of this badge.
24
31
  *
25
32
  * May vary by character sex or alignment.
26
33
  */
27
- readonly name: Alternates<string>
34
+ readonly name: Variants<string>
28
35
 
29
36
  /**
30
- * The character alignments that this badge is available to.
37
+ * The date that the badge was added to the game.
31
38
  */
32
- readonly alignment: Alignment[]
39
+ readonly releaseDate: Date
40
+
41
+ /**
42
+ * The character moralities that this badge is available to.
43
+ */
44
+ readonly morality: MoralityList
33
45
 
34
46
  /**
35
47
  * The badge text as it appears in-game. May vary by character sex or alignment.
36
48
  */
37
- readonly badgeText: Alternates<string>
49
+ readonly badgeText: Variants<MarkdownString>
38
50
 
39
51
  /**
40
- * Description of how to acquire the badge.
41
- *
42
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
52
+ * Short description of how to acquire the badge. Detailed instructions will be in the notes field.
43
53
  */
44
- readonly acquisition?: string
54
+ readonly acquisition?: MarkdownString
45
55
 
46
56
  /**
47
57
  * Absolute URL to this badge's icon.
48
58
  *
49
59
  * May vary by character sex or alignment.
50
60
  */
51
- readonly icon: Alternates<string>
61
+ readonly icon: Variants<string>
52
62
 
53
63
  /**
54
64
  * Freeform notes or tips about the badge.
55
- *
56
- * Supports {@link https://www.markdownguide.org/|Markdown} format.
57
65
  */
58
- readonly notes?: string
66
+ readonly notes?: MarkdownString
59
67
 
60
68
  /**
61
- * List of external links for this Badge. Wiki, forums, etc.
69
+ * List of external links. Wiki, forums, etc.
62
70
  */
63
- readonly links?: Link[]
71
+ readonly links: Link[]
64
72
 
65
73
  /**
66
- * For exploration badges, the key of the {@link GameMap} that this badge is found on.
74
+ * The id used with the in-game `/settitle` command to apply the badge.
75
+ * The first value is the id for primal characters and the (optional) second number is the id for praetorian characters.
67
76
  */
68
- readonly mapKey?: string
77
+ readonly setTitleId?: SetTitleIds
69
78
 
70
79
  /**
71
- * For exploration badges, the `/loc` coordinates of the badge on the in-game map.
80
+ * A description of the effect the badge will have, such as a buff or granting a temporary power.
72
81
  */
73
- readonly loc?: [number, number, number]
82
+ readonly effect?: MarkdownString
74
83
 
75
84
  /**
76
- * For badges that appear on a Vidiot Map, the number or letter the badge appears as.
85
+ * 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.
77
86
  */
78
- readonly vidiotMapKey?: string
87
+ readonly ignoreInTotals: boolean
79
88
 
80
- /**
81
- * ID used with the in-game `/settitle` command to apply the badge.
82
- */
83
- readonly setTitle?: {
84
- /**
85
- * `/settitle` id.
86
- */
87
- id?: number
88
- /**
89
- * `/settitle` id if different for praetorian characters.
90
- */
91
- praetorianId?: number
89
+ constructor(badgeData: BadgeData) {
90
+ this.key = new Key(badgeData.key).value
91
+ this.type = badgeData.type
92
+ this.name = new Variants(badgeData.name)
93
+ this.releaseDate = toDate(badgeData.releaseDate)
94
+ this.morality = new MoralityList(coalesceToArray(badgeData.morality))
95
+ this.badgeText = new Variants(badgeData.badgeText ?? [])
96
+ this.acquisition = badgeData.acquisition
97
+ this.icon = new Variants(badgeData.icon ?? [])
98
+ this.notes = badgeData.notes
99
+ this.links = badgeData.links ?? []
100
+ this.effect = badgeData.effect
101
+ this.setTitleId = badgeData.setTitleId ? new SetTitleIds(badgeData.setTitleId) : undefined
102
+ this.ignoreInTotals = badgeData.ignoreInTotals ?? false
103
+
104
+ this.#requirementsIndex = new AbstractIndex<BadgeRequirement>('key', badgeData.requirements?.map(x => new BadgeRequirement(x)))
105
+
106
+ for (const requirement of this.#requirementsIndex.values) {
107
+ if (requirement.location) for (const location of requirement.location) {
108
+ if (location.zoneKey) this.#zoneKeys.add(location.zoneKey)
109
+ }
110
+ }
92
111
  }
93
112
 
94
113
  /**
95
- * 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.
114
+ * Represents the requirements for badges with multiple fulfillment steps, such as visiting monuments for history badges, completing missions, or collecting other badges.
98
115
  */
99
- readonly effect?: string
116
+ get requirements(): BadgeRequirement[] {
117
+ return this.#requirementsIndex.values
118
+ }
119
+
120
+ getRequirement(key: string): BadgeRequirement | undefined {
121
+ return this.#requirementsIndex.get(key)
122
+ }
100
123
 
101
124
  /**
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.
125
+ * Return a list of all the zone keys referenced by this badge.
103
126
  */
104
- readonly partials?: BadgePartial[]
127
+ get zoneKeys(): string[] {
128
+ return [...this.#zoneKeys]
129
+ }
105
130
 
106
131
  /**
107
- * 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.
132
+ * The zone key if this badge relates to a single zone.
108
133
  */
109
- readonly ignoreInTotals: boolean
110
-
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
133
- })
134
+ get zoneKey(): string | undefined {
135
+ return this.#zoneKeys.size === 1 ? this.#zoneKeys.values().next().value : undefined
134
136
  }
137
+ }
135
138
 
136
- getPartial(key: string): BadgePartial {
137
- const result = this.#partialsIndex[key]
138
- if (result === undefined) throw new Error(`Unknown badge partial key [${key}]`)
139
- return result
140
- }
139
+ export function compareByName(a?: Badge, b?: Badge, context?: VariantContext): number {
140
+ const aName = a?.name?.getValue(context)
141
+ const bName = b?.name?.getValue(context)
142
+ if (!aName && !bName) return 0
143
+ if (!aName) return 1
144
+ if (!bName) return -1
145
+ return aName.localeCompare(bName)
146
+ }
147
+
148
+ export function compareByZoneKey(a?: Badge, b?: Badge): number {
149
+ const aZone = a?.zoneKey
150
+ const bZone = b?.zoneKey
151
+ if (!aZone && !bZone) return 0
152
+ if (!aZone) return 1
153
+ if (!bZone) return -1
154
+ return aZone.localeCompare(bZone)
155
+ }
156
+
157
+ export function compareByReleaseDate(a?: Badge, b?: Badge): number {
158
+ const aReleaseDate = a?.releaseDate?.getTime()
159
+ const bReleaseDate = b?.releaseDate?.getTime()
160
+ if (aReleaseDate === bReleaseDate) return 0
161
+ if (!aReleaseDate) return 1
162
+ if (!bReleaseDate) return -1
163
+ return aReleaseDate < bReleaseDate ? -1 : 1
141
164
  }
@@ -0,0 +1,52 @@
1
+ import { Link } from '../api/link'
2
+ import { MarkdownString } from '../api/markdown-string'
3
+ import { BundleHeaderData } from '../api/bundle-header-data'
4
+ import { toDate } from '../util/to-date'
5
+
6
+ export class BundleHeader {
7
+ /**
8
+ * Name of the fork this bundle contains data for.
9
+ */
10
+ readonly name: string
11
+
12
+ /**
13
+ * Version number for this data package.
14
+ */
15
+ readonly version: string
16
+
17
+ /**
18
+ * The time this bundle was last updated.
19
+ */
20
+ readonly lastUpdateTime: Date
21
+
22
+ /**
23
+ * Description of the fork.
24
+ */
25
+ readonly description?: MarkdownString
26
+
27
+ /**
28
+ * Url for the repository where the bundle is maintained.
29
+ */
30
+ readonly repositoryUrl?: string
31
+
32
+ /**
33
+ * Url for the location of the changelog.
34
+ */
35
+ readonly changelogUrl?: string
36
+
37
+ /**
38
+ * List of external links. Wiki, forums, etc.
39
+ */
40
+ readonly links?: Link[]
41
+
42
+ constructor(data: BundleHeaderData) {
43
+ if (!data) throw new Error('Missing header data')
44
+ this.name = data.name
45
+ this.version = data.version
46
+ this.lastUpdateTime = toDate(data.lastUpdateTime)
47
+ this.description = data?.description
48
+ this.repositoryUrl = data?.repositoryUrl
49
+ this.changelogUrl = data?.changelogUrl
50
+ this.links = data?.links ?? []
51
+ }
52
+ }
@@ -1,29 +1,138 @@
1
- import { ServerGroup } from './server-group'
2
- import { ServerGroupData } from '../api/server-group-data'
1
+ import { BundleData } from '../api/bundle-data'
2
+ import { Archetype } from './archetype'
3
+ import { Zone } from './zone'
4
+ import { Badge } from './badge'
5
+ import { BundleHeader } from './bundle-header'
6
+ import { BadgeSearchOptions } from './badge-search-options'
7
+ import { Paged } from './paged'
8
+ import { Contact } from './contact'
9
+ import { Mission } from './mission'
10
+ import { AbstractIndex } from './abstract-index'
11
+ import { BadgeIndex } from './badge-index'
3
12
 
4
13
  export class CohContentDatabase {
5
- readonly #serverGroups: Record<string, ServerGroup> = {}
14
+ readonly #archetypeIndex
15
+ readonly #zoneIndex
16
+ readonly #contactIndex
17
+ readonly #missionIndex
18
+ readonly #badgeIndex
19
+
20
+ readonly #header: BundleHeader
21
+ readonly #servers: string[]
22
+
23
+ /**
24
+ * Create a db instance from the given content bundle.
25
+ * @param bundle The bundle to load.
26
+ */
27
+ constructor(bundle: BundleData) {
28
+ this.#header = new BundleHeader(bundle.header)
29
+ this.#servers = bundle.servers ?? []
30
+
31
+ this.#archetypeIndex = new AbstractIndex<Archetype>('key', bundle.archetypes?.map(x => new Archetype(x)))
32
+ this.#zoneIndex = new AbstractIndex<Zone>('key', bundle.zones?.map(x => new Zone(x)))
33
+ this.#contactIndex = new AbstractIndex<Contact>('key', bundle.contacts?.map(x => new Contact(x)))
34
+ this.#missionIndex = new AbstractIndex<Mission>('key', bundle.missions?.map(x => new Mission(x)))
35
+ this.#badgeIndex = new BadgeIndex(bundle.badges?.map(x => new Badge(x)))
36
+ }
37
+
38
+ /**
39
+ * Header information about the content bundle.
40
+ */
41
+ get header(): BundleHeader {
42
+ return this.#header
43
+ }
44
+
45
+ /**
46
+ * List of the game server names.
47
+ *
48
+ * Torchbearer, Excelsior, etc.
49
+ */
50
+ get servers(): string[] {
51
+ return this.#servers
52
+ }
53
+
54
+ /**
55
+ * List of archetypes.
56
+ */
57
+ get archetypes(): Archetype[] {
58
+ return this.#archetypeIndex.values
59
+ }
60
+
61
+ /**
62
+ * Get archetype by key.
63
+ * @param key The key.
64
+ */
65
+ getArchetype(key: string | undefined): Archetype | undefined {
66
+ return this.#archetypeIndex.get(key)
67
+ }
68
+
69
+ /**
70
+ * List of game zones.
71
+ */
72
+ get zones(): Zone[] {
73
+ return this.#zoneIndex.values
74
+ }
75
+
76
+ /**
77
+ * Get zone by key.
78
+ * @param key The key.
79
+ */
80
+ getZone(key: string | undefined): Zone | undefined {
81
+ return this.#zoneIndex.get(key)
82
+ }
83
+
84
+ /**
85
+ * List of contacts.
86
+ */
87
+ get contacts(): Contact[] {
88
+ return this.#contactIndex.values
89
+ }
90
+
91
+ /**
92
+ * Get contact by key.
93
+ * @param key The key.
94
+ */
95
+ getContact(key: string | undefined): Contact | undefined {
96
+ return this.#contactIndex.get(key)
97
+ }
98
+
99
+ /**
100
+ * List of missions.
101
+ */
102
+ get missions(): Mission[] {
103
+ return this.#missionIndex.values
104
+ }
105
+
106
+ /**
107
+ * Get mission by key.
108
+ * @param key The key.
109
+ */
110
+ getMission(key: string | undefined): Mission | undefined {
111
+ return this.#missionIndex.get(key)
112
+ }
6
113
 
7
114
  /**
8
- * Load a server group data package into the database.
9
- * @param data The data to load.
115
+ * List of badges.
10
116
  */
11
- loadServerGroupData(data: ServerGroupData): void {
12
- this.#serverGroups[data.key] = new ServerGroup(data)
117
+ get badges(): Badge[] {
118
+ return this.#badgeIndex.values
13
119
  }
14
120
 
15
121
  /**
16
- * Get all the server groups currently loaded in the database.
122
+ * Get badge by key.
123
+ * @param key The key.
17
124
  */
18
- listServerGroups(): ServerGroup[] {
19
- return Object.values(this.#serverGroups)
125
+ getBadge(key: string | undefined): Badge | undefined {
126
+ return this.#badgeIndex.get(key)
20
127
  }
21
128
 
22
129
  /**
23
- * get a server group by key.
24
- * @param serverGroupKey The key.
130
+ * Search, sort and filter the badge list.
131
+ * This is a fairly brute-forced approach and will not be as performant as loading the badge data into a traditional
132
+ * database engine, but is sufficient for most operations.
133
+ * @param options {@link BadgeSearchOptions}
25
134
  */
26
- getServerGroup(serverGroupKey: string): ServerGroup | null {
27
- return this.#serverGroups[serverGroupKey]
135
+ searchBadges(options?: BadgeSearchOptions): Paged<Badge> {
136
+ return this.#badgeIndex.search(options)
28
137
  }
29
138
  }