ZH

Elfs & Orcs Tower Defense

作者:malabke · 策略
5.0 (1)
▶ 开玩 💬 反馈 源码 🕘 版本 编辑 GitHub
games/elfs-orcs-tower-defense / tests / ViewTest.php · commit 65500d9
<?phpnamespace Tests;use App\Td\GameData;use App\View3d\GroundMap;use App\View3d\MapModel;use App\View3d\Mesh;use App\View3d\Props;use App\View3d\Rig;use App\View3d\Skeleton;use App\View3d\Towers;use App\View3d\Units;use PHPUnit\Framework\Attributes\DataProvider;use PHPUnit\Framework\TestCase;/** * The 3D side without a window: every model builds with sound vertices, the camera shows every platform, a click on * the screen finds the ground point it was cast from. */final class ViewTest extends TestCase{    private static function data(): GameData    {        return new GameData(dirname(__DIR__) . '/assets/data');    }    /** No NaN, no infinity, unit normals, colours in range, whole triangles. */    private static function assertSound(Mesh $m, string $what): void    {        self::assertGreaterThan(0, $m->vertexCount(), "$what is empty");        self::assertSame(0, $m->vertexCount() % 3, "$what has a broken triangle");        foreach ($m->v as $i => $value) {            self::assertTrue(is_finite($value), "$what: vertex float $i is $value");        }        for ($i = 0, $n = count($m->v); $i < $n; $i += Mesh::STRIDE * 7) {            $len = sqrt($m->v[$i + 3] ** 2 + $m->v[$i + 4] ** 2 + $m->v[$i + 5] ** 2);            self::assertEqualsWithDelta(1.0, $len, 0.01, "$what: a normal is not a unit vector");            foreach ([6, 7, 8] as $k) {                self::assertGreaterThanOrEqual(0.0, $m->v[$i + $k]);                self::assertLessThan(2.1, $m->v[$i + $k]);            }            $mat = $m->v[$i + 9];            $bone = $mat >= Mesh::METAL_BONE ? $mat - Mesh::METAL_BONE : $mat - Mesh::BONE;            self::assertTrue(($mat >= 0.0 && $mat < 7.0) || ($mat >= Mesh::BONE && $bone >= 0 && $bone < Skeleton::MAX), "$what: unknown material $mat");        }    }    public function testEveryCreatureIsBuiltAndPosedInEveryAnimation(): void    {        foreach (array_keys(Units::KINDS) as $sprite) {            foreach (Units::parts($sprite) as $part) {                $mesh = Units::build($sprite, $part);                self::assertSound($mesh, "$sprite $part");                // every vertex moves with a bone of its skeleton                $bones = Units::skeleton($sprite, $part)->count();                for ($i = 9, $n = count($mesh->v); $i < $n; $i += Mesh::STRIDE) {                    $mat = $mesh->v[$i];                    self::assertGreaterThanOrEqual(Mesh::BONE, $mat, "$sprite $part: a vertex without a bone");                    self::assertLessThan($bones, $mat >= Mesh::METAL_BONE ? $mat - Mesh::METAL_BONE : $mat - Mesh::BONE, "$sprite $part: a bone it has not");                }                foreach (Units::ANIMS as $anim) {                    foreach ([0.0, 0.3, 0.7, 2.6] as $t) {                        $pose = Units::pose($sprite, $anim, $t, $part);                        self::assertCount(16 * Skeleton::MAX, $pose);                        foreach ($pose as $value) {                            self::assertTrue(is_finite($value), "$sprite $part $anim $t: a matrix is not finite");                        }                    }                }            }            self::assertGreaterThan(3.0, Units::height($sprite));            self::assertGreaterThan(3.0, Units::stride($sprite));        }    }    /** People and mounts keep their feet on the ground whatever they do: they neither sink nor float. */    public function testCreaturesKeepTheirFeetOnTheGround(): void    {        foreach (Units::KINDS as $sprite => $d) {            if ($d['k'] === 'wings') {                continue;            }            foreach (['walk', 'fight', 'idle', 'stun'] as $anim) {                foreach ([0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875] as $t) {                    $low = INF;                    foreach (Units::posed(Units::build($sprite), Units::pose($sprite, $anim, $t)) as $p) {                        $low = min($low, $p[1]);                    }                    self::assertGreaterThan(-0.6, $low, "$sprite sinks into the ground ($anim at $t)");                    self::assertLessThan(0.8, $low, "$sprite floats above the ground ($anim at $t)");                }            }        }    }    /** A rider sits on its saddle, above its mount's back, in every pose. */    public function testRidersSitOnTheirMounts(): void    {        foreach (Units::KINDS as $sprite => $d) {            if (!isset($d['rider'])) {                continue;            }            foreach (['walk', 'fight', 'idle'] as $anim) {                [$min] = self::partBounds($sprite, 'rider', $anim, 0.3);                [, $mountMax] = self::partBounds($sprite, 'body', $anim, 0.3);                self::assertGreaterThan(5.0, $min[1], "$sprite: the rider's feet hang along the flanks, above the ground");                self::assertGreaterThan($mountMax[1] - 9.0, $min[1] + 4.0);            }        }    }    /** @return array{0: array<int, float>, 1: array<int, float>} */    private static function partBounds(string $sprite, string $part, string $anim, float $t): array    {        $min = [INF, INF, INF];        $max = [-INF, -INF, -INF];        foreach (Units::posed(Units::build($sprite, $part), Units::pose($sprite, $anim, $t, $part)) as $p) {            for ($a = 0; $a < 3; $a++) {                $min[$a] = min($min[$a], $p[$a]);                $max[$a] = max($max[$a], $p[$a]);            }        }        return [$min, $max];    }    public function testEveryCreatureOfTheDataHasAModel(): void    {        $data = self::data();        $sprites = [];        foreach ($data->enemies as $enemy) {            $sprites[$enemy['sprite']] = true;        }        foreach ($data->heroes as $hero) {            $sprites[$hero['sprite']] = true;        }        foreach ($data->towers as $tower) {            foreach ([$tower['levels'][0]['soldiers']['sprite'] ?? null, $tower['levels'][1]['soldiers']['sprite'] ?? null] as $soldier) {                if ($soldier !== null) {                    $sprites[$soldier] = true;                }            }        }        foreach (array_keys($sprites) as $sprite) {            self::assertArrayHasKey($sprite, Units::KINDS, "no 3D model for the creature '$sprite'");        }    }    public function testEveryTowerOfTheDataHasAModelAtEveryLevelAndSpecialization(): void    {        $sprites = array_unique(array_column(self::data()->towers, 'sprite'));        foreach ($sprites as $sprite) {            self::assertContains($sprite, Towers::SPRITES);        }        foreach (Towers::SPRITES as $sprite) {            foreach ([1, 2, 3] as $level) {                self::assertSound(Towers::build($sprite, $level, null), "$sprite level $level");            }            foreach ([0, 1] as $spec) {                self::assertSound(Towers::build($sprite, 4, $spec), "$sprite spec $spec");            }        }        self::assertGreaterThan(Towers::height(1), Towers::height(3));    }    public function testThePropsBuild(): void    {        foreach (['arrow', 'bolt', 'javelin', 'stone', 'rock', 'pitch', 'spark', 'orb', 'note', 'star'] as $kind) {            self::assertSound(Props::projectile($kind), "projectile $kind");        }        foreach ([[false, true, false], [true, true, false], [false, false, false], [false, true, true]] as [$stone, $standing, $spiked]) {            self::assertSound(Props::wall($stone, $standing, $spiked), 'wall');        }        self::assertSound(Props::trap(true), 'trap');        self::assertSound(Props::trap(false), 'trap');        self::assertSound(Props::mound(), 'mound');        self::assertSound(Props::disc(), 'disc');        self::assertSound(Props::ring(), 'ring');        self::assertSound(Props::ball(), 'ball');    }    public static function maps(): array    {        return array_map(static fn (string $m) => [$m], self::data()->mapIds());    }    #[DataProvider('maps')]    public function testEveryMapBuilds(string $id): void    {        $data = self::data();        $built = MapModel::build($data->map($id), $data->faction('elves')['colors']);        self::assertSound($built['mesh'], "map $id");        // the browser's PHP builds it too, with 128 MB        self::assertLessThan(100000, $built['mesh']->vertexCount(), 'a map stays one light buffer');        self::assertCount(3, $built['fog']);        self::assertSame(GroundMap::W * GroundMap::H * 4, strlen($built['ground']), 'the ground texture is whole');        self::assertSame(0.0, MapModel::height(240.0, 120.0), 'the board is flat (clicks are cast on y = 0)');        self::assertGreaterThan(5.0, MapModel::height(240.0, -90.0), 'hills rise beyond the far edge');    }    /** Every platform (and the tower on it) is on the screen, under the top bar, with a camera that fits the map. */    #[DataProvider('maps')]    public function testTheCameraShowsEveryPlatform(string $id): void    {        $map = self::data()->map($id);        $rig = self::rigFor($map);        foreach ($map['slots'] as [$sx, $sy]) {            $x = ($sx + 1) * 16.0;            $z = ($sy + 1) * 16.0;            foreach ([0.0, 40.0] as $height) {                [$px, $py] = $rig->screen($x, $z, $height);                self::assertGreaterThan(30, $py, "$id: the platform $sx,$sy is under the top bar");                self::assertLessThan(270, $py, "$id: the platform $sx,$sy is below the screen");                self::assertGreaterThan(0, $px);                self::assertLessThan(480, $px);            }        }        [$gx, $gy] = $rig->screen($map['bastion'][0] * 16.0 + 16, $map['bastion'][1] * 16.0 + 48);        self::assertTrue($gx > 0 && $gx < 480 && $gy > 30 && $gy < 270, "$id: the castle's gate is on the screen");    }    #[DataProvider('maps')]    public function testAPointOfTheScreenFindsItsPointOnTheGround(string $id): void    {        $rig = self::rigFor(self::data()->map($id));        foreach ([[60.0, 40.0], [240.0, 120.0], [400.0, 200.0], [30.0, 220.0]] as [$x, $z]) {            [$sx, $sy] = $rig->screen($x, $z);            [$wx, $wz] = $rig->world($sx, $sy);            self::assertEqualsWithDelta($x, $wx, 0.2);            self::assertEqualsWithDelta($z, $wz, 0.2);        }        self::assertGreaterThan(0.4, $rig->scale(240.0, 120.0));    }    public function testTheNearEdgeOfTheMapIsBiggerThanTheFarOne(): void    {        $rig = self::rigFor(self::data()->map('thornvale'));        self::assertGreaterThan($rig->scale(240.0, 20.0), $rig->scale(240.0, 220.0));    }    private static function rigFor(array $map): Rig    {        $minX = $minZ = INF;        $maxX = $maxZ = -INF;        foreach ($map['slots'] as [$sx, $sy]) {            $minX = min($minX, ($sx + 1) * 16.0 - 18);            $maxX = max($maxX, ($sx + 1) * 16.0 + 18);            $minZ = min($minZ, ($sy + 1) * 16.0 - 18);            $maxZ = max($maxZ, ($sy + 1) * 16.0 + 18);        }        [$bx, $by] = $map['bastion'];        $minX = min($minX, $bx * 16.0);        $maxX = max($maxX, $bx * 16.0 + 48);        $minZ = min($minZ, $by * 16.0);        $maxZ = max($maxZ, $by * 16.0 + 96);        $rig = new Rig(480.0, 270.0);        $rig->fit($minX, $minZ, $maxX, $maxZ, 60.0, 36.0, 266.0, 52.0);        return $rig;    }}