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.
Files changed (33) hide show
  1. package/dist/cjs/src/data/init-models.js +8 -1
  2. package/dist/cjs/src/data/models/app-config.d.ts +17 -0
  3. package/dist/cjs/src/data/models/app-config.js +36 -0
  4. package/dist/cjs/src/data/models/index.d.ts +2 -0
  5. package/dist/cjs/src/data/models/index.js +2 -0
  6. package/dist/cjs/src/data/models/legacy-team-flag.d.ts +23 -0
  7. package/dist/cjs/src/data/models/legacy-team-flag.js +38 -0
  8. package/dist/cjs/src/data/models/team.d.ts +3 -1
  9. package/dist/cjs/src/data/transformers/team.js +3 -0
  10. package/dist/cjs/src/service/team/base-position.d.ts +1 -0
  11. package/dist/cjs/src/service/team/base-position.js +41 -15
  12. package/dist/cjs/src/service/team/base-position.test.js +39 -0
  13. package/dist/cjs/src/service/team/schemas/team.z.d.ts +1 -0
  14. package/dist/cjs/src/service/team/schemas/team.z.js +1 -0
  15. package/dist/cjs/src/service/team/team.d.ts +1 -0
  16. package/dist/cjs/src/service/team/team.js +2 -1
  17. package/dist/esm/src/data/init-models.js +9 -2
  18. package/dist/esm/src/data/models/app-config.d.ts +17 -0
  19. package/dist/esm/src/data/models/app-config.js +32 -0
  20. package/dist/esm/src/data/models/index.d.ts +2 -0
  21. package/dist/esm/src/data/models/index.js +2 -0
  22. package/dist/esm/src/data/models/legacy-team-flag.d.ts +23 -0
  23. package/dist/esm/src/data/models/legacy-team-flag.js +34 -0
  24. package/dist/esm/src/data/models/team.d.ts +3 -1
  25. package/dist/esm/src/data/transformers/team.js +3 -0
  26. package/dist/esm/src/service/team/base-position.d.ts +1 -0
  27. package/dist/esm/src/service/team/base-position.js +41 -15
  28. package/dist/esm/src/service/team/base-position.test.js +40 -1
  29. package/dist/esm/src/service/team/schemas/team.z.d.ts +1 -0
  30. package/dist/esm/src/service/team/schemas/team.z.js +1 -0
  31. package/dist/esm/src/service/team/team.d.ts +1 -0
  32. package/dist/esm/src/service/team/team.js +2 -1
  33. 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;
@@ -43,3 +43,5 @@ export * from './user-settings';
43
43
  export * from './notification';
44
44
  export * from './vper';
45
45
  export * from './views';
46
+ export * from './app-config';
47
+ export * from './legacy-team-flag';
@@ -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 setter is hidden at the net (not a passer); serves
12
- // are aimed at the back-row passers, so the setter / penetrating setter never receives.
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
- // SERVE-RECEIVE: hide the rally-setter at RIGHT_FRONT so they are not a passer (serves are aimed at the back
74
- // row). Swapping with the current RIGHT_FRONT occupant keeps the map a bijection.
75
- const serveReceive = new Map(base);
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
- if (setterId != null) {
78
- const setterZone = serveReceive.get(setterId);
79
- if (setterZone != null && setterZone !== match_1.CourtPosition.RIGHT_FRONT) {
80
- const occupant = [...serveReceive.entries()].find(([, z]) => z === match_1.CourtPosition.RIGHT_FRONT);
81
- serveReceive.set(setterId, match_1.CourtPosition.RIGHT_FRONT);
82
- if (occupant != null)
83
- serveReceive.set(occupant[0], setterZone);
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
- return { serveReceive, base };
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);
@@ -9,6 +9,7 @@ export declare class Team {
9
9
  readonly divisionId: string;
10
10
  readonly tactics?: Tactics;
11
11
  readonly country: Country;
12
+ readonly legacyFlagCountry?: Country;
12
13
  private _rating;
13
14
  static create(input: unknown): Team;
14
15
  private constructor();
@@ -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
+ }
@@ -43,3 +43,5 @@ export * from './user-settings';
43
43
  export * from './notification';
44
44
  export * from './vper';
45
45
  export * from './views';
46
+ export * from './app-config';
47
+ export * from './legacy-team-flag';
@@ -43,3 +43,5 @@ export * from './user-settings';
43
43
  export * from './notification';
44
44
  export * from './vper';
45
45
  export * from './views';
46
+ export * from './app-config';
47
+ export * from './legacy-team-flag';
@@ -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 setter is hidden at the net (not a passer); serves
7
- // are aimed at the back-row passers, so the setter / penetrating setter never receives.
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
- // SERVE-RECEIVE: hide the rally-setter at RIGHT_FRONT so they are not a passer (serves are aimed at the back
69
- // row). Swapping with the current RIGHT_FRONT occupant keeps the map a bijection.
70
- const serveReceive = new Map(base);
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
- if (setterId != null) {
73
- const setterZone = serveReceive.get(setterId);
74
- if (setterZone != null && setterZone !== CourtPosition.RIGHT_FRONT) {
75
- const occupant = [...serveReceive.entries()].find(([, z]) => z === CourtPosition.RIGHT_FRONT);
76
- serveReceive.set(setterId, CourtPosition.RIGHT_FRONT);
77
- if (occupant != null)
78
- serveReceive.set(occupant[0], setterZone);
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
- return { serveReceive, base };
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);
@@ -9,6 +9,7 @@ export declare class Team {
9
9
  readonly divisionId: string;
10
10
  readonly tactics?: Tactics;
11
11
  readonly country: Country;
12
+ readonly legacyFlagCountry?: Country;
12
13
  private _rating;
13
14
  static create(input: unknown): Team;
14
15
  private constructor();
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volleyballsimtypes",
3
- "version": "0.0.394",
3
+ "version": "0.0.396",
4
4
  "description": "vbsim types",
5
5
  "main": "./dist/cjs/src/index.js",
6
6
  "module": "./dist/esm/src/index.js",