volleyballsimtypes 0.0.514 → 0.0.516

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 (43) hide show
  1. package/dist/cjs/src/api/index.d.ts +1 -2
  2. package/dist/cjs/src/api/tactics-player-refs.js +2 -2
  3. package/dist/cjs/src/data/models/tactics.d.ts +2 -4
  4. package/dist/cjs/src/data/models/tactics.js +0 -5
  5. package/dist/cjs/src/data/transformers/tactics.js +4 -36
  6. package/dist/cjs/src/data/transformers/tactics.test.js +9 -46
  7. package/dist/cjs/src/service/team/attack-priority.d.ts +3 -0
  8. package/dist/cjs/src/service/team/attack-priority.js +11 -0
  9. package/dist/cjs/src/service/team/base-config.d.ts +2 -0
  10. package/dist/cjs/src/service/team/default-base-config.js +59 -3
  11. package/dist/cjs/src/service/team/default-base-config.test.d.ts +1 -0
  12. package/dist/cjs/src/service/team/default-base-config.test.js +44 -0
  13. package/dist/cjs/src/service/team/index.d.ts +1 -0
  14. package/dist/cjs/src/service/team/index.js +1 -0
  15. package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -1
  16. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +19 -5
  17. package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -33
  18. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +25 -92
  19. package/dist/cjs/src/service/team/schemas/team.z.d.ts +19 -5
  20. package/dist/cjs/src/service/team/tactics.d.ts +0 -2
  21. package/dist/cjs/src/service/team/tactics.js +1 -2
  22. package/dist/esm/src/api/index.d.ts +1 -2
  23. package/dist/esm/src/api/tactics-player-refs.js +2 -2
  24. package/dist/esm/src/data/models/tactics.d.ts +2 -4
  25. package/dist/esm/src/data/models/tactics.js +0 -5
  26. package/dist/esm/src/data/transformers/tactics.js +4 -36
  27. package/dist/esm/src/data/transformers/tactics.test.js +9 -46
  28. package/dist/esm/src/service/team/attack-priority.d.ts +3 -0
  29. package/dist/esm/src/service/team/attack-priority.js +8 -0
  30. package/dist/esm/src/service/team/base-config.d.ts +2 -0
  31. package/dist/esm/src/service/team/default-base-config.js +59 -3
  32. package/dist/esm/src/service/team/default-base-config.test.d.ts +1 -0
  33. package/dist/esm/src/service/team/default-base-config.test.js +42 -0
  34. package/dist/esm/src/service/team/index.d.ts +1 -0
  35. package/dist/esm/src/service/team/index.js +1 -0
  36. package/dist/esm/src/service/team/offensive-preference.d.ts +2 -1
  37. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +19 -5
  38. package/dist/esm/src/service/team/schemas/tactics.z.js +32 -33
  39. package/dist/esm/src/service/team/schemas/tactics.z.test.js +25 -92
  40. package/dist/esm/src/service/team/schemas/team.z.d.ts +19 -5
  41. package/dist/esm/src/service/team/tactics.d.ts +0 -2
  42. package/dist/esm/src/service/team/tactics.js +1 -2
  43. package/package.json +1 -1
@@ -50,13 +50,14 @@ const lineupSchema = zod_1.z.object({
50
50
  });
51
51
  // Per-player tempo preference (set-tempo rework): absent key = Auto.
52
52
  const tempoPreferenceSchema = zod_1.z.enum(['FAST', 'HIGH']);
53
- // One rotation's attacker weighting. `rotation` is the main setter's rotational slot (1..6); `weights` maps each
54
- // attacker's starter id to an integer percentage share (values sum to 100; 0 removes an attacker). `tempos`
55
- // optionally forces a per-starter tempo (Auto when absent). Cross-field checks (keys are starters, weights sum to
56
- // 100) live in the superRefine below.
53
+ // One rotation's attacker weighting. `rotation` is the main setter's rotational slot (1..6); `priorities` maps a
54
+ // player id (a starter, or a designated substitute that overrides its slot once on court) to an AttackPriority.
55
+ // `tempos` optionally forces a per-player tempo (Auto when absent). Cross-field checks (keys are starters or
56
+ // designated subs) live in the superRefine below. No sum constraint: the setter weights by PRIORITY_WEIGHT.
57
+ const attackPrioritySchema = zod_1.z.enum(['HIGH', 'MID', 'LOW', 'NONE']);
57
58
  const offensivePreferenceSchema = zod_1.z.object({
58
59
  rotation: zod_1.z.number().int().min(1).max(6),
59
- weights: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(0)),
60
+ priorities: zod_1.z.record(zod_1.z.string(), attackPrioritySchema),
60
61
  tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
61
62
  });
62
63
  // An opposing attacking function a front-row starter can commit to (every RoleEnum except the libero, which never
@@ -90,9 +91,15 @@ const baseCoordSchema = zod_1.z.object({ d: zod_1.z.number().finite(), r: zod_1.
90
91
  const baseConfigSchema = zod_1.z.object({
91
92
  coords: 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))),
92
93
  receive: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean())),
94
+ // Optional per-player receive override (owner 2026-08-13): [rotation][playerId] -> passes. Keys (designated subs)
95
+ // are validated in the superRefine. Optional so configs saved before this field validate unchanged.
96
+ receiveByPlayer: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean())).optional(),
93
97
  // Optional per-point-of-attack blocking (owner 2026-08-07): [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock.
94
98
  // Optional so presets/tactics saved before this field validate unchanged.
95
- 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()
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
+ // Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
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()
96
103
  });
97
104
  exports.TacticsInputSchema = zod_1.z.object({
98
105
  lineup: lineupSchema,
@@ -114,9 +121,6 @@ exports.TacticsInputSchema = zod_1.z.object({
114
121
  offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([]),
115
122
  // Attack shares on serve (libero-serve feature): per-player attack share the sim applies when a
116
123
  // libero-replaced player is on court unreplaced (serving, or through a libero injury). Keyed by starter id.
117
- // This is NOT part of the offensivePreferences sum-to-100; its band (0-100) and libero-replacement-key rules
118
- // are validated independently in the superRefine below.
119
- serveAttackShares: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int()).default({}),
120
124
  // Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
121
125
  // validated with the same cross-field rules as the flat fields (see superRefine below).
122
126
  systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
@@ -164,6 +168,11 @@ exports.TacticsInputSchema = zod_1.z.object({
164
168
  const starters = slots.map(([position, player]) => ({ position, id: player.id }));
165
169
  const starterIds = new Set(starters.map(s => s.id));
166
170
  const positionById = new Map(starters.map(s => [s.id, s.position]));
171
+ // Any bench player may carry its OWN per-player attack priority / tempo / block / receive (owner 2026-08-13):
172
+ // those maps are keyed by a player id, and the engine resolves them for whoever is on court. So the attack /
173
+ // block / receive key checks accept a starter OR any bench player (every bench player gets its own settings, not
174
+ // just the designated subs).
175
+ const allowedPlayerKeys = new Set([...starterIds, ...data.lineup.bench.map(p => p.id)]);
167
176
  // Designated setters: count must match the system, each must be an on-court starter, and in 4-2 / 6-2 the two
168
177
  // setters sit in opposite slots (3 rotations apart). Run on the flat fields and on every per-set entry.
169
178
  const validateSystem = (rotationSystem, designatedSetters, path) => {
@@ -194,19 +203,15 @@ exports.TacticsInputSchema = zod_1.z.object({
194
203
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
195
204
  }
196
205
  seenRotations.add(pref.rotation);
197
- // Weight keys are on-court starters, and the percentages must sum to exactly 100 (0 removes an attacker).
198
- for (const id of Object.keys(pref.weights)) {
199
- if (!starterIds.has(id)) {
206
+ // Priority + tempo keys are an on-court starter or a designated sub (no sum-to-100: the setter weights by
207
+ // PRIORITY_WEIGHT and normalizes by the real total).
208
+ for (const id of Object.keys(pref.priorities)) {
209
+ if (!allowedPlayerKeys.has(id)) {
200
210
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
201
211
  }
202
212
  }
203
- const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
204
- if (total !== 100) {
205
- ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
206
- }
207
- // Tempo preference keys are starter ids too.
208
213
  for (const id of Object.keys(pref.tempos ?? {})) {
209
- if (!starterIds.has(id)) {
214
+ if (!allowedPlayerKeys.has(id)) {
210
215
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
211
216
  }
212
217
  }
@@ -223,13 +228,13 @@ exports.TacticsInputSchema = zod_1.z.object({
223
228
  }
224
229
  seenRotations.add(pref.rotation);
225
230
  for (const id of Object.keys(pref.commit)) {
226
- if (!starterIds.has(id)) {
231
+ if (!allowedPlayerKeys.has(id)) {
227
232
  ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
228
233
  }
229
234
  }
230
235
  for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
231
236
  for (const id of pref.noBlock[form] ?? []) {
232
- if (!starterIds.has(id)) {
237
+ if (!allowedPlayerKeys.has(id)) {
233
238
  ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
234
239
  }
235
240
  }
@@ -248,19 +253,13 @@ exports.TacticsInputSchema = zod_1.z.object({
248
253
  for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
249
254
  validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
250
255
  }
251
- // Serve attack shares (libero-serve feature): only a current libero-replacement starter (flat or per-set) may
252
- // carry one, and each value must be in the 0-100 band. Validated independently of the offensive-preference
253
- // sum-to-100 (a serve share never enters that total).
254
- const liberoReplacementIds = new Set([
255
- ...data.liberoReplacements.map(p => p.id),
256
- ...(data.liberoReplacementSets ?? []).flatMap(set => set.map(p => p.id))
257
- ]);
258
- for (const [id, value] of Object.entries(data.serveAttackShares)) {
259
- if (!liberoReplacementIds.has(id)) {
260
- ctx.addIssue({ code: 'custom', message: 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT', path: ['serveAttackShares'] });
261
- }
262
- if (value < 0 || value > 100) {
263
- ctx.addIssue({ code: 'custom', message: 'SERVE_ATTACK_SHARE_OUT_OF_BAND', path: ['serveAttackShares'] });
256
+ // Per-player receive override (substitute settings, owner 2026-08-13): [rotation][playerId] -> passes. Each inner
257
+ // key is an on-court starter or a designated sub.
258
+ for (const byPlayer of Object.values(data.basePositions?.receiveByPlayer ?? {})) {
259
+ for (const id of Object.keys(byPlayer)) {
260
+ if (!allowedPlayerKeys.has(id)) {
261
+ ctx.addIssue({ code: 'custom', message: 'RECEIVE_OVERRIDE_NOT_A_STARTER', path: ['basePositions', 'receiveByPlayer'] });
262
+ }
264
263
  }
265
264
  }
266
265
  // Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
@@ -96,7 +96,7 @@ const test_helpers_1 = require("../../test-helpers");
96
96
  { rotationSystem: '4-2', designatedSetters: [p1, p4] }
97
97
  ],
98
98
  offensivePreferenceSets: [
99
- [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }],
99
+ [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }],
100
100
  []
101
101
  ]
102
102
  }));
@@ -105,7 +105,7 @@ const test_helpers_1 = require("../../test-helpers");
105
105
  (0, globals_1.it)('carries the per-set fields through Tactics.create parsing', () => {
106
106
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
107
107
  systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
108
- offensivePreferenceSets: [[{ rotation: 2, weights: { [p3.id]: 100 } }]]
108
+ offensivePreferenceSets: [[{ rotation: 2, priorities: { [p3.id]: 'HIGH' } }]]
109
109
  }));
110
110
  (0, globals_1.expect)(res.success).toBe(true);
111
111
  if (res.success) {
@@ -154,7 +154,7 @@ const test_helpers_1 = require("../../test-helpers");
154
154
  });
155
155
  (0, globals_1.it)('rejects a per-set preferences list with a duplicate rotation', () => {
156
156
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
157
- offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 100 } }, { rotation: 1, weights: { [p3.id]: 100 } }]]
157
+ offensivePreferenceSets: [[{ rotation: 1, priorities: { [p2.id]: 'HIGH' } }, { rotation: 1, priorities: { [p3.id]: 'HIGH' } }]]
158
158
  }));
159
159
  (0, globals_1.expect)(res.success).toBe(false);
160
160
  if (!res.success) {
@@ -164,25 +164,38 @@ const test_helpers_1 = require("../../test-helpers");
164
164
  });
165
165
  (0, globals_1.it)('rejects a per-set preferences map that keys a non-starter', () => {
166
166
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
167
- offensivePreferenceSets: [[{ rotation: 1, weights: { [reserve.id]: 100 } }]]
167
+ offensivePreferenceSets: [[{ rotation: 1, priorities: { [reserve.id]: 'HIGH' } }]]
168
168
  }));
169
169
  (0, globals_1.expect)(res.success).toBe(false);
170
170
  if (!res.success) {
171
171
  (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
172
172
  }
173
173
  });
174
- (0, globals_1.it)('rejects offensive preference weights that do not sum to 100', () => {
174
+ (0, globals_1.it)('accepts offensive priorities with no sum constraint', () => {
175
175
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
176
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 50, [p3.id]: 40 } }]
176
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'LOW' } }]
177
+ }));
178
+ (0, globals_1.expect)(res.success).toBe(true);
179
+ });
180
+ (0, globals_1.it)('rejects an invalid attack priority value', () => {
181
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
182
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'MEGA' } }]
177
183
  }));
178
184
  (0, globals_1.expect)(res.success).toBe(false);
179
- if (!res.success) {
180
- (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100')).toBe(true);
181
- }
185
+ });
186
+ (0, globals_1.it)('accepts a priority / tempo / no-block keyed by a designated substitute', () => {
187
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
188
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [reserve] },
189
+ designatedSubs: [{ starter: p2, bench: reserve }],
190
+ offensivePreferences: [{ rotation: 1, priorities: { [reserve.id]: 'HIGH' }, tempos: { [reserve.id]: 'FAST' } }],
191
+ blockingPreferences: [{ rotation: 1, commit: {}, noBlock: { DEF_L: [reserve.id], DEF_M: [], DEF_R: [] } }],
192
+ basePositions: { coords: {}, receive: {}, receiveByPlayer: { 1: { [reserve.id]: false } } }
193
+ }));
194
+ (0, globals_1.expect)(res.success).toBe(true);
182
195
  });
183
196
  (0, globals_1.it)('accepts offensive preferences with a starter-keyed tempos map', () => {
184
197
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
185
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
198
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
186
199
  }));
187
200
  (0, globals_1.expect)(res.success).toBe(true);
188
201
  if (res.success) {
@@ -191,7 +204,7 @@ const test_helpers_1 = require("../../test-helpers");
191
204
  });
192
205
  (0, globals_1.it)('rejects a tempos map keyed by a non-starter', () => {
193
206
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
194
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [reserve.id]: 'FAST' } }]
207
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH' }, tempos: { [reserve.id]: 'FAST' } }]
195
208
  }));
196
209
  (0, globals_1.expect)(res.success).toBe(false);
197
210
  if (!res.success) {
@@ -200,7 +213,7 @@ const test_helpers_1 = require("../../test-helpers");
200
213
  });
201
214
  (0, globals_1.it)('rejects an invalid tempo value', () => {
202
215
  const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
203
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [p2.id]: 'SLOW' } }]
216
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH' }, tempos: { [p2.id]: 'SLOW' } }]
204
217
  }));
205
218
  (0, globals_1.expect)(res.success).toBe(false);
206
219
  });
@@ -325,83 +338,3 @@ const test_helpers_1 = require("../../test-helpers");
325
338
  (0, globals_1.expect)(res.success).toBe(false);
326
339
  });
327
340
  });
328
- (0, globals_1.describe)('TacticsInputSchema — serve attack shares', () => {
329
- const country = (0, test_helpers_1.makeCountry)();
330
- const p1 = (0, test_helpers_1.makePlayer)(country);
331
- const p2 = (0, test_helpers_1.makePlayer)(country);
332
- const p3 = (0, test_helpers_1.makePlayer)(country);
333
- const p4 = (0, test_helpers_1.makePlayer)(country);
334
- const p5 = (0, test_helpers_1.makePlayer)(country);
335
- const p6 = (0, test_helpers_1.makePlayer)(country);
336
- const reserve = (0, test_helpers_1.makePlayer)(country);
337
- // p5 sits at zone 5 (back row) and is the configured libero replacement, so it may carry a serve share.
338
- function baseInput(overrides = {}) {
339
- return {
340
- lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
341
- liberoReplacements: [p5],
342
- ...overrides
343
- };
344
- }
345
- (0, globals_1.it)('accepts a serve share keyed by a libero replacement, value in band', () => {
346
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 30 } }));
347
- (0, globals_1.expect)(res.success).toBe(true);
348
- if (res.success)
349
- (0, globals_1.expect)(res.data.serveAttackShares[p5.id]).toBe(30);
350
- });
351
- (0, globals_1.it)('accepts 0 and 100 at the band edges', () => {
352
- (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 0 } })).success).toBe(true);
353
- (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 100 } })).success).toBe(true);
354
- });
355
- (0, globals_1.it)('rejects a serve share keyed by a starter who is not a libero replacement', () => {
356
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p2.id]: 30 } }));
357
- (0, globals_1.expect)(res.success).toBe(false);
358
- if (!res.success) {
359
- (0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT')).toBe(true);
360
- }
361
- });
362
- (0, globals_1.it)('rejects a serve share keyed by a reserve', () => {
363
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [reserve.id]: 30 } }));
364
- (0, globals_1.expect)(res.success).toBe(false);
365
- if (!res.success) {
366
- (0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT')).toBe(true);
367
- }
368
- });
369
- (0, globals_1.it)('rejects an out-of-band serve share (over 100)', () => {
370
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 101 } }));
371
- (0, globals_1.expect)(res.success).toBe(false);
372
- if (!res.success) {
373
- (0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_OUT_OF_BAND')).toBe(true);
374
- }
375
- });
376
- (0, globals_1.it)('rejects a negative serve share', () => {
377
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: -1 } }));
378
- (0, globals_1.expect)(res.success).toBe(false);
379
- if (!res.success) {
380
- (0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_OUT_OF_BAND')).toBe(true);
381
- }
382
- });
383
- (0, globals_1.it)('validates independently of the offensive-preference sum-to-100 (never enters that total)', () => {
384
- // The other attackers' weights sum to exactly 100; the serve share on the libero replacement is separate
385
- // and must not push the rotation over 100 or otherwise fail.
386
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
387
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 50, [p3.id]: 50 } }],
388
- serveAttackShares: { [p5.id]: 40 }
389
- }));
390
- (0, globals_1.expect)(res.success).toBe(true);
391
- });
392
- (0, globals_1.it)('accepts a serve share on a per-set libero replacement', () => {
393
- // p6 is a back-row starter made a libero replacement only via liberoReplacementSets.
394
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
395
- liberoReplacements: [],
396
- liberoReplacementSets: [[p6]],
397
- serveAttackShares: { [p6.id]: 25 }
398
- }));
399
- (0, globals_1.expect)(res.success).toBe(true);
400
- });
401
- (0, globals_1.it)('defaults serveAttackShares to an empty map when omitted', () => {
402
- const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
403
- (0, globals_1.expect)(res.success).toBe(true);
404
- if (res.success)
405
- (0, globals_1.expect)(res.data.serveAttackShares).toEqual({});
406
- });
407
- });
@@ -269,23 +269,32 @@ export declare const TeamInputSchema: z.ZodObject<{
269
269
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
270
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
271
  rotation: z.ZodNumber;
272
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
272
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
273
+ HIGH: "HIGH";
274
+ MID: "MID";
275
+ LOW: "LOW";
276
+ NONE: "NONE";
277
+ }>>;
273
278
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
274
- FAST: "FAST";
275
279
  HIGH: "HIGH";
280
+ FAST: "FAST";
276
281
  }>>>;
277
282
  }, z.core.$strip>>>;
278
- serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
279
283
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
280
284
  rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
281
285
  designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
282
286
  }, z.core.$strip>>>;
283
287
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
284
288
  rotation: z.ZodNumber;
285
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
289
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
290
+ HIGH: "HIGH";
291
+ MID: "MID";
292
+ LOW: "LOW";
293
+ NONE: "NONE";
294
+ }>>;
286
295
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
287
- FAST: "FAST";
288
296
  HIGH: "HIGH";
297
+ FAST: "FAST";
289
298
  }>>>;
290
299
  }, z.core.$strip>>>>;
291
300
  blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -312,7 +321,12 @@ export declare const TeamInputSchema: z.ZodObject<{
312
321
  r: z.ZodNumber;
313
322
  }, z.core.$strip>>>>;
314
323
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
324
+ receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
315
325
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
326
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
327
+ d: z.ZodNumber;
328
+ r: z.ZodNumber;
329
+ }, z.core.$strip>>>>;
316
330
  }, z.core.$strip>>;
317
331
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
318
332
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -35,7 +35,6 @@ export interface TacticsOpts {
35
35
  readonly rotationSystem: RotationSystemEnum;
36
36
  readonly designatedSetters: Player[];
37
37
  readonly offensivePreferences: OffensivePreference[];
38
- readonly serveAttackShares: Record<string, number>;
39
38
  readonly systemSets?: SystemSet[];
40
39
  readonly offensivePreferenceSets?: OffensivePreference[][];
41
40
  readonly replaceKnockedImmediately: Record<string, boolean>;
@@ -57,7 +56,6 @@ export declare class Tactics {
57
56
  readonly rotationSystem: RotationSystemEnum;
58
57
  readonly designatedSetters: Player[];
59
58
  readonly offensivePreferences: OffensivePreference[];
60
- readonly serveAttackShares: Record<string, number>;
61
59
  readonly systemSets?: SystemSet[];
62
60
  readonly offensivePreferenceSets?: OffensivePreference[][];
63
61
  readonly replaceKnockedImmediately: Record<string, boolean>;
@@ -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 = [], serveAttackShares = {}, 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 = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
22
22
  this.lineup = lineup;
23
23
  this.liberoReplacements = liberoReplacements;
24
24
  this.receiveRotationOffset = receiveRotationOffset;
@@ -30,7 +30,6 @@ class Tactics {
30
30
  this.rotationSystem = rotationSystem;
31
31
  this.designatedSetters = designatedSetters;
32
32
  this.offensivePreferences = offensivePreferences;
33
- this.serveAttackShares = serveAttackShares;
34
33
  this.systemSets = systemSets;
35
34
  this.offensivePreferenceSets = offensivePreferenceSets;
36
35
  this.replaceKnockedImmediately = replaceKnockedImmediately;
@@ -97,7 +97,7 @@ export interface ApiDesignatedSub extends ApiSetSubConfig {
97
97
  }
98
98
  export interface ApiOffensivePreference {
99
99
  rotation: number;
100
- weights: Record<string, number>;
100
+ priorities: Record<string, 'HIGH' | 'MID' | 'LOW' | 'NONE'>;
101
101
  tempos?: Record<string, 'FAST' | 'HIGH'>;
102
102
  }
103
103
  export interface ApiBlockingPreference {
@@ -140,7 +140,6 @@ export interface Tactics {
140
140
  offensivePreferences: ApiOffensivePreference[];
141
141
  systemSets?: ApiSystemSet[];
142
142
  offensivePreferenceSets?: ApiOffensivePreference[][];
143
- serveAttackShares?: Record<string, number>;
144
143
  basePositions?: BaseConfig;
145
144
  blockingPreferences: ApiBlockingPreference[];
146
145
  blockingPreferenceSets?: ApiBlockingPreference[][];
@@ -23,9 +23,9 @@ export function collectTacticsPlayerIds(config) {
23
23
  config.secondLibero,
24
24
  ...((config.liberoReplacementSets ?? []).flat()),
25
25
  ...(config.designatedSetters ?? []),
26
- ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.weights))),
26
+ ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
27
27
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
28
- ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
28
+ ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
29
29
  ...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
30
30
  ...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
31
31
  // Injury replacements reference reserves (roster players outside the lineup); roster membership is the only
@@ -31,7 +31,7 @@ export interface DesignatedSubAttributes extends SetSubConfigAttributes {
31
31
  }
32
32
  export interface OffensivePreferenceAttributes {
33
33
  rotation: number;
34
- weights: Record<PlayerId, number>;
34
+ priorities: Record<PlayerId, 'HIGH' | 'MID' | 'LOW' | 'NONE'>;
35
35
  tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
36
36
  }
37
37
  export interface BlockingPreferenceAttributes {
@@ -77,7 +77,6 @@ export interface TacticsAttributes {
77
77
  offensive_preferences: OffensivePreferenceAttributes[];
78
78
  system_sets?: SystemSetAttributes[];
79
79
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
80
- serve_attack_shares: Record<PlayerId, number>;
81
80
  replace_knocked_immediately?: Record<PlayerId, boolean>;
82
81
  injury_replacements?: InjuryReplacementAttributes[];
83
82
  libero_injury?: LiberoInjuryAttributes;
@@ -87,7 +86,7 @@ export interface TacticsAttributes {
87
86
  }
88
87
  export type TacticsPk = 'team_id';
89
88
  export type TacticsId = TacticsModel[TacticsPk];
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' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
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';
91
90
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
92
91
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
93
92
  team_id: string;
@@ -104,7 +103,6 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
104
103
  offensive_preferences: OffensivePreferenceAttributes[];
105
104
  system_sets?: SystemSetAttributes[];
106
105
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
107
- serve_attack_shares: Record<PlayerId, number>;
108
106
  replace_knocked_immediately?: Record<PlayerId, boolean>;
109
107
  injury_replacements?: InjuryReplacementAttributes[];
110
108
  libero_injury?: LiberoInjuryAttributes;
@@ -72,11 +72,6 @@ export class TacticsModel extends Model {
72
72
  type: DataTypes.JSONB,
73
73
  allowNull: true
74
74
  },
75
- serve_attack_shares: {
76
- type: DataTypes.JSONB,
77
- allowNull: false,
78
- defaultValue: {}
79
- },
80
75
  replace_knocked_immediately: {
81
76
  type: DataTypes.JSONB,
82
77
  allowNull: true
@@ -67,7 +67,7 @@ function transformToAttributes(tactics, teamId) {
67
67
  designated_setters: tactics.designatedSetters.map((s) => s.id),
68
68
  offensive_preferences: tactics.offensivePreferences.map(p => ({
69
69
  rotation: p.rotation,
70
- weights: p.weights,
70
+ priorities: p.priorities,
71
71
  tempos: p.tempos
72
72
  })),
73
73
  system_sets: tactics.systemSets?.map(s => ({
@@ -76,10 +76,9 @@ function transformToAttributes(tactics, teamId) {
76
76
  })),
77
77
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
78
78
  rotation: p.rotation,
79
- weights: p.weights,
79
+ priorities: p.priorities,
80
80
  tempos: p.tempos
81
81
  }))),
82
- serve_attack_shares: tactics.serveAttackShares ?? {},
83
82
  base_positions: tactics.basePositions,
84
83
  blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
85
84
  blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -175,7 +174,7 @@ function transformToObject(model, roster) {
175
174
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
176
175
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
177
176
  rotation: p.rotation,
178
- weights: p.weights,
177
+ priorities: p.priorities,
179
178
  tempos: p.tempos
180
179
  })),
181
180
  systemSets: model.system_sets?.map(s => ({
@@ -184,24 +183,9 @@ function transformToObject(model, roster) {
184
183
  })),
185
184
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
186
185
  rotation: p.rotation,
187
- weights: p.weights,
186
+ priorities: p.priorities,
188
187
  tempos: p.tempos
189
188
  }))),
190
- // LENIENT: keep only serve shares keyed by a current libero-replacement (flat or per-set) with a value in the
191
- // 0-100 band, so a stale key (a released player, or a libero-replacement config change) can never fail schema
192
- // validation and crash a team load; a dropped key = no serve share.
193
- serveAttackShares: (() => {
194
- const liberoReplacementIds = new Set([
195
- ...model.libero_replacements,
196
- ...(model.libero_replacement_sets ?? []).flat()
197
- ]);
198
- const healed = {};
199
- for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
200
- if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
201
- healed[id] = value;
202
- }
203
- return healed;
204
- })(),
205
189
  basePositions: model.base_positions ?? undefined,
206
190
  blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
207
191
  blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -348,21 +332,6 @@ function tacticsColumnsToApiDto(model) {
348
332
  offensivePreferences: model.offensive_preferences,
349
333
  systemSets: model.system_sets,
350
334
  offensivePreferenceSets: model.offensive_preference_sets,
351
- // Same stale-data policy as the heals below: only serve shares keyed by a current libero-replacement (flat or
352
- // per-set) with a value in the 0-100 band survive; an empty map surfaces as absent so the UI never reloads and
353
- // re-emits a stale key.
354
- serveAttackShares: (() => {
355
- const liberoReplacementIds = new Set([
356
- ...model.libero_replacements,
357
- ...(model.libero_replacement_sets ?? []).flat()
358
- ]);
359
- const healed = {};
360
- for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
361
- if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
362
- healed[id] = value;
363
- }
364
- return Object.keys(healed).length > 0 ? healed : undefined;
365
- })(),
366
335
  basePositions: model.base_positions ?? undefined,
367
336
  blockingPreferences: model.blocking_preferences,
368
337
  blockingPreferenceSets: model.blocking_preference_sets,
@@ -471,7 +440,6 @@ function apiTacticsConfigToAttributes(config, teamId) {
471
440
  offensive_preferences: config.offensivePreferences,
472
441
  system_sets: config.systemSets,
473
442
  offensive_preference_sets: config.offensivePreferenceSets,
474
- serve_attack_shares: config.serveAttackShares ?? {},
475
443
  base_positions: config.basePositions,
476
444
  blocking_preferences: config.blockingPreferences ?? [],
477
445
  blocking_preference_sets: config.blockingPreferenceSets,