volleyballsimtypes 0.0.504 → 0.0.506
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/api/index.d.ts +3 -2
- package/dist/cjs/src/api/tactics-player-refs.js +2 -2
- package/dist/cjs/src/data/init-models.js +4 -0
- package/dist/cjs/src/data/models/base-position-preset.d.ts +22 -0
- package/dist/cjs/src/data/models/base-position-preset.js +54 -0
- package/dist/cjs/src/data/models/index.d.ts +1 -0
- package/dist/cjs/src/data/models/index.js +1 -0
- package/dist/cjs/src/data/models/tactics.d.ts +5 -3
- package/dist/cjs/src/data/models/tactics.js +4 -0
- package/dist/cjs/src/data/transformers/rally-event-injury.test.js +1 -1
- package/dist/cjs/src/data/transformers/rally-event-positional.test.js +10 -2
- package/dist/cjs/src/data/transformers/rally-event.js +3 -1
- package/dist/cjs/src/data/transformers/rally.test.js +2 -2
- package/dist/cjs/src/data/transformers/tactics.js +4 -0
- package/dist/cjs/src/data/transformers/tactics.test.js +34 -10
- package/dist/cjs/src/service/competition/competition-rewards.d.ts +20 -0
- package/dist/cjs/src/service/competition/competition-rewards.js +47 -0
- package/dist/cjs/src/service/competition/competition-rewards.test.d.ts +1 -0
- package/dist/cjs/src/service/competition/competition-rewards.test.js +32 -0
- package/dist/cjs/src/service/competition/index.d.ts +2 -0
- package/dist/cjs/src/service/competition/index.js +2 -0
- package/dist/cjs/src/service/competition/league-rewards.d.ts +21 -0
- package/dist/cjs/src/service/competition/league-rewards.js +58 -0
- package/dist/cjs/src/service/competition/league-rewards.test.d.ts +1 -0
- package/dist/cjs/src/service/competition/league-rewards.test.js +50 -0
- package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
- package/dist/cjs/src/service/event/spike.d.ts +2 -4
- package/dist/cjs/src/service/event/spike.js +5 -3
- package/dist/cjs/src/service/team/base-config.d.ts +10 -0
- package/dist/cjs/src/service/team/base-config.js +14 -0
- package/dist/cjs/src/service/team/blocking-preference.d.ts +3 -1
- package/dist/cjs/src/service/team/blocking-preference.js +2 -1
- package/dist/cjs/src/service/team/default-base-config.d.ts +4 -0
- package/dist/cjs/src/service/team/default-base-config.js +128 -0
- package/dist/cjs/src/service/team/index.d.ts +2 -0
- package/dist/cjs/src/service/team/index.js +2 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +17 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.js +24 -4
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +29 -16
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +17 -2
- package/dist/cjs/src/service/team/tactics.d.ts +3 -0
- package/dist/cjs/src/service/team/tactics.js +2 -1
- package/dist/cjs/src/service/team/team-generator.js +6 -1
- package/dist/esm/src/api/index.d.ts +3 -2
- package/dist/esm/src/api/tactics-player-refs.js +2 -2
- package/dist/esm/src/data/init-models.js +5 -1
- package/dist/esm/src/data/models/base-position-preset.d.ts +22 -0
- package/dist/esm/src/data/models/base-position-preset.js +50 -0
- package/dist/esm/src/data/models/index.d.ts +1 -0
- package/dist/esm/src/data/models/index.js +1 -0
- package/dist/esm/src/data/models/tactics.d.ts +5 -3
- package/dist/esm/src/data/models/tactics.js +4 -0
- package/dist/esm/src/data/transformers/rally-event-injury.test.js +1 -1
- package/dist/esm/src/data/transformers/rally-event-positional.test.js +10 -2
- package/dist/esm/src/data/transformers/rally-event.js +3 -1
- package/dist/esm/src/data/transformers/rally.test.js +2 -2
- package/dist/esm/src/data/transformers/tactics.js +4 -0
- package/dist/esm/src/data/transformers/tactics.test.js +34 -10
- package/dist/esm/src/service/competition/competition-rewards.d.ts +20 -0
- package/dist/esm/src/service/competition/competition-rewards.js +43 -0
- package/dist/esm/src/service/competition/competition-rewards.test.d.ts +1 -0
- package/dist/esm/src/service/competition/competition-rewards.test.js +30 -0
- package/dist/esm/src/service/competition/index.d.ts +2 -0
- package/dist/esm/src/service/competition/index.js +2 -0
- package/dist/esm/src/service/competition/league-rewards.d.ts +21 -0
- package/dist/esm/src/service/competition/league-rewards.js +53 -0
- package/dist/esm/src/service/competition/league-rewards.test.d.ts +1 -0
- package/dist/esm/src/service/competition/league-rewards.test.js +48 -0
- package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
- package/dist/esm/src/service/event/spike.d.ts +2 -4
- package/dist/esm/src/service/event/spike.js +5 -3
- package/dist/esm/src/service/team/base-config.d.ts +10 -0
- package/dist/esm/src/service/team/base-config.js +11 -0
- package/dist/esm/src/service/team/blocking-preference.d.ts +3 -1
- package/dist/esm/src/service/team/blocking-preference.js +1 -0
- package/dist/esm/src/service/team/default-base-config.d.ts +4 -0
- package/dist/esm/src/service/team/default-base-config.js +124 -0
- package/dist/esm/src/service/team/index.d.ts +2 -0
- package/dist/esm/src/service/team/index.js +2 -0
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +17 -2
- package/dist/esm/src/service/team/schemas/tactics.z.js +24 -4
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +29 -16
- package/dist/esm/src/service/team/schemas/team.z.d.ts +17 -2
- package/dist/esm/src/service/team/tactics.d.ts +3 -0
- package/dist/esm/src/service/team/tactics.js +2 -1
- package/dist/esm/src/service/team/team-generator.js +6 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { leaguePayoutFor, leagueRewardTier, LEAGUE_REWARDS } from './league-rewards';
|
|
3
|
+
// The cumulative per-tier totals the owner specified (2026-08-07). Every entry is the coins a team finishing at
|
|
4
|
+
// that position earns, summing its band and every band below it. Positions past the lowest band earn 0.
|
|
5
|
+
describe('leaguePayoutFor', () => {
|
|
6
|
+
const TIER1 = {
|
|
7
|
+
1: 3000, 2: 2500, 3: 2250, 4: 2000, 5: 1500, 6: 1500, 7: 1500, 8: 1500, 9: 1000, 10: 1000, 11: 1000, 12: 1000, 13: 0, 14: 0, 15: 0, 16: 0
|
|
8
|
+
};
|
|
9
|
+
const TIER2 = {
|
|
10
|
+
1: 1500, 2: 1250, 3: 1000, 4: 1000, 5: 500, 6: 500, 7: 500, 8: 500, 9: 0, 12: 0, 16: 0
|
|
11
|
+
};
|
|
12
|
+
const TIER3 = {
|
|
13
|
+
1: 1000, 2: 750, 3: 500, 4: 500, 5: 0, 8: 0, 16: 0
|
|
14
|
+
};
|
|
15
|
+
it('matches the owner cumulative totals for tier 1', () => {
|
|
16
|
+
for (const [pos, total] of Object.entries(TIER1)) {
|
|
17
|
+
expect(leaguePayoutFor(1, Number(pos))).toBe(total);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
it('matches the owner cumulative totals for tier 2', () => {
|
|
21
|
+
for (const [pos, total] of Object.entries(TIER2)) {
|
|
22
|
+
expect(leaguePayoutFor(2, Number(pos))).toBe(total);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
it('matches the owner cumulative totals for tier 3', () => {
|
|
26
|
+
for (const [pos, total] of Object.entries(TIER3)) {
|
|
27
|
+
expect(leaguePayoutFor(3, Number(pos))).toBe(total);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
it('treats division 3 and lower with the tier-3 table', () => {
|
|
31
|
+
for (const tier of [3, 4, 5, 10]) {
|
|
32
|
+
expect(leagueRewardTier(tier)).toBe(3);
|
|
33
|
+
expect(leaguePayoutFor(tier, 1)).toBe(1000);
|
|
34
|
+
expect(leaguePayoutFor(tier, 4)).toBe(500);
|
|
35
|
+
expect(leaguePayoutFor(tier, 5)).toBe(0);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
it('clamps tier 0 or negative to tier 1', () => {
|
|
39
|
+
expect(leagueRewardTier(0)).toBe(1);
|
|
40
|
+
expect(leaguePayoutFor(0, 1)).toBe(3000);
|
|
41
|
+
});
|
|
42
|
+
it('each tier band table sums to the champion total', () => {
|
|
43
|
+
const sum = (t) => LEAGUE_REWARDS[t].reduce((s, r) => s + r.coins, 0);
|
|
44
|
+
expect(sum(1)).toBe(3000);
|
|
45
|
+
expect(sum(2)).toBe(1500);
|
|
46
|
+
expect(sum(3)).toBe(1000);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -2,7 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
3
|
import { EventIncidentSchema } from './incident.z';
|
|
4
4
|
import { positionalEventFields } from './positional.z';
|
|
5
|
-
|
|
5
|
+
// SPIKE(0), TIP(3), DOWN_BALL(4), OVERHAND(5), TOOL(6). 1/2 (legacy LINE_SHOT/REVERSE_CROSS) are retired; the
|
|
6
|
+
// codec (transformToSpike) maps them to 0 before this validates, so they never reach here.
|
|
7
|
+
const spikeTypeValues = [0, 3, 4, 5, 6];
|
|
6
8
|
const spikeFailureValues = [0, 1, 2, 3, 4];
|
|
7
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
8
10
|
export const SpikeInputSchema = z.object({
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { InPlayEvent } from './in-play-event';
|
|
2
2
|
export declare enum SpikeTypeEnum {
|
|
3
|
-
|
|
4
|
-
LINE_SHOT = 1,
|
|
5
|
-
REVERSE_CROSS = 2,
|
|
3
|
+
SPIKE = 0,
|
|
6
4
|
TIP = 3,
|
|
7
5
|
DOWN_BALL = 4,
|
|
8
6
|
OVERHAND = 5,
|
|
9
7
|
TOOL = 6
|
|
10
8
|
}
|
|
11
|
-
export type SpikeType = SpikeTypeEnum.
|
|
9
|
+
export type SpikeType = SpikeTypeEnum.SPIKE | SpikeTypeEnum.TIP | SpikeTypeEnum.DOWN_BALL | SpikeTypeEnum.OVERHAND | SpikeTypeEnum.TOOL;
|
|
12
10
|
export declare enum SpikeFailureEnum {
|
|
13
11
|
NO_FAILURE = 0,
|
|
14
12
|
FAULT = 1,
|
|
@@ -3,9 +3,11 @@ import { InPlayEvent } from './in-play-event';
|
|
|
3
3
|
import { SpikeInputSchema } from './schemas/spike.z';
|
|
4
4
|
export var SpikeTypeEnum;
|
|
5
5
|
(function (SpikeTypeEnum) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// A hard swing (owner 2026-08-03). The cross / line / reverse DIRECTION is not stored: it is fully derivable
|
|
7
|
+
// from the persisted contact + landing coordinates, so a single SPIKE value replaces the old CROSS_SHOT(0) /
|
|
8
|
+
// LINE_SHOT(1) / REVERSE_CROSS(2). 1 and 2 are retired (left as gaps); the codec maps any legacy 1/2 back to
|
|
9
|
+
// SPIKE(0), so existing (zone-engine) matches keep decoding.
|
|
10
|
+
SpikeTypeEnum[SpikeTypeEnum["SPIKE"] = 0] = "SPIKE";
|
|
9
11
|
SpikeTypeEnum[SpikeTypeEnum["TIP"] = 3] = "TIP";
|
|
10
12
|
SpikeTypeEnum[SpikeTypeEnum["DOWN_BALL"] = 4] = "DOWN_BALL";
|
|
11
13
|
SpikeTypeEnum[SpikeTypeEnum["OVERHAND"] = 5] = "OVERHAND";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type BaseFormation = 'SERVE' | 'RECEIVE' | 'DEFENCE' | 'DEF_L' | 'DEF_M' | 'DEF_R' | 'ATTACK';
|
|
2
|
+
export declare const BASE_FORMATIONS: readonly BaseFormation[];
|
|
3
|
+
export interface BaseCoord {
|
|
4
|
+
readonly d: number;
|
|
5
|
+
readonly r: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BaseConfig {
|
|
8
|
+
readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
|
|
9
|
+
readonly receive: Record<string, Record<string, boolean>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Per-team base-position configuration (owner 2026-08-03). Generalises the sim's hardcoded 5-1 preset into a
|
|
2
|
+
// system-agnostic, per-team shape: for each formation, rotation and rotational ZONE, where that zone's player
|
|
3
|
+
// stands, plus (serve-receive only) whether that zone's player passes the serve. Authored in the UI base editor
|
|
4
|
+
// (and populated from a picked default preset); consumed by the positional engine to place players. A team with no
|
|
5
|
+
// `basePositions` falls back to the default preset for its `rotationSystem`.
|
|
6
|
+
//
|
|
7
|
+
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The stored rotation convention is
|
|
8
|
+
// the ENGINE's (SETTER_ZONE_TO_ROTATION); the UI editor re-keys the mapper's setter-zone rotation to it on save.
|
|
9
|
+
// `coords[formation][rotation][zone]` is the standing coordinate for that formation; `receive[rotation][zone]` is
|
|
10
|
+
// the reception-participation flag (absent zone => default to participating).
|
|
11
|
+
export const BASE_FORMATIONS = ['SERVE', 'RECEIVE', 'DEFENCE', 'DEF_L', 'DEF_M', 'DEF_R', 'ATTACK'];
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { LineupFunction } from './lineup-function';
|
|
2
2
|
export declare const NO_BLOCK: "NO_BLOCK";
|
|
3
3
|
export type BlockingAssignment = LineupFunction | typeof NO_BLOCK;
|
|
4
|
+
export type DefenceFormation = 'DEF_L' | 'DEF_M' | 'DEF_R';
|
|
5
|
+
export declare const DEFENCE_FORMATIONS: readonly DefenceFormation[];
|
|
4
6
|
export interface BlockingPreference {
|
|
5
7
|
readonly rotation: number;
|
|
6
|
-
readonly targets: Record<string, BlockingAssignment
|
|
8
|
+
readonly targets: Record<DefenceFormation, Record<string, BlockingAssignment>>;
|
|
7
9
|
}
|
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
// roll are all suppressed, as is a dedicated-blocking commit. Sits in the same `targets` map as a commit
|
|
5
5
|
// assignment, so the two are mutually exclusive by construction: a player is reading, committed, or out.
|
|
6
6
|
export const NO_BLOCK = 'NO_BLOCK';
|
|
7
|
+
export const DEFENCE_FORMATIONS = ['DEF_L', 'DEF_M', 'DEF_R'];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BaseConfig } from './base-config';
|
|
2
|
+
import { RotationSystemEnum } from './rotation-system';
|
|
3
|
+
export declare function defaultReceiveMap(system: RotationSystemEnum): BaseConfig['receive'];
|
|
4
|
+
export declare function defaultBaseConfig(system: RotationSystemEnum): BaseConfig;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { slotFunctionAt } from './lineup-function';
|
|
2
|
+
import { RoleEnum } from '../player';
|
|
3
|
+
import { isBackRow } from '../match';
|
|
4
|
+
const COORDS = {
|
|
5
|
+
SERVE: {
|
|
6
|
+
1: { 1: { d: 9.90, r: 3.15 }, 2: { d: 0.30, r: -2.55 }, 3: { d: 0.40, r: 0.00 }, 4: { d: 0.30, r: 2.55 }, 5: { d: 6.90, r: -0.15 }, 6: { d: 5.20, r: -2.35 } },
|
|
7
|
+
2: { 1: { d: 10.10, r: 3.00 }, 2: { d: 0.45, r: 0.00 }, 3: { d: 0.55, r: 2.95 }, 4: { d: 0.45, r: -2.95 }, 5: { d: 5.20, r: -2.95 }, 6: { d: 5.25, r: 3.35 } },
|
|
8
|
+
3: { 1: { d: 10.45, r: -3.40 }, 2: { d: 0.35, r: 2.70 }, 3: { d: 0.25, r: -3.00 }, 4: { d: 0.30, r: 0.00 }, 5: { d: 5.35, r: 2.55 }, 6: { d: 6.90, r: -0.05 } },
|
|
9
|
+
4: { 1: { d: 10.50, r: 3.40 }, 2: { d: 0.35, r: -3.05 }, 3: { d: 0.45, r: 0.00 }, 4: { d: 0.45, r: 2.55 }, 5: { d: 7.25, r: 0.15 }, 6: { d: 6.25, r: -2.55 } },
|
|
10
|
+
5: { 1: { d: 10.05, r: 3.50 }, 2: { d: 0.30, r: 0.10 }, 3: { d: 0.25, r: 2.85 }, 4: { d: 0.25, r: -2.80 }, 5: { d: 5.10, r: -2.65 }, 6: { d: 5.15, r: 2.70 } },
|
|
11
|
+
6: { 1: { d: 10.30, r: -3.50 }, 2: { d: 0.65, r: 2.60 }, 3: { d: 0.50, r: -2.55 }, 4: { d: 0.55, r: 0.05 }, 5: { d: 6.45, r: 2.65 }, 6: { d: 6.90, r: 0.10 } }
|
|
12
|
+
},
|
|
13
|
+
RECEIVE: {
|
|
14
|
+
1: { 1: { d: 6.75, r: 2.85 }, 2: { d: 6.25, r: 2.05 }, 3: { d: 3.10, r: -3.65 }, 4: { d: 3.80, r: -4.10 }, 5: { d: 6.40, r: -2.80 }, 6: { d: 6.50, r: 0.00 } },
|
|
15
|
+
2: { 1: { d: 6.00, r: 2.50 }, 2: { d: 3.15, r: 4.00 }, 3: { d: 0.50, r: 0.40 }, 4: { d: 5.85, r: -2.85 }, 5: { d: 6.20, r: -0.05 }, 6: { d: 1.50, r: 0.65 } },
|
|
16
|
+
3: { 1: { d: 6.10, r: 2.55 }, 2: { d: 3.15, r: 4.10 }, 3: { d: 6.50, r: -2.80 }, 4: { d: 0.80, r: -3.70 }, 5: { d: 1.10, r: -1.00 }, 6: { d: 6.65, r: -0.10 } },
|
|
17
|
+
4: { 1: { d: 8.45, r: 3.20 }, 2: { d: 6.25, r: -2.85 }, 3: { d: 1.35, r: -3.70 }, 4: { d: 0.50, r: -4.05 }, 5: { d: 6.75, r: -0.35 }, 6: { d: 6.55, r: 2.40 } },
|
|
18
|
+
5: { 1: { d: 5.90, r: 2.65 }, 2: { d: 2.60, r: 3.90 }, 3: { d: 0.65, r: 1.50 }, 4: { d: 6.05, r: -3.05 }, 5: { d: 6.10, r: 0.10 }, 6: { d: 8.45, r: 0.65 } },
|
|
19
|
+
6: { 1: { d: 6.25, r: 2.60 }, 2: { d: 0.80, r: 1.50 }, 3: { d: 6.40, r: -2.60 }, 4: { d: 3.30, r: -4.30 }, 5: { d: 8.65, r: -2.15 }, 6: { d: 6.70, r: 0.00 } }
|
|
20
|
+
},
|
|
21
|
+
DEFENCE: {
|
|
22
|
+
1: { 1: { d: 5.95, r: 2.65 }, 2: { d: 0.55, r: -2.95 }, 3: { d: 0.50, r: -0.20 }, 4: { d: 0.45, r: 2.70 }, 5: { d: 6.30, r: -0.30 }, 6: { d: 5.90, r: -3.15 } },
|
|
23
|
+
2: { 1: { d: 6.30, r: -0.40 }, 2: { d: 0.05, r: -0.10 }, 3: { d: 0.05, r: 2.70 }, 4: { d: 0.05, r: -2.85 }, 5: { d: 5.50, r: -3.00 }, 6: { d: 5.55, r: 2.80 } },
|
|
24
|
+
3: { 1: { d: 5.25, r: -2.65 }, 2: { d: 0.05, r: 3.05 }, 3: { d: 0.05, r: -2.80 }, 4: { d: 0.05, r: 0.30 }, 5: { d: 5.35, r: 2.95 }, 6: { d: 6.45, r: 0.20 } },
|
|
25
|
+
4: { 1: { d: 6.00, r: 2.90 }, 2: { d: 0.35, r: -3.10 }, 3: { d: 0.25, r: -0.05 }, 4: { d: 0.35, r: 3.10 }, 5: { d: 6.35, r: -0.10 }, 6: { d: 6.00, r: -2.60 } },
|
|
26
|
+
5: { 1: { d: 6.30, r: 0.00 }, 2: { d: 0.10, r: -0.05 }, 3: { d: 0.05, r: 3.00 }, 4: { d: 0.05, r: -3.05 }, 5: { d: 5.90, r: -2.90 }, 6: { d: 6.15, r: 3.25 } },
|
|
27
|
+
6: { 1: { d: 5.70, r: -2.85 }, 2: { d: 0.55, r: 2.50 }, 3: { d: 0.50, r: -2.95 }, 4: { d: 0.50, r: -0.25 }, 5: { d: 5.80, r: 2.90 }, 6: { d: 6.95, r: 0.00 } }
|
|
28
|
+
},
|
|
29
|
+
DEF_L: {
|
|
30
|
+
1: { 1: { d: 5.80, r: 1.50 }, 2: { d: 0.05, r: -3.40 }, 3: { d: 0.05, r: -2.85 }, 4: { d: 1.80, r: 1.30 }, 5: { d: 8.00, r: 0.65 }, 6: { d: 4.50, r: -4.20 } },
|
|
31
|
+
2: { 1: { d: 7.95, r: 0.70 }, 2: { d: 0.05, r: -2.85 }, 3: { d: 1.80, r: 1.30 }, 4: { d: 0.10, r: -3.40 }, 5: { d: 4.50, r: -4.20 }, 6: { d: 5.80, r: 1.50 } },
|
|
32
|
+
3: { 1: { d: 0.05, r: -2.85 }, 2: { d: 1.80, r: 1.30 }, 3: { d: 0.05, r: -3.45 }, 4: { d: 4.50, r: -4.20 }, 5: { d: 5.80, r: 1.50 }, 6: { d: 8.05, r: 0.70 } },
|
|
33
|
+
4: { 1: { d: 5.85, r: 1.50 }, 2: { d: 0.05, r: -3.45 }, 3: { d: 4.50, r: -4.20 }, 4: { d: 1.80, r: 1.30 }, 5: { d: 8.00, r: 0.95 }, 6: { d: 0.05, r: -2.85 } },
|
|
34
|
+
5: { 1: { d: 8.10, r: 0.80 }, 2: { d: 4.50, r: -4.20 }, 3: { d: 1.80, r: 1.30 }, 4: { d: 0.05, r: -3.45 }, 5: { d: 0.05, r: -2.85 }, 6: { d: 5.85, r: 1.50 } },
|
|
35
|
+
6: { 1: { d: 4.50, r: -4.20 }, 2: { d: 1.80, r: 1.30 }, 3: { d: 0.05, r: -3.45 }, 4: { d: 0.05, r: -2.85 }, 5: { d: 5.85, r: 1.50 }, 6: { d: 8.10, r: 0.80 } }
|
|
36
|
+
},
|
|
37
|
+
DEF_M: {
|
|
38
|
+
1: { 1: { d: 4.95, r: 2.35 }, 2: { d: 1.70, r: -2.60 }, 3: { d: 0.05, r: -0.15 }, 4: { d: 2.00, r: 2.80 }, 5: { d: 7.20, r: 1.00 }, 6: { d: 4.70, r: -2.35 } },
|
|
39
|
+
2: { 1: { d: 7.15, r: 1.00 }, 2: { d: 0.05, r: -0.15 }, 3: { d: 2.00, r: 2.80 }, 4: { d: 1.70, r: -2.60 }, 5: { d: 4.70, r: -2.35 }, 6: { d: 4.95, r: 2.35 } },
|
|
40
|
+
3: { 1: { d: 4.95, r: -2.35 }, 2: { d: 2.00, r: 2.80 }, 3: { d: 2.35, r: -2.65 }, 4: { d: 0.05, r: 0.00 }, 5: { d: 4.95, r: 2.35 }, 6: { d: 7.00, r: 1.20 } },
|
|
41
|
+
4: { 1: { d: 4.95, r: 2.15 }, 2: { d: 1.70, r: -2.60 }, 3: { d: 0.15, r: -0.10 }, 4: { d: 2.00, r: 2.85 }, 5: { d: 7.15, r: 1.00 }, 6: { d: 4.95, r: -2.25 } },
|
|
42
|
+
5: { 1: { d: 7.25, r: 1.10 }, 2: { d: 0.05, r: 0.00 }, 3: { d: 1.95, r: 2.60 }, 4: { d: 1.70, r: -2.50 }, 5: { d: 4.85, r: -2.05 }, 6: { d: 5.00, r: 2.15 } },
|
|
43
|
+
6: { 1: { d: 4.70, r: -2.35 }, 2: { d: 2.00, r: 2.75 }, 3: { d: 1.80, r: -2.65 }, 4: { d: 0.05, r: -0.15 }, 5: { d: 4.80, r: 2.40 }, 6: { d: 7.40, r: 1.00 } }
|
|
44
|
+
},
|
|
45
|
+
DEF_R: {
|
|
46
|
+
1: { 1: { d: 4.45, r: 4.20 }, 2: { d: 1.90, r: -1.50 }, 3: { d: 0.10, r: 2.90 }, 4: { d: 0.10, r: 3.45 }, 5: { d: 8.80, r: 0.45 }, 6: { d: 5.25, r: -1.45 } },
|
|
47
|
+
2: { 1: { d: 8.55, r: -0.65 }, 2: { d: 0.05, r: 2.90 }, 3: { d: 0.05, r: 3.50 }, 4: { d: 2.00, r: -2.20 }, 5: { d: 4.80, r: -1.60 }, 6: { d: 4.50, r: 4.15 } },
|
|
48
|
+
3: { 1: { d: 4.75, r: -1.70 }, 2: { d: 0.05, r: 3.45 }, 3: { d: 1.60, r: -1.75 }, 4: { d: 0.05, r: 2.85 }, 5: { d: 4.55, r: 4.15 }, 6: { d: 8.40, r: -0.35 } },
|
|
49
|
+
4: { 1: { d: 5.20, r: 4.15 }, 2: { d: 1.75, r: -1.85 }, 3: { d: 0.05, r: 3.20 }, 4: { d: 0.05, r: 3.75 }, 5: { d: 8.45, r: -0.65 }, 6: { d: 5.15, r: -1.85 } },
|
|
50
|
+
5: { 1: { d: 8.35, r: -0.75 }, 2: { d: 0.05, r: 2.90 }, 3: { d: 0.05, r: 3.50 }, 4: { d: 1.80, r: -1.25 }, 5: { d: 5.25, r: -1.70 }, 6: { d: 5.00, r: 4.15 } },
|
|
51
|
+
6: { 1: { d: 5.15, r: -1.50 }, 2: { d: 0.05, r: 3.45 }, 3: { d: 2.55, r: -2.40 }, 4: { d: 0.05, r: 2.90 }, 5: { d: 4.60, r: 4.10 }, 6: { d: 8.75, r: 0.25 } }
|
|
52
|
+
},
|
|
53
|
+
ATTACK: {
|
|
54
|
+
1: { 1: { d: 0.50, r: 1.55 }, 2: { d: 3.40, r: -5.15 }, 3: { d: 1.15, r: 0.40 }, 4: { d: 4.80, r: 5.20 }, 5: { d: 6.50, r: 0.20 }, 6: { d: 4.85, r: -2.90 } },
|
|
55
|
+
2: { 1: { d: 6.50, r: -0.40 }, 2: { d: 1.60, r: 0.75 }, 3: { d: 3.65, r: 5.05 }, 4: { d: 3.50, r: -5.05 }, 5: { d: 4.45, r: -2.60 }, 6: { d: 0.50, r: 1.60 } },
|
|
56
|
+
3: { 1: { d: 5.05, r: -1.90 }, 2: { d: 3.95, r: 5.00 }, 3: { d: 3.95, r: -4.90 }, 4: { d: 1.80, r: 1.10 }, 5: { d: 0.35, r: 1.55 }, 6: { d: 6.45, r: 0.15 } },
|
|
57
|
+
4: { 1: { d: 6.15, r: 4.95 }, 2: { d: 3.95, r: -5.05 }, 3: { d: 1.45, r: 1.05 }, 4: { d: 0.45, r: 1.80 }, 5: { d: 6.45, r: -0.50 }, 6: { d: 4.95, r: -2.00 } },
|
|
58
|
+
5: { 1: { d: 6.50, r: -0.40 }, 2: { d: 1.80, r: 1.10 }, 3: { d: 0.25, r: 1.85 }, 4: { d: 3.45, r: -4.95 }, 5: { d: 5.35, r: -2.55 }, 6: { d: 6.50, r: 4.90 } },
|
|
59
|
+
6: { 1: { d: 5.85, r: -2.20 }, 2: { d: 0.55, r: 2.00 }, 3: { d: 4.00, r: -4.85 }, 4: { d: 1.35, r: 1.40 }, 5: { d: 6.85, r: 4.90 }, 6: { d: 6.60, r: 0.35 } }
|
|
60
|
+
},
|
|
61
|
+
ATK_L: {
|
|
62
|
+
1: { 1: { d: 0.75, r: 0.10 }, 2: { d: 0.50, r: -4.05 }, 3: { d: 0.95, r: -2.55 }, 4: { d: 3.45, r: 0.80 }, 5: { d: 6.25, r: -0.75 }, 6: { d: 3.60, r: -3.95 } },
|
|
63
|
+
2: { 1: { d: 0.50, r: -4.05 }, 2: { d: 0.95, r: -2.55 }, 3: { d: 3.75, r: 1.05 }, 4: { d: 6.25, r: -0.75 }, 5: { d: 3.60, r: -3.95 }, 6: { d: 0.75, r: 0.10 } },
|
|
64
|
+
3: { 1: { d: 0.95, r: -2.55 }, 2: { d: 3.45, r: 0.80 }, 3: { d: 6.25, r: -0.75 }, 4: { d: 3.60, r: -3.95 }, 5: { d: 0.75, r: 0.10 }, 6: { d: 0.50, r: -4.05 } },
|
|
65
|
+
4: { 1: { d: 3.60, r: 0.90 }, 2: { d: 0.55, r: -4.05 }, 3: { d: 1.15, r: -2.60 }, 4: { d: 0.85, r: 0.25 }, 5: { d: 6.25, r: -0.80 }, 6: { d: 3.45, r: -4.10 } },
|
|
66
|
+
5: { 1: { d: 6.25, r: -0.65 }, 2: { d: 1.20, r: -2.55 }, 3: { d: 0.80, r: 0.10 }, 4: { d: 0.50, r: -4.10 }, 5: { d: 3.90, r: -4.00 }, 6: { d: 3.75, r: 0.80 } },
|
|
67
|
+
6: { 1: { d: 3.60, r: -3.95 }, 2: { d: 0.75, r: 0.10 }, 3: { d: 0.50, r: -4.05 }, 4: { d: 0.95, r: -2.55 }, 5: { d: 3.45, r: 0.80 }, 6: { d: 6.25, r: -0.75 } }
|
|
68
|
+
},
|
|
69
|
+
ATK_M: {
|
|
70
|
+
1: { 1: { d: 0.60, r: 1.25 }, 2: { d: 1.20, r: -2.00 }, 3: { d: 0.70, r: 0.50 }, 4: { d: 2.25, r: 2.60 }, 5: { d: 6.40, r: 1.40 }, 6: { d: 4.05, r: -1.55 } },
|
|
71
|
+
2: { 1: { d: 6.65, r: 1.55 }, 2: { d: 0.70, r: 0.50 }, 3: { d: 2.25, r: 2.60 }, 4: { d: 1.40, r: -1.90 }, 5: { d: 4.05, r: -1.55 }, 6: { d: 0.60, r: 1.25 } },
|
|
72
|
+
3: { 1: { d: 4.05, r: -1.55 }, 2: { d: 2.25, r: 2.60 }, 3: { d: 1.20, r: -2.00 }, 4: { d: 0.70, r: 0.45 }, 5: { d: 0.65, r: 1.15 }, 6: { d: 6.40, r: 1.40 } },
|
|
73
|
+
4: { 1: { d: 2.25, r: 2.60 }, 2: { d: 1.35, r: -1.90 }, 3: { d: 0.65, r: 0.50 }, 4: { d: 0.60, r: 1.25 }, 5: { d: 6.40, r: 1.45 }, 6: { d: 4.15, r: -1.45 } },
|
|
74
|
+
5: { 1: { d: 6.40, r: 1.40 }, 2: { d: 0.65, r: 0.50 }, 3: { d: 0.60, r: 1.25 }, 4: { d: 1.20, r: -2.00 }, 5: { d: 4.15, r: -1.40 }, 6: { d: 2.25, r: 2.60 } },
|
|
75
|
+
6: { 1: { d: 4.05, r: -1.55 }, 2: { d: 0.60, r: 1.25 }, 3: { d: 1.20, r: -2.00 }, 4: { d: 0.70, r: 0.50 }, 5: { d: 2.25, r: 2.60 }, 6: { d: 6.40, r: 1.40 } }
|
|
76
|
+
},
|
|
77
|
+
ATK_R: {
|
|
78
|
+
1: { 1: { d: 1.75, r: 2.60 }, 2: { d: 1.60, r: -1.75 }, 3: { d: 0.75, r: 1.80 }, 4: { d: 0.60, r: 3.90 }, 5: { d: 4.90, r: 3.25 }, 6: { d: 5.85, r: -1.90 } },
|
|
79
|
+
2: { 1: { d: 5.15, r: 3.20 }, 2: { d: 0.75, r: 1.80 }, 3: { d: 0.60, r: 3.90 }, 4: { d: 1.60, r: -1.40 }, 5: { d: 5.85, r: -1.90 }, 6: { d: 1.75, r: 2.60 } },
|
|
80
|
+
3: { 1: { d: 6.05, r: -2.00 }, 2: { d: 0.60, r: 3.90 }, 3: { d: 1.55, r: -1.55 }, 4: { d: 0.75, r: 1.70 }, 5: { d: 1.75, r: 2.60 }, 6: { d: 5.05, r: 3.15 } },
|
|
81
|
+
4: { 1: { d: 2.25, r: 4.20 }, 2: { d: 2.40, r: -1.65 }, 3: { d: 0.85, r: 1.40 }, 4: { d: 1.40, r: 2.30 }, 5: { d: 6.00, r: 1.85 }, 6: { d: 5.25, r: -0.90 } },
|
|
82
|
+
5: { 1: { d: 5.95, r: 2.35 }, 2: { d: 0.85, r: 1.40 }, 3: { d: 1.40, r: 2.30 }, 4: { d: 2.20, r: -1.80 }, 5: { d: 5.25, r: -0.90 }, 6: { d: 2.25, r: 4.20 } },
|
|
83
|
+
6: { 1: { d: 5.30, r: -0.90 }, 2: { d: 1.45, r: 2.30 }, 3: { d: 2.25, r: -1.80 }, 4: { d: 0.75, r: 1.40 }, 5: { d: 2.25, r: 4.05 }, 6: { d: 5.90, r: 2.35 } }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
// The default coordinate table as a BaseConfig.coords (jsonb string keys). Shared, readonly; materialized (and so
|
|
87
|
+
// serialized) per team on save, so sharing the reference in memory is safe.
|
|
88
|
+
const DEFAULT_COORDS = (() => {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [formation, rotations] of Object.entries(COORDS)) {
|
|
91
|
+
out[formation] = {};
|
|
92
|
+
for (const [rotation, zones] of Object.entries(rotations)) {
|
|
93
|
+
out[formation][rotation] = {};
|
|
94
|
+
for (const [zone, coord] of Object.entries(zones))
|
|
95
|
+
out[formation][rotation][zone] = { d: coord.d, r: coord.r };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
})();
|
|
100
|
+
// Rotation index -> the setter's rotational zone (inverse of SETTER_ZONE_TO_ROTATION {1:1,6:2,5:3,4:4,3:5,2:6}).
|
|
101
|
+
const ROTATION_TO_SETTER_ZONE = { 1: 1, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2 };
|
|
102
|
+
// The default serve-receive participation map: the standard 3-passer formation. A rotational slot passes iff its
|
|
103
|
+
// slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot). The setter, the
|
|
104
|
+
// opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2, so the
|
|
105
|
+
// second setter is excluded too, giving the same 3-passer map for every system.
|
|
106
|
+
export function defaultReceiveMap(system) {
|
|
107
|
+
const receive = {};
|
|
108
|
+
for (let rotation = 1; rotation <= 6; rotation++) {
|
|
109
|
+
const setterZone = ROTATION_TO_SETTER_ZONE[rotation];
|
|
110
|
+
const perZone = {};
|
|
111
|
+
for (let zone = 1; zone <= 6; zone++) {
|
|
112
|
+
const fn = slotFunctionAt(system, setterZone, zone);
|
|
113
|
+
perZone[String(zone)] = fn === RoleEnum.OUTSIDE_HITTER || (fn === RoleEnum.MIDDLE_BLOCKER && isBackRow(zone));
|
|
114
|
+
}
|
|
115
|
+
receive[String(rotation)] = perZone;
|
|
116
|
+
}
|
|
117
|
+
return receive;
|
|
118
|
+
}
|
|
119
|
+
// The default base config a team of the given system starts with. Every team always has one (materialized on team
|
|
120
|
+
// creation and by the backfill migration); the coordinates are the same 5-1 table for every system, only the
|
|
121
|
+
// receive map is derived per system.
|
|
122
|
+
export function defaultBaseConfig(system) {
|
|
123
|
+
return { coords: DEFAULT_COORDS, receive: defaultReceiveMap(system) };
|
|
124
|
+
}
|
|
@@ -2,6 +2,8 @@ export * from './team';
|
|
|
2
2
|
export * from './team-generator';
|
|
3
3
|
export * from './formation';
|
|
4
4
|
export * from './tactics';
|
|
5
|
+
export * from './base-config';
|
|
6
|
+
export * from './default-base-config';
|
|
5
7
|
export * from './team-name';
|
|
6
8
|
export * from './energy-band';
|
|
7
9
|
export * from './pinch-condition';
|
|
@@ -2,6 +2,8 @@ export * from './team';
|
|
|
2
2
|
export * from './team-generator';
|
|
3
3
|
export * from './formation';
|
|
4
4
|
export * from './tactics';
|
|
5
|
+
export * from './base-config';
|
|
6
|
+
export * from './default-base-config';
|
|
5
7
|
export * from './team-name';
|
|
6
8
|
export * from './energy-band';
|
|
7
9
|
export * from './pinch-condition';
|
|
@@ -282,12 +282,27 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
282
282
|
}, z.core.$strip>>>>;
|
|
283
283
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
284
284
|
rotation: z.ZodNumber;
|
|
285
|
-
targets: z.
|
|
285
|
+
targets: z.ZodObject<{
|
|
286
|
+
DEF_L: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
287
|
+
DEF_M: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
288
|
+
DEF_R: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
289
|
+
}, z.core.$strip>;
|
|
286
290
|
}, z.core.$strip>>>;
|
|
287
291
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
288
292
|
rotation: z.ZodNumber;
|
|
289
|
-
targets: z.
|
|
293
|
+
targets: z.ZodObject<{
|
|
294
|
+
DEF_L: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
295
|
+
DEF_M: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
296
|
+
DEF_R: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
297
|
+
}, z.core.$strip>;
|
|
290
298
|
}, z.core.$strip>>>>;
|
|
299
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
300
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
301
|
+
d: z.ZodNumber;
|
|
302
|
+
r: z.ZodNumber;
|
|
303
|
+
}, z.core.$strip>>>>;
|
|
304
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
305
|
+
}, z.core.$strip>>;
|
|
291
306
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
292
307
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
293
308
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -65,15 +65,31 @@ const blockingFunctionSchema = z.union([
|
|
|
65
65
|
]);
|
|
66
66
|
// One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
|
|
67
67
|
// each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
|
|
68
|
+
const blockingTargetsSchema = z.record(z.string(), blockingFunctionSchema);
|
|
69
|
+
// Per point-of-attack defence (owner 2026-08-06): a separate front-row commit map for an attack from my left /
|
|
70
|
+
// middle / right. Each defaults to {} so a partial config stays valid.
|
|
68
71
|
const blockingPreferenceSchema = z.object({
|
|
69
72
|
rotation: z.number().int().min(1).max(6),
|
|
70
|
-
targets: z.
|
|
73
|
+
targets: z.object({
|
|
74
|
+
DEF_L: blockingTargetsSchema.default({}),
|
|
75
|
+
DEF_M: blockingTargetsSchema.default({}),
|
|
76
|
+
DEF_R: blockingTargetsSchema.default({})
|
|
77
|
+
})
|
|
71
78
|
});
|
|
72
79
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
73
80
|
const systemSetSchema = z.object({
|
|
74
81
|
rotationSystem: z.nativeEnum(RotationSystemEnum),
|
|
75
82
|
designatedSetters: z.array(playerInstanceSchema)
|
|
76
83
|
});
|
|
84
|
+
// Base positions (owner 2026-08-03): coords[formation][rotation][zone] -> {d,r} + receive[rotation][zone] -> bool.
|
|
85
|
+
// jsonb keys are strings; the coordinate + boolean leaves are validated. Structural key ranges (formation names,
|
|
86
|
+
// rotations 1-6, zones 1-6) are trusted from the UI editor / preset compiler, which write the engine rotation
|
|
87
|
+
// convention. Out-of-court coordinates are allowed, so d/r are not range-clamped here.
|
|
88
|
+
const baseCoordSchema = z.object({ d: z.number().finite(), r: z.number().finite() });
|
|
89
|
+
const baseConfigSchema = z.object({
|
|
90
|
+
coords: z.record(z.string(), z.record(z.string(), z.record(z.string(), baseCoordSchema))),
|
|
91
|
+
receive: z.record(z.string(), z.record(z.string(), z.boolean()))
|
|
92
|
+
});
|
|
77
93
|
export const TacticsInputSchema = z.object({
|
|
78
94
|
lineup: lineupSchema,
|
|
79
95
|
liberoReplacements: z.array(playerInstanceSchema),
|
|
@@ -104,6 +120,8 @@ export const TacticsInputSchema = z.object({
|
|
|
104
120
|
// Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
|
|
105
121
|
blockingPreferences: z.array(blockingPreferenceSchema).default([]),
|
|
106
122
|
blockingPreferenceSets: z.array(z.array(blockingPreferenceSchema)).max(5).optional(),
|
|
123
|
+
// Base positions (owner 2026-08-03). Optional -> the sim falls back to the default preset for the system.
|
|
124
|
+
basePositions: baseConfigSchema.optional(),
|
|
107
125
|
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
108
126
|
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
109
127
|
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
@@ -199,9 +217,11 @@ export const TacticsInputSchema = z.object({
|
|
|
199
217
|
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
200
218
|
}
|
|
201
219
|
seenRotations.add(pref.rotation);
|
|
202
|
-
for (const
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
|
|
221
|
+
for (const id of Object.keys(pref.targets[form] ?? {})) {
|
|
222
|
+
if (!starterIds.has(id)) {
|
|
223
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
224
|
+
}
|
|
205
225
|
}
|
|
206
226
|
}
|
|
207
227
|
}
|
|
@@ -234,18 +234,31 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
234
234
|
...overrides
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
|
-
it('accepts a blocking preference that keys starters to attacking functions', () => {
|
|
237
|
+
it('accepts a blocking preference that keys starters to attacking functions, per defence side', () => {
|
|
238
238
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
239
|
-
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
239
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: RoleEnum.OUTSIDE_HITTER }, DEF_M: {}, DEF_R: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }]
|
|
240
240
|
}));
|
|
241
241
|
expect(res.success).toBe(true);
|
|
242
242
|
if (res.success) {
|
|
243
|
-
expect(res.data.blockingPreferences[0].targets[p4.id]).toBe(RoleEnum.OUTSIDE_HITTER);
|
|
243
|
+
expect(res.data.blockingPreferences[0].targets.DEF_L[p4.id]).toBe(RoleEnum.OUTSIDE_HITTER);
|
|
244
|
+
expect(res.data.blockingPreferences[0].targets.DEF_R[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
245
|
+
expect(res.data.blockingPreferences[0].targets.DEF_M).toEqual({}); // the middle side is independent (empty here)
|
|
244
246
|
}
|
|
245
247
|
});
|
|
246
|
-
it('
|
|
248
|
+
it('defaults the omitted defence sides to empty maps', () => {
|
|
247
249
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
248
|
-
blockingPreferences: [{ rotation: 1, targets: { [
|
|
250
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: NO_BLOCK } } }]
|
|
251
|
+
}));
|
|
252
|
+
expect(res.success).toBe(true);
|
|
253
|
+
if (res.success) {
|
|
254
|
+
expect(res.data.blockingPreferences[0].targets.DEF_L[p4.id]).toBe(NO_BLOCK);
|
|
255
|
+
expect(res.data.blockingPreferences[0].targets.DEF_M).toEqual({});
|
|
256
|
+
expect(res.data.blockingPreferences[0].targets.DEF_R).toEqual({});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
it('rejects a blocking target (on any side) that is not a starter', () => {
|
|
260
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
261
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_M: { [reserve.id]: RoleEnum.OUTSIDE_HITTER } } }]
|
|
249
262
|
}));
|
|
250
263
|
expect(res.success).toBe(false);
|
|
251
264
|
if (!res.success) {
|
|
@@ -254,7 +267,7 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
254
267
|
});
|
|
255
268
|
it('rejects a blocking target function of libero', () => {
|
|
256
269
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
257
|
-
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.LIBERO } }]
|
|
270
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: RoleEnum.LIBERO } } }]
|
|
258
271
|
}));
|
|
259
272
|
expect(res.success).toBe(false);
|
|
260
273
|
if (!res.success) {
|
|
@@ -264,8 +277,8 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
264
277
|
it('rejects a duplicate rotation in blocking preferences', () => {
|
|
265
278
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
266
279
|
blockingPreferences: [
|
|
267
|
-
{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER } },
|
|
268
|
-
{ rotation: 1, targets: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
280
|
+
{ rotation: 1, targets: { DEF_L: { [p4.id]: RoleEnum.OUTSIDE_HITTER } } },
|
|
281
|
+
{ rotation: 1, targets: { DEF_M: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }
|
|
269
282
|
]
|
|
270
283
|
}));
|
|
271
284
|
expect(res.success).toBe(false);
|
|
@@ -275,29 +288,29 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
275
288
|
});
|
|
276
289
|
it('accepts per-set blocking preference overrides', () => {
|
|
277
290
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
278
|
-
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: RoleEnum.OPPOSITE_HITTER } }], []]
|
|
291
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { DEF_R: { [p4.id]: RoleEnum.OPPOSITE_HITTER } } }], []]
|
|
279
292
|
}));
|
|
280
293
|
expect(res.success).toBe(true);
|
|
281
294
|
});
|
|
282
|
-
it('accepts NO_BLOCK as an assignment, alongside a commit on another starter', () => {
|
|
295
|
+
it('accepts NO_BLOCK as an assignment, alongside a commit on another starter, on the same side', () => {
|
|
283
296
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
284
|
-
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
297
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }]
|
|
285
298
|
}));
|
|
286
299
|
expect(res.success).toBe(true);
|
|
287
300
|
if (res.success) {
|
|
288
|
-
expect(res.data.blockingPreferences[0].targets[p4.id]).toBe(NO_BLOCK);
|
|
289
|
-
expect(res.data.blockingPreferences[0].targets[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
301
|
+
expect(res.data.blockingPreferences[0].targets.DEF_L[p4.id]).toBe(NO_BLOCK);
|
|
302
|
+
expect(res.data.blockingPreferences[0].targets.DEF_L[p3.id]).toBe(RoleEnum.MIDDLE_BLOCKER);
|
|
290
303
|
}
|
|
291
304
|
});
|
|
292
305
|
it('accepts NO_BLOCK in a per-set override', () => {
|
|
293
306
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
294
|
-
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: NO_BLOCK } }], []]
|
|
307
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { DEF_M: { [p4.id]: NO_BLOCK } } }], []]
|
|
295
308
|
}));
|
|
296
309
|
expect(res.success).toBe(true);
|
|
297
310
|
});
|
|
298
311
|
it('still rejects a NO_BLOCK assignment on a player who is not a starter', () => {
|
|
299
312
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
300
|
-
blockingPreferences: [{ rotation: 1, targets: { [reserve.id]: NO_BLOCK } }]
|
|
313
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_R: { [reserve.id]: NO_BLOCK } } }]
|
|
301
314
|
}));
|
|
302
315
|
expect(res.success).toBe(false);
|
|
303
316
|
if (!res.success) {
|
|
@@ -306,7 +319,7 @@ describe('TacticsInputSchema — blocking preferences', () => {
|
|
|
306
319
|
});
|
|
307
320
|
it('rejects an unknown assignment string', () => {
|
|
308
321
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
309
|
-
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: 'NO_BLOCKING' } }]
|
|
322
|
+
blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: 'NO_BLOCKING' } } }]
|
|
310
323
|
}));
|
|
311
324
|
expect(res.success).toBe(false);
|
|
312
325
|
});
|
|
@@ -290,12 +290,27 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
290
290
|
}, z.core.$strip>>>>;
|
|
291
291
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
292
292
|
rotation: z.ZodNumber;
|
|
293
|
-
targets: z.
|
|
293
|
+
targets: z.ZodObject<{
|
|
294
|
+
DEF_L: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
295
|
+
DEF_M: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
296
|
+
DEF_R: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
297
|
+
}, z.core.$strip>;
|
|
294
298
|
}, z.core.$strip>>>;
|
|
295
299
|
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
296
300
|
rotation: z.ZodNumber;
|
|
297
|
-
targets: z.
|
|
301
|
+
targets: z.ZodObject<{
|
|
302
|
+
DEF_L: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
303
|
+
DEF_M: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
304
|
+
DEF_R: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>>;
|
|
305
|
+
}, z.core.$strip>;
|
|
298
306
|
}, z.core.$strip>>>>;
|
|
307
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
308
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
309
|
+
d: z.ZodNumber;
|
|
310
|
+
r: z.ZodNumber;
|
|
311
|
+
}, z.core.$strip>>>>;
|
|
312
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
313
|
+
}, z.core.$strip>>;
|
|
299
314
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
300
315
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
301
316
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -8,6 +8,7 @@ import { OffensivePreference } from './offensive-preference';
|
|
|
8
8
|
import { BlockingPreference } from './blocking-preference';
|
|
9
9
|
import { InjuryReplacement } from './injury-replacement';
|
|
10
10
|
import { LiberoInjuryConfig } from './libero-injury';
|
|
11
|
+
import { BaseConfig } from './base-config';
|
|
11
12
|
export interface StartingLineup {
|
|
12
13
|
readonly [CourtPosition.LEFT_FRONT]: Player;
|
|
13
14
|
readonly [CourtPosition.MIDDLE_FRONT]: Player;
|
|
@@ -42,6 +43,7 @@ export interface TacticsOpts {
|
|
|
42
43
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
43
44
|
readonly blockingPreferences: BlockingPreference[];
|
|
44
45
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
46
|
+
readonly basePositions?: BaseConfig;
|
|
45
47
|
}
|
|
46
48
|
export declare class Tactics {
|
|
47
49
|
readonly lineup: StartingLineup;
|
|
@@ -63,6 +65,7 @@ export declare class Tactics {
|
|
|
63
65
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
64
66
|
readonly blockingPreferences: BlockingPreference[];
|
|
65
67
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
68
|
+
readonly basePositions?: BaseConfig;
|
|
66
69
|
static create(input: unknown): Tactics;
|
|
67
70
|
private constructor();
|
|
68
71
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -15,7 +15,7 @@ export class Tactics {
|
|
|
15
15
|
}
|
|
16
16
|
return new Tactics(result.data);
|
|
17
17
|
}
|
|
18
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets }) {
|
|
18
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
|
|
19
19
|
this.lineup = lineup;
|
|
20
20
|
this.liberoReplacements = liberoReplacements;
|
|
21
21
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -35,6 +35,7 @@ export class Tactics {
|
|
|
35
35
|
this.liberoInjury = liberoInjury;
|
|
36
36
|
this.blockingPreferences = blockingPreferences;
|
|
37
37
|
this.blockingPreferenceSets = blockingPreferenceSets;
|
|
38
|
+
this.basePositions = basePositions;
|
|
38
39
|
}
|
|
39
40
|
findPlayerInLineup(id) {
|
|
40
41
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -5,6 +5,8 @@ import { Team } from './team';
|
|
|
5
5
|
import { Tactics } from './tactics';
|
|
6
6
|
import { EnergyBand } from './energy-band';
|
|
7
7
|
import { Formation } from './formation';
|
|
8
|
+
import { RotationSystemEnum } from './rotation-system';
|
|
9
|
+
import { defaultBaseConfig } from './default-base-config';
|
|
8
10
|
import { CourtPosition } from '../match';
|
|
9
11
|
import logger from '../../logger';
|
|
10
12
|
const buildBlock = (rarity, count) => Array.from({ length: count }, () => rarity);
|
|
@@ -62,7 +64,10 @@ export class TeamGenerator {
|
|
|
62
64
|
return Tactics.create({
|
|
63
65
|
lineup,
|
|
64
66
|
liberoReplacements: [lineup[3], lineup[6]],
|
|
65
|
-
substitutionBand: TeamGenerator.BOT_SUB_BANDS[Math.floor(Math.random() * TeamGenerator.BOT_SUB_BANDS.length)]
|
|
67
|
+
substitutionBand: TeamGenerator.BOT_SUB_BANDS[Math.floor(Math.random() * TeamGenerator.BOT_SUB_BANDS.length)],
|
|
68
|
+
// basePositions is never null: a team always carries the default base config for its system. createTactics
|
|
69
|
+
// makes 6-0 teams (Tactics.create default); when a user picks another system the API re-materializes.
|
|
70
|
+
basePositions: defaultBaseConfig(RotationSystemEnum.SIX_ZERO)
|
|
66
71
|
});
|
|
67
72
|
}
|
|
68
73
|
static createLineup(players, formation) {
|