fwrite(STDERR, sprintf("[%5.1fs] %s\n", microtime(true) - $t0, $m)); /** Row-major sprite (Raster3d::crop) -> column-major + opaque range of each column. */ function columns(array $s): array { $cols = ''; $ranges = []; for ($x = 0; $x < $s['w']; $x++) { $first = -1; $last = -1; for ($y = 0; $y < $s['h']; $y++) { $c = $s['px'][$y * $s['w'] + $x]; $cols .= $c; if ($c !== "\xFF") { if ($first < 0) { $first = $y; } $last = $y; } } $ranges[] = [$first, $last]; } return ['w' => $s['w'], 'h' => $s['h'], 'left' => $s['left'], 'top' => $s['top'], 'px' => $cols, 'ranges' => $ranges]; } function png(string $file, int $w, int $h, string $rowMajor, int $scale = 2): void { $img = imagecreatetruecolor($w * $scale, $h * $scale); $rgb = P::rgb(); $cols = []; imagefill($img, 0, 0, imagecolorallocate($img, 255, 0, 255)); for ($y = 0; $y < $h; $y++) { for ($x = 0; $x < $w; $x++) { $i = ord($rowMajor[$y * $w + $x]); if ($i === P::TRANSPARENT) { continue; } $cols[$i] ??= imagecolorallocate($img, ...$rgb[$i]); imagefilledrectangle($img, $x * $scale, $y * $scale, $x * $scale + $scale - 1, $y * $scale + $scale - 1, $cols[$i]); } } imagepng($img, $file); } $pakFile = "$root/assets/data/art.pak"; $pak = is_file($pakFile) && $only !== null ? unserialize((string)file_get_contents($pakFile)) : []; $prev = "$root/var/art-preview"; if ($preview) { @mkdir($prev, 0777, true); } if ($only === null || $only === 'tex') { $pak['walls'] = []; foreach (Textures::walls() as $name => $c) { $pak['walls'][$name] = ['w' => $c->w, 'h' => $c->h, 'px' => $c->colMajor()]; $preview && png("$prev/wall-$name.png", $c->w, $c->h, $c->rowMajor()); } $pak['flats'] = []; foreach (Textures::flats() as $name => $c) { $pak['flats'][$name] = $c->rowMajor(); $preview && png("$prev/flat-$name.png", 64, 64, $c->rowMajor()); } $sky = Textures::sky(); $pak['sky'] = ['w' => 256, 'h' => 128, 'px' => $sky->colMajor()]; $preview && png("$prev/sky.png", 256, 128, $sky->rowMajor()); $log('textures'); } if ($only === null || $only === 'mon') { foreach (Models::MONSTERS as $name => $def) { $pak['sprites'][$name] = []; foreach ($def['frames'] as $letter => $pose) { $rotations = isset($pose['die']) ? [0] : range(1, 8); foreach ($rotations as $rot) { $model = Models::monster($name, $pose); $view = $rot === 0 ? 0.0 : ($rot - 1) * M_PI / 4; $s = $model->render($view, isset($pose['die']) ? 0.25 * $pose['die'] : 0.0, $def['size']); $pak['sprites'][$name][$letter][$rot] = columns($s); if ($preview && in_array($rot, [0, 1, 3, 5], true)) { png("$prev/spr-$name$letter$rot.png", $s['w'], $s['h'], $s['px']); } } } $log("monster $name"); } } if ($only === null || $only === 'things') { foreach (Models::things() as $name => $frames) { $pak['sprites'][$name] = []; foreach ($frames as $letter => $build) { $m = new Raster3d(); $build($m); $big = in_array($name, ['BEXP', 'LAMP', 'COLU', 'TREE'], true); $s = $m->render(0.35, in_array($name, ['PUFF', 'BLUD', 'ACID', 'PLSM', 'IONB', 'BEXP', 'ORB_', 'LAMP', 'COLU', 'TREE'], true) ? 0.0 : 0.45, $big ? 112 : 64); $pak['sprites'][$name][$letter][0] = columns($s); $preview && png("$prev/thing-$name$letter.png", $s['w'], $s['h'], $s['px']); } } $log('things'); } if ($only === null || $only === 'weapons') { $pak['weapons'] = []; foreach (Models::weapons() as $name => $frames) { foreach ($frames as $letter => $build) { $m = new Raster3d(); $build($m); $ox = 172; $oy = 135; $s = $m->render(M_PI - 0.22, 0.62, 320, 2.8, ['w' => 320, 'h' => 168, 'ox' => $ox, 'oy' => $oy]); // left / top relative to the view's centre (160, 84): the picture lands where it was drawn $s['left'] += 160 - $ox; $s['top'] += 84 - $oy; $pak['weapons'][$name][$letter] = columns($s); $preview && png("$prev/weap-$name$letter.png", $s['w'], $s['h'], $s['px']); } } $log('weapons'); } if ($only === null || $only === 'hud') { $hud = []; // status bar 320 x 32 $bar = new Canvas(320, 32); $bar->plate(0, 0, 320, 32, P::STEEL, 6, 1, 1.5); $boxes = ['AMMO' => [2, 48], 'HEALTH' => [52, 60], 'ARMS' => [114, 38], '' => [154, 32], 'ARMOR' => [188, 60], 'KEYS' => [250, 14], 'STOCK' => [266, 52]]; foreach ($boxes as $label => [$x, $w]) { $bar->plate($x, 2, $w, 28, P::GREY, 3, 1, 1, true); if ($label !== '' && $label !== 'KEYS' && $label !== 'STOCK') { $bar->text($x + (int)(($w - strlen($label) * 4) / 2), 24, $label, P::index(P::GREY, 10)); } } foreach ([2, 3, 4, 5] as $k => $n) { $bar->plate(118 + ($k % 2) * 16, 5 + intdiv($k, 2) * 12, 14, 10, P::GREY, 4, 1); } foreach (['BUL', 'SHL', 'CEL'] as $k => $label) { $bar->text(270, 5 + $k * 8, $label, P::index(P::GREY, 9)); } $hud['STBAR'] = ['w' => 320, 'h' => 32, 'px' => $bar->rowMajor()]; $preview && png("$prev/hud-stbar.png", 320, 32, $bar->rowMajor(), 3); // big digits 0-9 and % foreach (array_merge(range(0, 9), ['%']) as $d) { $c = new Canvas(14, 18); $c->text(1, 1, (string)$d, P::index(P::RED, 6), 3); $c->text(0, 0, (string)$d, P::index(P::RED, 13), 3); $c->paint(fn (int $x, int $y) => $c->get($x, $y) === P::index(P::RED, 13) ? P::index($y < 6 ? P::ORANGE : P::RED, 15 - intdiv($y, 4)) : null); $hud['BIG' . $d] = ['w' => 14, 'h' => 18, 'px' => $c->rowMajor()]; } // small digits (yellow and grey) foreach (range(0, 9) as $d) { foreach (['Y' => P::index(P::YELLOW, 14), 'G' => P::index(P::GREY, 8)] as $tone => $color) { $c = new Canvas(4, 5); $c->text(0, 0, (string)$d, $color); $hud["S$tone$d"] = ['w' => 4, 'h' => 5, 'px' => $c->rowMajor()]; } } // the helmet portraits: 5 damage levels, hurt variant for ($dmg = 0; $dmg <= 4; $dmg++) { foreach ([false, true] as $hurt) { $m = new Raster3d(); Models::helmet($m, $dmg, $hurt); $s = $m->render(0.25, 0.1, 40, 1.15); $c = new Canvas(30, 28); $c->blit($s, 15 - (int)($s['w'] / 2), 26 - $s['h']); $hud['FACE' . $dmg . ($hurt ? 'H' : '')] = ['w' => 30, 'h' => 28, 'px' => $c->rowMajor()]; $preview && png("$prev/hud-face$dmg" . ($hurt ? 'H' : '') . '.png', 30, 28, $c->rowMajor(), 4); } } // keys foreach (['R' => P::RED, 'B' => P::BLUE, 'Y' => P::YELLOW] as $k => $ramp) { $c = new Canvas(8, 7); $c->plate(0, 0, 8, 7, $ramp, 12, 1); $c->rect(2, 2, 4, 1, P::index(P::GREY, 14)); $hud["KEY$k"] = ['w' => 8, 'h' => 7, 'px' => $c->rowMajor()]; } // the logo $title = 'IRON ABYSS'; $scale = 6; $logo = new Canvas(strlen($title) * 4 * $scale + 8, 5 * $scale + 8); $mask = new Canvas($logo->w, $logo->h); $mask->text(4, 4, $title, 1, $scale); $logo->paint(function (int $x, int $y) use ($mask, $scale): ?int { if ($mask->get($x, $y) !== 1) { // a dark outline around the letters foreach ([[-1, 0], [1, 0], [0, -1], [0, 1], [-2, 0], [2, 0], [0, 2], [2, 2]] as [$dx, $dy]) { if ($mask->get($x + $dx, $y + $dy) === 1) { return P::index(P::GREY, 1); } } return null; } $lit = $mask->get($x - 1, $y) !== 1 || $mask->get($x, $y - 1) !== 1; $dark = $mask->get($x + 1, $y) !== 1 || $mask->get($x, $y + 1) !== 1; $g = ($y - 4) / (5 * $scale); $n = \Tools\Art\Noise::hash($x, $y) * 2; $ramp = $g < 0.45 ? P::YELLOW : ($g < 0.75 ? P::ORANGE : P::RED); $s = 14 - (int)($g * 6) + (int)$n + ($lit ? 2 : 0) - ($dark ? 5 : 0); return P::index($ramp, $s); }); $hud['LOGO'] = ['w' => $logo->w, 'h' => $logo->h, 'px' => $logo->rowMajor()]; $preview && png("$prev/logo.png", $logo->w, $logo->h, $logo->rowMajor(), 2); $pak['hud'] = $hud; $log('hud'); } @mkdir("$root/assets/data", 0777, true); file_put_contents($pakFile, serialize($pak)); $log('art.pak: ' . round(filesize($pakFile) / 1024) . ' KB'); if ($only === null || $only === 'sfx') { @mkdir("$root/assets/sfx", 0777, true); foreach (Sounds::all() as $name => $wav) { file_put_contents("$root/assets/sfx/$name.wav", $wav); } $log('sounds'); }