* @category Class * @license https://www.gnu.org/licenses/lgpl-3.0.en.html GNU Lesser General Public License * @link www.splendidbear.org * @since 2026. 04. 14. */ class BattleCardGenerator { private const W = 1200; private const H = 630; private const FONT = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'; public function __construct(private readonly string $cacheDir) { } /** Returns a deterministic UUID v5 for the given battle ID — same battle always maps to the same filename. */ public function cachePath(int $battleId): string { $uuid = Uuid::v5(Uuid::fromString(Uuid::NAMESPACE_URL), 'mineseeker-battle-' . $battleId); return $this->cacheDir . '/' . $uuid->toRfc4122() . '.png'; } public function generate(PlayedGame $game): string { $path = $this->cachePath((int)$game->getId()); if (is_file($path)) { return $path; } if (!is_dir($this->cacheDir)) { mkdir($this->cacheDir, 0755, true); } $this->render($game, $path); return $path; } private function render(PlayedGame $game, string $dest): void { $im = imagecreatetruecolor(self::W, self::H); // Palette $bg = imagecolorallocate($im, 13, 13, 28); $dot = imagecolorallocate($im, 30, 30, 55); $divider = imagecolorallocate($im, 40, 40, 70); $white = imagecolorallocate($im, 230, 230, 240); $muted = imagecolorallocate($im, 90, 90, 115); $red = imagecolorallocate($im, 246, 125, 82); $blue = imagecolorallocate($im, 149, 207, 245); $gold = imagecolorallocate($im, 255, 200, 50); // Background imagefill($im, 0, 0, $bg); // Dot-grid texture for ($x = 40; $x < self::W; $x += 40) { for ($y = 40; $y < self::H; $y += 40) { imagesetpixel($im, $x, $y, $dot); } } // Horizontal accent lines imageline($im, 0, 90, self::W, 90, $divider); imageline($im, 0, self::H - 60, self::W, self::H - 60, $divider); // Vertical centre divider imageline($im, self::W / 2, 110, self::W / 2, self::H - 80, $divider); // Resolve names $redName = $game->getRed()?->getUsername() ?? ($game->getRedAnon() !== null ? 'Anonymous' : 'Guest'); $blueName = $game->getBlue()?->getUsername() ?? ($game->getBlueAnon() !== null ? 'Anonymous' : 'Guest'); $redPts = $game->getRedPoints(); $bluePts = $game->getBluePoints(); $resign = $game->getResign(); // Winner $winner = null; if ($resign === 'red') { $winner = 'blue'; } elseif ($resign === 'blue') { $winner = 'red'; } elseif ($redPts !== null && $bluePts !== null) { if ($redPts > $bluePts) $winner = 'red'; elseif ($bluePts > $redPts) $winner = 'blue'; else $winner = 'draw'; } $this->centeredText($im, 'MineSeeker', 20, self::W / 2, 58, $muted); $this->centeredText($im, 'RED', 16, self::W / 4, 130, $red); $this->centeredText($im, 'BLUE', 16, self::W * 3 / 4, 130, $blue); $redColor = $winner === 'red' ? $gold : ($winner === 'draw' ? $white : $red); $blueColor = $winner === 'blue' ? $gold : ($winner === 'draw' ? $white : $blue); $this->centeredTextFit($im, $redName, 48, self::W / 4, 265, $redColor, self::W / 2 - 80); $this->centeredTextFit($im, $blueName, 48, self::W * 3 / 4, 265, $blueColor, self::W / 2 - 80); $scoreText = $redPts !== null && $bluePts !== null ? $redPts . ' : ' . $bluePts : 'VS'; $this->centeredText($im, $scoreText, 72, self::W / 2, 390, $white); if ($winner === 'red') { $resultText = $redName . ' wins'; $resultColor = $gold; } elseif ($winner === 'blue') { $resultText = $blueName . ' wins'; $resultColor = $gold; } elseif ($winner === 'draw') { $resultText = 'Draw'; $resultColor = $muted; } else { $resultText = ''; $resultColor = $muted; } if ($resultText !== '') { $this->centeredText($im, $resultText, 30, self::W / 2, 460, $resultColor); } if ($resign) { $this->centeredText($im, ucfirst($resign) . ' resigned', 18, self::W / 2, 498, $muted); } $this->centeredText($im, 'mineseeker.hu', 16, self::W / 2, self::H - 20, $muted); imagepng($im, $dest); imagedestroy($im); } /** Render text centered on $cx. */ private function centeredText(\GdImage $im, string $text, int $size, int $cx, int $y, int $color): void { $bbox = imagettfbbox($size, 0, self::FONT, $text); $w = $bbox[2] - $bbox[0]; imagettftext($im, $size, 0, (int)($cx - $w / 2), $y, $color, self::FONT, $text); } /** Render text centered on $cx, shrinking font size to fit $maxWidth. */ private function centeredTextFit( \GdImage $im, string $text, int $size, int $cx, int $y, int $color, int $maxWidth ): void { $bbox = imagettfbbox($size, 0, self::FONT, $text); $w = $bbox[2] - $bbox[0]; if ($w > $maxWidth) { $size = (int)($size * $maxWidth / $w); } $this->centeredText($im, $text, $size, $cx, $y, $color); } }