JA
games/doom-jumphp / tools / art / Models.php · commit 8e1cb9f
<?phpnamespace Tools\Art;use App\Engine\Palette as P;/** * The 3D models of IRON ABYSS's creatures, items and weapons (original designs), built from primitives for * Raster3d. Each builder takes a pose and returns a model; make-art.php photographs them. */final class Models{    // ------------------------------------------------------------------ monsters    /** Frames of each monster: letter => pose. 'rot' false = one picture for every angle. */    public const MONSTERS = [        'HUSK' => ['size' => 112, 'frames' => [            'A' => ['walk' => 0.0], 'B' => ['walk' => 0.25], 'C' => ['walk' => 0.5], 'D' => ['walk' => 0.75],            'E' => ['aim' => 1.0], 'F' => ['aim' => 1.0, 'flash' => true], 'G' => ['pain' => true],            'H' => ['die' => 0.25], 'I' => ['die' => 0.5], 'J' => ['die' => 0.75], 'K' => ['die' => 1.0], 'L' => ['die' => 1.0, 'pool' => true],        ]],        'GNSH' => ['size' => 112, 'frames' => [            'A' => ['walk' => 0.0], 'B' => ['walk' => 0.25], 'C' => ['walk' => 0.5], 'D' => ['walk' => 0.75],            'E' => ['bite' => 0.4], 'F' => ['bite' => 1.0], 'G' => ['pain' => true],            'H' => ['die' => 0.3], 'I' => ['die' => 0.6], 'J' => ['die' => 1.0], 'K' => ['die' => 1.0, 'pool' => true],        ]],        'SPIT' => ['size' => 112, 'frames' => [            'A' => ['walk' => 0.0], 'B' => ['walk' => 0.25], 'C' => ['walk' => 0.5], 'D' => ['walk' => 0.75],            'E' => ['raise' => 0.5], 'F' => ['raise' => 1.0, 'ball' => true], 'G' => ['throw' => true], 'H' => ['pain' => true],            'I' => ['die' => 0.3], 'J' => ['die' => 0.6], 'K' => ['die' => 1.0], 'L' => ['die' => 1.0, 'pool' => true],        ]],        'WARD' => ['size' => 176, 'frames' => [            'A' => ['walk' => 0.0], 'B' => ['walk' => 0.25], 'C' => ['walk' => 0.5], 'D' => ['walk' => 0.75],            'E' => ['aim' => 1.0], 'F' => ['aim' => 1.0, 'flash' => true], 'G' => ['pain' => true],            'H' => ['die' => 0.3], 'I' => ['die' => 0.6], 'J' => ['die' => 0.85], 'K' => ['die' => 1.0, 'pool' => true],        ]],    ];    public static function monster(string $name, array $pose): Raster3d    {        return match ($name) {            'HUSK' => self::husk($pose),            'GNSH' => self::gnasher($pose),            'SPIT' => self::spitter($pose),            'WARD' => self::warden($pose),        };    }    /** Legs of a biped: hips at $hipZ, $half = half the hip width. */    private static function legs(Raster3d $m, array $pose, float $hipZ, float $half, float $thigh, float $r, int $ramp, int $bootRamp): void    {        $phase = $pose['walk'] ?? null;        foreach ([1, -1] as $side) {            $swing = $phase === null ? 0.0 : sin(($phase + ($side > 0 ? 0 : 0.5)) * M_PI * 2) * 0.55;            $hip = [0, $side * $half, $hipZ];            $knee = [$hip[0] + sin($swing) * $thigh, $hip[1], $hip[2] - cos($swing) * $thigh];            $shin = $swing - 0.35 * max(0.0, -sin(($phase ?? 0) * M_PI * 2 * $side));            $foot = [$knee[0] + sin($shin * 0.4) * $thigh, $hip[1] * 1.05, max($r * 0.6, $knee[2] - cos($shin * 0.4) * $thigh)];            $m->capsule($hip, $knee, $r, $ramp);            $m->capsule($knee, $foot, $r * 0.85, $ramp);            $m->box([$foot[0] + $r * 0.6, $foot[1], max($r * 0.5, $foot[2] - $r * 0.3)], [$r * 1.5, $r * 0.9, $r * 0.55], $bootRamp);        }    }    private static function deathAndPool(Raster3d $m, array $pose, float $height, int $bloodRamp = P::RED): void    {        if (isset($pose['die'])) {            $t = $pose['die'];            $m->transform(0, [0, 0, 0], 0, -$t * 1.45, 0, [-$t * $height * 0.15, 0, 0], [1, 1, 1 - $t * 0.15]);            if (!empty($pose['pool'])) {                $m->ellipsoid([-$height * 0.35, 4, 1], [$height * 0.42, $height * 0.3, 1.2], $bloodRamp, ['max' => 9]);            }        } elseif (!empty($pose['pain'])) {            $m->transform(0, [0, 0, 0], 0.15, -0.22, 0.08);        }    }    private static function husk(array $pose): Raster3d    {        $m = new Raster3d();        $aim = $pose['aim'] ?? 0.0;        self::legs($m, $pose, 27, 6, 13.5, 3.6, P::STEEL, P::GREY);        // pelvis, torso, chest plate, backpack        $m->box([0, 0, 28], [5, 8, 3.5], P::GREY);        $m->capsule([0, 0, 31], [0, 0, 41], 7.5, P::STEEL, ['spec' => 1]);        $m->box([4.5, 0, 40], [3, 8.5, 6], P::OLIVE, ['bright' => 1]);        $m->box([-6.5, 0, 40], [3.5, 7, 7], P::GREY);        $m->sphere([-9, 3, 45], 1.5, P::RED, ['emit' => true]);        // head with the red visor, antenna        $m->ellipsoid([0, 0, 52.5], [5.5, 5, 5.5], P::STEEL, ['spec' => 1]);        $m->box([4.7, 0, 53], [1.4, 4.2, 1.3], P::RED, ['emit' => true]);        $m->capsule([-2, -3, 57], [-3, -4, 63], 0.6, P::GREY);        $m->sphere([-3, -4, 63.5], 1, P::YELLOW, ['emit' => true]);        // shoulders and arms        foreach ([1, -1] as $side) {            $sh = [0, $side * 11, 45];            $m->sphere($sh, 4.5, P::OLIVE);            $swing = isset($pose['walk']) ? -sin(($pose['walk'] + ($side > 0 ? 0 : 0.5)) * M_PI * 2) * 0.4 : 0.0;            if ($side < 0 && $aim > 0) {                // the gun arm, pointing forward                $elbow = [$sh[0] + 8, $sh[1] + 2, $sh[2] - 4];                $hand = [$elbow[0] + 8, $elbow[1] + 2, $elbow[2] + 2];            } else {                $elbow = [$sh[0] + sin($swing) * 10, $sh[1] + $side * 1.5, $sh[2] - cos($swing) * 10];                $hand = [$elbow[0] + sin($swing + 0.4) * 9, $elbow[1], $elbow[2] - cos($swing + 0.4) * 9];            }            $m->capsule($sh, $elbow, 2.8, P::STEEL);            $m->capsule($elbow, $hand, 2.5, P::STEEL);            if ($side < 0) {                // a rifle bolted to the forearm                $dir = Raster3d::norm([$hand[0] - $elbow[0], $hand[1] - $elbow[1], $hand[2] - $elbow[2]]);                $tip = [$hand[0] + $dir[0] * 9, $hand[1] + $dir[1] * 9, $hand[2] + $dir[2] * 9];                $m->capsule($elbow, $tip, 2.0, P::GREY, ['max' => 10]);                if (!empty($pose['flash'])) {                    $m->sphere([$tip[0] + $dir[0] * 3, $tip[1] + $dir[1] * 3, $tip[2] + $dir[2] * 3], 4.5, P::YELLOW, ['emit' => true]);                    $m->sphere([$tip[0] + $dir[0] * 3, $tip[1] + $dir[1] * 3, $tip[2] + $dir[2] * 3], 2.5, P::GREY, ['emit' => true]);                }            }        }        self::deathAndPool($m, $pose, 60, P::OLIVE);        if (isset($pose['die']) && empty($pose['pool'])) {            $m->sphere([-$pose['die'] * 20, 0, 10 + 30 * (1 - $pose['die'])], 2 + 3 * $pose['die'], P::ORANGE, ['emit' => true]);        }        return $m;    }    private static function gnasher(array $pose): Raster3d    {        $m = new Raster3d();        $bite = $pose['bite'] ?? 0.0;        $lunge = $bite * 6;        $phase = $pose['walk'] ?? null;        // four legs        foreach ([[10, 1, 0.0], [10, -1, 0.5], [-10, 1, 0.5], [-10, -1, 0.0]] as [$lx, $side, $off]) {            $swing = $phase === null ? 0.0 : sin(($phase + $off) * M_PI * 2) * 0.5;            $hip = [$lx, $side * 9, 24];            $knee = [$lx + sin($swing) * 11, $side * 11, 13];            $foot = [$lx + sin($swing) * 4 + 3, $side * 11, 2];            $m->capsule($hip, $knee, 4.5, P::FLESH, ['noise' => 1]);            $m->capsule($knee, $foot, 3.5, P::RUST);            $m->box([$foot[0] + 2, $foot[1], 1.5], [4, 3, 1.5], P::GREY, ['max' => 9]);        }        // body, back ridge        $m->ellipsoid([0, 0, 27], [19, 12.5, 11.5], P::FLESH, ['noise' => 2]);        $m->ellipsoid([-6, 0, 33], [12, 9, 8], P::FLESH, ['noise' => 2, 'bright' => -1]);        for ($i = 0; $i < 5; $i++) {            $m->capsule([-12 + $i * 5, 0, 36 + ($i % 2)], [-14 + $i * 5, 0, 43 + ($i % 2) * 2], 1.2, P::TAN);        }        // head and jaw        $hx = 18 + $lunge;        $m->ellipsoid([$hx, 0, 32], [10, 10, 8.5], P::FLESH, ['noise' => 2]);        $open = 0.25 + $bite * 0.6;        $m->ellipsoid([$hx + 5, 0, 28], [6, 7.5, 3], P::RED, ['max' => 5]);        $jaw = $m->count();        $m->ellipsoid([$hx + 6, 0, 26], [8, 8, 3], P::FLESH, ['noise' => 1]);        for ($i = -2; $i <= 2; $i++) {            $m->box([$hx + 11, $i * 2.6, 28], [0.8, 0.7, 1.8], P::GREY, ['bright' => 3]);        }        $m->transform($jaw, [$hx - 2, 0, 28], 0, $open, 0);        for ($i = -2; $i <= 2; $i++) {            $m->box([$hx + 12, $i * 2.6, 31], [0.8, 0.7, 1.8], P::GREY, ['bright' => 3]);        }        $m->sphere([$hx + 6, 4.5, 36], 1.8, P::YELLOW, ['emit' => true]);        $m->sphere([$hx + 6, -4.5, 36], 1.8, P::YELLOW, ['emit' => true]);        $m->capsule([$hx - 2, 5, 39], [$hx - 7, 8, 45], 1.4, P::TAN);        $m->capsule([$hx - 2, -5, 39], [$hx - 7, -8, 45], 1.4, P::TAN);        if (isset($pose['die'])) {            $t = $pose['die'];            $m->transform(0, [0, 0, 0], 0, 0, $t * 1.4, [0, 0, -$t * 6]);            if (!empty($pose['pool'])) {                $m->ellipsoid([0, -4, 1], [24, 18, 1.2], P::RED, ['max' => 9]);            }        } elseif (!empty($pose['pain'])) {            $m->transform(0, [0, 0, 0], 0.2, -0.15, 0.1);        }        return $m;    }    private static function spitter(array $pose): Raster3d    {        $m = new Raster3d();        self::legs($m, $pose, 24, 6, 12, 3.6, P::BROWN, P::BROWN);        // hunched body        $m->ellipsoid([0, 0, 30], [7, 8.5, 9], P::BROWN, ['noise' => 2]);        $m->ellipsoid([2, 0, 41], [9, 11, 8], P::BROWN, ['noise' => 2, 'bright' => 1]);        for ($i = 0; $i < 4; $i++) {            foreach ([1, -1] as $s) {                $m->capsule([-4, $s * (3 + $i * 2), 44 - $i * 2], [-11, $s * (5 + $i * 3), 49 - $i], 1.1, P::TAN, ['bright' => 1]);            }        }        // head pushed forward, green eyes, mouth        $m->ellipsoid([8, 0, 47], [6.5, 5.5, 5.5], P::BROWN, ['noise' => 1]);        $m->sphere([13.2, 2.4, 48.5], 1.3, P::GREEN, ['emit' => true]);        $m->sphere([13.2, -2.4, 48.5], 1.3, P::GREEN, ['emit' => true]);        $m->box([13, 0, 44.5], [1, 2.6, 0.8], P::RED, ['max' => 6]);        $raise = $pose['raise'] ?? 0.0;        foreach ([1, -1] as $side) {            $sh = [2, $side * 11, 43];            $swing = isset($pose['walk']) ? -sin(($pose['walk'] + ($side > 0 ? 0 : 0.5)) * M_PI * 2) * 0.4 : 0.0;            if ($side < 0 && ($raise > 0 || !empty($pose['throw']))) {                if (!empty($pose['throw'])) {                    $elbow = [$sh[0] + 9, $sh[1] + 2, $sh[2] + 2];                    $hand = [$elbow[0] + 9, $elbow[1] + 2, $elbow[2] - 2];                } else {                    $elbow = [$sh[0] - 2, $sh[1] - 1, $sh[2] + 6 * $raise + 2];                    $hand = [$elbow[0] - 1, $elbow[1] + 2, $elbow[2] + 9 * $raise];                }            } else {                $elbow = [$sh[0] + 3 + sin($swing) * 8, $sh[1] + $side, $sh[2] - 9];                $hand = [$elbow[0] + 6, $elbow[1], $elbow[2] - 6];            }            $m->capsule($sh, $elbow, 2.6, P::BROWN);            $m->capsule($elbow, $hand, 2.3, P::BROWN);            for ($c = -1; $c <= 1; $c++) {                $m->capsule($hand, [$hand[0] + 3.5, $hand[1] + $c * 1.6, $hand[2] - 2.5], 0.7, P::TAN, ['bright' => 2]);            }            if ($side < 0 && !empty($pose['ball'])) {                $m->sphere([$hand[0], $hand[1], $hand[2] + 4], 4.5, P::GREEN, ['emit' => true]);            }        }        self::deathAndPool($m, $pose, 56, P::GREEN);        return $m;    }    private static function warden(array $pose): Raster3d    {        $m = new Raster3d();        $aim = $pose['aim'] ?? 0.0;        self::legs($m, $pose, 44, 13, 22, 7.5, P::PURPLE, P::STEEL);        $m->box([0, 0, 46], [9, 15, 5], P::STEEL);        $m->ellipsoid([0, 0, 66], [16, 22, 18], P::PURPLE, ['noise' => 1.5]);        $m->box([10, 0, 70], [6, 16, 12], P::STEEL, ['spec' => 1, 'bright' => 1]);        $m->box([16.5, 0, 72], [0.6, 10, 2], P::YELLOW, ['emit' => true]);        $m->ellipsoid([4, 0, 88], [8, 8, 7], P::PURPLE);        $m->box([11, 0, 89], [1.2, 5, 1.4], P::YELLOW, ['emit' => true]);        foreach ([1, -1] as $side) {            $m->ellipsoid([0, $side * 24, 80], [10, 9, 8], P::STEEL, ['spec' => 1]);            for ($i = 0; $i < 3; $i++) {                $m->capsule([-2 + $i * 3, $side * 26, 86], [-4 + $i * 3, $side * 29, 95 - $i], 1.4, P::TAN);            }        }        // the right arm: a fist; the left arm: a plasma cannon        $m->capsule([0, 25, 76], [6, 28, 58], 6, P::PURPLE, ['noise' => 1]);        $m->sphere([8, 28, 52], 6.5, P::STEEL);        $cannonEnd = $aim > 0 ? [36, -24, 66] : [22, -28, 52];        $m->capsule([0, -25, 76], [8, -28, 62], 6, P::PURPLE, ['noise' => 1]);        $m->capsule([8, -28, 62], $cannonEnd, 7, P::STEEL, ['spec' => 1]);        $m->sphere($cannonEnd, 4, P::BLUE, ['emit' => true]);        if (!empty($pose['flash'])) {            $m->sphere([$cannonEnd[0] + 6, $cannonEnd[1], $cannonEnd[2] + ($aim > 0 ? 1 : -1)], 8, P::CYAN, ['emit' => true]);        }        self::deathAndPool($m, $pose, 100, P::PURPLE);        return $m;    }    // ------------------------------------------------------------------ things without rotations    /** name => [frames => [letter => model builder], 'elev' => radians, 'size' => px] */    public static function things(): array    {        $T = [];        $T['MEDK'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 6], [7, 10, 6], P::GREY, ['bright' => 1]);            $m->box([0, 0, 12.3], [2, 7, 0.4], P::RED, ['bright' => 2]);            $m->box([0, 0, 12.3], [6, 2, 0.4], P::RED, ['bright' => 2]);            $m->box([7.2, 0, 7], [0.3, 7, 2], P::RED, ['bright' => 2]);            $m->box([7.2, 0, 7], [0.3, 2, 5], P::RED, ['bright' => 2]);        }];        $T['STIM'] = ['A' => function (Raster3d $m) {            $m->capsule([0, 0, 2.5], [0, 0, 11], 3, P::CYAN, ['spec' => 1, 'bright' => 1]);            $m->box([0, 0, 13], [2.5, 2.5, 1.5], P::GREY, ['bright' => 2]);            $m->box([3, 0, 7], [0.2, 1.5, 2], P::RED, ['bright' => 3]);        }];        $T['VIAL'] = ['A' => fn (Raster3d $m) => self::vial($m, 11), 'B' => fn (Raster3d $m) => self::vial($m, 14)];        $T['ARM1'] = ['A' => fn (Raster3d $m) => self::vest($m, P::GREEN, false), 'B' => fn (Raster3d $m) => self::vest($m, P::GREEN, true)];        $T['ARM2'] = ['A' => fn (Raster3d $m) => self::vest($m, P::BLUE, false), 'B' => fn (Raster3d $m) => self::vest($m, P::BLUE, true)];        $T['SHRD'] = ['A' => fn (Raster3d $m) => self::shard($m, 0), 'B' => fn (Raster3d $m) => self::shard($m, 3)];        $T['CLIP'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 4], [2, 4, 4], P::OLIVE);            $m->box([0, 1, 9], [1.5, 1, 1], P::YELLOW, ['bright' => 2]);        }];        $T['AMMO'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 6], [7, 12, 6], P::OLIVE);            $m->box([7.2, 0, 7], [0.2, 9, 2.5], P::YELLOW, ['bright' => 2]);            for ($i = -2; $i <= 2; $i++) {                $m->capsule([0, $i * 3.5, 12], [0, $i * 3.5, 15], 1.3, P::YELLOW);            }        }];        $T['SHEL'] = ['A' => function (Raster3d $m) {            foreach ([-4.5, -1.5, 1.5, 4.5] as $y) {                $m->capsule([0, $y, 1.4], [0, $y, 8], 1.4, P::RED, ['bright' => 1]);                $m->capsule([0, $y, 1.4], [0, $y, 2.6], 1.5, P::YELLOW, ['bright' => 1]);            }        }];        $T['SBOX'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 5], [8, 13, 5], P::RED, ['bright' => -2]);            $m->box([8.2, 0, 5], [0.2, 10, 2], P::YELLOW, ['bright' => 2]);            $m->box([0, 0, 10.2], [7, 12, 0.2], P::BRICK);        }];        $T['CELL'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 6], [5, 9, 6], P::STEEL);            $m->box([5.3, 0, 7], [0.4, 6, 3], P::CYAN, ['emit' => true]);            $m->box([0, 0, 13], [3, 3, 1], P::GREY);        }];        foreach (['RKEY' => P::RED, 'BKEY' => P::BLUE, 'YKEY' => P::YELLOW] as $name => $ramp) {            $T[$name] = ['A' => fn (Raster3d $m) => self::card($m, $ramp, false), 'B' => fn (Raster3d $m) => self::card($m, $ramp, true)];        }        $T['ORB_'] = [];        foreach (['A' => 0, 'B' => 1, 'C' => 2, 'D' => 1] as $f => $k) {            $T['ORB_'][$f] = function (Raster3d $m) use ($k) {                $m->sphere([0, 0, 18], 10 + $k, P::BLUE, ['emit' => true]);                $m->sphere([0, 0, 18], 6 + $k, P::CYAN, ['emit' => true]);                $m->sphere([3, -3, 21], 2.5, P::GREY, ['emit' => true]);            };        }        $T['SCAT'] = ['A' => fn (Raster3d $m) => self::lying($m, fn (Raster3d $g) => self::scattergun($g, 0))];        $T['RIVT'] = ['A' => fn (Raster3d $m) => self::lying($m, fn (Raster3d $g) => self::riveter($g, 0, false))];        $T['IONG'] = ['A' => fn (Raster3d $m) => self::lying($m, fn (Raster3d $g) => self::ionGun($g, false))];        // decorations        $T['BARL'] = ['A' => fn (Raster3d $m) => self::barrel($m, 9), 'B' => fn (Raster3d $m) => self::barrel($m, 13)];        $T['BEXP'] = [];        foreach (['A' => 0.25, 'B' => 0.5, 'C' => 0.8, 'D' => 1.0, 'E' => 0.7] as $f => $k) {            $T['BEXP'][$f] = fn (Raster3d $m) => self::boom($m, $k, 28, 20);        }        $T['LAMP'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 2], [7, 7, 2], P::GREY);            $m->capsule([0, 0, 3], [0, 0, 50], 2, P::STEEL, ['spec' => 1]);            $m->capsule([0, 0, 50], [0, 0, 60], 5, P::YELLOW, ['emit' => true]);            $m->box([0, 0, 62], [5, 5, 1.5], P::GREY);        }];        $T['COLU'] = ['A' => function (Raster3d $m) {            $m->box([0, 0, 4], [10, 10, 4], P::STEEL);            $m->box([0, 0, 34], [7, 7, 26], P::STEEL, ['spec' => 1]);            $m->box([0, 0, 40], [7.3, 7.3, 4], P::CYAN, ['emit' => true]);            $m->box([0, 0, 63], [10, 10, 3], P::STEEL);        }];        $T['TREE'] = ['A' => function (Raster3d $m) {            $m->capsule([0, 0, 0], [2, 1, 40], 3, P::BROWN, ['noise' => 2]);            foreach ([[2, 1, 40, -8, 6, 54], [2, 1, 40, 10, -4, 56], [1, 0, 28, -10, -8, 38], [1, 0, 30, 9, 9, 42]] as $b) {                $m->capsule([$b[0], $b[1], $b[2]], [$b[3], $b[4], $b[5]], 1.5, P::BROWN, ['noise' => 2]);            }        }];        $T['SKUL'] = ['A' => function (Raster3d $m) {            foreach ([[0, 0, 4], [6, 5, 4], [-5, 6, 4], [1, 3, 11], [-4, -5, 4], [5, -5, 4]] as $p) {                $m->ellipsoid($p, [4, 3.5, 3.5], P::TAN, ['bright' => 2]);                $m->sphere([$p[0] + 3.3, $p[1] + 1.3, $p[2] + 0.5], 1, P::GREY, ['max' => 1]);                $m->sphere([$p[0] + 3.3, $p[1] - 1.3, $p[2] + 0.5], 1, P::GREY, ['max' => 1]);            }        }];        // effects        $T['PUFF'] = [];        foreach (['A' => 1.0, 'B' => 0.8, 'C' => 0.6, 'D' => 0.4] as $f => $k) {            $T['PUFF'][$f] = function (Raster3d $m) use ($k, $f) {                $m->sphere([0, 0, 4], 3.5 * $k, $f === 'A' ? P::YELLOW : P::GREY, ['emit' => $f === 'A', 'max' => 12]);                if ($f !== 'A') {                    $m->sphere([1, 2, 7], 2.2 * $k, P::GREY, ['max' => 10]);                }            };        }        $T['BLUD'] = [];        foreach (['A' => 1.0, 'B' => 0.75, 'C' => 0.5] as $f => $k) {            $T['BLUD'][$f] = function (Raster3d $m) use ($k) {                $m->sphere([0, 0, 4], 2.5 * $k + 0.6, P::RED, ['max' => 10]);                $m->sphere([1, 2.5, 1], 1.4 * $k + 0.4, P::RED, ['max' => 9]);                $m->sphere([-1, -2.5, 2], 1.2 * $k + 0.4, P::RED, ['max' => 8]);            };        }        foreach (['ACID' => [P::GREEN, P::YELLOW], 'PLSM' => [P::BLUE, P::CYAN], 'IONB' => [P::CYAN, P::GREY]] as $name => [$a, $b]) {            $T[$name] = [                'A' => fn (Raster3d $m) => self::ball($m, $a, $b, 0),                'B' => fn (Raster3d $m) => self::ball($m, $a, $b, 1),                'C' => fn (Raster3d $m) => self::boom($m, 0.5, 14, 10, $a, $b),                'D' => fn (Raster3d $m) => self::boom($m, 0.85, 14, 10, $a, $b),                'E' => fn (Raster3d $m) => self::boom($m, 0.6, 14, 10, $a, P::GREY),            ];        }        return $T;    }    private static function vial(Raster3d $m, int $glow): void    {        $m->capsule([0, 0, 3], [0, 0, 9], 3.2, P::BLUE, ['emit' => $glow > 12]);        $m->capsule([0, 0, 10], [0, 0, 13], 1.2, P::GREY);        $m->sphere([0, 0, 5], 2.2, P::CYAN, ['emit' => true]);    }    private static function vest(Raster3d $m, int $ramp, bool $shine): void    {        $m->ellipsoid([0, 0, 9], [5, 11, 9], $ramp, ['bright' => $shine ? 2 : 0, 'spec' => 1]);        $m->box([4, 0, 10], [1.5, 7, 6], P::GREY, ['bright' => $shine ? 3 : 1]);        $m->ellipsoid([0, 8, 17], [4, 4, 2.5], $ramp, ['bright' => 1]);        $m->ellipsoid([0, -8, 17], [4, 4, 2.5], $ramp, ['bright' => 1]);    }    private static function shard(Raster3d $m, int $b): void    {        $m->ellipsoid([0, 0, 5], [6, 6, 6], P::GREEN, ['bright' => $b, 'spec' => 1]);        $m->box([0, 0, 1], [7, 7, 1], P::GREY);        $m->box([5.5, 0, 6], [0.8, 4, 1.2], P::YELLOW, ['emit' => true]);    }    private static function card(Raster3d $m, int $ramp, bool $blink): void    {        $m->box([0, 0, 8], [0.8, 5, 7], $ramp, ['emit' => $blink, 'bright' => 2]);        $m->box([0.9, 0, 11], [0.1, 3, 2], P::GREY, ['bright' => 4]);    }    private static function barrel(Raster3d $m, int $glow): void    {        $m->capsule([0, 0, 4], [0, 0, 26], 10, P::STEEL, ['spec' => 1]);        foreach ([8, 22] as $z) {            $m->capsule([0, 0, $z - 1], [0, 0, $z + 1], 10.6, P::GREY);        }        $m->capsule([0, 0, 13], [0, 0, 17], 10.3, P::YELLOW, ['bright' => 1]);        $m->ellipsoid([0, 0, 31], [8, 8, 2], P::GREEN, ['emit' => $glow > 10, 'bright' => 2]);    }    private static function boom(Raster3d $m, float $k, float $r, float $z, int $a = P::ORANGE, int $b = P::YELLOW): void    {        $m->sphere([0, 0, $z + $r * $k * 0.3], $r * $k, $a, ['emit' => true]);        $m->sphere([2, 0, $z + $r * $k * 0.3 + 2], $r * $k * 0.6, $b, ['emit' => true]);        $m->sphere([-$r * 0.5 * $k, $r * 0.6 * $k, $z + $r * $k * 0.7], $r * $k * 0.45, $a, ['emit' => true]);        $m->sphere([$r * 0.4 * $k, -$r * 0.6 * $k, $z + $r * $k * 0.6], $r * $k * 0.4, P::RED, ['emit' => true]);    }    private static function ball(Raster3d $m, int $a, int $b, int $k): void    {        $m->sphere([0, 0, 6], 6 + $k, $a, ['emit' => true]);        $m->sphere([0, 0, 6], 3.5 + $k, $b, ['emit' => true]);        $m->sphere([-6 - $k * 2, 2, 7], 2.5, $a, ['emit' => true]);    }    /** A weapon lying on the floor, seen from the side. */    private static function lying(Raster3d $m, callable $gun): void    {        $gun($m);        $m->transform(0, [0, 0, 0], M_PI / 2, 0, M_PI / 2, [0, 0, 4]);    }    // ------------------------------------------------------------------ first-person weapons (barrel along +x)    public static function wrench(Raster3d $m, float $swing): void    {        self::hand($m, [10, -9, -14]);        $from = $m->count();        $m->box([24, -9, -13], [16, 1.6, 2.2], P::STEEL, ['spec' => 1]);        $m->box([41, -9, -13], [3, 5, 2.4], P::STEEL, ['spec' => 1, 'bright' => 1]);        $m->box([43, -9, -13], [2, 1.5, 2.6], P::GREY, ['max' => 3]);        $m->box([14, -9, -13], [5, 2.2, 2.8], P::RED, ['bright' => -1]);        $m->transform($from, [10, -9, -14], -0.3 - $swing * 0.9, -0.5 + $swing * 1.6, 0);    }    public static function sidearm(Raster3d $m, int $recoil, bool $flash): void    {        self::hand($m, [8, 0, -15 + $recoil]);        $m->box([16, 0, -12 + $recoil], [11, 2.2, 2.6], P::GREY, ['spec' => 1, 'bright' => 1]);        $m->box([14, 0, -16 + $recoil], [3, 1.8, 4], P::BROWN);        $m->box([26, 0, -10 + $recoil], [1, 0.6, 0.8], P::GREY);        if ($flash) {            self::flash($m, [30, 0, -12 + $recoil], 5);        }    }    public static function scattergun(Raster3d $m, int $pump, bool $flash = false): void    {        self::hand($m, [4, -2, -16]);        self::hand($m, [22 - $pump * 4, 1, -17]);        $m->box([6, 0, -14], [8, 3, 3.5], P::BROWN, ['noise' => 1]);        $m->capsule([10, 0, -11.5], [42, 0, -11.5], 1.8, P::GREY, ['spec' => 1]);        $m->capsule([10, 0, -14.5], [40, 0, -14.5], 1.6, P::GREY, ['spec' => 1]);        $m->box([24 - $pump * 4, 0, -15], [6, 2.4, 2.4], P::BROWN, ['bright' => 1]);        $m->box([-6, 0, -16], [9, 2.8, 4], P::BROWN);        if ($flash) {            self::flash($m, [46, 0, -12], 8);        }    }    public static function riveter(Raster3d $m, int $spin, bool $flash): void    {        self::hand($m, [2, -3, -18]);        self::hand($m, [18, 3, -19]);        $m->box([6, 0, -15], [12, 5, 4.5], P::OLIVE, ['spec' => 1]);        $m->box([6, 0, -9.5], [8, 2, 1.2], P::GREY);        for ($i = 0; $i < 4; $i++) {            $a = $i * M_PI / 2 + $spin * M_PI / 4;            $m->capsule([18, cos($a) * 2.6, -14 + sin($a) * 2.6], [44, cos($a) * 2.6, -14 + sin($a) * 2.6], 1.2, P::GREY, ['spec' => 1]);        }        $m->capsule([40, 0, -14], [41, 0, -14], 4.5, P::STEEL);        if ($flash) {            self::flash($m, [48, 0, -14], 7);        }    }    public static function ionGun(Raster3d $m, bool $flash): void    {        self::hand($m, [4, -3, -18]);        self::hand($m, [16, 4, -19]);        $m->box([12, 0, -14], [16, 5.5, 4.5], P::STEEL, ['spec' => 1]);        $m->box([12, 0, -8.5], [12, 3, 1.2], P::GREY);        $m->capsule([26, 0, -14], [40, 0, -14], 3.4, P::GREY, ['spec' => 1]);        foreach ([14, 20, 26] as $x) {            $m->box([$x, 0, -14], [1.2, 5.8, 4.8], P::CYAN, ['emit' => true]);        }        if ($flash) {            self::flash($m, [44, 0, -14], 7, P::CYAN, P::GREY);        }    }    private static function hand(Raster3d $m, array $p): void    {        $m->capsule([$p[0] - 7, $p[1] - 1, $p[2] - 3], $p, 3.4, P::OLIVE, ['noise' => 1]);        $m->ellipsoid($p, [3.4, 3.2, 3], P::FLESH);    }    private static function flash(Raster3d $m, array $p, float $r, int $a = P::YELLOW, int $b = P::GREY): void    {        $m->sphere($p, $r, $a, ['emit' => true]);        $m->sphere([$p[0] + $r * 0.6, $p[1], $p[2]], $r * 0.55, $b, ['emit' => true]);        $m->sphere([$p[0] + $r * 0.3, $p[1] + $r * 0.7, $p[2] + $r * 0.2], $r * 0.4, P::ORANGE, ['emit' => true]);        $m->sphere([$p[0] + $r * 0.3, $p[1] - $r * 0.7, $p[2] - $r * 0.2], $r * 0.4, P::ORANGE, ['emit' => true]);    }    /** First-person weapon frames: name => [letter => fn(Raster3d)] */    public static function weapons(): array    {        return [            'WRCH' => ['A' => fn (Raster3d $m) => self::wrench($m, 0), 'B' => fn (Raster3d $m) => self::wrench($m, 0.35), 'C' => fn (Raster3d $m) => self::wrench($m, 0.75), 'D' => fn (Raster3d $m) => self::wrench($m, 1.0)],            'SIDE' => ['A' => fn (Raster3d $m) => self::sidearm($m, 0, false), 'B' => fn (Raster3d $m) => self::sidearm($m, 2, true), 'C' => fn (Raster3d $m) => self::sidearm($m, 1, false)],            'SCAT' => ['A' => fn (Raster3d $m) => self::scattergun($m, 0), 'B' => fn (Raster3d $m) => self::scattergun($m, 0, true), 'C' => fn (Raster3d $m) => self::scattergun($m, 1), 'D' => fn (Raster3d $m) => self::scattergun($m, 2)],            'RIVT' => ['A' => fn (Raster3d $m) => self::riveter($m, 0, false), 'B' => fn (Raster3d $m) => self::riveter($m, 1, true), 'C' => fn (Raster3d $m) => self::riveter($m, 2, true)],            'IONG' => ['A' => fn (Raster3d $m) => self::ionGun($m, false), 'B' => fn (Raster3d $m) => self::ionGun($m, true)],        ];    }    /** The helmet of the status bar: $damage 0..4 (cracks), $dead. */    public static function helmet(Raster3d $m, int $damage, bool $hurt): void    {        $m->ellipsoid([0, 0, 12], [10, 10, 11], P::OLIVE, ['spec' => 1, 'bright' => 1]);        $m->box([8, 0, 12], [3, 7.5, 3.6], $hurt ? P::RED : P::CYAN, ['emit' => true]);        $m->box([8.5, 0, 12], [2.8, 7.8, 0.5], P::GREY, ['max' => 4]);        $m->box([0, 0, 2], [8, 8, 2.5], P::GREY);        for ($i = 0; $i < $damage; $i++) {            $y = -6 + $i * 3.5;            $m->box([9.5, $y, 13 + ($i % 2) * 2], [2.2, 0.35, 2.5], P::GREY, ['max' => 2, 'roll' => 0.6 * (($i % 2) * 2 - 1)]);        }    }}