coh-content-db 2.0.0-rc.8 → 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 (74) hide show
  1. package/.github/workflows/build.yml +3 -1
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +48 -17
  4. package/dist/coh-content-db.d.ts +257 -200
  5. package/dist/coh-content-db.js +509 -339
  6. package/dist/coh-content-db.js.map +1 -1
  7. package/dist/coh-content-db.mjs +501 -335
  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/{content-bundle.ts → bundle-data.ts} +5 -27
  13. package/src/main/api/bundle-header-data.ts +44 -0
  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 +52 -0
  30. package/src/main/db/coh-content-database.ts +22 -22
  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 +14 -8
  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 +7 -0
  46. package/src/test/api/bundle-header-data.fixture.ts +8 -0
  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 +89 -0
  54. package/src/test/db/coh-content-database.test.ts +137 -178
  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 +16 -0
  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/api/change.ts +0 -17
  67. package/src/main/changelog.ts +0 -29
  68. package/src/main/db/alternates.ts +0 -67
  69. package/src/main/db/bundle-metadata.ts +0 -45
  70. package/src/test/api/content-bundle.fixture.ts +0 -6
  71. package/src/test/api/content-bundle.test.ts +0 -14
  72. package/src/test/changelog.test.ts +0 -36
  73. package/src/test/db/bundle-metadata.test.ts +0 -84
  74. package/src/test/index.test.ts +0 -14
@@ -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,45 +1,45 @@
1
- import { ContentBundle } from '../api/content-bundle'
1
+ import { BundleData } from '../api/bundle-data'
2
2
  import { Archetype } from './archetype'
3
3
  import { Zone } from './zone'
4
4
  import { Badge } from './badge'
5
- import { BundleMetadata } from './bundle-metadata'
6
- import { BadgeIndex } from './badge-index'
5
+ import { BundleHeader } from './bundle-header'
7
6
  import { BadgeSearchOptions } from './badge-search-options'
8
7
  import { Paged } from './paged'
9
8
  import { Contact } from './contact'
10
9
  import { Mission } from './mission'
11
10
  import { AbstractIndex } from './abstract-index'
11
+ import { BadgeIndex } from './badge-index'
12
12
 
13
13
  export class CohContentDatabase {
14
- #archetypeIndex = new AbstractIndex<Archetype>('key')
15
- #zoneIndex = new AbstractIndex<Zone>('key')
16
- #contactIndex = new AbstractIndex<Contact>('key')
17
- #missionIndex = new AbstractIndex<Mission>('key')
18
- #badgeIndex = new BadgeIndex()
14
+ readonly #archetypeIndex
15
+ readonly #zoneIndex
16
+ readonly #contactIndex
17
+ readonly #missionIndex
18
+ readonly #badgeIndex
19
19
 
20
- #metadata?: BundleMetadata
21
- #servers?: string[]
20
+ readonly #header: BundleHeader
21
+ readonly #servers: string[]
22
22
 
23
23
  /**
24
- * Load the given content bundle, resetting the db if a bundle is already loaded.
24
+ * Create a db instance from the given content bundle.
25
25
  * @param bundle The bundle to load.
26
26
  */
27
- load(bundle: ContentBundle): void {
28
- this.#metadata = new BundleMetadata(bundle)
27
+ constructor(bundle: BundleData) {
28
+ this.#header = new BundleHeader(bundle.header)
29
29
  this.#servers = bundle.servers ?? []
30
30
 
31
- this.#archetypeIndex.load(bundle.archetypes?.map(x => new Archetype(x)))
32
- this.#zoneIndex.load(bundle.zones?.map(x => new Zone(x)))
33
- this.#contactIndex.load(bundle.contacts?.map(x => new Contact(x)))
34
- this.#missionIndex.load(bundle.missions?.map(x => new Mission(x)))
35
- this.#badgeIndex.load(bundle.badges?.map(x => new Badge(x)))
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
36
  }
37
37
 
38
38
  /**
39
- * Metadata about the content bundle.
39
+ * Header information about the content bundle.
40
40
  */
41
- get metadata(): BundleMetadata | undefined {
42
- return this.#metadata
41
+ get header(): BundleHeader {
42
+ return this.#header
43
43
  }
44
44
 
45
45
  /**
@@ -48,7 +48,7 @@ export class CohContentDatabase {
48
48
  * Torchbearer, Excelsior, etc.
49
49
  */
50
50
  get servers(): string[] {
51
- return this.#servers ?? []
51
+ return this.#servers
52
52
  }
53
53
 
54
54
  /**
@@ -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,19 +63,19 @@ 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
- readonly name?: string
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
- readonly morality?: MoralityList
78
+ readonly morality: MoralityList
78
79
 
79
80
  /**
80
81
  * Freeform notes or tips about the Flashback version of the mission.
@@ -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,
@@ -1,7 +1,11 @@
1
1
  export interface Paged<T> {
2
2
  items: T[]
3
- page: number
3
+
4
+ matchedItemCount: number
5
+ totalItemCount: number
6
+
4
7
  pageSize?: number
5
- totalItems: number
6
- totalPages: number
8
+ pageIndex: number
9
+ pageNumber: number
10
+ totalPageCount: number
7
11
  }
@@ -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
+ }
@@ -0,0 +1,84 @@
1
+ import { VariantData } from '../api/variant-data'
2
+ import { compareSex } from '../api/sex'
3
+ import { compareAlignment } from '../api/alignment'
4
+ import { VariantContext } from '../api/variant-context'
5
+ import { MoralityMap } from '../api/morality'
6
+
7
+ export class Variants<T> {
8
+ readonly #sortedValues: VariantData<T>[] = []
9
+
10
+ /**
11
+ * Create a variant set from either a list of categorized values, or a single value when there are no variants.
12
+ * @param value List of variants, or a single value.
13
+ */
14
+ constructor(value: VariantData<T>[] | T) {
15
+ if (Array.isArray(value)) {
16
+ this.#sortedValues = value.toSorted()
17
+ this.#sortedValues.sort((a, b) => this.#compareVariants(a, b))
18
+ } else {
19
+ this.#sortedValues = [{ value }]
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Get a variant by context
25
+ * @param context The context
26
+ */
27
+ getVariant(context?: VariantContext): VariantData<T> | undefined {
28
+ const alignment = context?.morality ? MoralityMap[context.morality] : undefined
29
+ const sex = context?.sex
30
+
31
+ for (let index = this.#sortedValues.length; index--;) {
32
+ const entry = this.#sortedValues[index]
33
+ if ((entry.alignment === undefined || entry.alignment === alignment)
34
+ && (entry.sex === undefined || entry.sex === sex)
35
+ ) return entry
36
+ }
37
+
38
+ return this.default
39
+ }
40
+
41
+ /**
42
+ * Get a value by variant context
43
+ * @param context The context
44
+ */
45
+ getValue(context?: VariantContext): T | undefined {
46
+ return this.getVariant(context)?.value
47
+ }
48
+
49
+ /**
50
+ * Get the default value for this list of variants, the value with the highest priority and lowest specificity.
51
+ */
52
+ get default(): VariantData<T> | undefined {
53
+ return this.#sortedValues[0]
54
+ }
55
+
56
+ /**
57
+ * Get the list of variants sorted in canonical order (alignment then sex, low to high specificity).
58
+ */
59
+ get canonical(): VariantData<T>[] {
60
+ return this.#sortedValues
61
+ }
62
+
63
+ /**
64
+ * Create a joined string from the variant values in canonical order.
65
+ * @param separator Separator to use. Default is ' / '
66
+ */
67
+ toString(separator: string): string {
68
+ return this.canonical.map(x => x.value).join(separator)
69
+ }
70
+
71
+ #compareVariants(a: VariantData<T>, b: VariantData<T>): number {
72
+ const aSpecificity = (a.alignment ? 2 : 0) + (a.sex ? 1 : 0)
73
+ const bSpecificity = (b.alignment ? 2 : 0) + (b.sex ? 1 : 0)
74
+ if (aSpecificity !== bSpecificity) return aSpecificity - bSpecificity // Order first by least-specific
75
+
76
+ const alignmentComparison = compareAlignment(a.alignment, b.alignment) // Next by alignment
77
+ if (alignmentComparison !== 0) return alignmentComparison
78
+
79
+ const sexComparison = compareSex(a.sex, b.sex) // Last by sex
80
+ if (sexComparison !== 0) return sexComparison
81
+
82
+ return String(a.value).localeCompare(String(b.value))
83
+ }
84
+ }
@@ -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
@@ -1,42 +1,48 @@
1
1
  // API
2
2
  export * from './api/alignment'
3
- export * from './api/alternate-data'
4
3
  export * from './api/archetype-data'
5
4
  export * from './api/badge-data'
6
5
  export * from './api/badge-requirement-data'
7
6
  export * from './api/badge-requirement-type'
8
7
  export * from './api/badge-type'
9
- export * from './api/change'
8
+ export * from './api/bundle-data'
9
+ export * from './api/bundle-header-data'
10
10
  export * from './api/contact-data'
11
- export * from './api/content-bundle'
12
11
  export * from './api/enhancement-category'
13
12
  export * from './api/link'
14
13
  export * from './api/location-data'
14
+ export * from './api/level-range-data'
15
15
  export * from './api/markdown-string'
16
16
  export * from './api/mission-data'
17
+ export * from './api/mission-flashback-data'
17
18
  export * from './api/mission-type'
18
19
  export * from './api/morality'
20
+ export * from './api/set-title-data'
19
21
  export * from './api/sex'
22
+ export * from './api/variant-context'
23
+ export * from './api/variant-data'
20
24
  export * from './api/zone-data'
25
+ export * from './api/zone-type'
21
26
 
22
27
  // DB
23
28
  export * from './db/alignment-list'
24
- export * from './db/alternates'
25
29
  export * from './db/archetype'
26
30
  export * from './db/badge'
27
31
  export * from './db/badge-index'
28
32
  export * from './db/badge-requirement'
29
33
  export * from './db/badge-search-options'
30
- export * from './db/bundle-metadata'
34
+ export * from './db/bundle-header'
31
35
  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'
44
+ export * from './db/variants'
38
45
  export * from './db/zone'
39
46
 
40
- // ROOT
41
- export { CHANGELOG } from './changelog'
42
- export * from './util'
47
+ // UTILS
48
+ 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
- }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Converts an iso string to a Date object, throwing an error if the iso string is invalid.
3
+ * @param iso ISO-8601 Date string
4
+ */
5
+ export function toDate(iso: string): Date {
6
+ const date = new Date(iso)
7
+ if (!date || Number.isNaN(date.getTime())) throw new Error(`Invalid date format: [${iso}]`)
8
+ return date
9
+ }
@@ -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
  })
@@ -5,4 +5,5 @@ export const badgeDataFixture = defineFixture<BadgeData>((t) => {
5
5
  t.key.as(index => `badge-${index}`)
6
6
  t.type.pickFrom([...BADGE_TYPE])
7
7
  t.name.as(index => [{ value: `Badge ${index}` }])
8
+ t.releaseDate.as(() => '2025-02-03')
8
9
  })
@@ -5,6 +5,7 @@ export const TEST_BADGE: BadgeData = {
5
5
  key: 'test-badge',
6
6
  type: 'achievement',
7
7
  name: [{ value: 'Test Badge' }, { alignment: 'praetorian', value: 'My Badge for Praetorians' }],
8
+ releaseDate: '2020-03-01',
8
9
  morality: ['hero', 'praetorian'],
9
10
  }
10
11
 
@@ -0,0 +1,7 @@
1
+ import { defineFixture } from 'efate'
2
+ import { BundleData } from '../../main'
3
+ import { bundleHeaderDataFixture } from './bundle-header-data.fixture'
4
+
5
+ export const bundleDataFixture = defineFixture<BundleData>((t) => {
6
+ t.header.fromFixture(bundleHeaderDataFixture)
7
+ })
@@ -0,0 +1,8 @@
1
+ import { defineFixture } from 'efate'
2
+ import { BundleHeaderData } from '../../main'
3
+
4
+ export const bundleHeaderDataFixture = defineFixture<BundleHeaderData>((t) => {
5
+ t.name.as(index => `Bundle ${index}`)
6
+ t.version.as(index => `${index}.${index}.${index}`)
7
+ t.lastUpdateTime.as(() => new Date().toISOString())
8
+ })
@@ -0,0 +1,31 @@
1
+ import { MoralityMap } from '../../main'
2
+
3
+ describe('MoralityMap', () => {
4
+ test('should map hero to hero', () => {
5
+ expect(MoralityMap.hero).toEqual('hero')
6
+ })
7
+
8
+ test('should map vigilante to hero', () => {
9
+ expect(MoralityMap.vigilante).toEqual('hero')
10
+ })
11
+
12
+ test('should map villain to villain', () => {
13
+ expect(MoralityMap.villain).toEqual('villain')
14
+ })
15
+
16
+ test('should map rogue to villain', () => {
17
+ expect(MoralityMap.rogue).toEqual('villain')
18
+ })
19
+
20
+ test('should map resistance to praetorian', () => {
21
+ expect(MoralityMap.resistance).toEqual('praetorian')
22
+ })
23
+
24
+ test('should map loyalist to praetorian', () => {
25
+ expect(MoralityMap.loyalist).toEqual('praetorian')
26
+ })
27
+
28
+ test('should map praetorian to praetorian', () => {
29
+ expect(MoralityMap.loyalist).toEqual('praetorian')
30
+ })
31
+ })
@@ -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
  })