volleyballsimtypes 0.0.475 → 0.0.476
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/data/transformers/tactics.js +17 -8
- package/dist/cjs/src/data/transformers/tactics.test.js +36 -0
- package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +11 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.js +5 -4
- package/dist/esm/src/data/transformers/tactics.js +17 -8
- package/dist/esm/src/data/transformers/tactics.test.js +36 -0
- package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +11 -2
- package/dist/esm/src/service/team/schemas/tactics.z.js +5 -4
- package/package.json +1 -1
|
@@ -178,15 +178,20 @@ function transformToObject(model, roster) {
|
|
|
178
178
|
rotation: p.rotation,
|
|
179
179
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
180
180
|
}))),
|
|
181
|
-
// LENIENT: keep only
|
|
182
|
-
//
|
|
181
|
+
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
182
|
+
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
183
|
+
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
184
|
+
// and crash a team load; a dropped key = the ON default.
|
|
183
185
|
replaceKnockedImmediately: (() => {
|
|
184
|
-
const
|
|
186
|
+
const allowedIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
|
|
185
187
|
lineup[service_1.CourtPosition.LEFT_BACK], lineup[service_1.CourtPosition.MIDDLE_BACK], lineup[service_1.CourtPosition.RIGHT_BACK]]
|
|
186
188
|
.map((p) => p.id));
|
|
189
|
+
const liberoZoneId = lineup[service_1.CourtPosition.LIBERO_ZONE]?.id;
|
|
190
|
+
if (liberoZoneId != null && secondLibero != null)
|
|
191
|
+
allowedIds.add(liberoZoneId);
|
|
187
192
|
const healed = {};
|
|
188
193
|
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
189
|
-
if (
|
|
194
|
+
if (allowedIds.has(id) && typeof value === 'boolean')
|
|
190
195
|
healed[id] = value;
|
|
191
196
|
}
|
|
192
197
|
return healed;
|
|
@@ -321,15 +326,19 @@ function tacticsColumnsToApiDto(model) {
|
|
|
321
326
|
offensivePreferences: model.offensive_preferences,
|
|
322
327
|
systemSets: model.system_sets,
|
|
323
328
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
324
|
-
// Same stale-data policy as the heals below: only
|
|
325
|
-
//
|
|
329
|
+
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
330
|
+
// libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
|
|
331
|
+
// survive; an empty map surfaces as absent (missing key = ON).
|
|
326
332
|
replaceKnockedImmediately: (() => {
|
|
327
|
-
const
|
|
333
|
+
const allowedSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
|
|
328
334
|
model.lineup[service_1.CourtPosition.RIGHT_FRONT], model.lineup[service_1.CourtPosition.LEFT_BACK],
|
|
329
335
|
model.lineup[service_1.CourtPosition.MIDDLE_BACK], model.lineup[service_1.CourtPosition.RIGHT_BACK]]);
|
|
336
|
+
const liberoZoneId = model.lineup[service_1.CourtPosition.LIBERO_ZONE];
|
|
337
|
+
if (liberoZoneId != null && validSecondLibero != null)
|
|
338
|
+
allowedSlotIds.add(liberoZoneId);
|
|
330
339
|
const healed = {};
|
|
331
340
|
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
332
|
-
if (
|
|
341
|
+
if (allowedSlotIds.has(id) && typeof value === 'boolean')
|
|
333
342
|
healed[id] = value;
|
|
334
343
|
}
|
|
335
344
|
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
@@ -78,6 +78,21 @@ function makeTactics(designatedSubs) {
|
|
|
78
78
|
(0, globals_1.expect)(dto.secondLibero).toBe('reserve1');
|
|
79
79
|
(0, globals_1.expect)(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
|
|
80
80
|
});
|
|
81
|
+
(0, globals_1.it)('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
|
|
82
|
+
const model = makeTactics([]);
|
|
83
|
+
model.lineup[service_1.CourtPosition.LIBERO_ZONE] = 'libero1';
|
|
84
|
+
model.second_libero = 'reserve1';
|
|
85
|
+
model.replace_knocked_immediately = { libero1: false, lf: false };
|
|
86
|
+
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
87
|
+
(0, globals_1.expect)(dto.replaceKnockedImmediately).toEqual({ libero1: false, lf: false });
|
|
88
|
+
});
|
|
89
|
+
(0, globals_1.it)('drops a libero-keyed replaceKnockedImmediately entry when there is no second libero', () => {
|
|
90
|
+
const model = makeTactics([]);
|
|
91
|
+
model.lineup[service_1.CourtPosition.LIBERO_ZONE] = 'libero1';
|
|
92
|
+
model.replace_knocked_immediately = { libero1: false, lf: false };
|
|
93
|
+
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
94
|
+
(0, globals_1.expect)(dto.replaceKnockedImmediately).toEqual({ lf: false });
|
|
95
|
+
});
|
|
81
96
|
(0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
|
|
82
97
|
const model = makeTactics([]);
|
|
83
98
|
model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
@@ -182,4 +197,25 @@ function makeTactics(designatedSubs) {
|
|
|
182
197
|
(0, globals_1.expect)(back.liberoInjury?.secondLiberoBackfill).toBeUndefined();
|
|
183
198
|
(0, globals_1.expect)(back.liberoInjury?.inMatchDesignate?.id).toBe(reserveB.id);
|
|
184
199
|
});
|
|
200
|
+
(0, globals_1.it)('keeps a libero-keyed replaceKnockedImmediately entry on load when an L2 exists', () => {
|
|
201
|
+
const withKnock = service_1.Tactics.create({
|
|
202
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
|
|
203
|
+
liberoReplacements: [],
|
|
204
|
+
secondLibero: l2,
|
|
205
|
+
replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
|
|
206
|
+
});
|
|
207
|
+
const attrs = (0, tactics_1.transformFromTactics)(withKnock, 'team-1');
|
|
208
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
209
|
+
(0, globals_1.expect)(back.replaceKnockedImmediately).toEqual({ [libero.id]: false, [p1.id]: false });
|
|
210
|
+
});
|
|
211
|
+
(0, globals_1.it)('drops a libero-keyed replaceKnockedImmediately entry on load when there is no L2', () => {
|
|
212
|
+
const withKnock = service_1.Tactics.create({
|
|
213
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
|
|
214
|
+
liberoReplacements: [],
|
|
215
|
+
replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
|
|
216
|
+
});
|
|
217
|
+
const attrs = (0, tactics_1.transformFromTactics)(withKnock, 'team-1');
|
|
218
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
219
|
+
(0, globals_1.expect)(back.replaceKnockedImmediately).toEqual({ [p1.id]: false });
|
|
220
|
+
});
|
|
185
221
|
});
|
|
@@ -49,8 +49,17 @@ const test_helpers_1 = require("../../test-helpers");
|
|
|
49
49
|
(0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [p1.id]: false, [p3.id]: true });
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
(0, globals_1.it)('
|
|
53
|
-
|
|
52
|
+
(0, globals_1.it)('accepts a libero-keyed replaceKnockedImmediately map', () => {
|
|
53
|
+
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
54
|
+
replaceKnockedImmediately: { [libero.id]: false }
|
|
55
|
+
}));
|
|
56
|
+
(0, globals_1.expect)(res.success).toBe(true);
|
|
57
|
+
if (res.success) {
|
|
58
|
+
(0, globals_1.expect)(res.data.replaceKnockedImmediately).toEqual({ [libero.id]: false });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
(0, globals_1.it)('rejects a replaceKnockedImmediately key that is not a court starter or the libero', () => {
|
|
62
|
+
for (const outsider of [benchA, reserveA]) {
|
|
54
63
|
const res = tactics_z_1.TacticsInputSchema.safeParse(baseInput({
|
|
55
64
|
replaceKnockedImmediately: { [outsider.id]: false }
|
|
56
65
|
}));
|
|
@@ -198,10 +198,11 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
|
-
// Per-
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
// Per-player knock toggle: every key must be a court starter OR the starting libero. The libero may carry its
|
|
202
|
+
// own key so the user can choose to keep a lightly knocked libero on court (owner 2026-07-06). Bench/reserve
|
|
203
|
+
// players still follow the slot they occupy, so they never carry their own key.
|
|
204
|
+
for (const knockedId of Object.keys(data.replaceKnockedImmediately)) {
|
|
205
|
+
if (!starterIds.has(knockedId) && knockedId !== libero?.id) {
|
|
205
206
|
ctx.addIssue({ code: 'custom', message: 'REPLACE_KNOCKED_KEY_NOT_A_STARTER', path: ['replaceKnockedImmediately'] });
|
|
206
207
|
}
|
|
207
208
|
}
|
|
@@ -172,15 +172,20 @@ function transformToObject(model, roster) {
|
|
|
172
172
|
rotation: p.rotation,
|
|
173
173
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
174
174
|
}))),
|
|
175
|
-
// LENIENT: keep only
|
|
176
|
-
//
|
|
175
|
+
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
176
|
+
// libero (the libero knock toggle is an L2-only feature, owner 2026-07-06) so stale jsonb (an ex-starter's
|
|
177
|
+
// key, a non-boolean value, a libero key left after the L2 was removed) can never fail schema validation
|
|
178
|
+
// and crash a team load; a dropped key = the ON default.
|
|
177
179
|
replaceKnockedImmediately: (() => {
|
|
178
|
-
const
|
|
180
|
+
const allowedIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
|
|
179
181
|
lineup[CourtPosition.LEFT_BACK], lineup[CourtPosition.MIDDLE_BACK], lineup[CourtPosition.RIGHT_BACK]]
|
|
180
182
|
.map((p) => p.id));
|
|
183
|
+
const liberoZoneId = lineup[CourtPosition.LIBERO_ZONE]?.id;
|
|
184
|
+
if (liberoZoneId != null && secondLibero != null)
|
|
185
|
+
allowedIds.add(liberoZoneId);
|
|
181
186
|
const healed = {};
|
|
182
187
|
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
183
|
-
if (
|
|
188
|
+
if (allowedIds.has(id) && typeof value === 'boolean')
|
|
184
189
|
healed[id] = value;
|
|
185
190
|
}
|
|
186
191
|
return healed;
|
|
@@ -315,15 +320,19 @@ function tacticsColumnsToApiDto(model) {
|
|
|
315
320
|
offensivePreferences: model.offensive_preferences,
|
|
316
321
|
systemSets: model.system_sets,
|
|
317
322
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
318
|
-
// Same stale-data policy as the heals below: only
|
|
319
|
-
//
|
|
323
|
+
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
324
|
+
// libero exists, the starting libero (the libero knock toggle is an L2-only feature, owner 2026-07-06)
|
|
325
|
+
// survive; an empty map surfaces as absent (missing key = ON).
|
|
320
326
|
replaceKnockedImmediately: (() => {
|
|
321
|
-
const
|
|
327
|
+
const allowedSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
|
|
322
328
|
model.lineup[CourtPosition.RIGHT_FRONT], model.lineup[CourtPosition.LEFT_BACK],
|
|
323
329
|
model.lineup[CourtPosition.MIDDLE_BACK], model.lineup[CourtPosition.RIGHT_BACK]]);
|
|
330
|
+
const liberoZoneId = model.lineup[CourtPosition.LIBERO_ZONE];
|
|
331
|
+
if (liberoZoneId != null && validSecondLibero != null)
|
|
332
|
+
allowedSlotIds.add(liberoZoneId);
|
|
324
333
|
const healed = {};
|
|
325
334
|
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
326
|
-
if (
|
|
335
|
+
if (allowedSlotIds.has(id) && typeof value === 'boolean')
|
|
327
336
|
healed[id] = value;
|
|
328
337
|
}
|
|
329
338
|
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
@@ -76,6 +76,21 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
76
76
|
expect(dto.secondLibero).toBe('reserve1');
|
|
77
77
|
expect(dto.liberoSub).toEqual({ mode: 'FATIGUE' });
|
|
78
78
|
});
|
|
79
|
+
it('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
|
|
80
|
+
const model = makeTactics([]);
|
|
81
|
+
model.lineup[CourtPosition.LIBERO_ZONE] = 'libero1';
|
|
82
|
+
model.second_libero = 'reserve1';
|
|
83
|
+
model.replace_knocked_immediately = { libero1: false, lf: false };
|
|
84
|
+
const dto = tacticsColumnsToApiDto(model);
|
|
85
|
+
expect(dto.replaceKnockedImmediately).toEqual({ libero1: false, lf: false });
|
|
86
|
+
});
|
|
87
|
+
it('drops a libero-keyed replaceKnockedImmediately entry when there is no second libero', () => {
|
|
88
|
+
const model = makeTactics([]);
|
|
89
|
+
model.lineup[CourtPosition.LIBERO_ZONE] = 'libero1';
|
|
90
|
+
model.replace_knocked_immediately = { libero1: false, lf: false };
|
|
91
|
+
const dto = tacticsColumnsToApiDto(model);
|
|
92
|
+
expect(dto.replaceKnockedImmediately).toEqual({ lf: false });
|
|
93
|
+
});
|
|
79
94
|
it('passes per-set tactics columns through to the API DTO shape', () => {
|
|
80
95
|
const model = makeTactics([]);
|
|
81
96
|
model.system_sets = [{ rotationSystem: RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
@@ -180,4 +195,25 @@ describe('libero injuries — service <-> row round-trip + lenient heal', () =>
|
|
|
180
195
|
expect(back.liberoInjury?.secondLiberoBackfill).toBeUndefined();
|
|
181
196
|
expect(back.liberoInjury?.inMatchDesignate?.id).toBe(reserveB.id);
|
|
182
197
|
});
|
|
198
|
+
it('keeps a libero-keyed replaceKnockedImmediately entry on load when an L2 exists', () => {
|
|
199
|
+
const withKnock = Tactics.create({
|
|
200
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
|
|
201
|
+
liberoReplacements: [],
|
|
202
|
+
secondLibero: l2,
|
|
203
|
+
replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
|
|
204
|
+
});
|
|
205
|
+
const attrs = transformFromTactics(withKnock, 'team-1');
|
|
206
|
+
const back = transformToTactics(attrs, roster);
|
|
207
|
+
expect(back.replaceKnockedImmediately).toEqual({ [libero.id]: false, [p1.id]: false });
|
|
208
|
+
});
|
|
209
|
+
it('drops a libero-keyed replaceKnockedImmediately entry on load when there is no L2', () => {
|
|
210
|
+
const withKnock = Tactics.create({
|
|
211
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, 0: libero, bench: [] },
|
|
212
|
+
liberoReplacements: [],
|
|
213
|
+
replaceKnockedImmediately: { [libero.id]: false, [p1.id]: false }
|
|
214
|
+
});
|
|
215
|
+
const attrs = transformFromTactics(withKnock, 'team-1');
|
|
216
|
+
const back = transformToTactics(attrs, roster);
|
|
217
|
+
expect(back.replaceKnockedImmediately).toEqual({ [p1.id]: false });
|
|
218
|
+
});
|
|
183
219
|
});
|
|
@@ -47,8 +47,17 @@ describe('TacticsInputSchema — injuryReplacements (owner rules 2026-07-03)', (
|
|
|
47
47
|
expect(res.data.replaceKnockedImmediately).toEqual({ [p1.id]: false, [p3.id]: true });
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
|
-
it('
|
|
51
|
-
|
|
50
|
+
it('accepts a libero-keyed replaceKnockedImmediately map', () => {
|
|
51
|
+
const res = TacticsInputSchema.safeParse(baseInput({
|
|
52
|
+
replaceKnockedImmediately: { [libero.id]: false }
|
|
53
|
+
}));
|
|
54
|
+
expect(res.success).toBe(true);
|
|
55
|
+
if (res.success) {
|
|
56
|
+
expect(res.data.replaceKnockedImmediately).toEqual({ [libero.id]: false });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
it('rejects a replaceKnockedImmediately key that is not a court starter or the libero', () => {
|
|
60
|
+
for (const outsider of [benchA, reserveA]) {
|
|
52
61
|
const res = TacticsInputSchema.safeParse(baseInput({
|
|
53
62
|
replaceKnockedImmediately: { [outsider.id]: false }
|
|
54
63
|
}));
|
|
@@ -195,10 +195,11 @@ export const TacticsInputSchema = z.object({
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
// Per-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// Per-player knock toggle: every key must be a court starter OR the starting libero. The libero may carry its
|
|
199
|
+
// own key so the user can choose to keep a lightly knocked libero on court (owner 2026-07-06). Bench/reserve
|
|
200
|
+
// players still follow the slot they occupy, so they never carry their own key.
|
|
201
|
+
for (const knockedId of Object.keys(data.replaceKnockedImmediately)) {
|
|
202
|
+
if (!starterIds.has(knockedId) && knockedId !== libero?.id) {
|
|
202
203
|
ctx.addIssue({ code: 'custom', message: 'REPLACE_KNOCKED_KEY_NOT_A_STARTER', path: ['replaceKnockedImmediately'] });
|
|
203
204
|
}
|
|
204
205
|
}
|