volleyballsimtypes 0.0.519 → 0.0.521
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/api/index.d.ts +14 -1
- package/dist/cjs/src/data/init-models.js +14 -0
- package/dist/cjs/src/data/models/auth-identity.d.ts +1 -1
- package/dist/cjs/src/data/models/auth-identity.js +1 -1
- package/dist/cjs/src/data/models/index.d.ts +3 -0
- package/dist/cjs/src/data/models/index.js +3 -0
- package/dist/cjs/src/data/models/jersey-badge.d.ts +32 -0
- package/dist/cjs/src/data/models/jersey-badge.js +60 -0
- package/dist/cjs/src/data/models/premium-grant.d.ts +39 -0
- package/dist/cjs/src/data/models/premium-grant.js +82 -0
- package/dist/cjs/src/data/models/team-jersey.d.ts +20 -0
- package/dist/cjs/src/data/models/team-jersey.js +38 -0
- package/dist/cjs/src/data/models/team.d.ts +3 -1
- package/dist/cjs/src/data/transformers/team.js +4 -1
- package/dist/cjs/src/service/competition/event-schedule.d.ts +1 -1
- package/dist/cjs/src/service/competition/event-schedule.js +3 -1
- package/dist/cjs/src/service/competition/event-schedule.test.js +4 -2
- package/dist/cjs/src/service/index.d.ts +1 -0
- package/dist/cjs/src/service/index.js +1 -0
- package/dist/cjs/src/service/player/improvement.test.js +5 -5
- package/dist/cjs/src/service/player/stats.js +30 -16
- package/dist/cjs/src/service/player/stats.test.js +6 -6
- package/dist/cjs/src/service/premium/index.d.ts +3 -0
- package/dist/cjs/src/service/premium/index.js +19 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.d.ts +5 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.js +22 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.test.d.ts +1 -0
- package/dist/cjs/src/service/premium/patreon-tier-map.test.js +48 -0
- package/dist/cjs/src/service/premium/premium-grant.d.ts +17 -0
- package/dist/cjs/src/service/premium/premium-grant.js +15 -0
- package/dist/cjs/src/service/premium/premium-tier.d.ts +10 -0
- package/dist/cjs/src/service/premium/premium-tier.js +24 -0
- package/dist/cjs/src/service/premium/premium.test.d.ts +1 -0
- package/dist/cjs/src/service/premium/premium.test.js +93 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/jersey.d.ts +74 -0
- package/dist/cjs/src/service/team/jersey.js +33 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +69 -0
- package/dist/cjs/src/service/team/schemas/team.z.js +3 -1
- package/dist/cjs/src/service/team/team.d.ts +2 -0
- package/dist/cjs/src/service/team/team.js +2 -1
- package/dist/cjs/src/stat-config/improvement-floors.json +204 -204
- package/dist/esm/src/api/index.d.ts +14 -1
- package/dist/esm/src/data/init-models.js +15 -1
- package/dist/esm/src/data/models/auth-identity.d.ts +1 -1
- package/dist/esm/src/data/models/auth-identity.js +1 -1
- package/dist/esm/src/data/models/index.d.ts +3 -0
- package/dist/esm/src/data/models/index.js +3 -0
- package/dist/esm/src/data/models/jersey-badge.d.ts +32 -0
- package/dist/esm/src/data/models/jersey-badge.js +56 -0
- package/dist/esm/src/data/models/premium-grant.d.ts +39 -0
- package/dist/esm/src/data/models/premium-grant.js +78 -0
- package/dist/esm/src/data/models/team-jersey.d.ts +20 -0
- package/dist/esm/src/data/models/team-jersey.js +34 -0
- package/dist/esm/src/data/models/team.d.ts +3 -1
- package/dist/esm/src/data/transformers/team.js +4 -1
- package/dist/esm/src/service/competition/event-schedule.d.ts +1 -1
- package/dist/esm/src/service/competition/event-schedule.js +3 -1
- package/dist/esm/src/service/competition/event-schedule.test.js +4 -2
- package/dist/esm/src/service/index.d.ts +1 -0
- package/dist/esm/src/service/index.js +1 -0
- package/dist/esm/src/service/player/improvement.test.js +5 -5
- package/dist/esm/src/service/player/stats.js +30 -16
- package/dist/esm/src/service/player/stats.test.js +6 -6
- package/dist/esm/src/service/premium/index.d.ts +3 -0
- package/dist/esm/src/service/premium/index.js +3 -0
- package/dist/esm/src/service/premium/patreon-tier-map.d.ts +5 -0
- package/dist/esm/src/service/premium/patreon-tier-map.js +18 -0
- package/dist/esm/src/service/premium/patreon-tier-map.test.d.ts +1 -0
- package/dist/esm/src/service/premium/patreon-tier-map.test.js +46 -0
- package/dist/esm/src/service/premium/premium-grant.d.ts +17 -0
- package/dist/esm/src/service/premium/premium-grant.js +11 -0
- package/dist/esm/src/service/premium/premium-tier.d.ts +10 -0
- package/dist/esm/src/service/premium/premium-tier.js +19 -0
- package/dist/esm/src/service/premium/premium.test.d.ts +1 -0
- package/dist/esm/src/service/premium/premium.test.js +91 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/jersey.d.ts +74 -0
- package/dist/esm/src/service/team/jersey.js +30 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +69 -0
- package/dist/esm/src/service/team/schemas/team.z.js +3 -1
- package/dist/esm/src/service/team/team.d.ts +2 -0
- package/dist/esm/src/service/team/team.js +2 -1
- package/dist/esm/src/stat-config/improvement-floors.json +204 -204
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, PhysicianModel, PhysiotherapistModel, BoxScoreTotalsModel, CompetitionChampionModel, 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, LineupPresetModel, MatchPresetModel, EventModel, EventCompetitionModel, EventSquadModel, EventPackModel, EventPackPlayerModel, EventResultModel, EventBotTeamModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, 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, 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);
|
|
@@ -6,6 +6,7 @@ export function initModels(sequelize) {
|
|
|
6
6
|
const Physiotherapist = PhysiotherapistModel.initModel(sequelize);
|
|
7
7
|
const CurrencyTransaction = CurrencyTransactionModel.initModel(sequelize);
|
|
8
8
|
const Tactics = TacticsModel.initModel(sequelize);
|
|
9
|
+
const TeamJersey = TeamJerseyModel.initModel(sequelize);
|
|
9
10
|
const LineupPreset = LineupPresetModel.initModel(sequelize);
|
|
10
11
|
const MatchPreset = MatchPresetModel.initModel(sequelize);
|
|
11
12
|
const Event = EventModel.initModel(sequelize);
|
|
@@ -21,6 +22,7 @@ export function initModels(sequelize) {
|
|
|
21
22
|
const GachaPity = GachaPityModel.initModel(sequelize);
|
|
22
23
|
const GachaPullHistory = GachaPullHistoryModel.initModel(sequelize);
|
|
23
24
|
const UserPullGrant = UserPullGrantModel.initModel(sequelize);
|
|
25
|
+
const PremiumGrant = PremiumGrantModel.initModel(sequelize);
|
|
24
26
|
const Wishlist = WishlistModel.initModel(sequelize);
|
|
25
27
|
const UserSettings = UserSettingsModel.initModel(sequelize);
|
|
26
28
|
const UserDeviceToken = UserDeviceTokenModel.initModel(sequelize);
|
|
@@ -36,6 +38,7 @@ export function initModels(sequelize) {
|
|
|
36
38
|
const CompetitionStandingsMatch = CompetitionStandingsMatchModel.initModel(sequelize);
|
|
37
39
|
const CompetitionTeams = CompetitionTeamsModel.initModel(sequelize);
|
|
38
40
|
const CompetitionChampion = CompetitionChampionModel.initModel(sequelize);
|
|
41
|
+
const JerseyBadge = JerseyBadgeModel.initModel(sequelize);
|
|
39
42
|
const CompetitionMatch = CompetitionMatchModel.initModel(sequelize);
|
|
40
43
|
const Iteration = IterationModel.initModel(sequelize);
|
|
41
44
|
const League = LeagueModel.initModel(sequelize);
|
|
@@ -80,6 +83,7 @@ export function initModels(sequelize) {
|
|
|
80
83
|
Notification.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
81
84
|
AuthUser.hasMany(GachaPullHistory, { as: 'GachaPullHistory', foreignKey: 'user_id' });
|
|
82
85
|
AuthUser.hasMany(UserPullGrant, { as: 'UserPullGrants', foreignKey: 'user_id' });
|
|
86
|
+
AuthUser.hasMany(PremiumGrant, { as: 'PremiumGrants', foreignKey: 'user_id' });
|
|
83
87
|
AuthIdentity.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
84
88
|
AuthSession.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
85
89
|
UserTeam.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
@@ -90,6 +94,8 @@ export function initModels(sequelize) {
|
|
|
90
94
|
GachaPullHistory.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
91
95
|
GachaPullHistory.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
|
|
92
96
|
UserPullGrant.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
97
|
+
PremiumGrant.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
98
|
+
PremiumGrant.belongsTo(AuthUser, { as: 'grantedByUser', foreignKey: 'granted_by' });
|
|
93
99
|
Wishlist.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
94
100
|
UserSettings.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
|
|
95
101
|
Competition.belongsTo(Iteration, { as: 'Iteration', foreignKey: 'iteration' });
|
|
@@ -107,6 +113,8 @@ export function initModels(sequelize) {
|
|
|
107
113
|
});
|
|
108
114
|
CompetitionChampion.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
|
|
109
115
|
CompetitionChampion.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
116
|
+
JerseyBadge.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
|
|
117
|
+
JerseyBadge.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
110
118
|
CompetitionMVP.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
|
|
111
119
|
CompetitionMVP.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
|
|
112
120
|
CompetitionMatch.belongsTo(Competition, { as: 'Competition', foreignKey: 'competition_id' });
|
|
@@ -225,6 +233,10 @@ export function initModels(sequelize) {
|
|
|
225
233
|
Team.hasOne(Physiotherapist, { as: 'Physiotherapist', foreignKey: 'team_id' });
|
|
226
234
|
Physiotherapist.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
227
235
|
Team.hasOne(Tactics, { as: 'tactics', foreignKey: 'team_id' });
|
|
236
|
+
// Saved premium kit. Like lineup presets, deliberately NOT in Team.defaultScope: only the management API (editor)
|
|
237
|
+
// and the effective-jersey resolver include it, so the simulator's team loads are unchanged.
|
|
238
|
+
Team.hasOne(TeamJersey, { as: 'TeamJersey', foreignKey: 'team_id' });
|
|
239
|
+
TeamJersey.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
228
240
|
// Lineup presets are loaded only by the management API; deliberately NOT in Team.defaultScope so the
|
|
229
241
|
// simulator's team loads are unchanged (it reads the active config from the mirrored Tactics row).
|
|
230
242
|
Team.hasMany(LineupPreset, { as: 'lineupPresets', foreignKey: 'team_id' });
|
|
@@ -306,6 +318,7 @@ export function initModels(sequelize) {
|
|
|
306
318
|
AuthUser,
|
|
307
319
|
Competition,
|
|
308
320
|
CompetitionChampion,
|
|
321
|
+
JerseyBadge,
|
|
309
322
|
CompetitionMatch,
|
|
310
323
|
CompetitionMVP,
|
|
311
324
|
CompetitionStandings,
|
|
@@ -351,6 +364,7 @@ export function initModels(sequelize) {
|
|
|
351
364
|
GachaPity,
|
|
352
365
|
GachaPullHistory,
|
|
353
366
|
UserPullGrant,
|
|
367
|
+
PremiumGrant,
|
|
354
368
|
Wishlist,
|
|
355
369
|
UserSettings,
|
|
356
370
|
UserDeviceToken,
|
|
@@ -14,7 +14,7 @@ export interface AuthIdentityAttributes {
|
|
|
14
14
|
}
|
|
15
15
|
export type AuthIdentityPk = 'identity_id';
|
|
16
16
|
export type AuthIdentityId = AuthIdentityModel[AuthIdentityPk];
|
|
17
|
-
export type AuthProvider = 'EMAIL' | 'GOOGLE' | 'FACEBOOK' | 'APPLE';
|
|
17
|
+
export type AuthProvider = 'EMAIL' | 'GOOGLE' | 'FACEBOOK' | 'APPLE' | 'PATREON';
|
|
18
18
|
export type AuthIdentityOptionalAttributes = 'email' | 'password_hash' | 'last_used_at' | 'created_at' | 'updated_at';
|
|
19
19
|
export type AuthIdentityCreationAttributes = Optional<AuthIdentityAttributes, AuthIdentityOptionalAttributes>;
|
|
20
20
|
export declare class AuthIdentityModel extends Model<AuthIdentityAttributes, AuthIdentityCreationAttributes> implements AuthIdentityAttributes {
|
|
@@ -16,7 +16,7 @@ export class AuthIdentityModel extends Model {
|
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
provider: {
|
|
19
|
-
type: DataTypes.ENUM('EMAIL', 'GOOGLE', 'FACEBOOK', 'APPLE'),
|
|
19
|
+
type: DataTypes.ENUM('EMAIL', 'GOOGLE', 'FACEBOOK', 'APPLE', 'PATREON'),
|
|
20
20
|
allowNull: false
|
|
21
21
|
},
|
|
22
22
|
provider_user_id: {
|
|
@@ -7,6 +7,7 @@ export * from './competition';
|
|
|
7
7
|
export * from './competition-mvp';
|
|
8
8
|
export * from './competition-champion';
|
|
9
9
|
export * from './competition-match';
|
|
10
|
+
export * from './jersey-badge';
|
|
10
11
|
export * from './competition-standings';
|
|
11
12
|
export * from './competition-standings-match';
|
|
12
13
|
export * from './competition-teams';
|
|
@@ -37,8 +38,10 @@ export * from './physiotherapist';
|
|
|
37
38
|
export * from './team';
|
|
38
39
|
export * from './team-replacement';
|
|
39
40
|
export * from './tactics';
|
|
41
|
+
export * from './team-jersey';
|
|
40
42
|
export * from './lineup-preset';
|
|
41
43
|
export * from './base-position-preset';
|
|
44
|
+
export * from './premium-grant';
|
|
42
45
|
export * from './event';
|
|
43
46
|
export * from './event-competition';
|
|
44
47
|
export * from './event-bot-team';
|
|
@@ -7,6 +7,7 @@ export * from './competition';
|
|
|
7
7
|
export * from './competition-mvp';
|
|
8
8
|
export * from './competition-champion';
|
|
9
9
|
export * from './competition-match';
|
|
10
|
+
export * from './jersey-badge';
|
|
10
11
|
export * from './competition-standings';
|
|
11
12
|
export * from './competition-standings-match';
|
|
12
13
|
export * from './competition-teams';
|
|
@@ -37,8 +38,10 @@ export * from './physiotherapist';
|
|
|
37
38
|
export * from './team';
|
|
38
39
|
export * from './team-replacement';
|
|
39
40
|
export * from './tactics';
|
|
41
|
+
export * from './team-jersey';
|
|
40
42
|
export * from './lineup-preset';
|
|
41
43
|
export * from './base-position-preset';
|
|
44
|
+
export * from './premium-grant';
|
|
42
45
|
export * from './event';
|
|
43
46
|
export * from './event-competition';
|
|
44
47
|
export * from './event-bot-team';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { CompetitionId, CompetitionModel, TeamId, TeamModel } from '.';
|
|
4
|
+
export type JerseyBadgeKind = 'LEAGUE' | 'NATIONALS' | 'REGIONALS' | 'WCC';
|
|
5
|
+
export interface JerseyBadgeAttributes {
|
|
6
|
+
competition_id: string;
|
|
7
|
+
team_id: string;
|
|
8
|
+
badge: JerseyBadgeKind;
|
|
9
|
+
region: string | null;
|
|
10
|
+
start_time: Date;
|
|
11
|
+
end_time: Date;
|
|
12
|
+
}
|
|
13
|
+
export type JerseyBadgePk = 'competition_id';
|
|
14
|
+
export type JerseyBadgeId = JerseyBadgeModel[JerseyBadgePk];
|
|
15
|
+
export type JerseyBadgeCreationAttributes = JerseyBadgeAttributes;
|
|
16
|
+
export declare class JerseyBadgeModel extends Model<JerseyBadgeAttributes, JerseyBadgeCreationAttributes> implements JerseyBadgeAttributes {
|
|
17
|
+
competition_id: string;
|
|
18
|
+
team_id: string;
|
|
19
|
+
badge: JerseyBadgeKind;
|
|
20
|
+
region: string | null;
|
|
21
|
+
start_time: Date;
|
|
22
|
+
end_time: Date;
|
|
23
|
+
competition: CompetitionModel;
|
|
24
|
+
getCompetition: Sequelize.BelongsToGetAssociationMixin<CompetitionModel>;
|
|
25
|
+
setCompetition: Sequelize.BelongsToSetAssociationMixin<CompetitionModel, CompetitionId>;
|
|
26
|
+
createCompetition: Sequelize.BelongsToCreateAssociationMixin<CompetitionModel>;
|
|
27
|
+
team: TeamModel;
|
|
28
|
+
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
29
|
+
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
30
|
+
createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
|
|
31
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof JerseyBadgeModel;
|
|
32
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class JerseyBadgeModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return JerseyBadgeModel.init({
|
|
5
|
+
competition_id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
references: {
|
|
10
|
+
model: 'Competition',
|
|
11
|
+
key: 'competition_id'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
team_id: {
|
|
15
|
+
type: DataTypes.UUID,
|
|
16
|
+
allowNull: false,
|
|
17
|
+
references: {
|
|
18
|
+
model: 'Team',
|
|
19
|
+
key: 'team_id'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
badge: {
|
|
23
|
+
type: DataTypes.ENUM('LEAGUE', 'NATIONALS', 'REGIONALS', 'WCC'),
|
|
24
|
+
allowNull: false
|
|
25
|
+
},
|
|
26
|
+
region: {
|
|
27
|
+
type: DataTypes.STRING(3),
|
|
28
|
+
allowNull: true
|
|
29
|
+
},
|
|
30
|
+
start_time: {
|
|
31
|
+
type: DataTypes.DATE,
|
|
32
|
+
allowNull: false
|
|
33
|
+
},
|
|
34
|
+
end_time: {
|
|
35
|
+
type: DataTypes.DATE,
|
|
36
|
+
allowNull: false
|
|
37
|
+
}
|
|
38
|
+
}, {
|
|
39
|
+
sequelize,
|
|
40
|
+
tableName: 'JerseyBadge',
|
|
41
|
+
schema: 'public',
|
|
42
|
+
timestamps: false,
|
|
43
|
+
indexes: [
|
|
44
|
+
{
|
|
45
|
+
name: 'jerseybadge_pk',
|
|
46
|
+
unique: true,
|
|
47
|
+
fields: [{ name: 'competition_id' }]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'JerseyBadge_team_active_idx',
|
|
51
|
+
fields: [{ name: 'team_id' }, { name: 'start_time' }, { name: 'end_time' }]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model, Optional } from 'sequelize';
|
|
3
|
+
import { PremiumGrantSource } from '../../service';
|
|
4
|
+
import { AuthUserId, AuthUserModel } from '.';
|
|
5
|
+
export interface PremiumGrantAttributes {
|
|
6
|
+
grant_id: string;
|
|
7
|
+
user_id: string;
|
|
8
|
+
tier: number;
|
|
9
|
+
source: PremiumGrantSource;
|
|
10
|
+
granted_by?: string | null;
|
|
11
|
+
granted_at: Date;
|
|
12
|
+
expires_at?: Date | null;
|
|
13
|
+
revoked_at?: Date | null;
|
|
14
|
+
patreon_member_id?: string | null;
|
|
15
|
+
note?: string | null;
|
|
16
|
+
}
|
|
17
|
+
export type PremiumGrantPk = 'grant_id';
|
|
18
|
+
export type PremiumGrantId = PremiumGrantModel[PremiumGrantPk];
|
|
19
|
+
export type PremiumGrantOptionalAttributes = 'grant_id' | 'granted_by' | 'granted_at' | 'expires_at' | 'revoked_at' | 'patreon_member_id' | 'note';
|
|
20
|
+
export type PremiumGrantCreationAttributes = Optional<PremiumGrantAttributes, PremiumGrantOptionalAttributes>;
|
|
21
|
+
export declare class PremiumGrantModel extends Model<PremiumGrantAttributes, PremiumGrantCreationAttributes> implements PremiumGrantAttributes {
|
|
22
|
+
grant_id: string;
|
|
23
|
+
user_id: string;
|
|
24
|
+
tier: number;
|
|
25
|
+
source: PremiumGrantSource;
|
|
26
|
+
granted_by?: string | null;
|
|
27
|
+
granted_at: Date;
|
|
28
|
+
expires_at?: Date | null;
|
|
29
|
+
revoked_at?: Date | null;
|
|
30
|
+
patreon_member_id?: string | null;
|
|
31
|
+
note?: string | null;
|
|
32
|
+
user: AuthUserModel;
|
|
33
|
+
getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
34
|
+
setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
|
|
35
|
+
createUser: Sequelize.BelongsToCreateAssociationMixin<AuthUserModel>;
|
|
36
|
+
grantedByUser?: AuthUserModel | null;
|
|
37
|
+
getGrantedByUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
38
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof PremiumGrantModel;
|
|
39
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class PremiumGrantModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return PremiumGrantModel.init({
|
|
5
|
+
grant_id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
defaultValue: DataTypes.UUIDV4
|
|
10
|
+
},
|
|
11
|
+
user_id: {
|
|
12
|
+
type: DataTypes.UUID,
|
|
13
|
+
allowNull: false,
|
|
14
|
+
references: {
|
|
15
|
+
model: 'AuthUser',
|
|
16
|
+
key: 'user_id'
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
tier: {
|
|
20
|
+
type: DataTypes.INTEGER,
|
|
21
|
+
allowNull: false
|
|
22
|
+
},
|
|
23
|
+
source: {
|
|
24
|
+
type: DataTypes.ENUM('ADMIN', 'PATREON'),
|
|
25
|
+
allowNull: false
|
|
26
|
+
},
|
|
27
|
+
granted_by: {
|
|
28
|
+
type: DataTypes.UUID,
|
|
29
|
+
allowNull: true,
|
|
30
|
+
references: {
|
|
31
|
+
model: 'AuthUser',
|
|
32
|
+
key: 'user_id'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
granted_at: {
|
|
36
|
+
type: DataTypes.DATE,
|
|
37
|
+
allowNull: false,
|
|
38
|
+
defaultValue: DataTypes.NOW
|
|
39
|
+
},
|
|
40
|
+
expires_at: {
|
|
41
|
+
type: DataTypes.DATE,
|
|
42
|
+
allowNull: true
|
|
43
|
+
},
|
|
44
|
+
revoked_at: {
|
|
45
|
+
type: DataTypes.DATE,
|
|
46
|
+
allowNull: true
|
|
47
|
+
},
|
|
48
|
+
patreon_member_id: {
|
|
49
|
+
type: DataTypes.STRING,
|
|
50
|
+
allowNull: true
|
|
51
|
+
},
|
|
52
|
+
note: {
|
|
53
|
+
type: DataTypes.TEXT,
|
|
54
|
+
allowNull: true
|
|
55
|
+
}
|
|
56
|
+
}, {
|
|
57
|
+
sequelize,
|
|
58
|
+
tableName: 'PremiumGrant',
|
|
59
|
+
schema: 'public',
|
|
60
|
+
timestamps: false,
|
|
61
|
+
indexes: [
|
|
62
|
+
{
|
|
63
|
+
name: 'PremiumGrant_pk',
|
|
64
|
+
unique: true,
|
|
65
|
+
fields: [{ name: 'grant_id' }]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'PremiumGrant_user_id_idx',
|
|
69
|
+
fields: [{ name: 'user_id' }]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'PremiumGrant_patreon_member_id_idx',
|
|
73
|
+
fields: [{ name: 'patreon_member_id' }]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { TeamId, TeamModel } from '.';
|
|
4
|
+
import { JerseyConfig } from '../../service';
|
|
5
|
+
export interface TeamJerseyAttributes {
|
|
6
|
+
team_id: string;
|
|
7
|
+
config: JerseyConfig;
|
|
8
|
+
}
|
|
9
|
+
export type TeamJerseyPk = 'team_id';
|
|
10
|
+
export type TeamJerseyId = TeamJerseyModel[TeamJerseyPk];
|
|
11
|
+
export type TeamJerseyCreationAttributes = TeamJerseyAttributes;
|
|
12
|
+
export declare class TeamJerseyModel extends Model<TeamJerseyAttributes, TeamJerseyCreationAttributes> implements TeamJerseyAttributes {
|
|
13
|
+
team_id: string;
|
|
14
|
+
config: JerseyConfig;
|
|
15
|
+
team: TeamModel;
|
|
16
|
+
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
17
|
+
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
18
|
+
createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
|
|
19
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof TeamJerseyModel;
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class TeamJerseyModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return TeamJerseyModel.init({
|
|
5
|
+
team_id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
references: {
|
|
10
|
+
model: 'Team',
|
|
11
|
+
key: 'team_id'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
config: {
|
|
15
|
+
type: DataTypes.JSONB,
|
|
16
|
+
allowNull: false
|
|
17
|
+
}
|
|
18
|
+
}, {
|
|
19
|
+
sequelize,
|
|
20
|
+
tableName: 'TeamJersey',
|
|
21
|
+
schema: 'public',
|
|
22
|
+
timestamps: false,
|
|
23
|
+
indexes: [
|
|
24
|
+
{
|
|
25
|
+
name: 'TeamJersey_pk',
|
|
26
|
+
unique: true,
|
|
27
|
+
fields: [
|
|
28
|
+
{ name: 'team_id' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
|
-
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, LegacyTeamFlagModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel } from '.';
|
|
3
|
+
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, LegacyTeamFlagModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel, TeamJerseyModel } from '.';
|
|
4
4
|
export interface TeamAttributes {
|
|
5
5
|
team_id: string;
|
|
6
6
|
name: string;
|
|
@@ -29,6 +29,8 @@ export declare class TeamModel extends Model<TeamAttributes, TeamCreationAttribu
|
|
|
29
29
|
getTactics: Sequelize.HasOneGetAssociationMixin<TacticsModel>;
|
|
30
30
|
setTactics: Sequelize.HasOneSetAssociationMixin<TacticsModel, TacticsId>;
|
|
31
31
|
createTactics: Sequelize.HasOneCreateAssociationMixin<TacticsModel>;
|
|
32
|
+
TeamJersey?: TeamJerseyModel;
|
|
33
|
+
getTeamJersey: Sequelize.HasOneGetAssociationMixin<TeamJerseyModel>;
|
|
32
34
|
country: CountryModel;
|
|
33
35
|
getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
|
|
34
36
|
setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
|
|
@@ -43,7 +43,10 @@ function transformToObject(model, currentIteration, tacticsOverride) {
|
|
|
43
43
|
rating: model.rating,
|
|
44
44
|
tactics,
|
|
45
45
|
roster,
|
|
46
|
-
divisionId: model.division_id
|
|
46
|
+
divisionId: model.division_id,
|
|
47
|
+
// Present only when the caller explicitly included the TeamJersey association (editor / effective-jersey
|
|
48
|
+
// resolver); undefined on ordinary team loads, which heals to the id-derived default in the UI.
|
|
49
|
+
jerseyConfig: model.TeamJersey?.config
|
|
47
50
|
});
|
|
48
51
|
}
|
|
49
52
|
export { transformToObject as transformToTeam, transformToAttributes as transformFromTeam };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Rarity } from '../player';
|
|
2
|
-
export type EventKey = 'WAR_OF_THE_COMMONS' | 'RARE_GEMS' | 'STUFF_OF_LEGENDS';
|
|
2
|
+
export type EventKey = 'WAR_OF_THE_COMMONS' | 'RARE_GEMS' | 'STUFF_OF_LEGENDS' | 'AGE_OF_MYTHOLOGIES' | 'SUPER_SPECIAL';
|
|
3
3
|
/**
|
|
4
4
|
* How an event is played out. One member today; a new format is a new member here AND the code path in the
|
|
5
5
|
* simulator that builds its schedule, which is why formats live in code rather than in a table.
|
|
@@ -7,7 +7,9 @@ dayjs.extend(utc);
|
|
|
7
7
|
export const EVENT_CATALOGUE = [
|
|
8
8
|
{ key: 'WAR_OF_THE_COMMONS', format: 'GROUPS_FINAL', rarity: RarityEnum.COMMON, durationDays: 7 },
|
|
9
9
|
{ key: 'RARE_GEMS', format: 'GROUPS_FINAL', rarity: RarityEnum.RARE, durationDays: 7 },
|
|
10
|
-
{ key: 'STUFF_OF_LEGENDS', format: 'GROUPS_FINAL', rarity: RarityEnum.LEGENDARY, durationDays: 7 }
|
|
10
|
+
{ key: 'STUFF_OF_LEGENDS', format: 'GROUPS_FINAL', rarity: RarityEnum.LEGENDARY, durationDays: 7 },
|
|
11
|
+
{ key: 'AGE_OF_MYTHOLOGIES', format: 'GROUPS_FINAL', rarity: RarityEnum.MYTHIC, durationDays: 7 },
|
|
12
|
+
{ key: 'SUPER_SPECIAL', format: 'GROUPS_FINAL', rarity: RarityEnum.SPECIAL, durationDays: 7 }
|
|
11
13
|
];
|
|
12
14
|
/** An event must be long enough to hold a group stage AND a final day. */
|
|
13
15
|
export const MIN_DURATION_DAYS = 2;
|
|
@@ -3,8 +3,10 @@ import { EVENT_CATALOGUE, MAX_FINAL_SIZE, MIN_GROUP_SIZE, MIN_ROUND_SPACING_MINU
|
|
|
3
3
|
import { RarityEnum } from '../player';
|
|
4
4
|
const COMMONS = eventDefinitionForKey('WAR_OF_THE_COMMONS');
|
|
5
5
|
describe('EVENT_CATALOGUE', () => {
|
|
6
|
-
it('restricts
|
|
7
|
-
expect(EVENT_CATALOGUE.map(e => e.rarity)).toEqual([
|
|
6
|
+
it('restricts each preset to a single rarity, one per rarity, over one week', () => {
|
|
7
|
+
expect(EVENT_CATALOGUE.map(e => e.rarity)).toEqual([
|
|
8
|
+
RarityEnum.COMMON, RarityEnum.RARE, RarityEnum.LEGENDARY, RarityEnum.MYTHIC, RarityEnum.SPECIAL
|
|
9
|
+
]);
|
|
8
10
|
expect(EVENT_CATALOGUE.every(e => e.durationDays === 7)).toBe(true);
|
|
9
11
|
expect(EVENT_CATALOGUE.every(e => e.format === 'GROUPS_FINAL')).toBe(true);
|
|
10
12
|
});
|
|
@@ -192,8 +192,8 @@ describe('selectImprovableStat()', () => {
|
|
|
192
192
|
const cap = getStatCap(RarityEnum.COMMON); // 59
|
|
193
193
|
expect(selectImprovableStat(RarityEnum.COMMON, StatsEnum.ATTACK, allStats(cap))).toBeNull();
|
|
194
194
|
});
|
|
195
|
-
it('returns a stat from the SERVE focus pool (serve, awareness, attack)', () => {
|
|
196
|
-
const focusPool = new Set(['serve', 'awareness', 'attack']);
|
|
195
|
+
it('returns a stat from the SERVE focus pool (serve, awareness, focus, attack)', () => {
|
|
196
|
+
const focusPool = new Set(['serve', 'awareness', 'focus', 'attack']);
|
|
197
197
|
const stats = allStats(1);
|
|
198
198
|
// Run many times to confirm we never escape the focus pool
|
|
199
199
|
for (let i = 0; i < 100; i++) {
|
|
@@ -213,13 +213,13 @@ describe('selectImprovableStat()', () => {
|
|
|
213
213
|
});
|
|
214
214
|
it('falls back to any improvable stat when all focus-pool stats are capped', () => {
|
|
215
215
|
const cap = getStatCap(RarityEnum.COMMON);
|
|
216
|
-
// Cap the SERVE focus stats: serve, awareness, attack
|
|
217
|
-
const stats = { ...allStats(1), serve: cap, awareness: cap, attack: cap };
|
|
216
|
+
// Cap the SERVE focus stats: serve, awareness, focus, attack
|
|
217
|
+
const stats = { ...allStats(1), serve: cap, awareness: cap, focus: cap, attack: cap };
|
|
218
218
|
for (let i = 0; i < 50; i++) {
|
|
219
219
|
const result = selectImprovableStat(RarityEnum.COMMON, StatsEnum.SERVE, stats);
|
|
220
220
|
expect(result).not.toBeNull();
|
|
221
221
|
// Must never return one of the capped focus stats
|
|
222
|
-
expect(['serve', 'awareness', 'attack']).not.toContain(result);
|
|
222
|
+
expect(['serve', 'awareness', 'focus', 'attack']).not.toContain(result);
|
|
223
223
|
}
|
|
224
224
|
});
|
|
225
225
|
it('returns a string (DB column name) not a PerformanceStatsKey', () => {
|
|
@@ -17,12 +17,14 @@ export function getMultipliers(statName, performanceStats) {
|
|
|
17
17
|
switch (statName) {
|
|
18
18
|
case StatsEnum.ATTACK:
|
|
19
19
|
return {
|
|
20
|
-
spike: 0.
|
|
20
|
+
spike: 0.5,
|
|
21
21
|
backAttack: 0.1,
|
|
22
22
|
power: performanceStats == null ? 0.125 : performanceStats.power >= performanceStats.quick ? 0.2 : 0.05,
|
|
23
23
|
quick: performanceStats == null ? 0.125 : performanceStats.quick > performanceStats.power ? 0.2 : 0.05,
|
|
24
24
|
awareness: 0.05,
|
|
25
|
-
attack: 0.05
|
|
25
|
+
attack: 0.05,
|
|
26
|
+
// focus places the spike inside its aim cone (attack-shot.focusLanding): placement precision, kill vs dug.
|
|
27
|
+
focus: 0.05
|
|
26
28
|
};
|
|
27
29
|
case StatsEnum.SET:
|
|
28
30
|
return {
|
|
@@ -31,33 +33,45 @@ export function getMultipliers(statName, performanceStats) {
|
|
|
31
33
|
// rating AND in the SET training pool. Without it a setter on SET focus can never raise `quick` (it has
|
|
32
34
|
// weight 0 in every role too), and their whole tempo game would be fixed forever by a generation roll.
|
|
33
35
|
quick: 0.07,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
// set TECHNIQUE: overhand sets good passes, bump sets scramble/negative passes (both +/-15% on set score).
|
|
37
|
+
overhand: 0.08,
|
|
38
|
+
bump: 0.07,
|
|
39
|
+
// attack is the setter's crit-style upside on the set (setAttackMod in the engine, owner 2026-08-17), the
|
|
40
|
+
// same modifier the spike + serve use. awareness dropped: the engine reads it only for the setter DUMP.
|
|
36
41
|
attack: 0.05
|
|
37
42
|
};
|
|
38
43
|
case StatsEnum.RECEIVE:
|
|
44
|
+
// reflex carries the most (owner 2026-08-19): reception success is gated by REFLEX (the reach + lunge-save on
|
|
45
|
+
// any serve that lands out of position), not by the `reception` control stat. A low-reflex passer is aced
|
|
46
|
+
// regardless of reception -- match 3aebe480 had a Mythic (reception 88, reflex 36) shank 79% of receptions,
|
|
47
|
+
// and the league >=40%-ace group averages reflex 24-32 across every rarity. `reception` is the pass CONTROL
|
|
48
|
+
// once the ball is reached; bump/overhand are technique; defense is the pass variance; focus avoids the seam ace.
|
|
39
49
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
bump: 0.
|
|
43
|
-
|
|
44
|
-
defense: 0.
|
|
45
|
-
|
|
50
|
+
reflex: 0.45,
|
|
51
|
+
reception: 0.3,
|
|
52
|
+
bump: 0.1,
|
|
53
|
+
overhand: 0.05,
|
|
54
|
+
defense: 0.05,
|
|
55
|
+
focus: 0.05
|
|
46
56
|
};
|
|
47
57
|
case StatsEnum.SERVE:
|
|
48
58
|
return {
|
|
49
|
-
serve: 0.
|
|
50
|
-
awareness
|
|
51
|
-
|
|
59
|
+
serve: 0.75,
|
|
60
|
+
// awareness (target selection) + focus (aim-cone placement) are important serve drivers (owner 2026-08-17).
|
|
61
|
+
awareness: 0.1,
|
|
62
|
+
focus: 0.1,
|
|
63
|
+
attack: 0.05
|
|
52
64
|
};
|
|
53
65
|
case StatsEnum.BLOCK:
|
|
54
66
|
return {
|
|
55
|
-
block: 0.
|
|
67
|
+
block: 0.6,
|
|
56
68
|
read: performanceStats == null ? 0.125 : performanceStats.read >= performanceStats.commit ? 0.2 : 0.05,
|
|
57
69
|
commit: performanceStats == null ? 0.125 : performanceStats.commit > performanceStats.read ? 0.2 : 0.05,
|
|
58
|
-
|
|
70
|
+
// reflex sets block REACH (help blockers forming doubles/triples; dominant for surprise/dump blocks): raised
|
|
71
|
+
// from 0.02 to match its impact.
|
|
72
|
+
reflex: 0.08,
|
|
59
73
|
defense: 0.04,
|
|
60
|
-
|
|
74
|
+
focus: 0.03
|
|
61
75
|
};
|
|
62
76
|
case StatsEnum.STAMINA:
|
|
63
77
|
return { stamina: 1 };
|
|
@@ -62,10 +62,10 @@ describe('getMultipliers()', () => {
|
|
|
62
62
|
const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
|
|
63
63
|
expect(total).toBeCloseTo(1, 10);
|
|
64
64
|
});
|
|
65
|
-
it('
|
|
65
|
+
it('reflex has the highest weight (reflex gates the reach + lunge-save)', () => {
|
|
66
66
|
const m = getMultipliers(StatsEnum.RECEIVE);
|
|
67
67
|
const max = Math.max(...Object.values(m).map(v => v ?? 0));
|
|
68
|
-
expect(m.
|
|
68
|
+
expect(m.reflex).toBe(max);
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
describe('SERVE', () => {
|
|
@@ -145,11 +145,11 @@ describe('calculateStatScore()', () => {
|
|
|
145
145
|
const strong = calculateStatScore(makeStats(80), StatsEnum.SERVE);
|
|
146
146
|
expect(strong).toBeGreaterThan(weak);
|
|
147
147
|
});
|
|
148
|
-
it('SERVE: serve=99 dominates, contributing 0.
|
|
149
|
-
// All other serve-relevant stats at 1: awareness=1, attack=1
|
|
150
|
-
// score ≈ 99*0.
|
|
148
|
+
it('SERVE: serve=99 dominates, contributing 0.75 of the total', () => {
|
|
149
|
+
// All other serve-relevant stats at 1: awareness=1, focus=1, attack=1
|
|
150
|
+
// score ≈ 99*0.75 + 1*0.1 + 1*0.1 + 1*0.05 = 74.25 + 0.25 = 74.5 → 74
|
|
151
151
|
const stats = makeStats(1, { serve: 99 });
|
|
152
|
-
expect(calculateStatScore(stats, StatsEnum.SERVE)).toBe(
|
|
152
|
+
expect(calculateStatScore(stats, StatsEnum.SERVE)).toBe(74);
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
155
|
// ─── invariants (every stat, every branch) ────────────────────────────────────
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const PATREON_TIER_MAP_KEY = "patreon_tier_map";
|
|
3
|
+
export type PatreonTierMap = Record<string, number>;
|
|
4
|
+
export declare const patreonTierMapSchema: z.ZodType<PatreonTierMap>;
|
|
5
|
+
export declare function resolvePremiumTier(entitledTierIds: string[], map: PatreonTierMap): number;
|