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.
- package/dist/cjs/src/api/index.d.ts +1 -2
- package/dist/cjs/src/api/tactics-player-refs.js +2 -2
- package/dist/cjs/src/data/models/tactics.d.ts +2 -4
- package/dist/cjs/src/data/models/tactics.js +0 -5
- package/dist/cjs/src/data/transformers/tactics.js +4 -36
- package/dist/cjs/src/data/transformers/tactics.test.js +9 -46
- package/dist/cjs/src/service/team/attack-priority.d.ts +3 -0
- package/dist/cjs/src/service/team/attack-priority.js +11 -0
- package/dist/cjs/src/service/team/base-config.d.ts +1 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -1
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +15 -5
- package/dist/cjs/src/service/team/schemas/tactics.z.js +28 -32
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +15 -5
- package/dist/cjs/src/service/team/tactics.d.ts +0 -2
- package/dist/cjs/src/service/team/tactics.js +1 -2
- package/dist/esm/src/api/index.d.ts +1 -2
- package/dist/esm/src/api/tactics-player-refs.js +2 -2
- package/dist/esm/src/data/models/tactics.d.ts +2 -4
- package/dist/esm/src/data/models/tactics.js +0 -5
- package/dist/esm/src/data/transformers/tactics.js +4 -36
- package/dist/esm/src/data/transformers/tactics.test.js +9 -46
- package/dist/esm/src/service/team/attack-priority.d.ts +3 -0
- package/dist/esm/src/service/team/attack-priority.js +8 -0
- package/dist/esm/src/service/team/base-config.d.ts +1 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/offensive-preference.d.ts +2 -1
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +15 -5
- package/dist/esm/src/service/team/schemas/tactics.z.js +28 -32
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/esm/src/service/team/schemas/team.z.d.ts +15 -5
- package/dist/esm/src/service/team/tactics.d.ts +0 -2
- package/dist/esm/src/service/team/tactics.js +1 -2
- 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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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' | '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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].
|
|
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,
|
|
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,
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
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); `
|
|
54
|
-
//
|
|
55
|
-
// optionally forces a per-
|
|
56
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|