volleyballsimtypes 0.0.482 → 0.0.483

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 (59) hide show
  1. package/dist/cjs/src/api/index.d.ts +3 -17
  2. package/dist/cjs/src/data/models/tactics.d.ts +7 -17
  3. package/dist/cjs/src/data/transformers/match-preset-override.test.js +10 -1
  4. package/dist/cjs/src/data/transformers/rally-event.d.ts +2 -1
  5. package/dist/cjs/src/data/transformers/rally-event.js +12 -0
  6. package/dist/cjs/src/data/transformers/rally.js +2 -0
  7. package/dist/cjs/src/data/transformers/rally.test.d.ts +1 -0
  8. package/dist/cjs/src/data/transformers/rally.test.js +62 -0
  9. package/dist/cjs/src/data/transformers/tactics.js +26 -54
  10. package/dist/cjs/src/data/transformers/tactics.test.js +16 -11
  11. package/dist/cjs/src/service/team/designated-sub.d.ts +2 -4
  12. package/dist/cjs/src/service/team/index.d.ts +1 -0
  13. package/dist/cjs/src/service/team/index.js +1 -0
  14. package/dist/cjs/src/service/team/libero-sub.d.ts +0 -4
  15. package/dist/cjs/src/service/team/pinch-condition.d.ts +11 -3
  16. package/dist/cjs/src/service/team/pinch-condition.js +9 -4
  17. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  18. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +27 -23
  19. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +4 -2
  20. package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  21. package/dist/cjs/src/service/team/schemas/libero-sub.z.js +10 -26
  22. package/dist/cjs/src/service/team/schemas/libero-sub.z.test.js +74 -35
  23. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +28 -36
  24. package/dist/cjs/src/service/team/schemas/tactics.z.js +4 -3
  25. package/dist/cjs/src/service/team/schemas/team.z.d.ts +28 -36
  26. package/dist/cjs/src/service/team/sub-config-convert.d.ts +13 -0
  27. package/dist/cjs/src/service/team/sub-config-convert.js +91 -0
  28. package/dist/cjs/src/service/team/sub-config-convert.test.d.ts +1 -0
  29. package/dist/cjs/src/service/team/sub-config-convert.test.js +84 -0
  30. package/dist/esm/src/api/index.d.ts +3 -17
  31. package/dist/esm/src/data/models/tactics.d.ts +7 -17
  32. package/dist/esm/src/data/transformers/match-preset-override.test.js +10 -1
  33. package/dist/esm/src/data/transformers/rally-event.d.ts +2 -1
  34. package/dist/esm/src/data/transformers/rally-event.js +12 -1
  35. package/dist/esm/src/data/transformers/rally.js +4 -2
  36. package/dist/esm/src/data/transformers/rally.test.d.ts +1 -0
  37. package/dist/esm/src/data/transformers/rally.test.js +60 -0
  38. package/dist/esm/src/data/transformers/tactics.js +27 -55
  39. package/dist/esm/src/data/transformers/tactics.test.js +16 -11
  40. package/dist/esm/src/service/team/designated-sub.d.ts +2 -4
  41. package/dist/esm/src/service/team/index.d.ts +1 -0
  42. package/dist/esm/src/service/team/index.js +1 -0
  43. package/dist/esm/src/service/team/libero-sub.d.ts +0 -4
  44. package/dist/esm/src/service/team/pinch-condition.d.ts +11 -3
  45. package/dist/esm/src/service/team/pinch-condition.js +9 -4
  46. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  47. package/dist/esm/src/service/team/schemas/designated-sub.z.js +26 -22
  48. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +4 -2
  49. package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  50. package/dist/esm/src/service/team/schemas/libero-sub.z.js +9 -25
  51. package/dist/esm/src/service/team/schemas/libero-sub.z.test.js +75 -36
  52. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +28 -36
  53. package/dist/esm/src/service/team/schemas/tactics.z.js +4 -3
  54. package/dist/esm/src/service/team/schemas/team.z.d.ts +28 -36
  55. package/dist/esm/src/service/team/sub-config-convert.d.ts +13 -0
  56. package/dist/esm/src/service/team/sub-config-convert.js +85 -0
  57. package/dist/esm/src/service/team/sub-config-convert.test.d.ts +1 -0
  58. package/dist/esm/src/service/team/sub-config-convert.test.js +82 -0
  59. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, EnergyBand, LiberoSubMode, MatchForfeitType, PerformanceStats, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../service';
1
+ import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand } from '../service';
2
2
  export type Rally = DataProps<_Rally> & {
3
3
  homePlayerPosition: PlayerPosition[];
4
4
  awayPlayerPosition: PlayerPosition[];
@@ -81,29 +81,17 @@ export interface StartingLineup {
81
81
  bench: string[];
82
82
  }
83
83
  export interface ApiSetSubConfig {
84
- mode: SubMode;
85
84
  benchId?: string;
86
- fatigueBand?: EnergyBand;
87
- benchFatigueBand?: EnergyBand;
88
85
  conditions?: PinchCondition[];
89
86
  conditionLogic?: ConditionLogic;
90
87
  pinchServer?: boolean;
88
+ benchFatigueBand?: EnergyBand;
91
89
  subBackMode?: SubBackMode;
92
90
  subBackConditions?: PinchCondition[];
93
91
  subBackConditionLogic?: ConditionLogic;
94
92
  }
95
- export interface ApiDesignatedSub {
93
+ export interface ApiDesignatedSub extends ApiSetSubConfig {
96
94
  starterId: string;
97
- benchId?: string;
98
- mode: SubMode;
99
- fatigueBand?: EnergyBand;
100
- benchFatigueBand?: EnergyBand;
101
- conditions?: PinchCondition[];
102
- conditionLogic?: ConditionLogic;
103
- pinchServer?: boolean;
104
- subBackMode?: SubBackMode;
105
- subBackConditions?: PinchCondition[];
106
- subBackConditionLogic?: ConditionLogic;
107
95
  sets?: ApiSetSubConfig[];
108
96
  }
109
97
  export interface ApiOffensivePreference {
@@ -130,8 +118,6 @@ export interface ApiLiberoInjury {
130
118
  inMatchDesignateId?: string;
131
119
  }
132
120
  export interface ApiLiberoSetSubConfig {
133
- mode: LiberoSubMode;
134
- fatigueBand?: EnergyBand;
135
121
  conditions?: PinchCondition[];
136
122
  conditionLogic?: ConditionLogic;
137
123
  }
@@ -1,7 +1,7 @@
1
1
  import * as Sequelize from 'sequelize';
2
2
  import { Model, Optional } from 'sequelize';
3
3
  import { PlayerId, TeamId, TeamModel } from '.';
4
- import { ConditionLogic, CourtPosition, EnergyBand, LiberoSubMode, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
4
+ import { ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -13,30 +13,20 @@ export interface TacticsLineupAttributes {
13
13
  bench: PlayerId[];
14
14
  }
15
15
  export interface SetSubConfigAttributes {
16
- mode: SubMode;
17
16
  benchId?: PlayerId;
18
- fatigueBand?: EnergyBand;
19
- benchFatigueBand?: EnergyBand;
20
17
  conditions?: PinchCondition[];
21
18
  conditionLogic?: ConditionLogic;
22
19
  pinchServer?: boolean;
20
+ benchFatigueBand?: EnergyBand;
23
21
  subBackMode?: SubBackMode;
24
22
  subBackConditions?: PinchCondition[];
25
23
  subBackConditionLogic?: ConditionLogic;
24
+ mode?: SubMode;
25
+ fatigueBand?: SubBand;
26
26
  }
27
- export interface DesignatedSubAttributes {
27
+ export interface DesignatedSubAttributes extends SetSubConfigAttributes {
28
28
  starterId: PlayerId;
29
- benchId?: PlayerId;
30
- mode?: SubMode;
31
29
  isPinchServer?: boolean;
32
- fatigueBand?: SubBand;
33
- benchFatigueBand?: EnergyBand;
34
- conditions?: PinchCondition[];
35
- conditionLogic?: ConditionLogic;
36
- pinchServer?: boolean;
37
- subBackMode?: SubBackMode;
38
- subBackConditions?: PinchCondition[];
39
- subBackConditionLogic?: ConditionLogic;
40
30
  sets?: SetSubConfigAttributes[];
41
31
  }
42
32
  export interface OffensivePreferenceAttributes {
@@ -63,10 +53,10 @@ export interface LiberoInjuryAttributes {
63
53
  inMatchDesignateId?: PlayerId;
64
54
  }
65
55
  export interface LiberoSetSubConfigAttributes {
66
- mode: LiberoSubMode;
67
- fatigueBand?: EnergyBand;
68
56
  conditions?: PinchCondition[];
69
57
  conditionLogic?: ConditionLogic;
58
+ mode?: string;
59
+ fatigueBand?: EnergyBand;
70
60
  }
71
61
  export interface LiberoSubConfigAttributes extends LiberoSetSubConfigAttributes {
72
62
  sets?: LiberoSetSubConfigAttributes[];
@@ -150,7 +150,16 @@ function starterAt(tactics, zone) {
150
150
  (0, globals_1.expect)(back.lineup).toEqual(full.lineup);
151
151
  (0, globals_1.expect)(back.libero_replacements).toEqual(full.libero_replacements);
152
152
  (0, globals_1.expect)(back.receive_rotation_offset).toBe(true);
153
- (0, globals_1.expect)(back.designated_subs).toEqual(full.designated_subs);
153
+ // The dto path converts legacy mode-shaped subs to the unified condition engine, so the round-trip lands
154
+ // in the converted (unified) shape rather than the legacy input.
155
+ (0, globals_1.expect)(back.designated_subs).toEqual([{
156
+ starterId: ids[0],
157
+ conditions: [{ type: 'FATIGUE', band: 'WORN' }],
158
+ conditionLogic: 'ALL',
159
+ pinchServer: false,
160
+ subBackMode: 'FATIGUE',
161
+ sets: [{ conditions: [] }]
162
+ }]);
154
163
  (0, globals_1.expect)(back.libero_replacement_sets).toEqual(full.libero_replacement_sets);
155
164
  (0, globals_1.expect)(back.rotation_system).toBe('5-1');
156
165
  (0, globals_1.expect)(back.designated_setters).toEqual(full.designated_setters);
@@ -1,4 +1,4 @@
1
- import { Block, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
1
+ import { Block, FreeBall, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
2
2
  export interface CompactEvent {
3
3
  p: number;
4
4
  e: number;
@@ -21,4 +21,5 @@ export declare function transformToReception(event: CompactEvent, roster: string
21
21
  export declare function transformToServe(event: CompactEvent, roster: string[]): Serve;
22
22
  export declare function transformToSet(event: CompactEvent, roster: string[]): Set;
23
23
  export declare function transformToSpike(event: CompactEvent, roster: string[]): Spike;
24
+ export declare function transformToFreeBall(event: CompactEvent, roster: string[]): FreeBall;
24
25
  export declare function transformToSubstitution(event: CompactEvent, roster: string[]): Substitution;
@@ -7,6 +7,7 @@ exports.transformToReception = transformToReception;
7
7
  exports.transformToServe = transformToServe;
8
8
  exports.transformToSet = transformToSet;
9
9
  exports.transformToSpike = transformToSpike;
10
+ exports.transformToFreeBall = transformToFreeBall;
10
11
  exports.transformToSubstitution = transformToSubstitution;
11
12
  const service_1 = require("../../service");
12
13
  // Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
@@ -118,6 +119,17 @@ function transformToSpike(event, roster) {
118
119
  incident: incidentFromCompact(event.i, event.h, roster)
119
120
  });
120
121
  }
122
+ // FreeBall (Last Resort save) is a plain InPlayEvent with no incident of its own, so it decodes from f/t/a/s
123
+ // exactly like a spike minus the incident. Must stay in lockstep with the case 7 branch in rally.ts.
124
+ function transformToFreeBall(event, roster) {
125
+ return service_1.FreeBall.create({
126
+ failure: event.f,
127
+ type: event.t,
128
+ playerId: roster[event.p],
129
+ target: event.a,
130
+ score: event.s
131
+ });
132
+ }
121
133
  function transformToSubstitution(event, roster) {
122
134
  return service_1.Substitution.create({
123
135
  playerId: roster[event.p],
@@ -61,6 +61,7 @@ function decodeCompactEvent(event, roster) {
61
61
  case 4: return (0, rally_event_1.transformToSet)(event, roster);
62
62
  case 5: return (0, rally_event_1.transformToSpike)(event, roster);
63
63
  case 6: return (0, rally_event_1.transformToBlock)(event, roster);
64
+ case 7: return (0, rally_event_1.transformToFreeBall)(event, roster);
64
65
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
65
66
  }
66
67
  }
@@ -73,6 +74,7 @@ function decodeLegacyEvent(event) {
73
74
  case 4: return service_1.Set.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
74
75
  case 5: return service_1.Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
75
76
  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 });
77
+ case 7: return service_1.FreeBall.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
76
78
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
77
79
  }
78
80
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
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_1 = require("./rally");
7
+ // End-to-end rally round trip (encode -> bytea -> decode) exercising the decodeCompactEvent switch, with one
8
+ // event of EVERY EventTypeEnum member. This is the guard that would have caught the FREE_BALL decoder gap: the
9
+ // encoder is generic (out.e = evt.eventType) but the decoder is a hardcoded switch, so a new event type that is
10
+ // not wired into the switch throws UNKNOWN_EVENT_TYPE here. The coverage test forces new members into this list.
11
+ (0, globals_1.describe)('rally compact round trip — every event type', () => {
12
+ const pA = (0, uuid_1.v4)();
13
+ const pB = (0, uuid_1.v4)();
14
+ const pC = (0, uuid_1.v4)();
15
+ const setId = (0, uuid_1.v4)();
16
+ const events = [
17
+ service_1.LiberoReplacement.create({ playerId: pA, libero: pB, type: service_1.LiberoReplacementTypeEnum.LIBERO_IN }),
18
+ service_1.Substitution.create({ playerId: pA, playerOut: pC, injury: true }),
19
+ service_1.Serve.create({ playerId: pA, target: 6, score: 55, failure: service_1.ServeFailureEnum.NO_FAILURE, type: service_1.ServeTypeEnum.JUMP_TOPSPIN }),
20
+ service_1.Reception.create({ playerId: pB, target: 5, score: 50, failure: service_1.ReceptionFailureEnum.NO_FAILURE, type: service_1.ReceptionTypeEnum.DIG }),
21
+ service_1.Set.create({ playerId: pC, target: 3, score: 48, failure: service_1.SetFailureEnum.NO_FAILURE, type: service_1.SetTypeEnum.OVERHAND, tempo: service_1.SetTempoEnum.FIRST }),
22
+ service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.CROSS_SHOT }),
23
+ service_1.Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: service_1.BlockFailureEnum.NO_FAILURE, type: service_1.BlockTypeEnum.DOUBLE }),
24
+ service_1.FreeBall.create({ playerId: pC, target: 6, score: 45, failure: service_1.FreeBallFailureEnum.NO_FAILURE, type: service_1.FreeBallTypeEnum.OVER })
25
+ ];
26
+ const rally = service_1.Rally.create({ id: (0, uuid_1.v4)(), order: 0, servingTeamId: (0, uuid_1.v4)(), events });
27
+ (0, globals_1.it)('covers every EventTypeEnum member (adding a new event type must extend this list)', () => {
28
+ const numericTypes = Object.values(service_1.EventTypeEnum).filter((v) => typeof v === 'number');
29
+ const present = new Set(events.map(e => e.eventType));
30
+ for (const t of numericTypes)
31
+ (0, globals_1.expect)(present.has(t)).toBe(true);
32
+ (0, globals_1.expect)(present.size).toBe(numericTypes.length);
33
+ });
34
+ (0, globals_1.it)('encodes then decodes every event type back to an equal event (guards the decoder switch)', () => {
35
+ const attrs = (0, rally_1.transformFromRally)(rally, setId);
36
+ const decoded = (0, rally_1.transformToRally)(attrs);
37
+ (0, globals_1.expect)(decoded.events).toHaveLength(events.length);
38
+ (0, globals_1.expect)(decoded.events.map(e => e.eventType)).toEqual(events.map(e => e.eventType));
39
+ const [lib, sub, serve, rec, set, spike, block, free] = decoded.events;
40
+ (0, globals_1.expect)(lib.playerId).toBe(pA);
41
+ (0, globals_1.expect)(lib.libero).toBe(pB);
42
+ (0, globals_1.expect)(lib.type).toBe(service_1.LiberoReplacementTypeEnum.LIBERO_IN);
43
+ (0, globals_1.expect)(sub.playerOut).toBe(pC);
44
+ (0, globals_1.expect)(sub.injury).toBe(true);
45
+ (0, globals_1.expect)(serve.target).toBe(6);
46
+ (0, globals_1.expect)(serve.score).toBe(55);
47
+ (0, globals_1.expect)(serve.type).toBe(service_1.ServeTypeEnum.JUMP_TOPSPIN);
48
+ (0, globals_1.expect)(rec.target).toBe(5);
49
+ (0, globals_1.expect)(rec.type).toBe(service_1.ReceptionTypeEnum.DIG);
50
+ (0, globals_1.expect)(set.type).toBe(service_1.SetTypeEnum.OVERHAND);
51
+ (0, globals_1.expect)(set.tempo).toBe(service_1.SetTempoEnum.FIRST);
52
+ (0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.CROSS_SHOT);
53
+ (0, globals_1.expect)(spike.score).toBe(62.5);
54
+ (0, globals_1.expect)(block.type).toBe(service_1.BlockTypeEnum.DOUBLE);
55
+ (0, globals_1.expect)(block.blockers).toEqual([pB, pC]);
56
+ (0, globals_1.expect)(free.playerId).toBe(pC);
57
+ (0, globals_1.expect)(free.target).toBe(6);
58
+ (0, globals_1.expect)(free.score).toBe(45);
59
+ (0, globals_1.expect)(free.type).toBe(service_1.FreeBallTypeEnum.OVER);
60
+ (0, globals_1.expect)(free.failure).toBe(service_1.FreeBallFailureEnum.NO_FAILURE);
61
+ });
62
+ });
@@ -5,24 +5,9 @@ exports.transformFromTactics = transformToAttributes;
5
5
  exports.tacticsColumnsToApiDto = tacticsColumnsToApiDto;
6
6
  exports.apiTacticsConfigToTacticsAttributes = apiTacticsConfigToAttributes;
7
7
  const service_1 = require("../../service");
8
- // Back-compat: new rows persist `mode`; rows written before the mode change persist `isPinchServer` (+ a
9
- // 'NEVER' fatigueBand for the old "never fatigue-sub" state). Derive the mode so existing data keeps working
10
- // until the next tactics save rewrites it in the new shape.
11
- function subModeFromAttributes(d) {
12
- if (d.mode != null)
13
- return d.mode;
14
- if (d.isPinchServer === true)
15
- return 'PINCH';
16
- if (d.fatigueBand === 'NEVER')
17
- return 'NEVER';
18
- return 'FATIGUE';
19
- }
20
- // pinchServer only applies to PINCH ("Conditions") mode. A LEGACY PINCH row (no flag) defaults to true so
21
- // existing pinch servers keep serve-and-return behavior; a new conditional sub persists an explicit false. This
22
- // default must resolve identically on every READ path (both transformToObject and tacticsColumnsToApiDto).
23
- function effectivePinchServer(mode, pinchServer) {
24
- return mode === 'PINCH' ? (pinchServer ?? true) : undefined;
25
- }
8
+ // Unified condition engine (owner 2026-07-11): the READ path passes legacy fields (mode / fatigueBand /
9
+ // isPinchServer) through RAW; the schema preprocess in Tactics.create converts every legacy shape (see
10
+ // service/team/sub-config-convert.ts). The WRITE path persists only the unified shape.
26
11
  function findPlayer(id, roster) {
27
12
  const player = roster.find((p) => p.id === id);
28
13
  if (player == null)
@@ -66,23 +51,19 @@ function transformToAttributes(tactics, teamId) {
66
51
  designated_subs: tactics.designatedSubs.map(ds => ({
67
52
  starterId: ds.starter.id,
68
53
  benchId: ds.bench?.id,
69
- mode: ds.mode,
70
- fatigueBand: ds.fatigueBand,
71
- benchFatigueBand: ds.benchFatigueBand,
72
54
  conditions: ds.conditions,
73
55
  conditionLogic: ds.conditionLogic,
74
56
  pinchServer: ds.pinchServer,
57
+ benchFatigueBand: ds.benchFatigueBand,
75
58
  subBackMode: ds.subBackMode,
76
59
  subBackConditions: ds.subBackConditions,
77
60
  subBackConditionLogic: ds.subBackConditionLogic,
78
61
  sets: ds.sets?.map(s => ({
79
- mode: s.mode,
80
62
  benchId: s.bench?.id,
81
- fatigueBand: s.fatigueBand,
82
- benchFatigueBand: s.benchFatigueBand,
83
63
  conditions: s.conditions,
84
64
  conditionLogic: s.conditionLogic,
85
65
  pinchServer: s.pinchServer,
66
+ benchFatigueBand: s.benchFatigueBand,
86
67
  subBackMode: s.subBackMode,
87
68
  subBackConditions: s.subBackConditions,
88
69
  subBackConditionLogic: s.subBackConditionLogic
@@ -165,15 +146,19 @@ function transformToObject(model, roster) {
165
146
  secondLibero,
166
147
  liberoSub: secondLibero != null ? (model.libero_sub ?? undefined) : undefined,
167
148
  liberoReplacementSets,
149
+ // Legacy fields (mode / fatigueBand / isPinchServer) pass through RAW: the DesignatedSubSchema preprocess
150
+ // inside Tactics.create converts every legacy shape (mode-era AND isPinchServer-era rows) to the unified
151
+ // condition engine. Only the player references are resolved here.
168
152
  designatedSubs: (model.designated_subs ?? []).map(d => ({
169
153
  starter: findPlayer(d.starterId, roster),
170
154
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
171
- mode: subModeFromAttributes(d),
172
- fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
155
+ mode: d.mode,
156
+ isPinchServer: d.isPinchServer,
157
+ fatigueBand: d.fatigueBand,
173
158
  benchFatigueBand: d.benchFatigueBand,
174
159
  conditions: d.conditions,
175
160
  conditionLogic: d.conditionLogic,
176
- pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
161
+ pinchServer: d.pinchServer,
177
162
  subBackMode: d.subBackMode,
178
163
  subBackConditions: d.subBackConditions,
179
164
  subBackConditionLogic: d.subBackConditionLogic,
@@ -184,7 +169,7 @@ function transformToObject(model, roster) {
184
169
  benchFatigueBand: s.benchFatigueBand,
185
170
  conditions: s.conditions,
186
171
  conditionLogic: s.conditionLogic,
187
- pinchServer: effectivePinchServer(s.mode, s.pinchServer),
172
+ pinchServer: s.pinchServer,
188
173
  subBackMode: s.subBackMode,
189
174
  subBackConditions: s.subBackConditions,
190
175
  subBackConditionLogic: s.subBackConditionLogic
@@ -332,33 +317,20 @@ function tacticsColumnsToApiDto(model) {
332
317
  receiveRotationOffset: model.receive_rotation_offset,
333
318
  substitutionBand: model.substitution_band,
334
319
  secondLibero: validSecondLibero,
335
- liberoSub: validSecondLibero != null ? model.libero_sub : undefined,
320
+ // The API surfaces the UNIFIED condition-engine shape: legacy rows (mode-era / isPinchServer-era) convert
321
+ // here via the same service converters the schema preprocess uses, so clients never see a mode.
322
+ liberoSub: validSecondLibero != null
323
+ ? (model.libero_sub != null ? (0, service_1.convertLegacyLiberoSubConfig)(model.libero_sub) : undefined)
324
+ : undefined,
336
325
  liberoReplacementSets: validLiberoReplacementSets,
337
- designatedSubs: (model.designated_subs ?? []).map(d => ({
338
- starterId: d.starterId,
339
- benchId: d.benchId,
340
- mode: subModeFromAttributes(d),
341
- fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
342
- benchFatigueBand: d.benchFatigueBand,
343
- conditions: d.conditions,
344
- conditionLogic: d.conditionLogic,
345
- pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
346
- subBackMode: d.subBackMode,
347
- subBackConditions: d.subBackConditions,
348
- subBackConditionLogic: d.subBackConditionLogic,
349
- sets: d.sets?.map(s => ({
350
- mode: s.mode,
351
- benchId: s.benchId,
352
- fatigueBand: s.fatigueBand,
353
- benchFatigueBand: s.benchFatigueBand,
354
- conditions: s.conditions,
355
- conditionLogic: s.conditionLogic,
356
- pinchServer: effectivePinchServer(s.mode, s.pinchServer),
357
- subBackMode: s.subBackMode,
358
- subBackConditions: s.subBackConditions,
359
- subBackConditionLogic: s.subBackConditionLogic
360
- }))
361
- })),
326
+ designatedSubs: (model.designated_subs ?? []).map(d => {
327
+ const { starterId, sets, ...cfg } = d;
328
+ return {
329
+ ...(0, service_1.convertLegacySetSubConfig)(cfg),
330
+ starterId,
331
+ sets: sets?.map(s => (0, service_1.convertLegacySetSubConfig)(s))
332
+ };
333
+ }),
362
334
  rotationSystem: model.rotation_system,
363
335
  designatedSetters: model.designated_setters,
364
336
  offensivePreferences: model.offensive_preferences,
@@ -40,25 +40,28 @@ function makeTactics(designatedSubs) {
40
40
  (0, globals_1.expect)(dto.lineup.bench).toEqual(['b1', 'b2']);
41
41
  (0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
42
42
  });
43
- (0, globals_1.it)("resolves a legacy isPinchServer entry to mode 'PINCH'", () => {
43
+ (0, globals_1.it)('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
44
44
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
45
45
  { starterId: 's1', benchId: 'b1', isPinchServer: true, conditions: [{ type: 'ASAP' }] }
46
46
  ]));
47
- (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('PINCH');
47
+ (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBe(true);
48
+ (0, globals_1.expect)(dto.designatedSubs[0].conditions).toEqual([{ type: 'ASAP' }]);
49
+ (0, globals_1.expect)(dto.designatedSubs[0].mode).toBeUndefined();
48
50
  });
49
- (0, globals_1.it)("resolves a legacy 'NEVER' fatigueBand to mode NEVER and drops the band", () => {
51
+ (0, globals_1.it)("converts a legacy 'NEVER' fatigueBand to an explicit empty condition list", () => {
50
52
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
51
53
  { starterId: 's1', fatigueBand: 'NEVER' }
52
54
  ]));
53
- (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('NEVER');
55
+ (0, globals_1.expect)(dto.designatedSubs[0].conditions).toEqual([]);
54
56
  (0, globals_1.expect)(dto.designatedSubs[0].fatigueBand).toBeUndefined();
55
57
  });
56
- (0, globals_1.it)('passes a fatigue entry with its own bands through unchanged', () => {
58
+ (0, globals_1.it)('converts a legacy fatigue entry to a FATIGUE condition carrying its band', () => {
57
59
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
58
60
  { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: service_1.EnergyBand.WORN, benchFatigueBand: service_1.EnergyBand.TIRED }
59
61
  ]));
60
- (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('FATIGUE');
61
- (0, globals_1.expect)(dto.designatedSubs[0].fatigueBand).toBe(service_1.EnergyBand.WORN);
62
+ (0, globals_1.expect)(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: service_1.EnergyBand.WORN }]);
63
+ (0, globals_1.expect)(dto.designatedSubs[0].subBackMode).toBe('FATIGUE');
64
+ (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBe(false);
62
65
  (0, globals_1.expect)(dto.designatedSubs[0].benchFatigueBand).toBe(service_1.EnergyBand.TIRED);
63
66
  });
64
67
  (0, globals_1.it)('defaults a legacy PINCH row (no pinchServer flag) to pinchServer true', () => {
@@ -67,11 +70,12 @@ function makeTactics(designatedSubs) {
67
70
  ]));
68
71
  (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBe(true);
69
72
  });
70
- (0, globals_1.it)('leaves pinchServer undefined for a non-PINCH row', () => {
73
+ (0, globals_1.it)('passes an already-unified entry through untouched', () => {
71
74
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
72
- { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: service_1.EnergyBand.TIRED }
75
+ { starterId: 's1', benchId: 'b1', conditions: [{ type: 'FATIGUE', band: service_1.EnergyBand.TIRED }], conditionLogic: 'ALL', pinchServer: false, subBackMode: 'FATIGUE' }
73
76
  ]));
74
- (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBeUndefined();
77
+ (0, globals_1.expect)(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: service_1.EnergyBand.TIRED }]);
78
+ (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBe(false);
75
79
  });
76
80
  (0, globals_1.it)('passes a conditional regular sub (pinchServer false + condition sub-back) through unchanged', () => {
77
81
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
@@ -108,7 +112,8 @@ function makeTactics(designatedSubs) {
108
112
  model.libero_sub = { mode: 'FATIGUE' };
109
113
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
110
114
  (0, globals_1.expect)(dto.secondLibero).toBe('reserve1');
111
- (0, globals_1.expect)(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
115
+ // Legacy FATIGUE libero rule converts to the unified condition shape on the API read path.
116
+ (0, globals_1.expect)(dto.liberoSub).toEqual({ conditions: [{ type: 'FATIGUE' }], conditionLogic: 'ALL' });
112
117
  });
113
118
  (0, globals_1.it)('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
114
119
  const model = makeTactics([]);
@@ -1,15 +1,13 @@
1
1
  import { Player } from '../player';
2
- import { EnergyBand, SubMode } from './energy-band';
2
+ import { EnergyBand } from './energy-band';
3
3
  import { ConditionLogic, PinchCondition } from './pinch-condition';
4
4
  export type SubBackMode = 'FATIGUE' | 'CONDITIONS';
5
5
  export interface SetSubConfig {
6
- readonly mode: SubMode;
7
6
  readonly bench?: Player;
8
- readonly fatigueBand?: EnergyBand;
9
- readonly benchFatigueBand?: EnergyBand;
10
7
  readonly conditions?: PinchCondition[];
11
8
  readonly conditionLogic?: ConditionLogic;
12
9
  readonly pinchServer?: boolean;
10
+ readonly benchFatigueBand?: EnergyBand;
13
11
  readonly subBackMode?: SubBackMode;
14
12
  readonly subBackConditions?: PinchCondition[];
15
13
  readonly subBackConditionLogic?: ConditionLogic;
@@ -7,6 +7,7 @@ export * from './energy-band';
7
7
  export * from './pinch-condition';
8
8
  export * from './designated-sub';
9
9
  export * from './libero-sub';
10
+ export * from './sub-config-convert';
10
11
  export * from './rotation-system';
11
12
  export * from './lineup-function';
12
13
  export * from './base-position';
@@ -23,6 +23,7 @@ __exportStar(require("./energy-band"), exports);
23
23
  __exportStar(require("./pinch-condition"), exports);
24
24
  __exportStar(require("./designated-sub"), exports);
25
25
  __exportStar(require("./libero-sub"), exports);
26
+ __exportStar(require("./sub-config-convert"), exports);
26
27
  __exportStar(require("./rotation-system"), exports);
27
28
  __exportStar(require("./lineup-function"), exports);
28
29
  __exportStar(require("./base-position"), exports);
@@ -1,9 +1,5 @@
1
- import { EnergyBand } from './energy-band';
2
1
  import { ConditionLogic, PinchCondition } from './pinch-condition';
3
- export type LiberoSubMode = 'NEVER' | 'FATIGUE' | 'ALWAYS' | 'CONDITIONS';
4
2
  export interface LiberoSetSubConfig {
5
- readonly mode: LiberoSubMode;
6
- readonly fatigueBand?: EnergyBand;
7
3
  readonly conditions?: PinchCondition[];
8
4
  readonly conditionLogic?: ConditionLogic;
9
5
  }
@@ -1,5 +1,6 @@
1
+ import { EnergyBand, SubBand } from './energy-band';
1
2
  export declare enum PinchConditionType {
2
- ASAP = "ASAP",// every time the linked starter wins back the serve
3
+ ASAP = "ASAP",// unconditionally true (pinch server: every serve gain; libero rule: "Always")
3
4
  TEAM_SET_POINT = "TEAM_SET_POINT",// team within `margin` of winning the set
4
5
  OPPONENT_SET_POINT = "OPPONENT_SET_POINT",// opponent within `margin` of winning the set
5
6
  SCORE_DIFF = "SCORE_DIFF",// team trailing/leading by at least `points`
@@ -7,7 +8,8 @@ export declare enum PinchConditionType {
7
8
  FROM_SET = "FROM_SET",// only set N or later
8
9
  MIN_OWN_SCORE = "MIN_OWN_SCORE",// only once own score >= N
9
10
  MIN_OPPONENT_SCORE = "MIN_OPPONENT_SCORE",// only once opponent score >= N
10
- PHASE = "PHASE"
11
+ PHASE = "PHASE",// the team is serving or receiving this rally (used by the libero deployment rule)
12
+ FATIGUE = "FATIGUE"
11
13
  }
12
14
  export declare enum MatchPhase {
13
15
  SERVING = "SERVING",
@@ -66,7 +68,11 @@ export interface PhaseCondition {
66
68
  readonly type: PinchConditionType.PHASE;
67
69
  readonly phase: MatchPhase;
68
70
  }
69
- export type PinchCondition = AsapCondition | TeamSetPointCondition | OpponentSetPointCondition | ScoreDiffCondition | AfterRotationsCondition | FromSetCondition | MinOwnScoreCondition | MinOpponentScoreCondition | PhaseCondition;
71
+ export interface FatigueCondition {
72
+ readonly type: PinchConditionType.FATIGUE;
73
+ readonly band?: EnergyBand;
74
+ }
75
+ export type PinchCondition = AsapCondition | TeamSetPointCondition | OpponentSetPointCondition | ScoreDiffCondition | AfterRotationsCondition | FromSetCondition | MinOwnScoreCondition | MinOpponentScoreCondition | PhaseCondition | FatigueCondition;
70
76
  export type ConditionLogic = 'ALL' | 'ANY';
71
77
  export interface PinchSetState {
72
78
  readonly ownScore: number;
@@ -75,4 +81,6 @@ export interface PinchSetState {
75
81
  readonly setNumber: number;
76
82
  readonly ownServeTurns: number;
77
83
  readonly phase: MatchPhase;
84
+ readonly subjectBand?: EnergyBand;
85
+ readonly defaultBand?: SubBand;
78
86
  }
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
- // Conditions that gate when a pinch server is forced in to serve (in for its linked starter when the team
3
- // wins back the serve, out the moment the serve is lost). Each condition is evaluated against the live set
4
- // state; a pinch entry combines them with ALL (every condition must hold) or ANY (one is enough).
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.Comparison = exports.ScoreDirection = exports.OpponentRelation = exports.MatchPhase = exports.PinchConditionType = void 0;
4
+ // The ONE condition vocabulary of the substitution engine (owner 2026-07-11): designated substitutions,
5
+ // pinch servers, sub-backs, and the libero deployment rule all evaluate the same catalog against the live
6
+ // set state, combined with ALL (every condition must hold) or ANY (one is enough). A config with NO
7
+ // conditions never fires.
7
8
  var PinchConditionType;
8
9
  (function (PinchConditionType) {
9
10
  PinchConditionType["ASAP"] = "ASAP";
@@ -14,7 +15,11 @@ var PinchConditionType;
14
15
  PinchConditionType["FROM_SET"] = "FROM_SET";
15
16
  PinchConditionType["MIN_OWN_SCORE"] = "MIN_OWN_SCORE";
16
17
  PinchConditionType["MIN_OPPONENT_SCORE"] = "MIN_OPPONENT_SCORE";
17
- PinchConditionType["PHASE"] = "PHASE"; // the team is serving or receiving this rally (used by the libero deployment rule)
18
+ PinchConditionType["PHASE"] = "PHASE";
19
+ // The SUBJECT player's energy is at-or-worse than `band` (absent band = the team `substitutionBand`). The
20
+ // subject comes from the evaluation context, never from the condition: sub-in conditions judge the STARTER
21
+ // on court, sub-back conditions judge the SUBSTITUTE on court, the libero rule judges the STARTING libero.
22
+ PinchConditionType["FATIGUE"] = "FATIGUE";
18
23
  })(PinchConditionType || (exports.PinchConditionType = PinchConditionType = {}));
19
24
  // Which side of the rally the team is on. Known at each dead ball: the serving team is SERVING, the other
20
25
  // RECEIVING. Consumed by the PHASE condition (libero deployment); pinch servers always evaluate while SERVING.