volleyballsimtypes 0.0.475 → 0.0.477

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cjs/src/api/index.d.ts +9 -1
  2. package/dist/cjs/src/data/models/auth-session.d.ts +5 -1
  3. package/dist/cjs/src/data/models/auth-session.js +8 -0
  4. package/dist/cjs/src/data/models/tactics.d.ts +9 -1
  5. package/dist/cjs/src/data/transformers/tactics.js +50 -11
  6. package/dist/cjs/src/data/transformers/tactics.test.js +67 -0
  7. package/dist/cjs/src/service/team/designated-sub.d.ts +5 -0
  8. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +109 -0
  9. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +15 -3
  10. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +73 -0
  11. package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +11 -2
  12. package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +4 -4
  13. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +72 -2
  14. package/dist/cjs/src/service/team/schemas/tactics.z.js +5 -4
  15. package/dist/cjs/src/service/team/schemas/team.z.d.ts +72 -2
  16. package/dist/esm/src/api/index.d.ts +9 -1
  17. package/dist/esm/src/data/models/auth-session.d.ts +5 -1
  18. package/dist/esm/src/data/models/auth-session.js +8 -0
  19. package/dist/esm/src/data/models/tactics.d.ts +9 -1
  20. package/dist/esm/src/data/transformers/tactics.js +50 -11
  21. package/dist/esm/src/data/transformers/tactics.test.js +67 -0
  22. package/dist/esm/src/service/team/designated-sub.d.ts +5 -0
  23. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +109 -0
  24. package/dist/esm/src/service/team/schemas/designated-sub.z.js +14 -2
  25. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +73 -0
  26. package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +11 -2
  27. package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +4 -4
  28. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +72 -2
  29. package/dist/esm/src/service/team/schemas/tactics.z.js +5 -4
  30. package/dist/esm/src/service/team/schemas/team.z.d.ts +72 -2
  31. 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, RotationSystemEnum, 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, LiberoSubMode, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../service';
2
2
  export type Rally = DataProps<_Rally> & {
3
3
  homePlayerPosition: PlayerPosition[];
4
4
  awayPlayerPosition: PlayerPosition[];
@@ -87,6 +87,10 @@ export interface ApiSetSubConfig {
87
87
  benchFatigueBand?: EnergyBand;
88
88
  conditions?: PinchCondition[];
89
89
  conditionLogic?: ConditionLogic;
90
+ pinchServer?: boolean;
91
+ subBackMode?: SubBackMode;
92
+ subBackConditions?: PinchCondition[];
93
+ subBackConditionLogic?: ConditionLogic;
90
94
  }
91
95
  export interface ApiDesignatedSub {
92
96
  starterId: string;
@@ -96,6 +100,10 @@ export interface ApiDesignatedSub {
96
100
  benchFatigueBand?: EnergyBand;
97
101
  conditions?: PinchCondition[];
98
102
  conditionLogic?: ConditionLogic;
103
+ pinchServer?: boolean;
104
+ subBackMode?: SubBackMode;
105
+ subBackConditions?: PinchCondition[];
106
+ subBackConditionLogic?: ConditionLogic;
99
107
  sets?: ApiSetSubConfig[];
100
108
  }
101
109
  export interface ApiOffensivePreference {
@@ -11,10 +11,12 @@ export interface AuthSessionAttributes {
11
11
  user_agent?: string | null;
12
12
  created_at?: Date;
13
13
  updated_at?: Date;
14
+ previous_refresh_token_hash?: string | null;
15
+ previous_token_expires_at?: Date | null;
14
16
  }
15
17
  export type AuthSessionPk = 'session_id';
16
18
  export type AuthSessionId = AuthSessionModel[AuthSessionPk];
17
- export type AuthSessionOptionalAttributes = 'revoked_at' | 'ip_address' | 'user_agent' | 'created_at' | 'updated_at';
19
+ export type AuthSessionOptionalAttributes = 'revoked_at' | 'ip_address' | 'user_agent' | 'created_at' | 'updated_at' | 'previous_refresh_token_hash' | 'previous_token_expires_at';
18
20
  export type AuthSessionCreationAttributes = Optional<AuthSessionAttributes, AuthSessionOptionalAttributes>;
19
21
  export declare class AuthSessionModel extends Model<AuthSessionAttributes, AuthSessionCreationAttributes> implements AuthSessionAttributes {
20
22
  session_id: string;
@@ -26,6 +28,8 @@ export declare class AuthSessionModel extends Model<AuthSessionAttributes, AuthS
26
28
  user_agent?: string | null;
27
29
  created_at?: Date;
28
30
  updated_at?: Date;
31
+ previous_refresh_token_hash?: string | null;
32
+ previous_token_expires_at?: Date | null;
29
33
  user: AuthUserModel;
30
34
  getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
31
35
  setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
@@ -47,6 +47,14 @@ class AuthSessionModel extends sequelize_1.Model {
47
47
  type: sequelize_1.DataTypes.DATE,
48
48
  allowNull: false,
49
49
  defaultValue: sequelize_1.DataTypes.NOW
50
+ },
51
+ previous_refresh_token_hash: {
52
+ type: sequelize_1.DataTypes.STRING,
53
+ allowNull: true
54
+ },
55
+ previous_token_expires_at: {
56
+ type: sequelize_1.DataTypes.DATE,
57
+ allowNull: true
50
58
  }
51
59
  }, {
52
60
  sequelize,
@@ -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, RotationSystemEnum, SubBand, SubMode } from '../../service';
4
+ import { ConditionLogic, CourtPosition, EnergyBand, LiberoSubMode, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -19,6 +19,10 @@ export interface SetSubConfigAttributes {
19
19
  benchFatigueBand?: EnergyBand;
20
20
  conditions?: PinchCondition[];
21
21
  conditionLogic?: ConditionLogic;
22
+ pinchServer?: boolean;
23
+ subBackMode?: SubBackMode;
24
+ subBackConditions?: PinchCondition[];
25
+ subBackConditionLogic?: ConditionLogic;
22
26
  }
23
27
  export interface DesignatedSubAttributes {
24
28
  starterId: PlayerId;
@@ -29,6 +33,10 @@ export interface DesignatedSubAttributes {
29
33
  benchFatigueBand?: EnergyBand;
30
34
  conditions?: PinchCondition[];
31
35
  conditionLogic?: ConditionLogic;
36
+ pinchServer?: boolean;
37
+ subBackMode?: SubBackMode;
38
+ subBackConditions?: PinchCondition[];
39
+ subBackConditionLogic?: ConditionLogic;
32
40
  sets?: SetSubConfigAttributes[];
33
41
  }
34
42
  export interface OffensivePreferenceAttributes {
@@ -17,6 +17,12 @@ function subModeFromAttributes(d) {
17
17
  return 'NEVER';
18
18
  return 'FATIGUE';
19
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
+ }
20
26
  function findPlayer(id, roster) {
21
27
  const player = roster.find((p) => p.id === id);
22
28
  if (player == null)
@@ -65,13 +71,21 @@ function transformToAttributes(tactics, teamId) {
65
71
  benchFatigueBand: ds.benchFatigueBand,
66
72
  conditions: ds.conditions,
67
73
  conditionLogic: ds.conditionLogic,
74
+ pinchServer: ds.pinchServer,
75
+ subBackMode: ds.subBackMode,
76
+ subBackConditions: ds.subBackConditions,
77
+ subBackConditionLogic: ds.subBackConditionLogic,
68
78
  sets: ds.sets?.map(s => ({
69
79
  mode: s.mode,
70
80
  benchId: s.bench?.id,
71
81
  fatigueBand: s.fatigueBand,
72
82
  benchFatigueBand: s.benchFatigueBand,
73
83
  conditions: s.conditions,
74
- conditionLogic: s.conditionLogic
84
+ conditionLogic: s.conditionLogic,
85
+ pinchServer: s.pinchServer,
86
+ subBackMode: s.subBackMode,
87
+ subBackConditions: s.subBackConditions,
88
+ subBackConditionLogic: s.subBackConditionLogic
75
89
  }))
76
90
  })),
77
91
  rotation_system: tactics.rotationSystem,
@@ -155,13 +169,21 @@ function transformToObject(model, roster) {
155
169
  benchFatigueBand: d.benchFatigueBand,
156
170
  conditions: d.conditions,
157
171
  conditionLogic: d.conditionLogic,
172
+ pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
173
+ subBackMode: d.subBackMode,
174
+ subBackConditions: d.subBackConditions,
175
+ subBackConditionLogic: d.subBackConditionLogic,
158
176
  sets: d.sets?.map(s => ({
159
177
  mode: s.mode,
160
178
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
161
179
  fatigueBand: s.fatigueBand,
162
180
  benchFatigueBand: s.benchFatigueBand,
163
181
  conditions: s.conditions,
164
- conditionLogic: s.conditionLogic
182
+ conditionLogic: s.conditionLogic,
183
+ pinchServer: effectivePinchServer(s.mode, s.pinchServer),
184
+ subBackMode: s.subBackMode,
185
+ subBackConditions: s.subBackConditions,
186
+ subBackConditionLogic: s.subBackConditionLogic
165
187
  }))
166
188
  })),
167
189
  rotationSystem: model.rotation_system,
@@ -178,15 +200,20 @@ function transformToObject(model, roster) {
178
200
  rotation: p.rotation,
179
201
  order: p.order.map((id) => findPlayer(id, roster))
180
202
  }))),
181
- // LENIENT: keep only starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
182
- // value) can never fail schema validation and crash a team load; a dropped key = the ON default.
203
+ // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
204
+ // libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
205
+ // key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
206
+ // and crash a team load; a dropped key = the ON default.
183
207
  replaceKnockedImmediately: (() => {
184
- const starterIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
208
+ const allowedIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
185
209
  lineup[service_1.CourtPosition.LEFT_BACK], lineup[service_1.CourtPosition.MIDDLE_BACK], lineup[service_1.CourtPosition.RIGHT_BACK]]
186
210
  .map((p) => p.id));
211
+ const liberoZoneId = lineup[service_1.CourtPosition.LIBERO_ZONE]?.id;
212
+ if (liberoZoneId != null && secondLibero != null)
213
+ allowedIds.add(liberoZoneId);
187
214
  const healed = {};
188
215
  for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
189
- if (starterIds.has(id) && typeof value === 'boolean')
216
+ if (allowedIds.has(id) && typeof value === 'boolean')
190
217
  healed[id] = value;
191
218
  }
192
219
  return healed;
@@ -307,13 +334,21 @@ function tacticsColumnsToApiDto(model) {
307
334
  benchFatigueBand: d.benchFatigueBand,
308
335
  conditions: d.conditions,
309
336
  conditionLogic: d.conditionLogic,
337
+ pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
338
+ subBackMode: d.subBackMode,
339
+ subBackConditions: d.subBackConditions,
340
+ subBackConditionLogic: d.subBackConditionLogic,
310
341
  sets: d.sets?.map(s => ({
311
342
  mode: s.mode,
312
343
  benchId: s.benchId,
313
344
  fatigueBand: s.fatigueBand,
314
345
  benchFatigueBand: s.benchFatigueBand,
315
346
  conditions: s.conditions,
316
- conditionLogic: s.conditionLogic
347
+ conditionLogic: s.conditionLogic,
348
+ pinchServer: effectivePinchServer(s.mode, s.pinchServer),
349
+ subBackMode: s.subBackMode,
350
+ subBackConditions: s.subBackConditions,
351
+ subBackConditionLogic: s.subBackConditionLogic
317
352
  }))
318
353
  })),
319
354
  rotationSystem: model.rotation_system,
@@ -321,15 +356,19 @@ function tacticsColumnsToApiDto(model) {
321
356
  offensivePreferences: model.offensive_preferences,
322
357
  systemSets: model.system_sets,
323
358
  offensivePreferenceSets: model.offensive_preference_sets,
324
- // Same stale-data policy as the heals below: only starter-keyed boolean entries survive, and an empty
325
- // map surfaces as absent (missing key = ON).
359
+ // Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
360
+ // libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
361
+ // survive; an empty map surfaces as absent (missing key = ON).
326
362
  replaceKnockedImmediately: (() => {
327
- const starterSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
363
+ const allowedSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
328
364
  model.lineup[service_1.CourtPosition.RIGHT_FRONT], model.lineup[service_1.CourtPosition.LEFT_BACK],
329
365
  model.lineup[service_1.CourtPosition.MIDDLE_BACK], model.lineup[service_1.CourtPosition.RIGHT_BACK]]);
366
+ const liberoZoneId = model.lineup[service_1.CourtPosition.LIBERO_ZONE];
367
+ if (liberoZoneId != null && validSecondLibero != null)
368
+ allowedSlotIds.add(liberoZoneId);
330
369
  const healed = {};
331
370
  for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
332
- if (starterSlotIds.has(id) && typeof value === 'boolean')
371
+ if (allowedSlotIds.has(id) && typeof value === 'boolean')
333
372
  healed[id] = value;
334
373
  }
335
374
  return Object.keys(healed).length > 0 ? healed : undefined;
@@ -60,6 +60,37 @@ function makeTactics(designatedSubs) {
60
60
  (0, globals_1.expect)(dto.designatedSubs[0].fatigueBand).toBe(service_1.EnergyBand.WORN);
61
61
  (0, globals_1.expect)(dto.designatedSubs[0].benchFatigueBand).toBe(service_1.EnergyBand.TIRED);
62
62
  });
63
+ (0, globals_1.it)('defaults a legacy PINCH row (no pinchServer flag) to pinchServer true', () => {
64
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
65
+ { starterId: 's1', benchId: 'b1', mode: 'PINCH', conditions: [{ type: 'ASAP' }] }
66
+ ]));
67
+ (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBe(true);
68
+ });
69
+ (0, globals_1.it)('leaves pinchServer undefined for a non-PINCH row', () => {
70
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
71
+ { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: service_1.EnergyBand.TIRED }
72
+ ]));
73
+ (0, globals_1.expect)(dto.designatedSubs[0].pinchServer).toBeUndefined();
74
+ });
75
+ (0, globals_1.it)('passes a conditional regular sub (pinchServer false + condition sub-back) through unchanged', () => {
76
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
77
+ {
78
+ starterId: 's1',
79
+ benchId: 'b1',
80
+ mode: 'PINCH',
81
+ conditions: [{ type: 'SCORE_DIFF', direction: 'TRAILING', points: 3 }],
82
+ pinchServer: false,
83
+ subBackMode: 'CONDITIONS',
84
+ subBackConditions: [{ type: 'TEAM_SET_POINT', margin: 2, opponent: 'EITHER' }],
85
+ subBackConditionLogic: 'ANY'
86
+ }
87
+ ]));
88
+ const ds = dto.designatedSubs[0];
89
+ (0, globals_1.expect)(ds.pinchServer).toBe(false);
90
+ (0, globals_1.expect)(ds.subBackMode).toBe('CONDITIONS');
91
+ (0, globals_1.expect)(ds.subBackConditions).toEqual([{ type: 'TEAM_SET_POINT', margin: 2, opponent: 'EITHER' }]);
92
+ (0, globals_1.expect)(ds.subBackConditionLogic).toBe('ANY');
93
+ });
63
94
  (0, globals_1.it)('drops a second libero that collides with the starting libero (not a reserve)', () => {
64
95
  const model = makeTactics([]);
65
96
  model.lineup[service_1.CourtPosition.LIBERO_ZONE] = 'libero1';
@@ -78,6 +109,21 @@ function makeTactics(designatedSubs) {
78
109
  (0, globals_1.expect)(dto.secondLibero).toBe('reserve1');
79
110
  (0, globals_1.expect)(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
80
111
  });
112
+ (0, globals_1.it)('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
113
+ const model = makeTactics([]);
114
+ model.lineup[service_1.CourtPosition.LIBERO_ZONE] = 'libero1';
115
+ model.second_libero = 'reserve1';
116
+ model.replace_knocked_immediately = { libero1: false, lf: false };
117
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
118
+ (0, globals_1.expect)(dto.replaceKnockedImmediately).toEqual({ libero1: false, lf: false });
119
+ });
120
+ (0, globals_1.it)('drops a libero-keyed replaceKnockedImmediately entry when there is no second libero', () => {
121
+ const model = makeTactics([]);
122
+ model.lineup[service_1.CourtPosition.LIBERO_ZONE] = 'libero1';
123
+ model.replace_knocked_immediately = { libero1: false, lf: false };
124
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
125
+ (0, globals_1.expect)(dto.replaceKnockedImmediately).toEqual({ lf: false });
126
+ });
81
127
  (0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
82
128
  const model = makeTactics([]);
83
129
  model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
@@ -182,4 +228,25 @@ function makeTactics(designatedSubs) {
182
228
  (0, globals_1.expect)(back.liberoInjury?.secondLiberoBackfill).toBeUndefined();
183
229
  (0, globals_1.expect)(back.liberoInjury?.inMatchDesignate?.id).toBe(reserveB.id);
184
230
  });
231
+ (0, globals_1.it)('keeps a libero-keyed replaceKnockedImmediately entry on load when an L2 exists', () => {
232
+ const withKnock = service_1.Tactics.create({
233
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
234
+ liberoReplacements: [],
235
+ secondLibero: l2,
236
+ replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
237
+ });
238
+ const attrs = (0, tactics_1.transformFromTactics)(withKnock, 'team-1');
239
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
240
+ (0, globals_1.expect)(back.replaceKnockedImmediately).toEqual({ [libero.id]: false, [p1.id]: false });
241
+ });
242
+ (0, globals_1.it)('drops a libero-keyed replaceKnockedImmediately entry on load when there is no L2', () => {
243
+ const withKnock = service_1.Tactics.create({
244
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
245
+ liberoReplacements: [],
246
+ replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
247
+ });
248
+ const attrs = (0, tactics_1.transformFromTactics)(withKnock, 'team-1');
249
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
250
+ (0, globals_1.expect)(back.replaceKnockedImmediately).toEqual({ [p1.id]: false });
251
+ });
185
252
  });
@@ -1,6 +1,7 @@
1
1
  import { Player } from '../player';
2
2
  import { EnergyBand, SubMode } from './energy-band';
3
3
  import { ConditionLogic, PinchCondition } from './pinch-condition';
4
+ export type SubBackMode = 'FATIGUE' | 'CONDITIONS';
4
5
  export interface SetSubConfig {
5
6
  readonly mode: SubMode;
6
7
  readonly bench?: Player;
@@ -8,6 +9,10 @@ export interface SetSubConfig {
8
9
  readonly benchFatigueBand?: EnergyBand;
9
10
  readonly conditions?: PinchCondition[];
10
11
  readonly conditionLogic?: ConditionLogic;
12
+ readonly pinchServer?: boolean;
13
+ readonly subBackMode?: SubBackMode;
14
+ readonly subBackConditions?: PinchCondition[];
15
+ readonly subBackConditionLogic?: ConditionLogic;
11
16
  }
12
17
  export interface DesignatedSub extends SetSubConfig {
13
18
  readonly starter: Player;
@@ -38,6 +38,10 @@ export declare const SubModeSchema: z.ZodEnum<{
38
38
  FATIGUE: "FATIGUE";
39
39
  PINCH: "PINCH";
40
40
  }>;
41
+ export declare const SubBackModeSchema: z.ZodEnum<{
42
+ FATIGUE: "FATIGUE";
43
+ CONDITIONS: "CONDITIONS";
44
+ }>;
41
45
  export declare const SetSubConfigSchema: z.ZodObject<{
42
46
  mode: z.ZodEnum<{
43
47
  NEVER: "NEVER";
@@ -77,6 +81,41 @@ export declare const SetSubConfigSchema: z.ZodObject<{
77
81
  phase: z.ZodEnum<typeof MatchPhase>;
78
82
  }, z.core.$strip>], "type">>>;
79
83
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
84
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
85
+ subBackMode: z.ZodOptional<z.ZodEnum<{
86
+ FATIGUE: "FATIGUE";
87
+ CONDITIONS: "CONDITIONS";
88
+ }>>;
89
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
90
+ type: z.ZodLiteral<PinchConditionType.ASAP>;
91
+ }, z.core.$strip>, z.ZodObject<{
92
+ type: z.ZodLiteral<PinchConditionType.TEAM_SET_POINT>;
93
+ margin: z.ZodNumber;
94
+ opponent: z.ZodEnum<typeof OpponentRelation>;
95
+ }, z.core.$strip>, z.ZodObject<{
96
+ type: z.ZodLiteral<PinchConditionType.OPPONENT_SET_POINT>;
97
+ margin: z.ZodNumber;
98
+ }, z.core.$strip>, z.ZodObject<{
99
+ type: z.ZodLiteral<PinchConditionType.SCORE_DIFF>;
100
+ direction: z.ZodEnum<typeof ScoreDirection>;
101
+ points: z.ZodNumber;
102
+ }, z.core.$strip>, z.ZodObject<{
103
+ type: z.ZodLiteral<PinchConditionType.AFTER_ROTATIONS>;
104
+ rotations: z.ZodNumber;
105
+ }, z.core.$strip>, z.ZodObject<{
106
+ type: z.ZodLiteral<PinchConditionType.FROM_SET>;
107
+ set: z.ZodNumber;
108
+ }, z.core.$strip>, z.ZodObject<{
109
+ type: z.ZodLiteral<PinchConditionType.MIN_OWN_SCORE>;
110
+ score: z.ZodNumber;
111
+ }, z.core.$strip>, z.ZodObject<{
112
+ type: z.ZodLiteral<PinchConditionType.MIN_OPPONENT_SCORE>;
113
+ score: z.ZodNumber;
114
+ }, z.core.$strip>, z.ZodObject<{
115
+ type: z.ZodLiteral<PinchConditionType.PHASE>;
116
+ phase: z.ZodEnum<typeof MatchPhase>;
117
+ }, z.core.$strip>], "type">>>;
118
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
80
119
  }, z.core.$strip>;
81
120
  export declare const DesignatedSubSchema: z.ZodObject<{
82
121
  sets: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -118,6 +157,41 @@ export declare const DesignatedSubSchema: z.ZodObject<{
118
157
  phase: z.ZodEnum<typeof MatchPhase>;
119
158
  }, z.core.$strip>], "type">>>;
120
159
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
160
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
161
+ subBackMode: z.ZodOptional<z.ZodEnum<{
162
+ FATIGUE: "FATIGUE";
163
+ CONDITIONS: "CONDITIONS";
164
+ }>>;
165
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
166
+ type: z.ZodLiteral<PinchConditionType.ASAP>;
167
+ }, z.core.$strip>, z.ZodObject<{
168
+ type: z.ZodLiteral<PinchConditionType.TEAM_SET_POINT>;
169
+ margin: z.ZodNumber;
170
+ opponent: z.ZodEnum<typeof OpponentRelation>;
171
+ }, z.core.$strip>, z.ZodObject<{
172
+ type: z.ZodLiteral<PinchConditionType.OPPONENT_SET_POINT>;
173
+ margin: z.ZodNumber;
174
+ }, z.core.$strip>, z.ZodObject<{
175
+ type: z.ZodLiteral<PinchConditionType.SCORE_DIFF>;
176
+ direction: z.ZodEnum<typeof ScoreDirection>;
177
+ points: z.ZodNumber;
178
+ }, z.core.$strip>, z.ZodObject<{
179
+ type: z.ZodLiteral<PinchConditionType.AFTER_ROTATIONS>;
180
+ rotations: z.ZodNumber;
181
+ }, z.core.$strip>, z.ZodObject<{
182
+ type: z.ZodLiteral<PinchConditionType.FROM_SET>;
183
+ set: z.ZodNumber;
184
+ }, z.core.$strip>, z.ZodObject<{
185
+ type: z.ZodLiteral<PinchConditionType.MIN_OWN_SCORE>;
186
+ score: z.ZodNumber;
187
+ }, z.core.$strip>, z.ZodObject<{
188
+ type: z.ZodLiteral<PinchConditionType.MIN_OPPONENT_SCORE>;
189
+ score: z.ZodNumber;
190
+ }, z.core.$strip>, z.ZodObject<{
191
+ type: z.ZodLiteral<PinchConditionType.PHASE>;
192
+ phase: z.ZodEnum<typeof MatchPhase>;
193
+ }, z.core.$strip>], "type">>>;
194
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
121
195
  }, z.core.$strip>>>;
122
196
  mode: z.ZodEnum<{
123
197
  NEVER: "NEVER";
@@ -157,5 +231,40 @@ export declare const DesignatedSubSchema: z.ZodObject<{
157
231
  phase: z.ZodEnum<typeof MatchPhase>;
158
232
  }, z.core.$strip>], "type">>>;
159
233
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
234
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
235
+ subBackMode: z.ZodOptional<z.ZodEnum<{
236
+ FATIGUE: "FATIGUE";
237
+ CONDITIONS: "CONDITIONS";
238
+ }>>;
239
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
240
+ type: z.ZodLiteral<PinchConditionType.ASAP>;
241
+ }, z.core.$strip>, z.ZodObject<{
242
+ type: z.ZodLiteral<PinchConditionType.TEAM_SET_POINT>;
243
+ margin: z.ZodNumber;
244
+ opponent: z.ZodEnum<typeof OpponentRelation>;
245
+ }, z.core.$strip>, z.ZodObject<{
246
+ type: z.ZodLiteral<PinchConditionType.OPPONENT_SET_POINT>;
247
+ margin: z.ZodNumber;
248
+ }, z.core.$strip>, z.ZodObject<{
249
+ type: z.ZodLiteral<PinchConditionType.SCORE_DIFF>;
250
+ direction: z.ZodEnum<typeof ScoreDirection>;
251
+ points: z.ZodNumber;
252
+ }, z.core.$strip>, z.ZodObject<{
253
+ type: z.ZodLiteral<PinchConditionType.AFTER_ROTATIONS>;
254
+ rotations: z.ZodNumber;
255
+ }, z.core.$strip>, z.ZodObject<{
256
+ type: z.ZodLiteral<PinchConditionType.FROM_SET>;
257
+ set: z.ZodNumber;
258
+ }, z.core.$strip>, z.ZodObject<{
259
+ type: z.ZodLiteral<PinchConditionType.MIN_OWN_SCORE>;
260
+ score: z.ZodNumber;
261
+ }, z.core.$strip>, z.ZodObject<{
262
+ type: z.ZodLiteral<PinchConditionType.MIN_OPPONENT_SCORE>;
263
+ score: z.ZodNumber;
264
+ }, z.core.$strip>, z.ZodObject<{
265
+ type: z.ZodLiteral<PinchConditionType.PHASE>;
266
+ phase: z.ZodEnum<typeof MatchPhase>;
267
+ }, z.core.$strip>], "type">>>;
268
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
160
269
  starter: z.ZodCustom<Player, Player>;
161
270
  }, z.core.$strip>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DesignatedSubSchema = exports.SetSubConfigSchema = exports.SubModeSchema = exports.ConditionLogicSchema = exports.PinchConditionSchema = exports.SubBandSchema = void 0;
3
+ exports.DesignatedSubSchema = exports.SetSubConfigSchema = exports.SubBackModeSchema = exports.SubModeSchema = exports.ConditionLogicSchema = exports.PinchConditionSchema = exports.SubBandSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
6
  const energy_band_1 = require("../energy-band");
@@ -23,6 +23,7 @@ exports.PinchConditionSchema = zod_1.z.discriminatedUnion('type', [
23
23
  ]);
24
24
  exports.ConditionLogicSchema = zod_1.z.union([zod_1.z.literal('ALL'), zod_1.z.literal('ANY')]);
25
25
  exports.SubModeSchema = zod_1.z.enum(['FATIGUE', 'PINCH', 'NEVER']);
26
+ exports.SubBackModeSchema = zod_1.z.enum(['FATIGUE', 'CONDITIONS']);
26
27
  // Fields shared by the all-sets config and each per-set config.
27
28
  const setSubConfigShape = {
28
29
  mode: exports.SubModeSchema,
@@ -30,9 +31,16 @@ const setSubConfigShape = {
30
31
  fatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
31
32
  benchFatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
32
33
  conditions: zod_1.z.array(exports.PinchConditionSchema).optional(),
33
- conditionLogic: exports.ConditionLogicSchema.optional()
34
+ conditionLogic: exports.ConditionLogicSchema.optional(),
35
+ // pinch ("Conditions") mode. true = serve-and-return pinch server; false/absent = regular conditional sub.
36
+ pinchServer: zod_1.z.boolean().optional(),
37
+ // a regular conditional sub's sub-back trigger + its condition set (subBackMode === 'CONDITIONS').
38
+ subBackMode: exports.SubBackModeSchema.optional(),
39
+ subBackConditions: zod_1.z.array(exports.PinchConditionSchema).optional(),
40
+ subBackConditionLogic: exports.ConditionLogicSchema.optional()
34
41
  };
35
- // Pinch mode needs a designated bench player (the server) and at least one condition.
42
+ // Pinch ("Conditions") mode needs a designated bench player and at least one sub-in condition. A regular
43
+ // conditional sub (pinchServer !== true) with a CONDITIONS sub-back also needs at least one sub-back condition.
36
44
  function refinePinch(data, ctx) {
37
45
  if (data.mode !== 'PINCH')
38
46
  return;
@@ -42,6 +50,10 @@ function refinePinch(data, ctx) {
42
50
  if (data.conditions == null || data.conditions.length === 0) {
43
51
  ctx.addIssue({ code: 'custom', message: 'PINCH_SERVER_REQUIRES_CONDITIONS', path: ['conditions'] });
44
52
  }
53
+ if (data.pinchServer !== true && data.subBackMode === 'CONDITIONS' &&
54
+ (data.subBackConditions == null || data.subBackConditions.length === 0)) {
55
+ ctx.addIssue({ code: 'custom', message: 'CONDITIONAL_SUB_BACK_REQUIRES_CONDITIONS', path: ['subBackConditions'] });
56
+ }
45
57
  }
46
58
  exports.SetSubConfigSchema = zod_1.z.object(setSubConfigShape).superRefine(refinePinch);
47
59
  exports.DesignatedSubSchema = zod_1.z.object({
@@ -218,4 +218,77 @@ const test_helpers_1 = require("../../test-helpers");
218
218
  });
219
219
  (0, globals_1.expect)(res.success).toBe(false);
220
220
  });
221
+ (0, globals_1.it)('accepts a pinch-mode entry marked as a pinch server (serve-and-return)', () => {
222
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
223
+ starter,
224
+ bench,
225
+ mode: 'PINCH',
226
+ conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }],
227
+ pinchServer: true
228
+ });
229
+ (0, globals_1.expect)(res.success).toBe(true);
230
+ });
231
+ (0, globals_1.it)('accepts a regular conditional sub (pinchServer false) with a tiredness sub-back', () => {
232
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
233
+ starter,
234
+ bench,
235
+ mode: 'PINCH',
236
+ conditions: [{ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 3 }],
237
+ pinchServer: false,
238
+ subBackMode: 'FATIGUE',
239
+ benchFatigueBand: energy_band_1.EnergyBand.TIRED
240
+ });
241
+ (0, globals_1.expect)(res.success).toBe(true);
242
+ });
243
+ (0, globals_1.it)('accepts a regular conditional sub with a condition-based sub-back', () => {
244
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
245
+ starter,
246
+ bench,
247
+ mode: 'PINCH',
248
+ conditions: [{ type: pinch_condition_1.PinchConditionType.SCORE_DIFF, direction: pinch_condition_1.ScoreDirection.TRAILING, points: 3 }],
249
+ pinchServer: false,
250
+ subBackMode: 'CONDITIONS',
251
+ subBackConditions: [{ type: pinch_condition_1.PinchConditionType.TEAM_SET_POINT, margin: 2, opponent: pinch_condition_1.OpponentRelation.EITHER }],
252
+ subBackConditionLogic: 'ANY'
253
+ });
254
+ (0, globals_1.expect)(res.success).toBe(true);
255
+ });
256
+ (0, globals_1.it)('rejects a condition-based sub-back with no sub-back conditions', () => {
257
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
258
+ starter,
259
+ bench,
260
+ mode: 'PINCH',
261
+ conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }],
262
+ pinchServer: false,
263
+ subBackMode: 'CONDITIONS',
264
+ subBackConditions: []
265
+ });
266
+ (0, globals_1.expect)(res.success).toBe(false);
267
+ if (!res.success) {
268
+ (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'CONDITIONAL_SUB_BACK_REQUIRES_CONDITIONS')).toBe(true);
269
+ }
270
+ });
271
+ (0, globals_1.it)('does not require sub-back conditions for a pinch server even under subBackMode CONDITIONS', () => {
272
+ // A pinch server never uses a sub-back rule (it serve-reverts), so the condition requirement is skipped.
273
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
274
+ starter,
275
+ bench,
276
+ mode: 'PINCH',
277
+ conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }],
278
+ pinchServer: true,
279
+ subBackMode: 'CONDITIONS'
280
+ });
281
+ (0, globals_1.expect)(res.success).toBe(true);
282
+ });
283
+ (0, globals_1.it)('rejects an invalid subBackMode', () => {
284
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
285
+ starter,
286
+ bench,
287
+ mode: 'PINCH',
288
+ conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }],
289
+ pinchServer: false,
290
+ subBackMode: 'WHENEVER'
291
+ });
292
+ (0, globals_1.expect)(res.success).toBe(false);
293
+ });
221
294
  });
@@ -49,8 +49,17 @@ const test_helpers_1 = require("../../test-helpers");
49
49
  (0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [p1.id]: false, [p3.id]: true });
50
50
  }
51
51
  });
52
- (0, globals_1.it)('rejects a replaceKnockedImmediately key that is not a court starter', () => {
53
- for (const outsider of [benchA, libero, reserveA]) {
52
+ (0, globals_1.it)('accepts a libero-keyed replaceKnockedImmediately map', () => {
53
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
54
+ replaceKnockedImmediately: { [libero.id]: false }
55
+ }));
56
+ (0, globals_1.expect)(res.success).toBe(true);
57
+ if (res.success) {
58
+ (0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [libero.id]: false });
59
+ }
60
+ });
61
+ (0, globals_1.it)('rejects a replaceKnockedImmediately key that is not a court starter or the libero', () => {
62
+ for (const outsider of [benchA, reserveA]) {
54
63
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
55
64
  replaceKnockedImmediately: { [outsider.id]: false }
56
65
  }));
@@ -3,15 +3,15 @@ import { EnergyBand } from '../energy-band';
3
3
  export declare const LiberoSubModeSchema: z.ZodEnum<{
4
4
  NEVER: "NEVER";
5
5
  FATIGUE: "FATIGUE";
6
- ALWAYS: "ALWAYS";
7
6
  CONDITIONS: "CONDITIONS";
7
+ ALWAYS: "ALWAYS";
8
8
  }>;
9
9
  export declare const LiberoSetSubConfigSchema: z.ZodObject<{
10
10
  mode: z.ZodEnum<{
11
11
  NEVER: "NEVER";
12
12
  FATIGUE: "FATIGUE";
13
- ALWAYS: "ALWAYS";
14
13
  CONDITIONS: "CONDITIONS";
14
+ ALWAYS: "ALWAYS";
15
15
  }>;
16
16
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
17
17
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -50,8 +50,8 @@ export declare const LiberoSubConfigSchema: z.ZodObject<{
50
50
  mode: z.ZodEnum<{
51
51
  NEVER: "NEVER";
52
52
  FATIGUE: "FATIGUE";
53
- ALWAYS: "ALWAYS";
54
53
  CONDITIONS: "CONDITIONS";
54
+ ALWAYS: "ALWAYS";
55
55
  }>;
56
56
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
57
57
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -88,8 +88,8 @@ export declare const LiberoSubConfigSchema: z.ZodObject<{
88
88
  mode: z.ZodEnum<{
89
89
  NEVER: "NEVER";
90
90
  FATIGUE: "FATIGUE";
91
- ALWAYS: "ALWAYS";
92
91
  CONDITIONS: "CONDITIONS";
92
+ ALWAYS: "ALWAYS";
93
93
  }>;
94
94
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
95
95
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{