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.
- package/dist/cjs/src/service/team/base-config.d.ts +1 -0
- package/dist/cjs/src/service/team/default-base-config.js +59 -3
- package/dist/cjs/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/cjs/src/service/team/default-base-config.test.js +44 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +4 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +4 -1
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +4 -0
- package/dist/esm/src/service/team/base-config.d.ts +1 -0
- package/dist/esm/src/service/team/default-base-config.js +59 -3
- package/dist/esm/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/esm/src/service/team/default-base-config.test.js +42 -0
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +4 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +4 -1
- package/dist/esm/src/service/team/schemas/team.z.d.ts +4 -0
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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<{
|