volleyballsimtypes 0.0.515 → 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 (37) 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 +1 -0
  10. package/dist/cjs/src/service/team/index.d.ts +1 -0
  11. package/dist/cjs/src/service/team/index.js +1 -0
  12. package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -1
  13. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +15 -5
  14. package/dist/cjs/src/service/team/schemas/tactics.z.js +28 -32
  15. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +25 -92
  16. package/dist/cjs/src/service/team/schemas/team.z.d.ts +15 -5
  17. package/dist/cjs/src/service/team/tactics.d.ts +0 -2
  18. package/dist/cjs/src/service/team/tactics.js +1 -2
  19. package/dist/esm/src/api/index.d.ts +1 -2
  20. package/dist/esm/src/api/tactics-player-refs.js +2 -2
  21. package/dist/esm/src/data/models/tactics.d.ts +2 -4
  22. package/dist/esm/src/data/models/tactics.js +0 -5
  23. package/dist/esm/src/data/transformers/tactics.js +4 -36
  24. package/dist/esm/src/data/transformers/tactics.test.js +9 -46
  25. package/dist/esm/src/service/team/attack-priority.d.ts +3 -0
  26. package/dist/esm/src/service/team/attack-priority.js +8 -0
  27. package/dist/esm/src/service/team/base-config.d.ts +1 -0
  28. package/dist/esm/src/service/team/index.d.ts +1 -0
  29. package/dist/esm/src/service/team/index.js +1 -0
  30. package/dist/esm/src/service/team/offensive-preference.d.ts +2 -1
  31. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +15 -5
  32. package/dist/esm/src/service/team/schemas/tactics.z.js +28 -32
  33. package/dist/esm/src/service/team/schemas/tactics.z.test.js +25 -92
  34. package/dist/esm/src/service/team/schemas/team.z.d.ts +15 -5
  35. package/dist/esm/src/service/team/tactics.d.ts +0 -2
  36. package/dist/esm/src/service/team/tactics.js +1 -2
  37. package/package.json +1 -1
@@ -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,6 +321,7 @@ 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>>>>;
316
326
  attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
317
327
  d: z.ZodNumber;
@@ -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,
@@ -24,7 +24,7 @@ function makeTactics(designatedSubs) {
24
24
  libero_replacement_sets: undefined,
25
25
  rotation_system: '5-1',
26
26
  designated_setters: ['lf'],
27
- offensive_preferences: [{ rotation: 1, weights: { lf: 60, rf: 40 } }],
27
+ offensive_preferences: [{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }],
28
28
  blocking_preferences: []
29
29
  };
30
30
  }
@@ -36,7 +36,7 @@ describe('tacticsColumnsToApiDto()', () => {
36
36
  expect(dto.rotationSystem).toBe('5-1');
37
37
  expect(dto.liberoReplacements).toEqual(['lf']);
38
38
  expect(dto.lineup.bench).toEqual(['b1', 'b2']);
39
- expect(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
39
+ expect(dto.offensivePreferences).toEqual([{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }]);
40
40
  });
41
41
  it('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
42
42
  const dto = tacticsColumnsToApiDto(makeTactics([
@@ -131,32 +131,16 @@ describe('tacticsColumnsToApiDto()', () => {
131
131
  it('passes per-set tactics columns through to the API DTO shape', () => {
132
132
  const model = makeTactics([]);
133
133
  model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
134
- model.offensive_preference_sets = [[{ rotation: 1, weights: { rf: 55, mf: 45 } }]];
134
+ model.offensive_preference_sets = [[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]];
135
135
  const dto = tacticsColumnsToApiDto(model);
136
136
  expect(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
137
- expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, weights: { rf: 55, mf: 45 } }]]);
137
+ expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]]);
138
138
  });
139
139
  it('leaves the per-set DTO fields undefined when the columns are absent', () => {
140
140
  const dto = tacticsColumnsToApiDto(makeTactics([]));
141
141
  expect(dto.systemSets).toBeUndefined();
142
142
  expect(dto.offensivePreferenceSets).toBeUndefined();
143
143
  });
144
- it('surfaces serve attack shares keyed by a libero replacement', () => {
145
- const model = makeTactics([]);
146
- model.serve_attack_shares = { lf: 30 }; // lf is the configured libero_replacement
147
- const dto = tacticsColumnsToApiDto(model);
148
- expect(dto.serveAttackShares).toEqual({ lf: 30 });
149
- });
150
- it('drops a serve attack share keyed by a non-libero-replacement (stale heal)', () => {
151
- const model = makeTactics([]);
152
- model.serve_attack_shares = { lf: 30, rf: 40 }; // rf is not in libero_replacements (['lf'])
153
- const dto = tacticsColumnsToApiDto(model);
154
- expect(dto.serveAttackShares).toEqual({ lf: 30 });
155
- });
156
- it('leaves serveAttackShares undefined when the column is empty', () => {
157
- const dto = tacticsColumnsToApiDto(makeTactics([]));
158
- expect(dto.serveAttackShares).toBeUndefined();
159
- });
160
144
  });
161
145
  describe('per-set tactics — service <-> row round-trip', () => {
162
146
  const country = makeCountry();
@@ -174,7 +158,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
174
158
  { rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
175
159
  { rotationSystem: RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
176
160
  ],
177
- offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]
161
+ offensivePreferenceSets: [[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]
178
162
  });
179
163
  it('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
180
164
  const attrs = transformFromTactics(tactics, 'team-1');
@@ -182,7 +166,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
182
166
  { rotationSystem: '5-1', designatedSetters: [p1.id] },
183
167
  { rotationSystem: '6-0', designatedSetters: [] }
184
168
  ]);
185
- expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]);
169
+ expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]);
186
170
  });
187
171
  it('transformToTactics resolves the stored ids back to roster players', () => {
188
172
  const attrs = transformFromTactics(tactics, 'team-1');
@@ -190,7 +174,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
190
174
  expect(back.systemSets?.[0].rotationSystem).toBe(RotationSystemEnum.FIVE_ONE);
191
175
  expect(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
192
176
  expect(back.systemSets?.[1].designatedSetters).toEqual([]);
193
- expect(back.offensivePreferenceSets?.[0][0].weights).toEqual({ [p2.id]: 60, [p3.id]: 40 });
177
+ expect(back.offensivePreferenceSets?.[0][0].priorities).toEqual({ [p2.id]: 'HIGH', [p3.id]: 'MID' });
194
178
  });
195
179
  it('omits the per-set columns entirely for an all-sets config', () => {
196
180
  const flat = Tactics.create({
@@ -204,27 +188,6 @@ describe('per-set tactics — service <-> row round-trip', () => {
204
188
  expect(back.systemSets).toBeUndefined();
205
189
  expect(back.offensivePreferenceSets).toBeUndefined();
206
190
  });
207
- it('round-trips serve attack shares keyed by a libero replacement', () => {
208
- const withServe = Tactics.create({
209
- lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
210
- liberoReplacements: [p5],
211
- serveAttackShares: { [p5.id]: 35 }
212
- });
213
- const attrs = transformFromTactics(withServe, 'team-1');
214
- expect(attrs.serve_attack_shares).toEqual({ [p5.id]: 35 });
215
- const back = transformToTactics(attrs, roster);
216
- expect(back.serveAttackShares).toEqual({ [p5.id]: 35 });
217
- });
218
- it('drops a stale serve attack share key on load (lenient heal, no crash)', () => {
219
- const attrs = transformFromTactics(Tactics.create({
220
- lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
221
- liberoReplacements: [p5],
222
- serveAttackShares: { [p5.id]: 35 }
223
- }), 'team-1');
224
- attrs.libero_replacements = [];
225
- const back = transformToTactics(attrs, roster);
226
- expect(back.serveAttackShares).toEqual({});
227
- });
228
191
  it('round-trips base positions (coords + reception participation)', () => {
229
192
  const config = {
230
193
  coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
@@ -253,11 +216,11 @@ describe('per-set tactics — service <-> row round-trip', () => {
253
216
  const withTempos = Tactics.create({
254
217
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
255
218
  liberoReplacements: [],
256
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
219
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
257
220
  });
258
221
  const attrs = transformFromTactics(withTempos, 'team-1');
259
222
  expect(attrs.offensive_preferences).toEqual([
260
- { rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
223
+ { rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
261
224
  ]);
262
225
  const back = transformToTactics(attrs, roster);
263
226
  expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
@@ -0,0 +1,3 @@
1
+ export type AttackPriority = 'HIGH' | 'MID' | 'LOW' | 'NONE';
2
+ export declare const ATTACK_PRIORITIES: readonly AttackPriority[];
3
+ export declare const PRIORITY_WEIGHT: Record<AttackPriority, number>;
@@ -0,0 +1,8 @@
1
+ export const ATTACK_PRIORITIES = ['HIGH', 'MID', 'LOW', 'NONE'];
2
+ // The relative pull of each priority. Tunable in one place; the engine reads these to weight the setter's target.
3
+ export const PRIORITY_WEIGHT = {
4
+ HIGH: 20,
5
+ MID: 10,
6
+ LOW: 5,
7
+ NONE: 0
8
+ };
@@ -7,6 +7,7 @@ export interface BaseCoord {
7
7
  export interface BaseConfig {
8
8
  readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
9
9
  readonly receive: Record<string, Record<string, boolean>>;
10
+ readonly receiveByPlayer?: Record<string, Record<string, boolean>>;
10
11
  readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
11
12
  readonly attackContact?: Record<string, Record<string, BaseCoord>>;
12
13
  }
@@ -3,6 +3,7 @@ export * from './team-generator';
3
3
  export * from './formation';
4
4
  export * from './tactics';
5
5
  export * from './base-config';
6
+ export * from './attack-priority';
6
7
  export * from './default-base-config';
7
8
  export * from './receive-overlap';
8
9
  export * from './team-name';
@@ -3,6 +3,7 @@ export * from './team-generator';
3
3
  export * from './formation';
4
4
  export * from './tactics';
5
5
  export * from './base-config';
6
+ export * from './attack-priority';
6
7
  export * from './default-base-config';
7
8
  export * from './receive-overlap';
8
9
  export * from './team-name';
@@ -1,6 +1,7 @@
1
+ import { AttackPriority } from './attack-priority';
1
2
  export type TempoPreference = 'FAST' | 'HIGH';
2
3
  export interface OffensivePreference {
3
4
  readonly rotation: number;
4
- readonly weights: Record<string, number>;
5
+ readonly priorities: Record<string, AttackPriority>;
5
6
  readonly tempos?: Record<string, TempoPreference>;
6
7
  }
@@ -261,23 +261,32 @@ export declare const TacticsInputSchema: z.ZodObject<{
261
261
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
262
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
263
263
  rotation: z.ZodNumber;
264
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
264
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
265
+ HIGH: "HIGH";
266
+ MID: "MID";
267
+ LOW: "LOW";
268
+ NONE: "NONE";
269
+ }>>;
265
270
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
266
- FAST: "FAST";
267
271
  HIGH: "HIGH";
272
+ FAST: "FAST";
268
273
  }>>>;
269
274
  }, z.core.$strip>>>;
270
- serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
271
275
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
272
276
  rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
273
277
  designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
274
278
  }, z.core.$strip>>>;
275
279
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
276
280
  rotation: z.ZodNumber;
277
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
281
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
282
+ HIGH: "HIGH";
283
+ MID: "MID";
284
+ LOW: "LOW";
285
+ NONE: "NONE";
286
+ }>>;
278
287
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
279
- FAST: "FAST";
280
288
  HIGH: "HIGH";
289
+ FAST: "FAST";
281
290
  }>>>;
282
291
  }, z.core.$strip>>>>;
283
292
  blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -304,6 +313,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
304
313
  r: z.ZodNumber;
305
314
  }, z.core.$strip>>>>;
306
315
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
316
+ receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
307
317
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
308
318
  attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
309
319
  d: z.ZodNumber;