volleyballsimtypes 0.0.479 → 0.0.480

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 (39) hide show
  1. package/dist/cjs/src/api/index.d.ts +1 -0
  2. package/dist/cjs/src/data/models/tactics.d.ts +1 -0
  3. package/dist/cjs/src/data/transformers/rally-event-tempo.test.d.ts +1 -0
  4. package/dist/cjs/src/data/transformers/rally-event-tempo.test.js +40 -0
  5. package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
  6. package/dist/cjs/src/data/transformers/rally-event.js +5 -1
  7. package/dist/cjs/src/data/transformers/tactics.js +8 -4
  8. package/dist/cjs/src/data/transformers/tactics.test.js +13 -0
  9. package/dist/cjs/src/service/event/schemas/set.z.d.ts +1 -0
  10. package/dist/cjs/src/service/event/schemas/set.z.js +4 -1
  11. package/dist/cjs/src/service/event/schemas/set.z.test.d.ts +1 -0
  12. package/dist/cjs/src/service/event/schemas/set.z.test.js +29 -0
  13. package/dist/cjs/src/service/event/set.d.ts +7 -0
  14. package/dist/cjs/src/service/event/set.js +15 -3
  15. package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -0
  16. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +8 -0
  17. package/dist/cjs/src/service/team/schemas/tactics.z.js +12 -2
  18. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +24 -0
  19. package/dist/cjs/src/service/team/schemas/team.z.d.ts +8 -0
  20. package/dist/esm/src/api/index.d.ts +1 -0
  21. package/dist/esm/src/data/models/tactics.d.ts +1 -0
  22. package/dist/esm/src/data/transformers/rally-event-tempo.test.d.ts +1 -0
  23. package/dist/esm/src/data/transformers/rally-event-tempo.test.js +38 -0
  24. package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
  25. package/dist/esm/src/data/transformers/rally-event.js +5 -1
  26. package/dist/esm/src/data/transformers/tactics.js +8 -4
  27. package/dist/esm/src/data/transformers/tactics.test.js +13 -0
  28. package/dist/esm/src/service/event/schemas/set.z.d.ts +1 -0
  29. package/dist/esm/src/service/event/schemas/set.z.js +4 -1
  30. package/dist/esm/src/service/event/schemas/set.z.test.d.ts +1 -0
  31. package/dist/esm/src/service/event/schemas/set.z.test.js +27 -0
  32. package/dist/esm/src/service/event/set.d.ts +7 -0
  33. package/dist/esm/src/service/event/set.js +14 -2
  34. package/dist/esm/src/service/team/offensive-preference.d.ts +2 -0
  35. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +8 -0
  36. package/dist/esm/src/service/team/schemas/tactics.z.js +12 -2
  37. package/dist/esm/src/service/team/schemas/tactics.z.test.js +24 -0
  38. package/dist/esm/src/service/team/schemas/team.z.d.ts +8 -0
  39. package/package.json +1 -1
@@ -109,6 +109,7 @@ export interface ApiDesignatedSub {
109
109
  export interface ApiOffensivePreference {
110
110
  rotation: number;
111
111
  order: string[];
112
+ tempos?: Record<string, 'FAST' | 'HIGH'>;
112
113
  }
113
114
  export interface ApiSystemSet {
114
115
  rotationSystem: RotationSystemEnum;
@@ -42,6 +42,7 @@ export interface DesignatedSubAttributes {
42
42
  export interface OffensivePreferenceAttributes {
43
43
  rotation: number;
44
44
  order: PlayerId[];
45
+ tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
45
46
  }
46
47
  export interface SystemSetAttributes {
47
48
  rotationSystem: RotationSystemEnum;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const uuid_1 = require("uuid");
5
+ const service_1 = require("../../service");
6
+ const rally_event_1 = require("./rally-event");
7
+ (0, globals_1.describe)('rally-event compact round trip — set tempo marker', () => {
8
+ const playerId = (0, uuid_1.v4)();
9
+ const roster = [playerId];
10
+ const playerIndex = new Map(roster.map((id, i) => [id, i]));
11
+ function makeSet(tempo) {
12
+ return service_1.Set.create({
13
+ playerId,
14
+ score: 90,
15
+ target: 4,
16
+ failure: service_1.SetFailureEnum.NO_FAILURE,
17
+ type: service_1.SetTypeEnum.OVERHAND,
18
+ ...(tempo !== undefined ? { tempo } : {})
19
+ });
20
+ }
21
+ (0, globals_1.it)('packs each tempo as m and round-trips it', () => {
22
+ for (const tempo of [service_1.SetTempoEnum.FIRST, service_1.SetTempoEnum.SECOND, service_1.SetTempoEnum.HIGH]) {
23
+ const compact = (0, rally_event_1.transformToCompact)(makeSet(tempo), playerIndex);
24
+ (0, globals_1.expect)(compact.m).toBe(tempo);
25
+ const decoded = (0, rally_event_1.transformToSet)(compact, roster);
26
+ (0, globals_1.expect)(decoded.tempo).toBe(tempo);
27
+ }
28
+ });
29
+ (0, globals_1.it)('omits m entirely for a set without a tempo', () => {
30
+ const compact = (0, rally_event_1.transformToCompact)(makeSet(), playerIndex);
31
+ (0, globals_1.expect)('m' in compact).toBe(false);
32
+ const decoded = (0, rally_event_1.transformToSet)(compact, roster);
33
+ (0, globals_1.expect)(decoded.tempo).toBeUndefined();
34
+ });
35
+ (0, globals_1.it)('decodes a legacy compact event (no m key) with no tempo', () => {
36
+ const legacy = { p: 0, e: service_1.EventTypeEnum.SET, f: 0, t: 0, a: 4, s: 90 };
37
+ const decoded = (0, rally_event_1.transformToSet)(legacy, roster);
38
+ (0, globals_1.expect)(decoded.tempo).toBeUndefined();
39
+ });
40
+ });
@@ -12,6 +12,7 @@ export interface CompactEvent {
12
12
  j?: number;
13
13
  i?: number;
14
14
  h?: number;
15
+ m?: number;
15
16
  }
16
17
  export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
17
18
  export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
@@ -44,6 +44,9 @@ function transformToCompact(evt, playerIndex) {
44
44
  if (evt instanceof service_1.Block) {
45
45
  out.b = evt.blockers.map(id => playerIndex.get(id));
46
46
  }
47
+ if (evt instanceof service_1.Set && evt.tempo !== undefined) {
48
+ out.m = evt.tempo;
49
+ }
47
50
  }
48
51
  else if (evt instanceof service_1.LiberoReplacement) {
49
52
  out.t = evt.type;
@@ -101,7 +104,8 @@ function transformToSet(event, roster) {
101
104
  playerId: roster[event.p],
102
105
  target: event.a,
103
106
  score: event.s,
104
- incident: incidentFromCompact(event.i, event.h, roster)
107
+ incident: incidentFromCompact(event.i, event.h, roster),
108
+ tempo: event.m
105
109
  });
106
110
  }
107
111
  function transformToSpike(event, roster) {
@@ -92,7 +92,8 @@ function transformToAttributes(tactics, teamId) {
92
92
  designated_setters: tactics.designatedSetters.map((s) => s.id),
93
93
  offensive_preferences: tactics.offensivePreferences.map(p => ({
94
94
  rotation: p.rotation,
95
- order: p.order.map((a) => a.id)
95
+ order: p.order.map((a) => a.id),
96
+ tempos: p.tempos
96
97
  })),
97
98
  system_sets: tactics.systemSets?.map(s => ({
98
99
  rotationSystem: s.rotationSystem,
@@ -100,7 +101,8 @@ function transformToAttributes(tactics, teamId) {
100
101
  })),
101
102
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
102
103
  rotation: p.rotation,
103
- order: p.order.map((a) => a.id)
104
+ order: p.order.map((a) => a.id),
105
+ tempos: p.tempos
104
106
  }))),
105
107
  replace_knocked_immediately: tactics.replaceKnockedImmediately,
106
108
  injury_replacements: tactics.injuryReplacements.map(ir => ({
@@ -190,7 +192,8 @@ function transformToObject(model, roster) {
190
192
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
191
193
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
192
194
  rotation: p.rotation,
193
- order: p.order.map((id) => findPlayer(id, roster))
195
+ order: p.order.map((id) => findPlayer(id, roster)),
196
+ tempos: p.tempos
194
197
  })),
195
198
  systemSets: model.system_sets?.map(s => ({
196
199
  rotationSystem: s.rotationSystem,
@@ -198,7 +201,8 @@ function transformToObject(model, roster) {
198
201
  })),
199
202
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
200
203
  rotation: p.rotation,
201
- order: p.order.map((id) => findPlayer(id, roster))
204
+ order: p.order.map((id) => findPlayer(id, roster)),
205
+ tempos: p.tempos
202
206
  }))),
203
207
  // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
204
208
  // libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
@@ -184,6 +184,19 @@ function makeTactics(designatedSubs) {
184
184
  (0, globals_1.expect)(back.systemSets).toBeUndefined();
185
185
  (0, globals_1.expect)(back.offensivePreferenceSets).toBeUndefined();
186
186
  });
187
+ (0, globals_1.it)('round-trips a per-starter tempos map on offensive preferences', () => {
188
+ const withTempos = service_1.Tactics.create({
189
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
190
+ liberoReplacements: [],
191
+ offensivePreferences: [{ rotation: 1, order: [p2, p3], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
192
+ });
193
+ const attrs = (0, tactics_1.transformFromTactics)(withTempos, 'team-1');
194
+ (0, globals_1.expect)(attrs.offensive_preferences).toEqual([
195
+ { rotation: 1, order: [p2.id, p3.id], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
196
+ ]);
197
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
198
+ (0, globals_1.expect)(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
199
+ });
187
200
  });
188
201
  (0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
189
202
  const country = (0, test_helpers_1.makeCountry)();
@@ -34,5 +34,6 @@ export declare const SetInputSchema: z.ZodObject<{
34
34
  severity: z.ZodNumber;
35
35
  playerId: z.ZodOptional<z.ZodUUID>;
36
36
  }, z.core.$strip>>;
37
+ tempo: z.ZodOptional<z.ZodNumber>;
37
38
  }, z.core.$strip>;
38
39
  export type SetInput = z.infer<typeof SetInputSchema>;
@@ -6,6 +6,7 @@ const player_1 = require("../../player");
6
6
  const incident_z_1 = require("./incident.z");
7
7
  const setTypeValues = [0, 1];
8
8
  const setFailureValues = [0, 1, 2, 3, 4];
9
+ const setTempoValues = [0, 1, 2];
9
10
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
10
11
  exports.SetInputSchema = zod_1.z.object({
11
12
  playerId: zod_1.z.uuid(),
@@ -14,5 +15,7 @@ exports.SetInputSchema = zod_1.z.object({
14
15
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
15
16
  failure: zod_1.z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
16
17
  type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' }),
17
- incident: incident_z_1.EventIncidentSchema.optional()
18
+ incident: incident_z_1.EventIncidentSchema.optional(),
19
+ // Set tempo (FIRST=0, SECOND=1, HIGH=2). Optional: absent on dumps, failed sets, and every pre-tempo event.
20
+ tempo: zod_1.z.number().int().refine((v) => setTempoValues.includes(v), { message: 'INVALID_SET_TEMPO' }).optional()
18
21
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const set_z_1 = require("./set.z");
5
+ const base = {
6
+ playerId: '11111111-1111-4111-8111-111111111111',
7
+ score: 90,
8
+ target: 4,
9
+ activeTraits: [],
10
+ failure: 0,
11
+ type: 0
12
+ };
13
+ (0, globals_1.describe)('SetInputSchema — tempo', () => {
14
+ (0, globals_1.it)('accepts every tempo 0-2 (FIRST / SECOND / HIGH)', () => {
15
+ for (const tempo of [0, 1, 2]) {
16
+ (0, globals_1.expect)(set_z_1.SetInputSchema.safeParse({ ...base, tempo }).success).toBe(true);
17
+ }
18
+ });
19
+ (0, globals_1.it)('accepts an absent tempo (Auto, dumps, failed sets)', () => {
20
+ (0, globals_1.expect)(set_z_1.SetInputSchema.safeParse(base).success).toBe(true);
21
+ });
22
+ (0, globals_1.it)('rejects an out-of-range tempo', () => {
23
+ (0, globals_1.expect)(set_z_1.SetInputSchema.safeParse({ ...base, tempo: 3 }).success).toBe(false);
24
+ (0, globals_1.expect)(set_z_1.SetInputSchema.safeParse({ ...base, tempo: -1 }).success).toBe(false);
25
+ });
26
+ (0, globals_1.it)('rejects a non-integer tempo', () => {
27
+ (0, globals_1.expect)(set_z_1.SetInputSchema.safeParse({ ...base, tempo: 1.5 }).success).toBe(false);
28
+ });
29
+ });
@@ -4,6 +4,12 @@ export declare enum SetTypeEnum {
4
4
  DUMP = 1
5
5
  }
6
6
  export type SetType = SetTypeEnum.OVERHAND | SetTypeEnum.DUMP;
7
+ export declare enum SetTempoEnum {
8
+ FIRST = 0,
9
+ SECOND = 1,
10
+ HIGH = 2
11
+ }
12
+ export type SetTempo = SetTempoEnum.FIRST | SetTempoEnum.SECOND | SetTempoEnum.HIGH;
7
13
  export declare enum SetFailureEnum {
8
14
  NO_FAILURE = 0,
9
15
  FAULT = 1,
@@ -15,6 +21,7 @@ export type SetFailure = SetFailureEnum.NO_FAILURE | SetFailureEnum.FAULT | SetF
15
21
  export declare class Set extends InPlayEvent<SetFailure, SetType> {
16
22
  readonly failure: SetFailure;
17
23
  readonly type: SetType;
24
+ readonly tempo?: SetTempo;
18
25
  static create(input: unknown): Set;
19
26
  private constructor();
20
27
  toString(): string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Set = exports.SetFailureEnum = exports.SetTypeEnum = void 0;
3
+ exports.Set = exports.SetFailureEnum = exports.SetTempoEnum = exports.SetTypeEnum = void 0;
4
4
  const rally_event_1 = require("./rally-event");
5
5
  const in_play_event_1 = require("./in-play-event");
6
6
  const set_z_1 = require("./schemas/set.z");
@@ -9,6 +9,16 @@ var SetTypeEnum;
9
9
  SetTypeEnum[SetTypeEnum["OVERHAND"] = 0] = "OVERHAND";
10
10
  SetTypeEnum[SetTypeEnum["DUMP"] = 1] = "DUMP";
11
11
  })(SetTypeEnum || (exports.SetTypeEnum = SetTypeEnum = {}));
12
+ // The tempo of a set (set-tempo rework). Decided by the simulator (pass quality + target lane + setter skill)
13
+ // or forced by a per-player offensive preference. FIRST = quick (swings the `quick` stat, hardest to nail,
14
+ // hardest to block); SECOND = second tempo; HIGH = high ball (the fallback, easiest to block). Only present on
15
+ // a successful overhand set; absent for dumps, failed sets, and free-ball/scramble attacks.
16
+ var SetTempoEnum;
17
+ (function (SetTempoEnum) {
18
+ SetTempoEnum[SetTempoEnum["FIRST"] = 0] = "FIRST";
19
+ SetTempoEnum[SetTempoEnum["SECOND"] = 1] = "SECOND";
20
+ SetTempoEnum[SetTempoEnum["HIGH"] = 2] = "HIGH";
21
+ })(SetTempoEnum || (exports.SetTempoEnum = SetTempoEnum = {}));
12
22
  var SetFailureEnum;
13
23
  (function (SetFailureEnum) {
14
24
  SetFailureEnum[SetFailureEnum["NO_FAILURE"] = 0] = "NO_FAILURE";
@@ -31,15 +41,17 @@ class Set extends in_play_event_1.InPlayEvent {
31
41
  }
32
42
  return new Set(result.data);
33
43
  }
34
- constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
44
+ constructor({ score, playerId, target, failure, type, activeTraits, incident, tempo }) {
35
45
  super({ score, playerId, target, activeTraits, incident, eventType });
36
46
  this.type = type;
37
47
  this.failure = failure;
48
+ this.tempo = tempo;
38
49
  }
39
50
  toString() {
40
51
  const failure = `"failure":${this.failure}`;
41
52
  const type = `"type":${this.type}`;
42
- return `{${this._toString()},${failure},${type}}`;
53
+ const tempo = this.tempo !== undefined ? `,"tempo":${this.tempo}` : '';
54
+ return `{${this._toString()},${failure},${type}${tempo}}`;
43
55
  }
44
56
  }
45
57
  exports.Set = Set;
@@ -1,5 +1,7 @@
1
1
  import { Player } from '../player';
2
+ export type TempoPreference = 'FAST' | 'HIGH';
2
3
  export interface OffensivePreference {
3
4
  readonly rotation: number;
4
5
  readonly order: Player[];
6
+ readonly tempos?: Record<string, TempoPreference>;
5
7
  }
@@ -270,6 +270,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
270
270
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
271
  rotation: z.ZodNumber;
272
272
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
273
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
274
+ FAST: "FAST";
275
+ HIGH: "HIGH";
276
+ }>>>;
273
277
  }, z.core.$strip>>>;
274
278
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
275
279
  rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
@@ -278,6 +282,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
278
282
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
279
283
  rotation: z.ZodNumber;
280
284
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
285
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
286
+ FAST: "FAST";
287
+ HIGH: "HIGH";
288
+ }>>>;
281
289
  }, z.core.$strip>>>>;
282
290
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
283
291
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -48,11 +48,15 @@ const lineupSchema = zod_1.z.object({
48
48
  });
49
49
  }
50
50
  });
51
+ // Per-player tempo preference (set-tempo rework): absent key = Auto.
52
+ const tempoPreferenceSchema = zod_1.z.enum(['FAST', 'HIGH']);
51
53
  // One rotation's ranked attacker priority. `rotation` is the main setter's rotational slot (1..6); `order` lists
52
- // the attackers ranked best-first. Cross-field checks (starters only, no dupes) live in the superRefine below.
54
+ // the attackers ranked best-first. `tempos` optionally forces a per-starter tempo (Auto when absent). Cross-field
55
+ // checks (starters only in both order and tempos, no dupes) live in the superRefine below.
53
56
  const offensivePreferenceSchema = zod_1.z.object({
54
57
  rotation: zod_1.z.number().int().min(1).max(6),
55
- order: zod_1.z.array(playerInstanceSchema)
58
+ order: zod_1.z.array(playerInstanceSchema),
59
+ tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
56
60
  });
57
61
  // One set's rotation-system override (per-set tactics): system + its designated setters travel together.
58
62
  const systemSetSchema = zod_1.z.object({
@@ -158,6 +162,12 @@ exports.TacticsInputSchema = zod_1.z.object({
158
162
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
159
163
  }
160
164
  }
165
+ // Tempo preference keys are starter ids too (same rule as order).
166
+ for (const id of Object.keys(pref.tempos ?? {})) {
167
+ if (!starterIds.has(id)) {
168
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
169
+ }
170
+ }
161
171
  }
162
172
  };
163
173
  validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
@@ -170,6 +170,30 @@ const test_helpers_1 = require("../../test-helpers");
170
170
  (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
171
171
  }
172
172
  });
173
+ (0, globals_1.it)('accepts offensive preferences with a starter-keyed tempos map', () => {
174
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
175
+ offensivePreferences: [{ rotation: 1, order: [p2, p3], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
176
+ }));
177
+ (0, globals_1.expect)(res.success).toBe(true);
178
+ if (res.success) {
179
+ (0, globals_1.expect)(res.data.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
180
+ }
181
+ });
182
+ (0, globals_1.it)('rejects a tempos map keyed by a non-starter', () => {
183
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
184
+ offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [reserve.id]: 'FAST' } }]
185
+ }));
186
+ (0, globals_1.expect)(res.success).toBe(false);
187
+ if (!res.success) {
188
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER')).toBe(true);
189
+ }
190
+ });
191
+ (0, globals_1.it)('rejects an invalid tempo value', () => {
192
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
193
+ offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [p2.id]: 'SLOW' } }]
194
+ }));
195
+ (0, globals_1.expect)(res.success).toBe(false);
196
+ });
173
197
  (0, globals_1.it)('still validates the flat fields when per-set overrides are present', () => {
174
198
  // Flat 5-1 with no setter is invalid even though every set entry is valid.
175
199
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
@@ -278,6 +278,10 @@ export declare const TeamInputSchema: z.ZodObject<{
278
278
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
279
279
  rotation: z.ZodNumber;
280
280
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
281
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
282
+ FAST: "FAST";
283
+ HIGH: "HIGH";
284
+ }>>>;
281
285
  }, z.core.$strip>>>;
282
286
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
283
287
  rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
@@ -286,6 +290,10 @@ export declare const TeamInputSchema: z.ZodObject<{
286
290
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
287
291
  rotation: z.ZodNumber;
288
292
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
293
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
294
+ FAST: "FAST";
295
+ HIGH: "HIGH";
296
+ }>>>;
289
297
  }, z.core.$strip>>>>;
290
298
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
291
299
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -109,6 +109,7 @@ export interface ApiDesignatedSub {
109
109
  export interface ApiOffensivePreference {
110
110
  rotation: number;
111
111
  order: string[];
112
+ tempos?: Record<string, 'FAST' | 'HIGH'>;
112
113
  }
113
114
  export interface ApiSystemSet {
114
115
  rotationSystem: RotationSystemEnum;
@@ -42,6 +42,7 @@ export interface DesignatedSubAttributes {
42
42
  export interface OffensivePreferenceAttributes {
43
43
  rotation: number;
44
44
  order: PlayerId[];
45
+ tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
45
46
  }
46
47
  export interface SystemSetAttributes {
47
48
  rotationSystem: RotationSystemEnum;
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { EventTypeEnum, Set, SetFailureEnum, SetTempoEnum, SetTypeEnum } from '../../service';
4
+ import { transformToCompact, transformToSet } from './rally-event';
5
+ describe('rally-event compact round trip — set tempo marker', () => {
6
+ const playerId = uuidv4();
7
+ const roster = [playerId];
8
+ const playerIndex = new Map(roster.map((id, i) => [id, i]));
9
+ function makeSet(tempo) {
10
+ return Set.create({
11
+ playerId,
12
+ score: 90,
13
+ target: 4,
14
+ failure: SetFailureEnum.NO_FAILURE,
15
+ type: SetTypeEnum.OVERHAND,
16
+ ...(tempo !== undefined ? { tempo } : {})
17
+ });
18
+ }
19
+ it('packs each tempo as m and round-trips it', () => {
20
+ for (const tempo of [SetTempoEnum.FIRST, SetTempoEnum.SECOND, SetTempoEnum.HIGH]) {
21
+ const compact = transformToCompact(makeSet(tempo), playerIndex);
22
+ expect(compact.m).toBe(tempo);
23
+ const decoded = transformToSet(compact, roster);
24
+ expect(decoded.tempo).toBe(tempo);
25
+ }
26
+ });
27
+ it('omits m entirely for a set without a tempo', () => {
28
+ const compact = transformToCompact(makeSet(), playerIndex);
29
+ expect('m' in compact).toBe(false);
30
+ const decoded = transformToSet(compact, roster);
31
+ expect(decoded.tempo).toBeUndefined();
32
+ });
33
+ it('decodes a legacy compact event (no m key) with no tempo', () => {
34
+ const legacy = { p: 0, e: EventTypeEnum.SET, f: 0, t: 0, a: 4, s: 90 };
35
+ const decoded = transformToSet(legacy, roster);
36
+ expect(decoded.tempo).toBeUndefined();
37
+ });
38
+ });
@@ -12,6 +12,7 @@ export interface CompactEvent {
12
12
  j?: number;
13
13
  i?: number;
14
14
  h?: number;
15
+ m?: number;
15
16
  }
16
17
  export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
17
18
  export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
@@ -34,6 +34,9 @@ export function transformToCompact(evt, playerIndex) {
34
34
  if (evt instanceof Block) {
35
35
  out.b = evt.blockers.map(id => playerIndex.get(id));
36
36
  }
37
+ if (evt instanceof Set && evt.tempo !== undefined) {
38
+ out.m = evt.tempo;
39
+ }
37
40
  }
38
41
  else if (evt instanceof LiberoReplacement) {
39
42
  out.t = evt.type;
@@ -91,7 +94,8 @@ export function transformToSet(event, roster) {
91
94
  playerId: roster[event.p],
92
95
  target: event.a,
93
96
  score: event.s,
94
- incident: incidentFromCompact(event.i, event.h, roster)
97
+ incident: incidentFromCompact(event.i, event.h, roster),
98
+ tempo: event.m
95
99
  });
96
100
  }
97
101
  export function transformToSpike(event, roster) {
@@ -86,7 +86,8 @@ function transformToAttributes(tactics, teamId) {
86
86
  designated_setters: tactics.designatedSetters.map((s) => s.id),
87
87
  offensive_preferences: tactics.offensivePreferences.map(p => ({
88
88
  rotation: p.rotation,
89
- order: p.order.map((a) => a.id)
89
+ order: p.order.map((a) => a.id),
90
+ tempos: p.tempos
90
91
  })),
91
92
  system_sets: tactics.systemSets?.map(s => ({
92
93
  rotationSystem: s.rotationSystem,
@@ -94,7 +95,8 @@ function transformToAttributes(tactics, teamId) {
94
95
  })),
95
96
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
96
97
  rotation: p.rotation,
97
- order: p.order.map((a) => a.id)
98
+ order: p.order.map((a) => a.id),
99
+ tempos: p.tempos
98
100
  }))),
99
101
  replace_knocked_immediately: tactics.replaceKnockedImmediately,
100
102
  injury_replacements: tactics.injuryReplacements.map(ir => ({
@@ -184,7 +186,8 @@ function transformToObject(model, roster) {
184
186
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
185
187
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
186
188
  rotation: p.rotation,
187
- order: p.order.map((id) => findPlayer(id, roster))
189
+ order: p.order.map((id) => findPlayer(id, roster)),
190
+ tempos: p.tempos
188
191
  })),
189
192
  systemSets: model.system_sets?.map(s => ({
190
193
  rotationSystem: s.rotationSystem,
@@ -192,7 +195,8 @@ function transformToObject(model, roster) {
192
195
  })),
193
196
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
194
197
  rotation: p.rotation,
195
- order: p.order.map((id) => findPlayer(id, roster))
198
+ order: p.order.map((id) => findPlayer(id, roster)),
199
+ tempos: p.tempos
196
200
  }))),
197
201
  // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
198
202
  // libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
@@ -182,6 +182,19 @@ describe('per-set tactics — service <-> row round-trip', () => {
182
182
  expect(back.systemSets).toBeUndefined();
183
183
  expect(back.offensivePreferenceSets).toBeUndefined();
184
184
  });
185
+ it('round-trips a per-starter tempos map on offensive preferences', () => {
186
+ const withTempos = Tactics.create({
187
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
188
+ liberoReplacements: [],
189
+ offensivePreferences: [{ rotation: 1, order: [p2, p3], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
190
+ });
191
+ const attrs = transformFromTactics(withTempos, 'team-1');
192
+ expect(attrs.offensive_preferences).toEqual([
193
+ { rotation: 1, order: [p2.id, p3.id], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
194
+ ]);
195
+ const back = transformToTactics(attrs, roster);
196
+ expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
197
+ });
185
198
  });
186
199
  describe('libero injuries — service <-> row round-trip + lenient heal', () => {
187
200
  const country = makeCountry();
@@ -34,5 +34,6 @@ export declare const SetInputSchema: z.ZodObject<{
34
34
  severity: z.ZodNumber;
35
35
  playerId: z.ZodOptional<z.ZodUUID>;
36
36
  }, z.core.$strip>>;
37
+ tempo: z.ZodOptional<z.ZodNumber>;
37
38
  }, z.core.$strip>;
38
39
  export type SetInput = z.infer<typeof SetInputSchema>;
@@ -3,6 +3,7 @@ import { TraitEnum } from '../../player';
3
3
  import { EventIncidentSchema } from './incident.z';
4
4
  const setTypeValues = [0, 1];
5
5
  const setFailureValues = [0, 1, 2, 3, 4];
6
+ const setTempoValues = [0, 1, 2];
6
7
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
7
8
  export const SetInputSchema = z.object({
8
9
  playerId: z.uuid(),
@@ -11,5 +12,7 @@ export const SetInputSchema = z.object({
11
12
  activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
12
13
  failure: z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
13
14
  type: z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' }),
14
- incident: EventIncidentSchema.optional()
15
+ incident: EventIncidentSchema.optional(),
16
+ // Set tempo (FIRST=0, SECOND=1, HIGH=2). Optional: absent on dumps, failed sets, and every pre-tempo event.
17
+ tempo: z.number().int().refine((v) => setTempoValues.includes(v), { message: 'INVALID_SET_TEMPO' }).optional()
15
18
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { SetInputSchema } from './set.z';
3
+ const base = {
4
+ playerId: '11111111-1111-4111-8111-111111111111',
5
+ score: 90,
6
+ target: 4,
7
+ activeTraits: [],
8
+ failure: 0,
9
+ type: 0
10
+ };
11
+ describe('SetInputSchema — tempo', () => {
12
+ it('accepts every tempo 0-2 (FIRST / SECOND / HIGH)', () => {
13
+ for (const tempo of [0, 1, 2]) {
14
+ expect(SetInputSchema.safeParse({ ...base, tempo }).success).toBe(true);
15
+ }
16
+ });
17
+ it('accepts an absent tempo (Auto, dumps, failed sets)', () => {
18
+ expect(SetInputSchema.safeParse(base).success).toBe(true);
19
+ });
20
+ it('rejects an out-of-range tempo', () => {
21
+ expect(SetInputSchema.safeParse({ ...base, tempo: 3 }).success).toBe(false);
22
+ expect(SetInputSchema.safeParse({ ...base, tempo: -1 }).success).toBe(false);
23
+ });
24
+ it('rejects a non-integer tempo', () => {
25
+ expect(SetInputSchema.safeParse({ ...base, tempo: 1.5 }).success).toBe(false);
26
+ });
27
+ });
@@ -4,6 +4,12 @@ export declare enum SetTypeEnum {
4
4
  DUMP = 1
5
5
  }
6
6
  export type SetType = SetTypeEnum.OVERHAND | SetTypeEnum.DUMP;
7
+ export declare enum SetTempoEnum {
8
+ FIRST = 0,
9
+ SECOND = 1,
10
+ HIGH = 2
11
+ }
12
+ export type SetTempo = SetTempoEnum.FIRST | SetTempoEnum.SECOND | SetTempoEnum.HIGH;
7
13
  export declare enum SetFailureEnum {
8
14
  NO_FAILURE = 0,
9
15
  FAULT = 1,
@@ -15,6 +21,7 @@ export type SetFailure = SetFailureEnum.NO_FAILURE | SetFailureEnum.FAULT | SetF
15
21
  export declare class Set extends InPlayEvent<SetFailure, SetType> {
16
22
  readonly failure: SetFailure;
17
23
  readonly type: SetType;
24
+ readonly tempo?: SetTempo;
18
25
  static create(input: unknown): Set;
19
26
  private constructor();
20
27
  toString(): string;
@@ -6,6 +6,16 @@ export var SetTypeEnum;
6
6
  SetTypeEnum[SetTypeEnum["OVERHAND"] = 0] = "OVERHAND";
7
7
  SetTypeEnum[SetTypeEnum["DUMP"] = 1] = "DUMP";
8
8
  })(SetTypeEnum || (SetTypeEnum = {}));
9
+ // The tempo of a set (set-tempo rework). Decided by the simulator (pass quality + target lane + setter skill)
10
+ // or forced by a per-player offensive preference. FIRST = quick (swings the `quick` stat, hardest to nail,
11
+ // hardest to block); SECOND = second tempo; HIGH = high ball (the fallback, easiest to block). Only present on
12
+ // a successful overhand set; absent for dumps, failed sets, and free-ball/scramble attacks.
13
+ export var SetTempoEnum;
14
+ (function (SetTempoEnum) {
15
+ SetTempoEnum[SetTempoEnum["FIRST"] = 0] = "FIRST";
16
+ SetTempoEnum[SetTempoEnum["SECOND"] = 1] = "SECOND";
17
+ SetTempoEnum[SetTempoEnum["HIGH"] = 2] = "HIGH";
18
+ })(SetTempoEnum || (SetTempoEnum = {}));
9
19
  export var SetFailureEnum;
10
20
  (function (SetFailureEnum) {
11
21
  SetFailureEnum[SetFailureEnum["NO_FAILURE"] = 0] = "NO_FAILURE";
@@ -28,14 +38,16 @@ export class Set extends InPlayEvent {
28
38
  }
29
39
  return new Set(result.data);
30
40
  }
31
- constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
41
+ constructor({ score, playerId, target, failure, type, activeTraits, incident, tempo }) {
32
42
  super({ score, playerId, target, activeTraits, incident, eventType });
33
43
  this.type = type;
34
44
  this.failure = failure;
45
+ this.tempo = tempo;
35
46
  }
36
47
  toString() {
37
48
  const failure = `"failure":${this.failure}`;
38
49
  const type = `"type":${this.type}`;
39
- return `{${this._toString()},${failure},${type}}`;
50
+ const tempo = this.tempo !== undefined ? `,"tempo":${this.tempo}` : '';
51
+ return `{${this._toString()},${failure},${type}${tempo}}`;
40
52
  }
41
53
  }
@@ -1,5 +1,7 @@
1
1
  import { Player } from '../player';
2
+ export type TempoPreference = 'FAST' | 'HIGH';
2
3
  export interface OffensivePreference {
3
4
  readonly rotation: number;
4
5
  readonly order: Player[];
6
+ readonly tempos?: Record<string, TempoPreference>;
5
7
  }
@@ -270,6 +270,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
270
270
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
271
  rotation: z.ZodNumber;
272
272
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
273
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
274
+ FAST: "FAST";
275
+ HIGH: "HIGH";
276
+ }>>>;
273
277
  }, z.core.$strip>>>;
274
278
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
275
279
  rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
@@ -278,6 +282,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
278
282
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
279
283
  rotation: z.ZodNumber;
280
284
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
285
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
286
+ FAST: "FAST";
287
+ HIGH: "HIGH";
288
+ }>>>;
281
289
  }, z.core.$strip>>>>;
282
290
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
283
291
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -45,11 +45,15 @@ const lineupSchema = z.object({
45
45
  });
46
46
  }
47
47
  });
48
+ // Per-player tempo preference (set-tempo rework): absent key = Auto.
49
+ const tempoPreferenceSchema = z.enum(['FAST', 'HIGH']);
48
50
  // One rotation's ranked attacker priority. `rotation` is the main setter's rotational slot (1..6); `order` lists
49
- // the attackers ranked best-first. Cross-field checks (starters only, no dupes) live in the superRefine below.
51
+ // the attackers ranked best-first. `tempos` optionally forces a per-starter tempo (Auto when absent). Cross-field
52
+ // checks (starters only in both order and tempos, no dupes) live in the superRefine below.
50
53
  const offensivePreferenceSchema = z.object({
51
54
  rotation: z.number().int().min(1).max(6),
52
- order: z.array(playerInstanceSchema)
55
+ order: z.array(playerInstanceSchema),
56
+ tempos: z.record(z.string(), tempoPreferenceSchema).optional()
53
57
  });
54
58
  // One set's rotation-system override (per-set tactics): system + its designated setters travel together.
55
59
  const systemSetSchema = z.object({
@@ -155,6 +159,12 @@ export const TacticsInputSchema = z.object({
155
159
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
156
160
  }
157
161
  }
162
+ // Tempo preference keys are starter ids too (same rule as order).
163
+ for (const id of Object.keys(pref.tempos ?? {})) {
164
+ if (!starterIds.has(id)) {
165
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
166
+ }
167
+ }
158
168
  }
159
169
  };
160
170
  validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
@@ -168,6 +168,30 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
168
168
  expect(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
169
169
  }
170
170
  });
171
+ it('accepts offensive preferences with a starter-keyed tempos map', () => {
172
+ const res = TacticsInputSchema.safeParse(baseInput({
173
+ offensivePreferences: [{ rotation: 1, order: [p2, p3], tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
174
+ }));
175
+ expect(res.success).toBe(true);
176
+ if (res.success) {
177
+ expect(res.data.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
178
+ }
179
+ });
180
+ it('rejects a tempos map keyed by a non-starter', () => {
181
+ const res = TacticsInputSchema.safeParse(baseInput({
182
+ offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [reserve.id]: 'FAST' } }]
183
+ }));
184
+ expect(res.success).toBe(false);
185
+ if (!res.success) {
186
+ expect(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER')).toBe(true);
187
+ }
188
+ });
189
+ it('rejects an invalid tempo value', () => {
190
+ const res = TacticsInputSchema.safeParse(baseInput({
191
+ offensivePreferences: [{ rotation: 1, order: [p2], tempos: { [p2.id]: 'SLOW' } }]
192
+ }));
193
+ expect(res.success).toBe(false);
194
+ });
171
195
  it('still validates the flat fields when per-set overrides are present', () => {
172
196
  // Flat 5-1 with no setter is invalid even though every set entry is valid.
173
197
  const res = TacticsInputSchema.safeParse(baseInput({
@@ -278,6 +278,10 @@ export declare const TeamInputSchema: z.ZodObject<{
278
278
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
279
279
  rotation: z.ZodNumber;
280
280
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
281
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
282
+ FAST: "FAST";
283
+ HIGH: "HIGH";
284
+ }>>>;
281
285
  }, z.core.$strip>>>;
282
286
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
283
287
  rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
@@ -286,6 +290,10 @@ export declare const TeamInputSchema: z.ZodObject<{
286
290
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
287
291
  rotation: z.ZodNumber;
288
292
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
293
+ tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
294
+ FAST: "FAST";
295
+ HIGH: "HIGH";
296
+ }>>>;
289
297
  }, z.core.$strip>>>>;
290
298
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
291
299
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.479",
3
+ "version": "0.0.480",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",