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.
- package/dist/cjs/src/api/index.d.ts +10 -18
- package/dist/cjs/src/data/models/tactics.d.ts +17 -19
- package/dist/cjs/src/data/models/tactics.js +9 -0
- package/dist/cjs/src/data/transformers/match-preset-override.test.js +10 -1
- package/dist/cjs/src/data/transformers/rally-event.d.ts +2 -1
- package/dist/cjs/src/data/transformers/rally-event.js +12 -0
- package/dist/cjs/src/data/transformers/rally.js +2 -0
- package/dist/cjs/src/data/transformers/rally.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally.test.js +62 -0
- package/dist/cjs/src/data/transformers/tactics.js +38 -58
- package/dist/cjs/src/data/transformers/tactics.test.js +39 -20
- package/dist/cjs/src/service/team/blocking-preference.d.ts +5 -0
- package/dist/cjs/src/service/team/blocking-preference.js +2 -0
- package/dist/cjs/src/service/team/designated-sub.d.ts +2 -4
- package/dist/cjs/src/service/team/index.d.ts +2 -0
- package/dist/cjs/src/service/team/index.js +2 -0
- package/dist/cjs/src/service/team/libero-sub.d.ts +0 -4
- package/dist/cjs/src/service/team/offensive-preference.d.ts +1 -2
- package/dist/cjs/src/service/team/pinch-condition.d.ts +11 -3
- package/dist/cjs/src/service/team/pinch-condition.js +9 -4
- package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +30 -32
- package/dist/cjs/src/service/team/schemas/designated-sub.z.js +27 -23
- package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +4 -2
- package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +15 -34
- package/dist/cjs/src/service/team/schemas/libero-sub.z.js +10 -26
- package/dist/cjs/src/service/team/schemas/libero-sub.z.test.js +74 -35
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +39 -39
- package/dist/cjs/src/service/team/schemas/tactics.z.js +47 -13
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +81 -8
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +38 -38
- package/dist/cjs/src/service/team/sub-config-convert.d.ts +13 -0
- package/dist/cjs/src/service/team/sub-config-convert.js +91 -0
- package/dist/cjs/src/service/team/sub-config-convert.test.d.ts +1 -0
- package/dist/cjs/src/service/team/sub-config-convert.test.js +84 -0
- package/dist/cjs/src/service/team/tactics.d.ts +5 -0
- package/dist/cjs/src/service/team/tactics.js +3 -1
- package/dist/esm/src/api/index.d.ts +10 -18
- package/dist/esm/src/data/models/tactics.d.ts +17 -19
- package/dist/esm/src/data/models/tactics.js +9 -0
- package/dist/esm/src/data/transformers/match-preset-override.test.js +10 -1
- package/dist/esm/src/data/transformers/rally-event.d.ts +2 -1
- package/dist/esm/src/data/transformers/rally-event.js +12 -1
- package/dist/esm/src/data/transformers/rally.js +4 -2
- package/dist/esm/src/data/transformers/rally.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally.test.js +60 -0
- package/dist/esm/src/data/transformers/tactics.js +39 -59
- package/dist/esm/src/data/transformers/tactics.test.js +40 -21
- package/dist/esm/src/service/team/blocking-preference.d.ts +5 -0
- package/dist/esm/src/service/team/blocking-preference.js +1 -0
- package/dist/esm/src/service/team/designated-sub.d.ts +2 -4
- package/dist/esm/src/service/team/index.d.ts +2 -0
- package/dist/esm/src/service/team/index.js +2 -0
- package/dist/esm/src/service/team/libero-sub.d.ts +0 -4
- package/dist/esm/src/service/team/offensive-preference.d.ts +1 -2
- package/dist/esm/src/service/team/pinch-condition.d.ts +11 -3
- package/dist/esm/src/service/team/pinch-condition.js +9 -4
- package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +30 -32
- package/dist/esm/src/service/team/schemas/designated-sub.z.js +26 -22
- package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +4 -2
- package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +15 -34
- package/dist/esm/src/service/team/schemas/libero-sub.z.js +9 -25
- package/dist/esm/src/service/team/schemas/libero-sub.z.test.js +75 -36
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +39 -39
- package/dist/esm/src/service/team/schemas/tactics.z.js +48 -14
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +81 -8
- package/dist/esm/src/service/team/schemas/team.z.d.ts +38 -38
- package/dist/esm/src/service/team/sub-config-convert.d.ts +13 -0
- package/dist/esm/src/service/team/sub-config-convert.js +85 -0
- package/dist/esm/src/service/team/sub-config-convert.test.d.ts +1 -0
- package/dist/esm/src/service/team/sub-config-convert.test.js +82 -0
- package/dist/esm/src/service/team/tactics.d.ts +5 -0
- package/dist/esm/src/service/team/tactics.js +3 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
|
|
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>;
|
|
@@ -83,6 +83,15 @@ export class TacticsModel extends Model {
|
|
|
83
83
|
libero_injury: {
|
|
84
84
|
type: DataTypes.JSONB,
|
|
85
85
|
allowNull: true
|
|
86
|
+
},
|
|
87
|
+
blocking_preferences: {
|
|
88
|
+
type: DataTypes.JSONB,
|
|
89
|
+
allowNull: false,
|
|
90
|
+
defaultValue: []
|
|
91
|
+
},
|
|
92
|
+
blocking_preference_sets: {
|
|
93
|
+
type: DataTypes.JSONB,
|
|
94
|
+
allowNull: true
|
|
86
95
|
}
|
|
87
96
|
}, {
|
|
88
97
|
sequelize,
|
|
@@ -148,7 +148,16 @@ describe('apiTacticsConfigToTacticsAttributes()', () => {
|
|
|
148
148
|
expect(back.lineup).toEqual(full.lineup);
|
|
149
149
|
expect(back.libero_replacements).toEqual(full.libero_replacements);
|
|
150
150
|
expect(back.receive_rotation_offset).toBe(true);
|
|
151
|
-
|
|
151
|
+
// The dto path converts legacy mode-shaped subs to the unified condition engine, so the round-trip lands
|
|
152
|
+
// in the converted (unified) shape rather than the legacy input.
|
|
153
|
+
expect(back.designated_subs).toEqual([{
|
|
154
|
+
starterId: ids[0],
|
|
155
|
+
conditions: [{ type: 'FATIGUE', band: 'WORN' }],
|
|
156
|
+
conditionLogic: 'ALL',
|
|
157
|
+
pinchServer: false,
|
|
158
|
+
subBackMode: 'FATIGUE',
|
|
159
|
+
sets: [{ conditions: [] }]
|
|
160
|
+
}]);
|
|
152
161
|
expect(back.libero_replacement_sets).toEqual(full.libero_replacement_sets);
|
|
153
162
|
expect(back.rotation_system).toBe('5-1');
|
|
154
163
|
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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Block, InPlayEvent, LiberoReplacement, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
1
|
+
import { Block, FreeBall, InPlayEvent, LiberoReplacement, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
2
2
|
// Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
|
|
3
3
|
function incidentToCompact(incident) {
|
|
4
4
|
if (incident == null)
|
|
@@ -108,6 +108,17 @@ export function transformToSpike(event, roster) {
|
|
|
108
108
|
incident: incidentFromCompact(event.i, event.h, roster)
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
// FreeBall (Last Resort save) is a plain InPlayEvent with no incident of its own, so it decodes from f/t/a/s
|
|
112
|
+
// exactly like a spike minus the incident. Must stay in lockstep with the case 7 branch in rally.ts.
|
|
113
|
+
export function transformToFreeBall(event, roster) {
|
|
114
|
+
return FreeBall.create({
|
|
115
|
+
failure: event.f,
|
|
116
|
+
type: event.t,
|
|
117
|
+
playerId: roster[event.p],
|
|
118
|
+
target: event.a,
|
|
119
|
+
score: event.s
|
|
120
|
+
});
|
|
121
|
+
}
|
|
111
122
|
export function transformToSubstitution(event, roster) {
|
|
112
123
|
return Substitution.create({
|
|
113
124
|
playerId: roster[event.p],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { pack, unpack } from 'msgpackr';
|
|
2
2
|
import { deflateRawSync, inflateRawSync } from 'zlib';
|
|
3
3
|
import { decompressFromBase64 } from 'lz-string';
|
|
4
|
-
import { Block, LiberoReplacement, Rally, Reception, Serve, Set as VolleySet, Spike, Substitution } from '../../service';
|
|
5
|
-
import { transformToBlock, transformToCompact, transformToLiberoReplacement, transformToReception, transformToServe, transformToSet, transformToSpike, transformToSubstitution } from './rally-event';
|
|
4
|
+
import { Block, FreeBall, LiberoReplacement, Rally, Reception, Serve, Set as VolleySet, Spike, Substitution } from '../../service';
|
|
5
|
+
import { transformToBlock, transformToCompact, transformToFreeBall, transformToLiberoReplacement, transformToReception, transformToServe, transformToSet, transformToSpike, transformToSubstitution } from './rally-event';
|
|
6
6
|
const VERSION_BYTE = 0x01;
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
8
|
function deflate(data) { return deflateRawSync(data); }
|
|
@@ -57,6 +57,7 @@ function decodeCompactEvent(event, roster) {
|
|
|
57
57
|
case 4: return transformToSet(event, roster);
|
|
58
58
|
case 5: return transformToSpike(event, roster);
|
|
59
59
|
case 6: return transformToBlock(event, roster);
|
|
60
|
+
case 7: return transformToFreeBall(event, roster);
|
|
60
61
|
default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -69,6 +70,7 @@ function decodeLegacyEvent(event) {
|
|
|
69
70
|
case 4: return VolleySet.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
70
71
|
case 5: return Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
71
72
|
case 6: return Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s });
|
|
73
|
+
case 7: return FreeBall.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
72
74
|
default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { Block, BlockFailureEnum, BlockTypeEnum, EventTypeEnum, FreeBall, FreeBallFailureEnum, FreeBallTypeEnum, LiberoReplacement, LiberoReplacementTypeEnum, Rally, Reception, ReceptionFailureEnum, ReceptionTypeEnum, Serve, ServeFailureEnum, ServeTypeEnum, Set as VolleySet, SetFailureEnum, SetTempoEnum, SetTypeEnum, Spike, SpikeFailureEnum, SpikeTypeEnum, Substitution } from '../../service';
|
|
4
|
+
import { transformFromRally, transformToRally } from './rally';
|
|
5
|
+
// End-to-end rally round trip (encode -> bytea -> decode) exercising the decodeCompactEvent switch, with one
|
|
6
|
+
// event of EVERY EventTypeEnum member. This is the guard that would have caught the FREE_BALL decoder gap: the
|
|
7
|
+
// encoder is generic (out.e = evt.eventType) but the decoder is a hardcoded switch, so a new event type that is
|
|
8
|
+
// not wired into the switch throws UNKNOWN_EVENT_TYPE here. The coverage test forces new members into this list.
|
|
9
|
+
describe('rally compact round trip — every event type', () => {
|
|
10
|
+
const pA = uuidv4();
|
|
11
|
+
const pB = uuidv4();
|
|
12
|
+
const pC = uuidv4();
|
|
13
|
+
const setId = uuidv4();
|
|
14
|
+
const events = [
|
|
15
|
+
LiberoReplacement.create({ playerId: pA, libero: pB, type: LiberoReplacementTypeEnum.LIBERO_IN }),
|
|
16
|
+
Substitution.create({ playerId: pA, playerOut: pC, injury: true }),
|
|
17
|
+
Serve.create({ playerId: pA, target: 6, score: 55, failure: ServeFailureEnum.NO_FAILURE, type: ServeTypeEnum.JUMP_TOPSPIN }),
|
|
18
|
+
Reception.create({ playerId: pB, target: 5, score: 50, failure: ReceptionFailureEnum.NO_FAILURE, type: ReceptionTypeEnum.DIG }),
|
|
19
|
+
VolleySet.create({ playerId: pC, target: 3, score: 48, failure: SetFailureEnum.NO_FAILURE, type: SetTypeEnum.OVERHAND, tempo: SetTempoEnum.FIRST }),
|
|
20
|
+
Spike.create({ playerId: pA, target: 4, score: 62.5, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.CROSS_SHOT }),
|
|
21
|
+
Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: BlockFailureEnum.NO_FAILURE, type: BlockTypeEnum.DOUBLE }),
|
|
22
|
+
FreeBall.create({ playerId: pC, target: 6, score: 45, failure: FreeBallFailureEnum.NO_FAILURE, type: FreeBallTypeEnum.OVER })
|
|
23
|
+
];
|
|
24
|
+
const rally = Rally.create({ id: uuidv4(), order: 0, servingTeamId: uuidv4(), events });
|
|
25
|
+
it('covers every EventTypeEnum member (adding a new event type must extend this list)', () => {
|
|
26
|
+
const numericTypes = Object.values(EventTypeEnum).filter((v) => typeof v === 'number');
|
|
27
|
+
const present = new Set(events.map(e => e.eventType));
|
|
28
|
+
for (const t of numericTypes)
|
|
29
|
+
expect(present.has(t)).toBe(true);
|
|
30
|
+
expect(present.size).toBe(numericTypes.length);
|
|
31
|
+
});
|
|
32
|
+
it('encodes then decodes every event type back to an equal event (guards the decoder switch)', () => {
|
|
33
|
+
const attrs = transformFromRally(rally, setId);
|
|
34
|
+
const decoded = transformToRally(attrs);
|
|
35
|
+
expect(decoded.events).toHaveLength(events.length);
|
|
36
|
+
expect(decoded.events.map(e => e.eventType)).toEqual(events.map(e => e.eventType));
|
|
37
|
+
const [lib, sub, serve, rec, set, spike, block, free] = decoded.events;
|
|
38
|
+
expect(lib.playerId).toBe(pA);
|
|
39
|
+
expect(lib.libero).toBe(pB);
|
|
40
|
+
expect(lib.type).toBe(LiberoReplacementTypeEnum.LIBERO_IN);
|
|
41
|
+
expect(sub.playerOut).toBe(pC);
|
|
42
|
+
expect(sub.injury).toBe(true);
|
|
43
|
+
expect(serve.target).toBe(6);
|
|
44
|
+
expect(serve.score).toBe(55);
|
|
45
|
+
expect(serve.type).toBe(ServeTypeEnum.JUMP_TOPSPIN);
|
|
46
|
+
expect(rec.target).toBe(5);
|
|
47
|
+
expect(rec.type).toBe(ReceptionTypeEnum.DIG);
|
|
48
|
+
expect(set.type).toBe(SetTypeEnum.OVERHAND);
|
|
49
|
+
expect(set.tempo).toBe(SetTempoEnum.FIRST);
|
|
50
|
+
expect(spike.type).toBe(SpikeTypeEnum.CROSS_SHOT);
|
|
51
|
+
expect(spike.score).toBe(62.5);
|
|
52
|
+
expect(block.type).toBe(BlockTypeEnum.DOUBLE);
|
|
53
|
+
expect(block.blockers).toEqual([pB, pC]);
|
|
54
|
+
expect(free.playerId).toBe(pC);
|
|
55
|
+
expect(free.target).toBe(6);
|
|
56
|
+
expect(free.score).toBe(45);
|
|
57
|
+
expect(free.type).toBe(FreeBallTypeEnum.OVER);
|
|
58
|
+
expect(free.failure).toBe(FreeBallFailureEnum.NO_FAILURE);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { CourtPosition, Tactics } from '../../service';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
function subModeFromAttributes(d) {
|
|
6
|
-
if (d.mode != null)
|
|
7
|
-
return d.mode;
|
|
8
|
-
if (d.isPinchServer === true)
|
|
9
|
-
return 'PINCH';
|
|
10
|
-
if (d.fatigueBand === 'NEVER')
|
|
11
|
-
return 'NEVER';
|
|
12
|
-
return 'FATIGUE';
|
|
13
|
-
}
|
|
14
|
-
// pinchServer only applies to PINCH ("Conditions") mode. A LEGACY PINCH row (no flag) defaults to true so
|
|
15
|
-
// existing pinch servers keep serve-and-return behavior; a new conditional sub persists an explicit false. This
|
|
16
|
-
// default must resolve identically on every READ path (both transformToObject and tacticsColumnsToApiDto).
|
|
17
|
-
function effectivePinchServer(mode, pinchServer) {
|
|
18
|
-
return mode === 'PINCH' ? (pinchServer ?? true) : undefined;
|
|
19
|
-
}
|
|
1
|
+
import { convertLegacyLiberoSubConfig, convertLegacySetSubConfig, CourtPosition, Tactics } from '../../service';
|
|
2
|
+
// Unified condition engine (owner 2026-07-11): the READ path passes legacy fields (mode / fatigueBand /
|
|
3
|
+
// isPinchServer) through RAW; the schema preprocess in Tactics.create converts every legacy shape (see
|
|
4
|
+
// service/team/sub-config-convert.ts). The WRITE path persists only the unified shape.
|
|
20
5
|
function findPlayer(id, roster) {
|
|
21
6
|
const player = roster.find((p) => p.id === id);
|
|
22
7
|
if (player == null)
|
|
@@ -60,23 +45,19 @@ function transformToAttributes(tactics, teamId) {
|
|
|
60
45
|
designated_subs: tactics.designatedSubs.map(ds => ({
|
|
61
46
|
starterId: ds.starter.id,
|
|
62
47
|
benchId: ds.bench?.id,
|
|
63
|
-
mode: ds.mode,
|
|
64
|
-
fatigueBand: ds.fatigueBand,
|
|
65
|
-
benchFatigueBand: ds.benchFatigueBand,
|
|
66
48
|
conditions: ds.conditions,
|
|
67
49
|
conditionLogic: ds.conditionLogic,
|
|
68
50
|
pinchServer: ds.pinchServer,
|
|
51
|
+
benchFatigueBand: ds.benchFatigueBand,
|
|
69
52
|
subBackMode: ds.subBackMode,
|
|
70
53
|
subBackConditions: ds.subBackConditions,
|
|
71
54
|
subBackConditionLogic: ds.subBackConditionLogic,
|
|
72
55
|
sets: ds.sets?.map(s => ({
|
|
73
|
-
mode: s.mode,
|
|
74
56
|
benchId: s.bench?.id,
|
|
75
|
-
fatigueBand: s.fatigueBand,
|
|
76
|
-
benchFatigueBand: s.benchFatigueBand,
|
|
77
57
|
conditions: s.conditions,
|
|
78
58
|
conditionLogic: s.conditionLogic,
|
|
79
59
|
pinchServer: s.pinchServer,
|
|
60
|
+
benchFatigueBand: s.benchFatigueBand,
|
|
80
61
|
subBackMode: s.subBackMode,
|
|
81
62
|
subBackConditions: s.subBackConditions,
|
|
82
63
|
subBackConditionLogic: s.subBackConditionLogic
|
|
@@ -86,7 +67,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
86
67
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
87
68
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
88
69
|
rotation: p.rotation,
|
|
89
|
-
|
|
70
|
+
weights: p.weights,
|
|
90
71
|
tempos: p.tempos
|
|
91
72
|
})),
|
|
92
73
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -95,9 +76,11 @@ function transformToAttributes(tactics, teamId) {
|
|
|
95
76
|
})),
|
|
96
77
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
97
78
|
rotation: p.rotation,
|
|
98
|
-
|
|
79
|
+
weights: p.weights,
|
|
99
80
|
tempos: p.tempos
|
|
100
81
|
}))),
|
|
82
|
+
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
83
|
+
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
101
84
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
102
85
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
103
86
|
starterId: ir.starter.id,
|
|
@@ -157,15 +140,19 @@ function transformToObject(model, roster) {
|
|
|
157
140
|
secondLibero,
|
|
158
141
|
liberoSub: secondLibero != null ? (model.libero_sub ?? undefined) : undefined,
|
|
159
142
|
liberoReplacementSets,
|
|
143
|
+
// Legacy fields (mode / fatigueBand / isPinchServer) pass through RAW: the DesignatedSubSchema preprocess
|
|
144
|
+
// inside Tactics.create converts every legacy shape (mode-era AND isPinchServer-era rows) to the unified
|
|
145
|
+
// condition engine. Only the player references are resolved here.
|
|
160
146
|
designatedSubs: (model.designated_subs ?? []).map(d => ({
|
|
161
147
|
starter: findPlayer(d.starterId, roster),
|
|
162
148
|
bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
|
|
163
|
-
mode:
|
|
164
|
-
|
|
149
|
+
mode: d.mode,
|
|
150
|
+
isPinchServer: d.isPinchServer,
|
|
151
|
+
fatigueBand: d.fatigueBand,
|
|
165
152
|
benchFatigueBand: d.benchFatigueBand,
|
|
166
153
|
conditions: d.conditions,
|
|
167
154
|
conditionLogic: d.conditionLogic,
|
|
168
|
-
pinchServer:
|
|
155
|
+
pinchServer: d.pinchServer,
|
|
169
156
|
subBackMode: d.subBackMode,
|
|
170
157
|
subBackConditions: d.subBackConditions,
|
|
171
158
|
subBackConditionLogic: d.subBackConditionLogic,
|
|
@@ -176,7 +163,7 @@ function transformToObject(model, roster) {
|
|
|
176
163
|
benchFatigueBand: s.benchFatigueBand,
|
|
177
164
|
conditions: s.conditions,
|
|
178
165
|
conditionLogic: s.conditionLogic,
|
|
179
|
-
pinchServer:
|
|
166
|
+
pinchServer: s.pinchServer,
|
|
180
167
|
subBackMode: s.subBackMode,
|
|
181
168
|
subBackConditions: s.subBackConditions,
|
|
182
169
|
subBackConditionLogic: s.subBackConditionLogic
|
|
@@ -186,7 +173,7 @@ function transformToObject(model, roster) {
|
|
|
186
173
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
187
174
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
188
175
|
rotation: p.rotation,
|
|
189
|
-
|
|
176
|
+
weights: p.weights,
|
|
190
177
|
tempos: p.tempos
|
|
191
178
|
})),
|
|
192
179
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -195,9 +182,11 @@ function transformToObject(model, roster) {
|
|
|
195
182
|
})),
|
|
196
183
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
197
184
|
rotation: p.rotation,
|
|
198
|
-
|
|
185
|
+
weights: p.weights,
|
|
199
186
|
tempos: p.tempos
|
|
200
187
|
}))),
|
|
188
|
+
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
189
|
+
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
201
190
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
202
191
|
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
203
192
|
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
@@ -322,38 +311,27 @@ function tacticsColumnsToApiDto(model) {
|
|
|
322
311
|
receiveRotationOffset: model.receive_rotation_offset,
|
|
323
312
|
substitutionBand: model.substitution_band,
|
|
324
313
|
secondLibero: validSecondLibero,
|
|
325
|
-
|
|
314
|
+
// The API surfaces the UNIFIED condition-engine shape: legacy rows (mode-era / isPinchServer-era) convert
|
|
315
|
+
// here via the same service converters the schema preprocess uses, so clients never see a mode.
|
|
316
|
+
liberoSub: validSecondLibero != null
|
|
317
|
+
? (model.libero_sub != null ? convertLegacyLiberoSubConfig(model.libero_sub) : undefined)
|
|
318
|
+
: undefined,
|
|
326
319
|
liberoReplacementSets: validLiberoReplacementSets,
|
|
327
|
-
designatedSubs: (model.designated_subs ?? []).map(d =>
|
|
328
|
-
starterId
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
|
|
336
|
-
subBackMode: d.subBackMode,
|
|
337
|
-
subBackConditions: d.subBackConditions,
|
|
338
|
-
subBackConditionLogic: d.subBackConditionLogic,
|
|
339
|
-
sets: d.sets?.map(s => ({
|
|
340
|
-
mode: s.mode,
|
|
341
|
-
benchId: s.benchId,
|
|
342
|
-
fatigueBand: s.fatigueBand,
|
|
343
|
-
benchFatigueBand: s.benchFatigueBand,
|
|
344
|
-
conditions: s.conditions,
|
|
345
|
-
conditionLogic: s.conditionLogic,
|
|
346
|
-
pinchServer: effectivePinchServer(s.mode, s.pinchServer),
|
|
347
|
-
subBackMode: s.subBackMode,
|
|
348
|
-
subBackConditions: s.subBackConditions,
|
|
349
|
-
subBackConditionLogic: s.subBackConditionLogic
|
|
350
|
-
}))
|
|
351
|
-
})),
|
|
320
|
+
designatedSubs: (model.designated_subs ?? []).map(d => {
|
|
321
|
+
const { starterId, sets, ...cfg } = d;
|
|
322
|
+
return {
|
|
323
|
+
...convertLegacySetSubConfig(cfg),
|
|
324
|
+
starterId,
|
|
325
|
+
sets: sets?.map(s => convertLegacySetSubConfig(s))
|
|
326
|
+
};
|
|
327
|
+
}),
|
|
352
328
|
rotationSystem: model.rotation_system,
|
|
353
329
|
designatedSetters: model.designated_setters,
|
|
354
330
|
offensivePreferences: model.offensive_preferences,
|
|
355
331
|
systemSets: model.system_sets,
|
|
356
332
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
333
|
+
blockingPreferences: model.blocking_preferences,
|
|
334
|
+
blockingPreferenceSets: model.blocking_preference_sets,
|
|
357
335
|
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
358
336
|
// libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
|
|
359
337
|
// survive; an empty map surfaces as absent (missing key = ON).
|
|
@@ -459,6 +437,8 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
459
437
|
offensive_preferences: config.offensivePreferences,
|
|
460
438
|
system_sets: config.systemSets,
|
|
461
439
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
440
|
+
blocking_preferences: config.blockingPreferences ?? [],
|
|
441
|
+
blocking_preference_sets: config.blockingPreferenceSets,
|
|
462
442
|
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
463
443
|
injury_replacements: config.injuryReplacements,
|
|
464
444
|
libero_injury: config.liberoInjury
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { tacticsColumnsToApiDto, transformFromTactics, transformToTactics } from './tactics';
|
|
3
|
-
import { CourtPosition, EnergyBand, RotationSystemEnum, Tactics } from '../../service';
|
|
3
|
+
import { CourtPosition, EnergyBand, RoleEnum, RotationSystemEnum, Tactics } from '../../service';
|
|
4
4
|
import { makeCountry, makePlayer } from '../../service/test-helpers';
|
|
5
5
|
// Minimal TacticsModel-shaped object; cast through unknown since the transformer only reads columns.
|
|
6
6
|
function makeTactics(designatedSubs) {
|
|
@@ -24,7 +24,8 @@ function makeTactics(designatedSubs) {
|
|
|
24
24
|
libero_replacement_sets: undefined,
|
|
25
25
|
rotation_system: '5-1',
|
|
26
26
|
designated_setters: ['lf'],
|
|
27
|
-
offensive_preferences: [{ rotation: 1,
|
|
27
|
+
offensive_preferences: [{ rotation: 1, weights: { lf: 60, rf: 40 } }],
|
|
28
|
+
blocking_preferences: []
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
describe('tacticsColumnsToApiDto()', () => {
|
|
@@ -35,27 +36,30 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
35
36
|
expect(dto.rotationSystem).toBe('5-1');
|
|
36
37
|
expect(dto.liberoReplacements).toEqual(['lf']);
|
|
37
38
|
expect(dto.lineup.bench).toEqual(['b1', 'b2']);
|
|
38
|
-
expect(dto.offensivePreferences).toEqual([{ rotation: 1,
|
|
39
|
+
expect(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
|
|
39
40
|
});
|
|
40
|
-
it(
|
|
41
|
+
it('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
|
|
41
42
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
42
43
|
{ starterId: 's1', benchId: 'b1', isPinchServer: true, conditions: [{ type: 'ASAP' }] }
|
|
43
44
|
]));
|
|
44
|
-
expect(dto.designatedSubs[0].
|
|
45
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(true);
|
|
46
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'ASAP' }]);
|
|
47
|
+
expect(dto.designatedSubs[0].mode).toBeUndefined();
|
|
45
48
|
});
|
|
46
|
-
it("
|
|
49
|
+
it("converts a legacy 'NEVER' fatigueBand to an explicit empty condition list", () => {
|
|
47
50
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
48
51
|
{ starterId: 's1', fatigueBand: 'NEVER' }
|
|
49
52
|
]));
|
|
50
|
-
expect(dto.designatedSubs[0].
|
|
53
|
+
expect(dto.designatedSubs[0].conditions).toEqual([]);
|
|
51
54
|
expect(dto.designatedSubs[0].fatigueBand).toBeUndefined();
|
|
52
55
|
});
|
|
53
|
-
it('
|
|
56
|
+
it('converts a legacy fatigue entry to a FATIGUE condition carrying its band', () => {
|
|
54
57
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
55
58
|
{ starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: EnergyBand.WORN, benchFatigueBand: EnergyBand.TIRED }
|
|
56
59
|
]));
|
|
57
|
-
expect(dto.designatedSubs[0].
|
|
58
|
-
expect(dto.designatedSubs[0].
|
|
60
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: EnergyBand.WORN }]);
|
|
61
|
+
expect(dto.designatedSubs[0].subBackMode).toBe('FATIGUE');
|
|
62
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(false);
|
|
59
63
|
expect(dto.designatedSubs[0].benchFatigueBand).toBe(EnergyBand.TIRED);
|
|
60
64
|
});
|
|
61
65
|
it('defaults a legacy PINCH row (no pinchServer flag) to pinchServer true', () => {
|
|
@@ -64,11 +68,12 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
64
68
|
]));
|
|
65
69
|
expect(dto.designatedSubs[0].pinchServer).toBe(true);
|
|
66
70
|
});
|
|
67
|
-
it('
|
|
71
|
+
it('passes an already-unified entry through untouched', () => {
|
|
68
72
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
69
|
-
{ starterId: 's1', benchId: 'b1',
|
|
73
|
+
{ starterId: 's1', benchId: 'b1', conditions: [{ type: 'FATIGUE', band: EnergyBand.TIRED }], conditionLogic: 'ALL', pinchServer: false, subBackMode: 'FATIGUE' }
|
|
70
74
|
]));
|
|
71
|
-
expect(dto.designatedSubs[0].
|
|
75
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: EnergyBand.TIRED }]);
|
|
76
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(false);
|
|
72
77
|
});
|
|
73
78
|
it('passes a conditional regular sub (pinchServer false + condition sub-back) through unchanged', () => {
|
|
74
79
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
@@ -105,7 +110,8 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
105
110
|
model.libero_sub = { mode: 'FATIGUE' };
|
|
106
111
|
const dto = tacticsColumnsToApiDto(model);
|
|
107
112
|
expect(dto.secondLibero).toBe('reserve1');
|
|
108
|
-
|
|
113
|
+
// Legacy FATIGUE libero rule converts to the unified condition shape on the API read path.
|
|
114
|
+
expect(dto.liberoSub).toEqual({ conditions: [{ type: 'FATIGUE' }], conditionLogic: 'ALL' });
|
|
109
115
|
});
|
|
110
116
|
it('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
|
|
111
117
|
const model = makeTactics([]);
|
|
@@ -125,10 +131,10 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
125
131
|
it('passes per-set tactics columns through to the API DTO shape', () => {
|
|
126
132
|
const model = makeTactics([]);
|
|
127
133
|
model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
128
|
-
model.offensive_preference_sets = [[{ rotation: 1,
|
|
134
|
+
model.offensive_preference_sets = [[{ rotation: 1, weights: { rf: 55, mf: 45 } }]];
|
|
129
135
|
const dto = tacticsColumnsToApiDto(model);
|
|
130
136
|
expect(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
|
|
131
|
-
expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1,
|
|
137
|
+
expect(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, weights: { rf: 55, mf: 45 } }]]);
|
|
132
138
|
});
|
|
133
139
|
it('leaves the per-set DTO fields undefined when the columns are absent', () => {
|
|
134
140
|
const dto = tacticsColumnsToApiDto(makeTactics([]));
|
|
@@ -152,7 +158,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
152
158
|
{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
|
|
153
159
|
{ rotationSystem: RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
|
|
154
160
|
],
|
|
155
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
161
|
+
offensivePreferenceSets: [[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]
|
|
156
162
|
});
|
|
157
163
|
it('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
|
|
158
164
|
const attrs = transformFromTactics(tactics, 'team-1');
|
|
@@ -160,7 +166,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
160
166
|
{ rotationSystem: '5-1', designatedSetters: [p1.id] },
|
|
161
167
|
{ rotationSystem: '6-0', designatedSetters: [] }
|
|
162
168
|
]);
|
|
163
|
-
expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1,
|
|
169
|
+
expect(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 } }]]);
|
|
164
170
|
});
|
|
165
171
|
it('transformToTactics resolves the stored ids back to roster players', () => {
|
|
166
172
|
const attrs = transformFromTactics(tactics, 'team-1');
|
|
@@ -168,7 +174,7 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
168
174
|
expect(back.systemSets?.[0].rotationSystem).toBe(RotationSystemEnum.FIVE_ONE);
|
|
169
175
|
expect(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
|
|
170
176
|
expect(back.systemSets?.[1].designatedSetters).toEqual([]);
|
|
171
|
-
expect(back.offensivePreferenceSets?.[0][0].
|
|
177
|
+
expect(back.offensivePreferenceSets?.[0][0].weights).toEqual({ [p2.id]: 60, [p3.id]: 40 });
|
|
172
178
|
});
|
|
173
179
|
it('omits the per-set columns entirely for an all-sets config', () => {
|
|
174
180
|
const flat = Tactics.create({
|
|
@@ -186,15 +192,28 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
186
192
|
const withTempos = Tactics.create({
|
|
187
193
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
188
194
|
liberoReplacements: [],
|
|
189
|
-
offensivePreferences: [{ rotation: 1,
|
|
195
|
+
offensivePreferences: [{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
190
196
|
});
|
|
191
197
|
const attrs = transformFromTactics(withTempos, 'team-1');
|
|
192
198
|
expect(attrs.offensive_preferences).toEqual([
|
|
193
|
-
{ rotation: 1,
|
|
199
|
+
{ rotation: 1, weights: { [p2.id]: 60, [p3.id]: 40 }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
|
|
194
200
|
]);
|
|
195
201
|
const back = transformToTactics(attrs, roster);
|
|
196
202
|
expect(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
197
203
|
});
|
|
204
|
+
it('round-trips blocking preferences as id-keyed function maps', () => {
|
|
205
|
+
const withBlocking = Tactics.create({
|
|
206
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
207
|
+
liberoReplacements: [],
|
|
208
|
+
blockingPreferences: [{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }]
|
|
209
|
+
});
|
|
210
|
+
const attrs = transformFromTactics(withBlocking, 'team-1');
|
|
211
|
+
expect(attrs.blocking_preferences).toEqual([
|
|
212
|
+
{ rotation: 1, targets: { [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER } }
|
|
213
|
+
]);
|
|
214
|
+
const back = transformToTactics(attrs, roster);
|
|
215
|
+
expect(back.blockingPreferences[0].targets).toEqual({ [p4.id]: RoleEnum.OUTSIDE_HITTER, [p3.id]: RoleEnum.MIDDLE_BLOCKER });
|
|
216
|
+
});
|
|
198
217
|
});
|
|
199
218
|
describe('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
200
219
|
const country = makeCountry();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { Player } from '../player';
|
|
2
|
-
import { EnergyBand
|
|
2
|
+
import { EnergyBand } from './energy-band';
|
|
3
3
|
import { ConditionLogic, PinchCondition } from './pinch-condition';
|
|
4
4
|
export type SubBackMode = 'FATIGUE' | 'CONDITIONS';
|
|
5
5
|
export interface SetSubConfig {
|
|
6
|
-
readonly mode: SubMode;
|
|
7
6
|
readonly bench?: Player;
|
|
8
|
-
readonly fatigueBand?: EnergyBand;
|
|
9
|
-
readonly benchFatigueBand?: EnergyBand;
|
|
10
7
|
readonly conditions?: PinchCondition[];
|
|
11
8
|
readonly conditionLogic?: ConditionLogic;
|
|
12
9
|
readonly pinchServer?: boolean;
|
|
10
|
+
readonly benchFatigueBand?: EnergyBand;
|
|
13
11
|
readonly subBackMode?: SubBackMode;
|
|
14
12
|
readonly subBackConditions?: PinchCondition[];
|
|
15
13
|
readonly subBackConditionLogic?: ConditionLogic;
|
|
@@ -7,9 +7,11 @@ export * from './energy-band';
|
|
|
7
7
|
export * from './pinch-condition';
|
|
8
8
|
export * from './designated-sub';
|
|
9
9
|
export * from './libero-sub';
|
|
10
|
+
export * from './sub-config-convert';
|
|
10
11
|
export * from './rotation-system';
|
|
11
12
|
export * from './lineup-function';
|
|
12
13
|
export * from './base-position';
|
|
13
14
|
export * from './offensive-preference';
|
|
15
|
+
export * from './blocking-preference';
|
|
14
16
|
export * from './injury-replacement';
|
|
15
17
|
export * from './libero-injury';
|