volleyballsimtypes 0.0.504 → 0.0.505
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 -1
- package/dist/cjs/src/data/models/tactics.d.ts +4 -2
- package/dist/cjs/src/data/models/tactics.js +4 -0
- package/dist/cjs/src/data/transformers/rally-event-injury.test.js +1 -1
- package/dist/cjs/src/data/transformers/rally-event-positional.test.js +10 -2
- package/dist/cjs/src/data/transformers/rally-event.js +3 -1
- package/dist/cjs/src/data/transformers/rally.test.js +2 -2
- package/dist/cjs/src/data/transformers/tactics.js +4 -0
- package/dist/cjs/src/data/transformers/tactics.test.js +24 -0
- package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
- package/dist/cjs/src/service/event/spike.d.ts +2 -4
- package/dist/cjs/src/service/event/spike.js +5 -3
- package/dist/cjs/src/service/team/base-config.d.ts +10 -0
- package/dist/cjs/src/service/team/base-config.js +14 -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/schemas/tactics.z.d.ts +7 -0
- package/dist/cjs/src/service/team/schemas/tactics.z.js +11 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +7 -0
- package/dist/cjs/src/service/team/tactics.d.ts +3 -0
- package/dist/cjs/src/service/team/tactics.js +2 -1
- package/dist/esm/src/api/index.d.ts +2 -1
- package/dist/esm/src/data/models/tactics.d.ts +4 -2
- package/dist/esm/src/data/models/tactics.js +4 -0
- package/dist/esm/src/data/transformers/rally-event-injury.test.js +1 -1
- package/dist/esm/src/data/transformers/rally-event-positional.test.js +10 -2
- package/dist/esm/src/data/transformers/rally-event.js +3 -1
- package/dist/esm/src/data/transformers/rally.test.js +2 -2
- package/dist/esm/src/data/transformers/tactics.js +4 -0
- package/dist/esm/src/data/transformers/tactics.test.js +24 -0
- package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
- package/dist/esm/src/service/event/spike.d.ts +2 -4
- package/dist/esm/src/service/event/spike.js +5 -3
- package/dist/esm/src/service/team/base-config.d.ts +10 -0
- package/dist/esm/src/service/team/base-config.js +11 -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/schemas/tactics.z.d.ts +7 -0
- package/dist/esm/src/service/team/schemas/tactics.z.js +11 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +7 -0
- package/dist/esm/src/service/team/tactics.d.ts +3 -0
- package/dist/esm/src/service/team/tactics.js +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
1
|
+
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export * from './tactics-player-refs';
|
|
3
3
|
export type Rally = DataProps<_Rally> & {
|
|
4
4
|
homePlayerPosition: PlayerPosition[];
|
|
@@ -140,6 +140,7 @@ export interface Tactics {
|
|
|
140
140
|
systemSets?: ApiSystemSet[];
|
|
141
141
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
142
142
|
serveAttackShares?: Record<string, number>;
|
|
143
|
+
basePositions?: BaseConfig;
|
|
143
144
|
blockingPreferences: ApiBlockingPreference[];
|
|
144
145
|
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
145
146
|
replaceKnockedImmediately?: Record<string, boolean>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
3
|
import { PlayerId, TeamId, TeamModel } from '.';
|
|
4
|
-
import { BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
4
|
+
import { BaseConfig, BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -82,10 +82,11 @@ export interface TacticsAttributes {
|
|
|
82
82
|
libero_injury?: LiberoInjuryAttributes;
|
|
83
83
|
blocking_preferences: BlockingPreferenceAttributes[];
|
|
84
84
|
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
85
|
+
base_positions?: BaseConfig;
|
|
85
86
|
}
|
|
86
87
|
export type TacticsPk = 'team_id';
|
|
87
88
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
88
|
-
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' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_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' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
|
|
89
90
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
90
91
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
91
92
|
team_id: string;
|
|
@@ -108,6 +109,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
108
109
|
libero_injury?: LiberoInjuryAttributes;
|
|
109
110
|
blocking_preferences: BlockingPreferenceAttributes[];
|
|
110
111
|
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
112
|
+
base_positions?: BaseConfig;
|
|
111
113
|
team: TeamModel;
|
|
112
114
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
113
115
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -100,6 +100,10 @@ class TacticsModel extends sequelize_1.Model {
|
|
|
100
100
|
blocking_preference_sets: {
|
|
101
101
|
type: sequelize_1.DataTypes.JSONB,
|
|
102
102
|
allowNull: true
|
|
103
|
+
},
|
|
104
|
+
base_positions: {
|
|
105
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
106
|
+
allowNull: true
|
|
103
107
|
}
|
|
104
108
|
}, {
|
|
105
109
|
sequelize,
|
|
@@ -41,7 +41,7 @@ const rally_event_1 = require("./rally-event");
|
|
|
41
41
|
score: 62.5,
|
|
42
42
|
target: 4,
|
|
43
43
|
failure: service_1.SpikeFailureEnum.NO_FAILURE,
|
|
44
|
-
type: service_1.SpikeTypeEnum.
|
|
44
|
+
type: service_1.SpikeTypeEnum.SPIKE,
|
|
45
45
|
...(incident != null ? { incident } : {})
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -14,7 +14,7 @@ const rally_event_1 = require("./rally-event");
|
|
|
14
14
|
score: 60,
|
|
15
15
|
target: 4,
|
|
16
16
|
failure: service_1.SpikeFailureEnum.NO_FAILURE,
|
|
17
|
-
type: service_1.SpikeTypeEnum.
|
|
17
|
+
type: service_1.SpikeTypeEnum.SPIKE,
|
|
18
18
|
contact: { x: 1.234, y: -0.5 },
|
|
19
19
|
landing: { x: -2.5, y: 6.789 },
|
|
20
20
|
speed: 92.456
|
|
@@ -43,7 +43,7 @@ const rally_event_1 = require("./rally-event");
|
|
|
43
43
|
(0, globals_1.expect)(decoded.digQuality).toBe(service_1.DigQualityEnum.MEDIOCRE);
|
|
44
44
|
});
|
|
45
45
|
(0, globals_1.it)('omits the positional keys when absent, and a legacy event decodes them as undefined', () => {
|
|
46
|
-
const spike = service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.
|
|
46
|
+
const spike = service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE });
|
|
47
47
|
const compact = (0, rally_event_1.transformToCompact)(spike, playerIndex);
|
|
48
48
|
(0, globals_1.expect)('c' in compact).toBe(false);
|
|
49
49
|
(0, globals_1.expect)('v' in compact).toBe(false);
|
|
@@ -53,4 +53,12 @@ const rally_event_1 = require("./rally-event");
|
|
|
53
53
|
(0, globals_1.expect)(decoded.speed).toBeUndefined();
|
|
54
54
|
(0, globals_1.expect)(decoded.digQuality).toBeUndefined();
|
|
55
55
|
});
|
|
56
|
+
(0, globals_1.it)('collapses legacy LINE_SHOT(1)/REVERSE_CROSS(2) to SPIKE(0) on decode (owner 2026-08-03)', () => {
|
|
57
|
+
// A zone-engine match persisted before the collapse carries type 1 or 2 in its jsonb; decode heals them to
|
|
58
|
+
// SPIKE so the match still opens. TIP(3) and up are untouched.
|
|
59
|
+
const base = (0, rally_event_1.transformToCompact)(service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE }), playerIndex);
|
|
60
|
+
(0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 1 }, roster).type).toBe(service_1.SpikeTypeEnum.SPIKE);
|
|
61
|
+
(0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 2 }, roster).type).toBe(service_1.SpikeTypeEnum.SPIKE);
|
|
62
|
+
(0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 3 }, roster).type).toBe(service_1.SpikeTypeEnum.TIP);
|
|
63
|
+
});
|
|
56
64
|
});
|
|
@@ -138,7 +138,9 @@ function transformToSet(event, roster) {
|
|
|
138
138
|
function transformToSpike(event, roster) {
|
|
139
139
|
return service_1.Spike.create({
|
|
140
140
|
failure: event.f,
|
|
141
|
-
|
|
141
|
+
// Legacy LINE_SHOT(1)/REVERSE_CROSS(2) collapse to SPIKE(0) (owner 2026-08-03): direction is redundant with
|
|
142
|
+
// the stored contact+landing coords. Maps old zone-engine matches so they keep decoding; new events emit 0/3-6.
|
|
143
|
+
type: event.t === 1 || event.t === 2 ? 0 : event.t,
|
|
142
144
|
playerId: roster[event.p],
|
|
143
145
|
target: event.a,
|
|
144
146
|
score: event.s,
|
|
@@ -19,7 +19,7 @@ const rally_1 = require("./rally");
|
|
|
19
19
|
service_1.Serve.create({ playerId: pA, target: 6, score: 55, failure: service_1.ServeFailureEnum.NO_FAILURE, type: service_1.ServeTypeEnum.JUMP_TOPSPIN }),
|
|
20
20
|
service_1.Reception.create({ playerId: pB, target: 5, score: 50, failure: service_1.ReceptionFailureEnum.NO_FAILURE, type: service_1.ReceptionTypeEnum.DIG }),
|
|
21
21
|
service_1.Set.create({ playerId: pC, target: 3, score: 48, failure: service_1.SetFailureEnum.NO_FAILURE, type: service_1.SetTypeEnum.OVERHAND, tempo: service_1.SetTempoEnum.FIRST }),
|
|
22
|
-
service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.
|
|
22
|
+
service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE }),
|
|
23
23
|
service_1.Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: service_1.BlockFailureEnum.NO_FAILURE, type: service_1.BlockTypeEnum.DOUBLE }),
|
|
24
24
|
service_1.FreeBall.create({ playerId: pC, target: 6, score: 45, failure: service_1.FreeBallFailureEnum.NO_FAILURE, type: service_1.FreeBallTypeEnum.OVER })
|
|
25
25
|
];
|
|
@@ -49,7 +49,7 @@ const rally_1 = require("./rally");
|
|
|
49
49
|
(0, globals_1.expect)(rec.type).toBe(service_1.ReceptionTypeEnum.DIG);
|
|
50
50
|
(0, globals_1.expect)(set.type).toBe(service_1.SetTypeEnum.OVERHAND);
|
|
51
51
|
(0, globals_1.expect)(set.tempo).toBe(service_1.SetTempoEnum.FIRST);
|
|
52
|
-
(0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.
|
|
52
|
+
(0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.SPIKE);
|
|
53
53
|
(0, globals_1.expect)(spike.score).toBe(62.5);
|
|
54
54
|
(0, globals_1.expect)(block.type).toBe(service_1.BlockTypeEnum.DOUBLE);
|
|
55
55
|
(0, globals_1.expect)(block.blockers).toEqual([pB, pC]);
|
|
@@ -86,6 +86,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
86
86
|
tempos: p.tempos
|
|
87
87
|
}))),
|
|
88
88
|
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
89
|
+
base_positions: tactics.basePositions,
|
|
89
90
|
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
90
91
|
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
91
92
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
@@ -207,6 +208,7 @@ function transformToObject(model, roster) {
|
|
|
207
208
|
}
|
|
208
209
|
return healed;
|
|
209
210
|
})(),
|
|
211
|
+
basePositions: model.base_positions ?? undefined,
|
|
210
212
|
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
211
213
|
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
212
214
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
@@ -367,6 +369,7 @@ function tacticsColumnsToApiDto(model) {
|
|
|
367
369
|
}
|
|
368
370
|
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
369
371
|
})(),
|
|
372
|
+
basePositions: model.base_positions ?? undefined,
|
|
370
373
|
blockingPreferences: model.blocking_preferences,
|
|
371
374
|
blockingPreferenceSets: model.blocking_preference_sets,
|
|
372
375
|
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
@@ -475,6 +478,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
475
478
|
system_sets: config.systemSets,
|
|
476
479
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
477
480
|
serve_attack_shares: config.serveAttackShares ?? {},
|
|
481
|
+
base_positions: config.basePositions,
|
|
478
482
|
blocking_preferences: config.blockingPreferences ?? [],
|
|
479
483
|
blocking_preference_sets: config.blockingPreferenceSets,
|
|
480
484
|
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
@@ -227,6 +227,30 @@ function makeTactics(designatedSubs) {
|
|
|
227
227
|
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
228
228
|
(0, globals_1.expect)(back.serveAttackShares).toEqual({});
|
|
229
229
|
});
|
|
230
|
+
(0, globals_1.it)('round-trips base positions (coords + reception participation)', () => {
|
|
231
|
+
const config = {
|
|
232
|
+
coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
|
|
233
|
+
receive: { 1: { 5: true, 2: false }, 4: { 6: true } }
|
|
234
|
+
};
|
|
235
|
+
const withBases = service_1.Tactics.create({
|
|
236
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
237
|
+
liberoReplacements: [],
|
|
238
|
+
basePositions: config
|
|
239
|
+
});
|
|
240
|
+
const attrs = (0, tactics_1.transformFromTactics)(withBases, 'team-1');
|
|
241
|
+
(0, globals_1.expect)(attrs.base_positions).toEqual(config);
|
|
242
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
243
|
+
(0, globals_1.expect)(back.basePositions).toEqual(config);
|
|
244
|
+
});
|
|
245
|
+
(0, globals_1.it)('leaves basePositions undefined when the column is absent', () => {
|
|
246
|
+
const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
|
|
247
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
248
|
+
liberoReplacements: []
|
|
249
|
+
}), 'team-1');
|
|
250
|
+
(0, globals_1.expect)(attrs.base_positions).toBeUndefined();
|
|
251
|
+
const back = (0, tactics_1.transformToTactics)(attrs, roster);
|
|
252
|
+
(0, globals_1.expect)(back.basePositions).toBeUndefined();
|
|
253
|
+
});
|
|
230
254
|
(0, globals_1.it)('round-trips a per-starter tempos map on offensive preferences', () => {
|
|
231
255
|
const withTempos = service_1.Tactics.create({
|
|
232
256
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
@@ -5,7 +5,9 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const player_1 = require("../../player");
|
|
6
6
|
const incident_z_1 = require("./incident.z");
|
|
7
7
|
const positional_z_1 = require("./positional.z");
|
|
8
|
-
|
|
8
|
+
// SPIKE(0), TIP(3), DOWN_BALL(4), OVERHAND(5), TOOL(6). 1/2 (legacy LINE_SHOT/REVERSE_CROSS) are retired; the
|
|
9
|
+
// codec (transformToSpike) maps them to 0 before this validates, so they never reach here.
|
|
10
|
+
const spikeTypeValues = [0, 3, 4, 5, 6];
|
|
9
11
|
const spikeFailureValues = [0, 1, 2, 3, 4];
|
|
10
12
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
11
13
|
exports.SpikeInputSchema = zod_1.z.object({
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { InPlayEvent } from './in-play-event';
|
|
2
2
|
export declare enum SpikeTypeEnum {
|
|
3
|
-
|
|
4
|
-
LINE_SHOT = 1,
|
|
5
|
-
REVERSE_CROSS = 2,
|
|
3
|
+
SPIKE = 0,
|
|
6
4
|
TIP = 3,
|
|
7
5
|
DOWN_BALL = 4,
|
|
8
6
|
OVERHAND = 5,
|
|
9
7
|
TOOL = 6
|
|
10
8
|
}
|
|
11
|
-
export type SpikeType = SpikeTypeEnum.
|
|
9
|
+
export type SpikeType = SpikeTypeEnum.SPIKE | SpikeTypeEnum.TIP | SpikeTypeEnum.DOWN_BALL | SpikeTypeEnum.OVERHAND | SpikeTypeEnum.TOOL;
|
|
12
10
|
export declare enum SpikeFailureEnum {
|
|
13
11
|
NO_FAILURE = 0,
|
|
14
12
|
FAULT = 1,
|
|
@@ -6,9 +6,11 @@ const in_play_event_1 = require("./in-play-event");
|
|
|
6
6
|
const spike_z_1 = require("./schemas/spike.z");
|
|
7
7
|
var SpikeTypeEnum;
|
|
8
8
|
(function (SpikeTypeEnum) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// A hard swing (owner 2026-08-03). The cross / line / reverse DIRECTION is not stored: it is fully derivable
|
|
10
|
+
// from the persisted contact + landing coordinates, so a single SPIKE value replaces the old CROSS_SHOT(0) /
|
|
11
|
+
// LINE_SHOT(1) / REVERSE_CROSS(2). 1 and 2 are retired (left as gaps); the codec maps any legacy 1/2 back to
|
|
12
|
+
// SPIKE(0), so existing (zone-engine) matches keep decoding.
|
|
13
|
+
SpikeTypeEnum[SpikeTypeEnum["SPIKE"] = 0] = "SPIKE";
|
|
12
14
|
SpikeTypeEnum[SpikeTypeEnum["TIP"] = 3] = "TIP";
|
|
13
15
|
SpikeTypeEnum[SpikeTypeEnum["DOWN_BALL"] = 4] = "DOWN_BALL";
|
|
14
16
|
SpikeTypeEnum[SpikeTypeEnum["OVERHAND"] = 5] = "OVERHAND";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type BaseFormation = 'SERVE' | 'RECEIVE' | 'DEFENCE' | 'DEF_L' | 'DEF_M' | 'DEF_R' | 'ATTACK';
|
|
2
|
+
export declare const BASE_FORMATIONS: readonly BaseFormation[];
|
|
3
|
+
export interface BaseCoord {
|
|
4
|
+
readonly d: number;
|
|
5
|
+
readonly r: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BaseConfig {
|
|
8
|
+
readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
|
|
9
|
+
readonly receive: Record<string, Record<string, boolean>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Per-team base-position configuration (owner 2026-08-03). Generalises the sim's hardcoded 5-1 preset into a
|
|
3
|
+
// system-agnostic, per-team shape: for each formation, rotation and rotational ZONE, where that zone's player
|
|
4
|
+
// stands, plus (serve-receive only) whether that zone's player passes the serve. Authored in the UI base editor
|
|
5
|
+
// (and populated from a picked default preset); consumed by the positional engine to place players. A team with no
|
|
6
|
+
// `basePositions` falls back to the default preset for its `rotationSystem`.
|
|
7
|
+
//
|
|
8
|
+
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The stored rotation convention is
|
|
9
|
+
// the ENGINE's (SETTER_ZONE_TO_ROTATION); the UI editor re-keys the mapper's setter-zone rotation to it on save.
|
|
10
|
+
// `coords[formation][rotation][zone]` is the standing coordinate for that formation; `receive[rotation][zone]` is
|
|
11
|
+
// the reception-participation flag (absent zone => default to participating).
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.BASE_FORMATIONS = void 0;
|
|
14
|
+
exports.BASE_FORMATIONS = ['SERVE', 'RECEIVE', 'DEFENCE', 'DEF_L', 'DEF_M', 'DEF_R', 'ATTACK'];
|
|
@@ -18,6 +18,7 @@ __exportStar(require("./team"), exports);
|
|
|
18
18
|
__exportStar(require("./team-generator"), exports);
|
|
19
19
|
__exportStar(require("./formation"), exports);
|
|
20
20
|
__exportStar(require("./tactics"), exports);
|
|
21
|
+
__exportStar(require("./base-config"), exports);
|
|
21
22
|
__exportStar(require("./team-name"), exports);
|
|
22
23
|
__exportStar(require("./energy-band"), exports);
|
|
23
24
|
__exportStar(require("./pinch-condition"), exports);
|
|
@@ -288,6 +288,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
288
288
|
rotation: z.ZodNumber;
|
|
289
289
|
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
290
290
|
}, z.core.$strip>>>>;
|
|
291
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
292
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
293
|
+
d: z.ZodNumber;
|
|
294
|
+
r: z.ZodNumber;
|
|
295
|
+
}, z.core.$strip>>>>;
|
|
296
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
297
|
+
}, z.core.$strip>>;
|
|
291
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
292
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
293
300
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -77,6 +77,15 @@ const systemSetSchema = zod_1.z.object({
|
|
|
77
77
|
rotationSystem: zod_1.z.nativeEnum(rotation_system_1.RotationSystemEnum),
|
|
78
78
|
designatedSetters: zod_1.z.array(playerInstanceSchema)
|
|
79
79
|
});
|
|
80
|
+
// Base positions (owner 2026-08-03): coords[formation][rotation][zone] -> {d,r} + receive[rotation][zone] -> bool.
|
|
81
|
+
// jsonb keys are strings; the coordinate + boolean leaves are validated. Structural key ranges (formation names,
|
|
82
|
+
// rotations 1-6, zones 1-6) are trusted from the UI editor / preset compiler, which write the engine rotation
|
|
83
|
+
// convention. Out-of-court coordinates are allowed, so d/r are not range-clamped here.
|
|
84
|
+
const baseCoordSchema = zod_1.z.object({ d: zod_1.z.number().finite(), r: zod_1.z.number().finite() });
|
|
85
|
+
const baseConfigSchema = zod_1.z.object({
|
|
86
|
+
coords: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), baseCoordSchema))),
|
|
87
|
+
receive: zod_1.z.record(zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.boolean()))
|
|
88
|
+
});
|
|
80
89
|
exports.TacticsInputSchema = zod_1.z.object({
|
|
81
90
|
lineup: lineupSchema,
|
|
82
91
|
liberoReplacements: zod_1.z.array(playerInstanceSchema),
|
|
@@ -107,6 +116,8 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
107
116
|
// Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
|
|
108
117
|
blockingPreferences: zod_1.z.array(blockingPreferenceSchema).default([]),
|
|
109
118
|
blockingPreferenceSets: zod_1.z.array(zod_1.z.array(blockingPreferenceSchema)).max(5).optional(),
|
|
119
|
+
// Base positions (owner 2026-08-03). Optional -> the sim falls back to the default preset for the system.
|
|
120
|
+
basePositions: baseConfigSchema.optional(),
|
|
110
121
|
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
111
122
|
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
112
123
|
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
@@ -296,6 +296,13 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
296
296
|
rotation: z.ZodNumber;
|
|
297
297
|
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
298
298
|
}, z.core.$strip>>>>;
|
|
299
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
300
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
301
|
+
d: z.ZodNumber;
|
|
302
|
+
r: z.ZodNumber;
|
|
303
|
+
}, z.core.$strip>>>>;
|
|
304
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
305
|
+
}, z.core.$strip>>;
|
|
299
306
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
300
307
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
301
308
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -8,6 +8,7 @@ import { OffensivePreference } from './offensive-preference';
|
|
|
8
8
|
import { BlockingPreference } from './blocking-preference';
|
|
9
9
|
import { InjuryReplacement } from './injury-replacement';
|
|
10
10
|
import { LiberoInjuryConfig } from './libero-injury';
|
|
11
|
+
import { BaseConfig } from './base-config';
|
|
11
12
|
export interface StartingLineup {
|
|
12
13
|
readonly [CourtPosition.LEFT_FRONT]: Player;
|
|
13
14
|
readonly [CourtPosition.MIDDLE_FRONT]: Player;
|
|
@@ -42,6 +43,7 @@ export interface TacticsOpts {
|
|
|
42
43
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
43
44
|
readonly blockingPreferences: BlockingPreference[];
|
|
44
45
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
46
|
+
readonly basePositions?: BaseConfig;
|
|
45
47
|
}
|
|
46
48
|
export declare class Tactics {
|
|
47
49
|
readonly lineup: StartingLineup;
|
|
@@ -63,6 +65,7 @@ export declare class Tactics {
|
|
|
63
65
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
64
66
|
readonly blockingPreferences: BlockingPreference[];
|
|
65
67
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
68
|
+
readonly basePositions?: BaseConfig;
|
|
66
69
|
static create(input: unknown): Tactics;
|
|
67
70
|
private constructor();
|
|
68
71
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -18,7 +18,7 @@ class Tactics {
|
|
|
18
18
|
}
|
|
19
19
|
return new Tactics(result.data);
|
|
20
20
|
}
|
|
21
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets }) {
|
|
21
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = energy_band_1.EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = rotation_system_1.RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -38,6 +38,7 @@ class Tactics {
|
|
|
38
38
|
this.liberoInjury = liberoInjury;
|
|
39
39
|
this.blockingPreferences = blockingPreferences;
|
|
40
40
|
this.blockingPreferenceSets = blockingPreferenceSets;
|
|
41
|
+
this.basePositions = basePositions;
|
|
41
42
|
}
|
|
42
43
|
findPlayerInLineup(id) {
|
|
43
44
|
if (this.lineup.bench.some(p => p.id === id))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
1
|
+
import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export * from './tactics-player-refs';
|
|
3
3
|
export type Rally = DataProps<_Rally> & {
|
|
4
4
|
homePlayerPosition: PlayerPosition[];
|
|
@@ -140,6 +140,7 @@ export interface Tactics {
|
|
|
140
140
|
systemSets?: ApiSystemSet[];
|
|
141
141
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
142
142
|
serveAttackShares?: Record<string, number>;
|
|
143
|
+
basePositions?: BaseConfig;
|
|
143
144
|
blockingPreferences: ApiBlockingPreference[];
|
|
144
145
|
blockingPreferenceSets?: ApiBlockingPreference[][];
|
|
145
146
|
replaceKnockedImmediately?: Record<string, boolean>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
3
|
import { PlayerId, TeamId, TeamModel } from '.';
|
|
4
|
-
import { BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
4
|
+
import { BaseConfig, BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -82,10 +82,11 @@ export interface TacticsAttributes {
|
|
|
82
82
|
libero_injury?: LiberoInjuryAttributes;
|
|
83
83
|
blocking_preferences: BlockingPreferenceAttributes[];
|
|
84
84
|
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
85
|
+
base_positions?: BaseConfig;
|
|
85
86
|
}
|
|
86
87
|
export type TacticsPk = 'team_id';
|
|
87
88
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
88
|
-
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' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_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' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
|
|
89
90
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
90
91
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
91
92
|
team_id: string;
|
|
@@ -108,6 +109,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
108
109
|
libero_injury?: LiberoInjuryAttributes;
|
|
109
110
|
blocking_preferences: BlockingPreferenceAttributes[];
|
|
110
111
|
blocking_preference_sets?: BlockingPreferenceAttributes[][];
|
|
112
|
+
base_positions?: BaseConfig;
|
|
111
113
|
team: TeamModel;
|
|
112
114
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
113
115
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -39,7 +39,7 @@ describe('rally-event compact round trip — incident marker on the causing even
|
|
|
39
39
|
score: 62.5,
|
|
40
40
|
target: 4,
|
|
41
41
|
failure: SpikeFailureEnum.NO_FAILURE,
|
|
42
|
-
type: SpikeTypeEnum.
|
|
42
|
+
type: SpikeTypeEnum.SPIKE,
|
|
43
43
|
...(incident != null ? { incident } : {})
|
|
44
44
|
});
|
|
45
45
|
}
|
|
@@ -12,7 +12,7 @@ describe('rally-event compact round trip -- positional fields (coords / speed /
|
|
|
12
12
|
score: 60,
|
|
13
13
|
target: 4,
|
|
14
14
|
failure: SpikeFailureEnum.NO_FAILURE,
|
|
15
|
-
type: SpikeTypeEnum.
|
|
15
|
+
type: SpikeTypeEnum.SPIKE,
|
|
16
16
|
contact: { x: 1.234, y: -0.5 },
|
|
17
17
|
landing: { x: -2.5, y: 6.789 },
|
|
18
18
|
speed: 92.456
|
|
@@ -41,7 +41,7 @@ describe('rally-event compact round trip -- positional fields (coords / speed /
|
|
|
41
41
|
expect(decoded.digQuality).toBe(DigQualityEnum.MEDIOCRE);
|
|
42
42
|
});
|
|
43
43
|
it('omits the positional keys when absent, and a legacy event decodes them as undefined', () => {
|
|
44
|
-
const spike = Spike.create({ playerId, score: 60, target: 4, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.
|
|
44
|
+
const spike = Spike.create({ playerId, score: 60, target: 4, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.SPIKE });
|
|
45
45
|
const compact = transformToCompact(spike, playerIndex);
|
|
46
46
|
expect('c' in compact).toBe(false);
|
|
47
47
|
expect('v' in compact).toBe(false);
|
|
@@ -51,4 +51,12 @@ describe('rally-event compact round trip -- positional fields (coords / speed /
|
|
|
51
51
|
expect(decoded.speed).toBeUndefined();
|
|
52
52
|
expect(decoded.digQuality).toBeUndefined();
|
|
53
53
|
});
|
|
54
|
+
it('collapses legacy LINE_SHOT(1)/REVERSE_CROSS(2) to SPIKE(0) on decode (owner 2026-08-03)', () => {
|
|
55
|
+
// A zone-engine match persisted before the collapse carries type 1 or 2 in its jsonb; decode heals them to
|
|
56
|
+
// SPIKE so the match still opens. TIP(3) and up are untouched.
|
|
57
|
+
const base = transformToCompact(Spike.create({ playerId, score: 60, target: 4, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.SPIKE }), playerIndex);
|
|
58
|
+
expect(transformToSpike({ ...base, t: 1 }, roster).type).toBe(SpikeTypeEnum.SPIKE);
|
|
59
|
+
expect(transformToSpike({ ...base, t: 2 }, roster).type).toBe(SpikeTypeEnum.SPIKE);
|
|
60
|
+
expect(transformToSpike({ ...base, t: 3 }, roster).type).toBe(SpikeTypeEnum.TIP);
|
|
61
|
+
});
|
|
54
62
|
});
|
|
@@ -127,7 +127,9 @@ export function transformToSet(event, roster) {
|
|
|
127
127
|
export function transformToSpike(event, roster) {
|
|
128
128
|
return Spike.create({
|
|
129
129
|
failure: event.f,
|
|
130
|
-
|
|
130
|
+
// Legacy LINE_SHOT(1)/REVERSE_CROSS(2) collapse to SPIKE(0) (owner 2026-08-03): direction is redundant with
|
|
131
|
+
// the stored contact+landing coords. Maps old zone-engine matches so they keep decoding; new events emit 0/3-6.
|
|
132
|
+
type: event.t === 1 || event.t === 2 ? 0 : event.t,
|
|
131
133
|
playerId: roster[event.p],
|
|
132
134
|
target: event.a,
|
|
133
135
|
score: event.s,
|
|
@@ -17,7 +17,7 @@ describe('rally compact round trip — every event type', () => {
|
|
|
17
17
|
Serve.create({ playerId: pA, target: 6, score: 55, failure: ServeFailureEnum.NO_FAILURE, type: ServeTypeEnum.JUMP_TOPSPIN }),
|
|
18
18
|
Reception.create({ playerId: pB, target: 5, score: 50, failure: ReceptionFailureEnum.NO_FAILURE, type: ReceptionTypeEnum.DIG }),
|
|
19
19
|
VolleySet.create({ playerId: pC, target: 3, score: 48, failure: SetFailureEnum.NO_FAILURE, type: SetTypeEnum.OVERHAND, tempo: SetTempoEnum.FIRST }),
|
|
20
|
-
Spike.create({ playerId: pA, target: 4, score: 62.5, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.
|
|
20
|
+
Spike.create({ playerId: pA, target: 4, score: 62.5, failure: SpikeFailureEnum.NO_FAILURE, type: SpikeTypeEnum.SPIKE }),
|
|
21
21
|
Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: BlockFailureEnum.NO_FAILURE, type: BlockTypeEnum.DOUBLE }),
|
|
22
22
|
FreeBall.create({ playerId: pC, target: 6, score: 45, failure: FreeBallFailureEnum.NO_FAILURE, type: FreeBallTypeEnum.OVER })
|
|
23
23
|
];
|
|
@@ -47,7 +47,7 @@ describe('rally compact round trip — every event type', () => {
|
|
|
47
47
|
expect(rec.type).toBe(ReceptionTypeEnum.DIG);
|
|
48
48
|
expect(set.type).toBe(SetTypeEnum.OVERHAND);
|
|
49
49
|
expect(set.tempo).toBe(SetTempoEnum.FIRST);
|
|
50
|
-
expect(spike.type).toBe(SpikeTypeEnum.
|
|
50
|
+
expect(spike.type).toBe(SpikeTypeEnum.SPIKE);
|
|
51
51
|
expect(spike.score).toBe(62.5);
|
|
52
52
|
expect(block.type).toBe(BlockTypeEnum.DOUBLE);
|
|
53
53
|
expect(block.blockers).toEqual([pB, pC]);
|
|
@@ -80,6 +80,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
80
80
|
tempos: p.tempos
|
|
81
81
|
}))),
|
|
82
82
|
serve_attack_shares: tactics.serveAttackShares ?? {},
|
|
83
|
+
base_positions: tactics.basePositions,
|
|
83
84
|
blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
84
85
|
blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
85
86
|
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
@@ -201,6 +202,7 @@ function transformToObject(model, roster) {
|
|
|
201
202
|
}
|
|
202
203
|
return healed;
|
|
203
204
|
})(),
|
|
205
|
+
basePositions: model.base_positions ?? undefined,
|
|
204
206
|
blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
|
|
205
207
|
blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
|
|
206
208
|
// LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
|
|
@@ -361,6 +363,7 @@ function tacticsColumnsToApiDto(model) {
|
|
|
361
363
|
}
|
|
362
364
|
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
363
365
|
})(),
|
|
366
|
+
basePositions: model.base_positions ?? undefined,
|
|
364
367
|
blockingPreferences: model.blocking_preferences,
|
|
365
368
|
blockingPreferenceSets: model.blocking_preference_sets,
|
|
366
369
|
// Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
|
|
@@ -469,6 +472,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
469
472
|
system_sets: config.systemSets,
|
|
470
473
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
471
474
|
serve_attack_shares: config.serveAttackShares ?? {},
|
|
475
|
+
base_positions: config.basePositions,
|
|
472
476
|
blocking_preferences: config.blockingPreferences ?? [],
|
|
473
477
|
blocking_preference_sets: config.blockingPreferenceSets,
|
|
474
478
|
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
@@ -225,6 +225,30 @@ describe('per-set tactics — service <-> row round-trip', () => {
|
|
|
225
225
|
const back = transformToTactics(attrs, roster);
|
|
226
226
|
expect(back.serveAttackShares).toEqual({});
|
|
227
227
|
});
|
|
228
|
+
it('round-trips base positions (coords + reception participation)', () => {
|
|
229
|
+
const config = {
|
|
230
|
+
coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
|
|
231
|
+
receive: { 1: { 5: true, 2: false }, 4: { 6: true } }
|
|
232
|
+
};
|
|
233
|
+
const withBases = Tactics.create({
|
|
234
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
235
|
+
liberoReplacements: [],
|
|
236
|
+
basePositions: config
|
|
237
|
+
});
|
|
238
|
+
const attrs = transformFromTactics(withBases, 'team-1');
|
|
239
|
+
expect(attrs.base_positions).toEqual(config);
|
|
240
|
+
const back = transformToTactics(attrs, roster);
|
|
241
|
+
expect(back.basePositions).toEqual(config);
|
|
242
|
+
});
|
|
243
|
+
it('leaves basePositions undefined when the column is absent', () => {
|
|
244
|
+
const attrs = transformFromTactics(Tactics.create({
|
|
245
|
+
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
246
|
+
liberoReplacements: []
|
|
247
|
+
}), 'team-1');
|
|
248
|
+
expect(attrs.base_positions).toBeUndefined();
|
|
249
|
+
const back = transformToTactics(attrs, roster);
|
|
250
|
+
expect(back.basePositions).toBeUndefined();
|
|
251
|
+
});
|
|
228
252
|
it('round-trips a per-starter tempos map on offensive preferences', () => {
|
|
229
253
|
const withTempos = Tactics.create({
|
|
230
254
|
lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
|
|
@@ -2,7 +2,9 @@ import { z } from 'zod';
|
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
3
|
import { EventIncidentSchema } from './incident.z';
|
|
4
4
|
import { positionalEventFields } from './positional.z';
|
|
5
|
-
|
|
5
|
+
// SPIKE(0), TIP(3), DOWN_BALL(4), OVERHAND(5), TOOL(6). 1/2 (legacy LINE_SHOT/REVERSE_CROSS) are retired; the
|
|
6
|
+
// codec (transformToSpike) maps them to 0 before this validates, so they never reach here.
|
|
7
|
+
const spikeTypeValues = [0, 3, 4, 5, 6];
|
|
6
8
|
const spikeFailureValues = [0, 1, 2, 3, 4];
|
|
7
9
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
8
10
|
export const SpikeInputSchema = z.object({
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { InPlayEvent } from './in-play-event';
|
|
2
2
|
export declare enum SpikeTypeEnum {
|
|
3
|
-
|
|
4
|
-
LINE_SHOT = 1,
|
|
5
|
-
REVERSE_CROSS = 2,
|
|
3
|
+
SPIKE = 0,
|
|
6
4
|
TIP = 3,
|
|
7
5
|
DOWN_BALL = 4,
|
|
8
6
|
OVERHAND = 5,
|
|
9
7
|
TOOL = 6
|
|
10
8
|
}
|
|
11
|
-
export type SpikeType = SpikeTypeEnum.
|
|
9
|
+
export type SpikeType = SpikeTypeEnum.SPIKE | SpikeTypeEnum.TIP | SpikeTypeEnum.DOWN_BALL | SpikeTypeEnum.OVERHAND | SpikeTypeEnum.TOOL;
|
|
12
10
|
export declare enum SpikeFailureEnum {
|
|
13
11
|
NO_FAILURE = 0,
|
|
14
12
|
FAULT = 1,
|
|
@@ -3,9 +3,11 @@ import { InPlayEvent } from './in-play-event';
|
|
|
3
3
|
import { SpikeInputSchema } from './schemas/spike.z';
|
|
4
4
|
export var SpikeTypeEnum;
|
|
5
5
|
(function (SpikeTypeEnum) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// A hard swing (owner 2026-08-03). The cross / line / reverse DIRECTION is not stored: it is fully derivable
|
|
7
|
+
// from the persisted contact + landing coordinates, so a single SPIKE value replaces the old CROSS_SHOT(0) /
|
|
8
|
+
// LINE_SHOT(1) / REVERSE_CROSS(2). 1 and 2 are retired (left as gaps); the codec maps any legacy 1/2 back to
|
|
9
|
+
// SPIKE(0), so existing (zone-engine) matches keep decoding.
|
|
10
|
+
SpikeTypeEnum[SpikeTypeEnum["SPIKE"] = 0] = "SPIKE";
|
|
9
11
|
SpikeTypeEnum[SpikeTypeEnum["TIP"] = 3] = "TIP";
|
|
10
12
|
SpikeTypeEnum[SpikeTypeEnum["DOWN_BALL"] = 4] = "DOWN_BALL";
|
|
11
13
|
SpikeTypeEnum[SpikeTypeEnum["OVERHAND"] = 5] = "OVERHAND";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type BaseFormation = 'SERVE' | 'RECEIVE' | 'DEFENCE' | 'DEF_L' | 'DEF_M' | 'DEF_R' | 'ATTACK';
|
|
2
|
+
export declare const BASE_FORMATIONS: readonly BaseFormation[];
|
|
3
|
+
export interface BaseCoord {
|
|
4
|
+
readonly d: number;
|
|
5
|
+
readonly r: number;
|
|
6
|
+
}
|
|
7
|
+
export interface BaseConfig {
|
|
8
|
+
readonly coords: Record<string, Record<string, Record<string, BaseCoord>>>;
|
|
9
|
+
readonly receive: Record<string, Record<string, boolean>>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Per-team base-position configuration (owner 2026-08-03). Generalises the sim's hardcoded 5-1 preset into a
|
|
2
|
+
// system-agnostic, per-team shape: for each formation, rotation and rotational ZONE, where that zone's player
|
|
3
|
+
// stands, plus (serve-receive only) whether that zone's player passes the serve. Authored in the UI base editor
|
|
4
|
+
// (and populated from a picked default preset); consumed by the positional engine to place players. A team with no
|
|
5
|
+
// `basePositions` falls back to the default preset for its `rotationSystem`.
|
|
6
|
+
//
|
|
7
|
+
// KEYING: rotation 1..6 and zone 1..6 are the ROTATIONAL (at-the-whistle) slots. The stored rotation convention is
|
|
8
|
+
// the ENGINE's (SETTER_ZONE_TO_ROTATION); the UI editor re-keys the mapper's setter-zone rotation to it on save.
|
|
9
|
+
// `coords[formation][rotation][zone]` is the standing coordinate for that formation; `receive[rotation][zone]` is
|
|
10
|
+
// the reception-participation flag (absent zone => default to participating).
|
|
11
|
+
export const BASE_FORMATIONS = ['SERVE', 'RECEIVE', 'DEFENCE', 'DEF_L', 'DEF_M', 'DEF_R', 'ATTACK'];
|
|
@@ -288,6 +288,13 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
288
288
|
rotation: z.ZodNumber;
|
|
289
289
|
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof RoleEnum> & z.ZodType<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum, z.core.$ZodTypeInternals<RoleEnum.SETTER | RoleEnum.OUTSIDE_HITTER | RoleEnum.OPPOSITE_HITTER | RoleEnum.MIDDLE_BLOCKER, RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
290
290
|
}, z.core.$strip>>>>;
|
|
291
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
292
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
293
|
+
d: z.ZodNumber;
|
|
294
|
+
r: z.ZodNumber;
|
|
295
|
+
}, z.core.$strip>>>>;
|
|
296
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
297
|
+
}, z.core.$strip>>;
|
|
291
298
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
292
299
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
293
300
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -74,6 +74,15 @@ const systemSetSchema = z.object({
|
|
|
74
74
|
rotationSystem: z.nativeEnum(RotationSystemEnum),
|
|
75
75
|
designatedSetters: z.array(playerInstanceSchema)
|
|
76
76
|
});
|
|
77
|
+
// Base positions (owner 2026-08-03): coords[formation][rotation][zone] -> {d,r} + receive[rotation][zone] -> bool.
|
|
78
|
+
// jsonb keys are strings; the coordinate + boolean leaves are validated. Structural key ranges (formation names,
|
|
79
|
+
// rotations 1-6, zones 1-6) are trusted from the UI editor / preset compiler, which write the engine rotation
|
|
80
|
+
// convention. Out-of-court coordinates are allowed, so d/r are not range-clamped here.
|
|
81
|
+
const baseCoordSchema = z.object({ d: z.number().finite(), r: z.number().finite() });
|
|
82
|
+
const baseConfigSchema = z.object({
|
|
83
|
+
coords: z.record(z.string(), z.record(z.string(), z.record(z.string(), baseCoordSchema))),
|
|
84
|
+
receive: z.record(z.string(), z.record(z.string(), z.boolean()))
|
|
85
|
+
});
|
|
77
86
|
export const TacticsInputSchema = z.object({
|
|
78
87
|
lineup: lineupSchema,
|
|
79
88
|
liberoReplacements: z.array(playerInstanceSchema),
|
|
@@ -104,6 +113,8 @@ export const TacticsInputSchema = z.object({
|
|
|
104
113
|
// Blocking preferences (dedicated blocking). Defaults keep every existing Tactics.create({...}) call valid.
|
|
105
114
|
blockingPreferences: z.array(blockingPreferenceSchema).default([]),
|
|
106
115
|
blockingPreferenceSets: z.array(z.array(blockingPreferenceSchema)).max(5).optional(),
|
|
116
|
+
// Base positions (owner 2026-08-03). Optional -> the sim falls back to the default preset for the system.
|
|
117
|
+
basePositions: baseConfigSchema.optional(),
|
|
107
118
|
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
108
119
|
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
109
120
|
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
@@ -296,6 +296,13 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
296
296
|
rotation: z.ZodNumber;
|
|
297
297
|
targets: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodEnum<typeof import("../../player").RoleEnum> & z.ZodType<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum, z.core.$ZodTypeInternals<import("../../player").RoleEnum.SETTER | import("../../player").RoleEnum.OUTSIDE_HITTER | import("../../player").RoleEnum.OPPOSITE_HITTER | import("../../player").RoleEnum.MIDDLE_BLOCKER, import("../../player").RoleEnum>>, z.ZodLiteral<"NO_BLOCK">]>>;
|
|
298
298
|
}, z.core.$strip>>>>;
|
|
299
|
+
basePositions: z.ZodOptional<z.ZodObject<{
|
|
300
|
+
coords: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
301
|
+
d: z.ZodNumber;
|
|
302
|
+
r: z.ZodNumber;
|
|
303
|
+
}, z.core.$strip>>>>;
|
|
304
|
+
receive: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
305
|
+
}, z.core.$strip>>;
|
|
299
306
|
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
300
307
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
301
308
|
starter: z.ZodCustom<Player, Player>;
|
|
@@ -8,6 +8,7 @@ import { OffensivePreference } from './offensive-preference';
|
|
|
8
8
|
import { BlockingPreference } from './blocking-preference';
|
|
9
9
|
import { InjuryReplacement } from './injury-replacement';
|
|
10
10
|
import { LiberoInjuryConfig } from './libero-injury';
|
|
11
|
+
import { BaseConfig } from './base-config';
|
|
11
12
|
export interface StartingLineup {
|
|
12
13
|
readonly [CourtPosition.LEFT_FRONT]: Player;
|
|
13
14
|
readonly [CourtPosition.MIDDLE_FRONT]: Player;
|
|
@@ -42,6 +43,7 @@ export interface TacticsOpts {
|
|
|
42
43
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
43
44
|
readonly blockingPreferences: BlockingPreference[];
|
|
44
45
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
46
|
+
readonly basePositions?: BaseConfig;
|
|
45
47
|
}
|
|
46
48
|
export declare class Tactics {
|
|
47
49
|
readonly lineup: StartingLineup;
|
|
@@ -63,6 +65,7 @@ export declare class Tactics {
|
|
|
63
65
|
readonly liberoInjury?: LiberoInjuryConfig;
|
|
64
66
|
readonly blockingPreferences: BlockingPreference[];
|
|
65
67
|
readonly blockingPreferenceSets?: BlockingPreference[][];
|
|
68
|
+
readonly basePositions?: BaseConfig;
|
|
66
69
|
static create(input: unknown): Tactics;
|
|
67
70
|
private constructor();
|
|
68
71
|
findPlayerInLineup(id: string): CourtPosition | undefined;
|
|
@@ -15,7 +15,7 @@ export class Tactics {
|
|
|
15
15
|
}
|
|
16
16
|
return new Tactics(result.data);
|
|
17
17
|
}
|
|
18
|
-
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets }) {
|
|
18
|
+
constructor({ lineup, liberoReplacements, receiveRotationOffset = false, designatedSubs = [], substitutionBand = EnergyBand.TIRED, secondLibero, liberoSub, liberoReplacementSets, rotationSystem = RotationSystemEnum.SIX_ZERO, designatedSetters = [], offensivePreferences = [], serveAttackShares = {}, systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [], liberoInjury, blockingPreferences = [], blockingPreferenceSets, basePositions }) {
|
|
19
19
|
this.lineup = lineup;
|
|
20
20
|
this.liberoReplacements = liberoReplacements;
|
|
21
21
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -35,6 +35,7 @@ export class Tactics {
|
|
|
35
35
|
this.liberoInjury = liberoInjury;
|
|
36
36
|
this.blockingPreferences = blockingPreferences;
|
|
37
37
|
this.blockingPreferenceSets = blockingPreferenceSets;
|
|
38
|
+
this.basePositions = basePositions;
|
|
38
39
|
}
|
|
39
40
|
findPlayerInLineup(id) {
|
|
40
41
|
if (this.lineup.bench.some(p => p.id === id))
|