volleyballsimtypes 0.0.525 → 0.0.527

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 (67) hide show
  1. package/dist/cjs/src/api/index.d.ts +2 -2
  2. package/dist/cjs/src/data/init-models.js +4 -22
  3. package/dist/cjs/src/data/models/national-team-claim.d.ts +5 -5
  4. package/dist/cjs/src/data/models/national-team-claim.js +8 -0
  5. package/dist/cjs/src/data/models/national-team.d.ts +1 -7
  6. package/dist/cjs/src/data/models/team-jersey.d.ts +3 -1
  7. package/dist/cjs/src/data/models/team-jersey.js +4 -0
  8. package/dist/cjs/src/data/models/wnl-callup.d.ts +0 -10
  9. package/dist/cjs/src/data/models/wnl-season.d.ts +0 -7
  10. package/dist/cjs/src/data/models/wnl-squad.d.ts +0 -13
  11. package/dist/cjs/src/data/transformers/team.js +2 -1
  12. package/dist/cjs/src/service/notification/catalog/en.json +13 -0
  13. package/dist/cjs/src/service/notification/catalog/es.json +13 -0
  14. package/dist/cjs/src/service/notification/catalog/fr.json +13 -0
  15. package/dist/cjs/src/service/notification/catalog/it.json +13 -0
  16. package/dist/cjs/src/service/notification/catalog/pl.json +13 -0
  17. package/dist/cjs/src/service/notification/catalog/pt-BR.json +13 -0
  18. package/dist/cjs/src/service/notification/catalog/tr.json +13 -0
  19. package/dist/cjs/src/service/notification/notification-type.d.ts +1 -1
  20. package/dist/cjs/src/service/notification/notification-type.js +4 -1
  21. package/dist/cjs/src/service/notification/payloads.d.ts +7 -1
  22. package/dist/cjs/src/service/notification/registry.d.ts +1 -2
  23. package/dist/cjs/src/service/notification/registry.js +52 -25
  24. package/dist/cjs/src/service/team/index.d.ts +1 -0
  25. package/dist/cjs/src/service/team/index.js +1 -0
  26. package/dist/cjs/src/service/team/jersey-defaults.d.ts +13 -0
  27. package/dist/cjs/src/service/team/jersey-defaults.js +79 -0
  28. package/dist/cjs/src/service/team/jersey.d.ts +29 -0
  29. package/dist/cjs/src/service/team/jersey.js +125 -1
  30. package/dist/cjs/src/service/team/schemas/team.z.d.ts +24 -0
  31. package/dist/cjs/src/service/team/schemas/team.z.js +2 -1
  32. package/dist/cjs/src/service/team/team.d.ts +2 -1
  33. package/dist/cjs/src/service/team/team.js +2 -1
  34. package/dist/esm/src/api/index.d.ts +2 -2
  35. package/dist/esm/src/data/init-models.js +4 -22
  36. package/dist/esm/src/data/models/national-team-claim.d.ts +5 -5
  37. package/dist/esm/src/data/models/national-team-claim.js +8 -0
  38. package/dist/esm/src/data/models/national-team.d.ts +1 -7
  39. package/dist/esm/src/data/models/team-jersey.d.ts +3 -1
  40. package/dist/esm/src/data/models/team-jersey.js +4 -0
  41. package/dist/esm/src/data/models/wnl-callup.d.ts +0 -10
  42. package/dist/esm/src/data/models/wnl-season.d.ts +0 -7
  43. package/dist/esm/src/data/models/wnl-squad.d.ts +0 -13
  44. package/dist/esm/src/data/transformers/team.js +2 -1
  45. package/dist/esm/src/service/notification/catalog/en.json +13 -0
  46. package/dist/esm/src/service/notification/catalog/es.json +13 -0
  47. package/dist/esm/src/service/notification/catalog/fr.json +13 -0
  48. package/dist/esm/src/service/notification/catalog/it.json +13 -0
  49. package/dist/esm/src/service/notification/catalog/pl.json +13 -0
  50. package/dist/esm/src/service/notification/catalog/pt-BR.json +13 -0
  51. package/dist/esm/src/service/notification/catalog/tr.json +13 -0
  52. package/dist/esm/src/service/notification/notification-type.d.ts +1 -1
  53. package/dist/esm/src/service/notification/notification-type.js +4 -1
  54. package/dist/esm/src/service/notification/payloads.d.ts +7 -1
  55. package/dist/esm/src/service/notification/registry.d.ts +1 -2
  56. package/dist/esm/src/service/notification/registry.js +51 -24
  57. package/dist/esm/src/service/team/index.d.ts +1 -0
  58. package/dist/esm/src/service/team/index.js +1 -0
  59. package/dist/esm/src/service/team/jersey-defaults.d.ts +13 -0
  60. package/dist/esm/src/service/team/jersey-defaults.js +75 -0
  61. package/dist/esm/src/service/team/jersey.d.ts +29 -0
  62. package/dist/esm/src/service/team/jersey.js +120 -0
  63. package/dist/esm/src/service/team/schemas/team.z.d.ts +24 -0
  64. package/dist/esm/src/service/team/schemas/team.z.js +3 -2
  65. package/dist/esm/src/service/team/team.d.ts +2 -1
  66. package/dist/esm/src/service/team/team.js +2 -1
  67. package/package.json +1 -1
@@ -1,12 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PUSH_PREF_KEYS = exports.PUSH_NOTIFICATION_TYPES = exports.NOTIFICATION_REGISTRY = void 0;
3
+ exports.PUSH_PREF_KEYS = exports.PUSH_NOTIFICATION_TYPES = exports.NOTIFICATION_REGISTRY = exports.NATIONAL_ACCESS_PREF_KEY = void 0;
4
4
  exports.pushPrefKeyFor = pushPrefKeyFor;
5
5
  exports.pushEnabledForType = pushEnabledForType;
6
6
  const notification_type_1 = require("./notification-type");
7
7
  function readString(value) {
8
8
  return typeof value === 'string' && value !== '' ? value : null;
9
9
  }
10
+ // The preference key the three national-team access types share. Deliberately NOT the 'WNL' group: muting routine
11
+ // call-up chatter must never also mute "you are about to lose your national team" (owner 2026-08-29).
12
+ exports.NATIONAL_ACCESS_PREF_KEY = 'NATIONAL_ACCESS';
13
+ // Whole days from now until an ISO instant, floored at 0. NATIONAL_ACCESS_EXPIRING stores the ABSOLUTE deadline
14
+ // rather than a day count because the in-app bell renders a stored payload at READ time: a stored "3" would still
15
+ // read "3 days" two days later. Push resolves the number here at SEND time and the bell resolves it again at render,
16
+ // so neither surface can go stale.
17
+ function daysUntilIso(value) {
18
+ const iso = readString(value);
19
+ if (iso == null)
20
+ return 0;
21
+ const ms = new Date(iso).getTime() - Date.now();
22
+ if (!Number.isFinite(ms) || ms <= 0)
23
+ return 0;
24
+ return Math.ceil(ms / 86400000);
25
+ }
10
26
  function teamPath(payload) {
11
27
  const teamId = readString(payload.teamId);
12
28
  return teamId != null ? `/team/${encodeURIComponent(teamId)}` : '/';
@@ -42,7 +58,6 @@ exports.NOTIFICATION_REGISTRY = {
42
58
  return `matchResult.${outcome}${named}`;
43
59
  },
44
60
  params: (p) => ({ opponent: matchOpponent(p) ?? '' }),
45
- richRender: 'MATCH_RESULT',
46
61
  catalogKeys: ['matchResult.title', 'matchResult.wonNamed', 'matchResult.wonUnnamed', 'matchResult.lostNamed', 'matchResult.lostUnnamed']
47
62
  },
48
63
  INJURY: {
@@ -50,7 +65,6 @@ exports.NOTIFICATION_REGISTRY = {
50
65
  deepLink: () => '/infirmary',
51
66
  pushTitleKey: 'injury.title',
52
67
  pushBodyKey: 'injury.body',
53
- inAppMessageKey: 'injury.body',
54
68
  params: (p) => ({ name: injuredName(p) }),
55
69
  catalogKeys: ['injury.title', 'injury.body']
56
70
  },
@@ -59,7 +73,6 @@ exports.NOTIFICATION_REGISTRY = {
59
73
  deepLink: teamPath,
60
74
  pushTitleKey: 'injuryRecovered.title',
61
75
  pushBodyKey: 'injuryRecovered.body',
62
- inAppMessageKey: 'injuryRecovered.body',
63
76
  params: (p) => ({ name: injuredName(p) }),
64
77
  catalogKeys: ['injuryRecovered.title', 'injuryRecovered.body']
65
78
  },
@@ -71,7 +84,6 @@ exports.NOTIFICATION_REGISTRY = {
71
84
  },
72
85
  pushTitleKey: 'seasonEnd.title',
73
86
  pushBodyKey: 'seasonEnd.body',
74
- inAppMessageKey: 'seasonEnd.body',
75
87
  catalogKeys: ['seasonEnd.title', 'seasonEnd.body']
76
88
  },
77
89
  GRANT_RECEIVED: {
@@ -79,7 +91,6 @@ exports.NOTIFICATION_REGISTRY = {
79
91
  deepLink: () => '/recruit',
80
92
  pushTitleKey: 'grant.title',
81
93
  pushBodyKey: 'grant.body',
82
- inAppMessageKey: 'grant.body',
83
94
  catalogKeys: ['grant.title', 'grant.body']
84
95
  },
85
96
  PROMOTION: {
@@ -87,7 +98,6 @@ exports.NOTIFICATION_REGISTRY = {
87
98
  deepLink: teamPath,
88
99
  pushTitleKey: 'promotion.title',
89
100
  pushBodyKey: 'promotion.body',
90
- inAppMessageKey: 'promotion.body',
91
101
  catalogKeys: ['promotion.title', 'promotion.body']
92
102
  },
93
103
  // In-game events. All five deep-link to the Events section, which is where every phase of an event lives.
@@ -98,7 +108,6 @@ exports.NOTIFICATION_REGISTRY = {
98
108
  deepLink: () => '/events',
99
109
  pushTitleKey: 'eventRegistrationOpen.title',
100
110
  pushBodyKey: 'eventRegistrationOpen.body',
101
- inAppMessageKey: 'eventRegistrationOpen.body',
102
111
  catalogKeys: ['eventRegistrationOpen.title', 'eventRegistrationOpen.body']
103
112
  },
104
113
  EVENT_REGISTRATION_CLOSING: {
@@ -106,7 +115,6 @@ exports.NOTIFICATION_REGISTRY = {
106
115
  deepLink: () => '/events',
107
116
  pushTitleKey: 'eventRegistrationClosing.title',
108
117
  pushBodyKey: 'eventRegistrationClosing.body',
109
- inAppMessageKey: 'eventRegistrationClosing.body',
110
118
  catalogKeys: ['eventRegistrationClosing.title', 'eventRegistrationClosing.body']
111
119
  },
112
120
  EVENT_STARTED: {
@@ -114,7 +122,6 @@ exports.NOTIFICATION_REGISTRY = {
114
122
  deepLink: () => '/events',
115
123
  pushTitleKey: 'eventStarted.title',
116
124
  pushBodyKey: 'eventStarted.body',
117
- inAppMessageKey: 'eventStarted.body',
118
125
  catalogKeys: ['eventStarted.title', 'eventStarted.body']
119
126
  },
120
127
  EVENT_FINAL_ROUND: {
@@ -122,7 +129,6 @@ exports.NOTIFICATION_REGISTRY = {
122
129
  deepLink: () => '/events',
123
130
  pushTitleKey: 'eventFinalRound.title',
124
131
  pushBodyKey: 'eventFinalRound.body',
125
- inAppMessageKey: 'eventFinalRound.body',
126
132
  catalogKeys: ['eventFinalRound.title', 'eventFinalRound.body']
127
133
  },
128
134
  EVENT_FINISHED: {
@@ -133,9 +139,6 @@ exports.NOTIFICATION_REGISTRY = {
133
139
  pushBodyKey: (p) => readString(p.position) != null || typeof p.position === 'number'
134
140
  ? 'eventFinished.placed'
135
141
  : 'eventFinished.body',
136
- inAppMessageKey: (p) => readString(p.position) != null || typeof p.position === 'number'
137
- ? 'eventFinished.placed'
138
- : 'eventFinished.body',
139
142
  params: (p) => ({
140
143
  position: typeof p.position === 'number' ? String(p.position) : (readString(p.position) ?? ''),
141
144
  entrants: typeof p.entrants === 'number' ? String(p.entrants) : (readString(p.entrants) ?? '')
@@ -148,7 +151,6 @@ exports.NOTIFICATION_REGISTRY = {
148
151
  deepLink: () => '/daily-login',
149
152
  pushTitleKey: 'dailyLogin.title',
150
153
  pushBodyKey: 'dailyLogin.body',
151
- inAppMessageKey: 'dailyLogin.body',
152
154
  catalogKeys: ['dailyLogin.title', 'dailyLogin.body']
153
155
  },
154
156
  WELCOME: {
@@ -156,13 +158,11 @@ exports.NOTIFICATION_REGISTRY = {
156
158
  deepLink: () => '/recruit',
157
159
  pushTitleKey: 'welcome.title',
158
160
  pushBodyKey: 'welcome.body',
159
- richRender: 'WELCOME',
160
161
  catalogKeys: ['welcome.title', 'welcome.body']
161
162
  },
162
163
  // In-app only (no push); the UI keeps a bespoke renderer (amount formatting) and its own `compensation` label.
163
164
  COMPENSATION: {
164
165
  emitsPush: false,
165
- richRender: 'COMPENSATION',
166
166
  catalogKeys: []
167
167
  },
168
168
  // One-off relocation warning: sent to users whose team is being moved to Kyorin. Informational; opens their team.
@@ -171,7 +171,6 @@ exports.NOTIFICATION_REGISTRY = {
171
171
  deepLink: teamPath,
172
172
  pushTitleKey: 'regionRelocation.title',
173
173
  pushBodyKey: 'regionRelocation.body',
174
- inAppMessageKey: 'regionRelocation.body',
175
174
  catalogKeys: ['regionRelocation.title', 'regionRelocation.body']
176
175
  },
177
176
  // World Nations League (owner 2026-08-28). All seven share the single 'WNL' preference toggle. The three callup
@@ -183,7 +182,6 @@ exports.NOTIFICATION_REGISTRY = {
183
182
  deepLink: () => '/wnl/callup',
184
183
  pushTitleKey: 'wnlCallupOpen.title',
185
184
  pushBodyKey: 'wnlCallupOpen.body',
186
- inAppMessageKey: 'wnlCallupOpen.body',
187
185
  // The deadline arrives pre-formatted (locale-neutral 'YYYY-MM-DD HH:mm UTC') because the stored payload is
188
186
  // shared by the push and the in-app bell and cannot be re-localised per render.
189
187
  params: (p) => ({ date: readString(p.deadline) ?? '' }),
@@ -195,7 +193,6 @@ exports.NOTIFICATION_REGISTRY = {
195
193
  deepLink: () => '/wnl/callup',
196
194
  pushTitleKey: 'wnlCallupDeadline.title',
197
195
  pushBodyKey: 'wnlCallupDeadline.body',
198
- inAppMessageKey: 'wnlCallupDeadline.body',
199
196
  catalogKeys: ['wnlCallupDeadline.title', 'wnlCallupDeadline.body']
200
197
  },
201
198
  WNL_CALLUP_CLOSED: {
@@ -204,7 +201,6 @@ exports.NOTIFICATION_REGISTRY = {
204
201
  deepLink: () => '/wnl/manage/lineup',
205
202
  pushTitleKey: 'wnlCallupClosed.title',
206
203
  pushBodyKey: 'wnlCallupClosed.body',
207
- inAppMessageKey: 'wnlCallupClosed.body',
208
204
  catalogKeys: ['wnlCallupClosed.title', 'wnlCallupClosed.body']
209
205
  },
210
206
  WNL_LEAGUE_START: {
@@ -213,7 +209,6 @@ exports.NOTIFICATION_REGISTRY = {
213
209
  deepLink: () => '/wnl',
214
210
  pushTitleKey: 'wnlLeagueStart.title',
215
211
  pushBodyKey: 'wnlLeagueStart.body',
216
- inAppMessageKey: 'wnlLeagueStart.body',
217
212
  catalogKeys: ['wnlLeagueStart.title', 'wnlLeagueStart.body']
218
213
  },
219
214
  WNL_FINALS_START: {
@@ -222,7 +217,6 @@ exports.NOTIFICATION_REGISTRY = {
222
217
  deepLink: () => '/wnl/finals',
223
218
  pushTitleKey: 'wnlFinalsStart.title',
224
219
  pushBodyKey: 'wnlFinalsStart.body',
225
- inAppMessageKey: 'wnlFinalsStart.body',
226
220
  catalogKeys: ['wnlFinalsStart.title', 'wnlFinalsStart.body']
227
221
  },
228
222
  WNL_CHAMPION: {
@@ -232,7 +226,6 @@ exports.NOTIFICATION_REGISTRY = {
232
226
  pushTitleKey: 'wnlChampion.title',
233
227
  // A missing team name never renders a subjectless sentence (mirrors the MATCH_RESULT named/unnamed split).
234
228
  pushBodyKey: (p) => readString(p.team) != null ? 'wnlChampion.body' : 'wnlChampion.bodyUnnamed',
235
- inAppMessageKey: (p) => readString(p.team) != null ? 'wnlChampion.body' : 'wnlChampion.bodyUnnamed',
236
229
  params: (p) => ({ team: readString(p.team) ?? '' }),
237
230
  catalogKeys: ['wnlChampion.title', 'wnlChampion.body', 'wnlChampion.bodyUnnamed']
238
231
  },
@@ -242,8 +235,42 @@ exports.NOTIFICATION_REGISTRY = {
242
235
  deepLink: () => '/wnl/teams',
243
236
  pushTitleKey: 'wnlPlayerCalledUp.title',
244
237
  pushBodyKey: 'wnlPlayerCalledUp.body',
245
- inAppMessageKey: 'wnlPlayerCalledUp.body',
246
238
  catalogKeys: ['wnlPlayerCalledUp.title', 'wnlPlayerCalledUp.body']
239
+ },
240
+ // National-team ACCESS lifecycle (owner 2026-08-29): the free Tier-2 trial is granted, is about to end, and the
241
+ // team is released. These share their OWN toggle rather than the 'WNL' group so that muting call-up chatter
242
+ // cannot silence the warning that a team is about to be lost. They are also intentionally not named WNL_*: the
243
+ // completeness test asserts every WNL_-prefixed type resolves to the 'WNL' key.
244
+ NATIONAL_TRIAL_GRANTED: {
245
+ emitsPush: true,
246
+ prefKey: exports.NATIONAL_ACCESS_PREF_KEY,
247
+ deepLink: () => '/wnl/claim',
248
+ pushTitleKey: 'nationalTrialGranted.title',
249
+ pushBodyKey: 'nationalTrialGranted.body',
250
+ // The end date arrives pre-formatted (locale-neutral 'YYYY-MM-DD'), same reason as wnlCallupOpen: one stored
251
+ // payload serves both the push and the bell and cannot be re-localised per render.
252
+ params: (p) => ({ date: readString(p.endsOn) ?? '' }),
253
+ catalogKeys: ['nationalTrialGranted.title', 'nationalTrialGranted.body']
254
+ },
255
+ NATIONAL_ACCESS_EXPIRING: {
256
+ emitsPush: true,
257
+ prefKey: exports.NATIONAL_ACCESS_PREF_KEY,
258
+ // Settings is where the Patreon card lives, so the action the copy asks for is one tap away.
259
+ deepLink: () => '/settings',
260
+ pushTitleKey: 'nationalAccessExpiring.title',
261
+ // The catalog has no pluralisation (interpolate is a bare {{param}} replace), so the final day is its own key
262
+ // instead of rendering "1 days" (mirrors the wnlChampion named/unnamed split).
263
+ pushBodyKey: (p) => daysUntilIso(p.expiresAt) <= 1 ? 'nationalAccessExpiring.bodyLastDay' : 'nationalAccessExpiring.bodyDays',
264
+ params: (p) => ({ days: String(daysUntilIso(p.expiresAt)) }),
265
+ catalogKeys: ['nationalAccessExpiring.title', 'nationalAccessExpiring.bodyDays', 'nationalAccessExpiring.bodyLastDay']
266
+ },
267
+ NATIONAL_TEAM_REVOKED: {
268
+ emitsPush: true,
269
+ prefKey: exports.NATIONAL_ACCESS_PREF_KEY,
270
+ deepLink: () => '/wnl/teams',
271
+ pushTitleKey: 'nationalTeamRevoked.title',
272
+ pushBodyKey: 'nationalTeamRevoked.body',
273
+ catalogKeys: ['nationalTeamRevoked.title', 'nationalTeamRevoked.body']
247
274
  }
248
275
  };
249
276
  // The notification types that emit a mobile push (everything except COMPENSATION). Source of truth = each
@@ -1,5 +1,6 @@
1
1
  export * from './team';
2
2
  export * from './jersey';
3
+ export * from './jersey-defaults';
3
4
  export * from './team-generator';
4
5
  export * from './formation';
5
6
  export * from './tactics';
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./team"), exports);
18
18
  __exportStar(require("./jersey"), exports);
19
+ __exportStar(require("./jersey-defaults"), exports);
19
20
  __exportStar(require("./team-generator"), exports);
20
21
  __exportStar(require("./formation"), exports);
21
22
  __exportStar(require("./tactics"), exports);
@@ -0,0 +1,13 @@
1
+ import type { JerseyConfig, JerseyClothing, JerseyPattern, JerseyFont } from './jersey';
2
+ export interface JerseyPalette {
3
+ color1: string;
4
+ color2: string;
5
+ color3: string;
6
+ number1: string;
7
+ number2: string;
8
+ }
9
+ export declare const JERSEY_PALETTES: readonly JerseyPalette[];
10
+ export declare const JERSEY_PATTERN_ORDER: readonly JerseyPattern[];
11
+ export declare const JERSEY_CLOTHING_ORDER: readonly JerseyClothing[];
12
+ export declare const JERSEY_FONT_ORDER: readonly JerseyFont[];
13
+ export declare function jerseyFromTeamId(teamId: string): JerseyConfig;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JERSEY_FONT_ORDER = exports.JERSEY_CLOTHING_ORDER = exports.JERSEY_PATTERN_ORDER = exports.JERSEY_PALETTES = void 0;
4
+ exports.jerseyFromTeamId = jerseyFromTeamId;
5
+ // FNV-1a hash of a string, used to seed the PRNG below from a team id.
6
+ function hashSeed(str) {
7
+ let h = 2166136261;
8
+ for (let i = 0; i < str.length; i++) {
9
+ h ^= str.charCodeAt(i);
10
+ h = Math.imul(h, 16777619);
11
+ }
12
+ return h >>> 0;
13
+ }
14
+ // mulberry32: a tiny deterministic PRNG. Same seed -> same stream, so a team's kit is stable and reproducible.
15
+ function mulberry32(seed) {
16
+ let a = seed;
17
+ return () => {
18
+ a = (a + 0x6d2b79f5) | 0;
19
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
20
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
21
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
22
+ };
23
+ }
24
+ exports.JERSEY_PALETTES = [
25
+ { color1: '#606c38', color2: '#dda15e', color3: '#bc6c25', number1: '#fefae0', number2: '#283618' },
26
+ { color1: '#780000', color2: '#669bbc', color3: '#c1121f', number1: '#fdf0d5', number2: '#003049' },
27
+ { color1: '#2a9d8f', color2: '#f4a261', color3: '#e76f51', number1: '#264653', number2: '#e9c46a' },
28
+ { color1: '#219ebc', color2: '#fb8500', color3: '#8ecae6', number1: '#023047', number2: '#ffb703' },
29
+ { color1: '#1d3557', color2: '#e63946', color3: '#a8dadc', number1: '#f1faee', number2: '#457b9d' },
30
+ { color1: '#003566', color2: '#ffc300', color3: '#001d3d', number1: '#ffd60a', number2: '#000814' },
31
+ { color1: '#bc4749', color2: '#6a994e', color3: '#a7c957', number1: '#f2e8cf', number2: '#386641' },
32
+ { color1: '#6f1d1b', color2: '#99582a', color3: '#bb9457', number1: '#ffe6a7', number2: '#432818' },
33
+ { color1: '#ef476f', color2: '#118ab2', color3: '#ffd166', number1: '#ffd166', number2: '#073b4c' },
34
+ { color1: '#16425b', color2: '#3a7ca5', color3: '#81c3d7', number1: '#d9dcd6', number2: '#2f6690' },
35
+ { color1: '#ee6c4d', color2: '#3d5a80', color3: '#98c1d9', number1: '#e0fbfc', number2: '#293241' },
36
+ { color1: '#e09f3e', color2: '#335c67', color3: '#9e2a2b', number1: '#fff3b0', number2: '#540b0e' },
37
+ { color1: '#9a031e', color2: '#fb8b24', color3: '#e36414', number1: '#0f4c5c', number2: '#5f0f40' },
38
+ { color1: '#127475', color2: '#f2542d', color3: '#0e9594', number1: '#f5dfbb', number2: '#562c2c' },
39
+ { color1: '#bc4b51', color2: '#8cb369', color3: '#f4a259', number1: '#f4e285', number2: '#5b8e7d' },
40
+ { color1: '#7678ed', color2: '#f18701', color3: '#f35b04', number1: '#f7b801', number2: '#3d348b' },
41
+ { color1: '#14213d', color2: '#fca311', color3: '#e5e5e5', number1: '#ffffff', number2: '#000000' },
42
+ { color1: '#e07a5f', color2: '#81b29a', color3: '#f2cc8f', number1: '#f4f1de', number2: '#3d405b' },
43
+ { color1: '#f79824', color2: '#33a1fd', color3: '#fdca40', number1: '#33a1fd', number2: '#31393c' },
44
+ { color1: '#619b8a', color2: '#fe7f2d', color3: '#a1c181', number1: '#fcca46', number2: '#233d4d' },
45
+ { color1: '#227c9d', color2: '#17c3b2', color3: '#ffcb77', number1: '#fef9ef', number2: '#fe6d73' },
46
+ { color1: '#19647e', color2: '#119da4', color3: '#1f2041', number1: '#4b3f72', number2: '#ffc857' }
47
+ ];
48
+ exports.JERSEY_PATTERN_ORDER = [
49
+ 'solid', 'verticalStripes', 'verticalBars', 'pinstripes', 'hoops', 'checkered', 'diagonalSash', 'splitHalves',
50
+ 'chevron', 'shoulderYoke', 'sidePanels', 'centerBand', 'gradientFade', 'twoToneSplit'
51
+ ];
52
+ exports.JERSEY_CLOTHING_ORDER = [
53
+ 'shirtCrewNeck', 'shirtCrewNeckSleeveless', 'shirtVNeck', 'shirtVNeckSleeveless', 'collarAndSweater', 'collarAndSweaterSleeveless'
54
+ ];
55
+ // Free-user fonts: the id-derived random for a team's default kit picks ONLY from these (premium fonts are vector
56
+ // glyph paths that require the UI rendering pipeline).
57
+ exports.JERSEY_FONT_ORDER = ['sans', 'wide', 'mono', 'serif', 'block'];
58
+ // The full jersey a team wears by default: every category is drawn deterministically from the team id, so each team
59
+ // gets its own stable, distinct kit. Colours come from ONE randomly-assigned JERSEY_PALETTE (harmonious by design);
60
+ // clothing / pattern / font stay random per team.
61
+ function jerseyFromTeamId(teamId) {
62
+ const rng = mulberry32(hashSeed(teamId));
63
+ const pickFrom = (arr) => arr[Math.floor(rng() * arr.length)];
64
+ const palette = pickFrom(exports.JERSEY_PALETTES);
65
+ return {
66
+ clothing: pickFrom(exports.JERSEY_CLOTHING_ORDER),
67
+ pattern: pickFrom(exports.JERSEY_PATTERN_ORDER),
68
+ colors: [palette.color1, palette.color2, palette.color3],
69
+ font: pickFrom(exports.JERSEY_FONT_ORDER),
70
+ numberColor: palette.number1,
71
+ numberOutline: palette.number2,
72
+ numberOutlineEnabled: true,
73
+ numberSpacing: 0,
74
+ numberSize: 46,
75
+ brand: 'none',
76
+ brandColor1: '#ffffff',
77
+ brandColor2: '#1e293b'
78
+ };
79
+ }
@@ -72,3 +72,32 @@ export type JerseyConfig = z.infer<typeof JerseySchema>;
72
72
  export type JerseyClothing = JerseyConfig['clothing'];
73
73
  export type JerseyPattern = JerseyConfig['pattern'];
74
74
  export type JerseyFont = JerseyConfig['font'];
75
+ export declare const AlternateJerseySchema: z.ZodObject<{
76
+ pattern: z.ZodEnum<{
77
+ solid: "solid";
78
+ verticalStripes: "verticalStripes";
79
+ verticalBars: "verticalBars";
80
+ pinstripes: "pinstripes";
81
+ hoops: "hoops";
82
+ checkered: "checkered";
83
+ diagonalSash: "diagonalSash";
84
+ splitHalves: "splitHalves";
85
+ chevron: "chevron";
86
+ shoulderYoke: "shoulderYoke";
87
+ sidePanels: "sidePanels";
88
+ centerBand: "centerBand";
89
+ gradientFade: "gradientFade";
90
+ twoToneSplit: "twoToneSplit";
91
+ }>;
92
+ colors: z.ZodArray<z.ZodString>;
93
+ numberColor: z.ZodString;
94
+ numberOutline: z.ZodString;
95
+ numberOutlineEnabled: z.ZodDefault<z.ZodBoolean>;
96
+ brandColor1: z.ZodDefault<z.ZodString>;
97
+ brandColor2: z.ZodDefault<z.ZodString>;
98
+ }, z.core.$strip>;
99
+ export type AlternateJerseyConfig = z.infer<typeof AlternateJerseySchema>;
100
+ export declare function complementaryHex(hex: string): string;
101
+ export declare function relativeLuminance(hex: string): number;
102
+ export declare function computeComplementaryJersey(main: JerseyConfig): JerseyConfig;
103
+ export declare function mergeAlternateJersey(main: JerseyConfig, alt: AlternateJerseyConfig): JerseyConfig;
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.JerseySchema = void 0;
3
+ exports.AlternateJerseySchema = exports.JerseySchema = void 0;
4
+ exports.complementaryHex = complementaryHex;
5
+ exports.relativeLuminance = relativeLuminance;
6
+ exports.computeComplementaryJersey = computeComplementaryJersey;
7
+ exports.mergeAlternateJersey = mergeAlternateJersey;
4
8
  const zod_1 = require("zod");
5
9
  // 6-digit hex (#rrggbb) or 8-digit hex with alpha (#rrggbbaa). The 8-digit form comes from the UI's opacity-enabled
6
10
  // colour pickers; SVG fills render both natively.
@@ -31,3 +35,123 @@ exports.JerseySchema = zod_1.z.object({
31
35
  brandColor1: hexColor.default('#ffffff'),
32
36
  brandColor2: hexColor.default('#1e293b')
33
37
  });
38
+ // The alternate (libero) kit stores only the fields the premium editor allows changing. Locked fields (clothing,
39
+ // font, brand icon, numberSpacing, numberSize) are always inherited from the main kit at render time.
40
+ exports.AlternateJerseySchema = zod_1.z.object({
41
+ pattern: exports.JerseySchema.shape.pattern,
42
+ colors: exports.JerseySchema.shape.colors,
43
+ numberColor: hexColor,
44
+ numberOutline: hexColor,
45
+ numberOutlineEnabled: zod_1.z.boolean().default(true),
46
+ brandColor1: hexColor.default('#ffffff'),
47
+ brandColor2: hexColor.default('#1e293b')
48
+ });
49
+ // ---------------------------------------------------------------------------
50
+ // Colour utilities (pure math, zero deps)
51
+ // ---------------------------------------------------------------------------
52
+ function hexToRgb(hex) {
53
+ const h = hex.replace('#', '').slice(0, 6);
54
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
55
+ }
56
+ function rgbToHex(r, g, b) {
57
+ return '#' + [r, g, b].map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
58
+ }
59
+ function rgbToHsl(r, g, b) {
60
+ r /= 255;
61
+ g /= 255;
62
+ b /= 255;
63
+ const max = Math.max(r, g, b);
64
+ const min = Math.min(r, g, b);
65
+ const l = (max + min) / 2;
66
+ if (max === min)
67
+ return [0, 0, l];
68
+ const d = max - min;
69
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
70
+ let h = 0;
71
+ if (max === r)
72
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
73
+ else if (max === g)
74
+ h = ((b - r) / d + 2) / 6;
75
+ else
76
+ h = ((r - g) / d + 4) / 6;
77
+ return [h, s, l];
78
+ }
79
+ function hslToRgb(h, s, l) {
80
+ if (s === 0) {
81
+ const v = Math.round(l * 255);
82
+ return [v, v, v];
83
+ }
84
+ const hue2rgb = (p, q, t) => {
85
+ if (t < 0)
86
+ t += 1;
87
+ if (t > 1)
88
+ t -= 1;
89
+ if (t < 1 / 6)
90
+ return p + (q - p) * 6 * t;
91
+ if (t < 1 / 2)
92
+ return q;
93
+ if (t < 2 / 3)
94
+ return p + (q - p) * (2 / 3 - t) * 6;
95
+ return p;
96
+ };
97
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
98
+ const p = 2 * l - q;
99
+ return [Math.round(hue2rgb(p, q, h + 1 / 3) * 255), Math.round(hue2rgb(p, q, h) * 255), Math.round(hue2rgb(p, q, h - 1 / 3) * 255)];
100
+ }
101
+ // Rotate a hex colour's hue by 180 degrees (complementary).
102
+ function complementaryHex(hex) {
103
+ const [r, g, b] = hexToRgb(hex);
104
+ const [h, s, l] = rgbToHsl(r, g, b);
105
+ const [cr, cg, cb] = hslToRgb((h + 0.5) % 1, s, l);
106
+ return rgbToHex(cr, cg, cb);
107
+ }
108
+ // Relative luminance (WCAG 2.x formula, 0 = black, 1 = white).
109
+ function relativeLuminance(hex) {
110
+ const [r, g, b] = hexToRgb(hex);
111
+ const srgb = [r, g, b].map(v => {
112
+ v /= 255;
113
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
114
+ });
115
+ return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
116
+ }
117
+ // Build the libero alternate kit from the main kit using complementary colours. Every colour in the
118
+ // pattern and the brand is hue-rotated 180 degrees; the chest-number fill/outline are picked for
119
+ // readability against the new primary colour (dark number on light, light number on dark).
120
+ function computeComplementaryJersey(main) {
121
+ const compColors = main.colors.map(c => complementaryHex(c));
122
+ const lum = relativeLuminance(compColors[0]);
123
+ return {
124
+ // Locked fields: inherited from main
125
+ clothing: main.clothing,
126
+ font: main.font,
127
+ brand: main.brand,
128
+ numberSpacing: main.numberSpacing,
129
+ numberSize: main.numberSize,
130
+ // Editable fields: complementary-derived
131
+ pattern: main.pattern,
132
+ colors: compColors,
133
+ numberColor: lum > 0.4 ? '#1a1a1a' : '#ffffff',
134
+ numberOutline: lum > 0.4 ? '#ffffff' : '#1a1a1a',
135
+ numberOutlineEnabled: main.numberOutlineEnabled,
136
+ brandColor1: complementaryHex(main.brandColor1),
137
+ brandColor2: complementaryHex(main.brandColor2)
138
+ };
139
+ }
140
+ // Merge a stored alternate (partial, editable fields only) with the main kit's locked fields to
141
+ // produce a full JerseyConfig for rendering.
142
+ function mergeAlternateJersey(main, alt) {
143
+ return {
144
+ clothing: main.clothing,
145
+ font: main.font,
146
+ brand: main.brand,
147
+ numberSpacing: main.numberSpacing,
148
+ numberSize: main.numberSize,
149
+ pattern: alt.pattern,
150
+ colors: alt.colors,
151
+ numberColor: alt.numberColor,
152
+ numberOutline: alt.numberOutline,
153
+ numberOutlineEnabled: alt.numberOutlineEnabled,
154
+ brandColor1: alt.brandColor1,
155
+ brandColor2: alt.brandColor2
156
+ };
157
+ }
@@ -414,5 +414,29 @@ export declare const TeamInputSchema: z.ZodObject<{
414
414
  brandColor1: z.ZodDefault<z.ZodString>;
415
415
  brandColor2: z.ZodDefault<z.ZodString>;
416
416
  }, z.core.$strip>>;
417
+ alternateJerseyConfig: z.ZodOptional<z.ZodObject<{
418
+ pattern: z.ZodEnum<{
419
+ solid: "solid";
420
+ verticalStripes: "verticalStripes";
421
+ verticalBars: "verticalBars";
422
+ pinstripes: "pinstripes";
423
+ hoops: "hoops";
424
+ checkered: "checkered";
425
+ diagonalSash: "diagonalSash";
426
+ splitHalves: "splitHalves";
427
+ chevron: "chevron";
428
+ shoulderYoke: "shoulderYoke";
429
+ sidePanels: "sidePanels";
430
+ centerBand: "centerBand";
431
+ gradientFade: "gradientFade";
432
+ twoToneSplit: "twoToneSplit";
433
+ }>;
434
+ colors: z.ZodArray<z.ZodString>;
435
+ numberColor: z.ZodString;
436
+ numberOutline: z.ZodString;
437
+ numberOutlineEnabled: z.ZodDefault<z.ZodBoolean>;
438
+ brandColor1: z.ZodDefault<z.ZodString>;
439
+ brandColor2: z.ZodDefault<z.ZodString>;
440
+ }, z.core.$strip>>;
417
441
  }, z.core.$strip>;
418
442
  export type TeamInput = z.infer<typeof TeamInputSchema>;
@@ -22,7 +22,8 @@ exports.TeamInputSchema = zod_1.z.object({
22
22
  country: countrySchema,
23
23
  legacyFlagCountry: countrySchema.optional(),
24
24
  tactics: tactics_z_1.TacticsInputSchema.optional(),
25
- jerseyConfig: jersey_1.JerseySchema.optional()
25
+ jerseyConfig: jersey_1.JerseySchema.optional(),
26
+ alternateJerseyConfig: jersey_1.AlternateJerseySchema.optional()
26
27
  }).superRefine((data, ctx) => {
27
28
  const ids = data.roster.map(p => p.id);
28
29
  if (new Set(ids).size !== ids.length) {
@@ -1,7 +1,7 @@
1
1
  import { Player } from '../player';
2
2
  import { Country } from '../country';
3
3
  import { Tactics } from './tactics';
4
- import type { JerseyConfig } from './jersey';
4
+ import type { JerseyConfig, AlternateJerseyConfig } from './jersey';
5
5
  export declare class Team {
6
6
  readonly id: string;
7
7
  readonly roster: Player[];
@@ -12,6 +12,7 @@ export declare class Team {
12
12
  readonly country: Country;
13
13
  readonly legacyFlagCountry?: Country;
14
14
  readonly jerseyConfig?: JerseyConfig;
15
+ readonly alternateJerseyConfig?: AlternateJerseyConfig;
15
16
  private _rating;
16
17
  static create(input: unknown): Team;
17
18
  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, legacyFlagCountry, roster, tactics, jerseyConfig }) {
22
+ constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics, jerseyConfig, alternateJerseyConfig }) {
23
23
  this.id = id;
24
24
  this._rating = rating;
25
25
  this.roster = roster;
@@ -30,6 +30,7 @@ class Team {
30
30
  this.country = country;
31
31
  this.legacyFlagCountry = legacyFlagCountry;
32
32
  this.jerseyConfig = jerseyConfig;
33
+ this.alternateJerseyConfig = alternateJerseyConfig;
33
34
  }
34
35
  get rating() {
35
36
  return this._rating;
@@ -17,7 +17,7 @@ export interface Results {
17
17
  away: number[];
18
18
  }
19
19
  export interface MatchCompetition {
20
- type: 'LEAGUE' | 'NATIONAL' | 'QUALIFIER' | 'TOURNAMENT' | 'EVENT';
20
+ type: 'LEAGUE' | 'NATIONAL' | 'QUALIFIER' | 'TOURNAMENT' | 'EVENT' | 'WNL' | 'WNLF';
21
21
  countryName?: string;
22
22
  countryAlpha2?: string;
23
23
  countryAlpha3?: string;
@@ -196,7 +196,7 @@ export type Team = Omit<DataProps<_Team>, 'roster' | '_rating' | 'rating' | 'tac
196
196
  nationalManager?: {
197
197
  displayName: string;
198
198
  teamId: string;
199
- teamShortName: string;
199
+ teamName: string;
200
200
  };
201
201
  };
202
202
  export type Standing = DataProps<_Standing> & {
@@ -233,28 +233,10 @@ export function initModels(sequelize) {
233
233
  Competition.hasOne(NationalCountry, { as: 'NationalCountry', foreignKey: 'competition_id' });
234
234
  Country.hasMany(NationalCountry, { as: 'NationalCountries', foreignKey: 'country_id' });
235
235
  Iteration.hasMany(NationalCountry, { as: 'NationalCountries', foreignKey: 'iteration' });
236
- // National teams (WNL). The registry maps a country to its permanent active=false national Team; the claim links a
237
- // Tier-2 manager to it; per-edition squad + callup + season rows hang off the WNL league Competition.
238
- NationalTeam.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
239
- NationalTeam.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
240
- Country.hasOne(NationalTeam, { as: 'NationalTeam', foreignKey: 'country_id' });
241
- Team.hasOne(NationalTeam, { as: 'NationalTeam', foreignKey: 'team_id' });
242
- NationalTeam.hasOne(NationalTeamClaim, { as: 'claim', foreignKey: 'country_id' });
243
- NationalTeamClaim.belongsTo(NationalTeam, { as: 'nationalTeam', foreignKey: 'country_id' });
244
- NationalTeamClaim.belongsTo(AuthUser, { as: 'user', foreignKey: 'user_id' });
245
- AuthUser.hasOne(NationalTeamClaim, { as: 'NationalTeamClaim', foreignKey: 'user_id' });
246
- WnlSquad.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
247
- WnlSquad.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
248
- WnlSquad.belongsTo(Team, { as: 'team', foreignKey: 'team_id' });
249
- WnlSquad.belongsTo(AuthUser, { as: 'manager', foreignKey: 'manager_user_id' });
250
- Competition.hasMany(WnlSquad, { as: 'WnlSquads', foreignKey: 'competition_id' });
251
- WnlCallup.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
252
- WnlCallup.belongsTo(Player, { as: 'player', foreignKey: 'player_id' });
253
- WnlCallup.belongsTo(Country, { as: 'country', foreignKey: 'country_id' });
254
- Competition.hasMany(WnlCallup, { as: 'WnlCallups', foreignKey: 'competition_id' });
255
- WnlSeason.belongsTo(Competition, { as: 'competition', foreignKey: 'competition_id' });
256
- WnlSeason.belongsTo(Competition, { as: 'finalsCompetition', foreignKey: 'finals_competition_id' });
257
- Competition.hasOne(WnlSeason, { as: 'WnlSeason', foreignKey: 'competition_id' });
236
+ // National teams (WNL): NO associations on purpose. Every consumer of the five WNL tables (Sim scheduler,
237
+ // API /wnl routes) reads them with direct model queries or raw SQL joins; the alias wiring that once lived
238
+ // here was confirmed unused in all four repos and removed as dead code (review fleet 2026-08-28). Add an
239
+ // association back only together with the include that uses it.
258
240
  VPER.belongsTo(Player, { as: 'Player', foreignKey: 'player_id' });
259
241
  VPER.belongsTo(Match, { as: 'Match', foreignKey: 'match_id' });
260
242
  Team.hasOne(Coach, { as: 'Coach', foreignKey: 'team_id' });