volleyballsimtypes 0.0.469 → 0.0.471
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 -1
- package/dist/cjs/src/data/models/tactics.d.ts +3 -3
- package/dist/cjs/src/data/models/tactics.js +3 -4
- package/dist/cjs/src/data/transformers/lineup-preset.js +4 -7
- package/dist/cjs/src/data/transformers/lineup-preset.test.js +9 -7
- package/dist/cjs/src/data/transformers/rally-event-injury.test.js +52 -0
- package/dist/cjs/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally-event.js +24 -5
- package/dist/cjs/src/data/transformers/tactics.js +28 -4
- package/dist/cjs/src/service/event/block.js +2 -2
- package/dist/cjs/src/service/event/in-play-event.d.ts +7 -1
- package/dist/cjs/src/service/event/in-play-event.js +2 -1
- package/dist/cjs/src/service/event/reception.js +2 -2
- package/dist/cjs/src/service/event/schemas/block.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/block.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/incident.z.d.ts +9 -0
- package/dist/cjs/src/service/event/schemas/incident.z.js +11 -0
- package/dist/cjs/src/service/event/schemas/reception.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/reception.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/serve.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/serve.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/set.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/set.z.js +3 -1
- package/dist/cjs/src/service/event/schemas/spike.z.d.ts +7 -0
- package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
- package/dist/cjs/src/service/event/serve.js +2 -2
- package/dist/cjs/src/service/event/set.js +2 -2
- package/dist/cjs/src/service/event/spike.js +2 -2
- package/dist/cjs/src/service/team/schemas/injury-replacement.z.test.js +28 -2
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +1 -1
- package/dist/cjs/src/service/team/schemas/tactics.z.js +12 -4
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -1
- 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 +1 -1
- package/dist/esm/src/data/models/tactics.d.ts +3 -3
- package/dist/esm/src/data/models/tactics.js +3 -4
- package/dist/esm/src/data/transformers/lineup-preset.js +4 -7
- package/dist/esm/src/data/transformers/lineup-preset.test.js +9 -7
- package/dist/esm/src/data/transformers/rally-event-injury.test.js +54 -2
- package/dist/esm/src/data/transformers/rally-event.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally-event.js +24 -5
- package/dist/esm/src/data/transformers/tactics.js +28 -4
- package/dist/esm/src/service/event/block.js +2 -2
- package/dist/esm/src/service/event/in-play-event.d.ts +7 -1
- package/dist/esm/src/service/event/in-play-event.js +2 -1
- package/dist/esm/src/service/event/reception.js +2 -2
- package/dist/esm/src/service/event/schemas/block.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/block.z.js +3 -1
- package/dist/esm/src/service/event/schemas/incident.z.d.ts +9 -0
- package/dist/esm/src/service/event/schemas/incident.z.js +8 -0
- package/dist/esm/src/service/event/schemas/reception.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/reception.z.js +3 -1
- package/dist/esm/src/service/event/schemas/serve.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/serve.z.js +3 -1
- package/dist/esm/src/service/event/schemas/set.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/set.z.js +3 -1
- package/dist/esm/src/service/event/schemas/spike.z.d.ts +7 -0
- package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
- package/dist/esm/src/service/event/serve.js +2 -2
- package/dist/esm/src/service/event/set.js +2 -2
- package/dist/esm/src/service/event/spike.js +2 -2
- package/dist/esm/src/service/team/schemas/injury-replacement.z.test.js +28 -2
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +1 -1
- package/dist/esm/src/service/team/schemas/tactics.z.js +12 -4
- package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -1
- 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
|
@@ -123,7 +123,7 @@ export declare const TacticsInputSchema: z.ZodObject<{
|
|
|
123
123
|
rotation: z.ZodNumber;
|
|
124
124
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
125
125
|
}, z.core.$strip>>>>;
|
|
126
|
-
|
|
126
|
+
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
127
127
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
128
128
|
starter: z.ZodCustom<Player, Player>;
|
|
129
129
|
replacement: z.ZodCustom<Player, Player>;
|
|
@@ -81,10 +81,11 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
81
81
|
// validated with the same cross-field rules as the flat fields (see superRefine below).
|
|
82
82
|
systemSets: zod_1.z.array(systemSetSchema).max(5).optional(),
|
|
83
83
|
offensivePreferenceSets: zod_1.z.array(zod_1.z.array(offensivePreferenceSchema)).max(5).optional(),
|
|
84
|
-
// Injuries. Defaults keep every existing Tactics.create({...}) call valid.
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
84
|
+
// Injuries. Defaults keep every existing Tactics.create({...}) call valid. replaceKnockedImmediately is
|
|
85
|
+
// PER STARTER (owner order 2026-07-05): keyed by starter id, missing key = ON; keys validated against the
|
|
86
|
+
// lineup in the superRefine below. injuryReplacements is the per-starter explicit replacement mapping
|
|
87
|
+
// (owner rules: followed verbatim, bench-only auto fallback, reserves never auto-promoted).
|
|
88
|
+
replaceKnockedImmediately: zod_1.z.record(zod_1.z.string(), zod_1.z.boolean()).default({}),
|
|
88
89
|
injuryReplacements: zod_1.z.array(zod_1.z.object({
|
|
89
90
|
starter: playerInstanceSchema,
|
|
90
91
|
replacement: playerInstanceSchema,
|
|
@@ -187,6 +188,13 @@ exports.TacticsInputSchema = zod_1.z.object({
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
// Per-starter knock toggle: every key must be a configured court starter (bench/reserve/libero players
|
|
192
|
+
// follow the slot they occupy, so they never carry their own key).
|
|
193
|
+
for (const starterId of Object.keys(data.replaceKnockedImmediately)) {
|
|
194
|
+
if (!starterIds.has(starterId)) {
|
|
195
|
+
ctx.addIssue({ code: 'custom', message: 'REPLACE_KNOCKED_KEY_NOT_A_STARTER', path: ['replaceKnockedImmediately'] });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
190
198
|
// Injury replacements (owner rules 2026-07-03). starter = a court starter, at most one entry each.
|
|
191
199
|
// replacement = a bench player or a reserve (never another starter, the libero, or the L2). benchBackfill
|
|
192
200
|
// only when the replacement came from the bench, and must be a RESERVE (outside the whole lineup, not the
|
|
@@ -131,7 +131,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
131
131
|
rotation: z.ZodNumber;
|
|
132
132
|
order: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
133
133
|
}, z.core.$strip>>>>;
|
|
134
|
-
|
|
134
|
+
replaceKnockedImmediately: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodBoolean>>;
|
|
135
135
|
injuryReplacements: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
136
136
|
starter: z.ZodCustom<Player, Player>;
|
|
137
137
|
replacement: z.ZodCustom<Player, Player>;
|
|
@@ -34,7 +34,7 @@ export interface TacticsOpts {
|
|
|
34
34
|
readonly offensivePreferences: OffensivePreference[];
|
|
35
35
|
readonly systemSets?: SystemSet[];
|
|
36
36
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
37
|
-
readonly
|
|
37
|
+
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
38
38
|
readonly injuryReplacements: InjuryReplacement[];
|
|
39
39
|
}
|
|
40
40
|
export declare class Tactics {
|
|
@@ -51,7 +51,7 @@ export declare class Tactics {
|
|
|
51
51
|
readonly offensivePreferences: OffensivePreference[];
|
|
52
52
|
readonly systemSets?: SystemSet[];
|
|
53
53
|
readonly offensivePreferenceSets?: OffensivePreference[][];
|
|
54
|
-
readonly
|
|
54
|
+
readonly replaceKnockedImmediately: Record<string, boolean>;
|
|
55
55
|
readonly injuryReplacements: InjuryReplacement[];
|
|
56
56
|
static create(input: unknown): Tactics;
|
|
57
57
|
private constructor();
|
|
@@ -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 = [], systemSets, offensivePreferenceSets,
|
|
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 = [], systemSets, offensivePreferenceSets, replaceKnockedImmediately = {}, injuryReplacements = [] }) {
|
|
22
22
|
this.lineup = lineup;
|
|
23
23
|
this.liberoReplacements = liberoReplacements;
|
|
24
24
|
this.receiveRotationOffset = receiveRotationOffset;
|
|
@@ -32,7 +32,7 @@ class Tactics {
|
|
|
32
32
|
this.offensivePreferences = offensivePreferences;
|
|
33
33
|
this.systemSets = systemSets;
|
|
34
34
|
this.offensivePreferenceSets = offensivePreferenceSets;
|
|
35
|
-
this.
|
|
35
|
+
this.replaceKnockedImmediately = replaceKnockedImmediately;
|
|
36
36
|
this.injuryReplacements = injuryReplacements;
|
|
37
37
|
}
|
|
38
38
|
findPlayerInLineup(id) {
|
|
@@ -132,7 +132,7 @@ export interface Tactics {
|
|
|
132
132
|
offensivePreferences: ApiOffensivePreference[];
|
|
133
133
|
systemSets?: ApiSystemSet[];
|
|
134
134
|
offensivePreferenceSets?: ApiOffensivePreference[][];
|
|
135
|
-
|
|
135
|
+
replaceKnockedImmediately?: Record<string, boolean>;
|
|
136
136
|
injuryReplacements?: ApiInjuryReplacement[];
|
|
137
137
|
}
|
|
138
138
|
export interface ApiLineupPreset {
|
|
@@ -66,12 +66,12 @@ export interface TacticsAttributes {
|
|
|
66
66
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
67
67
|
system_sets?: SystemSetAttributes[];
|
|
68
68
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
69
|
-
|
|
69
|
+
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
70
70
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
71
71
|
}
|
|
72
72
|
export type TacticsPk = 'team_id';
|
|
73
73
|
export type TacticsId = TacticsModel[TacticsPk];
|
|
74
|
-
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' | '
|
|
74
|
+
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';
|
|
75
75
|
export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
|
|
76
76
|
export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
|
|
77
77
|
team_id: string;
|
|
@@ -88,7 +88,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
|
|
|
88
88
|
offensive_preferences: OffensivePreferenceAttributes[];
|
|
89
89
|
system_sets?: SystemSetAttributes[];
|
|
90
90
|
offensive_preference_sets?: OffensivePreferenceAttributes[][];
|
|
91
|
-
|
|
91
|
+
replace_knocked_immediately?: Record<PlayerId, boolean>;
|
|
92
92
|
injury_replacements?: InjuryReplacementAttributes[];
|
|
93
93
|
team: TeamModel;
|
|
94
94
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
@@ -72,10 +72,9 @@ export class TacticsModel extends Model {
|
|
|
72
72
|
type: DataTypes.JSONB,
|
|
73
73
|
allowNull: true
|
|
74
74
|
},
|
|
75
|
-
|
|
76
|
-
type: DataTypes.
|
|
77
|
-
allowNull:
|
|
78
|
-
defaultValue: true
|
|
75
|
+
replace_knocked_immediately: {
|
|
76
|
+
type: DataTypes.JSONB,
|
|
77
|
+
allowNull: true
|
|
79
78
|
},
|
|
80
79
|
injury_replacements: {
|
|
81
80
|
type: DataTypes.JSONB,
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
// `config` is already stored as the API Tactics DTO, so a preset row maps to its API shape with no hydration.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// No injuries heal here anymore: replaceKnockedImmediately is optional (absent = every starter ON), and old
|
|
3
|
+
// configs carrying the superseded replaceInjuredImmediately boolean pass through the spread as a dead key
|
|
4
|
+
// that the save DTO strips (owner 2026-07-05: the old global value is dropped, default ON).
|
|
5
5
|
function transformToObject(model) {
|
|
6
6
|
return {
|
|
7
7
|
presetId: model.preset_id,
|
|
8
8
|
name: model.name,
|
|
9
9
|
isActive: model.is_active,
|
|
10
10
|
orderIndex: model.order_index,
|
|
11
|
-
config:
|
|
12
|
-
...model.config,
|
|
13
|
-
replaceInjuredImmediately: model.config.replaceInjuredImmediately ?? true
|
|
14
|
-
}
|
|
11
|
+
config: model.config
|
|
15
12
|
};
|
|
16
13
|
}
|
|
17
14
|
export { transformToObject as transformToLineupPreset };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { transformToLineupPreset } from './lineup-preset';
|
|
3
3
|
describe('transformToLineupPreset()', () => {
|
|
4
|
-
it('maps preset columns to the API shape
|
|
4
|
+
it('maps preset columns to the API shape verbatim (no injuries heal: absent map = every starter ON)', () => {
|
|
5
5
|
const config = { lineup: { bench: [] }, substitutionBand: 'TIRED' };
|
|
6
6
|
const preset = transformToLineupPreset({
|
|
7
7
|
preset_id: 'p1',
|
|
@@ -16,13 +16,15 @@ describe('transformToLineupPreset()', () => {
|
|
|
16
16
|
name: 'Starters',
|
|
17
17
|
isActive: true,
|
|
18
18
|
orderIndex: 2,
|
|
19
|
-
|
|
20
|
-
// to true (the effective hydration default) so the DTO's required field is honest.
|
|
21
|
-
config: { ...config, replaceInjuredImmediately: true }
|
|
19
|
+
config
|
|
22
20
|
});
|
|
23
21
|
});
|
|
24
|
-
it('keeps an explicit
|
|
25
|
-
const config = {
|
|
22
|
+
it('keeps an explicit per-starter replaceKnockedImmediately map', () => {
|
|
23
|
+
const config = {
|
|
24
|
+
lineup: { bench: [] },
|
|
25
|
+
substitutionBand: 'TIRED',
|
|
26
|
+
replaceKnockedImmediately: { 'starter-1': false }
|
|
27
|
+
};
|
|
26
28
|
const preset = transformToLineupPreset({
|
|
27
29
|
preset_id: 'p2',
|
|
28
30
|
team_id: 't1',
|
|
@@ -31,6 +33,6 @@ describe('transformToLineupPreset()', () => {
|
|
|
31
33
|
order_index: 3,
|
|
32
34
|
config
|
|
33
35
|
});
|
|
34
|
-
expect(preset.config.
|
|
36
|
+
expect(preset.config.replaceKnockedImmediately).toEqual({ 'starter-1': false });
|
|
35
37
|
});
|
|
36
38
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from '@jest/globals';
|
|
2
2
|
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import { EventTypeEnum, Substitution } from '../../service';
|
|
4
|
-
import { transformToCompact, transformToSubstitution } from './rally-event';
|
|
3
|
+
import { EventTypeEnum, Serve, ServeFailureEnum, ServeTypeEnum, Spike, SpikeFailureEnum, SpikeTypeEnum, Substitution } from '../../service';
|
|
4
|
+
import { transformToCompact, transformToServe, transformToSpike, transformToSubstitution } from './rally-event';
|
|
5
5
|
describe('rally-event compact round trip — injury substitution marker', () => {
|
|
6
6
|
const playerIn = uuidv4();
|
|
7
7
|
const playerOut = uuidv4();
|
|
@@ -29,3 +29,55 @@ describe('rally-event compact round trip — injury substitution marker', () =>
|
|
|
29
29
|
expect(decoded.injury).toBeUndefined();
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
|
+
describe('rally-event compact round trip — incident marker on the causing event', () => {
|
|
33
|
+
const playerId = uuidv4();
|
|
34
|
+
const roster = [playerId];
|
|
35
|
+
const playerIndex = new Map(roster.map((id, i) => [id, i]));
|
|
36
|
+
function makeSpike(incident) {
|
|
37
|
+
return Spike.create({
|
|
38
|
+
playerId,
|
|
39
|
+
score: 62.5,
|
|
40
|
+
target: 4,
|
|
41
|
+
failure: SpikeFailureEnum.NO_FAILURE,
|
|
42
|
+
type: SpikeTypeEnum.CROSS_SHOT,
|
|
43
|
+
...(incident != null ? { incident } : {})
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
it('packs an INJURY incident as i = severity (1-4) and round-trips kind + severity', () => {
|
|
47
|
+
const compact = transformToCompact(makeSpike({ kind: 'INJURY', severity: 4 }), playerIndex);
|
|
48
|
+
expect(compact.i).toBe(4);
|
|
49
|
+
const decoded = transformToSpike(compact, roster);
|
|
50
|
+
expect(decoded.incident).toEqual({ kind: 'INJURY', severity: 4 });
|
|
51
|
+
});
|
|
52
|
+
it('packs a KNOCK incident as i = 10 + tier and round-trips kind + severity', () => {
|
|
53
|
+
const serve = Serve.create({
|
|
54
|
+
playerId,
|
|
55
|
+
score: 55,
|
|
56
|
+
target: 6,
|
|
57
|
+
failure: ServeFailureEnum.NO_FAILURE,
|
|
58
|
+
type: ServeTypeEnum.JUMP_TOPSPIN,
|
|
59
|
+
incident: { kind: 'KNOCK', severity: 2 }
|
|
60
|
+
});
|
|
61
|
+
const compact = transformToCompact(serve, playerIndex);
|
|
62
|
+
expect(compact.i).toBe(12);
|
|
63
|
+
const decoded = transformToServe(compact, roster);
|
|
64
|
+
expect(decoded.incident).toEqual({ kind: 'KNOCK', severity: 2 });
|
|
65
|
+
});
|
|
66
|
+
it('omits i entirely for an event without an incident', () => {
|
|
67
|
+
const compact = transformToCompact(makeSpike(), playerIndex);
|
|
68
|
+
expect('i' in compact).toBe(false);
|
|
69
|
+
const decoded = transformToSpike(compact, roster);
|
|
70
|
+
expect(decoded.incident).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
it('decodes a legacy compact event (no i key) with no incident', () => {
|
|
73
|
+
const legacy = { p: 0, e: EventTypeEnum.SPIKE, f: 0, t: 0, a: 4, s: 50 };
|
|
74
|
+
const decoded = transformToSpike(legacy, roster);
|
|
75
|
+
expect(decoded.incident).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
it('rejects an out-of-range or malformed incident at the schema', () => {
|
|
78
|
+
expect(() => makeSpike({ kind: 'INJURY', severity: 5 })).toThrow(/INVALID_SPIKE/);
|
|
79
|
+
expect(() => makeSpike({ kind: 'INJURY', severity: 0 })).toThrow(/INVALID_SPIKE/);
|
|
80
|
+
expect(() => makeSpike({ kind: 'KNOCK', severity: 4 })).toThrow(/INVALID_SPIKE/);
|
|
81
|
+
expect(() => makeSpike({ kind: 'BRUISE', severity: 1 })).toThrow(/INVALID_SPIKE/);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -10,6 +10,7 @@ export interface CompactEvent {
|
|
|
10
10
|
l?: number;
|
|
11
11
|
o?: number;
|
|
12
12
|
j?: number;
|
|
13
|
+
i?: number;
|
|
13
14
|
}
|
|
14
15
|
export declare function transformToCompact(evt: RallyEvent, playerIndex: Map<string, number>): CompactEvent;
|
|
15
16
|
export declare function transformToBlock(event: CompactEvent, roster: string[]): Block;
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { Block, InPlayEvent, LiberoReplacement, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
2
|
+
// Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
|
|
3
|
+
function incidentToCompact(incident) {
|
|
4
|
+
if (incident == null)
|
|
5
|
+
return undefined;
|
|
6
|
+
return incident.kind === 'KNOCK' ? 10 + incident.severity : incident.severity;
|
|
7
|
+
}
|
|
8
|
+
function incidentFromCompact(i) {
|
|
9
|
+
if (i == null)
|
|
10
|
+
return undefined;
|
|
11
|
+
return i >= 10 ? { kind: 'KNOCK', severity: i - 10 } : { kind: 'INJURY', severity: i };
|
|
12
|
+
}
|
|
2
13
|
export function transformToCompact(evt, playerIndex) {
|
|
3
14
|
const out = {
|
|
4
15
|
p: playerIndex.get(evt.playerId),
|
|
@@ -9,6 +20,9 @@ export function transformToCompact(evt, playerIndex) {
|
|
|
9
20
|
out.t = evt.type;
|
|
10
21
|
out.a = evt.target;
|
|
11
22
|
out.s = evt.score;
|
|
23
|
+
const i = incidentToCompact(evt.incident);
|
|
24
|
+
if (i != null)
|
|
25
|
+
out.i = i;
|
|
12
26
|
if (evt instanceof Block) {
|
|
13
27
|
out.b = evt.blockers.map(id => playerIndex.get(id));
|
|
14
28
|
}
|
|
@@ -31,7 +45,8 @@ export function transformToBlock(event, roster) {
|
|
|
31
45
|
playerId: roster[event.p],
|
|
32
46
|
target: event.a,
|
|
33
47
|
blockers: (event.b ?? []).map(i => roster[i]),
|
|
34
|
-
score: event.s
|
|
48
|
+
score: event.s,
|
|
49
|
+
incident: incidentFromCompact(event.i)
|
|
35
50
|
});
|
|
36
51
|
}
|
|
37
52
|
export function transformToLiberoReplacement(event, roster) {
|
|
@@ -47,7 +62,8 @@ export function transformToReception(event, roster) {
|
|
|
47
62
|
type: event.t,
|
|
48
63
|
playerId: roster[event.p],
|
|
49
64
|
target: event.a,
|
|
50
|
-
score: event.s
|
|
65
|
+
score: event.s,
|
|
66
|
+
incident: incidentFromCompact(event.i)
|
|
51
67
|
});
|
|
52
68
|
}
|
|
53
69
|
export function transformToServe(event, roster) {
|
|
@@ -56,7 +72,8 @@ export function transformToServe(event, roster) {
|
|
|
56
72
|
type: event.t,
|
|
57
73
|
playerId: roster[event.p],
|
|
58
74
|
target: event.a,
|
|
59
|
-
score: event.s
|
|
75
|
+
score: event.s,
|
|
76
|
+
incident: incidentFromCompact(event.i)
|
|
60
77
|
});
|
|
61
78
|
}
|
|
62
79
|
export function transformToSet(event, roster) {
|
|
@@ -65,7 +82,8 @@ export function transformToSet(event, roster) {
|
|
|
65
82
|
type: event.t,
|
|
66
83
|
playerId: roster[event.p],
|
|
67
84
|
target: event.a,
|
|
68
|
-
score: event.s
|
|
85
|
+
score: event.s,
|
|
86
|
+
incident: incidentFromCompact(event.i)
|
|
69
87
|
});
|
|
70
88
|
}
|
|
71
89
|
export function transformToSpike(event, roster) {
|
|
@@ -74,7 +92,8 @@ export function transformToSpike(event, roster) {
|
|
|
74
92
|
type: event.t,
|
|
75
93
|
playerId: roster[event.p],
|
|
76
94
|
target: event.a,
|
|
77
|
-
score: event.s
|
|
95
|
+
score: event.s,
|
|
96
|
+
incident: incidentFromCompact(event.i)
|
|
78
97
|
});
|
|
79
98
|
}
|
|
80
99
|
export function transformToSubstitution(event, roster) {
|
|
@@ -82,7 +82,7 @@ function transformToAttributes(tactics, teamId) {
|
|
|
82
82
|
rotation: p.rotation,
|
|
83
83
|
order: p.order.map((a) => a.id)
|
|
84
84
|
}))),
|
|
85
|
-
|
|
85
|
+
replace_knocked_immediately: tactics.replaceKnockedImmediately,
|
|
86
86
|
injury_replacements: tactics.injuryReplacements.map(ir => ({
|
|
87
87
|
starterId: ir.starter.id,
|
|
88
88
|
replacementId: ir.replacement.id,
|
|
@@ -165,7 +165,19 @@ function transformToObject(model, roster) {
|
|
|
165
165
|
rotation: p.rotation,
|
|
166
166
|
order: p.order.map((id) => findPlayer(id, roster))
|
|
167
167
|
}))),
|
|
168
|
-
|
|
168
|
+
// LENIENT: keep only starter-keyed boolean entries so stale jsonb (an ex-starter's key, a non-boolean
|
|
169
|
+
// value) can never fail schema validation and crash a team load; a dropped key = the ON default.
|
|
170
|
+
replaceKnockedImmediately: (() => {
|
|
171
|
+
const starterIds = new Set([lineup[CourtPosition.LEFT_FRONT], lineup[CourtPosition.MIDDLE_FRONT], lineup[CourtPosition.RIGHT_FRONT],
|
|
172
|
+
lineup[CourtPosition.LEFT_BACK], lineup[CourtPosition.MIDDLE_BACK], lineup[CourtPosition.RIGHT_BACK]]
|
|
173
|
+
.map((p) => p.id));
|
|
174
|
+
const healed = {};
|
|
175
|
+
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
176
|
+
if (starterIds.has(id) && typeof value === 'boolean')
|
|
177
|
+
healed[id] = value;
|
|
178
|
+
}
|
|
179
|
+
return healed;
|
|
180
|
+
})(),
|
|
169
181
|
// LENIENT (roster.find, not findPlayer): an entry whose starter or replacement is gone/invalid is DROPPED
|
|
170
182
|
// rather than crashing the sim batch (a dropped entry = unconfigured = the bench-only auto fallback, per
|
|
171
183
|
// the owner rules); an invalid benchBackfill drops only the backfill. First occurrence wins on duplicates;
|
|
@@ -264,7 +276,19 @@ function tacticsColumnsToApiDto(model) {
|
|
|
264
276
|
offensivePreferences: model.offensive_preferences,
|
|
265
277
|
systemSets: model.system_sets,
|
|
266
278
|
offensivePreferenceSets: model.offensive_preference_sets,
|
|
267
|
-
|
|
279
|
+
// Same stale-data policy as the heals below: only starter-keyed boolean entries survive, and an empty
|
|
280
|
+
// map surfaces as absent (missing key = ON).
|
|
281
|
+
replaceKnockedImmediately: (() => {
|
|
282
|
+
const starterSlotIds = new Set([model.lineup[CourtPosition.LEFT_FRONT], model.lineup[CourtPosition.MIDDLE_FRONT],
|
|
283
|
+
model.lineup[CourtPosition.RIGHT_FRONT], model.lineup[CourtPosition.LEFT_BACK],
|
|
284
|
+
model.lineup[CourtPosition.MIDDLE_BACK], model.lineup[CourtPosition.RIGHT_BACK]]);
|
|
285
|
+
const healed = {};
|
|
286
|
+
for (const [id, value] of Object.entries(model.replace_knocked_immediately ?? {})) {
|
|
287
|
+
if (starterSlotIds.has(id) && typeof value === 'boolean')
|
|
288
|
+
healed[id] = value;
|
|
289
|
+
}
|
|
290
|
+
return Object.keys(healed).length > 0 ? healed : undefined;
|
|
291
|
+
})(),
|
|
268
292
|
// Same stale-data policy as validSecondLibero above: don't surface entries the service load would drop
|
|
269
293
|
// (starter no longer a starter, replacement now a starter/libero/L2, duplicates), or the UI reloads and
|
|
270
294
|
// re-emits them forever. Roster membership can't be checked here (ids only); the service heal covers that.
|
|
@@ -329,7 +353,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
|
|
|
329
353
|
offensive_preferences: config.offensivePreferences,
|
|
330
354
|
system_sets: config.systemSets,
|
|
331
355
|
offensive_preference_sets: config.offensivePreferenceSets,
|
|
332
|
-
|
|
356
|
+
replace_knocked_immediately: config.replaceKnockedImmediately,
|
|
333
357
|
injury_replacements: config.injuryReplacements
|
|
334
358
|
};
|
|
335
359
|
}
|
|
@@ -31,8 +31,8 @@ export class Block extends InPlayEvent {
|
|
|
31
31
|
}
|
|
32
32
|
return new Block(result.data);
|
|
33
33
|
}
|
|
34
|
-
constructor({ score, playerId, target, failure, type, blockers, activeTraits }) {
|
|
35
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
34
|
+
constructor({ score, playerId, target, failure, type, blockers, activeTraits, incident }) {
|
|
35
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
36
36
|
this.type = type;
|
|
37
37
|
this.failure = failure;
|
|
38
38
|
this.blockers = blockers;
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { CourtTarget } from '../match';
|
|
2
2
|
import { RallyEvent, RallyEventOpts } from './rally-event';
|
|
3
3
|
import { Trait } from '../player';
|
|
4
|
+
export interface EventIncident {
|
|
5
|
+
readonly kind: 'KNOCK' | 'INJURY';
|
|
6
|
+
readonly severity: number;
|
|
7
|
+
}
|
|
4
8
|
export interface InPlayEventOpts extends RallyEventOpts {
|
|
5
9
|
readonly target: CourtTarget;
|
|
6
10
|
readonly score: number;
|
|
7
11
|
readonly activeTraits: Trait[];
|
|
12
|
+
readonly incident?: EventIncident;
|
|
8
13
|
}
|
|
9
14
|
export declare abstract class InPlayEvent<F, T> extends RallyEvent {
|
|
10
15
|
abstract failure: F;
|
|
@@ -12,6 +17,7 @@ export declare abstract class InPlayEvent<F, T> extends RallyEvent {
|
|
|
12
17
|
readonly target: CourtTarget;
|
|
13
18
|
readonly score: number;
|
|
14
19
|
readonly activeTraits: Trait[];
|
|
15
|
-
|
|
20
|
+
readonly incident?: EventIncident;
|
|
21
|
+
protected constructor({ eventType, playerId, target, score, activeTraits, incident }: InPlayEventOpts);
|
|
16
22
|
protected _toString(): string;
|
|
17
23
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { RallyEvent } from './rally-event';
|
|
2
2
|
export class InPlayEvent extends RallyEvent {
|
|
3
|
-
constructor({ eventType, playerId, target, score, activeTraits }) {
|
|
3
|
+
constructor({ eventType, playerId, target, score, activeTraits, incident }) {
|
|
4
4
|
super({ playerId, eventType });
|
|
5
5
|
this.target = target;
|
|
6
6
|
this.score = Number(score.toFixed(2));
|
|
7
7
|
this.activeTraits = activeTraits;
|
|
8
|
+
this.incident = incident;
|
|
8
9
|
}
|
|
9
10
|
_toString() {
|
|
10
11
|
const score = `"score":${this.score}`;
|
|
@@ -33,8 +33,8 @@ export class Reception extends InPlayEvent {
|
|
|
33
33
|
}
|
|
34
34
|
return new Reception(result.data);
|
|
35
35
|
}
|
|
36
|
-
constructor({ score, playerId, target, failure, type, activeTraits }) {
|
|
37
|
-
super({ score, playerId, target, activeTraits, eventType });
|
|
36
|
+
constructor({ score, playerId, target, failure, type, activeTraits, incident }) {
|
|
37
|
+
super({ score, playerId, target, activeTraits, incident, eventType });
|
|
38
38
|
this.type = type;
|
|
39
39
|
this.failure = failure;
|
|
40
40
|
}
|
|
@@ -27,5 +27,12 @@ export declare const BlockInputSchema: z.ZodObject<{
|
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
29
|
blockers: z.ZodArray<z.ZodUUID>;
|
|
30
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
31
|
+
kind: z.ZodEnum<{
|
|
32
|
+
KNOCK: "KNOCK";
|
|
33
|
+
INJURY: "INJURY";
|
|
34
|
+
}>;
|
|
35
|
+
severity: z.ZodNumber;
|
|
36
|
+
}, z.core.$strip>>;
|
|
30
37
|
}, z.core.$strip>;
|
|
31
38
|
export type BlockInput = z.infer<typeof BlockInputSchema>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
|
+
import { EventIncidentSchema } from './incident.z';
|
|
3
4
|
const blockTypeValues = [0, 1, 2, 3];
|
|
4
5
|
const blockFailureValues = [0, 1, 2, 3, 4, 5];
|
|
5
6
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -10,7 +11,8 @@ export const BlockInputSchema = z.object({
|
|
|
10
11
|
activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
|
|
11
12
|
failure: z.number().int().refine((v) => blockFailureValues.includes(v), { message: 'INVALID_BLOCK_FAILURE' }),
|
|
12
13
|
type: z.number().int().refine((v) => blockTypeValues.includes(v), { message: 'INVALID_BLOCK_TYPE' }),
|
|
13
|
-
blockers: z.array(z.uuid())
|
|
14
|
+
blockers: z.array(z.uuid()),
|
|
15
|
+
incident: EventIncidentSchema.optional()
|
|
14
16
|
}).superRefine((data, ctx) => {
|
|
15
17
|
// The block type IS the blocker count (NO_BLOCKER=0, SINGLE=1, DOUBLE=2, TRIPLE=3), so the two must agree.
|
|
16
18
|
if (data.blockers.length !== data.type) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const EventIncidentSchema: z.ZodObject<{
|
|
3
|
+
kind: z.ZodEnum<{
|
|
4
|
+
KNOCK: "KNOCK";
|
|
5
|
+
INJURY: "INJURY";
|
|
6
|
+
}>;
|
|
7
|
+
severity: z.ZodNumber;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type EventIncidentInput = z.infer<typeof EventIncidentSchema>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// The moment a player got hurt, attached to the causing in-play event (owner 2026-07-05: EVERY knock and
|
|
3
|
+
// injury marks the action that produced it). KNOCK severity is the knock tier (1-3); INJURY severity is the
|
|
4
|
+
// InjurySeverityEnum value (1-4).
|
|
5
|
+
export const EventIncidentSchema = z.object({
|
|
6
|
+
kind: z.enum(['KNOCK', 'INJURY']),
|
|
7
|
+
severity: z.number().int().min(1).max(4)
|
|
8
|
+
}).refine(v => v.kind !== 'KNOCK' || v.severity <= 3, { message: 'INVALID_KNOCK_TIER' });
|
|
@@ -26,5 +26,12 @@ export declare const ReceptionInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type ReceptionInput = z.infer<typeof ReceptionInputSchema>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
|
+
import { EventIncidentSchema } from './incident.z';
|
|
3
4
|
const receptionTypeValues = [0, 1, 2, 3, 4];
|
|
4
5
|
const receptionFailureValues = [0, 1, 2, 3, 4];
|
|
5
6
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -9,5 +10,6 @@ export const ReceptionInputSchema = z.object({
|
|
|
9
10
|
target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
10
11
|
activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
|
|
11
12
|
failure: z.number().int().refine((v) => receptionFailureValues.includes(v), { message: 'INVALID_RECEPTION_FAILURE' }),
|
|
12
|
-
type: z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' })
|
|
13
|
+
type: z.number().int().refine((v) => receptionTypeValues.includes(v), { message: 'INVALID_RECEPTION_TYPE' }),
|
|
14
|
+
incident: EventIncidentSchema.optional()
|
|
13
15
|
});
|
|
@@ -26,5 +26,12 @@ export declare const ServeInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type ServeInput = z.infer<typeof ServeInputSchema>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
|
+
import { EventIncidentSchema } from './incident.z';
|
|
3
4
|
const serveTypeValues = [0, 1, 2, 3];
|
|
4
5
|
const serveFailureValues = [0, 1, 2, 3, 4];
|
|
5
6
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -9,5 +10,6 @@ export const ServeInputSchema = z.object({
|
|
|
9
10
|
target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
10
11
|
activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
|
|
11
12
|
failure: z.number().int().refine((v) => serveFailureValues.includes(v), { message: 'INVALID_SERVE_FAILURE' }),
|
|
12
|
-
type: z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' })
|
|
13
|
+
type: z.number().int().refine((v) => serveTypeValues.includes(v), { message: 'INVALID_SERVE_TYPE' }),
|
|
14
|
+
incident: EventIncidentSchema.optional()
|
|
13
15
|
});
|
|
@@ -26,5 +26,12 @@ export declare const SetInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type SetInput = z.infer<typeof SetInputSchema>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { TraitEnum } from '../../player';
|
|
3
|
+
import { EventIncidentSchema } from './incident.z';
|
|
3
4
|
const setTypeValues = [0, 1];
|
|
4
5
|
const setFailureValues = [0, 1, 2, 3, 4];
|
|
5
6
|
const courtTargetValues = Array.from({ length: 13 }, (_, i) => i);
|
|
@@ -9,5 +10,6 @@ export const SetInputSchema = z.object({
|
|
|
9
10
|
target: z.number().int().refine((v) => courtTargetValues.includes(v), { message: 'INVALID_COURT_TARGET' }),
|
|
10
11
|
activeTraits: z.array(z.enum(Object.values(TraitEnum))).default([]),
|
|
11
12
|
failure: z.number().int().refine((v) => setFailureValues.includes(v), { message: 'INVALID_SET_FAILURE' }),
|
|
12
|
-
type: z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' })
|
|
13
|
+
type: z.number().int().refine((v) => setTypeValues.includes(v), { message: 'INVALID_SET_TYPE' }),
|
|
14
|
+
incident: EventIncidentSchema.optional()
|
|
13
15
|
});
|
|
@@ -26,5 +26,12 @@ export declare const SpikeInputSchema: z.ZodObject<{
|
|
|
26
26
|
}>>>;
|
|
27
27
|
failure: z.ZodNumber;
|
|
28
28
|
type: z.ZodNumber;
|
|
29
|
+
incident: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
kind: z.ZodEnum<{
|
|
31
|
+
KNOCK: "KNOCK";
|
|
32
|
+
INJURY: "INJURY";
|
|
33
|
+
}>;
|
|
34
|
+
severity: z.ZodNumber;
|
|
35
|
+
}, z.core.$strip>>;
|
|
29
36
|
}, z.core.$strip>;
|
|
30
37
|
export type SpikeInput = z.infer<typeof SpikeInputSchema>;
|