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)]); } } }