volleyballsimtypes 0.0.481 → 0.0.483

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.
Files changed (73) hide show
  1. package/dist/cjs/src/api/index.d.ts +10 -18
  2. package/dist/cjs/src/data/models/tactics.d.ts +17 -19
  3. package/dist/cjs/src/data/models/tactics.js +9 -0
  4. package/dist/cjs/src/data/transformers/match-preset-override.test.js +10 -1
  5. package/dist/cjs/src/data/transformers/rally-event.d.ts +2 -1
  6. package/dist/cjs/src/data/transformers/rally-event.js +12 -0
  7. package/dist/cjs/src/data/transformers/rally.js +2 -0
  8. package/dist/cjs/src/data/transformers/rally.test.d.ts +1 -0
  9. package/dist/cjs/src/data/transformers/rally.test.js +62 -0
  10. package/dist/cjs/src/data/transformers/tactics.js +38 -58
  11. package/dist/cjs/src/data/transformers/tactics.test.js +39 -20
  12. package/dist/cjs/src/service/team/blocking-preference.d.ts +5 -0
  13. package/dist/cjs/src/service/team/blocking-preference.js +2 -0
  14. package/dist/cjs/src/service/team/designated-sub.d.ts +2 -4
  15. package/dist/cjs/src/service/team/index.d.ts +2 -0
  16. package/dist/cjs/src/service/team/index.js +2 -0
  17. package/dist/cjs/src/service/team/libero-sub.d.ts +0 -4
  18. package/dist/cjs/src/service/team/offensive-preference.d.ts +1 -2
  19. package/dist/cjs/src/service/team/pinch-condition.d.ts +11 -3
  20. package/dist/cjs/src/service/team/pinch-condition.js +9 -4
  21. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  22. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +27 -23
  23. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +4 -2
  24. package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  25. package/dist/cjs/src/service/team/schemas/libero-sub.z.js +10 -26
  26. package/dist/cjs/src/service/team/schemas/libero-sub.z.test.js +74 -35
  27. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +39 -39
  28. package/dist/cjs/src/service/team/schemas/tactics.z.js +47 -13
  29. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +81 -8
  30. package/dist/cjs/src/service/team/schemas/team.z.d.ts +38 -38
  31. package/dist/cjs/src/service/team/sub-config-convert.d.ts +13 -0
  32. package/dist/cjs/src/service/team/sub-config-convert.js +91 -0
  33. package/dist/cjs/src/service/team/sub-config-convert.test.d.ts +1 -0
  34. package/dist/cjs/src/service/team/sub-config-convert.test.js +84 -0
  35. package/dist/cjs/src/service/team/tactics.d.ts +5 -0
  36. package/dist/cjs/src/service/team/tactics.js +3 -1
  37. package/dist/esm/src/api/index.d.ts +10 -18
  38. package/dist/esm/src/data/models/tactics.d.ts +17 -19
  39. package/dist/esm/src/data/models/tactics.js +9 -0
  40. package/dist/esm/src/data/transformers/match-preset-override.test.js +10 -1
  41. package/dist/esm/src/data/transformers/rally-event.d.ts +2 -1
  42. package/dist/esm/src/data/transformers/rally-event.js +12 -1
  43. package/dist/esm/src/data/transformers/rally.js +4 -2
  44. package/dist/esm/src/data/transformers/rally.test.d.ts +1 -0
  45. package/dist/esm/src/data/transformers/rally.test.js +60 -0
  46. package/dist/esm/src/data/transformers/tactics.js +39 -59
  47. package/dist/esm/src/data/transformers/tactics.test.js +40 -21
  48. package/dist/esm/src/service/team/blocking-preference.d.ts +5 -0
  49. package/dist/esm/src/service/team/blocking-preference.js +1 -0
  50. package/dist/esm/src/service/team/designated-sub.d.ts +2 -4
  51. package/dist/esm/src/service/team/index.d.ts +2 -0
  52. package/dist/esm/src/service/team/index.js +2 -0
  53. package/dist/esm/src/service/team/libero-sub.d.ts +0 -4
  54. package/dist/esm/src/service/team/offensive-preference.d.ts +1 -2
  55. package/dist/esm/src/service/team/pinch-condition.d.ts +11 -3
  56. package/dist/esm/src/service/team/pinch-condition.js +9 -4
  57. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  58. package/dist/esm/src/service/team/schemas/designated-sub.z.js +26 -22
  59. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +4 -2
  60. package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  61. package/dist/esm/src/service/team/schemas/libero-sub.z.js +9 -25
  62. package/dist/esm/src/service/team/schemas/libero-sub.z.test.js +75 -36
  63. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +39 -39
  64. package/dist/esm/src/service/team/schemas/tactics.z.js +48 -14
  65. package/dist/esm/src/service/team/schemas/tactics.z.test.js +81 -8
  66. package/dist/esm/src/service/team/schemas/team.z.d.ts +38 -38
  67. package/dist/esm/src/service/team/sub-config-convert.d.ts +13 -0
  68. package/dist/esm/src/service/team/sub-config-convert.js +85 -0
  69. package/dist/esm/src/service/team/sub-config-convert.test.d.ts +1 -0
  70. package/dist/esm/src/service/team/sub-config-convert.test.js +82 -0
  71. package/dist/esm/src/service/team/tactics.d.ts +5 -0
  72. package/dist/esm/src/service/team/tactics.js +3 -1
  73. package/package.json +1 -1
@@ -1,37 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LiberoSubConfigSchema = exports.LiberoSetSubConfigSchema = exports.LiberoSubModeSchema = void 0;
3
+ exports.LiberoSubConfigSchema = exports.LiberoSetSubConfigSchema = void 0;
4
4
  const zod_1 = require("zod");
5
- const energy_band_1 = require("../energy-band");
5
+ const sub_config_convert_1 = require("../sub-config-convert");
6
6
  const designated_sub_z_1 = require("./designated-sub.z");
7
- exports.LiberoSubModeSchema = zod_1.z.enum(['NEVER', 'FATIGUE', 'ALWAYS', 'CONDITIONS']);
8
- // Fields shared by the all-sets config and each per-set config. The swapped-in player is always the dedicated
9
- // `secondLibero`, so there is no bench to validate; the CONDITIONS mode reuses the pinch-server condition
10
- // catalog (PinchConditionSchema, which includes the libero-only PHASE condition) + ALL/ANY logic.
7
+ // Pure condition engine (owner 2026-07-11): the second libero is active while `conditions` hold (combined
8
+ // per `conditionLogic`, default ALL; the STARTING libero is the FATIGUE-condition subject). No conditions =
9
+ // the starting libero always plays; [{ASAP}] = always the second libero (the old ALWAYS). The swapped-in
10
+ // player is always the dedicated `secondLibero`, so there is no bench to validate. Legacy mode-shaped
11
+ // configs (NEVER/FATIGUE/ALWAYS/CONDITIONS) are auto-converted by the preprocess.
11
12
  const liberoSetSubConfigShape = {
12
- mode: exports.LiberoSubModeSchema,
13
- // fatigue mode only (absent => the team default `substitutionBand`):
14
- fatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
15
- // conditions mode only:
16
13
  conditions: zod_1.z.array(designated_sub_z_1.PinchConditionSchema).optional(),
17
14
  conditionLogic: designated_sub_z_1.ConditionLogicSchema.optional()
18
15
  };
19
- // CONDITIONS mode needs at least one condition (mirrors the designated-sub pinch refine).
20
- function refineConditions(data, ctx) {
21
- if (data.mode !== 'CONDITIONS')
22
- return;
23
- if (data.conditions == null || data.conditions.length === 0) {
24
- ctx.addIssue({ code: 'custom', message: 'LIBERO_CONDITIONS_REQUIRE_A_CONDITION', path: ['conditions'] });
25
- }
26
- }
27
- exports.LiberoSetSubConfigSchema = zod_1.z.object(liberoSetSubConfigShape).superRefine(refineConditions);
28
- exports.LiberoSubConfigSchema = zod_1.z.object({
16
+ exports.LiberoSetSubConfigSchema = zod_1.z.preprocess(sub_config_convert_1.convertLegacyLiberoSetSubConfig, zod_1.z.object(liberoSetSubConfigShape));
17
+ exports.LiberoSubConfigSchema = zod_1.z.preprocess(sub_config_convert_1.convertLegacyLiberoSubConfig, zod_1.z.object({
29
18
  ...liberoSetSubConfigShape,
30
19
  // Optional per-set overrides (index 0 = set 1 ... index 4 = set 5); absent => the flat config for all sets.
31
20
  sets: zod_1.z.array(exports.LiberoSetSubConfigSchema).max(5).optional()
32
- }).superRefine((data, ctx) => {
33
- // The flat config is the fallback for any set a partial `sets` array doesn't cover, so validate it whenever
34
- // `sets` is absent or covers fewer than 5 sets (each present per-set config is validated by its own schema).
35
- if (data.sets == null || data.sets.length < 5)
36
- refineConditions(data, ctx);
37
- });
21
+ }));
@@ -4,85 +4,124 @@ const globals_1 = require("@jest/globals");
4
4
  const libero_sub_z_1 = require("./libero-sub.z");
5
5
  const energy_band_1 = require("../energy-band");
6
6
  const pinch_condition_1 = require("../pinch-condition");
7
- (0, globals_1.describe)('LiberoSubModeSchema', () => {
8
- (0, globals_1.it)('accepts NEVER, FATIGUE, ALWAYS and CONDITIONS', () => {
9
- for (const mode of ['NEVER', 'FATIGUE', 'ALWAYS', 'CONDITIONS']) {
10
- (0, globals_1.expect)(libero_sub_z_1.LiberoSubModeSchema.safeParse(mode).success).toBe(true);
11
- }
12
- });
13
- (0, globals_1.it)('rejects an unknown mode', () => {
14
- (0, globals_1.expect)(libero_sub_z_1.LiberoSubModeSchema.safeParse('SOMETIMES').success).toBe(false);
7
+ // Unified condition engine (owner 2026-07-11): the libero rule is a pure condition list; legacy mode-shaped
8
+ // configs (NEVER/FATIGUE/ALWAYS/CONDITIONS) auto-convert in the schema preprocess.
9
+ (0, globals_1.describe)('LiberoSetSubConfigSchema (unified)', () => {
10
+ (0, globals_1.it)('accepts an empty config (no conditions = the starting libero always plays)', () => {
11
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({});
12
+ (0, globals_1.expect)(res.success).toBe(true);
13
+ if (res.success)
14
+ (0, globals_1.expect)(res.data.conditions ?? []).toHaveLength(0);
15
15
  });
16
- });
17
- (0, globals_1.describe)('LiberoSetSubConfigSchema', () => {
18
- (0, globals_1.it)('accepts a mode with no band', () => {
19
- (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'ALWAYS' }).success).toBe(true);
16
+ (0, globals_1.it)('accepts a FATIGUE condition with a band', () => {
17
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({
18
+ conditions: [{ type: pinch_condition_1.PinchConditionType.FATIGUE, band: energy_band_1.EnergyBand.TIRED }]
19
+ });
20
+ (0, globals_1.expect)(res.success).toBe(true);
20
21
  });
21
- (0, globals_1.it)('accepts a fatigue mode with a band', () => {
22
- (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'FATIGUE', fatigueBand: energy_band_1.EnergyBand.TIRED }).success).toBe(true);
22
+ (0, globals_1.it)('accepts a FATIGUE condition without a band (team default)', () => {
23
+ (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ conditions: [{ type: pinch_condition_1.PinchConditionType.FATIGUE }] }).success).toBe(true);
23
24
  });
24
- (0, globals_1.it)('rejects an unknown band', () => {
25
- (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'FATIGUE', fatigueBand: 'SLEEPY' }).success).toBe(false);
25
+ (0, globals_1.it)('rejects a FATIGUE condition with an unknown band', () => {
26
+ (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ conditions: [{ type: pinch_condition_1.PinchConditionType.FATIGUE, band: 'SLEEPY' }] }).success).toBe(false);
26
27
  });
27
- (0, globals_1.it)('accepts CONDITIONS with a PHASE condition (serve/receive split)', () => {
28
+ (0, globals_1.it)('accepts a PHASE condition (serve/receive split)', () => {
28
29
  const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({
29
- mode: 'CONDITIONS',
30
30
  conditionLogic: 'ALL',
31
31
  conditions: [{ type: pinch_condition_1.PinchConditionType.PHASE, phase: pinch_condition_1.MatchPhase.RECEIVING }]
32
32
  });
33
33
  (0, globals_1.expect)(res.success).toBe(true);
34
34
  });
35
- (0, globals_1.it)('accepts CONDITIONS mixing a pinch condition and a PHASE condition', () => {
35
+ (0, globals_1.it)('accepts mixed conditions (score + phase + fatigue) under ANY logic', () => {
36
36
  const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({
37
- mode: 'CONDITIONS',
38
37
  conditionLogic: 'ANY',
39
38
  conditions: [
40
39
  { type: pinch_condition_1.PinchConditionType.MIN_OWN_SCORE, score: 20 },
41
- { type: pinch_condition_1.PinchConditionType.PHASE, phase: pinch_condition_1.MatchPhase.SERVING }
40
+ { type: pinch_condition_1.PinchConditionType.PHASE, phase: pinch_condition_1.MatchPhase.SERVING },
41
+ { type: pinch_condition_1.PinchConditionType.FATIGUE, band: energy_band_1.EnergyBand.EXHAUSTED }
42
42
  ]
43
43
  });
44
44
  (0, globals_1.expect)(res.success).toBe(true);
45
45
  });
46
- (0, globals_1.it)('rejects CONDITIONS mode with no conditions', () => {
47
- (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'CONDITIONS' }).success).toBe(false);
48
- (0, globals_1.expect)(libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'CONDITIONS', conditions: [] }).success).toBe(false);
49
- });
50
46
  (0, globals_1.it)('rejects an invalid PHASE value', () => {
51
47
  const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({
52
- mode: 'CONDITIONS',
53
48
  conditions: [{ type: pinch_condition_1.PinchConditionType.PHASE, phase: 'ATTACKING' }]
54
49
  });
55
50
  (0, globals_1.expect)(res.success).toBe(false);
56
51
  });
57
52
  });
53
+ (0, globals_1.describe)('LiberoSetSubConfigSchema (legacy conversion)', () => {
54
+ (0, globals_1.it)('converts legacy NEVER to an empty condition list', () => {
55
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'NEVER' });
56
+ (0, globals_1.expect)(res.success).toBe(true);
57
+ if (res.success)
58
+ (0, globals_1.expect)(res.data.conditions).toEqual([]);
59
+ });
60
+ (0, globals_1.it)('converts legacy ALWAYS to a single ASAP condition', () => {
61
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'ALWAYS' });
62
+ (0, globals_1.expect)(res.success).toBe(true);
63
+ if (res.success)
64
+ (0, globals_1.expect)(res.data.conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.ASAP }]);
65
+ });
66
+ (0, globals_1.it)('converts legacy FATIGUE + band to a FATIGUE condition carrying the band', () => {
67
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'FATIGUE', fatigueBand: energy_band_1.EnergyBand.WORN });
68
+ (0, globals_1.expect)(res.success).toBe(true);
69
+ if (res.success)
70
+ (0, globals_1.expect)(res.data.conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.FATIGUE, band: energy_band_1.EnergyBand.WORN }]);
71
+ });
72
+ (0, globals_1.it)('converts legacy FATIGUE without a band to a bandless FATIGUE condition (team default)', () => {
73
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'FATIGUE' });
74
+ (0, globals_1.expect)(res.success).toBe(true);
75
+ if (res.success)
76
+ (0, globals_1.expect)(res.data.conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.FATIGUE }]);
77
+ });
78
+ (0, globals_1.it)('converts legacy CONDITIONS by stripping the mode and keeping the conditions', () => {
79
+ const conditions = [{ type: pinch_condition_1.PinchConditionType.PHASE, phase: pinch_condition_1.MatchPhase.RECEIVING }];
80
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'CONDITIONS', conditionLogic: 'ALL', conditions });
81
+ (0, globals_1.expect)(res.success).toBe(true);
82
+ if (res.success) {
83
+ (0, globals_1.expect)(res.data.conditions).toEqual(conditions);
84
+ (0, globals_1.expect)(res.data.mode).toBeUndefined();
85
+ }
86
+ });
87
+ (0, globals_1.it)('heals a legacy CONDITIONS config with no conditions to never (empty list) instead of rejecting', () => {
88
+ const res = libero_sub_z_1.LiberoSetSubConfigSchema.safeParse({ mode: 'CONDITIONS' });
89
+ (0, globals_1.expect)(res.success).toBe(true);
90
+ if (res.success)
91
+ (0, globals_1.expect)(res.data.conditions ?? []).toHaveLength(0);
92
+ });
93
+ });
58
94
  (0, globals_1.describe)('LiberoSubConfigSchema', () => {
59
- (0, globals_1.it)('accepts a flat config with no per-set overrides', () => {
95
+ (0, globals_1.it)('accepts a flat config with no per-set overrides (legacy shape converts)', () => {
60
96
  (0, globals_1.expect)(libero_sub_z_1.LiberoSubConfigSchema.safeParse({ mode: 'FATIGUE', fatigueBand: energy_band_1.EnergyBand.WORN }).success).toBe(true);
61
97
  });
62
- (0, globals_1.it)('accepts up to five per-set configs', () => {
98
+ (0, globals_1.it)('accepts up to five per-set configs (legacy per-set entries convert)', () => {
63
99
  const res = libero_sub_z_1.LiberoSubConfigSchema.safeParse({
64
100
  mode: 'NEVER',
65
101
  sets: [
66
102
  { mode: 'NEVER' },
67
103
  { mode: 'ALWAYS' },
68
104
  { mode: 'FATIGUE', fatigueBand: energy_band_1.EnergyBand.TIRED },
69
- { mode: 'NEVER' },
70
- { mode: 'ALWAYS' }
105
+ { conditions: [{ type: pinch_condition_1.PinchConditionType.FATIGUE }] },
106
+ {}
71
107
  ]
72
108
  });
73
109
  (0, globals_1.expect)(res.success).toBe(true);
110
+ if (res.success) {
111
+ (0, globals_1.expect)(res.data.sets?.[0].conditions).toEqual([]);
112
+ (0, globals_1.expect)(res.data.sets?.[1].conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.ASAP }]);
113
+ (0, globals_1.expect)(res.data.sets?.[2].conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.FATIGUE, band: energy_band_1.EnergyBand.TIRED }]);
114
+ }
74
115
  });
75
116
  (0, globals_1.it)('rejects more than five per-set configs', () => {
76
117
  const res = libero_sub_z_1.LiberoSubConfigSchema.safeParse({
77
- mode: 'NEVER',
78
- sets: Array.from({ length: 6 }, () => ({ mode: 'NEVER' }))
118
+ sets: Array.from({ length: 6 }, () => ({}))
79
119
  });
80
120
  (0, globals_1.expect)(res.success).toBe(false);
81
121
  });
82
- (0, globals_1.it)('rejects an unknown mode in a per-set config', () => {
122
+ (0, globals_1.it)('rejects an unknown condition type in a per-set config', () => {
83
123
  const res = libero_sub_z_1.LiberoSubConfigSchema.safeParse({
84
- mode: 'NEVER',
85
- sets: [{ mode: 'WHENEVER' }]
124
+ sets: [{ conditions: [{ type: 'WHENEVER' }] }]
86
125
  });
87
126
  (0, globals_1.expect)(res.success).toBe(false);
88
127
  });
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Player } from '../../player';
2
+ import { Player, RoleEnum } from '../../player';
3
3
  import { EnergyBand } from '../energy-band';
4
4
  import { RotationSystemEnum } from '../rotation-system';
5
5
  export declare const TacticsInputSchema: z.ZodObject<{
@@ -15,16 +15,9 @@ export declare const TacticsInputSchema: z.ZodObject<{
15
15
  }, z.core.$strip>;
16
16
  liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
17
17
  receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
18
- designatedSubs: z.ZodDefault<z.ZodArray<z.ZodObject<{
19
- sets: z.ZodOptional<z.ZodArray<z.ZodObject<{
20
- mode: z.ZodEnum<{
21
- NEVER: "NEVER";
22
- FATIGUE: "FATIGUE";
23
- PINCH: "PINCH";
24
- }>;
18
+ designatedSubs: z.ZodDefault<z.ZodArray<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
19
+ sets: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
25
20
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
26
- fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
27
- benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
28
21
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
29
22
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
30
23
  }, z.core.$strip>, z.ZodObject<{
@@ -56,9 +49,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
56
49
  }, z.core.$strip>, z.ZodObject<{
57
50
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
58
51
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
52
+ }, z.core.$strip>, z.ZodObject<{
53
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
54
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
59
55
  }, z.core.$strip>], "type">>>;
60
56
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
61
57
  pinchServer: z.ZodOptional<z.ZodBoolean>;
58
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
62
59
  subBackMode: z.ZodOptional<z.ZodEnum<{
63
60
  FATIGUE: "FATIGUE";
64
61
  CONDITIONS: "CONDITIONS";
@@ -94,17 +91,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
94
91
  }, z.core.$strip>, z.ZodObject<{
95
92
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
96
93
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
94
+ }, z.core.$strip>, z.ZodObject<{
95
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
96
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
97
97
  }, z.core.$strip>], "type">>>;
98
98
  subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
99
- }, z.core.$strip>>>;
100
- mode: z.ZodEnum<{
101
- NEVER: "NEVER";
102
- FATIGUE: "FATIGUE";
103
- PINCH: "PINCH";
104
- }>;
99
+ }, z.core.$strip>>>>;
105
100
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
106
- fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
107
- benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
108
101
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
109
102
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
110
103
  }, z.core.$strip>, z.ZodObject<{
@@ -136,9 +129,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
136
129
  }, z.core.$strip>, z.ZodObject<{
137
130
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
138
131
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
132
+ }, z.core.$strip>, z.ZodObject<{
133
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
134
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
139
135
  }, z.core.$strip>], "type">>>;
140
136
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
141
137
  pinchServer: z.ZodOptional<z.ZodBoolean>;
138
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
142
139
  subBackMode: z.ZodOptional<z.ZodEnum<{
143
140
  FATIGUE: "FATIGUE";
144
141
  CONDITIONS: "CONDITIONS";
@@ -174,21 +171,17 @@ export declare const TacticsInputSchema: z.ZodObject<{
174
171
  }, z.core.$strip>, z.ZodObject<{
175
172
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
176
173
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
174
+ }, z.core.$strip>, z.ZodObject<{
175
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
176
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
177
177
  }, z.core.$strip>], "type">>>;
178
178
  subBackConditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
179
179
  starter: z.ZodCustom<Player, Player>;
180
- }, z.core.$strip>>>;
180
+ }, z.core.$strip>>>>;
181
181
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
182
182
  secondLibero: z.ZodOptional<z.ZodCustom<Player, Player>>;
183
- liberoSub: z.ZodOptional<z.ZodObject<{
184
- sets: z.ZodOptional<z.ZodArray<z.ZodObject<{
185
- mode: z.ZodEnum<{
186
- NEVER: "NEVER";
187
- FATIGUE: "FATIGUE";
188
- CONDITIONS: "CONDITIONS";
189
- ALWAYS: "ALWAYS";
190
- }>;
191
- fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
183
+ liberoSub: z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
184
+ sets: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodObject<{
192
185
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
193
186
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
194
187
  }, z.core.$strip>, z.ZodObject<{
@@ -220,16 +213,12 @@ export declare const TacticsInputSchema: z.ZodObject<{
220
213
  }, z.core.$strip>, z.ZodObject<{
221
214
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
222
215
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
216
+ }, z.core.$strip>, z.ZodObject<{
217
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
218
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
223
219
  }, z.core.$strip>], "type">>>;
224
220
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
225
- }, z.core.$strip>>>;
226
- mode: z.ZodEnum<{
227
- NEVER: "NEVER";
228
- FATIGUE: "FATIGUE";
229
- CONDITIONS: "CONDITIONS";
230
- ALWAYS: "ALWAYS";
231
- }>;
232
- fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
221
+ }, z.core.$strip>>>>;
233
222
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
234
223
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
235
224
  }, z.core.$strip>, z.ZodObject<{
@@ -261,15 +250,18 @@ export declare const TacticsInputSchema: z.ZodObject<{
261
250
  }, z.core.$strip>, z.ZodObject<{
262
251
  type: z.ZodLiteral<import("..").PinchConditionType.PHASE>;
263
252
  phase: z.ZodEnum<typeof import("..").MatchPhase>;
253
+ }, z.core.$strip>, z.ZodObject<{
254
+ type: z.ZodLiteral<import("..").PinchConditionType.FATIGUE>;
255
+ band: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
264
256
  }, z.core.$strip>], "type">>>;
265
257
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
266
- }, z.core.$strip>>;
258
+ }, z.core.$strip>>>;
267
259
  liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
268
260
  rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
269
261
  designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
270
262
  offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
271
263
  rotation: z.ZodNumber;
272
- order: z.ZodArray<z.ZodCustom<Player, Player>>;
264
+ weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
273
265
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
274
266
  FAST: "FAST";
275
267
  HIGH: "HIGH";
@@ -281,12 +273,20 @@ export declare const TacticsInputSchema: z.ZodObject<{
281
273
  }, z.core.$strip>>>;
282
274
  offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
283
275
  rotation: z.ZodNumber;
284
- order: z.ZodArray<z.ZodCustom<Player, Player>>;
276
+ weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
285
277
  tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
286
278
  FAST: "FAST";
287
279
  HIGH: "HIGH";
288
280
  }>>>;
289
281
  }, z.core.$strip>>>>;
282
+ blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
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>>>;
285
+ }, z.core.$strip>>>;
286
+ blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
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>>>;
289
+ }, z.core.$strip>>>>;
290
290
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
291
291
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
292
292
  starter: z.ZodCustom<Player, Player>;
@@ -50,14 +50,23 @@ const lineupSchema = zod_1.z.object({
50
50
  });
51
51
  // Per-player tempo preference (set-tempo rework): absent key = Auto.
52
52
  const tempoPreferenceSchema = zod_1.z.enum(['FAST', 'HIGH']);
53
- // One rotation's ranked attacker priority. `rotation` is the main setter's rotational slot (1..6); `order` lists
54
- // the attackers ranked best-first. `tempos` optionally forces a per-starter tempo (Auto when absent). Cross-field
55
- // checks (starters only in both order and tempos, no dupes) live in the superRefine below.
53
+ // One rotation's attacker weighting. `rotation` is the main setter's rotational slot (1..6); `weights` maps each
54
+ // attacker's starter id to an integer percentage share (values sum to 100; 0 removes an attacker). `tempos`
55
+ // optionally forces a per-starter tempo (Auto when absent). Cross-field checks (keys are starters, weights sum to
56
+ // 100) live in the superRefine below.
56
57
  const offensivePreferenceSchema = zod_1.z.object({
57
58
  rotation: zod_1.z.number().int().min(1).max(6),
58
- order: zod_1.z.array(playerInstanceSchema),
59
+ weights: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(0)),
59
60
  tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
60
61
  });
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' });
64
+ // One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
65
+ // each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
66
+ const blockingPreferenceSchema = zod_1.z.object({
67
+ rotation: zod_1.z.number().int().min(1).max(6),
68
+ targets: zod_1.z.record(zod_1.z.string(), blockingFunctionSchema)
69
+ });
61
70
  // One set's rotation-system override (per-set tactics): system + its designated setters travel together.
62
71
  const systemSetSchema = zod_1.z.object({
63
72
  rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum),
@@ -85,6 +94,9 @@ exports.TacticsInputSchema = zod_1.z.object({
85
94
  // validated with the same cross-field rules as the flat fields (see superRefine below).
86
95
  systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
87
96
  offensivePreferenceSets: zod_1.z.array(zod_1.z.array(offensivePreferenceSchema)).max(5).optional(),
97
+ // Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
98
+ blockingPreferences: zod_1.z.array(blockingPreferenceSchema).default([]),
99
+ blockingPreferenceSets: zod_1.z.array(zod_1.z.array(blockingPreferenceSchema)).max(5).optional(),
88
100
  // Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
89
101
  // PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
90
102
  // lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
@@ -153,16 +165,17 @@ exports.TacticsInputSchema = zod_1.z.object({
153
165
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
154
166
  }
155
167
  seenRotations.add(pref.rotation);
156
- const ids = pref.order.map(p => p.id);
157
- if (new Set(ids).size !== ids.length) {
158
- ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path });
159
- }
160
- for (const id of ids) {
168
+ // Weight keys are on-court starters, and the percentages must sum to exactly 100 (0 removes an attacker).
169
+ for (const id of Object.keys(pref.weights)) {
161
170
  if (!starterIds.has(id)) {
162
171
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
163
172
  }
164
173
  }
165
- // Tempo preference keys are starter ids too (same rule as order).
174
+ const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
175
+ if (total !== 100) {
176
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
177
+ }
178
+ // Tempo preference keys are starter ids too.
166
179
  for (const id of Object.keys(pref.tempos ?? {})) {
167
180
  if (!starterIds.has(id)) {
168
181
  ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
@@ -170,6 +183,22 @@ exports.TacticsInputSchema = zod_1.z.object({
170
183
  }
171
184
  }
172
185
  };
186
+ // Blocking preferences: unique rotations; each assignment keys an on-court starter (the UI only offers front-row
187
+ // players, the schema checks starters). The target function is validated by blockingFunctionSchema.
188
+ const validateBlockingPreferences = (preferences, path) => {
189
+ const seenRotations = new Set();
190
+ for (const pref of preferences) {
191
+ if (seenRotations.has(pref.rotation)) {
192
+ ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
193
+ }
194
+ seenRotations.add(pref.rotation);
195
+ for (const id of Object.keys(pref.targets)) {
196
+ if (!starterIds.has(id)) {
197
+ ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
198
+ }
199
+ }
200
+ }
201
+ };
173
202
  validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
174
203
  for (const [i, systemSet] of (data.systemSets ?? []).entries()) {
175
204
  validateSystem(systemSet.rotationSystem, systemSet.designatedSetters, ['systemSets', i]);
@@ -178,6 +207,10 @@ exports.TacticsInputSchema = zod_1.z.object({
178
207
  for (const [i, preferences] of (data.offensivePreferenceSets ?? []).entries()) {
179
208
  validateOffensivePreferences(preferences, ['offensivePreferenceSets', i]);
180
209
  }
210
+ validateBlockingPreferences(data.blockingPreferences, ['blockingPreferences']);
211
+ for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
212
+ validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
213
+ }
181
214
  // Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
182
215
  // rule (FATIGUE/ALWAYS in any set) needs someone to swap in, so it requires a second libero, and a second
183
216
  // libero needs a starting libero to replace ("no L2 without an L"). Per-set libero replacements, like the
@@ -188,10 +221,11 @@ exports.TacticsInputSchema = zod_1.z.object({
188
221
  ...(libero != null ? [libero.id] : []),
189
222
  ...data.lineup.bench.map(p => p.id)
190
223
  ]);
191
- const liberoSubModes = data.liberoSub != null
192
- ? [data.liberoSub.mode, ...(data.liberoSub.sets ?? []).map(s => s.mode)]
224
+ // Unified condition engine: the rule wants a swap when ANY config (flat or per-set) has conditions.
225
+ const liberoConditionSets = data.liberoSub != null
226
+ ? [data.liberoSub.conditions ?? [], ...(data.liberoSub.sets ?? []).map(s => s.conditions ?? [])]
193
227
  : [];
194
- const wantsLiberoSwap = liberoSubModes.some(mode => mode !== 'NEVER');
228
+ const wantsLiberoSwap = liberoConditionSets.some(conditions => conditions.length > 0);
195
229
  if (wantsLiberoSwap && data.secondLibero == null) {
196
230
  ctx.addIssue({ code: 'custom', message: 'LIBERO_SWAP_REQUIRES_SECOND_LIBERO', path: ['secondLibero'] });
197
231
  }