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
@@ -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[][];
@@ -26,9 +26,9 @@ function collectTacticsPlayerIds(config) {
26
26
  config.secondLibero,
27
27
  ...((config.liberoReplacementSets ?? []).flat()),
28
28
  ...(config.designatedSetters ?? []),
29
- ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.weights))),
29
+ ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
30
30
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
31
- ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
31
+ ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
32
32
  ...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
33
33
  ...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
34
34
  // 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;
@@ -75,11 +75,6 @@ class TacticsModel extends sequelize_1.Model {
75
75
  type: sequelize_1.DataTypes.JSONB,
76
76
  allowNull: true
77
77
  },
78
- serve_attack_shares: {
79
- type: sequelize_1.DataTypes.JSONB,
80
- allowNull: false,
81
- defaultValue: {}
82
- },
83
78
  replace_knocked_immediately: {
84
79
  type: sequelize_1.DataTypes.JSONB,
85
80
  allowNull: true
@@ -73,7 +73,7 @@ function transformToAttributes(tactics, teamId) {
73
73
  designated_setters: tactics.designatedSetters.map((s) => s.id),
74
74
  offensive_preferences: tactics.offensivePreferences.map(p => ({
75
75
  rotation: p.rotation,
76
- weights: p.weights,
76
+ priorities: p.priorities,
77
77
  tempos: p.tempos
78
78
  })),
79
79
  system_sets: tactics.systemSets?.map(s => ({
@@ -82,10 +82,9 @@ function transformToAttributes(tactics, teamId) {
82
82
  })),
83
83
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
84
84
  rotation: p.rotation,
85
- weights: p.weights,
85
+ priorities: p.priorities,
86
86
  tempos: p.tempos
87
87
  }))),
88
- serve_attack_shares: tactics.serveAttackShares ?? {},
89
88
  base_positions: tactics.basePositions,
90
89
  blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
91
90
  blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -181,7 +180,7 @@ function transformToObject(model, roster) {
181
180
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
182
181
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
183
182
  rotation: p.rotation,
184
- weights: p.weights,
183
+ priorities: p.priorities,
185
184
  tempos: p.tempos
186
185
  })),
187
186
  systemSets: model.system_sets?.map(s => ({
@@ -190,24 +189,9 @@ function transformToObject(model, roster) {
190
189
  })),
191
190
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
192
191
  rotation: p.rotation,
193
- weights: p.weights,
192
+ priorities: p.priorities,
194
193
  tempos: p.tempos
195
194
  }))),
196
- // LENIENT: keep only serve shares keyed by a current libero-replacement (flat or per-set) with a value in the
197
- // 0-100 band, so a stale key (a released player, or a libero-replacement config change) can never fail schema
198
- // validation and crash a team load; a dropped key = no serve share.
199
- serveAttackShares: (() => {
200
- const liberoReplacementIds = new Set([
201
- ...model.libero_replacements,
202
- ...(model.libero_replacement_sets ?? []).flat()
203
- ]);
204
- const healed = {};
205
- for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
206
- if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
207
- healed[id] = value;
208
- }
209
- return healed;
210
- })(),
211
195
  basePositions: model.base_positions ?? undefined,
212
196
  blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
213
197
  blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -354,21 +338,6 @@ function tacticsColumnsToApiDto(model) {
354
338
  offensivePreferences: model.offensive_preferences,
355
339
  systemSets: model.system_sets,
356
340
  offensivePreferenceSets: model.offensive_preference_sets,
357
- // Same stale-data policy as the heals below: only serve shares keyed by a current libero-replacement (flat or
358
- // per-set) with a value in the 0-100 band survive; an empty map surfaces as absent so the UI never reloads and
359
- // re-emits a stale key.
360
- serveAttackShares: (() => {
361
- const liberoReplacementIds = new Set([
362
- ...model.libero_replacements,
363
- ...(model.libero_replacement_sets ?? []).flat()
364
- ]);
365
- const healed = {};
366
- for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
367
- if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
368
- healed[id] = value;
369
- }
370
- return Object.keys(healed).length > 0 ? healed : undefined;
371
- })(),
372
341
  basePositions: model.base_positions ?? undefined,
373
342
  blockingPreferences: model.blocking_preferences,
374
343
  blockingPreferenceSets: model.blocking_preference_sets,
@@ -477,7 +446,6 @@ function apiTacticsConfigToAttributes(config, teamId) {
477
446
  offensive_preferences: config.offensivePreferences,
478
447
  system_sets: config.systemSets,
479
448
  offensive_preference_sets: config.offensivePreferenceSets,
480
- serve_attack_shares: config.serveAttackShares ?? {},
481
449
  base_positions: config.basePositions,
482
450
  blocking_preferences: config.blockingPreferences ?? [],
483
451
  blocking_preference_sets: config.blockingPreferenceSets,
@@ -26,7 +26,7 @@ function makeTactics(designatedSubs) {
26
26
  libero_replacement_sets: undefined,
27
27
  rotation_system: '5-1',
28
28
  designated_setters: ['lf'],
29
- offensive_preferences: [{ rotation: 1, weights: { lf: 60, rf: 40 } }],
29
+ offensive_preferences: [{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }],
30
30
  blocking_preferences: []
31
31
  };
32
32
  }
@@ -38,7 +38,7 @@ function makeTactics(designatedSubs) {
38
38
  (0, globals_1.expect)(dto.rotationSystem).toBe('5-1');
39
39
  (0, globals_1.expect)(dto.liberoReplacements).toEqual(['lf']);
40
40
  (0, globals_1.expect)(dto.lineup.bench).toEqual(['b1', 'b2']);
41
- (0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
41
+ (0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }]);
42
42
  });
43
43
  (0, globals_1.it)('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
44
44
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
@@ -133,32 +133,16 @@ function makeTactics(designatedSubs) {
133
133
  (0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
134
134
  const model = makeTactics([]);
135
135
  model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
136
- model.offensive_preference_sets = [[{ rotation: 1, weights: { rf: 55, mf: 45 } }]];
136
+ model.offensive_preference_sets = [[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]];
137
137
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
138
138
  (0, globals_1.expect)(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
139
- (0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, weights: { rf: 55, mf: 45 } }]]);
139
+ (0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]]);
140
140
  });
141
141
  (0, globals_1.it)('leaves the per-set DTO fields undefined when the columns are absent', () => {
142
142
  const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
143
143
  (0, globals_1.expect)(dto.systemSets).toBeUndefined();
144
144
  (0, globals_1.expect)(dto.offensivePreferenceSets).toBeUndefined();
145
145
  });
146
- (0, globals_1.it)('surfaces serve attack shares keyed by a libero replacement', () => {
147
- const model = makeTactics([]);
148
- model.serve_attack_shares = { lf: 30 }; // lf is the configured libero_replacement
149
- const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
150
- (0, globals_1.expect)(dto.serveAttackShares).toEqual({ lf: 30 });
151
- });
152
- (0, globals_1.it)('drops a serve attack share keyed by a non-libero-replacement (stale heal)', () => {
153
- const model = makeTactics([]);
154
- model.serve_attack_shares = { lf: 30, rf: 40 }; // rf is not in libero_replacements (['lf'])
155
- const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
156
- (0, globals_1.expect)(dto.serveAttackShares).toEqual({ lf: 30 });
157
- });
158
- (0, globals_1.it)('leaves serveAttackShares undefined when the column is empty', () => {
159
- const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
160
- (0, globals_1.expect)(dto.serveAttackShares).toBeUndefined();
161
- });
162
146
  });
163
147
  (0, globals_1.describe)('per-set tactics — service <-> row round-trip', () => {
164
148
  const country = (0, test_helpers_1.makeCountry)();
@@ -176,7 +160,7 @@ function makeTactics(designatedSubs) {
176
160
  { rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
177
161
  { rotationSystem: service_1.RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
178
162
  ],
179
- offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]
163
+ offensivePreferenceSets: [[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]
180
164
  });
181
165
  (0, globals_1.it)('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
182
166
  const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
@@ -184,7 +168,7 @@ function makeTactics(designatedSubs) {
184
168
  { rotationSystem: '5-1', designatedSetters: [p1.id] },
185
169
  { rotationSystem: '6-0', designatedSetters: [] }
186
170
  ]);
187
- (0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]);
171
+ (0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]);
188
172
  });
189
173
  (0, globals_1.it)('transformToTactics resolves the stored ids back to roster players', () => {
190
174
  const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
@@ -192,7 +176,7 @@ function makeTactics(designatedSubs) {
192
176
  (0, globals_1.expect)(back.systemSets?.[0].rotationSystem).toBe(service_1.RotationSystemEnum.FIVE_ONE);
193
177
  (0, globals_1.expect)(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
194
178
  (0, globals_1.expect)(back.systemSets?.[1].designatedSetters).toEqual([]);
195
- (0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].weights).toEqual({ [p2.id]: 60, [p3.id]: 40 });
179
+ (0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].priorities).toEqual({ [p2.id]: 'HIGH', [p3.id]: 'MID' });
196
180
  });
197
181
  (0, globals_1.it)('omits the per-set columns entirely for an all-sets config', () => {
198
182
  const flat = service_1.Tactics.create({
@@ -206,27 +190,6 @@ function makeTactics(designatedSubs) {
206
190
  (0, globals_1.expect)(back.systemSets).toBeUndefined();
207
191
  (0, globals_1.expect)(back.offensivePreferenceSets).toBeUndefined();
208
192
  });
209
- (0, globals_1.it)('round-trips serve attack shares keyed by a libero replacement', () => {
210
- const withServe = service_1.Tactics.create({
211
- lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
212
- liberoReplacements: [p5],
213
- serveAttackShares: { [p5.id]: 35 }
214
- });
215
- const attrs = (0, tactics_1.transformFromTactics)(withServe, 'team-1');
216
- (0, globals_1.expect)(attrs.serve_attack_shares).toEqual({ [p5.id]: 35 });
217
- const back = (0, tactics_1.transformToTactics)(attrs, roster);
218
- (0, globals_1.expect)(back.serveAttackShares).toEqual({ [p5.id]: 35 });
219
- });
220
- (0, globals_1.it)('drops a stale serve attack share key on load (lenient heal, no crash)', () => {
221
- const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
222
- lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
223
- liberoReplacements: [p5],
224
- serveAttackShares: { [p5.id]: 35 }
225
- }), 'team-1');
226
- attrs.libero_replacements = [];
227
- const back = (0, tactics_1.transformToTactics)(attrs, roster);
228
- (0, globals_1.expect)(back.serveAttackShares).toEqual({});
229
- });
230
193
  (0, globals_1.it)('round-trips base positions (coords + reception participation)', () => {
231
194
  const config = {
232
195
  coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
@@ -255,11 +218,11 @@ function makeTactics(designatedSubs) {
255
218
  const withTempos = service_1.Tactics.create({
256
219
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
257
220
  liberoReplacements: [],
258
- offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
221
+ offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
259
222
  });
260
223
  const attrs = (0, tactics_1.transformFromTactics)(withTempos, 'team-1');
261
224
  (0, globals_1.expect)(attrs.offensive_preferences).toEqual([
262
- { rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
225
+ { rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
263
226
  ]);
264
227
  const back = (0, tactics_1.transformToTactics)(attrs, roster);
265
228
  (0, globals_1.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,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PRIORITY_WEIGHT = exports.ATTACK_PRIORITIES = void 0;
4
+ exports.ATTACK_PRIORITIES = ['HIGH', 'MID', 'LOW', 'NONE'];
5
+ // The relative pull of each priority. Tunable in one place; the engine reads these to weight the setter's target.
6
+ exports.PRIORITY_WEIGHT = {
7
+ HIGH: 20,
8
+ MID: 10,
9
+ LOW: 5,
10
+ NONE: 0
11
+ };
@@ -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';
@@ -19,6 +19,7 @@ __exportStar(require("./team-generator"), exports);
19
19
  __exportStar(require("./formation"), exports);
20
20
  __exportStar(require("./tactics"), exports);
21
21
  __exportStar(require("./base-config"), exports);
22
+ __exportStar(require("./attack-priority"), exports);
22
23
  __exportStar(require("./default-base-config"), exports);
23
24
  __exportStar(require("./receive-overlap"), exports);
24
25
  __exportStar(require("./team-name"), exports);
@@ -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;
@@ -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,6 +91,9 @@ 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
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(),
@@ -117,9 +121,6 @@ exports.TacticsInputSchema = zod_1.z.object({
117
121
  offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([]),
118
122
  // Attack shares on serve (libero-serve feature): per-player attack share the sim applies when a
119
123
  // libero-replaced player is on court unreplaced (serving, or through a libero injury). Keyed by starter id.
120
- // This is NOT part of the offensivePreferences sum-to-100; its band (0-100) and libero-replacement-key rules
121
- // are validated independently in the superRefine below.
122
- serveAttackShares: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int()).default({}),
123
124
  // Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
124
125
  // validated with the same cross-field rules as the flat fields (see superRefine below).
125
126
  systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
@@ -167,6 +168,11 @@ exports.TacticsInputSchema = zod_1.z.object({
167
168
  const starters = slots.map(([position, player]) => ({ position, id: player.id }));
168
169
  const starterIds = new Set(starters.map(s => s.id));
169
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)]);
170
176
  // Designated setters: count must match the system, each must be an on-court starter, and in 4-2 / 6-2 the two
171
177
  // setters sit in opposite slots (3 rotations apart). Run on the flat fields and on every per-set entry.
172
178
  const validateSystem = (rotationSystem, designatedSetters, path) => {
@@ -197,19 +203,15 @@ exports.TacticsInputSchema = zod_1.z.object({
197
203
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
198
204
  }
199
205
  seenRotations.add(pref.rotation);
200
- // Weight keys are on-court starters, and the percentages must sum to exactly 100 (0 removes an attacker).
201
- for (const id of Object.keys(pref.weights)) {
202
- 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)) {
203
210
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
204
211
  }
205
212
  }
206
- const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
207
- if (total !== 100) {
208
- ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
209
- }
210
- // Tempo preference keys are starter ids too.
211
213
  for (const id of Object.keys(pref.tempos ?? {})) {
212
- if (!starterIds.has(id)) {
214
+ if (!allowedPlayerKeys.has(id)) {
213
215
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
214
216
  }
215
217
  }
@@ -226,13 +228,13 @@ exports.TacticsInputSchema = zod_1.z.object({
226
228
  }
227
229
  seenRotations.add(pref.rotation);
228
230
  for (const id of Object.keys(pref.commit)) {
229
- if (!starterIds.has(id)) {
231
+ if (!allowedPlayerKeys.has(id)) {
230
232
  ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
231
233
  }
232
234
  }
233
235
  for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
234
236
  for (const id of pref.noBlock[form] ?? []) {
235
- if (!starterIds.has(id)) {
237
+ if (!allowedPlayerKeys.has(id)) {
236
238
  ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
237
239
  }
238
240
  }
@@ -251,19 +253,13 @@ exports.TacticsInputSchema = zod_1.z.object({
251
253
  for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
252
254
  validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
253
255
  }
254
- // Serve attack shares (libero-serve feature): only a current libero-replacement starter (flat or per-set) may
255
- // carry one, and each value must be in the 0-100 band. Validated independently of the offensive-preference
256
- // sum-to-100 (a serve share never enters that total).
257
- const liberoReplacementIds = new Set([
258
- ...data.liberoReplacements.map(p => p.id),
259
- ...(data.liberoReplacementSets ?? []).flatMap(set => set.map(p => p.id))
260
- ]);
261
- for (const [id, value] of Object.entries(data.serveAttackShares)) {
262
- if (!liberoReplacementIds.has(id)) {
263
- ctx.addIssue({ code: 'custom', message: 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT', path: ['serveAttackShares'] });
264
- }
265
- if (value < 0 || value > 100) {
266
- 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
+ }
267
263
  }
268
264
  }
269
265
  // Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap