volleyballsimtypes 0.0.516 → 0.0.518

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 (29) hide show
  1. package/dist/cjs/src/api/index.d.ts +1 -0
  2. package/dist/cjs/src/api/tactics-player-refs.d.ts +2 -2
  3. package/dist/cjs/src/api/tactics-player-refs.js +3 -2
  4. package/dist/cjs/src/data/models/tactics.d.ts +3 -1
  5. package/dist/cjs/src/data/models/tactics.js +5 -0
  6. package/dist/cjs/src/data/transformers/tactics.js +39 -0
  7. package/dist/cjs/src/data/transformers/tactics.test.js +25 -0
  8. package/dist/cjs/src/service/team/base-config.d.ts +1 -0
  9. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +5 -0
  10. package/dist/cjs/src/service/team/schemas/tactics.z.js +31 -1
  11. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +63 -0
  12. package/dist/cjs/src/service/team/schemas/team.z.d.ts +5 -0
  13. package/dist/cjs/src/service/team/tactics.d.ts +2 -0
  14. package/dist/cjs/src/service/team/tactics.js +2 -1
  15. package/dist/esm/src/api/index.d.ts +1 -0
  16. package/dist/esm/src/api/tactics-player-refs.d.ts +2 -2
  17. package/dist/esm/src/api/tactics-player-refs.js +3 -2
  18. package/dist/esm/src/data/models/tactics.d.ts +3 -1
  19. package/dist/esm/src/data/models/tactics.js +5 -0
  20. package/dist/esm/src/data/transformers/tactics.js +39 -0
  21. package/dist/esm/src/data/transformers/tactics.test.js +25 -0
  22. package/dist/esm/src/service/team/base-config.d.ts +1 -0
  23. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +5 -0
  24. package/dist/esm/src/service/team/schemas/tactics.z.js +31 -1
  25. package/dist/esm/src/service/team/schemas/tactics.z.test.js +63 -0
  26. package/dist/esm/src/service/team/schemas/team.z.d.ts +5 -0
  27. package/dist/esm/src/service/team/tactics.d.ts +2 -0
  28. package/dist/esm/src/service/team/tactics.js +2 -1
  29. package/package.json +1 -1
@@ -137,6 +137,7 @@ export interface Tactics {
137
137
  liberoReplacementSets?: string[][];
138
138
  rotationSystem: RotationSystemEnum;
139
139
  designatedSetters: string[];
140
+ emergencySetterOrder: string[];
140
141
  offensivePreferences: ApiOffensivePreference[];
141
142
  systemSets?: ApiSystemSet[];
142
143
  offensivePreferenceSets?: ApiOffensivePreference[][];
@@ -2,8 +2,8 @@ import type { Tactics } from '.';
2
2
  /**
3
3
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
4
4
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
5
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
6
- * replacements and the libero-injury reserves.
5
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
6
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
7
7
  *
8
8
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
9
9
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -4,8 +4,8 @@ exports.collectTacticsPlayerIds = collectTacticsPlayerIds;
4
4
  /**
5
5
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
6
6
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
7
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
8
- * replacements and the libero-injury reserves.
7
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
8
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
9
9
  *
10
10
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
11
11
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -26,6 +26,7 @@ function collectTacticsPlayerIds(config) {
26
26
  config.secondLibero,
27
27
  ...((config.liberoReplacementSets ?? []).flat()),
28
28
  ...(config.designatedSetters ?? []),
29
+ ...(config.emergencySetterOrder ?? []),
29
30
  ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
30
31
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
31
32
  ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
@@ -74,6 +74,7 @@ export interface TacticsAttributes {
74
74
  libero_replacement_sets?: PlayerId[][];
75
75
  rotation_system: RotationSystemEnum;
76
76
  designated_setters: PlayerId[];
77
+ emergency_setter_order: PlayerId[];
77
78
  offensive_preferences: OffensivePreferenceAttributes[];
78
79
  system_sets?: SystemSetAttributes[];
79
80
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
@@ -86,7 +87,7 @@ export interface TacticsAttributes {
86
87
  }
87
88
  export type TacticsPk = 'team_id';
88
89
  export type TacticsId = TacticsModel[TacticsPk];
89
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
90
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'emergency_setter_order' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
90
91
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
91
92
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
92
93
  team_id: string;
@@ -100,6 +101,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
100
101
  libero_replacement_sets?: PlayerId[][];
101
102
  rotation_system: RotationSystemEnum;
102
103
  designated_setters: PlayerId[];
104
+ emergency_setter_order: PlayerId[];
103
105
  offensive_preferences: OffensivePreferenceAttributes[];
104
106
  system_sets?: SystemSetAttributes[];
105
107
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
@@ -62,6 +62,11 @@ class TacticsModel extends sequelize_1.Model {
62
62
  allowNull: false,
63
63
  defaultValue: []
64
64
  },
65
+ emergency_setter_order: {
66
+ type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
67
+ allowNull: false,
68
+ defaultValue: []
69
+ },
65
70
  offensive_preferences: {
66
71
  type: sequelize_1.DataTypes.JSONB,
67
72
  allowNull: false,
@@ -71,6 +71,7 @@ function transformToAttributes(tactics, teamId) {
71
71
  })),
72
72
  rotation_system: tactics.rotationSystem,
73
73
  designated_setters: tactics.designatedSetters.map((s) => s.id),
74
+ emergency_setter_order: tactics.emergencySetterOrder.map((p) => p.id),
74
75
  offensive_preferences: tactics.offensivePreferences.map(p => ({
75
76
  rotation: p.rotation,
76
77
  priorities: p.priorities,
@@ -178,6 +179,27 @@ function transformToObject(model, roster) {
178
179
  })),
179
180
  rotationSystem: model.rotation_system,
180
181
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
182
+ // LENIENT (roster.find): the coach-ranked emergency-setter chain. Keep only ids still on the roster, inside the
183
+ // squad pool (starters + starting libero + L2 + bench), and NOT a current designated setter (setters are pinned
184
+ // ahead of this list, never stored inside it), first-occurrence-wins on duplicates. So stale jsonb (a departed
185
+ // player, a promoted setter, a non-pool id) can never fail schema validation and crash a team load; the next
186
+ // tactics save rewrites it cleanly.
187
+ emergencySetterOrder: (() => {
188
+ const poolIds = new Set([...inLineupIds, ...(secondLibero != null ? [secondLibero.id] : [])]);
189
+ const setterIds = new Set(model.designated_setters ?? []);
190
+ const seen = new Set();
191
+ const healed = [];
192
+ for (const id of model.emergency_setter_order ?? []) {
193
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
194
+ continue;
195
+ const p = roster.find((x) => x.id === id);
196
+ if (p == null)
197
+ continue;
198
+ seen.add(id);
199
+ healed.push(p);
200
+ }
201
+ return healed;
202
+ })(),
181
203
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
182
204
  rotation: p.rotation,
183
205
  priorities: p.priorities,
@@ -335,6 +357,22 @@ function tacticsColumnsToApiDto(model) {
335
357
  }),
336
358
  rotationSystem: model.rotation_system,
337
359
  designatedSetters: model.designated_setters,
360
+ // Same stale-data policy: drop any emergency-setter id the service load would drop (a current designated
361
+ // setter, or one outside the squad pool) so the UI never reloads and re-emits a rejected entry. Roster
362
+ // membership can't be checked here (ids only); the service heal covers that.
363
+ emergencySetterOrder: (() => {
364
+ const poolIds = new Set([...lineupIds, ...(validSecondLibero != null ? [validSecondLibero] : [])]);
365
+ const setterIds = new Set(model.designated_setters ?? []);
366
+ const seen = new Set();
367
+ const healed = [];
368
+ for (const id of model.emergency_setter_order ?? []) {
369
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
370
+ continue;
371
+ seen.add(id);
372
+ healed.push(id);
373
+ }
374
+ return healed;
375
+ })(),
338
376
  offensivePreferences: model.offensive_preferences,
339
377
  systemSets: model.system_sets,
340
378
  offensivePreferenceSets: model.offensive_preference_sets,
@@ -443,6 +481,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
443
481
  libero_replacement_sets: config.liberoReplacementSets,
444
482
  rotation_system: config.rotationSystem,
445
483
  designated_setters: config.designatedSetters,
484
+ emergency_setter_order: config.emergencySetterOrder ?? [],
446
485
  offensive_preferences: config.offensivePreferences,
447
486
  system_sets: config.systemSets,
448
487
  offensive_preference_sets: config.offensivePreferenceSets,
@@ -261,6 +261,31 @@ function makeTactics(designatedSubs) {
261
261
  (0, globals_1.expect)(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
262
262
  (0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
263
263
  });
264
+ (0, globals_1.it)('round-trips the emergency setter order (non-setter pool members)', () => {
265
+ const withOrder = service_1.Tactics.create({
266
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
267
+ liberoReplacements: [],
268
+ emergencySetterOrder: [p2, p3]
269
+ });
270
+ const attrs = (0, tactics_1.transformFromTactics)(withOrder, 'team-1');
271
+ (0, globals_1.expect)(attrs.emergency_setter_order).toEqual([p2.id, p3.id]);
272
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
273
+ (0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, p3.id]);
274
+ });
275
+ // The read path must never crash a team load on a stale order. Ids that are gone from the roster, outside the
276
+ // squad pool, or now a designated setter (setters are pinned, never stored) are silently dropped, order preserved.
277
+ (0, globals_1.it)('drops emergency-order ids that are gone or a designated setter on read (lenient)', () => {
278
+ const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
279
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
280
+ liberoReplacements: [],
281
+ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE,
282
+ designatedSetters: [p1],
283
+ emergencySetterOrder: [p2]
284
+ }), 'team-1');
285
+ attrs.emergency_setter_order = [p1.id, 'gone', p2.id];
286
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
287
+ (0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id]);
288
+ });
264
289
  });
265
290
  (0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
266
291
  const country = (0, test_helpers_1.makeCountry)();
@@ -10,4 +10,5 @@ export interface BaseConfig {
10
10
  readonly receiveByPlayer?: Record<string, Record<string, boolean>>;
11
11
  readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
12
12
  readonly attackContact?: Record<string, Record<string, BaseCoord>>;
13
+ readonly liberoCoords?: Record<string, Record<string, Record<string, BaseCoord>>>;
13
14
  }
@@ -259,6 +259,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
259
259
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
260
260
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
261
261
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
+ emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
263
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
263
264
  rotation: z.ZodNumber;
264
265
  priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
@@ -319,6 +320,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
319
320
  d: z.ZodNumber;
320
321
  r: z.ZodNumber;
321
322
  }, z.core.$strip>>>>;
323
+ liberoCoords: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
324
+ d: z.ZodNumber;
325
+ r: z.ZodNumber;
326
+ }, z.core.$strip>>>>>;
322
327
  }, z.core.$strip>>;
323
328
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
324
329
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -99,7 +99,10 @@ const baseConfigSchema = zod_1.z.object({
99
99
  blocking: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean()))).optional(),
100
100
  // Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
101
101
  // configs saved before this field validate unchanged.
102
- attackContact: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema)).optional()
102
+ attackContact: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema)).optional(),
103
+ // Independent libero coords (owner 2026-08-14): [formation][rotation][zone] -> {d,r}, same shape as coords. Optional
104
+ // so configs saved before this field validate unchanged; absent => the libero shares the zone coord.
105
+ liberoCoords: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema))).optional()
103
106
  });
104
107
  exports.TacticsInputSchema = zod_1.z.object({
105
108
  lineup: lineupSchema,
@@ -118,6 +121,10 @@ exports.TacticsInputSchema = zod_1.z.object({
118
121
  // Tactics.create({...}) call valid and make an unconfigured team play exactly as before.
119
122
  rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum).default(rotation_system_1.RotationSystemEnum.SIX_ZERO),
120
123
  designatedSetters: zod_1.z.array(playerInstanceSchema).default([]),
124
+ // Emergency setter order (owner 2026-08-14): the coach-ranked NON-setter chain the engine follows when the
125
+ // designated setter is out of system. GLOBAL. Holds only non-setter pool members (setters are pinned ahead of
126
+ // it, derived). Default [] keeps every existing Tactics.create({...}) call valid; cross-field pool rules below.
127
+ emergencySetterOrder: zod_1.z.array(playerInstanceSchema).default([]),
121
128
  offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([]),
122
129
  // Attack shares on serve (libero-serve feature): per-player attack share the sim applies when a
123
130
  // libero-replaced player is on court unreplaced (serving, or through a libero injury). Keyed by starter id.
@@ -293,6 +300,29 @@ exports.TacticsInputSchema = zod_1.z.object({
293
300
  }
294
301
  }
295
302
  }
303
+ // Emergency setter order (owner 2026-08-14): the coach-ranked NON-setter chain. Every entry must be a squad-pool
304
+ // member (a court starter, the starting libero, the second libero, or a bench player), appear at most once, and
305
+ // NOT be one of the flat designated setters (they are pinned ahead of this list and never stored inside it).
306
+ const emergencyPoolIds = new Set([
307
+ ...starterIds,
308
+ ...(libero != null ? [libero.id] : []),
309
+ ...(data.secondLibero != null ? [data.secondLibero.id] : []),
310
+ ...data.lineup.bench.map(p => p.id)
311
+ ]);
312
+ const flatSetterIds = new Set(data.designatedSetters.map(s => s.id));
313
+ const seenEmergency = new Set();
314
+ for (const player of data.emergencySetterOrder) {
315
+ if (!emergencyPoolIds.has(player.id)) {
316
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_NOT_IN_POOL', path: ['emergencySetterOrder'] });
317
+ }
318
+ if (flatSetterIds.has(player.id)) {
319
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_IS_DESIGNATED_SETTER', path: ['emergencySetterOrder'] });
320
+ }
321
+ if (seenEmergency.has(player.id)) {
322
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_DUPLICATE', path: ['emergencySetterOrder'] });
323
+ }
324
+ seenEmergency.add(player.id);
325
+ }
296
326
  // Per-player knock toggle: every key must be a court starter OR the starting libero. The libero may carry its
297
327
  // own key so the user can choose to keep a lightly knocked libero on court (owner 2026-07-06). Bench/reserve
298
328
  // players still follow the slot they occupy, so they never carry their own key.
@@ -338,3 +338,66 @@ const test_helpers_1 = require("../../test-helpers");
338
338
  (0, globals_1.expect)(res.success).toBe(false);
339
339
  });
340
340
  });
341
+ (0, globals_1.describe)('TacticsInputSchema — emergency setter order', () => {
342
+ const country = (0, test_helpers_1.makeCountry)();
343
+ const p1 = (0, test_helpers_1.makePlayer)(country);
344
+ const p2 = (0, test_helpers_1.makePlayer)(country);
345
+ const p3 = (0, test_helpers_1.makePlayer)(country);
346
+ const p4 = (0, test_helpers_1.makePlayer)(country);
347
+ const p5 = (0, test_helpers_1.makePlayer)(country);
348
+ const p6 = (0, test_helpers_1.makePlayer)(country);
349
+ const libero = (0, test_helpers_1.makePlayer)(country);
350
+ const benchA = (0, test_helpers_1.makePlayer)(country);
351
+ const secondLibero = (0, test_helpers_1.makePlayer)(country);
352
+ const reserve = (0, test_helpers_1.makePlayer)(country);
353
+ // Pool = the 6 starters (p1..p6), the starting libero, the second libero, and the bench (benchA). `reserve` is
354
+ // outside the pool. 6-0 default => no designated setters, so the whole pool is orderable.
355
+ function baseInput(overrides = {}) {
356
+ return {
357
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [benchA] },
358
+ liberoReplacements: [],
359
+ secondLibero,
360
+ ...overrides
361
+ };
362
+ }
363
+ (0, globals_1.it)('accepts an order of pool members (starters, libero, L2, bench), no duplicates', () => {
364
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
365
+ emergencySetterOrder: [p2, benchA, libero, secondLibero]
366
+ }));
367
+ (0, globals_1.expect)(res.success).toBe(true);
368
+ if (res.success) {
369
+ (0, globals_1.expect)(res.data.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, benchA.id, libero.id, secondLibero.id]);
370
+ }
371
+ });
372
+ (0, globals_1.it)('defaults to [] when omitted', () => {
373
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
374
+ (0, globals_1.expect)(res.success).toBe(true);
375
+ if (res.success)
376
+ (0, globals_1.expect)(res.data.emergencySetterOrder).toEqual([]);
377
+ });
378
+ (0, globals_1.it)('rejects an entry outside the squad pool', () => {
379
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ emergencySetterOrder: [reserve] }));
380
+ (0, globals_1.expect)(res.success).toBe(false);
381
+ if (!res.success) {
382
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_NOT_IN_POOL')).toBe(true);
383
+ }
384
+ });
385
+ (0, globals_1.it)('rejects a duplicate entry', () => {
386
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ emergencySetterOrder: [p2, p2] }));
387
+ (0, globals_1.expect)(res.success).toBe(false);
388
+ if (!res.success) {
389
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_DUPLICATE')).toBe(true);
390
+ }
391
+ });
392
+ (0, globals_1.it)('rejects an entry that is a flat designated setter (setters are pinned, never stored in the list)', () => {
393
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
394
+ rotationSystem: '5-1',
395
+ designatedSetters: [p1],
396
+ emergencySetterOrder: [p1]
397
+ }));
398
+ (0, globals_1.expect)(res.success).toBe(false);
399
+ if (!res.success) {
400
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_IS_DESIGNATED_SETTER')).toBe(true);
401
+ }
402
+ });
403
+ });
@@ -267,6 +267,7 @@ export declare const TeamInputSchema: z.ZodObject<{
267
267
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
268
268
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof import("..").RotationSystemEnum>>;
269
269
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
+ emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
271
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
272
  rotation: z.ZodNumber;
272
273
  priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
@@ -327,6 +328,10 @@ export declare const TeamInputSchema: z.ZodObject<{
327
328
  d: z.ZodNumber;
328
329
  r: z.ZodNumber;
329
330
  }, z.core.$strip>>>>;
331
+ liberoCoords: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
332
+ d: z.ZodNumber;
333
+ r: z.ZodNumber;
334
+ }, z.core.$strip>>>>>;
330
335
  }, z.core.$strip>>;
331
336
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
332
337
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -34,6 +34,7 @@ export interface TacticsOpts {
34
34
  readonly liberoReplacementSets?: Player[][];
35
35
  readonly rotationSystem: RotationSystemEnum;
36
36
  readonly designatedSetters: Player[];
37
+ readonly emergencySetterOrder: Player[];
37
38
  readonly offensivePreferences: OffensivePreference[];
38
39
  readonly systemSets?: SystemSet[];
39
40
  readonly offensivePreferenceSets?: OffensivePreference[][];
@@ -55,6 +56,7 @@ export declare class Tactics {
55
56
  readonly liberoReplacementSets?: Player[][];
56
57
  readonly rotationSystem: RotationSystemEnum;
57
58
  readonly designatedSetters: Player[];
59
+ readonly emergencySetterOrder: Player[];
58
60
  readonly offensivePreferences: OffensivePreference[];
59
61
  readonly systemSets?: SystemSet[];
60
62
  readonly offensivePreferenceSets?: OffensivePreference[][];
@@ -18,7 +18,7 @@ class Tactics {
18
18
  }
19
19
  return new Tactics(result.data);
20
20
  }
21
- constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
21
+ constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], emergencySetterOrder = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
22
22
  this.lineup = lineup;
23
23
  this.liberoReplacements = liberoReplacements;
24
24
  this.receiveRotationOffset = receiveRotationOffset;
@@ -29,6 +29,7 @@ class Tactics {
29
29
  this.liberoReplacementSets = liberoReplacementSets;
30
30
  this.rotationSystem = rotationSystem;
31
31
  this.designatedSetters = designatedSetters;
32
+ this.emergencySetterOrder = emergencySetterOrder;
32
33
  this.offensivePreferences = offensivePreferences;
33
34
  this.systemSets = systemSets;
34
35
  this.offensivePreferenceSets = offensivePreferenceSets;
@@ -137,6 +137,7 @@ export interface Tactics {
137
137
  liberoReplacementSets?: string[][];
138
138
  rotationSystem: RotationSystemEnum;
139
139
  designatedSetters: string[];
140
+ emergencySetterOrder: string[];
140
141
  offensivePreferences: ApiOffensivePreference[];
141
142
  systemSets?: ApiSystemSet[];
142
143
  offensivePreferenceSets?: ApiOffensivePreference[][];
@@ -2,8 +2,8 @@ import type { Tactics } from '.';
2
2
  /**
3
3
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
4
4
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
5
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
6
- * replacements and the libero-injury reserves.
5
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
6
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
7
7
  *
8
8
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
9
9
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
3
3
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
4
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
5
- * replacements and the libero-injury reserves.
4
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
5
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
6
6
  *
7
7
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
8
8
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -23,6 +23,7 @@ export function collectTacticsPlayerIds(config) {
23
23
  config.secondLibero,
24
24
  ...((config.liberoReplacementSets ?? []).flat()),
25
25
  ...(config.designatedSetters ?? []),
26
+ ...(config.emergencySetterOrder ?? []),
26
27
  ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
27
28
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
28
29
  ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
@@ -74,6 +74,7 @@ export interface TacticsAttributes {
74
74
  libero_replacement_sets?: PlayerId[][];
75
75
  rotation_system: RotationSystemEnum;
76
76
  designated_setters: PlayerId[];
77
+ emergency_setter_order: PlayerId[];
77
78
  offensive_preferences: OffensivePreferenceAttributes[];
78
79
  system_sets?: SystemSetAttributes[];
79
80
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
@@ -86,7 +87,7 @@ export interface TacticsAttributes {
86
87
  }
87
88
  export type TacticsPk = 'team_id';
88
89
  export type TacticsId = TacticsModel[TacticsPk];
89
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
90
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'emergency_setter_order' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
90
91
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
91
92
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
92
93
  team_id: string;
@@ -100,6 +101,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
100
101
  libero_replacement_sets?: PlayerId[][];
101
102
  rotation_system: RotationSystemEnum;
102
103
  designated_setters: PlayerId[];
104
+ emergency_setter_order: PlayerId[];
103
105
  offensive_preferences: OffensivePreferenceAttributes[];
104
106
  system_sets?: SystemSetAttributes[];
105
107
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
@@ -59,6 +59,11 @@ export class TacticsModel extends Model {
59
59
  allowNull: false,
60
60
  defaultValue: []
61
61
  },
62
+ emergency_setter_order: {
63
+ type: DataTypes.ARRAY(DataTypes.UUID),
64
+ allowNull: false,
65
+ defaultValue: []
66
+ },
62
67
  offensive_preferences: {
63
68
  type: DataTypes.JSONB,
64
69
  allowNull: false,
@@ -65,6 +65,7 @@ function transformToAttributes(tactics, teamId) {
65
65
  })),
66
66
  rotation_system: tactics.rotationSystem,
67
67
  designated_setters: tactics.designatedSetters.map((s) => s.id),
68
+ emergency_setter_order: tactics.emergencySetterOrder.map((p) => p.id),
68
69
  offensive_preferences: tactics.offensivePreferences.map(p => ({
69
70
  rotation: p.rotation,
70
71
  priorities: p.priorities,
@@ -172,6 +173,27 @@ function transformToObject(model, roster) {
172
173
  })),
173
174
  rotationSystem: model.rotation_system,
174
175
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
176
+ // LENIENT (roster.find): the coach-ranked emergency-setter chain. Keep only ids still on the roster, inside the
177
+ // squad pool (starters + starting libero + L2 + bench), and NOT a current designated setter (setters are pinned
178
+ // ahead of this list, never stored inside it), first-occurrence-wins on duplicates. So stale jsonb (a departed
179
+ // player, a promoted setter, a non-pool id) can never fail schema validation and crash a team load; the next
180
+ // tactics save rewrites it cleanly.
181
+ emergencySetterOrder: (() => {
182
+ const poolIds = new Set([...inLineupIds, ...(secondLibero != null ? [secondLibero.id] : [])]);
183
+ const setterIds = new Set(model.designated_setters ?? []);
184
+ const seen = new Set();
185
+ const healed = [];
186
+ for (const id of model.emergency_setter_order ?? []) {
187
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
188
+ continue;
189
+ const p = roster.find((x) => x.id === id);
190
+ if (p == null)
191
+ continue;
192
+ seen.add(id);
193
+ healed.push(p);
194
+ }
195
+ return healed;
196
+ })(),
175
197
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
176
198
  rotation: p.rotation,
177
199
  priorities: p.priorities,
@@ -329,6 +351,22 @@ function tacticsColumnsToApiDto(model) {
329
351
  }),
330
352
  rotationSystem: model.rotation_system,
331
353
  designatedSetters: model.designated_setters,
354
+ // Same stale-data policy: drop any emergency-setter id the service load would drop (a current designated
355
+ // setter, or one outside the squad pool) so the UI never reloads and re-emits a rejected entry. Roster
356
+ // membership can't be checked here (ids only); the service heal covers that.
357
+ emergencySetterOrder: (() => {
358
+ const poolIds = new Set([...lineupIds, ...(validSecondLibero != null ? [validSecondLibero] : [])]);
359
+ const setterIds = new Set(model.designated_setters ?? []);
360
+ const seen = new Set();
361
+ const healed = [];
362
+ for (const id of model.emergency_setter_order ?? []) {
363
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
364
+ continue;
365
+ seen.add(id);
366
+ healed.push(id);
367
+ }
368
+ return healed;
369
+ })(),
332
370
  offensivePreferences: model.offensive_preferences,
333
371
  systemSets: model.system_sets,
334
372
  offensivePreferenceSets: model.offensive_preference_sets,
@@ -437,6 +475,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
437
475
  libero_replacement_sets: config.liberoReplacementSets,
438
476
  rotation_system: config.rotationSystem,
439
477
  designated_setters: config.designatedSetters,
478
+ emergency_setter_order: config.emergencySetterOrder ?? [],
440
479
  offensive_preferences: config.offensivePreferences,
441
480
  system_sets: config.systemSets,
442
481
  offensive_preference_sets: config.offensivePreferenceSets,
@@ -259,6 +259,31 @@ describe('per-set tactics — service <-> row round-trip', () => {
259
259
  expect(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
260
260
  expect(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
261
261
  });
262
+ it('round-trips the emergency setter order (non-setter pool members)', () => {
263
+ const withOrder = Tactics.create({
264
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
265
+ liberoReplacements: [],
266
+ emergencySetterOrder: [p2, p3]
267
+ });
268
+ const attrs = transformFromTactics(withOrder, 'team-1');
269
+ expect(attrs.emergency_setter_order).toEqual([p2.id, p3.id]);
270
+ const back = transformToTactics(attrs, roster);
271
+ expect(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, p3.id]);
272
+ });
273
+ // The read path must never crash a team load on a stale order. Ids that are gone from the roster, outside the
274
+ // squad pool, or now a designated setter (setters are pinned, never stored) are silently dropped, order preserved.
275
+ it('drops emergency-order ids that are gone or a designated setter on read (lenient)', () => {
276
+ const attrs = transformFromTactics(Tactics.create({
277
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
278
+ liberoReplacements: [],
279
+ rotationSystem: RotationSystemEnum.FIVE_ONE,
280
+ designatedSetters: [p1],
281
+ emergencySetterOrder: [p2]
282
+ }), 'team-1');
283
+ attrs.emergency_setter_order = [p1.id, 'gone', p2.id];
284
+ const back = transformToTactics(attrs, roster);
285
+ expect(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id]);
286
+ });
262
287
  });
263
288
  describe('libero injuries — service <-> row round-trip + lenient heal', () => {
264
289
  const country = makeCountry();
@@ -10,4 +10,5 @@ export interface BaseConfig {
10
10
  readonly receiveByPlayer?: Record<string, Record<string, boolean>>;
11
11
  readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
12
12
  readonly attackContact?: Record<string, Record<string, BaseCoord>>;
13
+ readonly liberoCoords?: Record<string, Record<string, Record<string, BaseCoord>>>;
13
14
  }
@@ -259,6 +259,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
259
259
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
260
260
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
261
261
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
+ emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
263
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
263
264
  rotation: z.ZodNumber;
264
265
  priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
@@ -319,6 +320,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
319
320
  d: z.ZodNumber;
320
321
  r: z.ZodNumber;
321
322
  }, z.core.$strip>>>>;
323
+ liberoCoords: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
324
+ d: z.ZodNumber;
325
+ r: z.ZodNumber;
326
+ }, z.core.$strip>>>>>;
322
327
  }, z.core.$strip>>;
323
328
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
324
329
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -96,7 +96,10 @@ const baseConfigSchema = z.object({
96
96
  blocking: z.record(z.string(), z.record(z.string(), z.record(z.string(), z.boolean()))).optional(),
97
97
  // Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
98
98
  // configs saved before this field validate unchanged.
99
- attackContact: z.record(z.string(), z.record(z.string(), baseCoordSchema)).optional()
99
+ attackContact: z.record(z.string(), z.record(z.string(), baseCoordSchema)).optional(),
100
+ // Independent libero coords (owner 2026-08-14): [formation][rotation][zone] -> {d,r}, same shape as coords. Optional
101
+ // so configs saved before this field validate unchanged; absent => the libero shares the zone coord.
102
+ liberoCoords: z.record(z.string(), z.record(z.string(), z.record(z.string(), baseCoordSchema))).optional()
100
103
  });
101
104
  export const TacticsInputSchema = z.object({
102
105
  lineup: lineupSchema,
@@ -115,6 +118,10 @@ export const TacticsInputSchema = z.object({
115
118
  // Tactics.create({...}) call valid and make an unconfigured team play exactly as before.
116
119
  rotationSystem: z.nativeEnum(RotationSystemEnum).default(RotationSystemEnum.SIX_ZERO),
117
120
  designatedSetters: z.array(playerInstanceSchema).default([]),
121
+ // Emergency setter order (owner 2026-08-14): the coach-ranked NON-setter chain the engine follows when the
122
+ // designated setter is out of system. GLOBAL. Holds only non-setter pool members (setters are pinned ahead of
123
+ // it, derived). Default [] keeps every existing Tactics.create({...}) call valid; cross-field pool rules below.
124
+ emergencySetterOrder: z.array(playerInstanceSchema).default([]),
118
125
  offensivePreferences: z.array(offensivePreferenceSchema).default([]),
119
126
  // Attack shares on serve (libero-serve feature): per-player attack share the sim applies when a
120
127
  // libero-replaced player is on court unreplaced (serving, or through a libero injury). Keyed by starter id.
@@ -290,6 +297,29 @@ export const TacticsInputSchema = z.object({
290
297
  }
291
298
  }
292
299
  }
300
+ // Emergency setter order (owner 2026-08-14): the coach-ranked NON-setter chain. Every entry must be a squad-pool
301
+ // member (a court starter, the starting libero, the second libero, or a bench player), appear at most once, and
302
+ // NOT be one of the flat designated setters (they are pinned ahead of this list and never stored inside it).
303
+ const emergencyPoolIds = new Set([
304
+ ...starterIds,
305
+ ...(libero != null ? [libero.id] : []),
306
+ ...(data.secondLibero != null ? [data.secondLibero.id] : []),
307
+ ...data.lineup.bench.map(p => p.id)
308
+ ]);
309
+ const flatSetterIds = new Set(data.designatedSetters.map(s => s.id));
310
+ const seenEmergency = new Set();
311
+ for (const player of data.emergencySetterOrder) {
312
+ if (!emergencyPoolIds.has(player.id)) {
313
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_NOT_IN_POOL', path: ['emergencySetterOrder'] });
314
+ }
315
+ if (flatSetterIds.has(player.id)) {
316
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_IS_DESIGNATED_SETTER', path: ['emergencySetterOrder'] });
317
+ }
318
+ if (seenEmergency.has(player.id)) {
319
+ ctx.addIssue({ code: 'custom', message: 'EMERGENCY_SETTER_DUPLICATE', path: ['emergencySetterOrder'] });
320
+ }
321
+ seenEmergency.add(player.id);
322
+ }
293
323
  // Per-player knock toggle: every key must be a court starter OR the starting libero. The libero may carry its
294
324
  // own key so the user can choose to keep a lightly knocked libero on court (owner 2026-07-06). Bench/reserve
295
325
  // players still follow the slot they occupy, so they never carry their own key.
@@ -336,3 +336,66 @@ describe('TacticsInputSchema — blocking preferences', () => {
336
336
  expect(res.success).toBe(false);
337
337
  });
338
338
  });
339
+ describe('TacticsInputSchema — emergency setter order', () => {
340
+ const country = makeCountry();
341
+ const p1 = makePlayer(country);
342
+ const p2 = makePlayer(country);
343
+ const p3 = makePlayer(country);
344
+ const p4 = makePlayer(country);
345
+ const p5 = makePlayer(country);
346
+ const p6 = makePlayer(country);
347
+ const libero = makePlayer(country);
348
+ const benchA = makePlayer(country);
349
+ const secondLibero = makePlayer(country);
350
+ const reserve = makePlayer(country);
351
+ // Pool = the 6 starters (p1..p6), the starting libero, the second libero, and the bench (benchA). `reserve` is
352
+ // outside the pool. 6-0 default => no designated setters, so the whole pool is orderable.
353
+ function baseInput(overrides = {}) {
354
+ return {
355
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [benchA] },
356
+ liberoReplacements: [],
357
+ secondLibero,
358
+ ...overrides
359
+ };
360
+ }
361
+ it('accepts an order of pool members (starters, libero, L2, bench), no duplicates', () => {
362
+ const res = TacticsInputSchema.safeParse(baseInput({
363
+ emergencySetterOrder: [p2, benchA, libero, secondLibero]
364
+ }));
365
+ expect(res.success).toBe(true);
366
+ if (res.success) {
367
+ expect(res.data.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, benchA.id, libero.id, secondLibero.id]);
368
+ }
369
+ });
370
+ it('defaults to [] when omitted', () => {
371
+ const res = TacticsInputSchema.safeParse(baseInput());
372
+ expect(res.success).toBe(true);
373
+ if (res.success)
374
+ expect(res.data.emergencySetterOrder).toEqual([]);
375
+ });
376
+ it('rejects an entry outside the squad pool', () => {
377
+ const res = TacticsInputSchema.safeParse(baseInput({ emergencySetterOrder: [reserve] }));
378
+ expect(res.success).toBe(false);
379
+ if (!res.success) {
380
+ expect(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_NOT_IN_POOL')).toBe(true);
381
+ }
382
+ });
383
+ it('rejects a duplicate entry', () => {
384
+ const res = TacticsInputSchema.safeParse(baseInput({ emergencySetterOrder: [p2, p2] }));
385
+ expect(res.success).toBe(false);
386
+ if (!res.success) {
387
+ expect(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_DUPLICATE')).toBe(true);
388
+ }
389
+ });
390
+ it('rejects an entry that is a flat designated setter (setters are pinned, never stored in the list)', () => {
391
+ const res = TacticsInputSchema.safeParse(baseInput({
392
+ rotationSystem: '5-1',
393
+ designatedSetters: [p1],
394
+ emergencySetterOrder: [p1]
395
+ }));
396
+ expect(res.success).toBe(false);
397
+ if (!res.success) {
398
+ expect(res.error.issues.some(i => i.message === 'EMERGENCY_SETTER_IS_DESIGNATED_SETTER')).toBe(true);
399
+ }
400
+ });
401
+ });
@@ -267,6 +267,7 @@ export declare const TeamInputSchema: z.ZodObject<{
267
267
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
268
268
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof import("..").RotationSystemEnum>>;
269
269
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
+ emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
271
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
272
  rotation: z.ZodNumber;
272
273
  priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
@@ -327,6 +328,10 @@ export declare const TeamInputSchema: z.ZodObject<{
327
328
  d: z.ZodNumber;
328
329
  r: z.ZodNumber;
329
330
  }, z.core.$strip>>>>;
331
+ liberoCoords: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
332
+ d: z.ZodNumber;
333
+ r: z.ZodNumber;
334
+ }, z.core.$strip>>>>>;
330
335
  }, z.core.$strip>>;
331
336
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
332
337
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -34,6 +34,7 @@ export interface TacticsOpts {
34
34
  readonly liberoReplacementSets?: Player[][];
35
35
  readonly rotationSystem: RotationSystemEnum;
36
36
  readonly designatedSetters: Player[];
37
+ readonly emergencySetterOrder: Player[];
37
38
  readonly offensivePreferences: OffensivePreference[];
38
39
  readonly systemSets?: SystemSet[];
39
40
  readonly offensivePreferenceSets?: OffensivePreference[][];
@@ -55,6 +56,7 @@ export declare class Tactics {
55
56
  readonly liberoReplacementSets?: Player[][];
56
57
  readonly rotationSystem: RotationSystemEnum;
57
58
  readonly designatedSetters: Player[];
59
+ readonly emergencySetterOrder: Player[];
58
60
  readonly offensivePreferences: OffensivePreference[];
59
61
  readonly systemSets?: SystemSet[];
60
62
  readonly offensivePreferenceSets?: OffensivePreference[][];
@@ -15,7 +15,7 @@ export class Tactics {
15
15
  }
16
16
  return new Tactics(result.data);
17
17
  }
18
- constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
18
+ constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], emergencySetterOrder = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
19
19
  this.lineup = lineup;
20
20
  this.liberoReplacements = liberoReplacements;
21
21
  this.receiveRotationOffset = receiveRotationOffset;
@@ -26,6 +26,7 @@ export class Tactics {
26
26
  this.liberoReplacementSets = liberoReplacementSets;
27
27
  this.rotationSystem = rotationSystem;
28
28
  this.designatedSetters = designatedSetters;
29
+ this.emergencySetterOrder = emergencySetterOrder;
29
30
  this.offensivePreferences = offensivePreferences;
30
31
  this.systemSets = systemSets;
31
32
  this.offensivePreferenceSets = offensivePreferenceSets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.516",
3
+ "version": "0.0.518",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",