volleyballsimtypes 0.0.388 → 0.0.390

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 (47) hide show
  1. package/dist/cjs/src/api/index.d.ts +11 -1
  2. package/dist/cjs/src/data/models/tactics.d.ts +14 -2
  3. package/dist/cjs/src/data/models/tactics.js +10 -0
  4. package/dist/cjs/src/data/transformers/tactics.js +20 -2
  5. package/dist/cjs/src/service/competition/standing.js +15 -8
  6. package/dist/cjs/src/service/competition/standing.test.js +12 -0
  7. package/dist/cjs/src/service/team/designated-sub.d.ts +11 -0
  8. package/dist/cjs/src/service/team/designated-sub.js +2 -0
  9. package/dist/cjs/src/service/team/energy-band.d.ts +8 -0
  10. package/dist/cjs/src/service/team/energy-band.js +20 -0
  11. package/dist/cjs/src/service/team/index.d.ts +3 -0
  12. package/dist/cjs/src/service/team/index.js +3 -0
  13. package/dist/cjs/src/service/team/pinch-condition.d.ts +56 -0
  14. package/dist/cjs/src/service/team/pinch-condition.js +27 -0
  15. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +59 -0
  16. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +39 -0
  17. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +32 -0
  18. package/dist/cjs/src/service/team/schemas/tactics.z.js +8 -2
  19. package/dist/cjs/src/service/team/schemas/team.z.d.ts +31 -0
  20. package/dist/cjs/src/service/team/tactics.d.ts +6 -0
  21. package/dist/cjs/src/service/team/tactics.js +4 -1
  22. package/dist/cjs/src/service/team/team-generator.d.ts +2 -0
  23. package/dist/cjs/src/service/team/team-generator.js +7 -3
  24. package/dist/esm/src/api/index.d.ts +11 -1
  25. package/dist/esm/src/data/models/tactics.d.ts +14 -2
  26. package/dist/esm/src/data/models/tactics.js +10 -0
  27. package/dist/esm/src/data/transformers/tactics.js +20 -2
  28. package/dist/esm/src/service/competition/standing.js +15 -8
  29. package/dist/esm/src/service/competition/standing.test.js +12 -0
  30. package/dist/esm/src/service/team/designated-sub.d.ts +11 -0
  31. package/dist/esm/src/service/team/designated-sub.js +1 -0
  32. package/dist/esm/src/service/team/energy-band.d.ts +8 -0
  33. package/dist/esm/src/service/team/energy-band.js +17 -0
  34. package/dist/esm/src/service/team/index.d.ts +3 -0
  35. package/dist/esm/src/service/team/index.js +3 -0
  36. package/dist/esm/src/service/team/pinch-condition.d.ts +56 -0
  37. package/dist/esm/src/service/team/pinch-condition.js +24 -0
  38. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +59 -0
  39. package/dist/esm/src/service/team/schemas/designated-sub.z.js +36 -0
  40. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +32 -0
  41. package/dist/esm/src/service/team/schemas/tactics.z.js +8 -2
  42. package/dist/esm/src/service/team/schemas/team.z.d.ts +31 -0
  43. package/dist/esm/src/service/team/tactics.d.ts +6 -0
  44. package/dist/esm/src/service/team/tactics.js +4 -1
  45. package/dist/esm/src/service/team/team-generator.d.ts +2 -0
  46. package/dist/esm/src/service/team/team-generator.js +7 -3
  47. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition } from '../service';
1
+ import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, PinchCondition, SubBand } from '../service';
2
2
  export type Rally = DataProps<_Rally> & {
3
3
  homePlayerPosition: PlayerPosition[];
4
4
  awayPlayerPosition: PlayerPosition[];
@@ -56,12 +56,22 @@ export interface StartingLineup {
56
56
  [CourtPosition.LIBERO_ZONE]?: string;
57
57
  bench: string[];
58
58
  }
59
+ export interface ApiDesignatedSub {
60
+ starterId: string;
61
+ benchId?: string;
62
+ isPinchServer: boolean;
63
+ fatigueBand?: SubBand;
64
+ conditions?: PinchCondition[];
65
+ conditionLogic?: ConditionLogic;
66
+ }
59
67
  export interface Tactics {
60
68
  lineup: StartingLineup;
61
69
  substitutionTolerance: number;
62
70
  liberoReplacements: string[];
63
71
  pinchServerSubs: Record<string, string>;
64
72
  receiveRotationOffset: boolean;
73
+ designatedSubs: ApiDesignatedSub[];
74
+ substitutionBand: SubBand;
65
75
  }
66
76
  export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
67
77
  roster: Player[];
@@ -1,7 +1,7 @@
1
1
  import * as Sequelize from 'sequelize';
2
2
  import { Model, Optional } from 'sequelize';
3
3
  import { PlayerId, TeamId, TeamModel } from '.';
4
- import { CourtPosition } from '../../service';
4
+ import { ConditionLogic, CourtPosition, PinchCondition, SubBand } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -13,6 +13,14 @@ export interface TacticsLineupAttributes {
13
13
  bench: PlayerId[];
14
14
  }
15
15
  export type PinchServerSubsAttributes = Record<PlayerId, PlayerId>;
16
+ export interface DesignatedSubAttributes {
17
+ starterId: PlayerId;
18
+ benchId?: PlayerId;
19
+ isPinchServer: boolean;
20
+ fatigueBand?: SubBand;
21
+ conditions?: PinchCondition[];
22
+ conditionLogic?: ConditionLogic;
23
+ }
16
24
  export interface TacticsAttributes {
17
25
  team_id: string;
18
26
  substitution_tolerance: number;
@@ -20,10 +28,12 @@ export interface TacticsAttributes {
20
28
  libero_replacements: PlayerId[];
21
29
  pinch_server_subs: PinchServerSubsAttributes;
22
30
  receive_rotation_offset: boolean;
31
+ designated_subs: DesignatedSubAttributes[];
32
+ substitution_band: SubBand;
23
33
  }
24
34
  export type TacticsPk = 'team_id';
25
35
  export type TacticsId = TacticsModel[TacticsPk];
26
- export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset';
36
+ export type TacticsOptionalAttributes = 'substitution_tolerance' | 'lineup' | 'libero_replacements' | 'pinch_server_subs' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band';
27
37
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
28
38
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
29
39
  team_id: string;
@@ -32,6 +42,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
32
42
  libero_replacements: PlayerId[];
33
43
  pinch_server_subs: PinchServerSubsAttributes;
34
44
  receive_rotation_offset: boolean;
45
+ designated_subs: DesignatedSubAttributes[];
46
+ substitution_band: SubBand;
35
47
  team: TeamModel;
36
48
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
37
49
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -39,6 +39,16 @@ class TacticsModel extends sequelize_1.Model {
39
39
  type: sequelize_1.DataTypes.BOOLEAN,
40
40
  allowNull: false,
41
41
  defaultValue: false
42
+ },
43
+ designated_subs: {
44
+ type: sequelize_1.DataTypes.JSONB,
45
+ allowNull: false,
46
+ defaultValue: []
47
+ },
48
+ substitution_band: {
49
+ type: sequelize_1.DataTypes.STRING,
50
+ allowNull: false,
51
+ defaultValue: 'TIRED'
42
52
  }
43
53
  }, {
44
54
  sequelize,
@@ -40,7 +40,16 @@ function transformToAttributes(tactics, teamId) {
40
40
  receive_rotation_offset: tactics.receiveRotationOffset ?? false,
41
41
  lineup: transformFromLineup(tactics.lineup),
42
42
  libero_replacements: tactics.liberoReplacements.map((lr) => lr.id),
43
- pinch_server_subs: Object.fromEntries([...tactics.pinchServerSubs].map(([k, v]) => [k.id, v.id]))
43
+ pinch_server_subs: Object.fromEntries([...tactics.pinchServerSubs].map(([k, v]) => [k.id, v.id])),
44
+ substitution_band: tactics.substitutionBand,
45
+ designated_subs: tactics.designatedSubs.map(ds => ({
46
+ starterId: ds.starter.id,
47
+ benchId: ds.bench?.id,
48
+ isPinchServer: ds.isPinchServer,
49
+ fatigueBand: ds.fatigueBand,
50
+ conditions: ds.conditions,
51
+ conditionLogic: ds.conditionLogic
52
+ }))
44
53
  };
45
54
  }
46
55
  function buildPinchServerSubs(pinchServerSubs, roster) {
@@ -56,6 +65,15 @@ function transformToObject(model, roster) {
56
65
  receiveRotationOffset: model.receive_rotation_offset,
57
66
  lineup: transformToLineup(model.lineup, roster),
58
67
  liberoReplacements: model.libero_replacements.map((lr) => findPlayer(lr, roster)),
59
- pinchServerSubs: buildPinchServerSubs(model.pinch_server_subs, roster)
68
+ pinchServerSubs: buildPinchServerSubs(model.pinch_server_subs, roster),
69
+ substitutionBand: model.substitution_band,
70
+ designatedSubs: (model.designated_subs ?? []).map(d => ({
71
+ starter: findPlayer(d.starterId, roster),
72
+ bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
73
+ isPinchServer: d.isPinchServer,
74
+ fatigueBand: d.fatigueBand,
75
+ conditions: d.conditions,
76
+ conditionLogic: d.conditionLogic
77
+ }))
60
78
  });
61
79
  }
@@ -36,20 +36,28 @@ class Standing {
36
36
  this.totalMatches = this.matchesWon + this.matchesLost;
37
37
  }
38
38
  static sortFn(matches) {
39
+ // A zero denominator (no sets/points lost) is the BEST possible ratio, not the worst. The stored
40
+ // setsRatio/pointsRatio fall back to 0 in that case (and the table shows "MAX"), so we compare an
41
+ // effective ratio here that treats "won with nothing lost" as +Infinity, ranking it highest.
42
+ const effectiveRatio = (won, lost) => lost > 0 ? won / lost : (won > 0 ? Infinity : 0);
39
43
  return (standingA, standingB) => {
40
44
  if (standingB.points > standingA.points)
41
45
  return 1;
42
- else if (standingA.points > standingB.points)
46
+ if (standingA.points > standingB.points)
43
47
  return -1;
44
- else if (standingB.setsRatio > standingA.setsRatio)
48
+ const setRatioA = effectiveRatio(standingA.setsWon, standingA.setsLost);
49
+ const setRatioB = effectiveRatio(standingB.setsWon, standingB.setsLost);
50
+ if (setRatioB > setRatioA)
45
51
  return 1;
46
- else if (standingA.setsRatio > standingB.setsRatio)
52
+ if (setRatioA > setRatioB)
47
53
  return -1;
48
- else if (standingB.pointsRatio > standingA.pointsRatio)
54
+ const pointRatioA = effectiveRatio(standingA.pointsWon, standingA.pointsLost);
55
+ const pointRatioB = effectiveRatio(standingB.pointsWon, standingB.pointsLost);
56
+ if (pointRatioB > pointRatioA)
49
57
  return 1;
50
- else if (standingA.pointsRatio > standingB.pointsRatio)
58
+ if (pointRatioA > pointRatioB)
51
59
  return -1;
52
- else if (matches.length > 0) {
60
+ if (matches.length > 0) {
53
61
  const ab = matches.find((match) => match.homeTeam.id === standingA.teamId && match.awayTeam.id === standingB.teamId);
54
62
  const ba = matches.find((match) => match.awayTeam.id === standingA.teamId && match.homeTeam.id === standingB.teamId);
55
63
  if (ba == null && ab == null)
@@ -66,8 +74,7 @@ class Standing {
66
74
  const b = (ba.homeScore ?? 0) + (ab.awayScore ?? 0);
67
75
  return b - a;
68
76
  }
69
- else
70
- return 0;
77
+ return 0;
71
78
  };
72
79
  }
73
80
  calculateStanding(matches) {
@@ -136,6 +136,18 @@ function makeStanding(teamId, opts = {}) {
136
136
  const sorted = [b, a].sort(standing_1.Standing.sortFn([]));
137
137
  (0, globals_1.expect)(sorted[0].teamId).toBe(idA);
138
138
  });
139
+ (0, globals_1.it)('ranks a team with no sets lost (MAX ratio) above a finite setsRatio when points tie', () => {
140
+ const idA = (0, uuid_1.v4)();
141
+ const idB = (0, uuid_1.v4)();
142
+ // Both 6 points. A swept its matches (won30=2 → setsWon=6, setsLost=0 → ratio is MAX/infinite);
143
+ // B also has 6 points but conceded sets (won31=2 → setsWon=6, setsLost=2 → setsRatio=3).
144
+ // The undefeated-in-sets team must rank FIRST, not last (the divide-by-zero bug ranked it last).
145
+ const a = makeStanding(idA, { won30: 2 });
146
+ const b = makeStanding(idB, { won31: 2 });
147
+ const sorted = [b, a].sort(standing_1.Standing.sortFn([]));
148
+ (0, globals_1.expect)(sorted[0].teamId).toBe(idA);
149
+ (0, globals_1.expect)(sorted[1].teamId).toBe(idB);
150
+ });
139
151
  (0, globals_1.it)('falls back to pointsRatio when points and setsRatio are equal', () => {
140
152
  const idA = (0, uuid_1.v4)();
141
153
  const idB = (0, uuid_1.v4)();
@@ -0,0 +1,11 @@
1
+ import { Player } from '../player';
2
+ import { SubBand } from './energy-band';
3
+ import { ConditionLogic, PinchCondition } from './pinch-condition';
4
+ export interface DesignatedSub {
5
+ readonly starter: Player;
6
+ readonly bench?: Player;
7
+ readonly isPinchServer: boolean;
8
+ readonly fatigueBand?: SubBand;
9
+ readonly conditions?: PinchCondition[];
10
+ readonly conditionLogic?: ConditionLogic;
11
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,8 @@
1
+ export declare enum EnergyBand {
2
+ ENERGETIC = "ENERGETIC",
3
+ WORN = "WORN",
4
+ TIRED = "TIRED",
5
+ EXHAUSTED = "EXHAUSTED"
6
+ }
7
+ export type SubBand = EnergyBand | 'NEVER';
8
+ export declare const ENERGY_BAND_ORDER: readonly EnergyBand[];
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENERGY_BAND_ORDER = exports.EnergyBand = void 0;
4
+ // The named fatigue bands a player's energy falls into. The numeric cutoffs and per-band performance
5
+ // multipliers live in the simulator (VolleyballSim court-player.ts ENERGY_BANDS); this enum is the shared
6
+ // vocabulary used by tactics configuration (substitution thresholds) and, later, the UI.
7
+ var EnergyBand;
8
+ (function (EnergyBand) {
9
+ EnergyBand["ENERGETIC"] = "ENERGETIC";
10
+ EnergyBand["WORN"] = "WORN";
11
+ EnergyBand["TIRED"] = "TIRED";
12
+ EnergyBand["EXHAUSTED"] = "EXHAUSTED";
13
+ })(EnergyBand || (exports.EnergyBand = EnergyBand = {}));
14
+ // Ordered best -> worst. Used to compare "is this player at-or-below the configured sub band".
15
+ exports.ENERGY_BAND_ORDER = [
16
+ EnergyBand.ENERGETIC,
17
+ EnergyBand.WORN,
18
+ EnergyBand.TIRED,
19
+ EnergyBand.EXHAUSTED
20
+ ];
@@ -3,3 +3,6 @@ export * from './team-generator';
3
3
  export * from './formation';
4
4
  export * from './tactics';
5
5
  export * from './team-name';
6
+ export * from './energy-band';
7
+ export * from './pinch-condition';
8
+ export * from './designated-sub';
@@ -19,3 +19,6 @@ __exportStar(require("./team-generator"), exports);
19
19
  __exportStar(require("./formation"), exports);
20
20
  __exportStar(require("./tactics"), exports);
21
21
  __exportStar(require("./team-name"), exports);
22
+ __exportStar(require("./energy-band"), exports);
23
+ __exportStar(require("./pinch-condition"), exports);
24
+ __exportStar(require("./designated-sub"), exports);
@@ -0,0 +1,56 @@
1
+ export declare enum PinchConditionType {
2
+ ASAP = "ASAP",// every time the linked starter wins back the serve
3
+ TEAM_SET_POINT = "TEAM_SET_POINT",// team within `margin` of winning the set
4
+ OPPONENT_SET_POINT = "OPPONENT_SET_POINT",// opponent within `margin` of winning the set
5
+ SCORE_DIFF = "SCORE_DIFF",// team trailing/leading by at least `points`
6
+ AFTER_ROTATIONS = "AFTER_ROTATIONS",// from the team's Nth serve turn in the set onward
7
+ FROM_SET = "FROM_SET",// only set N or later
8
+ MIN_OWN_SCORE = "MIN_OWN_SCORE"
9
+ }
10
+ export declare enum OpponentRelation {
11
+ AHEAD = "AHEAD",
12
+ BEHIND = "BEHIND",
13
+ EITHER = "EITHER"
14
+ }
15
+ export declare enum ScoreDirection {
16
+ TRAILING = "TRAILING",
17
+ LEADING = "LEADING"
18
+ }
19
+ export interface AsapCondition {
20
+ readonly type: PinchConditionType.ASAP;
21
+ }
22
+ export interface TeamSetPointCondition {
23
+ readonly type: PinchConditionType.TEAM_SET_POINT;
24
+ readonly margin: number;
25
+ readonly opponent: OpponentRelation;
26
+ }
27
+ export interface OpponentSetPointCondition {
28
+ readonly type: PinchConditionType.OPPONENT_SET_POINT;
29
+ readonly margin: number;
30
+ }
31
+ export interface ScoreDiffCondition {
32
+ readonly type: PinchConditionType.SCORE_DIFF;
33
+ readonly direction: ScoreDirection;
34
+ readonly points: number;
35
+ }
36
+ export interface AfterRotationsCondition {
37
+ readonly type: PinchConditionType.AFTER_ROTATIONS;
38
+ readonly rotations: number;
39
+ }
40
+ export interface FromSetCondition {
41
+ readonly type: PinchConditionType.FROM_SET;
42
+ readonly set: number;
43
+ }
44
+ export interface MinOwnScoreCondition {
45
+ readonly type: PinchConditionType.MIN_OWN_SCORE;
46
+ readonly score: number;
47
+ }
48
+ export type PinchCondition = AsapCondition | TeamSetPointCondition | OpponentSetPointCondition | ScoreDiffCondition | AfterRotationsCondition | FromSetCondition | MinOwnScoreCondition;
49
+ export type ConditionLogic = 'ALL' | 'ANY';
50
+ export interface PinchSetState {
51
+ readonly ownScore: number;
52
+ readonly opponentScore: number;
53
+ readonly setTarget: number;
54
+ readonly setNumber: number;
55
+ readonly ownServeTurns: number;
56
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ // Conditions that gate when a pinch server is forced in to serve (in for its linked starter when the team
3
+ // wins back the serve, out the moment the serve is lost). Each condition is evaluated against the live set
4
+ // state; a pinch entry combines them with ALL (every condition must hold) or ANY (one is enough).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ScoreDirection = exports.OpponentRelation = exports.PinchConditionType = void 0;
7
+ var PinchConditionType;
8
+ (function (PinchConditionType) {
9
+ PinchConditionType["ASAP"] = "ASAP";
10
+ PinchConditionType["TEAM_SET_POINT"] = "TEAM_SET_POINT";
11
+ PinchConditionType["OPPONENT_SET_POINT"] = "OPPONENT_SET_POINT";
12
+ PinchConditionType["SCORE_DIFF"] = "SCORE_DIFF";
13
+ PinchConditionType["AFTER_ROTATIONS"] = "AFTER_ROTATIONS";
14
+ PinchConditionType["FROM_SET"] = "FROM_SET";
15
+ PinchConditionType["MIN_OWN_SCORE"] = "MIN_OWN_SCORE"; // only once own score >= N
16
+ })(PinchConditionType || (exports.PinchConditionType = PinchConditionType = {}));
17
+ var OpponentRelation;
18
+ (function (OpponentRelation) {
19
+ OpponentRelation["AHEAD"] = "AHEAD";
20
+ OpponentRelation["BEHIND"] = "BEHIND";
21
+ OpponentRelation["EITHER"] = "EITHER";
22
+ })(OpponentRelation || (exports.OpponentRelation = OpponentRelation = {}));
23
+ var ScoreDirection;
24
+ (function (ScoreDirection) {
25
+ ScoreDirection["TRAILING"] = "TRAILING";
26
+ ScoreDirection["LEADING"] = "LEADING";
27
+ })(ScoreDirection || (exports.ScoreDirection = ScoreDirection = {}));
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { Player } from '../../player';
3
+ import { EnergyBand } from '../energy-band';
4
+ import { OpponentRelation, PinchConditionType, ScoreDirection } from '../pinch-condition';
5
+ export declare const SubBandSchema: z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>;
6
+ export declare const PinchConditionSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
7
+ type: z.ZodLiteral<PinchConditionType.ASAP>;
8
+ }, z.core.$strip>, z.ZodObject<{
9
+ type: z.ZodLiteral<PinchConditionType.TEAM_SET_POINT>;
10
+ margin: z.ZodNumber;
11
+ opponent: z.ZodEnum<typeof OpponentRelation>;
12
+ }, z.core.$strip>, z.ZodObject<{
13
+ type: z.ZodLiteral<PinchConditionType.OPPONENT_SET_POINT>;
14
+ margin: z.ZodNumber;
15
+ }, z.core.$strip>, z.ZodObject<{
16
+ type: z.ZodLiteral<PinchConditionType.SCORE_DIFF>;
17
+ direction: z.ZodEnum<typeof ScoreDirection>;
18
+ points: z.ZodNumber;
19
+ }, z.core.$strip>, z.ZodObject<{
20
+ type: z.ZodLiteral<PinchConditionType.AFTER_ROTATIONS>;
21
+ rotations: z.ZodNumber;
22
+ }, z.core.$strip>, z.ZodObject<{
23
+ type: z.ZodLiteral<PinchConditionType.FROM_SET>;
24
+ set: z.ZodNumber;
25
+ }, z.core.$strip>, z.ZodObject<{
26
+ type: z.ZodLiteral<PinchConditionType.MIN_OWN_SCORE>;
27
+ score: z.ZodNumber;
28
+ }, z.core.$strip>], "type">;
29
+ export declare const ConditionLogicSchema: z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>;
30
+ export declare const DesignatedSubSchema: z.ZodObject<{
31
+ starter: z.ZodCustom<Player, Player>;
32
+ bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
33
+ isPinchServer: z.ZodBoolean;
34
+ fatigueBand: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
35
+ conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
36
+ type: z.ZodLiteral<PinchConditionType.ASAP>;
37
+ }, z.core.$strip>, z.ZodObject<{
38
+ type: z.ZodLiteral<PinchConditionType.TEAM_SET_POINT>;
39
+ margin: z.ZodNumber;
40
+ opponent: z.ZodEnum<typeof OpponentRelation>;
41
+ }, z.core.$strip>, z.ZodObject<{
42
+ type: z.ZodLiteral<PinchConditionType.OPPONENT_SET_POINT>;
43
+ margin: z.ZodNumber;
44
+ }, z.core.$strip>, z.ZodObject<{
45
+ type: z.ZodLiteral<PinchConditionType.SCORE_DIFF>;
46
+ direction: z.ZodEnum<typeof ScoreDirection>;
47
+ points: z.ZodNumber;
48
+ }, z.core.$strip>, z.ZodObject<{
49
+ type: z.ZodLiteral<PinchConditionType.AFTER_ROTATIONS>;
50
+ rotations: z.ZodNumber;
51
+ }, z.core.$strip>, z.ZodObject<{
52
+ type: z.ZodLiteral<PinchConditionType.FROM_SET>;
53
+ set: z.ZodNumber;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ type: z.ZodLiteral<PinchConditionType.MIN_OWN_SCORE>;
56
+ score: z.ZodNumber;
57
+ }, z.core.$strip>], "type">>>;
58
+ conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
59
+ }, z.core.$strip>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DesignatedSubSchema = exports.ConditionLogicSchema = exports.PinchConditionSchema = exports.SubBandSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const player_1 = require("../../player");
6
+ const energy_band_1 = require("../energy-band");
7
+ const pinch_condition_1 = require("../pinch-condition");
8
+ const playerInstanceSchema = zod_1.z.custom((v) => v instanceof player_1.Player, {
9
+ message: 'INVALID_PLAYER_INSTANCE'
10
+ });
11
+ exports.SubBandSchema = zod_1.z.union([zod_1.z.nativeEnum(energy_band_1.EnergyBand), zod_1.z.literal('NEVER')]);
12
+ exports.PinchConditionSchema = zod_1.z.discriminatedUnion('type', [
13
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.ASAP) }),
14
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.TEAM_SET_POINT), margin: zod_1.z.number().int().min(1).max(25), opponent: zod_1.z.nativeEnum(pinch_condition_1.OpponentRelation) }),
15
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.OPPONENT_SET_POINT), margin: zod_1.z.number().int().min(1).max(25) }),
16
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.SCORE_DIFF), direction: zod_1.z.nativeEnum(pinch_condition_1.ScoreDirection), points: zod_1.z.number().int().min(1).max(25) }),
17
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.AFTER_ROTATIONS), rotations: zod_1.z.number().int().min(1).max(6) }),
18
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.FROM_SET), set: zod_1.z.number().int().min(1).max(5) }),
19
+ zod_1.z.object({ type: zod_1.z.literal(pinch_condition_1.PinchConditionType.MIN_OWN_SCORE), score: zod_1.z.number().int().min(0).max(40) })
20
+ ]);
21
+ exports.ConditionLogicSchema = zod_1.z.union([zod_1.z.literal('ALL'), zod_1.z.literal('ANY')]);
22
+ exports.DesignatedSubSchema = zod_1.z.object({
23
+ starter: playerInstanceSchema,
24
+ bench: playerInstanceSchema.optional(),
25
+ isPinchServer: zod_1.z.boolean(),
26
+ fatigueBand: exports.SubBandSchema.optional(),
27
+ conditions: zod_1.z.array(exports.PinchConditionSchema).optional(),
28
+ conditionLogic: exports.ConditionLogicSchema.optional()
29
+ }).superRefine((data, ctx) => {
30
+ if (data.isPinchServer) {
31
+ // Pinch mode needs a designated bench player (the server) and at least one condition.
32
+ if (data.bench == null) {
33
+ ctx.addIssue({ code: 'custom', message: 'PINCH_SERVER_REQUIRES_BENCH', path: ['bench'] });
34
+ }
35
+ if (data.conditions == null || data.conditions.length === 0) {
36
+ ctx.addIssue({ code: 'custom', message: 'PINCH_SERVER_REQUIRES_CONDITIONS', path: ['conditions'] });
37
+ }
38
+ }
39
+ });
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { Player } from '../../player';
3
+ import { EnergyBand } from '../energy-band';
3
4
  export declare const TacticsInputSchema: z.ZodObject<{
4
5
  lineup: z.ZodObject<{
5
6
  4: z.ZodCustom<Player, Player>;
@@ -15,5 +16,36 @@ export declare const TacticsInputSchema: z.ZodObject<{
15
16
  liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
16
17
  receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
17
18
  pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
19
+ designatedSubs: z.ZodDefault<z.ZodArray<z.ZodObject<{
20
+ starter: z.ZodCustom<Player, Player>;
21
+ bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
22
+ isPinchServer: z.ZodBoolean;
23
+ fatigueBand: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
24
+ conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
25
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
26
+ }, z.core.$strip>, z.ZodObject<{
27
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
28
+ margin: z.ZodNumber;
29
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
30
+ }, z.core.$strip>, z.ZodObject<{
31
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
32
+ margin: z.ZodNumber;
33
+ }, z.core.$strip>, z.ZodObject<{
34
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
35
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
36
+ points: z.ZodNumber;
37
+ }, z.core.$strip>, z.ZodObject<{
38
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
39
+ rotations: z.ZodNumber;
40
+ }, z.core.$strip>, z.ZodObject<{
41
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
42
+ set: z.ZodNumber;
43
+ }, z.core.$strip>, z.ZodObject<{
44
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
45
+ score: z.ZodNumber;
46
+ }, z.core.$strip>], "type">>>;
47
+ conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
48
+ }, z.core.$strip>>>;
49
+ substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof EnergyBand>, z.ZodLiteral<"NEVER">]>>;
18
50
  }, z.core.$strip>;
19
51
  export type TacticsInput = z.infer<typeof TacticsInputSchema>;
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TacticsInputSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const player_1 = require("../../player");
6
+ const energy_band_1 = require("../energy-band");
7
+ const designated_sub_z_1 = require("./designated-sub.z");
6
8
  const playerInstanceSchema = zod_1.z.custom((v) => v instanceof player_1.Player, {
7
9
  message: 'INVALID_PLAYER_INSTANCE'
8
10
  });
@@ -21,7 +23,8 @@ const lineupSchema = zod_1.z.object({
21
23
  [MIDDLE_BACK]: playerInstanceSchema,
22
24
  [RIGHT_BACK]: playerInstanceSchema,
23
25
  [LIBERO_ZONE]: playerInstanceSchema.optional(),
24
- bench: zod_1.z.array(playerInstanceSchema)
26
+ // FIVB: up to 6 substitutes (the libero is tracked separately in LIBERO_ZONE).
27
+ bench: zod_1.z.array(playerInstanceSchema).max(6)
25
28
  }).superRefine((data, ctx) => {
26
29
  const players = [
27
30
  data[LEFT_FRONT],
@@ -50,5 +53,8 @@ exports.TacticsInputSchema = zod_1.z.object({
50
53
  // Default false keeps every existing Tactics.create({...}) call (that omits this) valid.
51
54
  receiveRotationOffset: zod_1.z.boolean().default(false),
52
55
  pinchServerSubs: zod_1.z.custom((v) => v instanceof Map &&
53
- Array.from(v.entries()).every(([k, val]) => k instanceof player_1.Player && val instanceof player_1.Player), { message: 'INVALID_PINCH_SERVER_SUBS' })
56
+ Array.from(v.entries()).every(([k, val]) => k instanceof player_1.Player && val instanceof player_1.Player), { message: 'INVALID_PINCH_SERVER_SUBS' }),
57
+ // New (designated-subs feature). Defaults keep every existing Tactics.create({...}) call valid.
58
+ designatedSubs: zod_1.z.array(designated_sub_z_1.DesignatedSubSchema).default([]),
59
+ substitutionBand: designated_sub_z_1.SubBandSchema.default(energy_band_1.EnergyBand.TIRED)
54
60
  });
@@ -24,6 +24,37 @@ export declare const TeamInputSchema: z.ZodObject<{
24
24
  liberoReplacements: z.ZodArray<z.ZodCustom<Player, Player>>;
25
25
  receiveRotationOffset: z.ZodDefault<z.ZodBoolean>;
26
26
  pinchServerSubs: z.ZodCustom<Map<Player, Player>, Map<Player, Player>>;
27
+ designatedSubs: z.ZodDefault<z.ZodArray<z.ZodObject<{
28
+ starter: z.ZodCustom<Player, Player>;
29
+ bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
30
+ isPinchServer: z.ZodBoolean;
31
+ fatigueBand: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<typeof import("..").EnergyBand>, z.ZodLiteral<"NEVER">]>>;
32
+ conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
+ type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
34
+ }, z.core.$strip>, z.ZodObject<{
35
+ type: z.ZodLiteral<import("..").PinchConditionType.TEAM_SET_POINT>;
36
+ margin: z.ZodNumber;
37
+ opponent: z.ZodEnum<typeof import("..").OpponentRelation>;
38
+ }, z.core.$strip>, z.ZodObject<{
39
+ type: z.ZodLiteral<import("..").PinchConditionType.OPPONENT_SET_POINT>;
40
+ margin: z.ZodNumber;
41
+ }, z.core.$strip>, z.ZodObject<{
42
+ type: z.ZodLiteral<import("..").PinchConditionType.SCORE_DIFF>;
43
+ direction: z.ZodEnum<typeof import("..").ScoreDirection>;
44
+ points: z.ZodNumber;
45
+ }, z.core.$strip>, z.ZodObject<{
46
+ type: z.ZodLiteral<import("..").PinchConditionType.AFTER_ROTATIONS>;
47
+ rotations: z.ZodNumber;
48
+ }, z.core.$strip>, z.ZodObject<{
49
+ type: z.ZodLiteral<import("..").PinchConditionType.FROM_SET>;
50
+ set: z.ZodNumber;
51
+ }, z.core.$strip>, z.ZodObject<{
52
+ type: z.ZodLiteral<import("..").PinchConditionType.MIN_OWN_SCORE>;
53
+ score: z.ZodNumber;
54
+ }, z.core.$strip>], "type">>>;
55
+ conditionLogic: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"ALL">, z.ZodLiteral<"ANY">]>>;
56
+ }, z.core.$strip>>>;
57
+ substitutionBand: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<typeof import("..").EnergyBand>, z.ZodLiteral<"NEVER">]>>;
27
58
  }, z.core.$strip>>;
28
59
  }, z.core.$strip>;
29
60
  export type TeamInput = z.infer<typeof TeamInputSchema>;
@@ -1,5 +1,7 @@
1
1
  import { CourtPosition } from '../match';
2
2
  import { Player } from '../player';
3
+ import { DesignatedSub } from './designated-sub';
4
+ import { SubBand } from './energy-band';
3
5
  export interface StartingLineup {
4
6
  readonly [CourtPosition.LEFT_FRONT]: Player;
5
7
  readonly [CourtPosition.MIDDLE_FRONT]: Player;
@@ -16,6 +18,8 @@ export interface TacticsOpts {
16
18
  readonly liberoReplacements: Player[];
17
19
  readonly pinchServerSubs: Map<Player, Player>;
18
20
  readonly receiveRotationOffset: boolean;
21
+ readonly designatedSubs: DesignatedSub[];
22
+ readonly substitutionBand: SubBand;
19
23
  }
20
24
  export declare class Tactics {
21
25
  readonly lineup: StartingLineup;
@@ -23,6 +27,8 @@ export declare class Tactics {
23
27
  readonly liberoReplacements: Player[];
24
28
  readonly pinchServerSubs: Map<Player, Player>;
25
29
  readonly receiveRotationOffset: boolean;
30
+ readonly designatedSubs: DesignatedSub[];
31
+ readonly substitutionBand: SubBand;
26
32
  static create(input: unknown): Tactics;
27
33
  private constructor();
28
34
  findPlayerInLineup(id: string): CourtPosition | undefined;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Tactics = void 0;
4
4
  const match_1 = require("../match");
5
+ const energy_band_1 = require("./energy-band");
5
6
  const tactics_z_1 = require("./schemas/tactics.z");
6
7
  class Tactics {
7
8
  static create(input) {
@@ -16,12 +17,14 @@ class Tactics {
16
17
  }
17
18
  return new Tactics(result.data);
18
19
  }
19
- constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false }) {
20
+ constructor({ lineup, pinchServerSubs, liberoReplacements, substitutionTolerance = 1, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED }) {
20
21
  this.lineup = lineup;
21
22
  this.substitutionTolerance = substitutionTolerance;
22
23
  this.pinchServerSubs = pinchServerSubs;
23
24
  this.liberoReplacements = liberoReplacements;
24
25
  this.receiveRotationOffset = receiveRotationOffset;
26
+ this.designatedSubs = designatedSubs;
27
+ this.substitutionBand = substitutionBand;
25
28
  }
26
29
  findPlayerInLineup(id) {
27
30
  if (this.lineup.bench.some(p => p.id === id))
@@ -2,6 +2,7 @@ import { Player, RarityEnum, Role } from '../player';
2
2
  import { Team } from './team';
3
3
  import { Country } from '../country';
4
4
  import { Tactics } from './tactics';
5
+ import { EnergyBand } from './energy-band';
5
6
  export declare class TeamGenerator {
6
7
  static readonly TEAM_PER_DIVISION = 16;
7
8
  static readonly RARITIES: RarityEnum[];
@@ -10,6 +11,7 @@ export declare class TeamGenerator {
10
11
  private static comparePlayers;
11
12
  static createRoster(country: Country): Player[];
12
13
  static createTeams(divisionId: string, country: Country): Team[];
14
+ static readonly BOT_SUB_BANDS: EnergyBand[];
13
15
  static createTactics(roster: Player[]): Tactics;
14
16
  private static createLineup;
15
17
  }