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
|
@@ -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>;
|
|
@@ -86,6 +86,15 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
86
86
|
libero_injury: {
|
|
87
87
|
type: sequelize_1.DataTypes.JSONB,
|
|
88
88
|
allowNull: true
|
|
89
|
+
},
|
|
90
|
+
blocking_preferences: {
|
|
91
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
92
|
+
allowNull: false,
|
|
93
|
+
defaultValue: []
|
|
94
|
+
},
|
|
95
|
+
blocking_preference_sets: {
|
|
96
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
97
|
+
allowNull: true
|
|
89
98
|
}
|
|
90
99
|
}, {
|
|
91
100
|
sequelize,
|
|
@@ -92,7 +92,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
92
92
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
93
93
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
94
94
|
rotation: p.rotation,
|
|
95
|
-
|
|
95
|
+
weights: p.weights,
|
|
96
96
|
tempos: p.tempos
|
|
97
97
|
})),
|
|
98
98
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -101,9 +101,11 @@ function transformToAttributes(tactics, teamId) {
|
|
|
101
101
|
})),
|
|
102
102
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
103
103
|
rotation: p.rotation,
|
|
104
|
-
|
|
104
|
+
weights: p.weights,
|
|
105
105
|
tempos: p.tempos
|
|
106
106
|
}))),
|
|
107
|
+
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
108
|
+
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
107
109
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
108
110
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
109
111
|
starterId: ir.starter.id,
|
|
@@ -192,7 +194,7 @@ function transformToObject(model, roster) {
|
|
|
192
194
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
193
195
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
194
196
|
rotation: p.rotation,
|
|
195
|
-
|
|
197
|
+
weights: p.weights,
|
|
196
198
|
tempos: p.tempos
|
|
197
199
|
})),
|
|
198
200
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -201,9 +203,11 @@ function transformToObject(model, roster) {
|
|
|
201
203
|
})),
|
|
202
204
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
203
205
|
rotation: p.rotation,
|
|
204
|
-
|
|
206
|
+
weights: p.weights,
|
|
205
207
|
tempos: p.tempos
|
|
206
208
|
}))),
|
|
209
|
+
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
210
|
+
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
207
211
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
208
212
|
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
209
213
|
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
@@ -360,6 +364,8 @@ function tacticsColumnsToApiDto(model) {
|
|
|
360
364
|
offensivePreferences: model.offensive_preferences,
|
|
361
365
|
systemSets: model.system_sets,
|
|
362
366
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
367
|
+
blockingPreferences: model.blocking_preferences,
|
|
368
|
+
blockingPreferenceSets: model.blocking_preference_sets,
|
|
363
369
|
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
364
370
|
// libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
|
|
365
371
|
// survive; an empty map surfaces as absent (missing key = ON).
|
|
@@ -465,6 +471,8 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
465
471
|
offensive_preferences: config.offensivePreferences,
|
|
466
472
|
system_sets: config.systemSets,
|
|
467
473
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
474
|
+
blocking_preferences: config.blockingPreferences ?? [],
|
|
475
|
+
blocking_preference_sets: config.blockingPreferenceSets,
|
|
468
476
|
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
469
477
|
injury_replacements: config.injuryReplacements,
|
|
470
478
|
libero_injury: config.liberoInjury
|
|
@@ -26,7 +26,8 @@ function makeTactics(designatedSubs) {
|
|
|
26
26
|
libero_replacement_sets: undefined,
|
|
27
27
|
rotation_system: '5-1',
|
|
28
28
|
designated_setters: ['lf'],
|
|
29
|
-
offensive_preferences: [{ rotation: 1,
|
|
29
|
+
offensive_preferences: [{ rotation: 1, weights: { lf: 60, rf: 40 } }],
|
|
30
|
+
blocking_preferences: []
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
(0, globals_1.describe)('tacticsColumnsToApiDto()', () => {
|
|
@@ -37,7 +38,7 @@ function makeTactics(designatedSubs) {
|
|
|
37
38
|
(0, globals_1.expect)(dto.rotationSystem).toBe('5-1');
|
|
38
39
|
(0, globals_1.expect)(dto.liberoReplacements).toEqual(['lf']);
|
|
39
40
|
(0, globals_1.expect)(dto.lineup.bench).toEqual(['b1', 'b2']);
|
|
40
|
-
(0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1,
|
|
41
|
+
(0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
|
|
41
42
|
});
|
|
42
43
|
(0, globals_1.it)("resolves a legacy isPinchServer entry to mode 'PINCH'", () => {
|
|
43
44
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
|
|
@@ -127,10 +128,10 @@ function makeTactics(designatedSubs) {
|
|
|
127
128
|
(0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
|
|
128
129
|
const model = makeTactics([]);
|
|
129
130
|
model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
130
|
-
model.offensive_preference_sets = [[{ rotation: 1,
|
|
131
|
+
model.offensive_preference_sets = [[{ rotation: 1, weights: { rf: 55, mf: 45 } }]];
|
|
131
132
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
132
133
|
(0, globals_1.expect)(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
|
|
133
|
-
(0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1,
|
|
134
|
+
(0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, weights: { rf: 55, mf: 45 } }]]);
|
|
134
135
|
});
|
|
135
136
|
(0, globals_1.it)('leaves the per-set DTO fields undefined when the columns are absent', () => {
|
|
136
137
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
|
|
@@ -154,7 +155,7 @@ function makeTactics(designatedSubs) {
|
|
|
154
155
|
{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
|
|
155
156
|
{ rotationSystem: service_1.RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
|
|
156
157
|
],
|
|
157
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
158
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]
|
|
158
159
|
});
|
|
159
160
|
(0, globals_1.it)('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
|
|
160
161
|
const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
|
|
@@ -162,7 +163,7 @@ function makeTactics(designatedSubs) {
|
|
|
162
163
|
{ rotationSystem: '5-1', designatedSetters: [p1.id] },
|
|
163
164
|
{ rotationSystem: '6-0', designatedSetters: [] }
|
|
164
165
|
]);
|
|
165
|
-
(0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1,
|
|
166
|
+
(0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]);
|
|
166
167
|
});
|
|
167
168
|
(0, globals_1.it)('transformToTactics resolves the stored ids back to roster players', () => {
|
|
168
169
|
const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
|
|
@@ -170,7 +171,7 @@ function makeTactics(designatedSubs) {
|
|
|
170
171
|
(0, globals_1.expect)(back.systemSets?.[0].rotationSystem).toBe(service_1.RotationSystemEnum.FIVE_ONE);
|
|
171
172
|
(0, globals_1.expect)(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
|
|
172
173
|
(0, globals_1.expect)(back.systemSets?.[1].designatedSetters).toEqual([]);
|
|
173
|
-
(0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].
|
|
174
|
+
(0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].weights).toEqual({ [p2.id]: 60, [p3.id]: 40 });
|
|
174
175
|
});
|
|
175
176
|
(0, globals_1.it)('omits the per-set columns entirely for an all-sets config', () => {
|
|
176
177
|
const flat = service_1.Tactics.create({
|
|
@@ -188,15 +189,28 @@ function makeTactics(designatedSubs) {
|
|
|
188
189
|
const withTempos = service_1.Tactics.create({
|
|
189
190
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
190
191
|
liberoReplacements: [],
|
|
191
|
-
offensivePreferences: [{ rotation: 1,
|
|
192
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
192
193
|
});
|
|
193
194
|
const attrs = (0, tactics_1.transformFromTactics)(withTempos, 'team-1');
|
|
194
195
|
(0, globals_1.expect)(attrs.offensive_preferences).toEqual([
|
|
195
|
-
{ rotation: 1,
|
|
196
|
+
{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
|
|
196
197
|
]);
|
|
197
198
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
198
199
|
(0, globals_1.expect)(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
199
200
|
});
|
|
201
|
+
(0, globals_1.it)('round-trips blocking preferences as id-keyed function maps', () => {
|
|
202
|
+
const withBlocking = service_1.Tactics.create({
|
|
203
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
204
|
+
liberoReplacements: [],
|
|
205
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }]
|
|
206
|
+
});
|
|
207
|
+
const attrs = (0, tactics_1.transformFromTactics)(withBlocking, 'team-1');
|
|
208
|
+
(0, globals_1.expect)(attrs.blocking_preferences).toEqual([
|
|
209
|
+
{ rotation: 1, targets: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }
|
|
210
|
+
]);
|
|
211
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
212
|
+
(0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
|
|
213
|
+
});
|
|
200
214
|
});
|
|
201
215
|
(0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
202
216
|
const country = (0, test_helpers_1.makeCountry)();
|
|
@@ -41,6 +41,11 @@ class Standing {
|
|
|
41
41
|
// effective ratio here that treats "won with nothing lost" as +Infinity, ranking it highest.
|
|
42
42
|
const effectiveRatio = (won, lost) => lost > 0 ? won / lost : (won > 0 ? Infinity : 0);
|
|
43
43
|
return (standingA, standingB) => {
|
|
44
|
+
// VNL/FIVB order: matches WON is the first criterion, then match points, then set ratio, point ratio, h2h.
|
|
45
|
+
if (standingB.matchesWon > standingA.matchesWon)
|
|
46
|
+
return 1;
|
|
47
|
+
if (standingA.matchesWon > standingB.matchesWon)
|
|
48
|
+
return -1;
|
|
44
49
|
if (standingB.points > standingA.points)
|
|
45
50
|
return 1;
|
|
46
51
|
if (standingA.points > standingB.points)
|
|
@@ -115,23 +115,34 @@ function makeStanding(teamId, opts = {}) {
|
|
|
115
115
|
});
|
|
116
116
|
// ─── sortFn ──────────────────────────────────────────────────────────────────
|
|
117
117
|
(0, globals_1.describe)('sortFn()', () => {
|
|
118
|
-
(0, globals_1.it)('
|
|
118
|
+
(0, globals_1.it)('ranks by matches won ahead of points (VNL first criterion)', () => {
|
|
119
119
|
const idA = (0, uuid_1.v4)();
|
|
120
120
|
const idB = (0, uuid_1.v4)();
|
|
121
|
-
// A
|
|
121
|
+
// A: won32=2 → 2 wins, 4 points. B: won30=1 + lost23=2 → 1 win, 5 points.
|
|
122
|
+
// Wins is the first criterion, so A ranks above B even though B has more points.
|
|
123
|
+
const a = makeStanding(idA, { won32: 2 });
|
|
124
|
+
const b = makeStanding(idB, { won30: 1, lost23: 2 });
|
|
125
|
+
const sorted = [b, a].sort(standing_1.Standing.sortFn([]));
|
|
126
|
+
(0, globals_1.expect)(sorted[0].teamId).toBe(idA);
|
|
127
|
+
(0, globals_1.expect)(sorted[1].teamId).toBe(idB);
|
|
128
|
+
});
|
|
129
|
+
(0, globals_1.it)('breaks ties by points when wins are equal', () => {
|
|
130
|
+
const idA = (0, uuid_1.v4)();
|
|
131
|
+
const idB = (0, uuid_1.v4)();
|
|
132
|
+
// Both 1 win. A: won30=1 → 3 points. B: won32=1 → 2 points. Points decides once wins tie.
|
|
122
133
|
const a = makeStanding(idA, { won30: 1 });
|
|
123
|
-
const b = makeStanding(idB);
|
|
134
|
+
const b = makeStanding(idB, { won32: 1 });
|
|
124
135
|
const sorted = [b, a].sort(standing_1.Standing.sortFn([]));
|
|
125
136
|
(0, globals_1.expect)(sorted[0].teamId).toBe(idA);
|
|
126
137
|
(0, globals_1.expect)(sorted[1].teamId).toBe(idB);
|
|
127
138
|
});
|
|
128
|
-
(0, globals_1.it)('breaks ties by setsRatio when points are equal', () => {
|
|
139
|
+
(0, globals_1.it)('breaks ties by setsRatio when wins and points are equal', () => {
|
|
129
140
|
const idA = (0, uuid_1.v4)();
|
|
130
141
|
const idB = (0, uuid_1.v4)();
|
|
131
|
-
// Both 4 points, but A has better setsRatio:
|
|
132
|
-
// A:
|
|
133
|
-
// B: won31=1, lost23=1 →
|
|
134
|
-
const a = makeStanding(idA, {
|
|
142
|
+
// Both 1 win and 4 points, but A has the better setsRatio:
|
|
143
|
+
// A: won30=1, lost23=1 → setsWon=5, setsLost=3, setsRatio≈1.67
|
|
144
|
+
// B: won31=1, lost23=1 → setsWon=5, setsLost=4, setsRatio=1.25
|
|
145
|
+
const a = makeStanding(idA, { won30: 1, lost23: 1 });
|
|
135
146
|
const b = makeStanding(idB, { won31: 1, lost23: 1 });
|
|
136
147
|
const sorted = [b, a].sort(standing_1.Standing.sortFn([]));
|
|
137
148
|
(0, globals_1.expect)(sorted[0].teamId).toBe(idA);
|
|
@@ -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';
|
|
@@ -27,5 +27,6 @@ __exportStar(require("./rotation-system"), exports);
|
|
|
27
27
|
__exportStar(require("./lineup-function"), exports);
|
|
28
28
|
__exportStar(require("./base-position"), exports);
|
|
29
29
|
__exportStar(require("./offensive-preference"), exports);
|
|
30
|
+
__exportStar(require("./blocking-preference"), exports);
|
|
30
31
|
__exportStar(require("./injury-replacement"), exports);
|
|
31
32
|
__exportStar(require("./libero-injury"), exports);
|
|
@@ -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>;
|
|
@@ -50,14 +50,23 @@ const lineupSchema = zod_1.z.object({
|
|
|
50
50
|
});
|
|
51
51
|
// Per-player tempo preference (set-tempo rework): absent key = Auto.
|
|
52
52
|
const tempoPreferenceSchema = zod_1.z.enum(['FAST', 'HIGH']);
|
|
53
|
-
// One rotation's
|
|
54
|
-
//
|
|
55
|
-
//
|
|
53
|
+
// One rotation's attacker weighting. `rotation` is the main setter's rotational slot (1..6); `weights` maps each
|
|
54
|
+
// attacker's starter id to an integer percentage share (values sum to 100; 0 removes an attacker). `tempos`
|
|
55
|
+
// optionally forces a per-starter tempo (Auto when absent). Cross-field checks (keys are starters, weights sum to
|
|
56
|
+
// 100) live in the superRefine below.
|
|
56
57
|
const offensivePreferenceSchema = zod_1.z.object({
|
|
57
58
|
rotation: zod_1.z.number().int().min(1).max(6),
|
|
58
|
-
|
|
59
|
+
weights: zod_1.z.record(zod_1.z.string(), zod_1.z.number().int().min(0)),
|
|
59
60
|
tempos: zod_1.z.record(zod_1.z.string(), tempoPreferenceSchema).optional()
|
|
60
61
|
});
|
|
62
|
+
// The opposing attacking functions a dedicated blocker can commit to: every RoleEnum except the libero.
|
|
63
|
+
const blockingFunctionSchema = zod_1.z.nativeEnum(player_1.RoleEnum).refine((v) => v !== player_1.RoleEnum.LIBERO, { message: 'BLOCKING_TARGET_CANNOT_BE_LIBERO' });
|
|
64
|
+
// One rotation's dedicated-blocking assignments: `targets` maps my front-row starter ids to the opposing function
|
|
65
|
+
// each commits to block. Cross-field checks (keys are starters) live in the superRefine below.
|
|
66
|
+
const blockingPreferenceSchema = zod_1.z.object({
|
|
67
|
+
rotation: zod_1.z.number().int().min(1).max(6),
|
|
68
|
+
targets: zod_1.z.record(zod_1.z.string(), blockingFunctionSchema)
|
|
69
|
+
});
|
|
61
70
|
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
62
71
|
const systemSetSchema = zod_1.z.object({
|
|
63
72
|
rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum),
|
|
@@ -85,6 +94,9 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
85
94
|
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
86
95
|
systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
|
|
87
96
|
offensivePreferenceSets: zod_1.z.array(zod_1.z.array(offensivePreferenceSchema)).max(5).optional(),
|
|
97
|
+
// Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
|
|
98
|
+
blockingPreferences: zod_1.z.array(blockingPreferenceSchema).default([]),
|
|
99
|
+
blockingPreferenceSets: zod_1.z.array(zod_1.z.array(blockingPreferenceSchema)).max(5).optional(),
|
|
88
100
|
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
89
101
|
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
90
102
|
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
@@ -153,16 +165,17 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
153
165
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
154
166
|
}
|
|
155
167
|
seenRotations.add(pref.rotation);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path });
|
|
159
|
-
}
|
|
160
|
-
for (const id of ids) {
|
|
168
|
+
// Weight keys are on-court starters, and the percentages must sum to exactly 100 (0 removes an attacker).
|
|
169
|
+
for (const id of Object.keys(pref.weights)) {
|
|
161
170
|
if (!starterIds.has(id)) {
|
|
162
171
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
|
-
|
|
174
|
+
const total = Object.values(pref.weights).reduce((sum, w) => sum + w, 0);
|
|
175
|
+
if (total !== 100) {
|
|
176
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100', path });
|
|
177
|
+
}
|
|
178
|
+
// Tempo preference keys are starter ids too.
|
|
166
179
|
for (const id of Object.keys(pref.tempos ?? {})) {
|
|
167
180
|
if (!starterIds.has(id)) {
|
|
168
181
|
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_TEMPO_NOT_A_STARTER', path });
|
|
@@ -170,6 +183,22 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
170
183
|
}
|
|
171
184
|
}
|
|
172
185
|
};
|
|
186
|
+
// Blocking preferences: unique rotations; each assignment keys an on-court starter (the UI only offers front-row
|
|
187
|
+
// players, the schema checks starters). The target function is validated by blockingFunctionSchema.
|
|
188
|
+
const validateBlockingPreferences = (preferences, path) => {
|
|
189
|
+
const seenRotations = new Set();
|
|
190
|
+
for (const pref of preferences) {
|
|
191
|
+
if (seenRotations.has(pref.rotation)) {
|
|
192
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
193
|
+
}
|
|
194
|
+
seenRotations.add(pref.rotation);
|
|
195
|
+
for (const id of Object.keys(pref.targets)) {
|
|
196
|
+
if (!starterIds.has(id)) {
|
|
197
|
+
ctx.addIssue({ code: 'custom', message: 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER', path });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
173
202
|
validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
|
|
174
203
|
for (const [i, systemSet] of (data.systemSets ?? []).entries()) {
|
|
175
204
|
validateSystem(systemSet.rotationSystem, systemSet.designatedSetters, ['systemSets', i]);
|
|
@@ -178,6 +207,10 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
178
207
|
for (const [i, preferences] of (data.offensivePreferenceSets ?? []).entries()) {
|
|
179
208
|
validateOffensivePreferences(preferences, ['offensivePreferenceSets', i]);
|
|
180
209
|
}
|
|
210
|
+
validateBlockingPreferences(data.blockingPreferences, ['blockingPreferences']);
|
|
211
|
+
for (const [i, preferences] of (data.blockingPreferenceSets ?? []).entries()) {
|
|
212
|
+
validateBlockingPreferences(preferences, ['blockingPreferenceSets', i]);
|
|
213
|
+
}
|
|
181
214
|
// Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
|
|
182
215
|
// rule (FATIGUE/ALWAYS in any set) needs someone to swap in, so it requires a second libero, and a second
|
|
183
216
|
// libero needs a starting libero to replace ("no L2 without an L"). Per-set libero replacements, like the
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const globals_1 = require("@jest/globals");
|
|
4
4
|
const tactics_z_1 = require("./tactics.z");
|
|
5
|
+
const player_1 = require("../../player");
|
|
5
6
|
const test_helpers_1 = require("../../test-helpers");
|
|
6
7
|
(0, globals_1.describe)('TacticsInputSchema — second libero rules', () => {
|
|
7
8
|
const country = (0, test_helpers_1.makeCountry)();
|
|
@@ -95,7 +96,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
95
96
|
{ rotationSystem: '4-2', designatedSetters: [p1, p4] }
|
|
96
97
|
],
|
|
97
98
|
offensivePreferenceSets: [
|
|
98
|
-
[{ rotation: 1,
|
|
99
|
+
[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }],
|
|
99
100
|
[]
|
|
100
101
|
]
|
|
101
102
|
}));
|
|
@@ -104,7 +105,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
104
105
|
(0, globals_1.it)('carries the per-set fields through Tactics.create parsing', () => {
|
|
105
106
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
106
107
|
systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
|
|
107
|
-
offensivePreferenceSets: [[{ rotation: 2,
|
|
108
|
+
offensivePreferenceSets: [[{ rotation: 2, weights: { [p3.id]: 100 } }]]
|
|
108
109
|
}));
|
|
109
110
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
110
111
|
if (res.success) {
|
|
@@ -153,7 +154,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
153
154
|
});
|
|
154
155
|
(0, globals_1.it)('rejects a per-set preferences list with a duplicate rotation', () => {
|
|
155
156
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
156
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
157
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 100 } }, { rotation: 1, weights: { [p3.id]: 100 } }]]
|
|
157
158
|
}));
|
|
158
159
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
159
160
|
if (!res.success) {
|
|
@@ -161,18 +162,27 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
161
162
|
(0, globals_1.expect)(issue?.path).toEqual(['offensivePreferenceSets', 0]);
|
|
162
163
|
}
|
|
163
164
|
});
|
|
164
|
-
(0, globals_1.it)('rejects a per-set preferences
|
|
165
|
+
(0, globals_1.it)('rejects a per-set preferences map that keys a non-starter', () => {
|
|
165
166
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
166
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
167
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [reserve.id]: 100 } }]]
|
|
167
168
|
}));
|
|
168
169
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
169
170
|
if (!res.success) {
|
|
170
171
|
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
|
|
171
172
|
}
|
|
172
173
|
});
|
|
174
|
+
(0, globals_1.it)('rejects offensive preference weights that do not sum to 100', () => {
|
|
175
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
176
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 50, [p3.id]: 40 } }]
|
|
177
|
+
}));
|
|
178
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
179
|
+
if (!res.success) {
|
|
180
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100')).toBe(true);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
173
183
|
(0, globals_1.it)('accepts offensive preferences with a starter-keyed tempos map', () => {
|
|
174
184
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
175
|
-
offensivePreferences: [{ rotation: 1,
|
|
185
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
176
186
|
}));
|
|
177
187
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
178
188
|
if (res.success) {
|
|
@@ -181,7 +191,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
181
191
|
});
|
|
182
192
|
(0, globals_1.it)('rejects a tempos map keyed by a non-starter', () => {
|
|
183
193
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
184
|
-
offensivePreferences: [{ rotation: 1,
|
|
194
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [reserve.id]: 'FAST' } }]
|
|
185
195
|
}));
|
|
186
196
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
187
197
|
if (!res.success) {
|
|
@@ -190,7 +200,7 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
190
200
|
});
|
|
191
201
|
(0, globals_1.it)('rejects an invalid tempo value', () => {
|
|
192
202
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
193
|
-
offensivePreferences: [{ rotation: 1,
|
|
203
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [p2.id]: 'SLOW' } }]
|
|
194
204
|
}));
|
|
195
205
|
(0, globals_1.expect)(res.success).toBe(false);
|
|
196
206
|
});
|
|
@@ -208,3 +218,66 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
208
218
|
}
|
|
209
219
|
});
|
|
210
220
|
});
|
|
221
|
+
(0, globals_1.describe)('TacticsInputSchema — blocking preferences', () => {
|
|
222
|
+
const country = (0, test_helpers_1.makeCountry)();
|
|
223
|
+
const p1 = (0, test_helpers_1.makePlayer)(country);
|
|
224
|
+
const p2 = (0, test_helpers_1.makePlayer)(country);
|
|
225
|
+
const p3 = (0, test_helpers_1.makePlayer)(country);
|
|
226
|
+
const p4 = (0, test_helpers_1.makePlayer)(country);
|
|
227
|
+
const p5 = (0, test_helpers_1.makePlayer)(country);
|
|
228
|
+
const p6 = (0, test_helpers_1.makePlayer)(country);
|
|
229
|
+
const reserve = (0, test_helpers_1.makePlayer)(country);
|
|
230
|
+
// Front-row starters (zones 2/3/4) are p2, p3, p4; the validator only requires target keys to be starters.
|
|
231
|
+
function baseInput(overrides = {}) {
|
|
232
|
+
return {
|
|
233
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
234
|
+
liberoReplacements: [],
|
|
235
|
+
...overrides
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
(0, globals_1.it)('accepts a blocking preference that keys starters to attacking functions', () => {
|
|
239
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
240
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: player_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER } }]
|
|
241
|
+
}));
|
|
242
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
243
|
+
if (res.success) {
|
|
244
|
+
(0, globals_1.expect)(res.data.blockingPreferences[0].targets[p4.id]).toBe(player_1.RoleEnum.OUTSIDE_HITTER);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
(0, globals_1.it)('rejects a blocking target that is not a starter', () => {
|
|
248
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
249
|
+
blockingPreferences: [{ rotation: 1, targets: { [reserve.id]: player_1.RoleEnum.OUTSIDE_HITTER } }]
|
|
250
|
+
}));
|
|
251
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
252
|
+
if (!res.success) {
|
|
253
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
(0, globals_1.it)('rejects a blocking target function of libero', () => {
|
|
257
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
258
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: player_1.RoleEnum.LIBERO } }]
|
|
259
|
+
}));
|
|
260
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
261
|
+
if (!res.success) {
|
|
262
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_TARGET_CANNOT_BE_LIBERO')).toBe(true);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
(0, globals_1.it)('rejects a duplicate rotation in blocking preferences', () => {
|
|
266
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
267
|
+
blockingPreferences: [
|
|
268
|
+
{ rotation: 1, targets: { [p4.id]: player_1.RoleEnum.OUTSIDE_HITTER } },
|
|
269
|
+
{ rotation: 1, targets: { [p3.id]: player_1.RoleEnum.MIDDLE_BLOCKER } }
|
|
270
|
+
]
|
|
271
|
+
}));
|
|
272
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
273
|
+
if (!res.success) {
|
|
274
|
+
(0, globals_1.expect)(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION')).toBe(true);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
(0, globals_1.it)('accepts per-set blocking preference overrides', () => {
|
|
278
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
279
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: player_1.RoleEnum.OPPOSITE_HITTER } }], []]
|
|
280
|
+
}));
|
|
281
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
});
|