xpt-shared-types 1.19.0 → 1.24.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/README.md +7 -0
- package/dist/generated/enums.d.ts +2 -0
- package/dist/generated/inputs.d.ts +24 -1
- package/dist/generated/models.d.ts +24 -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/tournamentEntry.d.ts +5 -1
- package/dist/manual/tournamentTemplate.d.ts +134 -0
- package/dist/manual/tournamentTemplate.js +160 -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 +25 -0
- package/src/generated/models.ts +25 -0
- package/src/manual/experience.ts +107 -0
- package/src/manual/index.ts +3 -0
- package/src/manual/tournamentEntry.ts +5 -1
- package/src/manual/tournamentTemplate.ts +243 -0
- package/src/manual/userUiState.ts +31 -0
package/README.md
CHANGED
|
@@ -117,6 +117,13 @@ using npm trusted publishing, so no npm token or 2FA prompt is involved.
|
|
|
117
117
|
2. `yarn version --patch` (or `--minor`), which commits and tags `vX.Y.Z`
|
|
118
118
|
3. `git push --follow-tags`
|
|
119
119
|
|
|
120
|
+
If the version was bumped by hand instead, commit it and tag with
|
|
121
|
+
`git tag -a vX.Y.Z -m vX.Y.Z` before pushing: `--follow-tags` only sends
|
|
122
|
+
annotated tags, so a plain `git tag vX.Y.Z` (or no tag at all) pushes nothing
|
|
123
|
+
and prints "Everything up-to-date". `git push origin vX.Y.Z` also works.
|
|
124
|
+
After the run succeeds the registry can lag a minute or two; confirm with
|
|
125
|
+
`npm view xpt-shared-types dist-tags.latest` before installing.
|
|
126
|
+
|
|
120
127
|
The workflow fires on the `v*` tag, refuses to run if the tag and
|
|
121
128
|
`package.json` version disagree, then builds and publishes.
|
|
122
129
|
|
|
@@ -62,6 +62,8 @@ export type TournamentParticipantStatus = "registered" | "active" | "eliminated"
|
|
|
62
62
|
export type TournamentRoleRole = "moderator" | "admin";
|
|
63
63
|
/** `Tournament.teamSize` */
|
|
64
64
|
export type TournamentTeamSize = "one" | "two" | "three" | "four" | "five";
|
|
65
|
+
/** `TournamentTemplate.teamSize` */
|
|
66
|
+
export type TournamentTemplateTeamSize = "one" | "two" | "three" | "four" | "five";
|
|
65
67
|
/** `Tournament.type` */
|
|
66
68
|
export type TournamentType = "Single Elimination" | "League" | "Double Elimination";
|
|
67
69
|
/** `Tournament.visibility` */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, MediaInput, RelationInput } from '../contracts';
|
|
2
|
-
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserGameAccountVerificationMethod, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
2
|
+
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentTemplateTeamSize, TournamentType, TournamentVisibility, UserGameAccountVerificationMethod, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** Write payload for `about`. */
|
|
4
4
|
export interface AboutInput {
|
|
5
5
|
heroTitle?: string;
|
|
@@ -77,6 +77,7 @@ export interface GameAccountInput {
|
|
|
77
77
|
name?: string;
|
|
78
78
|
code?: GameAccountCode;
|
|
79
79
|
verification?: GameAccountVerification;
|
|
80
|
+
sortOrder?: number;
|
|
80
81
|
tournaments?: RelationInput | RelationInput[];
|
|
81
82
|
user_game_accounts?: RelationInput | RelationInput[];
|
|
82
83
|
imgThumb?: MediaInput;
|
|
@@ -170,6 +171,7 @@ export interface MatchInput {
|
|
|
170
171
|
disputeReason?: MatchDisputeReason;
|
|
171
172
|
disputeNote?: string;
|
|
172
173
|
disputedBy?: RelationInput;
|
|
174
|
+
resultRecordedBy?: RelationInput;
|
|
173
175
|
}
|
|
174
176
|
/** Write payload for `match-message`. */
|
|
175
177
|
export interface MatchMessageInput {
|
|
@@ -336,6 +338,7 @@ export interface TournamentInput {
|
|
|
336
338
|
rules?: string;
|
|
337
339
|
hasThirdPlace?: boolean;
|
|
338
340
|
hasMatchLobby?: boolean;
|
|
341
|
+
requireVerifiedAccounts?: boolean;
|
|
339
342
|
visibility?: TournamentVisibility;
|
|
340
343
|
access?: TournamentAccess;
|
|
341
344
|
checkInTime?: number;
|
|
@@ -344,6 +347,9 @@ export interface TournamentInput {
|
|
|
344
347
|
gameAccount?: RelationInput;
|
|
345
348
|
tournament_stages?: unknown;
|
|
346
349
|
prizesDistributed?: boolean;
|
|
350
|
+
entryFeesCollected?: number;
|
|
351
|
+
entryFeesSettledAt?: string;
|
|
352
|
+
hostXpAwarded?: number;
|
|
347
353
|
discordUrl?: string;
|
|
348
354
|
twitterUrl?: string;
|
|
349
355
|
twitchUrl?: string;
|
|
@@ -389,6 +395,7 @@ export interface TournamentParticipantInput {
|
|
|
389
395
|
status?: TournamentParticipantStatus;
|
|
390
396
|
captainUser?: RelationInput;
|
|
391
397
|
membersSnapshot?: unknown;
|
|
398
|
+
feePaid?: number;
|
|
392
399
|
}
|
|
393
400
|
/** Write payload for `tournament-preset-banner`. */
|
|
394
401
|
export interface TournamentPresetBannerInput {
|
|
@@ -400,6 +407,22 @@ export interface TournamentRoleInput {
|
|
|
400
407
|
users_permissions_user?: RelationInput;
|
|
401
408
|
tournament?: RelationInput;
|
|
402
409
|
}
|
|
410
|
+
/** Write payload for `tournament-template`. */
|
|
411
|
+
export interface TournamentTemplateInput {
|
|
412
|
+
name?: string;
|
|
413
|
+
host?: RelationInput;
|
|
414
|
+
game?: RelationInput;
|
|
415
|
+
participants?: number;
|
|
416
|
+
teamSize?: TournamentTemplateTeamSize;
|
|
417
|
+
entryFee?: number;
|
|
418
|
+
checkInTime?: number;
|
|
419
|
+
hasThirdPlace?: boolean;
|
|
420
|
+
hasMatchLobby?: boolean;
|
|
421
|
+
rules?: string;
|
|
422
|
+
isDefault?: boolean;
|
|
423
|
+
useCount?: number;
|
|
424
|
+
lastUsedAt?: string;
|
|
425
|
+
}
|
|
403
426
|
/** Write payload for `user`. */
|
|
404
427
|
export interface UserInput {
|
|
405
428
|
username?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BlocksContent, StrapiDocument, StrapiMedia, UserRole } from '../contracts';
|
|
2
|
-
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentType, TournamentVisibility, UserGameAccountVerificationMethod, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
2
|
+
import type { AdminActionTargetType, FaqCategory, FriendshipStatus, GameAccountCode, GameAccountVerification, GameGenre, GameRequestCustomLobbies, GameRequestGenre, GameRequestTeamPlay, GameVisibility, MatchDisputeReason, MatchLobbyStatus, MatchResultType, MatchRound, MatchStreamPlatform, MatchWinnerSlot, PlatformCode, PlayerBanScope, ReferralStatus, TeamCurrentStatus, TeamInviteStatus, TeamInviteTeamRole, TeamPlayerTeamRole, TournamentAccess, TournamentCurrentStatus, TournamentInviteStatus, TournamentJoinRequestEntryType, TournamentJoinRequestStatus, TournamentParticipantEntryType, TournamentParticipantStatus, TournamentRoleRole, TournamentTeamSize, TournamentTemplateTeamSize, TournamentType, TournamentVisibility, UserGameAccountVerificationMethod, UserTransactionStripeStatus, UserTransactionType } from './enums';
|
|
3
3
|
/** `about` */
|
|
4
4
|
export interface About extends StrapiDocument {
|
|
5
5
|
heroTitle?: string | null;
|
|
@@ -77,6 +77,7 @@ export interface GameAccount extends StrapiDocument {
|
|
|
77
77
|
name?: string | null;
|
|
78
78
|
code?: GameAccountCode | null;
|
|
79
79
|
verification?: GameAccountVerification | null;
|
|
80
|
+
sortOrder?: number | null;
|
|
80
81
|
tournaments?: Tournament[];
|
|
81
82
|
user_game_accounts?: UserGameAccount[];
|
|
82
83
|
imgThumb?: StrapiMedia | null;
|
|
@@ -170,6 +171,7 @@ export interface Match extends StrapiDocument {
|
|
|
170
171
|
disputeReason?: MatchDisputeReason | null;
|
|
171
172
|
disputeNote?: string | null;
|
|
172
173
|
disputedBy?: User | null;
|
|
174
|
+
resultRecordedBy?: User | null;
|
|
173
175
|
}
|
|
174
176
|
/** `match-message` */
|
|
175
177
|
export interface MatchMessage extends StrapiDocument {
|
|
@@ -336,6 +338,7 @@ export interface Tournament extends StrapiDocument {
|
|
|
336
338
|
rules?: string | null;
|
|
337
339
|
hasThirdPlace?: boolean | null;
|
|
338
340
|
hasMatchLobby?: boolean | null;
|
|
341
|
+
requireVerifiedAccounts?: boolean | null;
|
|
339
342
|
visibility?: TournamentVisibility | null;
|
|
340
343
|
access?: TournamentAccess | null;
|
|
341
344
|
checkInTime?: number | null;
|
|
@@ -344,6 +347,9 @@ export interface Tournament extends StrapiDocument {
|
|
|
344
347
|
gameAccount?: GameAccount | null;
|
|
345
348
|
tournament_stages?: unknown | null;
|
|
346
349
|
prizesDistributed?: boolean | null;
|
|
350
|
+
entryFeesCollected?: number | null;
|
|
351
|
+
entryFeesSettledAt?: string | null;
|
|
352
|
+
hostXpAwarded?: number | null;
|
|
347
353
|
discordUrl?: string | null;
|
|
348
354
|
twitterUrl?: string | null;
|
|
349
355
|
twitchUrl?: string | null;
|
|
@@ -389,6 +395,7 @@ export interface TournamentParticipant extends StrapiDocument {
|
|
|
389
395
|
status?: TournamentParticipantStatus | null;
|
|
390
396
|
captainUser?: User | null;
|
|
391
397
|
membersSnapshot?: unknown | null;
|
|
398
|
+
feePaid?: number | null;
|
|
392
399
|
}
|
|
393
400
|
/** `tournament-preset-banner` */
|
|
394
401
|
export interface TournamentPresetBanner extends StrapiDocument {
|
|
@@ -400,6 +407,22 @@ export interface TournamentRole extends StrapiDocument {
|
|
|
400
407
|
users_permissions_user?: User | null;
|
|
401
408
|
tournament?: Tournament | null;
|
|
402
409
|
}
|
|
410
|
+
/** `tournament-template` */
|
|
411
|
+
export interface TournamentTemplate extends StrapiDocument {
|
|
412
|
+
name?: string | null;
|
|
413
|
+
host?: User | null;
|
|
414
|
+
game?: Game | null;
|
|
415
|
+
participants?: number | null;
|
|
416
|
+
teamSize?: TournamentTemplateTeamSize | null;
|
|
417
|
+
entryFee?: number | null;
|
|
418
|
+
checkInTime?: number | null;
|
|
419
|
+
hasThirdPlace?: boolean | null;
|
|
420
|
+
hasMatchLobby?: boolean | null;
|
|
421
|
+
rules?: string | null;
|
|
422
|
+
isDefault?: boolean | null;
|
|
423
|
+
useCount?: number | null;
|
|
424
|
+
lastUsedAt?: string | null;
|
|
425
|
+
}
|
|
403
426
|
/** `user` */
|
|
404
427
|
export interface User extends StrapiDocument {
|
|
405
428
|
username?: string | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
export declare const levelNames: string[];
|
|
22
|
+
export declare const BASE_EXP = 500;
|
|
23
|
+
export declare const EXP_MULTIPLIER = 1.25;
|
|
24
|
+
/** Total EXP needed to have reached `level`. Level 1 is the floor, at 0. */
|
|
25
|
+
export declare const getExpForLevel: (level: number) => number;
|
|
26
|
+
/** 1-based: everyone starts at level 1, which is `levelNames[0]`. */
|
|
27
|
+
export declare const getLevelFromExp: (totalExp: number, maxLevel: number) => number;
|
|
28
|
+
/**
|
|
29
|
+
* `MAX_LEVEL` is the number of rungs, so every name is reachable. It was 20
|
|
30
|
+
* against 21 names, which stranded 'Zenith' at the top of a ladder nobody
|
|
31
|
+
* could finish climbing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_LEVEL: number;
|
|
34
|
+
/**
|
|
35
|
+
* `getLevelFromExp` is 1-based and `levelNames` is 0-based, so the index is
|
|
36
|
+
* `level - 1`. It used to be `levelNames[level]`, which skipped 'Beginner'
|
|
37
|
+
* entirely and greeted a brand-new account with 0 EXP as 'Novice'.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getLevelName: (totalExp: number, maxLevel?: number) => string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether adding EXP moved someone up a rung, and to what.
|
|
42
|
+
*
|
|
43
|
+
* This is the only EXP event worth a notification. Awards themselves are a
|
|
44
|
+
* drip — a match is 10 EXP, so a bracket winner would collect five bells on
|
|
45
|
+
* top of the five they already get for the results — whereas a crossing is
|
|
46
|
+
* rare and is the thing the ladder exists to celebrate.
|
|
47
|
+
*/
|
|
48
|
+
export declare const levelCrossing: (before: number, after: number) => {
|
|
49
|
+
crossed: boolean;
|
|
50
|
+
level: number;
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The experience ladder — EXP, which is **not** XPT.
|
|
4
|
+
*
|
|
5
|
+
* XPT is the spendable currency: it lives on the user as `xptCoins`, moves
|
|
6
|
+
* through `createWalletTransaction`, and leaves a ledger row. EXP is
|
|
7
|
+
* progression: it lives in `*_exp_earned` columns on the stat tables, is never
|
|
8
|
+
* spendable, and surfaces only as a level name. Nothing converts one into the
|
|
9
|
+
* other.
|
|
10
|
+
*
|
|
11
|
+
* Three independent ladders share the maths below — a player's
|
|
12
|
+
* (`user_summary_stat.total_exp_earned`), a host's
|
|
13
|
+
* (`host_summary_stat.host_exp_earned`) and a team's
|
|
14
|
+
* (`team_summary_stat.total_exp_earned`) — so a change here moves all three.
|
|
15
|
+
*
|
|
16
|
+
* It lives in the shared package because the server needs `getLevelFromExp` to
|
|
17
|
+
* notice a level *crossing* (the only EXP event worth a notification; see the
|
|
18
|
+
* note on `getLevelName`), while the client needs the same ladder to render
|
|
19
|
+
* the level bar. Two copies would drift into telling a user two different
|
|
20
|
+
* things about the same number.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.levelCrossing = exports.getLevelName = exports.MAX_LEVEL = exports.getLevelFromExp = exports.getExpForLevel = exports.EXP_MULTIPLIER = exports.BASE_EXP = exports.levelNames = void 0;
|
|
24
|
+
exports.levelNames = [
|
|
25
|
+
'Beginner',
|
|
26
|
+
'Novice',
|
|
27
|
+
'Rookie',
|
|
28
|
+
'Apprentice',
|
|
29
|
+
'Initiate',
|
|
30
|
+
'Trainee',
|
|
31
|
+
'Competitor',
|
|
32
|
+
'Contender',
|
|
33
|
+
'Veteran',
|
|
34
|
+
'Expert',
|
|
35
|
+
'Elite',
|
|
36
|
+
'Master',
|
|
37
|
+
'Grandmaster',
|
|
38
|
+
'Champion',
|
|
39
|
+
'Legend',
|
|
40
|
+
'Mythic',
|
|
41
|
+
'Godlike',
|
|
42
|
+
'Immortal',
|
|
43
|
+
'Supreme',
|
|
44
|
+
'Transcendent',
|
|
45
|
+
'Zenith',
|
|
46
|
+
];
|
|
47
|
+
exports.BASE_EXP = 500;
|
|
48
|
+
exports.EXP_MULTIPLIER = 1.25;
|
|
49
|
+
/** Total EXP needed to have reached `level`. Level 1 is the floor, at 0. */
|
|
50
|
+
const getExpForLevel = (level) => {
|
|
51
|
+
if (level <= 0)
|
|
52
|
+
return 0;
|
|
53
|
+
let total = 0;
|
|
54
|
+
for (let i = 0; i < level; i++) {
|
|
55
|
+
total += Math.floor(exports.BASE_EXP * Math.pow(exports.EXP_MULTIPLIER, i));
|
|
56
|
+
}
|
|
57
|
+
return total;
|
|
58
|
+
};
|
|
59
|
+
exports.getExpForLevel = getExpForLevel;
|
|
60
|
+
/** 1-based: everyone starts at level 1, which is `levelNames[0]`. */
|
|
61
|
+
const getLevelFromExp = (totalExp, maxLevel) => {
|
|
62
|
+
let level = 1;
|
|
63
|
+
while (level < maxLevel && totalExp >= (0, exports.getExpForLevel)(level + 1)) {
|
|
64
|
+
level++;
|
|
65
|
+
}
|
|
66
|
+
return level;
|
|
67
|
+
};
|
|
68
|
+
exports.getLevelFromExp = getLevelFromExp;
|
|
69
|
+
/**
|
|
70
|
+
* `MAX_LEVEL` is the number of rungs, so every name is reachable. It was 20
|
|
71
|
+
* against 21 names, which stranded 'Zenith' at the top of a ladder nobody
|
|
72
|
+
* could finish climbing.
|
|
73
|
+
*/
|
|
74
|
+
exports.MAX_LEVEL = exports.levelNames.length;
|
|
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
|
+
const getLevelName = (totalExp, maxLevel = exports.MAX_LEVEL) => {
|
|
81
|
+
var _a;
|
|
82
|
+
const level = (0, exports.getLevelFromExp)(totalExp, maxLevel);
|
|
83
|
+
return (_a = exports.levelNames[level - 1]) !== null && _a !== void 0 ? _a : 'Unknown';
|
|
84
|
+
};
|
|
85
|
+
exports.getLevelName = getLevelName;
|
|
86
|
+
/**
|
|
87
|
+
* Whether adding EXP moved someone up a rung, and to what.
|
|
88
|
+
*
|
|
89
|
+
* This is the only EXP event worth a notification. Awards themselves are a
|
|
90
|
+
* drip — a match is 10 EXP, so a bracket winner would collect five bells on
|
|
91
|
+
* top of the five they already get for the results — whereas a crossing is
|
|
92
|
+
* rare and is the thing the ladder exists to celebrate.
|
|
93
|
+
*/
|
|
94
|
+
const levelCrossing = (before, after) => {
|
|
95
|
+
var _a;
|
|
96
|
+
const from = (0, exports.getLevelFromExp)(before, exports.MAX_LEVEL);
|
|
97
|
+
const to = (0, exports.getLevelFromExp)(after, exports.MAX_LEVEL);
|
|
98
|
+
return {
|
|
99
|
+
crossed: to > from,
|
|
100
|
+
level: to,
|
|
101
|
+
name: (_a = exports.levelNames[to - 1]) !== null && _a !== void 0 ? _a : 'Unknown',
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
exports.levelCrossing = levelCrossing;
|
package/dist/manual/index.d.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';
|
package/dist/manual/index.js
CHANGED
|
@@ -18,6 +18,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
18
18
|
* Hand-written types that are not derived from a Strapi content type.
|
|
19
19
|
*/
|
|
20
20
|
__exportStar(require("./country"), exports);
|
|
21
|
+
__exportStar(require("./experience"), exports);
|
|
21
22
|
__exportStar(require("./gameAccountHandle"), exports);
|
|
22
23
|
__exportStar(require("./lobby"), exports);
|
|
23
24
|
__exportStar(require("./notification"), exports);
|
|
@@ -26,3 +27,5 @@ __exportStar(require("./tournamentEntry"), exports);
|
|
|
26
27
|
__exportStar(require("./tournamentInvite"), exports);
|
|
27
28
|
__exportStar(require("./tournamentJoinRequest"), exports);
|
|
28
29
|
__exportStar(require("./tournamentStaff"), exports);
|
|
30
|
+
__exportStar(require("./tournamentTemplate"), exports);
|
|
31
|
+
__exportStar(require("./userUiState"), exports);
|
|
@@ -33,4 +33,8 @@ export interface TournamentTeamJoinInput {
|
|
|
33
33
|
* `error.details.name` values the entry endpoints raise that a client is
|
|
34
34
|
* expected to branch on. Anything else is a plain message.
|
|
35
35
|
*/
|
|
36
|
-
export type TournamentEntryErrorName = 'NotEntryLeader' | 'MembersMissingGameAccount' | 'InsufficientCoins' | 'GameAccountRequired' | 'TeamEntryRequired'
|
|
36
|
+
export type TournamentEntryErrorName = 'NotEntryLeader' | 'MembersMissingGameAccount' | 'InsufficientCoins' | 'GameAccountRequired' | 'TeamEntryRequired'
|
|
37
|
+
/** The tournament admits only accounts verified by signing in; yours is typed. */
|
|
38
|
+
| 'GameAccountUnverified'
|
|
39
|
+
/** Same, for the named players on a team roster. */
|
|
40
|
+
| 'MembersUnverifiedGameAccount';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament templates — the contract between `utils/tournamentTemplate.ts`
|
|
3
|
+
* in xpt-strapi and the create dialog's setup step, the post-publish
|
|
4
|
+
* "Save as template" prompt and the Settings → Templates page in xpt-client.
|
|
5
|
+
*
|
|
6
|
+
* A template holds settings only: never entries, dates or results. A null
|
|
7
|
+
* setting means the host chose not to save it and the XPT default applies
|
|
8
|
+
* (`resolveSetup`). Alongside the host's templates, the API derives their
|
|
9
|
+
* **last tournament per game** — the newest one taken out of draft in the
|
|
10
|
+
* last `LAST_RUN_WINDOW_DAYS` — so a host can repeat a night without ever
|
|
11
|
+
* having saved anything.
|
|
12
|
+
*
|
|
13
|
+
* Routes (all scoped to the caller):
|
|
14
|
+
* - `GET /tournament-templates/setups` → `{ data: TournamentSetupsResponse }`
|
|
15
|
+
* - `POST /tournament-templates` with `SaveTemplateBody` → `{ data: TournamentTemplateSummary }`
|
|
16
|
+
* - `PUT /tournament-templates/:id/default` → `{ data: TournamentTemplateSummary }`
|
|
17
|
+
* - `DELETE /tournament-templates/:id`
|
|
18
|
+
* - `POST /tournaments` accepts `entryFee`, `checkInTime`, `rules` and
|
|
19
|
+
* `template` (the documentId, for usage meta) alongside the usual fields.
|
|
20
|
+
*/
|
|
21
|
+
import type { StrapiMedia } from '../contracts';
|
|
22
|
+
import type { TournamentTeamSize } from '../generated/enums';
|
|
23
|
+
/** How far back the automatic "last tournament" looks. */
|
|
24
|
+
export declare const LAST_RUN_WINDOW_DAYS = 30;
|
|
25
|
+
/** Templates a host may keep; the save route refuses the next one. */
|
|
26
|
+
export declare const MAX_TEMPLATES_PER_HOST = 20;
|
|
27
|
+
/** The settings a host may leave out of a template — the "Also copied" rows. */
|
|
28
|
+
export declare const TEMPLATE_COPY_KEYS: readonly ["entryFee", "checkInTime", "hasMatchLobby", "hasThirdPlace", "rules"];
|
|
29
|
+
export type TemplateCopyKey = (typeof TEMPLATE_COPY_KEYS)[number];
|
|
30
|
+
export type TemplateIncludeFlags = Record<TemplateCopyKey, boolean>;
|
|
31
|
+
export interface TournamentSetupFields {
|
|
32
|
+
participants: number;
|
|
33
|
+
teamSize: TournamentTeamSize;
|
|
34
|
+
/** Null: not saved, XPT default applies. */
|
|
35
|
+
entryFee: number | null;
|
|
36
|
+
/** Minutes; 0 is "no check-in". Null: not saved. */
|
|
37
|
+
checkInTime: number | null;
|
|
38
|
+
hasThirdPlace: boolean | null;
|
|
39
|
+
hasMatchLobby: boolean | null;
|
|
40
|
+
rules: string | null;
|
|
41
|
+
}
|
|
42
|
+
/** What a blank tournament starts from, and what a null setting falls back to. */
|
|
43
|
+
export declare const TOURNAMENT_SETUP_DEFAULTS: TournamentSetupFields;
|
|
44
|
+
/** Enough of a game for the client to open the create dialog on it. */
|
|
45
|
+
export interface SetupGame {
|
|
46
|
+
id: number;
|
|
47
|
+
documentId: string;
|
|
48
|
+
title: string | null;
|
|
49
|
+
slug: string | null;
|
|
50
|
+
imgThumb: StrapiMedia | null;
|
|
51
|
+
}
|
|
52
|
+
/** A saved template as the routes return it. */
|
|
53
|
+
export interface TournamentTemplateSummary extends TournamentSetupFields {
|
|
54
|
+
documentId: string;
|
|
55
|
+
name: string;
|
|
56
|
+
game: SetupGame | null;
|
|
57
|
+
isDefault: boolean;
|
|
58
|
+
useCount: number;
|
|
59
|
+
lastUsedAt: string | null;
|
|
60
|
+
createdAt: string | null;
|
|
61
|
+
}
|
|
62
|
+
/** The host's last tournament for one game, derived at read time. */
|
|
63
|
+
export interface LastRunSummary extends TournamentSetupFields {
|
|
64
|
+
documentId: string;
|
|
65
|
+
title: string;
|
|
66
|
+
slug: string;
|
|
67
|
+
game: SetupGame | null;
|
|
68
|
+
dateTime: string | null;
|
|
69
|
+
createdAt: string | null;
|
|
70
|
+
}
|
|
71
|
+
/** `GET /tournament-templates/setups` — templates default first, runs newest first. */
|
|
72
|
+
export interface TournamentSetupsResponse {
|
|
73
|
+
templates: TournamentTemplateSummary[];
|
|
74
|
+
lastRuns: LastRunSummary[];
|
|
75
|
+
}
|
|
76
|
+
/** `POST /tournament-templates`. Missing include flags read as ticked. */
|
|
77
|
+
export interface SaveTemplateBody {
|
|
78
|
+
tournamentDocumentId: string;
|
|
79
|
+
name: string;
|
|
80
|
+
include?: Partial<TemplateIncludeFlags>;
|
|
81
|
+
isDefault?: boolean;
|
|
82
|
+
}
|
|
83
|
+
/** What the setup step offers, and what the details step was prefilled from. */
|
|
84
|
+
export type SetupChoice = {
|
|
85
|
+
kind: 'template';
|
|
86
|
+
template: TournamentTemplateSummary;
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'lastRun';
|
|
89
|
+
lastRun: LastRunSummary;
|
|
90
|
+
} | {
|
|
91
|
+
kind: 'blank';
|
|
92
|
+
};
|
|
93
|
+
/** Every setting resolved: nulls replaced by the XPT default. */
|
|
94
|
+
export declare function resolveSetup(fields: Partial<TournamentSetupFields> | null | undefined): TournamentSetupFields;
|
|
95
|
+
/**
|
|
96
|
+
* The name a tournament started from a setup gets: a trailing `#N` is bumped
|
|
97
|
+
* (`Friday Night Ops #3` → `Friday Night Ops #4`); anything else is copied
|
|
98
|
+
* verbatim.
|
|
99
|
+
*/
|
|
100
|
+
export declare function nextTournamentName(name: string): string;
|
|
101
|
+
/** A template name from a tournament title: the trailing `#N`, if any, dropped. */
|
|
102
|
+
export declare function templateNameFrom(title: string): string;
|
|
103
|
+
/** The setups offered for one game: its templates and, if any, its last run. */
|
|
104
|
+
export declare function setupsForGame(setups: TournamentSetupsResponse | null | undefined, gameDocumentId: string | null | undefined): {
|
|
105
|
+
templates: TournamentTemplateSummary[];
|
|
106
|
+
lastRun: LastRunSummary | null;
|
|
107
|
+
};
|
|
108
|
+
/** Badge counts for the game grid: templates for the game plus its last run. */
|
|
109
|
+
export declare function setupCountByGame(setups: TournamentSetupsResponse | null | undefined): Record<string, number>;
|
|
110
|
+
/**
|
|
111
|
+
* What the setup step starts on: the default template if the game has one,
|
|
112
|
+
* else the game's last run, else blank — so Continue is a single tap.
|
|
113
|
+
*/
|
|
114
|
+
export declare function preselectSetup(templates: TournamentTemplateSummary[], lastRun: LastRunSummary | null | undefined): SetupChoice;
|
|
115
|
+
/** The setup fields of a choice, or null for blank. */
|
|
116
|
+
export declare function setupOfChoice(choice: SetupChoice): TournamentSetupFields | null;
|
|
117
|
+
/** The name a choice was made under, for the provenance strip. */
|
|
118
|
+
export declare function nameOfChoice(choice: SetupChoice): string | null;
|
|
119
|
+
export interface DescribeSetupOptions {
|
|
120
|
+
/** `players` or `teams` for the team size. */
|
|
121
|
+
entryNoun: (teamSize: TournamentTeamSize) => string;
|
|
122
|
+
/** `1v1` … `5v5`. */
|
|
123
|
+
teamSizeLabel: (teamSize: TournamentTeamSize) => string;
|
|
124
|
+
/** `Free entry`, `50 XPT` — the app's own money formatting. */
|
|
125
|
+
feeLabel: (fee: number) => string;
|
|
126
|
+
/** Mention rules when the setup carries them. Default true. */
|
|
127
|
+
withRules?: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The detail line under a setup: `16 teams · 2v2 · 50 XPT · check-in 30 min
|
|
131
|
+
* · rules`. Nulls read as their defaults. The labels come from the caller
|
|
132
|
+
* so this package does not carry the app's copy.
|
|
133
|
+
*/
|
|
134
|
+
export declare function describeSetup(fields: Partial<TournamentSetupFields> | null | undefined, options: DescribeSetupOptions): string;
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
* Routes (all scoped to the caller):
|
|
15
|
+
* - `GET /tournament-templates/setups` → `{ data: TournamentSetupsResponse }`
|
|
16
|
+
* - `POST /tournament-templates` with `SaveTemplateBody` → `{ data: TournamentTemplateSummary }`
|
|
17
|
+
* - `PUT /tournament-templates/:id/default` → `{ data: TournamentTemplateSummary }`
|
|
18
|
+
* - `DELETE /tournament-templates/:id`
|
|
19
|
+
* - `POST /tournaments` accepts `entryFee`, `checkInTime`, `rules` and
|
|
20
|
+
* `template` (the documentId, for usage meta) alongside the usual fields.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.TOURNAMENT_SETUP_DEFAULTS = exports.TEMPLATE_COPY_KEYS = exports.MAX_TEMPLATES_PER_HOST = exports.LAST_RUN_WINDOW_DAYS = void 0;
|
|
24
|
+
exports.resolveSetup = resolveSetup;
|
|
25
|
+
exports.nextTournamentName = nextTournamentName;
|
|
26
|
+
exports.templateNameFrom = templateNameFrom;
|
|
27
|
+
exports.setupsForGame = setupsForGame;
|
|
28
|
+
exports.setupCountByGame = setupCountByGame;
|
|
29
|
+
exports.preselectSetup = preselectSetup;
|
|
30
|
+
exports.setupOfChoice = setupOfChoice;
|
|
31
|
+
exports.nameOfChoice = nameOfChoice;
|
|
32
|
+
exports.describeSetup = describeSetup;
|
|
33
|
+
/** How far back the automatic "last tournament" looks. */
|
|
34
|
+
exports.LAST_RUN_WINDOW_DAYS = 30;
|
|
35
|
+
/** Templates a host may keep; the save route refuses the next one. */
|
|
36
|
+
exports.MAX_TEMPLATES_PER_HOST = 20;
|
|
37
|
+
/** The settings a host may leave out of a template — the "Also copied" rows. */
|
|
38
|
+
exports.TEMPLATE_COPY_KEYS = [
|
|
39
|
+
'entryFee',
|
|
40
|
+
'checkInTime',
|
|
41
|
+
'hasMatchLobby',
|
|
42
|
+
'hasThirdPlace',
|
|
43
|
+
'rules',
|
|
44
|
+
];
|
|
45
|
+
/** What a blank tournament starts from, and what a null setting falls back to. */
|
|
46
|
+
exports.TOURNAMENT_SETUP_DEFAULTS = {
|
|
47
|
+
participants: 8,
|
|
48
|
+
teamSize: 'one',
|
|
49
|
+
entryFee: 0,
|
|
50
|
+
checkInTime: 0,
|
|
51
|
+
hasThirdPlace: false,
|
|
52
|
+
hasMatchLobby: true,
|
|
53
|
+
rules: null,
|
|
54
|
+
};
|
|
55
|
+
/** Every setting resolved: nulls replaced by the XPT default. */
|
|
56
|
+
function resolveSetup(fields) {
|
|
57
|
+
var _a, _b, _c, _d, _e, _f;
|
|
58
|
+
const source = fields !== null && fields !== void 0 ? fields : {};
|
|
59
|
+
return {
|
|
60
|
+
participants: source.participants || exports.TOURNAMENT_SETUP_DEFAULTS.participants,
|
|
61
|
+
teamSize: (_a = source.teamSize) !== null && _a !== void 0 ? _a : exports.TOURNAMENT_SETUP_DEFAULTS.teamSize,
|
|
62
|
+
entryFee: (_b = source.entryFee) !== null && _b !== void 0 ? _b : exports.TOURNAMENT_SETUP_DEFAULTS.entryFee,
|
|
63
|
+
checkInTime: (_c = source.checkInTime) !== null && _c !== void 0 ? _c : exports.TOURNAMENT_SETUP_DEFAULTS.checkInTime,
|
|
64
|
+
hasThirdPlace: (_d = source.hasThirdPlace) !== null && _d !== void 0 ? _d : exports.TOURNAMENT_SETUP_DEFAULTS.hasThirdPlace,
|
|
65
|
+
hasMatchLobby: (_e = source.hasMatchLobby) !== null && _e !== void 0 ? _e : exports.TOURNAMENT_SETUP_DEFAULTS.hasMatchLobby,
|
|
66
|
+
rules: (_f = source.rules) !== null && _f !== void 0 ? _f : exports.TOURNAMENT_SETUP_DEFAULTS.rules,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const TRAILING_COUNTER = /\s*#(\d+)$/;
|
|
70
|
+
/**
|
|
71
|
+
* The name a tournament started from a setup gets: a trailing `#N` is bumped
|
|
72
|
+
* (`Friday Night Ops #3` → `Friday Night Ops #4`); anything else is copied
|
|
73
|
+
* verbatim.
|
|
74
|
+
*/
|
|
75
|
+
function nextTournamentName(name) {
|
|
76
|
+
const match = TRAILING_COUNTER.exec(name);
|
|
77
|
+
if (!match)
|
|
78
|
+
return name;
|
|
79
|
+
const next = Number(match[1]) + 1;
|
|
80
|
+
return `${name.slice(0, match.index).trimEnd()} #${next}`;
|
|
81
|
+
}
|
|
82
|
+
/** A template name from a tournament title: the trailing `#N`, if any, dropped. */
|
|
83
|
+
function templateNameFrom(title) {
|
|
84
|
+
return title.replace(TRAILING_COUNTER, '').trim();
|
|
85
|
+
}
|
|
86
|
+
/** The setups offered for one game: its templates and, if any, its last run. */
|
|
87
|
+
function setupsForGame(setups, gameDocumentId) {
|
|
88
|
+
var _a;
|
|
89
|
+
if (!setups || !gameDocumentId)
|
|
90
|
+
return { templates: [], lastRun: null };
|
|
91
|
+
return {
|
|
92
|
+
templates: setups.templates.filter((t) => { var _a; return ((_a = t.game) === null || _a === void 0 ? void 0 : _a.documentId) === gameDocumentId; }),
|
|
93
|
+
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,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Badge counts for the game grid: templates for the game plus its last run. */
|
|
97
|
+
function setupCountByGame(setups) {
|
|
98
|
+
var _a, _b, _c, _d;
|
|
99
|
+
const counts = {};
|
|
100
|
+
if (!setups)
|
|
101
|
+
return counts;
|
|
102
|
+
for (const t of setups.templates) {
|
|
103
|
+
const id = (_a = t.game) === null || _a === void 0 ? void 0 : _a.documentId;
|
|
104
|
+
if (id)
|
|
105
|
+
counts[id] = ((_b = counts[id]) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
for (const r of setups.lastRuns) {
|
|
108
|
+
const id = (_c = r.game) === null || _c === void 0 ? void 0 : _c.documentId;
|
|
109
|
+
if (id)
|
|
110
|
+
counts[id] = ((_d = counts[id]) !== null && _d !== void 0 ? _d : 0) + 1;
|
|
111
|
+
}
|
|
112
|
+
return counts;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* What the setup step starts on: the default template if the game has one,
|
|
116
|
+
* else the game's last run, else blank — so Continue is a single tap.
|
|
117
|
+
*/
|
|
118
|
+
function preselectSetup(templates, lastRun) {
|
|
119
|
+
var _a;
|
|
120
|
+
const preferred = (_a = templates.find((t) => t.isDefault)) !== null && _a !== void 0 ? _a : templates[0];
|
|
121
|
+
if (preferred)
|
|
122
|
+
return { kind: 'template', template: preferred };
|
|
123
|
+
if (lastRun)
|
|
124
|
+
return { kind: 'lastRun', lastRun };
|
|
125
|
+
return { kind: 'blank' };
|
|
126
|
+
}
|
|
127
|
+
/** The setup fields of a choice, or null for blank. */
|
|
128
|
+
function setupOfChoice(choice) {
|
|
129
|
+
if (choice.kind === 'template')
|
|
130
|
+
return choice.template;
|
|
131
|
+
if (choice.kind === 'lastRun')
|
|
132
|
+
return choice.lastRun;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/** The name a choice was made under, for the provenance strip. */
|
|
136
|
+
function nameOfChoice(choice) {
|
|
137
|
+
if (choice.kind === 'template')
|
|
138
|
+
return choice.template.name;
|
|
139
|
+
if (choice.kind === 'lastRun')
|
|
140
|
+
return choice.lastRun.title;
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The detail line under a setup: `16 teams · 2v2 · 50 XPT · check-in 30 min
|
|
145
|
+
* · rules`. Nulls read as their defaults. The labels come from the caller
|
|
146
|
+
* so this package does not carry the app's copy.
|
|
147
|
+
*/
|
|
148
|
+
function describeSetup(fields, options) {
|
|
149
|
+
var _a;
|
|
150
|
+
const setup = resolveSetup(fields);
|
|
151
|
+
const parts = [
|
|
152
|
+
`${setup.participants} ${options.entryNoun(setup.teamSize)}`,
|
|
153
|
+
options.teamSizeLabel(setup.teamSize),
|
|
154
|
+
options.feeLabel((_a = setup.entryFee) !== null && _a !== void 0 ? _a : 0),
|
|
155
|
+
setup.checkInTime ? `check-in ${setup.checkInTime} min` : 'no check-in',
|
|
156
|
+
];
|
|
157
|
+
if (options.withRules !== false && setup.rules)
|
|
158
|
+
parts.push('rules');
|
|
159
|
+
return parts.join(' · ');
|
|
160
|
+
}
|
|
@@ -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`. */
|
|
@@ -416,6 +419,7 @@ export interface TournamentInput {
|
|
|
416
419
|
rules?: string;
|
|
417
420
|
hasThirdPlace?: boolean;
|
|
418
421
|
hasMatchLobby?: boolean;
|
|
422
|
+
requireVerifiedAccounts?: boolean;
|
|
419
423
|
visibility?: TournamentVisibility;
|
|
420
424
|
access?: TournamentAccess;
|
|
421
425
|
checkInTime?: number;
|
|
@@ -424,6 +428,9 @@ export interface TournamentInput {
|
|
|
424
428
|
gameAccount?: RelationInput;
|
|
425
429
|
tournament_stages?: unknown;
|
|
426
430
|
prizesDistributed?: boolean;
|
|
431
|
+
entryFeesCollected?: number;
|
|
432
|
+
entryFeesSettledAt?: string;
|
|
433
|
+
hostXpAwarded?: number;
|
|
427
434
|
discordUrl?: string;
|
|
428
435
|
twitterUrl?: string;
|
|
429
436
|
twitchUrl?: string;
|
|
@@ -472,6 +479,7 @@ export interface TournamentParticipantInput {
|
|
|
472
479
|
status?: TournamentParticipantStatus;
|
|
473
480
|
captainUser?: RelationInput;
|
|
474
481
|
membersSnapshot?: unknown;
|
|
482
|
+
feePaid?: number;
|
|
475
483
|
}
|
|
476
484
|
|
|
477
485
|
/** Write payload for `tournament-preset-banner`. */
|
|
@@ -486,6 +494,23 @@ export interface TournamentRoleInput {
|
|
|
486
494
|
tournament?: RelationInput;
|
|
487
495
|
}
|
|
488
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
|
+
isDefault?: boolean;
|
|
510
|
+
useCount?: number;
|
|
511
|
+
lastUsedAt?: string;
|
|
512
|
+
}
|
|
513
|
+
|
|
489
514
|
/** Write payload for `user`. */
|
|
490
515
|
export interface UserInput {
|
|
491
516
|
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` */
|
|
@@ -417,6 +420,7 @@ export interface Tournament extends StrapiDocument {
|
|
|
417
420
|
rules?: string | null;
|
|
418
421
|
hasThirdPlace?: boolean | null;
|
|
419
422
|
hasMatchLobby?: boolean | null;
|
|
423
|
+
requireVerifiedAccounts?: boolean | null;
|
|
420
424
|
visibility?: TournamentVisibility | null;
|
|
421
425
|
access?: TournamentAccess | null;
|
|
422
426
|
checkInTime?: number | null;
|
|
@@ -425,6 +429,9 @@ export interface Tournament extends StrapiDocument {
|
|
|
425
429
|
gameAccount?: GameAccount | null;
|
|
426
430
|
tournament_stages?: unknown | null;
|
|
427
431
|
prizesDistributed?: boolean | null;
|
|
432
|
+
entryFeesCollected?: number | null;
|
|
433
|
+
entryFeesSettledAt?: string | null;
|
|
434
|
+
hostXpAwarded?: number | null;
|
|
428
435
|
discordUrl?: string | null;
|
|
429
436
|
twitterUrl?: string | null;
|
|
430
437
|
twitchUrl?: string | null;
|
|
@@ -473,6 +480,7 @@ export interface TournamentParticipant extends StrapiDocument {
|
|
|
473
480
|
status?: TournamentParticipantStatus | null;
|
|
474
481
|
captainUser?: User | null;
|
|
475
482
|
membersSnapshot?: unknown | null;
|
|
483
|
+
feePaid?: number | null;
|
|
476
484
|
}
|
|
477
485
|
|
|
478
486
|
/** `tournament-preset-banner` */
|
|
@@ -487,6 +495,23 @@ export interface TournamentRole extends StrapiDocument {
|
|
|
487
495
|
tournament?: Tournament | null;
|
|
488
496
|
}
|
|
489
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
|
+
isDefault?: boolean | null;
|
|
511
|
+
useCount?: number | null;
|
|
512
|
+
lastUsedAt?: string | null;
|
|
513
|
+
}
|
|
514
|
+
|
|
490
515
|
/** `user` */
|
|
491
516
|
export interface User extends StrapiDocument {
|
|
492
517
|
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';
|
|
@@ -51,4 +51,8 @@ export type TournamentEntryErrorName =
|
|
|
51
51
|
| 'MembersMissingGameAccount'
|
|
52
52
|
| 'InsufficientCoins'
|
|
53
53
|
| 'GameAccountRequired'
|
|
54
|
-
| 'TeamEntryRequired'
|
|
54
|
+
| 'TeamEntryRequired'
|
|
55
|
+
/** The tournament admits only accounts verified by signing in; yours is typed. */
|
|
56
|
+
| 'GameAccountUnverified'
|
|
57
|
+
/** Same, for the named players on a team roster. */
|
|
58
|
+
| 'MembersUnverifiedGameAccount';
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tournament templates — the contract between `utils/tournamentTemplate.ts`
|
|
3
|
+
* in xpt-strapi and the create dialog's setup step, the post-publish
|
|
4
|
+
* "Save as template" prompt and the Settings → Templates page in xpt-client.
|
|
5
|
+
*
|
|
6
|
+
* A template holds settings only: never entries, dates or results. A null
|
|
7
|
+
* setting means the host chose not to save it and the XPT default applies
|
|
8
|
+
* (`resolveSetup`). Alongside the host's templates, the API derives their
|
|
9
|
+
* **last tournament per game** — the newest one taken out of draft in the
|
|
10
|
+
* last `LAST_RUN_WINDOW_DAYS` — so a host can repeat a night without ever
|
|
11
|
+
* having saved anything.
|
|
12
|
+
*
|
|
13
|
+
* Routes (all scoped to the caller):
|
|
14
|
+
* - `GET /tournament-templates/setups` → `{ data: TournamentSetupsResponse }`
|
|
15
|
+
* - `POST /tournament-templates` with `SaveTemplateBody` → `{ data: TournamentTemplateSummary }`
|
|
16
|
+
* - `PUT /tournament-templates/:id/default` → `{ data: TournamentTemplateSummary }`
|
|
17
|
+
* - `DELETE /tournament-templates/:id`
|
|
18
|
+
* - `POST /tournaments` accepts `entryFee`, `checkInTime`, `rules` and
|
|
19
|
+
* `template` (the documentId, for usage meta) alongside the usual fields.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { StrapiMedia } from '../contracts';
|
|
23
|
+
import type { TournamentTeamSize } from '../generated/enums';
|
|
24
|
+
|
|
25
|
+
/** How far back the automatic "last tournament" looks. */
|
|
26
|
+
export const LAST_RUN_WINDOW_DAYS = 30;
|
|
27
|
+
|
|
28
|
+
/** Templates a host may keep; the save route refuses the next one. */
|
|
29
|
+
export const MAX_TEMPLATES_PER_HOST = 20;
|
|
30
|
+
|
|
31
|
+
/** The settings a host may leave out of a template — the "Also copied" rows. */
|
|
32
|
+
export const TEMPLATE_COPY_KEYS = [
|
|
33
|
+
'entryFee',
|
|
34
|
+
'checkInTime',
|
|
35
|
+
'hasMatchLobby',
|
|
36
|
+
'hasThirdPlace',
|
|
37
|
+
'rules',
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type TemplateCopyKey = (typeof TEMPLATE_COPY_KEYS)[number];
|
|
41
|
+
export type TemplateIncludeFlags = Record<TemplateCopyKey, boolean>;
|
|
42
|
+
|
|
43
|
+
export interface TournamentSetupFields {
|
|
44
|
+
participants: number;
|
|
45
|
+
teamSize: TournamentTeamSize;
|
|
46
|
+
/** Null: not saved, XPT default applies. */
|
|
47
|
+
entryFee: number | null;
|
|
48
|
+
/** Minutes; 0 is "no check-in". Null: not saved. */
|
|
49
|
+
checkInTime: number | null;
|
|
50
|
+
hasThirdPlace: boolean | null;
|
|
51
|
+
hasMatchLobby: boolean | null;
|
|
52
|
+
rules: string | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What a blank tournament starts from, and what a null setting falls back to. */
|
|
56
|
+
export const TOURNAMENT_SETUP_DEFAULTS: TournamentSetupFields = {
|
|
57
|
+
participants: 8,
|
|
58
|
+
teamSize: 'one',
|
|
59
|
+
entryFee: 0,
|
|
60
|
+
checkInTime: 0,
|
|
61
|
+
hasThirdPlace: false,
|
|
62
|
+
hasMatchLobby: true,
|
|
63
|
+
rules: null,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Enough of a game for the client to open the create dialog on it. */
|
|
67
|
+
export interface SetupGame {
|
|
68
|
+
id: number;
|
|
69
|
+
documentId: string;
|
|
70
|
+
title: string | null;
|
|
71
|
+
slug: string | null;
|
|
72
|
+
imgThumb: StrapiMedia | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A saved template as the routes return it. */
|
|
76
|
+
export interface TournamentTemplateSummary extends TournamentSetupFields {
|
|
77
|
+
documentId: string;
|
|
78
|
+
name: string;
|
|
79
|
+
game: SetupGame | null;
|
|
80
|
+
isDefault: boolean;
|
|
81
|
+
useCount: number;
|
|
82
|
+
lastUsedAt: string | null;
|
|
83
|
+
createdAt: string | null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The host's last tournament for one game, derived at read time. */
|
|
87
|
+
export interface LastRunSummary extends TournamentSetupFields {
|
|
88
|
+
documentId: string;
|
|
89
|
+
title: string;
|
|
90
|
+
slug: string;
|
|
91
|
+
game: SetupGame | null;
|
|
92
|
+
dateTime: string | null;
|
|
93
|
+
createdAt: string | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `GET /tournament-templates/setups` — templates default first, runs newest first. */
|
|
97
|
+
export interface TournamentSetupsResponse {
|
|
98
|
+
templates: TournamentTemplateSummary[];
|
|
99
|
+
lastRuns: LastRunSummary[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** `POST /tournament-templates`. Missing include flags read as ticked. */
|
|
103
|
+
export interface SaveTemplateBody {
|
|
104
|
+
tournamentDocumentId: string;
|
|
105
|
+
name: string;
|
|
106
|
+
include?: Partial<TemplateIncludeFlags>;
|
|
107
|
+
isDefault?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What the setup step offers, and what the details step was prefilled from. */
|
|
111
|
+
export type SetupChoice =
|
|
112
|
+
| { kind: 'template'; template: TournamentTemplateSummary }
|
|
113
|
+
| { kind: 'lastRun'; lastRun: LastRunSummary }
|
|
114
|
+
| { kind: 'blank' };
|
|
115
|
+
|
|
116
|
+
/** Every setting resolved: nulls replaced by the XPT default. */
|
|
117
|
+
export function resolveSetup(
|
|
118
|
+
fields: Partial<TournamentSetupFields> | null | undefined
|
|
119
|
+
): TournamentSetupFields {
|
|
120
|
+
const source = fields ?? {};
|
|
121
|
+
return {
|
|
122
|
+
participants: source.participants || TOURNAMENT_SETUP_DEFAULTS.participants,
|
|
123
|
+
teamSize: source.teamSize ?? TOURNAMENT_SETUP_DEFAULTS.teamSize,
|
|
124
|
+
entryFee: source.entryFee ?? TOURNAMENT_SETUP_DEFAULTS.entryFee,
|
|
125
|
+
checkInTime: source.checkInTime ?? TOURNAMENT_SETUP_DEFAULTS.checkInTime,
|
|
126
|
+
hasThirdPlace: source.hasThirdPlace ?? TOURNAMENT_SETUP_DEFAULTS.hasThirdPlace,
|
|
127
|
+
hasMatchLobby: source.hasMatchLobby ?? TOURNAMENT_SETUP_DEFAULTS.hasMatchLobby,
|
|
128
|
+
rules: source.rules ?? TOURNAMENT_SETUP_DEFAULTS.rules,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const TRAILING_COUNTER = /\s*#(\d+)$/;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The name a tournament started from a setup gets: a trailing `#N` is bumped
|
|
136
|
+
* (`Friday Night Ops #3` → `Friday Night Ops #4`); anything else is copied
|
|
137
|
+
* verbatim.
|
|
138
|
+
*/
|
|
139
|
+
export function nextTournamentName(name: string): string {
|
|
140
|
+
const match = TRAILING_COUNTER.exec(name);
|
|
141
|
+
if (!match) return name;
|
|
142
|
+
const next = Number(match[1]) + 1;
|
|
143
|
+
return `${name.slice(0, match.index).trimEnd()} #${next}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A template name from a tournament title: the trailing `#N`, if any, dropped. */
|
|
147
|
+
export function templateNameFrom(title: string): string {
|
|
148
|
+
return title.replace(TRAILING_COUNTER, '').trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The setups offered for one game: its templates and, if any, its last run. */
|
|
152
|
+
export function setupsForGame(
|
|
153
|
+
setups: TournamentSetupsResponse | null | undefined,
|
|
154
|
+
gameDocumentId: string | null | undefined
|
|
155
|
+
): { templates: TournamentTemplateSummary[]; lastRun: LastRunSummary | null } {
|
|
156
|
+
if (!setups || !gameDocumentId) return { templates: [], lastRun: null };
|
|
157
|
+
return {
|
|
158
|
+
templates: setups.templates.filter(
|
|
159
|
+
(t) => t.game?.documentId === gameDocumentId
|
|
160
|
+
),
|
|
161
|
+
lastRun:
|
|
162
|
+
setups.lastRuns.find((r) => r.game?.documentId === gameDocumentId) ??
|
|
163
|
+
null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Badge counts for the game grid: templates for the game plus its last run. */
|
|
168
|
+
export function setupCountByGame(
|
|
169
|
+
setups: TournamentSetupsResponse | null | undefined
|
|
170
|
+
): Record<string, number> {
|
|
171
|
+
const counts: Record<string, number> = {};
|
|
172
|
+
if (!setups) return counts;
|
|
173
|
+
for (const t of setups.templates) {
|
|
174
|
+
const id = t.game?.documentId;
|
|
175
|
+
if (id) counts[id] = (counts[id] ?? 0) + 1;
|
|
176
|
+
}
|
|
177
|
+
for (const r of setups.lastRuns) {
|
|
178
|
+
const id = r.game?.documentId;
|
|
179
|
+
if (id) counts[id] = (counts[id] ?? 0) + 1;
|
|
180
|
+
}
|
|
181
|
+
return counts;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* What the setup step starts on: the default template if the game has one,
|
|
186
|
+
* else the game's last run, else blank — so Continue is a single tap.
|
|
187
|
+
*/
|
|
188
|
+
export function preselectSetup(
|
|
189
|
+
templates: TournamentTemplateSummary[],
|
|
190
|
+
lastRun: LastRunSummary | null | undefined
|
|
191
|
+
): SetupChoice {
|
|
192
|
+
const preferred = templates.find((t) => t.isDefault) ?? templates[0];
|
|
193
|
+
if (preferred) return { kind: 'template', template: preferred };
|
|
194
|
+
if (lastRun) return { kind: 'lastRun', lastRun };
|
|
195
|
+
return { kind: 'blank' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The setup fields of a choice, or null for blank. */
|
|
199
|
+
export function setupOfChoice(
|
|
200
|
+
choice: SetupChoice
|
|
201
|
+
): TournamentSetupFields | null {
|
|
202
|
+
if (choice.kind === 'template') return choice.template;
|
|
203
|
+
if (choice.kind === 'lastRun') return choice.lastRun;
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The name a choice was made under, for the provenance strip. */
|
|
208
|
+
export function nameOfChoice(choice: SetupChoice): string | null {
|
|
209
|
+
if (choice.kind === 'template') return choice.template.name;
|
|
210
|
+
if (choice.kind === 'lastRun') return choice.lastRun.title;
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface DescribeSetupOptions {
|
|
215
|
+
/** `players` or `teams` for the team size. */
|
|
216
|
+
entryNoun: (teamSize: TournamentTeamSize) => string;
|
|
217
|
+
/** `1v1` … `5v5`. */
|
|
218
|
+
teamSizeLabel: (teamSize: TournamentTeamSize) => string;
|
|
219
|
+
/** `Free entry`, `50 XPT` — the app's own money formatting. */
|
|
220
|
+
feeLabel: (fee: number) => string;
|
|
221
|
+
/** Mention rules when the setup carries them. Default true. */
|
|
222
|
+
withRules?: boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The detail line under a setup: `16 teams · 2v2 · 50 XPT · check-in 30 min
|
|
227
|
+
* · rules`. Nulls read as their defaults. The labels come from the caller
|
|
228
|
+
* so this package does not carry the app's copy.
|
|
229
|
+
*/
|
|
230
|
+
export function describeSetup(
|
|
231
|
+
fields: Partial<TournamentSetupFields> | null | undefined,
|
|
232
|
+
options: DescribeSetupOptions
|
|
233
|
+
): string {
|
|
234
|
+
const setup = resolveSetup(fields);
|
|
235
|
+
const parts = [
|
|
236
|
+
`${setup.participants} ${options.entryNoun(setup.teamSize)}`,
|
|
237
|
+
options.teamSizeLabel(setup.teamSize),
|
|
238
|
+
options.feeLabel(setup.entryFee ?? 0),
|
|
239
|
+
setup.checkInTime ? `check-in ${setup.checkInTime} min` : 'no check-in',
|
|
240
|
+
];
|
|
241
|
+
if (options.withRules !== false && setup.rules) parts.push('rules');
|
|
242
|
+
return parts.join(' · ');
|
|
243
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
|
|
27
|
+
/** How many tournaments `postPublishPending` remembers; oldest drop first. */
|
|
28
|
+
export const POST_PUBLISH_PENDING_LIMIT = 20;
|
|
29
|
+
|
|
30
|
+
/** How many tournaments `templatePromptDismissed` remembers; oldest drop first. */
|
|
31
|
+
export const TEMPLATE_PROMPT_DISMISSED_LIMIT = 20;
|