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.
@@ -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, RoleEnum, RotationSystemEnum, SubBackMode, SubBand } from '../service';
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, RoleEnum>;
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, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
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, RoleEnum>;
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, LineupFunction>;
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
- // The opposing attacking functions a dedicated blocker can commit to: every RoleEnum except the libero.
63
- const blockingFunctionSchema = zod_1.z.nativeEnum(player_1.RoleEnum).refine((v) => v !== player_1.RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
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, RoleEnum, RotationSystemEnum, SubBackMode, SubBand } from '../service';
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, RoleEnum>;
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, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
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, RoleEnum>;
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, LineupFunction>;
6
+ readonly targets: Record<string, BlockingAssignment>;
5
7
  }
@@ -1 +1,6 @@
1
- export {};
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
- // The opposing attacking functions a dedicated blocker can commit to: every RoleEnum except the libero.
60
- const blockingFunctionSchema = z.nativeEnum(RoleEnum).refine((v) => v !== RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
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<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.489",
3
+ "version": "0.0.491",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",