volleyballsimtypes 0.0.491 → 0.0.492

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 (135) hide show
  1. package/dist/cjs/src/api/index.d.ts +2 -1
  2. package/dist/cjs/src/api/index.js +15 -0
  3. package/dist/cjs/src/api/tactics-player-refs.d.ts +13 -0
  4. package/dist/cjs/src/api/tactics-player-refs.js +43 -0
  5. package/dist/cjs/src/data/event-purge.d.ts +14 -0
  6. package/dist/cjs/src/data/event-purge.js +116 -0
  7. package/dist/cjs/src/data/index.d.ts +1 -0
  8. package/dist/cjs/src/data/index.js +1 -0
  9. package/dist/cjs/src/data/init-models.js +41 -0
  10. package/dist/cjs/src/data/models/competition.d.ts +1 -1
  11. package/dist/cjs/src/data/models/competition.js +1 -1
  12. package/dist/cjs/src/data/models/event-bot-team.d.ts +21 -0
  13. package/dist/cjs/src/data/models/event-bot-team.js +44 -0
  14. package/dist/cjs/src/data/models/event-competition.d.ts +29 -0
  15. package/dist/cjs/src/data/models/event-competition.js +52 -0
  16. package/dist/cjs/src/data/models/event-pack-player.d.ts +22 -0
  17. package/dist/cjs/src/data/models/event-pack-player.js +31 -0
  18. package/dist/cjs/src/data/models/event-pack.d.ts +37 -0
  19. package/dist/cjs/src/data/models/event-pack.js +55 -0
  20. package/dist/cjs/src/data/models/event-result.d.ts +34 -0
  21. package/dist/cjs/src/data/models/event-result.js +48 -0
  22. package/dist/cjs/src/data/models/event-squad.d.ts +30 -0
  23. package/dist/cjs/src/data/models/event-squad.js +46 -0
  24. package/dist/cjs/src/data/models/event.d.ts +51 -0
  25. package/dist/cjs/src/data/models/event.js +82 -0
  26. package/dist/cjs/src/data/models/index.d.ts +7 -0
  27. package/dist/cjs/src/data/models/index.js +7 -0
  28. package/dist/cjs/src/data/models/lineup-preset.d.ts +3 -1
  29. package/dist/cjs/src/data/models/lineup-preset.js +53 -0
  30. package/dist/cjs/src/index.d.ts +1 -0
  31. package/dist/cjs/src/index.js +6 -0
  32. package/dist/cjs/src/service/competition/event-draft.d.ts +46 -0
  33. package/dist/cjs/src/service/competition/event-draft.js +85 -0
  34. package/dist/cjs/src/service/competition/event-draft.test.d.ts +1 -0
  35. package/dist/cjs/src/service/competition/event-draft.test.js +91 -0
  36. package/dist/cjs/src/service/competition/event-rewards.d.ts +26 -0
  37. package/dist/cjs/src/service/competition/event-rewards.js +49 -0
  38. package/dist/cjs/src/service/competition/event-rewards.test.d.ts +1 -0
  39. package/dist/cjs/src/service/competition/event-rewards.test.js +58 -0
  40. package/dist/cjs/src/service/competition/event-schedule.d.ts +126 -0
  41. package/dist/cjs/src/service/competition/event-schedule.js +141 -0
  42. package/dist/cjs/src/service/competition/event-schedule.test.d.ts +1 -0
  43. package/dist/cjs/src/service/competition/event-schedule.test.js +138 -0
  44. package/dist/cjs/src/service/competition/index.d.ts +3 -0
  45. package/dist/cjs/src/service/competition/index.js +3 -0
  46. package/dist/cjs/src/service/match/index.d.ts +1 -0
  47. package/dist/cjs/src/service/match/index.js +1 -0
  48. package/dist/cjs/src/service/match/match-generator.js +25 -48
  49. package/dist/cjs/src/service/match/match-generator.test.d.ts +1 -0
  50. package/dist/cjs/src/service/match/match-generator.test.js +48 -0
  51. package/dist/cjs/src/service/match/round-robin.d.ts +11 -0
  52. package/dist/cjs/src/service/match/round-robin.js +44 -0
  53. package/dist/cjs/src/service/match/round-robin.test.d.ts +1 -0
  54. package/dist/cjs/src/service/match/round-robin.test.js +71 -0
  55. package/dist/cjs/src/service/notification/catalog/en.json +21 -0
  56. package/dist/cjs/src/service/notification/catalog/es.json +21 -0
  57. package/dist/cjs/src/service/notification/catalog/fr.json +21 -0
  58. package/dist/cjs/src/service/notification/catalog/it.json +21 -0
  59. package/dist/cjs/src/service/notification/catalog/pl.json +21 -0
  60. package/dist/cjs/src/service/notification/catalog/pt-BR.json +21 -0
  61. package/dist/cjs/src/service/notification/catalog/tr.json +21 -0
  62. package/dist/cjs/src/service/notification/notification-type.d.ts +1 -1
  63. package/dist/cjs/src/service/notification/notification-type.js +6 -1
  64. package/dist/cjs/src/service/notification/notification.test.js +3 -1
  65. package/dist/cjs/src/service/notification/registry.js +52 -0
  66. package/dist/cjs/src/service/player/player-generator.d.ts +1 -0
  67. package/dist/cjs/src/service/player/player-generator.js +16 -1
  68. package/dist/esm/src/api/index.d.ts +2 -1
  69. package/dist/esm/src/api/index.js +1 -0
  70. package/dist/esm/src/api/tactics-player-refs.d.ts +13 -0
  71. package/dist/esm/src/api/tactics-player-refs.js +40 -0
  72. package/dist/esm/src/data/event-purge.d.ts +14 -0
  73. package/dist/esm/src/data/event-purge.js +110 -0
  74. package/dist/esm/src/data/index.d.ts +1 -0
  75. package/dist/esm/src/data/index.js +1 -0
  76. package/dist/esm/src/data/init-models.js +42 -1
  77. package/dist/esm/src/data/models/competition.d.ts +1 -1
  78. package/dist/esm/src/data/models/competition.js +1 -1
  79. package/dist/esm/src/data/models/event-bot-team.d.ts +21 -0
  80. package/dist/esm/src/data/models/event-bot-team.js +40 -0
  81. package/dist/esm/src/data/models/event-competition.d.ts +29 -0
  82. package/dist/esm/src/data/models/event-competition.js +48 -0
  83. package/dist/esm/src/data/models/event-pack-player.d.ts +22 -0
  84. package/dist/esm/src/data/models/event-pack-player.js +27 -0
  85. package/dist/esm/src/data/models/event-pack.d.ts +37 -0
  86. package/dist/esm/src/data/models/event-pack.js +51 -0
  87. package/dist/esm/src/data/models/event-result.d.ts +34 -0
  88. package/dist/esm/src/data/models/event-result.js +44 -0
  89. package/dist/esm/src/data/models/event-squad.d.ts +30 -0
  90. package/dist/esm/src/data/models/event-squad.js +42 -0
  91. package/dist/esm/src/data/models/event.d.ts +51 -0
  92. package/dist/esm/src/data/models/event.js +78 -0
  93. package/dist/esm/src/data/models/index.d.ts +7 -0
  94. package/dist/esm/src/data/models/index.js +7 -0
  95. package/dist/esm/src/data/models/lineup-preset.d.ts +3 -1
  96. package/dist/esm/src/data/models/lineup-preset.js +20 -0
  97. package/dist/esm/src/index.d.ts +1 -0
  98. package/dist/esm/src/index.js +4 -0
  99. package/dist/esm/src/service/competition/event-draft.d.ts +46 -0
  100. package/dist/esm/src/service/competition/event-draft.js +78 -0
  101. package/dist/esm/src/service/competition/event-draft.test.d.ts +1 -0
  102. package/dist/esm/src/service/competition/event-draft.test.js +89 -0
  103. package/dist/esm/src/service/competition/event-rewards.d.ts +26 -0
  104. package/dist/esm/src/service/competition/event-rewards.js +44 -0
  105. package/dist/esm/src/service/competition/event-rewards.test.d.ts +1 -0
  106. package/dist/esm/src/service/competition/event-rewards.test.js +56 -0
  107. package/dist/esm/src/service/competition/event-schedule.d.ts +126 -0
  108. package/dist/esm/src/service/competition/event-schedule.js +129 -0
  109. package/dist/esm/src/service/competition/event-schedule.test.d.ts +1 -0
  110. package/dist/esm/src/service/competition/event-schedule.test.js +136 -0
  111. package/dist/esm/src/service/competition/index.d.ts +3 -0
  112. package/dist/esm/src/service/competition/index.js +3 -0
  113. package/dist/esm/src/service/match/index.d.ts +1 -0
  114. package/dist/esm/src/service/match/index.js +1 -0
  115. package/dist/esm/src/service/match/match-generator.js +25 -48
  116. package/dist/esm/src/service/match/match-generator.test.d.ts +1 -0
  117. package/dist/esm/src/service/match/match-generator.test.js +43 -0
  118. package/dist/esm/src/service/match/round-robin.d.ts +11 -0
  119. package/dist/esm/src/service/match/round-robin.js +41 -0
  120. package/dist/esm/src/service/match/round-robin.test.d.ts +1 -0
  121. package/dist/esm/src/service/match/round-robin.test.js +69 -0
  122. package/dist/esm/src/service/notification/catalog/en.json +21 -0
  123. package/dist/esm/src/service/notification/catalog/es.json +21 -0
  124. package/dist/esm/src/service/notification/catalog/fr.json +21 -0
  125. package/dist/esm/src/service/notification/catalog/it.json +21 -0
  126. package/dist/esm/src/service/notification/catalog/pl.json +21 -0
  127. package/dist/esm/src/service/notification/catalog/pt-BR.json +21 -0
  128. package/dist/esm/src/service/notification/catalog/tr.json +21 -0
  129. package/dist/esm/src/service/notification/notification-type.d.ts +1 -1
  130. package/dist/esm/src/service/notification/notification-type.js +6 -1
  131. package/dist/esm/src/service/notification/notification.test.js +3 -1
  132. package/dist/esm/src/service/notification/registry.js +52 -0
  133. package/dist/esm/src/service/player/player-generator.d.ts +1 -0
  134. package/dist/esm/src/service/player/player-generator.js +15 -0
  135. package/package.json +6 -1
@@ -0,0 +1,44 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class EventResultModel extends Model {
3
+ static initModel(sequelize) {
4
+ return EventResultModel.init({
5
+ event_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ references: { model: 'Event', key: 'event_id' }
10
+ },
11
+ team_id: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ primaryKey: true,
15
+ references: { model: 'Team', key: 'team_id' }
16
+ },
17
+ position: {
18
+ type: DataTypes.INTEGER,
19
+ allowNull: true
20
+ },
21
+ entrants: {
22
+ type: DataTypes.INTEGER,
23
+ allowNull: false
24
+ },
25
+ key: {
26
+ type: DataTypes.TEXT,
27
+ allowNull: false
28
+ },
29
+ ended_at: {
30
+ type: DataTypes.DATE,
31
+ allowNull: false
32
+ }
33
+ }, {
34
+ sequelize,
35
+ tableName: 'EventResult',
36
+ schema: 'public',
37
+ timestamps: false,
38
+ indexes: [
39
+ { name: 'EventResult_pk', unique: true, fields: [{ name: 'event_id' }, { name: 'team_id' }] },
40
+ { name: 'EventResult_team_id_idx', fields: [{ name: 'team_id' }] }
41
+ ]
42
+ });
43
+ }
44
+ }
@@ -0,0 +1,30 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ import { EventId, EventModel, TeamAttributes, TeamId, TeamModel } from '.';
4
+ export interface EventSquadAttributes {
5
+ event_id: string;
6
+ user_id: string;
7
+ /** The throwaway Team (active = false) holding the drafted players. Deleted at purge. */
8
+ team_id: string;
9
+ complete: boolean;
10
+ drafted_at: Date | null;
11
+ team?: TeamAttributes;
12
+ }
13
+ export type EventSquadPk = 'event_id' | 'user_id';
14
+ export type EventSquadId = EventSquadModel[EventSquadPk];
15
+ export type EventSquadOptionalAttributes = 'complete' | 'drafted_at';
16
+ export type EventSquadCreationAttributes = Optional<EventSquadAttributes, EventSquadOptionalAttributes>;
17
+ export declare class EventSquadModel extends Model<EventSquadAttributes, EventSquadCreationAttributes> implements EventSquadAttributes {
18
+ event_id: string;
19
+ user_id: string;
20
+ team_id: string;
21
+ complete: boolean;
22
+ drafted_at: Date | null;
23
+ Event: EventModel;
24
+ getEvent: Sequelize.BelongsToGetAssociationMixin<EventModel>;
25
+ setEvent: Sequelize.BelongsToSetAssociationMixin<EventModel, EventId>;
26
+ team: TeamModel;
27
+ getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
28
+ setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
29
+ static initModel(sequelize: Sequelize.Sequelize): typeof EventSquadModel;
30
+ }
@@ -0,0 +1,42 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class EventSquadModel extends Model {
3
+ static initModel(sequelize) {
4
+ return EventSquadModel.init({
5
+ event_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ references: { model: 'Event', key: 'event_id' }
10
+ },
11
+ user_id: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ primaryKey: true,
15
+ references: { model: 'AuthUser', key: 'user_id' }
16
+ },
17
+ team_id: {
18
+ type: DataTypes.UUID,
19
+ allowNull: false,
20
+ references: { model: 'Team', key: 'team_id' }
21
+ },
22
+ complete: {
23
+ type: DataTypes.BOOLEAN,
24
+ allowNull: false,
25
+ defaultValue: false
26
+ },
27
+ drafted_at: {
28
+ type: DataTypes.DATE,
29
+ allowNull: true
30
+ }
31
+ }, {
32
+ sequelize,
33
+ tableName: 'EventSquad',
34
+ schema: 'public',
35
+ timestamps: false,
36
+ indexes: [
37
+ { name: 'EventSquad_pk', unique: true, fields: [{ name: 'event_id' }, { name: 'user_id' }] },
38
+ { name: 'EventSquad_team_id_idx', fields: [{ name: 'team_id' }] }
39
+ ]
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,51 @@
1
+ import * as Sequelize from 'sequelize';
2
+ import { Model, Optional } from 'sequelize';
3
+ import type { EventFormat, EventKey, EventStatus } from '../../service/competition/event-schedule';
4
+ import { EventCompetitionAttributes, EventCompetitionModel, EventSquadAttributes, EventSquadModel } from '.';
5
+ export interface EventAttributes {
6
+ event_id: string;
7
+ key: EventKey;
8
+ format: EventFormat;
9
+ /** The rarity every player in an entrant's lineup must be. */
10
+ restriction: string;
11
+ status: EventStatus;
12
+ registration_opens_at: Date;
13
+ registration_closes_at: Date;
14
+ starts_at: Date;
15
+ final_starts_at: Date;
16
+ ends_at: Date;
17
+ purge_at: Date;
18
+ /** Set when the event's data was deleted; null while it still has competitions and matches. */
19
+ purged_at: Date | null;
20
+ /**
21
+ * Which of this event's notifications have already gone out, keyed by notification type. The tick runs every
22
+ * ~60 seconds, so without a marker every phase transition would re-send on every pass.
23
+ */
24
+ notified: Record<string, boolean>;
25
+ EventCompetitions?: EventCompetitionAttributes[];
26
+ EventSquads?: EventSquadAttributes[];
27
+ }
28
+ export type EventPk = 'event_id';
29
+ export type EventId = EventModel[EventPk];
30
+ export type EventOptionalAttributes = 'event_id' | 'purged_at' | 'notified';
31
+ export type EventCreationAttributes = Optional<EventAttributes, EventOptionalAttributes>;
32
+ export declare class EventModel extends Model<EventAttributes, EventCreationAttributes> implements EventAttributes {
33
+ event_id: string;
34
+ key: EventKey;
35
+ format: EventFormat;
36
+ restriction: string;
37
+ status: EventStatus;
38
+ registration_opens_at: Date;
39
+ registration_closes_at: Date;
40
+ starts_at: Date;
41
+ final_starts_at: Date;
42
+ ends_at: Date;
43
+ purge_at: Date;
44
+ purged_at: Date | null;
45
+ notified: Record<string, boolean>;
46
+ EventCompetitions: EventCompetitionModel[];
47
+ getEventCompetitions: Sequelize.HasManyGetAssociationsMixin<EventCompetitionModel>;
48
+ EventSquads: EventSquadModel[];
49
+ getEventSquads: Sequelize.HasManyGetAssociationsMixin<EventSquadModel>;
50
+ static initModel(sequelize: Sequelize.Sequelize): typeof EventModel;
51
+ }
@@ -0,0 +1,78 @@
1
+ import { DataTypes, Model } from 'sequelize';
2
+ export class EventModel extends Model {
3
+ static initModel(sequelize) {
4
+ return EventModel.init({
5
+ event_id: {
6
+ type: DataTypes.UUID,
7
+ allowNull: false,
8
+ primaryKey: true,
9
+ defaultValue: DataTypes.UUIDV4
10
+ },
11
+ key: {
12
+ type: DataTypes.TEXT,
13
+ allowNull: false
14
+ },
15
+ format: {
16
+ type: DataTypes.TEXT,
17
+ allowNull: false
18
+ },
19
+ restriction: {
20
+ type: DataTypes.TEXT,
21
+ allowNull: false
22
+ },
23
+ status: {
24
+ type: DataTypes.TEXT,
25
+ allowNull: false
26
+ },
27
+ registration_opens_at: {
28
+ type: DataTypes.DATE,
29
+ allowNull: false
30
+ },
31
+ registration_closes_at: {
32
+ type: DataTypes.DATE,
33
+ allowNull: false
34
+ },
35
+ starts_at: {
36
+ type: DataTypes.DATE,
37
+ allowNull: false
38
+ },
39
+ final_starts_at: {
40
+ type: DataTypes.DATE,
41
+ allowNull: false
42
+ },
43
+ ends_at: {
44
+ type: DataTypes.DATE,
45
+ allowNull: false
46
+ },
47
+ purge_at: {
48
+ type: DataTypes.DATE,
49
+ allowNull: false
50
+ },
51
+ purged_at: {
52
+ type: DataTypes.DATE,
53
+ allowNull: true
54
+ },
55
+ notified: {
56
+ type: DataTypes.JSONB,
57
+ allowNull: false,
58
+ defaultValue: {}
59
+ }
60
+ }, {
61
+ sequelize,
62
+ tableName: 'Event',
63
+ schema: 'public',
64
+ timestamps: false,
65
+ indexes: [
66
+ {
67
+ name: 'Event_pk',
68
+ unique: true,
69
+ fields: [{ name: 'event_id' }]
70
+ },
71
+ {
72
+ name: 'Event_starts_at_idx',
73
+ fields: [{ name: 'starts_at' }]
74
+ }
75
+ ]
76
+ });
77
+ }
78
+ }
@@ -38,6 +38,13 @@ export * from './team';
38
38
  export * from './team-replacement';
39
39
  export * from './tactics';
40
40
  export * from './lineup-preset';
41
+ export * from './event';
42
+ export * from './event-competition';
43
+ export * from './event-bot-team';
44
+ export * from './event-squad';
45
+ export * from './event-pack';
46
+ export * from './event-pack-player';
47
+ export * from './event-result';
41
48
  export * from './match-preset';
42
49
  export * from './user-currency';
43
50
  export * from './user-player-progress';
@@ -38,6 +38,13 @@ export * from './team';
38
38
  export * from './team-replacement';
39
39
  export * from './tactics';
40
40
  export * from './lineup-preset';
41
+ export * from './event';
42
+ export * from './event-competition';
43
+ export * from './event-bot-team';
44
+ export * from './event-squad';
45
+ export * from './event-pack';
46
+ export * from './event-pack-player';
47
+ export * from './event-result';
41
48
  export * from './match-preset';
42
49
  export * from './user-currency';
43
50
  export * from './user-player-progress';
@@ -9,10 +9,11 @@ export interface LineupPresetAttributes {
9
9
  is_active: boolean;
10
10
  order_index: number;
11
11
  config: ApiTactics;
12
+ event_id: string | null;
12
13
  }
13
14
  export type LineupPresetPk = 'preset_id';
14
15
  export type LineupPresetId = LineupPresetModel[LineupPresetPk];
15
- export type LineupPresetOptionalAttributes = 'preset_id' | 'is_active';
16
+ export type LineupPresetOptionalAttributes = 'preset_id' | 'is_active' | 'event_id';
16
17
  export type LineupPresetCreationAttributes = Optional<LineupPresetAttributes, LineupPresetOptionalAttributes>;
17
18
  export declare class LineupPresetModel extends Model<LineupPresetAttributes, LineupPresetCreationAttributes> implements LineupPresetAttributes {
18
19
  preset_id: string;
@@ -21,6 +22,7 @@ export declare class LineupPresetModel extends Model<LineupPresetAttributes, Lin
21
22
  is_active: boolean;
22
23
  order_index: number;
23
24
  config: ApiTactics;
25
+ event_id: string | null;
24
26
  team: TeamModel;
25
27
  getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
26
28
  setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
@@ -1,3 +1,4 @@
1
+ import * as Sequelize from 'sequelize';
1
2
  import { DataTypes, Model } from 'sequelize';
2
3
  export class LineupPresetModel extends Model {
3
4
  static initModel(sequelize) {
@@ -32,6 +33,15 @@ export class LineupPresetModel extends Model {
32
33
  config: {
33
34
  type: DataTypes.JSONB,
34
35
  allowNull: false
36
+ },
37
+ event_id: {
38
+ type: DataTypes.UUID,
39
+ allowNull: true,
40
+ defaultValue: null,
41
+ references: {
42
+ model: 'Event',
43
+ key: 'event_id'
44
+ }
35
45
  }
36
46
  }, {
37
47
  sequelize,
@@ -60,6 +70,16 @@ export class LineupPresetModel extends Model {
60
70
  { name: 'team_id' }
61
71
  ],
62
72
  where: { is_active: true }
73
+ },
74
+ {
75
+ // At most one event lineup per team per event, so the save endpoint can upsert safely.
76
+ name: 'LineupPreset_one_per_team_event',
77
+ unique: true,
78
+ fields: [
79
+ { name: 'team_id' },
80
+ { name: 'event_id' }
81
+ ],
82
+ where: { event_id: { [Sequelize.Op.ne]: null } }
63
83
  }
64
84
  ]
65
85
  });
@@ -1,3 +1,4 @@
1
1
  export * from './service';
2
2
  export * from './data';
3
3
  export * from './stat-config';
4
+ export { collectTacticsPlayerIds } from './api/tactics-player-refs';
@@ -1,3 +1,7 @@
1
1
  export * from './service';
2
2
  export * from './data';
3
3
  export * from './stat-config';
4
+ // Named rather than `export * from './api'`: the api module re-declares Tactics/Player/Team/Match as DTO
5
+ // shapes, which would collide with the service classes of the same name at the root. Consumers that want the
6
+ // DTO types import them from the `volleyballsimtypes/api` subpath.
7
+ export { collectTacticsPlayerIds } from './api/tactics-player-refs';
@@ -0,0 +1,46 @@
1
+ import { Country } from '../country';
2
+ import { Player, Rarity, Role } from '../player';
3
+ /** A pack targets one role, or ANY for the single free pick at the end. */
4
+ export type DraftRole = Role | 'ANY';
5
+ /** How many players an opened pack offers. */
6
+ export declare const PACK_SIZE = 4;
7
+ /**
8
+ * The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
9
+ * 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
10
+ *
11
+ * Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
12
+ * wrong, so nobody drafts six liberos and then cannot field a legal lineup.
13
+ */
14
+ export declare const DRAFT_PLAN: readonly DraftRole[];
15
+ /** The size of a drafted squad: one pick per pack. */
16
+ export declare const DRAFT_SQUAD_SIZE: number;
17
+ /**
18
+ * How far a pack's players may sit from the median for their rarity, as a fraction of the role score.
19
+ *
20
+ * The whole point of a generated draft is that the material is comparable: without this, one entrant opens a
21
+ * pack of duds while another opens a pack of stars and the event is decided before a ball is served. A player
22
+ * whose role score falls outside the band is rerolled.
23
+ */
24
+ export declare const PACK_QUALITY_TOLERANCE = 0.08;
25
+ export interface PackQualityBand {
26
+ readonly role: Role;
27
+ readonly min: number;
28
+ readonly max: number;
29
+ }
30
+ /**
31
+ * The in-band range for a role at a rarity, measured from a sample of freshly generated players rather than
32
+ * from hardcoded numbers, so it tracks the generator instead of drifting away from it whenever stats are
33
+ * retuned. Deterministic enough in aggregate: the sample is large and the band is a fraction of its median.
34
+ */
35
+ export declare function packQualityBand(country: Country, rarity: Rarity, role: Role, sampleSize?: number): PackQualityBand;
36
+ /**
37
+ * The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity, every one of them inside
38
+ * the fairness band.
39
+ *
40
+ * `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
41
+ * randomness (and a test can make it deterministic).
42
+ */
43
+ export declare function generatePack(country: Country, rarity: Rarity, packRole: DraftRole, band: (role: Role) => PackQualityBand, pickRole: (roles: readonly Role[]) => Role): Player[];
44
+ /** The role of the pack at `index`, or undefined once the draft is complete. */
45
+ export declare function draftPackRole(index: number): DraftRole | undefined;
46
+ export declare function isDraftComplete(picks: number): boolean;
@@ -0,0 +1,78 @@
1
+ import { PlayerGenerator, RoleEnum, calculateRoleScore } from '../player';
2
+ /** How many players an opened pack offers. */
3
+ export const PACK_SIZE = 4;
4
+ /**
5
+ * The pack sequence, in the order they are opened. One pick each, so this is also the squad: 2 setters,
6
+ * 4 outsides, 3 middles, 2 opposites, 2 liberos, plus one free pick, for a squad of 14 like every other team.
7
+ *
8
+ * Role-targeted rather than free choice on purpose: it makes a positionally valid squad impossible to get
9
+ * wrong, so nobody drafts six liberos and then cannot field a legal lineup.
10
+ */
11
+ export const DRAFT_PLAN = [
12
+ RoleEnum.SETTER, RoleEnum.SETTER,
13
+ RoleEnum.OUTSIDE_HITTER, RoleEnum.OUTSIDE_HITTER, RoleEnum.OUTSIDE_HITTER, RoleEnum.OUTSIDE_HITTER,
14
+ RoleEnum.MIDDLE_BLOCKER, RoleEnum.MIDDLE_BLOCKER, RoleEnum.MIDDLE_BLOCKER,
15
+ RoleEnum.OPPOSITE_HITTER, RoleEnum.OPPOSITE_HITTER,
16
+ RoleEnum.LIBERO, RoleEnum.LIBERO,
17
+ 'ANY'
18
+ ];
19
+ /** The size of a drafted squad: one pick per pack. */
20
+ export const DRAFT_SQUAD_SIZE = DRAFT_PLAN.length;
21
+ /** The role a free pack rolls its choices at, so an ANY pack still offers a coherent set. */
22
+ const ANY_PACK_ROLES = [
23
+ RoleEnum.SETTER, RoleEnum.OUTSIDE_HITTER, RoleEnum.MIDDLE_BLOCKER, RoleEnum.OPPOSITE_HITTER, RoleEnum.LIBERO
24
+ ];
25
+ /**
26
+ * How far a pack's players may sit from the median for their rarity, as a fraction of the role score.
27
+ *
28
+ * The whole point of a generated draft is that the material is comparable: without this, one entrant opens a
29
+ * pack of duds while another opens a pack of stars and the event is decided before a ball is served. A player
30
+ * whose role score falls outside the band is rerolled.
31
+ */
32
+ export const PACK_QUALITY_TOLERANCE = 0.08;
33
+ /** Give up rerolling after this many attempts and take what we have, so generation can never spin forever. */
34
+ const MAX_REROLLS = 40;
35
+ /**
36
+ * The in-band range for a role at a rarity, measured from a sample of freshly generated players rather than
37
+ * from hardcoded numbers, so it tracks the generator instead of drifting away from it whenever stats are
38
+ * retuned. Deterministic enough in aggregate: the sample is large and the band is a fraction of its median.
39
+ */
40
+ export function packQualityBand(country, rarity, role, sampleSize = 60) {
41
+ const scores = Array.from({ length: sampleSize }, () => calculateRoleScore(PlayerGenerator.generatePlayer(country, rarity, role, false, 1).stats, role)).sort((a, b) => a - b);
42
+ const median = scores[Math.floor(scores.length / 2)];
43
+ return {
44
+ role,
45
+ min: median * (1 - PACK_QUALITY_TOLERANCE),
46
+ max: median * (1 + PACK_QUALITY_TOLERANCE)
47
+ };
48
+ }
49
+ /**
50
+ * The players a pack offers: `PACK_SIZE` of the pack's role at the event's rarity, every one of them inside
51
+ * the fairness band.
52
+ *
53
+ * `pickRole` decides what an ANY pack rolls; it is passed in rather than chosen here so the caller controls the
54
+ * randomness (and a test can make it deterministic).
55
+ */
56
+ export function generatePack(country, rarity, packRole, band, pickRole) {
57
+ const role = packRole === 'ANY' ? pickRole(ANY_PACK_ROLES) : packRole;
58
+ const { min, max } = band(role);
59
+ const players = [];
60
+ for (let i = 0; i < PACK_SIZE; i++) {
61
+ let player = PlayerGenerator.generatePlayer(country, rarity, role, false, 1);
62
+ for (let attempt = 0; attempt < MAX_REROLLS; attempt++) {
63
+ const score = calculateRoleScore(player.stats, role);
64
+ if (score >= min && score <= max)
65
+ break;
66
+ player = PlayerGenerator.generatePlayer(country, rarity, role, false, 1);
67
+ }
68
+ players.push(player);
69
+ }
70
+ return players;
71
+ }
72
+ /** The role of the pack at `index`, or undefined once the draft is complete. */
73
+ export function draftPackRole(index) {
74
+ return DRAFT_PLAN[index];
75
+ }
76
+ export function isDraftComplete(picks) {
77
+ return picks >= DRAFT_SQUAD_SIZE;
78
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { DRAFT_PLAN, DRAFT_SQUAD_SIZE, PACK_SIZE, draftPackRole, generatePack, isDraftComplete, packQualityBand } from './event-draft';
3
+ import { RarityEnum, RoleEnum, calculateRoleScore } from '../player';
4
+ import { makeCountry } from '../test-helpers';
5
+ const COUNTRY = makeCountry();
6
+ describe('DRAFT_PLAN', () => {
7
+ it('drafts a full 14-player squad, one pick per pack', () => {
8
+ expect(DRAFT_SQUAD_SIZE).toBe(14);
9
+ expect(DRAFT_PLAN).toHaveLength(14);
10
+ });
11
+ it('guarantees a positionally valid squad', () => {
12
+ const counts = DRAFT_PLAN.reduce((acc, role) => {
13
+ acc[role] = (acc[role] ?? 0) + 1;
14
+ return acc;
15
+ }, {});
16
+ expect(counts[RoleEnum.SETTER]).toBe(2);
17
+ expect(counts[RoleEnum.OUTSIDE_HITTER]).toBe(4);
18
+ expect(counts[RoleEnum.MIDDLE_BLOCKER]).toBe(3);
19
+ expect(counts[RoleEnum.OPPOSITE_HITTER]).toBe(2);
20
+ expect(counts[RoleEnum.LIBERO]).toBe(2);
21
+ expect(counts.ANY).toBe(1);
22
+ // Enough for a legal starting six plus a libero, whatever the free pack is spent on: a 5-1 needs a setter,
23
+ // two middles, two outsides and an opposite on court, and a libero off it.
24
+ expect(counts[RoleEnum.SETTER]).toBeGreaterThanOrEqual(1);
25
+ expect(counts[RoleEnum.MIDDLE_BLOCKER]).toBeGreaterThanOrEqual(2);
26
+ expect(counts[RoleEnum.OUTSIDE_HITTER]).toBeGreaterThanOrEqual(2);
27
+ expect(counts[RoleEnum.OPPOSITE_HITTER]).toBeGreaterThanOrEqual(1);
28
+ expect(counts[RoleEnum.LIBERO]).toBeGreaterThanOrEqual(1);
29
+ });
30
+ it('walks pack by pack and then reports the draft complete', () => {
31
+ for (let i = 0; i < DRAFT_SQUAD_SIZE; i++) {
32
+ expect(draftPackRole(i)).toBeDefined();
33
+ expect(isDraftComplete(i)).toBe(false);
34
+ }
35
+ expect(draftPackRole(DRAFT_SQUAD_SIZE)).toBeUndefined();
36
+ expect(isDraftComplete(DRAFT_SQUAD_SIZE)).toBe(true);
37
+ });
38
+ });
39
+ describe('generatePack()', () => {
40
+ const first = (roles) => roles[0];
41
+ it.each([RarityEnum.COMMON, RarityEnum.RARE, RarityEnum.LEGENDARY])('offers %s players of the pack role, all inside the fairness band', (rarity) => {
42
+ const role = RoleEnum.OUTSIDE_HITTER;
43
+ const band = packQualityBand(COUNTRY, rarity, role);
44
+ const pack = generatePack(COUNTRY, rarity, role, () => band, first);
45
+ expect(pack).toHaveLength(PACK_SIZE);
46
+ for (const player of pack) {
47
+ expect(player.rarity).toBe(rarity);
48
+ expect(player.roles).toContain(role);
49
+ const score = calculateRoleScore(player.stats, role);
50
+ expect(score).toBeGreaterThanOrEqual(band.min);
51
+ expect(score).toBeLessThanOrEqual(band.max);
52
+ }
53
+ });
54
+ it('keeps two packs of the same role comparable', () => {
55
+ const role = RoleEnum.MIDDLE_BLOCKER;
56
+ const band = packQualityBand(COUNTRY, RarityEnum.RARE, role);
57
+ const score = (p) => calculateRoleScore(p.stats, role);
58
+ const a = generatePack(COUNTRY, RarityEnum.RARE, role, () => band, first).map(score);
59
+ const b = generatePack(COUNTRY, RarityEnum.RARE, role, () => band, first).map(score);
60
+ // The point of the band: the best pack cannot run away from the worst one.
61
+ const spread = Math.max(...a, ...b) - Math.min(...a, ...b);
62
+ expect(spread).toBeLessThanOrEqual(band.max - band.min);
63
+ });
64
+ it('rolls a free pack at the role it is handed', () => {
65
+ const band = packQualityBand(COUNTRY, RarityEnum.COMMON, RoleEnum.LIBERO);
66
+ const pack = generatePack(COUNTRY, RarityEnum.COMMON, 'ANY', () => band, () => RoleEnum.LIBERO);
67
+ expect(pack).toHaveLength(PACK_SIZE);
68
+ for (const player of pack)
69
+ expect(player.roles).toContain(RoleEnum.LIBERO);
70
+ });
71
+ it('terminates even when nothing can land in band', () => {
72
+ const impossible = { role: RoleEnum.SETTER, min: Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER };
73
+ const pack = generatePack(COUNTRY, RarityEnum.COMMON, RoleEnum.SETTER, () => impossible, first);
74
+ // Generation gives up rather than spinning: a full pack is still returned.
75
+ expect(pack).toHaveLength(PACK_SIZE);
76
+ });
77
+ });
78
+ describe('packQualityBand()', () => {
79
+ it('brackets the median of what the generator actually produces', () => {
80
+ const band = packQualityBand(COUNTRY, RarityEnum.LEGENDARY, RoleEnum.SETTER);
81
+ expect(band.min).toBeLessThan(band.max);
82
+ expect(band.min).toBeGreaterThan(0);
83
+ });
84
+ it('scales with rarity, so a legendary band sits above a common one', () => {
85
+ const common = packQualityBand(COUNTRY, RarityEnum.COMMON, RoleEnum.OUTSIDE_HITTER);
86
+ const legendary = packQualityBand(COUNTRY, RarityEnum.LEGENDARY, RoleEnum.OUTSIDE_HITTER);
87
+ expect(legendary.min).toBeGreaterThan(common.max);
88
+ });
89
+ });
@@ -0,0 +1,26 @@
1
+ export type EventRewardBand = 'PARTICIPATION' | 'TOP_20' | 'TOP_10' | 'TOP_5' | 'TOP_3' | 'TOP_1';
2
+ export interface EventRewardRow {
3
+ readonly band: EventRewardBand;
4
+ /** Best final position that earns this row; PARTICIPATION has none. */
5
+ readonly maxPosition: number | null;
6
+ /** Event Packs awarded by this row alone. */
7
+ readonly packs: number;
8
+ /** Coins awarded by this row alone. */
9
+ readonly coins: number;
10
+ }
11
+ /**
12
+ * Best band first. A team earns its own row and every row BELOW it, which is what "cumulative" means: first
13
+ * place collects all six.
14
+ */
15
+ export declare const EVENT_REWARDS: readonly EventRewardRow[];
16
+ /**
17
+ * The band a finish falls into. `position` is the place in the FINAL table; null means the team never reached
18
+ * the final, and so does any position past the last banded row, which both count as taking part.
19
+ */
20
+ export declare function eventRewardBandFor(position: number | null): EventRewardBand;
21
+ export interface EventPayout {
22
+ readonly packs: number;
23
+ readonly coins: number;
24
+ }
25
+ /** Everything a finish earns, adding its own row to every row beneath it. */
26
+ export declare function eventPayoutFor(position: number | null): EventPayout;
@@ -0,0 +1,44 @@
1
+ // What an event pays out, and the band a team's finish falls into. Owner-defined 2026-07-23.
2
+ //
3
+ // One source of truth for three consumers: the simulator grants from it, the About view lists it, and the
4
+ // accolades page names the band. Rewards are CUMULATIVE, so a team collects every row from Participation down
5
+ // to its own placement.
6
+ /**
7
+ * Best band first. A team earns its own row and every row BELOW it, which is what "cumulative" means: first
8
+ * place collects all six.
9
+ */
10
+ export const EVENT_REWARDS = [
11
+ { band: 'TOP_1', maxPosition: 1, packs: 1, coins: 0 },
12
+ { band: 'TOP_3', maxPosition: 3, packs: 0, coins: 500 },
13
+ { band: 'TOP_5', maxPosition: 5, packs: 0, coins: 1000 },
14
+ { band: 'TOP_10', maxPosition: 10, packs: 1, coins: 0 },
15
+ { band: 'TOP_20', maxPosition: 20, packs: 0, coins: 1000 },
16
+ { band: 'PARTICIPATION', maxPosition: null, packs: 1, coins: 1000 }
17
+ ];
18
+ /**
19
+ * The band a finish falls into. `position` is the place in the FINAL table; null means the team never reached
20
+ * the final, and so does any position past the last banded row, which both count as taking part.
21
+ */
22
+ export function eventRewardBandFor(position) {
23
+ if (position == null)
24
+ return 'PARTICIPATION';
25
+ // Rows are ordered best-first, so the FIRST row the position satisfies is the narrowest band it earns.
26
+ // Scanning to the end instead would always land on the widest, handing first place a "top 20" badge.
27
+ for (const row of EVENT_REWARDS) {
28
+ if (row.maxPosition != null && position <= row.maxPosition)
29
+ return row.band;
30
+ }
31
+ return 'PARTICIPATION';
32
+ }
33
+ /** Everything a finish earns, adding its own row to every row beneath it. */
34
+ export function eventPayoutFor(position) {
35
+ const band = eventRewardBandFor(position);
36
+ const from = EVENT_REWARDS.findIndex(row => row.band === band);
37
+ let packs = 0;
38
+ let coins = 0;
39
+ for (const row of EVENT_REWARDS.slice(from)) {
40
+ packs += row.packs;
41
+ coins += row.coins;
42
+ }
43
+ return { packs, coins };
44
+ }