<?phpnamespace App\Engine;/** * The 256 colours of the game: 16 ramps of 16 shades (index = ramp * 16 + shade, shade 0 dark .. 15 bright). * Like the original engine's 8-bit renderer, every picture is a string of palette indices and the light is a * "colormap": a 256-byte table that maps each colour to a darker one. Ramps make the colormaps exact and cheap: * darkening a colour moves it down its own ramp. * * Index 255 (the brightest teal) is never drawn: it is the transparent pixel of sprites. */final class Palette{ public const TRANSPARENT = 255; public const LEVELS = 32; // colormaps: 0 = full bright .. 31 = nearly black public const GREY = 0, BROWN = 1, TAN = 2, RED = 3, ORANGE = 4, YELLOW = 5, GREEN = 6, OLIVE = 7, BLUE = 8, CYAN = 9, PURPLE = 10, FLESH = 11, STEEL = 12, RUST = 13, BRICK = 14, TEAL = 15; /** The bright end of each ramp. */ private const RAMPS = [ [255, 255, 255], [196, 136, 84], [232, 204, 156], [255, 44, 32], [255, 152, 44], [255, 236, 96], [116, 255, 64], [156, 172, 92], [84, 128, 255], [96, 232, 255], [196, 96, 255], [255, 176, 152], [176, 194, 220], [208, 108, 64], [184, 76, 62], [88, 204, 168], ]; /** @var array<int, array{0:int,1:int,2:int}>|null */ private static ?array $rgb = null; /** @var string[]|null the 32 colormaps */ private static ?array $maps = null; private static ?string $identity = null; /** @return array<int, array{0:int,1:int,2:int}> */ public static function rgb(): array { if (self::$rgb === null) { $colors = []; foreach (self::RAMPS as $ramp => [$r, $g, $b]) { for ($s = 0; $s < 16; $s++) { $t = (($s + 1) / 16) ** 1.15; $white = $s > 12 ? ($s - 12) * 0.07 : 0.0; $c = []; foreach ([$r, $g, $b] as $v) { $x = $v * $t; $c[] = (int)max(0, min(255, round($x + (255 - $x) * $white))); } $colors[$ramp * 16 + $s] = $c; } } $colors[0] = [0, 0, 0]; self::$rgb = $colors; } return self::$rgb; } public static function index(int $ramp, int $shade): int { $i = $ramp * 16 + max(0, min(15, $shade)); return $i === self::TRANSPARENT ? 254 : $i; } /** The colour of a palette index darkened to a light level (0 bright .. 31 dark). */ public static function shade(int $index, int $level): int { if ($index === self::TRANSPARENT) { return $index; } $f = 1.0 - $level / self::LEVELS; $s = (int)round((($index & 15) + 1) * $f) - 1; return $s < 0 ? 0 : ($index & 0xF0) + $s; } /** Colormap $level as a 256-byte string (for strtr() and lookups). */ public static function colormap(int $level): string { if (self::$maps === null) { self::$maps = []; for ($l = 0; $l < self::LEVELS; $l++) { $m = ''; for ($i = 0; $i < 256; $i++) { $m .= chr(self::shade($i, $l)); } self::$maps[$l] = $m; } } return self::$maps[max(0, min(self::LEVELS - 1, $level))]; } /** "\x00\x01...\xFF": the identity map, first argument of strtr(). */ public static function identity(): string { return self::$identity ??= implode('', array_map('chr', range(0, 255))); } /** A picture's pixels at a light level (strtr: done in C, fast). */ public static function darken(string $pixels, int $level): string { return $level <= 0 ? $pixels : strtr($pixels, self::identity(), self::colormap($level)); } /** * index char => RGBA bytes, for strtr($frame, $map): the whole 8-bit frame to RGBA in one C call. * $tint = [r, g, b, amount 0..1]: the damage / pickup flash mixed into every colour. * @return array<string, string> */ public static function rgbaMap(?array $tint = null): array { $map = []; foreach (self::rgb() as $i => [$r, $g, $b]) { if ($tint !== null) { [$tr, $tg, $tb, $a] = $tint; $r = (int)($r + ($tr - $r) * $a); $g = (int)($g + ($tg - $g) * $a); $b = (int)($b + ($tb - $b) * $a); } $map[chr($i)] = chr($r) . chr($g) . chr($b) . "\xFF"; } return $map; } /** The nearest palette index of an RGB colour (tools, slow). */ public static function nearest(int $r, int $g, int $b): int { $best = 0; $bestD = PHP_INT_MAX; foreach (self::rgb() as $i => [$pr, $pg, $pb]) { if ($i === self::TRANSPARENT) { continue; } $d = ($pr - $r) ** 2 * 3 + ($pg - $g) ** 2 * 4 + ($pb - $b) ** 2 * 2; if ($d < $bestD) { $bestD = $d; $best = $i; } } return $best; }}