volleyballsimtypes 0.0.513 → 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/data/transformers/keyframe-codec.d.ts +7 -0
- package/dist/cjs/src/data/transformers/keyframe-codec.js +66 -0
- package/dist/cjs/src/data/transformers/keyframe-codec.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/keyframe-codec.test.js +46 -0
- package/dist/cjs/src/data/transformers/rally.js +17 -4
- package/dist/cjs/src/data/transformers/rally.test.js +33 -0
- package/dist/cjs/src/service/match/index.d.ts +1 -0
- package/dist/cjs/src/service/match/index.js +1 -0
- package/dist/cjs/src/service/match/rally.d.ts +6 -0
- package/dist/cjs/src/service/match/rally.js +2 -1
- package/dist/cjs/src/service/match/replay-geometry.d.ts +8 -0
- package/dist/cjs/src/service/match/replay-geometry.js +45 -0
- package/dist/cjs/src/service/match/replay-geometry.test.d.ts +1 -0
- package/dist/cjs/src/service/match/replay-geometry.test.js +77 -0
- package/dist/cjs/src/service/match/schemas/rally.z.d.ts +5 -0
- package/dist/cjs/src/service/match/schemas/rally.z.js +4 -1
- 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/data/transformers/keyframe-codec.d.ts +7 -0
- package/dist/esm/src/data/transformers/keyframe-codec.js +62 -0
- package/dist/esm/src/data/transformers/keyframe-codec.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/keyframe-codec.test.js +44 -0
- package/dist/esm/src/data/transformers/rally.js +17 -4
- package/dist/esm/src/data/transformers/rally.test.js +33 -0
- package/dist/esm/src/service/match/index.d.ts +1 -0
- package/dist/esm/src/service/match/index.js +1 -0
- package/dist/esm/src/service/match/rally.d.ts +6 -0
- package/dist/esm/src/service/match/rally.js +2 -1
- package/dist/esm/src/service/match/replay-geometry.d.ts +8 -0
- package/dist/esm/src/service/match/replay-geometry.js +38 -0
- package/dist/esm/src/service/match/replay-geometry.test.d.ts +1 -0
- package/dist/esm/src/service/match/replay-geometry.test.js +75 -0
- package/dist/esm/src/service/match/schemas/rally.z.d.ts +5 -0
- package/dist/esm/src/service/match/schemas/rally.z.js +4 -1
- 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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface KeyframeCoord {
|
|
2
|
+
readonly playerId: string;
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function encodeKeyframes(keyframes: Array<KeyframeCoord[] | undefined>, playerIndex: Map<string, number>): number[][];
|
|
7
|
+
export declare function decodeKeyframes(k: number[][], roster: string[]): KeyframeCoord[][];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Compact codec for the replay position keyframes (owner 2026-08-11): quantize each coordinate to decimetres and
|
|
3
|
+
// delta-encode against the running frame, so a rally stores only the players who MOVED each event (players teleport
|
|
4
|
+
// only at phase changes, so most events add almost nothing). The result rides the existing msgpack + deflate rally
|
|
5
|
+
// blob. VISUALIZATION ONLY, additive + optional. See VolleyballSim/plans/positional-replay-keyframes.md.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.encodeKeyframes = encodeKeyframes;
|
|
8
|
+
exports.decodeKeyframes = decodeKeyframes;
|
|
9
|
+
// Decimetre quantization (~0.1 m, ample for a replay): x in [-4.5,4.5] -> qx in [-45,45], y in [-9,9] -> qy in [-90,90].
|
|
10
|
+
const SCALE = 10;
|
|
11
|
+
function quant(n) { return Math.round(n * SCALE); }
|
|
12
|
+
function unquant(n) { return n / SCALE; }
|
|
13
|
+
// Encode keyframes (aligned by index to a rally's events) into delta arrays: `k[i]` = flat `[idx, qx, qy, ...]` for the
|
|
14
|
+
// players whose quantized position CHANGED at event i vs the running map. An empty entry = carry the prior frame (a
|
|
15
|
+
// carry-forward event, or no movement). Trailing empty frames are trimmed. Players are referenced by `playerIndex`
|
|
16
|
+
// (the caller extends the rally roster to cover every keyframe player).
|
|
17
|
+
function encodeKeyframes(keyframes, playerIndex) {
|
|
18
|
+
const px = new Map();
|
|
19
|
+
const py = new Map();
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const kf of keyframes) {
|
|
22
|
+
if (kf == null) {
|
|
23
|
+
out.push([]);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const delta = [];
|
|
27
|
+
for (const p of kf) {
|
|
28
|
+
const idx = playerIndex.get(p.playerId);
|
|
29
|
+
if (idx == null)
|
|
30
|
+
continue;
|
|
31
|
+
const qx = quant(p.x);
|
|
32
|
+
const qy = quant(p.y);
|
|
33
|
+
if (px.get(idx) === qx && py.get(idx) === qy)
|
|
34
|
+
continue;
|
|
35
|
+
px.set(idx, qx);
|
|
36
|
+
py.set(idx, qy);
|
|
37
|
+
delta.push(idx, qx, qy);
|
|
38
|
+
}
|
|
39
|
+
out.push(delta);
|
|
40
|
+
}
|
|
41
|
+
while (out.length > 0 && out[out.length - 1].length === 0)
|
|
42
|
+
out.pop();
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
// Decode the delta arrays back to a per-event keyframe. Frame i = the full running map after applying deltas 0..i
|
|
46
|
+
// (so a carry-forward frame repeats the prior positions). Returns one frame per encoded entry; events past the
|
|
47
|
+
// encoded length reuse the last frame.
|
|
48
|
+
function decodeKeyframes(k, roster) {
|
|
49
|
+
const px = new Map();
|
|
50
|
+
const py = new Map();
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const delta of k) {
|
|
53
|
+
for (let j = 0; j + 2 < delta.length; j += 3) {
|
|
54
|
+
px.set(delta[j], delta[j + 1]);
|
|
55
|
+
py.set(delta[j], delta[j + 2]);
|
|
56
|
+
}
|
|
57
|
+
const frame = [];
|
|
58
|
+
for (const [idx, qx] of px) {
|
|
59
|
+
const id = roster[idx];
|
|
60
|
+
if (id != null)
|
|
61
|
+
frame.push({ playerId: id, x: unquant(qx), y: unquant(py.get(idx)) });
|
|
62
|
+
}
|
|
63
|
+
out.push(frame);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const keyframe_codec_1 = require("./keyframe-codec");
|
|
5
|
+
const idx = new Map([['A', 0], ['B', 1]]);
|
|
6
|
+
const roster = ['A', 'B'];
|
|
7
|
+
(0, globals_1.describe)('keyframe-codec round trip', () => {
|
|
8
|
+
(0, globals_1.it)('quantizes to decimetres and restores within 0.1 m', () => {
|
|
9
|
+
const frames = [[{ playerId: 'A', x: 1.23, y: -2.34 }, { playerId: 'B', x: 3, y: 4.5 }]];
|
|
10
|
+
const [out] = (0, keyframe_codec_1.decodeKeyframes)((0, keyframe_codec_1.encodeKeyframes)(frames, idx), roster);
|
|
11
|
+
(0, globals_1.expect)(out.find(p => p.playerId === 'A')).toEqual({ playerId: 'A', x: 1.2, y: -2.3 });
|
|
12
|
+
(0, globals_1.expect)(out.find(p => p.playerId === 'B')).toEqual({ playerId: 'B', x: 3, y: 4.5 });
|
|
13
|
+
});
|
|
14
|
+
(0, globals_1.it)('stores only the players who moved (delta), and carries the rest forward', () => {
|
|
15
|
+
const frames = [
|
|
16
|
+
[{ playerId: 'A', x: 1, y: 1 }, { playerId: 'B', x: 3, y: 3 }],
|
|
17
|
+
[{ playerId: 'A', x: 1, y: 1 }, { playerId: 'B', x: -1, y: 2 }] // A unchanged, B moved
|
|
18
|
+
];
|
|
19
|
+
const k = (0, keyframe_codec_1.encodeKeyframes)(frames, idx);
|
|
20
|
+
(0, globals_1.expect)(k[0]).toEqual([0, 10, 10, 1, 30, 30]); // both players
|
|
21
|
+
(0, globals_1.expect)(k[1]).toEqual([1, -10, 20]); // only B
|
|
22
|
+
const dec = (0, keyframe_codec_1.decodeKeyframes)(k, roster);
|
|
23
|
+
(0, globals_1.expect)(dec[0]).toEqual([{ playerId: 'A', x: 1, y: 1 }, { playerId: 'B', x: 3, y: 3 }]);
|
|
24
|
+
(0, globals_1.expect)(dec[1]).toEqual([{ playerId: 'A', x: 1, y: 1 }, { playerId: 'B', x: -1, y: 2 }]); // A carried forward
|
|
25
|
+
});
|
|
26
|
+
(0, globals_1.it)('an empty (carry-forward) frame repeats the prior positions', () => {
|
|
27
|
+
const frames = [
|
|
28
|
+
[{ playerId: 'A', x: 1, y: 1 }],
|
|
29
|
+
undefined, // carry forward
|
|
30
|
+
[{ playerId: 'A', x: 2, y: 2 }]
|
|
31
|
+
];
|
|
32
|
+
const k = (0, keyframe_codec_1.encodeKeyframes)(frames, idx);
|
|
33
|
+
(0, globals_1.expect)(k).toEqual([[0, 10, 10], [], [0, 20, 20]]);
|
|
34
|
+
const dec = (0, keyframe_codec_1.decodeKeyframes)(k, roster);
|
|
35
|
+
(0, globals_1.expect)(dec[1]).toEqual([{ playerId: 'A', x: 1, y: 1 }]); // unchanged at the carry-forward frame
|
|
36
|
+
(0, globals_1.expect)(dec[2]).toEqual([{ playerId: 'A', x: 2, y: 2 }]);
|
|
37
|
+
});
|
|
38
|
+
(0, globals_1.it)('trims trailing empty frames', () => {
|
|
39
|
+
const frames = [[{ playerId: 'A', x: 1, y: 1 }], undefined, undefined];
|
|
40
|
+
(0, globals_1.expect)((0, keyframe_codec_1.encodeKeyframes)(frames, idx)).toEqual([[0, 10, 10]]);
|
|
41
|
+
});
|
|
42
|
+
(0, globals_1.it)('skips players absent from the roster index', () => {
|
|
43
|
+
const frames = [[{ playerId: 'A', x: 1, y: 1 }, { playerId: 'GHOST', x: 5, y: 5 }]];
|
|
44
|
+
(0, globals_1.expect)((0, keyframe_codec_1.encodeKeyframes)(frames, idx)).toEqual([[0, 10, 10]]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -6,13 +6,14 @@ const msgpackr_1 = require("msgpackr");
|
|
|
6
6
|
const zlib_1 = require("zlib");
|
|
7
7
|
const lz_string_1 = require("lz-string");
|
|
8
8
|
const service_1 = require("../../service");
|
|
9
|
+
const keyframe_codec_1 = require("./keyframe-codec");
|
|
9
10
|
const rally_event_1 = require("./rally-event");
|
|
10
11
|
const VERSION_BYTE = 0x01;
|
|
11
12
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
13
|
function deflate(data) { return (0, zlib_1.deflateRawSync)(data); }
|
|
13
14
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
15
|
function inflate(data) { return (0, zlib_1.inflateRawSync)(data); }
|
|
15
|
-
function buildRoster(events) {
|
|
16
|
+
function buildRoster(events, keyframes) {
|
|
16
17
|
const seen = [];
|
|
17
18
|
const index = new Map();
|
|
18
19
|
function add(id) {
|
|
@@ -33,14 +34,22 @@ function buildRoster(events) {
|
|
|
33
34
|
add(evt.playerOut);
|
|
34
35
|
}
|
|
35
36
|
}
|
|
37
|
+
// Keyframes include non-touching on-court players, so extend the roster to cover them (appended after the event
|
|
38
|
+
// players, which keep their indices, so the compact events are unaffected).
|
|
39
|
+
if (keyframes != null)
|
|
40
|
+
for (const kf of keyframes)
|
|
41
|
+
if (kf != null)
|
|
42
|
+
for (const p of kf)
|
|
43
|
+
add(p.playerId);
|
|
36
44
|
return index;
|
|
37
45
|
}
|
|
38
46
|
function transformToAttributes(rally, setId) {
|
|
39
|
-
const playerIndex = buildRoster(rally.events);
|
|
47
|
+
const playerIndex = buildRoster(rally.events, rally.keyframes);
|
|
40
48
|
const roster = Array.from(playerIndex.keys());
|
|
41
49
|
const payload = {
|
|
42
50
|
r: roster,
|
|
43
|
-
e: rally.events.map(evt => (0, rally_event_1.transformToCompact)(evt, playerIndex))
|
|
51
|
+
e: rally.events.map(evt => (0, rally_event_1.transformToCompact)(evt, playerIndex)),
|
|
52
|
+
...(rally.keyframes != null ? { k: (0, keyframe_codec_1.encodeKeyframes)(rally.keyframes, playerIndex) } : {})
|
|
44
53
|
};
|
|
45
54
|
const compressed = deflate((0, msgpackr_1.pack)(payload));
|
|
46
55
|
const events = Buffer.from([VERSION_BYTE, ...compressed]);
|
|
@@ -81,9 +90,12 @@ function decodeLegacyEvent(event) {
|
|
|
81
90
|
function transformToObject(model) {
|
|
82
91
|
const raw = model.events;
|
|
83
92
|
let events;
|
|
93
|
+
let keyframes;
|
|
84
94
|
if (raw[0] === VERSION_BYTE) {
|
|
85
95
|
const payload = (0, msgpackr_1.unpack)(inflate(raw.subarray(1)));
|
|
86
96
|
events = payload.e.map(evt => decodeCompactEvent(evt, payload.r));
|
|
97
|
+
if (payload.k != null)
|
|
98
|
+
keyframes = (0, keyframe_codec_1.decodeKeyframes)(payload.k, payload.r);
|
|
87
99
|
}
|
|
88
100
|
else {
|
|
89
101
|
// Legacy: lz-string base64 text stored as UTF-8 bytes in the bytea column
|
|
@@ -97,6 +109,7 @@ function transformToObject(model) {
|
|
|
97
109
|
id: model.rally_id,
|
|
98
110
|
order: model.order,
|
|
99
111
|
servingTeamId: model.serving_team,
|
|
100
|
-
events
|
|
112
|
+
events,
|
|
113
|
+
keyframes
|
|
101
114
|
});
|
|
102
115
|
}
|
|
@@ -60,3 +60,36 @@ const rally_1 = require("./rally");
|
|
|
60
60
|
(0, globals_1.expect)(free.failure).toBe(service_1.FreeBallFailureEnum.NO_FAILURE);
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
|
+
(0, globals_1.describe)('rally compact round trip — position keyframes', () => {
|
|
64
|
+
const pA = (0, uuid_1.v4)(); // touches (serve)
|
|
65
|
+
const pB = (0, uuid_1.v4)(); // touches (reception)
|
|
66
|
+
const pC = (0, uuid_1.v4)(); // NON-touching: only appears in keyframes, so the roster must be extended to cover it
|
|
67
|
+
const setId = (0, uuid_1.v4)();
|
|
68
|
+
const events = [
|
|
69
|
+
service_1.Serve.create({ playerId: pA, target: 6, score: 55, failure: service_1.ServeFailureEnum.NO_FAILURE, type: service_1.ServeTypeEnum.JUMP_TOPSPIN }),
|
|
70
|
+
service_1.Reception.create({ playerId: pB, target: 5, score: 50, failure: service_1.ReceptionFailureEnum.NO_FAILURE, type: service_1.ReceptionTypeEnum.DIG })
|
|
71
|
+
];
|
|
72
|
+
const keyframes = [
|
|
73
|
+
[{ playerId: pA, x: 1.23, y: -2.34 }, { playerId: pB, x: 3, y: 4.5 }, { playerId: pC, x: -4, y: 8 }],
|
|
74
|
+
[{ playerId: pA, x: 1.23, y: -2.34 }, { playerId: pB, x: -1, y: 2 }, { playerId: pC, x: -4, y: 8 }] // only pB moved
|
|
75
|
+
];
|
|
76
|
+
const rally = service_1.Rally.create({ id: (0, uuid_1.v4)(), order: 0, servingTeamId: (0, uuid_1.v4)(), events, keyframes });
|
|
77
|
+
(0, globals_1.it)('encodes then decodes the keyframes through the bytea (quantized to 0.1 m, non-toucher included)', () => {
|
|
78
|
+
const decoded = (0, rally_1.transformToRally)((0, rally_1.transformFromRally)(rally, setId));
|
|
79
|
+
const kfs = decoded.keyframes ?? [];
|
|
80
|
+
(0, globals_1.expect)(kfs).toHaveLength(2);
|
|
81
|
+
const f0 = new Map(kfs[0].map(p => [p.playerId, p]));
|
|
82
|
+
(0, globals_1.expect)(f0.get(pA)).toEqual({ playerId: pA, x: 1.2, y: -2.3 });
|
|
83
|
+
(0, globals_1.expect)(f0.get(pB)).toEqual({ playerId: pB, x: 3, y: 4.5 });
|
|
84
|
+
(0, globals_1.expect)(f0.get(pC)).toEqual({ playerId: pC, x: -4, y: 8 }); // the non-touching player survived the roster extension
|
|
85
|
+
const f1 = new Map(kfs[1].map(p => [p.playerId, p]));
|
|
86
|
+
(0, globals_1.expect)(f1.get(pA)).toEqual({ playerId: pA, x: 1.2, y: -2.3 }); // carried forward (unchanged)
|
|
87
|
+
(0, globals_1.expect)(f1.get(pB)).toEqual({ playerId: pB, x: -1, y: 2 });
|
|
88
|
+
(0, globals_1.expect)(f1.get(pC)).toEqual({ playerId: pC, x: -4, y: 8 }); // carried forward
|
|
89
|
+
});
|
|
90
|
+
(0, globals_1.it)('omits the keyframe block entirely for a rally without keyframes', () => {
|
|
91
|
+
const plain = service_1.Rally.create({ id: (0, uuid_1.v4)(), order: 0, servingTeamId: (0, uuid_1.v4)(), events });
|
|
92
|
+
const decoded = (0, rally_1.transformToRally)((0, rally_1.transformFromRally)(plain, setId));
|
|
93
|
+
(0, globals_1.expect)(decoded.keyframes).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -4,11 +4,17 @@ export interface PlayerPosition {
|
|
|
4
4
|
position: CourtPosition;
|
|
5
5
|
playerId: string;
|
|
6
6
|
}
|
|
7
|
+
export interface KeyframePosition {
|
|
8
|
+
readonly playerId: string;
|
|
9
|
+
readonly x: number;
|
|
10
|
+
readonly y: number;
|
|
11
|
+
}
|
|
7
12
|
export declare class Rally {
|
|
8
13
|
readonly id: string;
|
|
9
14
|
readonly servingTeamId: string;
|
|
10
15
|
readonly events: RallyEvent[];
|
|
11
16
|
readonly order: number;
|
|
17
|
+
readonly keyframes?: KeyframePosition[][];
|
|
12
18
|
static create(input: unknown): Rally;
|
|
13
19
|
private constructor();
|
|
14
20
|
addEvent(event: RallyEvent): void;
|
|
@@ -15,11 +15,12 @@ class Rally {
|
|
|
15
15
|
}
|
|
16
16
|
return new Rally(result.data);
|
|
17
17
|
}
|
|
18
|
-
constructor({ id, order, servingTeamId, events }) {
|
|
18
|
+
constructor({ id, order, servingTeamId, events, keyframes }) {
|
|
19
19
|
this.id = id;
|
|
20
20
|
this.order = order;
|
|
21
21
|
this.servingTeamId = servingTeamId;
|
|
22
22
|
this.events = events;
|
|
23
|
+
this.keyframes = keyframes;
|
|
23
24
|
}
|
|
24
25
|
addEvent(event) {
|
|
25
26
|
this.events.push(event);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { EventCoord } from '../event/in-play-event';
|
|
2
|
+
export declare const COURT_BASELINE = 9;
|
|
3
|
+
export declare const COURT_HALF_WIDTH = 4.5;
|
|
4
|
+
export type DefendSign = -1 | 1;
|
|
5
|
+
export declare function defendSign(defenderIsHome: boolean): DefendSign;
|
|
6
|
+
export declare function blockCenter(attackSpot: EventCoord, side: DefendSign): EventCoord;
|
|
7
|
+
export declare function joinedBlockerSpots(center: EventCoord, count: number): EventCoord[];
|
|
8
|
+
export declare function failedBlockerSpot(homeNetX: number, center: EventCoord, reach: number): EventCoord;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COURT_HALF_WIDTH = exports.COURT_BASELINE = void 0;
|
|
4
|
+
exports.defendSign = defendSign;
|
|
5
|
+
exports.blockCenter = blockCenter;
|
|
6
|
+
exports.joinedBlockerSpots = joinedBlockerSpots;
|
|
7
|
+
exports.failedBlockerSpot = failedBlockerSpot;
|
|
8
|
+
// A back line sits at |y| = 9; the wall stands just off the net on the defending side; the court is 9 m wide.
|
|
9
|
+
exports.COURT_BASELINE = 9;
|
|
10
|
+
exports.COURT_HALF_WIDTH = 4.5;
|
|
11
|
+
const NET_STANDOFF = 0.3; // the block sits this far onto the defending side of the net
|
|
12
|
+
const WALL_ANTENNA = exports.COURT_HALF_WIDTH - 0.1; // keep a drawn token inside the antennas
|
|
13
|
+
const WALL_GAP = 0.75; // side-by-side spacing of blockers in the wall (~shoulder width; also keeps drawn tokens from overlapping)
|
|
14
|
+
function defendSign(defenderIsHome) {
|
|
15
|
+
return defenderIsHome ? -1 : 1;
|
|
16
|
+
}
|
|
17
|
+
function clampX(x) {
|
|
18
|
+
return Math.max(-WALL_ANTENNA, Math.min(WALL_ANTENNA, x));
|
|
19
|
+
}
|
|
20
|
+
// The block group CENTER for an attack (owner 2026-08-11): draw the sight line from the DEFENDING back-line center
|
|
21
|
+
// (0, side*9) to the attack spot, and take where it crosses the net. Because the line pivots on the back-line center,
|
|
22
|
+
// a wider or deeper attack pulls the block off the straight-in-front line toward the middle (it takes the line and
|
|
23
|
+
// leaves the cross open), while a straight-on attack sits the block dead in front.
|
|
24
|
+
function blockCenter(attackSpot, side) {
|
|
25
|
+
const toNet = exports.COURT_BASELINE / (exports.COURT_BASELINE + Math.abs(attackSpot.y)); // fraction of the line from back line to net
|
|
26
|
+
return { x: clampX(attackSpot.x * toNet), y: side * NET_STANDOFF };
|
|
27
|
+
}
|
|
28
|
+
// The joined blockers stand side by side around the block center along the net. Order the caller's blockers by their
|
|
29
|
+
// base lateral position and hand each the matching slot.
|
|
30
|
+
function joinedBlockerSpots(center, count) {
|
|
31
|
+
const spots = [];
|
|
32
|
+
for (let i = 0; i < count; i++) {
|
|
33
|
+
spots.push({ x: clampX(center.x + (i - (count - 1) / 2) * WALL_GAP), y: center.y });
|
|
34
|
+
}
|
|
35
|
+
return spots;
|
|
36
|
+
}
|
|
37
|
+
// A candidate blocker that FAILED its join roll (owner 2026-08-11): if the block spot is within its reach it is drawn
|
|
38
|
+
// where it meant to block; otherwise it is caught out and drawn at the EDGE of its reach on the line toward the block
|
|
39
|
+
// spot. `homeNetX` = the blocker's own net-lane position (its base lateral coord). `reach` = how far along the net it
|
|
40
|
+
// can travel (the sim block model's reflex reach, passed in so this module carries no sim dependency).
|
|
41
|
+
function failedBlockerSpot(homeNetX, center, reach) {
|
|
42
|
+
const dx = center.x - homeNetX;
|
|
43
|
+
const step = Math.max(-reach, Math.min(reach, dx));
|
|
44
|
+
return { x: clampX(homeNetX + step), y: center.y };
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const replay_geometry_1 = require("./replay-geometry");
|
|
5
|
+
(0, globals_1.describe)('defendSign', () => {
|
|
6
|
+
(0, globals_1.it)('maps HOME to -1 (own court y<0) and AWAY to +1', () => {
|
|
7
|
+
(0, globals_1.expect)((0, replay_geometry_1.defendSign)(true)).toBe(-1);
|
|
8
|
+
(0, globals_1.expect)((0, replay_geometry_1.defendSign)(false)).toBe(1);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
(0, globals_1.describe)('blockCenter', () => {
|
|
12
|
+
(0, globals_1.it)('a straight-on attack (x=0) sits the block dead in front', () => {
|
|
13
|
+
const c = (0, replay_geometry_1.blockCenter)({ x: 0, y: 3 }, -1);
|
|
14
|
+
(0, globals_1.expect)(c.x).toBeCloseTo(0, 6);
|
|
15
|
+
(0, globals_1.expect)(c.y).toBeCloseTo(-0.3, 6); // on the defending (HOME) side of the net
|
|
16
|
+
});
|
|
17
|
+
(0, globals_1.it)('a front-row attack (y=3) draws the block from the back-line center', () => {
|
|
18
|
+
// toNet = 9 / (9 + 3) = 0.75 -> x = 4 * 0.75 = 3.0
|
|
19
|
+
const c = (0, replay_geometry_1.blockCenter)({ x: 4, y: 3 }, -1);
|
|
20
|
+
(0, globals_1.expect)(c.x).toBeCloseTo(3, 6);
|
|
21
|
+
(0, globals_1.expect)(c.y).toBeCloseTo(-0.3, 6);
|
|
22
|
+
});
|
|
23
|
+
(0, globals_1.it)('a deeper attack pulls the block further toward the middle (covers the cross)', () => {
|
|
24
|
+
const shallow = (0, replay_geometry_1.blockCenter)({ x: 4, y: 3 }, -1).x; // 3.0
|
|
25
|
+
const deep = (0, replay_geometry_1.blockCenter)({ x: 4, y: 9 }, -1).x; // 9/18 = 0.5 -> 2.0
|
|
26
|
+
(0, globals_1.expect)(deep).toBeCloseTo(2, 6);
|
|
27
|
+
(0, globals_1.expect)(deep).toBeLessThan(shallow); // deeper attack -> block closer to center
|
|
28
|
+
});
|
|
29
|
+
(0, globals_1.it)('mirrors for an AWAY defender (attacker on the y<0 side)', () => {
|
|
30
|
+
const c = (0, replay_geometry_1.blockCenter)({ x: -4, y: -3 }, 1);
|
|
31
|
+
(0, globals_1.expect)(c.x).toBeCloseTo(-3, 6);
|
|
32
|
+
(0, globals_1.expect)(c.y).toBeCloseTo(0.3, 6); // on the defending (AWAY) side of the net
|
|
33
|
+
});
|
|
34
|
+
(0, globals_1.it)('clamps the center inside the antennas', () => {
|
|
35
|
+
const c = (0, replay_geometry_1.blockCenter)({ x: 4.5, y: 0 }, -1); // toNet = 1 -> x would be 4.5
|
|
36
|
+
(0, globals_1.expect)(c.x).toBeCloseTo(4.4, 6);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
(0, globals_1.describe)('joinedBlockerSpots', () => {
|
|
40
|
+
const center = { x: 3, y: -0.3 };
|
|
41
|
+
(0, globals_1.it)('a single blocker sits on the center', () => {
|
|
42
|
+
const [a] = (0, replay_geometry_1.joinedBlockerSpots)(center, 1);
|
|
43
|
+
(0, globals_1.expect)(a.x).toBeCloseTo(3, 6);
|
|
44
|
+
(0, globals_1.expect)(a.y).toBeCloseTo(-0.3, 6);
|
|
45
|
+
});
|
|
46
|
+
(0, globals_1.it)('a double straddles the center by half a gap each', () => {
|
|
47
|
+
const [a, b] = (0, replay_geometry_1.joinedBlockerSpots)(center, 2);
|
|
48
|
+
(0, globals_1.expect)(a.x).toBeCloseTo(2.625, 6);
|
|
49
|
+
(0, globals_1.expect)(b.x).toBeCloseTo(3.375, 6);
|
|
50
|
+
});
|
|
51
|
+
(0, globals_1.it)('a triple spreads one gap apart around the center', () => {
|
|
52
|
+
const [a, b, c] = (0, replay_geometry_1.joinedBlockerSpots)(center, 3);
|
|
53
|
+
(0, globals_1.expect)(a.x).toBeCloseTo(2.25, 6);
|
|
54
|
+
(0, globals_1.expect)(b.x).toBeCloseTo(3, 6);
|
|
55
|
+
(0, globals_1.expect)(c.x).toBeCloseTo(3.75, 6);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
(0, globals_1.describe)('failedBlockerSpot', () => {
|
|
59
|
+
const center = { x: 3, y: -0.3 };
|
|
60
|
+
(0, globals_1.it)('within reach: drawn at the block spot it meant to reach', () => {
|
|
61
|
+
const s = (0, replay_geometry_1.failedBlockerSpot)(0, center, 5); // dx = 3, reach 5 -> reaches the center
|
|
62
|
+
(0, globals_1.expect)(s.x).toBeCloseTo(3, 6);
|
|
63
|
+
(0, globals_1.expect)(s.y).toBeCloseTo(-0.3, 6);
|
|
64
|
+
});
|
|
65
|
+
(0, globals_1.it)('out of reach: drawn at the edge of its reach toward the block spot', () => {
|
|
66
|
+
const s = (0, replay_geometry_1.failedBlockerSpot)(0, center, 1); // dx = 3, reach 1 -> stops 1 m along
|
|
67
|
+
(0, globals_1.expect)(s.x).toBeCloseTo(1, 6);
|
|
68
|
+
});
|
|
69
|
+
(0, globals_1.it)('moves left toward a block spot on its left', () => {
|
|
70
|
+
const s = (0, replay_geometry_1.failedBlockerSpot)(4, center, 0.5); // dx = -1, reach 0.5 -> 4 - 0.5 = 3.5
|
|
71
|
+
(0, globals_1.expect)(s.x).toBeCloseTo(3.5, 6);
|
|
72
|
+
});
|
|
73
|
+
(0, globals_1.it)('clamps inside the antennas', () => {
|
|
74
|
+
const s = (0, replay_geometry_1.failedBlockerSpot)(4.3, { x: 4.5, y: -0.3 }, 1); // 4.3 + 0.2 = 4.5 -> clamped
|
|
75
|
+
(0, globals_1.expect)(s.x).toBeCloseTo(4.4, 6);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -5,5 +5,10 @@ export declare const RallyInputSchema: z.ZodObject<{
|
|
|
5
5
|
order: z.ZodNumber;
|
|
6
6
|
servingTeamId: z.ZodUUID;
|
|
7
7
|
events: z.ZodArray<z.ZodCustom<RallyEvent, RallyEvent>>;
|
|
8
|
+
keyframes: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
9
|
+
playerId: z.ZodString;
|
|
10
|
+
x: z.ZodNumber;
|
|
11
|
+
y: z.ZodNumber;
|
|
12
|
+
}, z.core.$strip>>>>;
|
|
8
13
|
}, z.core.$strip>;
|
|
9
14
|
export type RallyInput = z.infer<typeof RallyInputSchema>;
|
|
@@ -10,5 +10,8 @@ exports.RallyInputSchema = zod_1.z.object({
|
|
|
10
10
|
servingTeamId: zod_1.z.uuid(),
|
|
11
11
|
events: zod_1.z.array(zod_1.z.custom((v) => v instanceof event_1.RallyEvent, {
|
|
12
12
|
message: 'INVALID_RALLY_EVENT_INSTANCE'
|
|
13
|
-
}))
|
|
13
|
+
})),
|
|
14
|
+
// Optional replay position keyframes (owner 2026-08-11), aligned by index to `events`: keyframes[i] = every
|
|
15
|
+
// on-court player's coord at events[i]. Present only for captured (user, non-event) matches; visualization only.
|
|
16
|
+
keyframes: zod_1.z.array(zod_1.z.array(zod_1.z.object({ playerId: zod_1.z.string(), x: zod_1.z.number(), y: zod_1.z.number() }))).optional()
|
|
14
17
|
});
|
|
@@ -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<{
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface KeyframeCoord {
|
|
2
|
+
readonly playerId: string;
|
|
3
|
+
readonly x: number;
|
|
4
|
+
readonly y: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function encodeKeyframes(keyframes: Array<KeyframeCoord[] | undefined>, playerIndex: Map<string, number>): number[][];
|
|
7
|
+
export declare function decodeKeyframes(k: number[][], roster: string[]): KeyframeCoord[][];
|