volleyballsimtypes 0.0.394 → 0.0.396
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/data/init-models.js +8 -1
- package/dist/cjs/src/data/models/app-config.d.ts +17 -0
- package/dist/cjs/src/data/models/app-config.js +36 -0
- package/dist/cjs/src/data/models/index.d.ts +2 -0
- package/dist/cjs/src/data/models/index.js +2 -0
- package/dist/cjs/src/data/models/legacy-team-flag.d.ts +23 -0
- package/dist/cjs/src/data/models/legacy-team-flag.js +38 -0
- package/dist/cjs/src/data/models/team.d.ts +3 -1
- package/dist/cjs/src/data/transformers/team.js +3 -0
- package/dist/cjs/src/service/team/base-position.d.ts +1 -0
- package/dist/cjs/src/service/team/base-position.js +41 -15
- package/dist/cjs/src/service/team/base-position.test.js +39 -0
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -0
- package/dist/cjs/src/service/team/schemas/team.z.js +1 -0
- package/dist/cjs/src/service/team/team.d.ts +1 -0
- package/dist/cjs/src/service/team/team.js +2 -1
- package/dist/esm/src/data/init-models.js +9 -2
- package/dist/esm/src/data/models/app-config.d.ts +17 -0
- package/dist/esm/src/data/models/app-config.js +32 -0
- package/dist/esm/src/data/models/index.d.ts +2 -0
- package/dist/esm/src/data/models/index.js +2 -0
- package/dist/esm/src/data/models/legacy-team-flag.d.ts +23 -0
- package/dist/esm/src/data/models/legacy-team-flag.js +34 -0
- package/dist/esm/src/data/models/team.d.ts +3 -1
- package/dist/esm/src/data/transformers/team.js +3 -0
- package/dist/esm/src/service/team/base-position.d.ts +1 -0
- package/dist/esm/src/service/team/base-position.js +41 -15
- package/dist/esm/src/service/team/base-position.test.js +40 -1
- package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -0
- package/dist/esm/src/service/team/schemas/team.z.js +1 -0
- package/dist/esm/src/service/team/team.d.ts +1 -0
- package/dist/esm/src/service/team/team.js +2 -1
- package/package.json +1 -1
|
@@ -47,6 +47,8 @@ function initModels(sequelize) {
|
|
|
47
47
|
const NationalCountry = models_1.NationalCountryModel.initModel(sequelize);
|
|
48
48
|
const Team = models_1.TeamModel.initModel(sequelize);
|
|
49
49
|
const TeamReplacement = models_1.TeamReplacementModel.initModel(sequelize);
|
|
50
|
+
const LegacyTeamFlag = models_1.LegacyTeamFlagModel.initModel(sequelize);
|
|
51
|
+
const AppConfig = models_1.AppConfigModel.initModel(sequelize);
|
|
50
52
|
const VPER = models_1.VPERModel.initModel(sequelize);
|
|
51
53
|
Tactics.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
52
54
|
AuthUser.hasMany(AuthIdentity, { as: 'AuthIdentities', foreignKey: 'user_id' });
|
|
@@ -226,6 +228,9 @@ function initModels(sequelize) {
|
|
|
226
228
|
Team.hasOne(TeamReplacement, { as: 'TeamReplacement', foreignKey: 'team_id' });
|
|
227
229
|
TeamReplacement.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
228
230
|
TeamReplacement.belongsTo(Team, { as: 'replacedByTeam', foreignKey: 'replaced_by' });
|
|
231
|
+
Team.hasOne(LegacyTeamFlag, { as: 'LegacyTeamFlag', foreignKey: 'team_id' });
|
|
232
|
+
LegacyTeamFlag.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
233
|
+
LegacyTeamFlag.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
|
|
229
234
|
Player.hasOne(UserPlayerProgress, { as: 'UserPlayerProgress', foreignKey: 'player_id' });
|
|
230
235
|
UserPlayerProgress.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
|
|
231
236
|
return {
|
|
@@ -273,6 +278,8 @@ function initModels(sequelize) {
|
|
|
273
278
|
Notification,
|
|
274
279
|
Coach,
|
|
275
280
|
CurrencyTransaction,
|
|
276
|
-
VPER
|
|
281
|
+
VPER,
|
|
282
|
+
LegacyTeamFlag,
|
|
283
|
+
AppConfig
|
|
277
284
|
};
|
|
278
285
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model, Optional } from 'sequelize';
|
|
3
|
+
export interface AppConfigAttributes {
|
|
4
|
+
key: string;
|
|
5
|
+
value: Record<string, unknown>;
|
|
6
|
+
updated_at: Date;
|
|
7
|
+
}
|
|
8
|
+
export type AppConfigPk = 'key';
|
|
9
|
+
export type AppConfigId = AppConfigModel[AppConfigPk];
|
|
10
|
+
export type AppConfigOptionalAttributes = 'value' | 'updated_at';
|
|
11
|
+
export type AppConfigCreationAttributes = Optional<AppConfigAttributes, AppConfigOptionalAttributes>;
|
|
12
|
+
export declare class AppConfigModel extends Model<AppConfigAttributes, AppConfigCreationAttributes> implements AppConfigAttributes {
|
|
13
|
+
key: string;
|
|
14
|
+
value: Record<string, unknown>;
|
|
15
|
+
updated_at: Date;
|
|
16
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof AppConfigModel;
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppConfigModel = void 0;
|
|
4
|
+
const sequelize_1 = require("sequelize");
|
|
5
|
+
class AppConfigModel extends sequelize_1.Model {
|
|
6
|
+
static initModel(sequelize) {
|
|
7
|
+
return AppConfigModel.init({
|
|
8
|
+
key: {
|
|
9
|
+
type: sequelize_1.DataTypes.STRING,
|
|
10
|
+
allowNull: false,
|
|
11
|
+
primaryKey: true
|
|
12
|
+
},
|
|
13
|
+
value: {
|
|
14
|
+
type: sequelize_1.DataTypes.JSONB,
|
|
15
|
+
allowNull: false,
|
|
16
|
+
defaultValue: {}
|
|
17
|
+
},
|
|
18
|
+
updated_at: {
|
|
19
|
+
type: sequelize_1.DataTypes.DATE,
|
|
20
|
+
allowNull: false,
|
|
21
|
+
defaultValue: sequelize_1.DataTypes.NOW
|
|
22
|
+
}
|
|
23
|
+
}, {
|
|
24
|
+
sequelize,
|
|
25
|
+
tableName: 'AppConfig',
|
|
26
|
+
schema: 'public',
|
|
27
|
+
timestamps: false,
|
|
28
|
+
indexes: [{
|
|
29
|
+
name: 'AppConfig_pk',
|
|
30
|
+
unique: true,
|
|
31
|
+
fields: [{ name: 'key' }]
|
|
32
|
+
}]
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.AppConfigModel = AppConfigModel;
|
|
@@ -59,3 +59,5 @@ __exportStar(require("./user-settings"), exports);
|
|
|
59
59
|
__exportStar(require("./notification"), exports);
|
|
60
60
|
__exportStar(require("./vper"), exports);
|
|
61
61
|
__exportStar(require("./views"), exports);
|
|
62
|
+
__exportStar(require("./app-config"), exports);
|
|
63
|
+
__exportStar(require("./legacy-team-flag"), exports);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { CountryId, CountryModel, TeamId, TeamModel } from '.';
|
|
4
|
+
export interface LegacyTeamFlagAttributes {
|
|
5
|
+
team_id: string;
|
|
6
|
+
country_id: string;
|
|
7
|
+
}
|
|
8
|
+
export type LegacyTeamFlagPk = 'team_id';
|
|
9
|
+
export type LegacyTeamFlagId = LegacyTeamFlagModel[LegacyTeamFlagPk];
|
|
10
|
+
export type LegacyTeamFlagCreationAttributes = LegacyTeamFlagAttributes;
|
|
11
|
+
export declare class LegacyTeamFlagModel extends Model<LegacyTeamFlagAttributes, LegacyTeamFlagCreationAttributes> implements LegacyTeamFlagAttributes {
|
|
12
|
+
team_id: string;
|
|
13
|
+
country_id: string;
|
|
14
|
+
team: TeamModel;
|
|
15
|
+
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
16
|
+
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
17
|
+
createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
|
|
18
|
+
country: CountryModel;
|
|
19
|
+
getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
|
|
20
|
+
setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
|
|
21
|
+
createCountry: Sequelize.BelongsToCreateAssociationMixin<CountryModel>;
|
|
22
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof LegacyTeamFlagModel;
|
|
23
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LegacyTeamFlagModel = void 0;
|
|
4
|
+
const sequelize_1 = require("sequelize");
|
|
5
|
+
class LegacyTeamFlagModel extends sequelize_1.Model {
|
|
6
|
+
static initModel(sequelize) {
|
|
7
|
+
return LegacyTeamFlagModel.init({
|
|
8
|
+
team_id: {
|
|
9
|
+
type: sequelize_1.DataTypes.UUID,
|
|
10
|
+
allowNull: false,
|
|
11
|
+
primaryKey: true,
|
|
12
|
+
references: {
|
|
13
|
+
model: 'Team',
|
|
14
|
+
key: 'team_id'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
country_id: {
|
|
18
|
+
type: sequelize_1.DataTypes.UUID,
|
|
19
|
+
allowNull: false,
|
|
20
|
+
references: {
|
|
21
|
+
model: 'Country',
|
|
22
|
+
key: 'country_id'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, {
|
|
26
|
+
sequelize,
|
|
27
|
+
tableName: 'LegacyTeamFlag',
|
|
28
|
+
schema: 'public',
|
|
29
|
+
timestamps: false,
|
|
30
|
+
indexes: [{
|
|
31
|
+
name: 'LegacyTeamFlag_pk',
|
|
32
|
+
unique: true,
|
|
33
|
+
fields: [{ name: 'team_id' }]
|
|
34
|
+
}]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.LegacyTeamFlagModel = LegacyTeamFlagModel;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
|
-
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel } from '.';
|
|
3
|
+
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, LegacyTeamFlagModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel } from '.';
|
|
4
4
|
export interface TeamAttributes {
|
|
5
5
|
team_id: string;
|
|
6
6
|
name: string;
|
|
@@ -33,6 +33,8 @@ export declare class TeamModel extends Model<TeamAttributes, TeamCreationAttribu
|
|
|
33
33
|
getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
|
|
34
34
|
setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
|
|
35
35
|
createCountry: Sequelize.BelongsToCreateAssociationMixin<CountryModel>;
|
|
36
|
+
LegacyTeamFlag?: LegacyTeamFlagModel;
|
|
37
|
+
getLegacyTeamFlag: Sequelize.HasOneGetAssociationMixin<LegacyTeamFlagModel>;
|
|
36
38
|
Competitions: CompetitionModel[];
|
|
37
39
|
getCompetitions: Sequelize.BelongsToManyGetAssociationsMixin<CompetitionModel>;
|
|
38
40
|
setCompetitions: Sequelize.BelongsToManySetAssociationsMixin<CompetitionModel, CompetitionId>;
|
|
@@ -26,6 +26,9 @@ function transformToObject(model, currentIteration) {
|
|
|
26
26
|
name: model.name,
|
|
27
27
|
shortName: model.short_name,
|
|
28
28
|
country: (0, _1.transformToCountry)(model.country),
|
|
29
|
+
legacyFlagCountry: model.LegacyTeamFlag?.country != null
|
|
30
|
+
? (0, _1.transformToCountry)(model.LegacyTeamFlag.country)
|
|
31
|
+
: undefined,
|
|
29
32
|
rating: model.rating,
|
|
30
33
|
tactics,
|
|
31
34
|
roster,
|
|
@@ -14,6 +14,7 @@ export interface BaseResolverInput {
|
|
|
14
14
|
readonly system: RotationSystemEnum;
|
|
15
15
|
readonly designatedSetterIds: string[];
|
|
16
16
|
readonly players: BasePlayer[];
|
|
17
|
+
readonly liberoId?: string;
|
|
17
18
|
}
|
|
18
19
|
export type BaseMap = Map<string, CourtPosition>;
|
|
19
20
|
export interface ResolvedBases {
|
|
@@ -8,8 +8,9 @@ const player_1 = require("../player");
|
|
|
8
8
|
const rotation_system_1 = require("./rotation-system");
|
|
9
9
|
const lineup_function_1 = require("./lineup-function");
|
|
10
10
|
// The rally phase a base lookup is resolved against. Two contexts are enough in the 6-zone model:
|
|
11
|
-
// - SERVE_RECEIVE: first contact when receiving serve. The
|
|
12
|
-
// are
|
|
11
|
+
// - SERVE_RECEIVE: first contact when receiving serve. The passers (libero + outsides) hold the back row; the
|
|
12
|
+
// setter(s) and the opposite are hidden in the front (not passers). Serves are aimed at the back-row passers,
|
|
13
|
+
// so the setter / penetrating setter / opposite never receive by default (they can be deliberately hunted).
|
|
13
14
|
// - BASE: open play (offense AND defense). In the discrete 6-zone model these coincide: every player is at their
|
|
14
15
|
// one specialist lane whether attacking or digging. There is no visual layer and no benefit to splitting them.
|
|
15
16
|
var BaseContext;
|
|
@@ -58,7 +59,7 @@ function identityBases(players) {
|
|
|
58
59
|
// rotational layout in both contexts (= pre-feature behavior). Each returned map is a bijection over {1..6}; the
|
|
59
60
|
// sim asserts this after applying it.
|
|
60
61
|
function resolveBases(input) {
|
|
61
|
-
const { system, designatedSetterIds, players } = input;
|
|
62
|
+
const { system, designatedSetterIds, players, liberoId } = input;
|
|
62
63
|
if (system === rotation_system_1.RotationSystemEnum.SIX_ZERO)
|
|
63
64
|
return identityBases(players);
|
|
64
65
|
const mainSetter = players.find(p => p.playerId === designatedSetterIds[0]);
|
|
@@ -70,18 +71,43 @@ function resolveBases(input) {
|
|
|
70
71
|
const fn = (0, lineup_function_1.slotFunctionAt)(system, mainSetter.position, p.position);
|
|
71
72
|
base.set(p.playerId, specialistZone(fn, (0, match_1.isFrontRow)(p.position)));
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
return { serveReceive: buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId), base };
|
|
75
|
+
}
|
|
76
|
+
// The serve-receive base. Only the back row handles the serve, so the three back-row zones go to the actual
|
|
77
|
+
// passers (the libero + the two outside-hitter slots) and the three front zones hide the non-passers (the
|
|
78
|
+
// setter(s), the opposite, and a middle). This keeps the opposite -- which shares the right lane with the setter
|
|
79
|
+
// in open play -- out of the passing lanes; a server can still deliberately hunt the hidden setter or opposite
|
|
80
|
+
// (handled sim-side). Without a libero on court the back-row middle fills the third passing slot. Always a
|
|
81
|
+
// bijection over {1..6} (three front + three back, distinct zones).
|
|
82
|
+
function buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId) {
|
|
83
|
+
// Zone order, declared in-function so CourtPosition is read at call time, not module load: the service<->match
|
|
84
|
+
// import cycle leaves CourtPosition unbound while this module is first evaluated, so a module-level array would
|
|
85
|
+
// crash. Hidden players fill the front (rally-setter first -> RIGHT_FRONT); passers fill the back (libero first
|
|
86
|
+
// -> MIDDLE_BACK).
|
|
87
|
+
const frontZones = [match_1.CourtPosition.RIGHT_FRONT, match_1.CourtPosition.LEFT_FRONT, match_1.CourtPosition.MIDDLE_FRONT];
|
|
88
|
+
const backZones = [match_1.CourtPosition.MIDDLE_BACK, match_1.CourtPosition.LEFT_BACK, match_1.CourtPosition.RIGHT_BACK];
|
|
76
89
|
const setterId = rallySetterId(system, designatedSetterIds, players);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
const fnOf = (p) => (0, lineup_function_1.slotFunctionAt)(system, mainSetter.position, p.position);
|
|
91
|
+
const passers = [];
|
|
92
|
+
const middles = [];
|
|
93
|
+
const hidden = []; // setter(s) + opposite
|
|
94
|
+
for (const p of players) {
|
|
95
|
+
if (p.playerId === liberoId || fnOf(p) === player_1.RoleEnum.OUTSIDE_HITTER)
|
|
96
|
+
passers.push(p);
|
|
97
|
+
else if (fnOf(p) === player_1.RoleEnum.MIDDLE_BLOCKER)
|
|
98
|
+
middles.push(p);
|
|
99
|
+
else
|
|
100
|
+
hidden.push(p);
|
|
85
101
|
}
|
|
86
|
-
|
|
102
|
+
// Fill to three passers from the middles (back-row first, so a front-row middle stays hidden); the rest hide.
|
|
103
|
+
middles.sort((a, b) => Number((0, match_1.isFrontRow)(a.position)) - Number((0, match_1.isFrontRow)(b.position)));
|
|
104
|
+
while (passers.length < 3 && middles.length > 0)
|
|
105
|
+
passers.push(middles.shift());
|
|
106
|
+
hidden.push(...middles);
|
|
107
|
+
hidden.sort((a, b) => Number(b.playerId === setterId) - Number(a.playerId === setterId));
|
|
108
|
+
passers.sort((a, b) => Number(b.playerId === liberoId) - Number(a.playerId === liberoId));
|
|
109
|
+
const sr = new Map();
|
|
110
|
+
hidden.forEach((p, i) => sr.set(p.playerId, frontZones[i]));
|
|
111
|
+
passers.forEach((p, i) => sr.set(p.playerId, backZones[i]));
|
|
112
|
+
return sr;
|
|
87
113
|
}
|
|
@@ -16,6 +16,20 @@ function baseLineup() {
|
|
|
16
16
|
{ playerId: 'MB2', position: match_1.CourtPosition.MIDDLE_BACK, roles: [player_1.RoleEnum.MIDDLE_BLOCKER] }
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
|
+
// 5-1 with the libero on court in the back-row middle slot (replacing MB2).
|
|
20
|
+
function lineupWithLibero() {
|
|
21
|
+
return {
|
|
22
|
+
players: [
|
|
23
|
+
{ playerId: 'S', position: match_1.CourtPosition.RIGHT_BACK, roles: [player_1.RoleEnum.SETTER] },
|
|
24
|
+
{ playerId: 'OH1', position: match_1.CourtPosition.RIGHT_FRONT, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
|
|
25
|
+
{ playerId: 'MB1', position: match_1.CourtPosition.MIDDLE_FRONT, roles: [player_1.RoleEnum.MIDDLE_BLOCKER] },
|
|
26
|
+
{ playerId: 'OPP', position: match_1.CourtPosition.LEFT_FRONT, roles: [player_1.RoleEnum.OPPOSITE_HITTER] },
|
|
27
|
+
{ playerId: 'OH2', position: match_1.CourtPosition.LEFT_BACK, roles: [player_1.RoleEnum.OUTSIDE_HITTER] },
|
|
28
|
+
{ playerId: 'LIB', position: match_1.CourtPosition.MIDDLE_BACK, roles: [player_1.RoleEnum.LIBERO] }
|
|
29
|
+
],
|
|
30
|
+
liberoId: 'LIB'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
19
33
|
function rotate(players, times) {
|
|
20
34
|
let out = players;
|
|
21
35
|
for (let i = 0; i < times; i++)
|
|
@@ -63,6 +77,31 @@ describe('resolveBases() serve-receive hides the setter', () => {
|
|
|
63
77
|
}
|
|
64
78
|
});
|
|
65
79
|
});
|
|
80
|
+
describe('resolveBases() serve-receive hides the opposite (regression: opposites were receiving most serves)', () => {
|
|
81
|
+
it('5-1 opposite is hidden in the front, never in the serve-receive back row, every rotation', () => {
|
|
82
|
+
for (let r = 0; r < 6; r++) {
|
|
83
|
+
const players = rotate(baseLineup(), r);
|
|
84
|
+
const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
|
|
85
|
+
expect((0, match_1.isFrontRow)(bases.serveReceive.get('OPP'))).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
it('5-1 serve-receive back row holds exactly the passers -- never the setter or the opposite', () => {
|
|
89
|
+
for (let r = 0; r < 6; r++) {
|
|
90
|
+
const players = rotate(baseLineup(), r);
|
|
91
|
+
const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
|
|
92
|
+
const backRow = [...bases.serveReceive.entries()].filter(([, z]) => !(0, match_1.isFrontRow)(z)).map(([id]) => id);
|
|
93
|
+
expect(backRow).toHaveLength(3);
|
|
94
|
+
expect(backRow).not.toContain('S');
|
|
95
|
+
expect(backRow).not.toContain('OPP');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it('the on-court libero is always a passer (back row) while the opposite stays hidden', () => {
|
|
99
|
+
const { players, liberoId } = lineupWithLibero();
|
|
100
|
+
const bases = (0, base_position_1.resolveBases)({ system: rotation_system_1.RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players, liberoId });
|
|
101
|
+
expect((0, match_1.isFrontRow)(bases.serveReceive.get(liberoId))).toBe(false);
|
|
102
|
+
expect((0, match_1.isFrontRow)(bases.serveReceive.get('OPP'))).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
66
105
|
describe('rallySetterId()', () => {
|
|
67
106
|
it('5-1 returns the single setter in every rotation', () => {
|
|
68
107
|
for (let r = 0; r < 6; r++) {
|
|
@@ -9,6 +9,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
9
9
|
divisionId: z.ZodUUID;
|
|
10
10
|
roster: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
11
11
|
country: z.ZodCustom<Country, Country>;
|
|
12
|
+
legacyFlagCountry: z.ZodOptional<z.ZodCustom<Country, Country>>;
|
|
12
13
|
tactics: z.ZodOptional<z.ZodObject<{
|
|
13
14
|
lineup: z.ZodObject<{
|
|
14
15
|
4: z.ZodCustom<Player, Player>;
|
|
@@ -19,6 +19,7 @@ exports.TeamInputSchema = zod_1.z.object({
|
|
|
19
19
|
divisionId: zod_1.z.uuid(),
|
|
20
20
|
roster: zod_1.z.array(playerInstanceSchema),
|
|
21
21
|
country: countrySchema,
|
|
22
|
+
legacyFlagCountry: countrySchema.optional(),
|
|
22
23
|
tactics: tactics_z_1.TacticsInputSchema.optional()
|
|
23
24
|
}).superRefine((data, ctx) => {
|
|
24
25
|
const ids = data.roster.map(p => p.id);
|
|
@@ -19,7 +19,7 @@ class Team {
|
|
|
19
19
|
tactics: result.data.tactics != null ? tactics_1.Tactics.create(result.data.tactics) : undefined
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
constructor({ id, rating, name, shortName, divisionId, country, roster, tactics }) {
|
|
22
|
+
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics }) {
|
|
23
23
|
this.id = id;
|
|
24
24
|
this._rating = rating;
|
|
25
25
|
this.roster = roster;
|
|
@@ -28,6 +28,7 @@ class Team {
|
|
|
28
28
|
this.shortName = shortName;
|
|
29
29
|
this.divisionId = divisionId;
|
|
30
30
|
this.country = country;
|
|
31
|
+
this.legacyFlagCountry = legacyFlagCountry;
|
|
31
32
|
}
|
|
32
33
|
get rating() {
|
|
33
34
|
return this._rating;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel } from './models';
|
|
1
|
+
import { AuthIdentityModel, AuthSessionModel, AuthUserModel, BoxScoreModel, CoachModel, BoxScoreTotalsModel, CompetitionChampionModel, CurrencyTransactionModel, GachaPityModel, GachaPullHistoryModel, WishlistModel, UserSettingsModel, CompetitionMatchModel, CompetitionMVPModel, CompetitionModel, CompetitionStandingsMatchModel, CompetitionStandingsModel, CompetitionTeamsModel, CountryModel, DivisionModel, DivisionSeasonModel, IterationModel, LeagueModel, MatchModel, MatchRatingModel, MatchResultModel, MatchSetModel, PerformanceStatsModel, PlayerModel, PlayerImprovementLogModel, PlayerTeamModel, RetiredPlayerModel, PromotionMatchModel, RegionQualifierModel, NationalCountryModel, RallyModel, TacticsModel, TeamModel, TeamReplacementModel, UserCurrencyModel, UserPlayerProgressModel, UserPullGrantModel, UserTeamModel, VPERModel, NotificationModel, AppConfigModel, LegacyTeamFlagModel } from './models';
|
|
2
2
|
export function initModels(sequelize) {
|
|
3
3
|
const Coach = CoachModel.initModel(sequelize);
|
|
4
4
|
const CurrencyTransaction = CurrencyTransactionModel.initModel(sequelize);
|
|
@@ -44,6 +44,8 @@ export function initModels(sequelize) {
|
|
|
44
44
|
const NationalCountry = NationalCountryModel.initModel(sequelize);
|
|
45
45
|
const Team = TeamModel.initModel(sequelize);
|
|
46
46
|
const TeamReplacement = TeamReplacementModel.initModel(sequelize);
|
|
47
|
+
const LegacyTeamFlag = LegacyTeamFlagModel.initModel(sequelize);
|
|
48
|
+
const AppConfig = AppConfigModel.initModel(sequelize);
|
|
47
49
|
const VPER = VPERModel.initModel(sequelize);
|
|
48
50
|
Tactics.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
49
51
|
AuthUser.hasMany(AuthIdentity, { as: 'AuthIdentities', foreignKey: 'user_id' });
|
|
@@ -223,6 +225,9 @@ export function initModels(sequelize) {
|
|
|
223
225
|
Team.hasOne(TeamReplacement, { as: 'TeamReplacement', foreignKey: 'team_id' });
|
|
224
226
|
TeamReplacement.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
225
227
|
TeamReplacement.belongsTo(Team, { as: 'replacedByTeam', foreignKey: 'replaced_by' });
|
|
228
|
+
Team.hasOne(LegacyTeamFlag, { as: 'LegacyTeamFlag', foreignKey: 'team_id' });
|
|
229
|
+
LegacyTeamFlag.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
|
|
230
|
+
LegacyTeamFlag.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
|
|
226
231
|
Player.hasOne(UserPlayerProgress, { as: 'UserPlayerProgress', foreignKey: 'player_id' });
|
|
227
232
|
UserPlayerProgress.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
|
|
228
233
|
return {
|
|
@@ -270,6 +275,8 @@ export function initModels(sequelize) {
|
|
|
270
275
|
Notification,
|
|
271
276
|
Coach,
|
|
272
277
|
CurrencyTransaction,
|
|
273
|
-
VPER
|
|
278
|
+
VPER,
|
|
279
|
+
LegacyTeamFlag,
|
|
280
|
+
AppConfig
|
|
274
281
|
};
|
|
275
282
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model, Optional } from 'sequelize';
|
|
3
|
+
export interface AppConfigAttributes {
|
|
4
|
+
key: string;
|
|
5
|
+
value: Record<string, unknown>;
|
|
6
|
+
updated_at: Date;
|
|
7
|
+
}
|
|
8
|
+
export type AppConfigPk = 'key';
|
|
9
|
+
export type AppConfigId = AppConfigModel[AppConfigPk];
|
|
10
|
+
export type AppConfigOptionalAttributes = 'value' | 'updated_at';
|
|
11
|
+
export type AppConfigCreationAttributes = Optional<AppConfigAttributes, AppConfigOptionalAttributes>;
|
|
12
|
+
export declare class AppConfigModel extends Model<AppConfigAttributes, AppConfigCreationAttributes> implements AppConfigAttributes {
|
|
13
|
+
key: string;
|
|
14
|
+
value: Record<string, unknown>;
|
|
15
|
+
updated_at: Date;
|
|
16
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof AppConfigModel;
|
|
17
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class AppConfigModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return AppConfigModel.init({
|
|
5
|
+
key: {
|
|
6
|
+
type: DataTypes.STRING,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true
|
|
9
|
+
},
|
|
10
|
+
value: {
|
|
11
|
+
type: DataTypes.JSONB,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
defaultValue: {}
|
|
14
|
+
},
|
|
15
|
+
updated_at: {
|
|
16
|
+
type: DataTypes.DATE,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
defaultValue: DataTypes.NOW
|
|
19
|
+
}
|
|
20
|
+
}, {
|
|
21
|
+
sequelize,
|
|
22
|
+
tableName: 'AppConfig',
|
|
23
|
+
schema: 'public',
|
|
24
|
+
timestamps: false,
|
|
25
|
+
indexes: [{
|
|
26
|
+
name: 'AppConfig_pk',
|
|
27
|
+
unique: true,
|
|
28
|
+
fields: [{ name: 'key' }]
|
|
29
|
+
}]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as Sequelize from 'sequelize';
|
|
2
|
+
import { Model } from 'sequelize';
|
|
3
|
+
import { CountryId, CountryModel, TeamId, TeamModel } from '.';
|
|
4
|
+
export interface LegacyTeamFlagAttributes {
|
|
5
|
+
team_id: string;
|
|
6
|
+
country_id: string;
|
|
7
|
+
}
|
|
8
|
+
export type LegacyTeamFlagPk = 'team_id';
|
|
9
|
+
export type LegacyTeamFlagId = LegacyTeamFlagModel[LegacyTeamFlagPk];
|
|
10
|
+
export type LegacyTeamFlagCreationAttributes = LegacyTeamFlagAttributes;
|
|
11
|
+
export declare class LegacyTeamFlagModel extends Model<LegacyTeamFlagAttributes, LegacyTeamFlagCreationAttributes> implements LegacyTeamFlagAttributes {
|
|
12
|
+
team_id: string;
|
|
13
|
+
country_id: string;
|
|
14
|
+
team: TeamModel;
|
|
15
|
+
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
16
|
+
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
17
|
+
createTeam: Sequelize.BelongsToCreateAssociationMixin<TeamModel>;
|
|
18
|
+
country: CountryModel;
|
|
19
|
+
getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
|
|
20
|
+
setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
|
|
21
|
+
createCountry: Sequelize.BelongsToCreateAssociationMixin<CountryModel>;
|
|
22
|
+
static initModel(sequelize: Sequelize.Sequelize): typeof LegacyTeamFlagModel;
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
export class LegacyTeamFlagModel extends Model {
|
|
3
|
+
static initModel(sequelize) {
|
|
4
|
+
return LegacyTeamFlagModel.init({
|
|
5
|
+
team_id: {
|
|
6
|
+
type: DataTypes.UUID,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
primaryKey: true,
|
|
9
|
+
references: {
|
|
10
|
+
model: 'Team',
|
|
11
|
+
key: 'team_id'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
country_id: {
|
|
15
|
+
type: DataTypes.UUID,
|
|
16
|
+
allowNull: false,
|
|
17
|
+
references: {
|
|
18
|
+
model: 'Country',
|
|
19
|
+
key: 'country_id'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}, {
|
|
23
|
+
sequelize,
|
|
24
|
+
tableName: 'LegacyTeamFlag',
|
|
25
|
+
schema: 'public',
|
|
26
|
+
timestamps: false,
|
|
27
|
+
indexes: [{
|
|
28
|
+
name: 'LegacyTeamFlag_pk',
|
|
29
|
+
unique: true,
|
|
30
|
+
fields: [{ name: 'team_id' }]
|
|
31
|
+
}]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model, Optional } from 'sequelize';
|
|
3
|
-
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel } from '.';
|
|
3
|
+
import { CompetitionChampionId, CompetitionChampionModel, CompetitionId, CompetitionModel, CompetitionTeamsId, CompetitionTeamsModel, CountryId, CountryModel, DivisionAttributes, DivisionId, DivisionModel, LegacyTeamFlagModel, MatchId, MatchModel, MatchRatingId, MatchRatingModel, PlayerAttributes, PlayerId, PlayerModel, PlayerTeamId, PlayerTeamModel, RallyId, RallyModel, TacticsAttributes, TacticsId, TacticsModel } from '.';
|
|
4
4
|
export interface TeamAttributes {
|
|
5
5
|
team_id: string;
|
|
6
6
|
name: string;
|
|
@@ -33,6 +33,8 @@ export declare class TeamModel extends Model<TeamAttributes, TeamCreationAttribu
|
|
|
33
33
|
getCountry: Sequelize.BelongsToGetAssociationMixin<CountryModel>;
|
|
34
34
|
setCountry: Sequelize.BelongsToSetAssociationMixin<CountryModel, CountryId>;
|
|
35
35
|
createCountry: Sequelize.BelongsToCreateAssociationMixin<CountryModel>;
|
|
36
|
+
LegacyTeamFlag?: LegacyTeamFlagModel;
|
|
37
|
+
getLegacyTeamFlag: Sequelize.HasOneGetAssociationMixin<LegacyTeamFlagModel>;
|
|
36
38
|
Competitions: CompetitionModel[];
|
|
37
39
|
getCompetitions: Sequelize.BelongsToManyGetAssociationsMixin<CompetitionModel>;
|
|
38
40
|
setCompetitions: Sequelize.BelongsToManySetAssociationsMixin<CompetitionModel, CompetitionId>;
|
|
@@ -22,6 +22,9 @@ function transformToObject(model, currentIteration) {
|
|
|
22
22
|
name: model.name,
|
|
23
23
|
shortName: model.short_name,
|
|
24
24
|
country: transformToCountry(model.country),
|
|
25
|
+
legacyFlagCountry: model.LegacyTeamFlag?.country != null
|
|
26
|
+
? transformToCountry(model.LegacyTeamFlag.country)
|
|
27
|
+
: undefined,
|
|
25
28
|
rating: model.rating,
|
|
26
29
|
tactics,
|
|
27
30
|
roster,
|
|
@@ -14,6 +14,7 @@ export interface BaseResolverInput {
|
|
|
14
14
|
readonly system: RotationSystemEnum;
|
|
15
15
|
readonly designatedSetterIds: string[];
|
|
16
16
|
readonly players: BasePlayer[];
|
|
17
|
+
readonly liberoId?: string;
|
|
17
18
|
}
|
|
18
19
|
export type BaseMap = Map<string, CourtPosition>;
|
|
19
20
|
export interface ResolvedBases {
|
|
@@ -3,8 +3,9 @@ import { RoleEnum } from '../player';
|
|
|
3
3
|
import { RotationSystemEnum } from './rotation-system';
|
|
4
4
|
import { slotFunctionAt } from './lineup-function';
|
|
5
5
|
// The rally phase a base lookup is resolved against. Two contexts are enough in the 6-zone model:
|
|
6
|
-
// - SERVE_RECEIVE: first contact when receiving serve. The
|
|
7
|
-
// are
|
|
6
|
+
// - SERVE_RECEIVE: first contact when receiving serve. The passers (libero + outsides) hold the back row; the
|
|
7
|
+
// setter(s) and the opposite are hidden in the front (not passers). Serves are aimed at the back-row passers,
|
|
8
|
+
// so the setter / penetrating setter / opposite never receive by default (they can be deliberately hunted).
|
|
8
9
|
// - BASE: open play (offense AND defense). In the discrete 6-zone model these coincide: every player is at their
|
|
9
10
|
// one specialist lane whether attacking or digging. There is no visual layer and no benefit to splitting them.
|
|
10
11
|
export var BaseContext;
|
|
@@ -53,7 +54,7 @@ function identityBases(players) {
|
|
|
53
54
|
// rotational layout in both contexts (= pre-feature behavior). Each returned map is a bijection over {1..6}; the
|
|
54
55
|
// sim asserts this after applying it.
|
|
55
56
|
export function resolveBases(input) {
|
|
56
|
-
const { system, designatedSetterIds, players } = input;
|
|
57
|
+
const { system, designatedSetterIds, players, liberoId } = input;
|
|
57
58
|
if (system === RotationSystemEnum.SIX_ZERO)
|
|
58
59
|
return identityBases(players);
|
|
59
60
|
const mainSetter = players.find(p => p.playerId === designatedSetterIds[0]);
|
|
@@ -65,18 +66,43 @@ export function resolveBases(input) {
|
|
|
65
66
|
const fn = slotFunctionAt(system, mainSetter.position, p.position);
|
|
66
67
|
base.set(p.playerId, specialistZone(fn, isFrontRow(p.position)));
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
return { serveReceive: buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId), base };
|
|
70
|
+
}
|
|
71
|
+
// The serve-receive base. Only the back row handles the serve, so the three back-row zones go to the actual
|
|
72
|
+
// passers (the libero + the two outside-hitter slots) and the three front zones hide the non-passers (the
|
|
73
|
+
// setter(s), the opposite, and a middle). This keeps the opposite -- which shares the right lane with the setter
|
|
74
|
+
// in open play -- out of the passing lanes; a server can still deliberately hunt the hidden setter or opposite
|
|
75
|
+
// (handled sim-side). Without a libero on court the back-row middle fills the third passing slot. Always a
|
|
76
|
+
// bijection over {1..6} (three front + three back, distinct zones).
|
|
77
|
+
function buildServeReceive(system, designatedSetterIds, players, mainSetter, liberoId) {
|
|
78
|
+
// Zone order, declared in-function so CourtPosition is read at call time, not module load: the service<->match
|
|
79
|
+
// import cycle leaves CourtPosition unbound while this module is first evaluated, so a module-level array would
|
|
80
|
+
// crash. Hidden players fill the front (rally-setter first -> RIGHT_FRONT); passers fill the back (libero first
|
|
81
|
+
// -> MIDDLE_BACK).
|
|
82
|
+
const frontZones = [CourtPosition.RIGHT_FRONT, CourtPosition.LEFT_FRONT, CourtPosition.MIDDLE_FRONT];
|
|
83
|
+
const backZones = [CourtPosition.MIDDLE_BACK, CourtPosition.LEFT_BACK, CourtPosition.RIGHT_BACK];
|
|
71
84
|
const setterId = rallySetterId(system, designatedSetterIds, players);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
85
|
+
const fnOf = (p) => slotFunctionAt(system, mainSetter.position, p.position);
|
|
86
|
+
const passers = [];
|
|
87
|
+
const middles = [];
|
|
88
|
+
const hidden = []; // setter(s) + opposite
|
|
89
|
+
for (const p of players) {
|
|
90
|
+
if (p.playerId === liberoId || fnOf(p) === RoleEnum.OUTSIDE_HITTER)
|
|
91
|
+
passers.push(p);
|
|
92
|
+
else if (fnOf(p) === RoleEnum.MIDDLE_BLOCKER)
|
|
93
|
+
middles.push(p);
|
|
94
|
+
else
|
|
95
|
+
hidden.push(p);
|
|
80
96
|
}
|
|
81
|
-
|
|
97
|
+
// Fill to three passers from the middles (back-row first, so a front-row middle stays hidden); the rest hide.
|
|
98
|
+
middles.sort((a, b) => Number(isFrontRow(a.position)) - Number(isFrontRow(b.position)));
|
|
99
|
+
while (passers.length < 3 && middles.length > 0)
|
|
100
|
+
passers.push(middles.shift());
|
|
101
|
+
hidden.push(...middles);
|
|
102
|
+
hidden.sort((a, b) => Number(b.playerId === setterId) - Number(a.playerId === setterId));
|
|
103
|
+
passers.sort((a, b) => Number(b.playerId === liberoId) - Number(a.playerId === liberoId));
|
|
104
|
+
const sr = new Map();
|
|
105
|
+
hidden.forEach((p, i) => sr.set(p.playerId, frontZones[i]));
|
|
106
|
+
passers.forEach((p, i) => sr.set(p.playerId, backZones[i]));
|
|
107
|
+
return sr;
|
|
82
108
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CourtPosition, rotatePosition } from '../match';
|
|
1
|
+
import { CourtPosition, isFrontRow, rotatePosition } from '../match';
|
|
2
2
|
import { RoleEnum } from '../player';
|
|
3
3
|
import { RotationSystemEnum } from './rotation-system';
|
|
4
4
|
import { getLineupFunctions, isOutOfPosition, outOfPositionPenalty, OUT_OF_POSITION_PENALTY, slotFunctionAt } from './lineup-function';
|
|
@@ -14,6 +14,20 @@ function baseLineup() {
|
|
|
14
14
|
{ playerId: 'MB2', position: CourtPosition.MIDDLE_BACK, roles: [RoleEnum.MIDDLE_BLOCKER] }
|
|
15
15
|
];
|
|
16
16
|
}
|
|
17
|
+
// 5-1 with the libero on court in the back-row middle slot (replacing MB2).
|
|
18
|
+
function lineupWithLibero() {
|
|
19
|
+
return {
|
|
20
|
+
players: [
|
|
21
|
+
{ playerId: 'S', position: CourtPosition.RIGHT_BACK, roles: [RoleEnum.SETTER] },
|
|
22
|
+
{ playerId: 'OH1', position: CourtPosition.RIGHT_FRONT, roles: [RoleEnum.OUTSIDE_HITTER] },
|
|
23
|
+
{ playerId: 'MB1', position: CourtPosition.MIDDLE_FRONT, roles: [RoleEnum.MIDDLE_BLOCKER] },
|
|
24
|
+
{ playerId: 'OPP', position: CourtPosition.LEFT_FRONT, roles: [RoleEnum.OPPOSITE_HITTER] },
|
|
25
|
+
{ playerId: 'OH2', position: CourtPosition.LEFT_BACK, roles: [RoleEnum.OUTSIDE_HITTER] },
|
|
26
|
+
{ playerId: 'LIB', position: CourtPosition.MIDDLE_BACK, roles: [RoleEnum.LIBERO] }
|
|
27
|
+
],
|
|
28
|
+
liberoId: 'LIB'
|
|
29
|
+
};
|
|
30
|
+
}
|
|
17
31
|
function rotate(players, times) {
|
|
18
32
|
let out = players;
|
|
19
33
|
for (let i = 0; i < times; i++)
|
|
@@ -61,6 +75,31 @@ describe('resolveBases() serve-receive hides the setter', () => {
|
|
|
61
75
|
}
|
|
62
76
|
});
|
|
63
77
|
});
|
|
78
|
+
describe('resolveBases() serve-receive hides the opposite (regression: opposites were receiving most serves)', () => {
|
|
79
|
+
it('5-1 opposite is hidden in the front, never in the serve-receive back row, every rotation', () => {
|
|
80
|
+
for (let r = 0; r < 6; r++) {
|
|
81
|
+
const players = rotate(baseLineup(), r);
|
|
82
|
+
const bases = resolveBases({ system: RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
|
|
83
|
+
expect(isFrontRow(bases.serveReceive.get('OPP'))).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it('5-1 serve-receive back row holds exactly the passers -- never the setter or the opposite', () => {
|
|
87
|
+
for (let r = 0; r < 6; r++) {
|
|
88
|
+
const players = rotate(baseLineup(), r);
|
|
89
|
+
const bases = resolveBases({ system: RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players });
|
|
90
|
+
const backRow = [...bases.serveReceive.entries()].filter(([, z]) => !isFrontRow(z)).map(([id]) => id);
|
|
91
|
+
expect(backRow).toHaveLength(3);
|
|
92
|
+
expect(backRow).not.toContain('S');
|
|
93
|
+
expect(backRow).not.toContain('OPP');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
it('the on-court libero is always a passer (back row) while the opposite stays hidden', () => {
|
|
97
|
+
const { players, liberoId } = lineupWithLibero();
|
|
98
|
+
const bases = resolveBases({ system: RotationSystemEnum.FIVE_ONE, designatedSetterIds: ['S'], players, liberoId });
|
|
99
|
+
expect(isFrontRow(bases.serveReceive.get(liberoId))).toBe(false);
|
|
100
|
+
expect(isFrontRow(bases.serveReceive.get('OPP'))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
64
103
|
describe('rallySetterId()', () => {
|
|
65
104
|
it('5-1 returns the single setter in every rotation', () => {
|
|
66
105
|
for (let r = 0; r < 6; r++) {
|
|
@@ -9,6 +9,7 @@ export declare const TeamInputSchema: z.ZodObject<{
|
|
|
9
9
|
divisionId: z.ZodUUID;
|
|
10
10
|
roster: z.ZodArray<z.ZodCustom<Player, Player>>;
|
|
11
11
|
country: z.ZodCustom<Country, Country>;
|
|
12
|
+
legacyFlagCountry: z.ZodOptional<z.ZodCustom<Country, Country>>;
|
|
12
13
|
tactics: z.ZodOptional<z.ZodObject<{
|
|
13
14
|
lineup: z.ZodObject<{
|
|
14
15
|
4: z.ZodCustom<Player, Player>;
|
|
@@ -16,6 +16,7 @@ export const TeamInputSchema = z.object({
|
|
|
16
16
|
divisionId: z.uuid(),
|
|
17
17
|
roster: z.array(playerInstanceSchema),
|
|
18
18
|
country: countrySchema,
|
|
19
|
+
legacyFlagCountry: countrySchema.optional(),
|
|
19
20
|
tactics: TacticsInputSchema.optional()
|
|
20
21
|
}).superRefine((data, ctx) => {
|
|
21
22
|
const ids = data.roster.map(p => p.id);
|
|
@@ -16,7 +16,7 @@ export class Team {
|
|
|
16
16
|
tactics: result.data.tactics != null ? Tactics.create(result.data.tactics) : undefined
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
constructor({ id, rating, name, shortName, divisionId, country, roster, tactics }) {
|
|
19
|
+
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics }) {
|
|
20
20
|
this.id = id;
|
|
21
21
|
this._rating = rating;
|
|
22
22
|
this.roster = roster;
|
|
@@ -25,6 +25,7 @@ export class Team {
|
|
|
25
25
|
this.shortName = shortName;
|
|
26
26
|
this.divisionId = divisionId;
|
|
27
27
|
this.country = country;
|
|
28
|
+
this.legacyFlagCountry = legacyFlagCountry;
|
|
28
29
|
}
|
|
29
30
|
get rating() {
|
|
30
31
|
return this._rating;
|