coh-content-db 2.0.0-rc.15 → 2.0.0-rc.17

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 (45) hide show
  1. package/.github/workflows/build.yml +1 -1
  2. package/CHANGELOG.md +3 -1
  3. package/README.md +9 -1
  4. package/dist/coh-content-db.d.ts +93 -44
  5. package/dist/coh-content-db.js +230 -121
  6. package/dist/coh-content-db.js.map +1 -1
  7. package/dist/coh-content-db.mjs +228 -121
  8. package/dist/coh-content-db.mjs.map +1 -1
  9. package/jest.config.mjs +1 -0
  10. package/package.json +14 -14
  11. package/src/main/api/badge-data.ts +2 -1
  12. package/src/main/api/contact-data.ts +2 -1
  13. package/src/main/api/level-range-data.ts +4 -0
  14. package/src/main/api/mission-data.ts +3 -29
  15. package/src/main/api/mission-flashback-data.ts +31 -0
  16. package/src/main/api/set-title-data.ts +4 -0
  17. package/src/main/api/zone-data.ts +24 -0
  18. package/src/main/api/zone-type.ts +59 -0
  19. package/src/main/db/alternates.ts +1 -1
  20. package/src/main/db/badge-index.ts +10 -8
  21. package/src/main/db/badge-requirement.ts +1 -1
  22. package/src/main/db/badge.ts +5 -4
  23. package/src/main/db/bundle-header.ts +1 -1
  24. package/src/main/db/contact.ts +5 -4
  25. package/src/main/db/level-range.ts +15 -0
  26. package/src/main/db/mission.ts +9 -8
  27. package/src/main/db/set-title-ids.ts +10 -0
  28. package/src/main/db/zone.ts +29 -0
  29. package/src/main/index.ts +8 -2
  30. package/src/main/util/coalesce-to-array.ts +13 -0
  31. package/src/main/{util.ts → util/links.ts} +8 -22
  32. package/src/test/api/alignment.test.ts +2 -2
  33. package/src/test/api/sex.test.ts +2 -2
  34. package/src/test/api/zone-data.fixture.ts +1 -0
  35. package/src/test/db/badge.test.ts +24 -11
  36. package/src/test/db/contact.test.ts +2 -1
  37. package/src/test/db/level-range.test.ts +47 -0
  38. package/src/test/db/mission.test.ts +8 -6
  39. package/src/test/db/morality-list.test.ts +1 -1
  40. package/src/test/db/set-title-ids.test.ts +19 -0
  41. package/src/test/db/zone.test.ts +45 -0
  42. package/src/test/util/coalese-to-array.test.ts +17 -0
  43. package/src/test/{util.test.ts → util/links.test.ts} +5 -21
  44. package/src/test/{to-date.test.ts → util/to-date.test.ts} +1 -1
  45. /package/src/main/{to-date.ts → util/to-date.ts} +0 -0
@@ -11,7 +11,7 @@ export class Alternates<T> {
11
11
  */
12
12
  constructor(value: AlternateData<T>[] | T) {
13
13
  if (Array.isArray(value)) {
14
- this.#sortedValues = value.sort()
14
+ this.#sortedValues = value.toSorted()
15
15
  this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
16
16
  } else {
17
17
  this.#sortedValues = [{ value }]
@@ -38,7 +38,9 @@ export class BadgeIndex extends AbstractIndex<Badge> {
38
38
  || (fields.has('acquisition') && badge.acquisition?.toLowerCase().includes(queryString))
39
39
  || (fields.has('effect') && badge.effect?.toLowerCase().includes(queryString))
40
40
  || (fields.has('notes') && badge.notes?.toLowerCase().includes(queryString))
41
- || (fields.has('set-title-id') && (badge.setTitleId?.some(x => x?.toString().includes(queryString)))))
41
+ || (fields.has('set-title-id') && (badge.setTitleId?.primal.toString() === queryString))
42
+ || (fields.has('set-title-id') && (badge.setTitleId?.praetorian?.toString() === queryString))
43
+ )
42
44
  }
43
45
 
44
46
  #satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
@@ -50,25 +52,25 @@ export class BadgeIndex extends AbstractIndex<Badge> {
50
52
  #sort(badges: Badge[], sort?: BadgeSearchOptions['sort']): Badge[] {
51
53
  switch (sort) {
52
54
  case 'name.asc': {
53
- return badges.sort(compareByDefaultName)
55
+ return badges.toSorted(compareByDefaultName)
54
56
  }
55
57
  case 'name.desc': {
56
- return badges.sort((a, b) => compareByDefaultName(b, a))
58
+ return badges.toSorted((a, b) => compareByDefaultName(b, a))
57
59
  }
58
60
  case 'zone-key.asc': {
59
- return badges.sort(compareByZoneKey)
61
+ return badges.toSorted(compareByZoneKey)
60
62
  }
61
63
  case 'zone-key.desc': {
62
- return badges.sort((a, b) => compareByZoneKey(b, a))
64
+ return badges.toSorted((a, b) => compareByZoneKey(b, a))
63
65
  }
64
66
  case 'release-date.asc': {
65
- return badges.sort(compareByReleaseDate)
67
+ return badges.toSorted(compareByReleaseDate)
66
68
  }
67
69
  case 'release-date.desc': {
68
- return badges.sort((a, b) => compareByReleaseDate(b, a))
70
+ return badges.toSorted((a, b) => compareByReleaseDate(b, a))
69
71
  }
70
72
  case 'canonical.desc': {
71
- return badges.reverse()
73
+ return badges.toReversed()
72
74
  }
73
75
  default: {
74
76
  return [...badges]
@@ -5,7 +5,7 @@ import { Key } from './key'
5
5
  import { MarkdownString } from '../api/markdown-string'
6
6
  import { Link } from '../api/link'
7
7
  import { Location } from './location'
8
- import { coalesceToArray } from '../util'
8
+ import { coalesceToArray } from '../util/coalesce-to-array'
9
9
 
10
10
  export class BadgeRequirement {
11
11
  /**
@@ -5,10 +5,11 @@ import { BadgeRequirement } from './badge-requirement'
5
5
  import { Key } from './key'
6
6
  import { Alternates } from './alternates'
7
7
  import { MarkdownString } from '../api/markdown-string'
8
- import { coalesceToArray } from '../util'
9
8
  import { MoralityList } from './morality-list'
10
9
  import { AbstractIndex } from './abstract-index'
11
- import { toDate } from '../to-date'
10
+ import { toDate } from '../util/to-date'
11
+ import { coalesceToArray } from '../util/coalesce-to-array'
12
+ import { SetTitleIds } from './set-title-ids'
12
13
 
13
14
  export class Badge {
14
15
  readonly #requirementsIndex: AbstractIndex<BadgeRequirement>
@@ -72,7 +73,7 @@ export class Badge {
72
73
  * The id used with the in-game `/settitle` command to apply the badge.
73
74
  * The first value is the id for primal characters and the (optional) second number is the id for praetorian characters.
74
75
  */
75
- readonly setTitleId?: [number, number?]
76
+ readonly setTitleId?: SetTitleIds
76
77
 
77
78
  /**
78
79
  * A description of the effect the badge will have, such as a buff or granting a temporary power.
@@ -96,7 +97,7 @@ export class Badge {
96
97
  this.notes = badgeData.notes
97
98
  this.links = badgeData.links ?? []
98
99
  this.effect = badgeData.effect
99
- this.setTitleId = badgeData.setTitleId
100
+ this.setTitleId = badgeData.setTitleId ? new SetTitleIds(badgeData.setTitleId) : undefined
100
101
  this.ignoreInTotals = badgeData.ignoreInTotals ?? false
101
102
 
102
103
  this.#requirementsIndex = new AbstractIndex<BadgeRequirement>('key', badgeData.requirements?.map(x => new BadgeRequirement(x)))
@@ -1,7 +1,7 @@
1
1
  import { Link } from '../api/link'
2
2
  import { MarkdownString } from '../api/markdown-string'
3
3
  import { BundleHeaderData } from '../api/bundle-header-data'
4
- import { toDate } from '../to-date'
4
+ import { toDate } from '../util/to-date'
5
5
 
6
6
  export class BundleHeader {
7
7
  /**
@@ -4,7 +4,8 @@ import { MarkdownString } from '../api/markdown-string'
4
4
  import { ContactData } from '../api/contact-data'
5
5
  import { Location } from './location'
6
6
  import { MoralityList } from './morality-list'
7
- import { coalesceToArray } from '../util'
7
+ import { coalesceToArray } from '../util/coalesce-to-array'
8
+ import { LevelRange } from './level-range'
8
9
 
9
10
  export class Contact {
10
11
  /**
@@ -27,7 +28,7 @@ export class Contact {
27
28
  /**
28
29
  * The character moralities that this contact will interact with.
29
30
  */
30
- readonly morality?: MoralityList
31
+ readonly morality: MoralityList
31
32
 
32
33
  /**
33
34
  * The location of this contact.
@@ -37,7 +38,7 @@ export class Contact {
37
38
  /**
38
39
  * The level range this contact will offer missions for.
39
40
  */
40
- readonly levelRange?: [number, number?]
41
+ readonly levelRange?: LevelRange
41
42
 
42
43
  /**
43
44
  * Freeform notes or tips about the contact.
@@ -55,7 +56,7 @@ export class Contact {
55
56
  this.title = data.title
56
57
  this.morality = new MoralityList(coalesceToArray(data.morality))
57
58
  this.location = data.location
58
- this.levelRange = data.levelRange
59
+ this.levelRange = data.levelRange ? new LevelRange(data.levelRange) : undefined
59
60
  this.notes = data.notes
60
61
  this.links = data.links ?? []
61
62
  }
@@ -0,0 +1,15 @@
1
+ import { LevelRangeData } from '../api/level-range-data'
2
+
3
+ export class LevelRange {
4
+ readonly min: number
5
+ readonly max?: number
6
+
7
+ constructor(value: LevelRangeData) {
8
+ if (Array.isArray(value)) {
9
+ this.min = value[0]
10
+ this.max = value[1] === undefined ? undefined : value[1]
11
+ } else {
12
+ this.min = value
13
+ }
14
+ }
15
+ }
@@ -3,8 +3,9 @@ import { MarkdownString } from '../api/markdown-string'
3
3
  import { Link } from '../api/link'
4
4
  import { MissionData } from '../api/mission-data'
5
5
  import { Key } from './key'
6
- import { coalesceToArray } from '../util'
7
6
  import { MoralityList } from './morality-list'
7
+ import { coalesceToArray } from '../util/coalesce-to-array'
8
+ import { LevelRange } from './level-range'
8
9
 
9
10
  export class Mission {
10
11
  /**
@@ -39,7 +40,7 @@ export class Mission {
39
40
  /**
40
41
  * The level range this mission is available for.
41
42
  */
42
- readonly levelRange?: [number, number?]
43
+ readonly levelRange?: LevelRange
43
44
 
44
45
  /**
45
46
  * Freeform notes or tips about the mission.
@@ -62,17 +63,17 @@ export class Mission {
62
63
  readonly id: string
63
64
 
64
65
  /**
65
- * The level range this mission appears under as a Flashback. Leave undefined if the same as the base mission.
66
+ * The level range this mission appears under as a Flashback.
66
67
  */
67
- readonly levelRange?: [number, number?]
68
+ readonly levelRange?: LevelRange
68
69
 
69
70
  /**
70
- * The name as it appears in the Flashback list. Leave undefined if the same as the base mission.
71
+ * The name as it appears in the Flashback list.
71
72
  */
72
73
  readonly name?: string
73
74
 
74
75
  /**
75
- * The character moralities that the mission will appear for in the Flashback list. Leave undefined if the same as the base mission.
76
+ * The character moralities that the mission will appear for in the Flashback list.
76
77
  */
77
78
  readonly morality?: MoralityList
78
79
 
@@ -88,7 +89,7 @@ export class Mission {
88
89
  this.type = data.type
89
90
  this.morality = new MoralityList(coalesceToArray(data.morality))
90
91
  this.contactKeys = coalesceToArray(data.contactKeys)
91
- this.levelRange = data.levelRange
92
+ this.levelRange = data.levelRange ? new LevelRange(data.levelRange) : undefined
92
93
  this.notes = data.notes
93
94
  this.links = data.links ?? []
94
95
  this.flashback = createFlashback(data)
@@ -99,7 +100,7 @@ function createFlashback(data: MissionData): Mission['flashback'] {
99
100
  if (!data.flashback) return undefined
100
101
  return {
101
102
  id: data.flashback.id,
102
- levelRange: data.flashback.levelRange ?? data.levelRange,
103
+ levelRange: data.flashback.levelRange ? new LevelRange(data.flashback.levelRange) : undefined,
103
104
  name: data.flashback.name ?? data.name,
104
105
  morality: new MoralityList(coalesceToArray(data.flashback.morality ?? data.morality)),
105
106
  notes: data.flashback.notes,
@@ -0,0 +1,10 @@
1
+ import { SetTitleData } from '../api/set-title-data'
2
+
3
+ export class SetTitleIds {
4
+ readonly primal: number
5
+ readonly praetorian?: number
6
+
7
+ constructor(value: SetTitleData) {
8
+ [this.primal, this.praetorian] = value
9
+ }
10
+ }
@@ -1,6 +1,11 @@
1
1
  import { Link } from '../api/link'
2
2
  import { ZoneData } from '../api/zone-data'
3
3
  import { Key } from './key'
4
+ import { MoralityList } from './morality-list'
5
+ import { coalesceToArray } from '../util/coalesce-to-array'
6
+ import { ZoneType } from '../api/zone-type'
7
+ import { MarkdownString } from '../api/markdown-string'
8
+ import { LevelRange } from './level-range'
4
9
 
5
10
  export class Zone {
6
11
  /**
@@ -15,6 +20,26 @@ export class Zone {
15
20
  */
16
21
  readonly name: string
17
22
 
23
+ /**
24
+ * The type of zone.
25
+ */
26
+ readonly type: ZoneType
27
+
28
+ /**
29
+ * The character moralities that this zone is accessible by.
30
+ */
31
+ readonly morality: MoralityList
32
+
33
+ /**
34
+ * The level range this zone is recommended for.
35
+ */
36
+ readonly levelRange?: LevelRange
37
+
38
+ /**
39
+ * Freeform notes or tips about the zone.
40
+ */
41
+ readonly notes?: MarkdownString
42
+
18
43
  /**
19
44
  * List of external links. Wiki, forums, etc.
20
45
  */
@@ -23,6 +48,10 @@ export class Zone {
23
48
  constructor(data: ZoneData) {
24
49
  this.key = new Key(data.key).value
25
50
  this.name = data.name
51
+ this.type = data.type
52
+ this.morality = new MoralityList(coalesceToArray(data.morality))
53
+ this.levelRange = data.levelRange ? new LevelRange(data.levelRange) : undefined
54
+ this.notes = data.notes
26
55
  this.links = data.links ?? []
27
56
  }
28
57
  }
package/src/main/index.ts CHANGED
@@ -12,12 +12,16 @@ export * from './api/contact-data'
12
12
  export * from './api/enhancement-category'
13
13
  export * from './api/link'
14
14
  export * from './api/location-data'
15
+ export * from './api/level-range-data'
15
16
  export * from './api/markdown-string'
16
17
  export * from './api/mission-data'
18
+ export * from './api/mission-flashback-data'
17
19
  export * from './api/mission-type'
18
20
  export * from './api/morality'
21
+ export * from './api/set-title-data'
19
22
  export * from './api/sex'
20
23
  export * from './api/zone-data'
24
+ export * from './api/zone-type'
21
25
 
22
26
  // DB
23
27
  export * from './db/alignment-list'
@@ -32,10 +36,12 @@ export * from './db/coh-content-database'
32
36
  export * from './db/contact'
33
37
  export * from './db/key'
34
38
  export * from './db/location'
39
+ export * from './db/level-range'
35
40
  export * from './db/mission'
36
41
  export * from './db/morality-list'
37
42
  export * from './db/paged'
43
+ export * from './db/set-title-ids'
38
44
  export * from './db/zone'
39
45
 
40
- // ROOT
41
- export * from './util'
46
+ // UTILS
47
+ export * from './util/links'
@@ -0,0 +1,13 @@
1
+ /**
2
+ * For fields that accept either an array of values or a single value, coalesces the value to an array.
3
+ *
4
+ * Arrays are returned as-is.
5
+ * Single values are returned as a single-value array.
6
+ * Undefined values are returned as undefined.
7
+ *
8
+ * @param value The value to coalesce.
9
+ */
10
+ export function coalesceToArray<T>(value?: T | T[]): T[] | undefined {
11
+ if (!value) return undefined
12
+ return Array.isArray(value) ? value as T[] : [value]
13
+ }
@@ -1,11 +1,11 @@
1
- import { BadgeData } from './api/badge-data'
2
- import { Badge } from './db/badge'
3
- import { ZoneData } from './api/zone-data'
4
- import { Zone } from './db/zone'
5
- import { Contact } from './db/contact'
6
- import { ContactData } from './api/contact-data'
7
- import { Mission } from './db/mission'
8
- import { MissionData } from './api/mission-data'
1
+ import { BadgeData } from '../api/badge-data'
2
+ import { Badge } from '../db/badge'
3
+ import { ZoneData } from '../api/zone-data'
4
+ import { Zone } from '../db/zone'
5
+ import { Contact } from '../db/contact'
6
+ import { ContactData } from '../api/contact-data'
7
+ import { Mission } from '../db/mission'
8
+ import { MissionData } from '../api/mission-data'
9
9
 
10
10
  /**
11
11
  * Returns the URI of the given badge that can be used in {@link MarkdownString} fields.
@@ -102,17 +102,3 @@ export function zoneLink(target: string | Zone | ZoneData): string {
102
102
  const key = typeof target === 'string' ? target : target.key
103
103
  return `[${key}](${zoneUri(target)})`
104
104
  }
105
-
106
- /**
107
- * For fields that accept either an array of values or a single value, coalesces the value to an array.
108
- *
109
- * Arrays are returned as-is.
110
- * Single values are returned as a single-value array.
111
- * Undefined values are returned as undefined.
112
- *
113
- * @param value The value to coalesce.
114
- */
115
- export function coalesceToArray<T>(value?: T | T[]): T[] | undefined {
116
- if (!value) return undefined
117
- return Array.isArray(value) ? value as T[] : [value]
118
- }
@@ -51,14 +51,14 @@ describe('compareAlignment', () => {
51
51
 
52
52
  test('should work as a compare function', () => {
53
53
  const unsorted: (Alignment | undefined)[] = [undefined, 'hero', 'villain', 'praetorian', undefined, 'villain', 'praetorian']
54
- const sorted = unsorted.sort(compareAlignment)
54
+ const sorted = unsorted.toSorted(compareAlignment)
55
55
 
56
56
  expect(sorted).toStrictEqual(['hero', 'villain', 'villain', 'praetorian', 'praetorian', undefined, undefined])
57
57
  })
58
58
 
59
59
  test('should sort against undefined', () => {
60
60
  const unsorted: (Alignment | undefined)[] = [undefined, 'hero']
61
- const sorted = unsorted.sort(compareAlignment)
61
+ const sorted = unsorted.toSorted(compareAlignment)
62
62
 
63
63
  expect(sorted).toStrictEqual(['hero', undefined])
64
64
  })
@@ -49,14 +49,14 @@ describe('compareSex', () => {
49
49
 
50
50
  test('should work as a compare function', () => {
51
51
  const unsorted: (Sex | undefined)[] = [undefined, 'M', 'F', 'M', undefined, 'F', 'M']
52
- const sorted = unsorted.sort(compareSex)
52
+ const sorted = unsorted.toSorted(compareSex)
53
53
 
54
54
  expect(sorted).toStrictEqual(['M', 'M', 'M', 'F', 'F', undefined, undefined])
55
55
  })
56
56
 
57
57
  test('should sort against undefined', () => {
58
58
  const unsorted: (Sex | undefined)[] = [undefined, 'M']
59
- const sorted = unsorted.sort(compareSex)
59
+ const sorted = unsorted.toSorted(compareSex)
60
60
 
61
61
  expect(sorted).toStrictEqual(['M', undefined])
62
62
  })
@@ -4,5 +4,6 @@ import { defineFixture } from 'efate'
4
4
  export const zoneDataFixture = defineFixture<ZoneData>((t) => {
5
5
  t.key.as(index => `zone-${index}`)
6
6
  t.name.as(index => `Zone ${index}`)
7
+ t.type.withValue('city')
7
8
  t.links?.as(() => [{ title: 'foo', href: 'https://nouri.org' }])
8
9
  })
@@ -152,12 +152,14 @@ describe(Badge.name, () => {
152
152
  describe('setTitle', () => {
153
153
  test('should be set from the data', () => {
154
154
  const badge = new Badge(badgeDataFixture.create({ setTitleId: [123, 456] }))
155
- expect(badge.setTitleId).toStrictEqual([123, 456])
155
+ expect(badge.setTitleId?.primal).toEqual(123)
156
+ expect(badge.setTitleId?.praetorian).toEqual(456)
156
157
  })
157
158
 
158
159
  test('should treat the praetorian id as optional', () => {
159
160
  const badge = new Badge(badgeDataFixture.create({ setTitleId: [123] }))
160
- expect(badge.setTitleId).toStrictEqual([123])
161
+ expect(badge.setTitleId?.primal).toEqual(123)
162
+ expect(badge.setTitleId?.praetorian).toBeUndefined()
161
163
  })
162
164
 
163
165
  test('should be optional', () => {
@@ -254,6 +256,17 @@ describe(Badge.name, () => {
254
256
  }))
255
257
  expect(badge.zoneKeys).toStrictEqual(['a', 'c'])
256
258
  })
259
+
260
+ test(`should ignore locations with no zone key`, () => {
261
+ const badge = new Badge(badgeDataFixture.create({
262
+ requirements: [
263
+ badgeRequirementDataFixture.create({ location: { zoneKey: 'a' } }),
264
+ badgeRequirementDataFixture.create({ location: { coords: [1, 2, 3] } }),
265
+ badgeRequirementDataFixture.create({ location: { zoneKey: 'c' } }),
266
+ ],
267
+ }))
268
+ expect(badge.zoneKeys).toStrictEqual(['a', 'c'])
269
+ })
257
270
  })
258
271
 
259
272
  describe('zoneKey', () => {
@@ -291,7 +304,7 @@ describe(Badge.name, () => {
291
304
  const badgeA = new Badge(badgeDataFixture.create({ name: 'A' }))
292
305
  const badgeB = new Badge(badgeDataFixture.create({ name: 'B' }))
293
306
  expect(compareByDefaultName(badgeA, badgeB)).toBeLessThan(0)
294
- expect([badgeB, badgeA].sort(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
307
+ expect([badgeB, badgeA].toSorted(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
295
308
  })
296
309
 
297
310
  test(`should return 0 for equal names`, () => {
@@ -309,8 +322,8 @@ describe(Badge.name, () => {
309
322
  test(`should sort undefined values last`, () => {
310
323
  const badgeA = new Badge(badgeDataFixture.create({ name: 'A' }))
311
324
  const badgeB = new Badge(badgeDataFixture.create({ name: [] }))
312
- expect([badgeA, badgeB].sort(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
313
- expect([badgeB, badgeA].sort(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
325
+ expect([badgeA, badgeB].toSorted(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
326
+ expect([badgeB, badgeA].toSorted(compareByDefaultName)).toStrictEqual([badgeA, badgeB])
314
327
  })
315
328
  })
316
329
 
@@ -327,7 +340,7 @@ describe(Badge.name, () => {
327
340
  ],
328
341
  }))
329
342
  expect(compareByZoneKey(badgeA, badgeB)).toBeLessThan(0)
330
- expect([badgeB, badgeA].sort(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
343
+ expect([badgeB, badgeA].toSorted(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
331
344
  })
332
345
 
333
346
  test(`should return 0 for equal zoneKeys`, () => {
@@ -370,8 +383,8 @@ describe(Badge.name, () => {
370
383
  badgeRequirementDataFixture.create({ location: { zoneKey: 'c' } }),
371
384
  ],
372
385
  }))
373
- expect([badgeA, badgeB].sort(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
374
- expect([badgeB, badgeA].sort(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
386
+ expect([badgeA, badgeB].toSorted(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
387
+ expect([badgeB, badgeA].toSorted(compareByZoneKey)).toStrictEqual([badgeA, badgeB])
375
388
  })
376
389
  })
377
390
 
@@ -380,7 +393,7 @@ describe(Badge.name, () => {
380
393
  const badgeA = new Badge(badgeDataFixture.create({ releaseDate: '2024-01-01' }))
381
394
  const badgeB = new Badge(badgeDataFixture.create({ releaseDate: '2025-01-01' }))
382
395
  expect(compareByReleaseDate(badgeA, badgeB)).toBeLessThan(0)
383
- expect([badgeB, badgeA].sort(compareByReleaseDate)).toStrictEqual([badgeA, badgeB])
396
+ expect([badgeB, badgeA].toSorted(compareByReleaseDate)).toStrictEqual([badgeA, badgeB])
384
397
  })
385
398
 
386
399
  test(`should return 0 for equal releaseDates`, () => {
@@ -399,10 +412,10 @@ describe(Badge.name, () => {
399
412
  const badgeA = undefined
400
413
  const badgeB = new Badge(badgeDataFixture.create({ releaseDate: '2025-01-01' }))
401
414
  expect(compareByReleaseDate(badgeA, badgeB)).toBeGreaterThan(0)
402
- expect([badgeA, badgeB].sort(compareByReleaseDate)).toStrictEqual([badgeB, badgeA])
415
+ expect([badgeA, badgeB].toSorted(compareByReleaseDate)).toStrictEqual([badgeB, badgeA])
403
416
 
404
417
  expect(compareByReleaseDate(badgeB, badgeA)).toBeLessThan(0)
405
- expect([badgeB, badgeA].sort(compareByReleaseDate)).toStrictEqual([badgeB, badgeA])
418
+ expect([badgeB, badgeA].toSorted(compareByReleaseDate)).toStrictEqual([badgeB, badgeA])
406
419
  })
407
420
  })
408
421
  })
@@ -62,7 +62,8 @@ describe(Contact.name, () => {
62
62
  describe('levelRange', () => {
63
63
  test(`should be set from the data`, () => {
64
64
  const contact = new Contact(contactDataFixture.create({ levelRange: [1, 2] }))
65
- expect(contact.levelRange).toStrictEqual([1, 2])
65
+ expect(contact?.levelRange?.min).toEqual(1)
66
+ expect(contact?.levelRange?.max).toEqual(2)
66
67
  })
67
68
 
68
69
  test(`should be optional`, () => {
@@ -0,0 +1,47 @@
1
+ import { LevelRange } from '../../main'
2
+
3
+ describe(LevelRange.name, () => {
4
+ describe('Constructor', () => {
5
+ test('should accept a full array (5-10)', () => {
6
+ const range = new LevelRange([5, 10])
7
+ expect(range).toBeDefined()
8
+ expect(range.min).toEqual(5)
9
+ expect(range.max).toEqual(10)
10
+ })
11
+
12
+ test('should accept a partial array (15+)', () => {
13
+ const range = new LevelRange([15])
14
+ expect(range).toBeDefined()
15
+ expect(range.min).toEqual(15)
16
+ expect(range.max).toBeUndefined()
17
+ })
18
+
19
+ test('should not coalesce a explicit max of 50 (20-50)', () => {
20
+ const range = new LevelRange([20, 50])
21
+ expect(range).toBeDefined()
22
+ expect(range.min).toEqual(20)
23
+ expect(range.max).toEqual(50)
24
+ })
25
+
26
+ test('should accept a single-level range (1-1)', () => {
27
+ const range = new LevelRange([1, 1])
28
+ expect(range).toBeDefined()
29
+ expect(range.min).toEqual(1)
30
+ expect(range.max).toEqual(1)
31
+ })
32
+
33
+ test('should accept a zero value (0-0)', () => {
34
+ const range = new LevelRange([0, 0])
35
+ expect(range).toBeDefined()
36
+ expect(range.min).toEqual(0)
37
+ expect(range.max).toEqual(0)
38
+ })
39
+
40
+ test('should accept a number (5+)', () => {
41
+ const range = new LevelRange(5)
42
+ expect(range).toBeDefined()
43
+ expect(range.min).toEqual(5)
44
+ expect(range.max).toBeUndefined()
45
+ })
46
+ })
47
+ })
@@ -56,8 +56,9 @@ describe(Mission.name, () => {
56
56
 
57
57
  describe('levelRange', () => {
58
58
  test(`should be set from the data`, () => {
59
- const mission = new Mission(missionDataFixture.create({ levelRange: [1, 2] }))
60
- expect(mission.levelRange).toStrictEqual([1, 2])
59
+ const mission = new Mission(missionDataFixture.create({ levelRange: 25 }))
60
+ expect(mission?.levelRange?.min).toEqual(25)
61
+ expect(mission?.levelRange?.max).toBeUndefined()
61
62
  })
62
63
 
63
64
  test(`should be optional`, () => {
@@ -105,13 +106,14 @@ describe(Mission.name, () => {
105
106
 
106
107
  describe('levelRange', () => {
107
108
  test(`should be set from the data`, () => {
108
- const mission = new Mission(missionDataFixture.create({ flashback: { levelRange: [1, 2] } }))
109
- expect(mission.flashback?.levelRange).toStrictEqual([1, 2])
109
+ const mission = new Mission(missionDataFixture.create({ flashback: { levelRange: [1, 20] } }))
110
+ expect(mission.flashback?.levelRange?.min).toEqual(1)
111
+ expect(mission.flashback?.levelRange?.max).toEqual(20)
110
112
  })
111
113
 
112
- test(`should default to the mission value`, () => {
114
+ test(`should *not* default to the mission value due to Ouro level grouping`, () => {
113
115
  const mission = new Mission(missionDataFixture.create({ levelRange: [1, 2], flashback: missionFlashbackDataFixture.omit('levelRange').create() }))
114
- expect(mission.flashback?.levelRange).toStrictEqual([1, 2])
116
+ expect(mission.flashback?.levelRange).toBeUndefined()
115
117
  })
116
118
 
117
119
  test(`should be optional`, () => {
@@ -29,7 +29,7 @@ describe(MoralityList.name, () => {
29
29
  expect(new MoralityList().items).toStrictEqual(['hero', 'vigilante', 'villain', 'rogue', 'resistance', 'loyalist'])
30
30
  })
31
31
 
32
- test('should treat empty as no values', () => {
32
+ test('should treat explicit empty as no values', () => {
33
33
  expect(new MoralityList([]).items).toStrictEqual([])
34
34
  })
35
35
  })
@@ -0,0 +1,19 @@
1
+ import { SetTitleIds } from '../../main'
2
+
3
+ describe(SetTitleIds.name, () => {
4
+ describe('Constructor', () => {
5
+ test('should accept both primal and praetorian values', () => {
6
+ const ids = new SetTitleIds([5, 10])
7
+ expect(ids).toBeDefined()
8
+ expect(ids.primal).toEqual(5)
9
+ expect(ids.praetorian).toEqual(10)
10
+ })
11
+
12
+ test('should accept primal only', () => {
13
+ const ids = new SetTitleIds([1])
14
+ expect(ids).toBeDefined()
15
+ expect(ids.primal).toEqual(1)
16
+ expect(ids.praetorian).toBeUndefined()
17
+ })
18
+ })
19
+ })