volleyballsimtypes 0.0.489 → 0.0.491
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 +2 -2
- package/dist/cjs/src/data/models/tactics.d.ts +2 -2
- package/dist/cjs/src/data/transformers/tactics.test.js +19 -0
- package/dist/cjs/src/service/team/blocking-preference.d.ts +3 -1
- package/dist/cjs/src/service/team/blocking-preference.js +7 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +2 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.js +7 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +32 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +2 -2
- package/dist/esm/src/api/index.d.ts +2 -2
- package/dist/esm/src/data/models/tactics.d.ts +2 -2
- package/dist/esm/src/data/transformers/tactics.test.js +20 -1
- package/dist/esm/src/service/team/blocking-preference.d.ts +3 -1
- package/dist/esm/src/service/team/blocking-preference.js +6 -1
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +2 -2
- package/dist/esm/src/service/team/schemas/tactics.z.js +7 -2
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +32 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { 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, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition,
|
|
1
|
+
import { 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, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export type Rally = DataProps<_Rally> & {
|
|
3
3
|
homePlayerPosition: PlayerPosition[];
|
|
4
4
|
awayPlayerPosition: PlayerPosition[];
|
|
@@ -101,7 +101,7 @@ export interface ApiOffensivePreference {
|
|
|
101
101
|
}
|
|
102
102
|
export interface ApiBlockingPreference {
|
|
103
103
|
rotation: number;
|
|
104
|
-
targets: Record<string,
|
|
104
|
+
targets: Record<string, BlockingAssignment>;
|
|
105
105
|
}
|
|
106
106
|
export interface ApiSystemSet {
|
|
107
107
|
rotationSystem: RotationSystemEnum;
|
|
@@ -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 { ConditionLogic, CourtPosition, EnergyBand, PinchCondition,
|
|
4
|
+
import { BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, 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,7 @@ export interface OffensivePreferenceAttributes {
|
|
|
36
36
|
}
|
|
37
37
|
export interface BlockingPreferenceAttributes {
|
|
38
38
|
rotation: number;
|
|
39
|
-
targets: Record<PlayerId,
|
|
39
|
+
targets: Record<PlayerId, BlockingAssignment>;
|
|
40
40
|
}
|
|
41
41
|
export interface SystemSetAttributes {
|
|
42
42
|
rotationSystem: RotationSystemEnum;
|
|
@@ -216,6 +216,25 @@ function makeTactics(designatedSubs) {
|
|
|
216
216
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
217
217
|
(0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
|
|
218
218
|
});
|
|
219
|
+
// NO_BLOCK rides in the same targets map as a commit assignment, so the transformers copy it wholesale with no
|
|
220
|
+
// mapping code. This pins that: the sentinel must survive the row round-trip verbatim, mixed with a commit, and
|
|
221
|
+
// in the per-set overrides. If it ever gets translated or dropped, a player silently starts blocking again.
|
|
222
|
+
(0, globals_1.it)('round-trips a NO_BLOCK assignment, mixed with a commit and in per-set overrides', () => {
|
|
223
|
+
const withNoBlock = service_1.Tactics.create({
|
|
224
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
225
|
+
liberoReplacements: [],
|
|
226
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }],
|
|
227
|
+
blockingPreferenceSets: [[{ rotation: 2, targets: { [p2.id]: service_1.NO_BLOCK } }]]
|
|
228
|
+
});
|
|
229
|
+
const attrs = (0, tactics_1.transformFromTactics)(withNoBlock, 'team-1');
|
|
230
|
+
(0, globals_1.expect)(attrs.blocking_preferences).toEqual([
|
|
231
|
+
{ rotation: 1, targets: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }
|
|
232
|
+
]);
|
|
233
|
+
(0, globals_1.expect)(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { [p2.id]: service_1.NO_BLOCK } }]]);
|
|
234
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
235
|
+
(0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
|
|
236
|
+
(0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].targets).toEqual({ [p2.id]: service_1.NO_BLOCK });
|
|
237
|
+
});
|
|
219
238
|
});
|
|
220
239
|
(0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
221
240
|
const country = (0, test_helpers_1.makeCountry)();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { LineupFunction } from './lineup-function';
|
|
2
|
+
export declare const NO_BLOCK: "NO_BLOCK";
|
|
3
|
+
export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
|
|
2
4
|
export interface BlockingPreference {
|
|
3
5
|
readonly rotation: number;
|
|
4
|
-
readonly targets: Record<string,
|
|
6
|
+
readonly targets: Record<string, BlockingAssignment>;
|
|
5
7
|
}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_BLOCK = void 0;
|
|
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';
|
|
@@ -281,11 +281,11 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
281
281
|
}, z.core.$strip>>>>;
|
|
282
282
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
283
283
|
rotation: z.ZodNumber;
|
|
284
|
-
targets: 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
|
|
284
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
285
285
|
}, z.core.$strip>>>;
|
|
286
286
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
287
287
|
rotation: z.ZodNumber;
|
|
288
|
-
targets: 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
|
|
288
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
289
289
|
}, z.core.$strip>>>>;
|
|
290
290
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
291
291
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -3,6 +3,7 @@ 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");
|
|
6
7
|
const energy_band_1 = require("../energy-band");
|
|
7
8
|
const rotation_system_1 = require("../rotation-system");
|
|
8
9
|
const designated_sub_z_1 = require("./designated-sub.z");
|
|
@@ -59,8 +60,12 @@ const offensivePreferenceSchema = zod_1.z.object({
|
|
|
59
60
|
weights: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(0)),
|
|
60
61
|
tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
|
|
61
62
|
});
|
|
62
|
-
//
|
|
63
|
-
|
|
63
|
+
// What a front-row starter can be assigned: an opposing attacking function to commit to (every RoleEnum except
|
|
64
|
+
// the libero, which never attacks), or NO_BLOCK to sit the rotation out of the block entirely.
|
|
65
|
+
const blockingFunctionSchema = zod_1.z.union([
|
|
66
|
+
zod_1.z.nativeEnum(player_1.RoleEnum).refine((v) => v !== player_1.RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' }),
|
|
67
|
+
zod_1.z.literal(blocking_preference_1.NO_BLOCK)
|
|
68
|
+
]);
|
|
64
69
|
// One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
|
|
65
70
|
// each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
|
|
66
71
|
const blockingPreferenceSchema = zod_1.z.object({
|
|
@@ -2,6 +2,7 @@
|
|
|
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");
|
|
5
6
|
const player_1 = require("../../player");
|
|
6
7
|
const test_helpers_1 = require("../../test-helpers");
|
|
7
8
|
(0, globals_1.describe)('TacticsInputSchema — second libero rules', () => {
|
|
@@ -280,4 +281,35 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
280
281
|
}));
|
|
281
282
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
282
283
|
});
|
|
284
|
+
(0, globals_1.it)('accepts NO_BLOCK as an assignment, alongside a commit on another starter', () => {
|
|
285
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
286
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: blocking_preference_1.NO_BLOCK, [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER } }]
|
|
287
|
+
}));
|
|
288
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
289
|
+
if (res.success) {
|
|
290
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].targets[p4.id]).toBe(blocking_preference_1.NO_BLOCK);
|
|
291
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].targets[p3.id]).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
(0, globals_1.it)('accepts NO_BLOCK in a per-set override', () => {
|
|
295
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
296
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: blocking_preference_1.NO_BLOCK } }], []]
|
|
297
|
+
}));
|
|
298
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
(0, globals_1.it)('still rejects a NO_BLOCK assignment on a player who is not a starter', () => {
|
|
301
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
302
|
+
blockingPreferences: [{ rotation: 1, targets: { [reserve.id]: blocking_preference_1.NO_BLOCK } }]
|
|
303
|
+
}));
|
|
304
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
305
|
+
if (!res.success) {
|
|
306
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
(0, globals_1.it)('rejects an unknown assignment string', () => {
|
|
310
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
311
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: 'NO_BLOCKING' } }]
|
|
312
|
+
}));
|
|
313
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
314
|
+
});
|
|
283
315
|
});
|
|
@@ -289,11 +289,11 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
289
289
|
}, z.core.$strip>>>>;
|
|
290
290
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
291
291
|
rotation: z.ZodNumber;
|
|
292
|
-
targets: 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
|
|
292
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
293
293
|
}, z.core.$strip>>>;
|
|
294
294
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
295
295
|
rotation: z.ZodNumber;
|
|
296
|
-
targets: 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
|
|
296
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
297
297
|
}, z.core.$strip>>>>;
|
|
298
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
299
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { 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, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition,
|
|
1
|
+
import { 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, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export type Rally = DataProps<_Rally> & {
|
|
3
3
|
homePlayerPosition: PlayerPosition[];
|
|
4
4
|
awayPlayerPosition: PlayerPosition[];
|
|
@@ -101,7 +101,7 @@ export interface ApiOffensivePreference {
|
|
|
101
101
|
}
|
|
102
102
|
export interface ApiBlockingPreference {
|
|
103
103
|
rotation: number;
|
|
104
|
-
targets: Record<string,
|
|
104
|
+
targets: Record<string, BlockingAssignment>;
|
|
105
105
|
}
|
|
106
106
|
export interface ApiSystemSet {
|
|
107
107
|
rotationSystem: RotationSystemEnum;
|
|
@@ -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 { ConditionLogic, CourtPosition, EnergyBand, PinchCondition,
|
|
4
|
+
import { BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, 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,7 @@ export interface OffensivePreferenceAttributes {
|
|
|
36
36
|
}
|
|
37
37
|
export interface BlockingPreferenceAttributes {
|
|
38
38
|
rotation: number;
|
|
39
|
-
targets: Record<PlayerId,
|
|
39
|
+
targets: Record<PlayerId, BlockingAssignment>;
|
|
40
40
|
}
|
|
41
41
|
export interface SystemSetAttributes {
|
|
42
42
|
rotationSystem: RotationSystemEnum;
|
|
@@ -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, RoleEnum, RotationSystemEnum, Tactics } from '../../service';
|
|
3
|
+
import { CourtPosition, EnergyBand, NO_BLOCK, 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) {
|
|
@@ -214,6 +214,25 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
214
214
|
const back = transformToTactics(attrs, roster);
|
|
215
215
|
expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
216
216
|
});
|
|
217
|
+
// NO_BLOCK rides in the same targets map as a commit assignment, so the transformers copy it wholesale with no
|
|
218
|
+
// mapping code. This pins that: the sentinel must survive the row round-trip verbatim, mixed with a commit, and
|
|
219
|
+
// in the per-set overrides. If it ever gets translated or dropped, a player silently starts blocking again.
|
|
220
|
+
it('round-trips a NO_BLOCK assignment, mixed with a commit and in per-set overrides', () => {
|
|
221
|
+
const withNoBlock = Tactics.create({
|
|
222
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
223
|
+
liberoReplacements: [],
|
|
224
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }],
|
|
225
|
+
blockingPreferenceSets: [[{ rotation: 2, targets: { [p2.id]: NO_BLOCK } }]]
|
|
226
|
+
});
|
|
227
|
+
const attrs = transformFromTactics(withNoBlock, 'team-1');
|
|
228
|
+
expect(attrs.blocking_preferences).toEqual([
|
|
229
|
+
{ rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
230
|
+
]);
|
|
231
|
+
expect(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { [p2.id]: NO_BLOCK } }]]);
|
|
232
|
+
const back = transformToTactics(attrs, roster);
|
|
233
|
+
expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
234
|
+
expect(back.blockingPreferenceSets?.[0][0].targets).toEqual({ [p2.id]: NO_BLOCK });
|
|
235
|
+
});
|
|
217
236
|
});
|
|
218
237
|
describe('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
219
238
|
const country = makeCountry();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { LineupFunction } from './lineup-function';
|
|
2
|
+
export declare const NO_BLOCK: "NO_BLOCK";
|
|
3
|
+
export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
|
|
2
4
|
export interface BlockingPreference {
|
|
3
5
|
readonly rotation: number;
|
|
4
|
-
readonly targets: Record<string,
|
|
6
|
+
readonly targets: Record<string, BlockingAssignment>;
|
|
5
7
|
}
|
|
@@ -1 +1,6 @@
|
|
|
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';
|
|
@@ -281,11 +281,11 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
281
281
|
}, z.core.$strip>>>>;
|
|
282
282
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
283
283
|
rotation: z.ZodNumber;
|
|
284
|
-
targets: 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
|
|
284
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
285
285
|
}, z.core.$strip>>>;
|
|
286
286
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
287
287
|
rotation: z.ZodNumber;
|
|
288
|
-
targets: 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
|
|
288
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
289
289
|
}, z.core.$strip>>>>;
|
|
290
290
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
291
291
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { Player, RoleEnum } from '../../player';
|
|
3
|
+
import { NO_BLOCK } from '../blocking-preference';
|
|
3
4
|
import { EnergyBand } from '../energy-band';
|
|
4
5
|
import { RotationSystemEnum, setterCountFor } from '../rotation-system';
|
|
5
6
|
import { DesignatedSubSchema, SubBandSchema } from './designated-sub.z';
|
|
@@ -56,8 +57,12 @@ const offensivePreferenceSchema = z.object({
|
|
|
56
57
|
weights: z.record(z.string(), z.number().int().min(0)),
|
|
57
58
|
tempos: z.record(z.string(), tempoPreferenceSchema).optional()
|
|
58
59
|
});
|
|
59
|
-
//
|
|
60
|
-
|
|
60
|
+
// What a front-row starter can be assigned: an opposing attacking function to commit to (every RoleEnum except
|
|
61
|
+
// the libero, which never attacks), or NO_BLOCK to sit the rotation out of the block entirely.
|
|
62
|
+
const blockingFunctionSchema = z.union([
|
|
63
|
+
z.nativeEnum(RoleEnum).refine((v) => v !== RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' }),
|
|
64
|
+
z.literal(NO_BLOCK)
|
|
65
|
+
]);
|
|
61
66
|
// One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
|
|
62
67
|
// each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
|
|
63
68
|
const blockingPreferenceSchema = z.object({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { TacticsInputSchema } from './tactics.z';
|
|
3
|
+
import { NO_BLOCK } from '../blocking-preference';
|
|
3
4
|
import { RoleEnum } from '../../player';
|
|
4
5
|
import { makeCountry, makePlayer } from '../../test-helpers';
|
|
5
6
|
describe('TacticsInputSchema — second libero rules', () => {
|
|
@@ -278,4 +279,35 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
278
279
|
}));
|
|
279
280
|
expect(res.success).toBe(true);
|
|
280
281
|
});
|
|
282
|
+
it('accepts NO_BLOCK as an assignment, alongside a commit on another starter', () => {
|
|
283
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
284
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
285
|
+
}));
|
|
286
|
+
expect(res.success).toBe(true);
|
|
287
|
+
if (res.success) {
|
|
288
|
+
expect(res.data.blockingPreferences[0].targets[p4.id]).toBe(NO_BLOCK);
|
|
289
|
+
expect(res.data.blockingPreferences[0].targets[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
it('accepts NO_BLOCK in a per-set override', () => {
|
|
293
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
294
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: NO_BLOCK } }], []]
|
|
295
|
+
}));
|
|
296
|
+
expect(res.success).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
it('still rejects a NO_BLOCK assignment on a player who is not a starter', () => {
|
|
299
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
300
|
+
blockingPreferences: [{ rotation: 1, targets: { [reserve.id]: NO_BLOCK } }]
|
|
301
|
+
}));
|
|
302
|
+
expect(res.success).toBe(false);
|
|
303
|
+
if (!res.success) {
|
|
304
|
+
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
it('rejects an unknown assignment string', () => {
|
|
308
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
309
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: 'NO_BLOCKING' } }]
|
|
310
|
+
}));
|
|
311
|
+
expect(res.success).toBe(false);
|
|
312
|
+
});
|
|
281
313
|
});
|
|
@@ -289,11 +289,11 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
289
289
|
}, z.core.$strip>>>>;
|
|
290
290
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
291
291
|
rotation: z.ZodNumber;
|
|
292
|
-
targets: 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
|
|
292
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
293
293
|
}, z.core.$strip>>>;
|
|
294
294
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
295
295
|
rotation: z.ZodNumber;
|
|
296
|
-
targets: 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
|
|
296
|
+
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [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>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
297
297
|
}, z.core.$strip>>>>;
|
|
298
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
299
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|