volleyballsimtypes 0.0.360 → 0.0.361

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.
@@ -40,6 +40,7 @@ function initModels(sequelize) {
40
40
  const Player = models_1.PlayerModel.initModel(sequelize);
41
41
  const PlayerImprovementLog = models_1.PlayerImprovementLogModel.initModel(sequelize);
42
42
  const PlayerTeam = models_1.PlayerTeamModel.initModel(sequelize);
43
+ const RetiredPlayer = models_1.RetiredPlayerModel.initModel(sequelize);
43
44
  const Rally = models_1.RallyModel.initModel(sequelize);
44
45
  const PromotionMatch = models_1.PromotionMatchModel.initModel(sequelize);
45
46
  const RegionQualifier = models_1.RegionQualifierModel.initModel(sequelize);
@@ -168,6 +169,9 @@ function initModels(sequelize) {
168
169
  Player.hasOne(BoxScoreTotals, { as: 'BoxScoreTotals', foreignKey: 'player_id' });
169
170
  PlayerTeam.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
170
171
  PlayerTeam.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
172
+ RetiredPlayer.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
173
+ RetiredPlayer.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
174
+ Player.hasOne(RetiredPlayer, { as: 'RetiredPlayer', foreignKey: 'player_id' });
171
175
  Rally.belongsTo(MatchSet, { as: 'matchSet', foreignKey: 'match_set_id' });
172
176
  Rally.belongsTo(Team, { as: 'ServingTeam', foreignKey: 'serving_team' });
173
177
  PromotionMatch.belongsTo(Iteration, { as: 'iterationData', foreignKey: 'iteration' });
@@ -242,6 +246,7 @@ function initModels(sequelize) {
242
246
  PerformanceStats,
243
247
  Player,
244
248
  PlayerTeam,
249
+ RetiredPlayer,
245
250
  PlayerImprovementLog,
246
251
  PromotionMatch,
247
252
  RegionQualifier,
@@ -23,6 +23,7 @@ export * from './performance-stats';
23
23
  export * from './player';
24
24
  export * from './player-improvement-log';
25
25
  export * from './player-team';
26
+ export * from './retired-player';
26
27
  export * from './rally';
27
28
  export * from './promotion-match';
28
29
  export * from './region-qualifier';
@@ -39,6 +39,7 @@ __exportStar(require("./performance-stats"), exports);
39
39
  __exportStar(require("./player"), exports);
40
40
  __exportStar(require("./player-improvement-log"), exports);
41
41
  __exportStar(require("./player-team"), exports);
42
+ __exportStar(require("./retired-player"), exports);
42
43
  __exportStar(require("./rally"), exports);
43
44
  __exportStar(require("./promotion-match"), exports);
44
45
  __exportStar(require("./region-qualifier"), exports);
@@ -0,0 +1,27 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model } from 'sequelize';
3
+ import { PlayerId, PlayerModel, TeamId, TeamModel } from '.';
4
+ export interface RetiredPlayerAttributes {
5
+ player_id: string;
6
+ user_id: string;
7
+ team_id: string;
8
+ retired_iteration: number;
9
+ }
10
+ export type RetiredPlayerPk = 'player_id';
11
+ export type RetiredPlayerId = RetiredPlayerModel[RetiredPlayerPk];
12
+ export type RetiredPlayerCreationAttributes = RetiredPlayerAttributes;
13
+ export declare class RetiredPlayerModel extends Model<RetiredPlayerAttributes, RetiredPlayerCreationAttributes> implements RetiredPlayerAttributes {
14
+ player_id: string;
15
+ user_id: string;
16
+ team_id: string;
17
+ retired_iteration: number;
18
+ player: PlayerModel;
19
+ getPlayer: Sequelize.BelongsToGetAssociationMixin<PlayerModel>;
20
+ setPlayer: Sequelize.BelongsToSetAssociationMixin<PlayerModel, PlayerId>;
21
+ createPlayer: Sequelize.BelongsToCreateAssociationMixin<PlayerModel>;
22
+ team: TeamModel;
23
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
24
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
25
+ createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
26
+ static initModel(sequelize: Sequelize.Sequelize): typeof RetiredPlayerModel;
27
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetiredPlayerModel = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ class RetiredPlayerModel extends sequelize_1.Model {
6
+ static initModel(sequelize) {
7
+ return RetiredPlayerModel.init({
8
+ player_id: {
9
+ type: sequelize_1.DataTypes.UUID,
10
+ allowNull: false,
11
+ primaryKey: true,
12
+ references: {
13
+ model: 'Player',
14
+ key: 'player_id'
15
+ }
16
+ },
17
+ user_id: {
18
+ type: sequelize_1.DataTypes.UUID,
19
+ allowNull: false,
20
+ references: {
21
+ model: 'AuthUser',
22
+ key: 'user_id'
23
+ }
24
+ },
25
+ team_id: {
26
+ type: sequelize_1.DataTypes.UUID,
27
+ allowNull: false,
28
+ references: {
29
+ model: 'Team',
30
+ key: 'team_id'
31
+ }
32
+ },
33
+ retired_iteration: {
34
+ type: sequelize_1.DataTypes.INTEGER,
35
+ allowNull: false
36
+ }
37
+ }, {
38
+ sequelize,
39
+ tableName: 'RetiredPlayer',
40
+ schema: 'public',
41
+ timestamps: false,
42
+ indexes: [{
43
+ name: 'RetiredPlayer_pk',
44
+ unique: true,
45
+ fields: [{ name: 'player_id' }]
46
+ }]
47
+ });
48
+ }
49
+ }
50
+ exports.RetiredPlayerModel = RetiredPlayerModel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const decline_1 = require("./decline");
5
+ const performance_stats_keys_1 = require("./performance-stats-keys");
6
+ // ─── getDeclineProfile ─────────────────────────────────────────────────────────
7
+ (0, globals_1.describe)('getDeclineProfile()', () => {
8
+ globals_1.it.each([
9
+ [decline_1.DeclineProfileEnum.PRODIGY, 27, 13.0, 1.0, 16.0],
10
+ [decline_1.DeclineProfileEnum.STANDARD, 29, 9.0, 1.75, 16.0],
11
+ [decline_1.DeclineProfileEnum.VETERAN, 32, 10.0, 1.5, 16.0],
12
+ [decline_1.DeclineProfileEnum.IRONMAN, 34, 10.0, 1.5, 16.0]
13
+ ])('%s → startAge %i', (profile, startAge, initTicks, accel, peakTicks) => {
14
+ const entry = (0, decline_1.getDeclineProfile)(profile);
15
+ (0, globals_1.expect)(entry.startAge).toBe(startAge);
16
+ (0, globals_1.expect)(entry.initTicks).toBeCloseTo(initTicks, 5);
17
+ (0, globals_1.expect)(entry.accel).toBeCloseTo(accel, 5);
18
+ (0, globals_1.expect)(entry.peakTicks).toBeCloseTo(peakTicks, 5);
19
+ });
20
+ (0, globals_1.it)('throws UNKNOWN_DECLINE_PROFILE for an unrecognised profile', () => {
21
+ (0, globals_1.expect)(() => (0, decline_1.getDeclineProfile)('LEGEND')).toThrow('UNKNOWN_DECLINE_PROFILE');
22
+ });
23
+ (0, globals_1.it)('declineProfiles lists all four enum values', () => {
24
+ (0, globals_1.expect)([...decline_1.declineProfiles].sort()).toEqual([...Object.values(decline_1.DeclineProfileEnum)].sort());
25
+ });
26
+ });
27
+ // ─── stat pools ────────────────────────────────────────────────────────────────
28
+ (0, globals_1.describe)('decline stat pools', () => {
29
+ (0, globals_1.it)('SKILL_POOL and ENERGY_POOL are disjoint', () => {
30
+ const overlap = decline_1.SKILL_POOL.filter(k => decline_1.ENERGY_POOL.includes(k));
31
+ (0, globals_1.expect)(overlap).toEqual([]);
32
+ });
33
+ (0, globals_1.it)('together they cover every performance stat key exactly once', () => {
34
+ const union = [...decline_1.SKILL_POOL, ...decline_1.ENERGY_POOL].sort();
35
+ (0, globals_1.expect)(union).toEqual([...performance_stats_keys_1.performanceStatKeys].sort());
36
+ });
37
+ (0, globals_1.it)('ENERGY_POOL holds the reaction/energy stats (incl. crit-chance stats)', () => {
38
+ (0, globals_1.expect)([...decline_1.ENERGY_POOL].sort()).toEqual(['awareness', 'commit', 'focus', 'read', 'reflex', 'stamina'].sort());
39
+ });
40
+ });
41
+ // ─── computeDeclineTick ────────────────────────────────────────────────────────
42
+ (0, globals_1.describe)('computeDeclineTick()', () => {
43
+ (0, globals_1.it)('returns no decline before the profile start age', () => {
44
+ for (const profile of decline_1.declineProfiles) {
45
+ const { startAge } = (0, decline_1.getDeclineProfile)(profile);
46
+ const tick = (0, decline_1.computeDeclineTick)(startAge - 1, profile);
47
+ (0, globals_1.expect)(tick.ticks).toBe(0);
48
+ (0, globals_1.expect)(tick.atPeak).toBe(false);
49
+ (0, globals_1.expect)(tick.status).toBe('ACTIVE');
50
+ }
51
+ });
52
+ (0, globals_1.it)('at the start age, tick rate equals round(initTicks) and status is DECLINING', () => {
53
+ for (const profile of decline_1.declineProfiles) {
54
+ const { startAge, initTicks } = (0, decline_1.getDeclineProfile)(profile);
55
+ const tick = (0, decline_1.computeDeclineTick)(startAge, profile);
56
+ (0, globals_1.expect)(tick.ticks).toBe(Math.round(initTicks));
57
+ (0, globals_1.expect)(tick.atPeak).toBe(false);
58
+ (0, globals_1.expect)(tick.status).toBe('DECLINING');
59
+ }
60
+ });
61
+ (0, globals_1.it)('tick count is non-decreasing as age increases', () => {
62
+ for (const profile of decline_1.declineProfiles) {
63
+ const { startAge } = (0, decline_1.getDeclineProfile)(profile);
64
+ let prev = -1;
65
+ for (let age = startAge; age <= startAge + 30; age++) {
66
+ const { ticks } = (0, decline_1.computeDeclineTick)(age, profile);
67
+ (0, globals_1.expect)(ticks).toBeGreaterThanOrEqual(prev);
68
+ prev = ticks;
69
+ }
70
+ }
71
+ });
72
+ (0, globals_1.it)('never exceeds peakTicks, and reaching it flips atPeak / PEAK', () => {
73
+ for (const profile of decline_1.declineProfiles) {
74
+ const { startAge, peakTicks } = (0, decline_1.getDeclineProfile)(profile);
75
+ for (let age = startAge; age <= startAge + 40; age++) {
76
+ const tick = (0, decline_1.computeDeclineTick)(age, profile);
77
+ (0, globals_1.expect)(tick.ticks).toBeLessThanOrEqual(Math.round(peakTicks));
78
+ if (tick.atPeak) {
79
+ (0, globals_1.expect)(tick.ticks).toBe(Math.round(peakTicks));
80
+ (0, globals_1.expect)(tick.status).toBe('PEAK');
81
+ }
82
+ }
83
+ }
84
+ });
85
+ (0, globals_1.it)('peak triggers at the analytically expected age for each profile', () => {
86
+ // age at peak = startAge + ceil((peakTicks - initTicks) / accel)
87
+ for (const profile of decline_1.declineProfiles) {
88
+ const { startAge, initTicks, accel, peakTicks } = (0, decline_1.getDeclineProfile)(profile);
89
+ const peakAge = startAge + Math.ceil((peakTicks - initTicks) / accel);
90
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(peakAge - 1, profile).atPeak).toBe(false);
91
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(peakAge, profile).atPeak).toBe(true);
92
+ }
93
+ });
94
+ (0, globals_1.it)('Standard profile matches the validated model at key ages', () => {
95
+ // initTicks 9, accel 1.75, peakTicks 16, startAge 29
96
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(28, decline_1.DeclineProfileEnum.STANDARD)).toEqual({ ticks: 0, atPeak: false, status: 'ACTIVE' });
97
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(29, decline_1.DeclineProfileEnum.STANDARD).ticks).toBe(9);
98
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(30, decline_1.DeclineProfileEnum.STANDARD).ticks).toBe(Math.round(9 + 1.75)); // 11
99
+ // 9 + 4*1.75 = 16 → peak at age 33
100
+ (0, globals_1.expect)((0, decline_1.computeDeclineTick)(33, decline_1.DeclineProfileEnum.STANDARD).atPeak).toBe(true);
101
+ });
102
+ });
@@ -1,4 +1,4 @@
1
- import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, PromotionMatchModel, RegionQualifierModel, RallyModel, TacticsModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel } from './models';
1
+ import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, RegionQualifierModel, RallyModel, TacticsModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel } from './models';
2
2
  export function initModels(sequelize) {
3
3
  const Coach = CoachModel.initModel(sequelize);
4
4
  const CurrencyTransaction = CurrencyTransactionModel.initModel(sequelize);
@@ -37,6 +37,7 @@ export function initModels(sequelize) {
37
37
  const Player = PlayerModel.initModel(sequelize);
38
38
  const PlayerImprovementLog = PlayerImprovementLogModel.initModel(sequelize);
39
39
  const PlayerTeam = PlayerTeamModel.initModel(sequelize);
40
+ const RetiredPlayer = RetiredPlayerModel.initModel(sequelize);
40
41
  const Rally = RallyModel.initModel(sequelize);
41
42
  const PromotionMatch = PromotionMatchModel.initModel(sequelize);
42
43
  const RegionQualifier = RegionQualifierModel.initModel(sequelize);
@@ -165,6 +166,9 @@ export function initModels(sequelize) {
165
166
  Player.hasOne(BoxScoreTotals, { as: 'BoxScoreTotals', foreignKey: 'player_id' });
166
167
  PlayerTeam.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
167
168
  PlayerTeam.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
169
+ RetiredPlayer.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
170
+ RetiredPlayer.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
171
+ Player.hasOne(RetiredPlayer, { as: 'RetiredPlayer', foreignKey: 'player_id' });
168
172
  Rally.belongsTo(MatchSet, { as: 'matchSet', foreignKey: 'match_set_id' });
169
173
  Rally.belongsTo(Team, { as: 'ServingTeam', foreignKey: 'serving_team' });
170
174
  PromotionMatch.belongsTo(Iteration, { as: 'iterationData', foreignKey: 'iteration' });
@@ -239,6 +243,7 @@ export function initModels(sequelize) {
239
243
  PerformanceStats,
240
244
  Player,
241
245
  PlayerTeam,
246
+ RetiredPlayer,
242
247
  PlayerImprovementLog,
243
248
  PromotionMatch,
244
249
  RegionQualifier,
@@ -23,6 +23,7 @@ export * from './performance-stats';
23
23
  export * from './player';
24
24
  export * from './player-improvement-log';
25
25
  export * from './player-team';
26
+ export * from './retired-player';
26
27
  export * from './rally';
27
28
  export * from './promotion-match';
28
29
  export * from './region-qualifier';
@@ -23,6 +23,7 @@ export * from './performance-stats';
23
23
  export * from './player';
24
24
  export * from './player-improvement-log';
25
25
  export * from './player-team';
26
+ export * from './retired-player';
26
27
  export * from './rally';
27
28
  export * from './promotion-match';
28
29
  export * from './region-qualifier';
@@ -0,0 +1,27 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model } from 'sequelize';
3
+ import { PlayerId, PlayerModel, TeamId, TeamModel } from '.';
4
+ export interface RetiredPlayerAttributes {
5
+ player_id: string;
6
+ user_id: string;
7
+ team_id: string;
8
+ retired_iteration: number;
9
+ }
10
+ export type RetiredPlayerPk = 'player_id';
11
+ export type RetiredPlayerId = RetiredPlayerModel[RetiredPlayerPk];
12
+ export type RetiredPlayerCreationAttributes = RetiredPlayerAttributes;
13
+ export declare class RetiredPlayerModel extends Model<RetiredPlayerAttributes, RetiredPlayerCreationAttributes> implements RetiredPlayerAttributes {
14
+ player_id: string;
15
+ user_id: string;
16
+ team_id: string;
17
+ retired_iteration: number;
18
+ player: PlayerModel;
19
+ getPlayer: Sequelize.BelongsToGetAssociationMixin<PlayerModel>;
20
+ setPlayer: Sequelize.BelongsToSetAssociationMixin<PlayerModel, PlayerId>;
21
+ createPlayer: Sequelize.BelongsToCreateAssociationMixin<PlayerModel>;
22
+ team: TeamModel;
23
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
24
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
25
+ createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
26
+ static initModel(sequelize: Sequelize.Sequelize): typeof RetiredPlayerModel;
27
+ }
@@ -0,0 +1,46 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class RetiredPlayerModel extends Model {
3
+ static initModel(sequelize) {
4
+ return RetiredPlayerModel.init({
5
+ player_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ references: {
10
+ model: 'Player',
11
+ key: 'player_id'
12
+ }
13
+ },
14
+ user_id: {
15
+ type: DataTypes.UUID,
16
+ allowNull: false,
17
+ references: {
18
+ model: 'AuthUser',
19
+ key: 'user_id'
20
+ }
21
+ },
22
+ team_id: {
23
+ type: DataTypes.UUID,
24
+ allowNull: false,
25
+ references: {
26
+ model: 'Team',
27
+ key: 'team_id'
28
+ }
29
+ },
30
+ retired_iteration: {
31
+ type: DataTypes.INTEGER,
32
+ allowNull: false
33
+ }
34
+ }, {
35
+ sequelize,
36
+ tableName: 'RetiredPlayer',
37
+ schema: 'public',
38
+ timestamps: false,
39
+ indexes: [{
40
+ name: 'RetiredPlayer_pk',
41
+ unique: true,
42
+ fields: [{ name: 'player_id' }]
43
+ }]
44
+ });
45
+ }
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { DeclineProfileEnum, declineProfiles, getDeclineProfile, computeDeclineTick, SKILL_POOL, ENERGY_POOL } from './decline';
3
+ import { performanceStatKeys } from './performance-stats-keys';
4
+ // ─── getDeclineProfile ─────────────────────────────────────────────────────────
5
+ describe('getDeclineProfile()', () => {
6
+ it.each([
7
+ [DeclineProfileEnum.PRODIGY, 27, 13.0, 1.0, 16.0],
8
+ [DeclineProfileEnum.STANDARD, 29, 9.0, 1.75, 16.0],
9
+ [DeclineProfileEnum.VETERAN, 32, 10.0, 1.5, 16.0],
10
+ [DeclineProfileEnum.IRONMAN, 34, 10.0, 1.5, 16.0]
11
+ ])('%s → startAge %i', (profile, startAge, initTicks, accel, peakTicks) => {
12
+ const entry = getDeclineProfile(profile);
13
+ expect(entry.startAge).toBe(startAge);
14
+ expect(entry.initTicks).toBeCloseTo(initTicks, 5);
15
+ expect(entry.accel).toBeCloseTo(accel, 5);
16
+ expect(entry.peakTicks).toBeCloseTo(peakTicks, 5);
17
+ });
18
+ it('throws UNKNOWN_DECLINE_PROFILE for an unrecognised profile', () => {
19
+ expect(() => getDeclineProfile('LEGEND')).toThrow('UNKNOWN_DECLINE_PROFILE');
20
+ });
21
+ it('declineProfiles lists all four enum values', () => {
22
+ expect([...declineProfiles].sort()).toEqual([...Object.values(DeclineProfileEnum)].sort());
23
+ });
24
+ });
25
+ // ─── stat pools ────────────────────────────────────────────────────────────────
26
+ describe('decline stat pools', () => {
27
+ it('SKILL_POOL and ENERGY_POOL are disjoint', () => {
28
+ const overlap = SKILL_POOL.filter(k => ENERGY_POOL.includes(k));
29
+ expect(overlap).toEqual([]);
30
+ });
31
+ it('together they cover every performance stat key exactly once', () => {
32
+ const union = [...SKILL_POOL, ...ENERGY_POOL].sort();
33
+ expect(union).toEqual([...performanceStatKeys].sort());
34
+ });
35
+ it('ENERGY_POOL holds the reaction/energy stats (incl. crit-chance stats)', () => {
36
+ expect([...ENERGY_POOL].sort()).toEqual(['awareness', 'commit', 'focus', 'read', 'reflex', 'stamina'].sort());
37
+ });
38
+ });
39
+ // ─── computeDeclineTick ────────────────────────────────────────────────────────
40
+ describe('computeDeclineTick()', () => {
41
+ it('returns no decline before the profile start age', () => {
42
+ for (const profile of declineProfiles) {
43
+ const { startAge } = getDeclineProfile(profile);
44
+ const tick = computeDeclineTick(startAge - 1, profile);
45
+ expect(tick.ticks).toBe(0);
46
+ expect(tick.atPeak).toBe(false);
47
+ expect(tick.status).toBe('ACTIVE');
48
+ }
49
+ });
50
+ it('at the start age, tick rate equals round(initTicks) and status is DECLINING', () => {
51
+ for (const profile of declineProfiles) {
52
+ const { startAge, initTicks } = getDeclineProfile(profile);
53
+ const tick = computeDeclineTick(startAge, profile);
54
+ expect(tick.ticks).toBe(Math.round(initTicks));
55
+ expect(tick.atPeak).toBe(false);
56
+ expect(tick.status).toBe('DECLINING');
57
+ }
58
+ });
59
+ it('tick count is non-decreasing as age increases', () => {
60
+ for (const profile of declineProfiles) {
61
+ const { startAge } = getDeclineProfile(profile);
62
+ let prev = -1;
63
+ for (let age = startAge; age <= startAge + 30; age++) {
64
+ const { ticks } = computeDeclineTick(age, profile);
65
+ expect(ticks).toBeGreaterThanOrEqual(prev);
66
+ prev = ticks;
67
+ }
68
+ }
69
+ });
70
+ it('never exceeds peakTicks, and reaching it flips atPeak / PEAK', () => {
71
+ for (const profile of declineProfiles) {
72
+ const { startAge, peakTicks } = getDeclineProfile(profile);
73
+ for (let age = startAge; age <= startAge + 40; age++) {
74
+ const tick = computeDeclineTick(age, profile);
75
+ expect(tick.ticks).toBeLessThanOrEqual(Math.round(peakTicks));
76
+ if (tick.atPeak) {
77
+ expect(tick.ticks).toBe(Math.round(peakTicks));
78
+ expect(tick.status).toBe('PEAK');
79
+ }
80
+ }
81
+ }
82
+ });
83
+ it('peak triggers at the analytically expected age for each profile', () => {
84
+ // age at peak = startAge + ceil((peakTicks - initTicks) / accel)
85
+ for (const profile of declineProfiles) {
86
+ const { startAge, initTicks, accel, peakTicks } = getDeclineProfile(profile);
87
+ const peakAge = startAge + Math.ceil((peakTicks - initTicks) / accel);
88
+ expect(computeDeclineTick(peakAge - 1, profile).atPeak).toBe(false);
89
+ expect(computeDeclineTick(peakAge, profile).atPeak).toBe(true);
90
+ }
91
+ });
92
+ it('Standard profile matches the validated model at key ages', () => {
93
+ // initTicks 9, accel 1.75, peakTicks 16, startAge 29
94
+ expect(computeDeclineTick(28, DeclineProfileEnum.STANDARD)).toEqual({ ticks: 0, atPeak: false, status: 'ACTIVE' });
95
+ expect(computeDeclineTick(29, DeclineProfileEnum.STANDARD).ticks).toBe(9);
96
+ expect(computeDeclineTick(30, DeclineProfileEnum.STANDARD).ticks).toBe(Math.round(9 + 1.75)); // 11
97
+ // 9 + 4*1.75 = 16 → peak at age 33
98
+ expect(computeDeclineTick(33, DeclineProfileEnum.STANDARD).atPeak).toBe(true);
99
+ });
100
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.360",
3
+ "version": "0.0.361",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",