JA
games/doom-jumphp / tools / make-art.php · commit 8e1cb9f
<?php/** * Builds every picture and sound of IRON ABYSS from code (original art, no external file): *   bin/jumphp php games/doom-jumphp tools/make-art.php [preview] * Writes assets/data/art.pak (serialized palette pictures), assets/sfx/*.wav and, with "preview", * var/art-preview/*.png to look at them. */require __DIR__ . '/../vendor/autoload.php';foreach (['Noise', 'Canvas', 'Raster3d', 'Textures', 'Models', 'Sounds'] as $class) {    require __DIR__ . "/art/$class.php";}use App\Engine\Palette as P;use Tools\Art\Canvas;use Tools\Art\Models;use Tools\Art\Raster3d;use Tools\Art\Sounds;use Tools\Art\Textures;$root = dirname(__DIR__);$preview = in_array('preview', $argv, true);$only = null;foreach ($argv as $a) {    if (str_starts_with($a, 'only=')) {        $only = substr($a, 5);    }}$t0 = microtime(true);$log = fn (string $m) => 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');}