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
@@ -55,6 +55,41 @@ export declare const TacticsInputSchema: z.ZodObject<{
55
55
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
56
56
  }, z.core.$strip>], "type">>>;
57
57
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
58
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
59
+ subBackMode: z.ZodOptional<z.ZodEnum<{
60
+ FATIGUE: "FATIGUE";
61
+ CONDITIONS: "CONDITIONS";
62
+ }>>;
63
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
64
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
65
+ }, z.core.$strip>, z.ZodObject<{
66
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
67
+ margin: z.ZodNumber;
68
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
69
+ }, z.core.$strip>, z.ZodObject<{
70
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
71
+ margin: z.ZodNumber;
72
+ }, z.core.$strip>, z.ZodObject<{
73
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
74
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
75
+ points: z.ZodNumber;
76
+ }, z.core.$strip>, z.ZodObject<{
77
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
78
+ rotations: z.ZodNumber;
79
+ }, z.core.$strip>, z.ZodObject<{
80
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
81
+ set: z.ZodNumber;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
84
+ score: z.ZodNumber;
85
+ }, z.core.$strip>, z.ZodObject<{
86
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OPPONENT_SCORE>;
87
+ score: z.ZodNumber;
88
+ }, z.core.$strip>, z.ZodObject<{
89
+ type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
90
+ phase: z.ZodEnum<typeof import("..").MatchPhase>;
91
+ }, z.core.$strip>], "type">>>;
92
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
58
93
  }, z.core.$strip>>>;
59
94
  mode: z.ZodEnum<{
60
95
  NEVER: "NEVER";
@@ -94,6 +129,41 @@ export declare const TacticsInputSchema: z.ZodObject<{
94
129
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
95
130
  }, z.core.$strip>], "type">>>;
96
131
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
132
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
133
+ subBackMode: z.ZodOptional<z.ZodEnum<{
134
+ FATIGUE: "FATIGUE";
135
+ CONDITIONS: "CONDITIONS";
136
+ }>>;
137
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
138
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
139
+ }, z.core.$strip>, z.ZodObject<{
140
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
141
+ margin: z.ZodNumber;
142
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
143
+ }, z.core.$strip>, z.ZodObject<{
144
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
145
+ margin: z.ZodNumber;
146
+ }, z.core.$strip>, z.ZodObject<{
147
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
148
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
149
+ points: z.ZodNumber;
150
+ }, z.core.$strip>, z.ZodObject<{
151
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
152
+ rotations: z.ZodNumber;
153
+ }, z.core.$strip>, z.ZodObject<{
154
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
155
+ set: z.ZodNumber;
156
+ }, z.core.$strip>, z.ZodObject<{
157
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
158
+ score: z.ZodNumber;
159
+ }, z.core.$strip>, z.ZodObject<{
160
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OPPONENT_SCORE>;
161
+ score: z.ZodNumber;
162
+ }, z.core.$strip>, z.ZodObject<{
163
+ type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
164
+ phase: z.ZodEnum<typeof import("..").MatchPhase>;
165
+ }, z.core.$strip>], "type">>>;
166
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
97
167
  starter: z.ZodCustom<Player, Player>;
98
168
  }, z.core.$strip>>>;
99
169
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
@@ -103,8 +173,8 @@ export declare const TacticsInputSchema: z.ZodObject<{
103
173
  mode: z.ZodEnum<{
104
174
  NEVER: "NEVER";
105
175
  FATIGUE: "FATIGUE";
106
- ALWAYS: "ALWAYS";
107
176
  CONDITIONS: "CONDITIONS";
177
+ ALWAYS: "ALWAYS";
108
178
  }>;
109
179
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
110
180
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -141,8 +211,8 @@ export declare const TacticsInputSchema: z.ZodObject<{
141
211
  mode: z.ZodEnum<{
142
212
  NEVER: "NEVER";
143
213
  FATIGUE: "FATIGUE";
144
- ALWAYS: "ALWAYS";
145
214
  CONDITIONS: "CONDITIONS";
215
+ ALWAYS: "ALWAYS";
146
216
  }>;
147
217
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
148
218
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -198,10 +198,11 @@ exports.TacticsInputSchema = zod_1.z.object({
198
198
  }
199
199
  }
200
200
  }
201
- // Per-starter knock toggle: every key must be a configured court starter (bench/reserve/libero players
202
- // follow the slot they occupy, so they never carry their own key).
203
- for (const starterId of Object.keys(data.replaceKnockedImmediately)) {
204
- if (!starterIds.has(starterId)) {
201
+ // Per-player knock toggle: every key must be a court starter OR the starting libero. The libero may carry its
202
+ // own key so the user can choose to keep a lightly knocked libero on court (owner 2026-07-06). Bench/reserve
203
+ // players still follow the slot they occupy, so they never carry their own key.
204
+ for (const knockedId of Object.keys(data.replaceKnockedImmediately)) {
205
+ if (!starterIds.has(knockedId) && knockedId !== libero?.id) {
205
206
  ctx.addIssue({ code: 'custom', message: 'REPLACE_KNOCKED_KEY_NOT_A_STARTER', path: ['replaceKnockedImmediately'] });
206
207
  }
207
208
  }
@@ -63,6 +63,41 @@ export declare const TeamInputSchema: z.ZodObject<{
63
63
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
64
64
  }, z.core.$strip>], "type">>>;
65
65
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
66
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
67
+ subBackMode: z.ZodOptional<z.ZodEnum<{
68
+ FATIGUE: "FATIGUE";
69
+ CONDITIONS: "CONDITIONS";
70
+ }>>;
71
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
72
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
73
+ }, z.core.$strip>, z.ZodObject<{
74
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
75
+ margin: z.ZodNumber;
76
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
77
+ }, z.core.$strip>, z.ZodObject<{
78
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
79
+ margin: z.ZodNumber;
80
+ }, z.core.$strip>, z.ZodObject<{
81
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
82
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
83
+ points: z.ZodNumber;
84
+ }, z.core.$strip>, z.ZodObject<{
85
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
86
+ rotations: z.ZodNumber;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
89
+ set: z.ZodNumber;
90
+ }, z.core.$strip>, z.ZodObject<{
91
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
92
+ score: z.ZodNumber;
93
+ }, z.core.$strip>, z.ZodObject<{
94
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OPPONENT_SCORE>;
95
+ score: z.ZodNumber;
96
+ }, z.core.$strip>, z.ZodObject<{
97
+ type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
98
+ phase: z.ZodEnum<typeof import("..").MatchPhase>;
99
+ }, z.core.$strip>], "type">>>;
100
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
66
101
  }, z.core.$strip>>>;
67
102
  mode: z.ZodEnum<{
68
103
  NEVER: "NEVER";
@@ -102,6 +137,41 @@ export declare const TeamInputSchema: z.ZodObject<{
102
137
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
103
138
  }, z.core.$strip>], "type">>>;
104
139
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
140
+ pinchServer: z.ZodOptional<z.ZodBoolean>;
141
+ subBackMode: z.ZodOptional<z.ZodEnum<{
142
+ FATIGUE: "FATIGUE";
143
+ CONDITIONS: "CONDITIONS";
144
+ }>>;
145
+ subBackConditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
146
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
147
+ }, z.core.$strip>, z.ZodObject<{
148
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
149
+ margin: z.ZodNumber;
150
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
151
+ }, z.core.$strip>, z.ZodObject<{
152
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
153
+ margin: z.ZodNumber;
154
+ }, z.core.$strip>, z.ZodObject<{
155
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
156
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
157
+ points: z.ZodNumber;
158
+ }, z.core.$strip>, z.ZodObject<{
159
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
160
+ rotations: z.ZodNumber;
161
+ }, z.core.$strip>, z.ZodObject<{
162
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
163
+ set: z.ZodNumber;
164
+ }, z.core.$strip>, z.ZodObject<{
165
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
166
+ score: z.ZodNumber;
167
+ }, z.core.$strip>, z.ZodObject<{
168
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OPPONENT_SCORE>;
169
+ score: z.ZodNumber;
170
+ }, z.core.$strip>, z.ZodObject<{
171
+ type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
172
+ phase: z.ZodEnum<typeof import("..").MatchPhase>;
173
+ }, z.core.$strip>], "type">>>;
174
+ subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
105
175
  starter: z.ZodCustom<Player, Player>;
106
176
  }, z.core.$strip>>>;
107
177
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof import("..").EnergyBand>, z.ZodLiteral<"NEVER">]>>;
@@ -111,8 +181,8 @@ export declare const TeamInputSchema: z.ZodObject<{
111
181
  mode: z.ZodEnum<{
112
182
  NEVER: "NEVER";
113
183
  FATIGUE: "FATIGUE";
114
- ALWAYS: "ALWAYS";
115
184
  CONDITIONS: "CONDITIONS";
185
+ ALWAYS: "ALWAYS";
116
186
  }>;
117
187
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
118
188
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -149,8 +219,8 @@ export declare const TeamInputSchema: z.ZodObject<{
149
219
  mode: z.ZodEnum<{
150
220
  NEVER: "NEVER";
151
221
  FATIGUE: "FATIGUE";
152
- ALWAYS: "ALWAYS";
153
222
  CONDITIONS: "CONDITIONS";
223
+ ALWAYS: "ALWAYS";
154
224
  }>;
155
225
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
156
226
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -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>;
@@ -44,6 +44,14 @@ export class AuthSessionModel extends Model {
44
44
  type: DataTypes.DATE,
45
45
  allowNull: false,
46
46
  defaultValue: DataTypes.NOW
47
+ },
48
+ previous_refresh_token_hash: {
49
+ type: DataTypes.STRING,
50
+ allowNull: true
51
+ },
52
+ previous_token_expires_at: {
53
+ type: DataTypes.DATE,
54
+ allowNull: true
47
55
  }
48
56
  }, {
49
57
  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 {
@@ -11,6 +11,12 @@ function subModeFromAttributes(d) {
11
11
  return 'NEVER';
12
12
  return 'FATIGUE';
13
13
  }
14
+ // pinchServer only applies to PINCH ("Conditions") mode. A LEGACY PINCH row (no flag) defaults to true so
15
+ // existing pinch servers keep serve-and-return behavior; a new conditional sub persists an explicit false. This
16
+ // default must resolve identically on every READ path (both transformToObject and tacticsColumnsToApiDto).
17
+ function effectivePinchServer(mode, pinchServer) {
18
+ return mode === 'PINCH' ? (pinchServer ?? true) : undefined;
19
+ }
14
20
  function findPlayer(id, roster) {
15
21
  const player = roster.find((p) => p.id === id);
16
22
  if (player == null)
@@ -59,13 +65,21 @@ function transformToAttributes(tactics, teamId) {
59
65
  benchFatigueBand: ds.benchFatigueBand,
60
66
  conditions: ds.conditions,
61
67
  conditionLogic: ds.conditionLogic,
68
+ pinchServer: ds.pinchServer,
69
+ subBackMode: ds.subBackMode,
70
+ subBackConditions: ds.subBackConditions,
71
+ subBackConditionLogic: ds.subBackConditionLogic,
62
72
  sets: ds.sets?.map(s => ({
63
73
  mode: s.mode,
64
74
  benchId: s.bench?.id,
65
75
  fatigueBand: s.fatigueBand,
66
76
  benchFatigueBand: s.benchFatigueBand,
67
77
  conditions: s.conditions,
68
- conditionLogic: s.conditionLogic
78
+ conditionLogic: s.conditionLogic,
79
+ pinchServer: s.pinchServer,
80
+ subBackMode: s.subBackMode,
81
+ subBackConditions: s.subBackConditions,
82
+ subBackConditionLogic: s.subBackConditionLogic
69
83
  }))
70
84
  })),
71
85
  rotation_system: tactics.rotationSystem,
@@ -149,13 +163,21 @@ function transformToObject(model, roster) {
149
163
  benchFatigueBand: d.benchFatigueBand,
150
164
  conditions: d.conditions,
151
165
  conditionLogic: d.conditionLogic,
166
+ pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
167
+ subBackMode: d.subBackMode,
168
+ subBackConditions: d.subBackConditions,
169
+ subBackConditionLogic: d.subBackConditionLogic,
152
170
  sets: d.sets?.map(s => ({
153
171
  mode: s.mode,
154
172
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
155
173
  fatigueBand: s.fatigueBand,
156
174
  benchFatigueBand: s.benchFatigueBand,
157
175
  conditions: s.conditions,
158
- conditionLogic: s.conditionLogic
176
+ conditionLogic: s.conditionLogic,
177
+ pinchServer: effectivePinchServer(s.mode, s.pinchServer),
178
+ subBackMode: s.subBackMode,
179
+ subBackConditions: s.subBackConditions,
180
+ subBackConditionLogic: s.subBackConditionLogic
159
181
  }))
160
182
  })),
161
183
  rotationSystem: model.rotation_system,
@@ -172,15 +194,20 @@ function transformToObject(model, roster) {
172
194
  rotation: p.rotation,
173
195
  order: p.order.map((id) => findPlayer(id, roster))
174
196
  }))),
175
- // LENIENT: keep only starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
176
- // value) can never fail schema validation and crash a team load; a dropped key = the ON default.
197
+ // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
198
+ // libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
199
+ // key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
200
+ // and crash a team load; a dropped key = the ON default.
177
201
  replaceKnockedImmediately: (() => {
178
- const starterIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
202
+ const allowedIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
179
203
  lineup[CourtPosition.LEFT_BACK], lineup[CourtPosition.MIDDLE_BACK], lineup[CourtPosition.RIGHT_BACK]]
180
204
  .map((p) => p.id));
205
+ const liberoZoneId = lineup[CourtPosition.LIBERO_ZONE]?.id;
206
+ if (liberoZoneId != null && secondLibero != null)
207
+ allowedIds.add(liberoZoneId);
181
208
  const healed = {};
182
209
  for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
183
- if (starterIds.has(id) && typeof value === 'boolean')
210
+ if (allowedIds.has(id) && typeof value === 'boolean')
184
211
  healed[id] = value;
185
212
  }
186
213
  return healed;
@@ -301,13 +328,21 @@ function tacticsColumnsToApiDto(model) {
301
328
  benchFatigueBand: d.benchFatigueBand,
302
329
  conditions: d.conditions,
303
330
  conditionLogic: d.conditionLogic,
331
+ pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
332
+ subBackMode: d.subBackMode,
333
+ subBackConditions: d.subBackConditions,
334
+ subBackConditionLogic: d.subBackConditionLogic,
304
335
  sets: d.sets?.map(s => ({
305
336
  mode: s.mode,
306
337
  benchId: s.benchId,
307
338
  fatigueBand: s.fatigueBand,
308
339
  benchFatigueBand: s.benchFatigueBand,
309
340
  conditions: s.conditions,
310
- conditionLogic: s.conditionLogic
341
+ conditionLogic: s.conditionLogic,
342
+ pinchServer: effectivePinchServer(s.mode, s.pinchServer),
343
+ subBackMode: s.subBackMode,
344
+ subBackConditions: s.subBackConditions,
345
+ subBackConditionLogic: s.subBackConditionLogic
311
346
  }))
312
347
  })),
313
348
  rotationSystem: model.rotation_system,
@@ -315,15 +350,19 @@ function tacticsColumnsToApiDto(model) {
315
350
  offensivePreferences: model.offensive_preferences,
316
351
  systemSets: model.system_sets,
317
352
  offensivePreferenceSets: model.offensive_preference_sets,
318
- // Same stale-data policy as the heals below: only starter-keyed boolean entries survive, and an empty
319
- // map surfaces as absent (missing key = ON).
353
+ // Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
354
+ // libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
355
+ // survive; an empty map surfaces as absent (missing key = ON).
320
356
  replaceKnockedImmediately: (() => {
321
- const starterSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
357
+ const allowedSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
322
358
  model.lineup[CourtPosition.RIGHT_FRONT], model.lineup[CourtPosition.LEFT_BACK],
323
359
  model.lineup[CourtPosition.MIDDLE_BACK], model.lineup[CourtPosition.RIGHT_BACK]]);
360
+ const liberoZoneId = model.lineup[CourtPosition.LIBERO_ZONE];
361
+ if (liberoZoneId != null && validSecondLibero != null)
362
+ allowedSlotIds.add(liberoZoneId);
324
363
  const healed = {};
325
364
  for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
326
- if (starterSlotIds.has(id) && typeof value === 'boolean')
365
+ if (allowedSlotIds.has(id) && typeof value === 'boolean')
327
366
  healed[id] = value;
328
367
  }
329
368
  return Object.keys(healed).length > 0 ? healed : undefined;
@@ -58,6 +58,37 @@ describe('tacticsColumnsToApiDto()', () => {
58
58
  expect(dto.designatedSubs[0].fatigueBand).toBe(EnergyBand.WORN);
59
59
  expect(dto.designatedSubs[0].benchFatigueBand).toBe(EnergyBand.TIRED);
60
60
  });
61
+ it('defaults a legacy PINCH row (no pinchServer flag) to pinchServer true', () => {
62
+ const dto = tacticsColumnsToApiDto(makeTactics([
63
+ { starterId: 's1', benchId: 'b1', mode: 'PINCH', conditions: [{ type: 'ASAP' }] }
64
+ ]));
65
+ expect(dto.designatedSubs[0].pinchServer).toBe(true);
66
+ });
67
+ it('leaves pinchServer undefined for a non-PINCH row', () => {
68
+ const dto = tacticsColumnsToApiDto(makeTactics([
69
+ { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: EnergyBand.TIRED }
70
+ ]));
71
+ expect(dto.designatedSubs[0].pinchServer).toBeUndefined();
72
+ });
73
+ it('passes a conditional regular sub (pinchServer false + condition sub-back) through unchanged', () => {
74
+ const dto = tacticsColumnsToApiDto(makeTactics([
75
+ {
76
+ starterId: 's1',
77
+ benchId: 'b1',
78
+ mode: 'PINCH',
79
+ conditions: [{ type: 'SCORE_DIFF', direction: 'TRAILING', points: 3 }],
80
+ pinchServer: false,
81
+ subBackMode: 'CONDITIONS',
82
+ subBackConditions: [{ type: 'TEAM_SET_POINT', margin: 2, opponent: 'EITHER' }],
83
+ subBackConditionLogic: 'ANY'
84
+ }
85
+ ]));
86
+ const ds = dto.designatedSubs[0];
87
+ expect(ds.pinchServer).toBe(false);
88
+ expect(ds.subBackMode).toBe('CONDITIONS');
89
+ expect(ds.subBackConditions).toEqual([{ type: 'TEAM_SET_POINT', margin: 2, opponent: 'EITHER' }]);
90
+ expect(ds.subBackConditionLogic).toBe('ANY');
91
+ });
61
92
  it('drops a second libero that collides with the starting libero (not a reserve)', () => {
62
93
  const model = makeTactics([]);
63
94
  model.lineup[CourtPosition.LIBERO_ZONE] = 'libero1';
@@ -76,6 +107,21 @@ describe('tacticsColumnsToApiDto()', () => {
76
107
  expect(dto.secondLibero).toBe('reserve1');
77
108
  expect(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
78
109
  });
110
+ it('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
111
+ const model = makeTactics([]);
112
+ model.lineup[CourtPosition.LIBERO_ZONE] = 'libero1';
113
+ model.second_libero = 'reserve1';
114
+ model.replace_knocked_immediately = { libero1: false, lf: false };
115
+ const dto = tacticsColumnsToApiDto(model);
116
+ expect(dto.replaceKnockedImmediately).toEqual({ libero1: false, lf: false });
117
+ });
118
+ it('drops a libero-keyed replaceKnockedImmediately entry when there is no second libero', () => {
119
+ const model = makeTactics([]);
120
+ model.lineup[CourtPosition.LIBERO_ZONE] = 'libero1';
121
+ model.replace_knocked_immediately = { libero1: false, lf: false };
122
+ const dto = tacticsColumnsToApiDto(model);
123
+ expect(dto.replaceKnockedImmediately).toEqual({ lf: false });
124
+ });
79
125
  it('passes per-set tactics columns through to the API DTO shape', () => {
80
126
  const model = makeTactics([]);
81
127
  model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
@@ -180,4 +226,25 @@ describe('libero injuries — service <-> row round-trip + lenient heal', () =>
180
226
  expect(back.liberoInjury?.secondLiberoBackfill).toBeUndefined();
181
227
  expect(back.liberoInjury?.inMatchDesignate?.id).toBe(reserveB.id);
182
228
  });
229
+ it('keeps a libero-keyed replaceKnockedImmediately entry on load when an L2 exists', () => {
230
+ const withKnock = Tactics.create({
231
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
232
+ liberoReplacements: [],
233
+ secondLibero: l2,
234
+ replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
235
+ });
236
+ const attrs = transformFromTactics(withKnock, 'team-1');
237
+ const back = transformToTactics(attrs, roster);
238
+ expect(back.replaceKnockedImmediately).toEqual({ [libero.id]: false, [p1.id]: false });
239
+ });
240
+ it('drops a libero-keyed replaceKnockedImmediately entry on load when there is no L2', () => {
241
+ const withKnock = Tactics.create({
242
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
243
+ liberoReplacements: [],
244
+ replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
245
+ });
246
+ const attrs = transformFromTactics(withKnock, 'team-1');
247
+ const back = transformToTactics(attrs, roster);
248
+ expect(back.replaceKnockedImmediately).toEqual({ [p1.id]: false });
249
+ });
183
250
  });
@@ -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;