xpt-shared-types 1.20.0 → 1.25.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/dist/generated/enums.d.ts +2 -0
- package/dist/generated/inputs.d.ts +28 -1
- package/dist/generated/models.d.ts +28 -1
- package/dist/manual/experience.d.ts +52 -0
- package/dist/manual/experience.js +104 -0
- package/dist/manual/index.d.ts +3 -0
- package/dist/manual/index.js +3 -0
- package/dist/manual/tournamentTemplate.d.ts +177 -0
- package/dist/manual/tournamentTemplate.js +215 -0
- package/dist/manual/userUiState.d.ts +29 -0
- package/dist/manual/userUiState.js +7 -0
- package/package.json +1 -1
- package/src/generated/enums.ts +8 -0
- package/src/generated/inputs.ts +29 -0
- package/src/generated/models.ts +29 -0
- package/src/manual/experience.ts +107 -0
- package/src/manual/index.ts +3 -0
- package/src/manual/tournamentTemplate.ts +326 -0
- package/src/manual/userUiState.ts +31 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tournament templates — the contract between `utils/tournamentTemplate.ts`
|
|
4
|
+
* in xpt-strapi and the create dialog's setup step, the post-publish
|
|
5
|
+
* "Save as template" prompt and the Settings → Templates page in xpt-client.
|
|
6
|
+
*
|
|
7
|
+
* A template holds settings only: never entries, dates or results. A null
|
|
8
|
+
* setting means the host chose not to save it and the XPT default applies
|
|
9
|
+
* (`resolveSetup`). Alongside the host's templates, the API derives their
|
|
10
|
+
* **last tournament per game** — the newest one taken out of draft in the
|
|
11
|
+
* last `LAST_RUN_WINDOW_DAYS` — so a host can repeat a night without ever
|
|
12
|
+
* having saved anything.
|
|
13
|
+
*
|
|
14
|
+
* What a setup carries is close to the whole publish checklist: the catalogue
|
|
15
|
+
* choice (platform and account network), the region, the banner and the prize
|
|
16
|
+
* amounts ride along with the shape and the rules. Only the date is left out,
|
|
17
|
+
* because it belongs to one event rather than to the setup.
|
|
18
|
+
*
|
|
19
|
+
* Routes (all scoped to the caller):
|
|
20
|
+
* - `GET /tournament-templates/setups` → `{ data: TournamentSetupsResponse }`
|
|
21
|
+
* - `POST /tournament-templates` with `SaveTemplateBody` → `{ data: TournamentTemplateSummary }`
|
|
22
|
+
* - `PUT /tournament-templates/:id/default` → `{ data: TournamentTemplateSummary }`
|
|
23
|
+
* - `DELETE /tournament-templates/:id`
|
|
24
|
+
* - `POST /tournaments` accepts `entryFee`, `checkInTime`, `rules`, `region`,
|
|
25
|
+
* `platform`, `gameAccount`, `imgMain`, `prizes` and `template` (the
|
|
26
|
+
* documentId, for usage meta) alongside the usual fields.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.MAX_TOURNAMENT_NAME = exports.setupPrizePot = exports.TOURNAMENT_SETUP_DEFAULTS = exports.TEMPLATE_COPY_KEYS = exports.MAX_TEMPLATES_PER_HOST = exports.LAST_RUN_WINDOW_DAYS = void 0;
|
|
30
|
+
exports.resolveSetup = resolveSetup;
|
|
31
|
+
exports.copyableSetupKeys = copyableSetupKeys;
|
|
32
|
+
exports.nextTournamentName = nextTournamentName;
|
|
33
|
+
exports.templateNameFrom = templateNameFrom;
|
|
34
|
+
exports.setupsForGame = setupsForGame;
|
|
35
|
+
exports.setupCountByGame = setupCountByGame;
|
|
36
|
+
exports.preselectSetup = preselectSetup;
|
|
37
|
+
exports.setupOfChoice = setupOfChoice;
|
|
38
|
+
exports.nameOfChoice = nameOfChoice;
|
|
39
|
+
exports.describeSetup = describeSetup;
|
|
40
|
+
/** How far back the automatic "last tournament" looks. */
|
|
41
|
+
exports.LAST_RUN_WINDOW_DAYS = 30;
|
|
42
|
+
/** Templates a host may keep; the save route refuses the next one. */
|
|
43
|
+
exports.MAX_TEMPLATES_PER_HOST = 20;
|
|
44
|
+
/**
|
|
45
|
+
* The settings a host may leave out of a setup — the "Also copied" rows.
|
|
46
|
+
* `platform` covers the account network too: the two are chosen together and
|
|
47
|
+
* publish readiness counts them as one detail.
|
|
48
|
+
*/
|
|
49
|
+
exports.TEMPLATE_COPY_KEYS = [
|
|
50
|
+
'entryFee',
|
|
51
|
+
'prizes',
|
|
52
|
+
'checkInTime',
|
|
53
|
+
'platform',
|
|
54
|
+
'region',
|
|
55
|
+
'hasMatchLobby',
|
|
56
|
+
'hasThirdPlace',
|
|
57
|
+
'rules',
|
|
58
|
+
'imgMain',
|
|
59
|
+
];
|
|
60
|
+
/** What a blank tournament starts from, and what a null setting falls back to. */
|
|
61
|
+
exports.TOURNAMENT_SETUP_DEFAULTS = {
|
|
62
|
+
participants: 8,
|
|
63
|
+
teamSize: 'one',
|
|
64
|
+
entryFee: 0,
|
|
65
|
+
checkInTime: 0,
|
|
66
|
+
hasThirdPlace: false,
|
|
67
|
+
hasMatchLobby: true,
|
|
68
|
+
rules: null,
|
|
69
|
+
platform: null,
|
|
70
|
+
gameAccount: null,
|
|
71
|
+
region: null,
|
|
72
|
+
imgMain: null,
|
|
73
|
+
prizes: [],
|
|
74
|
+
};
|
|
75
|
+
/** Every setting resolved: nulls replaced by the XPT default. */
|
|
76
|
+
function resolveSetup(fields) {
|
|
77
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
78
|
+
const source = fields !== null && fields !== void 0 ? fields : {};
|
|
79
|
+
return {
|
|
80
|
+
participants: source.participants || exports.TOURNAMENT_SETUP_DEFAULTS.participants,
|
|
81
|
+
teamSize: (_a = source.teamSize) !== null && _a !== void 0 ? _a : exports.TOURNAMENT_SETUP_DEFAULTS.teamSize,
|
|
82
|
+
entryFee: (_b = source.entryFee) !== null && _b !== void 0 ? _b : exports.TOURNAMENT_SETUP_DEFAULTS.entryFee,
|
|
83
|
+
checkInTime: (_c = source.checkInTime) !== null && _c !== void 0 ? _c : exports.TOURNAMENT_SETUP_DEFAULTS.checkInTime,
|
|
84
|
+
hasThirdPlace: (_d = source.hasThirdPlace) !== null && _d !== void 0 ? _d : exports.TOURNAMENT_SETUP_DEFAULTS.hasThirdPlace,
|
|
85
|
+
hasMatchLobby: (_e = source.hasMatchLobby) !== null && _e !== void 0 ? _e : exports.TOURNAMENT_SETUP_DEFAULTS.hasMatchLobby,
|
|
86
|
+
rules: (_f = source.rules) !== null && _f !== void 0 ? _f : exports.TOURNAMENT_SETUP_DEFAULTS.rules,
|
|
87
|
+
platform: (_g = source.platform) !== null && _g !== void 0 ? _g : null,
|
|
88
|
+
gameAccount: (_h = source.gameAccount) !== null && _h !== void 0 ? _h : null,
|
|
89
|
+
region: (_j = source.region) !== null && _j !== void 0 ? _j : null,
|
|
90
|
+
imgMain: (_k = source.imgMain) !== null && _k !== void 0 ? _k : null,
|
|
91
|
+
prizes: (_l = source.prizes) !== null && _l !== void 0 ? _l : [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The copy keys this setup actually has something for. A setting the host
|
|
96
|
+
* unticked when saving, or never set on the tournament, carries nothing — so
|
|
97
|
+
* the "Also copied" list offers it no row rather than a tickable blank.
|
|
98
|
+
*/
|
|
99
|
+
function copyableSetupKeys(fields) {
|
|
100
|
+
var _a;
|
|
101
|
+
const setup = fields !== null && fields !== void 0 ? fields : {};
|
|
102
|
+
const has = {
|
|
103
|
+
entryFee: setup.entryFee != null,
|
|
104
|
+
prizes: ((_a = setup.prizes) !== null && _a !== void 0 ? _a : []).length > 0,
|
|
105
|
+
checkInTime: setup.checkInTime != null,
|
|
106
|
+
platform: setup.platform != null,
|
|
107
|
+
region: setup.region != null,
|
|
108
|
+
hasMatchLobby: setup.hasMatchLobby != null,
|
|
109
|
+
hasThirdPlace: setup.hasThirdPlace != null,
|
|
110
|
+
rules: !!setup.rules,
|
|
111
|
+
imgMain: setup.imgMain != null,
|
|
112
|
+
};
|
|
113
|
+
return exports.TEMPLATE_COPY_KEYS.filter((key) => has[key]);
|
|
114
|
+
}
|
|
115
|
+
/** The total a setup's prize amounts add up to. */
|
|
116
|
+
const setupPrizePot = (prizes) => (prizes !== null && prizes !== void 0 ? prizes : []).reduce((sum, prize) => sum + (Number(prize.value) || 0), 0);
|
|
117
|
+
exports.setupPrizePot = setupPrizePot;
|
|
118
|
+
const TRAILING_COUNTER = /\s*#(\d+)$/;
|
|
119
|
+
/** The tournament title rules' limit; a bumped name has to stay inside it. */
|
|
120
|
+
exports.MAX_TOURNAMENT_NAME = 50;
|
|
121
|
+
/**
|
|
122
|
+
* The name a tournament started from a setup gets. A trailing `#N` is bumped
|
|
123
|
+
* (`Friday Night Ops #3` → `Friday Night Ops #4`); a name without one is
|
|
124
|
+
* treated as the first of its series and becomes `#2`. Either way the host
|
|
125
|
+
* sees a name they can keep or type over, and never one that silently
|
|
126
|
+
* duplicates the tournament it came from.
|
|
127
|
+
*/
|
|
128
|
+
function nextTournamentName(name) {
|
|
129
|
+
const match = TRAILING_COUNTER.exec(name);
|
|
130
|
+
const base = (match ? name.slice(0, match.index) : name).trim();
|
|
131
|
+
if (!base)
|
|
132
|
+
return name;
|
|
133
|
+
const suffix = ` #${match ? Number(match[1]) + 1 : 2}`;
|
|
134
|
+
const room = exports.MAX_TOURNAMENT_NAME - suffix.length;
|
|
135
|
+
return `${base.length > room ? base.slice(0, room).trim() : base}${suffix}`;
|
|
136
|
+
}
|
|
137
|
+
/** A template name from a tournament title: the trailing `#N`, if any, dropped. */
|
|
138
|
+
function templateNameFrom(title) {
|
|
139
|
+
return title.replace(TRAILING_COUNTER, '').trim();
|
|
140
|
+
}
|
|
141
|
+
/** The setups offered for one game: its templates and, if any, its last run. */
|
|
142
|
+
function setupsForGame(setups, gameDocumentId) {
|
|
143
|
+
var _a;
|
|
144
|
+
if (!setups || !gameDocumentId)
|
|
145
|
+
return { templates: [], lastRun: null };
|
|
146
|
+
return {
|
|
147
|
+
templates: setups.templates.filter((t) => { var _a; return ((_a = t.game) === null || _a === void 0 ? void 0 : _a.documentId) === gameDocumentId; }),
|
|
148
|
+
lastRun: (_a = setups.lastRuns.find((r) => { var _a; return ((_a = r.game) === null || _a === void 0 ? void 0 : _a.documentId) === gameDocumentId; })) !== null && _a !== void 0 ? _a : null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Badge counts for the game grid: templates for the game plus its last run. */
|
|
152
|
+
function setupCountByGame(setups) {
|
|
153
|
+
var _a, _b, _c, _d;
|
|
154
|
+
const counts = {};
|
|
155
|
+
if (!setups)
|
|
156
|
+
return counts;
|
|
157
|
+
for (const t of setups.templates) {
|
|
158
|
+
const id = (_a = t.game) === null || _a === void 0 ? void 0 : _a.documentId;
|
|
159
|
+
if (id)
|
|
160
|
+
counts[id] = ((_b = counts[id]) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
161
|
+
}
|
|
162
|
+
for (const r of setups.lastRuns) {
|
|
163
|
+
const id = (_c = r.game) === null || _c === void 0 ? void 0 : _c.documentId;
|
|
164
|
+
if (id)
|
|
165
|
+
counts[id] = ((_d = counts[id]) !== null && _d !== void 0 ? _d : 0) + 1;
|
|
166
|
+
}
|
|
167
|
+
return counts;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* What the setup step starts on: the default template if the game has one,
|
|
171
|
+
* else the game's last run, else blank — so Continue is a single tap.
|
|
172
|
+
*/
|
|
173
|
+
function preselectSetup(templates, lastRun) {
|
|
174
|
+
var _a;
|
|
175
|
+
const preferred = (_a = templates.find((t) => t.isDefault)) !== null && _a !== void 0 ? _a : templates[0];
|
|
176
|
+
if (preferred)
|
|
177
|
+
return { kind: 'template', template: preferred };
|
|
178
|
+
if (lastRun)
|
|
179
|
+
return { kind: 'lastRun', lastRun };
|
|
180
|
+
return { kind: 'blank' };
|
|
181
|
+
}
|
|
182
|
+
/** The setup fields of a choice, or null for blank. */
|
|
183
|
+
function setupOfChoice(choice) {
|
|
184
|
+
if (choice.kind === 'template')
|
|
185
|
+
return choice.template;
|
|
186
|
+
if (choice.kind === 'lastRun')
|
|
187
|
+
return choice.lastRun;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/** The name a choice was made under, for the provenance strip. */
|
|
191
|
+
function nameOfChoice(choice) {
|
|
192
|
+
if (choice.kind === 'template')
|
|
193
|
+
return choice.template.name;
|
|
194
|
+
if (choice.kind === 'lastRun')
|
|
195
|
+
return choice.lastRun.title;
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The detail line under a setup: `16 teams · 2v2 · 50 XPT · check-in 30 min
|
|
200
|
+
* · rules`. Nulls read as their defaults. The labels come from the caller
|
|
201
|
+
* so this package does not carry the app's copy.
|
|
202
|
+
*/
|
|
203
|
+
function describeSetup(fields, options) {
|
|
204
|
+
var _a;
|
|
205
|
+
const setup = resolveSetup(fields);
|
|
206
|
+
const parts = [
|
|
207
|
+
`${setup.participants} ${options.entryNoun(setup.teamSize)}`,
|
|
208
|
+
options.teamSizeLabel(setup.teamSize),
|
|
209
|
+
options.feeLabel((_a = setup.entryFee) !== null && _a !== void 0 ? _a : 0),
|
|
210
|
+
setup.checkInTime ? `check-in ${setup.checkInTime} min` : 'no check-in',
|
|
211
|
+
];
|
|
212
|
+
if (options.withRules !== false && setup.rules)
|
|
213
|
+
parts.push('rules');
|
|
214
|
+
return parts.join(' · ');
|
|
215
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A user's UI state: small per-user flags that steer the interface — which
|
|
3
|
+
* hints are still showing, which onboarding steps are done.
|
|
4
|
+
*
|
|
5
|
+
* Stored as the private `uiState` JSON on the user, so it never appears on a
|
|
6
|
+
* user record. Read with GET /user/ui-state and written with
|
|
7
|
+
* PATCH /user/ui-state, which merges key by key: a patch naming one key never
|
|
8
|
+
* clears another. The server drops unknown keys and malformed values.
|
|
9
|
+
*/
|
|
10
|
+
export interface UserUiState {
|
|
11
|
+
/**
|
|
12
|
+
* Tournaments (documentId) whose host closed the publish success dialog
|
|
13
|
+
* without going to Manage, so the next-steps panel still shows. Cleared
|
|
14
|
+
* per tournament on Dismiss or when Manage is opened. Newest last, capped
|
|
15
|
+
* at {@link POST_PUBLISH_PENDING_LIMIT}.
|
|
16
|
+
*/
|
|
17
|
+
postPublishPending?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Tournaments (documentId) whose host answered "Not now" to the
|
|
20
|
+
* save-as-template prompt, or saved a template from it, so the prompt does
|
|
21
|
+
* not show again. Newest last, capped at
|
|
22
|
+
* {@link TEMPLATE_PROMPT_DISMISSED_LIMIT}.
|
|
23
|
+
*/
|
|
24
|
+
templatePromptDismissed?: string[];
|
|
25
|
+
}
|
|
26
|
+
/** How many tournaments `postPublishPending` remembers; oldest drop first. */
|
|
27
|
+
export declare const POST_PUBLISH_PENDING_LIMIT = 20;
|
|
28
|
+
/** How many tournaments `templatePromptDismissed` remembers; oldest drop first. */
|
|
29
|
+
export declare const TEMPLATE_PROMPT_DISMISSED_LIMIT = 20;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEMPLATE_PROMPT_DISMISSED_LIMIT = exports.POST_PUBLISH_PENDING_LIMIT = void 0;
|
|
4
|
+
/** How many tournaments `postPublishPending` remembers; oldest drop first. */
|
|
5
|
+
exports.POST_PUBLISH_PENDING_LIMIT = 20;
|
|
6
|
+
/** How many tournaments `templatePromptDismissed` remembers; oldest drop first. */
|
|
7
|
+
exports.TEMPLATE_PROMPT_DISMISSED_LIMIT = 20;
|
package/package.json
CHANGED
package/src/generated/enums.ts
CHANGED
|
@@ -244,6 +244,14 @@ export type TournamentTeamSize =
|
|
|
244
244
|
| "four"
|
|
245
245
|
| "five";
|
|
246
246
|
|
|
247
|
+
/** `TournamentTemplate.teamSize` */
|
|
248
|
+
export type TournamentTemplateTeamSize =
|
|
249
|
+
| "one"
|
|
250
|
+
| "two"
|
|
251
|
+
| "three"
|
|
252
|
+
| "four"
|
|
253
|
+
| "five";
|
|
254
|
+
|
|
247
255
|
/** `Tournament.type` */
|
|
248
256
|
export type TournamentType =
|
|
249
257
|
| "Single Elimination"
|
package/src/generated/inputs.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type {
|
|
|
40
40
|
TournamentParticipantStatus,
|
|
41
41
|
TournamentRoleRole,
|
|
42
42
|
TournamentTeamSize,
|
|
43
|
+
TournamentTemplateTeamSize,
|
|
43
44
|
TournamentType,
|
|
44
45
|
TournamentVisibility,
|
|
45
46
|
UserGameAccountVerificationMethod,
|
|
@@ -132,6 +133,7 @@ export interface GameAccountInput {
|
|
|
132
133
|
name?: string;
|
|
133
134
|
code?: GameAccountCode;
|
|
134
135
|
verification?: GameAccountVerification;
|
|
136
|
+
sortOrder?: number;
|
|
135
137
|
tournaments?: RelationInput | RelationInput[];
|
|
136
138
|
user_game_accounts?: RelationInput | RelationInput[];
|
|
137
139
|
imgThumb?: MediaInput;
|
|
@@ -231,6 +233,7 @@ export interface MatchInput {
|
|
|
231
233
|
disputeReason?: MatchDisputeReason;
|
|
232
234
|
disputeNote?: string;
|
|
233
235
|
disputedBy?: RelationInput;
|
|
236
|
+
resultRecordedBy?: RelationInput;
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
/** Write payload for `match-message`. */
|
|
@@ -425,6 +428,9 @@ export interface TournamentInput {
|
|
|
425
428
|
gameAccount?: RelationInput;
|
|
426
429
|
tournament_stages?: unknown;
|
|
427
430
|
prizesDistributed?: boolean;
|
|
431
|
+
entryFeesCollected?: number;
|
|
432
|
+
entryFeesSettledAt?: string;
|
|
433
|
+
hostXpAwarded?: number;
|
|
428
434
|
discordUrl?: string;
|
|
429
435
|
twitterUrl?: string;
|
|
430
436
|
twitchUrl?: string;
|
|
@@ -473,6 +479,7 @@ export interface TournamentParticipantInput {
|
|
|
473
479
|
status?: TournamentParticipantStatus;
|
|
474
480
|
captainUser?: RelationInput;
|
|
475
481
|
membersSnapshot?: unknown;
|
|
482
|
+
feePaid?: number;
|
|
476
483
|
}
|
|
477
484
|
|
|
478
485
|
/** Write payload for `tournament-preset-banner`. */
|
|
@@ -487,6 +494,28 @@ export interface TournamentRoleInput {
|
|
|
487
494
|
tournament?: RelationInput;
|
|
488
495
|
}
|
|
489
496
|
|
|
497
|
+
/** Write payload for `tournament-template`. */
|
|
498
|
+
export interface TournamentTemplateInput {
|
|
499
|
+
name?: string;
|
|
500
|
+
host?: RelationInput;
|
|
501
|
+
game?: RelationInput;
|
|
502
|
+
participants?: number;
|
|
503
|
+
teamSize?: TournamentTemplateTeamSize;
|
|
504
|
+
entryFee?: number;
|
|
505
|
+
checkInTime?: number;
|
|
506
|
+
hasThirdPlace?: boolean;
|
|
507
|
+
hasMatchLobby?: boolean;
|
|
508
|
+
rules?: string;
|
|
509
|
+
platform?: RelationInput;
|
|
510
|
+
gameAccount?: RelationInput;
|
|
511
|
+
region?: RelationInput;
|
|
512
|
+
imgMain?: MediaInput;
|
|
513
|
+
prizes?: unknown;
|
|
514
|
+
isDefault?: boolean;
|
|
515
|
+
useCount?: number;
|
|
516
|
+
lastUsedAt?: string;
|
|
517
|
+
}
|
|
518
|
+
|
|
490
519
|
/** Write payload for `user`. */
|
|
491
520
|
export interface UserInput {
|
|
492
521
|
username?: string;
|
package/src/generated/models.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type {
|
|
|
41
41
|
TournamentParticipantStatus,
|
|
42
42
|
TournamentRoleRole,
|
|
43
43
|
TournamentTeamSize,
|
|
44
|
+
TournamentTemplateTeamSize,
|
|
44
45
|
TournamentType,
|
|
45
46
|
TournamentVisibility,
|
|
46
47
|
UserGameAccountVerificationMethod,
|
|
@@ -133,6 +134,7 @@ export interface GameAccount extends StrapiDocument {
|
|
|
133
134
|
name?: string | null;
|
|
134
135
|
code?: GameAccountCode | null;
|
|
135
136
|
verification?: GameAccountVerification | null;
|
|
137
|
+
sortOrder?: number | null;
|
|
136
138
|
tournaments?: Tournament[];
|
|
137
139
|
user_game_accounts?: UserGameAccount[];
|
|
138
140
|
imgThumb?: StrapiMedia | null;
|
|
@@ -232,6 +234,7 @@ export interface Match extends StrapiDocument {
|
|
|
232
234
|
disputeReason?: MatchDisputeReason | null;
|
|
233
235
|
disputeNote?: string | null;
|
|
234
236
|
disputedBy?: User | null;
|
|
237
|
+
resultRecordedBy?: User | null;
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/** `match-message` */
|
|
@@ -426,6 +429,9 @@ export interface Tournament extends StrapiDocument {
|
|
|
426
429
|
gameAccount?: GameAccount | null;
|
|
427
430
|
tournament_stages?: unknown | null;
|
|
428
431
|
prizesDistributed?: boolean | null;
|
|
432
|
+
entryFeesCollected?: number | null;
|
|
433
|
+
entryFeesSettledAt?: string | null;
|
|
434
|
+
hostXpAwarded?: number | null;
|
|
429
435
|
discordUrl?: string | null;
|
|
430
436
|
twitterUrl?: string | null;
|
|
431
437
|
twitchUrl?: string | null;
|
|
@@ -474,6 +480,7 @@ export interface TournamentParticipant extends StrapiDocument {
|
|
|
474
480
|
status?: TournamentParticipantStatus | null;
|
|
475
481
|
captainUser?: User | null;
|
|
476
482
|
membersSnapshot?: unknown | null;
|
|
483
|
+
feePaid?: number | null;
|
|
477
484
|
}
|
|
478
485
|
|
|
479
486
|
/** `tournament-preset-banner` */
|
|
@@ -488,6 +495,28 @@ export interface TournamentRole extends StrapiDocument {
|
|
|
488
495
|
tournament?: Tournament | null;
|
|
489
496
|
}
|
|
490
497
|
|
|
498
|
+
/** `tournament-template` */
|
|
499
|
+
export interface TournamentTemplate extends StrapiDocument {
|
|
500
|
+
name?: string | null;
|
|
501
|
+
host?: User | null;
|
|
502
|
+
game?: Game | null;
|
|
503
|
+
participants?: number | null;
|
|
504
|
+
teamSize?: TournamentTemplateTeamSize | null;
|
|
505
|
+
entryFee?: number | null;
|
|
506
|
+
checkInTime?: number | null;
|
|
507
|
+
hasThirdPlace?: boolean | null;
|
|
508
|
+
hasMatchLobby?: boolean | null;
|
|
509
|
+
rules?: string | null;
|
|
510
|
+
platform?: Platform | null;
|
|
511
|
+
gameAccount?: GameAccount | null;
|
|
512
|
+
region?: Region | null;
|
|
513
|
+
imgMain?: StrapiMedia | null;
|
|
514
|
+
prizes?: unknown | null;
|
|
515
|
+
isDefault?: boolean | null;
|
|
516
|
+
useCount?: number | null;
|
|
517
|
+
lastUsedAt?: string | null;
|
|
518
|
+
}
|
|
519
|
+
|
|
491
520
|
/** `user` */
|
|
492
521
|
export interface User extends StrapiDocument {
|
|
493
522
|
username?: string | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The experience ladder — EXP, which is **not** XPT.
|
|
3
|
+
*
|
|
4
|
+
* XPT is the spendable currency: it lives on the user as `xptCoins`, moves
|
|
5
|
+
* through `createWalletTransaction`, and leaves a ledger row. EXP is
|
|
6
|
+
* progression: it lives in `*_exp_earned` columns on the stat tables, is never
|
|
7
|
+
* spendable, and surfaces only as a level name. Nothing converts one into the
|
|
8
|
+
* other.
|
|
9
|
+
*
|
|
10
|
+
* Three independent ladders share the maths below — a player's
|
|
11
|
+
* (`user_summary_stat.total_exp_earned`), a host's
|
|
12
|
+
* (`host_summary_stat.host_exp_earned`) and a team's
|
|
13
|
+
* (`team_summary_stat.total_exp_earned`) — so a change here moves all three.
|
|
14
|
+
*
|
|
15
|
+
* It lives in the shared package because the server needs `getLevelFromExp` to
|
|
16
|
+
* notice a level *crossing* (the only EXP event worth a notification; see the
|
|
17
|
+
* note on `getLevelName`), while the client needs the same ladder to render
|
|
18
|
+
* the level bar. Two copies would drift into telling a user two different
|
|
19
|
+
* things about the same number.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const levelNames = [
|
|
23
|
+
'Beginner',
|
|
24
|
+
'Novice',
|
|
25
|
+
'Rookie',
|
|
26
|
+
'Apprentice',
|
|
27
|
+
'Initiate',
|
|
28
|
+
'Trainee',
|
|
29
|
+
'Competitor',
|
|
30
|
+
'Contender',
|
|
31
|
+
'Veteran',
|
|
32
|
+
'Expert',
|
|
33
|
+
'Elite',
|
|
34
|
+
'Master',
|
|
35
|
+
'Grandmaster',
|
|
36
|
+
'Champion',
|
|
37
|
+
'Legend',
|
|
38
|
+
'Mythic',
|
|
39
|
+
'Godlike',
|
|
40
|
+
'Immortal',
|
|
41
|
+
'Supreme',
|
|
42
|
+
'Transcendent',
|
|
43
|
+
'Zenith',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const BASE_EXP = 500;
|
|
47
|
+
export const EXP_MULTIPLIER = 1.25;
|
|
48
|
+
|
|
49
|
+
/** Total EXP needed to have reached `level`. Level 1 is the floor, at 0. */
|
|
50
|
+
export const getExpForLevel = (level: number): number => {
|
|
51
|
+
if (level <= 0) return 0;
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (let i = 0; i < level; i++) {
|
|
54
|
+
total += Math.floor(BASE_EXP * Math.pow(EXP_MULTIPLIER, i));
|
|
55
|
+
}
|
|
56
|
+
return total;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 1-based: everyone starts at level 1, which is `levelNames[0]`. */
|
|
60
|
+
export const getLevelFromExp = (totalExp: number, maxLevel: number): number => {
|
|
61
|
+
let level = 1;
|
|
62
|
+
while (level < maxLevel && totalExp >= getExpForLevel(level + 1)) {
|
|
63
|
+
level++;
|
|
64
|
+
}
|
|
65
|
+
return level;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `MAX_LEVEL` is the number of rungs, so every name is reachable. It was 20
|
|
70
|
+
* against 21 names, which stranded 'Zenith' at the top of a ladder nobody
|
|
71
|
+
* could finish climbing.
|
|
72
|
+
*/
|
|
73
|
+
export const MAX_LEVEL = levelNames.length;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `getLevelFromExp` is 1-based and `levelNames` is 0-based, so the index is
|
|
77
|
+
* `level - 1`. It used to be `levelNames[level]`, which skipped 'Beginner'
|
|
78
|
+
* entirely and greeted a brand-new account with 0 EXP as 'Novice'.
|
|
79
|
+
*/
|
|
80
|
+
export const getLevelName = (
|
|
81
|
+
totalExp: number,
|
|
82
|
+
maxLevel = MAX_LEVEL
|
|
83
|
+
): string => {
|
|
84
|
+
const level = getLevelFromExp(totalExp, maxLevel);
|
|
85
|
+
return levelNames[level - 1] ?? 'Unknown';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Whether adding EXP moved someone up a rung, and to what.
|
|
90
|
+
*
|
|
91
|
+
* This is the only EXP event worth a notification. Awards themselves are a
|
|
92
|
+
* drip — a match is 10 EXP, so a bracket winner would collect five bells on
|
|
93
|
+
* top of the five they already get for the results — whereas a crossing is
|
|
94
|
+
* rare and is the thing the ladder exists to celebrate.
|
|
95
|
+
*/
|
|
96
|
+
export const levelCrossing = (
|
|
97
|
+
before: number,
|
|
98
|
+
after: number
|
|
99
|
+
): { crossed: boolean; level: number; name: string } => {
|
|
100
|
+
const from = getLevelFromExp(before, MAX_LEVEL);
|
|
101
|
+
const to = getLevelFromExp(after, MAX_LEVEL);
|
|
102
|
+
return {
|
|
103
|
+
crossed: to > from,
|
|
104
|
+
level: to,
|
|
105
|
+
name: levelNames[to - 1] ?? 'Unknown',
|
|
106
|
+
};
|
|
107
|
+
};
|
package/src/manual/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Hand-written types that are not derived from a Strapi content type.
|
|
3
3
|
*/
|
|
4
4
|
export * from './country';
|
|
5
|
+
export * from './experience';
|
|
5
6
|
export * from './gameAccountHandle';
|
|
6
7
|
export * from './lobby';
|
|
7
8
|
export * from './notification';
|
|
@@ -10,3 +11,5 @@ export * from './tournamentEntry';
|
|
|
10
11
|
export * from './tournamentInvite';
|
|
11
12
|
export * from './tournamentJoinRequest';
|
|
12
13
|
export * from './tournamentStaff';
|
|
14
|
+
export * from './tournamentTemplate';
|
|
15
|
+
export * from './userUiState';
|