volleyballsimtypes 0.0.480 → 0.0.482
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/api/index.d.ts +8 -2
- package/dist/cjs/src/data/models/tactics.d.ts +11 -3
- package/dist/cjs/src/data/models/tactics.js +9 -0
- package/dist/cjs/src/data/transformers/tactics.js +12 -4
- package/dist/cjs/src/data/transformers/tactics.test.js +23 -9
- package/dist/cjs/src/service/competition/standing.js +5 -0
- package/dist/cjs/src/service/competition/standing.test.js +19 -8
- package/dist/cjs/src/service/team/blocking-preference.d.ts +5 -0
- package/dist/cjs/src/service/team/blocking-preference.js +2 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/offensive-preference.d.ts +1 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +11 -3
- package/dist/cjs/src/service/team/schemas/tactics.z.js +43 -10
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +81 -8
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +10 -2
- package/dist/cjs/src/service/team/tactics.d.ts +5 -0
- package/dist/cjs/src/service/team/tactics.js +3 -1
- package/dist/esm/src/api/index.d.ts +8 -2
- package/dist/esm/src/data/models/tactics.d.ts +11 -3
- package/dist/esm/src/data/models/tactics.js +9 -0
- package/dist/esm/src/data/transformers/tactics.js +12 -4
- package/dist/esm/src/data/transformers/tactics.test.js +24 -10
- package/dist/esm/src/service/competition/standing.js +5 -0
- package/dist/esm/src/service/competition/standing.test.js +19 -8
- package/dist/esm/src/service/team/blocking-preference.d.ts +5 -0
- package/dist/esm/src/service/team/blocking-preference.js +1 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/offensive-preference.d.ts +1 -2
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +11 -3
- package/dist/esm/src/service/team/schemas/tactics.z.js +44 -11
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +81 -8
- package/dist/esm/src/service/team/schemas/team.z.d.ts +10 -2
- package/dist/esm/src/service/team/tactics.d.ts +5 -0
- package/dist/esm/src/service/team/tactics.js +3 -1
- package/package.json +1 -1
|
@@ -277,7 +277,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
277
277
|
designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
278
278
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
279
279
|
rotation: z.ZodNumber;
|
|
280
|
-
|
|
280
|
+
weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
281
281
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
282
282
|
FAST: "FAST";
|
|
283
283
|
HIGH: "HIGH";
|
|
@@ -289,12 +289,20 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
289
289
|
}, z.core.$strip>>>;
|
|
290
290
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
291
291
|
rotation: z.ZodNumber;
|
|
292
|
-
|
|
292
|
+
weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
293
293
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
294
294
|
FAST: "FAST";
|
|
295
295
|
HIGH: "HIGH";
|
|
296
296
|
}>>>;
|
|
297
297
|
}, z.core.$strip>>>>;
|
|
298
|
+
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
299
|
+
rotation: z.ZodNumber;
|
|
300
|
+
targets: z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>;
|
|
301
|
+
}, z.core.$strip>>>;
|
|
302
|
+
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
303
|
+
rotation: z.ZodNumber;
|
|
304
|
+
targets: z.ZodRecord<z.ZodString, z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>>;
|
|
305
|
+
}, z.core.$strip>>>>;
|
|
298
306
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
299
307
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
300
308
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -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 { BlockingPreference } from './blocking-preference';
|
|
8
9
|
import { InjuryReplacement } from './injury-replacement';
|
|
9
10
|
import { LiberoInjuryConfig } from './libero-injury';
|
|
10
11
|
export interface StartingLineup {
|
|
@@ -38,6 +39,8 @@ export interface TacticsOpts {
|
|
|
38
39
|
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
39
40
|
readonly injuryReplacements: InjuryReplacement[];
|
|
40
41
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
42
|
+
readonly blockingPreferences: BlockingPreference[];
|
|
43
|
+
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
41
44
|
}
|
|
42
45
|
export declare class Tactics {
|
|
43
46
|
readonly lineup: StartingLineup;
|
|
@@ -56,6 +59,8 @@ export declare class Tactics {
|
|
|
56
59
|
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
57
60
|
readonly injuryReplacements: InjuryReplacement[];
|
|
58
61
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
62
|
+
readonly blockingPreferences: BlockingPreference[];
|
|
63
|
+
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
59
64
|
static create(input: unknown): Tactics;
|
|
60
65
|
private constructor();
|
|
61
66
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -18,7 +18,7 @@ class Tactics {
|
|
|
18
18
|
}
|
|
19
19
|
return new Tactics(result.data);
|
|
20
20
|
}
|
|
21
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury }) {
|
|
21
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -35,6 +35,8 @@ class Tactics {
|
|
|
35
35
|
this.replaceKnockedImmediately = replaceKnockedImmediately;
|
|
36
36
|
this.injuryReplacements = injuryReplacements;
|
|
37
37
|
this.liberoInjury = liberoInjury;
|
|
38
|
+
this.blockingPreferences = blockingPreferences;
|
|
39
|
+
this.blockingPreferenceSets = blockingPreferenceSets;
|
|
38
40
|
}
|
|
39
41
|
findPlayerInLineup(id) {
|
|
40
42
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -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, PointBreakdown, RallyMetrics, 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, EnergyBand, LiberoSubMode, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } 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, PointBreakdown, RallyMetrics, 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, EnergyBand, LiberoSubMode, MatchForfeitType, PerformanceStats, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../service';
|
|
2
2
|
export type Rally = DataProps<_Rally> & {
|
|
3
3
|
homePlayerPosition: PlayerPosition[];
|
|
4
4
|
awayPlayerPosition: PlayerPosition[];
|
|
@@ -108,9 +108,13 @@ export interface ApiDesignatedSub {
|
|
|
108
108
|
}
|
|
109
109
|
export interface ApiOffensivePreference {
|
|
110
110
|
rotation: number;
|
|
111
|
-
|
|
111
|
+
weights: Record<string, number>;
|
|
112
112
|
tempos?: Record<string, 'FAST' | 'HIGH'>;
|
|
113
113
|
}
|
|
114
|
+
export interface ApiBlockingPreference {
|
|
115
|
+
rotation: number;
|
|
116
|
+
targets: Record<string, RoleEnum>;
|
|
117
|
+
}
|
|
114
118
|
export interface ApiSystemSet {
|
|
115
119
|
rotationSystem: RotationSystemEnum;
|
|
116
120
|
designatedSetters: string[];
|
|
@@ -148,6 +152,8 @@ export interface Tactics {
|
|
|
148
152
|
offensivePreferences: ApiOffensivePreference[];
|
|
149
153
|
systemSets?: ApiSystemSet[];
|
|
150
154
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
155
|
+
blockingPreferences: ApiBlockingPreference[];
|
|
156
|
+
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
151
157
|
replaceKnockedImmediately?: Record<string, boolean>;
|
|
152
158
|
injuryReplacements?: ApiInjuryReplacement[];
|
|
153
159
|
liberoInjury?: ApiLiberoInjury;
|
|
@@ -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 { ConditionLogic, CourtPosition, EnergyBand, LiberoSubMode, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
4
|
+
import { ConditionLogic, CourtPosition, EnergyBand, LiberoSubMode, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -41,9 +41,13 @@ export interface DesignatedSubAttributes {
|
|
|
41
41
|
}
|
|
42
42
|
export interface OffensivePreferenceAttributes {
|
|
43
43
|
rotation: number;
|
|
44
|
-
|
|
44
|
+
weights: Record<PlayerId, number>;
|
|
45
45
|
tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
|
|
46
46
|
}
|
|
47
|
+
export interface BlockingPreferenceAttributes {
|
|
48
|
+
rotation: number;
|
|
49
|
+
targets: Record<PlayerId, RoleEnum>;
|
|
50
|
+
}
|
|
47
51
|
export interface SystemSetAttributes {
|
|
48
52
|
rotationSystem: RotationSystemEnum;
|
|
49
53
|
designatedSetters: PlayerId[];
|
|
@@ -85,10 +89,12 @@ export interface TacticsAttributes {
|
|
|
85
89
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
86
90
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
87
91
|
libero_injury?: LiberoInjuryAttributes;
|
|
92
|
+
blocking_preferences: BlockingPreferenceAttributes[];
|
|
93
|
+
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
88
94
|
}
|
|
89
95
|
export type TacticsPk = 'team_id';
|
|
90
96
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
91
|
-
export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury';
|
|
97
|
+
export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets';
|
|
92
98
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
93
99
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
94
100
|
team_id: string;
|
|
@@ -108,6 +114,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
108
114
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
109
115
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
110
116
|
libero_injury?: LiberoInjuryAttributes;
|
|
117
|
+
blocking_preferences: BlockingPreferenceAttributes[];
|
|
118
|
+
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
111
119
|
team: TeamModel;
|
|
112
120
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
113
121
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -83,6 +83,15 @@ export class TacticsModel extends Model {
|
|
|
83
83
|
libero_injury: {
|
|
84
84
|
type: DataTypes.JSONB,
|
|
85
85
|
allowNull: true
|
|
86
|
+
},
|
|
87
|
+
blocking_preferences: {
|
|
88
|
+
type: DataTypes.JSONB,
|
|
89
|
+
allowNull: false,
|
|
90
|
+
defaultValue: []
|
|
91
|
+
},
|
|
92
|
+
blocking_preference_sets: {
|
|
93
|
+
type: DataTypes.JSONB,
|
|
94
|
+
allowNull: true
|
|
86
95
|
}
|
|
87
96
|
}, {
|
|
88
97
|
sequelize,
|
|
@@ -86,7 +86,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
86
86
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
87
87
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
88
88
|
rotation: p.rotation,
|
|
89
|
-
|
|
89
|
+
weights: p.weights,
|
|
90
90
|
tempos: p.tempos
|
|
91
91
|
})),
|
|
92
92
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -95,9 +95,11 @@ function transformToAttributes(tactics, teamId) {
|
|
|
95
95
|
})),
|
|
96
96
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
97
97
|
rotation: p.rotation,
|
|
98
|
-
|
|
98
|
+
weights: p.weights,
|
|
99
99
|
tempos: p.tempos
|
|
100
100
|
}))),
|
|
101
|
+
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
102
|
+
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
101
103
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
102
104
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
103
105
|
starterId: ir.starter.id,
|
|
@@ -186,7 +188,7 @@ function transformToObject(model, roster) {
|
|
|
186
188
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
187
189
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
188
190
|
rotation: p.rotation,
|
|
189
|
-
|
|
191
|
+
weights: p.weights,
|
|
190
192
|
tempos: p.tempos
|
|
191
193
|
})),
|
|
192
194
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -195,9 +197,11 @@ function transformToObject(model, roster) {
|
|
|
195
197
|
})),
|
|
196
198
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
197
199
|
rotation: p.rotation,
|
|
198
|
-
|
|
200
|
+
weights: p.weights,
|
|
199
201
|
tempos: p.tempos
|
|
200
202
|
}))),
|
|
203
|
+
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
204
|
+
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
201
205
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
202
206
|
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
203
207
|
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
@@ -354,6 +358,8 @@ function tacticsColumnsToApiDto(model) {
|
|
|
354
358
|
offensivePreferences: model.offensive_preferences,
|
|
355
359
|
systemSets: model.system_sets,
|
|
356
360
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
361
|
+
blockingPreferences: model.blocking_preferences,
|
|
362
|
+
blockingPreferenceSets: model.blocking_preference_sets,
|
|
357
363
|
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
358
364
|
// libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
|
|
359
365
|
// survive; an empty map surfaces as absent (missing key = ON).
|
|
@@ -459,6 +465,8 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
459
465
|
offensive_preferences: config.offensivePreferences,
|
|
460
466
|
system_sets: config.systemSets,
|
|
461
467
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
468
|
+
blocking_preferences: config.blockingPreferences ?? [],
|
|
469
|
+
blocking_preference_sets: config.blockingPreferenceSets,
|
|
462
470
|
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
463
471
|
injury_replacements: config.injuryReplacements,
|
|
464
472
|
libero_injury: config.liberoInjury
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { tacticsColumnsToApiDto, transformFromTactics, transformToTactics } from './tactics';
|
|
3
|
-
import { CourtPosition, EnergyBand, RotationSystemEnum, Tactics } from '../../service';
|
|
3
|
+
import { CourtPosition, EnergyBand, RoleEnum, RotationSystemEnum, Tactics } from '../../service';
|
|
4
4
|
import { makeCountry, makePlayer } from '../../service/test-helpers';
|
|
5
5
|
// Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
|
|
6
6
|
function makeTactics(designatedSubs) {
|
|
@@ -24,7 +24,8 @@ function makeTactics(designatedSubs) {
|
|
|
24
24
|
libero_replacement_sets: undefined,
|
|
25
25
|
rotation_system: '5-1',
|
|
26
26
|
designated_setters: ['lf'],
|
|
27
|
-
offensive_preferences: [{ rotation: 1,
|
|
27
|
+
offensive_preferences: [{ rotation: 1, weights: { lf: 60, rf: 40 } }],
|
|
28
|
+
blocking_preferences: []
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
describe('tacticsColumnsToApiDto()', () => {
|
|
@@ -35,7 +36,7 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
35
36
|
expect(dto.rotationSystem).toBe('5-1');
|
|
36
37
|
expect(dto.liberoReplacements).toEqual(['lf']);
|
|
37
38
|
expect(dto.lineup.bench).toEqual(['b1', 'b2']);
|
|
38
|
-
expect(dto.offensivePreferences).toEqual([{ rotation: 1,
|
|
39
|
+
expect(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
|
|
39
40
|
});
|
|
40
41
|
it("resolves a legacy isPinchServer entry to mode 'PINCH'", () => {
|
|
41
42
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
@@ -125,10 +126,10 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
125
126
|
it('passes per-set tactics columns through to the API DTO shape', () => {
|
|
126
127
|
const model = makeTactics([]);
|
|
127
128
|
model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
128
|
-
model.offensive_preference_sets = [[{ rotation: 1,
|
|
129
|
+
model.offensive_preference_sets = [[{ rotation: 1, weights: { rf: 55, mf: 45 } }]];
|
|
129
130
|
const dto = tacticsColumnsToApiDto(model);
|
|
130
131
|
expect(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
|
|
131
|
-
expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1,
|
|
132
|
+
expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, weights: { rf: 55, mf: 45 } }]]);
|
|
132
133
|
});
|
|
133
134
|
it('leaves the per-set DTO fields undefined when the columns are absent', () => {
|
|
134
135
|
const dto = tacticsColumnsToApiDto(makeTactics([]));
|
|
@@ -152,7 +153,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
152
153
|
{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
|
|
153
154
|
{ rotationSystem: RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
|
|
154
155
|
],
|
|
155
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
156
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]
|
|
156
157
|
});
|
|
157
158
|
it('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
|
|
158
159
|
const attrs = transformFromTactics(tactics, 'team-1');
|
|
@@ -160,7 +161,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
160
161
|
{ rotationSystem: '5-1', designatedSetters: [p1.id] },
|
|
161
162
|
{ rotationSystem: '6-0', designatedSetters: [] }
|
|
162
163
|
]);
|
|
163
|
-
expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1,
|
|
164
|
+
expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]);
|
|
164
165
|
});
|
|
165
166
|
it('transformToTactics resolves the stored ids back to roster players', () => {
|
|
166
167
|
const attrs = transformFromTactics(tactics, 'team-1');
|
|
@@ -168,7 +169,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
168
169
|
expect(back.systemSets?.[0].rotationSystem).toBe(RotationSystemEnum.FIVE_ONE);
|
|
169
170
|
expect(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
|
|
170
171
|
expect(back.systemSets?.[1].designatedSetters).toEqual([]);
|
|
171
|
-
expect(back.offensivePreferenceSets?.[0][0].
|
|
172
|
+
expect(back.offensivePreferenceSets?.[0][0].weights).toEqual({ [p2.id]: 60, [p3.id]: 40 });
|
|
172
173
|
});
|
|
173
174
|
it('omits the per-set columns entirely for an all-sets config', () => {
|
|
174
175
|
const flat = Tactics.create({
|
|
@@ -186,15 +187,28 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
186
187
|
const withTempos = Tactics.create({
|
|
187
188
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
188
189
|
liberoReplacements: [],
|
|
189
|
-
offensivePreferences: [{ rotation: 1,
|
|
190
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
190
191
|
});
|
|
191
192
|
const attrs = transformFromTactics(withTempos, 'team-1');
|
|
192
193
|
expect(attrs.offensive_preferences).toEqual([
|
|
193
|
-
{ rotation: 1,
|
|
194
|
+
{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
|
|
194
195
|
]);
|
|
195
196
|
const back = transformToTactics(attrs, roster);
|
|
196
197
|
expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
197
198
|
});
|
|
199
|
+
it('round-trips blocking preferences as id-keyed function maps', () => {
|
|
200
|
+
const withBlocking = Tactics.create({
|
|
201
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
202
|
+
liberoReplacements: [],
|
|
203
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
204
|
+
});
|
|
205
|
+
const attrs = transformFromTactics(withBlocking, 'team-1');
|
|
206
|
+
expect(attrs.blocking_preferences).toEqual([
|
|
207
|
+
{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
208
|
+
]);
|
|
209
|
+
const back = transformToTactics(attrs, roster);
|
|
210
|
+
expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
211
|
+
});
|
|
198
212
|
});
|
|
199
213
|
describe('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
200
214
|
const country = makeCountry();
|
|
@@ -38,6 +38,11 @@ export class Standing {
|
|
|
38
38
|
// effective ratio here that treats "won with nothing lost" as +Infinity, ranking it highest.
|
|
39
39
|
const effectiveRatio = (won, lost) => lost > 0 ? won / lost : (won > 0 ? Infinity : 0);
|
|
40
40
|
return (standingA, standingB) => {
|
|
41
|
+
// VNL/FIVB order: matches WON is the first criterion, then match points, then set ratio, point ratio, h2h.
|
|
42
|
+
if (standingB.matchesWon > standingA.matchesWon)
|
|
43
|
+
return 1;
|
|
44
|
+
if (standingA.matchesWon > standingB.matchesWon)
|
|
45
|
+
return -1;
|
|
41
46
|
if (standingB.points > standingA.points)
|
|
42
47
|
return 1;
|
|
43
48
|
if (standingA.points > standingB.points)
|
|
@@ -113,23 +113,34 @@ describe('Standing', () => {
|
|
|
113
113
|
});
|
|
114
114
|
// ─── sortFn ──────────────────────────────────────────────────────────────────
|
|
115
115
|
describe('sortFn()', () => {
|
|
116
|
-
it('
|
|
116
|
+
it('ranks by matches won ahead of points (VNL first criterion)', () => {
|
|
117
117
|
const idA = uuidv4();
|
|
118
118
|
const idB = uuidv4();
|
|
119
|
-
// A
|
|
119
|
+
// A: won32=2 → 2 wins, 4 points. B: won30=1 + lost23=2 → 1 win, 5 points.
|
|
120
|
+
// Wins is the first criterion, so A ranks above B even though B has more points.
|
|
121
|
+
const a = makeStanding(idA, { won32: 2 });
|
|
122
|
+
const b = makeStanding(idB, { won30: 1, lost23: 2 });
|
|
123
|
+
const sorted = [b, a].sort(Standing.sortFn([]));
|
|
124
|
+
expect(sorted[0].teamId).toBe(idA);
|
|
125
|
+
expect(sorted[1].teamId).toBe(idB);
|
|
126
|
+
});
|
|
127
|
+
it('breaks ties by points when wins are equal', () => {
|
|
128
|
+
const idA = uuidv4();
|
|
129
|
+
const idB = uuidv4();
|
|
130
|
+
// Both 1 win. A: won30=1 → 3 points. B: won32=1 → 2 points. Points decides once wins tie.
|
|
120
131
|
const a = makeStanding(idA, { won30: 1 });
|
|
121
|
-
const b = makeStanding(idB);
|
|
132
|
+
const b = makeStanding(idB, { won32: 1 });
|
|
122
133
|
const sorted = [b, a].sort(Standing.sortFn([]));
|
|
123
134
|
expect(sorted[0].teamId).toBe(idA);
|
|
124
135
|
expect(sorted[1].teamId).toBe(idB);
|
|
125
136
|
});
|
|
126
|
-
it('breaks ties by setsRatio when points are equal', () => {
|
|
137
|
+
it('breaks ties by setsRatio when wins and points are equal', () => {
|
|
127
138
|
const idA = uuidv4();
|
|
128
139
|
const idB = uuidv4();
|
|
129
|
-
// Both 4 points, but A has better setsRatio:
|
|
130
|
-
// A:
|
|
131
|
-
// B: won31=1, lost23=1 →
|
|
132
|
-
const a = makeStanding(idA, {
|
|
140
|
+
// Both 1 win and 4 points, but A has the better setsRatio:
|
|
141
|
+
// A: won30=1, lost23=1 → setsWon=5, setsLost=3, setsRatio≈1.67
|
|
142
|
+
// B: won31=1, lost23=1 → setsWon=5, setsLost=4, setsRatio=1.25
|
|
143
|
+
const a = makeStanding(idA, { won30: 1, lost23: 1 });
|
|
133
144
|
const b = makeStanding(idB, { won31: 1, lost23: 1 });
|
|
134
145
|
const sorted = [b, a].sort(Standing.sortFn([]));
|
|
135
146
|
expect(sorted[0].teamId).toBe(idA);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -11,5 +11,6 @@ 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 './blocking-preference';
|
|
14
15
|
export * from './injury-replacement';
|
|
15
16
|
export * from './libero-injury';
|
|
@@ -11,5 +11,6 @@ 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 './blocking-preference';
|
|
14
15
|
export * from './injury-replacement';
|
|
15
16
|
export * from './libero-injury';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Player } from '../player';
|
|
2
1
|
export type TempoPreference = 'FAST' | 'HIGH';
|
|
3
2
|
export interface OffensivePreference {
|
|
4
3
|
readonly rotation: number;
|
|
5
|
-
readonly
|
|
4
|
+
readonly weights: Record<string, number>;
|
|
6
5
|
readonly tempos?: Record<string, TempoPreference>;
|
|
7
6
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { Player } from '../../player';
|
|
2
|
+
import { Player, RoleEnum } from '../../player';
|
|
3
3
|
import { EnergyBand } from '../energy-band';
|
|
4
4
|
import { RotationSystemEnum } from '../rotation-system';
|
|
5
5
|
export declare const TacticsInputSchema: z.ZodObject<{
|
|
@@ -269,7 +269,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
269
269
|
designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
270
270
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
271
271
|
rotation: z.ZodNumber;
|
|
272
|
-
|
|
272
|
+
weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
273
273
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
274
274
|
FAST: "FAST";
|
|
275
275
|
HIGH: "HIGH";
|
|
@@ -281,12 +281,20 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
281
281
|
}, z.core.$strip>>>;
|
|
282
282
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
283
283
|
rotation: z.ZodNumber;
|
|
284
|
-
|
|
284
|
+
weights: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
285
285
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
286
286
|
FAST: "FAST";
|
|
287
287
|
HIGH: "HIGH";
|
|
288
288
|
}>>>;
|
|
289
289
|
}, z.core.$strip>>>>;
|
|
290
|
+
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
291
|
+
rotation: z.ZodNumber;
|
|
292
|
+
targets: z.ZodRecord<z.ZodString, 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>>>;
|
|
293
|
+
}, z.core.$strip>>>;
|
|
294
|
+
blockingPreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
295
|
+
rotation: z.ZodNumber;
|
|
296
|
+
targets: z.ZodRecord<z.ZodString, 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>>>;
|
|
297
|
+
}, z.core.$strip>>>>;
|
|
290
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
291
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
292
300
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { Player } from '../../player';
|
|
2
|
+
import { Player, RoleEnum } from '../../player';
|
|
3
3
|
import { EnergyBand } from '../energy-band';
|
|
4
4
|
import { RotationSystemEnum, setterCountFor } from '../rotation-system';
|
|
5
5
|
import { DesignatedSubSchema, SubBandSchema } from './designated-sub.z';
|
|
@@ -47,14 +47,23 @@ const lineupSchema = z.object({
|
|
|
47
47
|
});
|
|
48
48
|
// Per-player tempo preference (set-tempo rework): absent key = Auto.
|
|
49
49
|
const tempoPreferenceSchema = z.enum(['FAST', 'HIGH']);
|
|
50
|
-
// One rotation's
|
|
51
|
-
//
|
|
52
|
-
//
|
|
50
|
+
// One rotation's attacker weighting. `rotation` is the main setter's rotational slot (1..6); `weights` maps each
|
|
51
|
+
// attacker's starter id to an integer percentage share (values sum to 100; 0 removes an attacker). `tempos`
|
|
52
|
+
// optionally forces a per-starter tempo (Auto when absent). Cross-field checks (keys are starters, weights sum to
|
|
53
|
+
// 100) live in the superRefine below.
|
|
53
54
|
const offensivePreferenceSchema = z.object({
|
|
54
55
|
rotation: z.number().int().min(1).max(6),
|
|
55
|
-
|
|
56
|
+
weights: z.record(z.string(), z.number().int().min(0)),
|
|
56
57
|
tempos: z.record(z.string(), tempoPreferenceSchema).optional()
|
|
57
58
|
});
|
|
59
|
+
// The opposing attacking functions a dedicated blocker can commit to: every RoleEnum except the libero.
|
|
60
|
+
const blockingFunctionSchema = z.nativeEnum(RoleEnum).refine((v) => v !== RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
|
|
61
|
+
// One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
|
|
62
|
+
// each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
|
|
63
|
+
const blockingPreferenceSchema = z.object({
|
|
64
|
+
rotation: z.number().int().min(1).max(6),
|
|
65
|
+
targets: z.record(z.string(), blockingFunctionSchema)
|
|
66
|
+
});
|
|
58
67
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
59
68
|
const systemSetSchema = z.object({
|
|
60
69
|
rotationSystem: z.nativeEnum(RotationSystemEnum),
|
|
@@ -82,6 +91,9 @@ export const TacticsInputSchema = z.object({
|
|
|
82
91
|
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
83
92
|
systemSets: z.array(systemSetSchema).max(5).optional(),
|
|
84
93
|
offensivePreferenceSets: z.array(z.array(offensivePreferenceSchema)).max(5).optional(),
|
|
94
|
+
// Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
|
|
95
|
+
blockingPreferences: z.array(blockingPreferenceSchema).default([]),
|
|
96
|
+
blockingPreferenceSets: z.array(z.array(blockingPreferenceSchema)).max(5).optional(),
|
|
85
97
|
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
86
98
|
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
87
99
|
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
@@ -150,16 +162,17 @@ export const TacticsInputSchema = z.object({
|
|
|
150
162
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
151
163
|
}
|
|
152
164
|
seenRotations.add(pref.rotation);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path });
|
|
156
|
-
}
|
|
157
|
-
for (const id of ids) {
|
|
165
|
+
// Weight keys are on-court starters, and the percentages must sum to exactly 100 (0 removes an attacker).
|
|
166
|
+
for (const id of Object.keys(pref.weights)) {
|
|
158
167
|
if (!starterIds.has(id)) {
|
|
159
168
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
|
-
|
|
171
|
+
const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
|
|
172
|
+
if (total !== 100) {
|
|
173
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
|
|
174
|
+
}
|
|
175
|
+
// Tempo preference keys are starter ids too.
|
|
163
176
|
for (const id of Object.keys(pref.tempos ?? {})) {
|
|
164
177
|
if (!starterIds.has(id)) {
|
|
165
178
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
|
|
@@ -167,6 +180,22 @@ export const TacticsInputSchema = z.object({
|
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
};
|
|
183
|
+
// Blocking preferences: unique rotations; each assignment keys an on-court starter (the UI only offers front-row
|
|
184
|
+
// players, the schema checks starters). The target function is validated by blockingFunctionSchema.
|
|
185
|
+
const validateBlockingPreferences = (preferences, path) => {
|
|
186
|
+
const seenRotations = new Set();
|
|
187
|
+
for (const pref of preferences) {
|
|
188
|
+
if (seenRotations.has(pref.rotation)) {
|
|
189
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
190
|
+
}
|
|
191
|
+
seenRotations.add(pref.rotation);
|
|
192
|
+
for (const id of Object.keys(pref.targets)) {
|
|
193
|
+
if (!starterIds.has(id)) {
|
|
194
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
170
199
|
validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
|
|
171
200
|
for (const [i, systemSet] of (data.systemSets ?? []).entries()) {
|
|
172
201
|
validateSystem(systemSet.rotationSystem, systemSet.designatedSetters, ['systemSets', i]);
|
|
@@ -175,6 +204,10 @@ export const TacticsInputSchema = z.object({
|
|
|
175
204
|
for (const [i, preferences] of (data.offensivePreferenceSets ?? []).entries()) {
|
|
176
205
|
validateOffensivePreferences(preferences, ['offensivePreferenceSets', i]);
|
|
177
206
|
}
|
|
207
|
+
validateBlockingPreferences(data.blockingPreferences, ['blockingPreferences']);
|
|
208
|
+
for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
|
|
209
|
+
validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
|
|
210
|
+
}
|
|
178
211
|
// Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
|
|
179
212
|
// rule (FATIGUE/ALWAYS in any set) needs someone to swap in, so it requires a second libero, and a second
|
|
180
213
|
// libero needs a starting libero to replace ("no L2 without an L"). Per-set libero replacements, like the
|