volleyballsimtypes 0.0.485 → 0.0.487

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.
@@ -148,6 +148,39 @@ describe('getLineupFunctions()', () => {
148
148
  expect((0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.SIX_ZERO, [], baseLineup()).size).toBe(0);
149
149
  });
150
150
  });
151
+ describe('getAttackingFunctions()', () => {
152
+ it('maps the 6-2 FRONT-row setter to OPPOSITE and the back-row setter to SETTER, every rotation', () => {
153
+ for (let r = 0; r < 6; r++) {
154
+ const players = rotate(baseLineup(), r);
155
+ const fns = (0, lineup_function_1.getAttackingFunctions)(rotation_system_1.RotationSystemEnum.SIX_TWO, ['S', 'OPP'], players);
156
+ for (const id of ['S', 'OPP']) {
157
+ const p = players.find(s => s.playerId === id);
158
+ expect(fns.get(id)).toBe((0, match_1.isFrontRow)(p.position) ? player_1.RoleEnum.OPPOSITE_HITTER : player_1.RoleEnum.SETTER);
159
+ }
160
+ // Non-setters keep their normal hitting function.
161
+ expect(fns.get('OH1')).toBe(player_1.RoleEnum.OUTSIDE_HITTER);
162
+ expect(fns.get('MB1')).toBe(player_1.RoleEnum.MIDDLE_BLOCKER);
163
+ }
164
+ });
165
+ it('keeps BOTH 4-2 setters as SETTER (a 4-2 has no attacking opposite)', () => {
166
+ for (let r = 0; r < 6; r++) {
167
+ const players = rotate(baseLineup(), r);
168
+ const fns = (0, lineup_function_1.getAttackingFunctions)(rotation_system_1.RotationSystemEnum.FOUR_TWO, ['S', 'OPP'], players);
169
+ expect(fns.get('S')).toBe(player_1.RoleEnum.SETTER);
170
+ expect(fns.get('OPP')).toBe(player_1.RoleEnum.SETTER);
171
+ }
172
+ });
173
+ it('matches getLineupFunctions for 5-1 (a real opposite already exists) and 6-0 (empty)', () => {
174
+ const five = (0, lineup_function_1.getAttackingFunctions)(rotation_system_1.RotationSystemEnum.FIVE_ONE, ['S'], baseLineup());
175
+ expect(five.get('S')).toBe(player_1.RoleEnum.SETTER);
176
+ expect(five.get('OPP')).toBe(player_1.RoleEnum.OPPOSITE_HITTER);
177
+ expect((0, lineup_function_1.getAttackingFunctions)(rotation_system_1.RotationSystemEnum.SIX_ZERO, [], baseLineup()).size).toBe(0);
178
+ });
179
+ it('does NOT mutate the positional getLineupFunctions (6-2 front setter stays SETTER there)', () => {
180
+ const positional = (0, lineup_function_1.getLineupFunctions)(rotation_system_1.RotationSystemEnum.SIX_TWO, ['S', 'OPP'], baseLineup());
181
+ expect(positional.get('OPP')).toBe(player_1.RoleEnum.SETTER); // OPP is front-row at rotation 0
182
+ });
183
+ });
151
184
  describe('out-of-position penalty', () => {
152
185
  it('penalises a specialist played in a slot whose role they lack', () => {
153
186
  // A pure middle blocker placed in the opposite slot (function OPPOSITE_HITTER) is out of position.
@@ -9,5 +9,6 @@ export interface LineupSlot {
9
9
  }
10
10
  export declare function slotFunctionAt(system: RotationSystemEnum, setterPosition: CourtPosition, position: CourtPosition): RoleEnum;
11
11
  export declare function getLineupFunctions(system: RotationSystemEnum, designatedSetterIds: string[], starters: LineupSlot[]): Map<string, RoleEnum>;
12
+ export declare function getAttackingFunctions(system: RotationSystemEnum, designatedSetterIds: string[], starters: LineupSlot[]): Map<string, RoleEnum>;
12
13
  export declare function isOutOfPosition(roles: Role[], fn: RoleEnum | undefined): boolean;
13
14
  export declare function outOfPositionPenalty(roles: Role[], fn: RoleEnum | undefined): number;
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OUT_OF_POSITION_PENALTY = void 0;
4
4
  exports.slotFunctionAt = slotFunctionAt;
5
5
  exports.getLineupFunctions = getLineupFunctions;
6
+ exports.getAttackingFunctions = getAttackingFunctions;
6
7
  exports.isOutOfPosition = isOutOfPosition;
7
8
  exports.outOfPositionPenalty = outOfPositionPenalty;
9
+ const match_1 = require("../match");
8
10
  const player_1 = require("../player");
9
11
  const rotation_system_1 = require("./rotation-system");
10
12
  // Flat multiplier applied to EVERY action a player performs while played out of their preferred position.
@@ -54,6 +56,24 @@ function getLineupFunctions(system, designatedSetterIds, starters) {
54
56
  }
55
57
  return functions;
56
58
  }
59
+ // Like getLineupFunctions but for ATTACK + BLOCK purposes. In a 6-2 the FRONT-ROW designated setter attacks and
60
+ // blocks as the OPPOSITE this rotation, so it maps to OPPOSITE_HITTER here (its base-positioning + setting function
61
+ // stays SETTER via getLineupFunctions, so the out-of-position penalty is unaffected). Everything else is identical:
62
+ // the 6-2 back setter, both 4-2 setters, and the 5-1 setter all stay SETTER (they set / do not attack), and 6-0 is
63
+ // empty. Consumed by dedicated-block targeting and the opposite serve-hunt so defenders see the 6-2 opposite as the
64
+ // opposite, matching what the offence already does (it keeps the front-row setter as an attacker).
65
+ function getAttackingFunctions(system, designatedSetterIds, starters) {
66
+ const functions = getLineupFunctions(system, designatedSetterIds, starters);
67
+ if (system !== rotation_system_1.RotationSystemEnum.SIX_TWO)
68
+ return functions;
69
+ const setterIds = new Set(designatedSetterIds);
70
+ for (const slot of starters) {
71
+ if (setterIds.has(slot.playerId) && (0, match_1.isFrontRow)(slot.position)) {
72
+ functions.set(slot.playerId, player_1.RoleEnum.OPPOSITE_HITTER);
73
+ }
74
+ }
75
+ return functions;
76
+ }
57
77
  // True when a player's slot requires a role they do not hold (i.e. they are played out of position). A missing
58
78
  // function (6-0, or unknown player) is never out of position.
59
79
  function isOutOfPosition(roles, fn) {
@@ -23,12 +23,17 @@ function fatigueCondition(band) {
23
23
  function convertLegacySetSubConfig(raw) {
24
24
  if (!isRecord(raw))
25
25
  return raw;
26
- // Derive the mode for the oldest (pre-mode) generation; a unified shape has none of these keys.
26
+ // Derive the mode for the oldest (pre-mode) generation; a unified shape has none of these keys. Test the
27
+ // isPinchServer VALUE, not key presence: the sim read path (transformToObject) spreads `isPinchServer:
28
+ // d.isPinchServer` onto every row, so a unified row carries an `isPinchServer: undefined` key. `'isPinchServer'
29
+ // in raw` would then misread that unified row as a legacy FATIGUE sub and overwrite its real conditions /
30
+ // pinchServer flag. A genuine legacy isPinchServer-era row always has a boolean flag (true handled above), so
31
+ // the only remaining legacy case is an explicit `=== false`.
27
32
  const mode = raw.mode ?? (raw.isPinchServer === true
28
33
  ? 'PINCH'
29
34
  : raw.fatigueBand === 'NEVER'
30
35
  ? 'NEVER'
31
- : raw.fatigueBand != null || 'isPinchServer' in raw ? 'FATIGUE' : null);
36
+ : raw.fatigueBand != null || raw.isPinchServer === false ? 'FATIGUE' : null);
32
37
  if (mode == null)
33
38
  return raw;
34
39
  const { mode: _m, fatigueBand, isPinchServer: _p, ...rest } = raw;
@@ -12,6 +12,25 @@ const sub_config_convert_1 = require("./sub-config-convert");
12
12
  const unified = { bench: 'X', conditions: [{ type: pinch_condition_1.PinchConditionType.ASAP }], pinchServer: true };
13
13
  (0, globals_1.expect)((0, sub_config_convert_1.convertLegacySetSubConfig)(unified)).toBe(unified);
14
14
  });
15
+ (0, globals_1.it)('keeps a unified pinch server intact when a caller injects isPinchServer: undefined (sim read path)', () => {
16
+ // The sim read path (transformToObject) spreads `isPinchServer: d.isPinchServer` onto every row, so a
17
+ // unified row arrives carrying an `isPinchServer: undefined` key. Detecting the legacy shape by key
18
+ // PRESENCE misread it as a FATIGUE sub and wiped its real conditions + pinchServer flag; the value check
19
+ // (=== false) keeps the unified shape. Regression for the "pinch server never serves" prod bug.
20
+ const out = (0, sub_config_convert_1.convertLegacySetSubConfig)({
21
+ bench: 'X',
22
+ conditions: [{ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 1 }],
23
+ conditionLogic: 'ANY',
24
+ pinchServer: true,
25
+ subBackMode: 'FATIGUE',
26
+ isPinchServer: undefined,
27
+ mode: undefined,
28
+ fatigueBand: undefined
29
+ });
30
+ (0, globals_1.expect)(out.pinchServer).toBe(true);
31
+ (0, globals_1.expect)(out.conditions).toEqual([{ type: pinch_condition_1.PinchConditionType.AFTER_ROTATIONS, rotations: 1 }]);
32
+ (0, globals_1.expect)(out.conditionLogic).toBe('ANY');
33
+ });
15
34
  (0, globals_1.it)('converts mode NEVER to an explicit empty condition list, dropping every other field', () => {
16
35
  (0, globals_1.expect)((0, sub_config_convert_1.convertLegacySetSubConfig)({ mode: 'NEVER', bench: 'X' })).toEqual({ conditions: [] });
17
36
  });
@@ -1,7 +1,7 @@
1
1
  import { CourtPosition, isFrontRow, rotatePosition } from '../match';
2
2
  import { RoleEnum } from '../player';
3
3
  import { RotationSystemEnum } from './rotation-system';
4
- import { getLineupFunctions, isOutOfPosition, outOfPositionPenalty, OUT_OF_POSITION_PENALTY, slotFunctionAt } from './lineup-function';
4
+ import { getAttackingFunctions, getLineupFunctions, isOutOfPosition, outOfPositionPenalty, OUT_OF_POSITION_PENALTY, slotFunctionAt } from './lineup-function';
5
5
  import { rallySetterId, resolveBases } from './base-position';
6
6
  // Balanced 5-1 at rotation 1 (setter serving from RIGHT_BACK = 1): S, OH1, MB1, OPP, OH2, MB2 around the court.
7
7
  function baseLineup() {
@@ -146,6 +146,39 @@ describe('getLineupFunctions()', () => {
146
146
  expect(getLineupFunctions(RotationSystemEnum.SIX_ZERO, [], baseLineup()).size).toBe(0);
147
147
  });
148
148
  });
149
+ describe('getAttackingFunctions()', () => {
150
+ it('maps the 6-2 FRONT-row setter to OPPOSITE and the back-row setter to SETTER, every rotation', () => {
151
+ for (let r = 0; r < 6; r++) {
152
+ const players = rotate(baseLineup(), r);
153
+ const fns = getAttackingFunctions(RotationSystemEnum.SIX_TWO, ['S', 'OPP'], players);
154
+ for (const id of ['S', 'OPP']) {
155
+ const p = players.find(s => s.playerId === id);
156
+ expect(fns.get(id)).toBe(isFrontRow(p.position) ? RoleEnum.OPPOSITE_HITTER : RoleEnum.SETTER);
157
+ }
158
+ // Non-setters keep their normal hitting function.
159
+ expect(fns.get('OH1')).toBe(RoleEnum.OUTSIDE_HITTER);
160
+ expect(fns.get('MB1')).toBe(RoleEnum.MIDDLE_BLOCKER);
161
+ }
162
+ });
163
+ it('keeps BOTH 4-2 setters as SETTER (a 4-2 has no attacking opposite)', () => {
164
+ for (let r = 0; r < 6; r++) {
165
+ const players = rotate(baseLineup(), r);
166
+ const fns = getAttackingFunctions(RotationSystemEnum.FOUR_TWO, ['S', 'OPP'], players);
167
+ expect(fns.get('S')).toBe(RoleEnum.SETTER);
168
+ expect(fns.get('OPP')).toBe(RoleEnum.SETTER);
169
+ }
170
+ });
171
+ it('matches getLineupFunctions for 5-1 (a real opposite already exists) and 6-0 (empty)', () => {
172
+ const five = getAttackingFunctions(RotationSystemEnum.FIVE_ONE, ['S'], baseLineup());
173
+ expect(five.get('S')).toBe(RoleEnum.SETTER);
174
+ expect(five.get('OPP')).toBe(RoleEnum.OPPOSITE_HITTER);
175
+ expect(getAttackingFunctions(RotationSystemEnum.SIX_ZERO, [], baseLineup()).size).toBe(0);
176
+ });
177
+ it('does NOT mutate the positional getLineupFunctions (6-2 front setter stays SETTER there)', () => {
178
+ const positional = getLineupFunctions(RotationSystemEnum.SIX_TWO, ['S', 'OPP'], baseLineup());
179
+ expect(positional.get('OPP')).toBe(RoleEnum.SETTER); // OPP is front-row at rotation 0
180
+ });
181
+ });
149
182
  describe('out-of-position penalty', () => {
150
183
  it('penalises a specialist played in a slot whose role they lack', () => {
151
184
  // A pure middle blocker placed in the opposite slot (function OPPOSITE_HITTER) is out of position.
@@ -9,5 +9,6 @@ export interface LineupSlot {
9
9
  }
10
10
  export declare function slotFunctionAt(system: RotationSystemEnum, setterPosition: CourtPosition, position: CourtPosition): RoleEnum;
11
11
  export declare function getLineupFunctions(system: RotationSystemEnum, designatedSetterIds: string[], starters: LineupSlot[]): Map<string, RoleEnum>;
12
+ export declare function getAttackingFunctions(system: RotationSystemEnum, designatedSetterIds: string[], starters: LineupSlot[]): Map<string, RoleEnum>;
12
13
  export declare function isOutOfPosition(roles: Role[], fn: RoleEnum | undefined): boolean;
13
14
  export declare function outOfPositionPenalty(roles: Role[], fn: RoleEnum | undefined): number;
@@ -1,3 +1,4 @@
1
+ import { isFrontRow } from '../match';
1
2
  import { RoleEnum } from '../player';
2
3
  import { RotationSystemEnum } from './rotation-system';
3
4
  // Flat multiplier applied to EVERY action a player performs while played out of their preferred position.
@@ -47,6 +48,24 @@ export function getLineupFunctions(system, designatedSetterIds, starters) {
47
48
  }
48
49
  return functions;
49
50
  }
51
+ // Like getLineupFunctions but for ATTACK + BLOCK purposes. In a 6-2 the FRONT-ROW designated setter attacks and
52
+ // blocks as the OPPOSITE this rotation, so it maps to OPPOSITE_HITTER here (its base-positioning + setting function
53
+ // stays SETTER via getLineupFunctions, so the out-of-position penalty is unaffected). Everything else is identical:
54
+ // the 6-2 back setter, both 4-2 setters, and the 5-1 setter all stay SETTER (they set / do not attack), and 6-0 is
55
+ // empty. Consumed by dedicated-block targeting and the opposite serve-hunt so defenders see the 6-2 opposite as the
56
+ // opposite, matching what the offence already does (it keeps the front-row setter as an attacker).
57
+ export function getAttackingFunctions(system, designatedSetterIds, starters) {
58
+ const functions = getLineupFunctions(system, designatedSetterIds, starters);
59
+ if (system !== RotationSystemEnum.SIX_TWO)
60
+ return functions;
61
+ const setterIds = new Set(designatedSetterIds);
62
+ for (const slot of starters) {
63
+ if (setterIds.has(slot.playerId) && isFrontRow(slot.position)) {
64
+ functions.set(slot.playerId, RoleEnum.OPPOSITE_HITTER);
65
+ }
66
+ }
67
+ return functions;
68
+ }
50
69
  // True when a player's slot requires a role they do not hold (i.e. they are played out of position). A missing
51
70
  // function (6-0, or unknown player) is never out of position.
52
71
  export function isOutOfPosition(roles, fn) {
@@ -17,12 +17,17 @@ function fatigueCondition(band) {
17
17
  export function convertLegacySetSubConfig(raw) {
18
18
  if (!isRecord(raw))
19
19
  return raw;
20
- // Derive the mode for the oldest (pre-mode) generation; a unified shape has none of these keys.
20
+ // Derive the mode for the oldest (pre-mode) generation; a unified shape has none of these keys. Test the
21
+ // isPinchServer VALUE, not key presence: the sim read path (transformToObject) spreads `isPinchServer:
22
+ // d.isPinchServer` onto every row, so a unified row carries an `isPinchServer: undefined` key. `'isPinchServer'
23
+ // in raw` would then misread that unified row as a legacy FATIGUE sub and overwrite its real conditions /
24
+ // pinchServer flag. A genuine legacy isPinchServer-era row always has a boolean flag (true handled above), so
25
+ // the only remaining legacy case is an explicit `=== false`.
21
26
  const mode = raw.mode ?? (raw.isPinchServer === true
22
27
  ? 'PINCH'
23
28
  : raw.fatigueBand === 'NEVER'
24
29
  ? 'NEVER'
25
- : raw.fatigueBand != null || 'isPinchServer' in raw ? 'FATIGUE' : null);
30
+ : raw.fatigueBand != null || raw.isPinchServer === false ? 'FATIGUE' : null);
26
31
  if (mode == null)
27
32
  return raw;
28
33
  const { mode: _m, fatigueBand, isPinchServer: _p, ...rest } = raw;
@@ -10,6 +10,25 @@ describe('convertLegacySetSubConfig', () => {
10
10
  const unified = { bench: 'X', conditions: [{ type: PinchConditionType.ASAP }], pinchServer: true };
11
11
  expect(convertLegacySetSubConfig(unified)).toBe(unified);
12
12
  });
13
+ it('keeps a unified pinch server intact when a caller injects isPinchServer: undefined (sim read path)', () => {
14
+ // The sim read path (transformToObject) spreads `isPinchServer: d.isPinchServer` onto every row, so a
15
+ // unified row arrives carrying an `isPinchServer: undefined` key. Detecting the legacy shape by key
16
+ // PRESENCE misread it as a FATIGUE sub and wiped its real conditions + pinchServer flag; the value check
17
+ // (=== false) keeps the unified shape. Regression for the "pinch server never serves" prod bug.
18
+ const out = convertLegacySetSubConfig({
19
+ bench: 'X',
20
+ conditions: [{ type: PinchConditionType.AFTER_ROTATIONS, rotations: 1 }],
21
+ conditionLogic: 'ANY',
22
+ pinchServer: true,
23
+ subBackMode: 'FATIGUE',
24
+ isPinchServer: undefined,
25
+ mode: undefined,
26
+ fatigueBand: undefined
27
+ });
28
+ expect(out.pinchServer).toBe(true);
29
+ expect(out.conditions).toEqual([{ type: PinchConditionType.AFTER_ROTATIONS, rotations: 1 }]);
30
+ expect(out.conditionLogic).toBe('ANY');
31
+ });
13
32
  it('converts mode NEVER to an explicit empty condition list, dropping every other field', () => {
14
33
  expect(convertLegacySetSubConfig({ mode: 'NEVER', bench: 'X' })).toEqual({ conditions: [] });
15
34
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.485",
3
+ "version": "0.0.487",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",