volleyballsimtypes 0.0.515 → 0.0.517
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 +2 -2
- package/dist/cjs/src/api/tactics-player-refs.d.ts +2 -2
- package/dist/cjs/src/api/tactics-player-refs.js +5 -4
- package/dist/cjs/src/data/models/tactics.d.ts +4 -4
- package/dist/cjs/src/data/models/tactics.js +5 -5
- package/dist/cjs/src/data/transformers/tactics.js +43 -36
- package/dist/cjs/src/data/transformers/tactics.test.js +34 -46
- package/dist/cjs/src/service/team/attack-priority.d.ts +3 -0
- package/dist/cjs/src/service/team/attack-priority.js +11 -0
- package/dist/cjs/src/service/team/base-config.d.ts +1 -0
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/offensive-preference.d.ts +2 -1
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +16 -5
- package/dist/cjs/src/service/team/schemas/tactics.z.js +55 -32
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +59 -63
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +16 -5
- package/dist/cjs/src/service/team/tactics.d.ts +2 -2
- package/dist/cjs/src/service/team/tactics.js +2 -2
- package/dist/esm/src/api/index.d.ts +2 -2
- package/dist/esm/src/api/tactics-player-refs.d.ts +2 -2
- package/dist/esm/src/api/tactics-player-refs.js +5 -4
- package/dist/esm/src/data/models/tactics.d.ts +4 -4
- package/dist/esm/src/data/models/tactics.js +5 -5
- package/dist/esm/src/data/transformers/tactics.js +43 -36
- package/dist/esm/src/data/transformers/tactics.test.js +34 -46
- package/dist/esm/src/service/team/attack-priority.d.ts +3 -0
- package/dist/esm/src/service/team/attack-priority.js +8 -0
- package/dist/esm/src/service/team/base-config.d.ts +1 -0
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/offensive-preference.d.ts +2 -1
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +16 -5
- package/dist/esm/src/service/team/schemas/tactics.z.js +55 -32
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +59 -63
- package/dist/esm/src/service/team/schemas/team.z.d.ts +16 -5
- package/dist/esm/src/service/team/tactics.d.ts +2 -2
- package/dist/esm/src/service/team/tactics.js +2 -2
- package/package.json +1 -1
|
@@ -97,7 +97,7 @@ export interface ApiDesignatedSub extends ApiSetSubConfig {
|
|
|
97
97
|
}
|
|
98
98
|
export interface ApiOffensivePreference {
|
|
99
99
|
rotation: number;
|
|
100
|
-
|
|
100
|
+
priorities: Record<string, 'HIGH' | 'MID' | 'LOW' | 'NONE'>;
|
|
101
101
|
tempos?: Record<string, 'FAST' | 'HIGH'>;
|
|
102
102
|
}
|
|
103
103
|
export interface ApiBlockingPreference {
|
|
@@ -137,10 +137,10 @@ export interface Tactics {
|
|
|
137
137
|
liberoReplacementSets?: string[][];
|
|
138
138
|
rotationSystem: RotationSystemEnum;
|
|
139
139
|
designatedSetters: string[];
|
|
140
|
+
emergencySetterOrder: string[];
|
|
140
141
|
offensivePreferences: ApiOffensivePreference[];
|
|
141
142
|
systemSets?: ApiSystemSet[];
|
|
142
143
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
143
|
-
serveAttackShares?: Record<string, number>;
|
|
144
144
|
basePositions?: BaseConfig;
|
|
145
145
|
blockingPreferences: ApiBlockingPreference[];
|
|
146
146
|
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
@@ -2,8 +2,8 @@ import type { Tactics } from '.';
|
|
|
2
2
|
/**
|
|
3
3
|
* Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
|
|
4
4
|
* replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
|
|
5
|
-
* setters (flat and per-set), the keys of the offensive-weight map, the blocking
|
|
6
|
-
* replacements and the libero-injury reserves.
|
|
5
|
+
* setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
|
|
6
|
+
* commit + no-block ids, injury replacements and the libero-injury reserves.
|
|
7
7
|
*
|
|
8
8
|
* There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
|
|
9
9
|
* slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
|
|
@@ -4,8 +4,8 @@ exports.collectTacticsPlayerIds = collectTacticsPlayerIds;
|
|
|
4
4
|
/**
|
|
5
5
|
* Every player id a tactics config references, anywhere: the six court slots and the libero, the bench, libero
|
|
6
6
|
* replacements (flat and per-set), the second libero, designated subs and their per-set overrides, designated
|
|
7
|
-
* setters (flat and per-set), the keys of the offensive-weight map, the blocking
|
|
8
|
-
* replacements and the libero-injury reserves.
|
|
7
|
+
* setters (flat and per-set), the emergency setter order, the keys of the offensive-weight map, the blocking
|
|
8
|
+
* commit + no-block ids, injury replacements and the libero-injury reserves.
|
|
9
9
|
*
|
|
10
10
|
* There is exactly ONE implementation of this on purpose. It backs three rules that must agree or a player
|
|
11
11
|
* slips through: the dismiss/retire guard (a referenced player cannot be removed), the event rarity check
|
|
@@ -26,9 +26,10 @@ function collectTacticsPlayerIds(config) {
|
|
|
26
26
|
config.secondLibero,
|
|
27
27
|
...((config.liberoReplacementSets ?? []).flat()),
|
|
28
28
|
...(config.designatedSetters ?? []),
|
|
29
|
-
...(
|
|
29
|
+
...(config.emergencySetterOrder ?? []),
|
|
30
|
+
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
|
|
30
31
|
...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
|
|
31
|
-
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.
|
|
32
|
+
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
|
|
32
33
|
...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
33
34
|
...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
34
35
|
// Injury replacements reference reserves (roster players outside the lineup); roster membership is the only
|
|
@@ -31,7 +31,7 @@ export interface DesignatedSubAttributes extends SetSubConfigAttributes {
|
|
|
31
31
|
}
|
|
32
32
|
export interface OffensivePreferenceAttributes {
|
|
33
33
|
rotation: number;
|
|
34
|
-
|
|
34
|
+
priorities: Record<PlayerId, 'HIGH' | 'MID' | 'LOW' | 'NONE'>;
|
|
35
35
|
tempos?: Record<PlayerId, 'FAST' | 'HIGH'>;
|
|
36
36
|
}
|
|
37
37
|
export interface BlockingPreferenceAttributes {
|
|
@@ -74,10 +74,10 @@ export interface TacticsAttributes {
|
|
|
74
74
|
libero_replacement_sets?: PlayerId[][];
|
|
75
75
|
rotation_system: RotationSystemEnum;
|
|
76
76
|
designated_setters: PlayerId[];
|
|
77
|
+
emergency_setter_order: PlayerId[];
|
|
77
78
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
78
79
|
system_sets?: SystemSetAttributes[];
|
|
79
80
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
80
|
-
serve_attack_shares: Record<PlayerId, number>;
|
|
81
81
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
82
82
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
83
83
|
libero_injury?: LiberoInjuryAttributes;
|
|
@@ -87,7 +87,7 @@ export interface TacticsAttributes {
|
|
|
87
87
|
}
|
|
88
88
|
export type TacticsPk = 'team_id';
|
|
89
89
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
90
|
-
export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | '
|
|
90
|
+
export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'emergency_setter_order' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
|
|
91
91
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
92
92
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
93
93
|
team_id: string;
|
|
@@ -101,10 +101,10 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
101
101
|
libero_replacement_sets?: PlayerId[][];
|
|
102
102
|
rotation_system: RotationSystemEnum;
|
|
103
103
|
designated_setters: PlayerId[];
|
|
104
|
+
emergency_setter_order: PlayerId[];
|
|
104
105
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
105
106
|
system_sets?: SystemSetAttributes[];
|
|
106
107
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
107
|
-
serve_attack_shares: Record<PlayerId, number>;
|
|
108
108
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
109
109
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
110
110
|
libero_injury?: LiberoInjuryAttributes;
|
|
@@ -62,6 +62,11 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
62
62
|
allowNull: false,
|
|
63
63
|
defaultValue: []
|
|
64
64
|
},
|
|
65
|
+
emergency_setter_order: {
|
|
66
|
+
type: sequelize_1.DataTypes.ARRAY(sequelize_1.DataTypes.UUID),
|
|
67
|
+
allowNull: false,
|
|
68
|
+
defaultValue: []
|
|
69
|
+
},
|
|
65
70
|
offensive_preferences: {
|
|
66
71
|
type: sequelize_1.DataTypes.JSONB,
|
|
67
72
|
allowNull: false,
|
|
@@ -75,11 +80,6 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
75
80
|
type: sequelize_1.DataTypes.JSONB,
|
|
76
81
|
allowNull: true
|
|
77
82
|
},
|
|
78
|
-
serve_attack_shares: {
|
|
79
|
-
type: sequelize_1.DataTypes.JSONB,
|
|
80
|
-
allowNull: false,
|
|
81
|
-
defaultValue: {}
|
|
82
|
-
},
|
|
83
83
|
replace_knocked_immediately: {
|
|
84
84
|
type: sequelize_1.DataTypes.JSONB,
|
|
85
85
|
allowNull: true
|
|
@@ -71,9 +71,10 @@ function transformToAttributes(tactics, teamId) {
|
|
|
71
71
|
})),
|
|
72
72
|
rotation_system: tactics.rotationSystem,
|
|
73
73
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
74
|
+
emergency_setter_order: tactics.emergencySetterOrder.map((p) => p.id),
|
|
74
75
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
75
76
|
rotation: p.rotation,
|
|
76
|
-
|
|
77
|
+
priorities: p.priorities,
|
|
77
78
|
tempos: p.tempos
|
|
78
79
|
})),
|
|
79
80
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -82,10 +83,9 @@ function transformToAttributes(tactics, teamId) {
|
|
|
82
83
|
})),
|
|
83
84
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
84
85
|
rotation: p.rotation,
|
|
85
|
-
|
|
86
|
+
priorities: p.priorities,
|
|
86
87
|
tempos: p.tempos
|
|
87
88
|
}))),
|
|
88
|
-
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
89
89
|
base_positions: tactics.basePositions,
|
|
90
90
|
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
91
91
|
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -179,9 +179,30 @@ function transformToObject(model, roster) {
|
|
|
179
179
|
})),
|
|
180
180
|
rotationSystem: model.rotation_system,
|
|
181
181
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
182
|
+
// LENIENT (roster.find): the coach-ranked emergency-setter chain. Keep only ids still on the roster, inside the
|
|
183
|
+
// squad pool (starters + starting libero + L2 + bench), and NOT a current designated setter (setters are pinned
|
|
184
|
+
// ahead of this list, never stored inside it), first-occurrence-wins on duplicates. So stale jsonb (a departed
|
|
185
|
+
// player, a promoted setter, a non-pool id) can never fail schema validation and crash a team load; the next
|
|
186
|
+
// tactics save rewrites it cleanly.
|
|
187
|
+
emergencySetterOrder: (() => {
|
|
188
|
+
const poolIds = new Set([...inLineupIds, ...(secondLibero != null ? [secondLibero.id] : [])]);
|
|
189
|
+
const setterIds = new Set(model.designated_setters ?? []);
|
|
190
|
+
const seen = new Set();
|
|
191
|
+
const healed = [];
|
|
192
|
+
for (const id of model.emergency_setter_order ?? []) {
|
|
193
|
+
if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
|
|
194
|
+
continue;
|
|
195
|
+
const p = roster.find((x) => x.id === id);
|
|
196
|
+
if (p == null)
|
|
197
|
+
continue;
|
|
198
|
+
seen.add(id);
|
|
199
|
+
healed.push(p);
|
|
200
|
+
}
|
|
201
|
+
return healed;
|
|
202
|
+
})(),
|
|
182
203
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
183
204
|
rotation: p.rotation,
|
|
184
|
-
|
|
205
|
+
priorities: p.priorities,
|
|
185
206
|
tempos: p.tempos
|
|
186
207
|
})),
|
|
187
208
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -190,24 +211,9 @@ function transformToObject(model, roster) {
|
|
|
190
211
|
})),
|
|
191
212
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
192
213
|
rotation: p.rotation,
|
|
193
|
-
|
|
214
|
+
priorities: p.priorities,
|
|
194
215
|
tempos: p.tempos
|
|
195
216
|
}))),
|
|
196
|
-
// LENIENT: keep only serve shares keyed by a current libero-replacement (flat or per-set) with a value in the
|
|
197
|
-
// 0-100 band, so a stale key (a released player, or a libero-replacement config change) can never fail schema
|
|
198
|
-
// validation and crash a team load; a dropped key = no serve share.
|
|
199
|
-
serveAttackShares: (() => {
|
|
200
|
-
const liberoReplacementIds = new Set([
|
|
201
|
-
...model.libero_replacements,
|
|
202
|
-
...(model.libero_replacement_sets ?? []).flat()
|
|
203
|
-
]);
|
|
204
|
-
const healed = {};
|
|
205
|
-
for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
|
|
206
|
-
if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
|
|
207
|
-
healed[id] = value;
|
|
208
|
-
}
|
|
209
|
-
return healed;
|
|
210
|
-
})(),
|
|
211
217
|
basePositions: model.base_positions ?? undefined,
|
|
212
218
|
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
213
219
|
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -351,24 +357,25 @@ function tacticsColumnsToApiDto(model) {
|
|
|
351
357
|
}),
|
|
352
358
|
rotationSystem: model.rotation_system,
|
|
353
359
|
designatedSetters: model.designated_setters,
|
|
360
|
+
// Same stale-data policy: drop any emergency-setter id the service load would drop (a current designated
|
|
361
|
+
// setter, or one outside the squad pool) so the UI never reloads and re-emits a rejected entry. Roster
|
|
362
|
+
// membership can't be checked here (ids only); the service heal covers that.
|
|
363
|
+
emergencySetterOrder: (() => {
|
|
364
|
+
const poolIds = new Set([...lineupIds, ...(validSecondLibero != null ? [validSecondLibero] : [])]);
|
|
365
|
+
const setterIds = new Set(model.designated_setters ?? []);
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
const healed = [];
|
|
368
|
+
for (const id of model.emergency_setter_order ?? []) {
|
|
369
|
+
if (setterIds.has(id) || seen.has(id) || !poolIds.has(id))
|
|
370
|
+
continue;
|
|
371
|
+
seen.add(id);
|
|
372
|
+
healed.push(id);
|
|
373
|
+
}
|
|
374
|
+
return healed;
|
|
375
|
+
})(),
|
|
354
376
|
offensivePreferences: model.offensive_preferences,
|
|
355
377
|
systemSets: model.system_sets,
|
|
356
378
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
357
|
-
// Same stale-data policy as the heals below: only serve shares keyed by a current libero-replacement (flat or
|
|
358
|
-
// per-set) with a value in the 0-100 band survive; an empty map surfaces as absent so the UI never reloads and
|
|
359
|
-
// re-emits a stale key.
|
|
360
|
-
serveAttackShares: (() => {
|
|
361
|
-
const liberoReplacementIds = new Set([
|
|
362
|
-
...model.libero_replacements,
|
|
363
|
-
...(model.libero_replacement_sets ?? []).flat()
|
|
364
|
-
]);
|
|
365
|
-
const healed = {};
|
|
366
|
-
for (const [id, value] of Object.entries(model.serve_attack_shares ?? {})) {
|
|
367
|
-
if (liberoReplacementIds.has(id) && typeof value === 'number' && value >= 0 && value <= 100)
|
|
368
|
-
healed[id] = value;
|
|
369
|
-
}
|
|
370
|
-
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
371
|
-
})(),
|
|
372
379
|
basePositions: model.base_positions ?? undefined,
|
|
373
380
|
blockingPreferences: model.blocking_preferences,
|
|
374
381
|
blockingPreferenceSets: model.blocking_preference_sets,
|
|
@@ -474,10 +481,10 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
474
481
|
libero_replacement_sets: config.liberoReplacementSets,
|
|
475
482
|
rotation_system: config.rotationSystem,
|
|
476
483
|
designated_setters: config.designatedSetters,
|
|
484
|
+
emergency_setter_order: config.emergencySetterOrder ?? [],
|
|
477
485
|
offensive_preferences: config.offensivePreferences,
|
|
478
486
|
system_sets: config.systemSets,
|
|
479
487
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
480
|
-
serve_attack_shares: config.serveAttackShares ?? {},
|
|
481
488
|
base_positions: config.basePositions,
|
|
482
489
|
blocking_preferences: config.blockingPreferences ?? [],
|
|
483
490
|
blocking_preference_sets: config.blockingPreferenceSets,
|
|
@@ -26,7 +26,7 @@ function makeTactics(designatedSubs) {
|
|
|
26
26
|
libero_replacement_sets: undefined,
|
|
27
27
|
rotation_system: '5-1',
|
|
28
28
|
designated_setters: ['lf'],
|
|
29
|
-
offensive_preferences: [{ rotation: 1,
|
|
29
|
+
offensive_preferences: [{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }],
|
|
30
30
|
blocking_preferences: []
|
|
31
31
|
};
|
|
32
32
|
}
|
|
@@ -38,7 +38,7 @@ function makeTactics(designatedSubs) {
|
|
|
38
38
|
(0, globals_1.expect)(dto.rotationSystem).toBe('5-1');
|
|
39
39
|
(0, globals_1.expect)(dto.liberoReplacements).toEqual(['lf']);
|
|
40
40
|
(0, globals_1.expect)(dto.lineup.bench).toEqual(['b1', 'b2']);
|
|
41
|
-
(0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1,
|
|
41
|
+
(0, globals_1.expect)(dto.offensivePreferences).toEqual([{ rotation: 1, priorities: { lf: 'HIGH', rf: 'MID' } }]);
|
|
42
42
|
});
|
|
43
43
|
(0, globals_1.it)('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
|
|
44
44
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([
|
|
@@ -133,32 +133,16 @@ function makeTactics(designatedSubs) {
|
|
|
133
133
|
(0, globals_1.it)('passes per-set tactics columns through to the API DTO shape', () => {
|
|
134
134
|
const model = makeTactics([]);
|
|
135
135
|
model.system_sets = [{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: ['lf'] }];
|
|
136
|
-
model.offensive_preference_sets = [[{ rotation: 1,
|
|
136
|
+
model.offensive_preference_sets = [[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]];
|
|
137
137
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
138
138
|
(0, globals_1.expect)(dto.systemSets).toEqual([{ rotationSystem: '5-1', designatedSetters: ['lf'] }]);
|
|
139
|
-
(0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1,
|
|
139
|
+
(0, globals_1.expect)(dto.offensivePreferenceSets).toEqual([[{ rotation: 1, priorities: { rf: 'HIGH', mf: 'MID' } }]]);
|
|
140
140
|
});
|
|
141
141
|
(0, globals_1.it)('leaves the per-set DTO fields undefined when the columns are absent', () => {
|
|
142
142
|
const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
|
|
143
143
|
(0, globals_1.expect)(dto.systemSets).toBeUndefined();
|
|
144
144
|
(0, globals_1.expect)(dto.offensivePreferenceSets).toBeUndefined();
|
|
145
145
|
});
|
|
146
|
-
(0, globals_1.it)('surfaces serve attack shares keyed by a libero replacement', () => {
|
|
147
|
-
const model = makeTactics([]);
|
|
148
|
-
model.serve_attack_shares = { lf: 30 }; // lf is the configured libero_replacement
|
|
149
|
-
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
150
|
-
(0, globals_1.expect)(dto.serveAttackShares).toEqual({ lf: 30 });
|
|
151
|
-
});
|
|
152
|
-
(0, globals_1.it)('drops a serve attack share keyed by a non-libero-replacement (stale heal)', () => {
|
|
153
|
-
const model = makeTactics([]);
|
|
154
|
-
model.serve_attack_shares = { lf: 30, rf: 40 }; // rf is not in libero_replacements (['lf'])
|
|
155
|
-
const dto = (0, tactics_1.tacticsColumnsToApiDto)(model);
|
|
156
|
-
(0, globals_1.expect)(dto.serveAttackShares).toEqual({ lf: 30 });
|
|
157
|
-
});
|
|
158
|
-
(0, globals_1.it)('leaves serveAttackShares undefined when the column is empty', () => {
|
|
159
|
-
const dto = (0, tactics_1.tacticsColumnsToApiDto)(makeTactics([]));
|
|
160
|
-
(0, globals_1.expect)(dto.serveAttackShares).toBeUndefined();
|
|
161
|
-
});
|
|
162
146
|
});
|
|
163
147
|
(0, globals_1.describe)('per-set tactics — service <-> row round-trip', () => {
|
|
164
148
|
const country = (0, test_helpers_1.makeCountry)();
|
|
@@ -176,7 +160,7 @@ function makeTactics(designatedSubs) {
|
|
|
176
160
|
{ rotationSystem: service_1.RotationSystemEnum.FIVE_ONE, designatedSetters: [p1] },
|
|
177
161
|
{ rotationSystem: service_1.RotationSystemEnum.SIX_ZERO, designatedSetters: [] }
|
|
178
162
|
],
|
|
179
|
-
offensivePreferenceSets: [[{ rotation: 1,
|
|
163
|
+
offensivePreferenceSets: [[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]
|
|
180
164
|
});
|
|
181
165
|
(0, globals_1.it)('transformFromTactics writes the per-set overrides as id-based jsonb columns', () => {
|
|
182
166
|
const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
|
|
@@ -184,7 +168,7 @@ function makeTactics(designatedSubs) {
|
|
|
184
168
|
{ rotationSystem: '5-1', designatedSetters: [p1.id] },
|
|
185
169
|
{ rotationSystem: '6-0', designatedSetters: [] }
|
|
186
170
|
]);
|
|
187
|
-
(0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1,
|
|
171
|
+
(0, globals_1.expect)(attrs.offensive_preference_sets).toEqual([[{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' } }]]);
|
|
188
172
|
});
|
|
189
173
|
(0, globals_1.it)('transformToTactics resolves the stored ids back to roster players', () => {
|
|
190
174
|
const attrs = (0, tactics_1.transformFromTactics)(tactics, 'team-1');
|
|
@@ -192,7 +176,7 @@ function makeTactics(designatedSubs) {
|
|
|
192
176
|
(0, globals_1.expect)(back.systemSets?.[0].rotationSystem).toBe(service_1.RotationSystemEnum.FIVE_ONE);
|
|
193
177
|
(0, globals_1.expect)(back.systemSets?.[0].designatedSetters[0].id).toBe(p1.id);
|
|
194
178
|
(0, globals_1.expect)(back.systemSets?.[1].designatedSetters).toEqual([]);
|
|
195
|
-
(0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].
|
|
179
|
+
(0, globals_1.expect)(back.offensivePreferenceSets?.[0][0].priorities).toEqual({ [p2.id]: 'HIGH', [p3.id]: 'MID' });
|
|
196
180
|
});
|
|
197
181
|
(0, globals_1.it)('omits the per-set columns entirely for an all-sets config', () => {
|
|
198
182
|
const flat = service_1.Tactics.create({
|
|
@@ -206,27 +190,6 @@ function makeTactics(designatedSubs) {
|
|
|
206
190
|
(0, globals_1.expect)(back.systemSets).toBeUndefined();
|
|
207
191
|
(0, globals_1.expect)(back.offensivePreferenceSets).toBeUndefined();
|
|
208
192
|
});
|
|
209
|
-
(0, globals_1.it)('round-trips serve attack shares keyed by a libero replacement', () => {
|
|
210
|
-
const withServe = service_1.Tactics.create({
|
|
211
|
-
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
212
|
-
liberoReplacements: [p5],
|
|
213
|
-
serveAttackShares: { [p5.id]: 35 }
|
|
214
|
-
});
|
|
215
|
-
const attrs = (0, tactics_1.transformFromTactics)(withServe, 'team-1');
|
|
216
|
-
(0, globals_1.expect)(attrs.serve_attack_shares).toEqual({ [p5.id]: 35 });
|
|
217
|
-
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
218
|
-
(0, globals_1.expect)(back.serveAttackShares).toEqual({ [p5.id]: 35 });
|
|
219
|
-
});
|
|
220
|
-
(0, globals_1.it)('drops a stale serve attack share key on load (lenient heal, no crash)', () => {
|
|
221
|
-
const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
|
|
222
|
-
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
223
|
-
liberoReplacements: [p5],
|
|
224
|
-
serveAttackShares: { [p5.id]: 35 }
|
|
225
|
-
}), 'team-1');
|
|
226
|
-
attrs.libero_replacements = [];
|
|
227
|
-
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
228
|
-
(0, globals_1.expect)(back.serveAttackShares).toEqual({});
|
|
229
|
-
});
|
|
230
193
|
(0, globals_1.it)('round-trips base positions (coords + reception participation)', () => {
|
|
231
194
|
const config = {
|
|
232
195
|
coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
|
|
@@ -255,11 +218,11 @@ function makeTactics(designatedSubs) {
|
|
|
255
218
|
const withTempos = service_1.Tactics.create({
|
|
256
219
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
257
220
|
liberoReplacements: [],
|
|
258
|
-
offensivePreferences: [{ rotation: 1,
|
|
221
|
+
offensivePreferences: [{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }]
|
|
259
222
|
});
|
|
260
223
|
const attrs = (0, tactics_1.transformFromTactics)(withTempos, 'team-1');
|
|
261
224
|
(0, globals_1.expect)(attrs.offensive_preferences).toEqual([
|
|
262
|
-
{ rotation: 1,
|
|
225
|
+
{ rotation: 1, priorities: { [p2.id]: 'HIGH', [p3.id]: 'MID' }, tempos: { [p2.id]: 'FAST', [p3.id]: 'HIGH' } }
|
|
263
226
|
]);
|
|
264
227
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
265
228
|
(0, globals_1.expect)(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
|
|
@@ -298,6 +261,31 @@ function makeTactics(designatedSubs) {
|
|
|
298
261
|
(0, globals_1.expect)(back.blockingPreferences[0].noBlock).toEqual({ DEF_L: [], DEF_M: [p4.id], DEF_R: [] });
|
|
299
262
|
(0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].noBlock).toEqual({ DEF_L: [p2.id], DEF_M: [], DEF_R: [] });
|
|
300
263
|
});
|
|
264
|
+
(0, globals_1.it)('round-trips the emergency setter order (non-setter pool members)', () => {
|
|
265
|
+
const withOrder = service_1.Tactics.create({
|
|
266
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
267
|
+
liberoReplacements: [],
|
|
268
|
+
emergencySetterOrder: [p2, p3]
|
|
269
|
+
});
|
|
270
|
+
const attrs = (0, tactics_1.transformFromTactics)(withOrder, 'team-1');
|
|
271
|
+
(0, globals_1.expect)(attrs.emergency_setter_order).toEqual([p2.id, p3.id]);
|
|
272
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
273
|
+
(0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id, p3.id]);
|
|
274
|
+
});
|
|
275
|
+
// The read path must never crash a team load on a stale order. Ids that are gone from the roster, outside the
|
|
276
|
+
// squad pool, or now a designated setter (setters are pinned, never stored) are silently dropped, order preserved.
|
|
277
|
+
(0, globals_1.it)('drops emergency-order ids that are gone or a designated setter on read (lenient)', () => {
|
|
278
|
+
const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
|
|
279
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
280
|
+
liberoReplacements: [],
|
|
281
|
+
rotationSystem: service_1.RotationSystemEnum.FIVE_ONE,
|
|
282
|
+
designatedSetters: [p1],
|
|
283
|
+
emergencySetterOrder: [p2]
|
|
284
|
+
}), 'team-1');
|
|
285
|
+
attrs.emergency_setter_order = [p1.id, 'gone', p2.id];
|
|
286
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
287
|
+
(0, globals_1.expect)(back.emergencySetterOrder.map(p => p.id)).toEqual([p2.id]);
|
|
288
|
+
});
|
|
301
289
|
});
|
|
302
290
|
(0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
|
|
303
291
|
const country = (0, test_helpers_1.makeCountry)();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRIORITY_WEIGHT = exports.ATTACK_PRIORITIES = void 0;
|
|
4
|
+
exports.ATTACK_PRIORITIES = ['HIGH', 'MID', 'LOW', 'NONE'];
|
|
5
|
+
// The relative pull of each priority. Tunable in one place; the engine reads these to weight the setter's target.
|
|
6
|
+
exports.PRIORITY_WEIGHT = {
|
|
7
|
+
HIGH: 20,
|
|
8
|
+
MID: 10,
|
|
9
|
+
LOW: 5,
|
|
10
|
+
NONE: 0
|
|
11
|
+
};
|
|
@@ -7,6 +7,7 @@ export interface BaseCoord {
|
|
|
7
7
|
export interface BaseConfig {
|
|
8
8
|
readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
|
|
9
9
|
readonly receive: Record<string, Record<string, boolean>>;
|
|
10
|
+
readonly receiveByPlayer?: Record<string, Record<string, boolean>>;
|
|
10
11
|
readonly blocking?: Record<string, Record<string, Record<string, boolean>>>;
|
|
11
12
|
readonly attackContact?: Record<string, Record<string, BaseCoord>>;
|
|
12
13
|
}
|
|
@@ -3,6 +3,7 @@ export * from './team-generator';
|
|
|
3
3
|
export * from './formation';
|
|
4
4
|
export * from './tactics';
|
|
5
5
|
export * from './base-config';
|
|
6
|
+
export * from './attack-priority';
|
|
6
7
|
export * from './default-base-config';
|
|
7
8
|
export * from './receive-overlap';
|
|
8
9
|
export * from './team-name';
|
|
@@ -19,6 +19,7 @@ __exportStar(require("./team-generator"), exports);
|
|
|
19
19
|
__exportStar(require("./formation"), exports);
|
|
20
20
|
__exportStar(require("./tactics"), exports);
|
|
21
21
|
__exportStar(require("./base-config"), exports);
|
|
22
|
+
__exportStar(require("./attack-priority"), exports);
|
|
22
23
|
__exportStar(require("./default-base-config"), exports);
|
|
23
24
|
__exportStar(require("./receive-overlap"), exports);
|
|
24
25
|
__exportStar(require("./team-name"), exports);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { AttackPriority } from './attack-priority';
|
|
1
2
|
export type TempoPreference = 'FAST' | 'HIGH';
|
|
2
3
|
export interface OffensivePreference {
|
|
3
4
|
readonly rotation: number;
|
|
4
|
-
readonly
|
|
5
|
+
readonly priorities: Record<string, AttackPriority>;
|
|
5
6
|
readonly tempos?: Record<string, TempoPreference>;
|
|
6
7
|
}
|
|
@@ -259,25 +259,35 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
259
259
|
liberoReplacementSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodCustom<Player, Player>>>>;
|
|
260
260
|
rotationSystem: z.ZodDefault<z.ZodEnum<typeof RotationSystemEnum>>;
|
|
261
261
|
designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
262
|
+
emergencySetterOrder: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
262
263
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
263
264
|
rotation: z.ZodNumber;
|
|
264
|
-
|
|
265
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
266
|
+
HIGH: "HIGH";
|
|
267
|
+
MID: "MID";
|
|
268
|
+
LOW: "LOW";
|
|
269
|
+
NONE: "NONE";
|
|
270
|
+
}>>;
|
|
265
271
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
266
|
-
FAST: "FAST";
|
|
267
272
|
HIGH: "HIGH";
|
|
273
|
+
FAST: "FAST";
|
|
268
274
|
}>>>;
|
|
269
275
|
}, z.core.$strip>>>;
|
|
270
|
-
serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
271
276
|
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
272
277
|
rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
|
|
273
278
|
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
274
279
|
}, z.core.$strip>>>;
|
|
275
280
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
276
281
|
rotation: z.ZodNumber;
|
|
277
|
-
|
|
282
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
283
|
+
HIGH: "HIGH";
|
|
284
|
+
MID: "MID";
|
|
285
|
+
LOW: "LOW";
|
|
286
|
+
NONE: "NONE";
|
|
287
|
+
}>>;
|
|
278
288
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
279
|
-
FAST: "FAST";
|
|
280
289
|
HIGH: "HIGH";
|
|
290
|
+
FAST: "FAST";
|
|
281
291
|
}>>>;
|
|
282
292
|
}, z.core.$strip>>>>;
|
|
283
293
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -304,6 +314,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
304
314
|
r: z.ZodNumber;
|
|
305
315
|
}, z.core.$strip>>>>;
|
|
306
316
|
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
317
|
+
receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
307
318
|
blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
|
|
308
319
|
attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
309
320
|
d: z.ZodNumber;
|