volleyballsimtypes 0.0.504 → 0.0.506

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 (87) hide show
  1. package/dist/cjs/src/api/index.d.ts +3 -2
  2. package/dist/cjs/src/api/tactics-player-refs.js +2 -2
  3. package/dist/cjs/src/data/init-models.js +4 -0
  4. package/dist/cjs/src/data/models/base-position-preset.d.ts +22 -0
  5. package/dist/cjs/src/data/models/base-position-preset.js +54 -0
  6. package/dist/cjs/src/data/models/index.d.ts +1 -0
  7. package/dist/cjs/src/data/models/index.js +1 -0
  8. package/dist/cjs/src/data/models/tactics.d.ts +5 -3
  9. package/dist/cjs/src/data/models/tactics.js +4 -0
  10. package/dist/cjs/src/data/transformers/rally-event-injury.test.js +1 -1
  11. package/dist/cjs/src/data/transformers/rally-event-positional.test.js +10 -2
  12. package/dist/cjs/src/data/transformers/rally-event.js +3 -1
  13. package/dist/cjs/src/data/transformers/rally.test.js +2 -2
  14. package/dist/cjs/src/data/transformers/tactics.js +4 -0
  15. package/dist/cjs/src/data/transformers/tactics.test.js +34 -10
  16. package/dist/cjs/src/service/competition/competition-rewards.d.ts +20 -0
  17. package/dist/cjs/src/service/competition/competition-rewards.js +47 -0
  18. package/dist/cjs/src/service/competition/competition-rewards.test.d.ts +1 -0
  19. package/dist/cjs/src/service/competition/competition-rewards.test.js +32 -0
  20. package/dist/cjs/src/service/competition/index.d.ts +2 -0
  21. package/dist/cjs/src/service/competition/index.js +2 -0
  22. package/dist/cjs/src/service/competition/league-rewards.d.ts +21 -0
  23. package/dist/cjs/src/service/competition/league-rewards.js +58 -0
  24. package/dist/cjs/src/service/competition/league-rewards.test.d.ts +1 -0
  25. package/dist/cjs/src/service/competition/league-rewards.test.js +50 -0
  26. package/dist/cjs/src/service/event/schemas/spike.z.js +3 -1
  27. package/dist/cjs/src/service/event/spike.d.ts +2 -4
  28. package/dist/cjs/src/service/event/spike.js +5 -3
  29. package/dist/cjs/src/service/team/base-config.d.ts +10 -0
  30. package/dist/cjs/src/service/team/base-config.js +14 -0
  31. package/dist/cjs/src/service/team/blocking-preference.d.ts +3 -1
  32. package/dist/cjs/src/service/team/blocking-preference.js +2 -1
  33. package/dist/cjs/src/service/team/default-base-config.d.ts +4 -0
  34. package/dist/cjs/src/service/team/default-base-config.js +128 -0
  35. package/dist/cjs/src/service/team/index.d.ts +2 -0
  36. package/dist/cjs/src/service/team/index.js +2 -0
  37. package/dist/cjs/src/service/team/schemas/tactics.z.d.ts +17 -2
  38. package/dist/cjs/src/service/team/schemas/tactics.z.js +24 -4
  39. package/dist/cjs/src/service/team/schemas/tactics.z.test.js +29 -16
  40. package/dist/cjs/src/service/team/schemas/team.z.d.ts +17 -2
  41. package/dist/cjs/src/service/team/tactics.d.ts +3 -0
  42. package/dist/cjs/src/service/team/tactics.js +2 -1
  43. package/dist/cjs/src/service/team/team-generator.js +6 -1
  44. package/dist/esm/src/api/index.d.ts +3 -2
  45. package/dist/esm/src/api/tactics-player-refs.js +2 -2
  46. package/dist/esm/src/data/init-models.js +5 -1
  47. package/dist/esm/src/data/models/base-position-preset.d.ts +22 -0
  48. package/dist/esm/src/data/models/base-position-preset.js +50 -0
  49. package/dist/esm/src/data/models/index.d.ts +1 -0
  50. package/dist/esm/src/data/models/index.js +1 -0
  51. package/dist/esm/src/data/models/tactics.d.ts +5 -3
  52. package/dist/esm/src/data/models/tactics.js +4 -0
  53. package/dist/esm/src/data/transformers/rally-event-injury.test.js +1 -1
  54. package/dist/esm/src/data/transformers/rally-event-positional.test.js +10 -2
  55. package/dist/esm/src/data/transformers/rally-event.js +3 -1
  56. package/dist/esm/src/data/transformers/rally.test.js +2 -2
  57. package/dist/esm/src/data/transformers/tactics.js +4 -0
  58. package/dist/esm/src/data/transformers/tactics.test.js +34 -10
  59. package/dist/esm/src/service/competition/competition-rewards.d.ts +20 -0
  60. package/dist/esm/src/service/competition/competition-rewards.js +43 -0
  61. package/dist/esm/src/service/competition/competition-rewards.test.d.ts +1 -0
  62. package/dist/esm/src/service/competition/competition-rewards.test.js +30 -0
  63. package/dist/esm/src/service/competition/index.d.ts +2 -0
  64. package/dist/esm/src/service/competition/index.js +2 -0
  65. package/dist/esm/src/service/competition/league-rewards.d.ts +21 -0
  66. package/dist/esm/src/service/competition/league-rewards.js +53 -0
  67. package/dist/esm/src/service/competition/league-rewards.test.d.ts +1 -0
  68. package/dist/esm/src/service/competition/league-rewards.test.js +48 -0
  69. package/dist/esm/src/service/event/schemas/spike.z.js +3 -1
  70. package/dist/esm/src/service/event/spike.d.ts +2 -4
  71. package/dist/esm/src/service/event/spike.js +5 -3
  72. package/dist/esm/src/service/team/base-config.d.ts +10 -0
  73. package/dist/esm/src/service/team/base-config.js +11 -0
  74. package/dist/esm/src/service/team/blocking-preference.d.ts +3 -1
  75. package/dist/esm/src/service/team/blocking-preference.js +1 -0
  76. package/dist/esm/src/service/team/default-base-config.d.ts +4 -0
  77. package/dist/esm/src/service/team/default-base-config.js +124 -0
  78. package/dist/esm/src/service/team/index.d.ts +2 -0
  79. package/dist/esm/src/service/team/index.js +2 -0
  80. package/dist/esm/src/service/team/schemas/tactics.z.d.ts +17 -2
  81. package/dist/esm/src/service/team/schemas/tactics.z.js +24 -4
  82. package/dist/esm/src/service/team/schemas/tactics.z.test.js +29 -16
  83. package/dist/esm/src/service/team/schemas/team.z.d.ts +17 -2
  84. package/dist/esm/src/service/team/tactics.d.ts +3 -0
  85. package/dist/esm/src/service/team/tactics.js +2 -1
  86. package/dist/esm/src/service/team/team-generator.js +6 -1
  87. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
1
+ import { BaseConfig, BoxScore, DataProps, DeclineStatus, Division as _Division, League as _League, Match as _Match, MatchSet as _MatchSet, Player as _Player, PlayerPosition, Qualifier as _Qualifier, QualifierMatch as _QualifierMatch, Rally as _Rally, RallyEvent as _RallyEvent, PointBreakdown, RallyMetrics, Season as _Season, Standing as _Standing, Team as _Team, Tournament as _Tournament, TournamentMatch as _TournamentMatch, National as _National, NationalMatch as _NationalMatch, Country as _Country, BlockingAssignment, DefenceFormation, CourtPosition, ConditionLogic, EnergyBand, MatchForfeitType, PerformanceStats, PinchCondition, RotationSystemEnum, SubBackMode, SubBand } from '../service';
2
2
  export * from './tactics-player-refs';
3
3
  export type Rally = DataProps<_Rally> & {
4
4
  homePlayerPosition: PlayerPosition[];
@@ -102,7 +102,7 @@ export interface ApiOffensivePreference {
102
102
  }
103
103
  export interface ApiBlockingPreference {
104
104
  rotation: number;
105
- targets: Record<string, BlockingAssignment>;
105
+ targets: Record<DefenceFormation, Record<string, BlockingAssignment>>;
106
106
  }
107
107
  export interface ApiSystemSet {
108
108
  rotationSystem: RotationSystemEnum;
@@ -140,6 +140,7 @@ export interface Tactics {
140
140
  systemSets?: ApiSystemSet[];
141
141
  offensivePreferenceSets?: ApiOffensivePreference[][];
142
142
  serveAttackShares?: Record<string, number>;
143
+ basePositions?: BaseConfig;
143
144
  blockingPreferences: ApiBlockingPreference[];
144
145
  blockingPreferenceSets?: ApiBlockingPreference[][];
145
146
  replaceKnockedImmediately?: Record<string, boolean>;
@@ -29,8 +29,8 @@ function collectTacticsPlayerIds(config) {
29
29
  ...((config.offensivePreferences ?? []).flatMap(p => Object.keys(p.weights))),
30
30
  ...((config.systemSets ?? []).flatMap(s => s.designatedSetters)),
31
31
  ...((config.offensivePreferenceSets ?? []).flat().flatMap(p => Object.keys(p.weights))),
32
- ...((config.blockingPreferences ?? []).flatMap(p => Object.keys(p.targets))),
33
- ...((config.blockingPreferenceSets ?? []).flat().flatMap(p => Object.keys(p.targets))),
32
+ ...((config.blockingPreferences ?? []).flatMap(p => Object.values(p.targets).flatMap(m => Object.keys(m ?? {})))),
33
+ ...((config.blockingPreferenceSets ?? []).flat().flatMap(p => Object.values(p.targets).flatMap(m => Object.keys(m ?? {})))),
34
34
  // Injury replacements reference reserves (roster players outside the lineup); roster membership is the only
35
35
  // player check possible here (the schema enforces the bench/reserve role rules).
36
36
  ...((config.injuryReplacements ?? []).flatMap(ir => [ir.starterId, ir.replacementId, ir.benchBackfillId])),
@@ -27,6 +27,7 @@ function initModels(sequelize) {
27
27
  const Wishlist = models_1.WishlistModel.initModel(sequelize);
28
28
  const UserSettings = models_1.UserSettingsModel.initModel(sequelize);
29
29
  const UserDeviceToken = models_1.UserDeviceTokenModel.initModel(sequelize);
30
+ const BasePositionPreset = models_1.BasePositionPresetModel.initModel(sequelize);
30
31
  const Notification = models_1.NotificationModel.initModel(sequelize);
31
32
  const AuthIdentity = models_1.AuthIdentityModel.initModel(sequelize);
32
33
  const AuthSession = models_1.AuthSessionModel.initModel(sequelize);
@@ -75,8 +76,10 @@ function initModels(sequelize) {
75
76
  AuthUser.hasOne(Wishlist, { as: 'Wishlist', foreignKey: 'user_id' });
76
77
  AuthUser.hasOne(UserSettings, { as: 'UserSettings', foreignKey: 'user_id' });
77
78
  AuthUser.hasMany(UserDeviceToken, { as: 'UserDeviceTokens', foreignKey: 'user_id' });
79
+ AuthUser.hasMany(BasePositionPreset, { as: 'BasePositionPresets', foreignKey: 'user_id' });
78
80
  AuthUser.hasMany(Notification, { as: 'Notifications', foreignKey: 'user_id' });
79
81
  UserDeviceToken.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
82
+ BasePositionPreset.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
80
83
  Notification.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
81
84
  AuthUser.hasMany(GachaPullHistory, { as: 'GachaPullHistory', foreignKey: 'user_id' });
82
85
  AuthUser.hasMany(UserPullGrant, { as: 'UserPullGrants', foreignKey: 'user_id' });
@@ -354,6 +357,7 @@ function initModels(sequelize) {
354
357
  Wishlist,
355
358
  UserSettings,
356
359
  UserDeviceToken,
360
+ BasePositionPreset,
357
361
  Notification,
358
362
  Coach,
359
363
  Scout,
@@ -0,0 +1,22 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ import { BaseConfig } from '../../service';
4
+ export interface BasePositionPresetAttributes {
5
+ preset_id: string;
6
+ user_id: string;
7
+ name: string;
8
+ config: BaseConfig;
9
+ created_at?: Date;
10
+ }
11
+ export type BasePositionPresetPk = 'preset_id';
12
+ export type BasePositionPresetId = BasePositionPresetModel[BasePositionPresetPk];
13
+ export type BasePositionPresetOptionalAttributes = 'preset_id' | 'created_at';
14
+ export type BasePositionPresetCreationAttributes = Optional<BasePositionPresetAttributes, BasePositionPresetOptionalAttributes>;
15
+ export declare class BasePositionPresetModel extends Model<BasePositionPresetAttributes, BasePositionPresetCreationAttributes> implements BasePositionPresetAttributes {
16
+ preset_id: string;
17
+ user_id: string;
18
+ name: string;
19
+ config: BaseConfig;
20
+ created_at?: Date;
21
+ static initModel(sequelize: Sequelize.Sequelize): typeof BasePositionPresetModel;
22
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BasePositionPresetModel = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ class BasePositionPresetModel extends sequelize_1.Model {
6
+ static initModel(sequelize) {
7
+ return BasePositionPresetModel.init({
8
+ preset_id: {
9
+ type: sequelize_1.DataTypes.UUID,
10
+ allowNull: false,
11
+ primaryKey: true,
12
+ defaultValue: sequelize_1.DataTypes.UUIDV4
13
+ },
14
+ user_id: {
15
+ type: sequelize_1.DataTypes.UUID,
16
+ allowNull: false,
17
+ references: {
18
+ model: 'AuthUser',
19
+ key: 'user_id'
20
+ }
21
+ },
22
+ name: {
23
+ type: sequelize_1.DataTypes.STRING,
24
+ allowNull: false
25
+ },
26
+ config: {
27
+ type: sequelize_1.DataTypes.JSONB,
28
+ allowNull: false
29
+ },
30
+ created_at: {
31
+ type: sequelize_1.DataTypes.DATE,
32
+ allowNull: false,
33
+ defaultValue: sequelize_1.DataTypes.NOW
34
+ }
35
+ }, {
36
+ sequelize,
37
+ tableName: 'BasePositionPreset',
38
+ schema: 'public',
39
+ timestamps: false,
40
+ indexes: [
41
+ {
42
+ name: 'BasePositionPreset_pk',
43
+ unique: true,
44
+ fields: [{ name: 'preset_id' }]
45
+ },
46
+ {
47
+ name: 'BasePositionPreset_user_id_idx',
48
+ fields: [{ name: 'user_id' }]
49
+ }
50
+ ]
51
+ });
52
+ }
53
+ }
54
+ exports.BasePositionPresetModel = BasePositionPresetModel;
@@ -38,6 +38,7 @@ export * from './team';
38
38
  export * from './team-replacement';
39
39
  export * from './tactics';
40
40
  export * from './lineup-preset';
41
+ export * from './base-position-preset';
41
42
  export * from './event';
42
43
  export * from './event-competition';
43
44
  export * from './event-bot-team';
@@ -54,6 +54,7 @@ __exportStar(require("./team"), exports);
54
54
  __exportStar(require("./team-replacement"), exports);
55
55
  __exportStar(require("./tactics"), exports);
56
56
  __exportStar(require("./lineup-preset"), exports);
57
+ __exportStar(require("./base-position-preset"), exports);
57
58
  __exportStar(require("./event"), exports);
58
59
  __exportStar(require("./event-competition"), exports);
59
60
  __exportStar(require("./event-bot-team"), exports);
@@ -1,7 +1,7 @@
1
1
  import * as Sequelize from 'sequelize';
2
2
  import { Model, Optional } from 'sequelize';
3
3
  import { PlayerId, TeamId, TeamModel } from '.';
4
- import { BlockingAssignment, ConditionLogic, CourtPosition, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
4
+ import { BaseConfig, BlockingAssignment, ConditionLogic, CourtPosition, DefenceFormation, EnergyBand, PinchCondition, RotationSystemEnum, SubBackMode, SubBand, SubMode } from '../../service';
5
5
  export interface TacticsLineupAttributes {
6
6
  [CourtPosition.LIBERO_ZONE]?: PlayerId;
7
7
  [CourtPosition.LEFT_BACK]: PlayerId;
@@ -36,7 +36,7 @@ export interface OffensivePreferenceAttributes {
36
36
  }
37
37
  export interface BlockingPreferenceAttributes {
38
38
  rotation: number;
39
- targets: Record<PlayerId, BlockingAssignment>;
39
+ targets: Record<DefenceFormation, Record<PlayerId, BlockingAssignment>>;
40
40
  }
41
41
  export interface SystemSetAttributes {
42
42
  rotationSystem: RotationSystemEnum;
@@ -82,10 +82,11 @@ export interface TacticsAttributes {
82
82
  libero_injury?: LiberoInjuryAttributes;
83
83
  blocking_preferences: BlockingPreferenceAttributes[];
84
84
  blocking_preference_sets?: BlockingPreferenceAttributes[][];
85
+ base_positions?: BaseConfig;
85
86
  }
86
87
  export type TacticsPk = 'team_id';
87
88
  export type TacticsId = TacticsModel[TacticsPk];
88
- export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets';
89
+ export type TacticsOptionalAttributes = 'lineup' | 'libero_replacements' | 'receive_rotation_offset' | 'designated_subs' | 'substitution_band' | 'second_libero' | 'libero_sub' | 'libero_replacement_sets' | 'rotation_system' | 'designated_setters' | 'offensive_preferences' | 'system_sets' | 'offensive_preference_sets' | 'serve_attack_shares' | 'replace_knocked_immediately' | 'injury_replacements' | 'libero_injury' | 'blocking_preferences' | 'blocking_preference_sets' | 'base_positions';
89
90
  export type TacticsCreationAttributes = Optional<TacticsAttributes, TacticsOptionalAttributes>;
90
91
  export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreationAttributes> implements TacticsAttributes {
91
92
  team_id: string;
@@ -108,6 +109,7 @@ export declare class TacticsModel extends Model<TacticsAttributes, TacticsCreati
108
109
  libero_injury?: LiberoInjuryAttributes;
109
110
  blocking_preferences: BlockingPreferenceAttributes[];
110
111
  blocking_preference_sets?: BlockingPreferenceAttributes[][];
112
+ base_positions?: BaseConfig;
111
113
  team: TeamModel;
112
114
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
113
115
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -100,6 +100,10 @@ class TacticsModel extends sequelize_1.Model {
100
100
  blocking_preference_sets: {
101
101
  type: sequelize_1.DataTypes.JSONB,
102
102
  allowNull: true
103
+ },
104
+ base_positions: {
105
+ type: sequelize_1.DataTypes.JSONB,
106
+ allowNull: true
103
107
  }
104
108
  }, {
105
109
  sequelize,
@@ -41,7 +41,7 @@ const rally_event_1 = require("./rally-event");
41
41
  score: 62.5,
42
42
  target: 4,
43
43
  failure: service_1.SpikeFailureEnum.NO_FAILURE,
44
- type: service_1.SpikeTypeEnum.CROSS_SHOT,
44
+ type: service_1.SpikeTypeEnum.SPIKE,
45
45
  ...(incident != null ? { incident } : {})
46
46
  });
47
47
  }
@@ -14,7 +14,7 @@ const rally_event_1 = require("./rally-event");
14
14
  score: 60,
15
15
  target: 4,
16
16
  failure: service_1.SpikeFailureEnum.NO_FAILURE,
17
- type: service_1.SpikeTypeEnum.CROSS_SHOT,
17
+ type: service_1.SpikeTypeEnum.SPIKE,
18
18
  contact: { x: 1.234, y: -0.5 },
19
19
  landing: { x: -2.5, y: 6.789 },
20
20
  speed: 92.456
@@ -43,7 +43,7 @@ const rally_event_1 = require("./rally-event");
43
43
  (0, globals_1.expect)(decoded.digQuality).toBe(service_1.DigQualityEnum.MEDIOCRE);
44
44
  });
45
45
  (0, globals_1.it)('omits the positional keys when absent, and a legacy event decodes them as undefined', () => {
46
- const spike = service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.CROSS_SHOT });
46
+ const spike = service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE });
47
47
  const compact = (0, rally_event_1.transformToCompact)(spike, playerIndex);
48
48
  (0, globals_1.expect)('c' in compact).toBe(false);
49
49
  (0, globals_1.expect)('v' in compact).toBe(false);
@@ -53,4 +53,12 @@ const rally_event_1 = require("./rally-event");
53
53
  (0, globals_1.expect)(decoded.speed).toBeUndefined();
54
54
  (0, globals_1.expect)(decoded.digQuality).toBeUndefined();
55
55
  });
56
+ (0, globals_1.it)('collapses legacy LINE_SHOT(1)/REVERSE_CROSS(2) to SPIKE(0) on decode (owner 2026-08-03)', () => {
57
+ // A zone-engine match persisted before the collapse carries type 1 or 2 in its jsonb; decode heals them to
58
+ // SPIKE so the match still opens. TIP(3) and up are untouched.
59
+ const base = (0, rally_event_1.transformToCompact)(service_1.Spike.create({ playerId, score: 60, target: 4, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE }), playerIndex);
60
+ (0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 1 }, roster).type).toBe(service_1.SpikeTypeEnum.SPIKE);
61
+ (0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 2 }, roster).type).toBe(service_1.SpikeTypeEnum.SPIKE);
62
+ (0, globals_1.expect)((0, rally_event_1.transformToSpike)({ ...base, t: 3 }, roster).type).toBe(service_1.SpikeTypeEnum.TIP);
63
+ });
56
64
  });
@@ -138,7 +138,9 @@ function transformToSet(event, roster) {
138
138
  function transformToSpike(event, roster) {
139
139
  return service_1.Spike.create({
140
140
  failure: event.f,
141
- type: event.t,
141
+ // Legacy LINE_SHOT(1)/REVERSE_CROSS(2) collapse to SPIKE(0) (owner 2026-08-03): direction is redundant with
142
+ // the stored contact+landing coords. Maps old zone-engine matches so they keep decoding; new events emit 0/3-6.
143
+ type: event.t === 1 || event.t === 2 ? 0 : event.t,
142
144
  playerId: roster[event.p],
143
145
  target: event.a,
144
146
  score: event.s,
@@ -19,7 +19,7 @@ const rally_1 = require("./rally");
19
19
  service_1.Serve.create({ playerId: pA, target: 6, score: 55, failure: service_1.ServeFailureEnum.NO_FAILURE, type: service_1.ServeTypeEnum.JUMP_TOPSPIN }),
20
20
  service_1.Reception.create({ playerId: pB, target: 5, score: 50, failure: service_1.ReceptionFailureEnum.NO_FAILURE, type: service_1.ReceptionTypeEnum.DIG }),
21
21
  service_1.Set.create({ playerId: pC, target: 3, score: 48, failure: service_1.SetFailureEnum.NO_FAILURE, type: service_1.SetTypeEnum.OVERHAND, tempo: service_1.SetTempoEnum.FIRST }),
22
- service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.CROSS_SHOT }),
22
+ service_1.Spike.create({ playerId: pA, target: 4, score: 62.5, failure: service_1.SpikeFailureEnum.NO_FAILURE, type: service_1.SpikeTypeEnum.SPIKE }),
23
23
  service_1.Block.create({ playerId: pB, blockers: [pB, pC], target: 8, score: 48, failure: service_1.BlockFailureEnum.NO_FAILURE, type: service_1.BlockTypeEnum.DOUBLE }),
24
24
  service_1.FreeBall.create({ playerId: pC, target: 6, score: 45, failure: service_1.FreeBallFailureEnum.NO_FAILURE, type: service_1.FreeBallTypeEnum.OVER })
25
25
  ];
@@ -49,7 +49,7 @@ const rally_1 = require("./rally");
49
49
  (0, globals_1.expect)(rec.type).toBe(service_1.ReceptionTypeEnum.DIG);
50
50
  (0, globals_1.expect)(set.type).toBe(service_1.SetTypeEnum.OVERHAND);
51
51
  (0, globals_1.expect)(set.tempo).toBe(service_1.SetTempoEnum.FIRST);
52
- (0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.CROSS_SHOT);
52
+ (0, globals_1.expect)(spike.type).toBe(service_1.SpikeTypeEnum.SPIKE);
53
53
  (0, globals_1.expect)(spike.score).toBe(62.5);
54
54
  (0, globals_1.expect)(block.type).toBe(service_1.BlockTypeEnum.DOUBLE);
55
55
  (0, globals_1.expect)(block.blockers).toEqual([pB, pC]);
@@ -86,6 +86,7 @@ function transformToAttributes(tactics, teamId) {
86
86
  tempos: p.tempos
87
87
  }))),
88
88
  serve_attack_shares: tactics.serveAttackShares ?? {},
89
+ base_positions: tactics.basePositions,
89
90
  blocking_preferences: tactics.blockingPreferences.map(p => ({ rotation: p.rotation, targets: p.targets })),
90
91
  blocking_preference_sets: tactics.blockingPreferenceSets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
91
92
  replace_knocked_immediately: tactics.replaceKnockedImmediately,
@@ -207,6 +208,7 @@ function transformToObject(model, roster) {
207
208
  }
208
209
  return healed;
209
210
  })(),
211
+ basePositions: model.base_positions ?? undefined,
210
212
  blockingPreferences: (model.blocking_preferences ?? []).map(p => ({ rotation: p.rotation, targets: p.targets })),
211
213
  blockingPreferenceSets: model.blocking_preference_sets?.map(prefs => prefs.map(p => ({ rotation: p.rotation, targets: p.targets }))),
212
214
  // LENIENT: keep only boolean entries keyed by a court starter or, when a second libero exists, the starting
@@ -367,6 +369,7 @@ function tacticsColumnsToApiDto(model) {
367
369
  }
368
370
  return Object.keys(healed).length > 0 ? healed : undefined;
369
371
  })(),
372
+ basePositions: model.base_positions ?? undefined,
370
373
  blockingPreferences: model.blocking_preferences,
371
374
  blockingPreferenceSets: model.blocking_preference_sets,
372
375
  // Same stale-data policy as the heals below: only boolean entries keyed by a court starter or, when a second
@@ -475,6 +478,7 @@ function apiTacticsConfigToAttributes(config, teamId) {
475
478
  system_sets: config.systemSets,
476
479
  offensive_preference_sets: config.offensivePreferenceSets,
477
480
  serve_attack_shares: config.serveAttackShares ?? {},
481
+ base_positions: config.basePositions,
478
482
  blocking_preferences: config.blockingPreferences ?? [],
479
483
  blocking_preference_sets: config.blockingPreferenceSets,
480
484
  replace_knocked_immediately: config.replaceKnockedImmediately,
@@ -227,6 +227,30 @@ function makeTactics(designatedSubs) {
227
227
  const back = (0, tactics_1.transformToTactics)(attrs, roster);
228
228
  (0, globals_1.expect)(back.serveAttackShares).toEqual({});
229
229
  });
230
+ (0, globals_1.it)('round-trips base positions (coords + reception participation)', () => {
231
+ const config = {
232
+ coords: { RECEIVE: { 1: { 5: { d: 5.5, r: -3 }, 6: { d: 6.5, r: 0 } } }, DEFENCE: { 3: { 2: { d: 0.9, r: 3 } } } },
233
+ receive: { 1: { 5: true, 2: false }, 4: { 6: true } }
234
+ };
235
+ const withBases = service_1.Tactics.create({
236
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
237
+ liberoReplacements: [],
238
+ basePositions: config
239
+ });
240
+ const attrs = (0, tactics_1.transformFromTactics)(withBases, 'team-1');
241
+ (0, globals_1.expect)(attrs.base_positions).toEqual(config);
242
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
243
+ (0, globals_1.expect)(back.basePositions).toEqual(config);
244
+ });
245
+ (0, globals_1.it)('leaves basePositions undefined when the column is absent', () => {
246
+ const attrs = (0, tactics_1.transformFromTactics)(service_1.Tactics.create({
247
+ lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
248
+ liberoReplacements: []
249
+ }), 'team-1');
250
+ (0, globals_1.expect)(attrs.base_positions).toBeUndefined();
251
+ const back = (0, tactics_1.transformToTactics)(attrs, roster);
252
+ (0, globals_1.expect)(back.basePositions).toBeUndefined();
253
+ });
230
254
  (0, globals_1.it)('round-trips a per-starter tempos map on offensive preferences', () => {
231
255
  const withTempos = service_1.Tactics.create({
232
256
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
@@ -240,18 +264,18 @@ function makeTactics(designatedSubs) {
240
264
  const back = (0, tactics_1.transformToTactics)(attrs, roster);
241
265
  (0, globals_1.expect)(back.offensivePreferences[0].tempos).toEqual({ [p2.id]: 'FAST', [p3.id]: 'HIGH' });
242
266
  });
243
- (0, globals_1.it)('round-trips blocking preferences as id-keyed function maps', () => {
267
+ (0, globals_1.it)('round-trips blocking preferences as per-defence-side id-keyed function maps', () => {
244
268
  const withBlocking = service_1.Tactics.create({
245
269
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
246
270
  liberoReplacements: [],
247
- blockingPreferences: [{ rotation: 1, targets: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }]
271
+ blockingPreferences: [{ rotation: 1, targets: { DEF_L: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER }, DEF_R: { [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } } }]
248
272
  });
249
273
  const attrs = (0, tactics_1.transformFromTactics)(withBlocking, 'team-1');
250
274
  (0, globals_1.expect)(attrs.blocking_preferences).toEqual([
251
- { rotation: 1, targets: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }
275
+ { rotation: 1, targets: { DEF_L: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER }, DEF_M: {}, DEF_R: { [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } } }
252
276
  ]);
253
277
  const back = (0, tactics_1.transformToTactics)(attrs, roster);
254
- (0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
278
+ (0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ DEF_L: { [p4.id]: service_1.RoleEnum.OUTSIDE_HITTER }, DEF_M: {}, DEF_R: { [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } });
255
279
  });
256
280
  // NO_BLOCK rides in the same targets map as a commit assignment, so the transformers copy it wholesale with no
257
281
  // mapping code. This pins that: the sentinel must survive the row round-trip verbatim, mixed with a commit, and
@@ -260,17 +284,17 @@ function makeTactics(designatedSubs) {
260
284
  const withNoBlock = service_1.Tactics.create({
261
285
  lineup: { 4: p4, 3: p3, 2: p2, 5: p5, 6: p6, 1: p1, bench: [] },
262
286
  liberoReplacements: [],
263
- blockingPreferences: [{ rotation: 1, targets: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }],
264
- blockingPreferenceSets: [[{ rotation: 2, targets: { [p2.id]: service_1.NO_BLOCK } }]]
287
+ blockingPreferences: [{ rotation: 1, targets: { DEF_M: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } } }],
288
+ blockingPreferenceSets: [[{ rotation: 2, targets: { DEF_L: { [p2.id]: service_1.NO_BLOCK } } }]]
265
289
  });
266
290
  const attrs = (0, tactics_1.transformFromTactics)(withNoBlock, 'team-1');
267
291
  (0, globals_1.expect)(attrs.blocking_preferences).toEqual([
268
- { rotation: 1, targets: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER } }
292
+ { rotation: 1, targets: { DEF_L: {}, DEF_M: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, DEF_R: {} } }
269
293
  ]);
270
- (0, globals_1.expect)(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { [p2.id]: service_1.NO_BLOCK } }]]);
294
+ (0, globals_1.expect)(attrs.blocking_preference_sets).toEqual([[{ rotation: 2, targets: { DEF_L: { [p2.id]: service_1.NO_BLOCK }, DEF_M: {}, DEF_R: {} } }]]);
271
295
  const back = (0, tactics_1.transformToTactics)(attrs, roster);
272
- (0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER });
273
- (0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].targets).toEqual({ [p2.id]: service_1.NO_BLOCK });
296
+ (0, globals_1.expect)(back.blockingPreferences[0].targets).toEqual({ DEF_L: {}, DEF_M: { [p4.id]: service_1.NO_BLOCK, [p3.id]: service_1.RoleEnum.MIDDLE_BLOCKER }, DEF_R: {} });
297
+ (0, globals_1.expect)(back.blockingPreferenceSets?.[0][0].targets).toEqual({ DEF_L: { [p2.id]: service_1.NO_BLOCK }, DEF_M: {}, DEF_R: {} });
274
298
  });
275
299
  });
276
300
  (0, globals_1.describe)('libero injuries — service <-> row round-trip + lenient heal', () => {
@@ -0,0 +1,20 @@
1
+ export type CompetitionRewardKind = 'REGIONAL' | 'NATIONAL' | 'WCC';
2
+ export interface CompetitionRewardRow {
3
+ /** Best final position that still earns this row: the "Top N" band. */
4
+ readonly maxPosition: number;
5
+ /** Coins awarded by this row alone (the increment, not the running total). */
6
+ readonly coins: number;
7
+ /** Recruit-reward pulls awarded by this row alone (the competition's own pull type). */
8
+ readonly recruits: number;
9
+ }
10
+ /** Per-competition bands, widest band first (the order the docs list them, with a growing cumulative total). */
11
+ export declare const COMPETITION_REWARDS: Readonly<Record<CompetitionRewardKind, readonly CompetitionRewardRow[]>>;
12
+ export interface CompetitionPayout {
13
+ readonly coins: number;
14
+ readonly recruits: number;
15
+ }
16
+ /**
17
+ * Everything a finishing position earns in the given competition: the sum of every band that still includes the
18
+ * position (cumulative). Positions past the lowest band earn nothing. `position` is the 1-based final placement.
19
+ */
20
+ export declare function competitionPayoutFor(kind: CompetitionRewardKind, position: number): CompetitionPayout;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ // What the knockout competitions (Regional Cup, National Cup, WCC) pay out by final placement. Owner-defined
3
+ // 2026-08-07. Like the event and league rewards, these are CUMULATIVE: a finish earns its band and every band
4
+ // below it. Each band gives coins and/or "recruit rewards" (the competition's own pull grant: a Regional
5
+ // recruit is a QUALIFIER pull, a National recruit a NATIONAL pull, a WCC recruit a TOURNAMENT pull). Recruit
6
+ // rewards stack, so a champion collects one per qualifying band.
7
+ //
8
+ // One source of truth for two consumers: the simulator grants from it and the UI competition docs render it.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.COMPETITION_REWARDS = void 0;
11
+ exports.competitionPayoutFor = competitionPayoutFor;
12
+ /** Per-competition bands, widest band first (the order the docs list them, with a growing cumulative total). */
13
+ exports.COMPETITION_REWARDS = {
14
+ REGIONAL: [
15
+ { maxPosition: 4, coins: 1000, recruits: 0 },
16
+ { maxPosition: 3, coins: 0, recruits: 1 },
17
+ { maxPosition: 2, coins: 1000, recruits: 0 },
18
+ { maxPosition: 1, coins: 500, recruits: 1 }
19
+ ],
20
+ NATIONAL: [
21
+ { maxPosition: 8, coins: 1000, recruits: 0 },
22
+ { maxPosition: 4, coins: 500, recruits: 1 },
23
+ { maxPosition: 2, coins: 500, recruits: 0 },
24
+ { maxPosition: 1, coins: 0, recruits: 1 }
25
+ ],
26
+ WCC: [
27
+ { maxPosition: 4, coins: 1000, recruits: 0 },
28
+ { maxPosition: 3, coins: 0, recruits: 1 },
29
+ { maxPosition: 2, coins: 500, recruits: 1 },
30
+ { maxPosition: 1, coins: 500, recruits: 1 }
31
+ ]
32
+ };
33
+ /**
34
+ * Everything a finishing position earns in the given competition: the sum of every band that still includes the
35
+ * position (cumulative). Positions past the lowest band earn nothing. `position` is the 1-based final placement.
36
+ */
37
+ function competitionPayoutFor(kind, position) {
38
+ let coins = 0;
39
+ let recruits = 0;
40
+ for (const row of exports.COMPETITION_REWARDS[kind]) {
41
+ if (position <= row.maxPosition) {
42
+ coins += row.coins;
43
+ recruits += row.recruits;
44
+ }
45
+ }
46
+ return { coins, recruits };
47
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ const competition_rewards_1 = require("./competition-rewards");
5
+ // The cumulative per-position totals the owner specified (2026-08-07): { coins, recruits } a finish earns,
6
+ // summing its band and every band below. Positions past the lowest band earn nothing.
7
+ (0, globals_1.describe)('competitionPayoutFor', () => {
8
+ const CASES = {
9
+ // position: [coins, recruits]
10
+ REGIONAL: { 1: [2500, 2], 2: [2000, 1], 3: [1000, 1], 4: [1000, 0], 5: [0, 0] },
11
+ NATIONAL: { 1: [2000, 2], 2: [2000, 1], 3: [1500, 1], 4: [1500, 1], 5: [1000, 0], 8: [1000, 0], 9: [0, 0] },
12
+ WCC: { 1: [2000, 3], 2: [1500, 2], 3: [1000, 1], 4: [1000, 0], 5: [0, 0] }
13
+ };
14
+ for (const kind of Object.keys(CASES)) {
15
+ (0, globals_1.it)(`matches the owner totals for ${kind}`, () => {
16
+ for (const [pos, [coins, recruits]] of Object.entries(CASES[kind])) {
17
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)(kind, Number(pos))).toEqual({ coins, recruits });
18
+ }
19
+ });
20
+ }
21
+ (0, globals_1.it)('champion collects one recruit per qualifying band', () => {
22
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('REGIONAL', 1).recruits).toBe(2);
23
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('NATIONAL', 1).recruits).toBe(2);
24
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('WCC', 1).recruits).toBe(3);
25
+ });
26
+ (0, globals_1.it)('champion coin total equals the sum of the band table', () => {
27
+ const sum = (k) => competition_rewards_1.COMPETITION_REWARDS[k].reduce((s, r) => s + r.coins, 0);
28
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('REGIONAL', 1).coins).toBe(sum('REGIONAL'));
29
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('NATIONAL', 1).coins).toBe(sum('NATIONAL'));
30
+ (0, globals_1.expect)((0, competition_rewards_1.competitionPayoutFor)('WCC', 1).coins).toBe(sum('WCC'));
31
+ });
32
+ });
@@ -3,6 +3,8 @@ export * from './season';
3
3
  export * from './event-schedule';
4
4
  export * from './event-draft';
5
5
  export * from './event-rewards';
6
+ export * from './league-rewards';
7
+ export * from './competition-rewards';
6
8
  export * from './league';
7
9
  export * from './division';
8
10
  export * from './iteration';
@@ -19,6 +19,8 @@ __exportStar(require("./season"), exports);
19
19
  __exportStar(require("./event-schedule"), exports);
20
20
  __exportStar(require("./event-draft"), exports);
21
21
  __exportStar(require("./event-rewards"), exports);
22
+ __exportStar(require("./league-rewards"), exports);
23
+ __exportStar(require("./competition-rewards"), exports);
22
24
  __exportStar(require("./league"), exports);
23
25
  __exportStar(require("./division"), exports);
24
26
  __exportStar(require("./iteration"), exports);
@@ -0,0 +1,21 @@
1
+ export interface LeagueRewardRow {
2
+ /** Best final position that still earns this row: the "Top N" band. */
3
+ readonly maxPosition: number;
4
+ /** Coins awarded by this row alone (the increment, not the running total). */
5
+ readonly coins: number;
6
+ }
7
+ /** Division tiers that get their own reward table. Tier 3 covers "division 3 and lower". */
8
+ export type LeagueRewardTier = 1 | 2 | 3;
9
+ /**
10
+ * Per-tier bands, widest band first (the order the standings doc lists them, with a growing cumulative total).
11
+ * A team earns every row whose band still includes its finishing position, which is what "cumulative" means.
12
+ */
13
+ export declare const LEAGUE_REWARDS: Readonly<Record<LeagueRewardTier, readonly LeagueRewardRow[]>>;
14
+ /** Clamp any division tier to a reward table: tier 1, tier 2, and tier 3-and-lower share the tier-3 table. */
15
+ export declare function leagueRewardTier(tier: number): LeagueRewardTier;
16
+ /**
17
+ * Coins a finishing position earns in a division of the given tier: the sum of every band that still includes
18
+ * the position (cumulative). Positions past the lowest band earn 0. `position` is the 1-based final standings
19
+ * place.
20
+ */
21
+ export declare function leaguePayoutFor(tier: number, position: number): number;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // What a league season pays out at the end, by division tier and final standings position. Owner-defined
3
+ // 2026-08-07. Like the event rewards, these are CUMULATIVE: a team collects its own band and every band below
4
+ // it, so first place collects them all. Positions past the lowest band earn nothing.
5
+ //
6
+ // One source of truth for two consumers: the simulator grants from it (assignSeasonPayoutIfMissing) and the UI
7
+ // standings doc renders it.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.LEAGUE_REWARDS = void 0;
10
+ exports.leagueRewardTier = leagueRewardTier;
11
+ exports.leaguePayoutFor = leaguePayoutFor;
12
+ /**
13
+ * Per-tier bands, widest band first (the order the standings doc lists them, with a growing cumulative total).
14
+ * A team earns every row whose band still includes its finishing position, which is what "cumulative" means.
15
+ */
16
+ exports.LEAGUE_REWARDS = {
17
+ 1: [
18
+ { maxPosition: 12, coins: 1000 },
19
+ { maxPosition: 8, coins: 500 },
20
+ { maxPosition: 4, coins: 500 },
21
+ { maxPosition: 3, coins: 250 },
22
+ { maxPosition: 2, coins: 250 },
23
+ { maxPosition: 1, coins: 500 }
24
+ ],
25
+ 2: [
26
+ { maxPosition: 8, coins: 500 },
27
+ { maxPosition: 4, coins: 500 },
28
+ { maxPosition: 2, coins: 250 },
29
+ { maxPosition: 1, coins: 250 }
30
+ ],
31
+ 3: [
32
+ { maxPosition: 4, coins: 500 },
33
+ { maxPosition: 2, coins: 250 },
34
+ { maxPosition: 1, coins: 250 }
35
+ ]
36
+ };
37
+ /** Clamp any division tier to a reward table: tier 1, tier 2, and tier 3-and-lower share the tier-3 table. */
38
+ function leagueRewardTier(tier) {
39
+ const t = Math.trunc(tier);
40
+ if (t <= 1)
41
+ return 1;
42
+ if (t === 2)
43
+ return 2;
44
+ return 3;
45
+ }
46
+ /**
47
+ * Coins a finishing position earns in a division of the given tier: the sum of every band that still includes
48
+ * the position (cumulative). Positions past the lowest band earn 0. `position` is the 1-based final standings
49
+ * place.
50
+ */
51
+ function leaguePayoutFor(tier, position) {
52
+ let coins = 0;
53
+ for (const row of exports.LEAGUE_REWARDS[leagueRewardTier(tier)]) {
54
+ if (position <= row.maxPosition)
55
+ coins += row.coins;
56
+ }
57
+ return coins;
58
+ }