volleyballsimtypes 0.0.511 → 0.0.512
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 +3 -2
- package/dist/cjs/src/api/tactics-player-refs.d.ts +2 -2
- package/dist/cjs/src/api/tactics-player-refs.js +4 -4
- package/dist/cjs/src/data/models/tactics.d.ts +3 -2
- package/dist/cjs/src/data/transformers/tactics.js +4 -4
- package/dist/cjs/src/data/transformers/tactics.test.js +16 -14
- package/dist/cjs/src/service/team/blocking-preference.d.ts +2 -3
- package/dist/cjs/src/service/team/blocking-preference.js +1 -7
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +12 -10
- package/dist/cjs/src/service/team/schemas/tactics.z.js +24 -20
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +34 -35
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +12 -10
- package/dist/esm/src/api/index.d.ts +3 -2
- package/dist/esm/src/api/tactics-player-refs.d.ts +2 -2
- package/dist/esm/src/api/tactics-player-refs.js +4 -4
- package/dist/esm/src/data/models/tactics.d.ts +3 -2
- package/dist/esm/src/data/transformers/tactics.js +4 -4
- package/dist/esm/src/data/transformers/tactics.test.js +17 -15
- package/dist/esm/src/service/team/blocking-preference.d.ts +2 -3
- package/dist/esm/src/service/team/blocking-preference.js +0 -6
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +12 -10
- package/dist/esm/src/service/team/schemas/tactics.z.js +24 -20
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +34 -35
- package/dist/esm/src/service/team/schemas/team.z.d.ts +12 -10
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country,
|
|
1
|
+
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, DefenceFormation, LineupFunction, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export * from './tactics-player-refs';
|
|
3
3
|
export type Rally = DataProps<_Rally> & {
|
|
4
4
|
homePlayerPosition: PlayerPosition[];
|
|
@@ -102,7 +102,8 @@ export interface ApiOffensivePreference {
|
|
|
102
102
|
}
|
|
103
103
|
export interface ApiBlockingPreference {
|
|
104
104
|
rotation: number;
|
|
105
|
-
|
|
105
|
+
commit: Record<string, LineupFunction>;
|
|
106
|
+
noBlock: Record<DefenceFormation, string[]>;
|
|
106
107
|
}
|
|
107
108
|
export interface ApiSystemSet {
|
|
108
109
|
rotationSystem: RotationSystemEnum;
|
|
@@ -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
|
|
6
|
-
* and the libero-injury reserves.
|
|
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.
|
|
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
|
|
8
|
-
* and the libero-injury reserves.
|
|
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.
|
|
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
|
|
@@ -29,8 +29,8 @@ function collectTacticsPlayerIds(config) {
|
|
|
29
29
|
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.weights))),
|
|
30
30
|
...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
|
|
31
31
|
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
|
|
32
|
-
...((config.blockingPreferences ?? []).flatMap(p => Object.
|
|
33
|
-
...((config.blockingPreferenceSets ?? []).flat().flatMap(p => Object.
|
|
32
|
+
...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
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
|
|
35
35
|
// player check possible here (the schema enforces the bench/reserve role rules).
|
|
36
36
|
...((config.injuryReplacements ?? []).flatMap(ir => [ir.starterId, ir.replacementId, ir.benchBackfillId])),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
3
|
import { PlayerId, TeamId, TeamModel } from '.';
|
|
4
|
-
import { BaseConfig,
|
|
4
|
+
import { BaseConfig, ConditionLogic, CourtPosition, DefenceFormation, EnergyBand, LineupFunction, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -36,7 +36,8 @@ export interface OffensivePreferenceAttributes {
|
|
|
36
36
|
}
|
|
37
37
|
export interface BlockingPreferenceAttributes {
|
|
38
38
|
rotation: number;
|
|
39
|
-
|
|
39
|
+
commit: Record<PlayerId, LineupFunction>;
|
|
40
|
+
noBlock: Record<DefenceFormation, PlayerId[]>;
|
|
40
41
|
}
|
|
41
42
|
export interface SystemSetAttributes {
|
|
42
43
|
rotationSystem: RotationSystemEnum;
|
|
@@ -87,8 +87,8 @@ function transformToAttributes(tactics, teamId) {
|
|
|
87
87
|
}))),
|
|
88
88
|
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
89
89
|
base_positions: tactics.basePositions,
|
|
90
|
-
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation,
|
|
91
|
-
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation,
|
|
90
|
+
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
91
|
+
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
92
92
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
93
93
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
94
94
|
starterId: ir.starter.id,
|
|
@@ -209,8 +209,8 @@ function transformToObject(model, roster) {
|
|
|
209
209
|
return healed;
|
|
210
210
|
})(),
|
|
211
211
|
basePositions: model.base_positions ?? undefined,
|
|
212
|
-
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation,
|
|
213
|
-
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation,
|
|
212
|
+
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
213
|
+
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
214
214
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
215
215
|
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
216
216
|
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
@@ -264,37 +264,39 @@ function makeTactics(designatedSubs) {
|
|
|
264
264
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
265
265
|
(0, globals_1.expect)(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
266
266
|
});
|
|
267
|
-
(0, globals_1.it)('round-trips blocking preferences as per-
|
|
267
|
+
(0, globals_1.it)('round-trips blocking preferences as a base commit map plus per-side no-block ids', () => {
|
|
268
268
|
const withBlocking = service_1.Tactics.create({
|
|
269
269
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
270
270
|
liberoReplacements: [],
|
|
271
|
-
blockingPreferences: [{ rotation: 1,
|
|
271
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p2.id] } }]
|
|
272
272
|
});
|
|
273
273
|
const attrs = (0, tactics_1.transformFromTactics)(withBlocking, 'team-1');
|
|
274
274
|
(0, globals_1.expect)(attrs.blocking_preferences).toEqual([
|
|
275
|
-
{ rotation: 1,
|
|
275
|
+
{ rotation: 1, commit: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p2.id], DEF_M: [], DEF_R: [] } }
|
|
276
276
|
]);
|
|
277
277
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
278
|
-
(0, globals_1.expect)(back.blockingPreferences[0].
|
|
278
|
+
(0, globals_1.expect)(back.blockingPreferences[0].commit).toEqual({ [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
|
|
279
|
+
(0, globals_1.expect)(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
|
|
279
280
|
});
|
|
280
|
-
//
|
|
281
|
-
// mapping code. This pins that
|
|
282
|
-
//
|
|
283
|
-
(0, globals_1.it)('round-trips a
|
|
281
|
+
// The base commit map (blind) and the per-side no-block ids are copied wholesale by the transformers with no
|
|
282
|
+
// mapping code. This pins that they survive the row round-trip verbatim, including in the per-set overrides. If a
|
|
283
|
+
// no-block id is ever translated or dropped, a player silently starts blocking again.
|
|
284
|
+
(0, globals_1.it)('round-trips a base commit plus per-side no-block ids, including per-set overrides', () => {
|
|
284
285
|
const withNoBlock = service_1.Tactics.create({
|
|
285
286
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
286
287
|
liberoReplacements: [],
|
|
287
|
-
blockingPreferences: [{ rotation: 1,
|
|
288
|
-
blockingPreferenceSets: [[{ rotation: 2,
|
|
288
|
+
blockingPreferences: [{ rotation: 1, commit: { [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_M: [p4.id] } }],
|
|
289
|
+
blockingPreferenceSets: [[{ rotation: 2, noBlock: { DEF_L: [p2.id] } }]]
|
|
289
290
|
});
|
|
290
291
|
const attrs = (0, tactics_1.transformFromTactics)(withNoBlock, 'team-1');
|
|
291
292
|
(0, globals_1.expect)(attrs.blocking_preferences).toEqual([
|
|
292
|
-
{ rotation: 1,
|
|
293
|
+
{ rotation: 1, commit: { [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [], DEF_M: [p4.id], DEF_R: [] } }
|
|
293
294
|
]);
|
|
294
|
-
(0, globals_1.expect)(attrs.blocking_preference_sets).toEqual([[{ rotation: 2,
|
|
295
|
+
(0, globals_1.expect)(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, commit: {}, noBlock: { DEF_L: [p2.id], DEF_M: [], DEF_R: [] } }]]);
|
|
295
296
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
296
|
-
(0, globals_1.expect)(back.blockingPreferences[0].
|
|
297
|
-
(0, globals_1.expect)(back.
|
|
297
|
+
(0, globals_1.expect)(back.blockingPreferences[0].commit).toEqual({ [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
|
|
298
|
+
(0, globals_1.expect)(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
|
|
299
|
+
(0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
|
|
298
300
|
});
|
|
299
301
|
});
|
|
300
302
|
(0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { LineupFunction } from './lineup-function';
|
|
2
|
-
export declare const NO_BLOCK: "NO_BLOCK";
|
|
3
|
-
export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
|
|
4
2
|
export type DefenceFormation = 'DEF_L' | 'DEF_M' | 'DEF_R';
|
|
5
3
|
export declare const DEFENCE_FORMATIONS: readonly DefenceFormation[];
|
|
6
4
|
export interface BlockingPreference {
|
|
7
5
|
readonly rotation: number;
|
|
8
|
-
readonly
|
|
6
|
+
readonly commit: Record<string, LineupFunction>;
|
|
7
|
+
readonly noBlock: Record<DefenceFormation, string[]>;
|
|
9
8
|
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFENCE_FORMATIONS =
|
|
4
|
-
// A front-row player opted OUT of blocking for this rotation. They never join the wall under any circumstance:
|
|
5
|
-
// not as the lead blocker (the lane's next-nearest front-row player leads instead), not as a side blocker, and
|
|
6
|
-
// no trait can coerce them in -- MOVING_WALL's guaranteed lead hand, its beaten-read hand and its side-joiner
|
|
7
|
-
// roll are all suppressed, as is a dedicated-blocking commit. Sits in the same `targets` map as a commit
|
|
8
|
-
// assignment, so the two are mutually exclusive by construction: a player is reading, committed, or out.
|
|
9
|
-
exports.NO_BLOCK = 'NO_BLOCK';
|
|
3
|
+
exports.DEFENCE_FORMATIONS = void 0;
|
|
10
4
|
exports.DEFENCE_FORMATIONS = ['DEF_L', 'DEF_M', 'DEF_R'];
|
|
@@ -282,19 +282,21 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
282
282
|
}, z.core.$strip>>>>;
|
|
283
283
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
284
284
|
rotation: z.ZodNumber;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>>>;
|
|
286
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
287
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
288
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
289
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
290
|
+
}, z.core.$strip>>;
|
|
290
291
|
}, z.core.$strip>>>;
|
|
291
292
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
292
293
|
rotation: z.ZodNumber;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>>>;
|
|
295
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
296
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
297
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
298
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
299
|
+
}, z.core.$strip>>;
|
|
298
300
|
}, z.core.$strip>>>>;
|
|
299
301
|
basePositions: z.ZodOptional<z.ZodObject<{
|
|
300
302
|
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.TacticsInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
-
const blocking_preference_1 = require("../blocking-preference");
|
|
7
6
|
const energy_band_1 = require("../energy-band");
|
|
8
7
|
const rotation_system_1 = require("../rotation-system");
|
|
9
8
|
const designated_sub_z_1 = require("./designated-sub.z");
|
|
@@ -60,24 +59,23 @@ const offensivePreferenceSchema = zod_1.z.object({
|
|
|
60
59
|
weights: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(0)),
|
|
61
60
|
tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
|
|
62
61
|
});
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
// An opposing attacking function a front-row starter can commit to (every RoleEnum except the libero, which never
|
|
63
|
+
// attacks). The commit is the BLIND, pre-set Defence-base choice (owner 2026-08-10).
|
|
64
|
+
const commitFunctionSchema = zod_1.z.nativeEnum(player_1.RoleEnum).refine((v) => v !== player_1.RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
|
|
65
|
+
// One rotation's blocking config (owner 2026-08-10). `commit` maps my front-row starter ids to the opposing function
|
|
66
|
+
// each commits to (blind, rotation-wide; absent = reads); `noBlock` lists, per point-of-attack defence, the front-row
|
|
67
|
+
// starters that sit out of the block for an attack from that side (overrides the commit for that side). Each side
|
|
68
|
+
// defaults to [] so a partial config stays valid. Cross-field checks (keys/ids are starters) live in the superRefine.
|
|
69
|
+
const commitSchema = zod_1.z.record(zod_1.z.string(), commitFunctionSchema);
|
|
70
|
+
const noBlockSchema = zod_1.z.object({
|
|
71
|
+
DEF_L: zod_1.z.array(zod_1.z.string()).default([]),
|
|
72
|
+
DEF_M: zod_1.z.array(zod_1.z.string()).default([]),
|
|
73
|
+
DEF_R: zod_1.z.array(zod_1.z.string()).default([])
|
|
74
|
+
});
|
|
74
75
|
const blockingPreferenceSchema = zod_1.z.object({
|
|
75
76
|
rotation: zod_1.z.number().int().min(1).max(6),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
DEF_M: blockingTargetsSchema.default({}),
|
|
79
|
-
DEF_R: blockingTargetsSchema.default({})
|
|
80
|
-
})
|
|
77
|
+
commit: commitSchema.default({}),
|
|
78
|
+
noBlock: noBlockSchema.default({ DEF_L: [], DEF_M: [], DEF_R: [] })
|
|
81
79
|
});
|
|
82
80
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
83
81
|
const systemSetSchema = zod_1.z.object({
|
|
@@ -214,8 +212,9 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
214
212
|
}
|
|
215
213
|
}
|
|
216
214
|
};
|
|
217
|
-
// Blocking preferences: unique rotations;
|
|
218
|
-
// players, the schema checks starters). The
|
|
215
|
+
// Blocking preferences: unique rotations; every committer id (base commit) and every no-block id (per side) keys
|
|
216
|
+
// an on-court starter (the UI only offers front-row players, the schema checks starters). The commit function is
|
|
217
|
+
// validated by commitFunctionSchema.
|
|
219
218
|
const validateBlockingPreferences = (preferences, path) => {
|
|
220
219
|
const seenRotations = new Set();
|
|
221
220
|
for (const pref of preferences) {
|
|
@@ -223,8 +222,13 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
223
222
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
224
223
|
}
|
|
225
224
|
seenRotations.add(pref.rotation);
|
|
225
|
+
for (const id of Object.keys(pref.commit)) {
|
|
226
|
+
if (!starterIds.has(id)) {
|
|
227
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
226
230
|
for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
|
|
227
|
-
for (const id of
|
|
231
|
+
for (const id of pref.noBlock[form] ?? []) {
|
|
228
232
|
if (!starterIds.has(id)) {
|
|
229
233
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
230
234
|
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const globals_1 = require("@jest/globals");
|
|
4
4
|
const tactics_z_1 = require("./tactics.z");
|
|
5
|
-
const blocking_preference_1 = require("../blocking-preference");
|
|
6
5
|
const player_1 = require("../../player");
|
|
7
6
|
const test_helpers_1 = require("../../test-helpers");
|
|
8
7
|
(0, globals_1.describe)('TacticsInputSchema — second libero rules', () => {
|
|
@@ -236,40 +235,49 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
236
235
|
...overrides
|
|
237
236
|
};
|
|
238
237
|
}
|
|
239
|
-
(0, globals_1.it)('accepts a blocking preference that
|
|
238
|
+
(0, globals_1.it)('accepts a blocking preference that commits starters to attacking functions', () => {
|
|
240
239
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
241
|
-
blockingPreferences: [{ rotation: 1,
|
|
240
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: player_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [], DEF_M: [], DEF_R: [] } }]
|
|
242
241
|
}));
|
|
243
242
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
244
243
|
if (res.success) {
|
|
245
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
246
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
247
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].targets.DEF_M).toEqual({}); // the middle side is independent (empty here)
|
|
244
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].commit[p4.id]).toBe(player_1.RoleEnum.OUTSIDE_HITTER);
|
|
245
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].commit[p3.id]).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
|
|
248
246
|
}
|
|
249
247
|
});
|
|
250
|
-
(0, globals_1.it)('defaults
|
|
248
|
+
(0, globals_1.it)('defaults an omitted commit to empty and omitted no-block sides to empty arrays', () => {
|
|
251
249
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
252
|
-
blockingPreferences: [{ rotation: 1,
|
|
250
|
+
blockingPreferences: [{ rotation: 1, noBlock: { DEF_L: [p4.id] } }]
|
|
253
251
|
}));
|
|
254
252
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
255
253
|
if (res.success) {
|
|
256
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
257
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
258
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
254
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].commit).toEqual({});
|
|
255
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].noBlock.DEF_L).toEqual([p4.id]);
|
|
256
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].noBlock.DEF_M).toEqual([]);
|
|
257
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].noBlock.DEF_R).toEqual([]);
|
|
259
258
|
}
|
|
260
259
|
});
|
|
261
|
-
(0, globals_1.it)('rejects a
|
|
260
|
+
(0, globals_1.it)('rejects a commit id that is not a starter', () => {
|
|
262
261
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
263
|
-
blockingPreferences: [{ rotation: 1,
|
|
262
|
+
blockingPreferences: [{ rotation: 1, commit: { [reserve.id]: player_1.RoleEnum.OUTSIDE_HITTER } }]
|
|
264
263
|
}));
|
|
265
264
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
266
265
|
if (!res.success) {
|
|
267
266
|
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
268
267
|
}
|
|
269
268
|
});
|
|
270
|
-
(0, globals_1.it)('rejects a
|
|
269
|
+
(0, globals_1.it)('rejects a no-block id (on any side) that is not a starter', () => {
|
|
271
270
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
272
|
-
blockingPreferences: [{ rotation: 1,
|
|
271
|
+
blockingPreferences: [{ rotation: 1, noBlock: { DEF_M: [reserve.id] } }]
|
|
272
|
+
}));
|
|
273
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
274
|
+
if (!res.success) {
|
|
275
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
(0, globals_1.it)('rejects a commit function of libero', () => {
|
|
279
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
280
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: player_1.RoleEnum.LIBERO } }]
|
|
273
281
|
}));
|
|
274
282
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
275
283
|
if (!res.success) {
|
|
@@ -279,8 +287,8 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
279
287
|
(0, globals_1.it)('rejects a duplicate rotation in blocking preferences', () => {
|
|
280
288
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
281
289
|
blockingPreferences: [
|
|
282
|
-
{ rotation: 1,
|
|
283
|
-
{ rotation: 1,
|
|
290
|
+
{ rotation: 1, commit: { [p4.id]: player_1.RoleEnum.OUTSIDE_HITTER } },
|
|
291
|
+
{ rotation: 1, commit: { [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER } }
|
|
284
292
|
]
|
|
285
293
|
}));
|
|
286
294
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
@@ -290,38 +298,29 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
290
298
|
});
|
|
291
299
|
(0, globals_1.it)('accepts per-set blocking preference overrides', () => {
|
|
292
300
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
293
|
-
blockingPreferenceSets: [[{ rotation: 1,
|
|
301
|
+
blockingPreferenceSets: [[{ rotation: 1, commit: { [p4.id]: player_1.RoleEnum.OPPOSITE_HITTER } }], []]
|
|
294
302
|
}));
|
|
295
303
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
296
304
|
});
|
|
297
|
-
(0, globals_1.it)('accepts
|
|
305
|
+
(0, globals_1.it)('accepts a starter committed on the base AND no-blocked on a side (no-block overrides commit there)', () => {
|
|
298
306
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
299
|
-
blockingPreferences: [{ rotation: 1,
|
|
307
|
+
blockingPreferences: [{ rotation: 1, commit: { [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p3.id] } }]
|
|
300
308
|
}));
|
|
301
309
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
302
310
|
if (res.success) {
|
|
303
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
304
|
-
(0, globals_1.expect)(res.data.blockingPreferences[0].
|
|
311
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].commit[p3.id]).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
|
|
312
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].noBlock.DEF_L).toEqual([p3.id]);
|
|
305
313
|
}
|
|
306
314
|
});
|
|
307
|
-
(0, globals_1.it)('accepts
|
|
315
|
+
(0, globals_1.it)('accepts a no-block in a per-set override', () => {
|
|
308
316
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
309
|
-
blockingPreferenceSets: [[{ rotation: 1,
|
|
317
|
+
blockingPreferenceSets: [[{ rotation: 1, noBlock: { DEF_M: [p4.id] } }], []]
|
|
310
318
|
}));
|
|
311
319
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
312
320
|
});
|
|
313
|
-
(0, globals_1.it)('
|
|
314
|
-
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
315
|
-
blockingPreferences: [{ rotation: 1, targets: { DEF_R: { [reserve.id]: blocking_preference_1.NO_BLOCK } } }]
|
|
316
|
-
}));
|
|
317
|
-
(0, globals_1.expect)(res.success).toBe(false);
|
|
318
|
-
if (!res.success) {
|
|
319
|
-
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
(0, globals_1.it)('rejects an unknown assignment string', () => {
|
|
321
|
+
(0, globals_1.it)('rejects an unknown commit function string', () => {
|
|
323
322
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
324
|
-
blockingPreferences: [{ rotation: 1,
|
|
323
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: 'NOT_A_ROLE' } }]
|
|
325
324
|
}));
|
|
326
325
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
327
326
|
});
|
|
@@ -290,19 +290,21 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
290
290
|
}, z.core.$strip>>>>;
|
|
291
291
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
292
292
|
rotation: z.ZodNumber;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>>;
|
|
294
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
295
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
296
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
297
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
298
|
+
}, z.core.$strip>>;
|
|
298
299
|
}, z.core.$strip>>>;
|
|
299
300
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
300
301
|
rotation: z.ZodNumber;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>>;
|
|
303
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
304
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
305
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
306
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
307
|
+
}, z.core.$strip>>;
|
|
306
308
|
}, z.core.$strip>>>>;
|
|
307
309
|
basePositions: z.ZodOptional<z.ZodObject<{
|
|
308
310
|
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country,
|
|
1
|
+
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, DefenceFormation, LineupFunction, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export * from './tactics-player-refs';
|
|
3
3
|
export type Rally = DataProps<_Rally> & {
|
|
4
4
|
homePlayerPosition: PlayerPosition[];
|
|
@@ -102,7 +102,8 @@ export interface ApiOffensivePreference {
|
|
|
102
102
|
}
|
|
103
103
|
export interface ApiBlockingPreference {
|
|
104
104
|
rotation: number;
|
|
105
|
-
|
|
105
|
+
commit: Record<string, LineupFunction>;
|
|
106
|
+
noBlock: Record<DefenceFormation, string[]>;
|
|
106
107
|
}
|
|
107
108
|
export interface ApiSystemSet {
|
|
108
109
|
rotationSystem: RotationSystemEnum;
|
|
@@ -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
|
|
6
|
-
* and the libero-injury reserves.
|
|
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.
|
|
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
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
|
|
3
3
|
* replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
|
|
4
|
-
* setters (flat and per-set), the keys of the offensive-weight
|
|
5
|
-
* and the libero-injury reserves.
|
|
4
|
+
* setters (flat and per-set), the keys of the offensive-weight map, the blocking commit + no-block ids, injury
|
|
5
|
+
* replacements and the libero-injury reserves.
|
|
6
6
|
*
|
|
7
7
|
* There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
|
|
8
8
|
* slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
|
|
@@ -26,8 +26,8 @@ export function collectTacticsPlayerIds(config) {
|
|
|
26
26
|
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.weights))),
|
|
27
27
|
...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
|
|
28
28
|
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
|
|
29
|
-
...((config.blockingPreferences ?? []).flatMap(p => Object.
|
|
30
|
-
...((config.blockingPreferenceSets ?? []).flat().flatMap(p => Object.
|
|
29
|
+
...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
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
|
|
32
32
|
// player check possible here (the schema enforces the bench/reserve role rules).
|
|
33
33
|
...((config.injuryReplacements ?? []).flatMap(ir => [ir.starterId, ir.replacementId, ir.benchBackfillId])),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
3
|
import { PlayerId, TeamId, TeamModel } from '.';
|
|
4
|
-
import { BaseConfig,
|
|
4
|
+
import { BaseConfig, ConditionLogic, CourtPosition, DefenceFormation, EnergyBand, LineupFunction, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -36,7 +36,8 @@ export interface OffensivePreferenceAttributes {
|
|
|
36
36
|
}
|
|
37
37
|
export interface BlockingPreferenceAttributes {
|
|
38
38
|
rotation: number;
|
|
39
|
-
|
|
39
|
+
commit: Record<PlayerId, LineupFunction>;
|
|
40
|
+
noBlock: Record<DefenceFormation, PlayerId[]>;
|
|
40
41
|
}
|
|
41
42
|
export interface SystemSetAttributes {
|
|
42
43
|
rotationSystem: RotationSystemEnum;
|
|
@@ -81,8 +81,8 @@ function transformToAttributes(tactics, teamId) {
|
|
|
81
81
|
}))),
|
|
82
82
|
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
83
83
|
base_positions: tactics.basePositions,
|
|
84
|
-
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation,
|
|
85
|
-
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation,
|
|
84
|
+
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
85
|
+
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
86
86
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
87
87
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
88
88
|
starterId: ir.starter.id,
|
|
@@ -203,8 +203,8 @@ function transformToObject(model, roster) {
|
|
|
203
203
|
return healed;
|
|
204
204
|
})(),
|
|
205
205
|
basePositions: model.base_positions ?? undefined,
|
|
206
|
-
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation,
|
|
207
|
-
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation,
|
|
206
|
+
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
207
|
+
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
208
208
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
209
209
|
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
210
210
|
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { tacticsColumnsToApiDto, transformFromTactics, transformToTactics } from './tactics';
|
|
3
|
-
import { CourtPosition, EnergyBand,
|
|
3
|
+
import { CourtPosition, EnergyBand, RoleEnum, RotationSystemEnum, Tactics } from '../../service';
|
|
4
4
|
import { makeCountry, makePlayer } from '../../service/test-helpers';
|
|
5
5
|
// Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
|
|
6
6
|
function makeTactics(designatedSubs) {
|
|
@@ -262,37 +262,39 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
262
262
|
const back = transformToTactics(attrs, roster);
|
|
263
263
|
expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
264
264
|
});
|
|
265
|
-
it('round-trips blocking preferences as per-
|
|
265
|
+
it('round-trips blocking preferences as a base commit map plus per-side no-block ids', () => {
|
|
266
266
|
const withBlocking = Tactics.create({
|
|
267
267
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
268
268
|
liberoReplacements: [],
|
|
269
|
-
blockingPreferences: [{ rotation: 1,
|
|
269
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p2.id] } }]
|
|
270
270
|
});
|
|
271
271
|
const attrs = transformFromTactics(withBlocking, 'team-1');
|
|
272
272
|
expect(attrs.blocking_preferences).toEqual([
|
|
273
|
-
{ rotation: 1,
|
|
273
|
+
{ rotation: 1, commit: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p2.id], DEF_M: [], DEF_R: [] } }
|
|
274
274
|
]);
|
|
275
275
|
const back = transformToTactics(attrs, roster);
|
|
276
|
-
expect(back.blockingPreferences[0].
|
|
276
|
+
expect(back.blockingPreferences[0].commit).toEqual({ [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
277
|
+
expect(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
|
|
277
278
|
});
|
|
278
|
-
//
|
|
279
|
-
// mapping code. This pins that
|
|
280
|
-
//
|
|
281
|
-
it('round-trips a
|
|
279
|
+
// The base commit map (blind) and the per-side no-block ids are copied wholesale by the transformers with no
|
|
280
|
+
// mapping code. This pins that they survive the row round-trip verbatim, including in the per-set overrides. If a
|
|
281
|
+
// no-block id is ever translated or dropped, a player silently starts blocking again.
|
|
282
|
+
it('round-trips a base commit plus per-side no-block ids, including per-set overrides', () => {
|
|
282
283
|
const withNoBlock = Tactics.create({
|
|
283
284
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
284
285
|
liberoReplacements: [],
|
|
285
|
-
blockingPreferences: [{ rotation: 1,
|
|
286
|
-
blockingPreferenceSets: [[{ rotation: 2,
|
|
286
|
+
blockingPreferences: [{ rotation: 1, commit: { [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_M: [p4.id] } }],
|
|
287
|
+
blockingPreferenceSets: [[{ rotation: 2, noBlock: { DEF_L: [p2.id] } }]]
|
|
287
288
|
});
|
|
288
289
|
const attrs = transformFromTactics(withNoBlock, 'team-1');
|
|
289
290
|
expect(attrs.blocking_preferences).toEqual([
|
|
290
|
-
{ rotation: 1,
|
|
291
|
+
{ rotation: 1, commit: { [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [], DEF_M: [p4.id], DEF_R: [] } }
|
|
291
292
|
]);
|
|
292
|
-
expect(attrs.blocking_preference_sets).toEqual([[{ rotation: 2,
|
|
293
|
+
expect(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, commit: {}, noBlock: { DEF_L: [p2.id], DEF_M: [], DEF_R: [] } }]]);
|
|
293
294
|
const back = transformToTactics(attrs, roster);
|
|
294
|
-
expect(back.blockingPreferences[0].
|
|
295
|
-
expect(back.
|
|
295
|
+
expect(back.blockingPreferences[0].commit).toEqual({ [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
296
|
+
expect(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
|
|
297
|
+
expect(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
|
|
296
298
|
});
|
|
297
299
|
});
|
|
298
300
|
describe('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { LineupFunction } from './lineup-function';
|
|
2
|
-
export declare const NO_BLOCK: "NO_BLOCK";
|
|
3
|
-
export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
|
|
4
2
|
export type DefenceFormation = 'DEF_L' | 'DEF_M' | 'DEF_R';
|
|
5
3
|
export declare const DEFENCE_FORMATIONS: readonly DefenceFormation[];
|
|
6
4
|
export interface BlockingPreference {
|
|
7
5
|
readonly rotation: number;
|
|
8
|
-
readonly
|
|
6
|
+
readonly commit: Record<string, LineupFunction>;
|
|
7
|
+
readonly noBlock: Record<DefenceFormation, string[]>;
|
|
9
8
|
}
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
// A front-row player opted OUT of blocking for this rotation. They never join the wall under any circumstance:
|
|
2
|
-
// not as the lead blocker (the lane's next-nearest front-row player leads instead), not as a side blocker, and
|
|
3
|
-
// no trait can coerce them in -- MOVING_WALL's guaranteed lead hand, its beaten-read hand and its side-joiner
|
|
4
|
-
// roll are all suppressed, as is a dedicated-blocking commit. Sits in the same `targets` map as a commit
|
|
5
|
-
// assignment, so the two are mutually exclusive by construction: a player is reading, committed, or out.
|
|
6
|
-
export const NO_BLOCK = 'NO_BLOCK';
|
|
7
1
|
export const DEFENCE_FORMATIONS = ['DEF_L', 'DEF_M', 'DEF_R'];
|
|
@@ -282,19 +282,21 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
282
282
|
}, z.core.$strip>>>>;
|
|
283
283
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
284
284
|
rotation: z.ZodNumber;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>>>;
|
|
286
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
287
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
288
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
289
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
290
|
+
}, z.core.$strip>>;
|
|
290
291
|
}, z.core.$strip>>>;
|
|
291
292
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
292
293
|
rotation: z.ZodNumber;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>>>;
|
|
295
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
296
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
297
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
298
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
299
|
+
}, z.core.$strip>>;
|
|
298
300
|
}, z.core.$strip>>>>;
|
|
299
301
|
basePositions: z.ZodOptional<z.ZodObject<{
|
|
300
302
|
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { Player, RoleEnum } from '../../player';
|
|
3
|
-
import { NO_BLOCK } from '../blocking-preference';
|
|
4
3
|
import { EnergyBand } from '../energy-band';
|
|
5
4
|
import { RotationSystemEnum, setterCountFor } from '../rotation-system';
|
|
6
5
|
import { DesignatedSubSchema, SubBandSchema } from './designated-sub.z';
|
|
@@ -57,24 +56,23 @@ const offensivePreferenceSchema = z.object({
|
|
|
57
56
|
weights: z.record(z.string(), z.number().int().min(0)),
|
|
58
57
|
tempos: z.record(z.string(), tempoPreferenceSchema).optional()
|
|
59
58
|
});
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
// An opposing attacking function a front-row starter can commit to (every RoleEnum except the libero, which never
|
|
60
|
+
// attacks). The commit is the BLIND, pre-set Defence-base choice (owner 2026-08-10).
|
|
61
|
+
const commitFunctionSchema = z.nativeEnum(RoleEnum).refine((v) => v !== RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
|
|
62
|
+
// One rotation's blocking config (owner 2026-08-10). `commit` maps my front-row starter ids to the opposing function
|
|
63
|
+
// each commits to (blind, rotation-wide; absent = reads); `noBlock` lists, per point-of-attack defence, the front-row
|
|
64
|
+
// starters that sit out of the block for an attack from that side (overrides the commit for that side). Each side
|
|
65
|
+
// defaults to [] so a partial config stays valid. Cross-field checks (keys/ids are starters) live in the superRefine.
|
|
66
|
+
const commitSchema = z.record(z.string(), commitFunctionSchema);
|
|
67
|
+
const noBlockSchema = z.object({
|
|
68
|
+
DEF_L: z.array(z.string()).default([]),
|
|
69
|
+
DEF_M: z.array(z.string()).default([]),
|
|
70
|
+
DEF_R: z.array(z.string()).default([])
|
|
71
|
+
});
|
|
71
72
|
const blockingPreferenceSchema = z.object({
|
|
72
73
|
rotation: z.number().int().min(1).max(6),
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
DEF_M: blockingTargetsSchema.default({}),
|
|
76
|
-
DEF_R: blockingTargetsSchema.default({})
|
|
77
|
-
})
|
|
74
|
+
commit: commitSchema.default({}),
|
|
75
|
+
noBlock: noBlockSchema.default({ DEF_L: [], DEF_M: [], DEF_R: [] })
|
|
78
76
|
});
|
|
79
77
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
80
78
|
const systemSetSchema = z.object({
|
|
@@ -211,8 +209,9 @@ export const TacticsInputSchema = z.object({
|
|
|
211
209
|
}
|
|
212
210
|
}
|
|
213
211
|
};
|
|
214
|
-
// Blocking preferences: unique rotations;
|
|
215
|
-
// players, the schema checks starters). The
|
|
212
|
+
// Blocking preferences: unique rotations; every committer id (base commit) and every no-block id (per side) keys
|
|
213
|
+
// an on-court starter (the UI only offers front-row players, the schema checks starters). The commit function is
|
|
214
|
+
// validated by commitFunctionSchema.
|
|
216
215
|
const validateBlockingPreferences = (preferences, path) => {
|
|
217
216
|
const seenRotations = new Set();
|
|
218
217
|
for (const pref of preferences) {
|
|
@@ -220,8 +219,13 @@ export const TacticsInputSchema = z.object({
|
|
|
220
219
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
221
220
|
}
|
|
222
221
|
seenRotations.add(pref.rotation);
|
|
222
|
+
for (const id of Object.keys(pref.commit)) {
|
|
223
|
+
if (!starterIds.has(id)) {
|
|
224
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
223
227
|
for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
|
|
224
|
-
for (const id of
|
|
228
|
+
for (const id of pref.noBlock[form] ?? []) {
|
|
225
229
|
if (!starterIds.has(id)) {
|
|
226
230
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
227
231
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { TacticsInputSchema } from './tactics.z';
|
|
3
|
-
import { NO_BLOCK } from '../blocking-preference';
|
|
4
3
|
import { RoleEnum } from '../../player';
|
|
5
4
|
import { makeCountry, makePlayer } from '../../test-helpers';
|
|
6
5
|
describe('TacticsInputSchema — second libero rules', () => {
|
|
@@ -234,40 +233,49 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
234
233
|
...overrides
|
|
235
234
|
};
|
|
236
235
|
}
|
|
237
|
-
it('accepts a blocking preference that
|
|
236
|
+
it('accepts a blocking preference that commits starters to attacking functions', () => {
|
|
238
237
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
239
|
-
blockingPreferences: [{ rotation: 1,
|
|
238
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [], DEF_M: [], DEF_R: [] } }]
|
|
240
239
|
}));
|
|
241
240
|
expect(res.success).toBe(true);
|
|
242
241
|
if (res.success) {
|
|
243
|
-
expect(res.data.blockingPreferences[0].
|
|
244
|
-
expect(res.data.blockingPreferences[0].
|
|
245
|
-
expect(res.data.blockingPreferences[0].targets.DEF_M).toEqual({}); // the middle side is independent (empty here)
|
|
242
|
+
expect(res.data.blockingPreferences[0].commit[p4.id]).toBe(RoleEnum.OUTSIDE_HITTER);
|
|
243
|
+
expect(res.data.blockingPreferences[0].commit[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
246
244
|
}
|
|
247
245
|
});
|
|
248
|
-
it('defaults
|
|
246
|
+
it('defaults an omitted commit to empty and omitted no-block sides to empty arrays', () => {
|
|
249
247
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
250
|
-
blockingPreferences: [{ rotation: 1,
|
|
248
|
+
blockingPreferences: [{ rotation: 1, noBlock: { DEF_L: [p4.id] } }]
|
|
251
249
|
}));
|
|
252
250
|
expect(res.success).toBe(true);
|
|
253
251
|
if (res.success) {
|
|
254
|
-
expect(res.data.blockingPreferences[0].
|
|
255
|
-
expect(res.data.blockingPreferences[0].
|
|
256
|
-
expect(res.data.blockingPreferences[0].
|
|
252
|
+
expect(res.data.blockingPreferences[0].commit).toEqual({});
|
|
253
|
+
expect(res.data.blockingPreferences[0].noBlock.DEF_L).toEqual([p4.id]);
|
|
254
|
+
expect(res.data.blockingPreferences[0].noBlock.DEF_M).toEqual([]);
|
|
255
|
+
expect(res.data.blockingPreferences[0].noBlock.DEF_R).toEqual([]);
|
|
257
256
|
}
|
|
258
257
|
});
|
|
259
|
-
it('rejects a
|
|
258
|
+
it('rejects a commit id that is not a starter', () => {
|
|
260
259
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
261
|
-
blockingPreferences: [{ rotation: 1,
|
|
260
|
+
blockingPreferences: [{ rotation: 1, commit: { [reserve.id]: RoleEnum.OUTSIDE_HITTER } }]
|
|
262
261
|
}));
|
|
263
262
|
expect(res.success).toBe(false);
|
|
264
263
|
if (!res.success) {
|
|
265
264
|
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
266
265
|
}
|
|
267
266
|
});
|
|
268
|
-
it('rejects a
|
|
267
|
+
it('rejects a no-block id (on any side) that is not a starter', () => {
|
|
269
268
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
270
|
-
blockingPreferences: [{ rotation: 1,
|
|
269
|
+
blockingPreferences: [{ rotation: 1, noBlock: { DEF_M: [reserve.id] } }]
|
|
270
|
+
}));
|
|
271
|
+
expect(res.success).toBe(false);
|
|
272
|
+
if (!res.success) {
|
|
273
|
+
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
it('rejects a commit function of libero', () => {
|
|
277
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
278
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: RoleEnum.LIBERO } }]
|
|
271
279
|
}));
|
|
272
280
|
expect(res.success).toBe(false);
|
|
273
281
|
if (!res.success) {
|
|
@@ -277,8 +285,8 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
277
285
|
it('rejects a duplicate rotation in blocking preferences', () => {
|
|
278
286
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
279
287
|
blockingPreferences: [
|
|
280
|
-
{ rotation: 1,
|
|
281
|
-
{ rotation: 1,
|
|
288
|
+
{ rotation: 1, commit: { [p4.id]: RoleEnum.OUTSIDE_HITTER } },
|
|
289
|
+
{ rotation: 1, commit: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
282
290
|
]
|
|
283
291
|
}));
|
|
284
292
|
expect(res.success).toBe(false);
|
|
@@ -288,38 +296,29 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
288
296
|
});
|
|
289
297
|
it('accepts per-set blocking preference overrides', () => {
|
|
290
298
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
291
|
-
blockingPreferenceSets: [[{ rotation: 1,
|
|
299
|
+
blockingPreferenceSets: [[{ rotation: 1, commit: { [p4.id]: RoleEnum.OPPOSITE_HITTER } }], []]
|
|
292
300
|
}));
|
|
293
301
|
expect(res.success).toBe(true);
|
|
294
302
|
});
|
|
295
|
-
it('accepts
|
|
303
|
+
it('accepts a starter committed on the base AND no-blocked on a side (no-block overrides commit there)', () => {
|
|
296
304
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
297
|
-
blockingPreferences: [{ rotation: 1,
|
|
305
|
+
blockingPreferences: [{ rotation: 1, commit: { [p3.id]: RoleEnum.MIDDLE_BLOCKER }, noBlock: { DEF_L: [p3.id] } }]
|
|
298
306
|
}));
|
|
299
307
|
expect(res.success).toBe(true);
|
|
300
308
|
if (res.success) {
|
|
301
|
-
expect(res.data.blockingPreferences[0].
|
|
302
|
-
expect(res.data.blockingPreferences[0].
|
|
309
|
+
expect(res.data.blockingPreferences[0].commit[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
310
|
+
expect(res.data.blockingPreferences[0].noBlock.DEF_L).toEqual([p3.id]);
|
|
303
311
|
}
|
|
304
312
|
});
|
|
305
|
-
it('accepts
|
|
313
|
+
it('accepts a no-block in a per-set override', () => {
|
|
306
314
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
307
|
-
blockingPreferenceSets: [[{ rotation: 1,
|
|
315
|
+
blockingPreferenceSets: [[{ rotation: 1, noBlock: { DEF_M: [p4.id] } }], []]
|
|
308
316
|
}));
|
|
309
317
|
expect(res.success).toBe(true);
|
|
310
318
|
});
|
|
311
|
-
it('
|
|
312
|
-
const res = TacticsInputSchema.safeParse(baseInput({
|
|
313
|
-
blockingPreferences: [{ rotation: 1, targets: { DEF_R: { [reserve.id]: NO_BLOCK } } }]
|
|
314
|
-
}));
|
|
315
|
-
expect(res.success).toBe(false);
|
|
316
|
-
if (!res.success) {
|
|
317
|
-
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
it('rejects an unknown assignment string', () => {
|
|
319
|
+
it('rejects an unknown commit function string', () => {
|
|
321
320
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
322
|
-
blockingPreferences: [{ rotation: 1,
|
|
321
|
+
blockingPreferences: [{ rotation: 1, commit: { [p4.id]: 'NOT_A_ROLE' } }]
|
|
323
322
|
}));
|
|
324
323
|
expect(res.success).toBe(false);
|
|
325
324
|
});
|
|
@@ -290,19 +290,21 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
290
290
|
}, z.core.$strip>>>>;
|
|
291
291
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
292
292
|
rotation: z.ZodNumber;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>>;
|
|
294
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
295
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
296
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
297
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
298
|
+
}, z.core.$strip>>;
|
|
298
299
|
}, z.core.$strip>>>;
|
|
299
300
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
300
301
|
rotation: z.ZodNumber;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
commit: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>>;
|
|
303
|
+
noBlock: z.ZodDefault<z.ZodObject<{
|
|
304
|
+
DEF_L: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
305
|
+
DEF_M: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
306
|
+
DEF_R: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
307
|
+
}, z.core.$strip>>;
|
|
306
308
|
}, z.core.$strip>>>>;
|
|
307
309
|
basePositions: z.ZodOptional<z.ZodObject<{
|
|
308
310
|
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|