volleyballsimtypes 0.0.457 → 0.0.462

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cjs/src/api/index.d.ts +6 -0
  2. package/dist/cjs/src/data/init-models.js +4 -0
  3. package/dist/cjs/src/data/models/index.d.ts +1 -0
  4. package/dist/cjs/src/data/models/index.js +1 -0
  5. package/dist/cjs/src/data/models/tactics.d.ts +9 -1
  6. package/dist/cjs/src/data/models/tactics.js +8 -0
  7. package/dist/cjs/src/data/models/user-device-token.d.ts +23 -0
  8. package/dist/cjs/src/data/models/user-device-token.js +65 -0
  9. package/dist/cjs/src/data/transformers/tactics.js +21 -3
  10. package/dist/cjs/src/data/transformers/tactics.test.js +61 -0
  11. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +8 -0
  12. package/dist/cjs/src/service/team/schemas/tactics.z.js +53 -31
  13. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +115 -0
  14. package/dist/cjs/src/service/team/schemas/team.z.d.ts +8 -0
  15. package/dist/cjs/src/service/team/tactics.d.ts +8 -0
  16. package/dist/cjs/src/service/team/tactics.js +3 -1
  17. package/dist/esm/src/api/index.d.ts +6 -0
  18. package/dist/esm/src/data/init-models.js +5 -1
  19. package/dist/esm/src/data/models/index.d.ts +1 -0
  20. package/dist/esm/src/data/models/index.js +1 -0
  21. package/dist/esm/src/data/models/tactics.d.ts +9 -1
  22. package/dist/esm/src/data/models/tactics.js +8 -0
  23. package/dist/esm/src/data/models/user-device-token.d.ts +23 -0
  24. package/dist/esm/src/data/models/user-device-token.js +61 -0
  25. package/dist/esm/src/data/transformers/tactics.js +21 -3
  26. package/dist/esm/src/data/transformers/tactics.test.js +63 -2
  27. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +8 -0
  28. package/dist/esm/src/service/team/schemas/tactics.z.js +53 -31
  29. package/dist/esm/src/service/team/schemas/tactics.z.test.js +115 -0
  30. package/dist/esm/src/service/team/schemas/team.z.d.ts +8 -0
  31. package/dist/esm/src/service/team/tactics.d.ts +8 -0
  32. package/dist/esm/src/service/team/tactics.js +3 -1
  33. 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;
@@ -16,6 +16,7 @@ function initModels(sequelize) {
16
16
  const UserPullGrant = models_1.UserPullGrantModel.initModel(sequelize);
17
17
  const Wishlist = models_1.WishlistModel.initModel(sequelize);
18
18
  const UserSettings = models_1.UserSettingsModel.initModel(sequelize);
19
+ const UserDeviceToken = models_1.UserDeviceTokenModel.initModel(sequelize);
19
20
  const Notification = models_1.NotificationModel.initModel(sequelize);
20
21
  const AuthIdentity = models_1.AuthIdentityModel.initModel(sequelize);
21
22
  const AuthSession = models_1.AuthSessionModel.initModel(sequelize);
@@ -63,7 +64,9 @@ function initModels(sequelize) {
63
64
  AuthUser.hasOne(GachaPity, { as: 'GachaPity', foreignKey: 'user_id' });
64
65
  AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
65
66
  AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
67
+ AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
66
68
  AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
69
+ UserDeviceToken.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
67
70
  Notification.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
68
71
  AuthUser.hasMany(GachaPullHistory, { as: 'GachaPullHistory', foreignKey: 'user_id' });
69
72
  AuthUser.hasMany(UserPullGrant, { as: 'UserPullGrants', foreignKey: 'user_id' });
@@ -294,6 +297,7 @@ function initModels(sequelize) {
294
297
  UserPullGrant,
295
298
  Wishlist,
296
299
  UserSettings,
300
+ UserDeviceToken,
297
301
  Notification,
298
302
  Coach,
299
303
  Scout,
@@ -44,6 +44,7 @@ export * from './wishlist';
44
44
  export * from './currency-transaction';
45
45
  export * from './user-pull-grant';
46
46
  export * from './user-settings';
47
+ export * from './user-device-token';
47
48
  export * from './notification';
48
49
  export * from './vper';
49
50
  export * from './views';
@@ -60,6 +60,7 @@ __exportStar(require("./wishlist"), exports);
60
60
  __exportStar(require("./currency-transaction"), exports);
61
61
  __exportStar(require("./user-pull-grant"), exports);
62
62
  __exportStar(require("./user-settings"), exports);
63
+ __exportStar(require("./user-device-token"), exports);
63
64
  __exportStar(require("./notification"), exports);
64
65
  __exportStar(require("./vper"), exports);
65
66
  __exportStar(require("./views"), exports);
@@ -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,
@@ -0,0 +1,23 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ export interface UserDeviceTokenAttributes {
4
+ user_device_token_id: string;
5
+ user_id: string;
6
+ device_token: string;
7
+ platform: string;
8
+ created_at?: Date;
9
+ updated_at?: Date;
10
+ }
11
+ export type UserDeviceTokenPk = 'user_device_token_id';
12
+ export type UserDeviceTokenId = UserDeviceTokenModel[UserDeviceTokenPk];
13
+ export type UserDeviceTokenOptionalAttributes = 'user_device_token_id' | 'platform' | 'created_at' | 'updated_at';
14
+ export type UserDeviceTokenCreationAttributes = Optional<UserDeviceTokenAttributes, UserDeviceTokenOptionalAttributes>;
15
+ export declare class UserDeviceTokenModel extends Model<UserDeviceTokenAttributes, UserDeviceTokenCreationAttributes> implements UserDeviceTokenAttributes {
16
+ user_device_token_id: string;
17
+ user_id: string;
18
+ device_token: string;
19
+ platform: string;
20
+ created_at?: Date;
21
+ updated_at?: Date;
22
+ static initModel(sequelize: Sequelize.Sequelize): typeof UserDeviceTokenModel;
23
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UserDeviceTokenModel = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ class UserDeviceTokenModel extends sequelize_1.Model {
6
+ static initModel(sequelize) {
7
+ return UserDeviceTokenModel.init({
8
+ user_device_token_id: {
9
+ type: sequelize_1.DataTypes.UUID,
10
+ allowNull: false,
11
+ primaryKey: true,
12
+ defaultValue: sequelize_1.DataTypes.UUIDV4
13
+ },
14
+ user_id: {
15
+ type: sequelize_1.DataTypes.UUID,
16
+ allowNull: false,
17
+ references: {
18
+ model: 'AuthUser',
19
+ key: 'user_id'
20
+ }
21
+ },
22
+ device_token: {
23
+ type: sequelize_1.DataTypes.TEXT,
24
+ allowNull: false
25
+ },
26
+ platform: {
27
+ type: sequelize_1.DataTypes.TEXT,
28
+ allowNull: false,
29
+ defaultValue: 'android'
30
+ },
31
+ created_at: {
32
+ type: sequelize_1.DataTypes.DATE,
33
+ allowNull: false,
34
+ defaultValue: sequelize_1.DataTypes.NOW
35
+ },
36
+ updated_at: {
37
+ type: sequelize_1.DataTypes.DATE,
38
+ allowNull: false,
39
+ defaultValue: sequelize_1.DataTypes.NOW
40
+ }
41
+ }, {
42
+ sequelize,
43
+ tableName: 'UserDeviceToken',
44
+ schema: 'public',
45
+ timestamps: false,
46
+ indexes: [
47
+ {
48
+ name: 'UserDeviceToken_pk',
49
+ unique: true,
50
+ fields: [{ name: 'user_device_token_id' }]
51
+ },
52
+ {
53
+ name: 'UserDeviceToken_user_id_device_token_uq',
54
+ unique: true,
55
+ fields: [{ name: 'user_id' }, { name: 'device_token' }]
56
+ },
57
+ {
58
+ name: 'UserDeviceToken_user_id_idx',
59
+ fields: [{ name: 'user_id' }]
60
+ }
61
+ ]
62
+ });
63
+ }
64
+ }
65
+ exports.UserDeviceTokenModel = UserDeviceTokenModel;
@@ -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
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const globals_1 = require("@jest/globals");
4
4
  const tactics_1 = require("./tactics");
5
5
  const service_1 = require("../../service");
6
+ const test_helpers_1 = require("../../service/test-helpers");
6
7
  // Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
7
8
  function makeTactics(designatedSubs) {
8
9
  return {
@@ -77,4 +78,64 @@ function makeTactics(designatedSubs) {
77
78
  (0, globals_1.expect)(dto.secondLibero).toBe('reserve1');
78
79
  (0, globals_1.expect)(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
79
80
  });
81
+ (0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
82
+ const model = makeTactics([]);
83
+ model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
84
+ model.offensive_preference_sets = [[{ rotation: 1, order: ['rf', 'mf'] }]];
85
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
86
+ (0, globals_1.expect)(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
87
+ (0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, order: ['rf', 'mf'] }]]);
88
+ });
89
+ (0, globals_1.it)('leaves the per-set DTO fields undefined when the columns are absent', () => {
90
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
91
+ (0, globals_1.expect)(dto.systemSets).toBeUndefined();
92
+ (0, globals_1.expect)(dto.offensivePreferenceSets).toBeUndefined();
93
+ });
94
+ });
95
+ (0, globals_1.describe)('per-set tactics — service <-> row round-trip', () => {
96
+ const country = (0, test_helpers_1.makeCountry)();
97
+ const p1 = (0, test_helpers_1.makePlayer)(country);
98
+ const p2 = (0, test_helpers_1.makePlayer)(country);
99
+ const p3 = (0, test_helpers_1.makePlayer)(country);
100
+ const p4 = (0, test_helpers_1.makePlayer)(country);
101
+ const p5 = (0, test_helpers_1.makePlayer)(country);
102
+ const p6 = (0, test_helpers_1.makePlayer)(country);
103
+ const roster = [p1, p2, p3, p4, p5, p6];
104
+ const tactics = service_1.Tactics.create({
105
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
106
+ liberoReplacements: [],
107
+ systemSets: [
108
+ { rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
109
+ { rotationSystem: service_1.RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
110
+ ],
111
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2, p3] }]]
112
+ });
113
+ (0, globals_1.it)('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
114
+ const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
115
+ (0, globals_1.expect)(attrs.system_sets).toEqual([
116
+ { rotationSystem: '5-1', designatedSetters: [p1.id] },
117
+ { rotationSystem: '6-0', designatedSetters: [] }
118
+ ]);
119
+ (0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, order: [p2.id, p3.id] }]]);
120
+ });
121
+ (0, globals_1.it)('transformToTactics resolves the stored ids back to roster players', () => {
122
+ const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
123
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
124
+ (0, globals_1.expect)(back.systemSets?.[0].rotationSystem).toBe(service_1.RotationSystemEnum.FIVE_ONE);
125
+ (0, globals_1.expect)(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
126
+ (0, globals_1.expect)(back.systemSets?.[1].designatedSetters).toEqual([]);
127
+ (0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].order.map(p => p.id)).toEqual([p2.id, p3.id]);
128
+ });
129
+ (0, globals_1.it)('omits the per-set columns entirely for an all-sets config', () => {
130
+ const flat = service_1.Tactics.create({
131
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
132
+ liberoReplacements: []
133
+ });
134
+ const attrs = (0, tactics_1.transformFromTactics)(flat, 'team-1');
135
+ (0, globals_1.expect)(attrs.system_sets).toBeUndefined();
136
+ (0, globals_1.expect)(attrs.offensive_preference_sets).toBeUndefined();
137
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
138
+ (0, globals_1.expect)(back.systemSets).toBeUndefined();
139
+ (0, globals_1.expect)(back.offensivePreferenceSets).toBeUndefined();
140
+ });
80
141
  });
@@ -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, and each must be an on-court starter.
94
- const expectedSetters = (0, rotation_system_1.setterCountFor)(data.rotationSystem);
95
- if (data.designatedSetters.length !== expectedSetters) {
96
- ctx.addIssue({ code: 'custom', message: `DESIGNATED_SETTERS_COUNT_${data.rotationSystem}_EXPECTED_${expectedSetters}`, path: ['designatedSetters'] });
97
- }
98
- for (const setter of data.designatedSetters) {
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
- // Offensive preferences: unique rotations, and each order lists only on-court starters with no duplicates.
113
- const seenRotations = new Set();
114
- for (const pref of data.offensivePreferences) {
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
- seenRotations.add(pref.rotation);
119
- const ids = pref.order.map(p => p.id);
120
- if (new Set(ids).size !== ids.length) {
121
- ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_DUPLICATE_ATTACKER', path: ['offensivePreferences'] });
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
- for (const id of ids) {
124
- if (!starterIds.has(id)) {
125
- ctx.addIssue({ code: 'custom', message: 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER', path: ['offensivePreferences'] });
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
@@ -69,3 +69,118 @@ const test_helpers_1 = require("../../test-helpers");
69
69
  }
70
70
  });
71
71
  });
72
+ (0, globals_1.describe)('TacticsInputSchema — per-set tactics (systemSets / offensivePreferenceSets)', () => {
73
+ const country = (0, test_helpers_1.makeCountry)();
74
+ const p1 = (0, test_helpers_1.makePlayer)(country);
75
+ const p2 = (0, test_helpers_1.makePlayer)(country);
76
+ const p3 = (0, test_helpers_1.makePlayer)(country);
77
+ const p4 = (0, test_helpers_1.makePlayer)(country);
78
+ const p5 = (0, test_helpers_1.makePlayer)(country);
79
+ const p6 = (0, test_helpers_1.makePlayer)(country);
80
+ const reserve = (0, test_helpers_1.makePlayer)(country);
81
+ // Minimal valid flat input (6-0 default: no setters, no preferences). Zone layout: p1@1, p2@2, p3@3,
82
+ // p4@4, p5@5, p6@6 — opposite slot pairs (3 apart) are (1,4), (2,5), (3,6).
83
+ function baseInput(overrides = {}) {
84
+ return {
85
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
86
+ liberoReplacements: [],
87
+ ...overrides
88
+ };
89
+ }
90
+ (0, globals_1.it)('accepts valid per-set overrides for both scopes', () => {
91
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
92
+ systemSets: [
93
+ { rotationSystem: '5-1', designatedSetters: [p1] },
94
+ { rotationSystem: '6-0', designatedSetters: [] },
95
+ { rotationSystem: '4-2', designatedSetters: [p1, p4] }
96
+ ],
97
+ offensivePreferenceSets: [
98
+ [{ rotation: 1, order: [p2, p3] }],
99
+ []
100
+ ]
101
+ }));
102
+ (0, globals_1.expect)(res.success).toBe(true);
103
+ });
104
+ (0, globals_1.it)('carries the per-set fields through Tactics.create parsing', () => {
105
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
106
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [p1] }],
107
+ offensivePreferenceSets: [[{ rotation: 2, order: [p3] }]]
108
+ }));
109
+ (0, globals_1.expect)(res.success).toBe(true);
110
+ if (res.success) {
111
+ (0, globals_1.expect)(res.data.systemSets?.[0].rotationSystem).toBe('5-1');
112
+ (0, globals_1.expect)(res.data.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
113
+ (0, globals_1.expect)(res.data.offensivePreferenceSets?.[0][0]).toMatchObject({ rotation: 2 });
114
+ }
115
+ });
116
+ (0, globals_1.it)('rejects more than 5 per-set entries', () => {
117
+ const entry = { rotationSystem: '6-0', designatedSetters: [] };
118
+ (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ systemSets: Array.from({ length: 6 }, () => entry) })).success).toBe(false);
119
+ (0, globals_1.expect)(tactics_z_1.TacticsInputSchema.safeParse(baseInput({ offensivePreferenceSets: Array.from({ length: 6 }, () => []) })).success).toBe(false);
120
+ });
121
+ (0, globals_1.it)('rejects a set entry whose setter count does not match its system (5-1 with none)', () => {
122
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
123
+ systemSets: [{ rotationSystem: '5-1', designatedSetters: [] }]
124
+ }));
125
+ (0, globals_1.expect)(res.success).toBe(false);
126
+ if (!res.success) {
127
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
128
+ (0, globals_1.expect)(issue?.path).toEqual(['systemSets', 0]);
129
+ }
130
+ });
131
+ (0, globals_1.it)('rejects a set entry whose setter is not a court starter', () => {
132
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
133
+ systemSets: [
134
+ { rotationSystem: '6-0', designatedSetters: [] },
135
+ { rotationSystem: '5-1', designatedSetters: [reserve] }
136
+ ]
137
+ }));
138
+ (0, globals_1.expect)(res.success).toBe(false);
139
+ if (!res.success) {
140
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTER_NOT_A_STARTER');
141
+ (0, globals_1.expect)(issue?.path).toEqual(['systemSets', 1]);
142
+ }
143
+ });
144
+ (0, globals_1.it)('rejects a 4-2 set entry whose setters are not in opposite slots', () => {
145
+ // p1@1 and p2@2 are adjacent (not 3 apart), so the 4-2 pair rule fails for that set entry.
146
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
147
+ systemSets: [{ rotationSystem: '4-2', designatedSetters: [p1, p2] }]
148
+ }));
149
+ (0, globals_1.expect)(res.success).toBe(false);
150
+ if (!res.success) {
151
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'DESIGNATED_SETTERS_NOT_IN_OPPOSITE_SLOTS')).toBe(true);
152
+ }
153
+ });
154
+ (0, globals_1.it)('rejects a per-set preferences list with a duplicate rotation', () => {
155
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
156
+ offensivePreferenceSets: [[{ rotation: 1, order: [p2] }, { rotation: 1, order: [p3] }]]
157
+ }));
158
+ (0, globals_1.expect)(res.success).toBe(false);
159
+ if (!res.success) {
160
+ const issue = res.error.issues.find(i => i.message === 'OFFENSIVE_PREFERENCE_DUPLICATE_ROTATION');
161
+ (0, globals_1.expect)(issue?.path).toEqual(['offensivePreferenceSets', 0]);
162
+ }
163
+ });
164
+ (0, globals_1.it)('rejects a per-set preferences order that lists a non-starter', () => {
165
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
166
+ offensivePreferenceSets: [[{ rotation: 1, order: [reserve] }]]
167
+ }));
168
+ (0, globals_1.expect)(res.success).toBe(false);
169
+ if (!res.success) {
170
+ (0, globals_1.expect)(res.error.issues.some(i => i.message === 'OFFENSIVE_PREFERENCE_ATTACKER_NOT_A_STARTER')).toBe(true);
171
+ }
172
+ });
173
+ (0, globals_1.it)('still validates the flat fields when per-set overrides are present', () => {
174
+ // Flat 5-1 with no setter is invalid even though every set entry is valid.
175
+ const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
176
+ rotationSystem: '5-1',
177
+ designatedSetters: [],
178
+ systemSets: [{ rotationSystem: '6-0', designatedSetters: [] }]
179
+ }));
180
+ (0, globals_1.expect)(res.success).toBe(false);
181
+ if (!res.success) {
182
+ const issue = res.error.issues.find(i => i.message === 'DESIGNATED_SETTERS_COUNT_5-1_EXPECTED_1');
183
+ (0, globals_1.expect)(issue?.path).toEqual(['designatedSetters']);
184
+ }
185
+ });
186
+ });
@@ -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;