volleyballsimtypes 0.0.464 → 0.0.465
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 +5 -4
- package/dist/cjs/src/data/models/tactics.d.ts +7 -6
- package/dist/cjs/src/data/models/tactics.js +1 -1
- package/dist/cjs/src/data/transformers/tactics.js +71 -59
- package/dist/cjs/src/service/team/index.d.ts +1 -1
- package/dist/cjs/src/service/team/index.js +1 -1
- package/dist/cjs/src/service/team/injury-replacement.d.ts +6 -0
- package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +142 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +4 -3
- package/dist/cjs/src/service/team/schemas/tactics.z.js +38 -23
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +4 -3
- package/dist/cjs/src/service/team/tactics.d.ts +3 -3
- package/dist/cjs/src/service/team/tactics.js +2 -2
- package/dist/esm/src/api/index.d.ts +5 -4
- package/dist/esm/src/data/models/tactics.d.ts +7 -6
- package/dist/esm/src/data/models/tactics.js +1 -1
- package/dist/esm/src/data/transformers/tactics.js +71 -59
- package/dist/esm/src/service/team/index.d.ts +1 -1
- package/dist/esm/src/service/team/index.js +1 -1
- package/dist/esm/src/service/team/injury-replacement.d.ts +6 -0
- package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +140 -0
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +4 -3
- package/dist/esm/src/service/team/schemas/tactics.z.js +38 -23
- package/dist/esm/src/service/team/schemas/team.z.d.ts +4 -3
- package/dist/esm/src/service/team/tactics.d.ts +3 -3
- package/dist/esm/src/service/team/tactics.js +2 -2
- package/package.json +1 -1
- package/dist/cjs/src/service/team/reserve-priority.d.ts +0 -6
- package/dist/cjs/src/service/team/schemas/reserve-priority.z.test.js +0 -125
- package/dist/esm/src/service/team/reserve-priority.d.ts +0 -6
- package/dist/esm/src/service/team/schemas/reserve-priority.z.test.js +0 -123
- /package/dist/cjs/src/service/team/{reserve-priority.js → injury-replacement.js} +0 -0
- /package/dist/cjs/src/service/team/schemas/{reserve-priority.z.test.d.ts → injury-replacement.z.test.d.ts} +0 -0
- /package/dist/esm/src/service/team/{reserve-priority.js → injury-replacement.js} +0 -0
- /package/dist/esm/src/service/team/schemas/{reserve-priority.z.test.d.ts → injury-replacement.z.test.d.ts} +0 -0
|
@@ -105,9 +105,10 @@ export interface ApiSystemSet {
|
|
|
105
105
|
rotationSystem: RotationSystemEnum;
|
|
106
106
|
designatedSetters: string[];
|
|
107
107
|
}
|
|
108
|
-
export interface
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
export interface ApiInjuryReplacement {
|
|
109
|
+
starterId: string;
|
|
110
|
+
replacementId: string;
|
|
111
|
+
benchBackfillId?: string;
|
|
111
112
|
}
|
|
112
113
|
export interface ApiLiberoSetSubConfig {
|
|
113
114
|
mode: LiberoSubMode;
|
|
@@ -131,7 +132,7 @@ export interface Tactics {
|
|
|
131
132
|
systemSets?: ApiSystemSet[];
|
|
132
133
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
133
134
|
replaceInjuredImmediately: boolean;
|
|
134
|
-
|
|
135
|
+
injuryReplacements?: ApiInjuryReplacement[];
|
|
135
136
|
}
|
|
136
137
|
export interface ApiLineupPreset {
|
|
137
138
|
presetId: string;
|
|
@@ -39,9 +39,10 @@ export interface SystemSetAttributes {
|
|
|
39
39
|
rotationSystem: RotationSystemEnum;
|
|
40
40
|
designatedSetters: PlayerId[];
|
|
41
41
|
}
|
|
42
|
-
export interface
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
export interface InjuryReplacementAttributes {
|
|
43
|
+
starterId: PlayerId;
|
|
44
|
+
replacementId: PlayerId;
|
|
45
|
+
benchBackfillId?: PlayerId;
|
|
45
46
|
}
|
|
46
47
|
export interface LiberoSetSubConfigAttributes {
|
|
47
48
|
mode: LiberoSubMode;
|
|
@@ -66,11 +67,11 @@ export interface TacticsAttributes {
|
|
|
66
67
|
system_sets?: SystemSetAttributes[];
|
|
67
68
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
68
69
|
replace_injured_immediately?: boolean;
|
|
69
|
-
|
|
70
|
+
injury_replacements?: InjuryReplacementAttributes[];
|
|
70
71
|
}
|
|
71
72
|
export type TacticsPk = 'team_id';
|
|
72
73
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
73
|
-
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_injured_immediately' | '
|
|
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_injured_immediately' | 'injury_replacements';
|
|
74
75
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
75
76
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
76
77
|
team_id: string;
|
|
@@ -88,7 +89,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
88
89
|
system_sets?: SystemSetAttributes[];
|
|
89
90
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
90
91
|
replace_injured_immediately?: boolean;
|
|
91
|
-
|
|
92
|
+
injury_replacements?: InjuryReplacementAttributes[];
|
|
92
93
|
team: TeamModel;
|
|
93
94
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
94
95
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -89,9 +89,10 @@ function transformToAttributes(tactics, teamId) {
|
|
|
89
89
|
order: p.order.map((a) => a.id)
|
|
90
90
|
}))),
|
|
91
91
|
replace_injured_immediately: tactics.replaceInjuredImmediately,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
93
|
+
starterId: ir.starter.id,
|
|
94
|
+
replacementId: ir.replacement.id,
|
|
95
|
+
benchBackfillId: ir.benchBackfill?.id
|
|
95
96
|
}))
|
|
96
97
|
};
|
|
97
98
|
}
|
|
@@ -171,36 +172,43 @@ function transformToObject(model, roster) {
|
|
|
171
172
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
172
173
|
}))),
|
|
173
174
|
replaceInjuredImmediately: model.replace_injured_immediately ?? true,
|
|
174
|
-
// LENIENT (roster.find, not findPlayer):
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
175
|
+
// LENIENT (roster.find, not findPlayer): an entry whose starter or replacement is gone/invalid is DROPPED
|
|
176
|
+
// rather than crashing the sim batch (a dropped entry = unconfigured = the bench-only auto fallback, per
|
|
177
|
+
// the owner rules); an invalid benchBackfill drops only the backfill. First occurrence wins on duplicates;
|
|
178
|
+
// the next tactics save rewrites cleanly. Schema invariants mirrored: starter must be a court starter,
|
|
179
|
+
// replacement bench-or-reserve (never a starter/libero/L2), backfill only-with-bench-replacement + reserve.
|
|
180
|
+
injuryReplacements: (() => {
|
|
181
|
+
const starterIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
|
|
182
|
+
lineup[service_1.CourtPosition.LEFT_BACK], lineup[service_1.CourtPosition.MIDDLE_BACK], lineup[service_1.CourtPosition.RIGHT_BACK]]
|
|
183
|
+
.map((p) => p.id));
|
|
184
|
+
const benchIds = new Set(lineup.bench.map((p) => p.id));
|
|
185
|
+
const seenStarters = new Set();
|
|
186
|
+
const seenIncoming = new Set();
|
|
187
|
+
const healed = [];
|
|
188
|
+
for (const ir of model.injury_replacements ?? []) {
|
|
189
|
+
const starter = roster.find((p) => p.id === ir.starterId);
|
|
190
|
+
const replacement = roster.find((p) => p.id === ir.replacementId);
|
|
191
|
+
if (starter == null || replacement == null)
|
|
192
|
+
continue;
|
|
193
|
+
if (!starterIds.has(starter.id) || seenStarters.has(starter.id))
|
|
194
|
+
continue;
|
|
195
|
+
if (starterIds.has(replacement.id) || replacement.id === lineup[service_1.CourtPosition.LIBERO_ZONE]?.id ||
|
|
196
|
+
replacement.id === secondLibero?.id || seenIncoming.has(replacement.id))
|
|
197
|
+
continue;
|
|
198
|
+
seenStarters.add(starter.id);
|
|
199
|
+
seenIncoming.add(replacement.id);
|
|
200
|
+
let benchBackfill;
|
|
201
|
+
if (ir.benchBackfillId != null && benchIds.has(replacement.id)) {
|
|
202
|
+
const candidate = roster.find((p) => p.id === ir.benchBackfillId);
|
|
203
|
+
if (candidate != null && !inLineupIds.has(candidate.id) && candidate.id !== secondLibero?.id &&
|
|
204
|
+
!seenIncoming.has(candidate.id)) {
|
|
205
|
+
benchBackfill = candidate;
|
|
206
|
+
seenIncoming.add(candidate.id);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
healed.push({ starter, replacement, benchBackfill });
|
|
210
|
+
}
|
|
211
|
+
return healed;
|
|
204
212
|
})()
|
|
205
213
|
});
|
|
206
214
|
}
|
|
@@ -263,31 +271,35 @@ function tacticsColumnsToApiDto(model) {
|
|
|
263
271
|
systemSets: model.system_sets,
|
|
264
272
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
265
273
|
replaceInjuredImmediately: model.replace_injured_immediately ?? true,
|
|
266
|
-
// Same stale-data policy as validSecondLibero above: don't surface
|
|
267
|
-
//
|
|
268
|
-
// them forever. Roster membership can't be checked here (ids only); the service heal covers that.
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
274
|
+
// Same stale-data policy as validSecondLibero above: don't surface entries the service load would drop
|
|
275
|
+
// (starter no longer a starter, replacement now a starter/libero/L2, duplicates), or the UI reloads and
|
|
276
|
+
// re-emits them forever. Roster membership can't be checked here (ids only); the service heal covers that.
|
|
277
|
+
injuryReplacements: (() => {
|
|
278
|
+
const starterSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
|
|
279
|
+
model.lineup[service_1.CourtPosition.RIGHT_FRONT], model.lineup[service_1.CourtPosition.LEFT_BACK],
|
|
280
|
+
model.lineup[service_1.CourtPosition.MIDDLE_BACK], model.lineup[service_1.CourtPosition.RIGHT_BACK]]);
|
|
281
|
+
const benchIdSet = new Set(model.lineup.bench);
|
|
282
|
+
const liberoId = model.lineup[service_1.CourtPosition.LIBERO_ZONE];
|
|
283
|
+
const seenStarters = new Set();
|
|
284
|
+
const seenIncoming = new Set();
|
|
285
|
+
const healed = [];
|
|
286
|
+
for (const ir of model.injury_replacements ?? []) {
|
|
287
|
+
if (!starterSlotIds.has(ir.starterId) || seenStarters.has(ir.starterId))
|
|
288
|
+
continue;
|
|
289
|
+
if (starterSlotIds.has(ir.replacementId) || ir.replacementId === liberoId ||
|
|
290
|
+
ir.replacementId === validSecondLibero || seenIncoming.has(ir.replacementId))
|
|
291
|
+
continue;
|
|
292
|
+
seenStarters.add(ir.starterId);
|
|
293
|
+
seenIncoming.add(ir.replacementId);
|
|
294
|
+
let benchBackfillId;
|
|
295
|
+
if (ir.benchBackfillId != null && benchIdSet.has(ir.replacementId) &&
|
|
296
|
+
!lineupIds.has(ir.benchBackfillId) && ir.benchBackfillId !== validSecondLibero &&
|
|
297
|
+
!seenIncoming.has(ir.benchBackfillId)) {
|
|
298
|
+
benchBackfillId = ir.benchBackfillId;
|
|
299
|
+
seenIncoming.add(benchBackfillId);
|
|
300
|
+
}
|
|
301
|
+
healed.push({ starterId: ir.starterId, replacementId: ir.replacementId, benchBackfillId });
|
|
302
|
+
}
|
|
291
303
|
return healed.length > 0 ? healed : undefined;
|
|
292
304
|
})()
|
|
293
305
|
};
|
|
@@ -324,6 +336,6 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
324
336
|
system_sets: config.systemSets,
|
|
325
337
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
326
338
|
replace_injured_immediately: config.replaceInjuredImmediately,
|
|
327
|
-
|
|
339
|
+
injury_replacements: config.injuryReplacements
|
|
328
340
|
};
|
|
329
341
|
}
|
|
@@ -27,4 +27,4 @@ __exportStar(require("./rotation-system"), exports);
|
|
|
27
27
|
__exportStar(require("./lineup-function"), exports);
|
|
28
28
|
__exportStar(require("./base-position"), exports);
|
|
29
29
|
__exportStar(require("./offensive-preference"), exports);
|
|
30
|
-
__exportStar(require("./
|
|
30
|
+
__exportStar(require("./injury-replacement"), exports);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const tactics_z_1 = require("./tactics.z");
|
|
5
|
+
const test_helpers_1 = require("../../test-helpers");
|
|
6
|
+
(0, globals_1.describe)('TacticsInputSchema — injuryReplacements (owner rules 2026-07-03)', () => {
|
|
7
|
+
const country = (0, test_helpers_1.makeCountry)();
|
|
8
|
+
const p1 = (0, test_helpers_1.makePlayer)(country);
|
|
9
|
+
const p2 = (0, test_helpers_1.makePlayer)(country);
|
|
10
|
+
const p3 = (0, test_helpers_1.makePlayer)(country);
|
|
11
|
+
const p4 = (0, test_helpers_1.makePlayer)(country);
|
|
12
|
+
const p5 = (0, test_helpers_1.makePlayer)(country);
|
|
13
|
+
const p6 = (0, test_helpers_1.makePlayer)(country);
|
|
14
|
+
const benchA = (0, test_helpers_1.makePlayer)(country);
|
|
15
|
+
const benchB = (0, test_helpers_1.makePlayer)(country);
|
|
16
|
+
const libero = (0, test_helpers_1.makePlayer)(country);
|
|
17
|
+
const reserveA = (0, test_helpers_1.makePlayer)(country);
|
|
18
|
+
const reserveB = (0, test_helpers_1.makePlayer)(country);
|
|
19
|
+
function baseInput(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
lineup: {
|
|
22
|
+
4: p4,
|
|
23
|
+
3: p3,
|
|
24
|
+
2: p2,
|
|
25
|
+
5: p5,
|
|
26
|
+
6: p6,
|
|
27
|
+
1: p1,
|
|
28
|
+
0: libero,
|
|
29
|
+
bench: [benchA, benchB]
|
|
30
|
+
},
|
|
31
|
+
liberoReplacements: [],
|
|
32
|
+
...overrides
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
(0, globals_1.it)('defaults to true / empty', () => {
|
|
36
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput());
|
|
37
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
38
|
+
if (res.success) {
|
|
39
|
+
(0, globals_1.expect)(res.data.replaceInjuredImmediately).toBe(true);
|
|
40
|
+
(0, globals_1.expect)(res.data.injuryReplacements).toEqual([]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
(0, globals_1.it)('accepts a bench replacement with a reserve backfill', () => {
|
|
44
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
45
|
+
injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: reserveA }]
|
|
46
|
+
}));
|
|
47
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
(0, globals_1.it)('accepts a bench replacement WITHOUT a backfill (empty bench seat is allowed by design)', () => {
|
|
50
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
51
|
+
injuryReplacements: [{ starter: p1, replacement: benchA }]
|
|
52
|
+
}));
|
|
53
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
(0, globals_1.it)('accepts a reserve replacement (no backfill possible)', () => {
|
|
56
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
57
|
+
injuryReplacements: [{ starter: p1, replacement: reserveA }]
|
|
58
|
+
}));
|
|
59
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
(0, globals_1.it)('rejects a starter as replacement', () => {
|
|
62
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
63
|
+
injuryReplacements: [{ starter: p1, replacement: p2 }]
|
|
64
|
+
}));
|
|
65
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
66
|
+
if (!res.success) {
|
|
67
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE')).toBe(true);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
(0, globals_1.it)('rejects the libero as replacement', () => {
|
|
71
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
72
|
+
injuryReplacements: [{ starter: p1, replacement: libero }]
|
|
73
|
+
}));
|
|
74
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
75
|
+
if (!res.success) {
|
|
76
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE')).toBe(true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
(0, globals_1.it)('rejects a non-starter as the starter side', () => {
|
|
80
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
81
|
+
injuryReplacements: [{ starter: benchA, replacement: reserveA }]
|
|
82
|
+
}));
|
|
83
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
84
|
+
if (!res.success) {
|
|
85
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_STARTER_NOT_A_STARTER')).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
(0, globals_1.it)('rejects a backfill when the replacement is a reserve', () => {
|
|
89
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
90
|
+
injuryReplacements: [{ starter: p1, replacement: reserveA, benchBackfill: reserveB }]
|
|
91
|
+
}));
|
|
92
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
93
|
+
if (!res.success) {
|
|
94
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'BENCH_BACKFILL_REQUIRES_BENCH_REPLACEMENT')).toBe(true);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
(0, globals_1.it)('rejects a bench player as backfill (must be a reserve)', () => {
|
|
98
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
99
|
+
injuryReplacements: [{ starter: p1, replacement: benchA, benchBackfill: benchB }]
|
|
100
|
+
}));
|
|
101
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
102
|
+
if (!res.success) {
|
|
103
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'BENCH_BACKFILL_MUST_BE_RESERVE')).toBe(true);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
(0, globals_1.it)('rejects duplicate starter entries', () => {
|
|
107
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
108
|
+
injuryReplacements: [
|
|
109
|
+
{ starter: p1, replacement: benchA },
|
|
110
|
+
{ starter: p1, replacement: reserveA }
|
|
111
|
+
]
|
|
112
|
+
}));
|
|
113
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
114
|
+
if (!res.success) {
|
|
115
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_DUPLICATE_STARTER')).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
(0, globals_1.it)('rejects the same incoming player used twice', () => {
|
|
119
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
120
|
+
injuryReplacements: [
|
|
121
|
+
{ starter: p1, replacement: reserveA },
|
|
122
|
+
{ starter: p2, replacement: reserveA }
|
|
123
|
+
]
|
|
124
|
+
}));
|
|
125
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
126
|
+
if (!res.success) {
|
|
127
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_PLAYER_USED_TWICE')).toBe(true);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
(0, globals_1.it)('rejects a player used as replacement in one entry and backfill in another', () => {
|
|
131
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
132
|
+
injuryReplacements: [
|
|
133
|
+
{ starter: p1, replacement: reserveA },
|
|
134
|
+
{ starter: p2, replacement: benchA, benchBackfill: reserveA }
|
|
135
|
+
]
|
|
136
|
+
}));
|
|
137
|
+
(0, globals_1.expect)(res.success).toBe(false);
|
|
138
|
+
if (!res.success) {
|
|
139
|
+
(0, globals_1.expect)(res.error.issues.some(issue => issue.message === 'INJURY_REPLACEMENT_PLAYER_USED_TWICE')).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -124,9 +124,10 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
124
124
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
125
|
}, z.core.$strip>>>>;
|
|
126
126
|
replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
128
|
+
starter: z.ZodCustom<Player, Player>;
|
|
129
|
+
replacement: z.ZodCustom<Player, Player>;
|
|
130
|
+
benchBackfill: z.ZodOptional<z.ZodCustom<Player, Player>>;
|
|
130
131
|
}, z.core.$strip>>>;
|
|
131
132
|
}, z.core.$strip>;
|
|
132
133
|
export type TacticsInput = z.infer<typeof TacticsInputSchema>;
|
|
@@ -81,13 +81,14 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
81
81
|
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
82
82
|
systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
|
|
83
83
|
offensivePreferenceSets: zod_1.z.array(zod_1.z.array(offensivePreferenceSchema)).max(5).optional(),
|
|
84
|
-
// Injuries. Defaults keep every existing Tactics.create({...}) call valid.
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. injuryReplacements is the
|
|
85
|
+
// per-starter explicit replacement mapping (owner rules: followed verbatim, bench-only auto fallback,
|
|
86
|
+
// reserves never auto-promoted); cross-field rules in the superRefine below.
|
|
87
87
|
replaceInjuredImmediately: zod_1.z.boolean().default(true),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
injuryReplacements: zod_1.z.array(zod_1.z.object({
|
|
89
|
+
starter: playerInstanceSchema,
|
|
90
|
+
replacement: playerInstanceSchema,
|
|
91
|
+
benchBackfill: playerInstanceSchema.optional()
|
|
91
92
|
})).default([])
|
|
92
93
|
}).superRefine((data, ctx) => {
|
|
93
94
|
// Cross-field rules for the rotation system. These keep an invalid config from ever reaching the sim (the
|
|
@@ -186,27 +187,41 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// the
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
190
|
+
// Injury replacements (owner rules 2026-07-03). starter = a court starter, at most one entry each.
|
|
191
|
+
// replacement = a bench player or a reserve (never another starter, the libero, or the L2). benchBackfill
|
|
192
|
+
// only when the replacement came from the bench, and must be a RESERVE (outside the whole lineup, not the
|
|
193
|
+
// L2). No player appears as a replacement or backfill more than once across all entries.
|
|
194
|
+
const benchIds = new Set(data.lineup.bench.map((p) => p.id));
|
|
195
|
+
const liberoId = data.lineup[LIBERO_ZONE]?.id;
|
|
196
|
+
const seenInjuryStarters = new Set();
|
|
197
|
+
const seenIncoming = new Set();
|
|
198
|
+
for (const ir of data.injuryReplacements) {
|
|
199
|
+
if (!starterIds.has(ir.starter.id)) {
|
|
200
|
+
ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_STARTER_NOT_A_STARTER', path: ['injuryReplacements'] });
|
|
197
201
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
if (seenInjuryStarters.has(ir.starter.id)) {
|
|
203
|
+
ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_DUPLICATE_STARTER', path: ['injuryReplacements'] });
|
|
204
|
+
}
|
|
205
|
+
seenInjuryStarters.add(ir.starter.id);
|
|
206
|
+
const isReplacementBench = benchIds.has(ir.replacement.id);
|
|
207
|
+
if (starterIds.has(ir.replacement.id) || ir.replacement.id === liberoId || ir.replacement.id === data.secondLibero?.id) {
|
|
208
|
+
ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_MUST_BE_BENCH_OR_RESERVE', path: ['injuryReplacements'] });
|
|
209
|
+
}
|
|
210
|
+
if (seenIncoming.has(ir.replacement.id)) {
|
|
211
|
+
ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_PLAYER_USED_TWICE', path: ['injuryReplacements'] });
|
|
212
|
+
}
|
|
213
|
+
seenIncoming.add(ir.replacement.id);
|
|
214
|
+
if (ir.benchBackfill != null) {
|
|
215
|
+
if (!isReplacementBench) {
|
|
216
|
+
ctx.addIssue({ code: 'custom', message: 'BENCH_BACKFILL_REQUIRES_BENCH_REPLACEMENT', path: ['injuryReplacements'] });
|
|
202
217
|
}
|
|
203
|
-
if (
|
|
204
|
-
ctx.addIssue({ code: 'custom', message: '
|
|
218
|
+
if (inLineupIds.has(ir.benchBackfill.id) || ir.benchBackfill.id === data.secondLibero?.id) {
|
|
219
|
+
ctx.addIssue({ code: 'custom', message: 'BENCH_BACKFILL_MUST_BE_RESERVE', path: ['injuryReplacements'] });
|
|
205
220
|
}
|
|
206
|
-
if (
|
|
207
|
-
ctx.addIssue({ code: 'custom', message: '
|
|
221
|
+
if (seenIncoming.has(ir.benchBackfill.id)) {
|
|
222
|
+
ctx.addIssue({ code: 'custom', message: 'INJURY_REPLACEMENT_PLAYER_USED_TWICE', path: ['injuryReplacements'] });
|
|
208
223
|
}
|
|
209
|
-
|
|
224
|
+
seenIncoming.add(ir.benchBackfill.id);
|
|
210
225
|
}
|
|
211
226
|
}
|
|
212
227
|
});
|
|
@@ -132,9 +132,10 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
132
132
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
133
133
|
}, z.core.$strip>>>>;
|
|
134
134
|
replaceInjuredImmediately: z.ZodDefault<z.ZodBoolean>;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
136
|
+
starter: z.ZodCustom<Player, Player>;
|
|
137
|
+
replacement: z.ZodCustom<Player, Player>;
|
|
138
|
+
benchBackfill: z.ZodOptional<z.ZodCustom<Player, Player>>;
|
|
138
139
|
}, z.core.$strip>>>;
|
|
139
140
|
}, z.core.$strip>>;
|
|
140
141
|
}, z.core.$strip>;
|
|
@@ -5,7 +5,7 @@ import { LiberoSubConfig } from './libero-sub';
|
|
|
5
5
|
import { SubBand } from './energy-band';
|
|
6
6
|
import { RotationSystemEnum } from './rotation-system';
|
|
7
7
|
import { OffensivePreference } from './offensive-preference';
|
|
8
|
-
import {
|
|
8
|
+
import { InjuryReplacement } from './injury-replacement';
|
|
9
9
|
export interface StartingLineup {
|
|
10
10
|
readonly [CourtPosition.LEFT_FRONT]: Player;
|
|
11
11
|
readonly [CourtPosition.MIDDLE_FRONT]: Player;
|
|
@@ -35,7 +35,7 @@ export interface TacticsOpts {
|
|
|
35
35
|
readonly systemSets?: SystemSet[];
|
|
36
36
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
37
37
|
readonly replaceInjuredImmediately: boolean;
|
|
38
|
-
readonly
|
|
38
|
+
readonly injuryReplacements: InjuryReplacement[];
|
|
39
39
|
}
|
|
40
40
|
export declare class Tactics {
|
|
41
41
|
readonly lineup: StartingLineup;
|
|
@@ -52,7 +52,7 @@ export declare class Tactics {
|
|
|
52
52
|
readonly systemSets?: SystemSet[];
|
|
53
53
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
54
54
|
readonly replaceInjuredImmediately: boolean;
|
|
55
|
-
readonly
|
|
55
|
+
readonly injuryReplacements: InjuryReplacement[];
|
|
56
56
|
static create(input: unknown): Tactics;
|
|
57
57
|
private constructor();
|
|
58
58
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -18,7 +18,7 @@ class Tactics {
|
|
|
18
18
|
}
|
|
19
19
|
return new Tactics(result.data);
|
|
20
20
|
}
|
|
21
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceInjuredImmediately = true,
|
|
21
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], systemSets, offensivePreferenceSets, replaceInjuredImmediately = true, injuryReplacements = [] }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -33,7 +33,7 @@ class Tactics {
|
|
|
33
33
|
this.systemSets = systemSets;
|
|
34
34
|
this.offensivePreferenceSets = offensivePreferenceSets;
|
|
35
35
|
this.replaceInjuredImmediately = replaceInjuredImmediately;
|
|
36
|
-
this.
|
|
36
|
+
this.injuryReplacements = injuryReplacements;
|
|
37
37
|
}
|
|
38
38
|
findPlayerInLineup(id) {
|
|
39
39
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -105,9 +105,10 @@ export interface ApiSystemSet {
|
|
|
105
105
|
rotationSystem: RotationSystemEnum;
|
|
106
106
|
designatedSetters: string[];
|
|
107
107
|
}
|
|
108
|
-
export interface
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
export interface ApiInjuryReplacement {
|
|
109
|
+
starterId: string;
|
|
110
|
+
replacementId: string;
|
|
111
|
+
benchBackfillId?: string;
|
|
111
112
|
}
|
|
112
113
|
export interface ApiLiberoSetSubConfig {
|
|
113
114
|
mode: LiberoSubMode;
|
|
@@ -131,7 +132,7 @@ export interface Tactics {
|
|
|
131
132
|
systemSets?: ApiSystemSet[];
|
|
132
133
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
133
134
|
replaceInjuredImmediately: boolean;
|
|
134
|
-
|
|
135
|
+
injuryReplacements?: ApiInjuryReplacement[];
|
|
135
136
|
}
|
|
136
137
|
export interface ApiLineupPreset {
|
|
137
138
|
presetId: string;
|
|
@@ -39,9 +39,10 @@ export interface SystemSetAttributes {
|
|
|
39
39
|
rotationSystem: RotationSystemEnum;
|
|
40
40
|
designatedSetters: PlayerId[];
|
|
41
41
|
}
|
|
42
|
-
export interface
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
export interface InjuryReplacementAttributes {
|
|
43
|
+
starterId: PlayerId;
|
|
44
|
+
replacementId: PlayerId;
|
|
45
|
+
benchBackfillId?: PlayerId;
|
|
45
46
|
}
|
|
46
47
|
export interface LiberoSetSubConfigAttributes {
|
|
47
48
|
mode: LiberoSubMode;
|
|
@@ -66,11 +67,11 @@ export interface TacticsAttributes {
|
|
|
66
67
|
system_sets?: SystemSetAttributes[];
|
|
67
68
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
68
69
|
replace_injured_immediately?: boolean;
|
|
69
|
-
|
|
70
|
+
injury_replacements?: InjuryReplacementAttributes[];
|
|
70
71
|
}
|
|
71
72
|
export type TacticsPk = 'team_id';
|
|
72
73
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
73
|
-
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_injured_immediately' | '
|
|
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_injured_immediately' | 'injury_replacements';
|
|
74
75
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
75
76
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
76
77
|
team_id: string;
|
|
@@ -88,7 +89,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
88
89
|
system_sets?: SystemSetAttributes[];
|
|
89
90
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
90
91
|
replace_injured_immediately?: boolean;
|
|
91
|
-
|
|
92
|
+
injury_replacements?: InjuryReplacementAttributes[];
|
|
92
93
|
team: TeamModel;
|
|
93
94
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
94
95
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|