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.
- package/.github/workflows/build.yml +3 -1
- package/CHANGELOG.md +47 -0
- package/README.md +48 -17
- package/dist/coh-content-db.d.ts +257 -200
- package/dist/coh-content-db.js +509 -339
- package/dist/coh-content-db.js.map +1 -1
- package/dist/coh-content-db.mjs +501 -335
- package/dist/coh-content-db.mjs.map +1 -1
- package/jest.config.mjs +1 -0
- package/package.json +14 -14
- package/src/main/api/badge-data.ts +13 -7
- package/src/main/api/{content-bundle.ts → bundle-data.ts} +5 -27
- package/src/main/api/bundle-header-data.ts +44 -0
- package/src/main/api/contact-data.ts +2 -1
- package/src/main/api/level-range-data.ts +4 -0
- package/src/main/api/mission-data.ts +3 -29
- package/src/main/api/mission-flashback-data.ts +31 -0
- package/src/main/api/morality.ts +27 -9
- package/src/main/api/set-title-data.ts +4 -0
- package/src/main/api/variant-context.ts +11 -0
- package/src/main/api/{alternate-data.ts → variant-data.ts} +4 -4
- package/src/main/api/zone-data.ts +24 -0
- package/src/main/api/zone-type.ts +59 -0
- package/src/main/db/abstract-index.ts +12 -16
- package/src/main/db/badge-index.ts +53 -27
- package/src/main/db/badge-requirement.ts +1 -1
- package/src/main/db/badge-search-options.ts +15 -14
- package/src/main/db/badge.ts +46 -29
- package/src/main/db/bundle-header.ts +52 -0
- package/src/main/db/coh-content-database.ts +22 -22
- package/src/main/db/contact.ts +5 -4
- package/src/main/db/level-range.ts +15 -0
- package/src/main/db/mission.ts +11 -10
- package/src/main/db/paged.ts +7 -3
- package/src/main/db/set-title-ids.ts +10 -0
- package/src/main/db/variants.ts +84 -0
- package/src/main/db/zone.ts +29 -0
- package/src/main/index.ts +14 -8
- package/src/main/util/coalesce-to-array.ts +13 -0
- package/src/main/{util.ts → util/links.ts} +8 -22
- package/src/main/util/to-date.ts +9 -0
- package/src/test/api/alignment.test.ts +2 -2
- package/src/test/api/badge-data.fixture.ts +1 -0
- package/src/test/api/badge-data.test.ts +1 -0
- package/src/test/api/bundle-data.fixture.ts +7 -0
- package/src/test/api/bundle-header-data.fixture.ts +8 -0
- package/src/test/api/morality.test.ts +31 -0
- package/src/test/api/sex.test.ts +2 -2
- package/src/test/api/zone-data.fixture.ts +1 -0
- package/src/test/db/abstract-index.test.ts +12 -43
- package/src/test/db/badge-index.test.ts +197 -101
- package/src/test/db/badge.test.ts +122 -16
- package/src/test/db/bundle-header.test.ts +89 -0
- package/src/test/db/coh-content-database.test.ts +137 -178
- package/src/test/db/contact.test.ts +2 -1
- package/src/test/db/level-range.test.ts +47 -0
- package/src/test/db/mission.test.ts +8 -6
- package/src/test/db/morality-list.test.ts +1 -1
- package/src/test/db/set-title-ids.test.ts +19 -0
- package/src/test/db/{alternates.test.ts → variants.test.ts} +24 -24
- package/src/test/db/zone.test.ts +45 -0
- package/src/test/integration.test.ts +16 -0
- package/src/test/util/coalese-to-array.test.ts +17 -0
- package/src/test/{util.test.ts → util/links.test.ts} +5 -21
- package/src/test/util/to-date.test.ts +15 -0
- package/src/main/api/change.ts +0 -17
- package/src/main/changelog.ts +0 -29
- package/src/main/db/alternates.ts +0 -67
- package/src/main/db/bundle-metadata.ts +0 -45
- package/src/test/api/content-bundle.fixture.ts +0 -6
- package/src/test/api/content-bundle.test.ts +0 -14
- package/src/test/changelog.test.ts +0 -36
- package/src/test/db/bundle-metadata.test.ts +0 -84
- package/src/test/index.test.ts +0 -14
|
@@ -2,6 +2,8 @@ import { Link } from './link'
|
|
|
2
2
|
import { MarkdownString } from './markdown-string'
|
|
3
3
|
import { MissionType } from './mission-type'
|
|
4
4
|
import { MoralityExtended } from './morality'
|
|
5
|
+
import { LevelRangeData } from './level-range-data'
|
|
6
|
+
import { MissionFlashbackData } from './mission-flashback-data'
|
|
5
7
|
|
|
6
8
|
export interface MissionData {
|
|
7
9
|
/**
|
|
@@ -36,7 +38,7 @@ export interface MissionData {
|
|
|
36
38
|
/**
|
|
37
39
|
* The level range this mission is available for.
|
|
38
40
|
*/
|
|
39
|
-
readonly levelRange?:
|
|
41
|
+
readonly levelRange?: LevelRangeData
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
44
|
* Freeform notes or tips about the mission.
|
|
@@ -53,31 +55,3 @@ export interface MissionData {
|
|
|
53
55
|
*/
|
|
54
56
|
readonly flashback?: MissionFlashbackData
|
|
55
57
|
}
|
|
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
|
+
import { MarkdownString } from './markdown-string'
|
|
2
|
+
import { MoralityExtended } from './morality'
|
|
3
|
+
import { LevelRangeData } from './level-range-data'
|
|
4
|
+
|
|
5
|
+
export interface MissionFlashbackData {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The id of the mission as seen in the Flashback menu, i.e. '14.01'.
|
|
9
|
+
*/
|
|
10
|
+
readonly id: string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The level range this mission appears under as a Flashback.
|
|
14
|
+
*/
|
|
15
|
+
readonly levelRange?: LevelRangeData
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The name as it appears in the Flashback list. Leave undefined if the same as the base mission.
|
|
19
|
+
*/
|
|
20
|
+
readonly name?: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The character moralities that the mission will appear for in the Flashback list. Leave undefined if the same as the base mission.
|
|
24
|
+
*/
|
|
25
|
+
readonly morality?: MoralityExtended | MoralityExtended[]
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Freeform notes or tips about the Flashback version of the mission.
|
|
29
|
+
*/
|
|
30
|
+
readonly notes?: MarkdownString
|
|
31
|
+
}
|
package/src/main/api/morality.ts
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
|
+
import { Alignment } from './alignment'
|
|
2
|
+
|
|
1
3
|
export const MORALITY = ['hero', 'vigilante', 'villain', 'rogue', 'resistance', 'loyalist'] as const
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
+
export const MORALITY_EXTENDED = [
|
|
5
|
+
...MORALITY,
|
|
4
6
|
/**
|
|
5
7
|
* Any of the Primal Earth moralities - Hero, Vigilante, Villain, Rogue.
|
|
6
8
|
*/
|
|
7
|
-
|
|
9
|
+
'primal',
|
|
8
10
|
/**
|
|
9
11
|
* Either of the Praetorian Earth moralities - Resistance or Loyalist.
|
|
10
12
|
*/
|
|
11
|
-
|
|
13
|
+
'praetorian',
|
|
12
14
|
/**
|
|
13
15
|
* The moralities that roll up to the Hero {@link Alignment} - Hero and Vigilante.
|
|
14
16
|
*/
|
|
15
|
-
|
|
17
|
+
'heroic',
|
|
16
18
|
/**
|
|
17
19
|
* The moralities that roll up to the Villain {@link Alignment} - Villain and Rogue.
|
|
18
20
|
*/
|
|
19
|
-
|
|
21
|
+
'villainous',
|
|
20
22
|
/**
|
|
21
23
|
* Moralities with access to Paragon City - Hero, Vigilante and Rogue.
|
|
22
24
|
*/
|
|
23
|
-
|
|
25
|
+
'paragon-city-access',
|
|
24
26
|
/**
|
|
25
27
|
* Moralities with access to the Rogue Isles - Villain, Rogue and Vigilante.
|
|
26
28
|
*/
|
|
27
|
-
|
|
29
|
+
'rogue-isles-access',
|
|
28
30
|
/**
|
|
29
31
|
* All the moralities.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
'all',
|
|
34
|
+
] as const
|
|
35
|
+
export type Morality = typeof MORALITY[number]
|
|
36
|
+
export type MoralityExtended = typeof MORALITY_EXTENDED[number]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Maps a morality to the underlying alignment
|
|
40
|
+
*/
|
|
41
|
+
export const MoralityMap: Record<Morality | Alignment, Alignment> = {
|
|
42
|
+
hero: 'hero',
|
|
43
|
+
vigilante: 'hero',
|
|
44
|
+
villain: 'villain',
|
|
45
|
+
rogue: 'villain',
|
|
46
|
+
loyalist: 'praetorian',
|
|
47
|
+
resistance: 'praetorian',
|
|
48
|
+
praetorian: 'praetorian',
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Sex } from './sex'
|
|
2
|
+
import { Morality } from './morality'
|
|
3
|
+
import { Alignment } from './alignment'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* For badges, aspects like the name, icon, or badge text can vary depending on context, such as the alignment or sex of the character.
|
|
7
|
+
*/
|
|
8
|
+
export interface VariantContext {
|
|
9
|
+
readonly morality?: Morality | Alignment
|
|
10
|
+
readonly sex?: Sex
|
|
11
|
+
}
|
|
@@ -2,16 +2,16 @@ import { Sex } from './sex'
|
|
|
2
2
|
import { Alignment } from './alignment'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Some badge values differ based on the alignment or sex of
|
|
5
|
+
* Some badge values differ based on the alignment or sex of a character.
|
|
6
6
|
*/
|
|
7
|
-
export interface
|
|
7
|
+
export interface VariantData<V> {
|
|
8
8
|
/**
|
|
9
|
-
* The character alignment this
|
|
9
|
+
* The character alignment this variant applies to.
|
|
10
10
|
*/
|
|
11
11
|
readonly alignment?: Alignment
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* The character sex this
|
|
14
|
+
* The character sex this variant applies to.
|
|
15
15
|
*/
|
|
16
16
|
readonly sex?: Sex
|
|
17
17
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Link } from './link'
|
|
2
|
+
import { MoralityExtended } from './morality'
|
|
3
|
+
import { ZoneType } from './zone-type'
|
|
4
|
+
import { MarkdownString } from './markdown-string'
|
|
5
|
+
import { LevelRangeData } from './level-range-data'
|
|
2
6
|
|
|
3
7
|
export interface ZoneData {
|
|
4
8
|
/**
|
|
@@ -13,6 +17,26 @@ export interface ZoneData {
|
|
|
13
17
|
*/
|
|
14
18
|
readonly name: string
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The type of zone.
|
|
22
|
+
*/
|
|
23
|
+
readonly type: ZoneType
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The character moralities that this zone is accessible by.
|
|
27
|
+
*/
|
|
28
|
+
readonly morality?: MoralityExtended | MoralityExtended[]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The level range this zone is recommended for.
|
|
32
|
+
*/
|
|
33
|
+
readonly levelRange?: LevelRangeData
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Freeform notes or tips about the zone.
|
|
37
|
+
*/
|
|
38
|
+
readonly notes?: MarkdownString
|
|
39
|
+
|
|
16
40
|
/**
|
|
17
41
|
* List of external links. Wiki, forums, etc.
|
|
18
42
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const ZONE_TYPE = [
|
|
2
|
+
/**
|
|
3
|
+
* The standard zone type, even if not technically occurring in the 'City' proper.
|
|
4
|
+
*/
|
|
5
|
+
'city',
|
|
6
|
+
/**
|
|
7
|
+
* An Ouroboros flashback to a zone as it was in a previous era.
|
|
8
|
+
*/
|
|
9
|
+
'echo',
|
|
10
|
+
/**
|
|
11
|
+
* Tutorial zon, usually inaccessible after leaving.
|
|
12
|
+
*/
|
|
13
|
+
'tutorial',
|
|
14
|
+
/**
|
|
15
|
+
* Trial zones, like the Abandoned Sewers trial.
|
|
16
|
+
*/
|
|
17
|
+
'trial',
|
|
18
|
+
/**
|
|
19
|
+
* Hazard zones like the Hollows.
|
|
20
|
+
*/
|
|
21
|
+
'hazard',
|
|
22
|
+
/**
|
|
23
|
+
* Mayhem mission zones.
|
|
24
|
+
*/
|
|
25
|
+
'mayhem',
|
|
26
|
+
/**
|
|
27
|
+
* Safeguard mission zones.
|
|
28
|
+
*/
|
|
29
|
+
'safeguard',
|
|
30
|
+
/**
|
|
31
|
+
* Exists inside a mission not covered by the other types.
|
|
32
|
+
*/
|
|
33
|
+
'mission',
|
|
34
|
+
/**
|
|
35
|
+
* Incarnate trial zones.
|
|
36
|
+
*/
|
|
37
|
+
'incarnate',
|
|
38
|
+
/**
|
|
39
|
+
* Cooprative zones where Heroes and Villains can team up for PvE content.
|
|
40
|
+
*/
|
|
41
|
+
'co-op',
|
|
42
|
+
/**
|
|
43
|
+
* PvP zones like Bloody Bay.
|
|
44
|
+
*/
|
|
45
|
+
'pvp',
|
|
46
|
+
/**
|
|
47
|
+
* Located in an arena PvP map.
|
|
48
|
+
*/
|
|
49
|
+
'arena',
|
|
50
|
+
/**
|
|
51
|
+
* A building, usually contained within another zone, like the AE buildings.
|
|
52
|
+
*/
|
|
53
|
+
'building',
|
|
54
|
+
/**
|
|
55
|
+
* Stuff like the (Phone only) zone.
|
|
56
|
+
*/
|
|
57
|
+
'other',
|
|
58
|
+
] as const
|
|
59
|
+
export type ZoneType = typeof ZONE_TYPE[number]
|
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
type KeysOfType<T, V> = { [P in keyof T]: T[P] extends V ? P : never }[keyof T]
|
|
2
2
|
|
|
3
3
|
export class AbstractIndex<T> {
|
|
4
|
-
readonly #keyField: KeysOfType<T, string>
|
|
5
4
|
protected _values: T[] = []
|
|
6
5
|
protected _hashTable: Record<string, T> = {}
|
|
7
6
|
|
|
8
|
-
constructor(keyField: KeysOfType<T, string>) {
|
|
9
|
-
this.#keyField = keyField
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Return all indexed values
|
|
14
|
-
*/
|
|
15
|
-
get values(): T[] {
|
|
16
|
-
return this._values
|
|
17
|
-
}
|
|
18
|
-
|
|
19
7
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @param
|
|
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.
|
|
22
11
|
*/
|
|
23
|
-
|
|
12
|
+
constructor(keyField: KeysOfType<T, string>, values: T[] | undefined) {
|
|
24
13
|
this._values = values ?? []
|
|
25
14
|
this._hashTable = {}
|
|
26
15
|
for (const value of this.values) {
|
|
27
|
-
const key = value[
|
|
16
|
+
const key = value[keyField] as string
|
|
28
17
|
if (this._hashTable[key] !== undefined) throw new Error(`Duplicate key [${key}]`)
|
|
29
18
|
this._hashTable[key] = value
|
|
30
19
|
}
|
|
31
20
|
}
|
|
32
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Return all indexed values
|
|
24
|
+
*/
|
|
25
|
+
get values(): T[] {
|
|
26
|
+
return this._values
|
|
27
|
+
}
|
|
28
|
+
|
|
33
29
|
/**
|
|
34
30
|
* Get a value from the index
|
|
35
31
|
* @param key Key string
|
|
@@ -1,57 +1,83 @@
|
|
|
1
|
-
import { Badge,
|
|
1
|
+
import { Badge, compareByName, compareByReleaseDate, compareByZoneKey } from './badge'
|
|
2
2
|
import { BadgeSearchOptions } from './badge-search-options'
|
|
3
3
|
import { Paged } from './paged'
|
|
4
4
|
import { AbstractIndex } from './abstract-index'
|
|
5
5
|
|
|
6
6
|
export class BadgeIndex extends AbstractIndex<Badge> {
|
|
7
|
-
constructor() {
|
|
8
|
-
super('key')
|
|
7
|
+
constructor(values: Badge[] | undefined) {
|
|
8
|
+
super('key', values)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
search(options?: BadgeSearchOptions): Paged<Badge> {
|
|
12
|
-
const
|
|
12
|
+
const matched = (options?.query || options?.filter)
|
|
13
13
|
? this._values.filter(badge => this.#satisfiesQueryPredicate(badge, options?.query) && this.#satisfiesFilterPredicate(badge, options?.filter))
|
|
14
14
|
: this._values
|
|
15
15
|
|
|
16
|
-
const
|
|
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
|
|
16
|
+
const sorted = this.#sort(matched, options)
|
|
19
17
|
|
|
20
|
-
const
|
|
18
|
+
const totalPages = options?.pageSize ? Math.ceil(matched.length / (options?.pageSize)) : 1
|
|
19
|
+
const pageNumber = Math.max(1, Math.min(totalPages, options?.page ?? 1))
|
|
20
|
+
const items = options?.pageSize ? sorted.slice((pageNumber - 1) * options.pageSize, pageNumber * options?.pageSize) : sorted
|
|
21
21
|
|
|
22
22
|
return {
|
|
23
|
-
items:
|
|
24
|
-
|
|
23
|
+
items: items,
|
|
24
|
+
pageIndex: pageNumber - 1,
|
|
25
|
+
pageNumber: pageNumber,
|
|
25
26
|
pageSize: options?.pageSize,
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
matchedItemCount: matched.length,
|
|
28
|
+
totalItemCount: this._values.length,
|
|
29
|
+
totalPageCount: totalPages,
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
#satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
|
|
32
34
|
const queryString = query?.str?.toLowerCase() ?? ''
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|| (
|
|
38
|
-
|| (
|
|
35
|
+
const fields = query?.fields ? new Set(query?.fields) : new Set(['name']) // Default to name if not provided
|
|
36
|
+
if (fields.size === 0) return true
|
|
37
|
+
|
|
38
|
+
return !!((fields.has('name') && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
39
|
+
|| (fields.has('badge-text') && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
40
|
+
|| (fields.has('acquisition') && badge.acquisition?.toLowerCase().includes(queryString))
|
|
41
|
+
|| (fields.has('effect') && badge.effect?.toLowerCase().includes(queryString))
|
|
42
|
+
|| (fields.has('notes') && badge.notes?.toLowerCase().includes(queryString))
|
|
43
|
+
|| (fields.has('set-title-id') && (badge.setTitleId?.primal.toString() === queryString))
|
|
44
|
+
|| (fields.has('set-title-id') && (badge.setTitleId?.praetorian?.toString() === queryString))
|
|
45
|
+
)
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
#satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
|
|
42
49
|
return (!filter?.type || badge.type === filter.type)
|
|
43
50
|
&& (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
|
|
44
51
|
&& (!filter?.morality || badge.morality.has(filter.morality))
|
|
52
|
+
&& (!filter?.predicate || filter.predicate(badge))
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
#sort(badges: Badge[],
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
#sort(badges: Badge[], options?: BadgeSearchOptions): Badge[] {
|
|
56
|
+
switch (options?.sort) {
|
|
57
|
+
case 'name.asc': {
|
|
58
|
+
return badges.toSorted((a, b) => compareByName(a, b, options.context))
|
|
59
|
+
}
|
|
60
|
+
case 'name.desc': {
|
|
61
|
+
return badges.toSorted((a, b) => compareByName(b, a, options.context))
|
|
62
|
+
}
|
|
63
|
+
case 'zone-key.asc': {
|
|
64
|
+
return badges.toSorted(compareByZoneKey)
|
|
65
|
+
}
|
|
66
|
+
case 'zone-key.desc': {
|
|
67
|
+
return badges.toSorted((a, b) => compareByZoneKey(b, a))
|
|
68
|
+
}
|
|
69
|
+
case 'release-date.asc': {
|
|
70
|
+
return badges.toSorted(compareByReleaseDate)
|
|
71
|
+
}
|
|
72
|
+
case 'release-date.desc': {
|
|
73
|
+
return badges.toSorted((a, b) => compareByReleaseDate(b, a))
|
|
74
|
+
}
|
|
75
|
+
case 'canonical.desc': {
|
|
76
|
+
return badges.toReversed()
|
|
77
|
+
}
|
|
78
|
+
default: {
|
|
79
|
+
return [...badges]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
56
82
|
}
|
|
57
83
|
}
|
|
@@ -5,7 +5,7 @@ import { Key } from './key'
|
|
|
5
5
|
import { MarkdownString } from '../api/markdown-string'
|
|
6
6
|
import { Link } from '../api/link'
|
|
7
7
|
import { Location } from './location'
|
|
8
|
-
import { coalesceToArray } from '../util'
|
|
8
|
+
import { coalesceToArray } from '../util/coalesce-to-array'
|
|
9
9
|
|
|
10
10
|
export class BadgeRequirement {
|
|
11
11
|
/**
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { BadgeType } from '../api/badge-type'
|
|
2
|
-
import {
|
|
2
|
+
import { Morality } from '../api/morality'
|
|
3
|
+
import { Badge } from './badge'
|
|
4
|
+
import { VariantContext } from '../api/variant-context'
|
|
5
|
+
|
|
6
|
+
export type BadgeQueryableField = 'name' | 'badge-text' | 'acquisition' | 'notes' | 'effect' | 'set-title-id'
|
|
7
|
+
export type BadgeSort = `${'canonical' | 'name' | 'zone-key' | 'release-date'}.${'asc' | 'desc'}`
|
|
3
8
|
|
|
4
9
|
export interface BadgeSearchOptions {
|
|
5
10
|
|
|
@@ -10,14 +15,7 @@ export interface BadgeSearchOptions {
|
|
|
10
15
|
*/
|
|
11
16
|
query?: {
|
|
12
17
|
str?: string
|
|
13
|
-
|
|
14
|
-
name?: boolean
|
|
15
|
-
badgeText?: boolean
|
|
16
|
-
acquisition?: boolean
|
|
17
|
-
notes?: boolean
|
|
18
|
-
effect?: boolean
|
|
19
|
-
setTitle?: boolean
|
|
20
|
-
}
|
|
18
|
+
fields?: BadgeQueryableField[]
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
/**
|
|
@@ -26,18 +24,21 @@ export interface BadgeSearchOptions {
|
|
|
26
24
|
filter?: {
|
|
27
25
|
type?: BadgeType
|
|
28
26
|
zoneKey?: string
|
|
29
|
-
morality?:
|
|
27
|
+
morality?: Morality
|
|
28
|
+
predicate?: (badge: Badge) => boolean
|
|
30
29
|
}
|
|
31
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Adjust search results based on a given variant context (morality or sex of a character).
|
|
33
|
+
*/
|
|
34
|
+
context?: VariantContext
|
|
35
|
+
|
|
32
36
|
/**
|
|
33
37
|
* Sort results.
|
|
34
38
|
*
|
|
35
39
|
* Badges are assumed to be in canonical order in the content bundle, and should match the in-game display order.
|
|
36
40
|
*/
|
|
37
|
-
sort?:
|
|
38
|
-
by?: 'canonical' | 'badge-name' | 'zone-key'
|
|
39
|
-
dir?: 'asc' | 'desc'
|
|
40
|
-
}
|
|
41
|
+
sort?: BadgeSort
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* The page (1-based)
|
package/src/main/db/badge.ts
CHANGED
|
@@ -3,13 +3,17 @@ import { Link } from '../api/link'
|
|
|
3
3
|
import { BadgeData } from '../api/badge-data'
|
|
4
4
|
import { BadgeRequirement } from './badge-requirement'
|
|
5
5
|
import { Key } from './key'
|
|
6
|
-
import {
|
|
6
|
+
import { Variants } from './variants'
|
|
7
7
|
import { MarkdownString } from '../api/markdown-string'
|
|
8
|
-
import { coalesceToArray } from '../util'
|
|
9
8
|
import { MoralityList } from './morality-list'
|
|
9
|
+
import { AbstractIndex } from './abstract-index'
|
|
10
|
+
import { toDate } from '../util/to-date'
|
|
11
|
+
import { coalesceToArray } from '../util/coalesce-to-array'
|
|
12
|
+
import { SetTitleIds } from './set-title-ids'
|
|
13
|
+
import { VariantContext } from '../api/variant-context'
|
|
10
14
|
|
|
11
15
|
export class Badge {
|
|
12
|
-
readonly #requirementsIndex:
|
|
16
|
+
readonly #requirementsIndex: AbstractIndex<BadgeRequirement>
|
|
13
17
|
readonly #zoneKeys = new Set<string>()
|
|
14
18
|
|
|
15
19
|
/**
|
|
@@ -27,7 +31,12 @@ export class Badge {
|
|
|
27
31
|
*
|
|
28
32
|
* May vary by character sex or alignment.
|
|
29
33
|
*/
|
|
30
|
-
readonly name:
|
|
34
|
+
readonly name: Variants<string>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The date that the badge was added to the game.
|
|
38
|
+
*/
|
|
39
|
+
readonly releaseDate: Date
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* The character moralities that this badge is available to.
|
|
@@ -37,7 +46,7 @@ export class Badge {
|
|
|
37
46
|
/**
|
|
38
47
|
* The badge text as it appears in-game. May vary by character sex or alignment.
|
|
39
48
|
*/
|
|
40
|
-
readonly badgeText:
|
|
49
|
+
readonly badgeText: Variants<MarkdownString>
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
52
|
* Short description of how to acquire the badge. Detailed instructions will be in the notes field.
|
|
@@ -49,7 +58,7 @@ export class Badge {
|
|
|
49
58
|
*
|
|
50
59
|
* May vary by character sex or alignment.
|
|
51
60
|
*/
|
|
52
|
-
readonly icon:
|
|
61
|
+
readonly icon: Variants<string>
|
|
53
62
|
|
|
54
63
|
/**
|
|
55
64
|
* Freeform notes or tips about the badge.
|
|
@@ -65,18 +74,13 @@ export class Badge {
|
|
|
65
74
|
* The id used with the in-game `/settitle` command to apply the badge.
|
|
66
75
|
* The first value is the id for primal characters and the (optional) second number is the id for praetorian characters.
|
|
67
76
|
*/
|
|
68
|
-
readonly setTitleId?:
|
|
77
|
+
readonly setTitleId?: SetTitleIds
|
|
69
78
|
|
|
70
79
|
/**
|
|
71
80
|
* A description of the effect the badge will have, such as a buff or granting a temporary power.
|
|
72
81
|
*/
|
|
73
82
|
readonly effect?: MarkdownString
|
|
74
83
|
|
|
75
|
-
/**
|
|
76
|
-
* Represents the requirements for badges with multiple fulfillment steps, such as visiting monuments for history badges, completing missions, or collecting other badges.
|
|
77
|
-
*/
|
|
78
|
-
readonly requirements?: BadgeRequirement[]
|
|
79
|
-
|
|
80
84
|
/**
|
|
81
85
|
* 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.
|
|
82
86
|
*/
|
|
@@ -85,32 +89,36 @@ export class Badge {
|
|
|
85
89
|
constructor(badgeData: BadgeData) {
|
|
86
90
|
this.key = new Key(badgeData.key).value
|
|
87
91
|
this.type = badgeData.type
|
|
88
|
-
this.name = new
|
|
92
|
+
this.name = new Variants(badgeData.name)
|
|
93
|
+
this.releaseDate = toDate(badgeData.releaseDate)
|
|
89
94
|
this.morality = new MoralityList(coalesceToArray(badgeData.morality))
|
|
90
|
-
this.badgeText = new
|
|
95
|
+
this.badgeText = new Variants(badgeData.badgeText ?? [])
|
|
91
96
|
this.acquisition = badgeData.acquisition
|
|
92
|
-
this.icon = new
|
|
97
|
+
this.icon = new Variants(badgeData.icon ?? [])
|
|
93
98
|
this.notes = badgeData.notes
|
|
94
99
|
this.links = badgeData.links ?? []
|
|
95
100
|
this.effect = badgeData.effect
|
|
96
|
-
this.setTitleId = badgeData.setTitleId
|
|
101
|
+
this.setTitleId = badgeData.setTitleId ? new SetTitleIds(badgeData.setTitleId) : undefined
|
|
97
102
|
this.ignoreInTotals = badgeData.ignoreInTotals ?? false
|
|
98
103
|
|
|
99
|
-
this
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.#requirementsIndex[requirement.key] = requirement
|
|
104
|
+
this.#requirementsIndex = new AbstractIndex<BadgeRequirement>('key', badgeData.requirements?.map(x => new BadgeRequirement(x)))
|
|
105
|
+
|
|
106
|
+
for (const requirement of this.#requirementsIndex.values) {
|
|
103
107
|
if (requirement.location) for (const location of requirement.location) {
|
|
104
108
|
if (location.zoneKey) this.#zoneKeys.add(location.zoneKey)
|
|
105
109
|
}
|
|
106
|
-
|
|
107
|
-
})
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Represents the requirements for badges with multiple fulfillment steps, such as visiting monuments for history badges, completing missions, or collecting other badges.
|
|
115
|
+
*/
|
|
116
|
+
get requirements(): BadgeRequirement[] {
|
|
117
|
+
return this.#requirementsIndex.values
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getRequirement(key: string): BadgeRequirement | undefined {
|
|
121
|
+
return this.#requirementsIndex.get(key)
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
@@ -128,9 +136,9 @@ export class Badge {
|
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
export function
|
|
132
|
-
const aName = a?.name
|
|
133
|
-
const bName = b?.name
|
|
139
|
+
export function compareByName(a?: Badge, b?: Badge, context?: VariantContext): number {
|
|
140
|
+
const aName = a?.name?.getValue(context)
|
|
141
|
+
const bName = b?.name?.getValue(context)
|
|
134
142
|
if (!aName && !bName) return 0
|
|
135
143
|
if (!aName) return 1
|
|
136
144
|
if (!bName) return -1
|
|
@@ -145,3 +153,12 @@ export function compareByZoneKey(a?: Badge, b?: Badge): number {
|
|
|
145
153
|
if (!bZone) return -1
|
|
146
154
|
return aZone.localeCompare(bZone)
|
|
147
155
|
}
|
|
156
|
+
|
|
157
|
+
export function compareByReleaseDate(a?: Badge, b?: Badge): number {
|
|
158
|
+
const aReleaseDate = a?.releaseDate?.getTime()
|
|
159
|
+
const bReleaseDate = b?.releaseDate?.getTime()
|
|
160
|
+
if (aReleaseDate === bReleaseDate) return 0
|
|
161
|
+
if (!aReleaseDate) return 1
|
|
162
|
+
if (!bReleaseDate) return -1
|
|
163
|
+
return aReleaseDate < bReleaseDate ? -1 : 1
|
|
164
|
+
}
|