volleyballsimtypes 0.0.522 → 0.0.523
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/cjs/src/data/init-models.js +4 -0
- package/dist/cjs/src/data/models/currency-transaction.d.ts +3 -2
- package/dist/cjs/src/data/models/currency-transaction.js +1 -0
- package/dist/cjs/src/data/models/index.d.ts +1 -0
- package/dist/cjs/src/data/models/index.js +1 -0
- package/dist/cjs/src/data/models/user-achievements.d.ts +19 -0
- package/dist/cjs/src/data/models/user-achievements.js +37 -0
- package/dist/cjs/src/service/achievement/achievement-status.d.ts +5 -0
- package/dist/cjs/src/service/achievement/achievement-status.js +13 -0
- package/dist/cjs/src/service/achievement/achievement-type.d.ts +24 -0
- package/dist/cjs/src/service/achievement/achievement-type.js +85 -0
- package/dist/cjs/src/service/achievement/index.d.ts +3 -0
- package/dist/cjs/src/service/achievement/index.js +19 -0
- package/dist/cjs/src/service/achievement/schemas/achievements.z.d.ts +3 -0
- package/dist/cjs/src/service/achievement/schemas/achievements.z.js +9 -0
- package/dist/cjs/src/service/index.d.ts +1 -0
- package/dist/cjs/src/service/index.js +1 -0
- package/dist/cjs/src/service/team/base-config.js +8 -4
- package/dist/cjs/src/service/team/default-base-config.js +11 -9
- package/dist/cjs/src/service/team/default-base-config.test.js +21 -0
- package/dist/esm/src/data/init-models.js +5 -1
- package/dist/esm/src/data/models/currency-transaction.d.ts +3 -2
- package/dist/esm/src/data/models/currency-transaction.js +1 -0
- package/dist/esm/src/data/models/index.d.ts +1 -0
- package/dist/esm/src/data/models/index.js +1 -0
- package/dist/esm/src/data/models/user-achievements.d.ts +19 -0
- package/dist/esm/src/data/models/user-achievements.js +33 -0
- package/dist/esm/src/service/achievement/achievement-status.d.ts +5 -0
- package/dist/esm/src/service/achievement/achievement-status.js +10 -0
- package/dist/esm/src/service/achievement/achievement-type.d.ts +24 -0
- package/dist/esm/src/service/achievement/achievement-type.js +77 -0
- package/dist/esm/src/service/achievement/index.d.ts +3 -0
- package/dist/esm/src/service/achievement/index.js +3 -0
- package/dist/esm/src/service/achievement/schemas/achievements.z.d.ts +3 -0
- package/dist/esm/src/service/achievement/schemas/achievements.z.js +6 -0
- package/dist/esm/src/service/index.d.ts +1 -0
- package/dist/esm/src/service/index.js +1 -0
- package/dist/esm/src/service/team/base-config.js +8 -4
- package/dist/esm/src/service/team/default-base-config.js +11 -9
- package/dist/esm/src/service/team/default-base-config.test.js +22 -1
- package/package.json +1 -1
|
@@ -28,6 +28,7 @@ function initModels(sequelize) {
|
|
|
28
28
|
const PremiumGrant = models_1.PremiumGrantModel.initModel(sequelize);
|
|
29
29
|
const Wishlist = models_1.WishlistModel.initModel(sequelize);
|
|
30
30
|
const UserSettings = models_1.UserSettingsModel.initModel(sequelize);
|
|
31
|
+
const UserAchievements = models_1.UserAchievementsModel.initModel(sequelize);
|
|
31
32
|
const UserDeviceToken = models_1.UserDeviceTokenModel.initModel(sequelize);
|
|
32
33
|
const BasePositionPreset = models_1.BasePositionPresetModel.initModel(sequelize);
|
|
33
34
|
const Notification = models_1.NotificationModel.initModel(sequelize);
|
|
@@ -78,6 +79,7 @@ function initModels(sequelize) {
|
|
|
78
79
|
AuthUser.hasOne(GachaPity, { as: 'GachaPity', foreignKey: 'user_id' });
|
|
79
80
|
AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
|
|
80
81
|
AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
|
|
82
|
+
AuthUser.hasOne(UserAchievements, { as: 'UserAchievements', foreignKey: 'user_id' });
|
|
81
83
|
AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
|
|
82
84
|
AuthUser.hasMany(BasePositionPreset, { as: 'BasePositionPresets', foreignKey: 'user_id' });
|
|
83
85
|
AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
|
|
@@ -101,6 +103,7 @@ function initModels(sequelize) {
|
|
|
101
103
|
PremiumGrant.belongsTo(AuthUser, { as: 'grantedByUser', foreignKey: 'granted_by' });
|
|
102
104
|
Wishlist.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
103
105
|
UserSettings.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
106
|
+
UserAchievements.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
104
107
|
Competition.belongsTo(Iteration, { as: 'Iteration', foreignKey: 'iteration' });
|
|
105
108
|
Competition.hasMany(CompetitionMatch, { as: 'CompetitionMatches', foreignKey: 'competition_id' });
|
|
106
109
|
Competition.hasMany(CompetitionStandings, { as: 'CompetitionStandings', foreignKey: 'competition_id' });
|
|
@@ -370,6 +373,7 @@ function initModels(sequelize) {
|
|
|
370
373
|
PremiumGrant,
|
|
371
374
|
Wishlist,
|
|
372
375
|
UserSettings,
|
|
376
|
+
UserAchievements,
|
|
373
377
|
UserDeviceToken,
|
|
374
378
|
BasePositionPreset,
|
|
375
379
|
Notification,
|
|
@@ -17,9 +17,10 @@ export declare enum CurrencyTransactionSourceEnum {
|
|
|
17
17
|
SEASON_REWARD = "SEASON_REWARD",
|
|
18
18
|
COMPETITION_REWARD = "COMPETITION_REWARD",
|
|
19
19
|
PHYSICIAN_UPGRADE = "PHYSICIAN_UPGRADE",
|
|
20
|
-
PHYSIOTHERAPIST_UPGRADE = "PHYSIOTHERAPIST_UPGRADE"
|
|
20
|
+
PHYSIOTHERAPIST_UPGRADE = "PHYSIOTHERAPIST_UPGRADE",
|
|
21
|
+
ACHIEVEMENT_CLAIM = "ACHIEVEMENT_CLAIM"
|
|
21
22
|
}
|
|
22
|
-
export type CurrencyTransactionSource = CurrencyTransactionSourceEnum.MATCH_REWARD | CurrencyTransactionSourceEnum.GACHA_PULL | CurrencyTransactionSourceEnum.COACH_UPGRADE | CurrencyTransactionSourceEnum.SCOUT_UPGRADE | CurrencyTransactionSourceEnum.PLAYER_DISMISS | CurrencyTransactionSourceEnum.PLAYER_RETIRE | CurrencyTransactionSourceEnum.ADMIN_GRANT | CurrencyTransactionSourceEnum.SEASON_REWARD | CurrencyTransactionSourceEnum.COMPETITION_REWARD | CurrencyTransactionSourceEnum.PHYSICIAN_UPGRADE | CurrencyTransactionSourceEnum.PHYSIOTHERAPIST_UPGRADE;
|
|
23
|
+
export type CurrencyTransactionSource = CurrencyTransactionSourceEnum.MATCH_REWARD | CurrencyTransactionSourceEnum.GACHA_PULL | CurrencyTransactionSourceEnum.COACH_UPGRADE | CurrencyTransactionSourceEnum.SCOUT_UPGRADE | CurrencyTransactionSourceEnum.PLAYER_DISMISS | CurrencyTransactionSourceEnum.PLAYER_RETIRE | CurrencyTransactionSourceEnum.ADMIN_GRANT | CurrencyTransactionSourceEnum.SEASON_REWARD | CurrencyTransactionSourceEnum.COMPETITION_REWARD | CurrencyTransactionSourceEnum.PHYSICIAN_UPGRADE | CurrencyTransactionSourceEnum.PHYSIOTHERAPIST_UPGRADE | CurrencyTransactionSourceEnum.ACHIEVEMENT_CLAIM;
|
|
23
24
|
export interface CurrencyTransactionAttributes {
|
|
24
25
|
transaction_id: string;
|
|
25
26
|
user_id: string;
|
|
@@ -20,6 +20,7 @@ var CurrencyTransactionSourceEnum;
|
|
|
20
20
|
CurrencyTransactionSourceEnum["COMPETITION_REWARD"] = "COMPETITION_REWARD";
|
|
21
21
|
CurrencyTransactionSourceEnum["PHYSICIAN_UPGRADE"] = "PHYSICIAN_UPGRADE";
|
|
22
22
|
CurrencyTransactionSourceEnum["PHYSIOTHERAPIST_UPGRADE"] = "PHYSIOTHERAPIST_UPGRADE";
|
|
23
|
+
CurrencyTransactionSourceEnum["ACHIEVEMENT_CLAIM"] = "ACHIEVEMENT_CLAIM";
|
|
23
24
|
})(CurrencyTransactionSourceEnum || (exports.CurrencyTransactionSourceEnum = CurrencyTransactionSourceEnum = {}));
|
|
24
25
|
class CurrencyTransactionModel extends sequelize_1.Model {
|
|
25
26
|
static initModel(sequelize) {
|
|
@@ -58,6 +58,7 @@ export * from './wishlist';
|
|
|
58
58
|
export * from './currency-transaction';
|
|
59
59
|
export * from './user-pull-grant';
|
|
60
60
|
export * from './user-settings';
|
|
61
|
+
export * from './user-achievements';
|
|
61
62
|
export * from './user-device-token';
|
|
62
63
|
export * from './notification';
|
|
63
64
|
export * from './vper';
|
|
@@ -74,6 +74,7 @@ __exportStar(require("./wishlist"), exports);
|
|
|
74
74
|
__exportStar(require("./currency-transaction"), exports);
|
|
75
75
|
__exportStar(require("./user-pull-grant"), exports);
|
|
76
76
|
__exportStar(require("./user-settings"), exports);
|
|
77
|
+
__exportStar(require("./user-achievements"), exports);
|
|
77
78
|
__exportStar(require("./user-device-token"), exports);
|
|
78
79
|
__exportStar(require("./notification"), exports);
|
|
79
80
|
__exportStar(require("./vper"), exports);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { AuthUserId, AuthUserModel } from '.';
|
|
4
|
+
export interface UserAchievementsAttributes {
|
|
5
|
+
user_id: string;
|
|
6
|
+
achievements: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export type UserAchievementsPk = 'user_id';
|
|
9
|
+
export type UserAchievementsId = UserAchievementsModel[UserAchievementsPk];
|
|
10
|
+
export type UserAchievementsCreationAttributes = Sequelize.Optional<UserAchievementsAttributes, 'achievements'>;
|
|
11
|
+
export declare class UserAchievementsModel extends Model<UserAchievementsAttributes, UserAchievementsCreationAttributes> implements UserAchievementsAttributes {
|
|
12
|
+
user_id: string;
|
|
13
|
+
achievements: Record<string, string>;
|
|
14
|
+
user: AuthUserModel;
|
|
15
|
+
getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
16
|
+
setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
|
|
17
|
+
createUser: Sequelize.BelongsToCreateAssociationMixin<AuthUserModel>;
|
|
18
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof UserAchievementsModel;
|
|
19
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserAchievementsModel = void 0;
|
|
4
|
+
const sequelize_1 = require("sequelize");
|
|
5
|
+
class UserAchievementsModel extends sequelize_1.Model {
|
|
6
|
+
static initModel(sequelize) {
|
|
7
|
+
return UserAchievementsModel.init({
|
|
8
|
+
user_id: {
|
|
9
|
+
type: sequelize_1.DataTypes.UUID,
|
|
10
|
+
allowNull: false,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
references: {
|
|
13
|
+
model: 'AuthUser',
|
|
14
|
+
key: 'user_id'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
achievements: {
|
|
18
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
19
|
+
allowNull: false,
|
|
20
|
+
defaultValue: {}
|
|
21
|
+
}
|
|
22
|
+
}, {
|
|
23
|
+
sequelize,
|
|
24
|
+
tableName: 'UserAchievements',
|
|
25
|
+
schema: 'public',
|
|
26
|
+
timestamps: false,
|
|
27
|
+
indexes: [
|
|
28
|
+
{
|
|
29
|
+
name: 'UserAchievements_pk',
|
|
30
|
+
unique: true,
|
|
31
|
+
fields: [{ name: 'user_id' }]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.UserAchievementsModel = UserAchievementsModel;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AchievementStatus = void 0;
|
|
4
|
+
// Per-user achievement progress state. Stored only inside the UserAchievements.achievements jsonb map
|
|
5
|
+
// (achievementKey -> status); an absent key means NOT_DONE, so no data backfill is ever needed when new
|
|
6
|
+
// achievements are added. Because it lives only inside jsonb it is NOT a Sequelize DataTypes.ENUM and has no
|
|
7
|
+
// Postgres CREATE TYPE (mirrors how Tactics stores RoleEnum values inside jsonb without a DB enum).
|
|
8
|
+
var AchievementStatus;
|
|
9
|
+
(function (AchievementStatus) {
|
|
10
|
+
AchievementStatus["NOT_DONE"] = "NOT_DONE";
|
|
11
|
+
AchievementStatus["DONE"] = "DONE";
|
|
12
|
+
AchievementStatus["CLAIMED"] = "CLAIMED";
|
|
13
|
+
})(AchievementStatus || (exports.AchievementStatus = AchievementStatus = {}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AchievementStatus } from './achievement-status';
|
|
2
|
+
export declare const ACHIEVEMENT_KEYS: readonly ["NORMAL_RECRUIT", "SPECIAL_RECRUIT", "DISMISS_PLAYER", "RETIRE_PLAYER", "GIVE_NICKNAME", "PLAY_MATCH", "WIN_MATCH", "FINISH_LEAGUE_SEASON", "UPGRADE_COACH", "UPGRADE_SCOUT", "UPGRADE_PHYSICIAN", "UPGRADE_PHYSIOTHERAPIST", "CHANGE_TRAINING_FOCUS", "CHANGE_TACTICS", "REGISTER_EVENT", "WIN_LEAGUE", "WIN_NATIONALS", "WIN_REGIONALS", "WIN_WCC", "READ_WIKI", "CLAIM_DAILY", "VIEW_MATCH", "PREVIEW_MATCH"];
|
|
3
|
+
export type AchievementKey = typeof ACHIEVEMENT_KEYS[number];
|
|
4
|
+
export declare const ACHIEVEMENT_REWARD = 500;
|
|
5
|
+
export declare const MILESTONE_ACHIEVEMENTS: readonly AchievementKey[];
|
|
6
|
+
export declare function isMilestone(key: AchievementKey): boolean;
|
|
7
|
+
export declare const MILESTONE_THRESHOLDS: readonly number[];
|
|
8
|
+
export declare function nextMilestone(claimedLevel: number): number | null;
|
|
9
|
+
export interface MilestoneProgress {
|
|
10
|
+
c: number;
|
|
11
|
+
l: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MilestoneState {
|
|
14
|
+
count: number;
|
|
15
|
+
claimed: number;
|
|
16
|
+
target: number | null;
|
|
17
|
+
claimable: boolean;
|
|
18
|
+
maxed: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function milestoneState(value: unknown): MilestoneState;
|
|
21
|
+
export type AchievementMap = Partial<Record<AchievementKey, AchievementStatus | MilestoneProgress>>;
|
|
22
|
+
export declare const CLIENT_COMPLETABLE_ACHIEVEMENTS: readonly AchievementKey[];
|
|
23
|
+
export declare function isAchievementKey(value: string): value is AchievementKey;
|
|
24
|
+
export declare function isClientCompletable(key: AchievementKey): boolean;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLIENT_COMPLETABLE_ACHIEVEMENTS = exports.MILESTONE_THRESHOLDS = exports.MILESTONE_ACHIEVEMENTS = exports.ACHIEVEMENT_REWARD = exports.ACHIEVEMENT_KEYS = void 0;
|
|
4
|
+
exports.isMilestone = isMilestone;
|
|
5
|
+
exports.nextMilestone = nextMilestone;
|
|
6
|
+
exports.milestoneState = milestoneState;
|
|
7
|
+
exports.isAchievementKey = isAchievementKey;
|
|
8
|
+
exports.isClientCompletable = isClientCompletable;
|
|
9
|
+
// Single source of truth for the set of achievement keys (the tutorial checklist). Adding an achievement:
|
|
10
|
+
// 1. add its key here,
|
|
11
|
+
// 2. add its display config in the UI (src/view/achievements/achievements-config.ts),
|
|
12
|
+
// 3. add its name + description to every locale file,
|
|
13
|
+
// 4. wire its completion trigger (API handler or sim engine) to call completeAchievement.
|
|
14
|
+
// The key is only ever a jsonb map key, so there is no Postgres enum/column to migrate when the list grows.
|
|
15
|
+
exports.ACHIEVEMENT_KEYS = [
|
|
16
|
+
'NORMAL_RECRUIT',
|
|
17
|
+
'SPECIAL_RECRUIT',
|
|
18
|
+
'DISMISS_PLAYER',
|
|
19
|
+
'RETIRE_PLAYER',
|
|
20
|
+
'GIVE_NICKNAME',
|
|
21
|
+
'PLAY_MATCH',
|
|
22
|
+
'WIN_MATCH',
|
|
23
|
+
'FINISH_LEAGUE_SEASON',
|
|
24
|
+
'UPGRADE_COACH',
|
|
25
|
+
'UPGRADE_SCOUT',
|
|
26
|
+
'UPGRADE_PHYSICIAN',
|
|
27
|
+
'UPGRADE_PHYSIOTHERAPIST',
|
|
28
|
+
'CHANGE_TRAINING_FOCUS',
|
|
29
|
+
'CHANGE_TACTICS',
|
|
30
|
+
'REGISTER_EVENT',
|
|
31
|
+
'WIN_LEAGUE',
|
|
32
|
+
'WIN_NATIONALS',
|
|
33
|
+
'WIN_REGIONALS',
|
|
34
|
+
'WIN_WCC',
|
|
35
|
+
'READ_WIKI',
|
|
36
|
+
'CLAIM_DAILY',
|
|
37
|
+
'VIEW_MATCH',
|
|
38
|
+
'PREVIEW_MATCH'
|
|
39
|
+
];
|
|
40
|
+
// Every achievement grants the same flat coin reward when the user claims it (each milestone reached is a fresh claim).
|
|
41
|
+
exports.ACHIEVEMENT_REWARD = 500;
|
|
42
|
+
// Recurring "milestone" achievements: instead of a one-shot NOT_DONE->DONE->CLAIMED status, these store a running count
|
|
43
|
+
// and re-arm at escalating thresholds. Their map value is a MilestoneProgress object, not a status string.
|
|
44
|
+
exports.MILESTONE_ACHIEVEMENTS = [
|
|
45
|
+
'CLAIM_DAILY',
|
|
46
|
+
'PLAY_MATCH',
|
|
47
|
+
'WIN_MATCH',
|
|
48
|
+
'NORMAL_RECRUIT',
|
|
49
|
+
'DISMISS_PLAYER',
|
|
50
|
+
'WIN_LEAGUE',
|
|
51
|
+
'WIN_NATIONALS',
|
|
52
|
+
'WIN_REGIONALS',
|
|
53
|
+
'WIN_WCC'
|
|
54
|
+
];
|
|
55
|
+
function isMilestone(key) {
|
|
56
|
+
return exports.MILESTONE_ACHIEVEMENTS.includes(key);
|
|
57
|
+
}
|
|
58
|
+
// The claim thresholds every milestone steps through (x5, x2 alternating). Past the last one the milestone is "maxed".
|
|
59
|
+
exports.MILESTONE_THRESHOLDS = [1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000];
|
|
60
|
+
// The next threshold strictly greater than `claimedLevel` (0 = nothing claimed yet), or null when all are claimed.
|
|
61
|
+
function nextMilestone(claimedLevel) {
|
|
62
|
+
return exports.MILESTONE_THRESHOLDS.find(t => t > claimedLevel) ?? null;
|
|
63
|
+
}
|
|
64
|
+
// Derive a milestone's display/claim state from its raw map value (a MilestoneProgress object, a legacy status string,
|
|
65
|
+
// or undefined). Anything that is not a well-formed object counts as no progress ({c:0, l:0}). Shared by the API claim
|
|
66
|
+
// path and the UI.
|
|
67
|
+
function milestoneState(value) {
|
|
68
|
+
const mp = (value != null && typeof value === 'object') ? value : {};
|
|
69
|
+
const count = typeof mp.c === 'number' && Number.isFinite(mp.c) ? mp.c : 0;
|
|
70
|
+
const claimed = typeof mp.l === 'number' && Number.isFinite(mp.l) ? mp.l : 0;
|
|
71
|
+
const target = nextMilestone(claimed);
|
|
72
|
+
return { count, claimed, target, claimable: target != null && count >= target, maxed: target == null };
|
|
73
|
+
}
|
|
74
|
+
// Achievements the client is allowed to self-complete because there is no server-side action to hook: opening the
|
|
75
|
+
// wiki and opening a match detail. The POST /achievements/:key/complete endpoint MUST reject any key not in this set,
|
|
76
|
+
// so a client can never self-complete a server-authoritative achievement (recruit, win, upgrade, ...).
|
|
77
|
+
exports.CLIENT_COMPLETABLE_ACHIEVEMENTS = ['READ_WIKI', 'VIEW_MATCH', 'PREVIEW_MATCH'];
|
|
78
|
+
// Whether `value` is one of the known achievement keys (use before trusting a request param).
|
|
79
|
+
function isAchievementKey(value) {
|
|
80
|
+
return exports.ACHIEVEMENT_KEYS.includes(value);
|
|
81
|
+
}
|
|
82
|
+
// Whether the client is allowed to fire completion for this key (only READ_WIKI / VIEW_MATCH).
|
|
83
|
+
function isClientCompletable(key) {
|
|
84
|
+
return exports.CLIENT_COMPLETABLE_ACHIEVEMENTS.includes(key);
|
|
85
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./achievement-status"), exports);
|
|
18
|
+
__exportStar(require("./achievement-type"), exports);
|
|
19
|
+
__exportStar(require("./schemas/achievements.z"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.achievementMapSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const achievement_status_1 = require("../achievement-status");
|
|
6
|
+
// Validates a UserAchievements.achievements jsonb map (achievementKey -> status). Keys are loose strings so an
|
|
7
|
+
// unknown/legacy key never fails validation; values must be a known AchievementStatus. Mirrors the z.record idiom
|
|
8
|
+
// used by Tactics (tactics.z.ts).
|
|
9
|
+
exports.achievementMapSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.nativeEnum(achievement_status_1.AchievementStatus));
|
|
@@ -22,4 +22,5 @@ __exportStar(require("./player"), exports);
|
|
|
22
22
|
__exportStar(require("./competition"), exports);
|
|
23
23
|
__exportStar(require("./notification"), exports);
|
|
24
24
|
__exportStar(require("./premium"), exports);
|
|
25
|
+
__exportStar(require("./achievement"), exports);
|
|
25
26
|
__exportStar(require("./utils"), exports);
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// (and populated from a picked default preset); consumed by the positional engine to place players. A team with no
|
|
6
6
|
// `basePositions` falls back to the default preset for its `rotationSystem`.
|
|
7
7
|
//
|
|
8
|
-
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the
|
|
8
|
+
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The `rotation` key is the MAIN
|
|
9
|
+
// designated setter's (designatedSetters[0]) rotational ZONE, raw 1..6 (`rotation` == that setter's zone; there is
|
|
10
|
+
// NO setter-zone->rotation remap). This is the single convention everywhere: the editor writes it (posOf anchored on
|
|
11
|
+
// the main setter, saved verbatim), the engine reads it (positioning.rotationOf = getMainSetter zone), and the
|
|
12
|
+
// offensive/tempo/blocking prefs key by mainSetter.position. `coords[formation][rotation][zone]` is the standing
|
|
13
|
+
// coordinate for that formation; `receive[rotation][zone]` is the reception-participation flag (absent zone => default
|
|
14
|
+
// to participating). (History: rotationOf once remapped via SETTER_ZONE_TO_ROTATION and defaultReceiveMap once keyed
|
|
15
|
+
// by that index; both were unified to the raw main-setter zone. See VolleyballSim/plans/rotation-key-main-setter.md.)
|
|
12
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
17
|
exports.BASE_FORMATIONS = void 0;
|
|
14
18
|
exports.BASE_FORMATIONS = ['SERVE', 'RECEIVE', 'DEFENCE', 'DEF_L', 'DEF_M', 'DEF_R', 'ATTACK'];
|
|
@@ -199,22 +199,24 @@ const DEFAULT_5_1_RECEIVE = {
|
|
|
199
199
|
5: { 1: true, 2: false, 3: true, 4: false, 5: false, 6: true },
|
|
200
200
|
6: { 1: true, 2: false, 3: false, 4: true, 5: true, 6: false }
|
|
201
201
|
};
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
// slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot)
|
|
206
|
-
// opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2,
|
|
207
|
-
// second setter is excluded too, giving the same 3-passer map for every system.
|
|
202
|
+
// The default serve-receive participation map: the standard 3-passer formation. Keyed by the MAIN setter's rotational
|
|
203
|
+
// ZONE (raw 1..6 = the canonical convention: receive[Z] describes the formation with the setter at zone Z), matching
|
|
204
|
+
// DEFAULT_5_1_RECEIVE, the PRESET_6_2/PRESET_4_2 presets, the tactics editor, and the engine's rotationOf. A rotational
|
|
205
|
+
// slot passes iff its slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot);
|
|
206
|
+
// the setter, the opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2,
|
|
207
|
+
// so the second setter is excluded too, giving the same 3-passer map for every system.
|
|
208
|
+
// (History: this used to key by a rotation INDEX via ROTATION_TO_SETTER_ZONE {1:1,2:6,3:5,4:4,5:3,6:2}, a leftover of
|
|
209
|
+
// the old setter-zone->rotation remap removed from rotationOf; that transposed the non-5-1 default receive maps at
|
|
210
|
+
// rotations 2/3/5/6. Fixed 2026-08-23, see VolleyballSim/plans/rotation-key-main-setter.md + the re-key migration.)
|
|
208
211
|
function defaultReceiveMap(system) {
|
|
209
212
|
const receive = {};
|
|
210
|
-
for (let
|
|
211
|
-
const setterZone = ROTATION_TO_SETTER_ZONE[rotation];
|
|
213
|
+
for (let setterZone = 1; setterZone <= 6; setterZone++) {
|
|
212
214
|
const perZone = {};
|
|
213
215
|
for (let zone = 1; zone <= 6; zone++) {
|
|
214
216
|
const fn = (0, lineup_function_1.slotFunctionAt)(system, setterZone, zone);
|
|
215
217
|
perZone[String(zone)] = fn === player_1.RoleEnum.OUTSIDE_HITTER || (fn === player_1.RoleEnum.MIDDLE_BLOCKER && (0, match_1.isBackRow)(zone));
|
|
216
218
|
}
|
|
217
|
-
receive[String(
|
|
219
|
+
receive[String(setterZone)] = perZone;
|
|
218
220
|
}
|
|
219
221
|
return receive;
|
|
220
222
|
}
|
|
@@ -9,6 +9,27 @@ const BACK_ROW = new Set([1, 5, 6]);
|
|
|
9
9
|
// MUST itself pass the FIVB overlap / serve-line rules in every rotation. Otherwise a partial config (some zones
|
|
10
10
|
// stored, others defaulted) can render/commit an unset zone onto a neighbour and read "out of rotation even in
|
|
11
11
|
// base" for a formation the sim runs legally (rush6@ tournament report, 2026-08-21).
|
|
12
|
+
// The default receive map must key by the MAIN setter's RAW rotational zone (receive[R] describes the formation with
|
|
13
|
+
// the setter at zone R), the single canonical convention shared by DEFAULT_5_1_RECEIVE, the PRESET_6_2/4_2 presets,
|
|
14
|
+
// the tactics editor, and the engine's rotationOf. It used to key by a rotation INDEX (ROTATION_TO_SETTER_ZONE), which
|
|
15
|
+
// transposed non-5-1 maps at rotations 2/3/5/6 (fixed 2026-08-23; see plans/rotation-key-main-setter.md).
|
|
16
|
+
describe('defaultReceiveMap: raw main-setter-zone convention', () => {
|
|
17
|
+
it('5-1: the computed map equals the hand-authored raw DEFAULT_5_1_RECEIVE', () => {
|
|
18
|
+
// defaultBaseConfig(5-1).receive IS the hand-authored DEFAULT_5_1_RECEIVE. The computed map matching it proves
|
|
19
|
+
// defaultReceiveMap keys by the raw setter zone (receive[R] = setter@R), not the old index.
|
|
20
|
+
expect((0, default_base_config_1.defaultReceiveMap)(rotation_system_1.RotationSystemEnum.FIVE_ONE)).toEqual((0, default_base_config_1.defaultBaseConfig)(rotation_system_1.RotationSystemEnum.FIVE_ONE).receive);
|
|
21
|
+
});
|
|
22
|
+
it('every system: receive[R] hides the setter at zone R (raw), never at ROTATION_TO_SETTER_ZONE[R]', () => {
|
|
23
|
+
for (const system of Object.values(rotation_system_1.RotationSystemEnum)) {
|
|
24
|
+
if (system === rotation_system_1.RotationSystemEnum.SIX_ZERO)
|
|
25
|
+
continue; // 6-0 has no designated setter
|
|
26
|
+
const map = (0, default_base_config_1.defaultReceiveMap)(system);
|
|
27
|
+
for (const z of [2, 3, 5, 6]) { // the rotations where raw and the old index convention diverge
|
|
28
|
+
expect({ system, z, passes: map[String(z)]?.[String(z)] }).toEqual({ system, z, passes: false });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
12
33
|
describe('defaultBaseConfig base-config legality', () => {
|
|
13
34
|
it('has zero SERVE/RECEIVE overlap violations for every rotation system', () => {
|
|
14
35
|
for (const system of Object.values(rotation_system_1.RotationSystemEnum)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, PhysicianModel, PhysiotherapistModel, BoxScoreTotalsModel, CompetitionChampionModel, JerseyBadgeModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, UserDeviceTokenModel, BasePositionPresetModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerDeclineLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, FriendlyModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, TeamJerseyModel, LineupPresetModel, MatchPresetModel, EventModel, EventCompetitionModel, EventSquadModel, EventPackModel, EventPackPlayerModel, EventResultModel, EventBotTeamModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, PremiumGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
|
|
1
|
+
import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, PhysicianModel, PhysiotherapistModel, BoxScoreTotalsModel, CompetitionChampionModel, JerseyBadgeModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, UserAchievementsModel, UserDeviceTokenModel, BasePositionPresetModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerDeclineLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, FriendlyModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, TeamJerseyModel, LineupPresetModel, MatchPresetModel, EventModel, EventCompetitionModel, EventSquadModel, EventPackModel, EventPackPlayerModel, EventResultModel, EventBotTeamModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, PremiumGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
|
|
2
2
|
export function initModels(sequelize) {
|
|
3
3
|
const Coach = CoachModel.initModel(sequelize);
|
|
4
4
|
const Scout = ScoutModel.initModel(sequelize);
|
|
@@ -25,6 +25,7 @@ export function initModels(sequelize) {
|
|
|
25
25
|
const PremiumGrant = PremiumGrantModel.initModel(sequelize);
|
|
26
26
|
const Wishlist = WishlistModel.initModel(sequelize);
|
|
27
27
|
const UserSettings = UserSettingsModel.initModel(sequelize);
|
|
28
|
+
const UserAchievements = UserAchievementsModel.initModel(sequelize);
|
|
28
29
|
const UserDeviceToken = UserDeviceTokenModel.initModel(sequelize);
|
|
29
30
|
const BasePositionPreset = BasePositionPresetModel.initModel(sequelize);
|
|
30
31
|
const Notification = NotificationModel.initModel(sequelize);
|
|
@@ -75,6 +76,7 @@ export function initModels(sequelize) {
|
|
|
75
76
|
AuthUser.hasOne(GachaPity, { as: 'GachaPity', foreignKey: 'user_id' });
|
|
76
77
|
AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
|
|
77
78
|
AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
|
|
79
|
+
AuthUser.hasOne(UserAchievements, { as: 'UserAchievements', foreignKey: 'user_id' });
|
|
78
80
|
AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
|
|
79
81
|
AuthUser.hasMany(BasePositionPreset, { as: 'BasePositionPresets', foreignKey: 'user_id' });
|
|
80
82
|
AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
|
|
@@ -98,6 +100,7 @@ export function initModels(sequelize) {
|
|
|
98
100
|
PremiumGrant.belongsTo(AuthUser, { as: 'grantedByUser', foreignKey: 'granted_by' });
|
|
99
101
|
Wishlist.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
100
102
|
UserSettings.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
103
|
+
UserAchievements.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
101
104
|
Competition.belongsTo(Iteration, { as: 'Iteration', foreignKey: 'iteration' });
|
|
102
105
|
Competition.hasMany(CompetitionMatch, { as: 'CompetitionMatches', foreignKey: 'competition_id' });
|
|
103
106
|
Competition.hasMany(CompetitionStandings, { as: 'CompetitionStandings', foreignKey: 'competition_id' });
|
|
@@ -367,6 +370,7 @@ export function initModels(sequelize) {
|
|
|
367
370
|
PremiumGrant,
|
|
368
371
|
Wishlist,
|
|
369
372
|
UserSettings,
|
|
373
|
+
UserAchievements,
|
|
370
374
|
UserDeviceToken,
|
|
371
375
|
BasePositionPreset,
|
|
372
376
|
Notification,
|
|
@@ -17,9 +17,10 @@ export declare enum CurrencyTransactionSourceEnum {
|
|
|
17
17
|
SEASON_REWARD = "SEASON_REWARD",
|
|
18
18
|
COMPETITION_REWARD = "COMPETITION_REWARD",
|
|
19
19
|
PHYSICIAN_UPGRADE = "PHYSICIAN_UPGRADE",
|
|
20
|
-
PHYSIOTHERAPIST_UPGRADE = "PHYSIOTHERAPIST_UPGRADE"
|
|
20
|
+
PHYSIOTHERAPIST_UPGRADE = "PHYSIOTHERAPIST_UPGRADE",
|
|
21
|
+
ACHIEVEMENT_CLAIM = "ACHIEVEMENT_CLAIM"
|
|
21
22
|
}
|
|
22
|
-
export type CurrencyTransactionSource = CurrencyTransactionSourceEnum.MATCH_REWARD | CurrencyTransactionSourceEnum.GACHA_PULL | CurrencyTransactionSourceEnum.COACH_UPGRADE | CurrencyTransactionSourceEnum.SCOUT_UPGRADE | CurrencyTransactionSourceEnum.PLAYER_DISMISS | CurrencyTransactionSourceEnum.PLAYER_RETIRE | CurrencyTransactionSourceEnum.ADMIN_GRANT | CurrencyTransactionSourceEnum.SEASON_REWARD | CurrencyTransactionSourceEnum.COMPETITION_REWARD | CurrencyTransactionSourceEnum.PHYSICIAN_UPGRADE | CurrencyTransactionSourceEnum.PHYSIOTHERAPIST_UPGRADE;
|
|
23
|
+
export type CurrencyTransactionSource = CurrencyTransactionSourceEnum.MATCH_REWARD | CurrencyTransactionSourceEnum.GACHA_PULL | CurrencyTransactionSourceEnum.COACH_UPGRADE | CurrencyTransactionSourceEnum.SCOUT_UPGRADE | CurrencyTransactionSourceEnum.PLAYER_DISMISS | CurrencyTransactionSourceEnum.PLAYER_RETIRE | CurrencyTransactionSourceEnum.ADMIN_GRANT | CurrencyTransactionSourceEnum.SEASON_REWARD | CurrencyTransactionSourceEnum.COMPETITION_REWARD | CurrencyTransactionSourceEnum.PHYSICIAN_UPGRADE | CurrencyTransactionSourceEnum.PHYSIOTHERAPIST_UPGRADE | CurrencyTransactionSourceEnum.ACHIEVEMENT_CLAIM;
|
|
23
24
|
export interface CurrencyTransactionAttributes {
|
|
24
25
|
transaction_id: string;
|
|
25
26
|
user_id: string;
|
|
@@ -17,6 +17,7 @@ export var CurrencyTransactionSourceEnum;
|
|
|
17
17
|
CurrencyTransactionSourceEnum["COMPETITION_REWARD"] = "COMPETITION_REWARD";
|
|
18
18
|
CurrencyTransactionSourceEnum["PHYSICIAN_UPGRADE"] = "PHYSICIAN_UPGRADE";
|
|
19
19
|
CurrencyTransactionSourceEnum["PHYSIOTHERAPIST_UPGRADE"] = "PHYSIOTHERAPIST_UPGRADE";
|
|
20
|
+
CurrencyTransactionSourceEnum["ACHIEVEMENT_CLAIM"] = "ACHIEVEMENT_CLAIM";
|
|
20
21
|
})(CurrencyTransactionSourceEnum || (CurrencyTransactionSourceEnum = {}));
|
|
21
22
|
export class CurrencyTransactionModel extends Model {
|
|
22
23
|
static initModel(sequelize) {
|
|
@@ -58,6 +58,7 @@ export * from './wishlist';
|
|
|
58
58
|
export * from './currency-transaction';
|
|
59
59
|
export * from './user-pull-grant';
|
|
60
60
|
export * from './user-settings';
|
|
61
|
+
export * from './user-achievements';
|
|
61
62
|
export * from './user-device-token';
|
|
62
63
|
export * from './notification';
|
|
63
64
|
export * from './vper';
|
|
@@ -58,6 +58,7 @@ export * from './wishlist';
|
|
|
58
58
|
export * from './currency-transaction';
|
|
59
59
|
export * from './user-pull-grant';
|
|
60
60
|
export * from './user-settings';
|
|
61
|
+
export * from './user-achievements';
|
|
61
62
|
export * from './user-device-token';
|
|
62
63
|
export * from './notification';
|
|
63
64
|
export * from './vper';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { AuthUserId, AuthUserModel } from '.';
|
|
4
|
+
export interface UserAchievementsAttributes {
|
|
5
|
+
user_id: string;
|
|
6
|
+
achievements: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export type UserAchievementsPk = 'user_id';
|
|
9
|
+
export type UserAchievementsId = UserAchievementsModel[UserAchievementsPk];
|
|
10
|
+
export type UserAchievementsCreationAttributes = Sequelize.Optional<UserAchievementsAttributes, 'achievements'>;
|
|
11
|
+
export declare class UserAchievementsModel extends Model<UserAchievementsAttributes, UserAchievementsCreationAttributes> implements UserAchievementsAttributes {
|
|
12
|
+
user_id: string;
|
|
13
|
+
achievements: Record<string, string>;
|
|
14
|
+
user: AuthUserModel;
|
|
15
|
+
getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
16
|
+
setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
|
|
17
|
+
createUser: Sequelize.BelongsToCreateAssociationMixin<AuthUserModel>;
|
|
18
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof UserAchievementsModel;
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class UserAchievementsModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return UserAchievementsModel.init({
|
|
5
|
+
user_id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
references: {
|
|
10
|
+
model: 'AuthUser',
|
|
11
|
+
key: 'user_id'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
achievements: {
|
|
15
|
+
type: DataTypes.JSONB,
|
|
16
|
+
allowNull: false,
|
|
17
|
+
defaultValue: {}
|
|
18
|
+
}
|
|
19
|
+
}, {
|
|
20
|
+
sequelize,
|
|
21
|
+
tableName: 'UserAchievements',
|
|
22
|
+
schema: 'public',
|
|
23
|
+
timestamps: false,
|
|
24
|
+
indexes: [
|
|
25
|
+
{
|
|
26
|
+
name: 'UserAchievements_pk',
|
|
27
|
+
unique: true,
|
|
28
|
+
fields: [{ name: 'user_id' }]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Per-user achievement progress state. Stored only inside the UserAchievements.achievements jsonb map
|
|
2
|
+
// (achievementKey -> status); an absent key means NOT_DONE, so no data backfill is ever needed when new
|
|
3
|
+
// achievements are added. Because it lives only inside jsonb it is NOT a Sequelize DataTypes.ENUM and has no
|
|
4
|
+
// Postgres CREATE TYPE (mirrors how Tactics stores RoleEnum values inside jsonb without a DB enum).
|
|
5
|
+
export var AchievementStatus;
|
|
6
|
+
(function (AchievementStatus) {
|
|
7
|
+
AchievementStatus["NOT_DONE"] = "NOT_DONE";
|
|
8
|
+
AchievementStatus["DONE"] = "DONE";
|
|
9
|
+
AchievementStatus["CLAIMED"] = "CLAIMED";
|
|
10
|
+
})(AchievementStatus || (AchievementStatus = {}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AchievementStatus } from './achievement-status';
|
|
2
|
+
export declare const ACHIEVEMENT_KEYS: readonly ["NORMAL_RECRUIT", "SPECIAL_RECRUIT", "DISMISS_PLAYER", "RETIRE_PLAYER", "GIVE_NICKNAME", "PLAY_MATCH", "WIN_MATCH", "FINISH_LEAGUE_SEASON", "UPGRADE_COACH", "UPGRADE_SCOUT", "UPGRADE_PHYSICIAN", "UPGRADE_PHYSIOTHERAPIST", "CHANGE_TRAINING_FOCUS", "CHANGE_TACTICS", "REGISTER_EVENT", "WIN_LEAGUE", "WIN_NATIONALS", "WIN_REGIONALS", "WIN_WCC", "READ_WIKI", "CLAIM_DAILY", "VIEW_MATCH", "PREVIEW_MATCH"];
|
|
3
|
+
export type AchievementKey = typeof ACHIEVEMENT_KEYS[number];
|
|
4
|
+
export declare const ACHIEVEMENT_REWARD = 500;
|
|
5
|
+
export declare const MILESTONE_ACHIEVEMENTS: readonly AchievementKey[];
|
|
6
|
+
export declare function isMilestone(key: AchievementKey): boolean;
|
|
7
|
+
export declare const MILESTONE_THRESHOLDS: readonly number[];
|
|
8
|
+
export declare function nextMilestone(claimedLevel: number): number | null;
|
|
9
|
+
export interface MilestoneProgress {
|
|
10
|
+
c: number;
|
|
11
|
+
l: number;
|
|
12
|
+
}
|
|
13
|
+
export interface MilestoneState {
|
|
14
|
+
count: number;
|
|
15
|
+
claimed: number;
|
|
16
|
+
target: number | null;
|
|
17
|
+
claimable: boolean;
|
|
18
|
+
maxed: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function milestoneState(value: unknown): MilestoneState;
|
|
21
|
+
export type AchievementMap = Partial<Record<AchievementKey, AchievementStatus | MilestoneProgress>>;
|
|
22
|
+
export declare const CLIENT_COMPLETABLE_ACHIEVEMENTS: readonly AchievementKey[];
|
|
23
|
+
export declare function isAchievementKey(value: string): value is AchievementKey;
|
|
24
|
+
export declare function isClientCompletable(key: AchievementKey): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Single source of truth for the set of achievement keys (the tutorial checklist). Adding an achievement:
|
|
2
|
+
// 1. add its key here,
|
|
3
|
+
// 2. add its display config in the UI (src/view/achievements/achievements-config.ts),
|
|
4
|
+
// 3. add its name + description to every locale file,
|
|
5
|
+
// 4. wire its completion trigger (API handler or sim engine) to call completeAchievement.
|
|
6
|
+
// The key is only ever a jsonb map key, so there is no Postgres enum/column to migrate when the list grows.
|
|
7
|
+
export const ACHIEVEMENT_KEYS = [
|
|
8
|
+
'NORMAL_RECRUIT',
|
|
9
|
+
'SPECIAL_RECRUIT',
|
|
10
|
+
'DISMISS_PLAYER',
|
|
11
|
+
'RETIRE_PLAYER',
|
|
12
|
+
'GIVE_NICKNAME',
|
|
13
|
+
'PLAY_MATCH',
|
|
14
|
+
'WIN_MATCH',
|
|
15
|
+
'FINISH_LEAGUE_SEASON',
|
|
16
|
+
'UPGRADE_COACH',
|
|
17
|
+
'UPGRADE_SCOUT',
|
|
18
|
+
'UPGRADE_PHYSICIAN',
|
|
19
|
+
'UPGRADE_PHYSIOTHERAPIST',
|
|
20
|
+
'CHANGE_TRAINING_FOCUS',
|
|
21
|
+
'CHANGE_TACTICS',
|
|
22
|
+
'REGISTER_EVENT',
|
|
23
|
+
'WIN_LEAGUE',
|
|
24
|
+
'WIN_NATIONALS',
|
|
25
|
+
'WIN_REGIONALS',
|
|
26
|
+
'WIN_WCC',
|
|
27
|
+
'READ_WIKI',
|
|
28
|
+
'CLAIM_DAILY',
|
|
29
|
+
'VIEW_MATCH',
|
|
30
|
+
'PREVIEW_MATCH'
|
|
31
|
+
];
|
|
32
|
+
// Every achievement grants the same flat coin reward when the user claims it (each milestone reached is a fresh claim).
|
|
33
|
+
export const ACHIEVEMENT_REWARD = 500;
|
|
34
|
+
// Recurring "milestone" achievements: instead of a one-shot NOT_DONE->DONE->CLAIMED status, these store a running count
|
|
35
|
+
// and re-arm at escalating thresholds. Their map value is a MilestoneProgress object, not a status string.
|
|
36
|
+
export const MILESTONE_ACHIEVEMENTS = [
|
|
37
|
+
'CLAIM_DAILY',
|
|
38
|
+
'PLAY_MATCH',
|
|
39
|
+
'WIN_MATCH',
|
|
40
|
+
'NORMAL_RECRUIT',
|
|
41
|
+
'DISMISS_PLAYER',
|
|
42
|
+
'WIN_LEAGUE',
|
|
43
|
+
'WIN_NATIONALS',
|
|
44
|
+
'WIN_REGIONALS',
|
|
45
|
+
'WIN_WCC'
|
|
46
|
+
];
|
|
47
|
+
export function isMilestone(key) {
|
|
48
|
+
return MILESTONE_ACHIEVEMENTS.includes(key);
|
|
49
|
+
}
|
|
50
|
+
// The claim thresholds every milestone steps through (x5, x2 alternating). Past the last one the milestone is "maxed".
|
|
51
|
+
export const MILESTONE_THRESHOLDS = [1, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000];
|
|
52
|
+
// The next threshold strictly greater than `claimedLevel` (0 = nothing claimed yet), or null when all are claimed.
|
|
53
|
+
export function nextMilestone(claimedLevel) {
|
|
54
|
+
return MILESTONE_THRESHOLDS.find(t => t > claimedLevel) ?? null;
|
|
55
|
+
}
|
|
56
|
+
// Derive a milestone's display/claim state from its raw map value (a MilestoneProgress object, a legacy status string,
|
|
57
|
+
// or undefined). Anything that is not a well-formed object counts as no progress ({c:0, l:0}). Shared by the API claim
|
|
58
|
+
// path and the UI.
|
|
59
|
+
export function milestoneState(value) {
|
|
60
|
+
const mp = (value != null && typeof value === 'object') ? value : {};
|
|
61
|
+
const count = typeof mp.c === 'number' && Number.isFinite(mp.c) ? mp.c : 0;
|
|
62
|
+
const claimed = typeof mp.l === 'number' && Number.isFinite(mp.l) ? mp.l : 0;
|
|
63
|
+
const target = nextMilestone(claimed);
|
|
64
|
+
return { count, claimed, target, claimable: target != null && count >= target, maxed: target == null };
|
|
65
|
+
}
|
|
66
|
+
// Achievements the client is allowed to self-complete because there is no server-side action to hook: opening the
|
|
67
|
+
// wiki and opening a match detail. The POST /achievements/:key/complete endpoint MUST reject any key not in this set,
|
|
68
|
+
// so a client can never self-complete a server-authoritative achievement (recruit, win, upgrade, ...).
|
|
69
|
+
export const CLIENT_COMPLETABLE_ACHIEVEMENTS = ['READ_WIKI', 'VIEW_MATCH', 'PREVIEW_MATCH'];
|
|
70
|
+
// Whether `value` is one of the known achievement keys (use before trusting a request param).
|
|
71
|
+
export function isAchievementKey(value) {
|
|
72
|
+
return ACHIEVEMENT_KEYS.includes(value);
|
|
73
|
+
}
|
|
74
|
+
// Whether the client is allowed to fire completion for this key (only READ_WIKI / VIEW_MATCH).
|
|
75
|
+
export function isClientCompletable(key) {
|
|
76
|
+
return CLIENT_COMPLETABLE_ACHIEVEMENTS.includes(key);
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AchievementStatus } from '../achievement-status';
|
|
3
|
+
// Validates a UserAchievements.achievements jsonb map (achievementKey -> status). Keys are loose strings so an
|
|
4
|
+
// unknown/legacy key never fails validation; values must be a known AchievementStatus. Mirrors the z.record idiom
|
|
5
|
+
// used by Tactics (tactics.z.ts).
|
|
6
|
+
export const achievementMapSchema = z.record(z.string(), z.nativeEnum(AchievementStatus));
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
// (and populated from a picked default preset); consumed by the positional engine to place players. A team with no
|
|
5
5
|
// `basePositions` falls back to the default preset for its `rotationSystem`.
|
|
6
6
|
//
|
|
7
|
-
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the
|
|
7
|
+
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The `rotation` key is the MAIN
|
|
8
|
+
// designated setter's (designatedSetters[0]) rotational ZONE, raw 1..6 (`rotation` == that setter's zone; there is
|
|
9
|
+
// NO setter-zone->rotation remap). This is the single convention everywhere: the editor writes it (posOf anchored on
|
|
10
|
+
// the main setter, saved verbatim), the engine reads it (positioning.rotationOf = getMainSetter zone), and the
|
|
11
|
+
// offensive/tempo/blocking prefs key by mainSetter.position. `coords[formation][rotation][zone]` is the standing
|
|
12
|
+
// coordinate for that formation; `receive[rotation][zone]` is the reception-participation flag (absent zone => default
|
|
13
|
+
// to participating). (History: rotationOf once remapped via SETTER_ZONE_TO_ROTATION and defaultReceiveMap once keyed
|
|
14
|
+
// by that index; both were unified to the raw main-setter zone. See VolleyballSim/plans/rotation-key-main-setter.md.)
|
|
11
15
|
export const BASE_FORMATIONS = ['SERVE', 'RECEIVE', 'DEFENCE', 'DEF_L', 'DEF_M', 'DEF_R', 'ATTACK'];
|
|
@@ -195,22 +195,24 @@ const DEFAULT_5_1_RECEIVE = {
|
|
|
195
195
|
5: { 1: true, 2: false, 3: true, 4: false, 5: false, 6: true },
|
|
196
196
|
6: { 1: true, 2: false, 3: false, 4: true, 5: true, 6: false }
|
|
197
197
|
};
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
// slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot)
|
|
202
|
-
// opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2,
|
|
203
|
-
// second setter is excluded too, giving the same 3-passer map for every system.
|
|
198
|
+
// The default serve-receive participation map: the standard 3-passer formation. Keyed by the MAIN setter's rotational
|
|
199
|
+
// ZONE (raw 1..6 = the canonical convention: receive[Z] describes the formation with the setter at zone Z), matching
|
|
200
|
+
// DEFAULT_5_1_RECEIVE, the PRESET_6_2/PRESET_4_2 presets, the tactics editor, and the engine's rotationOf. A rotational
|
|
201
|
+
// slot passes iff its slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot);
|
|
202
|
+
// the setter, the opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2,
|
|
203
|
+
// so the second setter is excluded too, giving the same 3-passer map for every system.
|
|
204
|
+
// (History: this used to key by a rotation INDEX via ROTATION_TO_SETTER_ZONE {1:1,2:6,3:5,4:4,5:3,6:2}, a leftover of
|
|
205
|
+
// the old setter-zone->rotation remap removed from rotationOf; that transposed the non-5-1 default receive maps at
|
|
206
|
+
// rotations 2/3/5/6. Fixed 2026-08-23, see VolleyballSim/plans/rotation-key-main-setter.md + the re-key migration.)
|
|
204
207
|
export function defaultReceiveMap(system) {
|
|
205
208
|
const receive = {};
|
|
206
|
-
for (let
|
|
207
|
-
const setterZone = ROTATION_TO_SETTER_ZONE[rotation];
|
|
209
|
+
for (let setterZone = 1; setterZone <= 6; setterZone++) {
|
|
208
210
|
const perZone = {};
|
|
209
211
|
for (let zone = 1; zone <= 6; zone++) {
|
|
210
212
|
const fn = slotFunctionAt(system, setterZone, zone);
|
|
211
213
|
perZone[String(zone)] = fn === RoleEnum.OUTSIDE_HITTER || (fn === RoleEnum.MIDDLE_BLOCKER && isBackRow(zone));
|
|
212
214
|
}
|
|
213
|
-
receive[String(
|
|
215
|
+
receive[String(setterZone)] = perZone;
|
|
214
216
|
}
|
|
215
217
|
return receive;
|
|
216
218
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RotationSystemEnum } from './rotation-system';
|
|
2
|
-
import { defaultBaseConfig } from './default-base-config';
|
|
2
|
+
import { defaultBaseConfig, defaultReceiveMap } from './default-base-config';
|
|
3
3
|
import { baseConfigViolations } from './receive-overlap';
|
|
4
4
|
// Back-row rotational zones (right/left/middle back). Front row is {2,3,4}.
|
|
5
5
|
const BACK_ROW = new Set([1, 5, 6]);
|
|
@@ -7,6 +7,27 @@ const BACK_ROW = new Set([1, 5, 6]);
|
|
|
7
7
|
// MUST itself pass the FIVB overlap / serve-line rules in every rotation. Otherwise a partial config (some zones
|
|
8
8
|
// stored, others defaulted) can render/commit an unset zone onto a neighbour and read "out of rotation even in
|
|
9
9
|
// base" for a formation the sim runs legally (rush6@ tournament report, 2026-08-21).
|
|
10
|
+
// The default receive map must key by the MAIN setter's RAW rotational zone (receive[R] describes the formation with
|
|
11
|
+
// the setter at zone R), the single canonical convention shared by DEFAULT_5_1_RECEIVE, the PRESET_6_2/4_2 presets,
|
|
12
|
+
// the tactics editor, and the engine's rotationOf. It used to key by a rotation INDEX (ROTATION_TO_SETTER_ZONE), which
|
|
13
|
+
// transposed non-5-1 maps at rotations 2/3/5/6 (fixed 2026-08-23; see plans/rotation-key-main-setter.md).
|
|
14
|
+
describe('defaultReceiveMap: raw main-setter-zone convention', () => {
|
|
15
|
+
it('5-1: the computed map equals the hand-authored raw DEFAULT_5_1_RECEIVE', () => {
|
|
16
|
+
// defaultBaseConfig(5-1).receive IS the hand-authored DEFAULT_5_1_RECEIVE. The computed map matching it proves
|
|
17
|
+
// defaultReceiveMap keys by the raw setter zone (receive[R] = setter@R), not the old index.
|
|
18
|
+
expect(defaultReceiveMap(RotationSystemEnum.FIVE_ONE)).toEqual(defaultBaseConfig(RotationSystemEnum.FIVE_ONE).receive);
|
|
19
|
+
});
|
|
20
|
+
it('every system: receive[R] hides the setter at zone R (raw), never at ROTATION_TO_SETTER_ZONE[R]', () => {
|
|
21
|
+
for (const system of Object.values(RotationSystemEnum)) {
|
|
22
|
+
if (system === RotationSystemEnum.SIX_ZERO)
|
|
23
|
+
continue; // 6-0 has no designated setter
|
|
24
|
+
const map = defaultReceiveMap(system);
|
|
25
|
+
for (const z of [2, 3, 5, 6]) { // the rotations where raw and the old index convention diverge
|
|
26
|
+
expect({ system, z, passes: map[String(z)]?.[String(z)] }).toEqual({ system, z, passes: false });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
10
31
|
describe('defaultBaseConfig base-config legality', () => {
|
|
11
32
|
it('has zero SERVE/RECEIVE overlap violations for every rotation system', () => {
|
|
12
33
|
for (const system of Object.values(RotationSystemEnum)) {
|