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.
- 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 +2 -0
- package/dist/cjs/src/service/team/default-base-config.js +59 -3
- package/dist/cjs/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/cjs/src/service/team/default-base-config.test.js +44 -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 +19 -5
- package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -33
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +19 -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 +2 -0
- package/dist/esm/src/service/team/default-base-config.js +59 -3
- package/dist/esm/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/esm/src/service/team/default-base-config.test.js +42 -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 +19 -5
- package/dist/esm/src/service/team/schemas/tactics.z.js +32 -33
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/esm/src/service/team/schemas/team.z.d.ts +19 -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
|
@@ -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,9 +91,15 @@ 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
|
-
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()
|
|
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(),
|
|
100
|
+
// Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
|
|
101
|
+
// configs saved before this field validate unchanged.
|
|
102
|
+
attackContact: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema)).optional()
|
|
96
103
|
});
|
|
97
104
|
exports.TacticsInputSchema = zod_1.z.object({
|
|
98
105
|
lineup: lineupSchema,
|
|
@@ -114,9 +121,6 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
114
121
|
offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([]),
|
|
115
122
|
// Attack shares on serve (libero-serve feature): per-player attack share the sim applies when a
|
|
116
123
|
// libero-replaced player is on court unreplaced (serving, or through a libero injury). Keyed by starter id.
|
|
117
|
-
// This is NOT part of the offensivePreferences sum-to-100; its band (0-100) and libero-replacement-key rules
|
|
118
|
-
// are validated independently in the superRefine below.
|
|
119
|
-
serveAttackShares: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int()).default({}),
|
|
120
124
|
// Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
|
|
121
125
|
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
122
126
|
systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
|
|
@@ -164,6 +168,11 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
164
168
|
const starters = slots.map(([position, player]) => ({ position, id: player.id }));
|
|
165
169
|
const starterIds = new Set(starters.map(s => s.id));
|
|
166
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)]);
|
|
167
176
|
// Designated setters: count must match the system, each must be an on-court starter, and in 4-2 / 6-2 the two
|
|
168
177
|
// setters sit in opposite slots (3 rotations apart). Run on the flat fields and on every per-set entry.
|
|
169
178
|
const validateSystem = (rotationSystem, designatedSetters, path) => {
|
|
@@ -194,19 +203,15 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
194
203
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
195
204
|
}
|
|
196
205
|
seenRotations.add(pref.rotation);
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
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)) {
|
|
200
210
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
201
211
|
}
|
|
202
212
|
}
|
|
203
|
-
const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
|
|
204
|
-
if (total !== 100) {
|
|
205
|
-
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
|
|
206
|
-
}
|
|
207
|
-
// Tempo preference keys are starter ids too.
|
|
208
213
|
for (const id of Object.keys(pref.tempos ?? {})) {
|
|
209
|
-
if (!
|
|
214
|
+
if (!allowedPlayerKeys.has(id)) {
|
|
210
215
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
|
|
211
216
|
}
|
|
212
217
|
}
|
|
@@ -223,13 +228,13 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
223
228
|
}
|
|
224
229
|
seenRotations.add(pref.rotation);
|
|
225
230
|
for (const id of Object.keys(pref.commit)) {
|
|
226
|
-
if (!
|
|
231
|
+
if (!allowedPlayerKeys.has(id)) {
|
|
227
232
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
228
233
|
}
|
|
229
234
|
}
|
|
230
235
|
for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
|
|
231
236
|
for (const id of pref.noBlock[form] ?? []) {
|
|
232
|
-
if (!
|
|
237
|
+
if (!allowedPlayerKeys.has(id)) {
|
|
233
238
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
234
239
|
}
|
|
235
240
|
}
|
|
@@ -248,19 +253,13 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
248
253
|
for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
|
|
249
254
|
validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
|
|
250
255
|
}
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
for (const [id, value] of Object.entries(data.serveAttackShares)) {
|
|
259
|
-
if (!liberoReplacementIds.has(id)) {
|
|
260
|
-
ctx.addIssue({ code: 'custom', message: 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT', path: ['serveAttackShares'] });
|
|
261
|
-
}
|
|
262
|
-
if (value < 0 || value > 100) {
|
|
263
|
-
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
|
+
}
|
|
264
263
|
}
|
|
265
264
|
}
|
|
266
265
|
// Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
|
|
@@ -96,7 +96,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
96
96
|
{ rotationSystem: '4-2', designatedSetters: [p1, p4] }
|
|
97
97
|
],
|
|
98
98
|
offensivePreferenceSets: [
|
|
99
|
-
[{ rotation: 1,
|
|
99
|
+
[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }],
|
|
100
100
|
[]
|
|
101
101
|
]
|
|
102
102
|
}));
|
|
@@ -105,7 +105,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
105
105
|
(0, globals_1.it)('carries the per-set fields through Tactics.create parsing', () => {
|
|
106
106
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
107
107
|
systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
|
|
108
|
-
offensivePreferenceSets: [[{ rotation: 2,
|
|
108
|
+
offensivePreferenceSets: [[{ rotation: 2, priorities: { [p3.id]: 'HIGH' } }]]
|
|
109
109
|
}));
|
|
110
110
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
111
111
|
if (res.success) {
|
|
@@ -154,7 +154,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
154
154
|
});
|
|
155
155
|
(0, globals_1.it)('rejects a per-set preferences list with a duplicate rotation', () => {
|
|
156
156
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
157
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
157
|
+
offensivePreferenceSets: [[{ rotation: 1, priorities: { [p2.id]: 'HIGH' } }, { rotation: 1, priorities: { [p3.id]: 'HIGH' } }]]
|
|
158
158
|
}));
|
|
159
159
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
160
160
|
if (!res.success) {
|
|
@@ -164,25 +164,38 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
164
164
|
});
|
|
165
165
|
(0, globals_1.it)('rejects a per-set preferences map that keys a non-starter', () => {
|
|
166
166
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
167
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
167
|
+
offensivePreferenceSets: [[{ rotation: 1, priorities: { [reserve.id]: 'HIGH' } }]]
|
|
168
168
|
}));
|
|
169
169
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
170
170
|
if (!res.success) {
|
|
171
171
|
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
|
-
(0, globals_1.it)('
|
|
174
|
+
(0, globals_1.it)('accepts offensive priorities with no sum constraint', () => {
|
|
175
175
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
176
|
-
offensivePreferences: [{ rotation: 1,
|
|
176
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'LOW' } }]
|
|
177
|
+
}));
|
|
178
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
(0, globals_1.it)('rejects an invalid attack priority value', () => {
|
|
181
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
182
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'MEGA' } }]
|
|
177
183
|
}));
|
|
178
184
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
185
|
+
});
|
|
186
|
+
(0, globals_1.it)('accepts a priority / tempo / no-block keyed by a designated substitute', () => {
|
|
187
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
188
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [reserve] },
|
|
189
|
+
designatedSubs: [{ starter: p2, bench: reserve }],
|
|
190
|
+
offensivePreferences: [{ rotation: 1, priorities: { [reserve.id]: 'HIGH' }, tempos: { [reserve.id]: 'FAST' } }],
|
|
191
|
+
blockingPreferences: [{ rotation: 1, commit: {}, noBlock: { DEF_L: [reserve.id], DEF_M: [], DEF_R: [] } }],
|
|
192
|
+
basePositions: { coords: {}, receive: {}, receiveByPlayer: { 1: { [reserve.id]: false } } }
|
|
193
|
+
}));
|
|
194
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
182
195
|
});
|
|
183
196
|
(0, globals_1.it)('accepts offensive preferences with a starter-keyed tempos map', () => {
|
|
184
197
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
185
|
-
offensivePreferences: [{ rotation: 1,
|
|
198
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
186
199
|
}));
|
|
187
200
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
188
201
|
if (res.success) {
|
|
@@ -191,7 +204,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
191
204
|
});
|
|
192
205
|
(0, globals_1.it)('rejects a tempos map keyed by a non-starter', () => {
|
|
193
206
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
194
|
-
offensivePreferences: [{ rotation: 1,
|
|
207
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH' }, tempos: { [reserve.id]: 'FAST' } }]
|
|
195
208
|
}));
|
|
196
209
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
197
210
|
if (!res.success) {
|
|
@@ -200,7 +213,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
200
213
|
});
|
|
201
214
|
(0, globals_1.it)('rejects an invalid tempo value', () => {
|
|
202
215
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
203
|
-
offensivePreferences: [{ rotation: 1,
|
|
216
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH' }, tempos: { [p2.id]: 'SLOW' } }]
|
|
204
217
|
}));
|
|
205
218
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
206
219
|
});
|
|
@@ -325,83 +338,3 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
325
338
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
326
339
|
});
|
|
327
340
|
});
|
|
328
|
-
(0, globals_1.describe)('TacticsInputSchema — serve attack shares', () => {
|
|
329
|
-
const country = (0, test_helpers_1.makeCountry)();
|
|
330
|
-
const p1 = (0, test_helpers_1.makePlayer)(country);
|
|
331
|
-
const p2 = (0, test_helpers_1.makePlayer)(country);
|
|
332
|
-
const p3 = (0, test_helpers_1.makePlayer)(country);
|
|
333
|
-
const p4 = (0, test_helpers_1.makePlayer)(country);
|
|
334
|
-
const p5 = (0, test_helpers_1.makePlayer)(country);
|
|
335
|
-
const p6 = (0, test_helpers_1.makePlayer)(country);
|
|
336
|
-
const reserve = (0, test_helpers_1.makePlayer)(country);
|
|
337
|
-
// p5 sits at zone 5 (back row) and is the configured libero replacement, so it may carry a serve share.
|
|
338
|
-
function baseInput(overrides = {}) {
|
|
339
|
-
return {
|
|
340
|
-
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
341
|
-
liberoReplacements: [p5],
|
|
342
|
-
...overrides
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
(0, globals_1.it)('accepts a serve share keyed by a libero replacement, value in band', () => {
|
|
346
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 30 } }));
|
|
347
|
-
(0, globals_1.expect)(res.success).toBe(true);
|
|
348
|
-
if (res.success)
|
|
349
|
-
(0, globals_1.expect)(res.data.serveAttackShares[p5.id]).toBe(30);
|
|
350
|
-
});
|
|
351
|
-
(0, globals_1.it)('accepts 0 and 100 at the band edges', () => {
|
|
352
|
-
(0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 0 } })).success).toBe(true);
|
|
353
|
-
(0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 100 } })).success).toBe(true);
|
|
354
|
-
});
|
|
355
|
-
(0, globals_1.it)('rejects a serve share keyed by a starter who is not a libero replacement', () => {
|
|
356
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p2.id]: 30 } }));
|
|
357
|
-
(0, globals_1.expect)(res.success).toBe(false);
|
|
358
|
-
if (!res.success) {
|
|
359
|
-
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT')).toBe(true);
|
|
360
|
-
}
|
|
361
|
-
});
|
|
362
|
-
(0, globals_1.it)('rejects a serve share keyed by a reserve', () => {
|
|
363
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [reserve.id]: 30 } }));
|
|
364
|
-
(0, globals_1.expect)(res.success).toBe(false);
|
|
365
|
-
if (!res.success) {
|
|
366
|
-
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_NOT_A_LIBERO_REPLACEMENT')).toBe(true);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
(0, globals_1.it)('rejects an out-of-band serve share (over 100)', () => {
|
|
370
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: 101 } }));
|
|
371
|
-
(0, globals_1.expect)(res.success).toBe(false);
|
|
372
|
-
if (!res.success) {
|
|
373
|
-
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_OUT_OF_BAND')).toBe(true);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
(0, globals_1.it)('rejects a negative serve share', () => {
|
|
377
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({ serveAttackShares: { [p5.id]: -1 } }));
|
|
378
|
-
(0, globals_1.expect)(res.success).toBe(false);
|
|
379
|
-
if (!res.success) {
|
|
380
|
-
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'SERVE_ATTACK_SHARE_OUT_OF_BAND')).toBe(true);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
(0, globals_1.it)('validates independently of the offensive-preference sum-to-100 (never enters that total)', () => {
|
|
384
|
-
// The other attackers' weights sum to exactly 100; the serve share on the libero replacement is separate
|
|
385
|
-
// and must not push the rotation over 100 or otherwise fail.
|
|
386
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
387
|
-
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 50, [p3.id]: 50 } }],
|
|
388
|
-
serveAttackShares: { [p5.id]: 40 }
|
|
389
|
-
}));
|
|
390
|
-
(0, globals_1.expect)(res.success).toBe(true);
|
|
391
|
-
});
|
|
392
|
-
(0, globals_1.it)('accepts a serve share on a per-set libero replacement', () => {
|
|
393
|
-
// p6 is a back-row starter made a libero replacement only via liberoReplacementSets.
|
|
394
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
395
|
-
liberoReplacements: [],
|
|
396
|
-
liberoReplacementSets: [[p6]],
|
|
397
|
-
serveAttackShares: { [p6.id]: 25 }
|
|
398
|
-
}));
|
|
399
|
-
(0, globals_1.expect)(res.success).toBe(true);
|
|
400
|
-
});
|
|
401
|
-
(0, globals_1.it)('defaults serveAttackShares to an empty map when omitted', () => {
|
|
402
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
|
|
403
|
-
(0, globals_1.expect)(res.success).toBe(true);
|
|
404
|
-
if (res.success)
|
|
405
|
-
(0, globals_1.expect)(res.data.serveAttackShares).toEqual({});
|
|
406
|
-
});
|
|
407
|
-
});
|
|
@@ -269,23 +269,32 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
269
269
|
designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
270
270
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
271
271
|
rotation: z.ZodNumber;
|
|
272
|
-
|
|
272
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
273
|
+
HIGH: "HIGH";
|
|
274
|
+
MID: "MID";
|
|
275
|
+
LOW: "LOW";
|
|
276
|
+
NONE: "NONE";
|
|
277
|
+
}>>;
|
|
273
278
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
274
|
-
FAST: "FAST";
|
|
275
279
|
HIGH: "HIGH";
|
|
280
|
+
FAST: "FAST";
|
|
276
281
|
}>>>;
|
|
277
282
|
}, z.core.$strip>>>;
|
|
278
|
-
serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
279
283
|
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
280
284
|
rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
|
|
281
285
|
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
282
286
|
}, z.core.$strip>>>;
|
|
283
287
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
284
288
|
rotation: z.ZodNumber;
|
|
285
|
-
|
|
289
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
290
|
+
HIGH: "HIGH";
|
|
291
|
+
MID: "MID";
|
|
292
|
+
LOW: "LOW";
|
|
293
|
+
NONE: "NONE";
|
|
294
|
+
}>>;
|
|
286
295
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
287
|
-
FAST: "FAST";
|
|
288
296
|
HIGH: "HIGH";
|
|
297
|
+
FAST: "FAST";
|
|
289
298
|
}>>>;
|
|
290
299
|
}, z.core.$strip>>>>;
|
|
291
300
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -312,7 +321,12 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
312
321
|
r: z.ZodNumber;
|
|
313
322
|
}, z.core.$strip>>>>;
|
|
314
323
|
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
324
|
+
receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
315
325
|
blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
|
|
326
|
+
attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
327
|
+
d: z.ZodNumber;
|
|
328
|
+
r: z.ZodNumber;
|
|
329
|
+
}, z.core.$strip>>>>;
|
|
316
330
|
}, z.core.$strip>>;
|
|
317
331
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
318
332
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -35,7 +35,6 @@ export interface TacticsOpts {
|
|
|
35
35
|
readonly rotationSystem: RotationSystemEnum;
|
|
36
36
|
readonly designatedSetters: Player[];
|
|
37
37
|
readonly offensivePreferences: OffensivePreference[];
|
|
38
|
-
readonly serveAttackShares: Record<string, number>;
|
|
39
38
|
readonly systemSets?: SystemSet[];
|
|
40
39
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
41
40
|
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
@@ -57,7 +56,6 @@ export declare class Tactics {
|
|
|
57
56
|
readonly rotationSystem: RotationSystemEnum;
|
|
58
57
|
readonly designatedSetters: Player[];
|
|
59
58
|
readonly offensivePreferences: OffensivePreference[];
|
|
60
|
-
readonly serveAttackShares: Record<string, number>;
|
|
61
59
|
readonly systemSets?: SystemSet[];
|
|
62
60
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
63
61
|
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
@@ -18,7 +18,7 @@ class Tactics {
|
|
|
18
18
|
}
|
|
19
19
|
return new Tactics(result.data);
|
|
20
20
|
}
|
|
21
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [],
|
|
21
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -30,7 +30,6 @@ class Tactics {
|
|
|
30
30
|
this.rotationSystem = rotationSystem;
|
|
31
31
|
this.designatedSetters = designatedSetters;
|
|
32
32
|
this.offensivePreferences = offensivePreferences;
|
|
33
|
-
this.serveAttackShares = serveAttackShares;
|
|
34
33
|
this.systemSets = systemSets;
|
|
35
34
|
this.offensivePreferenceSets = offensivePreferenceSets;
|
|
36
35
|
this.replaceKnockedImmediately = replaceKnockedImmediately;
|
|
@@ -97,7 +97,7 @@ export interface ApiDesignatedSub extends ApiSetSubConfig {
|
|
|
97
97
|
}
|
|
98
98
|
export interface ApiOffensivePreference {
|
|
99
99
|
rotation: number;
|
|
100
|
-
|
|
100
|
+
priorities: Record<string, 'HIGH' | 'MID' | 'LOW' | 'NONE'>;
|
|
101
101
|
tempos?: Record<string, 'FAST' | 'HIGH'>;
|
|
102
102
|
}
|
|
103
103
|
export interface ApiBlockingPreference {
|
|
@@ -140,7 +140,6 @@ export interface Tactics {
|
|
|
140
140
|
offensivePreferences: ApiOffensivePreference[];
|
|
141
141
|
systemSets?: ApiSystemSet[];
|
|
142
142
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
143
|
-
serveAttackShares?: Record<string, number>;
|
|
144
143
|
basePositions?: BaseConfig;
|
|
145
144
|
blockingPreferences: ApiBlockingPreference[];
|
|
146
145
|
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
@@ -23,9 +23,9 @@ export function collectTacticsPlayerIds(config) {
|
|
|
23
23
|
config.secondLibero,
|
|
24
24
|
...((config.liberoReplacementSets ?? []).flat()),
|
|
25
25
|
...(config.designatedSetters ?? []),
|
|
26
|
-
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.
|
|
26
|
+
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
|
|
27
27
|
...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
|
|
28
|
-
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.
|
|
28
|
+
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
|
|
29
29
|
...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
30
30
|
...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
31
31
|
// Injury replacements reference reserves (roster players outside the lineup); roster membership is the only
|
|
@@ -31,7 +31,7 @@ export interface DesignatedSubAttributes extends SetSubConfigAttributes {
|
|
|
31
31
|
}
|
|
32
32
|
export interface OffensivePreferenceAttributes {
|
|
33
33
|
rotation: number;
|
|
34
|
-
|
|
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;
|
|
@@ -72,11 +72,6 @@ export class TacticsModel extends Model {
|
|
|
72
72
|
type: DataTypes.JSONB,
|
|
73
73
|
allowNull: true
|
|
74
74
|
},
|
|
75
|
-
serve_attack_shares: {
|
|
76
|
-
type: DataTypes.JSONB,
|
|
77
|
-
allowNull: false,
|
|
78
|
-
defaultValue: {}
|
|
79
|
-
},
|
|
80
75
|
replace_knocked_immediately: {
|
|
81
76
|
type: DataTypes.JSONB,
|
|
82
77
|
allowNull: true
|
|
@@ -67,7 +67,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
67
67
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
68
68
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
69
69
|
rotation: p.rotation,
|
|
70
|
-
|
|
70
|
+
priorities: p.priorities,
|
|
71
71
|
tempos: p.tempos
|
|
72
72
|
})),
|
|
73
73
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -76,10 +76,9 @@ function transformToAttributes(tactics, teamId) {
|
|
|
76
76
|
})),
|
|
77
77
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
78
78
|
rotation: p.rotation,
|
|
79
|
-
|
|
79
|
+
priorities: p.priorities,
|
|
80
80
|
tempos: p.tempos
|
|
81
81
|
}))),
|
|
82
|
-
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
83
82
|
base_positions: tactics.basePositions,
|
|
84
83
|
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
85
84
|
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -175,7 +174,7 @@ function transformToObject(model, roster) {
|
|
|
175
174
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
176
175
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
177
176
|
rotation: p.rotation,
|
|
178
|
-
|
|
177
|
+
priorities: p.priorities,
|
|
179
178
|
tempos: p.tempos
|
|
180
179
|
})),
|
|
181
180
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -184,24 +183,9 @@ function transformToObject(model, roster) {
|
|
|
184
183
|
})),
|
|
185
184
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
186
185
|
rotation: p.rotation,
|
|
187
|
-
|
|
186
|
+
priorities: p.priorities,
|
|
188
187
|
tempos: p.tempos
|
|
189
188
|
}))),
|
|
190
|
-
// LENIENT: keep only serve shares keyed by a current libero-replacement (flat or per-set) with a value in the
|
|
191
|
-
// 0-100 band, so a stale key (a released player, or a libero-replacement config change) can never fail schema
|
|
192
|
-
// validation and crash a team load; a dropped key = no serve share.
|
|
193
|
-
serveAttackShares: (() => {
|
|
194
|
-
const liberoReplacementIds = new Set([
|
|
195
|
-
...model.libero_replacements,
|
|
196
|
-
...(model.libero_replacement_sets ?? []).flat()
|
|
197
|
-
]);
|
|
198
|
-
const healed = {};
|
|
199
|
-
for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
|
|
200
|
-
if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
|
|
201
|
-
healed[id] = value;
|
|
202
|
-
}
|
|
203
|
-
return healed;
|
|
204
|
-
})(),
|
|
205
189
|
basePositions: model.base_positions ?? undefined,
|
|
206
190
|
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
207
191
|
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -348,21 +332,6 @@ function tacticsColumnsToApiDto(model) {
|
|
|
348
332
|
offensivePreferences: model.offensive_preferences,
|
|
349
333
|
systemSets: model.system_sets,
|
|
350
334
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
351
|
-
// Same stale-data policy as the heals below: only serve shares keyed by a current libero-replacement (flat or
|
|
352
|
-
// per-set) with a value in the 0-100 band survive; an empty map surfaces as absent so the UI never reloads and
|
|
353
|
-
// re-emits a stale key.
|
|
354
|
-
serveAttackShares: (() => {
|
|
355
|
-
const liberoReplacementIds = new Set([
|
|
356
|
-
...model.libero_replacements,
|
|
357
|
-
...(model.libero_replacement_sets ?? []).flat()
|
|
358
|
-
]);
|
|
359
|
-
const healed = {};
|
|
360
|
-
for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
|
|
361
|
-
if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
|
|
362
|
-
healed[id] = value;
|
|
363
|
-
}
|
|
364
|
-
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
365
|
-
})(),
|
|
366
335
|
basePositions: model.base_positions ?? undefined,
|
|
367
336
|
blockingPreferences: model.blocking_preferences,
|
|
368
337
|
blockingPreferenceSets: model.blocking_preference_sets,
|
|
@@ -471,7 +440,6 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
471
440
|
offensive_preferences: config.offensivePreferences,
|
|
472
441
|
system_sets: config.systemSets,
|
|
473
442
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
474
|
-
serve_attack_shares: config.serveAttackShares ?? {},
|
|
475
443
|
base_positions: config.basePositions,
|
|
476
444
|
blocking_preferences: config.blockingPreferences ?? [],
|
|
477
445
|
blocking_preference_sets: config.blockingPreferenceSets,
|