coh-content-db 1.4.1 → 2.0.0-rc.10
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.
- package/.editorconfig +24 -0
- package/.github/workflows/build.yml +38 -0
- package/.github/workflows/pull-request.yml +32 -0
- package/.github/workflows/release.yml +52 -0
- package/CHANGELOG.md +42 -0
- package/LICENSE +24 -674
- package/README.md +101 -16
- package/dist/coh-content-db.d.ts +988 -22
- package/dist/coh-content-db.js +1044 -2
- package/dist/coh-content-db.js.map +1 -0
- package/dist/coh-content-db.mjs +1009 -0
- package/dist/coh-content-db.mjs.map +1 -0
- package/eslint.config.mjs +31 -0
- package/jest.config.mjs +7 -0
- package/package.json +31 -23
- package/rollup.config.mjs +27 -0
- package/src/main/api/alignment.ts +19 -0
- package/src/main/api/alternate-data.ts +22 -0
- package/src/main/api/archetype-data.ts +5 -0
- package/src/main/api/badge-data.ts +81 -0
- package/src/main/api/badge-requirement-data.ts +64 -0
- package/src/main/api/badge-requirement-type.ts +32 -0
- package/src/main/api/badge-type.ts +19 -0
- package/src/main/api/bundle-data.ts +47 -0
- package/src/main/api/bundle-header-data.ts +37 -0
- package/src/main/api/contact-data.ts +48 -0
- package/src/main/api/enhancement-category.ts +30 -0
- package/src/main/api/link.ts +4 -0
- package/src/main/api/location-data.ts +28 -0
- package/src/main/api/markdown-string.ts +4 -0
- package/src/main/api/mission-data.ts +83 -0
- package/src/main/api/mission-type.ts +2 -0
- package/src/main/api/morality.ts +31 -0
- package/src/main/api/sex.ts +10 -0
- package/src/main/api/zone-data.ts +20 -0
- package/src/main/db/abstract-index.ts +37 -0
- package/src/main/db/alignment-list.ts +54 -0
- package/src/main/db/alternates.ts +67 -0
- package/src/main/db/archetype.ts +14 -0
- package/src/main/db/badge-index.ts +57 -0
- package/src/main/db/badge-requirement.ts +81 -0
- package/src/main/db/badge-search-options.ts +51 -0
- package/src/main/db/badge.ts +147 -0
- package/src/main/db/bundle-header.ts +44 -0
- package/src/main/db/coh-content-database.ts +138 -0
- package/src/main/db/contact.ts +62 -0
- package/src/main/db/key.ts +18 -0
- package/src/main/db/location.ts +30 -0
- package/src/main/db/mission.ts +107 -0
- package/src/main/db/morality-list.ts +99 -0
- package/src/main/db/paged.ts +7 -0
- package/src/main/db/zone.ts +28 -0
- package/src/main/index.ts +41 -0
- package/src/main/util.ts +118 -0
- package/src/test/api/alignment.test.ts +65 -0
- package/src/test/api/archetype-data.fixture.ts +8 -0
- package/src/test/api/badge-data.fixture.ts +8 -0
- package/src/test/api/badge-data.test.ts +15 -0
- package/src/test/api/badge-requirement-data.fixture.ts +7 -0
- package/src/test/api/badge-requirement-type.test.ts +31 -0
- package/src/test/api/badge-type.test.ts +35 -0
- package/src/test/api/bundle-data.fixture.ts +6 -0
- package/src/test/api/bundle-header-data.fixture.ts +6 -0
- package/src/test/api/contact-data.fixture.ts +7 -0
- package/src/test/api/enhancement-category.test.ts +35 -0
- package/src/test/api/mission-data.fixture.ts +12 -0
- package/src/test/api/sex.test.ts +63 -0
- package/src/test/api/zone-data.fixture.ts +8 -0
- package/src/test/db/abstract-index.test.ts +55 -0
- package/src/test/db/alignment-list.test.ts +200 -0
- package/src/test/db/alternates.test.ts +188 -0
- package/src/test/db/archetype.test.ts +38 -0
- package/src/test/db/badge-index.test.ts +519 -0
- package/src/test/db/badge-requirement.test.ts +145 -0
- package/src/test/db/badge.test.ts +337 -0
- package/src/test/db/bundle-header.test.ts +76 -0
- package/src/test/db/coh-content-database.test.ts +282 -0
- package/src/test/db/contact.test.ts +97 -0
- package/src/test/db/key.test.ts +22 -0
- package/src/test/db/location.test.ts +51 -0
- package/src/test/db/mission.test.ts +171 -0
- package/src/test/db/morality-list.test.ts +457 -0
- package/src/test/db/zone.test.ts +36 -0
- package/src/test/integration.test.ts +16 -0
- package/src/test/util.test.ts +165 -0
- package/tsconfig.json +117 -0
- package/dist/_changelog.d.ts +0 -3
- package/dist/coh-content-db.nomin.js +0 -635
- package/dist/content-refence-utils.d.ts +0 -4
- package/dist/index.d.ts +0 -8
- package/dist/internal/_common.d.ts +0 -4
- package/dist/internal/archetype.d.ts +0 -10
- package/dist/internal/badge.d.ts +0 -44
- package/dist/internal/game-map.d.ts +0 -33
- package/dist/internal/server-group.d.ts +0 -24
- package/dist/types/archetype.d.ts +0 -9
- package/dist/types/badge.d.ts +0 -192
- package/dist/types/enhancement.d.ts +0 -28
- package/dist/types/game-map.d.ts +0 -47
- package/dist/types/link.d.ts +0 -4
- package/dist/types/server-group.d.ts +0 -75
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Link } from './link'
|
|
2
|
+
import { MarkdownString } from './markdown-string'
|
|
3
|
+
import { LocationData } from './location-data'
|
|
4
|
+
import { MoralityExtended } from './morality'
|
|
5
|
+
|
|
6
|
+
export interface ContactData {
|
|
7
|
+
/**
|
|
8
|
+
* Unique key used to reference this contact.
|
|
9
|
+
*
|
|
10
|
+
* Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
|
|
11
|
+
*/
|
|
12
|
+
readonly key: string
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The name of this contact.
|
|
16
|
+
*/
|
|
17
|
+
readonly name: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The contact's title.
|
|
21
|
+
*/
|
|
22
|
+
readonly title?: string
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The character moralities that this contact will interact with.
|
|
26
|
+
*/
|
|
27
|
+
readonly morality?: MoralityExtended | MoralityExtended[]
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The location of this contact.
|
|
31
|
+
*/
|
|
32
|
+
readonly location?: LocationData
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The level range this contact will offer missions for.
|
|
36
|
+
*/
|
|
37
|
+
readonly levelRange?: [number, number?]
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Freeform notes or tips about the contact.
|
|
41
|
+
*/
|
|
42
|
+
readonly notes?: MarkdownString
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List of external links. Wiki, forums, etc.
|
|
46
|
+
*/
|
|
47
|
+
readonly links?: Link[]
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const ENHANCEMENT_CATEGORY = [
|
|
2
|
+
'defense-debuff',
|
|
3
|
+
'to-hit-debuff',
|
|
4
|
+
'taunt',
|
|
5
|
+
'confuse',
|
|
6
|
+
'healing',
|
|
7
|
+
'defense-buff',
|
|
8
|
+
'resist-damage',
|
|
9
|
+
'intangibility',
|
|
10
|
+
'sleep',
|
|
11
|
+
'slow',
|
|
12
|
+
'hold',
|
|
13
|
+
'stun',
|
|
14
|
+
'immobilize',
|
|
15
|
+
'fear',
|
|
16
|
+
'endurance-modification',
|
|
17
|
+
'endurance-reduction',
|
|
18
|
+
'recharge-reduction',
|
|
19
|
+
'interrupt-duration',
|
|
20
|
+
'accuracy',
|
|
21
|
+
'to-hit-buff',
|
|
22
|
+
'damage',
|
|
23
|
+
'knockback',
|
|
24
|
+
'run-speed',
|
|
25
|
+
'jump',
|
|
26
|
+
'fly-speed',
|
|
27
|
+
'range',
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
export type EnhancementCategory = typeof ENHANCEMENT_CATEGORY[number]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface LocationData {
|
|
2
|
+
/**
|
|
3
|
+
* Key of the {@link Zone} that the location references.
|
|
4
|
+
*/
|
|
5
|
+
readonly zoneKey?: string
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* In-game `/loc` coordinates of the location.
|
|
9
|
+
*/
|
|
10
|
+
readonly coords?: Coords
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The type of icon to use if the location appears on a map. (Typically the Vidiot map icon).
|
|
14
|
+
*/
|
|
15
|
+
readonly icon?: LocationIcon
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The text that should appear in the location icon. (Typically a number or symbol from the Vidiot map).
|
|
19
|
+
*/
|
|
20
|
+
readonly iconText?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Coordinates as they appear using the in-game `/loc` command.
|
|
25
|
+
*/
|
|
26
|
+
export type Coords = [number, number, number]
|
|
27
|
+
|
|
28
|
+
export type LocationIcon = 'badge' | 'plaque' | 'pedestal' | 'object'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Link } from './link'
|
|
2
|
+
import { MarkdownString } from './markdown-string'
|
|
3
|
+
import { MissionType } from './mission-type'
|
|
4
|
+
import { MoralityExtended } from './morality'
|
|
5
|
+
|
|
6
|
+
export interface MissionData {
|
|
7
|
+
/**
|
|
8
|
+
* Unique key used to reference this mission.
|
|
9
|
+
*
|
|
10
|
+
* Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
|
|
11
|
+
*/
|
|
12
|
+
readonly key: string
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The name of the mission as it appears from the contact.
|
|
16
|
+
*
|
|
17
|
+
* The name may be different when viewed in Ouroboros as a Flashback.
|
|
18
|
+
*/
|
|
19
|
+
readonly name: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The type of mission... Story arc, task force, trial, etc.
|
|
23
|
+
*/
|
|
24
|
+
readonly type: MissionType
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The character moralities that may accept the mission.
|
|
28
|
+
*/
|
|
29
|
+
readonly morality?: MoralityExtended | MoralityExtended[]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The keys of any contacts that provide this mission.
|
|
33
|
+
*/
|
|
34
|
+
readonly contactKeys?: string | string[]
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The level range this mission is available for.
|
|
38
|
+
*/
|
|
39
|
+
readonly levelRange?: [number, number?]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Freeform notes or tips about the mission.
|
|
43
|
+
*/
|
|
44
|
+
readonly notes?: MarkdownString
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* List of external links. Wiki, forums, etc.
|
|
48
|
+
*/
|
|
49
|
+
readonly links?: Link[]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* If the mission is available in Ouroboros as a Flashback.
|
|
53
|
+
*/
|
|
54
|
+
readonly flashback?: MissionFlashbackData
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface MissionFlashbackData {
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The id of the mission as seen in the Flashback menu, i.e. '14.01'.
|
|
61
|
+
*/
|
|
62
|
+
readonly id: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The level range this mission appears under as a Flashback. Leave undefined if the same as the base mission.
|
|
66
|
+
*/
|
|
67
|
+
readonly levelRange?: [number, number?]
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The name as it appears in the Flashback list. Leave undefined if the same as the base mission.
|
|
71
|
+
*/
|
|
72
|
+
readonly name?: string
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The character moralities that the mission will appear for in the Flashback list. Leave undefined if the same as the base mission.
|
|
76
|
+
*/
|
|
77
|
+
readonly morality?: MoralityExtended | MoralityExtended[]
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Freeform notes or tips about the Flashback version of the mission.
|
|
81
|
+
*/
|
|
82
|
+
readonly notes?: MarkdownString
|
|
83
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const MORALITY = ['hero', 'vigilante', 'villain', 'rogue', 'resistance', 'loyalist'] as const
|
|
2
|
+
export type Morality = typeof MORALITY[number]
|
|
3
|
+
export type MoralityExtended = Morality
|
|
4
|
+
/**
|
|
5
|
+
* Any of the Primal Earth moralities - Hero, Vigilante, Villain, Rogue.
|
|
6
|
+
*/
|
|
7
|
+
| 'primal'
|
|
8
|
+
/**
|
|
9
|
+
* Either of the Praetorian Earth moralities - Resistance or Loyalist.
|
|
10
|
+
*/
|
|
11
|
+
| 'praetorian'
|
|
12
|
+
/**
|
|
13
|
+
* The moralities that roll up to the Hero {@link Alignment} - Hero and Vigilante.
|
|
14
|
+
*/
|
|
15
|
+
| 'heroic'
|
|
16
|
+
/**
|
|
17
|
+
* The moralities that roll up to the Villain {@link Alignment} - Villain and Rogue.
|
|
18
|
+
*/
|
|
19
|
+
| 'villainous'
|
|
20
|
+
/**
|
|
21
|
+
* Moralities with access to Paragon City - Hero, Vigilante and Rogue.
|
|
22
|
+
*/
|
|
23
|
+
| 'paragon-city-access'
|
|
24
|
+
/**
|
|
25
|
+
* Moralities with access to the Rogue Isles - Villain, Rogue and Vigilante.
|
|
26
|
+
*/
|
|
27
|
+
| 'rogue-isles-access'
|
|
28
|
+
/**
|
|
29
|
+
* All the moralities.
|
|
30
|
+
*/
|
|
31
|
+
| 'all'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const SEX = ['M', 'F'] as const
|
|
2
|
+
export type Sex = typeof SEX[number]
|
|
3
|
+
|
|
4
|
+
const SEX_ORDER = Object.fromEntries(SEX.map((x, index) => [x, index]))
|
|
5
|
+
|
|
6
|
+
export function compareSex(a?: Sex, b?: Sex): number {
|
|
7
|
+
const orderA = a ? SEX_ORDER[a] : -1
|
|
8
|
+
const orderB = b ? SEX_ORDER[b] : -1
|
|
9
|
+
return orderA - orderB
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Link } from './link'
|
|
2
|
+
|
|
3
|
+
export interface ZoneData {
|
|
4
|
+
/**
|
|
5
|
+
* Unique key used to reference this zone.
|
|
6
|
+
*
|
|
7
|
+
* Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
|
|
8
|
+
*/
|
|
9
|
+
readonly key: string
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The name of the zone as it appears in-game.
|
|
13
|
+
*/
|
|
14
|
+
readonly name: string
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List of external links. Wiki, forums, etc.
|
|
18
|
+
*/
|
|
19
|
+
readonly links?: Link[]
|
|
20
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type KeysOfType<T, V> = { [P in keyof T]: T[P] extends V ? P : never }[keyof T]
|
|
2
|
+
|
|
3
|
+
export class AbstractIndex<T> {
|
|
4
|
+
protected _values: T[] = []
|
|
5
|
+
protected _hashTable: Record<string, T> = {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new index.
|
|
9
|
+
* @param keyField The field of the values that will act as the key.
|
|
10
|
+
* @param values Values to index.
|
|
11
|
+
*/
|
|
12
|
+
constructor(keyField: KeysOfType<T, string>, values: T[] | undefined) {
|
|
13
|
+
this._values = values ?? []
|
|
14
|
+
this._hashTable = {}
|
|
15
|
+
for (const value of this.values) {
|
|
16
|
+
const key = value[keyField] as string
|
|
17
|
+
if (this._hashTable[key] !== undefined) throw new Error(`Duplicate key [${key}]`)
|
|
18
|
+
this._hashTable[key] = value
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return all indexed values
|
|
24
|
+
*/
|
|
25
|
+
get values(): T[] {
|
|
26
|
+
return this._values
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a value from the index
|
|
31
|
+
* @param key Key string
|
|
32
|
+
*/
|
|
33
|
+
get(key: string | undefined): T | undefined {
|
|
34
|
+
if (!key) return undefined
|
|
35
|
+
return this._hashTable[key]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ALIGNMENT, Alignment, AlignmentExtended } from '../api/alignment'
|
|
2
|
+
|
|
3
|
+
export class AlignmentList {
|
|
4
|
+
readonly #items: Set<Alignment>
|
|
5
|
+
|
|
6
|
+
readonly hero: boolean
|
|
7
|
+
readonly villain: boolean
|
|
8
|
+
readonly praetorian: boolean
|
|
9
|
+
|
|
10
|
+
readonly primal: boolean
|
|
11
|
+
readonly all: boolean
|
|
12
|
+
|
|
13
|
+
constructor(items?: AlignmentExtended[]) {
|
|
14
|
+
const set = new Set(items ?? [...ALIGNMENT])
|
|
15
|
+
this.hero = set.has('hero') || set.has('primal') || set.has('all')
|
|
16
|
+
this.villain = set.has('villain') || set.has('primal') || set.has('all')
|
|
17
|
+
this.praetorian = set.has('praetorian') || set.has('all')
|
|
18
|
+
|
|
19
|
+
this.primal = this.hero && this.villain
|
|
20
|
+
this.all = this.hero && this.villain && this.praetorian
|
|
21
|
+
|
|
22
|
+
this.#items = new Set()
|
|
23
|
+
if (this.hero) this.#items.add('hero')
|
|
24
|
+
if (this.villain) this.#items.add('villain')
|
|
25
|
+
if (this.praetorian) this.#items.add('praetorian')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get items(): Alignment[] {
|
|
29
|
+
return [...this.#items]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
has(alignment?: AlignmentExtended): boolean {
|
|
33
|
+
switch (alignment) {
|
|
34
|
+
case 'hero': {
|
|
35
|
+
return this.hero
|
|
36
|
+
}
|
|
37
|
+
case 'villain': {
|
|
38
|
+
return this.villain
|
|
39
|
+
}
|
|
40
|
+
case 'praetorian': {
|
|
41
|
+
return this.praetorian
|
|
42
|
+
}
|
|
43
|
+
case 'primal' : {
|
|
44
|
+
return this.primal
|
|
45
|
+
}
|
|
46
|
+
case 'all': {
|
|
47
|
+
return this.all
|
|
48
|
+
}
|
|
49
|
+
default: {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Key } from './key'
|
|
2
|
+
import { ArchetypeData } from '../api/archetype-data'
|
|
3
|
+
|
|
4
|
+
export class Archetype {
|
|
5
|
+
readonly key: string
|
|
6
|
+
readonly name: string
|
|
7
|
+
readonly description?: string
|
|
8
|
+
|
|
9
|
+
constructor(data: ArchetypeData) {
|
|
10
|
+
this.key = new Key(data.key).value
|
|
11
|
+
this.name = data.name
|
|
12
|
+
this.description = data.description
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Badge, compareByDefaultName, compareByZoneKey } from './badge'
|
|
2
|
+
import { BadgeSearchOptions } from './badge-search-options'
|
|
3
|
+
import { Paged } from './paged'
|
|
4
|
+
import { AbstractIndex } from './abstract-index'
|
|
5
|
+
|
|
6
|
+
export class BadgeIndex extends AbstractIndex<Badge> {
|
|
7
|
+
constructor(values: Badge[] | undefined) {
|
|
8
|
+
super('key', values)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
search(options?: BadgeSearchOptions): Paged<Badge> {
|
|
12
|
+
const filtered = (options?.query || options?.filter)
|
|
13
|
+
? this._values.filter(badge => this.#satisfiesQueryPredicate(badge, options?.query) && this.#satisfiesFilterPredicate(badge, options?.filter))
|
|
14
|
+
: this._values
|
|
15
|
+
|
|
16
|
+
const totalPages = options?.pageSize ? Math.ceil(filtered.length / (options?.pageSize)) : 1
|
|
17
|
+
const page = Math.max(1, Math.min(totalPages, options?.page ?? 1))
|
|
18
|
+
const paged = options?.pageSize ? filtered.slice((page - 1) * options.pageSize, page * options?.pageSize) : filtered
|
|
19
|
+
|
|
20
|
+
const sorted = this.#sort(paged, options?.sort)
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
items: sorted,
|
|
24
|
+
page: page,
|
|
25
|
+
pageSize: options?.pageSize,
|
|
26
|
+
totalItems: filtered.length,
|
|
27
|
+
totalPages: totalPages,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
|
|
32
|
+
const queryString = query?.str?.toLowerCase() ?? ''
|
|
33
|
+
return !!(((query?.on?.name ?? true) && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
34
|
+
|| (query?.on?.badgeText && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
35
|
+
|| (query?.on?.acquisition && badge.acquisition?.toLowerCase().includes(queryString))
|
|
36
|
+
|| (query?.on?.effect && badge.effect?.toLowerCase().includes(queryString))
|
|
37
|
+
|| (query?.on?.notes && badge.notes?.toLowerCase().includes(queryString))
|
|
38
|
+
|| (query?.on?.setTitle && (badge.setTitleId?.some(x => x?.toString().includes(queryString)))))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
|
|
42
|
+
return (!filter?.type || badge.type === filter.type)
|
|
43
|
+
&& (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
|
|
44
|
+
&& (!filter?.morality || badge.morality.has(filter.morality))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#sort(badges: Badge[], sort?: BadgeSearchOptions['sort']): Badge[] {
|
|
48
|
+
if (!sort) return badges
|
|
49
|
+
const ascending = sort.dir !== 'desc'
|
|
50
|
+
|
|
51
|
+
if (sort.by === 'badge-name') return badges.sort((a, b) => ascending ? compareByDefaultName(a, b) : compareByDefaultName(b, a))
|
|
52
|
+
|
|
53
|
+
if (sort.by === 'zone-key') return badges.sort((a, b) => ascending ? compareByZoneKey(a, b) : compareByZoneKey(b, a))
|
|
54
|
+
|
|
55
|
+
return sort.dir === 'desc' ? badges.reverse() : badges
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { BadgeRequirementData } from '../api/badge-requirement-data'
|
|
2
|
+
import { BadgeRequirementType } from '../api/badge-requirement-type'
|
|
3
|
+
import { EnhancementCategory } from '../api/enhancement-category'
|
|
4
|
+
import { Key } from './key'
|
|
5
|
+
import { MarkdownString } from '../api/markdown-string'
|
|
6
|
+
import { Link } from '../api/link'
|
|
7
|
+
import { Location } from './location'
|
|
8
|
+
import { coalesceToArray } from '../util'
|
|
9
|
+
|
|
10
|
+
export class BadgeRequirement {
|
|
11
|
+
/**
|
|
12
|
+
* Unique key used to reference this badge requirement.
|
|
13
|
+
*
|
|
14
|
+
* Keys must be unique and can only contain lowercase letters, numbers and hyphens (`-`).
|
|
15
|
+
*/
|
|
16
|
+
readonly key: string
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The requirement type.
|
|
20
|
+
*/
|
|
21
|
+
readonly type: BadgeRequirementType
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* If the requirement involves a location, where it is.
|
|
25
|
+
*/
|
|
26
|
+
readonly location?: Location[]
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* If the requirement involves a badge, the badge key.
|
|
30
|
+
*/
|
|
31
|
+
readonly badgeKey?: string
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* If the requirement involves a mission, the mission key.
|
|
35
|
+
*/
|
|
36
|
+
readonly missionKey?: string
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* If the requirement involves a monument, the text that is displayed thereon.
|
|
40
|
+
*/
|
|
41
|
+
readonly monumentText?: string
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* If the requirement involves crafting an invention, the Level of the invention required.
|
|
45
|
+
*/
|
|
46
|
+
readonly inventionLevel?: number
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* If the requirement involves crafting an invention, the types of enhancements that will qualify.
|
|
50
|
+
*/
|
|
51
|
+
readonly inventionTypes?: EnhancementCategory[]
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Number of times the task needs to be repeated.
|
|
55
|
+
*/
|
|
56
|
+
readonly count?: number
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Additional information about the requirement.
|
|
60
|
+
*/
|
|
61
|
+
readonly notes?: MarkdownString
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List of external links. Wiki, forums, etc.
|
|
65
|
+
*/
|
|
66
|
+
readonly links: Link[]
|
|
67
|
+
|
|
68
|
+
constructor(data: BadgeRequirementData) {
|
|
69
|
+
this.key = new Key(data.key).value
|
|
70
|
+
this.type = data.type
|
|
71
|
+
this.location = coalesceToArray(data.location)
|
|
72
|
+
this.badgeKey = data.badgeKey
|
|
73
|
+
this.missionKey = data.missionKey
|
|
74
|
+
this.monumentText = data.monumentText
|
|
75
|
+
this.inventionLevel = data.inventionLevel
|
|
76
|
+
this.inventionTypes = data.inventionTypes
|
|
77
|
+
this.count = data.count
|
|
78
|
+
this.notes = data.notes
|
|
79
|
+
this.links = data.links ?? []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BadgeType } from '../api/badge-type'
|
|
2
|
+
import { MoralityExtended } from '../api/morality'
|
|
3
|
+
|
|
4
|
+
export interface BadgeSearchOptions {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Text-based search.
|
|
8
|
+
*
|
|
9
|
+
* Case-insensitive. Defaults to searching on name only.
|
|
10
|
+
*/
|
|
11
|
+
query?: {
|
|
12
|
+
str?: string
|
|
13
|
+
on?: {
|
|
14
|
+
name?: boolean
|
|
15
|
+
badgeText?: boolean
|
|
16
|
+
acquisition?: boolean
|
|
17
|
+
notes?: boolean
|
|
18
|
+
effect?: boolean
|
|
19
|
+
setTitle?: boolean
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Filter results matching the given values.
|
|
25
|
+
*/
|
|
26
|
+
filter?: {
|
|
27
|
+
type?: BadgeType
|
|
28
|
+
zoneKey?: string
|
|
29
|
+
morality?: MoralityExtended
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sort results.
|
|
34
|
+
*
|
|
35
|
+
* Badges are assumed to be in canonical order in the content bundle, and should match the in-game display order.
|
|
36
|
+
*/
|
|
37
|
+
sort?: {
|
|
38
|
+
by?: 'canonical' | 'badge-name' | 'zone-key'
|
|
39
|
+
dir?: 'asc' | 'desc'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The page (1-based)
|
|
44
|
+
*/
|
|
45
|
+
page?: number
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How many results per page
|
|
49
|
+
*/
|
|
50
|
+
pageSize?: number
|
|
51
|
+
}
|