volleyballsimtypes 0.0.469 → 0.0.471
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 +1 -1
- package/dist/cjs/src/data/models/tactics.d.ts +3 -3
- package/dist/cjs/src/data/models/tactics.js +3 -4
- package/dist/cjs/src/data/transformers/lineup-preset.js +4 -7
- package/dist/cjs/src/data/transformers/lineup-preset.test.js +9 -7
- package/dist/cjs/src/data/transformers/rally-event-injury.test.js +52 -0
- package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event.js +24 -5
- package/dist/cjs/src/data/transformers/tactics.js +28 -4
- package/dist/cjs/src/service/event/block.js +2 -2
- package/dist/cjs/src/service/event/in-play-event.d.ts +7 -1
- package/dist/cjs/src/service/event/in-play-event.js +2 -1
- package/dist/cjs/src/service/event/reception.js +2 -2
- package/dist/cjs/src/service/event/schemas/block.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/block.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/incident.z.d.ts +9 -0
- package/dist/cjs/src/service/event/schemas/incident.z.js +11 -0
- package/dist/cjs/src/service/event/schemas/reception.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/reception.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/serve.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/serve.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/set.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/set.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/spike.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
- package/dist/cjs/src/service/event/serve.js +2 -2
- package/dist/cjs/src/service/event/set.js +2 -2
- package/dist/cjs/src/service/event/spike.js +2 -2
- package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +28 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +1 -1
- package/dist/cjs/src/service/team/schemas/tactics.z.js +12 -4
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -1
- package/dist/cjs/src/service/team/tactics.d.ts +2 -2
- package/dist/cjs/src/service/team/tactics.js +2 -2
- package/dist/esm/src/api/index.d.ts +1 -1
- package/dist/esm/src/data/models/tactics.d.ts +3 -3
- package/dist/esm/src/data/models/tactics.js +3 -4
- package/dist/esm/src/data/transformers/lineup-preset.js +4 -7
- package/dist/esm/src/data/transformers/lineup-preset.test.js +9 -7
- package/dist/esm/src/data/transformers/rally-event-injury.test.js +54 -2
- package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event.js +24 -5
- package/dist/esm/src/data/transformers/tactics.js +28 -4
- package/dist/esm/src/service/event/block.js +2 -2
- package/dist/esm/src/service/event/in-play-event.d.ts +7 -1
- package/dist/esm/src/service/event/in-play-event.js +2 -1
- package/dist/esm/src/service/event/reception.js +2 -2
- package/dist/esm/src/service/event/schemas/block.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/block.z.js +3 -1
- package/dist/esm/src/service/event/schemas/incident.z.d.ts +9 -0
- package/dist/esm/src/service/event/schemas/incident.z.js +8 -0
- package/dist/esm/src/service/event/schemas/reception.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/reception.z.js +3 -1
- package/dist/esm/src/service/event/schemas/serve.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/serve.z.js +3 -1
- package/dist/esm/src/service/event/schemas/set.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/set.z.js +3 -1
- package/dist/esm/src/service/event/schemas/spike.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
- package/dist/esm/src/service/event/serve.js +2 -2
- package/dist/esm/src/service/event/set.js +2 -2
- package/dist/esm/src/service/event/spike.js +2 -2
- package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +28 -2
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +1 -1
- package/dist/esm/src/service/team/schemas/tactics.z.js +12 -4
- package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -1
- package/dist/esm/src/service/team/tactics.d.ts +2 -2
- package/dist/esm/src/service/team/tactics.js +2 -2
- package/package.json +1 -1
|
@@ -132,7 +132,7 @@ export interface Tactics {
|
|
|
132
132
|
offensivePreferences: ApiOffensivePreference[];
|
|
133
133
|
systemSets?: ApiSystemSet[];
|
|
134
134
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
135
|
-
|
|
135
|
+
replaceKnockedImmediately?: Record<string, boolean>;
|
|
136
136
|
injuryReplacements?: ApiInjuryReplacement[];
|
|
137
137
|
}
|
|
138
138
|
export interface ApiLineupPreset {
|
|
@@ -66,12 +66,12 @@ export interface TacticsAttributes {
|
|
|
66
66
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
67
67
|
system_sets?: SystemSetAttributes[];
|
|
68
68
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
69
|
-
|
|
69
|
+
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
70
70
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
71
71
|
}
|
|
72
72
|
export type TacticsPk = 'team_id';
|
|
73
73
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
74
|
-
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' | '
|
|
74
|
+
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';
|
|
75
75
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
76
76
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
77
77
|
team_id: string;
|
|
@@ -88,7 +88,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
88
88
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
89
89
|
system_sets?: SystemSetAttributes[];
|
|
90
90
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
91
|
-
|
|
91
|
+
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
92
92
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
93
93
|
team: TeamModel;
|
|
94
94
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
@@ -75,10 +75,9 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
75
75
|
type: sequelize_1.DataTypes.JSONB,
|
|
76
76
|
allowNull: true
|
|
77
77
|
},
|
|
78
|
-
|
|
79
|
-
type: sequelize_1.DataTypes.
|
|
80
|
-
allowNull:
|
|
81
|
-
defaultValue: true
|
|
78
|
+
replace_knocked_immediately: {
|
|
79
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
80
|
+
allowNull: true
|
|
82
81
|
},
|
|
83
82
|
injury_replacements: {
|
|
84
83
|
type: sequelize_1.DataTypes.JSONB,
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.transformToLineupPreset = transformToObject;
|
|
4
4
|
// `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// No injuries heal here anymore: replaceKnockedImmediately is optional (absent = every starter ON), and old
|
|
6
|
+
// configs carrying the superseded replaceInjuredImmediately boolean pass through the spread as a dead key
|
|
7
|
+
// that the save DTO strips (owner 2026-07-05: the old global value is dropped, default ON).
|
|
8
8
|
function transformToObject(model) {
|
|
9
9
|
return {
|
|
10
10
|
presetId: model.preset_id,
|
|
11
11
|
name: model.name,
|
|
12
12
|
isActive: model.is_active,
|
|
13
13
|
orderIndex: model.order_index,
|
|
14
|
-
config:
|
|
15
|
-
...model.config,
|
|
16
|
-
replaceInjuredImmediately: model.config.replaceInjuredImmediately ?? true
|
|
17
|
-
}
|
|
14
|
+
config: model.config
|
|
18
15
|
};
|
|
19
16
|
}
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const globals_1 = require("@jest/globals");
|
|
4
4
|
const lineup_preset_1 = require("./lineup-preset");
|
|
5
5
|
(0, globals_1.describe)('transformToLineupPreset()', () => {
|
|
6
|
-
(0, globals_1.it)('maps preset columns to the API shape
|
|
6
|
+
(0, globals_1.it)('maps preset columns to the API shape verbatim (no injuries heal: absent map = every starter ON)', () => {
|
|
7
7
|
const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
|
|
8
8
|
const preset = (0, lineup_preset_1.transformToLineupPreset)({
|
|
9
9
|
preset_id: 'p1',
|
|
@@ -18,13 +18,15 @@ const lineup_preset_1 = require("./lineup-preset");
|
|
|
18
18
|
name: 'Starters',
|
|
19
19
|
isActive: true,
|
|
20
20
|
orderIndex: 2,
|
|
21
|
-
|
|
22
|
-
// to true (the effective hydration default) so the DTO's required field is honest.
|
|
23
|
-
config: { ...config, replaceInjuredImmediately: true }
|
|
21
|
+
config
|
|
24
22
|
});
|
|
25
23
|
});
|
|
26
|
-
(0, globals_1.it)('keeps an explicit
|
|
27
|
-
const config = {
|
|
24
|
+
(0, globals_1.it)('keeps an explicit per-starter replaceKnockedImmediately map', () => {
|
|
25
|
+
const config = {
|
|
26
|
+
lineup: { bench: [] },
|
|
27
|
+
substitutionBand: 'TIRED',
|
|
28
|
+
replaceKnockedImmediately: { 'starter-1': false }
|
|
29
|
+
};
|
|
28
30
|
const preset = (0, lineup_preset_1.transformToLineupPreset)({
|
|
29
31
|
preset_id: 'p2',
|
|
30
32
|
team_id: 't1',
|
|
@@ -33,6 +35,6 @@ const lineup_preset_1 = require("./lineup-preset");
|
|
|
33
35
|
order_index: 3,
|
|
34
36
|
config
|
|
35
37
|
});
|
|
36
|
-
(0, globals_1.expect)(preset.config.
|
|
38
|
+
(0, globals_1.expect)(preset.config.replaceKnockedImmediately).toEqual({ 'starter-1': false });
|
|
37
39
|
});
|
|
38
40
|
});
|
|
@@ -31,3 +31,55 @@ const rally_event_1 = require("./rally-event");
|
|
|
31
31
|
(0, globals_1.expect)(decoded.injury).toBeUndefined();
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
+
(0, globals_1.describe)('rally-event compact round trip — incident marker on the causing event', () => {
|
|
35
|
+
const playerId = (0, uuid_1.v4)();
|
|
36
|
+
const roster = [playerId];
|
|
37
|
+
const playerIndex = new Map(roster.map((id, i) => [id, i]));
|
|
38
|
+
function makeSpike(incident) {
|
|
39
|
+
return service_1.Spike.create({
|
|
40
|
+
playerId,
|
|
41
|
+
score: 62.5,
|
|
42
|
+
target: 4,
|
|
43
|
+
failure: service_1.SpikeFailureEnum.NO_FAILURE,
|
|
44
|
+
type: service_1.SpikeTypeEnum.CROSS_SHOT,
|
|
45
|
+
...(incident != null ? { incident } : {})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
(0, globals_1.it)('packs an INJURY incident as i = severity (1-4) and round-trips kind + severity', () => {
|
|
49
|
+
const compact = (0, rally_event_1.transformToCompact)(makeSpike({ kind: 'INJURY', severity: 4 }), playerIndex);
|
|
50
|
+
(0, globals_1.expect)(compact.i).toBe(4);
|
|
51
|
+
const decoded = (0, rally_event_1.transformToSpike)(compact, roster);
|
|
52
|
+
(0, globals_1.expect)(decoded.incident).toEqual({ kind: 'INJURY', severity: 4 });
|
|
53
|
+
});
|
|
54
|
+
(0, globals_1.it)('packs a KNOCK incident as i = 10 + tier and round-trips kind + severity', () => {
|
|
55
|
+
const serve = service_1.Serve.create({
|
|
56
|
+
playerId,
|
|
57
|
+
score: 55,
|
|
58
|
+
target: 6,
|
|
59
|
+
failure: service_1.ServeFailureEnum.NO_FAILURE,
|
|
60
|
+
type: service_1.ServeTypeEnum.JUMP_TOPSPIN,
|
|
61
|
+
incident: { kind: 'KNOCK', severity: 2 }
|
|
62
|
+
});
|
|
63
|
+
const compact = (0, rally_event_1.transformToCompact)(serve, playerIndex);
|
|
64
|
+
(0, globals_1.expect)(compact.i).toBe(12);
|
|
65
|
+
const decoded = (0, rally_event_1.transformToServe)(compact, roster);
|
|
66
|
+
(0, globals_1.expect)(decoded.incident).toEqual({ kind: 'KNOCK', severity: 2 });
|
|
67
|
+
});
|
|
68
|
+
(0, globals_1.it)('omits i entirely for an event without an incident', () => {
|
|
69
|
+
const compact = (0, rally_event_1.transformToCompact)(makeSpike(), playerIndex);
|
|
70
|
+
(0, globals_1.expect)('i' in compact).toBe(false);
|
|
71
|
+
const decoded = (0, rally_event_1.transformToSpike)(compact, roster);
|
|
72
|
+
(0, globals_1.expect)(decoded.incident).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
(0, globals_1.it)('decodes a legacy compact event (no i key) with no incident', () => {
|
|
75
|
+
const legacy = { p: 0, e: service_1.EventTypeEnum.SPIKE, f: 0, t: 0, a: 4, s: 50 };
|
|
76
|
+
const decoded = (0, rally_event_1.transformToSpike)(legacy, roster);
|
|
77
|
+
(0, globals_1.expect)(decoded.incident).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
(0, globals_1.it)('rejects an out-of-range or malformed incident at the schema', () => {
|
|
80
|
+
(0, globals_1.expect)(() => makeSpike({ kind: 'INJURY', severity: 5 })).toThrow(/INVALID_SPIKE/);
|
|
81
|
+
(0, globals_1.expect)(() => makeSpike({ kind: 'INJURY', severity: 0 })).toThrow(/INVALID_SPIKE/);
|
|
82
|
+
(0, globals_1.expect)(() => makeSpike({ kind: 'KNOCK', severity: 4 })).toThrow(/INVALID_SPIKE/);
|
|
83
|
+
(0, globals_1.expect)(() => makeSpike({ kind: 'BRUISE', severity: 1 })).toThrow(/INVALID_SPIKE/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -10,6 +10,7 @@ export interface CompactEvent {
|
|
|
10
10
|
l?: number;
|
|
11
11
|
o?: number;
|
|
12
12
|
j?: number;
|
|
13
|
+
i?: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
|
|
15
16
|
export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
|
|
@@ -9,6 +9,17 @@ exports.transformToSet = transformToSet;
|
|
|
9
9
|
exports.transformToSpike = transformToSpike;
|
|
10
10
|
exports.transformToSubstitution = transformToSubstitution;
|
|
11
11
|
const service_1 = require("../../service");
|
|
12
|
+
// Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
|
|
13
|
+
function incidentToCompact(incident) {
|
|
14
|
+
if (incident == null)
|
|
15
|
+
return undefined;
|
|
16
|
+
return incident.kind === 'KNOCK' ? 10 + incident.severity : incident.severity;
|
|
17
|
+
}
|
|
18
|
+
function incidentFromCompact(i) {
|
|
19
|
+
if (i == null)
|
|
20
|
+
return undefined;
|
|
21
|
+
return i >= 10 ? { kind: 'KNOCK', severity: i - 10 } : { kind: 'INJURY', severity: i };
|
|
22
|
+
}
|
|
12
23
|
function transformToCompact(evt, playerIndex) {
|
|
13
24
|
const out = {
|
|
14
25
|
p: playerIndex.get(evt.playerId),
|
|
@@ -19,6 +30,9 @@ function transformToCompact(evt, playerIndex) {
|
|
|
19
30
|
out.t = evt.type;
|
|
20
31
|
out.a = evt.target;
|
|
21
32
|
out.s = evt.score;
|
|
33
|
+
const i = incidentToCompact(evt.incident);
|
|
34
|
+
if (i != null)
|
|
35
|
+
out.i = i;
|
|
22
36
|
if (evt instanceof service_1.Block) {
|
|
23
37
|
out.b = evt.blockers.map(id => playerIndex.get(id));
|
|
24
38
|
}
|
|
@@ -41,7 +55,8 @@ function transformToBlock(event, roster) {
|
|
|
41
55
|
playerId: roster[event.p],
|
|
42
56
|
target: event.a,
|
|
43
57
|
blockers: (event.b ?? []).map(i => roster[i]),
|
|
44
|
-
score: event.s
|
|
58
|
+
score: event.s,
|
|
59
|
+
incident: incidentFromCompact(event.i)
|
|
45
60
|
});
|
|
46
61
|
}
|
|
47
62
|
function transformToLiberoReplacement(event, roster) {
|
|
@@ -57,7 +72,8 @@ function transformToReception(event, roster) {
|
|
|
57
72
|
type: event.t,
|
|
58
73
|
playerId: roster[event.p],
|
|
59
74
|
target: event.a,
|
|
60
|
-
score: event.s
|
|
75
|
+
score: event.s,
|
|
76
|
+
incident: incidentFromCompact(event.i)
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
function transformToServe(event, roster) {
|
|
@@ -66,7 +82,8 @@ function transformToServe(event, roster) {
|
|
|
66
82
|
type: event.t,
|
|
67
83
|
playerId: roster[event.p],
|
|
68
84
|
target: event.a,
|
|
69
|
-
score: event.s
|
|
85
|
+
score: event.s,
|
|
86
|
+
incident: incidentFromCompact(event.i)
|
|
70
87
|
});
|
|
71
88
|
}
|
|
72
89
|
function transformToSet(event, roster) {
|
|
@@ -75,7 +92,8 @@ function transformToSet(event, roster) {
|
|
|
75
92
|
type: event.t,
|
|
76
93
|
playerId: roster[event.p],
|
|
77
94
|
target: event.a,
|
|
78
|
-
score: event.s
|
|
95
|
+
score: event.s,
|
|
96
|
+
incident: incidentFromCompact(event.i)
|
|
79
97
|
});
|
|
80
98
|
}
|
|
81
99
|
function transformToSpike(event, roster) {
|
|
@@ -84,7 +102,8 @@ function transformToSpike(event, roster) {
|
|
|
84
102
|
type: event.t,
|
|
85
103
|
playerId: roster[event.p],
|
|
86
104
|
target: event.a,
|
|
87
|
-
score: event.s
|
|
105
|
+
score: event.s,
|
|
106
|
+
incident: incidentFromCompact(event.i)
|
|
88
107
|
});
|
|
89
108
|
}
|
|
90
109
|
function transformToSubstitution(event, roster) {
|
|
@@ -88,7 +88,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
88
88
|
rotation: p.rotation,
|
|
89
89
|
order: p.order.map((a) => a.id)
|
|
90
90
|
}))),
|
|
91
|
-
|
|
91
|
+
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
92
92
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
93
93
|
starterId: ir.starter.id,
|
|
94
94
|
replacementId: ir.replacement.id,
|
|
@@ -171,7 +171,19 @@ function transformToObject(model, roster) {
|
|
|
171
171
|
rotation: p.rotation,
|
|
172
172
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
173
173
|
}))),
|
|
174
|
-
|
|
174
|
+
// LENIENT: keep only starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
|
|
175
|
+
// value) can never fail schema validation and crash a team load; a dropped key = the ON default.
|
|
176
|
+
replaceKnockedImmediately: (() => {
|
|
177
|
+
const starterIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
|
|
178
|
+
lineup[service_1.CourtPosition.LEFT_BACK], lineup[service_1.CourtPosition.MIDDLE_BACK], lineup[service_1.CourtPosition.RIGHT_BACK]]
|
|
179
|
+
.map((p) => p.id));
|
|
180
|
+
const healed = {};
|
|
181
|
+
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
182
|
+
if (starterIds.has(id) && typeof value === 'boolean')
|
|
183
|
+
healed[id] = value;
|
|
184
|
+
}
|
|
185
|
+
return healed;
|
|
186
|
+
})(),
|
|
175
187
|
// LENIENT (roster.find, not findPlayer): an entry whose starter or replacement is gone/invalid is DROPPED
|
|
176
188
|
// rather than crashing the sim batch (a dropped entry = unconfigured = the bench-only auto fallback, per
|
|
177
189
|
// the owner rules); an invalid benchBackfill drops only the backfill. First occurrence wins on duplicates;
|
|
@@ -270,7 +282,19 @@ function tacticsColumnsToApiDto(model) {
|
|
|
270
282
|
offensivePreferences: model.offensive_preferences,
|
|
271
283
|
systemSets: model.system_sets,
|
|
272
284
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
273
|
-
|
|
285
|
+
// Same stale-data policy as the heals below: only starter-keyed boolean entries survive, and an empty
|
|
286
|
+
// map surfaces as absent (missing key = ON).
|
|
287
|
+
replaceKnockedImmediately: (() => {
|
|
288
|
+
const starterSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
|
|
289
|
+
model.lineup[service_1.CourtPosition.RIGHT_FRONT], model.lineup[service_1.CourtPosition.LEFT_BACK],
|
|
290
|
+
model.lineup[service_1.CourtPosition.MIDDLE_BACK], model.lineup[service_1.CourtPosition.RIGHT_BACK]]);
|
|
291
|
+
const healed = {};
|
|
292
|
+
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
293
|
+
if (starterSlotIds.has(id) && typeof value === 'boolean')
|
|
294
|
+
healed[id] = value;
|
|
295
|
+
}
|
|
296
|
+
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
297
|
+
})(),
|
|
274
298
|
// Same stale-data policy as validSecondLibero above: don't surface entries the service load would drop
|
|
275
299
|
// (starter no longer a starter, replacement now a starter/libero/L2, duplicates), or the UI reloads and
|
|
276
300
|
// re-emits them forever. Roster membership can't be checked here (ids only); the service heal covers that.
|
|
@@ -335,7 +359,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
335
359
|
offensive_preferences: config.offensivePreferences,
|
|
336
360
|
system_sets: config.systemSets,
|
|
337
361
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
338
|
-
|
|
362
|
+
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
339
363
|
injury_replacements: config.injuryReplacements
|
|
340
364
|
};
|
|
341
365
|
}
|
|
@@ -34,8 +34,8 @@ class Block extends in_play_event_1.InPlayEvent {
|
|
|
34
34
|
}
|
|
35
35
|
return new Block(result.data);
|
|
36
36
|
}
|
|
37
|
-
constructor({ score, playerId, target, failure, type, blockers, activeTraits }) {
|
|
38
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
37
|
+
constructor({ score, playerId, target, failure, type, blockers, activeTraits, incident }) {
|
|
38
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
39
39
|
this.type = type;
|
|
40
40
|
this.failure = failure;
|
|
41
41
|
this.blockers = blockers;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { CourtTarget } from '../match';
|
|
2
2
|
import { RallyEvent, RallyEventOpts } from './rally-event';
|
|
3
3
|
import { Trait } from '../player';
|
|
4
|
+
export interface EventIncident {
|
|
5
|
+
readonly kind: 'KNOCK' | 'INJURY';
|
|
6
|
+
readonly severity: number;
|
|
7
|
+
}
|
|
4
8
|
export interface InPlayEventOpts extends RallyEventOpts {
|
|
5
9
|
readonly target: CourtTarget;
|
|
6
10
|
readonly score: number;
|
|
7
11
|
readonly activeTraits: Trait[];
|
|
12
|
+
readonly incident?: EventIncident;
|
|
8
13
|
}
|
|
9
14
|
export declare abstract class InPlayEvent<F, T> extends RallyEvent {
|
|
10
15
|
abstract failure: F;
|
|
@@ -12,6 +17,7 @@ export declare abstract class InPlayEvent<F, T> extends RallyEvent {
|
|
|
12
17
|
readonly target: CourtTarget;
|
|
13
18
|
readonly score: number;
|
|
14
19
|
readonly activeTraits: Trait[];
|
|
15
|
-
|
|
20
|
+
readonly incident?: EventIncident;
|
|
21
|
+
protected constructor({ eventType, playerId, target, score, activeTraits, incident }: InPlayEventOpts);
|
|
16
22
|
protected _toString(): string;
|
|
17
23
|
}
|
|
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.InPlayEvent = void 0;
|
|
4
4
|
const rally_event_1 = require("./rally-event");
|
|
5
5
|
class InPlayEvent extends rally_event_1.RallyEvent {
|
|
6
|
-
constructor({ eventType, playerId, target, score, activeTraits }) {
|
|
6
|
+
constructor({ eventType, playerId, target, score, activeTraits, incident }) {
|
|
7
7
|
super({ playerId, eventType });
|
|
8
8
|
this.target = target;
|
|
9
9
|
this.score = Number(score.toFixed(2));
|
|
10
10
|
this.activeTraits = activeTraits;
|
|
11
|
+
this.incident = incident;
|
|
11
12
|
}
|
|
12
13
|
_toString() {
|
|
13
14
|
const score = `"score":${this.score}`;
|
|
@@ -36,8 +36,8 @@ class Reception extends in_play_event_1.InPlayEvent {
|
|
|
36
36
|
}
|
|
37
37
|
return new Reception(result.data);
|
|
38
38
|
}
|
|
39
|
-
constructor({ score, playerId, target, failure, type, activeTraits }) {
|
|
40
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
39
|
+
constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
|
|
40
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
41
41
|
this.type = type;
|
|
42
42
|
this.failure = failure;
|
|
43
43
|
}
|
|
@@ -27,5 +27,12 @@ export declare const BlockInputSchema: z.ZodObject<{
|
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
29
|
blockers: z.ZodArray<z.ZodUUID>;
|
|
30
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
kind: z.ZodEnum<{
|
|
32
|
+
KNOCK: "KNOCK";
|
|
33
|
+
INJURY: "INJURY";
|
|
34
|
+
}>;
|
|
35
|
+
severity: z.ZodNumber;
|
|
36
|
+
}, z.core.$strip>>;
|
|
30
37
|
}, z.core.$strip>;
|
|
31
38
|
export type BlockInput = z.infer<typeof BlockInputSchema>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BlockInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
+
const incident_z_1 = require("./incident.z");
|
|
6
7
|
const blockTypeValues = [0, 1, 2, 3];
|
|
7
8
|
const blockFailureValues = [0, 1, 2, 3, 4, 5];
|
|
8
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -13,7 +14,8 @@ exports.BlockInputSchema = zod_1.z.object({
|
|
|
13
14
|
activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
|
|
14
15
|
failure: zod_1.z.number().int().refine((v) => blockFailureValues.includes(v), { message: 'INVALID_BLOCK_FAILURE' }),
|
|
15
16
|
type: zod_1.z.number().int().refine((v) => blockTypeValues.includes(v), { message: 'INVALID_BLOCK_TYPE' }),
|
|
16
|
-
blockers: zod_1.z.array(zod_1.z.uuid())
|
|
17
|
+
blockers: zod_1.z.array(zod_1.z.uuid()),
|
|
18
|
+
incident: incident_z_1.EventIncidentSchema.optional()
|
|
17
19
|
}).superRefine((data, ctx) => {
|
|
18
20
|
// The block type IS the blocker count (NO_BLOCKER=0, SINGLE=1, DOUBLE=2, TRIPLE=3), so the two must agree.
|
|
19
21
|
if (data.blockers.length !== data.type) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const EventIncidentSchema: z.ZodObject<{
|
|
3
|
+
kind: z.ZodEnum<{
|
|
4
|
+
KNOCK: "KNOCK";
|
|
5
|
+
INJURY: "INJURY";
|
|
6
|
+
}>;
|
|
7
|
+
severity: z.ZodNumber;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type EventIncidentInput = z.infer<typeof EventIncidentSchema>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventIncidentSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// The moment a player got hurt, attached to the causing in-play event (owner 2026-07-05: EVERY knock and
|
|
6
|
+
// injury marks the action that produced it). KNOCK severity is the knock tier (1-3); INJURY severity is the
|
|
7
|
+
// InjurySeverityEnum value (1-4).
|
|
8
|
+
exports.EventIncidentSchema = zod_1.z.object({
|
|
9
|
+
kind: zod_1.z.enum(['KNOCK', 'INJURY']),
|
|
10
|
+
severity: zod_1.z.number().int().min(1).max(4)
|
|
11
|
+
}).refine(v => v.kind !== 'KNOCK' || v.severity <= 3, { message: 'INVALID_KNOCK_TIER' });
|
|
@@ -26,5 +26,12 @@ export declare const ReceptionInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type ReceptionInput = z.infer<typeof ReceptionInputSchema>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ReceptionInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
+
const incident_z_1 = require("./incident.z");
|
|
6
7
|
const receptionTypeValues = [0, 1, 2, 3, 4];
|
|
7
8
|
const receptionFailureValues = [0, 1, 2, 3, 4];
|
|
8
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -12,5 +13,6 @@ exports.ReceptionInputSchema = zod_1.z.object({
|
|
|
12
13
|
target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
13
14
|
activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
|
|
14
15
|
failure: zod_1.z.number().int().refine((v) => receptionFailureValues.includes(v), { message: 'INVALID_RECEPTION_FAILURE' }),
|
|
15
|
-
type: zod_1.z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' })
|
|
16
|
+
type: zod_1.z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' }),
|
|
17
|
+
incident: incident_z_1.EventIncidentSchema.optional()
|
|
16
18
|
});
|
|
@@ -26,5 +26,12 @@ export declare const ServeInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type ServeInput = z.infer<typeof ServeInputSchema>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ServeInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
+
const incident_z_1 = require("./incident.z");
|
|
6
7
|
const serveTypeValues = [0, 1, 2, 3];
|
|
7
8
|
const serveFailureValues = [0, 1, 2, 3, 4];
|
|
8
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -12,5 +13,6 @@ exports.ServeInputSchema = zod_1.z.object({
|
|
|
12
13
|
target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
13
14
|
activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
|
|
14
15
|
failure: zod_1.z.number().int().refine((v) => serveFailureValues.includes(v), { message: 'INVALID_SERVE_FAILURE' }),
|
|
15
|
-
type: zod_1.z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' })
|
|
16
|
+
type: zod_1.z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' }),
|
|
17
|
+
incident: incident_z_1.EventIncidentSchema.optional()
|
|
16
18
|
});
|
|
@@ -26,5 +26,12 @@ export declare const SetInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type SetInput = z.infer<typeof SetInputSchema>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SetInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
+
const incident_z_1 = require("./incident.z");
|
|
6
7
|
const setTypeValues = [0, 1];
|
|
7
8
|
const setFailureValues = [0, 1, 2, 3, 4];
|
|
8
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -12,5 +13,6 @@ exports.SetInputSchema = zod_1.z.object({
|
|
|
12
13
|
target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
13
14
|
activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
|
|
14
15
|
failure: zod_1.z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
|
|
15
|
-
type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' })
|
|
16
|
+
type: zod_1.z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' }),
|
|
17
|
+
incident: incident_z_1.EventIncidentSchema.optional()
|
|
16
18
|
});
|
|
@@ -26,5 +26,12 @@ export declare const SpikeInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type SpikeInput = z.infer<typeof SpikeInputSchema>;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SpikeInputSchema = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
|
+
const incident_z_1 = require("./incident.z");
|
|
6
7
|
const spikeTypeValues = [0, 1, 2, 3, 4, 5];
|
|
7
8
|
const spikeFailureValues = [0, 1, 2, 3, 4];
|
|
8
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -12,5 +13,6 @@ exports.SpikeInputSchema = zod_1.z.object({
|
|
|
12
13
|
target: zod_1.z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
13
14
|
activeTraits: zod_1.z.array(zod_1.z.enum(Object.values(player_1.TraitEnum))).default([]),
|
|
14
15
|
failure: zod_1.z.number().int().refine((v) => spikeFailureValues.includes(v), { message: 'INVALID_SPIKE_FAILURE' }),
|
|
15
|
-
type: zod_1.z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' })
|
|
16
|
+
type: zod_1.z.number().int().refine((v) => spikeTypeValues.includes(v), { message: 'INVALID_SPIKE_TYPE' }),
|
|
17
|
+
incident: incident_z_1.EventIncidentSchema.optional()
|
|
16
18
|
});
|
|
@@ -33,8 +33,8 @@ class Serve extends in_play_event_1.InPlayEvent {
|
|
|
33
33
|
}
|
|
34
34
|
return new Serve(result.data);
|
|
35
35
|
}
|
|
36
|
-
constructor({ score, playerId, target, failure, type, activeTraits }) {
|
|
37
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
36
|
+
constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
|
|
37
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
38
38
|
this.type = type;
|
|
39
39
|
this.failure = failure;
|
|
40
40
|
}
|
|
@@ -31,8 +31,8 @@ class Set extends in_play_event_1.InPlayEvent {
|
|
|
31
31
|
}
|
|
32
32
|
return new Set(result.data);
|
|
33
33
|
}
|
|
34
|
-
constructor({ score, playerId, target, failure, type, activeTraits }) {
|
|
35
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
34
|
+
constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
|
|
35
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
36
36
|
this.type = type;
|
|
37
37
|
this.failure = failure;
|
|
38
38
|
}
|
|
@@ -35,8 +35,8 @@ class Spike extends in_play_event_1.InPlayEvent {
|
|
|
35
35
|
}
|
|
36
36
|
return new Spike(result.data);
|
|
37
37
|
}
|
|
38
|
-
constructor({ score, playerId, target, failure, type, activeTraits }) {
|
|
39
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
38
|
+
constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
|
|
39
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
40
40
|
this.type = type;
|
|
41
41
|
this.failure = failure;
|
|
42
42
|
}
|
|
@@ -32,14 +32,40 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
32
32
|
...overrides
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
(0, globals_1.it)('defaults to
|
|
35
|
+
(0, globals_1.it)('defaults to an empty knock map / empty replacements', () => {
|
|
36
36
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
|
|
37
37
|
(0, globals_1.expect)(res.success).toBe(true);
|
|
38
38
|
if (res.success) {
|
|
39
|
-
(0, globals_1.expect)(res.data.
|
|
39
|
+
(0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({});
|
|
40
40
|
(0, globals_1.expect)(res.data.injuryReplacements).toEqual([]);
|
|
41
41
|
}
|
|
42
42
|
});
|
|
43
|
+
(0, globals_1.it)('accepts a starter-keyed replaceKnockedImmediately map', () => {
|
|
44
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
45
|
+
replaceKnockedImmediately: { [p1.id]: false, [p3.id]: true }
|
|
46
|
+
}));
|
|
47
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
48
|
+
if (res.success) {
|
|
49
|
+
(0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [p1.id]: false, [p3.id]: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
(0, globals_1.it)('rejects a replaceKnockedImmediately key that is not a court starter', () => {
|
|
53
|
+
for (const outsider of [benchA, libero, reserveA]) {
|
|
54
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
55
|
+
replaceKnockedImmediately: { [outsider.id]: false }
|
|
56
|
+
}));
|
|
57
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
58
|
+
if (!res.success) {
|
|
59
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'REPLACE_KNOCKED_KEY_NOT_A_STARTER')).toBe(true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
(0, globals_1.it)('rejects a non-boolean replaceKnockedImmediately value', () => {
|
|
64
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
65
|
+
replaceKnockedImmediately: { [p1.id]: 'off' }
|
|
66
|
+
}));
|
|
67
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
68
|
+
});
|
|
43
69
|
(0, globals_1.it)('accepts a bench replacement with a reserve backfill', () => {
|
|
44
70
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
45
71
|
injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: reserveA }]
|