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.
@@ -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 starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
182
- // value) can never fail schema validation and crash a team load; a dropped key = the ON default.
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 starterIds = new Set([lineup[service_1.CourtPosition.LEFT_FRONT], lineup[service_1.CourtPosition.MIDDLE_FRONT], lineup[service_1.CourtPosition.RIGHT_FRONT],
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 (starterIds.has(id) && typeof value === 'boolean')
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 starter-keyed boolean entries survive, and an empty
325
- // map surfaces as absent (missing key = ON).
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 starterSlotIds = new Set([model.lineup[service_1.CourtPosition.LEFT_FRONT], model.lineup[service_1.CourtPosition.MIDDLE_FRONT],
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 (starterSlotIds.has(id) && typeof value === 'boolean')
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)('rejects a replaceKnockedImmediately key that is not a court starter', () => {
53
- for (const outsider of [benchA, libero, reserveA]) {
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-starter knock toggle: every key must be a configured court starter (bench/reserve/libero players
202
- // follow the slot they occupy, so they never carry their own key).
203
- for (const starterId of Object.keys(data.replaceKnockedImmediately)) {
204
- if (!starterIds.has(starterId)) {
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 starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
176
- // value) can never fail schema validation and crash a team load; a dropped key = the ON default.
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 starterIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
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 (starterIds.has(id) && typeof value === 'boolean')
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 starter-keyed boolean entries survive, and an empty
319
- // map surfaces as absent (missing key = ON).
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 starterSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
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 (starterSlotIds.has(id) && typeof value === 'boolean')
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('rejects a replaceKnockedImmediately key that is not a court starter', () => {
51
- for (const outsider of [benchA, libero, reserveA]) {
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-starter knock toggle: every key must be a configured court starter (bench/reserve/libero players
199
- // follow the slot they occupy, so they never carry their own key).
200
- for (const starterId of Object.keys(data.replaceKnockedImmediately)) {
201
- if (!starterIds.has(starterId)) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.475",
3
+ "version": "0.0.476",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",