volleyballsimtypes 0.0.456 → 0.0.459
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 +6 -0
- package/dist/cjs/src/data/models/tactics.d.ts +9 -1
- package/dist/cjs/src/data/models/tactics.js +8 -0
- package/dist/cjs/src/data/models/team.js +4 -0
- package/dist/cjs/src/data/transformers/tactics.js +21 -3
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +8 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +53 -31
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +8 -0
- package/dist/cjs/src/service/team/tactics.d.ts +8 -0
- package/dist/cjs/src/service/team/tactics.js +3 -1
- package/dist/esm/src/api/index.d.ts +6 -0
- package/dist/esm/src/data/models/tactics.d.ts +9 -1
- package/dist/esm/src/data/models/tactics.js +8 -0
- package/dist/esm/src/data/models/team.js +4 -0
- package/dist/esm/src/data/transformers/tactics.js +21 -3
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +8 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +53 -31
- package/dist/esm/src/service/team/schemas/team.z.d.ts +8 -0
- package/dist/esm/src/service/team/tactics.d.ts +8 -0
- package/dist/esm/src/service/team/tactics.js +3 -1
- package/package.json +1 -1
|
@@ -95,6 +95,10 @@ export interface ApiOffensivePreference {
|
|
|
95
95
|
rotation: number;
|
|
96
96
|
order: string[];
|
|
97
97
|
}
|
|
98
|
+
export interface ApiSystemSet {
|
|
99
|
+
rotationSystem: RotationSystemEnum;
|
|
100
|
+
designatedSetters: string[];
|
|
101
|
+
}
|
|
98
102
|
export interface ApiLiberoSetSubConfig {
|
|
99
103
|
mode: LiberoSubMode;
|
|
100
104
|
fatigueBand?: EnergyBand;
|
|
@@ -114,6 +118,8 @@ export interface Tactics {
|
|
|
114
118
|
rotationSystem: RotationSystemEnum;
|
|
115
119
|
designatedSetters: string[];
|
|
116
120
|
offensivePreferences: ApiOffensivePreference[];
|
|
121
|
+
systemSets?: ApiSystemSet[];
|
|
122
|
+
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
117
123
|
}
|
|
118
124
|
export interface ApiLineupPreset {
|
|
119
125
|
presetId: string;
|
|
@@ -35,6 +35,10 @@ export interface OffensivePreferenceAttributes {
|
|
|
35
35
|
rotation: number;
|
|
36
36
|
order: PlayerId[];
|
|
37
37
|
}
|
|
38
|
+
export interface SystemSetAttributes {
|
|
39
|
+
rotationSystem: RotationSystemEnum;
|
|
40
|
+
designatedSetters: PlayerId[];
|
|
41
|
+
}
|
|
38
42
|
export interface LiberoSetSubConfigAttributes {
|
|
39
43
|
mode: LiberoSubMode;
|
|
40
44
|
fatigueBand?: EnergyBand;
|
|
@@ -55,10 +59,12 @@ export interface TacticsAttributes {
|
|
|
55
59
|
rotation_system: RotationSystemEnum;
|
|
56
60
|
designated_setters: PlayerId[];
|
|
57
61
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
62
|
+
system_sets?: SystemSetAttributes[];
|
|
63
|
+
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
58
64
|
}
|
|
59
65
|
export type TacticsPk = 'team_id';
|
|
60
66
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
61
|
-
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';
|
|
67
|
+
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';
|
|
62
68
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
63
69
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
64
70
|
team_id: string;
|
|
@@ -73,6 +79,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
73
79
|
rotation_system: RotationSystemEnum;
|
|
74
80
|
designated_setters: PlayerId[];
|
|
75
81
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
82
|
+
system_sets?: SystemSetAttributes[];
|
|
83
|
+
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
76
84
|
team: TeamModel;
|
|
77
85
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
78
86
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -66,6 +66,14 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
66
66
|
type: sequelize_1.DataTypes.JSONB,
|
|
67
67
|
allowNull: false,
|
|
68
68
|
defaultValue: []
|
|
69
|
+
},
|
|
70
|
+
system_sets: {
|
|
71
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
72
|
+
allowNull: true
|
|
73
|
+
},
|
|
74
|
+
offensive_preference_sets: {
|
|
75
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
76
|
+
allowNull: true
|
|
69
77
|
}
|
|
70
78
|
}, {
|
|
71
79
|
sequelize,
|
|
@@ -56,6 +56,10 @@ class TeamModel extends sequelize_1.Model {
|
|
|
56
56
|
{ model: _1.TacticsModel, as: 'tactics' }
|
|
57
57
|
]
|
|
58
58
|
},
|
|
59
|
+
// A partial, case-insensitive UNIQUE index on lower(btrim(name)) WHERE active enforces unique team names
|
|
60
|
+
// among active teams. It lives in migration 2026-07-01-add-team-name-unique.sql + vb_sim.sql, not here:
|
|
61
|
+
// Sequelize's `indexes` can't cleanly express a partial functional index, prod applies it via migration,
|
|
62
|
+
// and local DBs seed it from vb_sim.sql.
|
|
59
63
|
indexes: [{
|
|
60
64
|
name: 'Team_pk',
|
|
61
65
|
unique: true,
|
|
@@ -78,7 +78,15 @@ function transformToAttributes(tactics, teamId) {
|
|
|
78
78
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
79
79
|
rotation: p.rotation,
|
|
80
80
|
order: p.order.map((a) => a.id)
|
|
81
|
-
}))
|
|
81
|
+
})),
|
|
82
|
+
system_sets: tactics.systemSets?.map(s => ({
|
|
83
|
+
rotationSystem: s.rotationSystem,
|
|
84
|
+
designatedSetters: s.designatedSetters.map((p) => p.id)
|
|
85
|
+
})),
|
|
86
|
+
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
87
|
+
rotation: p.rotation,
|
|
88
|
+
order: p.order.map((a) => a.id)
|
|
89
|
+
})))
|
|
82
90
|
};
|
|
83
91
|
}
|
|
84
92
|
function transformToObject(model, roster) {
|
|
@@ -145,7 +153,15 @@ function transformToObject(model, roster) {
|
|
|
145
153
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
146
154
|
rotation: p.rotation,
|
|
147
155
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
148
|
-
}))
|
|
156
|
+
})),
|
|
157
|
+
systemSets: model.system_sets?.map(s => ({
|
|
158
|
+
rotationSystem: s.rotationSystem,
|
|
159
|
+
designatedSetters: s.designatedSetters.map((id) => findPlayer(id, roster))
|
|
160
|
+
})),
|
|
161
|
+
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
162
|
+
rotation: p.rotation,
|
|
163
|
+
order: p.order.map((id) => findPlayer(id, roster))
|
|
164
|
+
})))
|
|
149
165
|
});
|
|
150
166
|
}
|
|
151
167
|
// Serialize a Tactics ROW (db columns) straight to the API Tactics DTO (player references stay as ids; no
|
|
@@ -203,6 +219,8 @@ function tacticsColumnsToApiDto(model) {
|
|
|
203
219
|
})),
|
|
204
220
|
rotationSystem: model.rotation_system,
|
|
205
221
|
designatedSetters: model.designated_setters,
|
|
206
|
-
offensivePreferences: model.offensive_preferences
|
|
222
|
+
offensivePreferences: model.offensive_preferences,
|
|
223
|
+
systemSets: model.system_sets,
|
|
224
|
+
offensivePreferenceSets: model.offensive_preference_sets
|
|
207
225
|
};
|
|
208
226
|
}
|
|
@@ -115,5 +115,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
115
115
|
rotation: z.ZodNumber;
|
|
116
116
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
117
117
|
}, z.core.$strip>>>;
|
|
118
|
+
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
119
|
+
rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
|
|
120
|
+
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
121
|
+
}, z.core.$strip>>>;
|
|
122
|
+
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
123
|
+
rotation: z.ZodNumber;
|
|
124
|
+
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
|
+
}, z.core.$strip>>>>;
|
|
118
126
|
}, z.core.$strip>;
|
|
119
127
|
export type TacticsInput = z.infer<typeof TacticsInputSchema>;
|
|
@@ -54,6 +54,11 @@ const offensivePreferenceSchema = zod_1.z.object({
|
|
|
54
54
|
rotation: zod_1.z.number().int().min(1).max(6),
|
|
55
55
|
order: zod_1.z.array(playerInstanceSchema)
|
|
56
56
|
});
|
|
57
|
+
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
58
|
+
const systemSetSchema = zod_1.z.object({
|
|
59
|
+
rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum),
|
|
60
|
+
designatedSetters: zod_1.z.array(playerInstanceSchema)
|
|
61
|
+
});
|
|
57
62
|
exports.TacticsInputSchema = zod_1.z.object({
|
|
58
63
|
lineup: lineupSchema,
|
|
59
64
|
liberoReplacements: zod_1.z.array(playerInstanceSchema),
|
|
@@ -71,7 +76,11 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
71
76
|
// Tactics.create({...}) call valid and make an unconfigured team play exactly as before.
|
|
72
77
|
rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum).default(rotation_system_1.RotationSystemEnum.SIX_ZERO),
|
|
73
78
|
designatedSetters: zod_1.z.array(playerInstanceSchema).default([]),
|
|
74
|
-
offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([])
|
|
79
|
+
offensivePreferences: zod_1.z.array(offensivePreferenceSchema).default([]),
|
|
80
|
+
// Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
|
|
81
|
+
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
82
|
+
systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
|
|
83
|
+
offensivePreferenceSets: zod_1.z.array(zod_1.z.array(offensivePreferenceSchema)).max(5).optional()
|
|
75
84
|
}).superRefine((data, ctx) => {
|
|
76
85
|
// Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
|
|
77
86
|
// pinch-server outage was caused by an unvalidated tactics payload).
|
|
@@ -90,41 +99,54 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
90
99
|
const starters = slots.map(([position, player]) => ({ position, id: player.id }));
|
|
91
100
|
const starterIds = new Set(starters.map(s => s.id));
|
|
92
101
|
const positionById = new Map(starters.map(s => [s.id, s.position]));
|
|
93
|
-
// Designated setters: count must match the system,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!starterIds.has(setter.id)) {
|
|
100
|
-
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTER_NOT_A_STARTER', path: ['designatedSetters'] });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// 4-2 / 6-2: the two setters sit in opposite slots (3 rotations apart).
|
|
104
|
-
if ((data.rotationSystem === rotation_system_1.RotationSystemEnum.FOUR_TWO || data.rotationSystem === rotation_system_1.RotationSystemEnum.SIX_TWO) &&
|
|
105
|
-
data.designatedSetters.length === 2) {
|
|
106
|
-
const a = positionById.get(data.designatedSetters[0].id);
|
|
107
|
-
const b = positionById.get(data.designatedSetters[1].id);
|
|
108
|
-
if (a != null && b != null && ((((a - b) % 6) + 6) % 6) !== 3) {
|
|
109
|
-
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS', path: ['designatedSetters'] });
|
|
102
|
+
// Designated setters: count must match the system, each must be an on-court starter, and in 4-2 / 6-2 the two
|
|
103
|
+
// setters sit in opposite slots (3 rotations apart). Run on the flat fields and on every per-set entry.
|
|
104
|
+
const validateSystem = (rotationSystem, designatedSetters, path) => {
|
|
105
|
+
const expectedSetters = (0, rotation_system_1.setterCountFor)(rotationSystem);
|
|
106
|
+
if (designatedSetters.length !== expectedSetters) {
|
|
107
|
+
ctx.addIssue({ code: 'custom', message: `DESIGNATED_SETTERS_COUNT_${rotationSystem}_EXPECTED_${expectedSetters}`, path });
|
|
110
108
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (seenRotations.has(pref.rotation)) {
|
|
116
|
-
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path: ['offensivePreferences'] });
|
|
109
|
+
for (const setter of designatedSetters) {
|
|
110
|
+
if (!starterIds.has(setter.id)) {
|
|
111
|
+
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTER_NOT_A_STARTER', path });
|
|
112
|
+
}
|
|
117
113
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
if ((rotationSystem === rotation_system_1.RotationSystemEnum.FOUR_TWO || rotationSystem === rotation_system_1.RotationSystemEnum.SIX_TWO) &&
|
|
115
|
+
designatedSetters.length === 2) {
|
|
116
|
+
const a = positionById.get(designatedSetters[0].id);
|
|
117
|
+
const b = positionById.get(designatedSetters[1].id);
|
|
118
|
+
if (a != null && b != null && ((((a - b) % 6) + 6) % 6) !== 3) {
|
|
119
|
+
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS', path });
|
|
120
|
+
}
|
|
122
121
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
};
|
|
123
|
+
// Offensive preferences: unique rotations, and each order lists only on-court starters with no duplicates.
|
|
124
|
+
// Run on the flat list and on every per-set list.
|
|
125
|
+
const validateOffensivePreferences = (preferences, path) => {
|
|
126
|
+
const seenRotations = new Set();
|
|
127
|
+
for (const pref of preferences) {
|
|
128
|
+
if (seenRotations.has(pref.rotation)) {
|
|
129
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
130
|
+
}
|
|
131
|
+
seenRotations.add(pref.rotation);
|
|
132
|
+
const ids = pref.order.map(p => p.id);
|
|
133
|
+
if (new Set(ids).size !== ids.length) {
|
|
134
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path });
|
|
135
|
+
}
|
|
136
|
+
for (const id of ids) {
|
|
137
|
+
if (!starterIds.has(id)) {
|
|
138
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
139
|
+
}
|
|
126
140
|
}
|
|
127
141
|
}
|
|
142
|
+
};
|
|
143
|
+
validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
|
|
144
|
+
for (const [i, systemSet] of (data.systemSets ?? []).entries()) {
|
|
145
|
+
validateSystem(systemSet.rotationSystem, systemSet.designatedSetters, ['systemSets', i]);
|
|
146
|
+
}
|
|
147
|
+
validateOffensivePreferences(data.offensivePreferences, ['offensivePreferences']);
|
|
148
|
+
for (const [i, preferences] of (data.offensivePreferenceSets ?? []).entries()) {
|
|
149
|
+
validateOffensivePreferences(preferences, ['offensivePreferenceSets', i]);
|
|
128
150
|
}
|
|
129
151
|
// Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
|
|
130
152
|
// rule (FATIGUE/ALWAYS in any set) needs someone to swap in, so it requires a second libero, and a second
|
|
@@ -123,6 +123,14 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
123
123
|
rotation: z.ZodNumber;
|
|
124
124
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
125
|
}, z.core.$strip>>>;
|
|
126
|
+
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
127
|
+
rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
|
|
128
|
+
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
129
|
+
}, z.core.$strip>>>;
|
|
130
|
+
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
131
|
+
rotation: z.ZodNumber;
|
|
132
|
+
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
133
|
+
}, z.core.$strip>>>>;
|
|
126
134
|
}, z.core.$strip>>;
|
|
127
135
|
}, z.core.$strip>;
|
|
128
136
|
export type TeamInput = z.infer<typeof TeamInputSchema>;
|
|
@@ -15,6 +15,10 @@ export interface StartingLineup {
|
|
|
15
15
|
readonly [CourtPosition.LIBERO_ZONE]?: Player;
|
|
16
16
|
readonly bench: Player[];
|
|
17
17
|
}
|
|
18
|
+
export interface SystemSet {
|
|
19
|
+
readonly rotationSystem: RotationSystemEnum;
|
|
20
|
+
readonly designatedSetters: Player[];
|
|
21
|
+
}
|
|
18
22
|
export interface TacticsOpts {
|
|
19
23
|
readonly lineup: StartingLineup;
|
|
20
24
|
readonly liberoReplacements: Player[];
|
|
@@ -27,6 +31,8 @@ export interface TacticsOpts {
|
|
|
27
31
|
readonly rotationSystem: RotationSystemEnum;
|
|
28
32
|
readonly designatedSetters: Player[];
|
|
29
33
|
readonly offensivePreferences: OffensivePreference[];
|
|
34
|
+
readonly systemSets?: SystemSet[];
|
|
35
|
+
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
30
36
|
}
|
|
31
37
|
export declare class Tactics {
|
|
32
38
|
readonly lineup: StartingLineup;
|
|
@@ -40,6 +46,8 @@ export declare class Tactics {
|
|
|
40
46
|
readonly rotationSystem: RotationSystemEnum;
|
|
41
47
|
readonly designatedSetters: Player[];
|
|
42
48
|
readonly offensivePreferences: OffensivePreference[];
|
|
49
|
+
readonly systemSets?: SystemSet[];
|
|
50
|
+
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
43
51
|
static create(input: unknown): Tactics;
|
|
44
52
|
private constructor();
|
|
45
53
|
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 = [] }) {
|
|
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 }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -30,6 +30,8 @@ class Tactics {
|
|
|
30
30
|
this.rotationSystem = rotationSystem;
|
|
31
31
|
this.designatedSetters = designatedSetters;
|
|
32
32
|
this.offensivePreferences = offensivePreferences;
|
|
33
|
+
this.systemSets = systemSets;
|
|
34
|
+
this.offensivePreferenceSets = offensivePreferenceSets;
|
|
33
35
|
}
|
|
34
36
|
findPlayerInLineup(id) {
|
|
35
37
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -95,6 +95,10 @@ export interface ApiOffensivePreference {
|
|
|
95
95
|
rotation: number;
|
|
96
96
|
order: string[];
|
|
97
97
|
}
|
|
98
|
+
export interface ApiSystemSet {
|
|
99
|
+
rotationSystem: RotationSystemEnum;
|
|
100
|
+
designatedSetters: string[];
|
|
101
|
+
}
|
|
98
102
|
export interface ApiLiberoSetSubConfig {
|
|
99
103
|
mode: LiberoSubMode;
|
|
100
104
|
fatigueBand?: EnergyBand;
|
|
@@ -114,6 +118,8 @@ export interface Tactics {
|
|
|
114
118
|
rotationSystem: RotationSystemEnum;
|
|
115
119
|
designatedSetters: string[];
|
|
116
120
|
offensivePreferences: ApiOffensivePreference[];
|
|
121
|
+
systemSets?: ApiSystemSet[];
|
|
122
|
+
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
117
123
|
}
|
|
118
124
|
export interface ApiLineupPreset {
|
|
119
125
|
presetId: string;
|
|
@@ -35,6 +35,10 @@ export interface OffensivePreferenceAttributes {
|
|
|
35
35
|
rotation: number;
|
|
36
36
|
order: PlayerId[];
|
|
37
37
|
}
|
|
38
|
+
export interface SystemSetAttributes {
|
|
39
|
+
rotationSystem: RotationSystemEnum;
|
|
40
|
+
designatedSetters: PlayerId[];
|
|
41
|
+
}
|
|
38
42
|
export interface LiberoSetSubConfigAttributes {
|
|
39
43
|
mode: LiberoSubMode;
|
|
40
44
|
fatigueBand?: EnergyBand;
|
|
@@ -55,10 +59,12 @@ export interface TacticsAttributes {
|
|
|
55
59
|
rotation_system: RotationSystemEnum;
|
|
56
60
|
designated_setters: PlayerId[];
|
|
57
61
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
62
|
+
system_sets?: SystemSetAttributes[];
|
|
63
|
+
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
58
64
|
}
|
|
59
65
|
export type TacticsPk = 'team_id';
|
|
60
66
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
61
|
-
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';
|
|
67
|
+
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';
|
|
62
68
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
63
69
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
64
70
|
team_id: string;
|
|
@@ -73,6 +79,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
73
79
|
rotation_system: RotationSystemEnum;
|
|
74
80
|
designated_setters: PlayerId[];
|
|
75
81
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
82
|
+
system_sets?: SystemSetAttributes[];
|
|
83
|
+
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
76
84
|
team: TeamModel;
|
|
77
85
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
78
86
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -63,6 +63,14 @@ export class TacticsModel extends Model {
|
|
|
63
63
|
type: DataTypes.JSONB,
|
|
64
64
|
allowNull: false,
|
|
65
65
|
defaultValue: []
|
|
66
|
+
},
|
|
67
|
+
system_sets: {
|
|
68
|
+
type: DataTypes.JSONB,
|
|
69
|
+
allowNull: true
|
|
70
|
+
},
|
|
71
|
+
offensive_preference_sets: {
|
|
72
|
+
type: DataTypes.JSONB,
|
|
73
|
+
allowNull: true
|
|
66
74
|
}
|
|
67
75
|
}, {
|
|
68
76
|
sequelize,
|
|
@@ -53,6 +53,10 @@ export class TeamModel extends Model {
|
|
|
53
53
|
{ model: TacticsModel, as: 'tactics' }
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
|
+
// A partial, case-insensitive UNIQUE index on lower(btrim(name)) WHERE active enforces unique team names
|
|
57
|
+
// among active teams. It lives in migration 2026-07-01-add-team-name-unique.sql + vb_sim.sql, not here:
|
|
58
|
+
// Sequelize's `indexes` can't cleanly express a partial functional index, prod applies it via migration,
|
|
59
|
+
// and local DBs seed it from vb_sim.sql.
|
|
56
60
|
indexes: [{
|
|
57
61
|
name: 'Team_pk',
|
|
58
62
|
unique: true,
|
|
@@ -73,7 +73,15 @@ function transformToAttributes(tactics, teamId) {
|
|
|
73
73
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
74
74
|
rotation: p.rotation,
|
|
75
75
|
order: p.order.map((a) => a.id)
|
|
76
|
-
}))
|
|
76
|
+
})),
|
|
77
|
+
system_sets: tactics.systemSets?.map(s => ({
|
|
78
|
+
rotationSystem: s.rotationSystem,
|
|
79
|
+
designatedSetters: s.designatedSetters.map((p) => p.id)
|
|
80
|
+
})),
|
|
81
|
+
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
82
|
+
rotation: p.rotation,
|
|
83
|
+
order: p.order.map((a) => a.id)
|
|
84
|
+
})))
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
function transformToObject(model, roster) {
|
|
@@ -140,7 +148,15 @@ function transformToObject(model, roster) {
|
|
|
140
148
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
141
149
|
rotation: p.rotation,
|
|
142
150
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
143
|
-
}))
|
|
151
|
+
})),
|
|
152
|
+
systemSets: model.system_sets?.map(s => ({
|
|
153
|
+
rotationSystem: s.rotationSystem,
|
|
154
|
+
designatedSetters: s.designatedSetters.map((id) => findPlayer(id, roster))
|
|
155
|
+
})),
|
|
156
|
+
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
157
|
+
rotation: p.rotation,
|
|
158
|
+
order: p.order.map((id) => findPlayer(id, roster))
|
|
159
|
+
})))
|
|
144
160
|
});
|
|
145
161
|
}
|
|
146
162
|
// Serialize a Tactics ROW (db columns) straight to the API Tactics DTO (player references stay as ids; no
|
|
@@ -198,7 +214,9 @@ function tacticsColumnsToApiDto(model) {
|
|
|
198
214
|
})),
|
|
199
215
|
rotationSystem: model.rotation_system,
|
|
200
216
|
designatedSetters: model.designated_setters,
|
|
201
|
-
offensivePreferences: model.offensive_preferences
|
|
217
|
+
offensivePreferences: model.offensive_preferences,
|
|
218
|
+
systemSets: model.system_sets,
|
|
219
|
+
offensivePreferenceSets: model.offensive_preference_sets
|
|
202
220
|
};
|
|
203
221
|
}
|
|
204
222
|
export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto };
|
|
@@ -115,5 +115,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
115
115
|
rotation: z.ZodNumber;
|
|
116
116
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
117
117
|
}, z.core.$strip>>>;
|
|
118
|
+
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
119
|
+
rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
|
|
120
|
+
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
121
|
+
}, z.core.$strip>>>;
|
|
122
|
+
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
123
|
+
rotation: z.ZodNumber;
|
|
124
|
+
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
|
+
}, z.core.$strip>>>>;
|
|
118
126
|
}, z.core.$strip>;
|
|
119
127
|
export type TacticsInput = z.infer<typeof TacticsInputSchema>;
|
|
@@ -51,6 +51,11 @@ const offensivePreferenceSchema = z.object({
|
|
|
51
51
|
rotation: z.number().int().min(1).max(6),
|
|
52
52
|
order: z.array(playerInstanceSchema)
|
|
53
53
|
});
|
|
54
|
+
// One set's rotation-system override (per-set tactics): system + its designated setters travel together.
|
|
55
|
+
const systemSetSchema = z.object({
|
|
56
|
+
rotationSystem: z.nativeEnum(RotationSystemEnum),
|
|
57
|
+
designatedSetters: z.array(playerInstanceSchema)
|
|
58
|
+
});
|
|
54
59
|
export const TacticsInputSchema = z.object({
|
|
55
60
|
lineup: lineupSchema,
|
|
56
61
|
liberoReplacements: z.array(playerInstanceSchema),
|
|
@@ -68,7 +73,11 @@ export const TacticsInputSchema = z.object({
|
|
|
68
73
|
// Tactics.create({...}) call valid and make an unconfigured team play exactly as before.
|
|
69
74
|
rotationSystem: z.nativeEnum(RotationSystemEnum).default(RotationSystemEnum.SIX_ZERO),
|
|
70
75
|
designatedSetters: z.array(playerInstanceSchema).default([]),
|
|
71
|
-
offensivePreferences: z.array(offensivePreferenceSchema).default([])
|
|
76
|
+
offensivePreferences: z.array(offensivePreferenceSchema).default([]),
|
|
77
|
+
// Per-set overrides (5 entries, index 0 = set 1; absent => the flat fields apply to all sets). Each entry is
|
|
78
|
+
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
79
|
+
systemSets: z.array(systemSetSchema).max(5).optional(),
|
|
80
|
+
offensivePreferenceSets: z.array(z.array(offensivePreferenceSchema)).max(5).optional()
|
|
72
81
|
}).superRefine((data, ctx) => {
|
|
73
82
|
// Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
|
|
74
83
|
// pinch-server outage was caused by an unvalidated tactics payload).
|
|
@@ -87,41 +96,54 @@ export const TacticsInputSchema = z.object({
|
|
|
87
96
|
const starters = slots.map(([position, player]) => ({ position, id: player.id }));
|
|
88
97
|
const starterIds = new Set(starters.map(s => s.id));
|
|
89
98
|
const positionById = new Map(starters.map(s => [s.id, s.position]));
|
|
90
|
-
// Designated setters: count must match the system,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (!starterIds.has(setter.id)) {
|
|
97
|
-
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTER_NOT_A_STARTER', path: ['designatedSetters'] });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// 4-2 / 6-2: the two setters sit in opposite slots (3 rotations apart).
|
|
101
|
-
if ((data.rotationSystem === RotationSystemEnum.FOUR_TWO || data.rotationSystem === RotationSystemEnum.SIX_TWO) &&
|
|
102
|
-
data.designatedSetters.length === 2) {
|
|
103
|
-
const a = positionById.get(data.designatedSetters[0].id);
|
|
104
|
-
const b = positionById.get(data.designatedSetters[1].id);
|
|
105
|
-
if (a != null && b != null && ((((a - b) % 6) + 6) % 6) !== 3) {
|
|
106
|
-
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS', path: ['designatedSetters'] });
|
|
99
|
+
// Designated setters: count must match the system, each must be an on-court starter, and in 4-2 / 6-2 the two
|
|
100
|
+
// setters sit in opposite slots (3 rotations apart). Run on the flat fields and on every per-set entry.
|
|
101
|
+
const validateSystem = (rotationSystem, designatedSetters, path) => {
|
|
102
|
+
const expectedSetters = setterCountFor(rotationSystem);
|
|
103
|
+
if (designatedSetters.length !== expectedSetters) {
|
|
104
|
+
ctx.addIssue({ code: 'custom', message: `DESIGNATED_SETTERS_COUNT_${rotationSystem}_EXPECTED_${expectedSetters}`, path });
|
|
107
105
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (seenRotations.has(pref.rotation)) {
|
|
113
|
-
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path: ['offensivePreferences'] });
|
|
106
|
+
for (const setter of designatedSetters) {
|
|
107
|
+
if (!starterIds.has(setter.id)) {
|
|
108
|
+
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTER_NOT_A_STARTER', path });
|
|
109
|
+
}
|
|
114
110
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
if ((rotationSystem === RotationSystemEnum.FOUR_TWO || rotationSystem === RotationSystemEnum.SIX_TWO) &&
|
|
112
|
+
designatedSetters.length === 2) {
|
|
113
|
+
const a = positionById.get(designatedSetters[0].id);
|
|
114
|
+
const b = positionById.get(designatedSetters[1].id);
|
|
115
|
+
if (a != null && b != null && ((((a - b) % 6) + 6) % 6) !== 3) {
|
|
116
|
+
ctx.addIssue({ code: 'custom', message: 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS', path });
|
|
117
|
+
}
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
};
|
|
120
|
+
// Offensive preferences: unique rotations, and each order lists only on-court starters with no duplicates.
|
|
121
|
+
// Run on the flat list and on every per-set list.
|
|
122
|
+
const validateOffensivePreferences = (preferences, path) => {
|
|
123
|
+
const seenRotations = new Set();
|
|
124
|
+
for (const pref of preferences) {
|
|
125
|
+
if (seenRotations.has(pref.rotation)) {
|
|
126
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION', path });
|
|
127
|
+
}
|
|
128
|
+
seenRotations.add(pref.rotation);
|
|
129
|
+
const ids = pref.order.map(p => p.id);
|
|
130
|
+
if (new Set(ids).size !== ids.length) {
|
|
131
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path });
|
|
132
|
+
}
|
|
133
|
+
for (const id of ids) {
|
|
134
|
+
if (!starterIds.has(id)) {
|
|
135
|
+
ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path });
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
}
|
|
139
|
+
};
|
|
140
|
+
validateSystem(data.rotationSystem, data.designatedSetters, ['designatedSetters']);
|
|
141
|
+
for (const [i, systemSet] of (data.systemSets ?? []).entries()) {
|
|
142
|
+
validateSystem(systemSet.rotationSystem, systemSet.designatedSetters, ['systemSets', i]);
|
|
143
|
+
}
|
|
144
|
+
validateOffensivePreferences(data.offensivePreferences, ['offensivePreferences']);
|
|
145
|
+
for (const [i, preferences] of (data.offensivePreferenceSets ?? []).entries()) {
|
|
146
|
+
validateOffensivePreferences(preferences, ['offensivePreferenceSets', i]);
|
|
125
147
|
}
|
|
126
148
|
// Second libero (L2). The second libero is a reserve (not a starter, the libero, or a bench player). A swap
|
|
127
149
|
// rule (FATIGUE/ALWAYS in any set) needs someone to swap in, so it requires a second libero, and a second
|
|
@@ -123,6 +123,14 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
123
123
|
rotation: z.ZodNumber;
|
|
124
124
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
125
|
}, z.core.$strip>>>;
|
|
126
|
+
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
127
|
+
rotationSystem: z.ZodEnum<typeof import("..").RotationSystemEnum>;
|
|
128
|
+
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
129
|
+
}, z.core.$strip>>>;
|
|
130
|
+
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
131
|
+
rotation: z.ZodNumber;
|
|
132
|
+
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
133
|
+
}, z.core.$strip>>>>;
|
|
126
134
|
}, z.core.$strip>>;
|
|
127
135
|
}, z.core.$strip>;
|
|
128
136
|
export type TeamInput = z.infer<typeof TeamInputSchema>;
|
|
@@ -15,6 +15,10 @@ export interface StartingLineup {
|
|
|
15
15
|
readonly [CourtPosition.LIBERO_ZONE]?: Player;
|
|
16
16
|
readonly bench: Player[];
|
|
17
17
|
}
|
|
18
|
+
export interface SystemSet {
|
|
19
|
+
readonly rotationSystem: RotationSystemEnum;
|
|
20
|
+
readonly designatedSetters: Player[];
|
|
21
|
+
}
|
|
18
22
|
export interface TacticsOpts {
|
|
19
23
|
readonly lineup: StartingLineup;
|
|
20
24
|
readonly liberoReplacements: Player[];
|
|
@@ -27,6 +31,8 @@ export interface TacticsOpts {
|
|
|
27
31
|
readonly rotationSystem: RotationSystemEnum;
|
|
28
32
|
readonly designatedSetters: Player[];
|
|
29
33
|
readonly offensivePreferences: OffensivePreference[];
|
|
34
|
+
readonly systemSets?: SystemSet[];
|
|
35
|
+
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
30
36
|
}
|
|
31
37
|
export declare class Tactics {
|
|
32
38
|
readonly lineup: StartingLineup;
|
|
@@ -40,6 +46,8 @@ export declare class Tactics {
|
|
|
40
46
|
readonly rotationSystem: RotationSystemEnum;
|
|
41
47
|
readonly designatedSetters: Player[];
|
|
42
48
|
readonly offensivePreferences: OffensivePreference[];
|
|
49
|
+
readonly systemSets?: SystemSet[];
|
|
50
|
+
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
43
51
|
static create(input: unknown): Tactics;
|
|
44
52
|
private constructor();
|
|
45
53
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -15,7 +15,7 @@ export class Tactics {
|
|
|
15
15
|
}
|
|
16
16
|
return new Tactics(result.data);
|
|
17
17
|
}
|
|
18
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [] }) {
|
|
18
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets }) {
|
|
19
19
|
this.lineup = lineup;
|
|
20
20
|
this.liberoReplacements = liberoReplacements;
|
|
21
21
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -27,6 +27,8 @@ export class Tactics {
|
|
|
27
27
|
this.rotationSystem = rotationSystem;
|
|
28
28
|
this.designatedSetters = designatedSetters;
|
|
29
29
|
this.offensivePreferences = offensivePreferences;
|
|
30
|
+
this.systemSets = systemSets;
|
|
31
|
+
this.offensivePreferenceSets = offensivePreferenceSets;
|
|
30
32
|
}
|
|
31
33
|
findPlayerInLineup(id) {
|
|
32
34
|
if (this.lineup.bench.some(p => p.id === id))
|