volleyballsimtypes 0.0.514 → 0.0.516
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 +1 -2
- package/dist/cjs/src/api/tactics-player-refs.js +2 -2
- package/dist/cjs/src/data/models/tactics.d.ts +2 -4
- package/dist/cjs/src/data/models/tactics.js +0 -5
- package/dist/cjs/src/data/transformers/tactics.js +4 -36
- package/dist/cjs/src/data/transformers/tactics.test.js +9 -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 +2 -0
- package/dist/cjs/src/service/team/default-base-config.js +59 -3
- package/dist/cjs/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/cjs/src/service/team/default-base-config.test.js +44 -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 +19 -5
- package/dist/cjs/src/service/team/schemas/tactics.z.js +32 -33
- package/dist/cjs/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +19 -5
- package/dist/cjs/src/service/team/tactics.d.ts +0 -2
- package/dist/cjs/src/service/team/tactics.js +1 -2
- package/dist/esm/src/api/index.d.ts +1 -2
- package/dist/esm/src/api/tactics-player-refs.js +2 -2
- package/dist/esm/src/data/models/tactics.d.ts +2 -4
- package/dist/esm/src/data/models/tactics.js +0 -5
- package/dist/esm/src/data/transformers/tactics.js +4 -36
- package/dist/esm/src/data/transformers/tactics.test.js +9 -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 +2 -0
- package/dist/esm/src/service/team/default-base-config.js +59 -3
- package/dist/esm/src/service/team/default-base-config.test.d.ts +1 -0
- package/dist/esm/src/service/team/default-base-config.test.js +42 -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 +19 -5
- package/dist/esm/src/service/team/schemas/tactics.z.js +32 -33
- package/dist/esm/src/service/team/schemas/tactics.z.test.js +25 -92
- package/dist/esm/src/service/team/schemas/team.z.d.ts +19 -5
- package/dist/esm/src/service/team/tactics.d.ts +0 -2
- package/dist/esm/src/service/team/tactics.js +1 -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 {
|
|
@@ -140,7 +140,6 @@ export interface Tactics {
|
|
|
140
140
|
offensivePreferences: ApiOffensivePreference[];
|
|
141
141
|
systemSets?: ApiSystemSet[];
|
|
142
142
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
143
|
-
serveAttackShares?: Record<string, number>;
|
|
144
143
|
basePositions?: BaseConfig;
|
|
145
144
|
blockingPreferences: ApiBlockingPreference[];
|
|
146
145
|
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
@@ -26,9 +26,9 @@ function collectTacticsPlayerIds(config) {
|
|
|
26
26
|
config.secondLibero,
|
|
27
27
|
...((config.liberoReplacementSets ?? []).flat()),
|
|
28
28
|
...(config.designatedSetters ?? []),
|
|
29
|
-
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.
|
|
29
|
+
...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.priorities))),
|
|
30
30
|
...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
|
|
31
|
-
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.
|
|
31
|
+
...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.priorities))),
|
|
32
32
|
...((config.blockingPreferences ?? []).flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
33
33
|
...((config.blockingPreferenceSets ?? []).flat().flatMap(p => [...Object.keys(p.commit ?? {}), ...Object.values(p.noBlock ?? {}).flat()])),
|
|
34
34
|
// 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 {
|
|
@@ -77,7 +77,6 @@ export interface TacticsAttributes {
|
|
|
77
77
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
78
78
|
system_sets?: SystemSetAttributes[];
|
|
79
79
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
80
|
-
serve_attack_shares: Record<PlayerId, number>;
|
|
81
80
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
82
81
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
83
82
|
libero_injury?: LiberoInjuryAttributes;
|
|
@@ -87,7 +86,7 @@ export interface TacticsAttributes {
|
|
|
87
86
|
}
|
|
88
87
|
export type TacticsPk = 'team_id';
|
|
89
88
|
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' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | '
|
|
89
|
+
export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
|
|
91
90
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
92
91
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
93
92
|
team_id: string;
|
|
@@ -104,7 +103,6 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
104
103
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
105
104
|
system_sets?: SystemSetAttributes[];
|
|
106
105
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
107
|
-
serve_attack_shares: Record<PlayerId, number>;
|
|
108
106
|
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
109
107
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
110
108
|
libero_injury?: LiberoInjuryAttributes;
|
|
@@ -75,11 +75,6 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
75
75
|
type: sequelize_1.DataTypes.JSONB,
|
|
76
76
|
allowNull: true
|
|
77
77
|
},
|
|
78
|
-
serve_attack_shares: {
|
|
79
|
-
type: sequelize_1.DataTypes.JSONB,
|
|
80
|
-
allowNull: false,
|
|
81
|
-
defaultValue: {}
|
|
82
|
-
},
|
|
83
78
|
replace_knocked_immediately: {
|
|
84
79
|
type: sequelize_1.DataTypes.JSONB,
|
|
85
80
|
allowNull: true
|
|
@@ -73,7 +73,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
73
73
|
designated_setters: tactics.designatedSetters.map((s) => s.id),
|
|
74
74
|
offensive_preferences: tactics.offensivePreferences.map(p => ({
|
|
75
75
|
rotation: p.rotation,
|
|
76
|
-
|
|
76
|
+
priorities: p.priorities,
|
|
77
77
|
tempos: p.tempos
|
|
78
78
|
})),
|
|
79
79
|
system_sets: tactics.systemSets?.map(s => ({
|
|
@@ -82,10 +82,9 @@ function transformToAttributes(tactics, teamId) {
|
|
|
82
82
|
})),
|
|
83
83
|
offensive_preference_sets: tactics.offensivePreferenceSets?.map(prefs => prefs.map(p => ({
|
|
84
84
|
rotation: p.rotation,
|
|
85
|
-
|
|
85
|
+
priorities: p.priorities,
|
|
86
86
|
tempos: p.tempos
|
|
87
87
|
}))),
|
|
88
|
-
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
89
88
|
base_positions: tactics.basePositions,
|
|
90
89
|
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
91
90
|
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -181,7 +180,7 @@ function transformToObject(model, roster) {
|
|
|
181
180
|
designatedSetters: (model.designated_setters ?? []).map((id) => findPlayer(id, roster)),
|
|
182
181
|
offensivePreferences: (model.offensive_preferences ?? []).map(p => ({
|
|
183
182
|
rotation: p.rotation,
|
|
184
|
-
|
|
183
|
+
priorities: p.priorities,
|
|
185
184
|
tempos: p.tempos
|
|
186
185
|
})),
|
|
187
186
|
systemSets: model.system_sets?.map(s => ({
|
|
@@ -190,24 +189,9 @@ function transformToObject(model, roster) {
|
|
|
190
189
|
})),
|
|
191
190
|
offensivePreferenceSets: model.offensive_preference_sets?.map(prefs => prefs.map(p => ({
|
|
192
191
|
rotation: p.rotation,
|
|
193
|
-
|
|
192
|
+
priorities: p.priorities,
|
|
194
193
|
tempos: p.tempos
|
|
195
194
|
}))),
|
|
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
195
|
basePositions: model.base_positions ?? undefined,
|
|
212
196
|
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock })),
|
|
213
197
|
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, commit: p.commit, noBlock: p.noBlock }))),
|
|
@@ -354,21 +338,6 @@ function tacticsColumnsToApiDto(model) {
|
|
|
354
338
|
offensivePreferences: model.offensive_preferences,
|
|
355
339
|
systemSets: model.system_sets,
|
|
356
340
|
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
341
|
basePositions: model.base_positions ?? undefined,
|
|
373
342
|
blockingPreferences: model.blocking_preferences,
|
|
374
343
|
blockingPreferenceSets: model.blocking_preference_sets,
|
|
@@ -477,7 +446,6 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
477
446
|
offensive_preferences: config.offensivePreferences,
|
|
478
447
|
system_sets: config.systemSets,
|
|
479
448
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
480
|
-
serve_attack_shares: config.serveAttackShares ?? {},
|
|
481
449
|
base_positions: config.basePositions,
|
|
482
450
|
blocking_preferences: config.blockingPreferences ?? [],
|
|
483
451
|
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' });
|
|
@@ -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,5 +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>>>;
|
|
12
|
+
readonly attackContact?: Record<string, Record<string, BaseCoord>>;
|
|
11
13
|
}
|
|
@@ -88,11 +88,40 @@ const COORDS = {
|
|
|
88
88
|
6: { 1: { d: 5.35, r: 2.35 }, 2: { d: 2.05, r: 2.1 }, 3: { d: 0.8, r: 4 }, 4: { d: 2.1, r: -1.95 }, 5: { d: 4.9, r: -1.4 }, 6: { d: 0.4, r: 1.3 } }
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
+
// Per-attacker attack subbases (owner 2026-08-12): the point-of-attack ATK_L/M/R coverage shapes are replaced by ONE
|
|
92
|
+
// subbase per attacker, keyed by the attacker's ZONE (ATK_Z1..ATK_Z6). Each is the whole-team formation used when the
|
|
93
|
+
// attacker at that zone gets the ball. A FRONT-row zone (2/3/4) reuses its lane's old ATK_L/M/R shape, where the lane
|
|
94
|
+
// is ranked by that zone's ATTACK-base lateral this rotation (leftmost r -> ATK_L, middle -> ATK_M, rightmost -> ATK_R);
|
|
95
|
+
// a BACK-row zone (1/5/6) replicates the ATTACK base. Derived from COORDS so the code default and the migration agree.
|
|
96
|
+
// The old ATK_L/M/R stay in COORDS as the derivation SOURCE but are NOT exposed in the default (below).
|
|
97
|
+
const FRONT_ZONES = [2, 3, 4];
|
|
98
|
+
const BACK_ZONES = [1, 5, 6];
|
|
99
|
+
function defaultAttackSubbases() {
|
|
100
|
+
const cloneFormation = (zones) => {
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const [zone, c] of Object.entries(zones))
|
|
103
|
+
out[zone] = { d: c.d, r: c.r };
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
const out = { ATK_Z1: {}, ATK_Z2: {}, ATK_Z3: {}, ATK_Z4: {}, ATK_Z5: {}, ATK_Z6: {} };
|
|
107
|
+
for (let rot = 1; rot <= 6; rot++) {
|
|
108
|
+
const ranked = [...FRONT_ZONES].sort((a, b) => COORDS.ATTACK[rot][a].r - COORDS.ATTACK[rot][b].r);
|
|
109
|
+
const laneFor = { [ranked[0]]: 'ATK_L', [ranked[1]]: 'ATK_M', [ranked[2]]: 'ATK_R' };
|
|
110
|
+
for (const z of FRONT_ZONES)
|
|
111
|
+
out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS[laneFor[z]][rot]);
|
|
112
|
+
for (const z of BACK_ZONES)
|
|
113
|
+
out['ATK_Z' + String(z)][String(rot)] = cloneFormation(COORDS.ATTACK[rot]);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
91
117
|
// The default coordinate table as a BaseConfig.coords (jsonb string keys). Shared, readonly; materialized (and so
|
|
92
|
-
// serialized) per team on save, so sharing the reference in memory is safe.
|
|
118
|
+
// serialized) per team on save, so sharing the reference in memory is safe. ATK_L/M/R are dropped from the exposed
|
|
119
|
+
// default (replaced by ATK_Z<n>); everything else is copied verbatim from COORDS.
|
|
93
120
|
const DEFAULT_COORDS = (() => {
|
|
94
121
|
const out = {};
|
|
95
122
|
for (const [formation, rotations] of Object.entries(COORDS)) {
|
|
123
|
+
if (formation === 'ATK_L' || formation === 'ATK_M' || formation === 'ATK_R')
|
|
124
|
+
continue; // replaced by ATK_Z<n>
|
|
96
125
|
out[formation] = {};
|
|
97
126
|
for (const [rotation, zones] of Object.entries(rotations)) {
|
|
98
127
|
out[formation][rotation] = {};
|
|
@@ -100,8 +129,35 @@ const DEFAULT_COORDS = (() => {
|
|
|
100
129
|
out[formation][rotation][zone] = { d: coord.d, r: coord.r };
|
|
101
130
|
}
|
|
102
131
|
}
|
|
103
|
-
return out;
|
|
132
|
+
return { ...out, ...defaultAttackSubbases() };
|
|
104
133
|
})();
|
|
134
|
+
// The default desired-contact per attacker (trajectory-set, owner 2026-08-12 decision D9): a near-net hitting point
|
|
135
|
+
// in each attacker's lane, NOT the old "set to the standing dot" location. `contact.d` = a near-net depth for the
|
|
136
|
+
// front row or the pipe depth for the back row; `contact.r` = the attacker's ATTACK-dot lateral clamped to the
|
|
137
|
+
// antenna. Keyed [rotation][zone] -> {d,r}. The near-net depth + antenna are first-cut calibration knobs. The sim
|
|
138
|
+
// does not read this yet (added with the engine phase); storing it here is inert. See plans/trajectory-set-and-approach.md.
|
|
139
|
+
const NEAR_NET_CONTACT_DEPTH = 0.5; // front-row contact, m off the net
|
|
140
|
+
const PIPE_CONTACT_DEPTH = 1.8; // back-row (pipe) contact, m off the net (matches the sim's BACKROW_SET_DEPTH)
|
|
141
|
+
const CONTACT_ANTENNA = 4.3; // keep the contact just inside the sideline / antenna
|
|
142
|
+
// LAZY + memoized (not an eager IIFE): `isBackRow` lives in the match module, which sits in a load cycle with the
|
|
143
|
+
// team barrel, so calling it at module-load time hits an uninitialised import. Computed once on first use, then the
|
|
144
|
+
// shared readonly reference is reused (like DEFAULT_COORDS; serialized per team on save, so sharing is safe).
|
|
145
|
+
let cachedAttackContact;
|
|
146
|
+
function defaultAttackContact() {
|
|
147
|
+
if (cachedAttackContact != null)
|
|
148
|
+
return cachedAttackContact;
|
|
149
|
+
const out = {};
|
|
150
|
+
for (const [rotation, zones] of Object.entries(COORDS.ATTACK)) {
|
|
151
|
+
out[rotation] = {};
|
|
152
|
+
for (const [zone, coord] of Object.entries(zones)) {
|
|
153
|
+
const backRow = (0, match_1.isBackRow)(Number(zone));
|
|
154
|
+
const r = Math.max(-CONTACT_ANTENNA, Math.min(CONTACT_ANTENNA, coord.r));
|
|
155
|
+
out[rotation][zone] = { d: backRow ? PIPE_CONTACT_DEPTH : NEAR_NET_CONTACT_DEPTH, r };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
cachedAttackContact = out;
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
105
161
|
// The owner's corrected per-point-of-attack block map for the 5-1 (exported 2026-08-09), keyed
|
|
106
162
|
// [DEF_L|DEF_M|DEF_R][rotation][zone] -> canBlock. `false` = that front-row zone sits OUT of the block for an attack
|
|
107
163
|
// from that side; `true` = reads/joins the wall. Only the front-row zones in play for the rotation are tagged. Same
|
|
@@ -167,5 +223,5 @@ function defaultReceiveMap(system) {
|
|
|
167
223
|
// 5-1 uses the owner's exported receive map, the other systems derive their receive map per system.
|
|
168
224
|
function defaultBaseConfig(system) {
|
|
169
225
|
const receive = system === rotation_system_1.RotationSystemEnum.FIVE_ONE ? DEFAULT_5_1_RECEIVE : defaultReceiveMap(system);
|
|
170
|
-
return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING };
|
|
226
|
+
return { coords: DEFAULT_COORDS, receive, blocking: DEFAULT_BLOCKING, attackContact: defaultAttackContact() };
|
|
171
227
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const rotation_system_1 = require("./rotation-system");
|
|
4
|
+
const default_base_config_1 = require("./default-base-config");
|
|
5
|
+
// Back-row rotational zones (right/left/middle back). Front row is {2,3,4}.
|
|
6
|
+
const BACK_ROW = new Set([1, 5, 6]);
|
|
7
|
+
// The trajectory-set default desired-contact (owner D9, 2026-08-12): a near-net hitting point in each attacker's
|
|
8
|
+
// lane, NOT the old set-to-the-standing-dot location. Front row contacts sit ~0.5 m off the net; back row on the
|
|
9
|
+
// ~1.8 m pipe line; the lateral is the ATTACK-dot's r clamped inside the antenna (|r| <= 4.3).
|
|
10
|
+
describe('defaultBaseConfig attackContact (trajectory-set default)', () => {
|
|
11
|
+
const cfg = (0, default_base_config_1.defaultBaseConfig)(rotation_system_1.RotationSystemEnum.FIVE_ONE);
|
|
12
|
+
it('provides a finite desired contact for every rotation and zone', () => {
|
|
13
|
+
expect(cfg.attackContact).toBeDefined();
|
|
14
|
+
for (let rot = 1; rot <= 6; rot++) {
|
|
15
|
+
for (let zone = 1; zone <= 6; zone++) {
|
|
16
|
+
const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
|
|
17
|
+
expect(c).toBeDefined();
|
|
18
|
+
expect(Number.isFinite(c?.d)).toBe(true);
|
|
19
|
+
expect(Number.isFinite(c?.r)).toBe(true);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it('places front-row contacts near the net (0.5 m) and back-row on the pipe line (1.8 m)', () => {
|
|
24
|
+
for (let rot = 1; rot <= 6; rot++) {
|
|
25
|
+
for (let zone = 1; zone <= 6; zone++) {
|
|
26
|
+
const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
|
|
27
|
+
expect(c.d).toBeCloseTo(BACK_ROW.has(zone) ? 1.8 : 0.5);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
it('clamps every contact inside the antenna (|r| <= 4.3), even for a wide-pin attack dot', () => {
|
|
32
|
+
for (let rot = 1; rot <= 6; rot++) {
|
|
33
|
+
for (let zone = 1; zone <= 6; zone++) {
|
|
34
|
+
const c = cfg.attackContact?.[String(rot)]?.[String(zone)];
|
|
35
|
+
expect(Math.abs(c.r)).toBeLessThanOrEqual(4.3 + 1e-9);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
it('keeps the contact on the attacker approach-lane side (rotation 1 OH wide dot r=5 -> +4.3)', () => {
|
|
40
|
+
const c = cfg.attackContact?.['1']?.['4'];
|
|
41
|
+
expect(c.d).toBeCloseTo(0.5); // front row, near the net
|
|
42
|
+
expect(c.r).toBeCloseTo(4.3); // clamped from the OH's wide ATTACK dot (r=5), same (positive) side
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -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
|
}
|
|
@@ -261,23 +261,32 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
261
261
|
designatedSetters: z.ZodDefault<z.ZodArray<z.ZodCustom<Player, Player>>>;
|
|
262
262
|
offensivePreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
263
263
|
rotation: z.ZodNumber;
|
|
264
|
-
|
|
264
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
265
|
+
HIGH: "HIGH";
|
|
266
|
+
MID: "MID";
|
|
267
|
+
LOW: "LOW";
|
|
268
|
+
NONE: "NONE";
|
|
269
|
+
}>>;
|
|
265
270
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
266
|
-
FAST: "FAST";
|
|
267
271
|
HIGH: "HIGH";
|
|
272
|
+
FAST: "FAST";
|
|
268
273
|
}>>>;
|
|
269
274
|
}, z.core.$strip>>>;
|
|
270
|
-
serveAttackShares: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
271
275
|
systemSets: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
272
276
|
rotationSystem: z.ZodEnum<typeof RotationSystemEnum>;
|
|
273
277
|
designatedSetters: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
274
278
|
}, z.core.$strip>>>;
|
|
275
279
|
offensivePreferenceSets: z.ZodOptional<z.ZodArray<z.ZodArray<z.ZodObject<{
|
|
276
280
|
rotation: z.ZodNumber;
|
|
277
|
-
|
|
281
|
+
priorities: z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
282
|
+
HIGH: "HIGH";
|
|
283
|
+
MID: "MID";
|
|
284
|
+
LOW: "LOW";
|
|
285
|
+
NONE: "NONE";
|
|
286
|
+
}>>;
|
|
278
287
|
tempos: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
279
|
-
FAST: "FAST";
|
|
280
288
|
HIGH: "HIGH";
|
|
289
|
+
FAST: "FAST";
|
|
281
290
|
}>>>;
|
|
282
291
|
}, z.core.$strip>>>>;
|
|
283
292
|
blockingPreferences: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
@@ -304,7 +313,12 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
304
313
|
r: z.ZodNumber;
|
|
305
314
|
}, z.core.$strip>>>>;
|
|
306
315
|
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
316
|
+
receiveByPlayer: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>;
|
|
307
317
|
blocking: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>>>;
|
|
318
|
+
attackContact: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
319
|
+
d: z.ZodNumber;
|
|
320
|
+
r: z.ZodNumber;
|
|
321
|
+
}, z.core.$strip>>>>;
|
|
308
322
|
}, z.core.$strip>>;
|
|
309
323
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
310
324
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|