coh-content-db 2.0.0-rc.6 → 2.0.0-rc.7

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 (65) hide show
  1. package/README.md +3 -3
  2. package/dist/coh-content-db.d.ts +384 -181
  3. package/dist/coh-content-db.js +604 -348
  4. package/dist/coh-content-db.js.map +1 -1
  5. package/dist/coh-content-db.mjs +592 -347
  6. package/dist/coh-content-db.mjs.map +1 -1
  7. package/eslint.config.mjs +1 -0
  8. package/package.json +1 -1
  9. package/src/main/api/alignment.ts +18 -2
  10. package/src/main/api/badge-data.ts +12 -42
  11. package/src/main/api/badge-requirement-data.ts +17 -35
  12. package/src/main/api/badge-requirement-type.ts +28 -7
  13. package/src/main/api/badge-type.ts +15 -15
  14. package/src/main/api/contact-data.ts +7 -5
  15. package/src/main/api/content-bundle.ts +6 -0
  16. package/src/main/api/enhancement-category.ts +26 -26
  17. package/src/main/api/location-data.ts +28 -0
  18. package/src/main/api/mission-data.ts +83 -0
  19. package/src/main/api/mission-type.ts +2 -0
  20. package/src/main/api/morality.ts +31 -0
  21. package/src/main/api/sex.ts +8 -1
  22. package/src/main/api/zone-data.ts +1 -1
  23. package/src/main/changelog.ts +5 -4
  24. package/src/main/db/alignment-list.ts +54 -0
  25. package/src/main/db/alternates.ts +15 -32
  26. package/src/main/db/badge-index.ts +11 -36
  27. package/src/main/db/badge-requirement.ts +22 -43
  28. package/src/main/db/badge-search-options.ts +4 -4
  29. package/src/main/db/badge.ts +53 -54
  30. package/src/main/db/coh-content-database.ts +28 -24
  31. package/src/main/db/contact.ts +17 -14
  32. package/src/main/db/location.ts +30 -0
  33. package/src/main/db/mission.ts +107 -0
  34. package/src/main/db/morality-list.ts +99 -0
  35. package/src/main/db/zone.ts +1 -1
  36. package/src/main/index.ts +8 -3
  37. package/src/main/util.ts +43 -3
  38. package/src/test/api/alignment.test.ts +38 -4
  39. package/src/test/api/badge-data.fixture.ts +1 -17
  40. package/src/test/api/badge-data.test.ts +3 -3
  41. package/src/test/api/badge-requirement-data.fixture.ts +1 -11
  42. package/src/test/api/badge-requirement-type.test.ts +3 -3
  43. package/src/test/api/badge-type.test.ts +5 -5
  44. package/src/test/api/contact-data.fixture.ts +0 -6
  45. package/src/test/api/content-bundle.fixture.ts +1 -17
  46. package/src/test/api/enhancement-category.test.ts +5 -5
  47. package/src/test/api/mission-data.fixture.ts +12 -0
  48. package/src/test/api/sex.test.ts +33 -1
  49. package/src/test/api/zone-data.fixture.ts +1 -1
  50. package/src/test/db/alignment-list.test.ts +200 -0
  51. package/src/test/db/alternates.test.ts +60 -56
  52. package/src/test/db/badge-index.test.ts +82 -72
  53. package/src/test/db/badge-requirement.test.ts +35 -70
  54. package/src/test/db/badge.test.ts +185 -64
  55. package/src/test/db/coh-content-database.test.ts +58 -69
  56. package/src/test/db/contact.test.ts +25 -24
  57. package/src/test/db/location.test.ts +51 -0
  58. package/src/test/db/mission.test.ts +171 -0
  59. package/src/test/db/morality-list.test.ts +457 -0
  60. package/src/test/db/zone.test.ts +4 -4
  61. package/src/test/util.test.ts +54 -1
  62. package/src/main/api/plaque-type.ts +0 -6
  63. package/src/main/db/alignments.ts +0 -17
  64. package/src/test/api/alignments.test.ts +0 -40
  65. package/src/test/api/plaque-type.test.ts +0 -31
@@ -4,11 +4,13 @@ import { BadgeData } from '../api/badge-data'
4
4
  import { BadgeRequirement } from './badge-requirement'
5
5
  import { Key } from './key'
6
6
  import { Alternates } from './alternates'
7
- import { Alignments } from './alignments'
8
7
  import { MarkdownString } from '../api/markdown-string'
8
+ import { coalesceToArray } from '../util'
9
+ import { MoralityList } from './morality-list'
9
10
 
10
11
  export class Badge {
11
12
  readonly #requirementsIndex: Record<string, BadgeRequirement> = {}
13
+ readonly #zoneKeys = new Set<string>()
12
14
 
13
15
  /**
14
16
  * The database key for this badge.
@@ -28,14 +30,14 @@ export class Badge {
28
30
  readonly name: Alternates<string>
29
31
 
30
32
  /**
31
- * The character alignments that this badge is available to.
33
+ * The character moralities that this badge is available to.
32
34
  */
33
- readonly alignment: Alignments
35
+ readonly morality: MoralityList
34
36
 
35
37
  /**
36
38
  * The badge text as it appears in-game. May vary by character sex or alignment.
37
39
  */
38
- readonly badgeText: Alternates<string>
40
+ readonly badgeText: Alternates<MarkdownString>
39
41
 
40
42
  /**
41
43
  * Short description of how to acquire the badge. Detailed instructions will be in the notes field.
@@ -60,33 +62,10 @@ export class Badge {
60
62
  readonly links: Link[]
61
63
 
62
64
  /**
63
- * For exploration badges, the key of the {@link Zone} that this badge is found on.
65
+ * The id used with the in-game `/settitle` command to apply the badge.
66
+ * The first value is the id for primal characters and the (optional) second number is the id for praetorian characters.
64
67
  */
65
- readonly zoneKey?: string
66
-
67
- /**
68
- * For exploration badges, the `/loc` coordinates of the badge.
69
- */
70
- readonly loc?: [number, number, number]
71
-
72
- /**
73
- * For plaques that appear on a Vidiot Map, the number or letter the badge appears as.
74
- */
75
- readonly vidiotMapKey?: string
76
-
77
- /**
78
- * ID used with the in-game `/settitle` command to apply the badge.
79
- */
80
- readonly setTitle?: {
81
- /**
82
- * `/settitle` id.
83
- */
84
- id?: number
85
- /**
86
- * `/settitle` id if different for praetorian characters.
87
- */
88
- praetorianId?: number
89
- }
68
+ readonly setTitleId?: [number, number?]
90
69
 
91
70
  /**
92
71
  * A description of the effect the badge will have, such as a buff or granting a temporary power.
@@ -94,16 +73,9 @@ export class Badge {
94
73
  readonly effect?: MarkdownString
95
74
 
96
75
  /**
97
- * Represents the layered requirements for badges with multiple fulfillment steps,
98
- * such as visiting plaques for history badges or collecting other badges.
99
- *
100
- * The outer array represents groups of requirements evaluated with OR logic —
101
- * fulfilling any group satisfies the badge.
102
- *
103
- * Each inner array represents individual requirements evaluated with AND logic —
104
- * all conditions in the group must be met.
76
+ * Represents the requirements for badges with multiple fulfillment steps, such as visiting monuments for history badges, completing missions, or collecting other badges.
105
77
  */
106
- readonly requirements?: BadgeRequirement[][]
78
+ readonly requirements?: BadgeRequirement[]
107
79
 
108
80
  /**
109
81
  * Some badges are not included in the badge total count... such as Flames of Prometheus, which can be removed by redeeming it for a Notice of the Well.
@@ -114,29 +86,24 @@ export class Badge {
114
86
  this.key = new Key(badgeData.key).value
115
87
  this.type = badgeData.type
116
88
  this.name = new Alternates(badgeData.name)
117
- this.alignment = new Alignments(badgeData.alignment)
89
+ this.morality = new MoralityList(coalesceToArray(badgeData.morality))
118
90
  this.badgeText = new Alternates(badgeData.badgeText ?? [])
119
91
  this.acquisition = badgeData.acquisition
120
92
  this.icon = new Alternates(badgeData.icon ?? [])
121
93
  this.notes = badgeData.notes
122
94
  this.links = badgeData.links ?? []
123
- this.zoneKey = badgeData.zoneKey
124
- this.loc = badgeData.loc
125
95
  this.effect = badgeData.effect
126
- this.vidiotMapKey = badgeData.vidiotMapKey
127
- this.setTitle = badgeData.setTitle
96
+ this.setTitleId = badgeData.setTitleId
128
97
  this.ignoreInTotals = badgeData.ignoreInTotals ?? false
129
98
 
130
- this.requirements = badgeData.requirements?.map((groups, index) => {
131
- const existingKeysInGroup = new Set<string>()
132
- return groups.map((requirementData) => {
133
- if (existingKeysInGroup.has(requirementData.key)) throw new Error(`Duplicate badge requirement key [${badgeData.key}:${requirementData.key}] in group [${index + 1}]`)
134
- existingKeysInGroup.add(requirementData.key)
135
-
136
- const badge = new BadgeRequirement(requirementData)
137
- this.#requirementsIndex[badge.key] = badge
138
- return badge
139
- })
99
+ this.requirements = badgeData.requirements?.map((requirementData) => {
100
+ if (this.#requirementsIndex[requirementData.key]) throw new Error(`Duplicate badge requirement key [${badgeData.key}:${requirementData.key}]`)
101
+ const requirement = new BadgeRequirement(requirementData)
102
+ this.#requirementsIndex[requirement.key] = requirement
103
+ if (requirement.location) for (const location of requirement.location) {
104
+ if (location.zoneKey) this.#zoneKeys.add(location.zoneKey)
105
+ }
106
+ return requirement
140
107
  })
141
108
  }
142
109
 
@@ -145,4 +112,36 @@ export class Badge {
145
112
  if (result === undefined) throw new Error(`Unknown badge requirement key [${key}]`)
146
113
  return result
147
114
  }
115
+
116
+ /**
117
+ * Return a list of all the zone keys referenced by this badge.
118
+ */
119
+ get zoneKeys(): string[] {
120
+ return [...this.#zoneKeys]
121
+ }
122
+
123
+ /**
124
+ * The zone key if this badge relates to a single zone.
125
+ */
126
+ get zoneKey(): string | undefined {
127
+ return this.#zoneKeys.size === 1 ? this.#zoneKeys.values().next().value : undefined
128
+ }
129
+ }
130
+
131
+ export function compareByDefaultName(a?: Badge, b?: Badge): number {
132
+ const aName = a?.name.default?.value
133
+ const bName = b?.name.default?.value
134
+ if (!aName && !bName) return 0
135
+ if (!aName) return 1
136
+ if (!bName) return -1
137
+ return aName.localeCompare(bName)
138
+ }
139
+
140
+ export function compareByZoneKey(a?: Badge, b?: Badge): number {
141
+ const aZone = a?.zoneKey
142
+ const bZone = b?.zoneKey
143
+ if (!aZone && !bZone) return 0
144
+ if (!aZone) return 1
145
+ if (!bZone) return -1
146
+ return aZone.localeCompare(bZone)
148
147
  }
@@ -7,11 +7,13 @@ import { BadgeIndex } from './badge-index'
7
7
  import { BadgeSearchOptions } from './badge-search-options'
8
8
  import { Paged } from './paged'
9
9
  import { Contact } from './contact'
10
+ import { Mission } from './mission'
10
11
 
11
12
  export class CohContentDatabase {
12
13
  readonly #archetypeIndex: Record<string, Archetype> = {}
13
14
  readonly #zoneIndex: Record<string, Zone> = {}
14
15
  readonly #contactIndex: Record<string, Contact> = {}
16
+ readonly #missionIndex: Record<string, Mission> = {}
15
17
  readonly #badgeIndex: BadgeIndex
16
18
 
17
19
  /**
@@ -41,6 +43,11 @@ export class CohContentDatabase {
41
43
  */
42
44
  readonly contacts: Contact[]
43
45
 
46
+ /**
47
+ * List of missions.
48
+ */
49
+ readonly missions: Contact[]
50
+
44
51
  /**
45
52
  * List of badges.
46
53
  */
@@ -75,44 +82,41 @@ export class CohContentDatabase {
75
82
  return contact
76
83
  }) ?? []
77
84
 
78
- this.badges = bundle.badges?.map(data => new Badge(data)) ?? []
79
- this.#badgeIndex = new BadgeIndex(this.badges, this.zones)
80
- }
85
+ this.missions = bundle.missions?.map((data) => {
86
+ if (this.#missionIndex[data.key] !== undefined) throw new Error(`Duplicate mission key '${data.key}'`)
87
+ const mission = new Mission(data)
88
+ this.#missionIndex[mission.key] = mission
89
+ return mission
90
+ }) ?? []
81
91
 
82
- getArchetype(key: string): Archetype {
83
- const result = this.#archetypeIndex[key]
84
- if (result === undefined) throw new Error(`Unknown archetype key '${key}'`)
85
- return result
92
+ this.badges = bundle.badges?.map(data => new Badge(data)) ?? []
93
+ this.#badgeIndex = new BadgeIndex(this.badges)
86
94
  }
87
95
 
88
- getZone(key: string): Zone {
89
- const result = this.#zoneIndex[key]
90
- if (result === undefined) throw new Error(`Unknown zone key '${key}'`)
91
- return result
96
+ getArchetype(key?: string): Archetype | undefined {
97
+ if (!key) return undefined
98
+ return this.#archetypeIndex[key]
92
99
  }
93
100
 
94
- getContact(key: string): Contact {
95
- const result = this.#contactIndex[key]
96
- if (result === undefined) throw new Error(`Unknown contact key '${key}'`)
97
- return result
101
+ getZone(key?: string): Zone | undefined {
102
+ if (!key) return undefined
103
+ return this.#zoneIndex[key]
98
104
  }
99
105
 
100
- zoneExists(key: string): boolean {
101
- return !!this.#zoneIndex[key]
106
+ getContact(key?: string): Contact | undefined {
107
+ if (!key) return undefined
108
+ return this.#contactIndex[key]
102
109
  }
103
110
 
104
- contactExists(key: string): boolean {
105
- return !!this.#contactIndex[key]
111
+ getMission(key?: string): Mission | undefined {
112
+ if (!key) return undefined
113
+ return this.#missionIndex[key]
106
114
  }
107
115
 
108
- getBadge(key: string): Badge {
116
+ getBadge(key?: string): Badge | undefined {
109
117
  return this.#badgeIndex.getBadge(key)
110
118
  }
111
119
 
112
- badgeExists(key: string): boolean {
113
- return this.#badgeIndex.badgeExists(key)
114
- }
115
-
116
120
  /**
117
121
  * Search, sort and filter the badge list.
118
122
  * This is a fairly brute-forced approach and will not be as performant as loading the badge data into a traditional
@@ -2,12 +2,15 @@ import { Link } from '../api/link'
2
2
  import { Key } from './key'
3
3
  import { MarkdownString } from '../api/markdown-string'
4
4
  import { ContactData } from '../api/contact-data'
5
+ import { Location } from './location'
6
+ import { MoralityList } from './morality-list'
7
+ import { coalesceToArray } from '../util'
5
8
 
6
9
  export class Contact {
7
10
  /**
8
11
  * Unique key used to reference this contact.
9
12
  *
10
- * Keys can only contain lowercase letters, numbers and hyphens (`-`).
13
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
11
14
  */
12
15
  readonly key: string
13
16
 
@@ -22,14 +25,14 @@ export class Contact {
22
25
  readonly title?: string
23
26
 
24
27
  /**
25
- * The zone this character is located in.
28
+ * The character moralities that this contact will interact with.
26
29
  */
27
- readonly zoneKey?: string
30
+ readonly morality?: MoralityList
28
31
 
29
32
  /**
30
- * The `/loc` coordinates of the contact.
33
+ * The location of this contact.
31
34
  */
32
- readonly loc?: [number, number, number]
35
+ readonly location?: Location
33
36
 
34
37
  /**
35
38
  * The level range this contact will offer missions for.
@@ -46,14 +49,14 @@ export class Contact {
46
49
  */
47
50
  readonly links: Link[]
48
51
 
49
- constructor(contactData: ContactData) {
50
- this.key = new Key(contactData.key).value
51
- this.name = contactData.name
52
- this.title = contactData.title
53
- this.zoneKey = contactData.zoneKey
54
- this.loc = contactData.loc
55
- this.levelRange = contactData.levelRange
56
- this.notes = contactData.notes
57
- this.links = contactData.links ?? []
52
+ constructor(data: ContactData) {
53
+ this.key = new Key(data.key).value
54
+ this.name = data.name
55
+ this.title = data.title
56
+ this.morality = new MoralityList(coalesceToArray(data.morality))
57
+ this.location = data.location
58
+ this.levelRange = data.levelRange
59
+ this.notes = data.notes
60
+ this.links = data.links ?? []
58
61
  }
59
62
  }
@@ -0,0 +1,30 @@
1
+ import { Coords, LocationData, LocationIcon } from '../api/location-data'
2
+
3
+ export class Location {
4
+ /**
5
+ * Key of the {@link Zone} that the location references.
6
+ */
7
+ readonly zoneKey?: string
8
+
9
+ /**
10
+ * In-game `/loc` coordinates of the location.
11
+ */
12
+ readonly coords?: Coords
13
+
14
+ /**
15
+ * The type of icon to use if the location appears on a map. (Typically the Vidiot map icon).
16
+ */
17
+ readonly icon?: LocationIcon
18
+
19
+ /**
20
+ * The text that should appear in the location icon. (Typically a number or symbol from the Vidiot map).
21
+ */
22
+ readonly iconText?: string
23
+
24
+ constructor(data: LocationData) {
25
+ this.zoneKey = data.zoneKey
26
+ this.coords = data.coords
27
+ this.icon = data.icon
28
+ this.iconText = data.iconText
29
+ }
30
+ }
@@ -0,0 +1,107 @@
1
+ import { MissionType } from '../api/mission-type'
2
+ import { MarkdownString } from '../api/markdown-string'
3
+ import { Link } from '../api/link'
4
+ import { MissionData } from '../api/mission-data'
5
+ import { Key } from './key'
6
+ import { coalesceToArray } from '../util'
7
+ import { MoralityList } from './morality-list'
8
+
9
+ export class Mission {
10
+ /**
11
+ * Unique key used to reference this mission.
12
+ *
13
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
14
+ */
15
+ readonly key: string
16
+
17
+ /**
18
+ * The name of the mission as it appears from the contact.
19
+ *
20
+ * The name may be different when viewed in Ouroboros as a Flashback.
21
+ */
22
+ readonly name: string
23
+
24
+ /**
25
+ * The type of mission... Story arc, task force, trial, etc.
26
+ */
27
+ readonly type: MissionType
28
+
29
+ /**
30
+ * The character moralities that may accept the mission.
31
+ */
32
+ readonly morality: MoralityList
33
+
34
+ /**
35
+ * The keys of any contacts that provide this mission.
36
+ */
37
+ readonly contactKeys?: string[]
38
+
39
+ /**
40
+ * The level range this mission is available for.
41
+ */
42
+ readonly levelRange?: [number, number?]
43
+
44
+ /**
45
+ * Freeform notes or tips about the mission.
46
+ */
47
+ readonly notes?: MarkdownString
48
+
49
+ /**
50
+ * List of external links. Wiki, forums, etc.
51
+ */
52
+ readonly links: Link[]
53
+
54
+ /**
55
+ * If the mission is available in Ouroboros as a Flashback.
56
+ */
57
+ readonly flashback?: {
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?: MoralityList
78
+
79
+ /**
80
+ * Freeform notes or tips about the Flashback version of the mission.
81
+ */
82
+ readonly notes?: MarkdownString
83
+ }
84
+
85
+ constructor(data: MissionData) {
86
+ this.key = new Key(data.key).value
87
+ this.name = data.name
88
+ this.type = data.type
89
+ this.morality = new MoralityList(coalesceToArray(data.morality))
90
+ this.contactKeys = coalesceToArray(data.contactKeys)
91
+ this.levelRange = data.levelRange
92
+ this.notes = data.notes
93
+ this.links = data.links ?? []
94
+ this.flashback = createFlashback(data)
95
+ }
96
+ }
97
+
98
+ function createFlashback(data: MissionData): Mission['flashback'] {
99
+ if (!data.flashback) return undefined
100
+ return {
101
+ id: data.flashback.id,
102
+ levelRange: data.flashback.levelRange ?? data.levelRange,
103
+ name: data.flashback.name ?? data.name,
104
+ morality: new MoralityList(coalesceToArray(data.flashback.morality ?? data.morality)),
105
+ notes: data.flashback.notes,
106
+ }
107
+ }
@@ -0,0 +1,99 @@
1
+ import { MORALITY, Morality, MoralityExtended } from '../api/morality'
2
+
3
+ export class MoralityList {
4
+ readonly #items: Set<Morality>
5
+
6
+ readonly hero: boolean
7
+ readonly vigilante: boolean
8
+ readonly villain: boolean
9
+ readonly rogue: boolean
10
+ readonly resistance: boolean
11
+ readonly loyalist: boolean
12
+
13
+ readonly primal: boolean
14
+ readonly praetorian: boolean
15
+ readonly heroic: boolean
16
+ readonly villainous: boolean
17
+ readonly paragonCityAccess: boolean
18
+ readonly rogueIslesAccess: boolean
19
+
20
+ readonly all: boolean
21
+
22
+ constructor(items?: MoralityExtended[]) {
23
+ const set = new Set(items ?? [...MORALITY])
24
+ this.hero = set.has('hero') || set.has('primal') || set.has('heroic') || set.has('paragon-city-access') || set.has('all')
25
+ this.vigilante = set.has('vigilante') || set.has('primal') || set.has('heroic') || set.has('paragon-city-access') || set.has('rogue-isles-access') || set.has('all')
26
+ this.villain = set.has('villain') || set.has('primal') || set.has('villainous') || set.has('rogue-isles-access') || set.has('all')
27
+ this.rogue = set.has('rogue') || set.has('primal') || set.has('villainous') || set.has('paragon-city-access') || set.has('rogue-isles-access') || set.has('all')
28
+ this.resistance = set.has('resistance') || set.has('praetorian') || set.has('all')
29
+ this.loyalist = set.has('loyalist') || set.has('praetorian') || set.has('all')
30
+
31
+ this.primal = this.hero && this.vigilante && this.villain && this.rogue
32
+ this.praetorian = this.loyalist && this.resistance
33
+ this.heroic = this.hero && this.vigilante
34
+ this.villainous = this.villain && this.rogue
35
+ this.paragonCityAccess = this.heroic && this.rogue
36
+ this.rogueIslesAccess = this.villainous && this.vigilante
37
+
38
+ this.all = this.primal && this.praetorian
39
+
40
+ this.#items = new Set()
41
+ if (this.hero) this.#items.add('hero')
42
+ if (this.vigilante) this.#items.add('vigilante')
43
+ if (this.villain) this.#items.add('villain')
44
+ if (this.rogue) this.#items.add('rogue')
45
+ if (this.resistance) this.#items.add('resistance')
46
+ if (this.loyalist) this.#items.add('loyalist')
47
+ }
48
+
49
+ get items(): Morality[] {
50
+ return [...this.#items]
51
+ }
52
+
53
+ has(morality?: MoralityExtended): boolean {
54
+ switch (morality) {
55
+ case 'hero': {
56
+ return this.hero
57
+ }
58
+ case 'vigilante': {
59
+ return this.vigilante
60
+ }
61
+ case 'villain': {
62
+ return this.villain
63
+ }
64
+ case 'rogue': {
65
+ return this.rogue
66
+ }
67
+ case 'resistance': {
68
+ return this.resistance
69
+ }
70
+ case 'loyalist': {
71
+ return this.loyalist
72
+ }
73
+ case 'primal' : {
74
+ return this.primal
75
+ }
76
+ case 'praetorian': {
77
+ return this.praetorian
78
+ }
79
+ case 'heroic': {
80
+ return this.hero
81
+ }
82
+ case 'paragon-city-access': {
83
+ return this.paragonCityAccess
84
+ }
85
+ case 'rogue-isles-access': {
86
+ return this.rogueIslesAccess
87
+ }
88
+ case 'villainous': {
89
+ return this.villainous
90
+ }
91
+ case 'all': {
92
+ return this.all
93
+ }
94
+ default: {
95
+ return false
96
+ }
97
+ }
98
+ }
99
+ }
@@ -6,7 +6,7 @@ export class Zone {
6
6
  /**
7
7
  * Unique key used to reference this zone.
8
8
  *
9
- * Keys can only contain lowercase letters, numbers and hyphens (`-`).
9
+ * Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
10
10
  */
11
11
  readonly key: string
12
12
 
package/src/main/index.ts CHANGED
@@ -11,13 +11,16 @@ export * from './api/contact-data'
11
11
  export * from './api/content-bundle'
12
12
  export * from './api/enhancement-category'
13
13
  export * from './api/link'
14
+ export * from './api/location-data'
14
15
  export * from './api/markdown-string'
15
- export * from './api/plaque-type'
16
+ export * from './api/mission-data'
17
+ export * from './api/mission-type'
18
+ export * from './api/morality'
16
19
  export * from './api/sex'
17
20
  export * from './api/zone-data'
18
21
 
19
22
  // DB
20
- export * from './db/alignments'
23
+ export * from './db/alignment-list'
21
24
  export * from './db/alternates'
22
25
  export * from './db/archetype'
23
26
  export * from './db/badge'
@@ -28,10 +31,12 @@ export * from './db/bundle-metadata'
28
31
  export * from './db/coh-content-database'
29
32
  export * from './db/contact'
30
33
  export * from './db/key'
34
+ export * from './db/location'
35
+ export * from './db/mission'
36
+ export * from './db/morality-list'
31
37
  export * from './db/paged'
32
38
  export * from './db/zone'
33
39
 
34
40
  // ROOT
35
41
  export { CHANGELOG } from './changelog'
36
-
37
42
  export * from './util'
package/src/main/util.ts CHANGED
@@ -4,9 +4,11 @@ import { ZoneData } from './api/zone-data'
4
4
  import { Zone } from './db/zone'
5
5
  import { Contact } from './db/contact'
6
6
  import { ContactData } from './api/contact-data'
7
+ import { Mission } from './db/mission'
8
+ import { MissionData } from './api/mission-data'
7
9
 
8
10
  /**
9
- * Returns the URI of the given badge that can be used in {@link MarkdownString} links.
11
+ * Returns the URI of the given badge that can be used in {@link MarkdownString} fields.
10
12
  *
11
13
  * URI format: `badge://<key>`
12
14
  *
@@ -30,7 +32,7 @@ export function badgeLink(target: string | Badge | BadgeData): string {
30
32
  }
31
33
 
32
34
  /**
33
- * Returns the URI of the given contact that can be used in {@link MarkdownString} links.
35
+ * Returns the URI of the given contact that can be used in {@link MarkdownString} fields.
34
36
  *
35
37
  * URI format: `contact://<key>`
36
38
  *
@@ -54,7 +56,31 @@ export function contactLink(target: string | Contact | ContactData): string {
54
56
  }
55
57
 
56
58
  /**
57
- * Returns the URI of the given zone that can be used in {@link MarkdownString} links.
59
+ * Returns the URI of the given mission that can be used in {@link MarkdownString} fields.
60
+ *
61
+ * URI format: `mission://<key>`
62
+ *
63
+ * @param target The {@link Mission} or mission key to target.
64
+ */
65
+ export function missionUri(target: string | Mission | MissionData): string {
66
+ const key = typeof target === 'string' ? target : target.key
67
+ return `mission://${key}`
68
+ }
69
+
70
+ /**
71
+ * Returns a {@link MarkdownString} link to the given mission.
72
+ *
73
+ * Link format: `[<key>](mission://<key>)`
74
+ *
75
+ * @param target The {@link Mission} or mission key to target.
76
+ */
77
+ export function missionLink(target: string | Mission | MissionData): string {
78
+ const key = typeof target === 'string' ? target : target.key
79
+ return `[${key}](${missionUri(target)})`
80
+ }
81
+
82
+ /**
83
+ * Returns the URI of the given zone that can be used in {@link MarkdownString} fields.
58
84
  *
59
85
  * URI format: `zone://<key>`
60
86
  *
@@ -76,3 +102,17 @@ export function zoneLink(target: string | Zone | ZoneData): string {
76
102
  const key = typeof target === 'string' ? target : target.key
77
103
  return `[${key}](${zoneUri(target)})`
78
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
+ }