volleyballsimtypes 0.0.481 → 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/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/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
|
@@ -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();
|
|
@@ -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
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { TacticsInputSchema } from './tactics.z';
|
|
3
|
+
import { RoleEnum } from '../../player';
|
|
3
4
|
import { makeCountry, makePlayer } from '../../test-helpers';
|
|
4
5
|
describe('TacticsInputSchema — second libero rules', () => {
|
|
5
6
|
const country = makeCountry();
|
|
@@ -93,7 +94,7 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
93
94
|
{ rotationSystem: '4-2', designatedSetters: [p1, p4] }
|
|
94
95
|
],
|
|
95
96
|
offensivePreferenceSets: [
|
|
96
|
-
[{ rotation: 1,
|
|
97
|
+
[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }],
|
|
97
98
|
[]
|
|
98
99
|
]
|
|
99
100
|
}));
|
|
@@ -102,7 +103,7 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
102
103
|
it('carries the per-set fields through Tactics.create parsing', () => {
|
|
103
104
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
104
105
|
systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
|
|
105
|
-
offensivePreferenceSets: [[{ rotation: 2,
|
|
106
|
+
offensivePreferenceSets: [[{ rotation: 2, weights: { [p3.id]: 100 } }]]
|
|
106
107
|
}));
|
|
107
108
|
expect(res.success).toBe(true);
|
|
108
109
|
if (res.success) {
|
|
@@ -151,7 +152,7 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
151
152
|
});
|
|
152
153
|
it('rejects a per-set preferences list with a duplicate rotation', () => {
|
|
153
154
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
154
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
155
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 100 } }, { rotation: 1, weights: { [p3.id]: 100 } }]]
|
|
155
156
|
}));
|
|
156
157
|
expect(res.success).toBe(false);
|
|
157
158
|
if (!res.success) {
|
|
@@ -159,18 +160,27 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
159
160
|
expect(issue?.path).toEqual(['offensivePreferenceSets', 0]);
|
|
160
161
|
}
|
|
161
162
|
});
|
|
162
|
-
it('rejects a per-set preferences
|
|
163
|
+
it('rejects a per-set preferences map that keys a non-starter', () => {
|
|
163
164
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
164
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
165
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [reserve.id]: 100 } }]]
|
|
165
166
|
}));
|
|
166
167
|
expect(res.success).toBe(false);
|
|
167
168
|
if (!res.success) {
|
|
168
169
|
expect(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
|
|
169
170
|
}
|
|
170
171
|
});
|
|
172
|
+
it('rejects offensive preference weights that do not sum to 100', () => {
|
|
173
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
174
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 50, [p3.id]: 40 } }]
|
|
175
|
+
}));
|
|
176
|
+
expect(res.success).toBe(false);
|
|
177
|
+
if (!res.success) {
|
|
178
|
+
expect(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_WEIGHTS_MUST_SUM_TO_100')).toBe(true);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
171
181
|
it('accepts offensive preferences with a starter-keyed tempos map', () => {
|
|
172
182
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
173
|
-
offensivePreferences: [{ rotation: 1,
|
|
183
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
174
184
|
}));
|
|
175
185
|
expect(res.success).toBe(true);
|
|
176
186
|
if (res.success) {
|
|
@@ -179,7 +189,7 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
179
189
|
});
|
|
180
190
|
it('rejects a tempos map keyed by a non-starter', () => {
|
|
181
191
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
182
|
-
offensivePreferences: [{ rotation: 1,
|
|
192
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [reserve.id]: 'FAST' } }]
|
|
183
193
|
}));
|
|
184
194
|
expect(res.success).toBe(false);
|
|
185
195
|
if (!res.success) {
|
|
@@ -188,7 +198,7 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
188
198
|
});
|
|
189
199
|
it('rejects an invalid tempo value', () => {
|
|
190
200
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
191
|
-
offensivePreferences: [{ rotation: 1,
|
|
201
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 100 }, tempos: { [p2.id]: 'SLOW' } }]
|
|
192
202
|
}));
|
|
193
203
|
expect(res.success).toBe(false);
|
|
194
204
|
});
|
|
@@ -206,3 +216,66 @@ describe('TacticsInputSchema — per-set tactics (systemSets / offensivePreferen
|
|
|
206
216
|
}
|
|
207
217
|
});
|
|
208
218
|
});
|
|
219
|
+
describe('TacticsInputSchema — blocking preferences', () => {
|
|
220
|
+
const country = makeCountry();
|
|
221
|
+
const p1 = makePlayer(country);
|
|
222
|
+
const p2 = makePlayer(country);
|
|
223
|
+
const p3 = makePlayer(country);
|
|
224
|
+
const p4 = makePlayer(country);
|
|
225
|
+
const p5 = makePlayer(country);
|
|
226
|
+
const p6 = makePlayer(country);
|
|
227
|
+
const reserve = makePlayer(country);
|
|
228
|
+
// Front-row starters (zones 2/3/4) are p2, p3, p4; the validator only requires target keys to be starters.
|
|
229
|
+
function baseInput(overrides = {}) {
|
|
230
|
+
return {
|
|
231
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
232
|
+
liberoReplacements: [],
|
|
233
|
+
...overrides
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
it('accepts a blocking preference that keys starters to attacking functions', () => {
|
|
237
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
238
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
239
|
+
}));
|
|
240
|
+
expect(res.success).toBe(true);
|
|
241
|
+
if (res.success) {
|
|
242
|
+
expect(res.data.blockingPreferences[0].targets[p4.id]).toBe(RoleEnum.OUTSIDE_HITTER);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
it('rejects a blocking target that is not a starter', () => {
|
|
246
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
247
|
+
blockingPreferences: [{ rotation: 1, targets: { [reserve.id]: RoleEnum.OUTSIDE_HITTER } }]
|
|
248
|
+
}));
|
|
249
|
+
expect(res.success).toBe(false);
|
|
250
|
+
if (!res.success) {
|
|
251
|
+
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_BLOCKER_NOT_A_STARTER')).toBe(true);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
it('rejects a blocking target function of libero', () => {
|
|
255
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
256
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.LIBERO } }]
|
|
257
|
+
}));
|
|
258
|
+
expect(res.success).toBe(false);
|
|
259
|
+
if (!res.success) {
|
|
260
|
+
expect(res.error.issues.some(i => i.message === 'BLOCKING_TARGET_CANNOT_BE_LIBERO')).toBe(true);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
it('rejects a duplicate rotation in blocking preferences', () => {
|
|
264
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
265
|
+
blockingPreferences: [
|
|
266
|
+
{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER } },
|
|
267
|
+
{ rotation: 1, targets: { [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
268
|
+
]
|
|
269
|
+
}));
|
|
270
|
+
expect(res.success).toBe(false);
|
|
271
|
+
if (!res.success) {
|
|
272
|
+
expect(res.error.issues.some(i => i.message === 'BLOCKING_PREFERENCE_DUPLICATE_ROTATION')).toBe(true);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
it('accepts per-set blocking preference overrides', () => {
|
|
276
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
277
|
+
blockingPreferenceSets: [[{ rotation: 1, targets: { [p4.id]: RoleEnum.OPPOSITE_HITTER } }], []]
|
|
278
|
+
}));
|
|
279
|
+
expect(res.success).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -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>;
|