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
@@ -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,5 +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>>>;
12
+ readonly attackContact?: Record<string, Record<string, BaseCoord>>;
11
13
  }
@@ -88,11 +88,40 @@ const COORDS = {
88
88
  6: { 1: { d: 5.35, r: 2.35 }, 2: { d: 2.05, r: 2.1 }, 3: { d: 0.8, r: 4 }, 4: { d: 2.1, r: -1.95 }, 5: { d: 4.9, r: -1.4 }, 6: { d: 0.4, r: 1.3 } }
89
89
  }
90
90
  };
91
+ // Per-attacker attack subbases (owner 2026-08-12): the point-of-attack ATK_L/M/R coverage shapes are replaced by ONE
92
+ // subbase per attacker, keyed by the attacker's ZONE (ATK_Z1..ATK_Z6). Each is the whole-team formation used when the
93
+ // attacker at that zone gets the ball. A FRONT-row zone (2/3/4) reuses its lane's old ATK_L/M/R shape, where the lane
94
+ // is ranked by that zone's ATTACK-base lateral this rotation (leftmost r -> ATK_L, middle -> ATK_M, rightmost -> ATK_R);
95
+ // a BACK-row zone (1/5/6) replicates the ATTACK base. Derived from COORDS so the code default and the migration agree.
96
+ // The old ATK_L/M/R stay in COORDS as the derivation SOURCE but are NOT exposed in the default (below).
97
+ const FRONT_ZONES = [2, 3, 4];
98
+ const BACK_ZONES = [1, 5, 6];
99
+ function defaultAttackSubbases() {
100
+ const cloneFormation = (zones) => {
101
+ const out = {};
102
+ for (const [zone, c] of Object.entries(zones))
103
+ out[zone] = { d: c.d, r: c.r };
104
+ return out;
105
+ };
106
+ const out = { ATK_Z1: {}, ATK_Z2: {}, ATK_Z3: {}, ATK_Z4: {}, ATK_Z5: {}, ATK_Z6: {} };
107
+ for (let rot = 1; rot <= 6; rot++) {
108
+ const ranked = [...FRONT_ZONES].sort((a, b) => COORDS.ATTACK[rot][a].r - COORDS.ATTACK[rot][b].r);
109
+ const laneFor = { [ranked[0]]: 'ATK_L', [ranked[1]]: 'ATK_M', [ranked[2]]: 'ATK_R' };
110
+ for (const z of FRONT_ZONES)
111
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS[laneFor[z]][rot]);
112
+ for (const z of BACK_ZONES)
113
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS.ATTACK[rot]);
114
+ }
115
+ return out;
116
+ }
91
117
  // The default coordinate table as a BaseConfig.coords (jsonb string keys). Shared, readonly; materialized (and so
92
- // serialized) per team on save, so sharing the reference in memory is safe.
118
+ // serialized) per team on save, so sharing the reference in memory is safe. ATK_L/M/R are dropped from the exposed
119
+ // default (replaced by ATK_Z<n>); everything else is copied verbatim from COORDS.
93
120
  const DEFAULT_COORDS = (() => {
94
121
  const out = {};
95
122
  for (const [formation, rotations] of Object.entries(COORDS)) {
123
+ if (formation === 'ATK_L' || formation === 'ATK_M' || formation === 'ATK_R')
124
+ continue; // replaced by ATK_Z<n>
96
125
  out[formation] = {};
97
126
  for (const [rotation, zones] of Object.entries(rotations)) {
98
127
  out[formation][rotation] = {};
@@ -100,8 +129,35 @@ const DEFAULT_COORDS = (() => {
100
129
  out[formation][rotation][zone] = { d: coord.d, r: coord.r };
101
130
  }
102
131
  }
103
- return out;
132
+ return { ...out, ...defaultAttackSubbases() };
104
133
  })();
134
+ // The default desired-contact per attacker (trajectory-set, owner 2026-08-12 decision D9): a near-net hitting point
135
+ // in each attacker's lane, NOT the old "set to the standing dot" location. `contact.d` = a near-net depth for the
136
+ // front row or the pipe depth for the back row; `contact.r` = the attacker's ATTACK-dot lateral clamped to the
137
+ // antenna. Keyed [rotation][zone] -> {d,r}. The near-net depth + antenna are first-cut calibration knobs. The sim
138
+ // does not read this yet (added with the engine phase); storing it here is inert. See plans/trajectory-set-and-approach.md.
139
+ const NEAR_NET_CONTACT_DEPTH = 0.5; // front-row contact, m off the net
140
+ const PIPE_CONTACT_DEPTH = 1.8; // back-row (pipe) contact, m off the net (matches the sim's BACKROW_SET_DEPTH)
141
+ const CONTACT_ANTENNA = 4.3; // keep the contact just inside the sideline / antenna
142
+ // LAZY + memoized (not an eager IIFE): `isBackRow` lives in the match module, which sits in a load cycle with the
143
+ // team barrel, so calling it at module-load time hits an uninitialised import. Computed once on first use, then the
144
+ // shared readonly reference is reused (like DEFAULT_COORDS; serialized per team on save, so sharing is safe).
145
+ let cachedAttackContact;
146
+ function defaultAttackContact() {
147
+ if (cachedAttackContact != null)
148
+ return cachedAttackContact;
149
+ const out = {};
150
+ for (const [rotation, zones] of Object.entries(COORDS.ATTACK)) {
151
+ out[rotation] = {};
152
+ for (const [zone, coord] of Object.entries(zones)) {
153
+ const backRow = (0, match_1.isBackRow)(Number(zone));
154
+ const r = Math.max(-CONTACT_ANTENNA, Math.min(CONTACT_ANTENNA, coord.r));
155
+ out[rotation][zone] = { d: backRow ? PIPE_CONTACT_DEPTH : NEAR_NET_CONTACT_DEPTH, r };
156
+ }
157
+ }
158
+ cachedAttackContact = out;
159
+ return out;
160
+ }
105
161
  // The owner's corrected per-point-of-attack block map for the 5-1 (exported 2026-08-09), keyed
106
162
  // [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock. `false` = that front-row zone sits OUT of the block for an attack
107
163
  // from that side; `true` = reads/joins the wall. Only the front-row zones in play for the rotation are tagged. Same
@@ -167,5 +223,5 @@ function defaultReceiveMap(system) {
167
223
  // 5-1 uses the owner's exported receive map, the other systems derive their receive map per system.
168
224
  function defaultBaseConfig(system) {
169
225
  const receive = system === rotation_system_1.RotationSystemEnum.FIVE_ONE ? DEFAULT_5_1_RECEIVE : defaultReceiveMap(system);
170
- return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING };
226
+ return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING, attackContact: defaultAttackContact() };
171
227
  }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rotation_system_1 = require("./rotation-system");
4
+ const default_base_config_1 = require("./default-base-config");
5
+ // Back-row rotational zones (right/left/middle back). Front row is {2,3,4}.
6
+ const BACK_ROW = new Set([1, 5, 6]);
7
+ // The trajectory-set default desired-contact (owner D9, 2026-08-12): a near-net hitting point in each attacker's
8
+ // lane, NOT the old set-to-the-standing-dot location. Front row contacts sit ~0.5 m off the net; back row on the
9
+ // ~1.8 m pipe line; the lateral is the ATTACK-dot's r clamped inside the antenna (|r| <= 4.3).
10
+ describe('defaultBaseConfig attackContact (trajectory-set default)', () => {
11
+ const cfg = (0, default_base_config_1.defaultBaseConfig)(rotation_system_1.RotationSystemEnum.FIVE_ONE);
12
+ it('provides a finite desired contact for every rotation and zone', () => {
13
+ expect(cfg.attackContact).toBeDefined();
14
+ for (let rot = 1; rot <= 6; rot++) {
15
+ for (let zone = 1; zone <= 6; zone++) {
16
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
17
+ expect(c).toBeDefined();
18
+ expect(Number.isFinite(c?.d)).toBe(true);
19
+ expect(Number.isFinite(c?.r)).toBe(true);
20
+ }
21
+ }
22
+ });
23
+ it('places front-row contacts near the net (0.5 m) and back-row on the pipe line (1.8 m)', () => {
24
+ for (let rot = 1; rot <= 6; rot++) {
25
+ for (let zone = 1; zone <= 6; zone++) {
26
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
27
+ expect(c.d).toBeCloseTo(BACK_ROW.has(zone) ? 1.8 : 0.5);
28
+ }
29
+ }
30
+ });
31
+ it('clamps every contact inside the antenna (|r| <= 4.3), even for a wide-pin attack dot', () => {
32
+ for (let rot = 1; rot <= 6; rot++) {
33
+ for (let zone = 1; zone <= 6; zone++) {
34
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
35
+ expect(Math.abs(c.r)).toBeLessThanOrEqual(4.3 + 1e-9);
36
+ }
37
+ }
38
+ });
39
+ it('keeps the contact on the attacker approach-lane side (rotation 1 OH wide dot r=5 -> +4.3)', () => {
40
+ const c = cfg.attackContact?.['1']?.['4'];
41
+ expect(c.d).toBeCloseTo(0.5); // front row, near the net
42
+ expect(c.r).toBeCloseTo(4.3); // clamped from the OH's wide ATTACK dot (r=5), same (positive) side
43
+ });
44
+ });
@@ -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,7 +313,12 @@ 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>>>>;
318
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
319
+ d: z.ZodNumber;
320
+ r: z.ZodNumber;
321
+ }, z.core.$strip>>>>;
308
322
  }, z.core.$strip>>;
309
323
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
310
324
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{