/** * What colour a force wears: its team's, shaded by which force of that team it * is. * * Two questions in one answer, and the order matters. A reader has to see * *which side* first and *which force of that side* second, so the team picks * the colour and the force only varies it. Bug Company's three pirate lances * are three shades of the same red rather than three unrelated hues, which is * what says they are one side; the player's own force is blue, always. * * The variants are stepped wide enough to tell four lances apart on an icon * the size of a unit sprite, which rules out the subtle end: a team that reads * as one solid block is a team whose lances cannot be told from each other on * the board, and the board is where it matters. * * These are also what a bot fights in. A lobby-assigned bot brings no camo of * its own, and MegaMek's default is close enough to white that a scenario full * of them is a screen full of identical pale machines — see `soloCamo` in * screens/lobby.ts, which paints one of these instead. */ /** Red, green, blue, each 0..255. */ export type RGB = readonly [number, number, number]; /** * One base colour per team, in the order teams are met — the player's team is * always first, so blue is always yours. * * Blue, red, green, amber. The first two carry almost every fight there is and * are the pair the daily card has always used; the second two exist for a * scenario with three or four sides and are far enough apart in hue to survive * being shaded. */ export const TEAM_COLOURS: readonly RGB[] = [ [86, 132, 199], // Green *below* blue, which puts the hue just past 0 into the cool half of // red. Green above blue reads as orange once MegaMek's warm sprite art is // multiplied through it. [201, 42, 58], [76, 158, 96], [214, 152, 58], ]; /** RGB to HSL, both 0..1 except hue in degrees. */ function toHsl([r, g, b]: RGB): [number, number, number] { const [rr, gg, bb] = [r / 255, g / 255, b / 255]; const max = Math.max(rr, gg, bb); const min = Math.min(rr, gg, bb); const l = (max + min) / 2; const d = max - min; if (d === 0) return [0, 0, l]; const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); let h: number; if (max === rr) h = ((gg - bb) / d + (gg < bb ? 6 : 0)) * 60; else if (max === gg) h = ((bb - rr) / d + 2) * 60; else h = ((rr - gg) / d + 4) * 60; return [h, s, l]; } /** HSL back to RGB, rounded to whole channels. */ function toRgb([h, s, l]: [number, number, number]): RGB { const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = l - c / 2; const seg = Math.floor((((h % 360) + 360) % 360) / 60); const [r, g, b] = ( [ [c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x], ] as const )[seg]!; return [ Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255), ]; } /** * The colour of the `slot`th force on the `team`th team. * * `slot` 0 is the team's own base colour, so a side with one force — which is * most of them — wears exactly the colour the team is named by, and the * shading only appears when there is something to tell apart. * * Lightness carries the variation, alternating above and below the base so * four forces spread across the range rather than fading in one direction into * either the background or the labels. The small hue drift stops two steps of * the same lightness reading as the same colour on a 40px icon. */ export function sideRgb(team: number, slot = 0): RGB { const base = TEAM_COLOURS[team % TEAM_COLOURS.length]!; if (slot <= 0) return base; const [h, s, l] = toHsl(base); // 1 -> +1, 2 -> -1, 3 -> +2 … so the steps walk outwards from the base. const step = Math.ceil(slot / 2) * (slot % 2 === 1 ? 1 : -1); const lightness = Math.min(0.78, Math.max(0.24, l + step * 0.11)); return toRgb([h + step * 7, s, lightness]); } /** The same colour as CSS, for text and rules beside the board. */ export function sideCss(team: number, slot = 0): string { const [r, g, b] = sideRgb(team, slot); return `rgb(${r} ${g} ${b})`; }