volleyballsimtypes 0.0.482 → 0.0.485
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 +3 -17
- package/dist/cjs/src/data/models/tactics.d.ts +7 -17
- package/dist/cjs/src/data/transformers/match-preset-override.test.js +10 -1
- package/dist/cjs/src/data/transformers/rally-event.d.ts +2 -1
- package/dist/cjs/src/data/transformers/rally-event.js +12 -0
- package/dist/cjs/src/data/transformers/rally.js +2 -0
- package/dist/cjs/src/data/transformers/rally.test.d.ts +1 -0
- package/dist/cjs/src/data/transformers/rally.test.js +62 -0
- package/dist/cjs/src/data/transformers/tactics.js +26 -54
- package/dist/cjs/src/data/transformers/tactics.test.js +16 -11
- package/dist/cjs/src/service/competition/season.d.ts +2 -3
- package/dist/cjs/src/service/competition/season.js +0 -11
- package/dist/cjs/src/service/event/block.d.ts +0 -1
- package/dist/cjs/src/service/event/block.js +0 -6
- package/dist/cjs/src/service/event/free-ball.d.ts +0 -1
- package/dist/cjs/src/service/event/free-ball.js +0 -5
- package/dist/cjs/src/service/event/in-play-event.d.ts +0 -1
- package/dist/cjs/src/service/event/in-play-event.js +0 -8
- package/dist/cjs/src/service/event/libero-replacement.d.ts +0 -1
- package/dist/cjs/src/service/event/libero-replacement.js +0 -7
- package/dist/cjs/src/service/event/rally-event.d.ts +0 -1
- package/dist/cjs/src/service/event/reception.d.ts +0 -1
- package/dist/cjs/src/service/event/reception.js +0 -5
- package/dist/cjs/src/service/event/serve.d.ts +0 -1
- package/dist/cjs/src/service/event/serve.js +0 -5
- package/dist/cjs/src/service/event/set.d.ts +0 -1
- package/dist/cjs/src/service/event/set.js +0 -6
- package/dist/cjs/src/service/event/spike.d.ts +0 -1
- package/dist/cjs/src/service/event/spike.js +0 -5
- package/dist/cjs/src/service/event/substitution.d.ts +0 -1
- package/dist/cjs/src/service/event/substitution.js +0 -7
- package/dist/cjs/src/service/player/improvement.d.ts +0 -1
- package/dist/cjs/src/service/player/improvement.js +0 -9
- package/dist/cjs/src/service/player/improvement.test.js +0 -15
- package/dist/cjs/src/service/team/designated-sub.d.ts +2 -4
- package/dist/cjs/src/service/team/energy-band.d.ts +0 -1
- package/dist/cjs/src/service/team/energy-band.js +1 -2
- package/dist/cjs/src/service/team/formation.d.ts +0 -1
- package/dist/cjs/src/service/team/formation.js +0 -3
- 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/libero-sub.d.ts +0 -4
- package/dist/cjs/src/service/team/pinch-condition.d.ts +11 -3
- package/dist/cjs/src/service/team/pinch-condition.js +9 -4
- package/dist/cjs/src/service/team/schemas/designated-sub.z.d.ts +30 -32
- package/dist/cjs/src/service/team/schemas/designated-sub.z.js +27 -23
- package/dist/cjs/src/service/team/schemas/designated-sub.z.test.js +4 -2
- package/dist/cjs/src/service/team/schemas/libero-sub.z.d.ts +15 -34
- package/dist/cjs/src/service/team/schemas/libero-sub.z.js +10 -26
- package/dist/cjs/src/service/team/schemas/libero-sub.z.test.js +74 -35
- package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +28 -36
- package/dist/cjs/src/service/team/schemas/tactics.z.js +4 -3
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +28 -36
- package/dist/cjs/src/service/team/sub-config-convert.d.ts +13 -0
- package/dist/cjs/src/service/team/sub-config-convert.js +91 -0
- package/dist/cjs/src/service/team/sub-config-convert.test.d.ts +1 -0
- package/dist/cjs/src/service/team/sub-config-convert.test.js +84 -0
- package/dist/cjs/src/service/utils/object-utils.d.ts +0 -1
- package/dist/cjs/src/service/utils/object-utils.js +0 -2
- package/dist/esm/src/api/index.d.ts +3 -17
- package/dist/esm/src/data/models/tactics.d.ts +7 -17
- package/dist/esm/src/data/transformers/match-preset-override.test.js +10 -1
- package/dist/esm/src/data/transformers/rally-event.d.ts +2 -1
- package/dist/esm/src/data/transformers/rally-event.js +12 -1
- package/dist/esm/src/data/transformers/rally.js +4 -2
- package/dist/esm/src/data/transformers/rally.test.d.ts +1 -0
- package/dist/esm/src/data/transformers/rally.test.js +60 -0
- package/dist/esm/src/data/transformers/tactics.js +27 -55
- package/dist/esm/src/data/transformers/tactics.test.js +16 -11
- package/dist/esm/src/service/competition/season.d.ts +2 -3
- package/dist/esm/src/service/competition/season.js +0 -11
- package/dist/esm/src/service/event/block.d.ts +0 -1
- package/dist/esm/src/service/event/block.js +0 -6
- package/dist/esm/src/service/event/free-ball.d.ts +0 -1
- package/dist/esm/src/service/event/free-ball.js +0 -5
- package/dist/esm/src/service/event/in-play-event.d.ts +0 -1
- package/dist/esm/src/service/event/in-play-event.js +0 -8
- package/dist/esm/src/service/event/libero-replacement.d.ts +0 -1
- package/dist/esm/src/service/event/libero-replacement.js +0 -7
- package/dist/esm/src/service/event/rally-event.d.ts +0 -1
- package/dist/esm/src/service/event/reception.d.ts +0 -1
- package/dist/esm/src/service/event/reception.js +0 -5
- package/dist/esm/src/service/event/serve.d.ts +0 -1
- package/dist/esm/src/service/event/serve.js +0 -5
- package/dist/esm/src/service/event/set.d.ts +0 -1
- package/dist/esm/src/service/event/set.js +0 -6
- package/dist/esm/src/service/event/spike.d.ts +0 -1
- package/dist/esm/src/service/event/spike.js +0 -5
- package/dist/esm/src/service/event/substitution.d.ts +0 -1
- package/dist/esm/src/service/event/substitution.js +0 -7
- package/dist/esm/src/service/player/improvement.d.ts +0 -1
- package/dist/esm/src/service/player/improvement.js +0 -8
- package/dist/esm/src/service/player/improvement.test.js +1 -16
- package/dist/esm/src/service/team/designated-sub.d.ts +2 -4
- package/dist/esm/src/service/team/energy-band.d.ts +0 -1
- package/dist/esm/src/service/team/energy-band.js +0 -1
- package/dist/esm/src/service/team/formation.d.ts +0 -1
- package/dist/esm/src/service/team/formation.js +0 -3
- 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/libero-sub.d.ts +0 -4
- package/dist/esm/src/service/team/pinch-condition.d.ts +11 -3
- package/dist/esm/src/service/team/pinch-condition.js +9 -4
- package/dist/esm/src/service/team/schemas/designated-sub.z.d.ts +30 -32
- package/dist/esm/src/service/team/schemas/designated-sub.z.js +26 -22
- package/dist/esm/src/service/team/schemas/designated-sub.z.test.js +4 -2
- package/dist/esm/src/service/team/schemas/libero-sub.z.d.ts +15 -34
- package/dist/esm/src/service/team/schemas/libero-sub.z.js +9 -25
- package/dist/esm/src/service/team/schemas/libero-sub.z.test.js +75 -36
- package/dist/esm/src/service/team/schemas/tactics.z.d.ts +28 -36
- package/dist/esm/src/service/team/schemas/tactics.z.js +4 -3
- package/dist/esm/src/service/team/schemas/team.z.d.ts +28 -36
- package/dist/esm/src/service/team/sub-config-convert.d.ts +13 -0
- package/dist/esm/src/service/team/sub-config-convert.js +85 -0
- package/dist/esm/src/service/team/sub-config-convert.test.d.ts +1 -0
- package/dist/esm/src/service/team/sub-config-convert.test.js +82 -0
- package/dist/esm/src/service/utils/object-utils.d.ts +0 -1
- package/dist/esm/src/service/utils/object-utils.js +1 -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, CourtPosition, ConditionLogic, EnergyBand,
|
|
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, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand } from '../service';
|
|
2
2
|
export type Rally = DataProps<_Rally> & {
|
|
3
3
|
homePlayerPosition: PlayerPosition[];
|
|
4
4
|
awayPlayerPosition: PlayerPosition[];
|
|
@@ -81,29 +81,17 @@ export interface StartingLineup {
|
|
|
81
81
|
bench: string[];
|
|
82
82
|
}
|
|
83
83
|
export interface ApiSetSubConfig {
|
|
84
|
-
mode: SubMode;
|
|
85
84
|
benchId?: string;
|
|
86
|
-
fatigueBand?: EnergyBand;
|
|
87
|
-
benchFatigueBand?: EnergyBand;
|
|
88
85
|
conditions?: PinchCondition[];
|
|
89
86
|
conditionLogic?: ConditionLogic;
|
|
90
87
|
pinchServer?: boolean;
|
|
88
|
+
benchFatigueBand?: EnergyBand;
|
|
91
89
|
subBackMode?: SubBackMode;
|
|
92
90
|
subBackConditions?: PinchCondition[];
|
|
93
91
|
subBackConditionLogic?: ConditionLogic;
|
|
94
92
|
}
|
|
95
|
-
export interface ApiDesignatedSub {
|
|
93
|
+
export interface ApiDesignatedSub extends ApiSetSubConfig {
|
|
96
94
|
starterId: string;
|
|
97
|
-
benchId?: string;
|
|
98
|
-
mode: SubMode;
|
|
99
|
-
fatigueBand?: EnergyBand;
|
|
100
|
-
benchFatigueBand?: EnergyBand;
|
|
101
|
-
conditions?: PinchCondition[];
|
|
102
|
-
conditionLogic?: ConditionLogic;
|
|
103
|
-
pinchServer?: boolean;
|
|
104
|
-
subBackMode?: SubBackMode;
|
|
105
|
-
subBackConditions?: PinchCondition[];
|
|
106
|
-
subBackConditionLogic?: ConditionLogic;
|
|
107
95
|
sets?: ApiSetSubConfig[];
|
|
108
96
|
}
|
|
109
97
|
export interface ApiOffensivePreference {
|
|
@@ -130,8 +118,6 @@ export interface ApiLiberoInjury {
|
|
|
130
118
|
inMatchDesignateId?: string;
|
|
131
119
|
}
|
|
132
120
|
export interface ApiLiberoSetSubConfig {
|
|
133
|
-
mode: LiberoSubMode;
|
|
134
|
-
fatigueBand?: EnergyBand;
|
|
135
121
|
conditions?: PinchCondition[];
|
|
136
122
|
conditionLogic?: ConditionLogic;
|
|
137
123
|
}
|
|
@@ -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 { ConditionLogic, CourtPosition, EnergyBand,
|
|
4
|
+
import { ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RoleEnum, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
|
|
5
5
|
export interface TacticsLineupAttributes {
|
|
6
6
|
[CourtPosition.LIBERO_ZONE]?: PlayerId;
|
|
7
7
|
[CourtPosition.LEFT_BACK]: PlayerId;
|
|
@@ -13,30 +13,20 @@ export interface TacticsLineupAttributes {
|
|
|
13
13
|
bench: PlayerId[];
|
|
14
14
|
}
|
|
15
15
|
export interface SetSubConfigAttributes {
|
|
16
|
-
mode: SubMode;
|
|
17
16
|
benchId?: PlayerId;
|
|
18
|
-
fatigueBand?: EnergyBand;
|
|
19
|
-
benchFatigueBand?: EnergyBand;
|
|
20
17
|
conditions?: PinchCondition[];
|
|
21
18
|
conditionLogic?: ConditionLogic;
|
|
22
19
|
pinchServer?: boolean;
|
|
20
|
+
benchFatigueBand?: EnergyBand;
|
|
23
21
|
subBackMode?: SubBackMode;
|
|
24
22
|
subBackConditions?: PinchCondition[];
|
|
25
23
|
subBackConditionLogic?: ConditionLogic;
|
|
24
|
+
mode?: SubMode;
|
|
25
|
+
fatigueBand?: SubBand;
|
|
26
26
|
}
|
|
27
|
-
export interface DesignatedSubAttributes {
|
|
27
|
+
export interface DesignatedSubAttributes extends SetSubConfigAttributes {
|
|
28
28
|
starterId: PlayerId;
|
|
29
|
-
benchId?: PlayerId;
|
|
30
|
-
mode?: SubMode;
|
|
31
29
|
isPinchServer?: boolean;
|
|
32
|
-
fatigueBand?: SubBand;
|
|
33
|
-
benchFatigueBand?: EnergyBand;
|
|
34
|
-
conditions?: PinchCondition[];
|
|
35
|
-
conditionLogic?: ConditionLogic;
|
|
36
|
-
pinchServer?: boolean;
|
|
37
|
-
subBackMode?: SubBackMode;
|
|
38
|
-
subBackConditions?: PinchCondition[];
|
|
39
|
-
subBackConditionLogic?: ConditionLogic;
|
|
40
30
|
sets?: SetSubConfigAttributes[];
|
|
41
31
|
}
|
|
42
32
|
export interface OffensivePreferenceAttributes {
|
|
@@ -63,10 +53,10 @@ export interface LiberoInjuryAttributes {
|
|
|
63
53
|
inMatchDesignateId?: PlayerId;
|
|
64
54
|
}
|
|
65
55
|
export interface LiberoSetSubConfigAttributes {
|
|
66
|
-
mode: LiberoSubMode;
|
|
67
|
-
fatigueBand?: EnergyBand;
|
|
68
56
|
conditions?: PinchCondition[];
|
|
69
57
|
conditionLogic?: ConditionLogic;
|
|
58
|
+
mode?: string;
|
|
59
|
+
fatigueBand?: EnergyBand;
|
|
70
60
|
}
|
|
71
61
|
export interface LiberoSubConfigAttributes extends LiberoSetSubConfigAttributes {
|
|
72
62
|
sets?: LiberoSetSubConfigAttributes[];
|
|
@@ -148,7 +148,16 @@ describe('apiTacticsConfigToTacticsAttributes()', () => {
|
|
|
148
148
|
expect(back.lineup).toEqual(full.lineup);
|
|
149
149
|
expect(back.libero_replacements).toEqual(full.libero_replacements);
|
|
150
150
|
expect(back.receive_rotation_offset).toBe(true);
|
|
151
|
-
|
|
151
|
+
// The dto path converts legacy mode-shaped subs to the unified condition engine, so the round-trip lands
|
|
152
|
+
// in the converted (unified) shape rather than the legacy input.
|
|
153
|
+
expect(back.designated_subs).toEqual([{
|
|
154
|
+
starterId: ids[0],
|
|
155
|
+
conditions: [{ type: 'FATIGUE', band: 'WORN' }],
|
|
156
|
+
conditionLogic: 'ALL',
|
|
157
|
+
pinchServer: false,
|
|
158
|
+
subBackMode: 'FATIGUE',
|
|
159
|
+
sets: [{ conditions: [] }]
|
|
160
|
+
}]);
|
|
152
161
|
expect(back.libero_replacement_sets).toEqual(full.libero_replacement_sets);
|
|
153
162
|
expect(back.rotation_system).toBe('5-1');
|
|
154
163
|
expect(back.designated_setters).toEqual(full.designated_setters);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Block, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
1
|
+
import { Block, FreeBall, LiberoReplacement, RallyEvent, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
2
2
|
export interface CompactEvent {
|
|
3
3
|
p: number;
|
|
4
4
|
e: number;
|
|
@@ -21,4 +21,5 @@ export declare function transformToReception(event: CompactEvent, roster: string
|
|
|
21
21
|
export declare function transformToServe(event: CompactEvent, roster: string[]): Serve;
|
|
22
22
|
export declare function transformToSet(event: CompactEvent, roster: string[]): Set;
|
|
23
23
|
export declare function transformToSpike(event: CompactEvent, roster: string[]): Spike;
|
|
24
|
+
export declare function transformToFreeBall(event: CompactEvent, roster: string[]): FreeBall;
|
|
24
25
|
export declare function transformToSubstitution(event: CompactEvent, roster: string[]): Substitution;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Block, InPlayEvent, LiberoReplacement, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
1
|
+
import { Block, FreeBall, InPlayEvent, LiberoReplacement, Reception, Serve, Set, Spike, Substitution } from '../../service';
|
|
2
2
|
// Incident packing for the compact key 'i': values below 10 are injury severities, 10+ are knock tiers.
|
|
3
3
|
function incidentToCompact(incident) {
|
|
4
4
|
if (incident == null)
|
|
@@ -108,6 +108,17 @@ export function transformToSpike(event, roster) {
|
|
|
108
108
|
incident: incidentFromCompact(event.i, event.h, roster)
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
// FreeBall (Last Resort save) is a plain InPlayEvent with no incident of its own, so it decodes from f/t/a/s
|
|
112
|
+
// exactly like a spike minus the incident. Must stay in lockstep with the case 7 branch in rally.ts.
|
|
113
|
+
export function transformToFreeBall(event, roster) {
|
|
114
|
+
return FreeBall.create({
|
|
115
|
+
failure: event.f,
|
|
116
|
+
type: event.t,
|
|
117
|
+
playerId: roster[event.p],
|
|
118
|
+
target: event.a,
|
|
119
|
+
score: event.s
|
|
120
|
+
});
|
|
121
|
+
}
|
|
111
122
|
export function transformToSubstitution(event, roster) {
|
|
112
123
|
return Substitution.create({
|
|
113
124
|
playerId: roster[event.p],
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { pack, unpack } from 'msgpackr';
|
|
2
2
|
import { deflateRawSync, inflateRawSync } from 'zlib';
|
|
3
3
|
import { decompressFromBase64 } from 'lz-string';
|
|
4
|
-
import { Block, LiberoReplacement, Rally, Reception, Serve, Set as VolleySet, Spike, Substitution } from '../../service';
|
|
5
|
-
import { transformToBlock, transformToCompact, transformToLiberoReplacement, transformToReception, transformToServe, transformToSet, transformToSpike, transformToSubstitution } from './rally-event';
|
|
4
|
+
import { Block, FreeBall, LiberoReplacement, Rally, Reception, Serve, Set as VolleySet, Spike, Substitution } from '../../service';
|
|
5
|
+
import { transformToBlock, transformToCompact, transformToFreeBall, transformToLiberoReplacement, transformToReception, transformToServe, transformToSet, transformToSpike, transformToSubstitution } from './rally-event';
|
|
6
6
|
const VERSION_BYTE = 0x01;
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
8
|
function deflate(data) { return deflateRawSync(data); }
|
|
@@ -57,6 +57,7 @@ function decodeCompactEvent(event, roster) {
|
|
|
57
57
|
case 4: return transformToSet(event, roster);
|
|
58
58
|
case 5: return transformToSpike(event, roster);
|
|
59
59
|
case 6: return transformToBlock(event, roster);
|
|
60
|
+
case 7: return transformToFreeBall(event, roster);
|
|
60
61
|
default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -69,6 +70,7 @@ function decodeLegacyEvent(event) {
|
|
|
69
70
|
case 4: return VolleySet.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
70
71
|
case 5: return Spike.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
71
72
|
case 6: return Block.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, blockers: event.b ?? [], score: event.s });
|
|
73
|
+
case 7: return FreeBall.create({ failure: event.f, type: event.t, playerId: event.p, target: event.a, score: event.s });
|
|
72
74
|
default: throw new Error(`UNKNOWN_EVENT_TYPE: ${String(event.e)}`);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { Block, BlockFailureEnum, BlockTypeEnum, EventTypeEnum, FreeBall, FreeBallFailureEnum, FreeBallTypeEnum, LiberoReplacement, LiberoReplacementTypeEnum, Rally, Reception, ReceptionFailureEnum, ReceptionTypeEnum, Serve, ServeFailureEnum, ServeTypeEnum, Set as VolleySet, SetFailureEnum, SetTempoEnum, SetTypeEnum, Spike, SpikeFailureEnum, SpikeTypeEnum, Substitution } from '../../service';
|
|
4
|
+
import { transformFromRally, transformToRally } from './rally';
|
|
5
|
+
// End-to-end rally round trip (encode -> bytea -> decode) exercising the decodeCompactEvent switch, with one
|
|
6
|
+
// event of EVERY EventTypeEnum member. This is the guard that would have caught the FREE_BALL decoder gap: the
|
|
7
|
+
// encoder is generic (out.e = evt.eventType) but the decoder is a hardcoded switch, so a new event type that is
|
|
8
|
+
// not wired into the switch throws UNKNOWN_EVENT_TYPE here. The coverage test forces new members into this list.
|
|
9
|
+
describe('rally compact round trip — every event type', () => {
|
|
10
|
+
const pA = uuidv4();
|
|
11
|
+
const pB = uuidv4();
|
|
12
|
+
const pC = uuidv4();
|
|
13
|
+
const setId = uuidv4();
|
|
14
|
+
const events = [
|
|
15
|
+
LiberoReplacement.create({ playerId: pA, libero: pB, type: LiberoReplacementTypeEnum.LIBERO_IN }),
|
|
16
|
+
Substitution.create({ playerId: pA, playerOut: pC, injury: true }),
|
|
17
|
+
Serve.create({ playerId: pA, target: 6, score: 55, failure: ServeFailureEnum.NO_FAILURE, type: ServeTypeEnum.JUMP_TOPSPIN }),
|
|
18
|
+
Reception.create({ playerId: pB, target: 5, score: 50, failure: ReceptionFailureEnum.NO_FAILURE, type: ReceptionTypeEnum.DIG }),
|
|
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.CROSS_SHOT }),
|
|
21
|
+
Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: BlockFailureEnum.NO_FAILURE, type: BlockTypeEnum.DOUBLE }),
|
|
22
|
+
FreeBall.create({ playerId: pC, target: 6, score: 45, failure: FreeBallFailureEnum.NO_FAILURE, type: FreeBallTypeEnum.OVER })
|
|
23
|
+
];
|
|
24
|
+
const rally = Rally.create({ id: uuidv4(), order: 0, servingTeamId: uuidv4(), events });
|
|
25
|
+
it('covers every EventTypeEnum member (adding a new event type must extend this list)', () => {
|
|
26
|
+
const numericTypes = Object.values(EventTypeEnum).filter((v) => typeof v === 'number');
|
|
27
|
+
const present = new Set(events.map(e => e.eventType));
|
|
28
|
+
for (const t of numericTypes)
|
|
29
|
+
expect(present.has(t)).toBe(true);
|
|
30
|
+
expect(present.size).toBe(numericTypes.length);
|
|
31
|
+
});
|
|
32
|
+
it('encodes then decodes every event type back to an equal event (guards the decoder switch)', () => {
|
|
33
|
+
const attrs = transformFromRally(rally, setId);
|
|
34
|
+
const decoded = transformToRally(attrs);
|
|
35
|
+
expect(decoded.events).toHaveLength(events.length);
|
|
36
|
+
expect(decoded.events.map(e => e.eventType)).toEqual(events.map(e => e.eventType));
|
|
37
|
+
const [lib, sub, serve, rec, set, spike, block, free] = decoded.events;
|
|
38
|
+
expect(lib.playerId).toBe(pA);
|
|
39
|
+
expect(lib.libero).toBe(pB);
|
|
40
|
+
expect(lib.type).toBe(LiberoReplacementTypeEnum.LIBERO_IN);
|
|
41
|
+
expect(sub.playerOut).toBe(pC);
|
|
42
|
+
expect(sub.injury).toBe(true);
|
|
43
|
+
expect(serve.target).toBe(6);
|
|
44
|
+
expect(serve.score).toBe(55);
|
|
45
|
+
expect(serve.type).toBe(ServeTypeEnum.JUMP_TOPSPIN);
|
|
46
|
+
expect(rec.target).toBe(5);
|
|
47
|
+
expect(rec.type).toBe(ReceptionTypeEnum.DIG);
|
|
48
|
+
expect(set.type).toBe(SetTypeEnum.OVERHAND);
|
|
49
|
+
expect(set.tempo).toBe(SetTempoEnum.FIRST);
|
|
50
|
+
expect(spike.type).toBe(SpikeTypeEnum.CROSS_SHOT);
|
|
51
|
+
expect(spike.score).toBe(62.5);
|
|
52
|
+
expect(block.type).toBe(BlockTypeEnum.DOUBLE);
|
|
53
|
+
expect(block.blockers).toEqual([pB, pC]);
|
|
54
|
+
expect(free.playerId).toBe(pC);
|
|
55
|
+
expect(free.target).toBe(6);
|
|
56
|
+
expect(free.score).toBe(45);
|
|
57
|
+
expect(free.type).toBe(FreeBallTypeEnum.OVER);
|
|
58
|
+
expect(free.failure).toBe(FreeBallFailureEnum.NO_FAILURE);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import { CourtPosition, Tactics } from '../../service';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
function subModeFromAttributes(d) {
|
|
6
|
-
if (d.mode != null)
|
|
7
|
-
return d.mode;
|
|
8
|
-
if (d.isPinchServer === true)
|
|
9
|
-
return 'PINCH';
|
|
10
|
-
if (d.fatigueBand === 'NEVER')
|
|
11
|
-
return 'NEVER';
|
|
12
|
-
return 'FATIGUE';
|
|
13
|
-
}
|
|
14
|
-
// pinchServer only applies to PINCH ("Conditions") mode. A LEGACY PINCH row (no flag) defaults to true so
|
|
15
|
-
// existing pinch servers keep serve-and-return behavior; a new conditional sub persists an explicit false. This
|
|
16
|
-
// default must resolve identically on every READ path (both transformToObject and tacticsColumnsToApiDto).
|
|
17
|
-
function effectivePinchServer(mode, pinchServer) {
|
|
18
|
-
return mode === 'PINCH' ? (pinchServer ?? true) : undefined;
|
|
19
|
-
}
|
|
1
|
+
import { convertLegacyLiberoSubConfig, convertLegacySetSubConfig, CourtPosition, Tactics } from '../../service';
|
|
2
|
+
// Unified condition engine (owner 2026-07-11): the READ path passes legacy fields (mode / fatigueBand /
|
|
3
|
+
// isPinchServer) through RAW; the schema preprocess in Tactics.create converts every legacy shape (see
|
|
4
|
+
// service/team/sub-config-convert.ts). The WRITE path persists only the unified shape.
|
|
20
5
|
function findPlayer(id, roster) {
|
|
21
6
|
const player = roster.find((p) => p.id === id);
|
|
22
7
|
if (player == null)
|
|
@@ -60,23 +45,19 @@ function transformToAttributes(tactics, teamId) {
|
|
|
60
45
|
designated_subs: tactics.designatedSubs.map(ds => ({
|
|
61
46
|
starterId: ds.starter.id,
|
|
62
47
|
benchId: ds.bench?.id,
|
|
63
|
-
mode: ds.mode,
|
|
64
|
-
fatigueBand: ds.fatigueBand,
|
|
65
|
-
benchFatigueBand: ds.benchFatigueBand,
|
|
66
48
|
conditions: ds.conditions,
|
|
67
49
|
conditionLogic: ds.conditionLogic,
|
|
68
50
|
pinchServer: ds.pinchServer,
|
|
51
|
+
benchFatigueBand: ds.benchFatigueBand,
|
|
69
52
|
subBackMode: ds.subBackMode,
|
|
70
53
|
subBackConditions: ds.subBackConditions,
|
|
71
54
|
subBackConditionLogic: ds.subBackConditionLogic,
|
|
72
55
|
sets: ds.sets?.map(s => ({
|
|
73
|
-
mode: s.mode,
|
|
74
56
|
benchId: s.bench?.id,
|
|
75
|
-
fatigueBand: s.fatigueBand,
|
|
76
|
-
benchFatigueBand: s.benchFatigueBand,
|
|
77
57
|
conditions: s.conditions,
|
|
78
58
|
conditionLogic: s.conditionLogic,
|
|
79
59
|
pinchServer: s.pinchServer,
|
|
60
|
+
benchFatigueBand: s.benchFatigueBand,
|
|
80
61
|
subBackMode: s.subBackMode,
|
|
81
62
|
subBackConditions: s.subBackConditions,
|
|
82
63
|
subBackConditionLogic: s.subBackConditionLogic
|
|
@@ -159,15 +140,19 @@ function transformToObject(model, roster) {
|
|
|
159
140
|
secondLibero,
|
|
160
141
|
liberoSub: secondLibero != null ? (model.libero_sub ?? undefined) : undefined,
|
|
161
142
|
liberoReplacementSets,
|
|
143
|
+
// Legacy fields (mode / fatigueBand / isPinchServer) pass through RAW: the DesignatedSubSchema preprocess
|
|
144
|
+
// inside Tactics.create converts every legacy shape (mode-era AND isPinchServer-era rows) to the unified
|
|
145
|
+
// condition engine. Only the player references are resolved here.
|
|
162
146
|
designatedSubs: (model.designated_subs ?? []).map(d => ({
|
|
163
147
|
starter: findPlayer(d.starterId, roster),
|
|
164
148
|
bench: d.benchId != null ? findPlayer(d.benchId, roster) : undefined,
|
|
165
|
-
mode:
|
|
166
|
-
|
|
149
|
+
mode: d.mode,
|
|
150
|
+
isPinchServer: d.isPinchServer,
|
|
151
|
+
fatigueBand: d.fatigueBand,
|
|
167
152
|
benchFatigueBand: d.benchFatigueBand,
|
|
168
153
|
conditions: d.conditions,
|
|
169
154
|
conditionLogic: d.conditionLogic,
|
|
170
|
-
pinchServer:
|
|
155
|
+
pinchServer: d.pinchServer,
|
|
171
156
|
subBackMode: d.subBackMode,
|
|
172
157
|
subBackConditions: d.subBackConditions,
|
|
173
158
|
subBackConditionLogic: d.subBackConditionLogic,
|
|
@@ -178,7 +163,7 @@ function transformToObject(model, roster) {
|
|
|
178
163
|
benchFatigueBand: s.benchFatigueBand,
|
|
179
164
|
conditions: s.conditions,
|
|
180
165
|
conditionLogic: s.conditionLogic,
|
|
181
|
-
pinchServer:
|
|
166
|
+
pinchServer: s.pinchServer,
|
|
182
167
|
subBackMode: s.subBackMode,
|
|
183
168
|
subBackConditions: s.subBackConditions,
|
|
184
169
|
subBackConditionLogic: s.subBackConditionLogic
|
|
@@ -326,33 +311,20 @@ function tacticsColumnsToApiDto(model) {
|
|
|
326
311
|
receiveRotationOffset: model.receive_rotation_offset,
|
|
327
312
|
substitutionBand: model.substitution_band,
|
|
328
313
|
secondLibero: validSecondLibero,
|
|
329
|
-
|
|
314
|
+
// The API surfaces the UNIFIED condition-engine shape: legacy rows (mode-era / isPinchServer-era) convert
|
|
315
|
+
// here via the same service converters the schema preprocess uses, so clients never see a mode.
|
|
316
|
+
liberoSub: validSecondLibero != null
|
|
317
|
+
? (model.libero_sub != null ? convertLegacyLiberoSubConfig(model.libero_sub) : undefined)
|
|
318
|
+
: undefined,
|
|
330
319
|
liberoReplacementSets: validLiberoReplacementSets,
|
|
331
|
-
designatedSubs: (model.designated_subs ?? []).map(d =>
|
|
332
|
-
starterId
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
pinchServer: effectivePinchServer(subModeFromAttributes(d), d.pinchServer),
|
|
340
|
-
subBackMode: d.subBackMode,
|
|
341
|
-
subBackConditions: d.subBackConditions,
|
|
342
|
-
subBackConditionLogic: d.subBackConditionLogic,
|
|
343
|
-
sets: d.sets?.map(s => ({
|
|
344
|
-
mode: s.mode,
|
|
345
|
-
benchId: s.benchId,
|
|
346
|
-
fatigueBand: s.fatigueBand,
|
|
347
|
-
benchFatigueBand: s.benchFatigueBand,
|
|
348
|
-
conditions: s.conditions,
|
|
349
|
-
conditionLogic: s.conditionLogic,
|
|
350
|
-
pinchServer: effectivePinchServer(s.mode, s.pinchServer),
|
|
351
|
-
subBackMode: s.subBackMode,
|
|
352
|
-
subBackConditions: s.subBackConditions,
|
|
353
|
-
subBackConditionLogic: s.subBackConditionLogic
|
|
354
|
-
}))
|
|
355
|
-
})),
|
|
320
|
+
designatedSubs: (model.designated_subs ?? []).map(d => {
|
|
321
|
+
const { starterId, sets, ...cfg } = d;
|
|
322
|
+
return {
|
|
323
|
+
...convertLegacySetSubConfig(cfg),
|
|
324
|
+
starterId,
|
|
325
|
+
sets: sets?.map(s => convertLegacySetSubConfig(s))
|
|
326
|
+
};
|
|
327
|
+
}),
|
|
356
328
|
rotationSystem: model.rotation_system,
|
|
357
329
|
designatedSetters: model.designated_setters,
|
|
358
330
|
offensivePreferences: model.offensive_preferences,
|
|
@@ -38,25 +38,28 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
38
38
|
expect(dto.lineup.bench).toEqual(['b1', 'b2']);
|
|
39
39
|
expect(dto.offensivePreferences).toEqual([{ rotation: 1, weights: { lf: 60, rf: 40 } }]);
|
|
40
40
|
});
|
|
41
|
-
it(
|
|
41
|
+
it('converts a legacy isPinchServer entry to a unified pinch server (conditions kept, no mode)', () => {
|
|
42
42
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
43
43
|
{ starterId: 's1', benchId: 'b1', isPinchServer: true, conditions: [{ type: 'ASAP' }] }
|
|
44
44
|
]));
|
|
45
|
-
expect(dto.designatedSubs[0].
|
|
45
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(true);
|
|
46
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'ASAP' }]);
|
|
47
|
+
expect(dto.designatedSubs[0].mode).toBeUndefined();
|
|
46
48
|
});
|
|
47
|
-
it("
|
|
49
|
+
it("converts a legacy 'NEVER' fatigueBand to an explicit empty condition list", () => {
|
|
48
50
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
49
51
|
{ starterId: 's1', fatigueBand: 'NEVER' }
|
|
50
52
|
]));
|
|
51
|
-
expect(dto.designatedSubs[0].
|
|
53
|
+
expect(dto.designatedSubs[0].conditions).toEqual([]);
|
|
52
54
|
expect(dto.designatedSubs[0].fatigueBand).toBeUndefined();
|
|
53
55
|
});
|
|
54
|
-
it('
|
|
56
|
+
it('converts a legacy fatigue entry to a FATIGUE condition carrying its band', () => {
|
|
55
57
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
56
58
|
{ starterId: 's1', benchId: 'b1', mode: 'FATIGUE', fatigueBand: EnergyBand.WORN, benchFatigueBand: EnergyBand.TIRED }
|
|
57
59
|
]));
|
|
58
|
-
expect(dto.designatedSubs[0].
|
|
59
|
-
expect(dto.designatedSubs[0].
|
|
60
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: EnergyBand.WORN }]);
|
|
61
|
+
expect(dto.designatedSubs[0].subBackMode).toBe('FATIGUE');
|
|
62
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(false);
|
|
60
63
|
expect(dto.designatedSubs[0].benchFatigueBand).toBe(EnergyBand.TIRED);
|
|
61
64
|
});
|
|
62
65
|
it('defaults a legacy PINCH row (no pinchServer flag) to pinchServer true', () => {
|
|
@@ -65,11 +68,12 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
65
68
|
]));
|
|
66
69
|
expect(dto.designatedSubs[0].pinchServer).toBe(true);
|
|
67
70
|
});
|
|
68
|
-
it('
|
|
71
|
+
it('passes an already-unified entry through untouched', () => {
|
|
69
72
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
70
|
-
{ starterId: 's1', benchId: 'b1',
|
|
73
|
+
{ starterId: 's1', benchId: 'b1', conditions: [{ type: 'FATIGUE', band: EnergyBand.TIRED }], conditionLogic: 'ALL', pinchServer: false, subBackMode: 'FATIGUE' }
|
|
71
74
|
]));
|
|
72
|
-
expect(dto.designatedSubs[0].
|
|
75
|
+
expect(dto.designatedSubs[0].conditions).toEqual([{ type: 'FATIGUE', band: EnergyBand.TIRED }]);
|
|
76
|
+
expect(dto.designatedSubs[0].pinchServer).toBe(false);
|
|
73
77
|
});
|
|
74
78
|
it('passes a conditional regular sub (pinchServer false + condition sub-back) through unchanged', () => {
|
|
75
79
|
const dto = tacticsColumnsToApiDto(makeTactics([
|
|
@@ -106,7 +110,8 @@ describe('tacticsColumnsToApiDto()', () => {
|
|
|
106
110
|
model.libero_sub = { mode: 'FATIGUE' };
|
|
107
111
|
const dto = tacticsColumnsToApiDto(model);
|
|
108
112
|
expect(dto.secondLibero).toBe('reserve1');
|
|
109
|
-
|
|
113
|
+
// Legacy FATIGUE libero rule converts to the unified condition shape on the API read path.
|
|
114
|
+
expect(dto.liberoSub).toEqual({ conditions: [{ type: 'FATIGUE' }], conditionLogic: 'ALL' });
|
|
110
115
|
});
|
|
111
116
|
it('keeps a libero-keyed replaceKnockedImmediately entry when a second libero exists (L2-only feature)', () => {
|
|
112
117
|
const model = makeTactics([]);
|
|
@@ -10,12 +10,11 @@ export declare class Season {
|
|
|
10
10
|
readonly iteration: Iteration;
|
|
11
11
|
readonly divisionId: string;
|
|
12
12
|
readonly standings: Standing[];
|
|
13
|
-
private _status;
|
|
14
|
-
private _champion?;
|
|
13
|
+
private readonly _status;
|
|
14
|
+
private readonly _champion?;
|
|
15
15
|
static create(input: unknown): Season;
|
|
16
16
|
private constructor();
|
|
17
17
|
calculateStandings(): Standing[];
|
|
18
|
-
updateStandings(): void;
|
|
19
18
|
get champion(): Team | undefined;
|
|
20
19
|
get status(): Status;
|
|
21
20
|
static createSeason(iteration: Iteration, teams: Team[], divisionId: string): Season;
|
|
@@ -34,17 +34,6 @@ export class Season {
|
|
|
34
34
|
return standing;
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
updateStandings() {
|
|
38
|
-
if (this.matches == null || this.matches.length < 1)
|
|
39
|
-
throw new Error('NO_MATCHES');
|
|
40
|
-
const updated = this.calculateStandings();
|
|
41
|
-
this.standings.splice(0, this.standings.length, ...updated);
|
|
42
|
-
const isOver = this.matches.every((match) => match.isOver());
|
|
43
|
-
if (isOver) {
|
|
44
|
-
this._status = StatusEnum.COMPLETE;
|
|
45
|
-
this._champion = this.teams.find((team) => team.id === this.standings[0].teamId);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
37
|
get champion() {
|
|
49
38
|
if (this._champion != null)
|
|
50
39
|
return this._champion;
|
|
@@ -37,10 +37,4 @@ export class Block extends InPlayEvent {
|
|
|
37
37
|
this.failure = failure;
|
|
38
38
|
this.blockers = blockers;
|
|
39
39
|
}
|
|
40
|
-
toString() {
|
|
41
|
-
const failure = `"failure":${this.failure}`;
|
|
42
|
-
const type = `"type":${this.type}`;
|
|
43
|
-
const blockers = `"blockers":[${this.blockers.map(b => `"${b}"`).join(',')}]`;
|
|
44
|
-
return `{${this._toString()},${failure},${type},${blockers}}`;
|
|
45
|
-
}
|
|
46
40
|
}
|
|
@@ -35,9 +35,4 @@ export class FreeBall extends InPlayEvent {
|
|
|
35
35
|
this.type = type;
|
|
36
36
|
this.failure = failure;
|
|
37
37
|
}
|
|
38
|
-
toString() {
|
|
39
|
-
const failure = `"failure":${this.failure}`;
|
|
40
|
-
const type = `"type":${this.type}`;
|
|
41
|
-
return `{${this._toString()},${failure},${type}}`;
|
|
42
|
-
}
|
|
43
38
|
}
|
|
@@ -20,5 +20,4 @@ export declare abstract class InPlayEvent<F, T> extends RallyEvent {
|
|
|
20
20
|
readonly activeTraits: Trait[];
|
|
21
21
|
readonly incident?: EventIncident;
|
|
22
22
|
protected constructor({ eventType, playerId, target, score, activeTraits, incident }: InPlayEventOpts);
|
|
23
|
-
protected _toString(): string;
|
|
24
23
|
}
|
|
@@ -7,12 +7,4 @@ export class InPlayEvent extends RallyEvent {
|
|
|
7
7
|
this.activeTraits = activeTraits;
|
|
8
8
|
this.incident = incident;
|
|
9
9
|
}
|
|
10
|
-
_toString() {
|
|
11
|
-
const score = `"score":${this.score}`;
|
|
12
|
-
const playerId = `"playerId":"${this.playerId}"`;
|
|
13
|
-
const target = `"target":${this.target}`;
|
|
14
|
-
const eventType = `"eventType":${this.eventType}`;
|
|
15
|
-
const traits = `"activeTraits":[${this.activeTraits.map(t => `"${t}"`).join(',')}]`;
|
|
16
|
-
return `${score},${playerId},${target},${eventType},${traits}`;
|
|
17
|
-
}
|
|
18
10
|
}
|
|
@@ -24,11 +24,4 @@ export class LiberoReplacement extends RallyEvent {
|
|
|
24
24
|
this.type = type;
|
|
25
25
|
this.libero = libero;
|
|
26
26
|
}
|
|
27
|
-
toString() {
|
|
28
|
-
const playerId = `"playerId":"${this.playerId}"`;
|
|
29
|
-
const type = `"type":${this.type}`;
|
|
30
|
-
const eventType = `"eventType":${this.eventType}`;
|
|
31
|
-
const libero = `"libero":"${this.libero}"`;
|
|
32
|
-
return `{${playerId},${type},${eventType},${libero}}`;
|
|
33
|
-
}
|
|
34
27
|
}
|
|
@@ -38,9 +38,4 @@ export class Reception extends InPlayEvent {
|
|
|
38
38
|
this.type = type;
|
|
39
39
|
this.failure = failure;
|
|
40
40
|
}
|
|
41
|
-
toString() {
|
|
42
|
-
const failure = `"failure":${this.failure}`;
|
|
43
|
-
const type = `"type":${this.type}`;
|
|
44
|
-
return `{${this._toString()},${failure},${type}}`;
|
|
45
|
-
}
|
|
46
41
|
}
|
|
@@ -35,9 +35,4 @@ export class Serve extends InPlayEvent {
|
|
|
35
35
|
this.type = type;
|
|
36
36
|
this.failure = failure;
|
|
37
37
|
}
|
|
38
|
-
toString() {
|
|
39
|
-
const failure = `"failure":${this.failure}`;
|
|
40
|
-
const type = `"type":${this.type}`;
|
|
41
|
-
return `{${this._toString()},${failure},${type}}`;
|
|
42
|
-
}
|
|
43
38
|
}
|
|
@@ -44,10 +44,4 @@ export class Set extends InPlayEvent {
|
|
|
44
44
|
this.failure = failure;
|
|
45
45
|
this.tempo = tempo;
|
|
46
46
|
}
|
|
47
|
-
toString() {
|
|
48
|
-
const failure = `"failure":${this.failure}`;
|
|
49
|
-
const type = `"type":${this.type}`;
|
|
50
|
-
const tempo = this.tempo !== undefined ? `,"tempo":${this.tempo}` : '';
|
|
51
|
-
return `{${this._toString()},${failure},${type}${tempo}}`;
|
|
52
|
-
}
|
|
53
47
|
}
|