volleyballsimtypes 0.0.515 → 0.0.517

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 (39) hide show
  1. package/dist/cjs/src/api/index.d.ts +2 -2
  2. package/dist/cjs/src/api/tactics-player-refs.d.ts +2 -2
  3. package/dist/cjs/src/api/tactics-player-refs.js +5 -4
  4. package/dist/cjs/src/data/models/tactics.d.ts +4 -4
  5. package/dist/cjs/src/data/models/tactics.js +5 -5
  6. package/dist/cjs/src/data/transformers/tactics.js +43 -36
  7. package/dist/cjs/src/data/transformers/tactics.test.js +34 -46
  8. package/dist/cjs/src/service/team/attack-priority.d.ts +3 -0
  9. package/dist/cjs/src/service/team/attack-priority.js +11 -0
  10. package/dist/cjs/src/service/team/base-config.d.ts +1 -0
  11. package/dist/cjs/src/service/team/index.d.ts +1 -0
  12. package/dist/cjs/src/service/team/index.js +1 -0
  13. package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -1
  14. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +16 -5
  15. package/dist/cjs/src/service/team/schemas/tactics.z.js +55 -32
  16. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +59 -63
  17. package/dist/cjs/src/service/team/schemas/team.z.d.ts +16 -5
  18. package/dist/cjs/src/service/team/tactics.d.ts +2 -2
  19. package/dist/cjs/src/service/team/tactics.js +2 -2
  20. package/dist/esm/src/api/index.d.ts +2 -2
  21. package/dist/esm/src/api/tactics-player-refs.d.ts +2 -2
  22. package/dist/esm/src/api/tactics-player-refs.js +5 -4
  23. package/dist/esm/src/data/models/tactics.d.ts +4 -4
  24. package/dist/esm/src/data/models/tactics.js +5 -5
  25. package/dist/esm/src/data/transformers/tactics.js +43 -36
  26. package/dist/esm/src/data/transformers/tactics.test.js +34 -46
  27. package/dist/esm/src/service/team/attack-priority.d.ts +3 -0
  28. package/dist/esm/src/service/team/attack-priority.js +8 -0
  29. package/dist/esm/src/service/team/base-config.d.ts +1 -0
  30. package/dist/esm/src/service/team/index.d.ts +1 -0
  31. package/dist/esm/src/service/team/index.js +1 -0
  32. package/dist/esm/src/service/team/offensive-preference.d.ts +2 -1
  33. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +16 -5
  34. package/dist/esm/src/service/team/schemas/tactics.z.js +55 -32
  35. package/dist/esm/src/service/team/schemas/tactics.z.test.js +59 -63
  36. package/dist/esm/src/service/team/schemas/team.z.d.ts +16 -5
  37. package/dist/esm/src/service/team/tactics.d.ts +2 -2
  38. package/dist/esm/src/service/team/tactics.js +2 -2
  39. 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 {
@@ -137,10 +137,10 @@ export interface Tactics {
137
137
  liberoReplacementSets?: string[][];
138
138
  rotationSystem: RotationSystemEnum;
139
139
  designatedSetters: string[];
140
+ emergencySetterOrder: string[];
140
141
  offensivePreferences: ApiOffensivePreference[];
141
142
  systemSets?: ApiSystemSet[];
142
143
  offensivePreferenceSets?: ApiOffensivePreference[][];
143
- serveAttackShares?: Record<string, number>;
144
144
  basePositions?: BaseConfig;
145
145
  blockingPreferences: ApiBlockingPreference[];
146
146
  blockingPreferenceSets?: ApiBlockingPreference[][];
@@ -2,8 +2,8 @@ import type { Tactics } from '.';
2
2
  /**
3
3
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
4
4
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
5
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
6
- * replacements and the libero-injury reserves.
5
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
6
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
7
7
  *
8
8
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
9
9
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -4,8 +4,8 @@ exports.collectTacticsPlayerIds = collectTacticsPlayerIds;
4
4
  /**
5
5
  * Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
6
6
  * replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
7
- * setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
8
- * replacements and the libero-injury reserves.
7
+ * setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
8
+ * commit + no-block ids, injury replacements and the libero-injury reserves.
9
9
  *
10
10
  * There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
11
11
  * slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
@@ -26,9 +26,10 @@ 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.emergencySetterOrder ?? []),
30
+ ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
30
31
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
31
- ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
32
+ ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
32
33
  ...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
33
34
  ...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
34
35
  // 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 {
@@ -74,10 +74,10 @@ export interface TacticsAttributes {
74
74
  libero_replacement_sets?: PlayerId[][];
75
75
  rotation_system: RotationSystemEnum;
76
76
  designated_setters: PlayerId[];
77
+ emergency_setter_order: PlayerId[];
77
78
  offensive_preferences: OffensivePreferenceAttributes[];
78
79
  system_sets?: SystemSetAttributes[];
79
80
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
80
- serve_attack_shares: Record<PlayerId, number>;
81
81
  replace_knocked_immediately?: Record<PlayerId, boolean>;
82
82
  injury_replacements?: InjuryReplacementAttributes[];
83
83
  libero_injury?: LiberoInjuryAttributes;
@@ -87,7 +87,7 @@ export interface TacticsAttributes {
87
87
  }
88
88
  export type TacticsPk = 'team_id';
89
89
  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';
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' | 'emergency_setter_order' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
91
91
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
92
92
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
93
93
  team_id: string;
@@ -101,10 +101,10 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
101
101
  libero_replacement_sets?: PlayerId[][];
102
102
  rotation_system: RotationSystemEnum;
103
103
  designated_setters: PlayerId[];
104
+ emergency_setter_order: PlayerId[];
104
105
  offensive_preferences: OffensivePreferenceAttributes[];
105
106
  system_sets?: SystemSetAttributes[];
106
107
  offensive_preference_sets?: OffensivePreferenceAttributes[][];
107
- serve_attack_shares: Record<PlayerId, number>;
108
108
  replace_knocked_immediately?: Record<PlayerId, boolean>;
109
109
  injury_replacements?: InjuryReplacementAttributes[];
110
110
  libero_injury?: LiberoInjuryAttributes;
@@ -62,6 +62,11 @@ class TacticsModel extends sequelize_1.Model {
62
62
  allowNull: false,
63
63
  defaultValue: []
64
64
  },
65
+ emergency_setter_order: {
66
+ type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
67
+ allowNull: false,
68
+ defaultValue: []
69
+ },
65
70
  offensive_preferences: {
66
71
  type: sequelize_1.DataTypes.JSONB,
67
72
  allowNull: false,
@@ -75,11 +80,6 @@ class TacticsModel extends sequelize_1.Model {
75
80
  type: sequelize_1.DataTypes.JSONB,
76
81
  allowNull: true
77
82
  },
78
- serve_attack_shares: {
79
- type: sequelize_1.DataTypes.JSONB,
80
- allowNull: false,
81
- defaultValue: {}
82
- },
83
83
  replace_knocked_immediately: {
84
84
  type: sequelize_1.DataTypes.JSONB,
85
85
  allowNull: true
@@ -71,9 +71,10 @@ function transformToAttributes(tactics, teamId) {
71
71
  })),
72
72
  rotation_system: tactics.rotationSystem,
73
73
  designated_setters: tactics.designatedSetters.map((s) => s.id),
74
+ emergency_setter_order: tactics.emergencySetterOrder.map((p) => p.id),
74
75
  offensive_preferences: tactics.offensivePreferences.map(p => ({
75
76
  rotation: p.rotation,
76
- weights: p.weights,
77
+ priorities: p.priorities,
77
78
  tempos: p.tempos
78
79
  })),
79
80
  system_sets: tactics.systemSets?.map(s => ({
@@ -82,10 +83,9 @@ function transformToAttributes(tactics, teamId) {
82
83
  })),
83
84
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
84
85
  rotation: p.rotation,
85
- weights: p.weights,
86
+ priorities: p.priorities,
86
87
  tempos: p.tempos
87
88
  }))),
88
- serve_attack_shares: tactics.serveAttackShares ?? {},
89
89
  base_positions: tactics.basePositions,
90
90
  blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
91
91
  blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -179,9 +179,30 @@ function transformToObject(model, roster) {
179
179
  })),
180
180
  rotationSystem: model.rotation_system,
181
181
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
182
+ // LENIENT (roster.find): the coach-ranked emergency-setter chain. Keep only ids still on the roster, inside the
183
+ // squad pool (starters + starting libero + L2 + bench), and NOT a current designated setter (setters are pinned
184
+ // ahead of this list, never stored inside it), first-occurrence-wins on duplicates. So stale jsonb (a departed
185
+ // player, a promoted setter, a non-pool id) can never fail schema validation and crash a team load; the next
186
+ // tactics save rewrites it cleanly.
187
+ emergencySetterOrder: (() => {
188
+ const poolIds = new Set([...inLineupIds, ...(secondLibero != null ? [secondLibero.id] : [])]);
189
+ const setterIds = new Set(model.designated_setters ?? []);
190
+ const seen = new Set();
191
+ const healed = [];
192
+ for (const id of model.emergency_setter_order ?? []) {
193
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
194
+ continue;
195
+ const p = roster.find((x) => x.id === id);
196
+ if (p == null)
197
+ continue;
198
+ seen.add(id);
199
+ healed.push(p);
200
+ }
201
+ return healed;
202
+ })(),
182
203
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
183
204
  rotation: p.rotation,
184
- weights: p.weights,
205
+ priorities: p.priorities,
185
206
  tempos: p.tempos
186
207
  })),
187
208
  systemSets: model.system_sets?.map(s => ({
@@ -190,24 +211,9 @@ function transformToObject(model, roster) {
190
211
  })),
191
212
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
192
213
  rotation: p.rotation,
193
- weights: p.weights,
214
+ priorities: p.priorities,
194
215
  tempos: p.tempos
195
216
  }))),
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
217
  basePositions: model.base_positions ?? undefined,
212
218
  blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
213
219
  blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
@@ -351,24 +357,25 @@ function tacticsColumnsToApiDto(model) {
351
357
  }),
352
358
  rotationSystem: model.rotation_system,
353
359
  designatedSetters: model.designated_setters,
360
+ // Same stale-data policy: drop any emergency-setter id the service load would drop (a current designated
361
+ // setter, or one outside the squad pool) so the UI never reloads and re-emits a rejected entry. Roster
362
+ // membership can't be checked here (ids only); the service heal covers that.
363
+ emergencySetterOrder: (() => {
364
+ const poolIds = new Set([...lineupIds, ...(validSecondLibero != null ? [validSecondLibero] : [])]);
365
+ const setterIds = new Set(model.designated_setters ?? []);
366
+ const seen = new Set();
367
+ const healed = [];
368
+ for (const id of model.emergency_setter_order ?? []) {
369
+ if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
370
+ continue;
371
+ seen.add(id);
372
+ healed.push(id);
373
+ }
374
+ return healed;
375
+ })(),
354
376
  offensivePreferences: model.offensive_preferences,
355
377
  systemSets: model.system_sets,
356
378
  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
379
  basePositions: model.base_positions ?? undefined,
373
380
  blockingPreferences: model.blocking_preferences,
374
381
  blockingPreferenceSets: model.blocking_preference_sets,
@@ -474,10 +481,10 @@ function apiTacticsConfigToAttributes(config, teamId) {
474
481
  libero_replacement_sets: config.liberoReplacementSets,
475
482
  rotation_system: config.rotationSystem,
476
483
  designated_setters: config.designatedSetters,
484
+ emergency_setter_order: config.emergencySetterOrder ?? [],
477
485
  offensive_preferences: config.offensivePreferences,
478
486
  system_sets: config.systemSets,
479
487
  offensive_preference_sets: config.offensivePreferenceSets,
480
- serve_attack_shares: config.serveAttackShares ?? {},
481
488
  base_positions: config.basePositions,
482
489
  blocking_preferences: config.blockingPreferences ?? [],
483
490
  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' });
@@ -298,6 +261,31 @@ function makeTactics(designatedSubs) {
298
261
  (0, globals_1.expect)(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
299
262
  (0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
300
263
  });
264
+ (0, globals_1.it)('round-trips the emergency setter order (non-setter pool members)', () => {
265
+ const withOrder = service_1.Tactics.create({
266
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
267
+ liberoReplacements: [],
268
+ emergencySetterOrder: [p2, p3]
269
+ });
270
+ const attrs = (0, tactics_1.transformFromTactics)(withOrder, 'team-1');
271
+ (0, globals_1.expect)(attrs.emergency_setter_order).toEqual([p2.id, p3.id]);
272
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
273
+ (0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, p3.id]);
274
+ });
275
+ // The read path must never crash a team load on a stale order. Ids that are gone from the roster, outside the
276
+ // squad pool, or now a designated setter (setters are pinned, never stored) are silently dropped, order preserved.
277
+ (0, globals_1.it)('drops emergency-order ids that are gone or a designated setter on read (lenient)', () => {
278
+ const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
279
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
280
+ liberoReplacements: [],
281
+ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE,
282
+ designatedSetters: [p1],
283
+ emergencySetterOrder: [p2]
284
+ }), 'team-1');
285
+ attrs.emergency_setter_order = [p1.id, 'gone', p2.id];
286
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
287
+ (0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id]);
288
+ });
301
289
  });
302
290
  (0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
303
291
  const country = (0, test_helpers_1.makeCountry)();
@@ -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
  }
@@ -259,25 +259,35 @@ export declare const TacticsInputSchema: z.ZodObject<{
259
259
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
260
260
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
261
261
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
+ emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
262
263
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
263
264
  rotation: z.ZodNumber;
264
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
265
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
266
+ HIGH: "HIGH";
267
+ MID: "MID";
268
+ LOW: "LOW";
269
+ NONE: "NONE";
270
+ }>>;
265
271
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
266
- FAST: "FAST";
267
272
  HIGH: "HIGH";
273
+ FAST: "FAST";
268
274
  }>>>;
269
275
  }, z.core.$strip>>>;
270
- serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
271
276
  systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
272
277
  rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
273
278
  designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
274
279
  }, z.core.$strip>>>;
275
280
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
276
281
  rotation: z.ZodNumber;
277
- weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
282
+ priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
283
+ HIGH: "HIGH";
284
+ MID: "MID";
285
+ LOW: "LOW";
286
+ NONE: "NONE";
287
+ }>>;
278
288
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
279
- FAST: "FAST";
280
289
  HIGH: "HIGH";
290
+ FAST: "FAST";
281
291
  }>>>;
282
292
  }, z.core.$strip>>>>;
283
293
  blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -304,6 +314,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
304
314
  r: z.ZodNumber;
305
315
  }, z.core.$strip>>>>;
306
316
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
317
+ receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
307
318
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
308
319
  attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
309
320
  d: z.ZodNumber;