volleyballsimtypes 0.0.505 → 0.0.507

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/cjs/src/api/index.d.ts +2 -2
  2. package/dist/cjs/src/api/tactics-player-refs.js +2 -2
  3. package/dist/cjs/src/data/init-models.js +4 -0
  4. package/dist/cjs/src/data/models/base-position-preset.d.ts +22 -0
  5. package/dist/cjs/src/data/models/base-position-preset.js +54 -0
  6. package/dist/cjs/src/data/models/index.d.ts +1 -0
  7. package/dist/cjs/src/data/models/index.js +1 -0
  8. package/dist/cjs/src/data/models/tactics.d.ts +2 -2
  9. package/dist/cjs/src/data/transformers/tactics.test.js +10 -10
  10. package/dist/cjs/src/service/competition/competition-rewards.d.ts +20 -0
  11. package/dist/cjs/src/service/competition/competition-rewards.js +47 -0
  12. package/dist/cjs/src/service/competition/competition-rewards.test.d.ts +1 -0
  13. package/dist/cjs/src/service/competition/competition-rewards.test.js +32 -0
  14. package/dist/cjs/src/service/competition/index.d.ts +2 -0
  15. package/dist/cjs/src/service/competition/index.js +2 -0
  16. package/dist/cjs/src/service/competition/league-rewards.d.ts +21 -0
  17. package/dist/cjs/src/service/competition/league-rewards.js +58 -0
  18. package/dist/cjs/src/service/competition/league-rewards.test.d.ts +1 -0
  19. package/dist/cjs/src/service/competition/league-rewards.test.js +50 -0
  20. package/dist/cjs/src/service/team/base-config.d.ts +1 -0
  21. package/dist/cjs/src/service/team/blocking-preference.d.ts +3 -1
  22. package/dist/cjs/src/service/team/blocking-preference.js +2 -1
  23. package/dist/cjs/src/service/team/default-base-config.d.ts +4 -0
  24. package/dist/cjs/src/service/team/default-base-config.js +158 -0
  25. package/dist/cjs/src/service/team/index.d.ts +1 -0
  26. package/dist/cjs/src/service/team/index.js +1 -0
  27. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +11 -2
  28. package/dist/cjs/src/service/team/schemas/tactics.z.js +17 -5
  29. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +29 -16
  30. package/dist/cjs/src/service/team/schemas/team.z.d.ts +11 -2
  31. package/dist/cjs/src/service/team/team-generator.js +6 -1
  32. package/dist/esm/src/api/index.d.ts +2 -2
  33. package/dist/esm/src/api/tactics-player-refs.js +2 -2
  34. package/dist/esm/src/data/init-models.js +5 -1
  35. package/dist/esm/src/data/models/base-position-preset.d.ts +22 -0
  36. package/dist/esm/src/data/models/base-position-preset.js +50 -0
  37. package/dist/esm/src/data/models/index.d.ts +1 -0
  38. package/dist/esm/src/data/models/index.js +1 -0
  39. package/dist/esm/src/data/models/tactics.d.ts +2 -2
  40. package/dist/esm/src/data/transformers/tactics.test.js +10 -10
  41. package/dist/esm/src/service/competition/competition-rewards.d.ts +20 -0
  42. package/dist/esm/src/service/competition/competition-rewards.js +43 -0
  43. package/dist/esm/src/service/competition/competition-rewards.test.d.ts +1 -0
  44. package/dist/esm/src/service/competition/competition-rewards.test.js +30 -0
  45. package/dist/esm/src/service/competition/index.d.ts +2 -0
  46. package/dist/esm/src/service/competition/index.js +2 -0
  47. package/dist/esm/src/service/competition/league-rewards.d.ts +21 -0
  48. package/dist/esm/src/service/competition/league-rewards.js +53 -0
  49. package/dist/esm/src/service/competition/league-rewards.test.d.ts +1 -0
  50. package/dist/esm/src/service/competition/league-rewards.test.js +48 -0
  51. package/dist/esm/src/service/team/base-config.d.ts +1 -0
  52. package/dist/esm/src/service/team/blocking-preference.d.ts +3 -1
  53. package/dist/esm/src/service/team/blocking-preference.js +1 -0
  54. package/dist/esm/src/service/team/default-base-config.d.ts +4 -0
  55. package/dist/esm/src/service/team/default-base-config.js +154 -0
  56. package/dist/esm/src/service/team/index.d.ts +1 -0
  57. package/dist/esm/src/service/team/index.js +1 -0
  58. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +11 -2
  59. package/dist/esm/src/service/team/schemas/tactics.z.js +17 -5
  60. package/dist/esm/src/service/team/schemas/tactics.z.test.js +29 -16
  61. package/dist/esm/src/service/team/schemas/team.z.d.ts +11 -2
  62. package/dist/esm/src/service/team/team-generator.js +6 -1
  63. package/package.json +1 -1
@@ -262,18 +262,18 @@ describe('per-set tactics — service <-> row round-trip', () => {
262
262
  const back = transformToTactics(attrs, roster);
263
263
  expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
264
264
  });
265
- it('round-trips blocking preferences as id-keyed function maps', () => {
265
+ it('round-trips blocking preferences as per-defence-side id-keyed function maps', () => {
266
266
  const withBlocking = Tactics.create({
267
267
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
268
268
  liberoReplacements: [],
269
- blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
269
+ blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: RoleEnum.OUTSIDE_HITTER }, DEF_R: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }]
270
270
  });
271
271
  const attrs = transformFromTactics(withBlocking, 'team-1');
272
272
  expect(attrs.blocking_preferences).toEqual([
273
- { rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
273
+ { rotation: 1, targets: { DEF_L: { [p4.id]: RoleEnum.OUTSIDE_HITTER }, DEF_M: {}, DEF_R: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }
274
274
  ]);
275
275
  const back = transformToTactics(attrs, roster);
276
- expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
276
+ expect(back.blockingPreferences[0].targets).toEqual({ DEF_L: { [p4.id]: RoleEnum.OUTSIDE_HITTER }, DEF_M: {}, DEF_R: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } });
277
277
  });
278
278
  // NO_BLOCK rides in the same targets map as a commit assignment, so the transformers copy it wholesale with no
279
279
  // mapping code. This pins that: the sentinel must survive the row round-trip verbatim, mixed with a commit, and
@@ -282,17 +282,17 @@ describe('per-set tactics — service <-> row round-trip', () => {
282
282
  const withNoBlock = Tactics.create({
283
283
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
284
284
  liberoReplacements: [],
285
- blockingPreferences: [{ rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }],
286
- blockingPreferenceSets: [[{ rotation: 2, targets: { [p2.id]: NO_BLOCK } }]]
285
+ blockingPreferences: [{ rotation: 1, targets: { DEF_M: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } } }],
286
+ blockingPreferenceSets: [[{ rotation: 2, targets: { DEF_L: { [p2.id]: NO_BLOCK } } }]]
287
287
  });
288
288
  const attrs = transformFromTactics(withNoBlock, 'team-1');
289
289
  expect(attrs.blocking_preferences).toEqual([
290
- { rotation: 1, targets: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
290
+ { rotation: 1, targets: { DEF_L: {}, DEF_M: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER }, DEF_R: {} } }
291
291
  ]);
292
- expect(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { [p2.id]: NO_BLOCK } }]]);
292
+ expect(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { DEF_L: { [p2.id]: NO_BLOCK }, DEF_M: {}, DEF_R: {} } }]]);
293
293
  const back = transformToTactics(attrs, roster);
294
- expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
295
- expect(back.blockingPreferenceSets?.[0][0].targets).toEqual({ [p2.id]: NO_BLOCK });
294
+ expect(back.blockingPreferences[0].targets).toEqual({ DEF_L: {}, DEF_M: { [p4.id]: NO_BLOCK, [p3.id]: RoleEnum.MIDDLE_BLOCKER }, DEF_R: {} });
295
+ expect(back.blockingPreferenceSets?.[0][0].targets).toEqual({ DEF_L: { [p2.id]: NO_BLOCK }, DEF_M: {}, DEF_R: {} });
296
296
  });
297
297
  });
298
298
  describe('libero injuries — service <-> row round-trip + lenient heal', () => {
@@ -0,0 +1,20 @@
1
+ export type CompetitionRewardKind = 'REGIONAL' | 'NATIONAL' | 'WCC';
2
+ export interface CompetitionRewardRow {
3
+ /** Best final position that still earns this row: the "Top N" band. */
4
+ readonly maxPosition: number;
5
+ /** Coins awarded by this row alone (the increment, not the running total). */
6
+ readonly coins: number;
7
+ /** Recruit-reward pulls awarded by this row alone (the competition's own pull type). */
8
+ readonly recruits: number;
9
+ }
10
+ /** Per-competition bands, widest band first (the order the docs list them, with a growing cumulative total). */
11
+ export declare const COMPETITION_REWARDS: Readonly<Record<CompetitionRewardKind, readonly CompetitionRewardRow[]>>;
12
+ export interface CompetitionPayout {
13
+ readonly coins: number;
14
+ readonly recruits: number;
15
+ }
16
+ /**
17
+ * Everything a finishing position earns in the given competition: the sum of every band that still includes the
18
+ * position (cumulative). Positions past the lowest band earn nothing. `position` is the 1-based final placement.
19
+ */
20
+ export declare function competitionPayoutFor(kind: CompetitionRewardKind, position: number): CompetitionPayout;
@@ -0,0 +1,43 @@
1
+ // What the knockout competitions (Regional Cup, National Cup, WCC) pay out by final placement. Owner-defined
2
+ // 2026-08-07. Like the event and league rewards, these are CUMULATIVE: a finish earns its band and every band
3
+ // below it. Each band gives coins and/or "recruit rewards" (the competition's own pull grant: a Regional
4
+ // recruit is a QUALIFIER pull, a National recruit a NATIONAL pull, a WCC recruit a TOURNAMENT pull). Recruit
5
+ // rewards stack, so a champion collects one per qualifying band.
6
+ //
7
+ // One source of truth for two consumers: the simulator grants from it and the UI competition docs render it.
8
+ /** Per-competition bands, widest band first (the order the docs list them, with a growing cumulative total). */
9
+ export const COMPETITION_REWARDS = {
10
+ REGIONAL: [
11
+ { maxPosition: 4, coins: 1000, recruits: 0 },
12
+ { maxPosition: 3, coins: 0, recruits: 1 },
13
+ { maxPosition: 2, coins: 1000, recruits: 0 },
14
+ { maxPosition: 1, coins: 500, recruits: 1 }
15
+ ],
16
+ NATIONAL: [
17
+ { maxPosition: 8, coins: 1000, recruits: 0 },
18
+ { maxPosition: 4, coins: 500, recruits: 1 },
19
+ { maxPosition: 2, coins: 500, recruits: 0 },
20
+ { maxPosition: 1, coins: 0, recruits: 1 }
21
+ ],
22
+ WCC: [
23
+ { maxPosition: 4, coins: 1000, recruits: 0 },
24
+ { maxPosition: 3, coins: 0, recruits: 1 },
25
+ { maxPosition: 2, coins: 500, recruits: 1 },
26
+ { maxPosition: 1, coins: 500, recruits: 1 }
27
+ ]
28
+ };
29
+ /**
30
+ * Everything a finishing position earns in the given competition: the sum of every band that still includes the
31
+ * position (cumulative). Positions past the lowest band earn nothing. `position` is the 1-based final placement.
32
+ */
33
+ export function competitionPayoutFor(kind, position) {
34
+ let coins = 0;
35
+ let recruits = 0;
36
+ for (const row of COMPETITION_REWARDS[kind]) {
37
+ if (position <= row.maxPosition) {
38
+ coins += row.coins;
39
+ recruits += row.recruits;
40
+ }
41
+ }
42
+ return { coins, recruits };
43
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { competitionPayoutFor, COMPETITION_REWARDS } from './competition-rewards';
3
+ // The cumulative per-position totals the owner specified (2026-08-07): { coins, recruits } a finish earns,
4
+ // summing its band and every band below. Positions past the lowest band earn nothing.
5
+ describe('competitionPayoutFor', () => {
6
+ const CASES = {
7
+ // position: [coins, recruits]
8
+ REGIONAL: { 1: [2500, 2], 2: [2000, 1], 3: [1000, 1], 4: [1000, 0], 5: [0, 0] },
9
+ NATIONAL: { 1: [2000, 2], 2: [2000, 1], 3: [1500, 1], 4: [1500, 1], 5: [1000, 0], 8: [1000, 0], 9: [0, 0] },
10
+ WCC: { 1: [2000, 3], 2: [1500, 2], 3: [1000, 1], 4: [1000, 0], 5: [0, 0] }
11
+ };
12
+ for (const kind of Object.keys(CASES)) {
13
+ it(`matches the owner totals for ${kind}`, () => {
14
+ for (const [pos, [coins, recruits]] of Object.entries(CASES[kind])) {
15
+ expect(competitionPayoutFor(kind, Number(pos))).toEqual({ coins, recruits });
16
+ }
17
+ });
18
+ }
19
+ it('champion collects one recruit per qualifying band', () => {
20
+ expect(competitionPayoutFor('REGIONAL', 1).recruits).toBe(2);
21
+ expect(competitionPayoutFor('NATIONAL', 1).recruits).toBe(2);
22
+ expect(competitionPayoutFor('WCC', 1).recruits).toBe(3);
23
+ });
24
+ it('champion coin total equals the sum of the band table', () => {
25
+ const sum = (k) => COMPETITION_REWARDS[k].reduce((s, r) => s + r.coins, 0);
26
+ expect(competitionPayoutFor('REGIONAL', 1).coins).toBe(sum('REGIONAL'));
27
+ expect(competitionPayoutFor('NATIONAL', 1).coins).toBe(sum('NATIONAL'));
28
+ expect(competitionPayoutFor('WCC', 1).coins).toBe(sum('WCC'));
29
+ });
30
+ });
@@ -3,6 +3,8 @@ export * from './season';
3
3
  export * from './event-schedule';
4
4
  export * from './event-draft';
5
5
  export * from './event-rewards';
6
+ export * from './league-rewards';
7
+ export * from './competition-rewards';
6
8
  export * from './league';
7
9
  export * from './division';
8
10
  export * from './iteration';
@@ -3,6 +3,8 @@ export * from './season';
3
3
  export * from './event-schedule';
4
4
  export * from './event-draft';
5
5
  export * from './event-rewards';
6
+ export * from './league-rewards';
7
+ export * from './competition-rewards';
6
8
  export * from './league';
7
9
  export * from './division';
8
10
  export * from './iteration';
@@ -0,0 +1,21 @@
1
+ export interface LeagueRewardRow {
2
+ /** Best final position that still earns this row: the "Top N" band. */
3
+ readonly maxPosition: number;
4
+ /** Coins awarded by this row alone (the increment, not the running total). */
5
+ readonly coins: number;
6
+ }
7
+ /** Division tiers that get their own reward table. Tier 3 covers "division 3 and lower". */
8
+ export type LeagueRewardTier = 1 | 2 | 3;
9
+ /**
10
+ * Per-tier bands, widest band first (the order the standings doc lists them, with a growing cumulative total).
11
+ * A team earns every row whose band still includes its finishing position, which is what "cumulative" means.
12
+ */
13
+ export declare const LEAGUE_REWARDS: Readonly<Record<LeagueRewardTier, readonly LeagueRewardRow[]>>;
14
+ /** Clamp any division tier to a reward table: tier 1, tier 2, and tier 3-and-lower share the tier-3 table. */
15
+ export declare function leagueRewardTier(tier: number): LeagueRewardTier;
16
+ /**
17
+ * Coins a finishing position earns in a division of the given tier: the sum of every band that still includes
18
+ * the position (cumulative). Positions past the lowest band earn 0. `position` is the 1-based final standings
19
+ * place.
20
+ */
21
+ export declare function leaguePayoutFor(tier: number, position: number): number;
@@ -0,0 +1,53 @@
1
+ // What a league season pays out at the end, by division tier and final standings position. Owner-defined
2
+ // 2026-08-07. Like the event rewards, these are CUMULATIVE: a team collects its own band and every band below
3
+ // it, so first place collects them all. Positions past the lowest band earn nothing.
4
+ //
5
+ // One source of truth for two consumers: the simulator grants from it (assignSeasonPayoutIfMissing) and the UI
6
+ // standings doc renders it.
7
+ /**
8
+ * Per-tier bands, widest band first (the order the standings doc lists them, with a growing cumulative total).
9
+ * A team earns every row whose band still includes its finishing position, which is what "cumulative" means.
10
+ */
11
+ export const LEAGUE_REWARDS = {
12
+ 1: [
13
+ { maxPosition: 12, coins: 1000 },
14
+ { maxPosition: 8, coins: 500 },
15
+ { maxPosition: 4, coins: 500 },
16
+ { maxPosition: 3, coins: 250 },
17
+ { maxPosition: 2, coins: 250 },
18
+ { maxPosition: 1, coins: 500 }
19
+ ],
20
+ 2: [
21
+ { maxPosition: 8, coins: 500 },
22
+ { maxPosition: 4, coins: 500 },
23
+ { maxPosition: 2, coins: 250 },
24
+ { maxPosition: 1, coins: 250 }
25
+ ],
26
+ 3: [
27
+ { maxPosition: 4, coins: 500 },
28
+ { maxPosition: 2, coins: 250 },
29
+ { maxPosition: 1, coins: 250 }
30
+ ]
31
+ };
32
+ /** Clamp any division tier to a reward table: tier 1, tier 2, and tier 3-and-lower share the tier-3 table. */
33
+ export function leagueRewardTier(tier) {
34
+ const t = Math.trunc(tier);
35
+ if (t <= 1)
36
+ return 1;
37
+ if (t === 2)
38
+ return 2;
39
+ return 3;
40
+ }
41
+ /**
42
+ * Coins a finishing position earns in a division of the given tier: the sum of every band that still includes
43
+ * the position (cumulative). Positions past the lowest band earn 0. `position` is the 1-based final standings
44
+ * place.
45
+ */
46
+ export function leaguePayoutFor(tier, position) {
47
+ let coins = 0;
48
+ for (const row of LEAGUE_REWARDS[leagueRewardTier(tier)]) {
49
+ if (position <= row.maxPosition)
50
+ coins += row.coins;
51
+ }
52
+ return coins;
53
+ }
@@ -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
+ });
@@ -7,4 +7,5 @@ export interface BaseCoord {
7
7
  export interface BaseConfig {
8
8
  readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
9
9
  readonly receive: Record<string, Record<string, boolean>>;
10
+ readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
10
11
  }
@@ -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,154 @@
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: 10.00, r: 3.50 }, 2: { d: 0.50, r: -2.50 }, 3: { d: 0.50, r: 0.00 }, 4: { d: 0.50, r: 2.50 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 5.00, r: -2.50 } },
7
+ 2: { 1: { d: 10.00, r: 3.50 }, 2: { d: 0.50, r: 0.00 }, 3: { d: 0.50, r: 2.50 }, 4: { d: 0.50, r: -2.50 }, 5: { d: 5.00, r: -2.50 }, 6: { d: 5.00, r: 2.50 } },
8
+ 3: { 1: { d: 10.00, r: -3.50 }, 2: { d: 0.50, r: 2.50 }, 3: { d: 0.50, r: -2.50 }, 4: { d: 0.50, r: 0.00 }, 5: { d: 5.00, r: 2.50 }, 6: { d: 6.50, r: 0.00 } },
9
+ 4: { 1: { d: 10.00, r: 3.50 }, 2: { d: 0.50, r: -2.50 }, 3: { d: 0.50, r: 0.00 }, 4: { d: 0.50, r: 2.50 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 5.00, r: -2.50 } },
10
+ 5: { 1: { d: 10.00, r: 3.50 }, 2: { d: 0.50, r: 0.00 }, 3: { d: 0.50, r: 2.50 }, 4: { d: 0.50, r: -2.50 }, 5: { d: 5.00, r: -2.50 }, 6: { d: 5.00, r: 2.50 } },
11
+ 6: { 1: { d: 10.00, r: -3.50 }, 2: { d: 0.50, r: 2.50 }, 3: { d: 0.50, r: -2.50 }, 4: { d: 0.50, r: 0.00 }, 5: { d: 5.00, r: 2.50 }, 6: { d: 6.50, r: 0.00 } }
12
+ },
13
+ RECEIVE: {
14
+ 1: { 1: { d: 6.50, r: 3.50 }, 2: { d: 6.00, r: 3.00 }, 3: { d: 3.00, r: -3.50 }, 4: { d: 3.50, r: -4.00 }, 5: { d: 6.00, r: -3.00 }, 6: { d: 6.50, r: 0.00 } },
15
+ 2: { 1: { d: 6.00, r: 3.00 }, 2: { d: 3.00, r: 4.00 }, 3: { d: 0.50, r: 0.50 }, 4: { d: 6.00, r: -3.00 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 1.25, r: 0.75 } },
16
+ 3: { 1: { d: 6.00, r: 3.00 }, 2: { d: 3.00, r: 4.00 }, 3: { d: 6.00, r: -3.00 }, 4: { d: 0.95, r: -4.00 }, 5: { d: 1.00, r: -1.00 }, 6: { d: 6.50, r: 0.00 } },
17
+ 4: { 1: { d: 8.50, r: 4.00 }, 2: { d: 6.00, r: -3.00 }, 3: { d: 1.25, r: -4.00 }, 4: { d: 0.75, r: -4.05 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 6.00, r: 3.00 } },
18
+ 5: { 1: { d: 6.00, r: 3.00 }, 2: { d: 2.50, r: 4.00 }, 3: { d: 1.00, r: 1.50 }, 4: { d: 6.00, r: -3.00 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 8.50, r: 1.50 } },
19
+ 6: { 1: { d: 6.00, r: 3.00 }, 2: { d: 1.00, r: 1.50 }, 3: { d: 6.00, r: -3.00 }, 4: { d: 3.00, r: -4.00 }, 5: { d: 8.50, r: -1.50 }, 6: { d: 6.50, r: 0.00 } }
20
+ },
21
+ DEFENCE: {
22
+ 1: { 1: { d: 5.00, r: 3.00 }, 2: { d: 0.50, r: -2.50 }, 3: { d: 0.50, r: 0.00 }, 4: { d: 0.50, r: 2.50 }, 5: { d: 7.00, r: 0.00 }, 6: { d: 5.00, r: -3.00 } },
23
+ 2: { 1: { d: 7.00, r: 0.00 }, 2: { d: 0.50, r: 0.00 }, 3: { d: 0.50, r: 2.50 }, 4: { d: 0.50, r: -2.50 }, 5: { d: 5.00, r: -3.00 }, 6: { d: 5.00, r: 3.00 } },
24
+ 3: { 1: { d: 5.00, r: -3.00 }, 2: { d: 0.50, r: 2.50 }, 3: { d: 0.50, r: -2.50 }, 4: { d: 0.50, r: 0.00 }, 5: { d: 5.00, r: 3.00 }, 6: { d: 7.00, r: 0.00 } },
25
+ 4: { 1: { d: 5.00, r: 3.00 }, 2: { d: 0.50, r: -2.50 }, 3: { d: 0.50, r: 0.00 }, 4: { d: 0.50, r: 2.50 }, 5: { d: 7.00, r: 0.00 }, 6: { d: 5.00, r: -3.00 } },
26
+ 5: { 1: { d: 7.00, r: 0.00 }, 2: { d: 0.50, r: 0.00 }, 3: { d: 0.50, r: 2.50 }, 4: { d: 0.50, r: -2.50 }, 5: { d: 5.00, r: -3.00 }, 6: { d: 5.00, r: 3.00 } },
27
+ 6: { 1: { d: 5.00, r: -3.00 }, 2: { d: 0.50, r: 2.50 }, 3: { d: 0.50, r: -2.50 }, 4: { d: 0.50, r: 0.00 }, 5: { d: 5.00, r: 3.00 }, 6: { d: 7.00, r: 0.00 } }
28
+ },
29
+ DEF_L: {
30
+ 1: { 1: { d: 5.00, r: 1.50 }, 2: { d: 0.25, r: -4.00 }, 3: { d: 0.25, r: -3.25 }, 4: { d: 1.75, r: 1.25 }, 5: { d: 8.00, r: -0.50 }, 6: { d: 5.00, r: -4.20 } },
31
+ 2: { 1: { d: 8.00, r: -0.50 }, 2: { d: 0.25, r: -3.25 }, 3: { d: 1.75, r: 1.25 }, 4: { d: 0.25, r: -4.00 }, 5: { d: 5.00, r: -4.20 }, 6: { d: 5.00, r: 1.50 } },
32
+ 3: { 1: { d: 5.00, r: -4.20 }, 2: { d: 1.75, r: 1.25 }, 3: { d: 0.25, r: -4.00 }, 4: { d: 0.25, r: -3.25 }, 5: { d: 5.00, r: 1.50 }, 6: { d: 8.00, r: -0.50 } },
33
+ 4: { 1: { d: 5.00, r: 1.50 }, 2: { d: 0.25, r: -4.00 }, 3: { d: 0.25, r: -3.25 }, 4: { d: 1.75, r: 1.25 }, 5: { d: 8.00, r: -0.50 }, 6: { d: 5.00, r: -4.20 } },
34
+ 5: { 1: { d: 8.00, r: -0.50 }, 2: { d: 0.25, r: -3.25 }, 3: { d: 1.75, r: 1.25 }, 4: { d: 0.25, r: -4.00 }, 5: { d: 5.00, r: -4.20 }, 6: { d: 5.00, r: 1.50 } },
35
+ 6: { 1: { d: 5.00, r: -4.20 }, 2: { d: 1.75, r: 1.25 }, 3: { d: 0.25, r: -4.00 }, 4: { d: 0.25, r: -3.25 }, 5: { d: 5.00, r: 1.50 }, 6: { d: 8.00, r: -0.50 } }
36
+ },
37
+ DEF_M: {
38
+ 1: { 1: { d: 5.00, r: 2.50 }, 2: { d: 1.75, r: -2.50 }, 3: { d: 0.25, r: 0.00 }, 4: { d: 1.75, r: 2.50 }, 5: { d: 7.00, r: 0.00 }, 6: { d: 5.00, r: -2.50 } },
39
+ 2: { 1: { d: 7.00, r: 0.00 }, 2: { d: 0.25, r: 0.00 }, 3: { d: 1.75, r: 2.50 }, 4: { d: 1.75, r: -2.50 }, 5: { d: 5.00, r: -2.50 }, 6: { d: 5.00, r: 2.50 } },
40
+ 3: { 1: { d: 5.00, r: -2.50 }, 2: { d: 1.75, r: 2.50 }, 3: { d: 1.75, r: -2.50 }, 4: { d: 0.25, r: 0.00 }, 5: { d: 5.00, r: 2.50 }, 6: { d: 7.00, r: 0.00 } },
41
+ 4: { 1: { d: 5.00, r: 2.50 }, 2: { d: 1.75, r: -2.50 }, 3: { d: 0.25, r: 0.00 }, 4: { d: 1.75, r: 2.50 }, 5: { d: 7.00, r: 0.00 }, 6: { d: 5.00, r: -2.50 } },
42
+ 5: { 1: { d: 7.00, r: 0.00 }, 2: { d: 0.25, r: 0.00 }, 3: { d: 1.75, r: 2.50 }, 4: { d: 1.75, r: -2.50 }, 5: { d: 5.00, r: -2.50 }, 6: { d: 5.00, r: 2.50 } },
43
+ 6: { 1: { d: 5.00, r: -2.50 }, 2: { d: 1.75, r: 2.50 }, 3: { d: 1.75, r: -2.50 }, 4: { d: 0.25, r: 0.00 }, 5: { d: 5.00, r: 2.50 }, 6: { d: 7.00, r: 0.00 } }
44
+ },
45
+ DEF_R: {
46
+ 1: { 1: { d: 5.00, r: 4.20 }, 2: { d: 1.75, r: -1.25 }, 3: { d: 0.25, r: 3.25 }, 4: { d: 0.25, r: 4.00 }, 5: { d: 8.00, r: 0.50 }, 6: { d: 5.00, r: -1.50 } },
47
+ 2: { 1: { d: 8.00, r: 0.50 }, 2: { d: 0.25, r: 3.25 }, 3: { d: 0.25, r: 4.00 }, 4: { d: 1.75, r: -1.25 }, 5: { d: 5.00, r: -1.50 }, 6: { d: 5.00, r: 4.20 } },
48
+ 3: { 1: { d: 5.00, r: -1.50 }, 2: { d: 0.25, r: 4.00 }, 3: { d: 1.75, r: -1.25 }, 4: { d: 0.25, r: 3.25 }, 5: { d: 5.00, r: 4.20 }, 6: { d: 8.00, r: 0.50 } },
49
+ 4: { 1: { d: 5.00, r: 4.20 }, 2: { d: 1.75, r: -1.25 }, 3: { d: 0.25, r: 3.25 }, 4: { d: 0.25, r: 4.00 }, 5: { d: 8.00, r: 0.50 }, 6: { d: 5.00, r: -1.50 } },
50
+ 5: { 1: { d: 8.00, r: 0.50 }, 2: { d: 0.25, r: 3.25 }, 3: { d: 0.25, r: 4.00 }, 4: { d: 1.75, r: -1.25 }, 5: { d: 5.00, r: -1.50 }, 6: { d: 5.00, r: 4.20 } },
51
+ 6: { 1: { d: 5.00, r: -1.50 }, 2: { d: 0.25, r: 4.00 }, 3: { d: 1.75, r: -1.25 }, 4: { d: 0.25, r: 3.25 }, 5: { d: 5.00, r: 4.20 }, 6: { d: 8.00, r: 0.50 } }
52
+ },
53
+ ATTACK: {
54
+ 1: { 1: { d: 0.50, r: 1.50 }, 2: { d: 3.75, r: -5.00 }, 3: { d: 3.00, r: 0.25 }, 4: { d: 3.75, r: 5.00 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 4.50, r: -2.50 } },
55
+ 2: { 1: { d: 6.50, r: 0.00 }, 2: { d: 3.00, r: 0.25 }, 3: { d: 3.75, r: 5.00 }, 4: { d: 3.75, r: -5.00 }, 5: { d: 4.50, r: -2.50 }, 6: { d: 0.50, r: 1.50 } },
56
+ 3: { 1: { d: 4.50, r: -2.50 }, 2: { d: 3.75, r: 5.00 }, 3: { d: 3.75, r: -5.00 }, 4: { d: 3.00, r: 0.25 }, 5: { d: 0.50, r: 1.50 }, 6: { d: 6.50, r: 0.00 } },
57
+ 4: { 1: { d: 6.50, r: 4.90 }, 2: { d: 3.75, r: -5.00 }, 3: { d: 3.00, r: 0.25 }, 4: { d: 0.50, r: 1.50 }, 5: { d: 6.50, r: 0.00 }, 6: { d: 4.50, r: -2.50 } },
58
+ 5: { 1: { d: 6.50, r: 0.00 }, 2: { d: 3.00, r: 0.25 }, 3: { d: 0.50, r: 1.50 }, 4: { d: 3.75, r: -5.00 }, 5: { d: 4.50, r: -2.50 }, 6: { d: 6.50, r: 4.90 } },
59
+ 6: { 1: { d: 4.50, r: -2.50 }, 2: { d: 0.50, r: 1.50 }, 3: { d: 3.75, r: -5.00 }, 4: { d: 3.00, r: 0.25 }, 5: { d: 6.50, r: 4.90 }, 6: { d: 6.50, r: 0.00 } }
60
+ },
61
+ ATK_L: {
62
+ 1: { 1: { d: 0.75, r: 0.00 }, 2: { d: 0.50, r: -4.00 }, 3: { d: 1.00, r: -2.50 }, 4: { d: 3.50, r: 0.75 }, 5: { d: 6.25, r: -0.75 }, 6: { d: 3.50, r: -4.00 } },
63
+ 2: { 1: { d: 6.25, r: -0.75 }, 2: { d: 1.00, r: -2.50 }, 3: { d: 3.50, r: 0.75 }, 4: { d: 0.50, r: -4.00 }, 5: { d: 3.50, r: -4.00 }, 6: { d: 0.75, r: 0.00 } },
64
+ 3: { 1: { d: 3.50, r: -4.00 }, 2: { d: 3.50, r: 0.75 }, 3: { d: 0.50, r: -4.00 }, 4: { d: 1.00, r: -2.50 }, 5: { d: 0.75, r: 0.00 }, 6: { d: 6.25, r: -0.75 } },
65
+ 4: { 1: { d: 3.50, r: 0.75 }, 2: { d: 0.50, r: -4.00 }, 3: { d: 1.00, r: -2.50 }, 4: { d: 0.75, r: 0.00 }, 5: { d: 6.25, r: -0.75 }, 6: { d: 3.50, r: -4.00 } },
66
+ 5: { 1: { d: 6.25, r: -0.75 }, 2: { d: 1.00, r: -2.50 }, 3: { d: 0.75, r: 0.00 }, 4: { d: 0.50, r: -4.00 }, 5: { d: 3.50, r: -4.00 }, 6: { d: 3.50, r: 0.75 } },
67
+ 6: { 1: { d: 3.50, r: -4.00 }, 2: { d: 0.75, r: 0.00 }, 3: { d: 0.50, r: -4.00 }, 4: { d: 1.00, r: -2.50 }, 5: { d: 3.50, r: 0.75 }, 6: { d: 6.25, r: -0.75 } }
68
+ },
69
+ ATK_M: {
70
+ 1: { 1: { d: 0.75, r: 1.25 }, 2: { d: 1.25, r: -2.00 }, 3: { d: 0.75, r: 0.50 }, 4: { d: 2.25, r: 2.60 }, 5: { d: 6.50, r: 1.50 }, 6: { d: 4.00, r: -1.50 } },
71
+ 2: { 1: { d: 6.50, r: 1.50 }, 2: { d: 0.75, r: 0.50 }, 3: { d: 2.25, r: 2.60 }, 4: { d: 1.25, r: -2.00 }, 5: { d: 4.00, r: -1.50 }, 6: { d: 0.75, r: 1.25 } },
72
+ 3: { 1: { d: 4.00, r: -1.50 }, 2: { d: 2.25, r: 2.60 }, 3: { d: 1.25, r: -2.00 }, 4: { d: 0.75, r: 0.50 }, 5: { d: 0.75, r: 1.25 }, 6: { d: 6.50, r: 1.50 } },
73
+ 4: { 1: { d: 2.25, r: 2.60 }, 2: { d: 1.25, r: -2.00 }, 3: { d: 0.75, r: 0.50 }, 4: { d: 0.75, r: 1.25 }, 5: { d: 6.50, r: 1.50 }, 6: { d: 4.00, r: -1.50 } },
74
+ 5: { 1: { d: 6.50, r: 1.50 }, 2: { d: 0.75, r: 0.50 }, 3: { d: 0.75, r: 1.25 }, 4: { d: 1.25, r: -2.00 }, 5: { d: 4.00, r: -1.50 }, 6: { d: 2.25, r: 2.60 } },
75
+ 6: { 1: { d: 4.00, r: -1.50 }, 2: { d: 0.75, r: 1.25 }, 3: { d: 1.25, r: -2.00 }, 4: { d: 0.75, r: 0.50 }, 5: { d: 2.25, r: 2.60 }, 6: { d: 6.50, r: 1.50 } }
76
+ },
77
+ ATK_R: {
78
+ 1: { 1: { d: 1.75, r: 2.50 }, 2: { d: 1.50, r: -1.75 }, 3: { d: 0.75, r: 1.75 }, 4: { d: 0.50, r: 4.00 }, 5: { d: 5.00, r: 3.25 }, 6: { d: 5.75, r: -2.00 } },
79
+ 2: { 1: { d: 5.00, r: 3.25 }, 2: { d: 0.75, r: 1.75 }, 3: { d: 0.50, r: 4.00 }, 4: { d: 1.50, r: -1.75 }, 5: { d: 5.75, r: -2.00 }, 6: { d: 1.75, r: 2.50 } },
80
+ 3: { 1: { d: 5.75, r: -2.00 }, 2: { d: 0.50, r: 4.00 }, 3: { d: 1.50, r: -1.75 }, 4: { d: 0.75, r: 1.75 }, 5: { d: 1.75, r: 2.50 }, 6: { d: 5.00, r: 3.25 } },
81
+ 4: { 1: { d: 2.25, r: 4.00 }, 2: { d: 2.25, r: -1.75 }, 3: { d: 0.75, r: 1.50 }, 4: { d: 1.50, r: 2.25 }, 5: { d: 6.00, r: 2.25 }, 6: { d: 5.00, r: -0.75 } },
82
+ 5: { 1: { d: 6.00, r: 2.25 }, 2: { d: 0.75, r: 1.50 }, 3: { d: 1.50, r: 2.25 }, 4: { d: 2.25, r: -1.75 }, 5: { d: 5.00, r: -0.75 }, 6: { d: 2.25, r: 4.00 } },
83
+ 6: { 1: { d: 5.00, r: -0.75 }, 2: { d: 1.50, r: 2.25 }, 3: { d: 2.25, r: -1.75 }, 4: { d: 0.75, r: 1.50 }, 5: { d: 2.25, r: 4.00 }, 6: { d: 6.00, r: 2.25 } }
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
+ // The owner's authored per-point-of-attack block map for the 5-1 (owner 2026-08-07), keyed
101
+ // [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock. `false` = that front-row zone sits OUT of the block for an attack
102
+ // from that side; `true` = reads/joins the wall. Only front-row zones (2/3/4) are tagged. Same rotation convention
103
+ // as DEFAULT_COORDS. Generated from the finalized 5-1 sr-mapper preset's Def L/M/R block/noblock tags.
104
+ const DEFAULT_BLOCKING = {
105
+ DEF_L: {
106
+ 1: { 2: true, 3: true, 4: false },
107
+ 2: { 2: true, 3: false, 4: true },
108
+ 3: { 2: false, 3: true, 4: true },
109
+ 4: { 2: true, 3: true, 4: false },
110
+ 5: { 2: true, 3: false, 4: true },
111
+ 6: { 2: false, 3: true, 4: true }
112
+ },
113
+ DEF_M: {
114
+ 1: { 2: false, 3: true, 4: false },
115
+ 2: { 2: true, 3: false, 4: false },
116
+ 3: { 2: false, 3: false, 4: true },
117
+ 4: { 2: false, 3: true, 4: false },
118
+ 5: { 2: true, 3: false, 4: false },
119
+ 6: { 2: false, 3: false, 4: true }
120
+ },
121
+ DEF_R: {
122
+ 1: { 2: false, 3: true, 4: true },
123
+ 2: { 2: true, 3: true, 4: false },
124
+ 3: { 2: true, 3: false, 4: true },
125
+ 4: { 2: false, 3: true, 4: true },
126
+ 5: { 2: true, 3: true, 4: false },
127
+ 6: { 2: true, 3: false, 4: true }
128
+ }
129
+ };
130
+ // 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}).
131
+ const ROTATION_TO_SETTER_ZONE = { 1: 1, 2: 6, 3: 5, 4: 4, 5: 3, 6: 2 };
132
+ // The default serve-receive participation map: the standard 3-passer formation. A rotational slot passes iff its
133
+ // slot function is an OUTSIDE_HITTER (either row) or a BACK-ROW MIDDLE_BLOCKER (the libero's slot). The setter, the
134
+ // opposite and the front-row middle hide. slotFunctionAt maps the opposite slot to SETTER in 4-2/6-2, so the
135
+ // second setter is excluded too, giving the same 3-passer map for every system.
136
+ export function defaultReceiveMap(system) {
137
+ const receive = {};
138
+ for (let rotation = 1; rotation <= 6; rotation++) {
139
+ const setterZone = ROTATION_TO_SETTER_ZONE[rotation];
140
+ const perZone = {};
141
+ for (let zone = 1; zone <= 6; zone++) {
142
+ const fn = slotFunctionAt(system, setterZone, zone);
143
+ perZone[String(zone)] = fn === RoleEnum.OUTSIDE_HITTER || (fn === RoleEnum.MIDDLE_BLOCKER && isBackRow(zone));
144
+ }
145
+ receive[String(rotation)] = perZone;
146
+ }
147
+ return receive;
148
+ }
149
+ // The default base config a team of the given system starts with. Every team always has one (materialized on team
150
+ // creation and by the backfill migration); the coordinates are the same 5-1 table for every system, only the
151
+ // receive map is derived per system.
152
+ export function defaultBaseConfig(system) {
153
+ return { coords: DEFAULT_COORDS, receive: defaultReceiveMap(system), blocking: DEFAULT_BLOCKING };
154
+ }
@@ -3,6 +3,7 @@ export * from './team-generator';
3
3
  export * from './formation';
4
4
  export * from './tactics';
5
5
  export * from './base-config';
6
+ export * from './default-base-config';
6
7
  export * from './team-name';
7
8
  export * from './energy-band';
8
9
  export * from './pinch-condition';
@@ -3,6 +3,7 @@ export * from './team-generator';
3
3
  export * from './formation';
4
4
  export * from './tactics';
5
5
  export * from './base-config';
6
+ export * from './default-base-config';
6
7
  export * from './team-name';
7
8
  export * from './energy-band';
8
9
  export * from './pinch-condition';
@@ -282,11 +282,19 @@ 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.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">]>>;
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.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">]>>;
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>>>>;
291
299
  basePositions: z.ZodOptional<z.ZodObject<{
292
300
  coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -294,6 +302,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
294
302
  r: z.ZodNumber;
295
303
  }, z.core.$strip>>>>;
296
304
  receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
305
+ blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
297
306
  }, z.core.$strip>>;
298
307
  replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
299
308
  injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
@@ -65,9 +65,16 @@ 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.record(z.string(), blockingFunctionSchema)
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({
@@ -81,7 +88,10 @@ const systemSetSchema = z.object({
81
88
  const baseCoordSchema = z.object({ d: z.number().finite(), r: z.number().finite() });
82
89
  const baseConfigSchema = z.object({
83
90
  coords: z.record(z.string(), z.record(z.string(), z.record(z.string(), baseCoordSchema))),
84
- receive: z.record(z.string(), z.record(z.string(), z.boolean()))
91
+ receive: z.record(z.string(), z.record(z.string(), z.boolean())),
92
+ // Optional per-point-of-attack blocking (owner 2026-08-07): [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock.
93
+ // Optional so presets/tactics saved before this field validate unchanged.
94
+ blocking: z.record(z.string(), z.record(z.string(), z.record(z.string(), z.boolean()))).optional()
85
95
  });
86
96
  export const TacticsInputSchema = z.object({
87
97
  lineup: lineupSchema,
@@ -210,9 +220,11 @@ export const TacticsInputSchema = z.object({
210
220
  ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
211
221
  }
212
222
  seenRotations.add(pref.rotation);
213
- for (const id of Object.keys(pref.targets)) {
214
- if (!starterIds.has(id)) {
215
- ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
223
+ for (const form of ['DEF_L', 'DEF_M', 'DEF_R']) {
224
+ for (const id of Object.keys(pref.targets[form] ?? {})) {
225
+ if (!starterIds.has(id)) {
226
+ ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
227
+ }
216
228
  }
217
229
  }
218
230
  }