volleyballsimtypes 0.0.393 → 0.0.395

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 (43) hide show
  1. package/dist/cjs/src/api/index.d.ts +8 -1
  2. package/dist/cjs/src/data/models/tactics.d.ts +12 -2
  3. package/dist/cjs/src/data/models/tactics.js +15 -0
  4. package/dist/cjs/src/data/transformers/tactics.js +12 -0
  5. package/dist/cjs/src/service/team/base-position.d.ts +25 -0
  6. package/dist/cjs/src/service/team/base-position.js +113 -0
  7. package/dist/cjs/src/service/team/base-position.test.d.ts +1 -0
  8. package/dist/cjs/src/service/team/base-position.test.js +175 -0
  9. package/dist/cjs/src/service/team/index.d.ts +4 -0
  10. package/dist/cjs/src/service/team/index.js +4 -0
  11. package/dist/cjs/src/service/team/lineup-function.d.ts +13 -0
  12. package/dist/cjs/src/service/team/lineup-function.js +67 -0
  13. package/dist/cjs/src/service/team/offensive-preference.d.ts +5 -0
  14. package/dist/cjs/src/service/team/offensive-preference.js +2 -0
  15. package/dist/cjs/src/service/team/rotation-system.d.ts +8 -0
  16. package/dist/cjs/src/service/team/rotation-system.js +25 -0
  17. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +7 -0
  18. package/dist/cjs/src/service/team/schemas/tactics.z.js +67 -1
  19. package/dist/cjs/src/service/team/schemas/team.z.d.ts +6 -0
  20. package/dist/cjs/src/service/team/tactics.d.ts +8 -0
  21. package/dist/cjs/src/service/team/tactics.js +5 -1
  22. package/dist/esm/src/api/index.d.ts +8 -1
  23. package/dist/esm/src/data/models/tactics.d.ts +12 -2
  24. package/dist/esm/src/data/models/tactics.js +15 -0
  25. package/dist/esm/src/data/transformers/tactics.js +12 -0
  26. package/dist/esm/src/service/team/base-position.d.ts +25 -0
  27. package/dist/esm/src/service/team/base-position.js +108 -0
  28. package/dist/esm/src/service/team/base-position.test.d.ts +1 -0
  29. package/dist/esm/src/service/team/base-position.test.js +173 -0
  30. package/dist/esm/src/service/team/index.d.ts +4 -0
  31. package/dist/esm/src/service/team/index.js +4 -0
  32. package/dist/esm/src/service/team/lineup-function.d.ts +13 -0
  33. package/dist/esm/src/service/team/lineup-function.js +60 -0
  34. package/dist/esm/src/service/team/offensive-preference.d.ts +5 -0
  35. package/dist/esm/src/service/team/offensive-preference.js +1 -0
  36. package/dist/esm/src/service/team/rotation-system.d.ts +8 -0
  37. package/dist/esm/src/service/team/rotation-system.js +21 -0
  38. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +7 -0
  39. package/dist/esm/src/service/team/schemas/tactics.z.js +67 -1
  40. package/dist/esm/src/service/team/schemas/team.z.d.ts +6 -0
  41. package/dist/esm/src/service/team/tactics.d.ts +8 -0
  42. package/dist/esm/src/service/team/tactics.js +5 -1
  43. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ import { RoleEnum } from '../player';
2
+ import { RotationSystemEnum } from './rotation-system';
3
+ // Flat multiplier applied to EVERY action a player performs while played out of their preferred position.
4
+ // Roles are a player's preference/specialisation (decoupled from raw stats), so this is the single standalone
5
+ // signal for "played out of position" and is not double-counting any stat disadvantage. Single tunable knob.
6
+ export const OUT_OF_POSITION_PENALTY = 0.85;
7
+ // The balanced-lineup function pattern by rotational offset from the main setter (in serving order):
8
+ // 0 setter, 1 outside, 2 middle, 3 opposite (the second setter in 4-2/6-2), 4 outside, 5 middle.
9
+ // Offset is rotation-invariant (the whole team rotates together), so a player's slot function is constant for the
10
+ // match until a substitution changes the personnel.
11
+ const PATTERN = [
12
+ RoleEnum.SETTER,
13
+ RoleEnum.OUTSIDE_HITTER,
14
+ RoleEnum.MIDDLE_BLOCKER,
15
+ RoleEnum.OPPOSITE_HITTER,
16
+ RoleEnum.OUTSIDE_HITTER,
17
+ RoleEnum.MIDDLE_BLOCKER
18
+ ];
19
+ // The slot function for a player at `position`, given the main setter sits at `setterPosition`, in `system`.
20
+ // In 4-2/6-2 the opposite slot (offset 3) is the second setter, so it maps to SETTER rather than OPPOSITE.
21
+ export function slotFunctionAt(system, setterPosition, position) {
22
+ const offset = (((position - setterPosition) % 6) + 6) % 6;
23
+ const fn = PATTERN[offset];
24
+ if (fn === RoleEnum.OPPOSITE_HITTER &&
25
+ (system === RotationSystemEnum.FOUR_TWO || system === RotationSystemEnum.SIX_TWO)) {
26
+ return RoleEnum.SETTER;
27
+ }
28
+ return fn;
29
+ }
30
+ // Map each starter's id -> the role their lineup slot requires. Empty for 6-0 (no functions => no penalty), or if
31
+ // the main setter is not among the starters (misconfigured -> degrade gracefully to no penalty).
32
+ // Designated setters are forced to SETTER so a slightly off lineup never penalises a correctly-rostered setter
33
+ // (validation enforces the opposite-slot rule upstream).
34
+ export function getLineupFunctions(system, designatedSetterIds, starters) {
35
+ const functions = new Map();
36
+ if (system === RotationSystemEnum.SIX_ZERO)
37
+ return functions;
38
+ const mainSetter = starters.find(s => s.playerId === designatedSetterIds[0]);
39
+ if (mainSetter == null)
40
+ return functions;
41
+ for (const slot of starters) {
42
+ functions.set(slot.playerId, slotFunctionAt(system, mainSetter.position, slot.position));
43
+ }
44
+ for (const id of designatedSetterIds) {
45
+ if (functions.has(id))
46
+ functions.set(id, RoleEnum.SETTER);
47
+ }
48
+ return functions;
49
+ }
50
+ // True when a player's slot requires a role they do not hold (i.e. they are played out of position). A missing
51
+ // function (6-0, or unknown player) is never out of position.
52
+ export function isOutOfPosition(roles, fn) {
53
+ if (fn == null)
54
+ return false;
55
+ return !roles.includes(fn);
56
+ }
57
+ // The action multiplier for a player given their slot function: penalised if out of position, else neutral.
58
+ export function outOfPositionPenalty(roles, fn) {
59
+ return isOutOfPosition(roles, fn) ? OUT_OF_POSITION_PENALTY : 1;
60
+ }
@@ -0,0 +1,5 @@
1
+ import { Player } from '../player';
2
+ export interface OffensivePreference {
3
+ readonly rotation: number;
4
+ readonly order: Player[];
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare enum RotationSystemEnum {
2
+ FIVE_ONE = "5-1",
3
+ FOUR_TWO = "4-2",
4
+ SIX_TWO = "6-2",
5
+ SIX_ZERO = "6-0"
6
+ }
7
+ export type RotationSystem = RotationSystemEnum;
8
+ export declare function setterCountFor(system: RotationSystemEnum): number;
@@ -0,0 +1,21 @@
1
+ // The offensive rotation system a team runs. Drives base-positioning, who sets each rally (the rally-setter),
2
+ // and the out-of-position penalty. '6-0' (treat '6-6' as an alias) is the neutral default: no designated setter,
3
+ // base positions = the rotational layout, so an unconfigured team plays exactly as before this feature.
4
+ export var RotationSystemEnum;
5
+ (function (RotationSystemEnum) {
6
+ RotationSystemEnum["FIVE_ONE"] = "5-1";
7
+ RotationSystemEnum["FOUR_TWO"] = "4-2";
8
+ RotationSystemEnum["SIX_TWO"] = "6-2";
9
+ RotationSystemEnum["SIX_ZERO"] = "6-0";
10
+ })(RotationSystemEnum || (RotationSystemEnum = {}));
11
+ // How many designated setters each system expects: 5-1 has one; 4-2 and 6-2 have two (main + the opposite slot,
12
+ // which acts as the second setter); 6-0 has none.
13
+ export function setterCountFor(system) {
14
+ switch (system) {
15
+ case RotationSystemEnum.FIVE_ONE: return 1;
16
+ case RotationSystemEnum.FOUR_TWO: return 2;
17
+ case RotationSystemEnum.SIX_TWO: return 2;
18
+ case RotationSystemEnum.SIX_ZERO: return 0;
19
+ default: return 0;
20
+ }
21
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Player } from '../../player';
3
3
  import { EnergyBand } from '../energy-band';
4
+ import { RotationSystemEnum } from '../rotation-system';
4
5
  export declare const TacticsInputSchema: z.ZodObject<{
5
6
  lineup: z.ZodObject<{
6
7
  4: z.ZodCustom<Player, Player>;
@@ -47,5 +48,11 @@ export declare const TacticsInputSchema: z.ZodObject<{
47
48
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
48
49
  }, z.core.$strip>>>;
49
50
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
51
+ rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
52
+ designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
53
+ offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
54
+ rotation: z.ZodNumber;
55
+ order: z.ZodArray<z.ZodCustom<Player, Player>>;
56
+ }, z.core.$strip>>>;
50
57
  }, z.core.$strip>;
51
58
  export type TacticsInput = z.infer<typeof TacticsInputSchema>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { Player } from '../../player';
3
3
  import { EnergyBand } from '../energy-band';
4
+ import { RotationSystemEnum, setterCountFor } from '../rotation-system';
4
5
  import { DesignatedSubSchema, SubBandSchema } from './designated-sub.z';
5
6
  const playerInstanceSchema = z.custom((v) => v instanceof Player, {
6
7
  message: 'INVALID_PLAYER_INSTANCE'
@@ -43,6 +44,12 @@ const lineupSchema = z.object({
43
44
  });
44
45
  }
45
46
  });
47
+ // One rotation's ranked attacker priority. `rotation` is the main setter's rotational slot (1..6); `order` lists
48
+ // the attackers ranked best-first. Cross-field checks (starters only, no dupes) live in the superRefine below.
49
+ const offensivePreferenceSchema = z.object({
50
+ rotation: z.number().int().min(1).max(6),
51
+ order: z.array(playerInstanceSchema)
52
+ });
46
53
  export const TacticsInputSchema = z.object({
47
54
  lineup: lineupSchema,
48
55
  substitutionTolerance: z.number().int().min(1).max(50),
@@ -53,5 +60,64 @@ export const TacticsInputSchema = z.object({
53
60
  Array.from(v.entries()).every(([k, val]) => k instanceof Player && val instanceof Player), { message: 'INVALID_PINCH_SERVER_SUBS' }),
54
61
  // New (designated-subs feature). Defaults keep every existing Tactics.create({...}) call valid.
55
62
  designatedSubs: z.array(DesignatedSubSchema).default([]),
56
- substitutionBand: SubBandSchema.default(EnergyBand.TIRED)
63
+ substitutionBand: SubBandSchema.default(EnergyBand.TIRED),
64
+ // Advanced tactics (rotation systems). Defaults (6-0 / no setters / no preferences) keep every existing
65
+ // Tactics.create({...}) call valid and make an unconfigured team play exactly as before.
66
+ rotationSystem: z.nativeEnum(RotationSystemEnum).default(RotationSystemEnum.SIX_ZERO),
67
+ designatedSetters: z.array(playerInstanceSchema).default([]),
68
+ offensivePreferences: z.array(offensivePreferenceSchema).default([])
69
+ }).superRefine((data, ctx) => {
70
+ // Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
71
+ // pinch-server outage was caused by an unvalidated tactics payload).
72
+ const slots = [
73
+ [RIGHT_BACK, data.lineup[RIGHT_BACK]],
74
+ [RIGHT_FRONT, data.lineup[RIGHT_FRONT]],
75
+ [MIDDLE_FRONT, data.lineup[MIDDLE_FRONT]],
76
+ [LEFT_FRONT, data.lineup[LEFT_FRONT]],
77
+ [LEFT_BACK, data.lineup[LEFT_BACK]],
78
+ [MIDDLE_BACK, data.lineup[MIDDLE_BACK]]
79
+ ];
80
+ // If the lineup itself failed to parse, its own issues are already reported; skip the cross-field checks
81
+ // rather than dereferencing a missing starter.
82
+ if (slots.some(([, player]) => player == null))
83
+ return;
84
+ const starters = slots.map(([position, player]) => ({ position, id: player.id }));
85
+ const starterIds = new Set(starters.map(s => s.id));
86
+ const positionById = new Map(starters.map(s => [s.id, s.position]));
87
+ // Designated setters: count must match the system, and each must be an on-court starter.
88
+ const expectedSetters = setterCountFor(data.rotationSystem);
89
+ if (data.designatedSetters.length !== expectedSetters) {
90
+ ctx.addIssue({ code: 'custom', message: `DESIGNATED_SETTERS_COUNT_${data.rotationSystem}_EXPECTED_${expectedSetters}`, path: ['designatedSetters'] });
91
+ }
92
+ for (const setter of data.designatedSetters) {
93
+ if (!starterIds.has(setter.id)) {
94
+ ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTER_NOT_A_STARTER', path: ['designatedSetters'] });
95
+ }
96
+ }
97
+ // 4-2 / 6-2: the two setters sit in opposite slots (3 rotations apart).
98
+ if ((data.rotationSystem === RotationSystemEnum.FOUR_TWO || data.rotationSystem === RotationSystemEnum.SIX_TWO) &&
99
+ data.designatedSetters.length === 2) {
100
+ const a = positionById.get(data.designatedSetters[0].id);
101
+ const b = positionById.get(data.designatedSetters[1].id);
102
+ if (a != null && b != null && ((((a - b) % 6) + 6) % 6) !== 3) {
103
+ ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS', path: ['designatedSetters'] });
104
+ }
105
+ }
106
+ // Offensive preferences: unique rotations, and each order lists only on-court starters with no duplicates.
107
+ const seenRotations = new Set();
108
+ for (const pref of data.offensivePreferences) {
109
+ if (seenRotations.has(pref.rotation)) {
110
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path: ['offensivePreferences'] });
111
+ }
112
+ seenRotations.add(pref.rotation);
113
+ const ids = pref.order.map(p => p.id);
114
+ if (new Set(ids).size !== ids.length) {
115
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path: ['offensivePreferences'] });
116
+ }
117
+ for (const id of ids) {
118
+ if (!starterIds.has(id)) {
119
+ ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path: ['offensivePreferences'] });
120
+ }
121
+ }
122
+ }
57
123
  });
@@ -55,6 +55,12 @@ export declare const TeamInputSchema: z.ZodObject<{
55
55
  conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
56
56
  }, z.core.$strip>>>;
57
57
  substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof import("..").EnergyBand>, z.ZodLiteral<"NEVER">]>>;
58
+ rotationSystem: z.ZodDefault<z.ZodEnum<typeof import("..").RotationSystemEnum>>;
59
+ designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
60
+ offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
61
+ rotation: z.ZodNumber;
62
+ order: z.ZodArray<z.ZodCustom<Player, Player>>;
63
+ }, z.core.$strip>>>;
58
64
  }, z.core.$strip>>;
59
65
  }, z.core.$strip>;
60
66
  export type TeamInput = z.infer<typeof TeamInputSchema>;
@@ -2,6 +2,8 @@ import { CourtPosition } from '../match';
2
2
  import { Player } from '../player';
3
3
  import { DesignatedSub } from './designated-sub';
4
4
  import { SubBand } from './energy-band';
5
+ import { RotationSystemEnum } from './rotation-system';
6
+ import { OffensivePreference } from './offensive-preference';
5
7
  export interface StartingLineup {
6
8
  readonly [CourtPosition.LEFT_FRONT]: Player;
7
9
  readonly [CourtPosition.MIDDLE_FRONT]: Player;
@@ -20,6 +22,9 @@ export interface TacticsOpts {
20
22
  readonly receiveRotationOffset: boolean;
21
23
  readonly designatedSubs: DesignatedSub[];
22
24
  readonly substitutionBand: SubBand;
25
+ readonly rotationSystem: RotationSystemEnum;
26
+ readonly designatedSetters: Player[];
27
+ readonly offensivePreferences: OffensivePreference[];
23
28
  }
24
29
  export declare class Tactics {
25
30
  readonly lineup: StartingLineup;
@@ -29,6 +34,9 @@ export declare class Tactics {
29
34
  readonly receiveRotationOffset: boolean;
30
35
  readonly designatedSubs: DesignatedSub[];
31
36
  readonly substitutionBand: SubBand;
37
+ readonly rotationSystem: RotationSystemEnum;
38
+ readonly designatedSetters: Player[];
39
+ readonly offensivePreferences: OffensivePreference[];
32
40
  static create(input: unknown): Tactics;
33
41
  private constructor();
34
42
  findPlayerInLineup(id: string): CourtPosition | undefined;
@@ -1,5 +1,6 @@
1
1
  import { CourtPosition } from '../match';
2
2
  import { EnergyBand } from './energy-band';
3
+ import { RotationSystemEnum } from './rotation-system';
3
4
  import { TacticsInputSchema } from './schemas/tactics.z';
4
5
  export class Tactics {
5
6
  static create(input) {
@@ -14,7 +15,7 @@ export class Tactics {
14
15
  }
15
16
  return new Tactics(result.data);
16
17
  }
17
- constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED }) {
18
+ constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [] }) {
18
19
  this.lineup = lineup;
19
20
  this.substitutionTolerance = substitutionTolerance;
20
21
  this.pinchServerSubs = pinchServerSubs;
@@ -22,6 +23,9 @@ export class Tactics {
22
23
  this.receiveRotationOffset = receiveRotationOffset;
23
24
  this.designatedSubs = designatedSubs;
24
25
  this.substitutionBand = substitutionBand;
26
+ this.rotationSystem = rotationSystem;
27
+ this.designatedSetters = designatedSetters;
28
+ this.offensivePreferences = offensivePreferences;
25
29
  }
26
30
  findPlayerInLineup(id) {
27
31
  if (this.lineup.bench.some(p => p.id === id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.393",
3
+ "version": "0.0.395",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",