volleyballsimtypes 0.0.526 → 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.
- package/dist/cjs/src/api/index.d.ts +1 -1
- package/dist/cjs/src/data/models/team-jersey.d.ts +3 -1
- package/dist/cjs/src/data/models/team-jersey.js +4 -0
- package/dist/cjs/src/data/transformers/team.js +2 -1
- package/dist/cjs/src/service/team/index.d.ts +1 -0
- package/dist/cjs/src/service/team/index.js +1 -0
- package/dist/cjs/src/service/team/jersey-defaults.d.ts +13 -0
- package/dist/cjs/src/service/team/jersey-defaults.js +79 -0
- package/dist/cjs/src/service/team/jersey.d.ts +29 -0
- package/dist/cjs/src/service/team/jersey.js +125 -1
- package/dist/cjs/src/service/team/schemas/team.z.d.ts +24 -0
- package/dist/cjs/src/service/team/schemas/team.z.js +2 -1
- package/dist/cjs/src/service/team/team.d.ts +2 -1
- package/dist/cjs/src/service/team/team.js +2 -1
- package/dist/esm/src/api/index.d.ts +1 -1
- package/dist/esm/src/data/models/team-jersey.d.ts +3 -1
- package/dist/esm/src/data/models/team-jersey.js +4 -0
- package/dist/esm/src/data/transformers/team.js +2 -1
- package/dist/esm/src/service/team/index.d.ts +1 -0
- package/dist/esm/src/service/team/index.js +1 -0
- package/dist/esm/src/service/team/jersey-defaults.d.ts +13 -0
- package/dist/esm/src/service/team/jersey-defaults.js +75 -0
- package/dist/esm/src/service/team/jersey.d.ts +29 -0
- package/dist/esm/src/service/team/jersey.js +120 -0
- package/dist/esm/src/service/team/schemas/team.z.d.ts +24 -0
- package/dist/esm/src/service/team/schemas/team.z.js +3 -2
- package/dist/esm/src/service/team/team.d.ts +2 -1
- package/dist/esm/src/service/team/team.js +2 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
199
|
+
teamName: string;
|
|
200
200
|
};
|
|
201
201
|
};
|
|
202
202
|
export type Standing = DataProps<_Standing> & {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model } from 'sequelize';
|
|
3
3
|
import { TeamId, TeamModel } from '.';
|
|
4
|
-
import { JerseyConfig } from '../../service';
|
|
4
|
+
import { JerseyConfig, AlternateJerseyConfig } from '../../service';
|
|
5
5
|
export interface TeamJerseyAttributes {
|
|
6
6
|
team_id: string;
|
|
7
7
|
config: JerseyConfig;
|
|
8
|
+
alternate_config?: AlternateJerseyConfig | null;
|
|
8
9
|
}
|
|
9
10
|
export type TeamJerseyPk = 'team_id';
|
|
10
11
|
export type TeamJerseyId = TeamJerseyModel[TeamJerseyPk];
|
|
@@ -12,6 +13,7 @@ export type TeamJerseyCreationAttributes = TeamJerseyAttributes;
|
|
|
12
13
|
export declare class TeamJerseyModel extends Model<TeamJerseyAttributes, TeamJerseyCreationAttributes> implements TeamJerseyAttributes {
|
|
13
14
|
team_id: string;
|
|
14
15
|
config: JerseyConfig;
|
|
16
|
+
alternate_config?: AlternateJerseyConfig | null;
|
|
15
17
|
team: TeamModel;
|
|
16
18
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
17
19
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -53,6 +53,7 @@ function transformToObject(model, currentIteration, tacticsOverride) {
|
|
|
53
53
|
divisionId: model.division_id,
|
|
54
54
|
// Present only when the caller explicitly included the TeamJersey association (editor / effective-jersey
|
|
55
55
|
// resolver); undefined on ordinary team loads, which heals to the id-derived default in the UI.
|
|
56
|
-
jerseyConfig: model.TeamJersey?.config
|
|
56
|
+
jerseyConfig: model.TeamJersey?.config,
|
|
57
|
+
alternateJerseyConfig: model.TeamJersey?.alternate_config ?? undefined
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -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;
|
|
@@ -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
|
-
|
|
199
|
+
teamName: string;
|
|
200
200
|
};
|
|
201
201
|
};
|
|
202
202
|
export type Standing = DataProps<_Standing> & {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as Sequelize from 'sequelize';
|
|
2
2
|
import { Model } from 'sequelize';
|
|
3
3
|
import { TeamId, TeamModel } from '.';
|
|
4
|
-
import { JerseyConfig } from '../../service';
|
|
4
|
+
import { JerseyConfig, AlternateJerseyConfig } from '../../service';
|
|
5
5
|
export interface TeamJerseyAttributes {
|
|
6
6
|
team_id: string;
|
|
7
7
|
config: JerseyConfig;
|
|
8
|
+
alternate_config?: AlternateJerseyConfig | null;
|
|
8
9
|
}
|
|
9
10
|
export type TeamJerseyPk = 'team_id';
|
|
10
11
|
export type TeamJerseyId = TeamJerseyModel[TeamJerseyPk];
|
|
@@ -12,6 +13,7 @@ export type TeamJerseyCreationAttributes = TeamJerseyAttributes;
|
|
|
12
13
|
export declare class TeamJerseyModel extends Model<TeamJerseyAttributes, TeamJerseyCreationAttributes> implements TeamJerseyAttributes {
|
|
13
14
|
team_id: string;
|
|
14
15
|
config: JerseyConfig;
|
|
16
|
+
alternate_config?: AlternateJerseyConfig | null;
|
|
15
17
|
team: TeamModel;
|
|
16
18
|
getTeam: Sequelize.BelongsToGetAssociationMixin<TeamModel>;
|
|
17
19
|
setTeam: Sequelize.BelongsToSetAssociationMixin<TeamModel, TeamId>;
|
|
@@ -46,7 +46,8 @@ function transformToObject(model, currentIteration, tacticsOverride) {
|
|
|
46
46
|
divisionId: model.division_id,
|
|
47
47
|
// Present only when the caller explicitly included the TeamJersey association (editor / effective-jersey
|
|
48
48
|
// resolver); undefined on ordinary team loads, which heals to the id-derived default in the UI.
|
|
49
|
-
jerseyConfig: model.TeamJersey?.config
|
|
49
|
+
jerseyConfig: model.TeamJersey?.config,
|
|
50
|
+
alternateJerseyConfig: model.TeamJersey?.alternate_config ?? undefined
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
export { transformToObject as transformToTeam, transformToAttributes as transformFromTeam };
|
|
@@ -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,75 @@
|
|
|
1
|
+
// FNV-1a hash of a string, used to seed the PRNG below from a team id.
|
|
2
|
+
function hashSeed(str) {
|
|
3
|
+
let h = 2166136261;
|
|
4
|
+
for (let i = 0; i < str.length; i++) {
|
|
5
|
+
h ^= str.charCodeAt(i);
|
|
6
|
+
h = Math.imul(h, 16777619);
|
|
7
|
+
}
|
|
8
|
+
return h >>> 0;
|
|
9
|
+
}
|
|
10
|
+
// mulberry32: a tiny deterministic PRNG. Same seed -> same stream, so a team's kit is stable and reproducible.
|
|
11
|
+
function mulberry32(seed) {
|
|
12
|
+
let a = seed;
|
|
13
|
+
return () => {
|
|
14
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
15
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
16
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
17
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export const JERSEY_PALETTES = [
|
|
21
|
+
{ color1: '#606c38', color2: '#dda15e', color3: '#bc6c25', number1: '#fefae0', number2: '#283618' },
|
|
22
|
+
{ color1: '#780000', color2: '#669bbc', color3: '#c1121f', number1: '#fdf0d5', number2: '#003049' },
|
|
23
|
+
{ color1: '#2a9d8f', color2: '#f4a261', color3: '#e76f51', number1: '#264653', number2: '#e9c46a' },
|
|
24
|
+
{ color1: '#219ebc', color2: '#fb8500', color3: '#8ecae6', number1: '#023047', number2: '#ffb703' },
|
|
25
|
+
{ color1: '#1d3557', color2: '#e63946', color3: '#a8dadc', number1: '#f1faee', number2: '#457b9d' },
|
|
26
|
+
{ color1: '#003566', color2: '#ffc300', color3: '#001d3d', number1: '#ffd60a', number2: '#000814' },
|
|
27
|
+
{ color1: '#bc4749', color2: '#6a994e', color3: '#a7c957', number1: '#f2e8cf', number2: '#386641' },
|
|
28
|
+
{ color1: '#6f1d1b', color2: '#99582a', color3: '#bb9457', number1: '#ffe6a7', number2: '#432818' },
|
|
29
|
+
{ color1: '#ef476f', color2: '#118ab2', color3: '#ffd166', number1: '#ffd166', number2: '#073b4c' },
|
|
30
|
+
{ color1: '#16425b', color2: '#3a7ca5', color3: '#81c3d7', number1: '#d9dcd6', number2: '#2f6690' },
|
|
31
|
+
{ color1: '#ee6c4d', color2: '#3d5a80', color3: '#98c1d9', number1: '#e0fbfc', number2: '#293241' },
|
|
32
|
+
{ color1: '#e09f3e', color2: '#335c67', color3: '#9e2a2b', number1: '#fff3b0', number2: '#540b0e' },
|
|
33
|
+
{ color1: '#9a031e', color2: '#fb8b24', color3: '#e36414', number1: '#0f4c5c', number2: '#5f0f40' },
|
|
34
|
+
{ color1: '#127475', color2: '#f2542d', color3: '#0e9594', number1: '#f5dfbb', number2: '#562c2c' },
|
|
35
|
+
{ color1: '#bc4b51', color2: '#8cb369', color3: '#f4a259', number1: '#f4e285', number2: '#5b8e7d' },
|
|
36
|
+
{ color1: '#7678ed', color2: '#f18701', color3: '#f35b04', number1: '#f7b801', number2: '#3d348b' },
|
|
37
|
+
{ color1: '#14213d', color2: '#fca311', color3: '#e5e5e5', number1: '#ffffff', number2: '#000000' },
|
|
38
|
+
{ color1: '#e07a5f', color2: '#81b29a', color3: '#f2cc8f', number1: '#f4f1de', number2: '#3d405b' },
|
|
39
|
+
{ color1: '#f79824', color2: '#33a1fd', color3: '#fdca40', number1: '#33a1fd', number2: '#31393c' },
|
|
40
|
+
{ color1: '#619b8a', color2: '#fe7f2d', color3: '#a1c181', number1: '#fcca46', number2: '#233d4d' },
|
|
41
|
+
{ color1: '#227c9d', color2: '#17c3b2', color3: '#ffcb77', number1: '#fef9ef', number2: '#fe6d73' },
|
|
42
|
+
{ color1: '#19647e', color2: '#119da4', color3: '#1f2041', number1: '#4b3f72', number2: '#ffc857' }
|
|
43
|
+
];
|
|
44
|
+
export const JERSEY_PATTERN_ORDER = [
|
|
45
|
+
'solid', 'verticalStripes', 'verticalBars', 'pinstripes', 'hoops', 'checkered', 'diagonalSash', 'splitHalves',
|
|
46
|
+
'chevron', 'shoulderYoke', 'sidePanels', 'centerBand', 'gradientFade', 'twoToneSplit'
|
|
47
|
+
];
|
|
48
|
+
export const JERSEY_CLOTHING_ORDER = [
|
|
49
|
+
'shirtCrewNeck', 'shirtCrewNeckSleeveless', 'shirtVNeck', 'shirtVNeckSleeveless', 'collarAndSweater', 'collarAndSweaterSleeveless'
|
|
50
|
+
];
|
|
51
|
+
// Free-user fonts: the id-derived random for a team's default kit picks ONLY from these (premium fonts are vector
|
|
52
|
+
// glyph paths that require the UI rendering pipeline).
|
|
53
|
+
export const JERSEY_FONT_ORDER = ['sans', 'wide', 'mono', 'serif', 'block'];
|
|
54
|
+
// The full jersey a team wears by default: every category is drawn deterministically from the team id, so each team
|
|
55
|
+
// gets its own stable, distinct kit. Colours come from ONE randomly-assigned JERSEY_PALETTE (harmonious by design);
|
|
56
|
+
// clothing / pattern / font stay random per team.
|
|
57
|
+
export function jerseyFromTeamId(teamId) {
|
|
58
|
+
const rng = mulberry32(hashSeed(teamId));
|
|
59
|
+
const pickFrom = (arr) => arr[Math.floor(rng() * arr.length)];
|
|
60
|
+
const palette = pickFrom(JERSEY_PALETTES);
|
|
61
|
+
return {
|
|
62
|
+
clothing: pickFrom(JERSEY_CLOTHING_ORDER),
|
|
63
|
+
pattern: pickFrom(JERSEY_PATTERN_ORDER),
|
|
64
|
+
colors: [palette.color1, palette.color2, palette.color3],
|
|
65
|
+
font: pickFrom(JERSEY_FONT_ORDER),
|
|
66
|
+
numberColor: palette.number1,
|
|
67
|
+
numberOutline: palette.number2,
|
|
68
|
+
numberOutlineEnabled: true,
|
|
69
|
+
numberSpacing: 0,
|
|
70
|
+
numberSize: 46,
|
|
71
|
+
brand: 'none',
|
|
72
|
+
brandColor1: '#ffffff',
|
|
73
|
+
brandColor2: '#1e293b'
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -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;
|
|
@@ -28,3 +28,123 @@ export const JerseySchema = z.object({
|
|
|
28
28
|
brandColor1: hexColor.default('#ffffff'),
|
|
29
29
|
brandColor2: hexColor.default('#1e293b')
|
|
30
30
|
});
|
|
31
|
+
// The alternate (libero) kit stores only the fields the premium editor allows changing. Locked fields (clothing,
|
|
32
|
+
// font, brand icon, numberSpacing, numberSize) are always inherited from the main kit at render time.
|
|
33
|
+
export const AlternateJerseySchema = z.object({
|
|
34
|
+
pattern: JerseySchema.shape.pattern,
|
|
35
|
+
colors: JerseySchema.shape.colors,
|
|
36
|
+
numberColor: hexColor,
|
|
37
|
+
numberOutline: hexColor,
|
|
38
|
+
numberOutlineEnabled: z.boolean().default(true),
|
|
39
|
+
brandColor1: hexColor.default('#ffffff'),
|
|
40
|
+
brandColor2: hexColor.default('#1e293b')
|
|
41
|
+
});
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Colour utilities (pure math, zero deps)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function hexToRgb(hex) {
|
|
46
|
+
const h = hex.replace('#', '').slice(0, 6);
|
|
47
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
48
|
+
}
|
|
49
|
+
function rgbToHex(r, g, b) {
|
|
50
|
+
return '#' + [r, g, b].map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
|
|
51
|
+
}
|
|
52
|
+
function rgbToHsl(r, g, b) {
|
|
53
|
+
r /= 255;
|
|
54
|
+
g /= 255;
|
|
55
|
+
b /= 255;
|
|
56
|
+
const max = Math.max(r, g, b);
|
|
57
|
+
const min = Math.min(r, g, b);
|
|
58
|
+
const l = (max + min) / 2;
|
|
59
|
+
if (max === min)
|
|
60
|
+
return [0, 0, l];
|
|
61
|
+
const d = max - min;
|
|
62
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
63
|
+
let h = 0;
|
|
64
|
+
if (max === r)
|
|
65
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
66
|
+
else if (max === g)
|
|
67
|
+
h = ((b - r) / d + 2) / 6;
|
|
68
|
+
else
|
|
69
|
+
h = ((r - g) / d + 4) / 6;
|
|
70
|
+
return [h, s, l];
|
|
71
|
+
}
|
|
72
|
+
function hslToRgb(h, s, l) {
|
|
73
|
+
if (s === 0) {
|
|
74
|
+
const v = Math.round(l * 255);
|
|
75
|
+
return [v, v, v];
|
|
76
|
+
}
|
|
77
|
+
const hue2rgb = (p, q, t) => {
|
|
78
|
+
if (t < 0)
|
|
79
|
+
t += 1;
|
|
80
|
+
if (t > 1)
|
|
81
|
+
t -= 1;
|
|
82
|
+
if (t < 1 / 6)
|
|
83
|
+
return p + (q - p) * 6 * t;
|
|
84
|
+
if (t < 1 / 2)
|
|
85
|
+
return q;
|
|
86
|
+
if (t < 2 / 3)
|
|
87
|
+
return p + (q - p) * (2 / 3 - t) * 6;
|
|
88
|
+
return p;
|
|
89
|
+
};
|
|
90
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
91
|
+
const p = 2 * l - q;
|
|
92
|
+
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)];
|
|
93
|
+
}
|
|
94
|
+
// Rotate a hex colour's hue by 180 degrees (complementary).
|
|
95
|
+
export function complementaryHex(hex) {
|
|
96
|
+
const [r, g, b] = hexToRgb(hex);
|
|
97
|
+
const [h, s, l] = rgbToHsl(r, g, b);
|
|
98
|
+
const [cr, cg, cb] = hslToRgb((h + 0.5) % 1, s, l);
|
|
99
|
+
return rgbToHex(cr, cg, cb);
|
|
100
|
+
}
|
|
101
|
+
// Relative luminance (WCAG 2.x formula, 0 = black, 1 = white).
|
|
102
|
+
export function relativeLuminance(hex) {
|
|
103
|
+
const [r, g, b] = hexToRgb(hex);
|
|
104
|
+
const srgb = [r, g, b].map(v => {
|
|
105
|
+
v /= 255;
|
|
106
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
107
|
+
});
|
|
108
|
+
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
|
|
109
|
+
}
|
|
110
|
+
// Build the libero alternate kit from the main kit using complementary colours. Every colour in the
|
|
111
|
+
// pattern and the brand is hue-rotated 180 degrees; the chest-number fill/outline are picked for
|
|
112
|
+
// readability against the new primary colour (dark number on light, light number on dark).
|
|
113
|
+
export function computeComplementaryJersey(main) {
|
|
114
|
+
const compColors = main.colors.map(c => complementaryHex(c));
|
|
115
|
+
const lum = relativeLuminance(compColors[0]);
|
|
116
|
+
return {
|
|
117
|
+
// Locked fields: inherited from main
|
|
118
|
+
clothing: main.clothing,
|
|
119
|
+
font: main.font,
|
|
120
|
+
brand: main.brand,
|
|
121
|
+
numberSpacing: main.numberSpacing,
|
|
122
|
+
numberSize: main.numberSize,
|
|
123
|
+
// Editable fields: complementary-derived
|
|
124
|
+
pattern: main.pattern,
|
|
125
|
+
colors: compColors,
|
|
126
|
+
numberColor: lum > 0.4 ? '#1a1a1a' : '#ffffff',
|
|
127
|
+
numberOutline: lum > 0.4 ? '#ffffff' : '#1a1a1a',
|
|
128
|
+
numberOutlineEnabled: main.numberOutlineEnabled,
|
|
129
|
+
brandColor1: complementaryHex(main.brandColor1),
|
|
130
|
+
brandColor2: complementaryHex(main.brandColor2)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Merge a stored alternate (partial, editable fields only) with the main kit's locked fields to
|
|
134
|
+
// produce a full JerseyConfig for rendering.
|
|
135
|
+
export function mergeAlternateJersey(main, alt) {
|
|
136
|
+
return {
|
|
137
|
+
clothing: main.clothing,
|
|
138
|
+
font: main.font,
|
|
139
|
+
brand: main.brand,
|
|
140
|
+
numberSpacing: main.numberSpacing,
|
|
141
|
+
numberSize: main.numberSize,
|
|
142
|
+
pattern: alt.pattern,
|
|
143
|
+
colors: alt.colors,
|
|
144
|
+
numberColor: alt.numberColor,
|
|
145
|
+
numberOutline: alt.numberOutline,
|
|
146
|
+
numberOutlineEnabled: alt.numberOutlineEnabled,
|
|
147
|
+
brandColor1: alt.brandColor1,
|
|
148
|
+
brandColor2: alt.brandColor2
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -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>;
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { Player } from '../../player';
|
|
3
3
|
import { Country } from '../../country';
|
|
4
4
|
import { TacticsInputSchema } from './tactics.z';
|
|
5
|
-
import { JerseySchema } from '../jersey';
|
|
5
|
+
import { JerseySchema, AlternateJerseySchema } from '../jersey';
|
|
6
6
|
const playerInstanceSchema = z.custom((v) => v instanceof Player, {
|
|
7
7
|
message: 'INVALID_PLAYER_INSTANCE'
|
|
8
8
|
});
|
|
@@ -19,7 +19,8 @@ export const TeamInputSchema = z.object({
|
|
|
19
19
|
country: countrySchema,
|
|
20
20
|
legacyFlagCountry: countrySchema.optional(),
|
|
21
21
|
tactics: TacticsInputSchema.optional(),
|
|
22
|
-
jerseyConfig: JerseySchema.optional()
|
|
22
|
+
jerseyConfig: JerseySchema.optional(),
|
|
23
|
+
alternateJerseyConfig: AlternateJerseySchema.optional()
|
|
23
24
|
}).superRefine((data, ctx) => {
|
|
24
25
|
const ids = data.roster.map(p => p.id);
|
|
25
26
|
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();
|
|
@@ -16,7 +16,7 @@ export class Team {
|
|
|
16
16
|
tactics: result.data.tactics != null ? Tactics.create(result.data.tactics) : undefined
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics, jerseyConfig }) {
|
|
19
|
+
constructor({ id, rating, name, shortName, divisionId, country, legacyFlagCountry, roster, tactics, jerseyConfig, alternateJerseyConfig }) {
|
|
20
20
|
this.id = id;
|
|
21
21
|
this._rating = rating;
|
|
22
22
|
this.roster = roster;
|
|
@@ -27,6 +27,7 @@ export class Team {
|
|
|
27
27
|
this.country = country;
|
|
28
28
|
this.legacyFlagCountry = legacyFlagCountry;
|
|
29
29
|
this.jerseyConfig = jerseyConfig;
|
|
30
|
+
this.alternateJerseyConfig = alternateJerseyConfig;
|
|
30
31
|
}
|
|
31
32
|
get rating() {
|
|
32
33
|
return this._rating;
|