volleyballsimtypes 0.0.425 → 0.0.427

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.
Files changed (31) hide show
  1. package/dist/cjs/src/api/index.d.ts +11 -0
  2. package/dist/cjs/src/data/models/tactics.d.ts +2 -0
  3. package/dist/cjs/src/data/transformers/tactics.js +4 -0
  4. package/dist/cjs/src/service/player/decline.d.ts +2 -0
  5. package/dist/cjs/src/service/player/decline.js +7 -0
  6. package/dist/cjs/src/service/player/decline.test.js +17 -0
  7. package/dist/cjs/src/service/player/improvement.d.ts +3 -0
  8. package/dist/cjs/src/service/player/improvement.js +23 -0
  9. package/dist/cjs/src/service/player/improvement.test.js +36 -0
  10. package/dist/cjs/src/service/team/designated-sub.d.ts +1 -0
  11. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +3 -0
  12. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +1 -0
  13. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +2 -0
  14. package/dist/cjs/src/service/team/schemas/team.z.d.ts +2 -0
  15. package/dist/cjs/src/stat-config/decline-profiles.json +4 -4
  16. package/dist/esm/src/api/index.d.ts +11 -0
  17. package/dist/esm/src/data/models/tactics.d.ts +2 -0
  18. package/dist/esm/src/data/transformers/tactics.js +4 -0
  19. package/dist/esm/src/service/player/decline.d.ts +2 -0
  20. package/dist/esm/src/service/player/decline.js +6 -0
  21. package/dist/esm/src/service/player/decline.test.js +18 -1
  22. package/dist/esm/src/service/player/improvement.d.ts +3 -0
  23. package/dist/esm/src/service/player/improvement.js +21 -0
  24. package/dist/esm/src/service/player/improvement.test.js +37 -1
  25. package/dist/esm/src/service/team/designated-sub.d.ts +1 -0
  26. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +3 -0
  27. package/dist/esm/src/service/team/schemas/designated-sub.z.js +1 -0
  28. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +2 -0
  29. package/dist/esm/src/service/team/schemas/team.z.d.ts +2 -0
  30. package/dist/esm/src/stat-config/decline-profiles.json +4 -4
  31. package/package.json +1 -1
@@ -24,6 +24,13 @@ export interface MatchCompetition {
24
24
  tier?: number;
25
25
  subdivision?: number;
26
26
  }
27
+ export interface MatchForm {
28
+ matchId: string;
29
+ result: 'WIN' | 'LOSE';
30
+ side: 'HOME' | 'AWAY';
31
+ opponent: string;
32
+ dR: number;
33
+ }
27
34
  export type Match = Omit<DataProps<_Match>, 'sets' | 'homeTeam' | 'awayTeam' | 'VPERs'> & {
28
35
  sets: MatchSet[];
29
36
  boxScores: BoxScore[];
@@ -37,6 +44,8 @@ export type Match = Omit<DataProps<_Match>, 'sets' | 'homeTeam' | 'awayTeam' | '
37
44
  results?: Results;
38
45
  competition?: MatchCompetition;
39
46
  rallyMetrics?: RallyMetrics;
47
+ homeForm?: MatchForm[];
48
+ awayForm?: MatchForm[];
40
49
  };
41
50
  export type RallyEvent = DataProps<_RallyEvent> & {
42
51
  team?: Team;
@@ -65,6 +74,7 @@ export interface ApiSetSubConfig {
65
74
  mode: SubMode;
66
75
  benchId?: string;
67
76
  fatigueBand?: EnergyBand;
77
+ benchFatigueBand?: EnergyBand;
68
78
  conditions?: PinchCondition[];
69
79
  conditionLogic?: ConditionLogic;
70
80
  }
@@ -73,6 +83,7 @@ export interface ApiDesignatedSub {
73
83
  benchId?: string;
74
84
  mode: SubMode;
75
85
  fatigueBand?: EnergyBand;
86
+ benchFatigueBand?: EnergyBand;
76
87
  conditions?: PinchCondition[];
77
88
  conditionLogic?: ConditionLogic;
78
89
  sets?: ApiSetSubConfig[];
@@ -17,6 +17,7 @@ export interface SetSubConfigAttributes {
17
17
  mode: SubMode;
18
18
  benchId?: PlayerId;
19
19
  fatigueBand?: EnergyBand;
20
+ benchFatigueBand?: EnergyBand;
20
21
  conditions?: PinchCondition[];
21
22
  conditionLogic?: ConditionLogic;
22
23
  }
@@ -26,6 +27,7 @@ export interface DesignatedSubAttributes {
26
27
  mode?: SubMode;
27
28
  isPinchServer?: boolean;
28
29
  fatigueBand?: SubBand;
30
+ benchFatigueBand?: EnergyBand;
29
31
  conditions?: PinchCondition[];
30
32
  conditionLogic?: ConditionLogic;
31
33
  sets?: SetSubConfigAttributes[];
@@ -62,12 +62,14 @@ function transformToAttributes(tactics, teamId) {
62
62
  benchId: ds.bench?.id,
63
63
  mode: ds.mode,
64
64
  fatigueBand: ds.fatigueBand,
65
+ benchFatigueBand: ds.benchFatigueBand,
65
66
  conditions: ds.conditions,
66
67
  conditionLogic: ds.conditionLogic,
67
68
  sets: ds.sets?.map(s => ({
68
69
  mode: s.mode,
69
70
  benchId: s.bench?.id,
70
71
  fatigueBand: s.fatigueBand,
72
+ benchFatigueBand: s.benchFatigueBand,
71
73
  conditions: s.conditions,
72
74
  conditionLogic: s.conditionLogic
73
75
  }))
@@ -115,12 +117,14 @@ function transformToObject(model, roster) {
115
117
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
116
118
  mode: subModeFromAttributes(d),
117
119
  fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
120
+ benchFatigueBand: d.benchFatigueBand,
118
121
  conditions: d.conditions,
119
122
  conditionLogic: d.conditionLogic,
120
123
  sets: d.sets?.map(s => ({
121
124
  mode: s.mode,
122
125
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
123
126
  fatigueBand: s.fatigueBand,
127
+ benchFatigueBand: s.benchFatigueBand,
124
128
  conditions: s.conditions,
125
129
  conditionLogic: s.conditionLogic
126
130
  }))
@@ -10,6 +10,7 @@ export declare const declineProfiles: DeclineProfileEnum[];
10
10
  interface DeclineProfileEntry {
11
11
  profile: string;
12
12
  startAge: number;
13
+ youthAge: number;
13
14
  initTicks: number;
14
15
  accel: number;
15
16
  peakTicks: number;
@@ -25,6 +26,7 @@ export declare const SKILL_POOL: PerformanceStatsKey[];
25
26
  */
26
27
  export declare const ENERGY_POOL: PerformanceStatsKey[];
27
28
  export declare function getDeclineProfile(profile: DeclineProfile): DeclineProfileEntry;
29
+ export declare function youthMultiplier(age: number, profile: DeclineProfile): number;
28
30
  export type DeclineStatus = 'ACTIVE' | 'DECLINING' | 'PEAK';
29
31
  export interface DeclineTick {
30
32
  /** Number of decline ticks to apply this iteration. */
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ENERGY_POOL = exports.SKILL_POOL = exports.declineProfiles = exports.DeclineProfileEnum = void 0;
4
4
  exports.getDeclineProfile = getDeclineProfile;
5
+ exports.youthMultiplier = youthMultiplier;
5
6
  exports.computeDeclineTick = computeDeclineTick;
6
7
  const stat_config_1 = require("../../stat-config");
7
8
  var DeclineProfileEnum;
@@ -34,6 +35,12 @@ function getDeclineProfile(profile) {
34
35
  throw new Error(`UNKNOWN_DECLINE_PROFILE: ${profile}`);
35
36
  return entry;
36
37
  }
38
+ // Prospects below their profile's youthAge develop faster: a multiplier applied to the per-match improvement
39
+ // experience. At or above youthAge (and pre-decline) growth is at the normal rate.
40
+ const YOUTH_EXP_MULTIPLIER = 1.25;
41
+ function youthMultiplier(age, profile) {
42
+ return age < getDeclineProfile(profile).youthAge ? YOUTH_EXP_MULTIPLIER : 1;
43
+ }
37
44
  /**
38
45
  * Decline is fully derivable from age + profile — no running state is persisted.
39
46
  * tickRate = min(peakTicks, initTicks + (age - startAge) * accel)
@@ -100,3 +100,20 @@ const performance_stats_keys_1 = require("./performance-stats-keys");
100
100
  (0, globals_1.expect)((0, decline_1.computeDeclineTick)(33, decline_1.DeclineProfileEnum.STANDARD).atPeak).toBe(true);
101
101
  });
102
102
  });
103
+ // ─── youthMultiplier ───────────────────────────────────────────────────────────
104
+ (0, globals_1.describe)('youthMultiplier()', () => {
105
+ globals_1.it.each([
106
+ [decline_1.DeclineProfileEnum.PRODIGY, 20],
107
+ [decline_1.DeclineProfileEnum.STANDARD, 21],
108
+ [decline_1.DeclineProfileEnum.VETERAN, 22],
109
+ [decline_1.DeclineProfileEnum.IRONMAN, 23]
110
+ ])('%s youthAge is %i', (profile, youthAge) => {
111
+ (0, globals_1.expect)((0, decline_1.getDeclineProfile)(profile).youthAge).toBe(youthAge);
112
+ });
113
+ (0, globals_1.it)('boosts players below youthAge by 1.25 and leaves the rest at 1', () => {
114
+ (0, globals_1.expect)((0, decline_1.youthMultiplier)(20, decline_1.DeclineProfileEnum.STANDARD)).toBe(1.25); // youthAge 21
115
+ (0, globals_1.expect)((0, decline_1.youthMultiplier)(21, decline_1.DeclineProfileEnum.STANDARD)).toBe(1);
116
+ (0, globals_1.expect)((0, decline_1.youthMultiplier)(19, decline_1.DeclineProfileEnum.PRODIGY)).toBe(1.25); // youthAge 20
117
+ (0, globals_1.expect)((0, decline_1.youthMultiplier)(20, decline_1.DeclineProfileEnum.PRODIGY)).toBe(1);
118
+ });
119
+ });
@@ -1,8 +1,11 @@
1
1
  import { Rarity } from './rarity';
2
2
  import { Stats } from './stats';
3
+ import { DeclineProfile } from './decline';
3
4
  export type TrainingFocus = Stats;
4
5
  export declare function getStatCap(rarity: Rarity): number;
5
6
  export declare function getRarityMultiplier(rarity: Rarity): number;
6
7
  export declare function getImprovementThreshold(statTotal: number, rarity: Rarity): number;
7
8
  export declare function getCoachMultiplier(rarity: Rarity): number;
9
+ export declare function matchExperience(pointsPlayed: number): number;
10
+ export declare function experienceGained(pointsPlayed: number, age: number, profile: DeclineProfile): number;
8
11
  export declare function selectImprovableStat(rarity: Rarity, trainingFocus: TrainingFocus, currentStats: Record<string, number>): string | null;
@@ -4,11 +4,14 @@ exports.getStatCap = getStatCap;
4
4
  exports.getRarityMultiplier = getRarityMultiplier;
5
5
  exports.getImprovementThreshold = getImprovementThreshold;
6
6
  exports.getCoachMultiplier = getCoachMultiplier;
7
+ exports.matchExperience = matchExperience;
8
+ exports.experienceGained = experienceGained;
7
9
  exports.selectImprovableStat = selectImprovableStat;
8
10
  const stat_config_1 = require("../../stat-config");
9
11
  const rarity_1 = require("./rarity");
10
12
  const performance_stats_1 = require("./performance-stats");
11
13
  const stats_1 = require("./stats");
14
+ const decline_1 = require("./decline");
12
15
  const CURVE_BASE = 100;
13
16
  const CURVE_SCALAR = 0.098;
14
17
  const CURVE_EXPONENT = 1.5;
@@ -40,6 +43,26 @@ const COACH_MULTIPLIERS = {
40
43
  function getCoachMultiplier(rarity) {
41
44
  return COACH_MULTIPLIERS[rarity] ?? 1.0;
42
45
  }
46
+ // ── Per-match experience curve ──────────────────────────────────────────────
47
+ // Experience is no longer 1:1 with points played: each point is worth progressively less (a saturating
48
+ // curve), so high-volume matches (5-setters, never-subbed players) don't improve disproportionately fast.
49
+ // MATCH_EXP_K sets the long-match premium - a ~206-point 5-setter yields only ~13% more exp than a ~147-point
50
+ // 3-setter. MATCH_EXP_SCALE is the asymptote, calibrated so exp == points at ~60 points played (the league-
51
+ // average match): a normal match keeps roughly its old value and only longer matches are throttled. Tuning
52
+ // MATCH_EXP_SCALE shifts overall pace without changing the premium; MATCH_EXP_K changes the premium.
53
+ const MATCH_EXP_SCALE = 160;
54
+ const MATCH_EXP_K = 100;
55
+ function matchExperience(pointsPlayed) {
56
+ if (pointsPlayed <= 0)
57
+ return 0;
58
+ return (MATCH_EXP_SCALE * pointsPlayed) / (pointsPlayed + MATCH_EXP_K);
59
+ }
60
+ // Total improvement experience a player earns from a match: the saturating per-match curve, boosted for
61
+ // players still below their decline profile's youthAge (prospects develop faster). Rounded - the progress
62
+ // counter holds whole units.
63
+ function experienceGained(pointsPlayed, age, profile) {
64
+ return Math.round(matchExperience(pointsPlayed) * (0, decline_1.youthMultiplier)(age, profile));
65
+ }
43
66
  const KEY_TO_COLUMN = {
44
67
  setting: 'setting',
45
68
  serve: 'serve',
@@ -4,6 +4,7 @@ const globals_1 = require("@jest/globals");
4
4
  const rarity_1 = require("./rarity");
5
5
  const improvement_1 = require("./improvement");
6
6
  const stats_1 = require("./stats");
7
+ const decline_1 = require("./decline");
7
8
  // ─── getStatCap ───────────────────────────────────────────────────────────────
8
9
  (0, globals_1.describe)('getStatCap()', () => {
9
10
  globals_1.it.each([
@@ -142,3 +143,38 @@ function allStats(value) {
142
143
  }
143
144
  });
144
145
  });
146
+ // ─── matchExperience / experienceGained ───────────────────────────────────────
147
+ (0, globals_1.describe)('matchExperience()', () => {
148
+ (0, globals_1.it)('breaks even (~1:1) at the league-average match (~60 points)', () => {
149
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(60)).toBeCloseTo(60, 0);
150
+ });
151
+ (0, globals_1.it)('throttles high-volume matches well below their raw points', () => {
152
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(206)).toBeLessThan(206);
153
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(206)).toBeCloseTo(108, 0);
154
+ });
155
+ (0, globals_1.it)('gives a 5-set match (~206 pts) only ~13% more than a 3-set match (~147 pts)', () => {
156
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(206) / (0, improvement_1.matchExperience)(147)).toBeCloseTo(1.13, 2);
157
+ });
158
+ (0, globals_1.it)('is monotonic non-decreasing in points played', () => {
159
+ let prev = -1;
160
+ for (let p = 0; p <= 250; p += 10) {
161
+ const v = (0, improvement_1.matchExperience)(p);
162
+ (0, globals_1.expect)(v).toBeGreaterThanOrEqual(prev);
163
+ prev = v;
164
+ }
165
+ });
166
+ (0, globals_1.it)('zero or negative points → zero exp', () => {
167
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(0)).toBe(0);
168
+ (0, globals_1.expect)((0, improvement_1.matchExperience)(-5)).toBe(0);
169
+ });
170
+ });
171
+ (0, globals_1.describe)('experienceGained()', () => {
172
+ (0, globals_1.it)('young players (below their profile youthAge) gain 25% more than the base', () => {
173
+ const base = Math.round((0, improvement_1.matchExperience)(100));
174
+ (0, globals_1.expect)((0, improvement_1.experienceGained)(100, 18, decline_1.DeclineProfileEnum.STANDARD)).toBe(Math.round((0, improvement_1.matchExperience)(100) * 1.25));
175
+ (0, globals_1.expect)((0, improvement_1.experienceGained)(100, 25, decline_1.DeclineProfileEnum.STANDARD)).toBe(base);
176
+ });
177
+ (0, globals_1.it)('zero points → zero exp regardless of age', () => {
178
+ (0, globals_1.expect)((0, improvement_1.experienceGained)(0, 18, decline_1.DeclineProfileEnum.STANDARD)).toBe(0);
179
+ });
180
+ });
@@ -5,6 +5,7 @@ export interface SetSubConfig {
5
5
  readonly mode: SubMode;
6
6
  readonly bench?: Player;
7
7
  readonly fatigueBand?: EnergyBand;
8
+ readonly benchFatigueBand?: EnergyBand;
8
9
  readonly conditions?: PinchCondition[];
9
10
  readonly conditionLogic?: ConditionLogic;
10
11
  }
@@ -43,6 +43,7 @@ export declare const SetSubConfigSchema: z.ZodObject<{
43
43
  }>;
44
44
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
45
45
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
47
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
47
48
  type: z.ZodLiteral<PinchConditionType.ASAP>;
48
49
  }, z.core.$strip>, z.ZodObject<{
@@ -80,6 +81,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
80
81
  }>;
81
82
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
82
83
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
84
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
83
85
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
84
86
  type: z.ZodLiteral<PinchConditionType.ASAP>;
85
87
  }, z.core.$strip>, z.ZodObject<{
@@ -115,6 +117,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
115
117
  }>;
116
118
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
117
119
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
120
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
118
121
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
119
122
  type: z.ZodLiteral<PinchConditionType.ASAP>;
120
123
  }, z.core.$strip>, z.ZodObject<{
@@ -26,6 +26,7 @@ const setSubConfigShape = {
26
26
  mode: exports.SubModeSchema,
27
27
  bench: playerInstanceSchema.optional(),
28
28
  fatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
29
+ benchFatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
29
30
  conditions: zod_1.z.array(exports.PinchConditionSchema).optional(),
30
31
  conditionLogic: exports.ConditionLogicSchema.optional()
31
32
  };
@@ -26,6 +26,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
26
26
  }>;
27
27
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
28
28
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
30
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
30
31
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
31
32
  }, z.core.$strip>, z.ZodObject<{
@@ -61,6 +62,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
61
62
  }>;
62
63
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
63
64
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
65
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
64
66
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
65
67
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
66
68
  }, z.core.$strip>, z.ZodObject<{
@@ -34,6 +34,7 @@ export declare const TeamInputSchema: z.ZodObject<{
34
34
  }>;
35
35
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
36
36
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
38
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
38
39
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
39
40
  }, z.core.$strip>, z.ZodObject<{
@@ -69,6 +70,7 @@ export declare const TeamInputSchema: z.ZodObject<{
69
70
  }>;
70
71
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
71
72
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
73
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
72
74
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
73
75
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
74
76
  }, z.core.$strip>, z.ZodObject<{
@@ -1,6 +1,6 @@
1
1
  [
2
- { "profile": "PRODIGY", "startAge": 27, "initTicks": 13.0, "accel": 1.00, "peakTicks": 16.0 },
3
- { "profile": "STANDARD", "startAge": 29, "initTicks": 9.0, "accel": 1.75, "peakTicks": 16.0 },
4
- { "profile": "VETERAN", "startAge": 32, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 },
5
- { "profile": "IRONMAN", "startAge": 34, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 }
2
+ { "profile": "PRODIGY", "startAge": 27, "youthAge": 20, "initTicks": 13.0, "accel": 1.00, "peakTicks": 16.0 },
3
+ { "profile": "STANDARD", "startAge": 29, "youthAge": 21, "initTicks": 9.0, "accel": 1.75, "peakTicks": 16.0 },
4
+ { "profile": "VETERAN", "startAge": 32, "youthAge": 22, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 },
5
+ { "profile": "IRONMAN", "startAge": 34, "youthAge": 23, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 }
6
6
  ]
@@ -24,6 +24,13 @@ export interface MatchCompetition {
24
24
  tier?: number;
25
25
  subdivision?: number;
26
26
  }
27
+ export interface MatchForm {
28
+ matchId: string;
29
+ result: 'WIN' | 'LOSE';
30
+ side: 'HOME' | 'AWAY';
31
+ opponent: string;
32
+ dR: number;
33
+ }
27
34
  export type Match = Omit<DataProps<_Match>, 'sets' | 'homeTeam' | 'awayTeam' | 'VPERs'> & {
28
35
  sets: MatchSet[];
29
36
  boxScores: BoxScore[];
@@ -37,6 +44,8 @@ export type Match = Omit<DataProps<_Match>, 'sets' | 'homeTeam' | 'awayTeam' | '
37
44
  results?: Results;
38
45
  competition?: MatchCompetition;
39
46
  rallyMetrics?: RallyMetrics;
47
+ homeForm?: MatchForm[];
48
+ awayForm?: MatchForm[];
40
49
  };
41
50
  export type RallyEvent = DataProps<_RallyEvent> & {
42
51
  team?: Team;
@@ -65,6 +74,7 @@ export interface ApiSetSubConfig {
65
74
  mode: SubMode;
66
75
  benchId?: string;
67
76
  fatigueBand?: EnergyBand;
77
+ benchFatigueBand?: EnergyBand;
68
78
  conditions?: PinchCondition[];
69
79
  conditionLogic?: ConditionLogic;
70
80
  }
@@ -73,6 +83,7 @@ export interface ApiDesignatedSub {
73
83
  benchId?: string;
74
84
  mode: SubMode;
75
85
  fatigueBand?: EnergyBand;
86
+ benchFatigueBand?: EnergyBand;
76
87
  conditions?: PinchCondition[];
77
88
  conditionLogic?: ConditionLogic;
78
89
  sets?: ApiSetSubConfig[];
@@ -17,6 +17,7 @@ export interface SetSubConfigAttributes {
17
17
  mode: SubMode;
18
18
  benchId?: PlayerId;
19
19
  fatigueBand?: EnergyBand;
20
+ benchFatigueBand?: EnergyBand;
20
21
  conditions?: PinchCondition[];
21
22
  conditionLogic?: ConditionLogic;
22
23
  }
@@ -26,6 +27,7 @@ export interface DesignatedSubAttributes {
26
27
  mode?: SubMode;
27
28
  isPinchServer?: boolean;
28
29
  fatigueBand?: SubBand;
30
+ benchFatigueBand?: EnergyBand;
29
31
  conditions?: PinchCondition[];
30
32
  conditionLogic?: ConditionLogic;
31
33
  sets?: SetSubConfigAttributes[];
@@ -58,12 +58,14 @@ function transformToAttributes(tactics, teamId) {
58
58
  benchId: ds.bench?.id,
59
59
  mode: ds.mode,
60
60
  fatigueBand: ds.fatigueBand,
61
+ benchFatigueBand: ds.benchFatigueBand,
61
62
  conditions: ds.conditions,
62
63
  conditionLogic: ds.conditionLogic,
63
64
  sets: ds.sets?.map(s => ({
64
65
  mode: s.mode,
65
66
  benchId: s.bench?.id,
66
67
  fatigueBand: s.fatigueBand,
68
+ benchFatigueBand: s.benchFatigueBand,
67
69
  conditions: s.conditions,
68
70
  conditionLogic: s.conditionLogic
69
71
  }))
@@ -111,12 +113,14 @@ function transformToObject(model, roster) {
111
113
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
112
114
  mode: subModeFromAttributes(d),
113
115
  fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
116
+ benchFatigueBand: d.benchFatigueBand,
114
117
  conditions: d.conditions,
115
118
  conditionLogic: d.conditionLogic,
116
119
  sets: d.sets?.map(s => ({
117
120
  mode: s.mode,
118
121
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
119
122
  fatigueBand: s.fatigueBand,
123
+ benchFatigueBand: s.benchFatigueBand,
120
124
  conditions: s.conditions,
121
125
  conditionLogic: s.conditionLogic
122
126
  }))
@@ -10,6 +10,7 @@ export declare const declineProfiles: DeclineProfileEnum[];
10
10
  interface DeclineProfileEntry {
11
11
  profile: string;
12
12
  startAge: number;
13
+ youthAge: number;
13
14
  initTicks: number;
14
15
  accel: number;
15
16
  peakTicks: number;
@@ -25,6 +26,7 @@ export declare const SKILL_POOL: PerformanceStatsKey[];
25
26
  */
26
27
  export declare const ENERGY_POOL: PerformanceStatsKey[];
27
28
  export declare function getDeclineProfile(profile: DeclineProfile): DeclineProfileEntry;
29
+ export declare function youthMultiplier(age: number, profile: DeclineProfile): number;
28
30
  export type DeclineStatus = 'ACTIVE' | 'DECLINING' | 'PEAK';
29
31
  export interface DeclineTick {
30
32
  /** Number of decline ticks to apply this iteration. */
@@ -29,6 +29,12 @@ export function getDeclineProfile(profile) {
29
29
  throw new Error(`UNKNOWN_DECLINE_PROFILE: ${profile}`);
30
30
  return entry;
31
31
  }
32
+ // Prospects below their profile's youthAge develop faster: a multiplier applied to the per-match improvement
33
+ // experience. At or above youthAge (and pre-decline) growth is at the normal rate.
34
+ const YOUTH_EXP_MULTIPLIER = 1.25;
35
+ export function youthMultiplier(age, profile) {
36
+ return age < getDeclineProfile(profile).youthAge ? YOUTH_EXP_MULTIPLIER : 1;
37
+ }
32
38
  /**
33
39
  * Decline is fully derivable from age + profile — no running state is persisted.
34
40
  * tickRate = min(peakTicks, initTicks + (age - startAge) * accel)
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from '@jest/globals';
2
- import { DeclineProfileEnum, declineProfiles, getDeclineProfile, computeDeclineTick, SKILL_POOL, ENERGY_POOL } from './decline';
2
+ import { DeclineProfileEnum, declineProfiles, getDeclineProfile, computeDeclineTick, youthMultiplier, SKILL_POOL, ENERGY_POOL } from './decline';
3
3
  import { performanceStatKeys } from './performance-stats-keys';
4
4
  // ─── getDeclineProfile ─────────────────────────────────────────────────────────
5
5
  describe('getDeclineProfile()', () => {
@@ -98,3 +98,20 @@ describe('computeDeclineTick()', () => {
98
98
  expect(computeDeclineTick(33, DeclineProfileEnum.STANDARD).atPeak).toBe(true);
99
99
  });
100
100
  });
101
+ // ─── youthMultiplier ───────────────────────────────────────────────────────────
102
+ describe('youthMultiplier()', () => {
103
+ it.each([
104
+ [DeclineProfileEnum.PRODIGY, 20],
105
+ [DeclineProfileEnum.STANDARD, 21],
106
+ [DeclineProfileEnum.VETERAN, 22],
107
+ [DeclineProfileEnum.IRONMAN, 23]
108
+ ])('%s youthAge is %i', (profile, youthAge) => {
109
+ expect(getDeclineProfile(profile).youthAge).toBe(youthAge);
110
+ });
111
+ it('boosts players below youthAge by 1.25 and leaves the rest at 1', () => {
112
+ expect(youthMultiplier(20, DeclineProfileEnum.STANDARD)).toBe(1.25); // youthAge 21
113
+ expect(youthMultiplier(21, DeclineProfileEnum.STANDARD)).toBe(1);
114
+ expect(youthMultiplier(19, DeclineProfileEnum.PRODIGY)).toBe(1.25); // youthAge 20
115
+ expect(youthMultiplier(20, DeclineProfileEnum.PRODIGY)).toBe(1);
116
+ });
117
+ });
@@ -1,8 +1,11 @@
1
1
  import { Rarity } from './rarity';
2
2
  import { Stats } from './stats';
3
+ import { DeclineProfile } from './decline';
3
4
  export type TrainingFocus = Stats;
4
5
  export declare function getStatCap(rarity: Rarity): number;
5
6
  export declare function getRarityMultiplier(rarity: Rarity): number;
6
7
  export declare function getImprovementThreshold(statTotal: number, rarity: Rarity): number;
7
8
  export declare function getCoachMultiplier(rarity: Rarity): number;
9
+ export declare function matchExperience(pointsPlayed: number): number;
10
+ export declare function experienceGained(pointsPlayed: number, age: number, profile: DeclineProfile): number;
8
11
  export declare function selectImprovableStat(rarity: Rarity, trainingFocus: TrainingFocus, currentStats: Record<string, number>): string | null;
@@ -2,6 +2,7 @@ import { statCapsJSON } from '../../stat-config';
2
2
  import { RarityEnum } from './rarity';
3
3
  import { performanceStatKeys } from './performance-stats';
4
4
  import { getMultipliers } from './stats';
5
+ import { youthMultiplier } from './decline';
5
6
  const CURVE_BASE = 100;
6
7
  const CURVE_SCALAR = 0.098;
7
8
  const CURVE_EXPONENT = 1.5;
@@ -33,6 +34,26 @@ const COACH_MULTIPLIERS = {
33
34
  export function getCoachMultiplier(rarity) {
34
35
  return COACH_MULTIPLIERS[rarity] ?? 1.0;
35
36
  }
37
+ // ── Per-match experience curve ──────────────────────────────────────────────
38
+ // Experience is no longer 1:1 with points played: each point is worth progressively less (a saturating
39
+ // curve), so high-volume matches (5-setters, never-subbed players) don't improve disproportionately fast.
40
+ // MATCH_EXP_K sets the long-match premium - a ~206-point 5-setter yields only ~13% more exp than a ~147-point
41
+ // 3-setter. MATCH_EXP_SCALE is the asymptote, calibrated so exp == points at ~60 points played (the league-
42
+ // average match): a normal match keeps roughly its old value and only longer matches are throttled. Tuning
43
+ // MATCH_EXP_SCALE shifts overall pace without changing the premium; MATCH_EXP_K changes the premium.
44
+ const MATCH_EXP_SCALE = 160;
45
+ const MATCH_EXP_K = 100;
46
+ export function matchExperience(pointsPlayed) {
47
+ if (pointsPlayed <= 0)
48
+ return 0;
49
+ return (MATCH_EXP_SCALE * pointsPlayed) / (pointsPlayed + MATCH_EXP_K);
50
+ }
51
+ // Total improvement experience a player earns from a match: the saturating per-match curve, boosted for
52
+ // players still below their decline profile's youthAge (prospects develop faster). Rounded - the progress
53
+ // counter holds whole units.
54
+ export function experienceGained(pointsPlayed, age, profile) {
55
+ return Math.round(matchExperience(pointsPlayed) * youthMultiplier(age, profile));
56
+ }
36
57
  const KEY_TO_COLUMN = {
37
58
  setting: 'setting',
38
59
  serve: 'serve',
@@ -1,7 +1,8 @@
1
1
  import { describe, it, expect } from '@jest/globals';
2
2
  import { RarityEnum } from './rarity';
3
- import { getStatCap, getRarityMultiplier, getImprovementThreshold, getCoachMultiplier, selectImprovableStat } from './improvement';
3
+ import { getStatCap, getRarityMultiplier, getImprovementThreshold, getCoachMultiplier, selectImprovableStat, matchExperience, experienceGained } from './improvement';
4
4
  import { StatsEnum } from './stats';
5
+ import { DeclineProfileEnum } from './decline';
5
6
  // ─── getStatCap ───────────────────────────────────────────────────────────────
6
7
  describe('getStatCap()', () => {
7
8
  it.each([
@@ -140,3 +141,38 @@ describe('selectImprovableStat()', () => {
140
141
  }
141
142
  });
142
143
  });
144
+ // ─── matchExperience / experienceGained ───────────────────────────────────────
145
+ describe('matchExperience()', () => {
146
+ it('breaks even (~1:1) at the league-average match (~60 points)', () => {
147
+ expect(matchExperience(60)).toBeCloseTo(60, 0);
148
+ });
149
+ it('throttles high-volume matches well below their raw points', () => {
150
+ expect(matchExperience(206)).toBeLessThan(206);
151
+ expect(matchExperience(206)).toBeCloseTo(108, 0);
152
+ });
153
+ it('gives a 5-set match (~206 pts) only ~13% more than a 3-set match (~147 pts)', () => {
154
+ expect(matchExperience(206) / matchExperience(147)).toBeCloseTo(1.13, 2);
155
+ });
156
+ it('is monotonic non-decreasing in points played', () => {
157
+ let prev = -1;
158
+ for (let p = 0; p <= 250; p += 10) {
159
+ const v = matchExperience(p);
160
+ expect(v).toBeGreaterThanOrEqual(prev);
161
+ prev = v;
162
+ }
163
+ });
164
+ it('zero or negative points → zero exp', () => {
165
+ expect(matchExperience(0)).toBe(0);
166
+ expect(matchExperience(-5)).toBe(0);
167
+ });
168
+ });
169
+ describe('experienceGained()', () => {
170
+ it('young players (below their profile youthAge) gain 25% more than the base', () => {
171
+ const base = Math.round(matchExperience(100));
172
+ expect(experienceGained(100, 18, DeclineProfileEnum.STANDARD)).toBe(Math.round(matchExperience(100) * 1.25));
173
+ expect(experienceGained(100, 25, DeclineProfileEnum.STANDARD)).toBe(base);
174
+ });
175
+ it('zero points → zero exp regardless of age', () => {
176
+ expect(experienceGained(0, 18, DeclineProfileEnum.STANDARD)).toBe(0);
177
+ });
178
+ });
@@ -5,6 +5,7 @@ export interface SetSubConfig {
5
5
  readonly mode: SubMode;
6
6
  readonly bench?: Player;
7
7
  readonly fatigueBand?: EnergyBand;
8
+ readonly benchFatigueBand?: EnergyBand;
8
9
  readonly conditions?: PinchCondition[];
9
10
  readonly conditionLogic?: ConditionLogic;
10
11
  }
@@ -43,6 +43,7 @@ export declare const SetSubConfigSchema: z.ZodObject<{
43
43
  }>;
44
44
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
45
45
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
47
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
47
48
  type: z.ZodLiteral<PinchConditionType.ASAP>;
48
49
  }, z.core.$strip>, z.ZodObject<{
@@ -80,6 +81,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
80
81
  }>;
81
82
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
82
83
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
84
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
83
85
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
84
86
  type: z.ZodLiteral<PinchConditionType.ASAP>;
85
87
  }, z.core.$strip>, z.ZodObject<{
@@ -115,6 +117,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
115
117
  }>;
116
118
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
117
119
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
120
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
118
121
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
119
122
  type: z.ZodLiteral<PinchConditionType.ASAP>;
120
123
  }, z.core.$strip>, z.ZodObject<{
@@ -23,6 +23,7 @@ const setSubConfigShape = {
23
23
  mode: SubModeSchema,
24
24
  bench: playerInstanceSchema.optional(),
25
25
  fatigueBand: z.nativeEnum(EnergyBand).optional(),
26
+ benchFatigueBand: z.nativeEnum(EnergyBand).optional(),
26
27
  conditions: z.array(PinchConditionSchema).optional(),
27
28
  conditionLogic: ConditionLogicSchema.optional()
28
29
  };
@@ -26,6 +26,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
26
26
  }>;
27
27
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
28
28
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
30
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
30
31
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
31
32
  }, z.core.$strip>, z.ZodObject<{
@@ -61,6 +62,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
61
62
  }>;
62
63
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
63
64
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
65
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
64
66
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
65
67
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
66
68
  }, z.core.$strip>, z.ZodObject<{
@@ -34,6 +34,7 @@ export declare const TeamInputSchema: z.ZodObject<{
34
34
  }>;
35
35
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
36
36
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
38
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
38
39
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
39
40
  }, z.core.$strip>, z.ZodObject<{
@@ -69,6 +70,7 @@ export declare const TeamInputSchema: z.ZodObject<{
69
70
  }>;
70
71
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
71
72
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
73
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
72
74
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
73
75
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
74
76
  }, z.core.$strip>, z.ZodObject<{
@@ -1,6 +1,6 @@
1
1
  [
2
- { "profile": "PRODIGY", "startAge": 27, "initTicks": 13.0, "accel": 1.00, "peakTicks": 16.0 },
3
- { "profile": "STANDARD", "startAge": 29, "initTicks": 9.0, "accel": 1.75, "peakTicks": 16.0 },
4
- { "profile": "VETERAN", "startAge": 32, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 },
5
- { "profile": "IRONMAN", "startAge": 34, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 }
2
+ { "profile": "PRODIGY", "startAge": 27, "youthAge": 20, "initTicks": 13.0, "accel": 1.00, "peakTicks": 16.0 },
3
+ { "profile": "STANDARD", "startAge": 29, "youthAge": 21, "initTicks": 9.0, "accel": 1.75, "peakTicks": 16.0 },
4
+ { "profile": "VETERAN", "startAge": 32, "youthAge": 22, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 },
5
+ { "profile": "IRONMAN", "startAge": 34, "youthAge": 23, "initTicks": 10.0, "accel": 1.50, "peakTicks": 16.0 }
6
6
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.425",
3
+ "version": "0.0.427",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",