volleyballsimtypes 0.0.463 → 0.0.464

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 (129) hide show
  1. package/dist/cjs/src/api/index.d.ts +12 -1
  2. package/dist/cjs/src/data/init-models.js +8 -0
  3. package/dist/cjs/src/data/models/currency-transaction.d.ts +4 -2
  4. package/dist/cjs/src/data/models/currency-transaction.js +2 -0
  5. package/dist/cjs/src/data/models/index.d.ts +2 -0
  6. package/dist/cjs/src/data/models/index.js +2 -0
  7. package/dist/cjs/src/data/models/match-set.d.ts +2 -0
  8. package/dist/cjs/src/data/models/match-set.js +5 -0
  9. package/dist/cjs/src/data/models/match.d.ts +3 -1
  10. package/dist/cjs/src/data/models/match.js +4 -0
  11. package/dist/cjs/src/data/models/physician.d.ts +25 -0
  12. package/dist/cjs/src/data/models/physician.js +57 -0
  13. package/dist/cjs/src/data/models/physiotherapist.d.ts +25 -0
  14. package/dist/cjs/src/data/models/physiotherapist.js +57 -0
  15. package/dist/cjs/src/data/models/player.d.ts +8 -0
  16. package/dist/cjs/src/data/models/player.js +17 -0
  17. package/dist/cjs/src/data/models/tactics.d.ts +9 -1
  18. package/dist/cjs/src/data/models/tactics.js +9 -0
  19. package/dist/cjs/src/data/transformers/lineup-preset.js +7 -1
  20. package/dist/cjs/src/data/transformers/lineup-preset.test.js +15 -2
  21. package/dist/cjs/src/data/transformers/match-set.js +2 -0
  22. package/dist/cjs/src/data/transformers/match.js +6 -2
  23. package/dist/cjs/src/data/transformers/player.js +15 -1
  24. package/dist/cjs/src/data/transformers/rally-event-injury.test.d.ts +1 -0
  25. package/dist/cjs/src/data/transformers/rally-event-injury.test.js +33 -0
  26. package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
  27. package/dist/cjs/src/data/transformers/rally-event.js +4 -1
  28. package/dist/cjs/src/data/transformers/tactics.js +71 -4
  29. package/dist/cjs/src/service/event/schemas/substitution.z.d.ts +1 -0
  30. package/dist/cjs/src/service/event/schemas/substitution.z.js +2 -1
  31. package/dist/cjs/src/service/event/substitution.d.ts +1 -0
  32. package/dist/cjs/src/service/event/substitution.js +4 -2
  33. package/dist/cjs/src/service/match/match-set-forfeit.test.d.ts +1 -0
  34. package/dist/cjs/src/service/match/match-set-forfeit.test.js +76 -0
  35. package/dist/cjs/src/service/match/match-set.d.ts +8 -0
  36. package/dist/cjs/src/service/match/match-set.js +23 -1
  37. package/dist/cjs/src/service/match/match.d.ts +2 -0
  38. package/dist/cjs/src/service/match/match.js +2 -1
  39. package/dist/cjs/src/service/match/schemas/match-set.z.d.ts +1 -0
  40. package/dist/cjs/src/service/match/schemas/match-set.z.js +30 -2
  41. package/dist/cjs/src/service/match/schemas/match.z.d.ts +4 -0
  42. package/dist/cjs/src/service/match/schemas/match.z.js +4 -1
  43. package/dist/cjs/src/service/player/index.d.ts +1 -0
  44. package/dist/cjs/src/service/player/index.js +1 -0
  45. package/dist/cjs/src/service/player/injury.d.ts +14 -0
  46. package/dist/cjs/src/service/player/injury.js +39 -0
  47. package/dist/cjs/src/service/player/injury.test.d.ts +1 -0
  48. package/dist/cjs/src/service/player/injury.test.js +78 -0
  49. package/dist/cjs/src/service/player/player-generator.js +3 -1
  50. package/dist/cjs/src/service/player/player.d.ts +3 -0
  51. package/dist/cjs/src/service/player/player.js +3 -1
  52. package/dist/cjs/src/service/player/schemas/player.z.d.ts +6 -0
  53. package/dist/cjs/src/service/player/schemas/player.z.js +7 -0
  54. package/dist/cjs/src/service/team/index.d.ts +1 -0
  55. package/dist/cjs/src/service/team/index.js +1 -0
  56. package/dist/cjs/src/service/team/reserve-priority.d.ts +6 -0
  57. package/dist/cjs/src/service/team/reserve-priority.js +2 -0
  58. package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
  59. package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.js +125 -0
  60. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +5 -0
  61. package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -1
  62. package/dist/cjs/src/service/team/schemas/team.z.d.ts +5 -0
  63. package/dist/cjs/src/service/team/tactics.d.ts +5 -0
  64. package/dist/cjs/src/service/team/tactics.js +3 -1
  65. package/dist/esm/src/api/index.d.ts +12 -1
  66. package/dist/esm/src/data/init-models.js +9 -1
  67. package/dist/esm/src/data/models/currency-transaction.d.ts +4 -2
  68. package/dist/esm/src/data/models/currency-transaction.js +2 -0
  69. package/dist/esm/src/data/models/index.d.ts +2 -0
  70. package/dist/esm/src/data/models/index.js +2 -0
  71. package/dist/esm/src/data/models/match-set.d.ts +2 -0
  72. package/dist/esm/src/data/models/match-set.js +5 -0
  73. package/dist/esm/src/data/models/match.d.ts +3 -1
  74. package/dist/esm/src/data/models/match.js +4 -0
  75. package/dist/esm/src/data/models/physician.d.ts +25 -0
  76. package/dist/esm/src/data/models/physician.js +53 -0
  77. package/dist/esm/src/data/models/physiotherapist.d.ts +25 -0
  78. package/dist/esm/src/data/models/physiotherapist.js +53 -0
  79. package/dist/esm/src/data/models/player.d.ts +8 -0
  80. package/dist/esm/src/data/models/player.js +17 -0
  81. package/dist/esm/src/data/models/tactics.d.ts +9 -1
  82. package/dist/esm/src/data/models/tactics.js +9 -0
  83. package/dist/esm/src/data/transformers/lineup-preset.js +7 -1
  84. package/dist/esm/src/data/transformers/lineup-preset.test.js +15 -2
  85. package/dist/esm/src/data/transformers/match-set.js +2 -0
  86. package/dist/esm/src/data/transformers/match.js +6 -2
  87. package/dist/esm/src/data/transformers/player.js +15 -1
  88. package/dist/esm/src/data/transformers/rally-event-injury.test.d.ts +1 -0
  89. package/dist/esm/src/data/transformers/rally-event-injury.test.js +31 -0
  90. package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
  91. package/dist/esm/src/data/transformers/rally-event.js +4 -1
  92. package/dist/esm/src/data/transformers/tactics.js +71 -4
  93. package/dist/esm/src/service/event/schemas/substitution.z.d.ts +1 -0
  94. package/dist/esm/src/service/event/schemas/substitution.z.js +2 -1
  95. package/dist/esm/src/service/event/substitution.d.ts +1 -0
  96. package/dist/esm/src/service/event/substitution.js +4 -2
  97. package/dist/esm/src/service/match/match-set-forfeit.test.d.ts +1 -0
  98. package/dist/esm/src/service/match/match-set-forfeit.test.js +74 -0
  99. package/dist/esm/src/service/match/match-set.d.ts +8 -0
  100. package/dist/esm/src/service/match/match-set.js +23 -1
  101. package/dist/esm/src/service/match/match.d.ts +2 -0
  102. package/dist/esm/src/service/match/match.js +2 -1
  103. package/dist/esm/src/service/match/schemas/match-set.z.d.ts +1 -0
  104. package/dist/esm/src/service/match/schemas/match-set.z.js +30 -2
  105. package/dist/esm/src/service/match/schemas/match.z.d.ts +4 -0
  106. package/dist/esm/src/service/match/schemas/match.z.js +4 -1
  107. package/dist/esm/src/service/player/index.d.ts +1 -0
  108. package/dist/esm/src/service/player/index.js +1 -0
  109. package/dist/esm/src/service/player/injury.d.ts +14 -0
  110. package/dist/esm/src/service/player/injury.js +33 -0
  111. package/dist/esm/src/service/player/injury.test.d.ts +1 -0
  112. package/dist/esm/src/service/player/injury.test.js +76 -0
  113. package/dist/esm/src/service/player/player-generator.js +3 -1
  114. package/dist/esm/src/service/player/player.d.ts +3 -0
  115. package/dist/esm/src/service/player/player.js +3 -1
  116. package/dist/esm/src/service/player/schemas/player.z.d.ts +6 -0
  117. package/dist/esm/src/service/player/schemas/player.z.js +7 -0
  118. package/dist/esm/src/service/team/index.d.ts +1 -0
  119. package/dist/esm/src/service/team/index.js +1 -0
  120. package/dist/esm/src/service/team/reserve-priority.d.ts +6 -0
  121. package/dist/esm/src/service/team/reserve-priority.js +1 -0
  122. package/dist/esm/src/service/team/schemas/reserve-priority.z.test.d.ts +1 -0
  123. package/dist/esm/src/service/team/schemas/reserve-priority.z.test.js +123 -0
  124. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +5 -0
  125. package/dist/esm/src/service/team/schemas/tactics.z.js +32 -1
  126. package/dist/esm/src/service/team/schemas/team.z.d.ts +5 -0
  127. package/dist/esm/src/service/team/tactics.d.ts +5 -0
  128. package/dist/esm/src/service/team/tactics.js +3 -1
  129. package/package.json +1 -1
@@ -18,5 +18,9 @@ export declare const MatchInputSchema: z.ZodObject<{
18
18
  homeScore: z.ZodOptional<z.ZodNumber>;
19
19
  awayScore: z.ZodOptional<z.ZodNumber>;
20
20
  winner: z.ZodOptional<z.ZodCustom<Team, Team>>;
21
+ forfeitType: z.ZodOptional<z.ZodEnum<{
22
+ INCOMPLETE: "INCOMPLETE";
23
+ DEFAULT: "DEFAULT";
24
+ }>>;
21
25
  }, z.core.$strip>;
22
26
  export type MatchInput = z.infer<typeof MatchInputSchema>;
@@ -20,7 +20,10 @@ export const MatchInputSchema = z.object({
20
20
  })).optional(),
21
21
  homeScore: z.number().int().optional(),
22
22
  awayScore: z.number().int().optional(),
23
- winner: teamSchema.optional()
23
+ winner: teamSchema.optional(),
24
+ // Abnormal ending marker (see MatchForfeitType). The match still validates as a normal COMPLETE result
25
+ // because forfeited sets carry FIVB-awarded scores.
26
+ forfeitType: z.enum(['INCOMPLETE', 'DEFAULT']).optional()
24
27
  }).superRefine((data, ctx) => {
25
28
  if (data.homeTeam.id === data.awayTeam.id) {
26
29
  ctx.addIssue({
@@ -1,6 +1,7 @@
1
1
  export * from './player';
2
2
  export * from './improvement';
3
3
  export * from './decline';
4
+ export * from './injury';
4
5
  export * from './player-generator';
5
6
  export * from './performance-stats';
6
7
  export * from './trait';
@@ -1,6 +1,7 @@
1
1
  export * from './player';
2
2
  export * from './improvement';
3
3
  export * from './decline';
4
+ export * from './injury';
4
5
  export * from './player-generator';
5
6
  export * from './performance-stats';
6
7
  export * from './trait';
@@ -0,0 +1,14 @@
1
+ import { Rarity } from './rarity';
2
+ export declare enum InjurySeverityEnum {
3
+ MINOR = 1,
4
+ MODERATE = 2,
5
+ SEVERE = 3
6
+ }
7
+ export interface PlayerInjury {
8
+ readonly severity: number;
9
+ readonly injuredUntil: Date;
10
+ readonly debuff?: number;
11
+ }
12
+ export declare function hasActiveInjury(injury: PlayerInjury | undefined, at: Date): boolean;
13
+ export declare function getPhysiotherapistRiskMultiplier(rarity: Rarity): number;
14
+ export declare function getPhysicianRecoveryMultiplier(rarity: Rarity): number;
@@ -0,0 +1,33 @@
1
+ import { RarityEnum } from './rarity';
2
+ export var InjurySeverityEnum;
3
+ (function (InjurySeverityEnum) {
4
+ InjurySeverityEnum[InjurySeverityEnum["MINOR"] = 1] = "MINOR";
5
+ InjurySeverityEnum[InjurySeverityEnum["MODERATE"] = 2] = "MODERATE";
6
+ InjurySeverityEnum[InjurySeverityEnum["SEVERE"] = 3] = "SEVERE";
7
+ })(InjurySeverityEnum || (InjurySeverityEnum = {}));
8
+ // A long-term injury is active while `at` is before injuredUntil.
9
+ export function hasActiveInjury(injury, at) {
10
+ return injury != null && injury.injuredUntil.getTime() > at.getTime();
11
+ }
12
+ // Staff effect curves (same Record-plus-accessor shape as getCoachMultiplier). COMMON = neutral baseline.
13
+ // Physiotherapist scales the per-action injury CHANCE; physician scales long-term recovery TIME.
14
+ const PHYSIOTHERAPIST_RISK_MULTIPLIERS = {
15
+ [RarityEnum.COMMON]: 1.0,
16
+ [RarityEnum.RARE]: 0.92,
17
+ [RarityEnum.LEGENDARY]: 0.84,
18
+ [RarityEnum.MYTHIC]: 0.75,
19
+ [RarityEnum.SPECIAL]: 0.65
20
+ };
21
+ const PHYSICIAN_RECOVERY_MULTIPLIERS = {
22
+ [RarityEnum.COMMON]: 1.0,
23
+ [RarityEnum.RARE]: 0.9,
24
+ [RarityEnum.LEGENDARY]: 0.8,
25
+ [RarityEnum.MYTHIC]: 0.65,
26
+ [RarityEnum.SPECIAL]: 0.5
27
+ };
28
+ export function getPhysiotherapistRiskMultiplier(rarity) {
29
+ return PHYSIOTHERAPIST_RISK_MULTIPLIERS[rarity] ?? 1.0;
30
+ }
31
+ export function getPhysicianRecoveryMultiplier(rarity) {
32
+ return PHYSICIAN_RECOVERY_MULTIPLIERS[rarity] ?? 1.0;
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { RarityEnum } from './rarity';
3
+ import { getPhysicianRecoveryMultiplier, getPhysiotherapistRiskMultiplier, hasActiveInjury } from './injury';
4
+ import { PlayerInputSchema } from './schemas/player.z';
5
+ import { makeCountry, makePlayer } from '../test-helpers';
6
+ describe('Player injury fields', () => {
7
+ const country = makeCountry();
8
+ it('defaults injuryProneProfile to 0.5 for legacy Player.create callers', () => {
9
+ const player = makePlayer(country);
10
+ expect(player.injuryProneProfile).toBe(0.5);
11
+ expect(player.injury).toBeUndefined();
12
+ });
13
+ it('accepts an injury object and coerces injuredUntil from an ISO string', () => {
14
+ const base = PlayerInputSchema.safeParse({
15
+ ...validInput(),
16
+ injuryProneProfile: 0.8,
17
+ injury: { severity: 2, injuredUntil: '2026-07-04T10:00:00.000Z', debuff: 0.75 }
18
+ });
19
+ expect(base.success).toBe(true);
20
+ if (base.success) {
21
+ expect(base.data.injuryProneProfile).toBe(0.8);
22
+ expect(base.data.injury?.injuredUntil).toBeInstanceOf(Date);
23
+ expect(base.data.injury?.severity).toBe(2);
24
+ }
25
+ });
26
+ it('rejects out-of-range severity and profile', () => {
27
+ expect(PlayerInputSchema.safeParse({ ...validInput(), injury: { severity: 4, injuredUntil: new Date() } }).success).toBe(false);
28
+ expect(PlayerInputSchema.safeParse({ ...validInput(), injury: { severity: 0, injuredUntil: new Date() } }).success).toBe(false);
29
+ expect(PlayerInputSchema.safeParse({ ...validInput(), injuryProneProfile: 1.2 }).success).toBe(false);
30
+ expect(PlayerInputSchema.safeParse({ ...validInput(), injuryProneProfile: -0.1 }).success).toBe(false);
31
+ });
32
+ function validInput() {
33
+ const p = makePlayer(country);
34
+ return {
35
+ id: p.id,
36
+ name: { first: p.name.first, last: p.name.last },
37
+ country,
38
+ stats: p.stats,
39
+ roles: p.roles,
40
+ traits: p.traits,
41
+ rarity: p.rarity,
42
+ age: p.age,
43
+ birthAge: p.birthAge,
44
+ birthIteration: p.birthIteration,
45
+ declineProfile: p.declineProfile
46
+ };
47
+ }
48
+ });
49
+ describe('hasActiveInjury', () => {
50
+ const at = new Date('2026-07-03T12:00:00.000Z');
51
+ it('is false for a healthy player', () => {
52
+ expect(hasActiveInjury(undefined, at)).toBe(false);
53
+ });
54
+ it('is true while injuredUntil is in the future', () => {
55
+ expect(hasActiveInjury({ severity: 1, injuredUntil: new Date('2026-07-03T18:00:00.000Z') }, at)).toBe(true);
56
+ });
57
+ it('is false once injuredUntil has passed (lazy expiry)', () => {
58
+ expect(hasActiveInjury({ severity: 3, injuredUntil: new Date('2026-07-03T11:59:59.000Z') }, at)).toBe(false);
59
+ });
60
+ });
61
+ describe('staff multipliers', () => {
62
+ it('physiotherapist reduces risk monotonically from COMMON to SPECIAL', () => {
63
+ expect(getPhysiotherapistRiskMultiplier(RarityEnum.COMMON)).toBe(1.0);
64
+ expect(getPhysiotherapistRiskMultiplier(RarityEnum.SPECIAL)).toBe(0.65);
65
+ const ladder = [RarityEnum.COMMON, RarityEnum.RARE, RarityEnum.LEGENDARY, RarityEnum.MYTHIC, RarityEnum.SPECIAL];
66
+ const values = ladder.map(getPhysiotherapistRiskMultiplier);
67
+ expect([...values].sort((a, b) => b - a)).toEqual(values);
68
+ });
69
+ it('physician reduces recovery monotonically and halves it at SPECIAL', () => {
70
+ expect(getPhysicianRecoveryMultiplier(RarityEnum.COMMON)).toBe(1.0);
71
+ expect(getPhysicianRecoveryMultiplier(RarityEnum.SPECIAL)).toBe(0.5);
72
+ const ladder = [RarityEnum.COMMON, RarityEnum.RARE, RarityEnum.LEGENDARY, RarityEnum.MYTHIC, RarityEnum.SPECIAL];
73
+ const values = ladder.map(getPhysicianRecoveryMultiplier);
74
+ expect([...values].sort((a, b) => b - a)).toEqual(values);
75
+ });
76
+ });
@@ -302,7 +302,9 @@ export class PlayerGenerator {
302
302
  const traits = assignTraits(roles, rarity, maxTraits);
303
303
  const birthAge = randomInt(15, 21);
304
304
  const declineProfile = declineProfiles[randomInt(0, declineProfiles.length)];
305
- return Player.create({ id: uuidv4(), name, country, stats, roles, traits, rarity, age: birthAge, birthAge, birthIteration, declineProfile });
305
+ // Uniform 0-1 (mean-neutral: the sim maps it to a 0.6-1.4 risk factor centered on 1.0).
306
+ const injuryProneProfile = randomInt(0, 101) / 100;
307
+ return Player.create({ id: uuidv4(), name, country, stats, roles, traits, rarity, age: birthAge, birthAge, birthIteration, declineProfile, injuryProneProfile });
306
308
  }
307
309
  static generatePlayers(count, countries) {
308
310
  const players = [];
@@ -6,6 +6,7 @@ import { Stats } from './stats';
6
6
  import { Role } from './role';
7
7
  import { BoxScore, BoxScoreTotals } from '../match';
8
8
  import { DeclineProfile } from './decline';
9
+ import { PlayerInjury } from './injury';
9
10
  export declare class Stat {
10
11
  readonly name: Stats;
11
12
  readonly value: number;
@@ -36,6 +37,8 @@ export declare class Player {
36
37
  readonly birthAge: number;
37
38
  readonly birthIteration: number;
38
39
  readonly declineProfile: DeclineProfile;
40
+ readonly injuryProneProfile: number;
41
+ readonly injury?: PlayerInjury;
39
42
  readonly boxScores?: BoxScore[];
40
43
  readonly boxScoreTotals?: BoxScoreTotals;
41
44
  static create(input: unknown): Player;
@@ -49,7 +49,7 @@ export class Player {
49
49
  stats: PerformanceStats.create(result.data.stats)
50
50
  });
51
51
  }
52
- constructor({ id, name, nickname, country, stats, roles, traits, rarity, age, birthAge, birthIteration, declineProfile, boxScores, boxScoreTotals }) {
52
+ constructor({ id, name, nickname, country, stats, roles, traits, rarity, age, birthAge, birthIteration, declineProfile, injuryProneProfile, injury, boxScores, boxScoreTotals }) {
53
53
  this.id = id;
54
54
  this.name = name;
55
55
  this.nickname = nickname;
@@ -62,6 +62,8 @@ export class Player {
62
62
  this.birthAge = birthAge;
63
63
  this.birthIteration = birthIteration;
64
64
  this.declineProfile = declineProfile;
65
+ this.injuryProneProfile = injuryProneProfile;
66
+ this.injury = injury;
65
67
  this.boxScores = boxScores;
66
68
  this.boxScoreTotals = boxScoreTotals;
67
69
  this.generalStats = Object.values(StatsEnum).map((stat) => ({
@@ -79,6 +79,12 @@ export declare const PlayerInputSchema: z.ZodObject<{
79
79
  VETERAN: DeclineProfileEnum.VETERAN;
80
80
  IRONMAN: DeclineProfileEnum.IRONMAN;
81
81
  }>;
82
+ injuryProneProfile: z.ZodDefault<z.ZodNumber>;
83
+ injury: z.ZodOptional<z.ZodObject<{
84
+ severity: z.ZodNumber;
85
+ injuredUntil: z.ZodCoercedDate<unknown>;
86
+ debuff: z.ZodOptional<z.ZodNumber>;
87
+ }, z.core.$strip>>;
82
88
  boxScores: z.ZodOptional<z.ZodArray<z.ZodObject<{
83
89
  playerId: z.ZodUUID;
84
90
  matchSetId: z.ZodUUID;
@@ -35,6 +35,13 @@ export const PlayerInputSchema = z.object({
35
35
  birthAge: z.number().int().min(0),
36
36
  birthIteration: z.number().int().min(1),
37
37
  declineProfile: z.enum(Object.values(DeclineProfileEnum)),
38
+ // Defaulted so every existing Player.create caller (generator, transformers, test helpers) stays valid.
39
+ injuryProneProfile: z.number().min(0).max(1).default(0.5),
40
+ injury: z.object({
41
+ severity: z.number().int().min(1).max(3),
42
+ injuredUntil: z.coerce.date(),
43
+ debuff: z.number().min(0).max(1).optional()
44
+ }).optional(),
38
45
  boxScores: z.array(BoxScoreInputSchema).optional(),
39
46
  boxScoreTotals: BoxScoreTotalsInputSchema.optional()
40
47
  });
@@ -11,3 +11,4 @@ export * from './rotation-system';
11
11
  export * from './lineup-function';
12
12
  export * from './base-position';
13
13
  export * from './offensive-preference';
14
+ export * from './reserve-priority';
@@ -11,3 +11,4 @@ export * from './rotation-system';
11
11
  export * from './lineup-function';
12
12
  export * from './base-position';
13
13
  export * from './offensive-preference';
14
+ export * from './reserve-priority';
@@ -0,0 +1,6 @@
1
+ import { CourtPosition } from '../match';
2
+ import { Player } from '../player';
3
+ export interface ReservePriority {
4
+ readonly position: CourtPosition;
5
+ readonly reserves: Player[];
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { TacticsInputSchema } from './tactics.z';
3
+ import { makeCountry, makePlayer } from '../../test-helpers';
4
+ describe('TacticsInputSchema — injuries (replaceInjuredImmediately / reservePriorities)', () => {
5
+ const country = makeCountry();
6
+ const p1 = makePlayer(country);
7
+ const p2 = makePlayer(country);
8
+ const p3 = makePlayer(country);
9
+ const p4 = makePlayer(country);
10
+ const p5 = makePlayer(country);
11
+ const p6 = makePlayer(country);
12
+ const benchPlayer = makePlayer(country);
13
+ const libero = makePlayer(country);
14
+ const reserveA = makePlayer(country);
15
+ const reserveB = makePlayer(country);
16
+ function baseInput(overrides = {}) {
17
+ return {
18
+ lineup: {
19
+ 4: p4,
20
+ 3: p3,
21
+ 2: p2,
22
+ 5: p5,
23
+ 6: p6,
24
+ 1: p1,
25
+ 0: libero,
26
+ bench: [benchPlayer]
27
+ },
28
+ liberoReplacements: [],
29
+ ...overrides
30
+ };
31
+ }
32
+ it('defaults replaceInjuredImmediately to true and reservePriorities to empty', () => {
33
+ const res = TacticsInputSchema.safeParse(baseInput());
34
+ expect(res.success).toBe(true);
35
+ if (res.success) {
36
+ expect(res.data.replaceInjuredImmediately).toBe(true);
37
+ expect(res.data.reservePriorities).toEqual([]);
38
+ }
39
+ });
40
+ it('accepts replaceInjuredImmediately false', () => {
41
+ const res = TacticsInputSchema.safeParse(baseInput({ replaceInjuredImmediately: false }));
42
+ expect(res.success).toBe(true);
43
+ if (res.success)
44
+ expect(res.data.replaceInjuredImmediately).toBe(false);
45
+ });
46
+ it('accepts reserve lists of non-lineup roster players', () => {
47
+ const res = TacticsInputSchema.safeParse(baseInput({
48
+ reservePriorities: [
49
+ { position: 4, reserves: [reserveA] },
50
+ { position: 3, reserves: [reserveB] }
51
+ ]
52
+ }));
53
+ expect(res.success).toBe(true);
54
+ });
55
+ it('rejects a reserve that is a starter', () => {
56
+ const res = TacticsInputSchema.safeParse(baseInput({
57
+ reservePriorities: [{ position: 4, reserves: [p1] }]
58
+ }));
59
+ expect(res.success).toBe(false);
60
+ if (!res.success) {
61
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
62
+ }
63
+ });
64
+ it('rejects a reserve that is on the bench', () => {
65
+ const res = TacticsInputSchema.safeParse(baseInput({
66
+ reservePriorities: [{ position: 4, reserves: [benchPlayer] }]
67
+ }));
68
+ expect(res.success).toBe(false);
69
+ if (!res.success) {
70
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
71
+ }
72
+ });
73
+ it('rejects a reserve that is the libero', () => {
74
+ const res = TacticsInputSchema.safeParse(baseInput({
75
+ reservePriorities: [{ position: 4, reserves: [libero] }]
76
+ }));
77
+ expect(res.success).toBe(false);
78
+ if (!res.success) {
79
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_IN_LINEUP')).toBe(true);
80
+ }
81
+ });
82
+ it('rejects a reserve that doubles as the second libero', () => {
83
+ const res = TacticsInputSchema.safeParse(baseInput({
84
+ secondLibero: reserveA,
85
+ liberoSub: { mode: 'ALWAYS' },
86
+ reservePriorities: [{ position: 4, reserves: [reserveA] }]
87
+ }));
88
+ expect(res.success).toBe(false);
89
+ if (!res.success) {
90
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_MUST_NOT_BE_SECOND_LIBERO')).toBe(true);
91
+ }
92
+ });
93
+ it('rejects the same player in two lists', () => {
94
+ const res = TacticsInputSchema.safeParse(baseInput({
95
+ reservePriorities: [
96
+ { position: 4, reserves: [reserveA] },
97
+ { position: 3, reserves: [reserveA] }
98
+ ]
99
+ }));
100
+ expect(res.success).toBe(false);
101
+ if (!res.success) {
102
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_IN_MULTIPLE_LISTS')).toBe(true);
103
+ }
104
+ });
105
+ it('rejects a duplicated position', () => {
106
+ const res = TacticsInputSchema.safeParse(baseInput({
107
+ reservePriorities: [
108
+ { position: 4, reserves: [reserveA] },
109
+ { position: 4, reserves: [reserveB] }
110
+ ]
111
+ }));
112
+ expect(res.success).toBe(false);
113
+ if (!res.success) {
114
+ expect(res.error.issues.some(issue => issue.message === 'RESERVE_PRIORITY_DUPLICATE_POSITION')).toBe(true);
115
+ }
116
+ });
117
+ it('rejects an out-of-range position', () => {
118
+ const res = TacticsInputSchema.safeParse(baseInput({
119
+ reservePriorities: [{ position: 7, reserves: [reserveA] }]
120
+ }));
121
+ expect(res.success).toBe(false);
122
+ });
123
+ });
@@ -123,5 +123,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
123
123
  rotation: z.ZodNumber;
124
124
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
125
125
  }, z.core.$strip>>>>;
126
+ replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
127
+ reservePriorities: z.ZodDefault<z.ZodArray<z.ZodObject<{
128
+ position: z.ZodNumber;
129
+ reserves: z.ZodArray<z.ZodCustom<Player, Player>>;
130
+ }, z.core.$strip>>>;
126
131
  }, z.core.$strip>;
127
132
  export type TacticsInput = z.infer<typeof TacticsInputSchema>;
@@ -77,7 +77,15 @@ export const TacticsInputSchema = z.object({
77
77
  // Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
78
78
  // validated with the same cross-field rules as the flat fields (see superRefine below).
79
79
  systemSets: z.array(systemSetSchema).max(5).optional(),
80
- offensivePreferenceSets: z.array(z.array(offensivePreferenceSchema)).max(5).optional()
80
+ offensivePreferenceSets: z.array(z.array(offensivePreferenceSchema)).max(5).optional(),
81
+ // Injuries. Defaults keep every existing Tactics.create({...}) call valid. reservePriorities is flat-only
82
+ // (whole match): per-position ordered reserve lists; cross-field rules (reserves must be OUTSIDE the lineup,
83
+ // no player in two lists, unique positions) in the superRefine below.
84
+ replaceInjuredImmediately: z.boolean().default(true),
85
+ reservePriorities: z.array(z.object({
86
+ position: z.number().int().min(1).max(6),
87
+ reserves: z.array(playerInstanceSchema)
88
+ })).default([])
81
89
  }).superRefine((data, ctx) => {
82
90
  // Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
83
91
  // pinch-server outage was caused by an unvalidated tactics payload).
@@ -175,4 +183,27 @@ export const TacticsInputSchema = z.object({
175
183
  }
176
184
  }
177
185
  }
186
+ // Reserve priorities (injuries). A reserve must be a roster member OUTSIDE the lineup (starters, libero zone
187
+ // and bench refill themselves via subs); each position appears at most once; no player sits in two lists; and
188
+ // the second libero cannot double as a reserve (its promotion would strand the L2 swap validation).
189
+ const seenReservePositions = new Set();
190
+ const seenReserveIds = new Set();
191
+ for (const rp of data.reservePriorities) {
192
+ if (seenReservePositions.has(rp.position)) {
193
+ ctx.addIssue({ code: 'custom', message: 'RESERVE_PRIORITY_DUPLICATE_POSITION', path: ['reservePriorities'] });
194
+ }
195
+ seenReservePositions.add(rp.position);
196
+ for (const reserve of rp.reserves) {
197
+ if (inLineupIds.has(reserve.id)) {
198
+ ctx.addIssue({ code: 'custom', message: 'RESERVE_MUST_NOT_BE_IN_LINEUP', path: ['reservePriorities'] });
199
+ }
200
+ if (data.secondLibero != null && reserve.id === data.secondLibero.id) {
201
+ ctx.addIssue({ code: 'custom', message: 'RESERVE_MUST_NOT_BE_SECOND_LIBERO', path: ['reservePriorities'] });
202
+ }
203
+ if (seenReserveIds.has(reserve.id)) {
204
+ ctx.addIssue({ code: 'custom', message: 'RESERVE_IN_MULTIPLE_LISTS', path: ['reservePriorities'] });
205
+ }
206
+ seenReserveIds.add(reserve.id);
207
+ }
208
+ }
178
209
  });
@@ -131,6 +131,11 @@ export declare const TeamInputSchema: z.ZodObject<{
131
131
  rotation: z.ZodNumber;
132
132
  order: z.ZodArray<z.ZodCustom<Player, Player>>;
133
133
  }, z.core.$strip>>>>;
134
+ replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
135
+ reservePriorities: z.ZodDefault<z.ZodArray<z.ZodObject<{
136
+ position: z.ZodNumber;
137
+ reserves: z.ZodArray<z.ZodCustom<Player, Player>>;
138
+ }, z.core.$strip>>>;
134
139
  }, z.core.$strip>>;
135
140
  }, z.core.$strip>;
136
141
  export type TeamInput = z.infer<typeof TeamInputSchema>;
@@ -5,6 +5,7 @@ import { LiberoSubConfig } from './libero-sub';
5
5
  import { SubBand } from './energy-band';
6
6
  import { RotationSystemEnum } from './rotation-system';
7
7
  import { OffensivePreference } from './offensive-preference';
8
+ import { ReservePriority } from './reserve-priority';
8
9
  export interface StartingLineup {
9
10
  readonly [CourtPosition.LEFT_FRONT]: Player;
10
11
  readonly [CourtPosition.MIDDLE_FRONT]: Player;
@@ -33,6 +34,8 @@ export interface TacticsOpts {
33
34
  readonly offensivePreferences: OffensivePreference[];
34
35
  readonly systemSets?: SystemSet[];
35
36
  readonly offensivePreferenceSets?: OffensivePreference[][];
37
+ readonly replaceInjuredImmediately: boolean;
38
+ readonly reservePriorities: ReservePriority[];
36
39
  }
37
40
  export declare class Tactics {
38
41
  readonly lineup: StartingLineup;
@@ -48,6 +51,8 @@ export declare class Tactics {
48
51
  readonly offensivePreferences: OffensivePreference[];
49
52
  readonly systemSets?: SystemSet[];
50
53
  readonly offensivePreferenceSets?: OffensivePreference[][];
54
+ readonly replaceInjuredImmediately: boolean;
55
+ readonly reservePriorities: ReservePriority[];
51
56
  static create(input: unknown): Tactics;
52
57
  private constructor();
53
58
  findPlayerInLineup(id: string): CourtPosition | undefined;
@@ -15,7 +15,7 @@ export class Tactics {
15
15
  }
16
16
  return new Tactics(result.data);
17
17
  }
18
- constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets }) {
18
+ constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceInjuredImmediately = true, reservePriorities = [] }) {
19
19
  this.lineup = lineup;
20
20
  this.liberoReplacements = liberoReplacements;
21
21
  this.receiveRotationOffset = receiveRotationOffset;
@@ -29,6 +29,8 @@ export class Tactics {
29
29
  this.offensivePreferences = offensivePreferences;
30
30
  this.systemSets = systemSets;
31
31
  this.offensivePreferenceSets = offensivePreferenceSets;
32
+ this.replaceInjuredImmediately = replaceInjuredImmediately;
33
+ this.reservePriorities = reservePriorities;
32
34
  }
33
35
  findPlayerInLineup(id) {
34
36
  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.463",
3
+ "version": "0.0.464",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",