volleyballsimtypes 0.0.445 → 0.0.448

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 (29) hide show
  1. package/dist/cjs/src/data/transformers/rally-event.js +5 -10
  2. package/dist/cjs/src/data/transformers/rally.js +5 -5
  3. package/dist/cjs/src/service/event/schemas/block.z.d.ts +2 -2
  4. package/dist/cjs/src/service/event/schemas/block.z.js +1 -1
  5. package/dist/cjs/src/service/event/schemas/reception.z.d.ts +2 -2
  6. package/dist/cjs/src/service/event/schemas/reception.z.js +1 -1
  7. package/dist/cjs/src/service/event/schemas/serve.z.d.ts +2 -2
  8. package/dist/cjs/src/service/event/schemas/serve.z.js +1 -1
  9. package/dist/cjs/src/service/event/schemas/set.z.d.ts +2 -2
  10. package/dist/cjs/src/service/event/schemas/set.z.js +1 -1
  11. package/dist/cjs/src/service/event/schemas/spike.z.d.ts +2 -2
  12. package/dist/cjs/src/service/event/schemas/spike.z.js +1 -1
  13. package/dist/cjs/src/service/player/stats.js +2 -2
  14. package/dist/cjs/src/service/player/stats.test.js +63 -0
  15. package/dist/esm/src/data/transformers/rally-event.js +5 -10
  16. package/dist/esm/src/data/transformers/rally.js +5 -5
  17. package/dist/esm/src/service/event/schemas/block.z.d.ts +2 -2
  18. package/dist/esm/src/service/event/schemas/block.z.js +1 -1
  19. package/dist/esm/src/service/event/schemas/reception.z.d.ts +2 -2
  20. package/dist/esm/src/service/event/schemas/reception.z.js +1 -1
  21. package/dist/esm/src/service/event/schemas/serve.z.d.ts +2 -2
  22. package/dist/esm/src/service/event/schemas/serve.z.js +1 -1
  23. package/dist/esm/src/service/event/schemas/set.z.d.ts +2 -2
  24. package/dist/esm/src/service/event/schemas/set.z.js +1 -1
  25. package/dist/esm/src/service/event/schemas/spike.z.d.ts +2 -2
  26. package/dist/esm/src/service/event/schemas/spike.z.js +1 -1
  27. package/dist/esm/src/service/player/stats.js +2 -2
  28. package/dist/esm/src/service/player/stats.test.js +63 -0
  29. package/package.json +1 -1
@@ -39,8 +39,7 @@ function transformToBlock(event, roster) {
39
39
  playerId: roster[event.p],
40
40
  target: event.a,
41
41
  blockers: (event.b ?? []).map(i => roster[i]),
42
- score: event.s,
43
- activeTraits: []
42
+ score: event.s
44
43
  });
45
44
  }
46
45
  function transformToLiberoReplacement(event, roster) {
@@ -56,8 +55,7 @@ function transformToReception(event, roster) {
56
55
  type: event.t,
57
56
  playerId: roster[event.p],
58
57
  target: event.a,
59
- score: event.s,
60
- activeTraits: []
58
+ score: event.s
61
59
  });
62
60
  }
63
61
  function transformToServe(event, roster) {
@@ -66,8 +64,7 @@ function transformToServe(event, roster) {
66
64
  type: event.t,
67
65
  playerId: roster[event.p],
68
66
  target: event.a,
69
- score: event.s,
70
- activeTraits: []
67
+ score: event.s
71
68
  });
72
69
  }
73
70
  function transformToSet(event, roster) {
@@ -76,8 +73,7 @@ function transformToSet(event, roster) {
76
73
  type: event.t,
77
74
  playerId: roster[event.p],
78
75
  target: event.a,
79
- score: event.s,
80
- activeTraits: []
76
+ score: event.s
81
77
  });
82
78
  }
83
79
  function transformToSpike(event, roster) {
@@ -86,8 +82,7 @@ function transformToSpike(event, roster) {
86
82
  type: event.t,
87
83
  playerId: roster[event.p],
88
84
  target: event.a,
89
- score: event.s,
90
- activeTraits: []
85
+ score: event.s
91
86
  });
92
87
  }
93
88
  function transformToSubstitution(event, roster) {
@@ -68,11 +68,11 @@ function decodeLegacyEvent(event) {
68
68
  switch (event.e) {
69
69
  case 0: return service_1.LiberoReplacement.create({ playerId: event.p, libero: event.l, type: event.t });
70
70
  case 1: return service_1.Substitution.create({ playerId: event.p, playerOut: event.o });
71
- case 2: return service_1.Serve.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
72
- case 3: return service_1.Reception.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
73
- case 4: return service_1.Set.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
74
- case 5: return service_1.Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
75
- case 6: return service_1.Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s, activeTraits: [] });
71
+ case 2: return service_1.Serve.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
72
+ case 3: return service_1.Reception.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
73
+ case 4: return service_1.Set.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
74
+ case 5: return service_1.Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
75
+ case 6: return service_1.Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s });
76
76
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
77
77
  }
78
78
  }
@@ -4,7 +4,7 @@ export declare const BlockInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const BlockInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  blockers: z.ZodArray<z.ZodUUID>;
@@ -10,7 +10,7 @@ exports.BlockInputSchema = zod_1.z.object({
10
10
  playerId: zod_1.z.uuid(),
11
11
  score: zod_1.z.number().finite(),
12
12
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
- activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))),
13
+ activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
14
  failure: zod_1.z.number().int().refine((v) => blockFailureValues.includes(v), { message: 'INVALID_BLOCK_FAILURE' }),
15
15
  type: zod_1.z.number().int().refine((v) => blockTypeValues.includes(v), { message: 'INVALID_BLOCK_TYPE' }),
16
16
  blockers: zod_1.z.array(zod_1.z.uuid())
@@ -4,7 +4,7 @@ export declare const ReceptionInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const ReceptionInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -10,7 +10,7 @@ exports.ReceptionInputSchema = zod_1.z.object({
10
10
  playerId: zod_1.z.uuid(),
11
11
  score: zod_1.z.number().finite(),
12
12
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
- activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))),
13
+ activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
14
  failure: zod_1.z.number().int().refine((v) => receptionFailureValues.includes(v), { message: 'INVALID_RECEPTION_FAILURE' }),
15
15
  type: zod_1.z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' })
16
16
  });
@@ -4,7 +4,7 @@ export declare const ServeInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const ServeInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -10,7 +10,7 @@ exports.ServeInputSchema = zod_1.z.object({
10
10
  playerId: zod_1.z.uuid(),
11
11
  score: zod_1.z.number().finite(),
12
12
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
- activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))),
13
+ activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
14
  failure: zod_1.z.number().int().refine((v) => serveFailureValues.includes(v), { message: 'INVALID_SERVE_FAILURE' }),
15
15
  type: zod_1.z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' })
16
16
  });
@@ -4,7 +4,7 @@ export declare const SetInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const SetInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -10,7 +10,7 @@ exports.SetInputSchema = zod_1.z.object({
10
10
  playerId: zod_1.z.uuid(),
11
11
  score: zod_1.z.number().finite(),
12
12
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
- activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))),
13
+ activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
14
  failure: zod_1.z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
15
15
  type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' })
16
16
  });
@@ -4,7 +4,7 @@ export declare const SpikeInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const SpikeInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -10,7 +10,7 @@ exports.SpikeInputSchema = zod_1.z.object({
10
10
  playerId: zod_1.z.uuid(),
11
11
  score: zod_1.z.number().finite(),
12
12
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
- activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))),
13
+ activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
14
  failure: zod_1.z.number().int().refine((v) => spikeFailureValues.includes(v), { message: 'INVALID_SPIKE_FAILURE' }),
15
15
  type: zod_1.z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' })
16
16
  });
@@ -24,7 +24,7 @@ function getMultipliers(statName, performanceStats) {
24
24
  return {
25
25
  spike: 0.55,
26
26
  backAttack: 0.1,
27
- power: performanceStats == null ? 0.125 : performanceStats.power > performanceStats.quick ? 0.2 : 0.05,
27
+ power: performanceStats == null ? 0.125 : performanceStats.power >= performanceStats.quick ? 0.2 : 0.05,
28
28
  quick: performanceStats == null ? 0.125 : performanceStats.quick > performanceStats.power ? 0.2 : 0.05,
29
29
  awareness: 0.05,
30
30
  attack: 0.05
@@ -54,7 +54,7 @@ function getMultipliers(statName, performanceStats) {
54
54
  case StatsEnum.BLOCK:
55
55
  return {
56
56
  block: 0.65,
57
- read: performanceStats == null ? 0.125 : performanceStats.read > performanceStats.commit ? 0.2 : 0.05,
57
+ read: performanceStats == null ? 0.125 : performanceStats.read >= performanceStats.commit ? 0.2 : 0.05,
58
58
  commit: performanceStats == null ? 0.125 : performanceStats.commit > performanceStats.read ? 0.2 : 0.05,
59
59
  focus: 0.04,
60
60
  defense: 0.04,
@@ -33,6 +33,18 @@ function makeStats(base = 50, overrides = {}) {
33
33
  const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
34
34
  (0, globals_1.expect)(total).toBeCloseTo(1, 10);
35
35
  });
36
+ // Regression: on a tie the `>` comparisons used to give BOTH sides 0.05 (pair = 0.10), dropping the stat's
37
+ // weights to 0.85. With `>=` the higher-or-equal side keeps 0.2 so the pair is 0.25 and the total stays 1.
38
+ (0, globals_1.it)('on a power === quick tie, power keeps 0.2 and quick 0.05 (pair sums to 0.25)', () => {
39
+ const m = (0, stats_1.getMultipliers)(stats_1.StatsEnum.ATTACK, makeStats(50, { power: 50, quick: 50 }));
40
+ (0, globals_1.expect)(m.power).toBe(0.2);
41
+ (0, globals_1.expect)(m.quick).toBe(0.05);
42
+ });
43
+ (0, globals_1.it)('multipliers sum to 1 on a power === quick tie', () => {
44
+ const m = (0, stats_1.getMultipliers)(stats_1.StatsEnum.ATTACK, makeStats(50, { power: 50, quick: 50 }));
45
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
46
+ (0, globals_1.expect)(total).toBeCloseTo(1, 10);
47
+ });
36
48
  });
37
49
  (0, globals_1.describe)('SET', () => {
38
50
  (0, globals_1.it)('multipliers sum to 1', () => {
@@ -91,6 +103,17 @@ function makeStats(base = 50, overrides = {}) {
91
103
  const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
92
104
  (0, globals_1.expect)(total).toBeCloseTo(1, 10);
93
105
  });
106
+ // Regression: same tie bug as ATTACK's power/quick. On read === commit both used to fall to 0.05.
107
+ (0, globals_1.it)('on a read === commit tie, read keeps 0.2 and commit 0.05 (pair sums to 0.25)', () => {
108
+ const m = (0, stats_1.getMultipliers)(stats_1.StatsEnum.BLOCK, makeStats(50, { read: 50, commit: 50 }));
109
+ (0, globals_1.expect)(m.read).toBe(0.2);
110
+ (0, globals_1.expect)(m.commit).toBe(0.05);
111
+ });
112
+ (0, globals_1.it)('multipliers sum to 1 on a read === commit tie', () => {
113
+ const m = (0, stats_1.getMultipliers)(stats_1.StatsEnum.BLOCK, makeStats(50, { read: 50, commit: 50 }));
114
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
115
+ (0, globals_1.expect)(total).toBeCloseTo(1, 10);
116
+ });
94
117
  });
95
118
  (0, globals_1.describe)('STAMINA', () => {
96
119
  (0, globals_1.it)('returns exactly { stamina: 1 }', () => {
@@ -131,3 +154,43 @@ function makeStats(base = 50, overrides = {}) {
131
154
  (0, globals_1.expect)((0, stats_1.calculateStatScore)(stats, stats_1.StatsEnum.SERVE)).toBe(84);
132
155
  });
133
156
  });
157
+ // ─── invariants (every stat, every branch) ────────────────────────────────────
158
+ // These are the catch-all guards. The per-stat suites above each test one branch by hand; the original
159
+ // power===quick / read===commit weight-sum bug slipped through precisely because no suite swept ALL branches.
160
+ (0, globals_1.describe)('multiplier + score invariants (every stat)', () => {
161
+ const ALL_STATS = Object.values(stats_1.StatsEnum);
162
+ // Profiles that exercise the conditional ATTACK/BLOCK branches: the omitted-stats default, the equal tie (the
163
+ // edge the `>` comparisons mishandled), and both `>` orderings. Non-conditional stats are weight-stable across
164
+ // all four, which is itself worth pinning. Every branch MUST keep the weights summing to 1 or the general stat
165
+ // silently mis-scales.
166
+ const profiles = [
167
+ ['omitted', undefined],
168
+ ['uniform (power===quick, read===commit)', makeStats(50)],
169
+ ['power & read dominant', makeStats(50, { power: 80, quick: 20, read: 80, commit: 20 })],
170
+ ['quick & commit dominant', makeStats(50, { quick: 80, power: 20, commit: 80, read: 20 })]
171
+ ];
172
+ for (const stat of ALL_STATS) {
173
+ for (const [label, ps] of profiles) {
174
+ (0, globals_1.it)(`${stat}: multipliers sum to 1 (${label})`, () => {
175
+ const m = (0, stats_1.getMultipliers)(stat, ps);
176
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
177
+ (0, globals_1.expect)(total).toBeCloseTo(1, 10);
178
+ });
179
+ }
180
+ }
181
+ // The strongest single guard: because the weights sum to 1, a uniformly-rated player scores that exact rating.
182
+ // Uniform stats also create the power===quick / read===commit ties, so this fails loudly on any mis-summed branch
183
+ // (pre-fix, ATTACK/BLOCK would have scored ~54 here, not 63).
184
+ for (const stat of ALL_STATS) {
185
+ (0, globals_1.it)(`${stat}: a uniformly-rated player scores that exact rating`, () => {
186
+ (0, globals_1.expect)((0, stats_1.calculateStatScore)(makeStats(63), stat)).toBe(63);
187
+ });
188
+ }
189
+ // The general stat shares the substat scale, so an all-99 (SPECIAL-cap) player must not exceed 99 either way:
190
+ // this catches the opposite failure of weights summing to MORE than 1.
191
+ for (const stat of ALL_STATS) {
192
+ (0, globals_1.it)(`${stat}: an all-99 player does not exceed the 99 cap`, () => {
193
+ (0, globals_1.expect)((0, stats_1.calculateStatScore)(makeStats(99), stat)).toBeLessThanOrEqual(99);
194
+ });
195
+ }
196
+ });
@@ -29,8 +29,7 @@ export function transformToBlock(event, roster) {
29
29
  playerId: roster[event.p],
30
30
  target: event.a,
31
31
  blockers: (event.b ?? []).map(i => roster[i]),
32
- score: event.s,
33
- activeTraits: []
32
+ score: event.s
34
33
  });
35
34
  }
36
35
  export function transformToLiberoReplacement(event, roster) {
@@ -46,8 +45,7 @@ export function transformToReception(event, roster) {
46
45
  type: event.t,
47
46
  playerId: roster[event.p],
48
47
  target: event.a,
49
- score: event.s,
50
- activeTraits: []
48
+ score: event.s
51
49
  });
52
50
  }
53
51
  export function transformToServe(event, roster) {
@@ -56,8 +54,7 @@ export function transformToServe(event, roster) {
56
54
  type: event.t,
57
55
  playerId: roster[event.p],
58
56
  target: event.a,
59
- score: event.s,
60
- activeTraits: []
57
+ score: event.s
61
58
  });
62
59
  }
63
60
  export function transformToSet(event, roster) {
@@ -66,8 +63,7 @@ export function transformToSet(event, roster) {
66
63
  type: event.t,
67
64
  playerId: roster[event.p],
68
65
  target: event.a,
69
- score: event.s,
70
- activeTraits: []
66
+ score: event.s
71
67
  });
72
68
  }
73
69
  export function transformToSpike(event, roster) {
@@ -76,8 +72,7 @@ export function transformToSpike(event, roster) {
76
72
  type: event.t,
77
73
  playerId: roster[event.p],
78
74
  target: event.a,
79
- score: event.s,
80
- activeTraits: []
75
+ score: event.s
81
76
  });
82
77
  }
83
78
  export function transformToSubstitution(event, roster) {
@@ -64,11 +64,11 @@ function decodeLegacyEvent(event) {
64
64
  switch (event.e) {
65
65
  case 0: return LiberoReplacement.create({ playerId: event.p, libero: event.l, type: event.t });
66
66
  case 1: return Substitution.create({ playerId: event.p, playerOut: event.o });
67
- case 2: return Serve.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
68
- case 3: return Reception.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
69
- case 4: return VolleySet.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
70
- case 5: return Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s, activeTraits: [] });
71
- case 6: return Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s, activeTraits: [] });
67
+ case 2: return Serve.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
68
+ case 3: return Reception.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
69
+ case 4: return VolleySet.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
70
+ case 5: return Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
71
+ case 6: return Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s });
72
72
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
73
73
  }
74
74
  }
@@ -4,7 +4,7 @@ export declare const BlockInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const BlockInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  blockers: z.ZodArray<z.ZodUUID>;
@@ -7,7 +7,7 @@ export const BlockInputSchema = z.object({
7
7
  playerId: z.uuid(),
8
8
  score: z.number().finite(),
9
9
  target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
10
- activeTraits: z.array(z.enum(Object.values(TraitEnum))),
10
+ activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
11
11
  failure: z.number().int().refine((v) => blockFailureValues.includes(v), { message: 'INVALID_BLOCK_FAILURE' }),
12
12
  type: z.number().int().refine((v) => blockTypeValues.includes(v), { message: 'INVALID_BLOCK_TYPE' }),
13
13
  blockers: z.array(z.uuid())
@@ -4,7 +4,7 @@ export declare const ReceptionInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const ReceptionInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -7,7 +7,7 @@ export const ReceptionInputSchema = z.object({
7
7
  playerId: z.uuid(),
8
8
  score: z.number().finite(),
9
9
  target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
10
- activeTraits: z.array(z.enum(Object.values(TraitEnum))),
10
+ activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
11
11
  failure: z.number().int().refine((v) => receptionFailureValues.includes(v), { message: 'INVALID_RECEPTION_FAILURE' }),
12
12
  type: z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' })
13
13
  });
@@ -4,7 +4,7 @@ export declare const ServeInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const ServeInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -7,7 +7,7 @@ export const ServeInputSchema = z.object({
7
7
  playerId: z.uuid(),
8
8
  score: z.number().finite(),
9
9
  target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
10
- activeTraits: z.array(z.enum(Object.values(TraitEnum))),
10
+ activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
11
11
  failure: z.number().int().refine((v) => serveFailureValues.includes(v), { message: 'INVALID_SERVE_FAILURE' }),
12
12
  type: z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' })
13
13
  });
@@ -4,7 +4,7 @@ export declare const SetInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const SetInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -7,7 +7,7 @@ export const SetInputSchema = z.object({
7
7
  playerId: z.uuid(),
8
8
  score: z.number().finite(),
9
9
  target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
10
- activeTraits: z.array(z.enum(Object.values(TraitEnum))),
10
+ activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
11
11
  failure: z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
12
12
  type: z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' })
13
13
  });
@@ -4,7 +4,7 @@ export declare const SpikeInputSchema: z.ZodObject<{
4
4
  playerId: z.ZodUUID;
5
5
  score: z.ZodNumber;
6
6
  target: z.ZodNumber;
7
- activeTraits: z.ZodArray<z.ZodEnum<{
7
+ activeTraits: z.ZodDefault<z.ZodArray<z.ZodEnum<{
8
8
  MASTER_MIND: TraitEnum.MASTER_MIND;
9
9
  MOVING_WALL: TraitEnum.MOVING_WALL;
10
10
  SURPRISE_ATTACK: TraitEnum.SURPRISE_ATTACK;
@@ -14,7 +14,7 @@ export declare const SpikeInputSchema: z.ZodObject<{
14
14
  GUARDIAN: TraitEnum.GUARDIAN;
15
15
  FLOAT_SERVE: TraitEnum.FLOAT_SERVE;
16
16
  DEFENSIVE_SPECIALIST: TraitEnum.DEFENSIVE_SPECIALIST;
17
- }>>;
17
+ }>>>;
18
18
  failure: z.ZodNumber;
19
19
  type: z.ZodNumber;
20
20
  }, z.core.$strip>;
@@ -7,7 +7,7 @@ export const SpikeInputSchema = z.object({
7
7
  playerId: z.uuid(),
8
8
  score: z.number().finite(),
9
9
  target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
10
- activeTraits: z.array(z.enum(Object.values(TraitEnum))),
10
+ activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
11
11
  failure: z.number().int().refine((v) => spikeFailureValues.includes(v), { message: 'INVALID_SPIKE_FAILURE' }),
12
12
  type: z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' })
13
13
  });
@@ -19,7 +19,7 @@ export function getMultipliers(statName, performanceStats) {
19
19
  return {
20
20
  spike: 0.55,
21
21
  backAttack: 0.1,
22
- power: performanceStats == null ? 0.125 : performanceStats.power > performanceStats.quick ? 0.2 : 0.05,
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
25
  attack: 0.05
@@ -49,7 +49,7 @@ export function getMultipliers(statName, performanceStats) {
49
49
  case StatsEnum.BLOCK:
50
50
  return {
51
51
  block: 0.65,
52
- read: performanceStats == null ? 0.125 : performanceStats.read > performanceStats.commit ? 0.2 : 0.05,
52
+ read: performanceStats == null ? 0.125 : performanceStats.read >= performanceStats.commit ? 0.2 : 0.05,
53
53
  commit: performanceStats == null ? 0.125 : performanceStats.commit > performanceStats.read ? 0.2 : 0.05,
54
54
  focus: 0.04,
55
55
  defense: 0.04,
@@ -31,6 +31,18 @@ describe('getMultipliers()', () => {
31
31
  const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
32
32
  expect(total).toBeCloseTo(1, 10);
33
33
  });
34
+ // Regression: on a tie the `>` comparisons used to give BOTH sides 0.05 (pair = 0.10), dropping the stat's
35
+ // weights to 0.85. With `>=` the higher-or-equal side keeps 0.2 so the pair is 0.25 and the total stays 1.
36
+ it('on a power === quick tie, power keeps 0.2 and quick 0.05 (pair sums to 0.25)', () => {
37
+ const m = getMultipliers(StatsEnum.ATTACK, makeStats(50, { power: 50, quick: 50 }));
38
+ expect(m.power).toBe(0.2);
39
+ expect(m.quick).toBe(0.05);
40
+ });
41
+ it('multipliers sum to 1 on a power === quick tie', () => {
42
+ const m = getMultipliers(StatsEnum.ATTACK, makeStats(50, { power: 50, quick: 50 }));
43
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
44
+ expect(total).toBeCloseTo(1, 10);
45
+ });
34
46
  });
35
47
  describe('SET', () => {
36
48
  it('multipliers sum to 1', () => {
@@ -89,6 +101,17 @@ describe('getMultipliers()', () => {
89
101
  const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
90
102
  expect(total).toBeCloseTo(1, 10);
91
103
  });
104
+ // Regression: same tie bug as ATTACK's power/quick. On read === commit both used to fall to 0.05.
105
+ it('on a read === commit tie, read keeps 0.2 and commit 0.05 (pair sums to 0.25)', () => {
106
+ const m = getMultipliers(StatsEnum.BLOCK, makeStats(50, { read: 50, commit: 50 }));
107
+ expect(m.read).toBe(0.2);
108
+ expect(m.commit).toBe(0.05);
109
+ });
110
+ it('multipliers sum to 1 on a read === commit tie', () => {
111
+ const m = getMultipliers(StatsEnum.BLOCK, makeStats(50, { read: 50, commit: 50 }));
112
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
113
+ expect(total).toBeCloseTo(1, 10);
114
+ });
92
115
  });
93
116
  describe('STAMINA', () => {
94
117
  it('returns exactly { stamina: 1 }', () => {
@@ -129,3 +152,43 @@ describe('calculateStatScore()', () => {
129
152
  expect(calculateStatScore(stats, StatsEnum.SERVE)).toBe(84);
130
153
  });
131
154
  });
155
+ // ─── invariants (every stat, every branch) ────────────────────────────────────
156
+ // These are the catch-all guards. The per-stat suites above each test one branch by hand; the original
157
+ // power===quick / read===commit weight-sum bug slipped through precisely because no suite swept ALL branches.
158
+ describe('multiplier + score invariants (every stat)', () => {
159
+ const ALL_STATS = Object.values(StatsEnum);
160
+ // Profiles that exercise the conditional ATTACK/BLOCK branches: the omitted-stats default, the equal tie (the
161
+ // edge the `>` comparisons mishandled), and both `>` orderings. Non-conditional stats are weight-stable across
162
+ // all four, which is itself worth pinning. Every branch MUST keep the weights summing to 1 or the general stat
163
+ // silently mis-scales.
164
+ const profiles = [
165
+ ['omitted', undefined],
166
+ ['uniform (power===quick, read===commit)', makeStats(50)],
167
+ ['power & read dominant', makeStats(50, { power: 80, quick: 20, read: 80, commit: 20 })],
168
+ ['quick & commit dominant', makeStats(50, { quick: 80, power: 20, commit: 80, read: 20 })]
169
+ ];
170
+ for (const stat of ALL_STATS) {
171
+ for (const [label, ps] of profiles) {
172
+ it(`${stat}: multipliers sum to 1 (${label})`, () => {
173
+ const m = getMultipliers(stat, ps);
174
+ const total = Object.values(m).reduce((s, v) => s + (v ?? 0), 0);
175
+ expect(total).toBeCloseTo(1, 10);
176
+ });
177
+ }
178
+ }
179
+ // The strongest single guard: because the weights sum to 1, a uniformly-rated player scores that exact rating.
180
+ // Uniform stats also create the power===quick / read===commit ties, so this fails loudly on any mis-summed branch
181
+ // (pre-fix, ATTACK/BLOCK would have scored ~54 here, not 63).
182
+ for (const stat of ALL_STATS) {
183
+ it(`${stat}: a uniformly-rated player scores that exact rating`, () => {
184
+ expect(calculateStatScore(makeStats(63), stat)).toBe(63);
185
+ });
186
+ }
187
+ // The general stat shares the substat scale, so an all-99 (SPECIAL-cap) player must not exceed 99 either way:
188
+ // this catches the opposite failure of weights summing to MORE than 1.
189
+ for (const stat of ALL_STATS) {
190
+ it(`${stat}: an all-99 player does not exceed the 99 cap`, () => {
191
+ expect(calculateStatScore(makeStats(99), stat)).toBeLessThanOrEqual(99);
192
+ });
193
+ }
194
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.445",
3
+ "version": "0.0.448",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",