coh-content-db 2.0.0-rc.4 → 2.0.0-rc.6
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/README.md +31 -7
- package/dist/coh-content-db.d.ts +353 -185
- package/dist/coh-content-db.js +460 -300
- package/dist/coh-content-db.js.map +1 -1
- package/dist/coh-content-db.mjs +448 -294
- package/dist/coh-content-db.mjs.map +1 -1
- package/package.json +1 -4
- package/src/main/api/alternate-data.ts +2 -2
- package/src/main/api/badge-data.ts +21 -19
- package/src/main/api/badge-requirement-data.ts +82 -0
- package/src/main/api/badge-requirement-type.ts +11 -0
- package/src/main/api/change.ts +5 -2
- package/src/main/api/contact-data.ts +46 -0
- package/src/main/api/content-bundle.ts +12 -7
- package/src/main/api/markdown-string.ts +4 -0
- package/src/main/api/zone-data.ts +20 -0
- package/src/main/changelog.ts +7 -2
- package/src/main/db/alignments.ts +17 -0
- package/src/main/db/alternates.ts +8 -14
- package/src/main/db/badge-index.ts +93 -0
- package/src/main/db/badge-requirement.ts +102 -0
- package/src/main/db/badge-search-options.ts +51 -0
- package/src/main/db/badge.ts +55 -48
- package/src/main/db/bundle-metadata.ts +5 -6
- package/src/main/db/coh-content-database.ts +65 -40
- package/src/main/db/contact.ts +59 -0
- package/src/main/db/paged.ts +7 -0
- package/src/main/db/zone.ts +28 -0
- package/src/main/index.ts +15 -11
- package/src/main/util.ts +68 -7
- package/src/test/api/alignments.test.ts +40 -0
- package/src/test/api/badge-data.fixture.ts +9 -7
- package/src/test/api/badge-requirement-data.fixture.ts +17 -0
- package/src/test/api/badge-requirement-type.test.ts +31 -0
- package/src/test/api/contact-data.fixture.ts +13 -0
- package/src/test/api/content-bundle.fixture.ts +2 -2
- package/src/test/api/content-bundle.test.ts +1 -1
- package/src/test/api/zone-data.fixture.ts +8 -0
- package/src/test/db/alternates.test.ts +16 -74
- package/src/test/db/badge-index.test.ts +520 -0
- package/src/test/db/badge-requirement.test.ts +180 -0
- package/src/test/db/badge.test.ts +190 -15
- package/src/test/db/coh-content-database.test.ts +125 -18
- package/src/test/db/contact.test.ts +96 -0
- package/src/test/db/zone.test.ts +36 -0
- package/src/test/index.test.ts +6 -2
- package/src/test/util.test.ts +91 -18
- package/src/main/api/badge-partial-data.ts +0 -65
- package/src/main/api/badge-partial-type.ts +0 -8
- package/src/main/api/game-map-data.ts +0 -26
- package/src/main/api/vidiot-map-data.ts +0 -18
- package/src/main/api/vidiot-map-point-of-interest-data.ts +0 -30
- package/src/main/db/badge-partial.ts +0 -35
- package/src/main/db/badge-search-document.ts +0 -16
- package/src/main/db/game-map.ts +0 -33
- package/src/main/db/vidiot-map-point-of-interest.ts +0 -40
- package/src/main/db/vidiot-map.ts +0 -25
- package/src/test/api/badge-partial-data.fixture.ts +0 -17
- package/src/test/api/badge-partial-type.test.ts +0 -31
- package/src/test/api/game-map-data.fixture.ts +0 -10
- package/src/test/api/vidiot-map-point-of-interest.fixture.ts +0 -10
- package/src/test/api/vidiot-map.fixture.ts +0 -9
- package/src/test/db/badge-search-document.test.ts +0 -35
- package/src/test/db/coh-content-database-search.test.ts +0 -119
|
@@ -13,7 +13,7 @@ export class Alternates<T> {
|
|
|
13
13
|
this.#sortedValues.sort((a, b) => this.#compareAlternates(a, b))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
getValue(alignment?: Alignment
|
|
16
|
+
getValue(alignment?: Alignment, sex?: Sex): T | undefined {
|
|
17
17
|
for (let index = this.#sortedValues.length; index--;) {
|
|
18
18
|
const entry = this.#sortedValues[index]
|
|
19
19
|
if ((entry.alignment === undefined || entry.alignment === alignment)
|
|
@@ -21,14 +21,14 @@ export class Alternates<T> {
|
|
|
21
21
|
) return entry.value
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
return this.default
|
|
24
|
+
return this.default?.value
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Get the default value for this list of alternates, the value with the highest priority and lowest specificity.
|
|
29
29
|
*/
|
|
30
|
-
get default(): T | undefined {
|
|
31
|
-
return this.#sortedValues[0]
|
|
30
|
+
get default(): AlternateData<T> | undefined {
|
|
31
|
+
return this.#sortedValues[0]
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -60,7 +60,7 @@ export class Alternates<T> {
|
|
|
60
60
|
return String(a.value).localeCompare(String(b.value))
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
#compareAlignment(a
|
|
63
|
+
#compareAlignment(a?: Alignment, b?: Alignment): number {
|
|
64
64
|
if (a === b) return 0
|
|
65
65
|
if (a === undefined && b !== undefined) return -1
|
|
66
66
|
if (b === undefined && a !== undefined) return 1
|
|
@@ -68,13 +68,10 @@ export class Alternates<T> {
|
|
|
68
68
|
const aSort = a === undefined ? -1 : ALIGNMENT_SORT[a] ?? -1 // Unknown values get -1 priority
|
|
69
69
|
const bSort = b === undefined ? -1 : ALIGNMENT_SORT[b] ?? -1
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Unknown values (not in ALIGNMENT_SORT) are sorted alphabetically
|
|
74
|
-
return a?.localeCompare(b ?? '') ?? 0
|
|
71
|
+
return bSort - aSort
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
#compareSex(a?: Sex
|
|
74
|
+
#compareSex(a?: Sex, b?: Sex): number {
|
|
78
75
|
if (a === b) return 0
|
|
79
76
|
if (a === undefined && b !== undefined) return -1
|
|
80
77
|
if (b === undefined && a !== undefined) return 1
|
|
@@ -82,9 +79,6 @@ export class Alternates<T> {
|
|
|
82
79
|
const aSort = SEX_SORT[a ?? -1] ?? -1 // Unknown values get -1 priority
|
|
83
80
|
const bSort = SEX_SORT[b ?? -1] ?? -1
|
|
84
81
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// Unknown values (not in SEX_SORT) are sorted alphabetically
|
|
88
|
-
return a?.localeCompare(b ?? '') ?? 0
|
|
82
|
+
return bSort - aSort
|
|
89
83
|
}
|
|
90
84
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Badge } from './badge'
|
|
2
|
+
import { BadgeSearchOptions } from './badge-search-options'
|
|
3
|
+
import { Zone } from './zone'
|
|
4
|
+
import { Paged } from './paged'
|
|
5
|
+
|
|
6
|
+
export class BadgeIndex {
|
|
7
|
+
readonly #badges: Badge[] = []
|
|
8
|
+
readonly #badgeIndex: Record<string, Badge> = {}
|
|
9
|
+
|
|
10
|
+
readonly #zoneOrder: Record<string, number> = {}
|
|
11
|
+
|
|
12
|
+
constructor(badges: Badge[], zones?: Zone[]) {
|
|
13
|
+
this.#zoneOrder = Object.fromEntries(
|
|
14
|
+
zones
|
|
15
|
+
?.sort((a, b) => a.name.localeCompare(b.name))
|
|
16
|
+
?.map((x, index) => [x.key, index]) ?? [],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
this.#badges = badges
|
|
20
|
+
for (const badge of badges) {
|
|
21
|
+
if (this.#badgeIndex[badge.key] !== undefined) throw new Error(`Duplicate badge key [${badge.key}]`)
|
|
22
|
+
this.#badgeIndex[badge.key] = badge
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getBadge(key: string): Badge {
|
|
27
|
+
const result = this.#badgeIndex[key]
|
|
28
|
+
if (result === undefined) throw new Error(`Unknown badge key [${key}]`)
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
badgeExists(key: string): boolean {
|
|
33
|
+
return !!this.#badgeIndex[key]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
searchBadges(options?: BadgeSearchOptions): Paged<Badge> {
|
|
37
|
+
const filtered = (options?.query || options?.filter)
|
|
38
|
+
? this.#badges.filter(badge => this.#satisfiesQueryPredicate(badge, options?.query) && this.#satisfiesFilterPredicate(badge, options?.filter))
|
|
39
|
+
: this.#badges
|
|
40
|
+
|
|
41
|
+
const totalPages = options?.pageSize ? Math.ceil(filtered.length / (options?.pageSize)) : 1
|
|
42
|
+
const page = Math.max(1, Math.min(totalPages, options?.page ?? 1))
|
|
43
|
+
const paged = options?.pageSize ? filtered.slice((page - 1) * options.pageSize, page * options?.pageSize) : filtered
|
|
44
|
+
|
|
45
|
+
const sorted = this.#sort(paged, options?.sort)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
items: sorted,
|
|
49
|
+
page: page,
|
|
50
|
+
pageSize: options?.pageSize,
|
|
51
|
+
totalItems: filtered.length,
|
|
52
|
+
totalPages: totalPages,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#satisfiesQueryPredicate(badge: Badge, query?: BadgeSearchOptions['query']): boolean {
|
|
57
|
+
const queryString = query?.str?.toLowerCase() ?? ''
|
|
58
|
+
return !!(((query?.on?.name ?? true) && badge.name.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
59
|
+
|| (query?.on?.badgeText && badge.badgeText.canonical.some(x => x.value.toLowerCase().includes(queryString)))
|
|
60
|
+
|| (query?.on?.acquisition && badge.acquisition?.toLowerCase().includes(queryString))
|
|
61
|
+
|| (query?.on?.effect && badge.effect?.toLowerCase().includes(queryString))
|
|
62
|
+
|| (query?.on?.notes && badge.notes?.toLowerCase().includes(queryString))
|
|
63
|
+
|| (query?.on?.setTitle && (badge.setTitle?.id?.toString().includes(queryString) || badge.setTitle?.praetorianId?.toString().includes(queryString))))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#satisfiesFilterPredicate(badge: Badge, filter?: BadgeSearchOptions['filter']): boolean {
|
|
67
|
+
return (!filter?.type || badge.type === filter.type)
|
|
68
|
+
&& (!filter?.zoneKey || badge.zoneKey === filter.zoneKey)
|
|
69
|
+
&& (!filter?.alignment || badge.alignment.items.includes(filter.alignment))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#sort(badges: Badge[], sort?: BadgeSearchOptions['sort']): Badge[] {
|
|
73
|
+
if (!sort) return badges
|
|
74
|
+
const ascending = sort.dir !== 'DESC'
|
|
75
|
+
|
|
76
|
+
if (!sort.by || sort.by === 'CANONICAL') return sort.dir === 'DESC' ? badges.reverse() : badges
|
|
77
|
+
|
|
78
|
+
if (sort.by === 'BADGE_NAME') return ascending
|
|
79
|
+
? badges.sort((a, b) => a.name.default?.value.localeCompare(b.name.default?.value ?? '') ?? 0)
|
|
80
|
+
: badges.sort((a, b) => b.name.default?.value.localeCompare(a.name.default?.value ?? '') ?? 0)
|
|
81
|
+
|
|
82
|
+
return badges.sort((a, b) => {
|
|
83
|
+
const aIndex = this.#zoneOrder[a.zoneKey ?? '']
|
|
84
|
+
const bIndex = this.#zoneOrder[b.zoneKey ?? '']
|
|
85
|
+
|
|
86
|
+
if (aIndex === bIndex) return 0
|
|
87
|
+
if (aIndex === undefined) return ascending ? 1 : -1
|
|
88
|
+
if (bIndex === undefined) return ascending ? -1 : 1
|
|
89
|
+
|
|
90
|
+
return ascending ? aIndex - bIndex : bIndex - aIndex
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { BadgeRequirementData } from '../api/badge-requirement-data'
|
|
2
|
+
import { PlaqueType } from '../api/plaque-type'
|
|
3
|
+
import { BadgeRequirementType } from '../api/badge-requirement-type'
|
|
4
|
+
import { EnhancementCategory } from '../api/enhancement-category'
|
|
5
|
+
import { Key } from './key'
|
|
6
|
+
import { MarkdownString } from '../api/markdown-string'
|
|
7
|
+
import { Link } from '../api/link'
|
|
8
|
+
|
|
9
|
+
export class BadgeRequirement {
|
|
10
|
+
/**
|
|
11
|
+
* Key.
|
|
12
|
+
*/
|
|
13
|
+
readonly key: string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type of requirement.
|
|
17
|
+
*/
|
|
18
|
+
readonly type: BadgeRequirementType
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Zone the requirement is located in.
|
|
22
|
+
*/
|
|
23
|
+
readonly zoneKey?: string
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* /loc coordinates.
|
|
27
|
+
*/
|
|
28
|
+
readonly loc?: number[]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Is it a wall plaque or a physical monument?
|
|
32
|
+
*/
|
|
33
|
+
readonly plaqueType?: PlaqueType
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plaque inscription.
|
|
37
|
+
*/
|
|
38
|
+
readonly plaqueInscription?: string
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The number or letter the plaque appears as on Vidiot Maps.
|
|
42
|
+
*/
|
|
43
|
+
readonly vidiotMapKey?: string
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The key of the badge for this requirement.
|
|
47
|
+
*/
|
|
48
|
+
readonly badgeKey?: string
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Mission name.
|
|
52
|
+
*/
|
|
53
|
+
readonly missionName?: string
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* {@link Contact} key for the story arc.
|
|
57
|
+
*/
|
|
58
|
+
readonly contactKey?: string
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Level of the invention required.
|
|
62
|
+
*/
|
|
63
|
+
readonly inventionLevel?: number
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The types of enhancements required to be crafted.
|
|
67
|
+
*/
|
|
68
|
+
readonly inventionTypes?: EnhancementCategory[]
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Number of invention crafts required.
|
|
72
|
+
*/
|
|
73
|
+
readonly inventionCount?: number
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Any additional notes.
|
|
77
|
+
*/
|
|
78
|
+
readonly notes?: MarkdownString
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* List of external links. Wiki, forums, etc.
|
|
82
|
+
*/
|
|
83
|
+
readonly links: Link[]
|
|
84
|
+
|
|
85
|
+
constructor(data: BadgeRequirementData) {
|
|
86
|
+
this.key = new Key(data.key).value
|
|
87
|
+
this.type = data.type
|
|
88
|
+
this.zoneKey = data.zoneKey
|
|
89
|
+
this.loc = data.loc
|
|
90
|
+
this.plaqueType = data.plaqueType
|
|
91
|
+
this.plaqueInscription = data.plaqueInscription
|
|
92
|
+
this.vidiotMapKey = data.vidiotMapKey
|
|
93
|
+
this.badgeKey = data.badgeKey
|
|
94
|
+
this.missionName = data.missionName
|
|
95
|
+
this.contactKey = data.contactKey
|
|
96
|
+
this.inventionLevel = data.inventionLevel
|
|
97
|
+
this.inventionTypes = data.inventionTypes
|
|
98
|
+
this.inventionCount = data.inventionCount
|
|
99
|
+
this.notes = data.notes
|
|
100
|
+
this.links = data.links ?? []
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BadgeType } from '../api/badge-type'
|
|
2
|
+
import { Alignment } from '../api/alignment'
|
|
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
|
+
alignment?: Alignment
|
|
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_NAME'
|
|
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
|
+
}
|
package/src/main/db/badge.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { BadgeType } from '../api/badge-type'
|
|
2
|
-
import { Alignment } from '../api/alignment'
|
|
3
2
|
import { Link } from '../api/link'
|
|
4
3
|
import { BadgeData } from '../api/badge-data'
|
|
5
|
-
import {
|
|
4
|
+
import { BadgeRequirement } from './badge-requirement'
|
|
6
5
|
import { Key } from './key'
|
|
7
6
|
import { Alternates } from './alternates'
|
|
7
|
+
import { Alignments } from './alignments'
|
|
8
|
+
import { MarkdownString } from '../api/markdown-string'
|
|
8
9
|
|
|
9
10
|
export class Badge {
|
|
10
|
-
readonly #
|
|
11
|
+
readonly #requirementsIndex: Record<string, BadgeRequirement> = {}
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The database key for this badge.
|
|
@@ -17,7 +18,7 @@ export class Badge {
|
|
|
17
18
|
/**
|
|
18
19
|
* The type of badge.
|
|
19
20
|
*/
|
|
20
|
-
readonly type: BadgeType
|
|
21
|
+
readonly type: BadgeType
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* The name of this badge.
|
|
@@ -29,7 +30,7 @@ export class Badge {
|
|
|
29
30
|
/**
|
|
30
31
|
* The character alignments that this badge is available to.
|
|
31
32
|
*/
|
|
32
|
-
readonly alignment:
|
|
33
|
+
readonly alignment: Alignments
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* The badge text as it appears in-game. May vary by character sex or alignment.
|
|
@@ -37,11 +38,9 @@ export class Badge {
|
|
|
37
38
|
readonly badgeText: Alternates<string>
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Supports {@link https://www.markdownguide.org/|Markdown} format.
|
|
41
|
+
* Short description of how to acquire the badge. Detailed instructions will be in the notes field.
|
|
43
42
|
*/
|
|
44
|
-
readonly acquisition?:
|
|
43
|
+
readonly acquisition?: MarkdownString
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Absolute URL to this badge's icon.
|
|
@@ -52,28 +51,26 @@ export class Badge {
|
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
53
|
* Freeform notes or tips about the badge.
|
|
55
|
-
*
|
|
56
|
-
* Supports {@link https://www.markdownguide.org/|Markdown} format.
|
|
57
54
|
*/
|
|
58
|
-
readonly notes?:
|
|
55
|
+
readonly notes?: MarkdownString
|
|
59
56
|
|
|
60
57
|
/**
|
|
61
|
-
* List of external links
|
|
58
|
+
* List of external links. Wiki, forums, etc.
|
|
62
59
|
*/
|
|
63
|
-
readonly links
|
|
60
|
+
readonly links: Link[]
|
|
64
61
|
|
|
65
62
|
/**
|
|
66
|
-
* For exploration badges, the key of the {@link
|
|
63
|
+
* For exploration badges, the key of the {@link Zone} that this badge is found on.
|
|
67
64
|
*/
|
|
68
|
-
readonly
|
|
65
|
+
readonly zoneKey?: string
|
|
69
66
|
|
|
70
67
|
/**
|
|
71
|
-
* For exploration badges, the `/loc` coordinates of the badge
|
|
68
|
+
* For exploration badges, the `/loc` coordinates of the badge.
|
|
72
69
|
*/
|
|
73
70
|
readonly loc?: [number, number, number]
|
|
74
71
|
|
|
75
72
|
/**
|
|
76
|
-
* For
|
|
73
|
+
* For plaques that appear on a Vidiot Map, the number or letter the badge appears as.
|
|
77
74
|
*/
|
|
78
75
|
readonly vidiotMapKey?: string
|
|
79
76
|
|
|
@@ -93,49 +90,59 @@ export class Badge {
|
|
|
93
90
|
|
|
94
91
|
/**
|
|
95
92
|
* A description of the effect the badge will have, such as a buff or granting a temporary power.
|
|
96
|
-
*
|
|
97
|
-
* Supports {@link https://www.markdownguide.org/|Markdown} format.
|
|
98
93
|
*/
|
|
99
|
-
readonly effect?:
|
|
94
|
+
readonly effect?: MarkdownString
|
|
100
95
|
|
|
101
96
|
/**
|
|
102
|
-
*
|
|
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.
|
|
103
105
|
*/
|
|
104
|
-
readonly
|
|
106
|
+
readonly requirements?: BadgeRequirement[][]
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
109
|
* 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.
|
|
108
110
|
*/
|
|
109
111
|
readonly ignoreInTotals: boolean
|
|
110
112
|
|
|
111
|
-
constructor(
|
|
112
|
-
this.key = new Key(
|
|
113
|
-
this.type =
|
|
114
|
-
this.name = new Alternates(
|
|
115
|
-
this.alignment =
|
|
116
|
-
this.badgeText = new Alternates(
|
|
117
|
-
this.acquisition =
|
|
118
|
-
this.icon = new Alternates(
|
|
119
|
-
this.notes =
|
|
120
|
-
this.links =
|
|
121
|
-
this.
|
|
122
|
-
this.loc =
|
|
123
|
-
this.effect =
|
|
124
|
-
this.vidiotMapKey =
|
|
125
|
-
this.setTitle =
|
|
126
|
-
this.ignoreInTotals =
|
|
127
|
-
|
|
128
|
-
this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
constructor(badgeData: BadgeData) {
|
|
114
|
+
this.key = new Key(badgeData.key).value
|
|
115
|
+
this.type = badgeData.type
|
|
116
|
+
this.name = new Alternates(badgeData.name)
|
|
117
|
+
this.alignment = new Alignments(badgeData.alignment)
|
|
118
|
+
this.badgeText = new Alternates(badgeData.badgeText ?? [])
|
|
119
|
+
this.acquisition = badgeData.acquisition
|
|
120
|
+
this.icon = new Alternates(badgeData.icon ?? [])
|
|
121
|
+
this.notes = badgeData.notes
|
|
122
|
+
this.links = badgeData.links ?? []
|
|
123
|
+
this.zoneKey = badgeData.zoneKey
|
|
124
|
+
this.loc = badgeData.loc
|
|
125
|
+
this.effect = badgeData.effect
|
|
126
|
+
this.vidiotMapKey = badgeData.vidiotMapKey
|
|
127
|
+
this.setTitle = badgeData.setTitle
|
|
128
|
+
this.ignoreInTotals = badgeData.ignoreInTotals ?? false
|
|
129
|
+
|
|
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
|
+
})
|
|
133
140
|
})
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
const result = this.#
|
|
138
|
-
if (result === undefined) throw new Error(`Unknown badge
|
|
143
|
+
getRequirement(key: string): BadgeRequirement {
|
|
144
|
+
const result = this.#requirementsIndex[key]
|
|
145
|
+
if (result === undefined) throw new Error(`Unknown badge requirement key [${key}]`)
|
|
139
146
|
return result
|
|
140
147
|
}
|
|
141
148
|
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { ContentBundle } from '../api/content-bundle'
|
|
2
2
|
import { Change } from '../api/change'
|
|
3
3
|
import { Link } from '../api/link'
|
|
4
|
+
import { MarkdownString } from '../api/markdown-string'
|
|
4
5
|
|
|
5
6
|
export class BundleMetadata {
|
|
6
7
|
/**
|
|
7
|
-
* Name of the
|
|
8
|
+
* Name of the content bundle.
|
|
8
9
|
*/
|
|
9
10
|
readonly name: string
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Description of the
|
|
13
|
-
*
|
|
14
|
-
* Supports {@link https://www.markdownguide.org/|Markdown} format.
|
|
13
|
+
* Description of the fork.
|
|
15
14
|
*/
|
|
16
|
-
readonly description?:
|
|
15
|
+
readonly description?: MarkdownString
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Repository where the db content package is maintained.
|
|
@@ -21,7 +20,7 @@ export class BundleMetadata {
|
|
|
21
20
|
readonly repository?: string
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
|
-
* List of external links
|
|
23
|
+
* List of external links. Wiki, forums, etc.
|
|
25
24
|
*/
|
|
26
25
|
readonly links: Link[]
|
|
27
26
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { ContentBundle } from '../api/content-bundle'
|
|
2
2
|
import { Archetype } from './archetype'
|
|
3
|
-
import {
|
|
3
|
+
import { Zone } from './zone'
|
|
4
4
|
import { Badge } from './badge'
|
|
5
5
|
import { BundleMetadata } from './bundle-metadata'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import { BadgeIndex } from './badge-index'
|
|
7
|
+
import { BadgeSearchOptions } from './badge-search-options'
|
|
8
|
+
import { Paged } from './paged'
|
|
9
|
+
import { Contact } from './contact'
|
|
8
10
|
|
|
9
11
|
export class CohContentDatabase {
|
|
10
12
|
readonly #archetypeIndex: Record<string, Archetype> = {}
|
|
11
|
-
readonly #
|
|
12
|
-
readonly #
|
|
13
|
-
readonly #
|
|
13
|
+
readonly #zoneIndex: Record<string, Zone> = {}
|
|
14
|
+
readonly #contactIndex: Record<string, Contact> = {}
|
|
15
|
+
readonly #badgeIndex: BadgeIndex
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Metadata about the content bundle.
|
|
@@ -18,23 +20,29 @@ export class CohContentDatabase {
|
|
|
18
20
|
readonly metadata: BundleMetadata
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* List of the game server names
|
|
23
|
+
* List of the game server names.
|
|
24
|
+
*
|
|
22
25
|
* Torchbearer, Excelsior, etc.
|
|
23
26
|
*/
|
|
24
27
|
readonly servers: string[]
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
|
-
* List of archetypes
|
|
30
|
+
* List of archetypes.
|
|
28
31
|
*/
|
|
29
32
|
readonly archetypes: Archetype[]
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
|
-
* List of game
|
|
35
|
+
* List of game zones.
|
|
33
36
|
*/
|
|
34
|
-
readonly
|
|
37
|
+
readonly zones: Zone[]
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
* List of
|
|
40
|
+
* List of contacts.
|
|
41
|
+
*/
|
|
42
|
+
readonly contacts: Contact[]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* List of badges.
|
|
38
46
|
*/
|
|
39
47
|
readonly badges: Badge[]
|
|
40
48
|
|
|
@@ -45,56 +53,73 @@ export class CohContentDatabase {
|
|
|
45
53
|
constructor(bundle: ContentBundle) {
|
|
46
54
|
this.metadata = new BundleMetadata(bundle)
|
|
47
55
|
this.servers = bundle.servers ?? []
|
|
56
|
+
|
|
48
57
|
this.archetypes = bundle.archetypes?.map((data) => {
|
|
49
|
-
if (this.#archetypeIndex[data.key] !== undefined) throw new Error(`Duplicate archetype key
|
|
58
|
+
if (this.#archetypeIndex[data.key] !== undefined) throw new Error(`Duplicate archetype key '${data.key}'`)
|
|
50
59
|
const archetype = new Archetype(data)
|
|
51
60
|
this.#archetypeIndex[archetype.key] = archetype
|
|
52
61
|
return archetype
|
|
53
62
|
}) ?? []
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
|
|
64
|
+
this.zones = bundle.zones?.map((data) => {
|
|
65
|
+
if (this.#zoneIndex[data.key] !== undefined) throw new Error(`Duplicate zone key '${data.key}'`)
|
|
66
|
+
const zone = new Zone(data)
|
|
67
|
+
this.#zoneIndex[zone.key] = zone
|
|
68
|
+
return zone
|
|
59
69
|
}) ?? []
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
|
|
71
|
+
this.contacts = bundle.contacts?.map((data) => {
|
|
72
|
+
if (this.#contactIndex[data.key] !== undefined) throw new Error(`Duplicate contact key '${data.key}'`)
|
|
73
|
+
const contact = new Contact(data)
|
|
74
|
+
this.#contactIndex[contact.key] = contact
|
|
75
|
+
return contact
|
|
65
76
|
}) ?? []
|
|
66
77
|
|
|
67
|
-
this
|
|
68
|
-
|
|
69
|
-
storeFields: ['key'],
|
|
70
|
-
})
|
|
71
|
-
for (const badge of this.badges) {
|
|
72
|
-
this.#badgeSearch.add(new BadgeSearchDocument(badge))
|
|
73
|
-
}
|
|
78
|
+
this.badges = bundle.badges?.map(data => new Badge(data)) ?? []
|
|
79
|
+
this.#badgeIndex = new BadgeIndex(this.badges, this.zones)
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
getArchetype(key: string): Archetype {
|
|
77
83
|
const result = this.#archetypeIndex[key]
|
|
78
|
-
if (result === undefined) throw new Error(`Unknown archetype key
|
|
84
|
+
if (result === undefined) throw new Error(`Unknown archetype key '${key}'`)
|
|
79
85
|
return result
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
const result = this.#
|
|
84
|
-
if (result === undefined) throw new Error(`Unknown
|
|
88
|
+
getZone(key: string): Zone {
|
|
89
|
+
const result = this.#zoneIndex[key]
|
|
90
|
+
if (result === undefined) throw new Error(`Unknown zone key '${key}'`)
|
|
85
91
|
return result
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
const result = this.#
|
|
90
|
-
if (result === undefined) throw new Error(`Unknown
|
|
94
|
+
getContact(key: string): Contact {
|
|
95
|
+
const result = this.#contactIndex[key]
|
|
96
|
+
if (result === undefined) throw new Error(`Unknown contact key '${key}'`)
|
|
91
97
|
return result
|
|
92
98
|
}
|
|
93
99
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
zoneExists(key: string): boolean {
|
|
101
|
+
return !!this.#zoneIndex[key]
|
|
102
|
+
}
|
|
97
103
|
|
|
98
|
-
|
|
104
|
+
contactExists(key: string): boolean {
|
|
105
|
+
return !!this.#contactIndex[key]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getBadge(key: string): Badge {
|
|
109
|
+
return this.#badgeIndex.getBadge(key)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
badgeExists(key: string): boolean {
|
|
113
|
+
return this.#badgeIndex.badgeExists(key)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Search, sort and filter the badge list.
|
|
118
|
+
* This is a fairly brute-forced approach and will not be as performant as loading the badge data into a traditional
|
|
119
|
+
* database engine, but is sufficient for most operations.
|
|
120
|
+
* @param options {@link BadgeSearchOptions}
|
|
121
|
+
*/
|
|
122
|
+
searchBadges(options?: BadgeSearchOptions): Paged<Badge> {
|
|
123
|
+
return this.#badgeIndex.searchBadges(options)
|
|
99
124
|
}
|
|
100
125
|
}
|