volleyballsimtypes 0.0.481 → 0.0.483

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 (73) hide show
  1. package/dist/cjs/src/api/index.d.ts +10 -18
  2. package/dist/cjs/src/data/models/tactics.d.ts +17 -19
  3. package/dist/cjs/src/data/models/tactics.js +9 -0
  4. package/dist/cjs/src/data/transformers/match-preset-override.test.js +10 -1
  5. package/dist/cjs/src/data/transformers/rally-event.d.ts +2 -1
  6. package/dist/cjs/src/data/transformers/rally-event.js +12 -0
  7. package/dist/cjs/src/data/transformers/rally.js +2 -0
  8. package/dist/cjs/src/data/transformers/rally.test.d.ts +1 -0
  9. package/dist/cjs/src/data/transformers/rally.test.js +62 -0
  10. package/dist/cjs/src/data/transformers/tactics.js +38 -58
  11. package/dist/cjs/src/data/transformers/tactics.test.js +39 -20
  12. package/dist/cjs/src/service/team/blocking-preference.d.ts +5 -0
  13. package/dist/cjs/src/service/team/blocking-preference.js +2 -0
  14. package/dist/cjs/src/service/team/designated-sub.d.ts +2 -4
  15. package/dist/cjs/src/service/team/index.d.ts +2 -0
  16. package/dist/cjs/src/service/team/index.js +2 -0
  17. package/dist/cjs/src/service/team/libero-sub.d.ts +0 -4
  18. package/dist/cjs/src/service/team/offensive-preference.d.ts +1 -2
  19. package/dist/cjs/src/service/team/pinch-condition.d.ts +11 -3
  20. package/dist/cjs/src/service/team/pinch-condition.js +9 -4
  21. package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  22. package/dist/cjs/src/service/team/schemas/designated-sub.z.js +27 -23
  23. package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +4 -2
  24. package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  25. package/dist/cjs/src/service/team/schemas/libero-sub.z.js +10 -26
  26. package/dist/cjs/src/service/team/schemas/libero-sub.z.test.js +74 -35
  27. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +39 -39
  28. package/dist/cjs/src/service/team/schemas/tactics.z.js +47 -13
  29. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +81 -8
  30. package/dist/cjs/src/service/team/schemas/team.z.d.ts +38 -38
  31. package/dist/cjs/src/service/team/sub-config-convert.d.ts +13 -0
  32. package/dist/cjs/src/service/team/sub-config-convert.js +91 -0
  33. package/dist/cjs/src/service/team/sub-config-convert.test.d.ts +1 -0
  34. package/dist/cjs/src/service/team/sub-config-convert.test.js +84 -0
  35. package/dist/cjs/src/service/team/tactics.d.ts +5 -0
  36. package/dist/cjs/src/service/team/tactics.js +3 -1
  37. package/dist/esm/src/api/index.d.ts +10 -18
  38. package/dist/esm/src/data/models/tactics.d.ts +17 -19
  39. package/dist/esm/src/data/models/tactics.js +9 -0
  40. package/dist/esm/src/data/transformers/match-preset-override.test.js +10 -1
  41. package/dist/esm/src/data/transformers/rally-event.d.ts +2 -1
  42. package/dist/esm/src/data/transformers/rally-event.js +12 -1
  43. package/dist/esm/src/data/transformers/rally.js +4 -2
  44. package/dist/esm/src/data/transformers/rally.test.d.ts +1 -0
  45. package/dist/esm/src/data/transformers/rally.test.js +60 -0
  46. package/dist/esm/src/data/transformers/tactics.js +39 -59
  47. package/dist/esm/src/data/transformers/tactics.test.js +40 -21
  48. package/dist/esm/src/service/team/blocking-preference.d.ts +5 -0
  49. package/dist/esm/src/service/team/blocking-preference.js +1 -0
  50. package/dist/esm/src/service/team/designated-sub.d.ts +2 -4
  51. package/dist/esm/src/service/team/index.d.ts +2 -0
  52. package/dist/esm/src/service/team/index.js +2 -0
  53. package/dist/esm/src/service/team/libero-sub.d.ts +0 -4
  54. package/dist/esm/src/service/team/offensive-preference.d.ts +1 -2
  55. package/dist/esm/src/service/team/pinch-condition.d.ts +11 -3
  56. package/dist/esm/src/service/team/pinch-condition.js +9 -4
  57. package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +30 -32
  58. package/dist/esm/src/service/team/schemas/designated-sub.z.js +26 -22
  59. package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +4 -2
  60. package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +15 -34
  61. package/dist/esm/src/service/team/schemas/libero-sub.z.js +9 -25
  62. package/dist/esm/src/service/team/schemas/libero-sub.z.test.js +75 -36
  63. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +39 -39
  64. package/dist/esm/src/service/team/schemas/tactics.z.js +48 -14
  65. package/dist/esm/src/service/team/schemas/tactics.z.test.js +81 -8
  66. package/dist/esm/src/service/team/schemas/team.z.d.ts +38 -38
  67. package/dist/esm/src/service/team/sub-config-convert.d.ts +13 -0
  68. package/dist/esm/src/service/team/sub-config-convert.js +85 -0
  69. package/dist/esm/src/service/team/sub-config-convert.test.d.ts +1 -0
  70. package/dist/esm/src/service/team/sub-config-convert.test.js +82 -0
  71. package/dist/esm/src/service/team/tactics.d.ts +5 -0
  72. package/dist/esm/src/service/team/tactics.js +3 -1
  73. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, EnergyBand, LiberoSubMode, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../service';
1
+ import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand } from '../service';
2
2
  export type Rally = DataProps<_Rally> & {
3
3
  homePlayerPosition: PlayerPosition[];
4
4
  awayPlayerPosition: PlayerPosition[];
@@ -81,36 +81,28 @@ export interface StartingLineup {
81
81
  bench: string[];
82
82
  }
83
83
  export interface ApiSetSubConfig {
84
- mode: SubMode;
85
84
  benchId?: string;
86
- fatigueBand?: EnergyBand;
87
- benchFatigueBand?: EnergyBand;
88
85
  conditions?: PinchCondition[];
89
86
  conditionLogic?: ConditionLogic;
90
87
  pinchServer?: boolean;
88
+ benchFatigueBand?: EnergyBand;
91
89
  subBackMode?: SubBackMode;
92
90
  subBackConditions?: PinchCondition[];
93
91
  subBackConditionLogic?: ConditionLogic;
94
92
  }
95
- export interface ApiDesignatedSub {
93
+ export interface ApiDesignatedSub extends ApiSetSubConfig {
96
94
  starterId: string;
97
- benchId?: string;
98
- mode: SubMode;
99
- fatigueBand?: EnergyBand;
100
- benchFatigueBand?: EnergyBand;
101
- conditions?: PinchCondition[];
102
- conditionLogic?: ConditionLogic;
103
- pinchServer?: boolean;
104
- subBackMode?: SubBackMode;
105
- subBackConditions?: PinchCondition[];
106
- subBackConditionLogic?: ConditionLogic;
107
95
  sets?: ApiSetSubConfig[];
108
96
  }
109
97
  export interface ApiOffensivePreference {
110
98
  rotation: number;
111
- order: string[];
99
+ weights: Record<string, number>;
112
100
  tempos?: Record<string, 'FAST' | 'HIGH'>;
113
101
  }
102
+ export interface ApiBlockingPreference {
103
+ rotation: number;
104
+ targets: Record<string, RoleEnum>;
105
+ }
114
106
  export interface ApiSystemSet {
115
107
  rotationSystem: RotationSystemEnum;
116
108
  designatedSetters: string[];
@@ -126,8 +118,6 @@ export interface ApiLiberoInjury {
126
118
  inMatchDesignateId?: string;
127
119
  }
128
120
  export interface ApiLiberoSetSubConfig {
129
- mode: LiberoSubMode;
130
- fatigueBand?: EnergyBand;
131
121
  conditions?: PinchCondition[];
132
122
  conditionLogic?: ConditionLogic;
133
123
  }
@@ -148,6 +138,8 @@ export interface Tactics {
148
138
  offensivePreferences: ApiOffensivePreference[];
149
139
  systemSets?: ApiSystemSet[];
150
140
  offensivePreferenceSets?: ApiOffensivePreference[][];
141
+ blockingPreferences: ApiBlockingPreference[];
142
+ blockingPreferenceSets?: ApiBlockingPreference[][];
151
143
  replaceKnockedImmediately?: Record<string, boolean>;
152
144
  injuryReplacements?: ApiInjuryReplacement[];
153
145
  liberoInjury?: ApiLiberoInjury;
@@ -1,7 +1,7 @@
1
1
  import * as Sequelize from 'sequelize';
2
2
  import { Model, Optional } from 'sequelize';
3
3
  import { PlayerId, TeamId, TeamModel } from '.';
4
- import { ConditionLogic, CourtPosition, EnergyBand, LiberoSubMode, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
4
+ import { ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -13,37 +13,31 @@ export interface TacticsLineupAttributes {
13
13
  bench: PlayerId[];
14
14
  }
15
15
  export interface SetSubConfigAttributes {
16
- mode: SubMode;
17
16
  benchId?: PlayerId;
18
- fatigueBand?: EnergyBand;
19
- benchFatigueBand?: EnergyBand;
20
17
  conditions?: PinchCondition[];
21
18
  conditionLogic?: ConditionLogic;
22
19
  pinchServer?: boolean;
20
+ benchFatigueBand?: EnergyBand;
23
21
  subBackMode?: SubBackMode;
24
22
  subBackConditions?: PinchCondition[];
25
23
  subBackConditionLogic?: ConditionLogic;
24
+ mode?: SubMode;
25
+ fatigueBand?: SubBand;
26
26
  }
27
- export interface DesignatedSubAttributes {
27
+ export interface DesignatedSubAttributes extends SetSubConfigAttributes {
28
28
  starterId: PlayerId;
29
- benchId?: PlayerId;
30
- mode?: SubMode;
31
29
  isPinchServer?: boolean;
32
- fatigueBand?: SubBand;
33
- benchFatigueBand?: EnergyBand;
34
- conditions?: PinchCondition[];
35
- conditionLogic?: ConditionLogic;
36
- pinchServer?: boolean;
37
- subBackMode?: SubBackMode;
38
- subBackConditions?: PinchCondition[];
39
- subBackConditionLogic?: ConditionLogic;
40
30
  sets?: SetSubConfigAttributes[];
41
31
  }
42
32
  export interface OffensivePreferenceAttributes {
43
33
  rotation: number;
44
- order: PlayerId[];
34
+ weights: Record<PlayerId, number>;
45
35
  tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
46
36
  }
37
+ export interface BlockingPreferenceAttributes {
38
+ rotation: number;
39
+ targets: Record<PlayerId, RoleEnum>;
40
+ }
47
41
  export interface SystemSetAttributes {
48
42
  rotationSystem: RotationSystemEnum;
49
43
  designatedSetters: PlayerId[];
@@ -59,10 +53,10 @@ export interface LiberoInjuryAttributes {
59
53
  inMatchDesignateId?: PlayerId;
60
54
  }
61
55
  export interface LiberoSetSubConfigAttributes {
62
- mode: LiberoSubMode;
63
- fatigueBand?: EnergyBand;
64
56
  conditions?: PinchCondition[];
65
57
  conditionLogic?: ConditionLogic;
58
+ mode?: string;
59
+ fatigueBand?: EnergyBand;
66
60
  }
67
61
  export interface LiberoSubConfigAttributes extends LiberoSetSubConfigAttributes {
68
62
  sets?: LiberoSetSubConfigAttributes[];
@@ -85,10 +79,12 @@ export interface TacticsAttributes {
85
79
  replace_knocked_immediately?: Record<PlayerId, boolean>;
86
80
  injury_replacements?: InjuryReplacementAttributes[];
87
81
  libero_injury?: LiberoInjuryAttributes;
82
+ blocking_preferences: BlockingPreferenceAttributes[];
83
+ blocking_preference_sets?: BlockingPreferenceAttributes[][];
88
84
  }
89
85
  export type TacticsPk = 'team_id';
90
86
  export type TacticsId = TacticsModel[TacticsPk];
91
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury';
87
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets';
92
88
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
93
89
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
94
90
  team_id: string;
@@ -108,6 +104,8 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
108
104
  replace_knocked_immediately?: Record<PlayerId, boolean>;
109
105
  injury_replacements?: InjuryReplacementAttributes[];
110
106
  libero_injury?: LiberoInjuryAttributes;
107
+ blocking_preferences: BlockingPreferenceAttributes[];
108
+ blocking_preference_sets?: BlockingPreferenceAttributes[][];
111
109
  team: TeamModel;
112
110
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
113
111
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -86,6 +86,15 @@ class TacticsModel extends sequelize_1.Model {
86
86
  libero_injury: {
87
87
  type: sequelize_1.DataTypes.JSONB,
88
88
  allowNull: true
89
+ },
90
+ blocking_preferences: {
91
+ type: sequelize_1.DataTypes.JSONB,
92
+ allowNull: false,
93
+ defaultValue: []
94
+ },
95
+ blocking_preference_sets: {
96
+ type: sequelize_1.DataTypes.JSONB,
97
+ allowNull: true
89
98
  }
90
99
  }, {
91
100
  sequelize,
@@ -150,7 +150,16 @@ function starterAt(tactics, zone) {
150
150
  (0, globals_1.expect)(back.lineup).toEqual(full.lineup);
151
151
  (0, globals_1.expect)(back.libero_replacements).toEqual(full.libero_replacements);
152
152
  (0, globals_1.expect)(back.receive_rotation_offset).toBe(true);
153
- (0, globals_1.expect)(back.designated_subs).toEqual(full.designated_subs);
153
+ // The dto path converts legacy mode-shaped subs to the unified condition engine, so the round-trip lands
154
+ // in the converted (unified) shape rather than the legacy input.
155
+ (0, globals_1.expect)(back.designated_subs).toEqual([{
156
+ starterId: ids[0],
157
+ conditions: [{ type: 'FATIGUE', band: 'WORN' }],
158
+ conditionLogic: 'ALL',
159
+ pinchServer: false,
160
+ subBackMode: 'FATIGUE',
161
+ sets: [{ conditions: [] }]
162
+ }]);
154
163
  (0, globals_1.expect)(back.libero_replacement_sets).toEqual(full.libero_replacement_sets);
155
164
  (0, globals_1.expect)(back.rotation_system).toBe('5-1');
156
165
  (0, globals_1.expect)(back.designated_setters).toEqual(full.designated_setters);
@@ -1,4 +1,4 @@
1
- import { Block, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
1
+ import { Block, FreeBall, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
2
2
  export interface CompactEvent {
3
3
  p: number;
4
4
  e: number;
@@ -21,4 +21,5 @@ export declare function transformToReception(event: CompactEvent, roster: string
21
21
  export declare function transformToServe(event: CompactEvent, roster: string[]): Serve;
22
22
  export declare function transformToSet(event: CompactEvent, roster: string[]): Set;
23
23
  export declare function transformToSpike(event: CompactEvent, roster: string[]): Spike;
24
+ export declare function transformToFreeBall(event: CompactEvent, roster: string[]): FreeBall;
24
25
  export declare function transformToSubstitution(event: CompactEvent, roster: string[]): Substitution;
@@ -7,6 +7,7 @@ exports.transformToReception = transformToReception;
7
7
  exports.transformToServe = transformToServe;
8
8
  exports.transformToSet = transformToSet;
9
9
  exports.transformToSpike = transformToSpike;
10
+ exports.transformToFreeBall = transformToFreeBall;
10
11
  exports.transformToSubstitution = transformToSubstitution;
11
12
  const service_1 = require("../../service");
12
13
  // Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
@@ -118,6 +119,17 @@ function transformToSpike(event, roster) {
118
119
  incident: incidentFromCompact(event.i, event.h, roster)
119
120
  });
120
121
  }
122
+ // FreeBall (Last Resort save) is a plain InPlayEvent with no incident of its own, so it decodes from f/t/a/s
123
+ // exactly like a spike minus the incident. Must stay in lockstep with the case 7 branch in rally.ts.
124
+ function transformToFreeBall(event, roster) {
125
+ return service_1.FreeBall.create({
126
+ failure: event.f,
127
+ type: event.t,
128
+ playerId: roster[event.p],
129
+ target: event.a,
130
+ score: event.s
131
+ });
132
+ }
121
133
  function transformToSubstitution(event, roster) {
122
134
  return service_1.Substitution.create({
123
135
  playerId: roster[event.p],
@@ -61,6 +61,7 @@ function decodeCompactEvent(event, roster) {
61
61
  case 4: return (0, rally_event_1.transformToSet)(event, roster);
62
62
  case 5: return (0, rally_event_1.transformToSpike)(event, roster);
63
63
  case 6: return (0, rally_event_1.transformToBlock)(event, roster);
64
+ case 7: return (0, rally_event_1.transformToFreeBall)(event, roster);
64
65
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
65
66
  }
66
67
  }
@@ -73,6 +74,7 @@ function decodeLegacyEvent(event) {
73
74
  case 4: return service_1.Set.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
74
75
  case 5: return service_1.Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
75
76
  case 6: return service_1.Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s });
77
+ case 7: return service_1.FreeBall.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
76
78
  default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
77
79
  }
78
80
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const uuid_1 = require("uuid");
5
+ const service_1 = require("../../service");
6
+ const rally_1 = require("./rally");
7
+ // End-to-end rally round trip (encode -> bytea -> decode) exercising the decodeCompactEvent switch, with one
8
+ // event of EVERY EventTypeEnum member. This is the guard that would have caught the FREE_BALL decoder gap: the
9
+ // encoder is generic (out.e = evt.eventType) but the decoder is a hardcoded switch, so a new event type that is
10
+ // not wired into the switch throws UNKNOWN_EVENT_TYPE here. The coverage test forces new members into this list.
11
+ (0, globals_1.describe)('rally compact round trip — every event type', () => {
12
+ const pA = (0, uuid_1.v4)();
13
+ const pB = (0, uuid_1.v4)();
14
+ const pC = (0, uuid_1.v4)();
15
+ const setId = (0, uuid_1.v4)();
16
+ const events = [
17
+ service_1.LiberoReplacement.create({ playerId: pA, libero: pB, type: service_1.LiberoReplacementTypeEnum.LIBERO_IN }),
18
+ service_1.Substitution.create({ playerId: pA, playerOut: pC, injury: true }),
19
+ service_1.Serve.create({ playerId: pA, target: 6, score: 55, failure: service_1.ServeFailureEnum.NO_FAILURE, type: service_1.ServeTypeEnum.JUMP_TOPSPIN }),
20
+ service_1.Reception.create({ playerId: pB, target: 5, score: 50, failure: service_1.ReceptionFailureEnum.NO_FAILURE, type: service_1.ReceptionTypeEnum.DIG }),
21
+ service_1.Set.create({ playerId: pC, target: 3, score: 48, failure: service_1.SetFailureEnum.NO_FAILURE, type: service_1.SetTypeEnum.OVERHAND, tempo: service_1.SetTempoEnum.FIRST }),
22
+ service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.CROSS_SHOT }),
23
+ service_1.Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: service_1.BlockFailureEnum.NO_FAILURE, type: service_1.BlockTypeEnum.DOUBLE }),
24
+ service_1.FreeBall.create({ playerId: pC, target: 6, score: 45, failure: service_1.FreeBallFailureEnum.NO_FAILURE, type: service_1.FreeBallTypeEnum.OVER })
25
+ ];
26
+ const rally = service_1.Rally.create({ id: (0, uuid_1.v4)(), order: 0, servingTeamId: (0, uuid_1.v4)(), events });
27
+ (0, globals_1.it)('covers every EventTypeEnum member (adding a new event type must extend this list)', () => {
28
+ const numericTypes = Object.values(service_1.EventTypeEnum).filter((v) => typeof v === 'number');
29
+ const present = new Set(events.map(e => e.eventType));
30
+ for (const t of numericTypes)
31
+ (0, globals_1.expect)(present.has(t)).toBe(true);
32
+ (0, globals_1.expect)(present.size).toBe(numericTypes.length);
33
+ });
34
+ (0, globals_1.it)('encodes then decodes every event type back to an equal event (guards the decoder switch)', () => {
35
+ const attrs = (0, rally_1.transformFromRally)(rally, setId);
36
+ const decoded = (0, rally_1.transformToRally)(attrs);
37
+ (0, globals_1.expect)(decoded.events).toHaveLength(events.length);
38
+ (0, globals_1.expect)(decoded.events.map(e => e.eventType)).toEqual(events.map(e => e.eventType));
39
+ const [lib, sub, serve, rec, set, spike, block, free] = decoded.events;
40
+ (0, globals_1.expect)(lib.playerId).toBe(pA);
41
+ (0, globals_1.expect)(lib.libero).toBe(pB);
42
+ (0, globals_1.expect)(lib.type).toBe(service_1.LiberoReplacementTypeEnum.LIBERO_IN);
43
+ (0, globals_1.expect)(sub.playerOut).toBe(pC);
44
+ (0, globals_1.expect)(sub.injury).toBe(true);
45
+ (0, globals_1.expect)(serve.target).toBe(6);
46
+ (0, globals_1.expect)(serve.score).toBe(55);
47
+ (0, globals_1.expect)(serve.type).toBe(service_1.ServeTypeEnum.JUMP_TOPSPIN);
48
+ (0, globals_1.expect)(rec.target).toBe(5);
49
+ (0, globals_1.expect)(rec.type).toBe(service_1.ReceptionTypeEnum.DIG);
50
+ (0, globals_1.expect)(set.type).toBe(service_1.SetTypeEnum.OVERHAND);
51
+ (0, globals_1.expect)(set.tempo).toBe(service_1.SetTempoEnum.FIRST);
52
+ (0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.CROSS_SHOT);
53
+ (0, globals_1.expect)(spike.score).toBe(62.5);
54
+ (0, globals_1.expect)(block.type).toBe(service_1.BlockTypeEnum.DOUBLE);
55
+ (0, globals_1.expect)(block.blockers).toEqual([pB, pC]);
56
+ (0, globals_1.expect)(free.playerId).toBe(pC);
57
+ (0, globals_1.expect)(free.target).toBe(6);
58
+ (0, globals_1.expect)(free.score).toBe(45);
59
+ (0, globals_1.expect)(free.type).toBe(service_1.FreeBallTypeEnum.OVER);
60
+ (0, globals_1.expect)(free.failure).toBe(service_1.FreeBallFailureEnum.NO_FAILURE);
61
+ });
62
+ });
@@ -5,24 +5,9 @@ exports.transformFromTactics = transformToAttributes;
5
5
  exports.tacticsColumnsToApiDto = tacticsColumnsToApiDto;
6
6
  exports.apiTacticsConfigToTacticsAttributes = apiTacticsConfigToAttributes;
7
7
  const service_1 = require("../../service");
8
- // Back-compat: new rows persist `mode`; rows written before the mode change persist `isPinchServer` (+ a
9
- // 'NEVER' fatigueBand for the old "never fatigue-sub" state). Derive the mode so existing data keeps working
10
- // until the next tactics save rewrites it in the new shape.
11
- function subModeFromAttributes(d) {
12
- if (d.mode != null)
13
- return d.mode;
14
- if (d.isPinchServer === true)
15
- return 'PINCH';
16
- if (d.fatigueBand === 'NEVER')
17
- return 'NEVER';
18
- return 'FATIGUE';
19
- }
20
- // pinchServer only applies to PINCH ("Conditions") mode. A LEGACY PINCH row (no flag) defaults to true so
21
- // existing pinch servers keep serve-and-return behavior; a new conditional sub persists an explicit false. This
22
- // default must resolve identically on every READ path (both transformToObject and tacticsColumnsToApiDto).
23
- function effectivePinchServer(mode, pinchServer) {
24
- return mode === 'PINCH' ? (pinchServer ?? true) : undefined;
25
- }
8
+ // Unified condition engine (owner 2026-07-11): the READ path passes legacy fields (mode / fatigueBand /
9
+ // isPinchServer) through RAW; the schema preprocess in Tactics.create converts every legacy shape (see
10
+ // service/team/sub-config-convert.ts). The WRITE path persists only the unified shape.
26
11
  function findPlayer(id, roster) {
27
12
  const player = roster.find((p) => p.id === id);
28
13
  if (player == null)
@@ -66,23 +51,19 @@ function transformToAttributes(tactics, teamId) {
66
51
  designated_subs: tactics.designatedSubs.map(ds => ({
67
52
  starterId: ds.starter.id,
68
53
  benchId: ds.bench?.id,
69
- mode: ds.mode,
70
- fatigueBand: ds.fatigueBand,
71
- benchFatigueBand: ds.benchFatigueBand,
72
54
  conditions: ds.conditions,
73
55
  conditionLogic: ds.conditionLogic,
74
56
  pinchServer: ds.pinchServer,
57
+ benchFatigueBand: ds.benchFatigueBand,
75
58
  subBackMode: ds.subBackMode,
76
59
  subBackConditions: ds.subBackConditions,
77
60
  subBackConditionLogic: ds.subBackConditionLogic,
78
61
  sets: ds.sets?.map(s => ({
79
- mode: s.mode,
80
62
  benchId: s.bench?.id,
81
- fatigueBand: s.fatigueBand,
82
- benchFatigueBand: s.benchFatigueBand,
83
63
  conditions: s.conditions,
84
64
  conditionLogic: s.conditionLogic,
85
65
  pinchServer: s.pinchServer,
66
+ benchFatigueBand: s.benchFatigueBand,
86
67
  subBackMode: s.subBackMode,
87
68
  subBackConditions: s.subBackConditions,
88
69
  subBackConditionLogic: s.subBackConditionLogic
@@ -92,7 +73,7 @@ function transformToAttributes(tactics, teamId) {
92
73
  designated_setters: tactics.designatedSetters.map((s) => s.id),
93
74
  offensive_preferences: tactics.offensivePreferences.map(p => ({
94
75
  rotation: p.rotation,
95
- order: p.order.map((a) => a.id),
76
+ weights: p.weights,
96
77
  tempos: p.tempos
97
78
  })),
98
79
  system_sets: tactics.systemSets?.map(s => ({
@@ -101,9 +82,11 @@ function transformToAttributes(tactics, teamId) {
101
82
  })),
102
83
  offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
103
84
  rotation: p.rotation,
104
- order: p.order.map((a) => a.id),
85
+ weights: p.weights,
105
86
  tempos: p.tempos
106
87
  }))),
88
+ blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
89
+ blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
107
90
  replace_knocked_immediately: tactics.replaceKnockedImmediately,
108
91
  injury_replacements: tactics.injuryReplacements.map(ir => ({
109
92
  starterId: ir.starter.id,
@@ -163,15 +146,19 @@ function transformToObject(model, roster) {
163
146
  secondLibero,
164
147
  liberoSub: secondLibero != null ? (model.libero_sub ?? undefined) : undefined,
165
148
  liberoReplacementSets,
149
+ // Legacy fields (mode / fatigueBand / isPinchServer) pass through RAW: the DesignatedSubSchema preprocess
150
+ // inside Tactics.create converts every legacy shape (mode-era AND isPinchServer-era rows) to the unified
151
+ // condition engine. Only the player references are resolved here.
166
152
  designatedSubs: (model.designated_subs ?? []).map(d => ({
167
153
  starter: findPlayer(d.starterId, roster),
168
154
  bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
169
- mode: subModeFromAttributes(d),
170
- fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
155
+ mode: d.mode,
156
+ isPinchServer: d.isPinchServer,
157
+ fatigueBand: d.fatigueBand,
171
158
  benchFatigueBand: d.benchFatigueBand,
172
159
  conditions: d.conditions,
173
160
  conditionLogic: d.conditionLogic,
174
- pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
161
+ pinchServer: d.pinchServer,
175
162
  subBackMode: d.subBackMode,
176
163
  subBackConditions: d.subBackConditions,
177
164
  subBackConditionLogic: d.subBackConditionLogic,
@@ -182,7 +169,7 @@ function transformToObject(model, roster) {
182
169
  benchFatigueBand: s.benchFatigueBand,
183
170
  conditions: s.conditions,
184
171
  conditionLogic: s.conditionLogic,
185
- pinchServer: effectivePinchServer(s.mode, s.pinchServer),
172
+ pinchServer: s.pinchServer,
186
173
  subBackMode: s.subBackMode,
187
174
  subBackConditions: s.subBackConditions,
188
175
  subBackConditionLogic: s.subBackConditionLogic
@@ -192,7 +179,7 @@ function transformToObject(model, roster) {
192
179
  designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
193
180
  offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
194
181
  rotation: p.rotation,
195
- order: p.order.map((id) => findPlayer(id, roster)),
182
+ weights: p.weights,
196
183
  tempos: p.tempos
197
184
  })),
198
185
  systemSets: model.system_sets?.map(s => ({
@@ -201,9 +188,11 @@ function transformToObject(model, roster) {
201
188
  })),
202
189
  offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
203
190
  rotation: p.rotation,
204
- order: p.order.map((id) => findPlayer(id, roster)),
191
+ weights: p.weights,
205
192
  tempos: p.tempos
206
193
  }))),
194
+ blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
195
+ blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
207
196
  // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
208
197
  // libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
209
198
  // key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
@@ -328,38 +317,27 @@ function tacticsColumnsToApiDto(model) {
328
317
  receiveRotationOffset: model.receive_rotation_offset,
329
318
  substitutionBand: model.substitution_band,
330
319
  secondLibero: validSecondLibero,
331
- liberoSub: validSecondLibero != null ? model.libero_sub : undefined,
320
+ // The API surfaces the UNIFIED condition-engine shape: legacy rows (mode-era / isPinchServer-era) convert
321
+ // here via the same service converters the schema preprocess uses, so clients never see a mode.
322
+ liberoSub: validSecondLibero != null
323
+ ? (model.libero_sub != null ? (0, service_1.convertLegacyLiberoSubConfig)(model.libero_sub) : undefined)
324
+ : undefined,
332
325
  liberoReplacementSets: validLiberoReplacementSets,
333
- designatedSubs: (model.designated_subs ?? []).map(d => ({
334
- starterId: d.starterId,
335
- benchId: d.benchId,
336
- mode: subModeFromAttributes(d),
337
- fatigueBand: d.fatigueBand === 'NEVER' ? undefined : d.fatigueBand,
338
- benchFatigueBand: d.benchFatigueBand,
339
- conditions: d.conditions,
340
- conditionLogic: d.conditionLogic,
341
- pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
342
- subBackMode: d.subBackMode,
343
- subBackConditions: d.subBackConditions,
344
- subBackConditionLogic: d.subBackConditionLogic,
345
- sets: d.sets?.map(s => ({
346
- mode: s.mode,
347
- benchId: s.benchId,
348
- fatigueBand: s.fatigueBand,
349
- benchFatigueBand: s.benchFatigueBand,
350
- conditions: s.conditions,
351
- conditionLogic: s.conditionLogic,
352
- pinchServer: effectivePinchServer(s.mode, s.pinchServer),
353
- subBackMode: s.subBackMode,
354
- subBackConditions: s.subBackConditions,
355
- subBackConditionLogic: s.subBackConditionLogic
356
- }))
357
- })),
326
+ designatedSubs: (model.designated_subs ?? []).map(d => {
327
+ const { starterId, sets, ...cfg } = d;
328
+ return {
329
+ ...(0, service_1.convertLegacySetSubConfig)(cfg),
330
+ starterId,
331
+ sets: sets?.map(s => (0, service_1.convertLegacySetSubConfig)(s))
332
+ };
333
+ }),
358
334
  rotationSystem: model.rotation_system,
359
335
  designatedSetters: model.designated_setters,
360
336
  offensivePreferences: model.offensive_preferences,
361
337
  systemSets: model.system_sets,
362
338
  offensivePreferenceSets: model.offensive_preference_sets,
339
+ blockingPreferences: model.blocking_preferences,
340
+ blockingPreferenceSets: model.blocking_preference_sets,
363
341
  // Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
364
342
  // libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
365
343
  // survive; an empty map surfaces as absent (missing key = ON).
@@ -465,6 +443,8 @@ function apiTacticsConfigToAttributes(config, teamId) {
465
443
  offensive_preferences: config.offensivePreferences,
466
444
  system_sets: config.systemSets,
467
445
  offensive_preference_sets: config.offensivePreferenceSets,
446
+ blocking_preferences: config.blockingPreferences ?? [],
447
+ blocking_preference_sets: config.blockingPreferenceSets,
468
448
  replace_knocked_immediately: config.replaceKnockedImmediately,
469
449
  injury_replacements: config.injuryReplacements,
470
450
  libero_injury: config.liberoInjury