volleyballsimtypes 0.0.514 → 0.0.515

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.
@@ -8,4 +8,5 @@ export interface BaseConfig {
8
8
  readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
9
9
  readonly receive: Record<string, Record<string, boolean>>;
10
10
  readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
11
+ readonly attackContact?: Record<string, Record<string, BaseCoord>>;
11
12
  }
@@ -88,11 +88,40 @@ const COORDS = {
88
88
  6: { 1: { d: 5.35, r: 2.35 }, 2: { d: 2.05, r: 2.1 }, 3: { d: 0.8, r: 4 }, 4: { d: 2.1, r: -1.95 }, 5: { d: 4.9, r: -1.4 }, 6: { d: 0.4, r: 1.3 } }
89
89
  }
90
90
  };
91
+ // Per-attacker attack subbases (owner 2026-08-12): the point-of-attack ATK_L/M/R coverage shapes are replaced by ONE
92
+ // subbase per attacker, keyed by the attacker's ZONE (ATK_Z1..ATK_Z6). Each is the whole-team formation used when the
93
+ // attacker at that zone gets the ball. A FRONT-row zone (2/3/4) reuses its lane's old ATK_L/M/R shape, where the lane
94
+ // is ranked by that zone's ATTACK-base lateral this rotation (leftmost r -> ATK_L, middle -> ATK_M, rightmost -> ATK_R);
95
+ // a BACK-row zone (1/5/6) replicates the ATTACK base. Derived from COORDS so the code default and the migration agree.
96
+ // The old ATK_L/M/R stay in COORDS as the derivation SOURCE but are NOT exposed in the default (below).
97
+ const FRONT_ZONES = [2, 3, 4];
98
+ const BACK_ZONES = [1, 5, 6];
99
+ function defaultAttackSubbases() {
100
+ const cloneFormation = (zones) => {
101
+ const out = {};
102
+ for (const [zone, c] of Object.entries(zones))
103
+ out[zone] = { d: c.d, r: c.r };
104
+ return out;
105
+ };
106
+ const out = { ATK_Z1: {}, ATK_Z2: {}, ATK_Z3: {}, ATK_Z4: {}, ATK_Z5: {}, ATK_Z6: {} };
107
+ for (let rot = 1; rot <= 6; rot++) {
108
+ const ranked = [...FRONT_ZONES].sort((a, b) => COORDS.ATTACK[rot][a].r - COORDS.ATTACK[rot][b].r);
109
+ const laneFor = { [ranked[0]]: 'ATK_L', [ranked[1]]: 'ATK_M', [ranked[2]]: 'ATK_R' };
110
+ for (const z of FRONT_ZONES)
111
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS[laneFor[z]][rot]);
112
+ for (const z of BACK_ZONES)
113
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS.ATTACK[rot]);
114
+ }
115
+ return out;
116
+ }
91
117
  // The default coordinate table as a BaseConfig.coords (jsonb string keys). Shared, readonly; materialized (and so
92
- // serialized) per team on save, so sharing the reference in memory is safe.
118
+ // serialized) per team on save, so sharing the reference in memory is safe. ATK_L/M/R are dropped from the exposed
119
+ // default (replaced by ATK_Z<n>); everything else is copied verbatim from COORDS.
93
120
  const DEFAULT_COORDS = (() => {
94
121
  const out = {};
95
122
  for (const [formation, rotations] of Object.entries(COORDS)) {
123
+ if (formation === 'ATK_L' || formation === 'ATK_M' || formation === 'ATK_R')
124
+ continue; // replaced by ATK_Z<n>
96
125
  out[formation] = {};
97
126
  for (const [rotation, zones] of Object.entries(rotations)) {
98
127
  out[formation][rotation] = {};
@@ -100,8 +129,35 @@ const DEFAULT_COORDS = (() => {
100
129
  out[formation][rotation][zone] = { d: coord.d, r: coord.r };
101
130
  }
102
131
  }
103
- return out;
132
+ return { ...out, ...defaultAttackSubbases() };
104
133
  })();
134
+ // The default desired-contact per attacker (trajectory-set, owner 2026-08-12 decision D9): a near-net hitting point
135
+ // in each attacker's lane, NOT the old "set to the standing dot" location. `contact.d` = a near-net depth for the
136
+ // front row or the pipe depth for the back row; `contact.r` = the attacker's ATTACK-dot lateral clamped to the
137
+ // antenna. Keyed [rotation][zone] -> {d,r}. The near-net depth + antenna are first-cut calibration knobs. The sim
138
+ // does not read this yet (added with the engine phase); storing it here is inert. See plans/trajectory-set-and-approach.md.
139
+ const NEAR_NET_CONTACT_DEPTH = 0.5; // front-row contact, m off the net
140
+ const PIPE_CONTACT_DEPTH = 1.8; // back-row (pipe) contact, m off the net (matches the sim's BACKROW_SET_DEPTH)
141
+ const CONTACT_ANTENNA = 4.3; // keep the contact just inside the sideline / antenna
142
+ // LAZY + memoized (not an eager IIFE): `isBackRow` lives in the match module, which sits in a load cycle with the
143
+ // team barrel, so calling it at module-load time hits an uninitialised import. Computed once on first use, then the
144
+ // shared readonly reference is reused (like DEFAULT_COORDS; serialized per team on save, so sharing is safe).
145
+ let cachedAttackContact;
146
+ function defaultAttackContact() {
147
+ if (cachedAttackContact != null)
148
+ return cachedAttackContact;
149
+ const out = {};
150
+ for (const [rotation, zones] of Object.entries(COORDS.ATTACK)) {
151
+ out[rotation] = {};
152
+ for (const [zone, coord] of Object.entries(zones)) {
153
+ const backRow = (0, match_1.isBackRow)(Number(zone));
154
+ const r = Math.max(-CONTACT_ANTENNA, Math.min(CONTACT_ANTENNA, coord.r));
155
+ out[rotation][zone] = { d: backRow ? PIPE_CONTACT_DEPTH : NEAR_NET_CONTACT_DEPTH, r };
156
+ }
157
+ }
158
+ cachedAttackContact = out;
159
+ return out;
160
+ }
105
161
  // The owner's corrected per-point-of-attack block map for the 5-1 (exported 2026-08-09), keyed
106
162
  // [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock. `false` = that front-row zone sits OUT of the block for an attack
107
163
  // from that side; `true` = reads/joins the wall. Only the front-row zones in play for the rotation are tagged. Same
@@ -167,5 +223,5 @@ function defaultReceiveMap(system) {
167
223
  // 5-1 uses the owner's exported receive map, the other systems derive their receive map per system.
168
224
  function defaultBaseConfig(system) {
169
225
  const receive = system === rotation_system_1.RotationSystemEnum.FIVE_ONE ? DEFAULT_5_1_RECEIVE : defaultReceiveMap(system);
170
- return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING };
226
+ return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING, attackContact: defaultAttackContact() };
171
227
  }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rotation_system_1 = require("./rotation-system");
4
+ const default_base_config_1 = require("./default-base-config");
5
+ // Back-row rotational zones (right/left/middle back). Front row is {2,3,4}.
6
+ const BACK_ROW = new Set([1, 5, 6]);
7
+ // The trajectory-set default desired-contact (owner D9, 2026-08-12): a near-net hitting point in each attacker's
8
+ // lane, NOT the old set-to-the-standing-dot location. Front row contacts sit ~0.5 m off the net; back row on the
9
+ // ~1.8 m pipe line; the lateral is the ATTACK-dot's r clamped inside the antenna (|r| <= 4.3).
10
+ describe('defaultBaseConfig attackContact (trajectory-set default)', () => {
11
+ const cfg = (0, default_base_config_1.defaultBaseConfig)(rotation_system_1.RotationSystemEnum.FIVE_ONE);
12
+ it('provides a finite desired contact for every rotation and zone', () => {
13
+ expect(cfg.attackContact).toBeDefined();
14
+ for (let rot = 1; rot <= 6; rot++) {
15
+ for (let zone = 1; zone <= 6; zone++) {
16
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
17
+ expect(c).toBeDefined();
18
+ expect(Number.isFinite(c?.d)).toBe(true);
19
+ expect(Number.isFinite(c?.r)).toBe(true);
20
+ }
21
+ }
22
+ });
23
+ it('places front-row contacts near the net (0.5 m) and back-row on the pipe line (1.8 m)', () => {
24
+ for (let rot = 1; rot <= 6; rot++) {
25
+ for (let zone = 1; zone <= 6; zone++) {
26
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
27
+ expect(c.d).toBeCloseTo(BACK_ROW.has(zone) ? 1.8 : 0.5);
28
+ }
29
+ }
30
+ });
31
+ it('clamps every contact inside the antenna (|r| <= 4.3), even for a wide-pin attack dot', () => {
32
+ for (let rot = 1; rot <= 6; rot++) {
33
+ for (let zone = 1; zone <= 6; zone++) {
34
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
35
+ expect(Math.abs(c.r)).toBeLessThanOrEqual(4.3 + 1e-9);
36
+ }
37
+ }
38
+ });
39
+ it('keeps the contact on the attacker approach-lane side (rotation 1 OH wide dot r=5 -> +4.3)', () => {
40
+ const c = cfg.attackContact?.['1']?.['4'];
41
+ expect(c.d).toBeCloseTo(0.5); // front row, near the net
42
+ expect(c.r).toBeCloseTo(4.3); // clamped from the OH's wide ATTACK dot (r=5), same (positive) side
43
+ });
44
+ });
@@ -305,6 +305,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
305
305
  }, z.core.$strip>>>>;
306
306
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
307
307
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
308
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
309
+ d: z.ZodNumber;
310
+ r: z.ZodNumber;
311
+ }, z.core.$strip>>>>;
308
312
  }, z.core.$strip>>;
309
313
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
310
314
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -92,7 +92,10 @@ const baseConfigSchema = zod_1.z.object({
92
92
  receive: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean())),
93
93
  // Optional per-point-of-attack blocking (owner 2026-08-07): [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock.
94
94
  // Optional so presets/tactics saved before this field validate unchanged.
95
- blocking: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean()))).optional()
95
+ blocking: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean()))).optional(),
96
+ // Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
97
+ // configs saved before this field validate unchanged.
98
+ attackContact: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema)).optional()
96
99
  });
97
100
  exports.TacticsInputSchema = zod_1.z.object({
98
101
  lineup: lineupSchema,
@@ -313,6 +313,10 @@ export declare const TeamInputSchema: z.ZodObject<{
313
313
  }, z.core.$strip>>>>;
314
314
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
315
315
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
316
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
317
+ d: z.ZodNumber;
318
+ r: z.ZodNumber;
319
+ }, z.core.$strip>>>>;
316
320
  }, z.core.$strip>>;
317
321
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
318
322
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -8,4 +8,5 @@ export interface BaseConfig {
8
8
  readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
9
9
  readonly receive: Record<string, Record<string, boolean>>;
10
10
  readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
11
+ readonly attackContact?: Record<string, Record<string, BaseCoord>>;
11
12
  }
@@ -84,11 +84,40 @@ const COORDS = {
84
84
  6: { 1: { d: 5.35, r: 2.35 }, 2: { d: 2.05, r: 2.1 }, 3: { d: 0.8, r: 4 }, 4: { d: 2.1, r: -1.95 }, 5: { d: 4.9, r: -1.4 }, 6: { d: 0.4, r: 1.3 } }
85
85
  }
86
86
  };
87
+ // Per-attacker attack subbases (owner 2026-08-12): the point-of-attack ATK_L/M/R coverage shapes are replaced by ONE
88
+ // subbase per attacker, keyed by the attacker's ZONE (ATK_Z1..ATK_Z6). Each is the whole-team formation used when the
89
+ // attacker at that zone gets the ball. A FRONT-row zone (2/3/4) reuses its lane's old ATK_L/M/R shape, where the lane
90
+ // is ranked by that zone's ATTACK-base lateral this rotation (leftmost r -> ATK_L, middle -> ATK_M, rightmost -> ATK_R);
91
+ // a BACK-row zone (1/5/6) replicates the ATTACK base. Derived from COORDS so the code default and the migration agree.
92
+ // The old ATK_L/M/R stay in COORDS as the derivation SOURCE but are NOT exposed in the default (below).
93
+ const FRONT_ZONES = [2, 3, 4];
94
+ const BACK_ZONES = [1, 5, 6];
95
+ function defaultAttackSubbases() {
96
+ const cloneFormation = (zones) => {
97
+ const out = {};
98
+ for (const [zone, c] of Object.entries(zones))
99
+ out[zone] = { d: c.d, r: c.r };
100
+ return out;
101
+ };
102
+ const out = { ATK_Z1: {}, ATK_Z2: {}, ATK_Z3: {}, ATK_Z4: {}, ATK_Z5: {}, ATK_Z6: {} };
103
+ for (let rot = 1; rot <= 6; rot++) {
104
+ const ranked = [...FRONT_ZONES].sort((a, b) => COORDS.ATTACK[rot][a].r - COORDS.ATTACK[rot][b].r);
105
+ const laneFor = { [ranked[0]]: 'ATK_L', [ranked[1]]: 'ATK_M', [ranked[2]]: 'ATK_R' };
106
+ for (const z of FRONT_ZONES)
107
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS[laneFor[z]][rot]);
108
+ for (const z of BACK_ZONES)
109
+ out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS.ATTACK[rot]);
110
+ }
111
+ return out;
112
+ }
87
113
  // The default coordinate table as a BaseConfig.coords (jsonb string keys). Shared, readonly; materialized (and so
88
- // serialized) per team on save, so sharing the reference in memory is safe.
114
+ // serialized) per team on save, so sharing the reference in memory is safe. ATK_L/M/R are dropped from the exposed
115
+ // default (replaced by ATK_Z<n>); everything else is copied verbatim from COORDS.
89
116
  const DEFAULT_COORDS = (() => {
90
117
  const out = {};
91
118
  for (const [formation, rotations] of Object.entries(COORDS)) {
119
+ if (formation === 'ATK_L' || formation === 'ATK_M' || formation === 'ATK_R')
120
+ continue; // replaced by ATK_Z<n>
92
121
  out[formation] = {};
93
122
  for (const [rotation, zones] of Object.entries(rotations)) {
94
123
  out[formation][rotation] = {};
@@ -96,8 +125,35 @@ const DEFAULT_COORDS = (() => {
96
125
  out[formation][rotation][zone] = { d: coord.d, r: coord.r };
97
126
  }
98
127
  }
99
- return out;
128
+ return { ...out, ...defaultAttackSubbases() };
100
129
  })();
130
+ // The default desired-contact per attacker (trajectory-set, owner 2026-08-12 decision D9): a near-net hitting point
131
+ // in each attacker's lane, NOT the old "set to the standing dot" location. `contact.d` = a near-net depth for the
132
+ // front row or the pipe depth for the back row; `contact.r` = the attacker's ATTACK-dot lateral clamped to the
133
+ // antenna. Keyed [rotation][zone] -> {d,r}. The near-net depth + antenna are first-cut calibration knobs. The sim
134
+ // does not read this yet (added with the engine phase); storing it here is inert. See plans/trajectory-set-and-approach.md.
135
+ const NEAR_NET_CONTACT_DEPTH = 0.5; // front-row contact, m off the net
136
+ const PIPE_CONTACT_DEPTH = 1.8; // back-row (pipe) contact, m off the net (matches the sim's BACKROW_SET_DEPTH)
137
+ const CONTACT_ANTENNA = 4.3; // keep the contact just inside the sideline / antenna
138
+ // LAZY + memoized (not an eager IIFE): `isBackRow` lives in the match module, which sits in a load cycle with the
139
+ // team barrel, so calling it at module-load time hits an uninitialised import. Computed once on first use, then the
140
+ // shared readonly reference is reused (like DEFAULT_COORDS; serialized per team on save, so sharing is safe).
141
+ let cachedAttackContact;
142
+ function defaultAttackContact() {
143
+ if (cachedAttackContact != null)
144
+ return cachedAttackContact;
145
+ const out = {};
146
+ for (const [rotation, zones] of Object.entries(COORDS.ATTACK)) {
147
+ out[rotation] = {};
148
+ for (const [zone, coord] of Object.entries(zones)) {
149
+ const backRow = isBackRow(Number(zone));
150
+ const r = Math.max(-CONTACT_ANTENNA, Math.min(CONTACT_ANTENNA, coord.r));
151
+ out[rotation][zone] = { d: backRow ? PIPE_CONTACT_DEPTH : NEAR_NET_CONTACT_DEPTH, r };
152
+ }
153
+ }
154
+ cachedAttackContact = out;
155
+ return out;
156
+ }
101
157
  // The owner's corrected per-point-of-attack block map for the 5-1 (exported 2026-08-09), keyed
102
158
  // [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock. `false` = that front-row zone sits OUT of the block for an attack
103
159
  // from that side; `true` = reads/joins the wall. Only the front-row zones in play for the rotation are tagged. Same
@@ -163,5 +219,5 @@ export function defaultReceiveMap(system) {
163
219
  // 5-1 uses the owner's exported receive map, the other systems derive their receive map per system.
164
220
  export function defaultBaseConfig(system) {
165
221
  const receive = system === RotationSystemEnum.FIVE_ONE ? DEFAULT_5_1_RECEIVE : defaultReceiveMap(system);
166
- return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING };
222
+ return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING, attackContact: defaultAttackContact() };
167
223
  }
@@ -0,0 +1,42 @@
1
+ import { RotationSystemEnum } from './rotation-system';
2
+ import { defaultBaseConfig } from './default-base-config';
3
+ // Back-row rotational zones (right/left/middle back). Front row is {2,3,4}.
4
+ const BACK_ROW = new Set([1, 5, 6]);
5
+ // The trajectory-set default desired-contact (owner D9, 2026-08-12): a near-net hitting point in each attacker's
6
+ // lane, NOT the old set-to-the-standing-dot location. Front row contacts sit ~0.5 m off the net; back row on the
7
+ // ~1.8 m pipe line; the lateral is the ATTACK-dot's r clamped inside the antenna (|r| <= 4.3).
8
+ describe('defaultBaseConfig attackContact (trajectory-set default)', () => {
9
+ const cfg = defaultBaseConfig(RotationSystemEnum.FIVE_ONE);
10
+ it('provides a finite desired contact for every rotation and zone', () => {
11
+ expect(cfg.attackContact).toBeDefined();
12
+ for (let rot = 1; rot <= 6; rot++) {
13
+ for (let zone = 1; zone <= 6; zone++) {
14
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
15
+ expect(c).toBeDefined();
16
+ expect(Number.isFinite(c?.d)).toBe(true);
17
+ expect(Number.isFinite(c?.r)).toBe(true);
18
+ }
19
+ }
20
+ });
21
+ it('places front-row contacts near the net (0.5 m) and back-row on the pipe line (1.8 m)', () => {
22
+ for (let rot = 1; rot <= 6; rot++) {
23
+ for (let zone = 1; zone <= 6; zone++) {
24
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
25
+ expect(c.d).toBeCloseTo(BACK_ROW.has(zone) ? 1.8 : 0.5);
26
+ }
27
+ }
28
+ });
29
+ it('clamps every contact inside the antenna (|r| <= 4.3), even for a wide-pin attack dot', () => {
30
+ for (let rot = 1; rot <= 6; rot++) {
31
+ for (let zone = 1; zone <= 6; zone++) {
32
+ const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
33
+ expect(Math.abs(c.r)).toBeLessThanOrEqual(4.3 + 1e-9);
34
+ }
35
+ }
36
+ });
37
+ it('keeps the contact on the attacker approach-lane side (rotation 1 OH wide dot r=5 -> +4.3)', () => {
38
+ const c = cfg.attackContact?.['1']?.['4'];
39
+ expect(c.d).toBeCloseTo(0.5); // front row, near the net
40
+ expect(c.r).toBeCloseTo(4.3); // clamped from the OH's wide ATTACK dot (r=5), same (positive) side
41
+ });
42
+ });
@@ -305,6 +305,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
305
305
  }, z.core.$strip>>>>;
306
306
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
307
307
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
308
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
309
+ d: z.ZodNumber;
310
+ r: z.ZodNumber;
311
+ }, z.core.$strip>>>>;
308
312
  }, z.core.$strip>>;
309
313
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
310
314
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -89,7 +89,10 @@ const baseConfigSchema = z.object({
89
89
  receive: z.record(z.string(), z.record(z.string(), z.boolean())),
90
90
  // Optional per-point-of-attack blocking (owner 2026-08-07): [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock.
91
91
  // Optional so presets/tactics saved before this field validate unchanged.
92
- blocking: z.record(z.string(), z.record(z.string(), z.record(z.string(), z.boolean()))).optional()
92
+ blocking: z.record(z.string(), z.record(z.string(), z.record(z.string(), z.boolean()))).optional(),
93
+ // Desired attack contact per attacker (trajectory-set, owner 2026-08-12): [rotation][zone] -> {d,r}. Optional so
94
+ // configs saved before this field validate unchanged.
95
+ attackContact: z.record(z.string(), z.record(z.string(), baseCoordSchema)).optional()
93
96
  });
94
97
  export const TacticsInputSchema = z.object({
95
98
  lineup: lineupSchema,
@@ -313,6 +313,10 @@ export declare const TeamInputSchema: z.ZodObject<{
313
313
  }, z.core.$strip>>>>;
314
314
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
315
315
  blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
316
+ attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
317
+ d: z.ZodNumber;
318
+ r: z.ZodNumber;
319
+ }, z.core.$strip>>>>;
316
320
  }, z.core.$strip>>;
317
321
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
318
322
  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.514",
3
+ "version": "0.0.515",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",