volleyballsimtypes 0.0.426 → 0.0.428

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 (47) hide show
  1. package/dist/cjs/src/api/index.d.ts +9 -0
  2. package/dist/cjs/src/data/init-models.js +6 -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/lineup-preset.d.ts +29 -0
  6. package/dist/cjs/src/data/models/lineup-preset.js +71 -0
  7. package/dist/cjs/src/data/models/tactics.d.ts +2 -0
  8. package/dist/cjs/src/data/transformers/index.d.ts +1 -0
  9. package/dist/cjs/src/data/transformers/index.js +1 -0
  10. package/dist/cjs/src/data/transformers/lineup-preset.d.ts +4 -0
  11. package/dist/cjs/src/data/transformers/lineup-preset.js +13 -0
  12. package/dist/cjs/src/data/transformers/lineup-preset.test.d.ts +1 -0
  13. package/dist/cjs/src/data/transformers/lineup-preset.test.js +25 -0
  14. package/dist/cjs/src/data/transformers/tactics.d.ts +3 -1
  15. package/dist/cjs/src/data/transformers/tactics.js +42 -0
  16. package/dist/cjs/src/data/transformers/tactics.test.d.ts +1 -0
  17. package/dist/cjs/src/data/transformers/tactics.test.js +66 -0
  18. package/dist/cjs/src/service/team/designated-sub.d.ts +1 -0
  19. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +3 -0
  20. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +1 -0
  21. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +19 -0
  22. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +2 -0
  23. package/dist/cjs/src/service/team/schemas/team.z.d.ts +2 -0
  24. package/dist/esm/src/api/index.d.ts +9 -0
  25. package/dist/esm/src/data/init-models.js +7 -1
  26. package/dist/esm/src/data/models/index.d.ts +1 -0
  27. package/dist/esm/src/data/models/index.js +1 -0
  28. package/dist/esm/src/data/models/lineup-preset.d.ts +29 -0
  29. package/dist/esm/src/data/models/lineup-preset.js +67 -0
  30. package/dist/esm/src/data/models/tactics.d.ts +2 -0
  31. package/dist/esm/src/data/transformers/index.d.ts +1 -0
  32. package/dist/esm/src/data/transformers/index.js +1 -0
  33. package/dist/esm/src/data/transformers/lineup-preset.d.ts +4 -0
  34. package/dist/esm/src/data/transformers/lineup-preset.js +11 -0
  35. package/dist/esm/src/data/transformers/lineup-preset.test.d.ts +1 -0
  36. package/dist/esm/src/data/transformers/lineup-preset.test.js +23 -0
  37. package/dist/esm/src/data/transformers/tactics.d.ts +3 -1
  38. package/dist/esm/src/data/transformers/tactics.js +42 -1
  39. package/dist/esm/src/data/transformers/tactics.test.d.ts +1 -0
  40. package/dist/esm/src/data/transformers/tactics.test.js +64 -0
  41. package/dist/esm/src/service/team/designated-sub.d.ts +1 -0
  42. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +3 -0
  43. package/dist/esm/src/service/team/schemas/designated-sub.z.js +1 -0
  44. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +19 -0
  45. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +2 -0
  46. package/dist/esm/src/service/team/schemas/team.z.d.ts +2 -0
  47. package/package.json +1 -1
@@ -74,6 +74,7 @@ export interface ApiSetSubConfig {
74
74
  mode: SubMode;
75
75
  benchId?: string;
76
76
  fatigueBand?: EnergyBand;
77
+ benchFatigueBand?: EnergyBand;
77
78
  conditions?: PinchCondition[];
78
79
  conditionLogic?: ConditionLogic;
79
80
  }
@@ -82,6 +83,7 @@ export interface ApiDesignatedSub {
82
83
  benchId?: string;
83
84
  mode: SubMode;
84
85
  fatigueBand?: EnergyBand;
86
+ benchFatigueBand?: EnergyBand;
85
87
  conditions?: PinchCondition[];
86
88
  conditionLogic?: ConditionLogic;
87
89
  sets?: ApiSetSubConfig[];
@@ -112,6 +114,13 @@ export interface Tactics {
112
114
  designatedSetters: string[];
113
115
  offensivePreferences: ApiOffensivePreference[];
114
116
  }
117
+ export interface ApiLineupPreset {
118
+ presetId: string;
119
+ name: string;
120
+ isActive: boolean;
121
+ orderIndex: number;
122
+ config: Tactics;
123
+ }
115
124
  export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
116
125
  roster: Player[];
117
126
  rating: {
@@ -7,6 +7,7 @@ function initModels(sequelize) {
7
7
  const Scout = models_1.ScoutModel.initModel(sequelize);
8
8
  const CurrencyTransaction = models_1.CurrencyTransactionModel.initModel(sequelize);
9
9
  const Tactics = models_1.TacticsModel.initModel(sequelize);
10
+ const LineupPreset = models_1.LineupPresetModel.initModel(sequelize);
10
11
  const AuthUser = models_1.AuthUserModel.initModel(sequelize);
11
12
  const UserCurrency = models_1.UserCurrencyModel.initModel(sequelize);
12
13
  const UserPlayerProgress = models_1.UserPlayerProgressModel.initModel(sequelize);
@@ -199,6 +200,10 @@ function initModels(sequelize) {
199
200
  Team.hasOne(Scout, { as: 'Scout', foreignKey: 'team_id' });
200
201
  Scout.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
201
202
  Team.hasOne(Tactics, { as: 'tactics', foreignKey: 'team_id' });
203
+ // Lineup presets are loaded only by the management API; deliberately NOT in Team.defaultScope so the
204
+ // simulator's team loads are unchanged (it reads the active config from the mirrored Tactics row).
205
+ Team.hasMany(LineupPreset, { as: 'lineupPresets', foreignKey: 'team_id' });
206
+ LineupPreset.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
202
207
  Team.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
203
208
  Team.belongsToMany(Competition, {
204
209
  as: 'Competitions',
@@ -270,6 +275,7 @@ function initModels(sequelize) {
270
275
  Team,
271
276
  TeamReplacement,
272
277
  Tactics,
278
+ LineupPreset,
273
279
  UserTeam,
274
280
  UserCurrency,
275
281
  UserPlayerProgress,
@@ -33,6 +33,7 @@ export * from './scout';
33
33
  export * from './team';
34
34
  export * from './team-replacement';
35
35
  export * from './tactics';
36
+ export * from './lineup-preset';
36
37
  export * from './user-currency';
37
38
  export * from './user-player-progress';
38
39
  export * from './gacha-pity';
@@ -49,6 +49,7 @@ __exportStar(require("./scout"), exports);
49
49
  __exportStar(require("./team"), exports);
50
50
  __exportStar(require("./team-replacement"), exports);
51
51
  __exportStar(require("./tactics"), exports);
52
+ __exportStar(require("./lineup-preset"), exports);
52
53
  __exportStar(require("./user-currency"), exports);
53
54
  __exportStar(require("./user-player-progress"), exports);
54
55
  __exportStar(require("./gacha-pity"), exports);
@@ -0,0 +1,29 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ import { TeamId, TeamModel } from '.';
4
+ import type { Tactics as ApiTactics } from '../../api';
5
+ export interface LineupPresetAttributes {
6
+ preset_id: string;
7
+ team_id: string;
8
+ name: string;
9
+ is_active: boolean;
10
+ order_index: number;
11
+ config: ApiTactics;
12
+ }
13
+ export type LineupPresetPk = 'preset_id';
14
+ export type LineupPresetId = LineupPresetModel[LineupPresetPk];
15
+ export type LineupPresetOptionalAttributes = 'preset_id' | 'is_active';
16
+ export type LineupPresetCreationAttributes = Optional<LineupPresetAttributes, LineupPresetOptionalAttributes>;
17
+ export declare class LineupPresetModel extends Model<LineupPresetAttributes, LineupPresetCreationAttributes> implements LineupPresetAttributes {
18
+ preset_id: string;
19
+ team_id: string;
20
+ name: string;
21
+ is_active: boolean;
22
+ order_index: number;
23
+ config: ApiTactics;
24
+ team: TeamModel;
25
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
26
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
27
+ createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
28
+ static initModel(sequelize: Sequelize.Sequelize): typeof LineupPresetModel;
29
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LineupPresetModel = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ class LineupPresetModel extends sequelize_1.Model {
6
+ static initModel(sequelize) {
7
+ return LineupPresetModel.init({
8
+ preset_id: {
9
+ type: sequelize_1.DataTypes.UUID,
10
+ allowNull: false,
11
+ primaryKey: true,
12
+ defaultValue: sequelize_1.DataTypes.UUIDV4
13
+ },
14
+ team_id: {
15
+ type: sequelize_1.DataTypes.UUID,
16
+ allowNull: false,
17
+ references: {
18
+ model: 'Team',
19
+ key: 'team_id'
20
+ }
21
+ },
22
+ name: {
23
+ type: sequelize_1.DataTypes.STRING,
24
+ allowNull: false
25
+ },
26
+ is_active: {
27
+ type: sequelize_1.DataTypes.BOOLEAN,
28
+ allowNull: false,
29
+ defaultValue: false
30
+ },
31
+ order_index: {
32
+ type: sequelize_1.DataTypes.INTEGER,
33
+ allowNull: false
34
+ },
35
+ config: {
36
+ type: sequelize_1.DataTypes.JSONB,
37
+ allowNull: false
38
+ }
39
+ }, {
40
+ sequelize,
41
+ tableName: 'LineupPreset',
42
+ schema: 'public',
43
+ timestamps: false,
44
+ indexes: [
45
+ {
46
+ name: 'LineupPreset_pk',
47
+ unique: true,
48
+ fields: [
49
+ { name: 'preset_id' }
50
+ ]
51
+ },
52
+ {
53
+ name: 'LineupPreset_team_id_idx',
54
+ fields: [
55
+ { name: 'team_id' }
56
+ ]
57
+ },
58
+ {
59
+ // At most one active preset per team.
60
+ name: 'LineupPreset_one_active_per_team',
61
+ unique: true,
62
+ fields: [
63
+ { name: 'team_id' }
64
+ ],
65
+ where: { is_active: true }
66
+ }
67
+ ]
68
+ });
69
+ }
70
+ }
71
+ exports.LineupPresetModel = LineupPresetModel;
@@ -17,6 +17,7 @@ export interface SetSubConfigAttributes {
17
17
  mode: SubMode;
18
18
  benchId?: PlayerId;
19
19
  fatigueBand?: EnergyBand;
20
+ benchFatigueBand?: EnergyBand;
20
21
  conditions?: PinchCondition[];
21
22
  conditionLogic?: ConditionLogic;
22
23
  }
@@ -26,6 +27,7 @@ export interface DesignatedSubAttributes {
26
27
  mode?: SubMode;
27
28
  isPinchServer?: boolean;
28
29
  fatigueBand?: SubBand;
30
+ benchFatigueBand?: EnergyBand;
29
31
  conditions?: PinchCondition[];
30
32
  conditionLogic?: ConditionLogic;
31
33
  sets?: SetSubConfigAttributes[];
@@ -26,4 +26,5 @@ export * from './national';
26
26
  export * from './national-match';
27
27
  export * from './team';
28
28
  export * from './tactics';
29
+ export * from './lineup-preset';
29
30
  export * from './vper';
@@ -42,4 +42,5 @@ __exportStar(require("./national"), exports);
42
42
  __exportStar(require("./national-match"), exports);
43
43
  __exportStar(require("./team"), exports);
44
44
  __exportStar(require("./tactics"), exports);
45
+ __exportStar(require("./lineup-preset"), exports);
45
46
  __exportStar(require("./vper"), exports);
@@ -0,0 +1,4 @@
1
+ import { LineupPresetModel } from '../models';
2
+ import type { ApiLineupPreset } from '../../api';
3
+ declare function transformToObject(model: LineupPresetModel): ApiLineupPreset;
4
+ export { transformToObject as transformToLineupPreset };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transformToLineupPreset = transformToObject;
4
+ // `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
5
+ function transformToObject(model) {
6
+ return {
7
+ presetId: model.preset_id,
8
+ name: model.name,
9
+ isActive: model.is_active,
10
+ orderIndex: model.order_index,
11
+ config: model.config
12
+ };
13
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const lineup_preset_1 = require("./lineup-preset");
5
+ (0, globals_1.describe)('transformToLineupPreset()', () => {
6
+ (0, globals_1.it)('maps preset columns to the API shape and passes config through by reference', () => {
7
+ const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
8
+ const preset = (0, lineup_preset_1.transformToLineupPreset)({
9
+ preset_id: 'p1',
10
+ team_id: 't1',
11
+ name: 'Starters',
12
+ is_active: true,
13
+ order_index: 2,
14
+ config
15
+ });
16
+ (0, globals_1.expect)(preset).toEqual({
17
+ presetId: 'p1',
18
+ name: 'Starters',
19
+ isActive: true,
20
+ orderIndex: 2,
21
+ config
22
+ });
23
+ (0, globals_1.expect)(preset.config).toBe(config);
24
+ });
25
+ });
@@ -1,5 +1,7 @@
1
1
  import { TacticsAttributes, TacticsModel } from '../models';
2
2
  import { Player, Tactics } from '../../service';
3
+ import type { Tactics as ApiTactics } from '../../api';
3
4
  declare function transformToAttributes(tactics: Tactics, teamId: string): TacticsAttributes;
4
5
  declare function transformToObject(model: TacticsModel, roster: Player[]): Tactics;
5
- export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics };
6
+ declare function tacticsColumnsToApiDto(model: TacticsModel): ApiTactics;
7
+ export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto };
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformToTactics = transformToObject;
4
4
  exports.transformFromTactics = transformToAttributes;
5
+ exports.tacticsColumnsToApiDto = tacticsColumnsToApiDto;
5
6
  const service_1 = require("../../service");
6
7
  // Back-compat: new rows persist `mode`; rows written before the mode change persist `isPinchServer` (+ a
7
8
  // 'NEVER' fatigueBand for the old "never fatigue-sub" state). Derive the mode so existing data keeps working
@@ -62,12 +63,14 @@ function transformToAttributes(tactics, teamId) {
62
63
  benchId: ds.bench?.id,
63
64
  mode: ds.mode,
64
65
  fatigueBand: ds.fatigueBand,
66
+ benchFatigueBand: ds.benchFatigueBand,
65
67
  conditions: ds.conditions,
66
68
  conditionLogic: ds.conditionLogic,
67
69
  sets: ds.sets?.map(s => ({
68
70
  mode: s.mode,
69
71
  benchId: s.bench?.id,
70
72
  fatigueBand: s.fatigueBand,
73
+ benchFatigueBand: s.benchFatigueBand,
71
74
  conditions: s.conditions,
72
75
  conditionLogic: s.conditionLogic
73
76
  }))
@@ -115,12 +118,14 @@ function transformToObject(model, roster) {
115
118
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
116
119
  mode: subModeFromAttributes(d),
117
120
  fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
121
+ benchFatigueBand: d.benchFatigueBand,
118
122
  conditions: d.conditions,
119
123
  conditionLogic: d.conditionLogic,
120
124
  sets: d.sets?.map(s => ({
121
125
  mode: s.mode,
122
126
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
123
127
  fatigueBand: s.fatigueBand,
128
+ benchFatigueBand: s.benchFatigueBand,
124
129
  conditions: s.conditions,
125
130
  conditionLogic: s.conditionLogic
126
131
  }))
@@ -133,3 +138,40 @@ function transformToObject(model, roster) {
133
138
  }))
134
139
  });
135
140
  }
141
+ // Serialize a Tactics ROW (db columns) straight to the API Tactics DTO (player references stay as ids; no
142
+ // roster needed). Used to seed a lineup preset's `config` from the existing single tactics row (backfill) and
143
+ // to keep the active preset and the mirrored Tactics row in the same shape. Applies the same legacy fixups as
144
+ // the service transformer: resolve the designated-sub `mode` and drop a legacy 'NEVER' fatigueBand.
145
+ function tacticsColumnsToApiDto(model) {
146
+ return {
147
+ lineup: model.lineup,
148
+ substitutionTolerance: model.substitution_tolerance,
149
+ liberoReplacements: model.libero_replacements,
150
+ pinchServerSubs: model.pinch_server_subs,
151
+ receiveRotationOffset: model.receive_rotation_offset,
152
+ substitutionBand: model.substitution_band,
153
+ secondLibero: model.second_libero,
154
+ liberoSub: model.libero_sub,
155
+ liberoReplacementSets: model.libero_replacement_sets,
156
+ designatedSubs: (model.designated_subs ?? []).map(d => ({
157
+ starterId: d.starterId,
158
+ benchId: d.benchId,
159
+ mode: subModeFromAttributes(d),
160
+ fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
161
+ benchFatigueBand: d.benchFatigueBand,
162
+ conditions: d.conditions,
163
+ conditionLogic: d.conditionLogic,
164
+ sets: d.sets?.map(s => ({
165
+ mode: s.mode,
166
+ benchId: s.benchId,
167
+ fatigueBand: s.fatigueBand,
168
+ benchFatigueBand: s.benchFatigueBand,
169
+ conditions: s.conditions,
170
+ conditionLogic: s.conditionLogic
171
+ }))
172
+ })),
173
+ rotationSystem: model.rotation_system,
174
+ designatedSetters: model.designated_setters,
175
+ offensivePreferences: model.offensive_preferences
176
+ };
177
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const tactics_1 = require("./tactics");
5
+ const service_1 = require("../../service");
6
+ // Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
7
+ function makeTactics(designatedSubs) {
8
+ return {
9
+ team_id: 'team-1',
10
+ substitution_tolerance: 10,
11
+ lineup: {
12
+ [service_1.CourtPosition.LEFT_FRONT]: 'lf',
13
+ [service_1.CourtPosition.MIDDLE_FRONT]: 'mf',
14
+ [service_1.CourtPosition.RIGHT_FRONT]: 'rf',
15
+ [service_1.CourtPosition.LEFT_BACK]: 'lb',
16
+ [service_1.CourtPosition.MIDDLE_BACK]: 'mb',
17
+ [service_1.CourtPosition.RIGHT_BACK]: 'rb',
18
+ bench: ['b1', 'b2']
19
+ },
20
+ libero_replacements: ['lf'],
21
+ pinch_server_subs: { starter1: 'server1' },
22
+ receive_rotation_offset: true,
23
+ designated_subs: designatedSubs,
24
+ substitution_band: service_1.EnergyBand.TIRED,
25
+ second_libero: undefined,
26
+ libero_sub: undefined,
27
+ libero_replacement_sets: undefined,
28
+ rotation_system: '5-1',
29
+ designated_setters: ['lf'],
30
+ offensive_preferences: [{ rotation: 1, order: ['lf', 'rf'] }]
31
+ };
32
+ }
33
+ (0, globals_1.describe)('tacticsColumnsToApiDto()', () => {
34
+ (0, globals_1.it)('passes scalar/array columns through to the API DTO shape', () => {
35
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
36
+ (0, globals_1.expect)(dto.substitutionTolerance).toBe(10);
37
+ (0, globals_1.expect)(dto.receiveRotationOffset).toBe(true);
38
+ (0, globals_1.expect)(dto.substitutionBand).toBe(service_1.EnergyBand.TIRED);
39
+ (0, globals_1.expect)(dto.rotationSystem).toBe('5-1');
40
+ (0, globals_1.expect)(dto.liberoReplacements).toEqual(['lf']);
41
+ (0, globals_1.expect)(dto.pinchServerSubs).toEqual({ starter1: 'server1' });
42
+ (0, globals_1.expect)(dto.lineup.bench).toEqual(['b1', 'b2']);
43
+ (0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, order: ['lf', 'rf'] }]);
44
+ });
45
+ (0, globals_1.it)("resolves a legacy isPinchServer entry to mode 'PINCH'", () => {
46
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
47
+ { starterId: 's1', benchId: 'b1', isPinchServer: true, conditions: [{ type: 'ASAP' }] }
48
+ ]));
49
+ (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('PINCH');
50
+ });
51
+ (0, globals_1.it)("resolves a legacy 'NEVER' fatigueBand to mode NEVER and drops the band", () => {
52
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
53
+ { starterId: 's1', fatigueBand: 'NEVER' }
54
+ ]));
55
+ (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('NEVER');
56
+ (0, globals_1.expect)(dto.designatedSubs[0].fatigueBand).toBeUndefined();
57
+ });
58
+ (0, globals_1.it)('passes a fatigue entry with its own bands through unchanged', () => {
59
+ const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
60
+ { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: service_1.EnergyBand.WORN, benchFatigueBand: service_1.EnergyBand.TIRED }
61
+ ]));
62
+ (0, globals_1.expect)(dto.designatedSubs[0].mode).toBe('FATIGUE');
63
+ (0, globals_1.expect)(dto.designatedSubs[0].fatigueBand).toBe(service_1.EnergyBand.WORN);
64
+ (0, globals_1.expect)(dto.designatedSubs[0].benchFatigueBand).toBe(service_1.EnergyBand.TIRED);
65
+ });
66
+ });
@@ -5,6 +5,7 @@ export interface SetSubConfig {
5
5
  readonly mode: SubMode;
6
6
  readonly bench?: Player;
7
7
  readonly fatigueBand?: EnergyBand;
8
+ readonly benchFatigueBand?: EnergyBand;
8
9
  readonly conditions?: PinchCondition[];
9
10
  readonly conditionLogic?: ConditionLogic;
10
11
  }
@@ -43,6 +43,7 @@ export declare const SetSubConfigSchema: z.ZodObject<{
43
43
  }>;
44
44
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
45
45
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
47
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
47
48
  type: z.ZodLiteral<PinchConditionType.ASAP>;
48
49
  }, z.core.$strip>, z.ZodObject<{
@@ -80,6 +81,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
80
81
  }>;
81
82
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
82
83
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
84
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
83
85
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
84
86
  type: z.ZodLiteral<PinchConditionType.ASAP>;
85
87
  }, z.core.$strip>, z.ZodObject<{
@@ -115,6 +117,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
115
117
  }>;
116
118
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
117
119
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
120
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
118
121
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
119
122
  type: z.ZodLiteral<PinchConditionType.ASAP>;
120
123
  }, z.core.$strip>, z.ZodObject<{
@@ -26,6 +26,7 @@ const setSubConfigShape = {
26
26
  mode: exports.SubModeSchema,
27
27
  bench: playerInstanceSchema.optional(),
28
28
  fatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
29
+ benchFatigueBand: zod_1.z.nativeEnum(energy_band_1.EnergyBand).optional(),
29
30
  conditions: zod_1.z.array(exports.PinchConditionSchema).optional(),
30
31
  conditionLogic: exports.ConditionLogicSchema.optional()
31
32
  };
@@ -73,6 +73,25 @@ const test_helpers_1 = require("../../test-helpers");
73
73
  });
74
74
  (0, globals_1.expect)(res.success).toBe(true);
75
75
  });
76
+ (0, globals_1.it)("accepts a fatigue-mode entry with the substitute's own benchFatigueBand", () => {
77
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
78
+ starter,
79
+ bench,
80
+ mode: 'FATIGUE',
81
+ fatigueBand: energy_band_1.EnergyBand.WORN,
82
+ benchFatigueBand: energy_band_1.EnergyBand.TIRED
83
+ });
84
+ (0, globals_1.expect)(res.success).toBe(true);
85
+ });
86
+ (0, globals_1.it)('rejects benchFatigueBand = NEVER (only the team default band may be NEVER)', () => {
87
+ const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
88
+ starter,
89
+ bench,
90
+ mode: 'FATIGUE',
91
+ benchFatigueBand: 'NEVER'
92
+ });
93
+ (0, globals_1.expect)(res.success).toBe(false);
94
+ });
76
95
  (0, globals_1.it)('accepts a fatigue-mode entry with no bench (falls back to closest sub)', () => {
77
96
  const res = designated_sub_z_1.DesignatedSubSchema.safeParse({
78
97
  starter,
@@ -26,6 +26,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
26
26
  }>;
27
27
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
28
28
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
30
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
30
31
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
31
32
  }, z.core.$strip>, z.ZodObject<{
@@ -61,6 +62,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
61
62
  }>;
62
63
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
63
64
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
65
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
64
66
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
65
67
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
66
68
  }, z.core.$strip>, z.ZodObject<{
@@ -34,6 +34,7 @@ export declare const TeamInputSchema: z.ZodObject<{
34
34
  }>;
35
35
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
36
36
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
38
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
38
39
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
39
40
  }, z.core.$strip>, z.ZodObject<{
@@ -69,6 +70,7 @@ export declare const TeamInputSchema: z.ZodObject<{
69
70
  }>;
70
71
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
71
72
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
73
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
72
74
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
73
75
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
74
76
  }, z.core.$strip>, z.ZodObject<{
@@ -74,6 +74,7 @@ export interface ApiSetSubConfig {
74
74
  mode: SubMode;
75
75
  benchId?: string;
76
76
  fatigueBand?: EnergyBand;
77
+ benchFatigueBand?: EnergyBand;
77
78
  conditions?: PinchCondition[];
78
79
  conditionLogic?: ConditionLogic;
79
80
  }
@@ -82,6 +83,7 @@ export interface ApiDesignatedSub {
82
83
  benchId?: string;
83
84
  mode: SubMode;
84
85
  fatigueBand?: EnergyBand;
86
+ benchFatigueBand?: EnergyBand;
85
87
  conditions?: PinchCondition[];
86
88
  conditionLogic?: ConditionLogic;
87
89
  sets?: ApiSetSubConfig[];
@@ -112,6 +114,13 @@ export interface Tactics {
112
114
  designatedSetters: string[];
113
115
  offensivePreferences: ApiOffensivePreference[];
114
116
  }
117
+ export interface ApiLineupPreset {
118
+ presetId: string;
119
+ name: string;
120
+ isActive: boolean;
121
+ orderIndex: number;
122
+ config: Tactics;
123
+ }
115
124
  export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tactics'> & {
116
125
  roster: Player[];
117
126
  rating: {
@@ -1,9 +1,10 @@
1
- import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
1
+ import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, ScoutModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, LineupPresetModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
2
2
  export function initModels(sequelize) {
3
3
  const Coach = CoachModel.initModel(sequelize);
4
4
  const Scout = ScoutModel.initModel(sequelize);
5
5
  const CurrencyTransaction = CurrencyTransactionModel.initModel(sequelize);
6
6
  const Tactics = TacticsModel.initModel(sequelize);
7
+ const LineupPreset = LineupPresetModel.initModel(sequelize);
7
8
  const AuthUser = AuthUserModel.initModel(sequelize);
8
9
  const UserCurrency = UserCurrencyModel.initModel(sequelize);
9
10
  const UserPlayerProgress = UserPlayerProgressModel.initModel(sequelize);
@@ -196,6 +197,10 @@ export function initModels(sequelize) {
196
197
  Team.hasOne(Scout, { as: 'Scout', foreignKey: 'team_id' });
197
198
  Scout.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
198
199
  Team.hasOne(Tactics, { as: 'tactics', foreignKey: 'team_id' });
200
+ // Lineup presets are loaded only by the management API; deliberately NOT in Team.defaultScope so the
201
+ // simulator's team loads are unchanged (it reads the active config from the mirrored Tactics row).
202
+ Team.hasMany(LineupPreset, { as: 'lineupPresets', foreignKey: 'team_id' });
203
+ LineupPreset.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
199
204
  Team.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
200
205
  Team.belongsToMany(Competition, {
201
206
  as: 'Competitions',
@@ -267,6 +272,7 @@ export function initModels(sequelize) {
267
272
  Team,
268
273
  TeamReplacement,
269
274
  Tactics,
275
+ LineupPreset,
270
276
  UserTeam,
271
277
  UserCurrency,
272
278
  UserPlayerProgress,
@@ -33,6 +33,7 @@ export * from './scout';
33
33
  export * from './team';
34
34
  export * from './team-replacement';
35
35
  export * from './tactics';
36
+ export * from './lineup-preset';
36
37
  export * from './user-currency';
37
38
  export * from './user-player-progress';
38
39
  export * from './gacha-pity';
@@ -33,6 +33,7 @@ export * from './scout';
33
33
  export * from './team';
34
34
  export * from './team-replacement';
35
35
  export * from './tactics';
36
+ export * from './lineup-preset';
36
37
  export * from './user-currency';
37
38
  export * from './user-player-progress';
38
39
  export * from './gacha-pity';
@@ -0,0 +1,29 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ import { TeamId, TeamModel } from '.';
4
+ import type { Tactics as ApiTactics } from '../../api';
5
+ export interface LineupPresetAttributes {
6
+ preset_id: string;
7
+ team_id: string;
8
+ name: string;
9
+ is_active: boolean;
10
+ order_index: number;
11
+ config: ApiTactics;
12
+ }
13
+ export type LineupPresetPk = 'preset_id';
14
+ export type LineupPresetId = LineupPresetModel[LineupPresetPk];
15
+ export type LineupPresetOptionalAttributes = 'preset_id' | 'is_active';
16
+ export type LineupPresetCreationAttributes = Optional<LineupPresetAttributes, LineupPresetOptionalAttributes>;
17
+ export declare class LineupPresetModel extends Model<LineupPresetAttributes, LineupPresetCreationAttributes> implements LineupPresetAttributes {
18
+ preset_id: string;
19
+ team_id: string;
20
+ name: string;
21
+ is_active: boolean;
22
+ order_index: number;
23
+ config: ApiTactics;
24
+ team: TeamModel;
25
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
26
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
27
+ createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
28
+ static initModel(sequelize: Sequelize.Sequelize): typeof LineupPresetModel;
29
+ }
@@ -0,0 +1,67 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class LineupPresetModel extends Model {
3
+ static initModel(sequelize) {
4
+ return LineupPresetModel.init({
5
+ preset_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ defaultValue: DataTypes.UUIDV4
10
+ },
11
+ team_id: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ references: {
15
+ model: 'Team',
16
+ key: 'team_id'
17
+ }
18
+ },
19
+ name: {
20
+ type: DataTypes.STRING,
21
+ allowNull: false
22
+ },
23
+ is_active: {
24
+ type: DataTypes.BOOLEAN,
25
+ allowNull: false,
26
+ defaultValue: false
27
+ },
28
+ order_index: {
29
+ type: DataTypes.INTEGER,
30
+ allowNull: false
31
+ },
32
+ config: {
33
+ type: DataTypes.JSONB,
34
+ allowNull: false
35
+ }
36
+ }, {
37
+ sequelize,
38
+ tableName: 'LineupPreset',
39
+ schema: 'public',
40
+ timestamps: false,
41
+ indexes: [
42
+ {
43
+ name: 'LineupPreset_pk',
44
+ unique: true,
45
+ fields: [
46
+ { name: 'preset_id' }
47
+ ]
48
+ },
49
+ {
50
+ name: 'LineupPreset_team_id_idx',
51
+ fields: [
52
+ { name: 'team_id' }
53
+ ]
54
+ },
55
+ {
56
+ // At most one active preset per team.
57
+ name: 'LineupPreset_one_active_per_team',
58
+ unique: true,
59
+ fields: [
60
+ { name: 'team_id' }
61
+ ],
62
+ where: { is_active: true }
63
+ }
64
+ ]
65
+ });
66
+ }
67
+ }
@@ -17,6 +17,7 @@ export interface SetSubConfigAttributes {
17
17
  mode: SubMode;
18
18
  benchId?: PlayerId;
19
19
  fatigueBand?: EnergyBand;
20
+ benchFatigueBand?: EnergyBand;
20
21
  conditions?: PinchCondition[];
21
22
  conditionLogic?: ConditionLogic;
22
23
  }
@@ -26,6 +27,7 @@ export interface DesignatedSubAttributes {
26
27
  mode?: SubMode;
27
28
  isPinchServer?: boolean;
28
29
  fatigueBand?: SubBand;
30
+ benchFatigueBand?: EnergyBand;
29
31
  conditions?: PinchCondition[];
30
32
  conditionLogic?: ConditionLogic;
31
33
  sets?: SetSubConfigAttributes[];
@@ -26,4 +26,5 @@ export * from './national';
26
26
  export * from './national-match';
27
27
  export * from './team';
28
28
  export * from './tactics';
29
+ export * from './lineup-preset';
29
30
  export * from './vper';
@@ -26,4 +26,5 @@ export * from './national';
26
26
  export * from './national-match';
27
27
  export * from './team';
28
28
  export * from './tactics';
29
+ export * from './lineup-preset';
29
30
  export * from './vper';
@@ -0,0 +1,4 @@
1
+ import { LineupPresetModel } from '../models';
2
+ import type { ApiLineupPreset } from '../../api';
3
+ declare function transformToObject(model: LineupPresetModel): ApiLineupPreset;
4
+ export { transformToObject as transformToLineupPreset };
@@ -0,0 +1,11 @@
1
+ // `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
2
+ function transformToObject(model) {
3
+ return {
4
+ presetId: model.preset_id,
5
+ name: model.name,
6
+ isActive: model.is_active,
7
+ orderIndex: model.order_index,
8
+ config: model.config
9
+ };
10
+ }
11
+ export { transformToObject as transformToLineupPreset };
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { transformToLineupPreset } from './lineup-preset';
3
+ describe('transformToLineupPreset()', () => {
4
+ it('maps preset columns to the API shape and passes config through by reference', () => {
5
+ const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
6
+ const preset = transformToLineupPreset({
7
+ preset_id: 'p1',
8
+ team_id: 't1',
9
+ name: 'Starters',
10
+ is_active: true,
11
+ order_index: 2,
12
+ config
13
+ });
14
+ expect(preset).toEqual({
15
+ presetId: 'p1',
16
+ name: 'Starters',
17
+ isActive: true,
18
+ orderIndex: 2,
19
+ config
20
+ });
21
+ expect(preset.config).toBe(config);
22
+ });
23
+ });
@@ -1,5 +1,7 @@
1
1
  import { TacticsAttributes, TacticsModel } from '../models';
2
2
  import { Player, Tactics } from '../../service';
3
+ import type { Tactics as ApiTactics } from '../../api';
3
4
  declare function transformToAttributes(tactics: Tactics, teamId: string): TacticsAttributes;
4
5
  declare function transformToObject(model: TacticsModel, roster: Player[]): Tactics;
5
- export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics };
6
+ declare function tacticsColumnsToApiDto(model: TacticsModel): ApiTactics;
7
+ export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto };
@@ -58,12 +58,14 @@ function transformToAttributes(tactics, teamId) {
58
58
  benchId: ds.bench?.id,
59
59
  mode: ds.mode,
60
60
  fatigueBand: ds.fatigueBand,
61
+ benchFatigueBand: ds.benchFatigueBand,
61
62
  conditions: ds.conditions,
62
63
  conditionLogic: ds.conditionLogic,
63
64
  sets: ds.sets?.map(s => ({
64
65
  mode: s.mode,
65
66
  benchId: s.bench?.id,
66
67
  fatigueBand: s.fatigueBand,
68
+ benchFatigueBand: s.benchFatigueBand,
67
69
  conditions: s.conditions,
68
70
  conditionLogic: s.conditionLogic
69
71
  }))
@@ -111,12 +113,14 @@ function transformToObject(model, roster) {
111
113
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
112
114
  mode: subModeFromAttributes(d),
113
115
  fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
116
+ benchFatigueBand: d.benchFatigueBand,
114
117
  conditions: d.conditions,
115
118
  conditionLogic: d.conditionLogic,
116
119
  sets: d.sets?.map(s => ({
117
120
  mode: s.mode,
118
121
  bench: s.benchId != null ? findPlayer(s.benchId, roster) : undefined,
119
122
  fatigueBand: s.fatigueBand,
123
+ benchFatigueBand: s.benchFatigueBand,
120
124
  conditions: s.conditions,
121
125
  conditionLogic: s.conditionLogic
122
126
  }))
@@ -129,4 +133,41 @@ function transformToObject(model, roster) {
129
133
  }))
130
134
  });
131
135
  }
132
- export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics };
136
+ // Serialize a Tactics ROW (db columns) straight to the API Tactics DTO (player references stay as ids; no
137
+ // roster needed). Used to seed a lineup preset's `config` from the existing single tactics row (backfill) and
138
+ // to keep the active preset and the mirrored Tactics row in the same shape. Applies the same legacy fixups as
139
+ // the service transformer: resolve the designated-sub `mode` and drop a legacy 'NEVER' fatigueBand.
140
+ function tacticsColumnsToApiDto(model) {
141
+ return {
142
+ lineup: model.lineup,
143
+ substitutionTolerance: model.substitution_tolerance,
144
+ liberoReplacements: model.libero_replacements,
145
+ pinchServerSubs: model.pinch_server_subs,
146
+ receiveRotationOffset: model.receive_rotation_offset,
147
+ substitutionBand: model.substitution_band,
148
+ secondLibero: model.second_libero,
149
+ liberoSub: model.libero_sub,
150
+ liberoReplacementSets: model.libero_replacement_sets,
151
+ designatedSubs: (model.designated_subs ?? []).map(d => ({
152
+ starterId: d.starterId,
153
+ benchId: d.benchId,
154
+ mode: subModeFromAttributes(d),
155
+ fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
156
+ benchFatigueBand: d.benchFatigueBand,
157
+ conditions: d.conditions,
158
+ conditionLogic: d.conditionLogic,
159
+ sets: d.sets?.map(s => ({
160
+ mode: s.mode,
161
+ benchId: s.benchId,
162
+ fatigueBand: s.fatigueBand,
163
+ benchFatigueBand: s.benchFatigueBand,
164
+ conditions: s.conditions,
165
+ conditionLogic: s.conditionLogic
166
+ }))
167
+ })),
168
+ rotationSystem: model.rotation_system,
169
+ designatedSetters: model.designated_setters,
170
+ offensivePreferences: model.offensive_preferences
171
+ };
172
+ }
173
+ export { transformToObject as transformToTactics, transformToAttributes as transformFromTactics, tacticsColumnsToApiDto };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { tacticsColumnsToApiDto } from './tactics';
3
+ import { CourtPosition, EnergyBand } from '../../service';
4
+ // Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
5
+ function makeTactics(designatedSubs) {
6
+ return {
7
+ team_id: 'team-1',
8
+ substitution_tolerance: 10,
9
+ lineup: {
10
+ [CourtPosition.LEFT_FRONT]: 'lf',
11
+ [CourtPosition.MIDDLE_FRONT]: 'mf',
12
+ [CourtPosition.RIGHT_FRONT]: 'rf',
13
+ [CourtPosition.LEFT_BACK]: 'lb',
14
+ [CourtPosition.MIDDLE_BACK]: 'mb',
15
+ [CourtPosition.RIGHT_BACK]: 'rb',
16
+ bench: ['b1', 'b2']
17
+ },
18
+ libero_replacements: ['lf'],
19
+ pinch_server_subs: { starter1: 'server1' },
20
+ receive_rotation_offset: true,
21
+ designated_subs: designatedSubs,
22
+ substitution_band: EnergyBand.TIRED,
23
+ second_libero: undefined,
24
+ libero_sub: undefined,
25
+ libero_replacement_sets: undefined,
26
+ rotation_system: '5-1',
27
+ designated_setters: ['lf'],
28
+ offensive_preferences: [{ rotation: 1, order: ['lf', 'rf'] }]
29
+ };
30
+ }
31
+ describe('tacticsColumnsToApiDto()', () => {
32
+ it('passes scalar/array columns through to the API DTO shape', () => {
33
+ const dto = tacticsColumnsToApiDto(makeTactics([]));
34
+ expect(dto.substitutionTolerance).toBe(10);
35
+ expect(dto.receiveRotationOffset).toBe(true);
36
+ expect(dto.substitutionBand).toBe(EnergyBand.TIRED);
37
+ expect(dto.rotationSystem).toBe('5-1');
38
+ expect(dto.liberoReplacements).toEqual(['lf']);
39
+ expect(dto.pinchServerSubs).toEqual({ starter1: 'server1' });
40
+ expect(dto.lineup.bench).toEqual(['b1', 'b2']);
41
+ expect(dto.offensivePreferences).toEqual([{ rotation: 1, order: ['lf', 'rf'] }]);
42
+ });
43
+ it("resolves a legacy isPinchServer entry to mode 'PINCH'", () => {
44
+ const dto = tacticsColumnsToApiDto(makeTactics([
45
+ { starterId: 's1', benchId: 'b1', isPinchServer: true, conditions: [{ type: 'ASAP' }] }
46
+ ]));
47
+ expect(dto.designatedSubs[0].mode).toBe('PINCH');
48
+ });
49
+ it("resolves a legacy 'NEVER' fatigueBand to mode NEVER and drops the band", () => {
50
+ const dto = tacticsColumnsToApiDto(makeTactics([
51
+ { starterId: 's1', fatigueBand: 'NEVER' }
52
+ ]));
53
+ expect(dto.designatedSubs[0].mode).toBe('NEVER');
54
+ expect(dto.designatedSubs[0].fatigueBand).toBeUndefined();
55
+ });
56
+ it('passes a fatigue entry with its own bands through unchanged', () => {
57
+ const dto = tacticsColumnsToApiDto(makeTactics([
58
+ { starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: EnergyBand.WORN, benchFatigueBand: EnergyBand.TIRED }
59
+ ]));
60
+ expect(dto.designatedSubs[0].mode).toBe('FATIGUE');
61
+ expect(dto.designatedSubs[0].fatigueBand).toBe(EnergyBand.WORN);
62
+ expect(dto.designatedSubs[0].benchFatigueBand).toBe(EnergyBand.TIRED);
63
+ });
64
+ });
@@ -5,6 +5,7 @@ export interface SetSubConfig {
5
5
  readonly mode: SubMode;
6
6
  readonly bench?: Player;
7
7
  readonly fatigueBand?: EnergyBand;
8
+ readonly benchFatigueBand?: EnergyBand;
8
9
  readonly conditions?: PinchCondition[];
9
10
  readonly conditionLogic?: ConditionLogic;
10
11
  }
@@ -43,6 +43,7 @@ export declare const SetSubConfigSchema: z.ZodObject<{
43
43
  }>;
44
44
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
45
45
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
46
47
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
47
48
  type: z.ZodLiteral<PinchConditionType.ASAP>;
48
49
  }, z.core.$strip>, z.ZodObject<{
@@ -80,6 +81,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
80
81
  }>;
81
82
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
82
83
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
84
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
83
85
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
84
86
  type: z.ZodLiteral<PinchConditionType.ASAP>;
85
87
  }, z.core.$strip>, z.ZodObject<{
@@ -115,6 +117,7 @@ export declare const DesignatedSubSchema: z.ZodObject<{
115
117
  }>;
116
118
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
117
119
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
120
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
118
121
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
119
122
  type: z.ZodLiteral<PinchConditionType.ASAP>;
120
123
  }, z.core.$strip>, z.ZodObject<{
@@ -23,6 +23,7 @@ const setSubConfigShape = {
23
23
  mode: SubModeSchema,
24
24
  bench: playerInstanceSchema.optional(),
25
25
  fatigueBand: z.nativeEnum(EnergyBand).optional(),
26
+ benchFatigueBand: z.nativeEnum(EnergyBand).optional(),
26
27
  conditions: z.array(PinchConditionSchema).optional(),
27
28
  conditionLogic: ConditionLogicSchema.optional()
28
29
  };
@@ -71,6 +71,25 @@ describe('DesignatedSubSchema', () => {
71
71
  });
72
72
  expect(res.success).toBe(true);
73
73
  });
74
+ it("accepts a fatigue-mode entry with the substitute's own benchFatigueBand", () => {
75
+ const res = DesignatedSubSchema.safeParse({
76
+ starter,
77
+ bench,
78
+ mode: 'FATIGUE',
79
+ fatigueBand: EnergyBand.WORN,
80
+ benchFatigueBand: EnergyBand.TIRED
81
+ });
82
+ expect(res.success).toBe(true);
83
+ });
84
+ it('rejects benchFatigueBand = NEVER (only the team default band may be NEVER)', () => {
85
+ const res = DesignatedSubSchema.safeParse({
86
+ starter,
87
+ bench,
88
+ mode: 'FATIGUE',
89
+ benchFatigueBand: 'NEVER'
90
+ });
91
+ expect(res.success).toBe(false);
92
+ });
74
93
  it('accepts a fatigue-mode entry with no bench (falls back to closest sub)', () => {
75
94
  const res = DesignatedSubSchema.safeParse({
76
95
  starter,
@@ -26,6 +26,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
26
26
  }>;
27
27
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
28
28
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
29
30
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
30
31
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
31
32
  }, z.core.$strip>, z.ZodObject<{
@@ -61,6 +62,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
61
62
  }>;
62
63
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
63
64
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
65
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof EnergyBand>>;
64
66
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
65
67
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
66
68
  }, z.core.$strip>, z.ZodObject<{
@@ -34,6 +34,7 @@ export declare const TeamInputSchema: z.ZodObject<{
34
34
  }>;
35
35
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
36
36
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
37
38
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
38
39
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
39
40
  }, z.core.$strip>, z.ZodObject<{
@@ -69,6 +70,7 @@ export declare const TeamInputSchema: z.ZodObject<{
69
70
  }>;
70
71
  bench: z.ZodOptional<z.ZodCustom<Player, Player>>;
71
72
  fatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
73
+ benchFatigueBand: z.ZodOptional<z.ZodEnum<typeof import("..").EnergyBand>>;
72
74
  conditions: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
73
75
  type: z.ZodLiteral<import("..").PinchConditionType.ASAP>;
74
76
  }, z.core.$strip>, z.ZodObject<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.426",
3
+ "version": "0.0.428",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",