volleyballsimtypes 0.0.486 → 0.0.488
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/models/user-settings.d.ts +2 -0
- package/dist/cjs/src/data/models/user-settings.js +5 -0
- package/dist/cjs/src/service/notification/notification-type.d.ts +2 -0
- package/dist/cjs/src/service/notification/notification-type.js +6 -0
- package/dist/cjs/src/service/notification/notification.test.js +18 -0
- package/dist/cjs/src/service/notification/registry.d.ts +1 -0
- package/dist/cjs/src/service/notification/registry.js +6 -1
- package/dist/cjs/src/service/team/base-position.test.js +33 -0
- package/dist/cjs/src/service/team/lineup-function.d.ts +1 -0
- package/dist/cjs/src/service/team/lineup-function.js +20 -0
- package/dist/esm/src/data/models/user-settings.d.ts +2 -0
- package/dist/esm/src/data/models/user-settings.js +5 -0
- package/dist/esm/src/service/notification/notification-type.d.ts +2 -0
- package/dist/esm/src/service/notification/notification-type.js +5 -0
- package/dist/esm/src/service/notification/notification.test.js +20 -2
- package/dist/esm/src/service/notification/registry.d.ts +1 -0
- package/dist/esm/src/service/notification/registry.js +5 -0
- package/dist/esm/src/service/team/base-position.test.js +34 -1
- package/dist/esm/src/service/team/lineup-function.d.ts +1 -0
- package/dist/esm/src/service/team/lineup-function.js +19 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ export interface UserSettingsAttributes {
|
|
|
10
10
|
user_id: string;
|
|
11
11
|
auto_dismiss: AutoDismissSettings;
|
|
12
12
|
locale: string | null;
|
|
13
|
+
notifications: Record<string, boolean>;
|
|
13
14
|
}
|
|
14
15
|
export type UserSettingsPk = 'user_id';
|
|
15
16
|
export type UserSettingsId = UserSettingsModel[UserSettingsPk];
|
|
@@ -18,6 +19,7 @@ export declare class UserSettingsModel extends Model<UserSettingsAttributes, Use
|
|
|
18
19
|
user_id: string;
|
|
19
20
|
auto_dismiss: AutoDismissSettings;
|
|
20
21
|
locale: string | null;
|
|
22
|
+
notifications: Record<string, boolean>;
|
|
21
23
|
user: AuthUserModel;
|
|
22
24
|
getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
23
25
|
setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
|
|
@@ -23,6 +23,11 @@ class UserSettingsModel extends sequelize_1.Model {
|
|
|
23
23
|
type: sequelize_1.DataTypes.STRING(8),
|
|
24
24
|
allowNull: true,
|
|
25
25
|
defaultValue: null
|
|
26
|
+
},
|
|
27
|
+
notifications: {
|
|
28
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
29
|
+
allowNull: false,
|
|
30
|
+
defaultValue: {}
|
|
26
31
|
}
|
|
27
32
|
}, {
|
|
28
33
|
sequelize,
|
|
@@ -4,3 +4,5 @@ export declare const NOTIFICATION_LOCALES: readonly ["en", "es", "fr", "it", "pl
|
|
|
4
4
|
export type NotificationLocale = typeof NOTIFICATION_LOCALES[number];
|
|
5
5
|
export declare const DEFAULT_NOTIFICATION_LOCALE: NotificationLocale;
|
|
6
6
|
export declare function normalizeNotificationLocale(locale: string | null | undefined): NotificationLocale;
|
|
7
|
+
export type NotificationPreferences = Record<string, boolean>;
|
|
8
|
+
export declare function pushEnabledFor(prefs: NotificationPreferences | null | undefined, type: string): boolean;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.DEFAULT_NOTIFICATION_LOCALE = exports.NOTIFICATION_LOCALES = exports.NOTIFICATION_TYPES = void 0;
|
|
8
8
|
exports.normalizeNotificationLocale = normalizeNotificationLocale;
|
|
9
|
+
exports.pushEnabledFor = pushEnabledFor;
|
|
9
10
|
exports.NOTIFICATION_TYPES = [
|
|
10
11
|
'MATCH_RESULT',
|
|
11
12
|
'INJURY',
|
|
@@ -28,3 +29,8 @@ function normalizeNotificationLocale(locale) {
|
|
|
28
29
|
}
|
|
29
30
|
return exports.DEFAULT_NOTIFICATION_LOCALE;
|
|
30
31
|
}
|
|
32
|
+
// Whether `type`'s mobile push should be sent for a user with these preferences (absent/true = send, false = suppress).
|
|
33
|
+
// Accepts a plain string like renderPush so a loose/unknown type still resolves (unknown = enabled).
|
|
34
|
+
function pushEnabledFor(prefs, type) {
|
|
35
|
+
return prefs?.[type] !== false;
|
|
36
|
+
}
|
|
@@ -126,3 +126,21 @@ function flattenStrings(node, prefix = '') {
|
|
|
126
126
|
(0, globals_1.expect)((0, render_1.notificationPath)('COMPENSATION', {})).toBe('/');
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
(0, globals_1.describe)('push preferences (PUSH_NOTIFICATION_TYPES + pushEnabledFor)', () => {
|
|
130
|
+
(0, globals_1.it)('is exactly the emitsPush types, COMPENSATION excluded', () => {
|
|
131
|
+
(0, globals_1.expect)([...registry_1.PUSH_NOTIFICATION_TYPES].sort((a, b) => a.localeCompare(b)))
|
|
132
|
+
.toEqual(notification_type_1.NOTIFICATION_TYPES.filter((t) => registry_1.NOTIFICATION_REGISTRY[t].emitsPush).sort((a, b) => a.localeCompare(b)));
|
|
133
|
+
(0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).not.toContain('COMPENSATION');
|
|
134
|
+
(0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).toContain('MATCH_RESULT');
|
|
135
|
+
(0, globals_1.expect)(registry_1.PUSH_NOTIFICATION_TYPES).toHaveLength(8);
|
|
136
|
+
});
|
|
137
|
+
(0, globals_1.it)('is opt-out: absent / null / empty / true all enable, only explicit false suppresses', () => {
|
|
138
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)(undefined, 'MATCH_RESULT')).toBe(true);
|
|
139
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)(null, 'MATCH_RESULT')).toBe(true);
|
|
140
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)({}, 'MATCH_RESULT')).toBe(true);
|
|
141
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)({ MATCH_RESULT: true }, 'MATCH_RESULT')).toBe(true);
|
|
142
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)({ MATCH_RESULT: false }, 'MATCH_RESULT')).toBe(false);
|
|
143
|
+
// disabling one type does not affect another
|
|
144
|
+
(0, globals_1.expect)((0, notification_type_1.pushEnabledFor)({ MATCH_RESULT: false }, 'INJURY')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NOTIFICATION_REGISTRY = void 0;
|
|
3
|
+
exports.PUSH_NOTIFICATION_TYPES = exports.NOTIFICATION_REGISTRY = void 0;
|
|
4
|
+
const notification_type_1 = require("./notification-type");
|
|
4
5
|
function readString(value) {
|
|
5
6
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
6
7
|
}
|
|
@@ -110,3 +111,7 @@ exports.NOTIFICATION_REGISTRY = {
|
|
|
110
111
|
catalogKeys: []
|
|
111
112
|
}
|
|
112
113
|
};
|
|
114
|
+
// The notification types that emit a mobile push (everything except COMPENSATION). Source of truth = each
|
|
115
|
+
// descriptor's emitsPush, so this cannot drift. This is the user-toggleable set surfaced in the mobile app's
|
|
116
|
+
// notification settings and validated by the API before persisting a preference.
|
|
117
|
+
exports.PUSH_NOTIFICATION_TYPES = notification_type_1.NOTIFICATION_TYPES.filter((type) => exports.NOTIFICATION_REGISTRY[type].emitsPush);
|
|
@@ -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) {
|
|
@@ -10,6 +10,7 @@ export interface UserSettingsAttributes {
|
|
|
10
10
|
user_id: string;
|
|
11
11
|
auto_dismiss: AutoDismissSettings;
|
|
12
12
|
locale: string | null;
|
|
13
|
+
notifications: Record<string, boolean>;
|
|
13
14
|
}
|
|
14
15
|
export type UserSettingsPk = 'user_id';
|
|
15
16
|
export type UserSettingsId = UserSettingsModel[UserSettingsPk];
|
|
@@ -18,6 +19,7 @@ export declare class UserSettingsModel extends Model<UserSettingsAttributes, Use
|
|
|
18
19
|
user_id: string;
|
|
19
20
|
auto_dismiss: AutoDismissSettings;
|
|
20
21
|
locale: string | null;
|
|
22
|
+
notifications: Record<string, boolean>;
|
|
21
23
|
user: AuthUserModel;
|
|
22
24
|
getUser: Sequelize.BelongsToGetAssociationMixin<AuthUserModel>;
|
|
23
25
|
setUser: Sequelize.BelongsToSetAssociationMixin<AuthUserModel, AuthUserId>;
|
|
@@ -4,3 +4,5 @@ export declare const NOTIFICATION_LOCALES: readonly ["en", "es", "fr", "it", "pl
|
|
|
4
4
|
export type NotificationLocale = typeof NOTIFICATION_LOCALES[number];
|
|
5
5
|
export declare const DEFAULT_NOTIFICATION_LOCALE: NotificationLocale;
|
|
6
6
|
export declare function normalizeNotificationLocale(locale: string | null | undefined): NotificationLocale;
|
|
7
|
+
export type NotificationPreferences = Record<string, boolean>;
|
|
8
|
+
export declare function pushEnabledFor(prefs: NotificationPreferences | null | undefined, type: string): boolean;
|
|
@@ -24,3 +24,8 @@ export function normalizeNotificationLocale(locale) {
|
|
|
24
24
|
}
|
|
25
25
|
return DEFAULT_NOTIFICATION_LOCALE;
|
|
26
26
|
}
|
|
27
|
+
// Whether `type`'s mobile push should be sent for a user with these preferences (absent/true = send, false = suppress).
|
|
28
|
+
// Accepts a plain string like renderPush so a loose/unknown type still resolves (unknown = enabled).
|
|
29
|
+
export function pushEnabledFor(prefs, type) {
|
|
30
|
+
return prefs?.[type] !== false;
|
|
31
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
|
-
import { NOTIFICATION_TYPES, NOTIFICATION_LOCALES } from './notification-type';
|
|
3
|
-
import { NOTIFICATION_REGISTRY } from './registry';
|
|
2
|
+
import { NOTIFICATION_TYPES, NOTIFICATION_LOCALES, pushEnabledFor } from './notification-type';
|
|
3
|
+
import { NOTIFICATION_REGISTRY, PUSH_NOTIFICATION_TYPES } from './registry';
|
|
4
4
|
import { NOTIFICATION_CATALOG } from './catalog';
|
|
5
5
|
import { renderPush, notificationPath, interpolate, translateNotification } from './render';
|
|
6
6
|
function readNested(node, dotKey) {
|
|
@@ -124,3 +124,21 @@ describe('translateNotification + interpolate + notificationPath', () => {
|
|
|
124
124
|
expect(notificationPath('COMPENSATION', {})).toBe('/');
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
describe('push preferences (PUSH_NOTIFICATION_TYPES + pushEnabledFor)', () => {
|
|
128
|
+
it('is exactly the emitsPush types, COMPENSATION excluded', () => {
|
|
129
|
+
expect([...PUSH_NOTIFICATION_TYPES].sort((a, b) => a.localeCompare(b)))
|
|
130
|
+
.toEqual(NOTIFICATION_TYPES.filter((t) => NOTIFICATION_REGISTRY[t].emitsPush).sort((a, b) => a.localeCompare(b)));
|
|
131
|
+
expect(PUSH_NOTIFICATION_TYPES).not.toContain('COMPENSATION');
|
|
132
|
+
expect(PUSH_NOTIFICATION_TYPES).toContain('MATCH_RESULT');
|
|
133
|
+
expect(PUSH_NOTIFICATION_TYPES).toHaveLength(8);
|
|
134
|
+
});
|
|
135
|
+
it('is opt-out: absent / null / empty / true all enable, only explicit false suppresses', () => {
|
|
136
|
+
expect(pushEnabledFor(undefined, 'MATCH_RESULT')).toBe(true);
|
|
137
|
+
expect(pushEnabledFor(null, 'MATCH_RESULT')).toBe(true);
|
|
138
|
+
expect(pushEnabledFor({}, 'MATCH_RESULT')).toBe(true);
|
|
139
|
+
expect(pushEnabledFor({ MATCH_RESULT: true }, 'MATCH_RESULT')).toBe(true);
|
|
140
|
+
expect(pushEnabledFor({ MATCH_RESULT: false }, 'MATCH_RESULT')).toBe(false);
|
|
141
|
+
// disabling one type does not affect another
|
|
142
|
+
expect(pushEnabledFor({ MATCH_RESULT: false }, 'INJURY')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NOTIFICATION_TYPES } from './notification-type';
|
|
1
2
|
function readString(value) {
|
|
2
3
|
return typeof value === 'string' && value !== '' ? value : null;
|
|
3
4
|
}
|
|
@@ -107,3 +108,7 @@ export const NOTIFICATION_REGISTRY = {
|
|
|
107
108
|
catalogKeys: []
|
|
108
109
|
}
|
|
109
110
|
};
|
|
111
|
+
// The notification types that emit a mobile push (everything except COMPENSATION). Source of truth = each
|
|
112
|
+
// descriptor's emitsPush, so this cannot drift. This is the user-toggleable set surfaced in the mobile app's
|
|
113
|
+
// notification settings and validated by the API before persisting a preference.
|
|
114
|
+
export const PUSH_NOTIFICATION_TYPES = NOTIFICATION_TYPES.filter((type) => NOTIFICATION_REGISTRY[type].emitsPush);
|
|
@@ -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) {
|