volleyballsimtypes 0.0.469 → 0.0.471

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 (69) hide show
  1. package/dist/cjs/src/api/index.d.ts +1 -1
  2. package/dist/cjs/src/data/models/tactics.d.ts +3 -3
  3. package/dist/cjs/src/data/models/tactics.js +3 -4
  4. package/dist/cjs/src/data/transformers/lineup-preset.js +4 -7
  5. package/dist/cjs/src/data/transformers/lineup-preset.test.js +9 -7
  6. package/dist/cjs/src/data/transformers/rally-event-injury.test.js +52 -0
  7. package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
  8. package/dist/cjs/src/data/transformers/rally-event.js +24 -5
  9. package/dist/cjs/src/data/transformers/tactics.js +28 -4
  10. package/dist/cjs/src/service/event/block.js +2 -2
  11. package/dist/cjs/src/service/event/in-play-event.d.ts +7 -1
  12. package/dist/cjs/src/service/event/in-play-event.js +2 -1
  13. package/dist/cjs/src/service/event/reception.js +2 -2
  14. package/dist/cjs/src/service/event/schemas/block.z.d.ts +7 -0
  15. package/dist/cjs/src/service/event/schemas/block.z.js +3 -1
  16. package/dist/cjs/src/service/event/schemas/incident.z.d.ts +9 -0
  17. package/dist/cjs/src/service/event/schemas/incident.z.js +11 -0
  18. package/dist/cjs/src/service/event/schemas/reception.z.d.ts +7 -0
  19. package/dist/cjs/src/service/event/schemas/reception.z.js +3 -1
  20. package/dist/cjs/src/service/event/schemas/serve.z.d.ts +7 -0
  21. package/dist/cjs/src/service/event/schemas/serve.z.js +3 -1
  22. package/dist/cjs/src/service/event/schemas/set.z.d.ts +7 -0
  23. package/dist/cjs/src/service/event/schemas/set.z.js +3 -1
  24. package/dist/cjs/src/service/event/schemas/spike.z.d.ts +7 -0
  25. package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
  26. package/dist/cjs/src/service/event/serve.js +2 -2
  27. package/dist/cjs/src/service/event/set.js +2 -2
  28. package/dist/cjs/src/service/event/spike.js +2 -2
  29. package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +28 -2
  30. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +1 -1
  31. package/dist/cjs/src/service/team/schemas/tactics.z.js +12 -4
  32. package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -1
  33. package/dist/cjs/src/service/team/tactics.d.ts +2 -2
  34. package/dist/cjs/src/service/team/tactics.js +2 -2
  35. package/dist/esm/src/api/index.d.ts +1 -1
  36. package/dist/esm/src/data/models/tactics.d.ts +3 -3
  37. package/dist/esm/src/data/models/tactics.js +3 -4
  38. package/dist/esm/src/data/transformers/lineup-preset.js +4 -7
  39. package/dist/esm/src/data/transformers/lineup-preset.test.js +9 -7
  40. package/dist/esm/src/data/transformers/rally-event-injury.test.js +54 -2
  41. package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
  42. package/dist/esm/src/data/transformers/rally-event.js +24 -5
  43. package/dist/esm/src/data/transformers/tactics.js +28 -4
  44. package/dist/esm/src/service/event/block.js +2 -2
  45. package/dist/esm/src/service/event/in-play-event.d.ts +7 -1
  46. package/dist/esm/src/service/event/in-play-event.js +2 -1
  47. package/dist/esm/src/service/event/reception.js +2 -2
  48. package/dist/esm/src/service/event/schemas/block.z.d.ts +7 -0
  49. package/dist/esm/src/service/event/schemas/block.z.js +3 -1
  50. package/dist/esm/src/service/event/schemas/incident.z.d.ts +9 -0
  51. package/dist/esm/src/service/event/schemas/incident.z.js +8 -0
  52. package/dist/esm/src/service/event/schemas/reception.z.d.ts +7 -0
  53. package/dist/esm/src/service/event/schemas/reception.z.js +3 -1
  54. package/dist/esm/src/service/event/schemas/serve.z.d.ts +7 -0
  55. package/dist/esm/src/service/event/schemas/serve.z.js +3 -1
  56. package/dist/esm/src/service/event/schemas/set.z.d.ts +7 -0
  57. package/dist/esm/src/service/event/schemas/set.z.js +3 -1
  58. package/dist/esm/src/service/event/schemas/spike.z.d.ts +7 -0
  59. package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
  60. package/dist/esm/src/service/event/serve.js +2 -2
  61. package/dist/esm/src/service/event/set.js +2 -2
  62. package/dist/esm/src/service/event/spike.js +2 -2
  63. package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +28 -2
  64. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +1 -1
  65. package/dist/esm/src/service/team/schemas/tactics.z.js +12 -4
  66. package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -1
  67. package/dist/esm/src/service/team/tactics.d.ts +2 -2
  68. package/dist/esm/src/service/team/tactics.js +2 -2
  69. package/package.json +1 -1
@@ -132,7 +132,7 @@ export interface Tactics {
132
132
  offensivePreferences: ApiOffensivePreference[];
133
133
  systemSets?: ApiSystemSet[];
134
134
  offensivePreferenceSets?: ApiOffensivePreference[][];
135
- replaceInjuredImmediately: boolean;
135
+ replaceKnockedImmediately?: Record<string, boolean>;
136
136
  injuryReplacements?: ApiInjuryReplacement[];
137
137
  }
138
138
  export interface ApiLineupPreset {
@@ -66,12 +66,12 @@ export interface TacticsAttributes {
66
66
  offensive_preferences: OffensivePreferenceAttributes[];
67
67
  system_sets?: SystemSetAttributes[];
68
68
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
69
- replace_injured_immediately?: boolean;
69
+ replace_knocked_immediately?: Record<PlayerId, boolean>;
70
70
  injury_replacements?: InjuryReplacementAttributes[];
71
71
  }
72
72
  export type TacticsPk = 'team_id';
73
73
  export type TacticsId = TacticsModel[TacticsPk];
74
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_injured_immediately' | 'injury_replacements';
74
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements';
75
75
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
76
76
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
77
77
  team_id: string;
@@ -88,7 +88,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
88
88
  offensive_preferences: OffensivePreferenceAttributes[];
89
89
  system_sets?: SystemSetAttributes[];
90
90
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
91
- replace_injured_immediately?: boolean;
91
+ replace_knocked_immediately?: Record<PlayerId, boolean>;
92
92
  injury_replacements?: InjuryReplacementAttributes[];
93
93
  team: TeamModel;
94
94
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
@@ -75,10 +75,9 @@ class TacticsModel extends sequelize_1.Model {
75
75
  type: sequelize_1.DataTypes.JSONB,
76
76
  allowNull: true
77
77
  },
78
- replace_injured_immediately: {
79
- type: sequelize_1.DataTypes.BOOLEAN,
80
- allowNull: false,
81
- defaultValue: true
78
+ replace_knocked_immediately: {
79
+ type: sequelize_1.DataTypes.JSONB,
80
+ allowNull: true
82
81
  },
83
82
  injury_replacements: {
84
83
  type: sequelize_1.DataTypes.JSONB,
@@ -2,18 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformToLineupPreset = transformToObject;
4
4
  // `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
5
- // Presets saved before the injuries fields exist lack replaceInjuredImmediately: heal it to the effective
6
- // default (true, matching transformToTactics' hydration) so the DTO's required field is honest and a UI
7
- // toggle seeded from an old preset cannot silently flip the user's effective setting on re-save.
5
+ // No injuries heal here anymore: replaceKnockedImmediately is optional (absent = every starter ON), and old
6
+ // configs carrying the superseded replaceInjuredImmediately boolean pass through the spread as a dead key
7
+ // that the save DTO strips (owner 2026-07-05: the old global value is dropped, default ON).
8
8
  function transformToObject(model) {
9
9
  return {
10
10
  presetId: model.preset_id,
11
11
  name: model.name,
12
12
  isActive: model.is_active,
13
13
  orderIndex: model.order_index,
14
- config: {
15
- ...model.config,
16
- replaceInjuredImmediately: model.config.replaceInjuredImmediately ?? true
17
- }
14
+ config: model.config
18
15
  };
19
16
  }
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const globals_1 = require("@jest/globals");
4
4
  const lineup_preset_1 = require("./lineup-preset");
5
5
  (0, globals_1.describe)('transformToLineupPreset()', () => {
6
- (0, globals_1.it)('maps preset columns to the API shape and heals a pre-injuries config to the effective default', () => {
6
+ (0, globals_1.it)('maps preset columns to the API shape verbatim (no injuries heal: absent map = every starter ON)', () => {
7
7
  const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
8
8
  const preset = (0, lineup_preset_1.transformToLineupPreset)({
9
9
  preset_id: 'p1',
@@ -18,13 +18,15 @@ const lineup_preset_1 = require("./lineup-preset");
18
18
  name: 'Starters',
19
19
  isActive: true,
20
20
  orderIndex: 2,
21
- // A config stored before the injuries fields lacks replaceInjuredImmediately; the transform heals it
22
- // to true (the effective hydration default) so the DTO's required field is honest.
23
- config: { ...config, replaceInjuredImmediately: true }
21
+ config
24
22
  });
25
23
  });
26
- (0, globals_1.it)('keeps an explicit replaceInjuredImmediately: false', () => {
27
- const config = { lineup: { bench: [] }, substitutionBand: 'TIRED', replaceInjuredImmediately: false };
24
+ (0, globals_1.it)('keeps an explicit per-starter replaceKnockedImmediately map', () => {
25
+ const config = {
26
+ lineup: { bench: [] },
27
+ substitutionBand: 'TIRED',
28
+ replaceKnockedImmediately: { 'starter-1': false }
29
+ };
28
30
  const preset = (0, lineup_preset_1.transformToLineupPreset)({
29
31
  preset_id: 'p2',
30
32
  team_id: 't1',
@@ -33,6 +35,6 @@ const lineup_preset_1 = require("./lineup-preset");
33
35
  order_index: 3,
34
36
  config
35
37
  });
36
- (0, globals_1.expect)(preset.config.replaceInjuredImmediately).toBe(false);
38
+ (0, globals_1.expect)(preset.config.replaceKnockedImmediately).toEqual({ 'starter-1': false });
37
39
  });
38
40
  });
@@ -31,3 +31,55 @@ const rally_event_1 = require("./rally-event");
31
31
  (0, globals_1.expect)(decoded.injury).toBeUndefined();
32
32
  });
33
33
  });
34
+ (0, globals_1.describe)('rally-event compact round trip — incident marker on the causing event', () => {
35
+ const playerId = (0, uuid_1.v4)();
36
+ const roster = [playerId];
37
+ const playerIndex = new Map(roster.map((id, i) => [id, i]));
38
+ function makeSpike(incident) {
39
+ return service_1.Spike.create({
40
+ playerId,
41
+ score: 62.5,
42
+ target: 4,
43
+ failure: service_1.SpikeFailureEnum.NO_FAILURE,
44
+ type: service_1.SpikeTypeEnum.CROSS_SHOT,
45
+ ...(incident != null ? { incident } : {})
46
+ });
47
+ }
48
+ (0, globals_1.it)('packs an INJURY incident as i = severity (1-4) and round-trips kind + severity', () => {
49
+ const compact = (0, rally_event_1.transformToCompact)(makeSpike({ kind: 'INJURY', severity: 4 }), playerIndex);
50
+ (0, globals_1.expect)(compact.i).toBe(4);
51
+ const decoded = (0, rally_event_1.transformToSpike)(compact, roster);
52
+ (0, globals_1.expect)(decoded.incident).toEqual({ kind: 'INJURY', severity: 4 });
53
+ });
54
+ (0, globals_1.it)('packs a KNOCK incident as i = 10 + tier and round-trips kind + severity', () => {
55
+ const serve = service_1.Serve.create({
56
+ playerId,
57
+ score: 55,
58
+ target: 6,
59
+ failure: service_1.ServeFailureEnum.NO_FAILURE,
60
+ type: service_1.ServeTypeEnum.JUMP_TOPSPIN,
61
+ incident: { kind: 'KNOCK', severity: 2 }
62
+ });
63
+ const compact = (0, rally_event_1.transformToCompact)(serve, playerIndex);
64
+ (0, globals_1.expect)(compact.i).toBe(12);
65
+ const decoded = (0, rally_event_1.transformToServe)(compact, roster);
66
+ (0, globals_1.expect)(decoded.incident).toEqual({ kind: 'KNOCK', severity: 2 });
67
+ });
68
+ (0, globals_1.it)('omits i entirely for an event without an incident', () => {
69
+ const compact = (0, rally_event_1.transformToCompact)(makeSpike(), playerIndex);
70
+ (0, globals_1.expect)('i' in compact).toBe(false);
71
+ const decoded = (0, rally_event_1.transformToSpike)(compact, roster);
72
+ (0, globals_1.expect)(decoded.incident).toBeUndefined();
73
+ });
74
+ (0, globals_1.it)('decodes a legacy compact event (no i key) with no incident', () => {
75
+ const legacy = { p: 0, e: service_1.EventTypeEnum.SPIKE, f: 0, t: 0, a: 4, s: 50 };
76
+ const decoded = (0, rally_event_1.transformToSpike)(legacy, roster);
77
+ (0, globals_1.expect)(decoded.incident).toBeUndefined();
78
+ });
79
+ (0, globals_1.it)('rejects an out-of-range or malformed incident at the schema', () => {
80
+ (0, globals_1.expect)(() => makeSpike({ kind: 'INJURY', severity: 5 })).toThrow(/INVALID_SPIKE/);
81
+ (0, globals_1.expect)(() => makeSpike({ kind: 'INJURY', severity: 0 })).toThrow(/INVALID_SPIKE/);
82
+ (0, globals_1.expect)(() => makeSpike({ kind: 'KNOCK', severity: 4 })).toThrow(/INVALID_SPIKE/);
83
+ (0, globals_1.expect)(() => makeSpike({ kind: 'BRUISE', severity: 1 })).toThrow(/INVALID_SPIKE/);
84
+ });
85
+ });
@@ -10,6 +10,7 @@ export interface CompactEvent {
10
10
  l?: number;
11
11
  o?: number;
12
12
  j?: number;
13
+ i?: number;
13
14
  }
14
15
  export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
15
16
  export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
@@ -9,6 +9,17 @@ exports.transformToSet = transformToSet;
9
9
  exports.transformToSpike = transformToSpike;
10
10
  exports.transformToSubstitution = transformToSubstitution;
11
11
  const service_1 = require("../../service");
12
+ // Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
13
+ function incidentToCompact(incident) {
14
+ if (incident == null)
15
+ return undefined;
16
+ return incident.kind === 'KNOCK' ? 10 + incident.severity : incident.severity;
17
+ }
18
+ function incidentFromCompact(i) {
19
+ if (i == null)
20
+ return undefined;
21
+ return i >= 10 ? { kind: 'KNOCK', severity: i - 10 } : { kind: 'INJURY', severity: i };
22
+ }
12
23
  function transformToCompact(evt, playerIndex) {
13
24
  const out = {
14
25
  p: playerIndex.get(evt.playerId),
@@ -19,6 +30,9 @@ function transformToCompact(evt, playerIndex) {
19
30
  out.t = evt.type;
20
31
  out.a = evt.target;
21
32
  out.s = evt.score;
33
+ const i = incidentToCompact(evt.incident);
34
+ if (i != null)
35
+ out.i = i;
22
36
  if (evt instanceof service_1.Block) {
23
37
  out.b = evt.blockers.map(id => playerIndex.get(id));
24
38
  }
@@ -41,7 +55,8 @@ function transformToBlock(event, roster) {
41
55
  playerId: roster[event.p],
42
56
  target: event.a,
43
57
  blockers: (event.b ?? []).map(i => roster[i]),
44
- score: event.s
58
+ score: event.s,
59
+ incident: incidentFromCompact(event.i)
45
60
  });
46
61
  }
47
62
  function transformToLiberoReplacement(event, roster) {
@@ -57,7 +72,8 @@ function transformToReception(event, roster) {
57
72
  type: event.t,
58
73
  playerId: roster[event.p],
59
74
  target: event.a,
60
- score: event.s
75
+ score: event.s,
76
+ incident: incidentFromCompact(event.i)
61
77
  });
62
78
  }
63
79
  function transformToServe(event, roster) {
@@ -66,7 +82,8 @@ function transformToServe(event, roster) {
66
82
  type: event.t,
67
83
  playerId: roster[event.p],
68
84
  target: event.a,
69
- score: event.s
85
+ score: event.s,
86
+ incident: incidentFromCompact(event.i)
70
87
  });
71
88
  }
72
89
  function transformToSet(event, roster) {
@@ -75,7 +92,8 @@ function transformToSet(event, roster) {
75
92
  type: event.t,
76
93
  playerId: roster[event.p],
77
94
  target: event.a,
78
- score: event.s
95
+ score: event.s,
96
+ incident: incidentFromCompact(event.i)
79
97
  });
80
98
  }
81
99
  function transformToSpike(event, roster) {
@@ -84,7 +102,8 @@ function transformToSpike(event, roster) {
84
102
  type: event.t,
85
103
  playerId: roster[event.p],
86
104
  target: event.a,
87
- score: event.s
105
+ score: event.s,
106
+ incident: incidentFromCompact(event.i)
88
107
  });
89
108
  }
90
109
  function transformToSubstitution(event, roster) {
@@ -88,7 +88,7 @@ function transformToAttributes(tactics, teamId) {
88
88
  rotation: p.rotation,
89
89
  order: p.order.map((a) => a.id)
90
90
  }))),
91
- replace_injured_immediately: tactics.replaceInjuredImmediately,
91
+ replace_knocked_immediately: tactics.replaceKnockedImmediately,
92
92
  injury_replacements: tactics.injuryReplacements.map(ir => ({
93
93
  starterId: ir.starter.id,
94
94
  replacementId: ir.replacement.id,
@@ -171,7 +171,19 @@ function transformToObject(model, roster) {
171
171
  rotation: p.rotation,
172
172
  order: p.order.map((id) => findPlayer(id, roster))
173
173
  }))),
174
- replaceInjuredImmediately: model.replace_injured_immediately ?? true,
174
+ // LENIENT: keep only starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
175
+ // value) can never fail schema validation and crash a team load; a dropped key = the ON default.
176
+ replaceKnockedImmediately: (() => {
177
+ const starterIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
178
+ lineup[service_1.CourtPosition.LEFT_BACK], lineup[service_1.CourtPosition.MIDDLE_BACK], lineup[service_1.CourtPosition.RIGHT_BACK]]
179
+ .map((p) => p.id));
180
+ const healed = {};
181
+ for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
182
+ if (starterIds.has(id) && typeof value === 'boolean')
183
+ healed[id] = value;
184
+ }
185
+ return healed;
186
+ })(),
175
187
  // LENIENT (roster.find, not findPlayer): an entry whose starter or replacement is gone/invalid is DROPPED
176
188
  // rather than crashing the sim batch (a dropped entry = unconfigured = the bench-only auto fallback, per
177
189
  // the owner rules); an invalid benchBackfill drops only the backfill. First occurrence wins on duplicates;
@@ -270,7 +282,19 @@ function tacticsColumnsToApiDto(model) {
270
282
  offensivePreferences: model.offensive_preferences,
271
283
  systemSets: model.system_sets,
272
284
  offensivePreferenceSets: model.offensive_preference_sets,
273
- replaceInjuredImmediately: model.replace_injured_immediately ?? true,
285
+ // Same stale-data policy as the heals below: only starter-keyed boolean entries survive, and an empty
286
+ // map surfaces as absent (missing key = ON).
287
+ replaceKnockedImmediately: (() => {
288
+ const starterSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
289
+ model.lineup[service_1.CourtPosition.RIGHT_FRONT], model.lineup[service_1.CourtPosition.LEFT_BACK],
290
+ model.lineup[service_1.CourtPosition.MIDDLE_BACK], model.lineup[service_1.CourtPosition.RIGHT_BACK]]);
291
+ const healed = {};
292
+ for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
293
+ if (starterSlotIds.has(id) && typeof value === 'boolean')
294
+ healed[id] = value;
295
+ }
296
+ return Object.keys(healed).length > 0 ? healed : undefined;
297
+ })(),
274
298
  // Same stale-data policy as validSecondLibero above: don't surface entries the service load would drop
275
299
  // (starter no longer a starter, replacement now a starter/libero/L2, duplicates), or the UI reloads and
276
300
  // re-emits them forever. Roster membership can't be checked here (ids only); the service heal covers that.
@@ -335,7 +359,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
335
359
  offensive_preferences: config.offensivePreferences,
336
360
  system_sets: config.systemSets,
337
361
  offensive_preference_sets: config.offensivePreferenceSets,
338
- replace_injured_immediately: config.replaceInjuredImmediately,
362
+ replace_knocked_immediately: config.replaceKnockedImmediately,
339
363
  injury_replacements: config.injuryReplacements
340
364
  };
341
365
  }
@@ -34,8 +34,8 @@ class Block extends in_play_event_1.InPlayEvent {
34
34
  }
35
35
  return new Block(result.data);
36
36
  }
37
- constructor({ score, playerId, target, failure, type, blockers, activeTraits }) {
38
- super({ score, playerId, target, activeTraits, eventType });
37
+ constructor({ score, playerId, target, failure, type, blockers, activeTraits, incident }) {
38
+ super({ score, playerId, target, activeTraits, incident, eventType });
39
39
  this.type = type;
40
40
  this.failure = failure;
41
41
  this.blockers = blockers;
@@ -1,10 +1,15 @@
1
1
  import { CourtTarget } from '../match';
2
2
  import { RallyEvent, RallyEventOpts } from './rally-event';
3
3
  import { Trait } from '../player';
4
+ export interface EventIncident {
5
+ readonly kind: 'KNOCK' | 'INJURY';
6
+ readonly severity: number;
7
+ }
4
8
  export interface InPlayEventOpts extends RallyEventOpts {
5
9
  readonly target: CourtTarget;
6
10
  readonly score: number;
7
11
  readonly activeTraits: Trait[];
12
+ readonly incident?: EventIncident;
8
13
  }
9
14
  export declare abstract class InPlayEvent<F, T> extends RallyEvent {
10
15
  abstract failure: F;
@@ -12,6 +17,7 @@ export declare abstract class InPlayEvent<F, T> extends RallyEvent {
12
17
  readonly target: CourtTarget;
13
18
  readonly score: number;
14
19
  readonly activeTraits: Trait[];
15
- protected constructor({ eventType, playerId, target, score, activeTraits }: InPlayEventOpts);
20
+ readonly incident?: EventIncident;
21
+ protected constructor({ eventType, playerId, target, score, activeTraits, incident }: InPlayEventOpts);
16
22
  protected _toString(): string;
17
23
  }
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InPlayEvent = void 0;
4
4
  const rally_event_1 = require("./rally-event");
5
5
  class InPlayEvent extends rally_event_1.RallyEvent {
6
- constructor({ eventType, playerId, target, score, activeTraits }) {
6
+ constructor({ eventType, playerId, target, score, activeTraits, incident }) {
7
7
  super({ playerId, eventType });
8
8
  this.target = target;
9
9
  this.score = Number(score.toFixed(2));
10
10
  this.activeTraits = activeTraits;
11
+ this.incident = incident;
11
12
  }
12
13
  _toString() {
13
14
  const score = `"score":${this.score}`;
@@ -36,8 +36,8 @@ class Reception extends in_play_event_1.InPlayEvent {
36
36
  }
37
37
  return new Reception(result.data);
38
38
  }
39
- constructor({ score, playerId, target, failure, type, activeTraits }) {
40
- super({ score, playerId, target, activeTraits, eventType });
39
+ constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
40
+ super({ score, playerId, target, activeTraits, incident, eventType });
41
41
  this.type = type;
42
42
  this.failure = failure;
43
43
  }
@@ -27,5 +27,12 @@ export declare const BlockInputSchema: z.ZodObject<{
27
27
  failure: z.ZodNumber;
28
28
  type: z.ZodNumber;
29
29
  blockers: z.ZodArray<z.ZodUUID>;
30
+ incident: z.ZodOptional<z.ZodObject<{
31
+ kind: z.ZodEnum<{
32
+ KNOCK: "KNOCK";
33
+ INJURY: "INJURY";
34
+ }>;
35
+ severity: z.ZodNumber;
36
+ }, z.core.$strip>>;
30
37
  }, z.core.$strip>;
31
38
  export type BlockInput = z.infer<typeof BlockInputSchema>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BlockInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const incident_z_1 = require("./incident.z");
6
7
  const blockTypeValues = [0, 1, 2, 3];
7
8
  const blockFailureValues = [0, 1, 2, 3, 4, 5];
8
9
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
@@ -13,7 +14,8 @@ exports.BlockInputSchema = zod_1.z.object({
13
14
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
15
  failure: zod_1.z.number().int().refine((v) => blockFailureValues.includes(v), { message: 'INVALID_BLOCK_FAILURE' }),
15
16
  type: zod_1.z.number().int().refine((v) => blockTypeValues.includes(v), { message: 'INVALID_BLOCK_TYPE' }),
16
- blockers: zod_1.z.array(zod_1.z.uuid())
17
+ blockers: zod_1.z.array(zod_1.z.uuid()),
18
+ incident: incident_z_1.EventIncidentSchema.optional()
17
19
  }).superRefine((data, ctx) => {
18
20
  // The block type IS the blocker count (NO_BLOCKER=0, SINGLE=1, DOUBLE=2, TRIPLE=3), so the two must agree.
19
21
  if (data.blockers.length !== data.type) {
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export declare const EventIncidentSchema: z.ZodObject<{
3
+ kind: z.ZodEnum<{
4
+ KNOCK: "KNOCK";
5
+ INJURY: "INJURY";
6
+ }>;
7
+ severity: z.ZodNumber;
8
+ }, z.core.$strip>;
9
+ export type EventIncidentInput = z.infer<typeof EventIncidentSchema>;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventIncidentSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ // The moment a player got hurt, attached to the causing in-play event (owner 2026-07-05: EVERY knock and
6
+ // injury marks the action that produced it). KNOCK severity is the knock tier (1-3); INJURY severity is the
7
+ // InjurySeverityEnum value (1-4).
8
+ exports.EventIncidentSchema = zod_1.z.object({
9
+ kind: zod_1.z.enum(['KNOCK', 'INJURY']),
10
+ severity: zod_1.z.number().int().min(1).max(4)
11
+ }).refine(v => v.kind !== 'KNOCK' || v.severity <= 3, { message: 'INVALID_KNOCK_TIER' });
@@ -26,5 +26,12 @@ export declare const ReceptionInputSchema: z.ZodObject<{
26
26
  }>>>;
27
27
  failure: z.ZodNumber;
28
28
  type: z.ZodNumber;
29
+ incident: z.ZodOptional<z.ZodObject<{
30
+ kind: z.ZodEnum<{
31
+ KNOCK: "KNOCK";
32
+ INJURY: "INJURY";
33
+ }>;
34
+ severity: z.ZodNumber;
35
+ }, z.core.$strip>>;
29
36
  }, z.core.$strip>;
30
37
  export type ReceptionInput = z.infer<typeof ReceptionInputSchema>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReceptionInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const incident_z_1 = require("./incident.z");
6
7
  const receptionTypeValues = [0, 1, 2, 3, 4];
7
8
  const receptionFailureValues = [0, 1, 2, 3, 4];
8
9
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
@@ -12,5 +13,6 @@ exports.ReceptionInputSchema = zod_1.z.object({
12
13
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
14
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
15
  failure: zod_1.z.number().int().refine((v) => receptionFailureValues.includes(v), { message: 'INVALID_RECEPTION_FAILURE' }),
15
- type: zod_1.z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' })
16
+ type: zod_1.z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' }),
17
+ incident: incident_z_1.EventIncidentSchema.optional()
16
18
  });
@@ -26,5 +26,12 @@ export declare const ServeInputSchema: z.ZodObject<{
26
26
  }>>>;
27
27
  failure: z.ZodNumber;
28
28
  type: z.ZodNumber;
29
+ incident: z.ZodOptional<z.ZodObject<{
30
+ kind: z.ZodEnum<{
31
+ KNOCK: "KNOCK";
32
+ INJURY: "INJURY";
33
+ }>;
34
+ severity: z.ZodNumber;
35
+ }, z.core.$strip>>;
29
36
  }, z.core.$strip>;
30
37
  export type ServeInput = z.infer<typeof ServeInputSchema>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ServeInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const incident_z_1 = require("./incident.z");
6
7
  const serveTypeValues = [0, 1, 2, 3];
7
8
  const serveFailureValues = [0, 1, 2, 3, 4];
8
9
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
@@ -12,5 +13,6 @@ exports.ServeInputSchema = zod_1.z.object({
12
13
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
14
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
15
  failure: zod_1.z.number().int().refine((v) => serveFailureValues.includes(v), { message: 'INVALID_SERVE_FAILURE' }),
15
- type: zod_1.z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' })
16
+ type: zod_1.z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' }),
17
+ incident: incident_z_1.EventIncidentSchema.optional()
16
18
  });
@@ -26,5 +26,12 @@ export declare const SetInputSchema: z.ZodObject<{
26
26
  }>>>;
27
27
  failure: z.ZodNumber;
28
28
  type: z.ZodNumber;
29
+ incident: z.ZodOptional<z.ZodObject<{
30
+ kind: z.ZodEnum<{
31
+ KNOCK: "KNOCK";
32
+ INJURY: "INJURY";
33
+ }>;
34
+ severity: z.ZodNumber;
35
+ }, z.core.$strip>>;
29
36
  }, z.core.$strip>;
30
37
  export type SetInput = z.infer<typeof SetInputSchema>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SetInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const incident_z_1 = require("./incident.z");
6
7
  const setTypeValues = [0, 1];
7
8
  const setFailureValues = [0, 1, 2, 3, 4];
8
9
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
@@ -12,5 +13,6 @@ exports.SetInputSchema = zod_1.z.object({
12
13
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
14
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
15
  failure: zod_1.z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
15
- type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' })
16
+ type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' }),
17
+ incident: incident_z_1.EventIncidentSchema.optional()
16
18
  });
@@ -26,5 +26,12 @@ export declare const SpikeInputSchema: z.ZodObject<{
26
26
  }>>>;
27
27
  failure: z.ZodNumber;
28
28
  type: z.ZodNumber;
29
+ incident: z.ZodOptional<z.ZodObject<{
30
+ kind: z.ZodEnum<{
31
+ KNOCK: "KNOCK";
32
+ INJURY: "INJURY";
33
+ }>;
34
+ severity: z.ZodNumber;
35
+ }, z.core.$strip>>;
29
36
  }, z.core.$strip>;
30
37
  export type SpikeInput = z.infer<typeof SpikeInputSchema>;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SpikeInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const incident_z_1 = require("./incident.z");
6
7
  const spikeTypeValues = [0, 1, 2, 3, 4, 5];
7
8
  const spikeFailureValues = [0, 1, 2, 3, 4];
8
9
  const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
@@ -12,5 +13,6 @@ exports.SpikeInputSchema = zod_1.z.object({
12
13
  target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
13
14
  activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
14
15
  failure: zod_1.z.number().int().refine((v) => spikeFailureValues.includes(v), { message: 'INVALID_SPIKE_FAILURE' }),
15
- type: zod_1.z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' })
16
+ type: zod_1.z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' }),
17
+ incident: incident_z_1.EventIncidentSchema.optional()
16
18
  });
@@ -33,8 +33,8 @@ class Serve extends in_play_event_1.InPlayEvent {
33
33
  }
34
34
  return new Serve(result.data);
35
35
  }
36
- constructor({ score, playerId, target, failure, type, activeTraits }) {
37
- super({ score, playerId, target, activeTraits, eventType });
36
+ constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
37
+ super({ score, playerId, target, activeTraits, incident, eventType });
38
38
  this.type = type;
39
39
  this.failure = failure;
40
40
  }
@@ -31,8 +31,8 @@ class Set extends in_play_event_1.InPlayEvent {
31
31
  }
32
32
  return new Set(result.data);
33
33
  }
34
- constructor({ score, playerId, target, failure, type, activeTraits }) {
35
- super({ score, playerId, target, activeTraits, eventType });
34
+ constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
35
+ super({ score, playerId, target, activeTraits, incident, eventType });
36
36
  this.type = type;
37
37
  this.failure = failure;
38
38
  }
@@ -35,8 +35,8 @@ class Spike extends in_play_event_1.InPlayEvent {
35
35
  }
36
36
  return new Spike(result.data);
37
37
  }
38
- constructor({ score, playerId, target, failure, type, activeTraits }) {
39
- super({ score, playerId, target, activeTraits, eventType });
38
+ constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
39
+ super({ score, playerId, target, activeTraits, incident, eventType });
40
40
  this.type = type;
41
41
  this.failure = failure;
42
42
  }
@@ -32,14 +32,40 @@ const test_helpers_1 = require("../../test-helpers");
32
32
  ...overrides
33
33
  };
34
34
  }
35
- (0, globals_1.it)('defaults to true / empty', () => {
35
+ (0, globals_1.it)('defaults to an empty knock map / empty replacements', () => {
36
36
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
37
37
  (0, globals_1.expect)(res.success).toBe(true);
38
38
  if (res.success) {
39
- (0, globals_1.expect)(res.data.replaceInjuredImmediately).toBe(true);
39
+ (0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({});
40
40
  (0, globals_1.expect)(res.data.injuryReplacements).toEqual([]);
41
41
  }
42
42
  });
43
+ (0, globals_1.it)('accepts a starter-keyed replaceKnockedImmediately map', () => {
44
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
45
+ replaceKnockedImmediately: { [p1.id]: false, [p3.id]: true }
46
+ }));
47
+ (0, globals_1.expect)(res.success).toBe(true);
48
+ if (res.success) {
49
+ (0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [p1.id]: false, [p3.id]: true });
50
+ }
51
+ });
52
+ (0, globals_1.it)('rejects a replaceKnockedImmediately key that is not a court starter', () => {
53
+ for (const outsider of [benchA, libero, reserveA]) {
54
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
55
+ replaceKnockedImmediately: { [outsider.id]: false }
56
+ }));
57
+ (0, globals_1.expect)(res.success).toBe(false);
58
+ if (!res.success) {
59
+ (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'REPLACE_KNOCKED_KEY_NOT_A_STARTER')).toBe(true);
60
+ }
61
+ }
62
+ });
63
+ (0, globals_1.it)('rejects a non-boolean replaceKnockedImmediately value', () => {
64
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
65
+ replaceKnockedImmediately: { [p1.id]: 'off' }
66
+ }));
67
+ (0, globals_1.expect)(res.success).toBe(false);
68
+ });
43
69
  (0, globals_1.it)('accepts a bench replacement with a reserve backfill', () => {
44
70
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
45
71
  injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: reserveA }]