coh-content-db 2.0.0-rc.1 → 2.0.0-rc.11

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 (100) 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 +2 -2
  5. package/CHANGELOG.md +43 -0
  6. package/README.md +52 -24
  7. package/dist/coh-content-db.d.ts +678 -279
  8. package/dist/coh-content-db.js +828 -371
  9. package/dist/coh-content-db.js.map +1 -1
  10. package/dist/coh-content-db.mjs +803 -362
  11. package/dist/coh-content-db.mjs.map +1 -1
  12. package/eslint.config.mjs +1 -0
  13. package/package.json +1 -1
  14. package/src/main/api/alignment.ts +18 -2
  15. package/src/main/api/alternate-data.ts +2 -2
  16. package/src/main/api/badge-data.ts +20 -48
  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 +37 -0
  22. package/src/main/api/contact-data.ts +48 -0
  23. package/src/main/api/enhancement-category.ts +26 -26
  24. package/src/main/api/location-data.ts +28 -0
  25. package/src/main/api/markdown-string.ts +4 -0
  26. package/src/main/api/mission-data.ts +83 -0
  27. package/src/main/api/mission-type.ts +2 -0
  28. package/src/main/api/morality.ts +31 -0
  29. package/src/main/api/sex.ts +8 -1
  30. package/src/main/api/zone-data.ts +20 -0
  31. package/src/main/db/abstract-index.ts +37 -0
  32. package/src/main/db/alignment-list.ts +54 -0
  33. package/src/main/db/alternates.ts +28 -42
  34. package/src/main/db/badge-index.ts +60 -0
  35. package/src/main/db/badge-requirement.ts +81 -0
  36. package/src/main/db/badge-search-options.ts +47 -0
  37. package/src/main/db/badge.ts +76 -71
  38. package/src/main/db/bundle-header.ts +44 -0
  39. package/src/main/db/coh-content-database.ts +123 -14
  40. package/src/main/db/contact.ts +62 -0
  41. package/src/main/db/location.ts +30 -0
  42. package/src/main/db/mission.ts +107 -0
  43. package/src/main/db/morality-list.ts +99 -0
  44. package/src/main/db/paged.ts +7 -0
  45. package/src/main/db/zone.ts +28 -0
  46. package/src/main/index.ts +23 -15
  47. package/src/main/util.ts +108 -7
  48. package/src/test/api/alignment.test.ts +38 -4
  49. package/src/test/api/badge-data.fixture.ts +1 -15
  50. package/src/test/api/badge-data.test.ts +4 -4
  51. package/src/test/api/badge-requirement-data.fixture.ts +7 -0
  52. package/src/test/api/badge-requirement-type.test.ts +31 -0
  53. package/src/test/api/badge-type.test.ts +5 -5
  54. package/src/test/api/bundle-data.fixture.ts +6 -0
  55. package/src/test/api/bundle-header-data.fixture.ts +6 -0
  56. package/src/test/api/contact-data.fixture.ts +7 -0
  57. package/src/test/api/enhancement-category.test.ts +5 -5
  58. package/src/test/api/mission-data.fixture.ts +12 -0
  59. package/src/test/api/sex.test.ts +33 -1
  60. package/src/test/api/zone-data.fixture.ts +8 -0
  61. package/src/test/db/abstract-index.test.ts +55 -0
  62. package/src/test/db/alignment-list.test.ts +200 -0
  63. package/src/test/db/alternates.test.ts +82 -117
  64. package/src/test/db/badge-index.test.ts +547 -0
  65. package/src/test/db/badge-requirement.test.ts +145 -0
  66. package/src/test/db/badge.test.ts +322 -14
  67. package/src/test/db/bundle-header.test.ts +76 -0
  68. package/src/test/db/coh-content-database.test.ts +264 -24
  69. package/src/test/db/contact.test.ts +97 -0
  70. package/src/test/db/location.test.ts +51 -0
  71. package/src/test/db/mission.test.ts +171 -0
  72. package/src/test/db/morality-list.test.ts +457 -0
  73. package/src/test/db/zone.test.ts +36 -0
  74. package/src/test/integration.test.ts +16 -0
  75. package/src/test/util.test.ts +144 -18
  76. package/src/main/api/badge-partial-data.ts +0 -65
  77. package/src/main/api/badge-partial-type.ts +0 -8
  78. package/src/main/api/change.ts +0 -14
  79. package/src/main/api/game-map-data.ts +0 -26
  80. package/src/main/api/plaque-type.ts +0 -6
  81. package/src/main/api/server-group-data.ts +0 -65
  82. package/src/main/api/vidiot-map-data.ts +0 -18
  83. package/src/main/api/vidiot-map-point-of-interest-data.ts +0 -30
  84. package/src/main/changelog.ts +0 -20
  85. package/src/main/db/badge-partial.ts +0 -35
  86. package/src/main/db/game-map.ts +0 -33
  87. package/src/main/db/server-group.ts +0 -112
  88. package/src/main/db/vidiot-map-point-of-interest.ts +0 -40
  89. package/src/main/db/vidiot-map.ts +0 -25
  90. package/src/test/api/badge-partial-data.fixture.ts +0 -17
  91. package/src/test/api/badge-partial-type.test.ts +0 -31
  92. package/src/test/api/game-map-data.fixture.ts +0 -10
  93. package/src/test/api/plaque-type.test.ts +0 -31
  94. package/src/test/api/server-group-data.fixture.ts +0 -23
  95. package/src/test/api/server-group-data.test.ts +0 -15
  96. package/src/test/api/vidiot-map-point-of-interest.fixture.ts +0 -10
  97. package/src/test/api/vidiot-map.fixture.ts +0 -9
  98. package/src/test/changelog.test.ts +0 -36
  99. package/src/test/db/server-group.test.ts +0 -124
  100. package/src/test/index.test.ts +0 -10
@@ -0,0 +1,37 @@
1
+ import { Link } from './link'
2
+ import { MarkdownString } from './markdown-string'
3
+
4
+ /**
5
+ * Metadata about a content bundle.
6
+ */
7
+ export interface BundleHeaderData {
8
+ /**
9
+ * Name of the fork this bundle contains data for.
10
+ */
11
+ readonly name?: string
12
+
13
+ /**
14
+ * Description of the fork.
15
+ */
16
+ readonly description?: MarkdownString
17
+
18
+ /**
19
+ * Url for the repository where the bundle is maintained.
20
+ */
21
+ readonly repositoryUrl?: string
22
+
23
+ /**
24
+ * Url for the location of the changelog.
25
+ */
26
+ readonly changelogUrl?: string
27
+
28
+ /**
29
+ * List of external links. Wiki, forums, etc.
30
+ */
31
+ readonly links?: Link[]
32
+
33
+ /**
34
+ * Version number for this data package.
35
+ */
36
+ readonly version?: string
37
+ }
@@ -0,0 +1,48 @@
1
+ import { Link } from './link'
2
+ import { MarkdownString } from './markdown-string'
3
+ import { LocationData } from './location-data'
4
+ import { MoralityExtended } from './morality'
5
+
6
+ export interface ContactData {
7
+ /**
8
+ * Unique key used to reference this contact.
9
+ *
10
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
11
+ */
12
+ readonly key: string
13
+
14
+ /**
15
+ * The name of this contact.
16
+ */
17
+ readonly name: string
18
+
19
+ /**
20
+ * The contact's title.
21
+ */
22
+ readonly title?: string
23
+
24
+ /**
25
+ * The character moralities that this contact will interact with.
26
+ */
27
+ readonly morality?: MoralityExtended | MoralityExtended[]
28
+
29
+ /**
30
+ * The location of this contact.
31
+ */
32
+ readonly location?: LocationData
33
+
34
+ /**
35
+ * The level range this contact will offer missions for.
36
+ */
37
+ readonly levelRange?: [number, number?]
38
+
39
+ /**
40
+ * Freeform notes or tips about the contact.
41
+ */
42
+ readonly notes?: MarkdownString
43
+
44
+ /**
45
+ * List of external links. Wiki, forums, etc.
46
+ */
47
+ readonly links?: Link[]
48
+ }
@@ -1,30 +1,30 @@
1
1
  export const ENHANCEMENT_CATEGORY = [
2
- 'DEFENSE_DEBUFF',
3
- 'TO_HIT_DEBUFF',
4
- 'TAUNT',
5
- 'CONFUSE',
6
- 'HEALING',
7
- 'DEFENSE_BUFF',
8
- 'RESIST_DAMAGE',
9
- 'INTANGIBILITY',
10
- 'SLEEP',
11
- 'SLOW',
12
- 'HOLD',
13
- 'STUN',
14
- 'IMMOBILIZE',
15
- 'FEAR',
16
- 'ENDURANCE_MODIFICATION',
17
- 'ENDURANCE_REDUCTION',
18
- 'RECHARGE_REDUCTION',
19
- 'INTERRUPT_DURATION',
20
- 'ACCURACY',
21
- 'TO_HIT_BUFF',
22
- 'DAMAGE',
23
- 'KNOCKBACK',
24
- 'RUN_SPEED',
25
- 'JUMP',
26
- 'FLY_SPEED',
27
- 'RANGE',
2
+ 'defense-debuff',
3
+ 'to-hit-debuff',
4
+ 'taunt',
5
+ 'confuse',
6
+ 'healing',
7
+ 'defense-buff',
8
+ 'resist-damage',
9
+ 'intangibility',
10
+ 'sleep',
11
+ 'slow',
12
+ 'hold',
13
+ 'stun',
14
+ 'immobilize',
15
+ 'fear',
16
+ 'endurance-modification',
17
+ 'endurance-reduction',
18
+ 'recharge-reduction',
19
+ 'interrupt-duration',
20
+ 'accuracy',
21
+ 'to-hit-buff',
22
+ 'damage',
23
+ 'knockback',
24
+ 'run-speed',
25
+ 'jump',
26
+ 'fly-speed',
27
+ 'range',
28
28
  ] as const
29
29
 
30
30
  export type EnhancementCategory = typeof ENHANCEMENT_CATEGORY[number]
@@ -0,0 +1,28 @@
1
+ export interface LocationData {
2
+ /**
3
+ * Key of the {@link Zone} that the location references.
4
+ */
5
+ readonly zoneKey?: string
6
+
7
+ /**
8
+ * In-game `/loc` coordinates of the location.
9
+ */
10
+ readonly coords?: Coords
11
+
12
+ /**
13
+ * The type of icon to use if the location appears on a map. (Typically the Vidiot map icon).
14
+ */
15
+ readonly icon?: LocationIcon
16
+
17
+ /**
18
+ * The text that should appear in the location icon. (Typically a number or symbol from the Vidiot map).
19
+ */
20
+ readonly iconText?: string
21
+ }
22
+
23
+ /**
24
+ * Coordinates as they appear using the in-game `/loc` command.
25
+ */
26
+ export type Coords = [number, number, number]
27
+
28
+ export type LocationIcon = 'badge' | 'plaque' | 'pedestal' | 'object'
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tage type to indicate that a string will accept {@link https://www.markdownguide.org/|Markdown} format.
3
+ */
4
+ export type MarkdownString = string
@@ -0,0 +1,83 @@
1
+ import { Link } from './link'
2
+ import { MarkdownString } from './markdown-string'
3
+ import { MissionType } from './mission-type'
4
+ import { MoralityExtended } from './morality'
5
+
6
+ export interface MissionData {
7
+ /**
8
+ * Unique key used to reference this mission.
9
+ *
10
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
11
+ */
12
+ readonly key: string
13
+
14
+ /**
15
+ * The name of the mission as it appears from the contact.
16
+ *
17
+ * The name may be different when viewed in Ouroboros as a Flashback.
18
+ */
19
+ readonly name: string
20
+
21
+ /**
22
+ * The type of mission... Story arc, task force, trial, etc.
23
+ */
24
+ readonly type: MissionType
25
+
26
+ /**
27
+ * The character moralities that may accept the mission.
28
+ */
29
+ readonly morality?: MoralityExtended | MoralityExtended[]
30
+
31
+ /**
32
+ * The keys of any contacts that provide this mission.
33
+ */
34
+ readonly contactKeys?: string | string[]
35
+
36
+ /**
37
+ * The level range this mission is available for.
38
+ */
39
+ readonly levelRange?: [number, number?]
40
+
41
+ /**
42
+ * Freeform notes or tips about the mission.
43
+ */
44
+ readonly notes?: MarkdownString
45
+
46
+ /**
47
+ * List of external links. Wiki, forums, etc.
48
+ */
49
+ readonly links?: Link[]
50
+
51
+ /**
52
+ * If the mission is available in Ouroboros as a Flashback.
53
+ */
54
+ readonly flashback?: MissionFlashbackData
55
+ }
56
+
57
+ export interface MissionFlashbackData {
58
+
59
+ /**
60
+ * The id of the mission as seen in the Flashback menu, i.e. '14.01'.
61
+ */
62
+ readonly id: string
63
+
64
+ /**
65
+ * The level range this mission appears under as a Flashback. Leave undefined if the same as the base mission.
66
+ */
67
+ readonly levelRange?: [number, number?]
68
+
69
+ /**
70
+ * The name as it appears in the Flashback list. Leave undefined if the same as the base mission.
71
+ */
72
+ readonly name?: string
73
+
74
+ /**
75
+ * The character moralities that the mission will appear for in the Flashback list. Leave undefined if the same as the base mission.
76
+ */
77
+ readonly morality?: MoralityExtended | MoralityExtended[]
78
+
79
+ /**
80
+ * Freeform notes or tips about the Flashback version of the mission.
81
+ */
82
+ readonly notes?: MarkdownString
83
+ }
@@ -0,0 +1,2 @@
1
+ export const MISSION_TYPE = ['story-arc', 'mission', 'task-force', 'strike-force', 'trial', 'personal-story'] as const
2
+ export type MissionType = typeof MISSION_TYPE[number]
@@ -0,0 +1,31 @@
1
+ export const MORALITY = ['hero', 'vigilante', 'villain', 'rogue', 'resistance', 'loyalist'] as const
2
+ export type Morality = typeof MORALITY[number]
3
+ export type MoralityExtended = Morality
4
+ /**
5
+ * Any of the Primal Earth moralities - Hero, Vigilante, Villain, Rogue.
6
+ */
7
+ | 'primal'
8
+ /**
9
+ * Either of the Praetorian Earth moralities - Resistance or Loyalist.
10
+ */
11
+ | 'praetorian'
12
+ /**
13
+ * The moralities that roll up to the Hero {@link Alignment} - Hero and Vigilante.
14
+ */
15
+ | 'heroic'
16
+ /**
17
+ * The moralities that roll up to the Villain {@link Alignment} - Villain and Rogue.
18
+ */
19
+ | 'villainous'
20
+ /**
21
+ * Moralities with access to Paragon City - Hero, Vigilante and Rogue.
22
+ */
23
+ | 'paragon-city-access'
24
+ /**
25
+ * Moralities with access to the Rogue Isles - Villain, Rogue and Vigilante.
26
+ */
27
+ | 'rogue-isles-access'
28
+ /**
29
+ * All the moralities.
30
+ */
31
+ | 'all'
@@ -1,3 +1,10 @@
1
1
  export const SEX = ['M', 'F'] as const
2
-
3
2
  export type Sex = typeof SEX[number]
3
+
4
+ const SEX_ORDER = Object.fromEntries(SEX.map((x, index) => [x, index]))
5
+
6
+ export function compareSex(a?: Sex, b?: Sex): number {
7
+ const orderA = a ? SEX_ORDER[a] : -1
8
+ const orderB = b ? SEX_ORDER[b] : -1
9
+ return orderA - orderB
10
+ }
@@ -0,0 +1,20 @@
1
+ import { Link } from './link'
2
+
3
+ export interface ZoneData {
4
+ /**
5
+ * Unique key used to reference this zone.
6
+ *
7
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
8
+ */
9
+ readonly key: string
10
+
11
+ /**
12
+ * The name of the zone as it appears in-game.
13
+ */
14
+ readonly name: string
15
+
16
+ /**
17
+ * List of external links. Wiki, forums, etc.
18
+ */
19
+ readonly links?: Link[]
20
+ }
@@ -0,0 +1,37 @@
1
+ type KeysOfType<T, V> = { [P in keyof T]: T[P] extends V ? P : never }[keyof T]
2
+
3
+ export class AbstractIndex<T> {
4
+ protected _values: T[] = []
5
+ protected _hashTable: Record<string, T> = {}
6
+
7
+ /**
8
+ * Create a new index.
9
+ * @param keyField The field of the values that will act as the key.
10
+ * @param values Values to index.
11
+ */
12
+ constructor(keyField: KeysOfType<T, string>, values: T[] | undefined) {
13
+ this._values = values ?? []
14
+ this._hashTable = {}
15
+ for (const value of this.values) {
16
+ const key = value[keyField] as string
17
+ if (this._hashTable[key] !== undefined) throw new Error(`Duplicate key [${key}]`)
18
+ this._hashTable[key] = value
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Return all indexed values
24
+ */
25
+ get values(): T[] {
26
+ return this._values
27
+ }
28
+
29
+ /**
30
+ * Get a value from the index
31
+ * @param key Key string
32
+ */
33
+ get(key: string | undefined): T | undefined {
34
+ if (!key) return undefined
35
+ return this._hashTable[key]
36
+ }
37
+ }
@@ -0,0 +1,54 @@
1
+ import { ALIGNMENT, Alignment, AlignmentExtended } from '../api/alignment'
2
+
3
+ export class AlignmentList {
4
+ readonly #items: Set<Alignment>
5
+
6
+ readonly hero: boolean
7
+ readonly villain: boolean
8
+ readonly praetorian: boolean
9
+
10
+ readonly primal: boolean
11
+ readonly all: boolean
12
+
13
+ constructor(items?: AlignmentExtended[]) {
14
+ const set = new Set(items ?? [...ALIGNMENT])
15
+ this.hero = set.has('hero') || set.has('primal') || set.has('all')
16
+ this.villain = set.has('villain') || set.has('primal') || set.has('all')
17
+ this.praetorian = set.has('praetorian') || set.has('all')
18
+
19
+ this.primal = this.hero && this.villain
20
+ this.all = this.hero && this.villain && this.praetorian
21
+
22
+ this.#items = new Set()
23
+ if (this.hero) this.#items.add('hero')
24
+ if (this.villain) this.#items.add('villain')
25
+ if (this.praetorian) this.#items.add('praetorian')
26
+ }
27
+
28
+ get items(): Alignment[] {
29
+ return [...this.#items]
30
+ }
31
+
32
+ has(alignment?: AlignmentExtended): boolean {
33
+ switch (alignment) {
34
+ case 'hero': {
35
+ return this.hero
36
+ }
37
+ case 'villain': {
38
+ return this.villain
39
+ }
40
+ case 'praetorian': {
41
+ return this.praetorian
42
+ }
43
+ case 'primal' : {
44
+ return this.primal
45
+ }
46
+ case 'all': {
47
+ return this.all
48
+ }
49
+ default: {
50
+ return false
51
+ }
52
+ }
53
+ }
54
+ }
@@ -1,33 +1,39 @@
1
1
  import { AlternateData } from '../api/alternate-data'
2
- import { Sex } from '../api/sex'
3
- import { Alignment } from '../api/alignment'
4
-
5
- const ALIGNMENT_SORT: Record<string, number> = { H: 2, V: 1, P: 0 }
6
- const SEX_SORT: Record<string, number> = { M: 1, F: 0 }
2
+ import { compareSex, Sex } from '../api/sex'
3
+ import { Alignment, compareAlignment } from '../api/alignment'
7
4
 
8
5
  export class Alternates<T> {
9
6
  readonly #sortedValues: AlternateData<T>[] = []
10
7
 
11
- constructor(values: AlternateData<T>[]) {
12
- this.#sortedValues = values.sort()
13
- this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
8
+ /**
9
+ * Create an alternate set from either a list of categorized values, or a single value when there are no alternates.
10
+ * @param value List of alternates, or a single value.
11
+ */
12
+ constructor(value: AlternateData<T>[] | T) {
13
+ if (Array.isArray(value)) {
14
+ this.#sortedValues = value.sort()
15
+ this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
16
+ } else {
17
+ this.#sortedValues = [{ value }]
18
+ }
14
19
  }
15
20
 
16
- getValue(alignment?: Alignment | string, sex?: Sex | string): T | undefined {
21
+ getValue(alignment?: Alignment, sex?: Sex): T | undefined {
17
22
  for (let index = this.#sortedValues.length; index--;) {
18
23
  const entry = this.#sortedValues[index]
19
24
  if ((entry.alignment === undefined || entry.alignment === alignment)
20
25
  && (entry.sex === undefined || entry.sex === sex)
21
26
  ) return entry.value
22
27
  }
23
- return undefined
28
+
29
+ return this.default?.value
24
30
  }
25
31
 
26
32
  /**
27
33
  * Get the default value for this list of alternates, the value with the highest priority and lowest specificity.
28
34
  */
29
- get default(): T | undefined {
30
- return this.#sortedValues[0]?.value
35
+ get default(): AlternateData<T> | undefined {
36
+ return this.#sortedValues[0]
31
37
  }
32
38
 
33
39
  /**
@@ -37,45 +43,25 @@ export class Alternates<T> {
37
43
  return this.#sortedValues
38
44
  }
39
45
 
46
+ /**
47
+ * Create a joined string from the alternate values in canonical order.
48
+ * @param separator Separator to use. Default is ' / '
49
+ */
50
+ toString(separator: string): string {
51
+ return this.canonical.map(x => x.value).join(separator)
52
+ }
53
+
40
54
  #compareAlternates(a: AlternateData<T>, b: AlternateData<T>): number {
41
55
  const aSpecificity = (a.alignment ? 2 : 0) + (a.sex ? 1 : 0)
42
56
  const bSpecificity = (b.alignment ? 2 : 0) + (b.sex ? 1 : 0)
43
57
  if (aSpecificity !== bSpecificity) return aSpecificity - bSpecificity // Order first by least-specific
44
58
 
45
- const alignmentComparison = this.#compareAlignment(a.alignment, b.alignment) // Next by alignment
59
+ const alignmentComparison = compareAlignment(a.alignment, b.alignment) // Next by alignment
46
60
  if (alignmentComparison !== 0) return alignmentComparison
47
61
 
48
- const sexComparison = this.#compareSex(a.sex, b.sex) // Last by sex
62
+ const sexComparison = compareSex(a.sex, b.sex) // Last by sex
49
63
  if (sexComparison !== 0) return sexComparison
50
64
 
51
65
  return String(a.value).localeCompare(String(b.value))
52
66
  }
53
-
54
- #compareAlignment(a: Alignment | string | undefined, b: Alignment | string | undefined): number {
55
- if (a === b) return 0
56
- if (a === undefined && b !== undefined) return -1
57
- if (b === undefined && a !== undefined) return 1
58
-
59
- const aSort = a === undefined ? -1 : ALIGNMENT_SORT[a] ?? -1 // Unknown values get -1 priority
60
- const bSort = b === undefined ? -1 : ALIGNMENT_SORT[b] ?? -1
61
-
62
- if (aSort !== bSort) return bSort - aSort
63
-
64
- // Unknown values (not in ALIGNMENT_SORT) are sorted alphabetically
65
- return a?.localeCompare(b ?? '') ?? 0
66
- }
67
-
68
- #compareSex(a?: Sex | string | undefined, b?: Sex | string | undefined): number {
69
- if (a === b) return 0
70
- if (a === undefined && b !== undefined) return -1
71
- if (b === undefined && a !== undefined) return 1
72
-
73
- const aSort = SEX_SORT[a ?? -1] ?? -1 // Unknown values get -1 priority
74
- const bSort = SEX_SORT[b ?? -1] ?? -1
75
-
76
- if (aSort !== bSort) return bSort - aSort
77
-
78
- // Unknown values (not in SEX_SORT) are sorted alphabetically
79
- return a?.localeCompare(b ?? '') ?? 0
80
- }
81
67
  }
@@ -0,0 +1,60 @@
1
+ import { Badge, compareByDefaultName, 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 filtered = (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 totalPages = options?.pageSize ? Math.ceil(filtered.length / (options?.pageSize)) : 1
17
+ const page = Math.max(1, Math.min(totalPages, options?.page ?? 1))
18
+ const paged = options?.pageSize ? filtered.slice((page - 1) * options.pageSize, page * options?.pageSize) : filtered
19
+
20
+ const sorted = this.#sort(paged, options?.sort)
21
+
22
+ return {
23
+ items: sorted,
24
+ page: page,
25
+ pageSize: options?.pageSize,
26
+ totalItems: filtered.length,
27
+ totalPages: totalPages,
28
+ }
29
+ }
30
+
31
+ #satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
32
+ const queryString = query?.str?.toLowerCase() ?? ''
33
+ const fields = query?.fields ? new Set(query?.fields) : new Set(['name']) // Default to name if not provided
34
+ if (fields.size === 0) return true
35
+
36
+ return !!((fields.has('name') && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
37
+ || (fields.has('badge-text') && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
38
+ || (fields.has('acquisition') && badge.acquisition?.toLowerCase().includes(queryString))
39
+ || (fields.has('effect') && badge.effect?.toLowerCase().includes(queryString))
40
+ || (fields.has('notes') && badge.notes?.toLowerCase().includes(queryString))
41
+ || (fields.has('set-title-id') && (badge.setTitleId?.some(x => x?.toString().includes(queryString)))))
42
+ }
43
+
44
+ #satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
45
+ return (!filter?.type || badge.type === filter.type)
46
+ && (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
47
+ && (!filter?.morality || badge.morality.has(filter.morality))
48
+ }
49
+
50
+ #sort(badges: Badge[], sort?: BadgeSearchOptions['sort']): Badge[] {
51
+ if (!sort) return badges
52
+ const ascending = sort.dir !== 'desc'
53
+
54
+ if (sort.by === 'badge-name') return badges.sort((a, b) => ascending ? compareByDefaultName(a, b) : compareByDefaultName(b, a))
55
+
56
+ if (sort.by === 'zone-key') return badges.sort((a, b) => ascending ? compareByZoneKey(a, b) : compareByZoneKey(b, a))
57
+
58
+ return sort.dir === 'desc' ? badges.reverse() : badges
59
+ }
60
+ }
@@ -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'
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
+ }