volleyballsimtypes 0.0.464 → 0.0.465

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 (35) hide show
  1. package/dist/cjs/src/api/index.d.ts +5 -4
  2. package/dist/cjs/src/data/models/tactics.d.ts +7 -6
  3. package/dist/cjs/src/data/models/tactics.js +1 -1
  4. package/dist/cjs/src/data/transformers/tactics.js +71 -59
  5. package/dist/cjs/src/service/team/index.d.ts +1 -1
  6. package/dist/cjs/src/service/team/index.js +1 -1
  7. package/dist/cjs/src/service/team/injury-replacement.d.ts +6 -0
  8. package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +142 -0
  9. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +4 -3
  10. package/dist/cjs/src/service/team/schemas/tactics.z.js +38 -23
  11. package/dist/cjs/src/service/team/schemas/team.z.d.ts +4 -3
  12. package/dist/cjs/src/service/team/tactics.d.ts +3 -3
  13. package/dist/cjs/src/service/team/tactics.js +2 -2
  14. package/dist/esm/src/api/index.d.ts +5 -4
  15. package/dist/esm/src/data/models/tactics.d.ts +7 -6
  16. package/dist/esm/src/data/models/tactics.js +1 -1
  17. package/dist/esm/src/data/transformers/tactics.js +71 -59
  18. package/dist/esm/src/service/team/index.d.ts +1 -1
  19. package/dist/esm/src/service/team/index.js +1 -1
  20. package/dist/esm/src/service/team/injury-replacement.d.ts +6 -0
  21. package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +140 -0
  22. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +4 -3
  23. package/dist/esm/src/service/team/schemas/tactics.z.js +38 -23
  24. package/dist/esm/src/service/team/schemas/team.z.d.ts +4 -3
  25. package/dist/esm/src/service/team/tactics.d.ts +3 -3
  26. package/dist/esm/src/service/team/tactics.js +2 -2
  27. package/package.json +1 -1
  28. package/dist/cjs/src/service/team/reserve-priority.d.ts +0 -6
  29. package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.js +0 -125
  30. package/dist/esm/src/service/team/reserve-priority.d.ts +0 -6
  31. package/dist/esm/src/service/team/schemas/reserve-priority.z.test.js +0 -123
  32. /package/dist/cjs/src/service/team/{reserve-priority.js → injury-replacement.js} +0 -0
  33. /package/dist/cjs/src/service/team/schemas/{reserve-priority.z.test.d.ts → injury-replacement.z.test.d.ts} +0 -0
  34. /package/dist/esm/src/service/team/{reserve-priority.js → injury-replacement.js} +0 -0
  35. /package/dist/esm/src/service/team/schemas/{reserve-priority.z.test.d.ts → injury-replacement.z.test.d.ts} +0 -0
@@ -83,9 +83,10 @@ function transformToAttributes(tactics, teamId) {
83
83
  order: p.order.map((a) => a.id)
84
84
  }))),
85
85
  replace_injured_immediately: tactics.replaceInjuredImmediately,
86
- reserve_priorities: tactics.reservePriorities.map(rp => ({
87
- position: rp.position,
88
- reserves: rp.reserves.map((p) => p.id)
86
+ injury_replacements: tactics.injuryReplacements.map(ir => ({
87
+ starterId: ir.starter.id,
88
+ replacementId: ir.replacement.id,
89
+ benchBackfillId: ir.benchBackfill?.id
89
90
  }))
90
91
  };
91
92
  }
@@ -165,36 +166,43 @@ function transformToObject(model, roster) {
165
166
  order: p.order.map((id) => findPlayer(id, roster))
166
167
  }))),
167
168
  replaceInjuredImmediately: model.replace_injured_immediately ?? true,
168
- // LENIENT (roster.find, not findPlayer): a dismissed/retired reserve, one that has since moved INTO the
169
- // lineup, one that doubles as the resolved second libero, or a duplicate (player in two lists, repeated
170
- // position) is DROPPED rather than crashing the sim batch (reserves are depth, not structure); now-empty
171
- // entries are pruned. First occurrence wins on duplicates; the next tactics save rewrites cleanly.
172
- reservePriorities: (() => {
173
- const seenIds = new Set();
174
- const seenPositions = new Set();
175
- return (model.reserve_priorities ?? [])
176
- .filter(rp => {
177
- // Court slots only (1-6): jsonb has no constraint, and an out-of-range position would pass the
178
- // heal only to be rejected by the schema, crashing the very sim batch this heal protects.
179
- if (!Number.isInteger(rp.position) || rp.position < 1 || rp.position > 6)
180
- return false;
181
- if (seenPositions.has(rp.position))
182
- return false;
183
- seenPositions.add(rp.position);
184
- return true;
185
- })
186
- .map(rp => ({
187
- position: rp.position,
188
- reserves: rp.reserves
189
- .map((id) => roster.find((p) => p.id === id))
190
- .filter((p) => {
191
- if (p == null || inLineupIds.has(p.id) || p.id === secondLibero?.id || seenIds.has(p.id))
192
- return false;
193
- seenIds.add(p.id);
194
- return true;
195
- })
196
- }))
197
- .filter(rp => rp.reserves.length > 0);
169
+ // LENIENT (roster.find, not findPlayer): an entry whose starter or replacement is gone/invalid is DROPPED
170
+ // rather than crashing the sim batch (a dropped entry = unconfigured = the bench-only auto fallback, per
171
+ // the owner rules); an invalid benchBackfill drops only the backfill. First occurrence wins on duplicates;
172
+ // the next tactics save rewrites cleanly. Schema invariants mirrored: starter must be a court starter,
173
+ // replacement bench-or-reserve (never a starter/libero/L2), backfill only-with-bench-replacement + reserve.
174
+ injuryReplacements: (() => {
175
+ const starterIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
176
+ lineup[CourtPosition.LEFT_BACK], lineup[CourtPosition.MIDDLE_BACK], lineup[CourtPosition.RIGHT_BACK]]
177
+ .map((p) => p.id));
178
+ const benchIds = new Set(lineup.bench.map((p) => p.id));
179
+ const seenStarters = new Set();
180
+ const seenIncoming = new Set();
181
+ const healed = [];
182
+ for (const ir of model.injury_replacements ?? []) {
183
+ const starter = roster.find((p) => p.id === ir.starterId);
184
+ const replacement = roster.find((p) => p.id === ir.replacementId);
185
+ if (starter == null || replacement == null)
186
+ continue;
187
+ if (!starterIds.has(starter.id) || seenStarters.has(starter.id))
188
+ continue;
189
+ if (starterIds.has(replacement.id) || replacement.id === lineup[CourtPosition.LIBERO_ZONE]?.id ||
190
+ replacement.id === secondLibero?.id || seenIncoming.has(replacement.id))
191
+ continue;
192
+ seenStarters.add(starter.id);
193
+ seenIncoming.add(replacement.id);
194
+ let benchBackfill;
195
+ if (ir.benchBackfillId != null && benchIds.has(replacement.id)) {
196
+ const candidate = roster.find((p) => p.id === ir.benchBackfillId);
197
+ if (candidate != null && !inLineupIds.has(candidate.id) && candidate.id !== secondLibero?.id &&
198
+ !seenIncoming.has(candidate.id)) {
199
+ benchBackfill = candidate;
200
+ seenIncoming.add(candidate.id);
201
+ }
202
+ }
203
+ healed.push({ starter, replacement, benchBackfill });
204
+ }
205
+ return healed;
198
206
  })()
199
207
  });
200
208
  }
@@ -257,31 +265,35 @@ function tacticsColumnsToApiDto(model) {
257
265
  systemSets: model.system_sets,
258
266
  offensivePreferenceSets: model.offensive_preference_sets,
259
267
  replaceInjuredImmediately: model.replace_injured_immediately ?? true,
260
- // Same stale-data policy as validSecondLibero above: don't surface reserve entries the service load
261
- // would drop (bad position, id now in the lineup, the L2, duplicates), or the UI reloads and re-emits
262
- // them forever. Roster membership can't be checked here (ids only); the service heal covers that.
263
- reservePriorities: (() => {
264
- const seenIds = new Set();
265
- const seenPositions = new Set();
266
- const healed = (model.reserve_priorities ?? [])
267
- .filter(rp => {
268
- if (!Number.isInteger(rp.position) || rp.position < 1 || rp.position > 6)
269
- return false;
270
- if (seenPositions.has(rp.position))
271
- return false;
272
- seenPositions.add(rp.position);
273
- return true;
274
- })
275
- .map(rp => ({
276
- position: rp.position,
277
- reserves: rp.reserves.filter(id => {
278
- if (lineupIds.has(id) || id === validSecondLibero || seenIds.has(id))
279
- return false;
280
- seenIds.add(id);
281
- return true;
282
- })
283
- }))
284
- .filter(rp => rp.reserves.length > 0);
268
+ // Same stale-data policy as validSecondLibero above: don't surface entries the service load would drop
269
+ // (starter no longer a starter, replacement now a starter/libero/L2, duplicates), or the UI reloads and
270
+ // re-emits them forever. Roster membership can't be checked here (ids only); the service heal covers that.
271
+ injuryReplacements: (() => {
272
+ const starterSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
273
+ model.lineup[CourtPosition.RIGHT_FRONT], model.lineup[CourtPosition.LEFT_BACK],
274
+ model.lineup[CourtPosition.MIDDLE_BACK], model.lineup[CourtPosition.RIGHT_BACK]]);
275
+ const benchIdSet = new Set(model.lineup.bench);
276
+ const liberoId = model.lineup[CourtPosition.LIBERO_ZONE];
277
+ const seenStarters = new Set();
278
+ const seenIncoming = new Set();
279
+ const healed = [];
280
+ for (const ir of model.injury_replacements ?? []) {
281
+ if (!starterSlotIds.has(ir.starterId) || seenStarters.has(ir.starterId))
282
+ continue;
283
+ if (starterSlotIds.has(ir.replacementId) || ir.replacementId === liberoId ||
284
+ ir.replacementId === validSecondLibero || seenIncoming.has(ir.replacementId))
285
+ continue;
286
+ seenStarters.add(ir.starterId);
287
+ seenIncoming.add(ir.replacementId);
288
+ let benchBackfillId;
289
+ if (ir.benchBackfillId != null && benchIdSet.has(ir.replacementId) &&
290
+ !lineupIds.has(ir.benchBackfillId) && ir.benchBackfillId !== validSecondLibero &&
291
+ !seenIncoming.has(ir.benchBackfillId)) {
292
+ benchBackfillId = ir.benchBackfillId;
293
+ seenIncoming.add(benchBackfillId);
294
+ }
295
+ healed.push({ starterId: ir.starterId, replacementId: ir.replacementId, benchBackfillId });
296
+ }
285
297
  return healed.length > 0 ? healed : undefined;
286
298
  })()
287
299
  };
@@ -318,7 +330,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
318
330
  system_sets: config.systemSets,
319
331
  offensive_preference_sets: config.offensivePreferenceSets,
320
332
  replace_injured_immediately: config.replaceInjuredImmediately,
321
- reserve_priorities: config.reservePriorities
333
+ injury_replacements: config.injuryReplacements
322
334
  };
323
335
  }
324
336
  export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto, apiTacticsConfigToAttributes as apiTacticsConfigToTacticsAttributes };
@@ -11,4 +11,4 @@ export * from './rotation-system';
11
11
  export * from './lineup-function';
12
12
  export * from './base-position';
13
13
  export * from './offensive-preference';
14
- export * from './reserve-priority';
14
+ export * from './injury-replacement';
@@ -11,4 +11,4 @@ export * from './rotation-system';
11
11
  export * from './lineup-function';
12
12
  export * from './base-position';
13
13
  export * from './offensive-preference';
14
- export * from './reserve-priority';
14
+ export * from './injury-replacement';
@@ -0,0 +1,6 @@
1
+ import { Player } from '../player';
2
+ export interface InjuryReplacement {
3
+ readonly starter: Player;
4
+ readonly replacement: Player;
5
+ readonly benchBackfill?: Player;
6
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { TacticsInputSchema } from './tactics.z';
3
+ import { makeCountry, makePlayer } from '../../test-helpers';
4
+ describe('TacticsInputSchema — injuryReplacements (owner rules 2026-07-03)', () => {
5
+ const country = makeCountry();
6
+ const p1 = makePlayer(country);
7
+ const p2 = makePlayer(country);
8
+ const p3 = makePlayer(country);
9
+ const p4 = makePlayer(country);
10
+ const p5 = makePlayer(country);
11
+ const p6 = makePlayer(country);
12
+ const benchA = makePlayer(country);
13
+ const benchB = makePlayer(country);
14
+ const libero = makePlayer(country);
15
+ const reserveA = makePlayer(country);
16
+ const reserveB = makePlayer(country);
17
+ function baseInput(overrides = {}) {
18
+ return {
19
+ lineup: {
20
+ 4: p4,
21
+ 3: p3,
22
+ 2: p2,
23
+ 5: p5,
24
+ 6: p6,
25
+ 1: p1,
26
+ 0: libero,
27
+ bench: [benchA, benchB]
28
+ },
29
+ liberoReplacements: [],
30
+ ...overrides
31
+ };
32
+ }
33
+ it('defaults to true / empty', () => {
34
+ const res = TacticsInputSchema.safeParse(baseInput());
35
+ expect(res.success).toBe(true);
36
+ if (res.success) {
37
+ expect(res.data.replaceInjuredImmediately).toBe(true);
38
+ expect(res.data.injuryReplacements).toEqual([]);
39
+ }
40
+ });
41
+ it('accepts a bench replacement with a reserve backfill', () => {
42
+ const res = TacticsInputSchema.safeParse(baseInput({
43
+ injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: reserveA }]
44
+ }));
45
+ expect(res.success).toBe(true);
46
+ });
47
+ it('accepts a bench replacement WITHOUT a backfill (empty bench seat is allowed by design)', () => {
48
+ const res = TacticsInputSchema.safeParse(baseInput({
49
+ injuryReplacements: [{ starter: p1, replacement: benchA }]
50
+ }));
51
+ expect(res.success).toBe(true);
52
+ });
53
+ it('accepts a reserve replacement (no backfill possible)', () => {
54
+ const res = TacticsInputSchema.safeParse(baseInput({
55
+ injuryReplacements: [{ starter: p1, replacement: reserveA }]
56
+ }));
57
+ expect(res.success).toBe(true);
58
+ });
59
+ it('rejects a starter as replacement', () => {
60
+ const res = TacticsInputSchema.safeParse(baseInput({
61
+ injuryReplacements: [{ starter: p1, replacement: p2 }]
62
+ }));
63
+ expect(res.success).toBe(false);
64
+ if (!res.success) {
65
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE')).toBe(true);
66
+ }
67
+ });
68
+ it('rejects the libero as replacement', () => {
69
+ const res = TacticsInputSchema.safeParse(baseInput({
70
+ injuryReplacements: [{ starter: p1, replacement: libero }]
71
+ }));
72
+ expect(res.success).toBe(false);
73
+ if (!res.success) {
74
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE')).toBe(true);
75
+ }
76
+ });
77
+ it('rejects a non-starter as the starter side', () => {
78
+ const res = TacticsInputSchema.safeParse(baseInput({
79
+ injuryReplacements: [{ starter: benchA, replacement: reserveA }]
80
+ }));
81
+ expect(res.success).toBe(false);
82
+ if (!res.success) {
83
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_STARTER_NOT_A_STARTER')).toBe(true);
84
+ }
85
+ });
86
+ it('rejects a backfill when the replacement is a reserve', () => {
87
+ const res = TacticsInputSchema.safeParse(baseInput({
88
+ injuryReplacements: [{ starter: p1, replacement: reserveA, benchBackfill: reserveB }]
89
+ }));
90
+ expect(res.success).toBe(false);
91
+ if (!res.success) {
92
+ expect(res.error.issues.some(issue => issue.message === 'BENCH_BACKFILL_REQUIRES_BENCH_REPLACEMENT')).toBe(true);
93
+ }
94
+ });
95
+ it('rejects a bench player as backfill (must be a reserve)', () => {
96
+ const res = TacticsInputSchema.safeParse(baseInput({
97
+ injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: benchB }]
98
+ }));
99
+ expect(res.success).toBe(false);
100
+ if (!res.success) {
101
+ expect(res.error.issues.some(issue => issue.message === 'BENCH_BACKFILL_MUST_BE_RESERVE')).toBe(true);
102
+ }
103
+ });
104
+ it('rejects duplicate starter entries', () => {
105
+ const res = TacticsInputSchema.safeParse(baseInput({
106
+ injuryReplacements: [
107
+ { starter: p1, replacement: benchA },
108
+ { starter: p1, replacement: reserveA }
109
+ ]
110
+ }));
111
+ expect(res.success).toBe(false);
112
+ if (!res.success) {
113
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_DUPLICATE_STARTER')).toBe(true);
114
+ }
115
+ });
116
+ it('rejects the same incoming player used twice', () => {
117
+ const res = TacticsInputSchema.safeParse(baseInput({
118
+ injuryReplacements: [
119
+ { starter: p1, replacement: reserveA },
120
+ { starter: p2, replacement: reserveA }
121
+ ]
122
+ }));
123
+ expect(res.success).toBe(false);
124
+ if (!res.success) {
125
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_PLAYER_USED_TWICE')).toBe(true);
126
+ }
127
+ });
128
+ it('rejects a player used as replacement in one entry and backfill in another', () => {
129
+ const res = TacticsInputSchema.safeParse(baseInput({
130
+ injuryReplacements: [
131
+ { starter: p1, replacement: reserveA },
132
+ { starter: p2, replacement: benchA, benchBackfill: reserveA }
133
+ ]
134
+ }));
135
+ expect(res.success).toBe(false);
136
+ if (!res.success) {
137
+ expect(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_PLAYER_USED_TWICE')).toBe(true);
138
+ }
139
+ });
140
+ });
@@ -124,9 +124,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
124
124
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
125
125
  }, z.core.$strip>>>>;
126
126
  replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
127
- reservePriorities: z.ZodDefault<z.ZodArray<z.ZodObject<{
128
- position: z.ZodNumber;
129
- reserves: z.ZodArray<z.ZodCustom<Player, Player>>;
127
+ injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
128
+ starter: z.ZodCustom<Player, Player>;
129
+ replacement: z.ZodCustom<Player, Player>;
130
+ benchBackfill: z.ZodOptional<z.ZodCustom<Player, Player>>;
130
131
  }, z.core.$strip>>>;
131
132
  }, z.core.$strip>;
132
133
  export type TacticsInput = z.infer<typeof TacticsInputSchema>;
@@ -78,13 +78,14 @@ export const TacticsInputSchema = z.object({
78
78
  // validated with the same cross-field rules as the flat fields (see superRefine below).
79
79
  systemSets: z.array(systemSetSchema).max(5).optional(),
80
80
  offensivePreferenceSets: z.array(z.array(offensivePreferenceSchema)).max(5).optional(),
81
- // Injuries. Defaults keep every existing Tactics.create({...}) call valid. reservePriorities is flat-only
82
- // (whole match): per-position ordered reserve lists; cross-field rules (reserves must be OUTSIDE the lineup,
83
- // no player in two lists, unique positions) in the superRefine below.
81
+ // Injuries. Defaults keep every existing Tactics.create({...}) call valid. injuryReplacements is the
82
+ // per-starter explicit replacement mapping (owner rules: followed verbatim, bench-only auto fallback,
83
+ // reserves never auto-promoted); cross-field rules in the superRefine below.
84
84
  replaceInjuredImmediately: z.boolean().default(true),
85
- reservePriorities: z.array(z.object({
86
- position: z.number().int().min(1).max(6),
87
- reserves: z.array(playerInstanceSchema)
85
+ injuryReplacements: z.array(z.object({
86
+ starter: playerInstanceSchema,
87
+ replacement: playerInstanceSchema,
88
+ benchBackfill: playerInstanceSchema.optional()
88
89
  })).default([])
89
90
  }).superRefine((data, ctx) => {
90
91
  // Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
@@ -183,27 +184,41 @@ export const TacticsInputSchema = z.object({
183
184
  }
184
185
  }
185
186
  }
186
- // Reserve priorities (injuries). A reserve must be a roster member OUTSIDE the lineup (starters, libero zone
187
- // and bench refill themselves via subs); each position appears at most once; no player sits in two lists; and
188
- // the second libero cannot double as a reserve (its promotion would strand the L2 swap validation).
189
- const seenReservePositions = new Set();
190
- const seenReserveIds = new Set();
191
- for (const rp of data.reservePriorities) {
192
- if (seenReservePositions.has(rp.position)) {
193
- ctx.addIssue({ code: 'custom', message: 'RESERVE_PRIORITY_DUPLICATE_POSITION', path: ['reservePriorities'] });
187
+ // Injury replacements (owner rules 2026-07-03). starter = a court starter, at most one entry each.
188
+ // replacement = a bench player or a reserve (never another starter, the libero, or the L2). benchBackfill
189
+ // only when the replacement came from the bench, and must be a RESERVE (outside the whole lineup, not the
190
+ // L2). No player appears as a replacement or backfill more than once across all entries.
191
+ const benchIds = new Set(data.lineup.bench.map((p) => p.id));
192
+ const liberoId = data.lineup[LIBERO_ZONE]?.id;
193
+ const seenInjuryStarters = new Set();
194
+ const seenIncoming = new Set();
195
+ for (const ir of data.injuryReplacements) {
196
+ if (!starterIds.has(ir.starter.id)) {
197
+ ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_STARTER_NOT_A_STARTER', path: ['injuryReplacements'] });
194
198
  }
195
- seenReservePositions.add(rp.position);
196
- for (const reserve of rp.reserves) {
197
- if (inLineupIds.has(reserve.id)) {
198
- ctx.addIssue({ code: 'custom', message: 'RESERVE_MUST_NOT_BE_IN_LINEUP', path: ['reservePriorities'] });
199
+ if (seenInjuryStarters.has(ir.starter.id)) {
200
+ ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_DUPLICATE_STARTER', path: ['injuryReplacements'] });
201
+ }
202
+ seenInjuryStarters.add(ir.starter.id);
203
+ const isReplacementBench = benchIds.has(ir.replacement.id);
204
+ if (starterIds.has(ir.replacement.id) || ir.replacement.id === liberoId || ir.replacement.id === data.secondLibero?.id) {
205
+ ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE', path: ['injuryReplacements'] });
206
+ }
207
+ if (seenIncoming.has(ir.replacement.id)) {
208
+ ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_PLAYER_USED_TWICE', path: ['injuryReplacements'] });
209
+ }
210
+ seenIncoming.add(ir.replacement.id);
211
+ if (ir.benchBackfill != null) {
212
+ if (!isReplacementBench) {
213
+ ctx.addIssue({ code: 'custom', message: 'BENCH_BACKFILL_REQUIRES_BENCH_REPLACEMENT', path: ['injuryReplacements'] });
199
214
  }
200
- if (data.secondLibero != null && reserve.id === data.secondLibero.id) {
201
- ctx.addIssue({ code: 'custom', message: 'RESERVE_MUST_NOT_BE_SECOND_LIBERO', path: ['reservePriorities'] });
215
+ if (inLineupIds.has(ir.benchBackfill.id) || ir.benchBackfill.id === data.secondLibero?.id) {
216
+ ctx.addIssue({ code: 'custom', message: 'BENCH_BACKFILL_MUST_BE_RESERVE', path: ['injuryReplacements'] });
202
217
  }
203
- if (seenReserveIds.has(reserve.id)) {
204
- ctx.addIssue({ code: 'custom', message: 'RESERVE_IN_MULTIPLE_LISTS', path: ['reservePriorities'] });
218
+ if (seenIncoming.has(ir.benchBackfill.id)) {
219
+ ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_PLAYER_USED_TWICE', path: ['injuryReplacements'] });
205
220
  }
206
- seenReserveIds.add(reserve.id);
221
+ seenIncoming.add(ir.benchBackfill.id);
207
222
  }
208
223
  }
209
224
  });
@@ -132,9 +132,10 @@ export declare const TeamInputSchema: z.ZodObject<{
132
132
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
133
133
  }, z.core.$strip>>>>;
134
134
  replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
135
- reservePriorities: z.ZodDefault<z.ZodArray<z.ZodObject<{
136
- position: z.ZodNumber;
137
- reserves: z.ZodArray<z.ZodCustom<Player, Player>>;
135
+ injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
136
+ starter: z.ZodCustom<Player, Player>;
137
+ replacement: z.ZodCustom<Player, Player>;
138
+ benchBackfill: z.ZodOptional<z.ZodCustom<Player, Player>>;
138
139
  }, z.core.$strip>>>;
139
140
  }, z.core.$strip>>;
140
141
  }, z.core.$strip>;
@@ -5,7 +5,7 @@ import { LiberoSubConfig } from './libero-sub';
5
5
  import { SubBand } from './energy-band';
6
6
  import { RotationSystemEnum } from './rotation-system';
7
7
  import { OffensivePreference } from './offensive-preference';
8
- import { ReservePriority } from './reserve-priority';
8
+ import { InjuryReplacement } from './injury-replacement';
9
9
  export interface StartingLineup {
10
10
  readonly [CourtPosition.LEFT_FRONT]: Player;
11
11
  readonly [CourtPosition.MIDDLE_FRONT]: Player;
@@ -35,7 +35,7 @@ export interface TacticsOpts {
35
35
  readonly systemSets?: SystemSet[];
36
36
  readonly offensivePreferenceSets?: OffensivePreference[][];
37
37
  readonly replaceInjuredImmediately: boolean;
38
- readonly reservePriorities: ReservePriority[];
38
+ readonly injuryReplacements: InjuryReplacement[];
39
39
  }
40
40
  export declare class Tactics {
41
41
  readonly lineup: StartingLineup;
@@ -52,7 +52,7 @@ export declare class Tactics {
52
52
  readonly systemSets?: SystemSet[];
53
53
  readonly offensivePreferenceSets?: OffensivePreference[][];
54
54
  readonly replaceInjuredImmediately: boolean;
55
- readonly reservePriorities: ReservePriority[];
55
+ readonly injuryReplacements: InjuryReplacement[];
56
56
  static create(input: unknown): Tactics;
57
57
  private constructor();
58
58
  findPlayerInLineup(id: string): CourtPosition | undefined;
@@ -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, replaceInjuredImmediately = true, reservePriorities = [] }) {
18
+ constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceInjuredImmediately = true, injuryReplacements = [] }) {
19
19
  this.lineup = lineup;
20
20
  this.liberoReplacements = liberoReplacements;
21
21
  this.receiveRotationOffset = receiveRotationOffset;
@@ -30,7 +30,7 @@ export class Tactics {
30
30
  this.systemSets = systemSets;
31
31
  this.offensivePreferenceSets = offensivePreferenceSets;
32
32
  this.replaceInjuredImmediately = replaceInjuredImmediately;
33
- this.reservePriorities = reservePriorities;
33
+ this.injuryReplacements = injuryReplacements;
34
34
  }
35
35
  findPlayerInLineup(id) {
36
36
  if (this.lineup.bench.some(p => p.id === id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.464",
3
+ "version": "0.0.465",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",
@@ -1,6 +0,0 @@
1
- import { CourtPosition } from '../match';
2
- import { Player } from '../player';
3
- export interface ReservePriority {
4
- readonly position: CourtPosition;
5
- readonly reserves: Player[];
6
- }
@@ -1,125 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const globals_1 = require("@jest/globals");
4
- const tactics_z_1 = require("./tactics.z");
5
- const test_helpers_1 = require("../../test-helpers");
6
- (0, globals_1.describe)('TacticsInputSchema — injuries (replaceInjuredImmediately / reservePriorities)', () => {
7
- const country = (0, test_helpers_1.makeCountry)();
8
- const p1 = (0, test_helpers_1.makePlayer)(country);
9
- const p2 = (0, test_helpers_1.makePlayer)(country);
10
- const p3 = (0, test_helpers_1.makePlayer)(country);
11
- const p4 = (0, test_helpers_1.makePlayer)(country);
12
- const p5 = (0, test_helpers_1.makePlayer)(country);
13
- const p6 = (0, test_helpers_1.makePlayer)(country);
14
- const benchPlayer = (0, test_helpers_1.makePlayer)(country);
15
- const libero = (0, test_helpers_1.makePlayer)(country);
16
- const reserveA = (0, test_helpers_1.makePlayer)(country);
17
- const reserveB = (0, test_helpers_1.makePlayer)(country);
18
- function baseInput(overrides = {}) {
19
- return {
20
- lineup: {
21
- 4: p4,
22
- 3: p3,
23
- 2: p2,
24
- 5: p5,
25
- 6: p6,
26
- 1: p1,
27
- 0: libero,
28
- bench: [benchPlayer]
29
- },
30
- liberoReplacements: [],
31
- ...overrides
32
- };
33
- }
34
- (0, globals_1.it)('defaults replaceInjuredImmediately to true and reservePriorities to empty', () => {
35
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
36
- (0, globals_1.expect)(res.success).toBe(true);
37
- if (res.success) {
38
- (0, globals_1.expect)(res.data.replaceInjuredImmediately).toBe(true);
39
- (0, globals_1.expect)(res.data.reservePriorities).toEqual([]);
40
- }
41
- });
42
- (0, globals_1.it)('accepts replaceInjuredImmediately false', () => {
43
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ replaceInjuredImmediately: false }));
44
- (0, globals_1.expect)(res.success).toBe(true);
45
- if (res.success)
46
- (0, globals_1.expect)(res.data.replaceInjuredImmediately).toBe(false);
47
- });
48
- (0, globals_1.it)('accepts reserve lists of non-lineup roster players', () => {
49
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
50
- reservePriorities: [
51
- { position: 4, reserves: [reserveA] },
52
- { position: 3, reserves: [reserveB] }
53
- ]
54
- }));
55
- (0, globals_1.expect)(res.success).toBe(true);
56
- });
57
- (0, globals_1.it)('rejects a reserve that is a starter', () => {
58
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
59
- reservePriorities: [{ position: 4, reserves: [p1] }]
60
- }));
61
- (0, globals_1.expect)(res.success).toBe(false);
62
- if (!res.success) {
63
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
64
- }
65
- });
66
- (0, globals_1.it)('rejects a reserve that is on the bench', () => {
67
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
68
- reservePriorities: [{ position: 4, reserves: [benchPlayer] }]
69
- }));
70
- (0, globals_1.expect)(res.success).toBe(false);
71
- if (!res.success) {
72
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
73
- }
74
- });
75
- (0, globals_1.it)('rejects a reserve that is the libero', () => {
76
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
77
- reservePriorities: [{ position: 4, reserves: [libero] }]
78
- }));
79
- (0, globals_1.expect)(res.success).toBe(false);
80
- if (!res.success) {
81
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
82
- }
83
- });
84
- (0, globals_1.it)('rejects a reserve that doubles as the second libero', () => {
85
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
86
- secondLibero: reserveA,
87
- liberoSub: { mode: 'ALWAYS' },
88
- reservePriorities: [{ position: 4, reserves: [reserveA] }]
89
- }));
90
- (0, globals_1.expect)(res.success).toBe(false);
91
- if (!res.success) {
92
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_SECOND_LIBERO')).toBe(true);
93
- }
94
- });
95
- (0, globals_1.it)('rejects the same player in two lists', () => {
96
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
97
- reservePriorities: [
98
- { position: 4, reserves: [reserveA] },
99
- { position: 3, reserves: [reserveA] }
100
- ]
101
- }));
102
- (0, globals_1.expect)(res.success).toBe(false);
103
- if (!res.success) {
104
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_IN_MULTIPLE_LISTS')).toBe(true);
105
- }
106
- });
107
- (0, globals_1.it)('rejects a duplicated position', () => {
108
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
109
- reservePriorities: [
110
- { position: 4, reserves: [reserveA] },
111
- { position: 4, reserves: [reserveB] }
112
- ]
113
- }));
114
- (0, globals_1.expect)(res.success).toBe(false);
115
- if (!res.success) {
116
- (0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'RESERVE_PRIORITY_DUPLICATE_POSITION')).toBe(true);
117
- }
118
- });
119
- (0, globals_1.it)('rejects an out-of-range position', () => {
120
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
121
- reservePriorities: [{ position: 7, reserves: [reserveA] }]
122
- }));
123
- (0, globals_1.expect)(res.success).toBe(false);
124
- });
125
- });
@@ -1,6 +0,0 @@
1
- import { CourtPosition } from '../match';
2
- import { Player } from '../player';
3
- export interface ReservePriority {
4
- readonly position: CourtPosition;
5
- readonly reserves: Player[];
6
- }