coh-content-db 2.0.0-rc.9 → 2.0.0

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 (66) hide show
  1. package/.github/workflows/build.yml +3 -1
  2. package/CHANGELOG.md +11 -2
  3. package/README.md +33 -19
  4. package/dist/coh-content-db.d.ts +230 -170
  5. package/dist/coh-content-db.js +495 -296
  6. package/dist/coh-content-db.js.map +1 -1
  7. package/dist/coh-content-db.mjs +488 -294
  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 +13 -7
  12. package/src/main/api/bundle-data.ts +1 -1
  13. package/src/main/api/bundle-header-data.ts +13 -6
  14. package/src/main/api/contact-data.ts +2 -1
  15. package/src/main/api/level-range-data.ts +4 -0
  16. package/src/main/api/mission-data.ts +3 -29
  17. package/src/main/api/mission-flashback-data.ts +31 -0
  18. package/src/main/api/morality.ts +27 -9
  19. package/src/main/api/set-title-data.ts +4 -0
  20. package/src/main/api/variant-context.ts +11 -0
  21. package/src/main/api/{alternate-data.ts → variant-data.ts} +4 -4
  22. package/src/main/api/zone-data.ts +24 -0
  23. package/src/main/api/zone-type.ts +59 -0
  24. package/src/main/db/abstract-index.ts +12 -16
  25. package/src/main/db/badge-index.ts +53 -27
  26. package/src/main/db/badge-requirement.ts +1 -1
  27. package/src/main/db/badge-search-options.ts +15 -14
  28. package/src/main/db/badge.ts +46 -29
  29. package/src/main/db/bundle-header.ts +18 -10
  30. package/src/main/db/coh-content-database.ts +17 -17
  31. package/src/main/db/contact.ts +5 -4
  32. package/src/main/db/level-range.ts +15 -0
  33. package/src/main/db/mission.ts +11 -10
  34. package/src/main/db/paged.ts +7 -3
  35. package/src/main/db/set-title-ids.ts +10 -0
  36. package/src/main/db/variants.ts +84 -0
  37. package/src/main/db/zone.ts +29 -0
  38. package/src/main/index.ts +11 -4
  39. package/src/main/util/coalesce-to-array.ts +13 -0
  40. package/src/main/{util.ts → util/links.ts} +8 -22
  41. package/src/main/util/to-date.ts +9 -0
  42. package/src/test/api/alignment.test.ts +2 -2
  43. package/src/test/api/badge-data.fixture.ts +1 -0
  44. package/src/test/api/badge-data.test.ts +1 -0
  45. package/src/test/api/bundle-data.fixture.ts +3 -2
  46. package/src/test/api/bundle-header-data.fixture.ts +4 -2
  47. package/src/test/api/morality.test.ts +31 -0
  48. package/src/test/api/sex.test.ts +2 -2
  49. package/src/test/api/zone-data.fixture.ts +1 -0
  50. package/src/test/db/abstract-index.test.ts +12 -43
  51. package/src/test/db/badge-index.test.ts +197 -101
  52. package/src/test/db/badge.test.ts +122 -16
  53. package/src/test/db/bundle-header.test.ts +25 -12
  54. package/src/test/db/coh-content-database.test.ts +134 -175
  55. package/src/test/db/contact.test.ts +2 -1
  56. package/src/test/db/level-range.test.ts +47 -0
  57. package/src/test/db/mission.test.ts +8 -6
  58. package/src/test/db/morality-list.test.ts +1 -1
  59. package/src/test/db/set-title-ids.test.ts +19 -0
  60. package/src/test/db/{alternates.test.ts → variants.test.ts} +24 -24
  61. package/src/test/db/zone.test.ts +45 -0
  62. package/src/test/integration.test.ts +3 -3
  63. package/src/test/util/coalese-to-array.test.ts +17 -0
  64. package/src/test/{util.test.ts → util/links.test.ts} +5 -21
  65. package/src/test/util/to-date.test.ts +15 -0
  66. package/src/main/db/alternates.ts +0 -67
@@ -1,9 +1,9 @@
1
- import { Alternates } from '../../main'
1
+ import { Variants } from '../../main'
2
2
 
3
- describe(Alternates.name, () => {
3
+ describe(Variants.name, () => {
4
4
  describe('Constructor', () => {
5
- test('should accept a list of alternate values', () => {
6
- new Alternates([
5
+ test('should accept a list of variant values', () => {
6
+ new Variants([
7
7
  { value: 'Default' },
8
8
  { sex: 'M', value: 'Male' },
9
9
  { alignment: 'hero', value: 'Hero' },
@@ -13,17 +13,17 @@ describe(Alternates.name, () => {
13
13
  })
14
14
 
15
15
  test('should accept a single value', () => {
16
- expect(new Alternates('foo').default?.value).toBe('foo')
16
+ expect(new Variants('foo').default?.value).toBe('foo')
17
17
  })
18
18
  })
19
19
 
20
20
  describe('getValue', () => {
21
21
  test('should return undefined if there are no values', () => {
22
- expect(new Alternates([]).getValue()).toBeUndefined()
22
+ expect(new Variants([]).getValue()).toBeUndefined()
23
23
  })
24
24
 
25
25
  test('should return the least-specific value when no classifiers are provided', () => {
26
- expect(new Alternates([
26
+ expect(new Variants([
27
27
  { value: 'Default' },
28
28
  { sex: 'M', value: 'Male' },
29
29
  { alignment: 'hero', value: 'Hero' },
@@ -33,7 +33,7 @@ describe(Alternates.name, () => {
33
33
  })
34
34
 
35
35
  test('should return the least-specific value when no classifiers are provided, regardless of insert order', () => {
36
- expect(new Alternates([
36
+ expect(new Variants([
37
37
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
38
38
  { alignment: 'hero', value: 'Hero' },
39
39
  { value: 'Default' },
@@ -43,27 +43,27 @@ describe(Alternates.name, () => {
43
43
  })
44
44
 
45
45
  test('should return the most specific match', () => {
46
- expect(new Alternates([
46
+ expect(new Variants([
47
47
  { value: 'Default' },
48
48
  { sex: 'M', value: 'Male' },
49
49
  { alignment: 'hero', value: 'Hero' },
50
50
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
51
51
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
52
- ]).getValue('villain', 'M')).toBe('Male Villain')
52
+ ]).getValue({ morality: 'villain', sex: 'M' })).toBe('Male Villain')
53
53
  })
54
54
 
55
55
  test('should return the most specific match, regardless of insert order', () => {
56
- expect(new Alternates([
56
+ expect(new Variants([
57
57
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
58
58
  { sex: 'M', value: 'Male' },
59
59
  { alignment: 'hero', value: 'Hero' },
60
60
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
61
61
  { value: 'Default' },
62
- ]).getValue('villain', 'M')).toBe('Male Villain')
62
+ ]).getValue({ morality: 'villain', sex: 'M' })).toBe('Male Villain')
63
63
  })
64
64
 
65
65
  test('should return the lowest canonical value if there is no default', () => {
66
- expect(new Alternates([
66
+ expect(new Variants([
67
67
  { alignment: 'hero', value: 'Hero' },
68
68
  { sex: 'M', value: 'Male' },
69
69
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
@@ -72,22 +72,22 @@ describe(Alternates.name, () => {
72
72
  })
73
73
 
74
74
  test('should return the lowest canonical value if a specific is requested that does not exist', () => {
75
- expect(new Alternates([
75
+ expect(new Variants([
76
76
  { alignment: 'hero', value: 'Hero' },
77
77
  { alignment: 'villain', value: 'Villain' },
78
78
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
79
79
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
80
- ]).getValue(undefined, 'F')).toBe('Hero')
80
+ ]).getValue({ sex: 'F' })).toBe('Hero')
81
81
  })
82
82
  })
83
83
 
84
84
  describe('default', () => {
85
85
  test('should return undefined if there are no values', () => {
86
- expect(new Alternates([]).default).toBeUndefined()
86
+ expect(new Variants([]).default).toBeUndefined()
87
87
  })
88
88
 
89
89
  test('should return the lowest priority value', () => {
90
- expect(new Alternates([
90
+ expect(new Variants([
91
91
  { value: 'Default' },
92
92
  { sex: 'M', value: 'Male' },
93
93
  { alignment: 'hero', value: 'Hero' },
@@ -95,7 +95,7 @@ describe(Alternates.name, () => {
95
95
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
96
96
  ]).default?.value).toBe('Default')
97
97
 
98
- expect(new Alternates([
98
+ expect(new Variants([
99
99
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
100
100
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
101
101
  { sex: 'M', value: 'Male' },
@@ -103,7 +103,7 @@ describe(Alternates.name, () => {
103
103
  { alignment: 'hero', value: 'Hero' },
104
104
  ]).default?.value).toBe('Default')
105
105
 
106
- expect(new Alternates([
106
+ expect(new Variants([
107
107
  { alignment: 'villain', sex: 'M', value: 'Male Villain' },
108
108
  { alignment: 'praetorian', sex: 'F', value: 'Praetorian Female' },
109
109
  { sex: 'M', value: 'Male' },
@@ -114,11 +114,11 @@ describe(Alternates.name, () => {
114
114
 
115
115
  describe('canonical', () => {
116
116
  test('should be empty if there are no values', () => {
117
- expect(new Alternates([]).canonical).toHaveLength(0)
117
+ expect(new Variants([]).canonical).toHaveLength(0)
118
118
  })
119
119
 
120
120
  test('should return values sorted in canonical order', () => {
121
- const result = new Alternates([
121
+ const result = new Variants([
122
122
  { alignment: 'hero', sex: 'F', value: 'Female Hero' },
123
123
  { alignment: 'praetorian', value: 'Praetorian' },
124
124
  { sex: 'F', value: 'Female' },
@@ -150,7 +150,7 @@ describe(Alternates.name, () => {
150
150
  })
151
151
 
152
152
  test('should sort unspecified values by alpha', () => {
153
- expect(new Alternates([
153
+ expect(new Variants([
154
154
  { value: 'A' },
155
155
  { value: 'C' },
156
156
  { value: 'B' },
@@ -162,7 +162,7 @@ describe(Alternates.name, () => {
162
162
  })
163
163
 
164
164
  test('should sort identical values by value alpha', () => {
165
- expect(new Alternates([
165
+ expect(new Variants([
166
166
  { alignment: 'villain', value: 'B' },
167
167
  { sex: 'M', value: 'D' },
168
168
  { alignment: 'villain', value: 'A' },
@@ -178,7 +178,7 @@ describe(Alternates.name, () => {
178
178
 
179
179
  describe('toString', () => {
180
180
  test('should create a string separated by the separator', () => {
181
- expect(new Alternates([
181
+ expect(new Variants([
182
182
  { sex: 'M', value: 'A' },
183
183
  { sex: 'F', value: 'B' },
184
184
  { alignment: 'hero', value: 'C' },
@@ -22,6 +22,51 @@ describe(Zone.name, () => {
22
22
  })
23
23
  })
24
24
 
25
+ describe('type', () => {
26
+ test(`should be set from the data`, () => {
27
+ const zone = new Zone(zoneDataFixture.create({ type: 'city' }))
28
+ expect(zone.type).toEqual('city')
29
+ })
30
+ })
31
+
32
+ describe('morality', () => {
33
+ test(`should be set from the data`, () => {
34
+ const zone = new Zone(zoneDataFixture.create({ morality: ['hero'] }))
35
+ expect(zone.morality?.hero).toBeTruthy()
36
+ expect(zone.morality?.vigilante).toBeFalsy()
37
+ })
38
+
39
+ test(`should be optional`, () => {
40
+ const zone = new Zone(zoneDataFixture.omit('morality').create())
41
+ expect(zone.morality?.all).toBeTruthy()
42
+ })
43
+ })
44
+
45
+ describe('levelRange', () => {
46
+ test(`should be set from the data`, () => {
47
+ const zone = new Zone(zoneDataFixture.create({ levelRange: [10] }))
48
+ expect(zone.levelRange?.min).toEqual(10)
49
+ expect(zone.levelRange?.max).toBeUndefined()
50
+ })
51
+
52
+ test(`should be optional`, () => {
53
+ const zone = new Zone(zoneDataFixture.omit('levelRange').create())
54
+ expect(zone.levelRange).toBeUndefined()
55
+ })
56
+ })
57
+
58
+ describe('notes', () => {
59
+ test(`should be set from the data`, () => {
60
+ const zone = new Zone(zoneDataFixture.create({ notes: 'foo' }))
61
+ expect(zone.notes).toEqual('foo')
62
+ })
63
+
64
+ test(`should be optional`, () => {
65
+ const zone = new Zone(zoneDataFixture.omit('notes').create())
66
+ expect(zone.notes).toBeUndefined()
67
+ })
68
+ })
69
+
25
70
  describe('links', () => {
26
71
  test(`should be set from the data`, () => {
27
72
  const zone = new Zone(zoneDataFixture.create({ links: [{ title: 'foo', href: 'bar' }] }))
@@ -4,13 +4,13 @@ import { TEST_BADGE } from './api/badge-data.test'
4
4
  /**
5
5
  * If you change this test, update the example in the README as well
6
6
  */
7
- export const TEST_BUNDLE: BundleData = {
8
- header: { name: 'My Content Bundle' },
7
+ export const MyBundle: BundleData = {
8
+ header: { name: 'My Content Bundle', version: '1.0.0', lastUpdateTime: '2025-04-21T00:00:00Z' },
9
9
  badges: [TEST_BADGE],
10
10
  }
11
11
 
12
12
  describe('BundleData', () => {
13
13
  test('should be a usable interface', () => {
14
- expect(TEST_BUNDLE).not.toBeUndefined()
14
+ expect(MyBundle).not.toBeUndefined()
15
15
  })
16
16
  })
@@ -0,0 +1,17 @@
1
+ import { coalesceToArray } from '../../main/util/coalesce-to-array'
2
+
3
+ describe(coalesceToArray.name, () => {
4
+ test('should return an array unmodified', () => {
5
+ expect(coalesceToArray(['a', 'b'])).toStrictEqual(['a', 'b'])
6
+ expect(coalesceToArray([1, 2])).toStrictEqual([1, 2])
7
+ })
8
+
9
+ test('should return a single value as a single-value array', () => {
10
+ expect(coalesceToArray('a')).toStrictEqual(['a'])
11
+ expect(coalesceToArray(1)).toStrictEqual([1])
12
+ })
13
+
14
+ test('should return undefined value as undefined', () => {
15
+ expect(coalesceToArray()).toBeUndefined()
16
+ })
17
+ })
@@ -1,8 +1,8 @@
1
- import { Badge, badgeLink, badgeUri, coalesceToArray, Contact, contactLink, contactUri, Mission, missionLink, missionUri, Zone, zoneLink, zoneUri } from '../main'
2
- import { badgeDataFixture } from './api/badge-data.fixture'
3
- import { zoneDataFixture } from './api/zone-data.fixture'
4
- import { contactDataFixture } from './api/contact-data.fixture'
5
- import { missionDataFixture } from './api/mission-data.fixture'
1
+ import { Badge, badgeLink, badgeUri, Contact, contactLink, contactUri, Mission, missionLink, missionUri, Zone, zoneLink, zoneUri } from '../../main'
2
+ import { badgeDataFixture } from '../api/badge-data.fixture'
3
+ import { zoneDataFixture } from '../api/zone-data.fixture'
4
+ import { contactDataFixture } from '../api/contact-data.fixture'
5
+ import { missionDataFixture } from '../api/mission-data.fixture'
6
6
 
7
7
  describe(badgeUri.name, () => {
8
8
  test('should return the expected pattern', () => {
@@ -147,19 +147,3 @@ describe(zoneLink.name, () => {
147
147
  expect(zoneLink(zone)).toBe('[foo](zone://foo)')
148
148
  })
149
149
  })
150
-
151
- describe(coalesceToArray.name, () => {
152
- test('should return an array unmodified', () => {
153
- expect(coalesceToArray(['a', 'b'])).toStrictEqual(['a', 'b'])
154
- expect(coalesceToArray([1, 2])).toStrictEqual([1, 2])
155
- })
156
-
157
- test('should return a single value as a single-value array', () => {
158
- expect(coalesceToArray('a')).toStrictEqual(['a'])
159
- expect(coalesceToArray(1)).toStrictEqual([1])
160
- })
161
-
162
- test('should return undefined value as undefined', () => {
163
- expect(coalesceToArray()).toBeUndefined()
164
- })
165
- })
@@ -0,0 +1,15 @@
1
+ import { toDate } from '../../main/util/to-date'
2
+
3
+ describe(toDate.name, () => {
4
+ test('should return a valid date', () => {
5
+ expect(toDate('2025-02-01')).toStrictEqual(new Date('2025-02-01'))
6
+ })
7
+
8
+ test('should return a valid time', () => {
9
+ expect(toDate('2025-04-21T02:57:52.402Z')).toStrictEqual(new Date('2025-04-21T02:57:52.402Z'))
10
+ })
11
+
12
+ test('should throw on invalid ISO string', () => {
13
+ expect(() => toDate('foo')).toThrow('Invalid date')
14
+ })
15
+ })
@@ -1,67 +0,0 @@
1
- import { AlternateData } from '../api/alternate-data'
2
- import { compareSex, Sex } from '../api/sex'
3
- import { Alignment, compareAlignment } from '../api/alignment'
4
-
5
- export class Alternates<T> {
6
- readonly #sortedValues: AlternateData<T>[] = []
7
-
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
- }
19
- }
20
-
21
- getValue(alignment?: Alignment, sex?: Sex): T | undefined {
22
- for (let index = this.#sortedValues.length; index--;) {
23
- const entry = this.#sortedValues[index]
24
- if ((entry.alignment === undefined || entry.alignment === alignment)
25
- && (entry.sex === undefined || entry.sex === sex)
26
- ) return entry.value
27
- }
28
-
29
- return this.default?.value
30
- }
31
-
32
- /**
33
- * Get the default value for this list of alternates, the value with the highest priority and lowest specificity.
34
- */
35
- get default(): AlternateData<T> | undefined {
36
- return this.#sortedValues[0]
37
- }
38
-
39
- /**
40
- * Get the list of alternates sorted in canonical order (alignment then sex, low to high specificity).
41
- */
42
- get canonical(): AlternateData<T>[] {
43
- return this.#sortedValues
44
- }
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
-
54
- #compareAlternates(a: AlternateData<T>, b: AlternateData<T>): number {
55
- const aSpecificity = (a.alignment ? 2 : 0) + (a.sex ? 1 : 0)
56
- const bSpecificity = (b.alignment ? 2 : 0) + (b.sex ? 1 : 0)
57
- if (aSpecificity !== bSpecificity) return aSpecificity - bSpecificity // Order first by least-specific
58
-
59
- const alignmentComparison = compareAlignment(a.alignment, b.alignment) // Next by alignment
60
- if (alignmentComparison !== 0) return alignmentComparison
61
-
62
- const sexComparison = compareSex(a.sex, b.sex) // Last by sex
63
- if (sexComparison !== 0) return sexComparison
64
-
65
- return String(a.value).localeCompare(String(b.value))
66
- }
67
- }