From f06f871921dd816d72aae65493d7a77117e07912 Mon Sep 17 00:00:00 2001 From: Andy Kernel <171799466+starswaitforus@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:41:10 +0200 Subject: [PATCH] Stability++ --- .github/workflows/electron.yml | 2 +- .github/workflows/test.yml | 11 ++- .gitignore | 5 +- electron/package.json | 2 +- server/src/Core/Bullet.php | 9 +- server/src/Core/Game.php | 8 +- server/src/Core/Graph.php | 1 + server/src/Core/Item.php | 15 ++- server/src/Core/PathFinder.php | 12 +-- server/src/Core/Point.php | 10 +- server/src/Core/Point2D.php | 40 -------- server/src/Core/World.php | 38 +++----- server/src/Equipment/Smoke.php | 2 + server/src/Event/DropEvent.php | 1 + server/src/Event/GrillEvent.php | 4 - server/src/Event/SmokeEvent.php | 9 +- server/src/Event/ThrowEvent.php | 7 +- server/src/Event/VolumetricEvent.php | 10 +- server/src/HitGeometry/HitBoxBack.php | 7 +- server/src/HitGeometry/HitBoxChest.php | 7 +- server/src/HitGeometry/HitBoxHead.php | 7 +- server/src/HitGeometry/HitBoxLegs.php | 3 +- server/src/HitGeometry/HitBoxStomach.php | 7 +- server/src/HitGeometry/SphereGroupHitBox.php | 14 +-- server/src/HitGeometry/SphereHitBox.php | 21 +++-- server/src/Map/TestMap.php | 2 +- server/src/Net/Server.php | 9 +- server/src/Weapon/AmmoBasedWeapon.php | 5 +- test/og/Game/RoundTest.php | 3 +- test/og/Inventory/SimpleInventoryTest.php | 84 ++++++++++++++++- test/og/Movement/MovementTest.php | 57 ++++++++++++ test/og/Shooting/BombTest.php | 47 +++++++++- test/og/Shooting/MolotovGrenadeTest.php | 96 ++++++++++++++++++++ test/og/Shooting/PlayerKillTest.php | 6 ++ test/og/Shooting/SimpleShootTest.php | 24 ++++- test/og/Unit/CollisionTest.php | 3 + test/og/Unit/ProtocolTest.php | 7 +- test/og/Unit/ServerTest.php | 23 ++++- test/og/Unit/UtilTest.php | 19 +++- test/og/World/NavigationMeshTest.php | 8 +- www/demoPlayer.php | 8 +- www/playerGenerator.php | 21 ++--- 42 files changed, 470 insertions(+), 204 deletions(-) diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml index df0e14b..0097dbd 100644 --- a/.github/workflows/electron.yml +++ b/.github/workflows/electron.yml @@ -10,7 +10,7 @@ jobs: contents: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0df5aa..53db6aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: composer-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false @@ -38,3 +38,12 @@ jobs: timeout-minutes: 1 run: | composer check + + - name: "Check code coverage min percentage" + timeout-minutes: 5 + run: | + echo ' cc.php + XDEBUG_MODE=coverage php vendor/bin/phpunit -d memory_limit=70M \ + --coverage-text --only-summary-for-coverage-text --stderr --no-progress --colors=never 2> cc.txt + cat cc.txt + grep 'Lines: ' cc.txt | php -d error_reporting=E_ALL cc.php diff --git a/.gitignore b/.gitignore index ffbf3e8..10a3d04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/vendor/ -/electron/package-lock.json /electron/build/ /electron/node_modules/ +/electron/package-lock.json +/vendor/ +/www/coverage/ diff --git a/electron/package.json b/electron/package.json index d709877..dbab992 100644 --- a/electron/package.json +++ b/electron/package.json @@ -5,7 +5,7 @@ "build": "electron-packager . build --platform=all --asar --executable-name=start_game --ignore='\\.php$' --out='build'" }, "devDependencies": { - "electron": "25.*", + "electron": "*", "electron-packager": "*" } } diff --git a/server/src/Core/Bullet.php b/server/src/Core/Bullet.php index f0d1b95..279ac08 100644 --- a/server/src/Core/Bullet.php +++ b/server/src/Core/Bullet.php @@ -13,7 +13,6 @@ class Bullet private bool $originPlayerIsAttacker; private int $distanceTraveled; private int $damage = 1; - private int $damageArmor = 1; /** @var array [playerId => true] */ private array $playerSkipIds = []; @@ -22,10 +21,9 @@ public function __construct(private AttackEnable $item, private int $distanceMax $this->distanceTraveled = Setting::playerHeadRadius(); // shooting from center of player head so lets start on head edge } - public function setProperties(int $damage = 1, int $damageArmor = 1): void + public function setProperties(int $damage): void { $this->damage = $damage; - $this->damageArmor = $damageArmor; } public function setOriginPlayer(int $playerId, bool $attackerSide, Point $origin): void @@ -57,11 +55,6 @@ public function getDamage(): int return $this->damage; } - public function getDamageArmor(): int - { - return $this->damageArmor; - } - public function isActive(): bool { return ($this->damage > 0 && $this->distanceTraveled < $this->distanceMax); diff --git a/server/src/Core/Game.php b/server/src/Core/Game.php index b7f3127..ec682e7 100644 --- a/server/src/Core/Game.php +++ b/server/src/Core/Game.php @@ -244,6 +244,7 @@ public function getRoundNumber(): int return $this->roundNumber; } + /** @infection-ignore-all */ public function addSoundEvent(SoundEvent $event): void { $this->addEvent($event); @@ -308,7 +309,7 @@ public function playerFallDamageKilledEvent(Player $playerDead): void $this->addSoundEvent($sound->setPlayer($playerDead)); } - public function playerBombKilledEvent(Player $playerDead): void + protected function playerBombKilledEvent(Player $playerDead): void { $this->addEvent(new KillEvent($playerDead, $playerDead, ItemId::BOMB, false)); $sound = new SoundEvent($playerDead->getPositionClone(), SoundType::PLAYER_DEAD); @@ -467,7 +468,8 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $ $amount += match ($roundEndEvent->reason) { RoundEndReason::ALL_ENEMIES_ELIMINATED => 3250, RoundEndReason::BOMB_EXPLODED => 3500, - default => throw new GameException("New win reason? " . $roundEndEvent->reason->value), + RoundEndReason::TIME_RUNS_OUT, + RoundEndReason::BOMB_DEFUSED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore }; } elseif (!$player->isAlive()) { $amount += $this->score->getMoneyLossBonus(true); @@ -481,7 +483,7 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $ $amount += match ($roundEndEvent->reason) { RoundEndReason::ALL_ENEMIES_ELIMINATED, RoundEndReason::TIME_RUNS_OUT => 3250, RoundEndReason::BOMB_DEFUSED => 3500, - default => throw new GameException("New win reason? " . $roundEndEvent->reason->value), + RoundEndReason::BOMB_EXPLODED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore }; } else { $amount += $this->score->getMoneyLossBonus(false); diff --git a/server/src/Core/Graph.php b/server/src/Core/Graph.php index 01f98d5..8ddcf2d 100644 --- a/server/src/Core/Graph.php +++ b/server/src/Core/Graph.php @@ -42,6 +42,7 @@ public function generateNeighbors(): void /** * @return array * @internal + * @codeCoverageIgnore */ public function internalGetGeneratedNeighbors(): array { diff --git a/server/src/Core/Item.php b/server/src/Core/Item.php index 31fd81a..6697dad 100644 --- a/server/src/Core/Item.php +++ b/server/src/Core/Item.php @@ -70,16 +70,19 @@ public function getScopeLevel(): int return $this->scopeLevel; } + /** @codeCoverageIgnore */ public function decrementQuantity(): void { // empty hook } + /** @codeCoverageIgnore */ public function incrementQuantity(): void { // empty hook } + /** @codeCoverageIgnore */ public function clone(): static { throw new GameException('Override clone() method if makes sense for item: ' . get_class($this)); @@ -111,14 +114,10 @@ public function getPrice(?self $alreadyHaveSlotItem = null): int public function canPurchaseMultipleTime(self $newSlotItem): bool { - if ($this->getType() === ItemType::TYPE_WEAPON_PRIMARY) { - return true; - } - if ($this->getType() === ItemType::TYPE_WEAPON_SECONDARY) { - return true; - } - - return false; + return match ($this->getType()) { + ItemType::TYPE_WEAPON_PRIMARY, ItemType::TYPE_WEAPON_SECONDARY => true, + default => GameException::notImplementedYet('New item? ' . get_class($this)) // @codeCoverageIgnore + }; } public function equip(): ?EquipEvent diff --git a/server/src/Core/PathFinder.php b/server/src/Core/PathFinder.php index 572337a..8f5ab5e 100644 --- a/server/src/Core/PathFinder.php +++ b/server/src/Core/PathFinder.php @@ -20,7 +20,7 @@ final class PathFinder public function __construct(private readonly World $world, public readonly int $tileSize, public readonly int $colliderHeight) { if ($this->tileSize < 3 || $tileSize % 2 !== 1) { - throw new GameException('Tile size should be odd and greater than 1.'); + throw new GameException('Tile size should be odd and greater than 1.'); // @codeCoverageIgnore } $this->tileSizeHalf = (int)ceil(($this->tileSize - 1) / 2); @@ -37,7 +37,7 @@ public function __construct(private readonly World $world, public readonly int $ protected function canFullyMoveTo(Point $candidate, int $angle, int $targetDistance, int $radius, int $height): bool { if ($angle % 90 !== 0) { - GameException::notImplementedYet(); + GameException::notImplementedYet(); // @codeCoverageIgnore } $looseFloor = false; @@ -165,13 +165,13 @@ public function findTile(Point $pointOnFloor, int $radius): Point } } - GameException::notImplementedYet('Should always find something? ' . $pointOnFloor->hash()); + GameException::notImplementedYet('Should always find something? ' . $pointOnFloor->hash()); // @codeCoverageIgnore } public function convertToNavMeshNode(Point $point): void { if ($point->x < 1 || $point->z < 1) { - throw new GameException('World start from 1'); + throw new GameException('World start from 1'); // @codeCoverageIgnore } $fmodX = fmod($point->x, $this->tileSize); @@ -188,7 +188,7 @@ public function buildNavigationMesh(Point $start, int $objectHeight): void $startPoint = $start->clone(); $this->convertToNavMeshNode($startPoint); if (!$this->world->findFloorSquare($startPoint, 1)) { - throw new GameException('No floor on start point'); + throw new GameException('No floor on start point'); // @codeCoverageIgnore } /** @var SplQueue $queue */ @@ -226,7 +226,7 @@ public function buildNavigationMesh(Point $start, int $objectHeight): void $this->graph->addNode($currentNode); } if (++$this->iterationCount === 10_000) { - GameException::notImplementedYet('New map, tileSize or bad test (no boundary box, bad starting point)?'); + GameException::notImplementedYet('New map, tileSize or bad test (no boundary box, bad starting point)?'); // @codeCoverageIgnore } } } diff --git a/server/src/Core/Point.php b/server/src/Core/Point.php index 7e5fe78..7b9473b 100644 --- a/server/src/Core/Point.php +++ b/server/src/Core/Point.php @@ -137,7 +137,7 @@ public function to2D(string $XYaxis): Point2D return new Point2D($this->z, $this->y); } - GameException::notImplementedYet("New axis '$XYaxis'?"); + GameException::notImplementedYet("New axis '$XYaxis'?"); // @codeCoverageIgnore } /** @@ -160,12 +160,4 @@ public function toArray(): array ]; } - /** - * @return int[] - */ - public function toFlatArray(): array - { - return [$this->x, $this->y, $this->z]; - } - } diff --git a/server/src/Core/Point2D.php b/server/src/Core/Point2D.php index 7bc9863..b218dd3 100644 --- a/server/src/Core/Point2D.php +++ b/server/src/Core/Point2D.php @@ -9,11 +9,6 @@ public function __construct(public int $x = 0, public int $y = 0) { } - public function equals(self $point): bool - { - return ($this->x === $point->x && $this->y === $point->y); - } - public function add(int $xAmount, int $yAmount): self { $this->x += $xAmount; @@ -21,46 +16,11 @@ public function add(int $xAmount, int $yAmount): self return $this; } - public function addX(int $amount): self - { - $this->x += $amount; - return $this; - } - - public function setX(int $int): self - { - $this->x = $int; - return $this; - } - - public function addY(int $amount): self - { - $this->y += $amount; - return $this; - } - - public function setY(int $int): self - { - $this->y = $int; - return $this; - } - public function __toString(): string { return "Point2D({$this->x},{$this->y})"; } - public function clone(): self - { - return new self($this->x, $this->y); - } - - public function setFrom(self $point): void - { - $this->x = $point->x; - $this->y = $point->y; - } - /** * @return array */ diff --git a/server/src/Core/World.php b/server/src/Core/World.php index b33c1b7..f7c4b1e 100644 --- a/server/src/Core/World.php +++ b/server/src/Core/World.php @@ -541,8 +541,7 @@ public function processFlammableExplosion(Player $thrower, Point $epicentre, Fla public function smokeTryToExtinguishFlames(Column $smoke): void { foreach ($this->activeMolotovs as $fire) { - if (!Collision::boxWithBox($smoke->boundaryMin, $smoke->boundaryMax, $fire->boundaryMin, $fire->boundaryMax) - ) { + if (!Collision::boxWithBox($smoke->boundaryMin, $smoke->boundaryMax, $fire->boundaryMin, $fire->boundaryMax)) { continue; } @@ -557,8 +556,7 @@ public function smokeTryToExtinguishFlames(Column $smoke): void public function flameCanIgnite(Column $flame): bool { foreach ($this->activeSmokes as $smoke) { - if (!Collision::boxWithBox($smoke->boundaryMin, $smoke->boundaryMax, $flame->boundaryMin, $flame->boundaryMax) - ) { + if (!Collision::boxWithBox($smoke->boundaryMin, $smoke->boundaryMax, $flame->boundaryMin, $flame->boundaryMax)) { continue; } @@ -596,12 +594,7 @@ public function checkFlameDamage(GrillEvent $fire, int $tickId): void } foreach ($fire->parts as $flame) { - if (!$flame->active || !Collision::pointWithCylinder( - $flame->highestPoint, - $pp, - $playerRadius, - $playerHeight) - ) { + if (!$flame->active || !Collision::pointWithCylinder($flame->highestPoint, $pp, $playerRadius, $playerHeight)) { continue; } @@ -625,8 +618,7 @@ public function checkFlameDamage(GrillEvent $fire, int $tickId): void public function isCollisionWithMolotov(Point $pos): bool { foreach ($this->activeMolotovs as $molotov) { - if (!Collision::pointWithBoxBoundary($pos, $molotov->boundaryMin, $molotov->boundaryMax) - ) { + if (!Collision::pointWithBoxBoundary($pos, $molotov->boundaryMin, $molotov->boundaryMax)) { continue; } @@ -723,10 +715,10 @@ public function playerDiedToFallDamage(Player $playerDead): void $this->game->playerFallDamageKilledEvent($playerDead); } - public function playerDiedToFlame(Player $playerCulprit, Player $playerDead, Flammable $item): void + protected function playerDiedToFlame(Player $playerCulprit, Player $playerDead, Flammable $item): void { if (false === ($item instanceof Grenade)) { - throw new GameException("New flammable non grenade type?"); + throw new GameException("New flammable non grenade type?"); // @codeCoverageIgnore } $this->game->playerGrenadeKilledEvent($playerCulprit, $playerDead, $item); } @@ -735,13 +727,13 @@ public function buildNavigationMesh(int $tileSize, int $objectHeight): PathFinde { $boundingRadius = Setting::playerBoundingRadius(); if ($tileSize > $boundingRadius - 4) { - throw new GameException('Tile size should be decently lower than player bounding radius.'); + throw new GameException('Tile size should be decently lower than player bounding radius.'); // @codeCoverageIgnore } $pathFinder = new PathFinder($this, $tileSize, $objectHeight); $startPoints = $this->getMap()->getStartingPointsForNavigationMesh(); if ([] === $startPoints) { - throw new GameException('No starting point for navigation defined!'); + throw new GameException('No starting point for navigation defined!'); // @codeCoverageIgnore } foreach ($startPoints as $point) { $pathFinder->buildNavigationMesh($point, $objectHeight); @@ -817,7 +809,7 @@ public function surfaceHit(Point $hitPoint, SolidSurface $hit, int $attackerId, $this->makeSound($soundEvent); } - public function playerHit(Point $hitPoint, Player $playerHit, Player $playerCulprit, SoundType $soundType, Item $item, Point $origin, int $damage): void + protected function playerHit(Point $hitPoint, Player $playerHit, Player $playerCulprit, SoundType $soundType, Item $item, Point $origin, int $damage): void { $attackerId = $playerCulprit->getId(); $soundEvent = new SoundEvent($hitPoint, $soundType); @@ -912,7 +904,7 @@ public function isCollisionWithOtherPlayers(int $playerIdSkip, Point $point, int /** * @return Wall[] */ - public function getXWalls(int $x): array + protected function getXWalls(int $x): array { return ($this->walls[self::WALL_X][$x] ?? []); } @@ -920,7 +912,7 @@ public function getXWalls(int $x): array /** * @return Wall[] */ - public function getZWalls(int $z): array + protected function getZWalls(int $z): array { return ($this->walls[self::WALL_Z][$z] ?? []); } @@ -960,14 +952,6 @@ public function getFloors(): array return $output; } - /** - * @return Floor[] - */ - public function getYFloors(int $y): array - { - return ($this->floors[$y] ?? []); - } - /** * @return DropItem[] * @internal diff --git a/server/src/Equipment/Smoke.php b/server/src/Equipment/Smoke.php index 451160b..c0feebd 100644 --- a/server/src/Equipment/Smoke.php +++ b/server/src/Equipment/Smoke.php @@ -7,6 +7,8 @@ class Smoke extends Grenade implements Volumetric { + public const MAX_HEIGHT = 350; + public const MAX_CORNER_HEIGHT = 270; public const MAX_TIME_MS = 18_000; protected int $price = 300; diff --git a/server/src/Event/DropEvent.php b/server/src/Event/DropEvent.php index 4879ab4..476832b 100644 --- a/server/src/Event/DropEvent.php +++ b/server/src/Event/DropEvent.php @@ -131,6 +131,7 @@ public function getDropItem(): DropItem return $this->dropItem; } + /** @codeCoverageIgnore */ public function serialize(): array { return [ diff --git a/server/src/Event/GrillEvent.php b/server/src/Event/GrillEvent.php index 15476c1..3192ff1 100644 --- a/server/src/Event/GrillEvent.php +++ b/server/src/Event/GrillEvent.php @@ -49,10 +49,6 @@ protected function expandPart(Point $center): Column public function extinguish(Column $flame): void { - if (!$flame->active) { - return; - } - $flame->active = false; $this->shrinkPart($flame); } diff --git a/server/src/Event/SmokeEvent.php b/server/src/Event/SmokeEvent.php index b06b383..539a73f 100644 --- a/server/src/Event/SmokeEvent.php +++ b/server/src/Event/SmokeEvent.php @@ -6,17 +6,16 @@ use cs\Core\GameException; use cs\Core\Point; use cs\Enum\SoundType; +use cs\Equipment\Smoke; final class SmokeEvent extends VolumetricEvent { - public const MAX_HEIGHT = 350; - public const MAX_CORNER_HEIGHT = 270; - private int $maxHeight = self::MAX_HEIGHT; + private int $maxHeight = Smoke::MAX_HEIGHT; protected function setup(): void { - if (min(self::MAX_CORNER_HEIGHT, self::MAX_HEIGHT) < $this->partHeight) { + if (min(Smoke::MAX_CORNER_HEIGHT, Smoke::MAX_HEIGHT) < $this->partHeight) { throw new GameException('Part height is too high'); // @codeCoverageIgnore } } @@ -34,7 +33,7 @@ protected function expandPart(Point $center): Column { $count = count($this->parts); if ($count > 10 && $count % 2 === 0) { - $this->maxHeight = max(self::MAX_CORNER_HEIGHT, $this->maxHeight - 1); + $this->maxHeight = max(Smoke::MAX_CORNER_HEIGHT, $this->maxHeight - 1); } $height = $this->partHeight; diff --git a/server/src/Event/ThrowEvent.php b/server/src/Event/ThrowEvent.php index 5292da6..16c7c24 100644 --- a/server/src/Event/ThrowEvent.php +++ b/server/src/Event/ThrowEvent.php @@ -196,20 +196,19 @@ public function getTickId(): int return $this->world->getTickId(); } + /** @codeCoverageIgnore */ public function applyRecoil(float $offsetHorizontal, float $offsetVertical): void { // no recoil on throw } - public function setAngles(float $angleHorizontal, float $angleVertical): void + protected function setAngles(float $angleHorizontal, float $angleVertical): void { $this->angleHorizontal = $angleHorizontal; $this->angleVertical = $angleVertical; } - /** - * @codeCoverageIgnore - */ + /** @codeCoverageIgnore */ public function serialize(): array { return [ diff --git a/server/src/Event/VolumetricEvent.php b/server/src/Event/VolumetricEvent.php index d35ca34..2fd1d4b 100644 --- a/server/src/Event/VolumetricEvent.php +++ b/server/src/Event/VolumetricEvent.php @@ -47,14 +47,14 @@ public function __construct( { $startNode = $this->graph->getNodeById($start->hash()); if (null === $startNode) { - throw new GameException("No node for start point: " . $start->hash()); + throw new GameException("No node for start point: " . $start->hash()); // @codeCoverageIgnore } $this->id = Sequence::next(); $this->partSize = $this->partRadius * 2 + 1; $this->startedTickId = $this->world->getTickId(); $this->spawnTickCount = $this->timeMsToTick(20); - $this->maxTicksCount = $this->timeMsToTick($this->item->getMaxTimeMs()); + $this->maxTicksCount = $this->timeMsToTick($this->item->getMaxTimeMs()); $partArea = ($this->partSize) ** 2; $this->spawnPartCount = (int)ceil($this->item->getSpawnAreaMetersSquared() * 100 / $partArea); @@ -67,10 +67,7 @@ public function __construct( $this->queue->enqueue($startNode); } - protected function setup(): void - { - // empty hook - } + protected abstract function setup(): void; private function shrink(int $tick): void { @@ -174,6 +171,7 @@ public function getItem(): Volumetric return $this->item; } + /** @codeCoverageIgnore */ public function serialize(): array { return [ diff --git a/server/src/HitGeometry/HitBoxBack.php b/server/src/HitGeometry/HitBoxBack.php index 306dc1d..7de184a 100644 --- a/server/src/HitGeometry/HitBoxBack.php +++ b/server/src/HitGeometry/HitBoxBack.php @@ -2,19 +2,14 @@ namespace cs\HitGeometry; -use cs\Core\Player; use cs\Core\Point; class HitBoxBack extends SphereGroupHitBox { - private Point $centerPoint; public function __construct() { - $this->centerPoint = new Point(); - parent::__construct(function (Player $player): Point { - return $this->centerPoint->setScalar(0)->addY($player->getHeadHeight()); - }); + parent::__construct(true); $this->createBackLeft(); $this->createBackRight(); diff --git a/server/src/HitGeometry/HitBoxChest.php b/server/src/HitGeometry/HitBoxChest.php index 2f6e08b..3133651 100644 --- a/server/src/HitGeometry/HitBoxChest.php +++ b/server/src/HitGeometry/HitBoxChest.php @@ -2,19 +2,14 @@ namespace cs\HitGeometry; -use cs\Core\Player; use cs\Core\Point; class HitBoxChest extends SphereGroupHitBox { - private Point $centerPoint; public function __construct() { - $this->centerPoint = new Point(); - parent::__construct(function (Player $player): Point { - return $this->centerPoint->setScalar(0)->addY($player->getHeadHeight()); - }); + parent::__construct(true); $this->createChestLeft(); $this->createChestRight(); diff --git a/server/src/HitGeometry/HitBoxHead.php b/server/src/HitGeometry/HitBoxHead.php index 3f98f3a..9dbd74d 100644 --- a/server/src/HitGeometry/HitBoxHead.php +++ b/server/src/HitGeometry/HitBoxHead.php @@ -2,19 +2,14 @@ namespace cs\HitGeometry; -use cs\Core\Player; use cs\Core\Point; class HitBoxHead extends SphereGroupHitBox { - private Point $centerPoint; public function __construct() { - $this->centerPoint = new Point(); - parent::__construct(function (Player $player): Point { - return $this->centerPoint->setScalar(0)->addY($player->getHeadHeight()); - }); + parent::__construct(true); $this->addHitBox(new Point(0, -8, 1), 8); $this->addHitBox(new Point(0, -9, 4), 8); diff --git a/server/src/HitGeometry/HitBoxLegs.php b/server/src/HitGeometry/HitBoxLegs.php index be3e466..aefa10d 100644 --- a/server/src/HitGeometry/HitBoxLegs.php +++ b/server/src/HitGeometry/HitBoxLegs.php @@ -13,7 +13,8 @@ class HitBoxLegs extends SphereGroupHitBox public function __construct() { - parent::__construct(); + parent::__construct(false); + $this->createLeftLimb(); $this->createRightLimb(); diff --git a/server/src/HitGeometry/HitBoxStomach.php b/server/src/HitGeometry/HitBoxStomach.php index bf05ab4..b62e08c 100644 --- a/server/src/HitGeometry/HitBoxStomach.php +++ b/server/src/HitGeometry/HitBoxStomach.php @@ -2,19 +2,14 @@ namespace cs\HitGeometry; -use cs\Core\Player; use cs\Core\Point; class HitBoxStomach extends SphereGroupHitBox { - private Point $centerPoint; public function __construct() { - $this->centerPoint = new Point(); - parent::__construct(function (Player $player): Point { - return $this->centerPoint->setScalar(0)->addY($player->getHeadHeight()); - }); + parent::__construct(true); $this->createFrontRight(); $this->createFrontLeft(); diff --git a/server/src/HitGeometry/SphereGroupHitBox.php b/server/src/HitGeometry/SphereGroupHitBox.php index 7d87cbc..a1112ef 100644 --- a/server/src/HitGeometry/SphereGroupHitBox.php +++ b/server/src/HitGeometry/SphereGroupHitBox.php @@ -2,7 +2,6 @@ namespace cs\HitGeometry; -use Closure; use cs\Core\Collision; use cs\Core\Player; use cs\Core\Point; @@ -12,21 +11,18 @@ class SphereGroupHitBox implements HitIntersect { /** @var SphereHitBox[] */ private array $parts = []; + private Point $point; - /** - * @param ?Closure $centerPointModifier function (Player $player): Point {} - */ - public function __construct(public readonly ?Closure $centerPointModifier = null) + public function __construct(public readonly bool $usePlayerHeight) { + $this->point = new Point(); } public function intersect(Player $player, Point $point): bool { - /** @var Point $modifier */ - $modifier = $this->centerPointModifier ? call_user_func($this->centerPointModifier, $player) : null; + $this->point->setScalar(0)->addY($this->usePlayerHeight ? $player->getHeadHeight() : 0); foreach ($this->getParts($player) as $part) { - $center = $part->calculateWorldCoordinate($player, $modifier); - if (Collision::pointWithSphere($point, $center, $part->radius)) { + if (Collision::pointWithSphere($point, $part->calculateWorldCoordinate($player, $this->point), $part->radius)) { return true; } } diff --git a/server/src/HitGeometry/SphereHitBox.php b/server/src/HitGeometry/SphereHitBox.php index 99a2bc7..9e7c804 100644 --- a/server/src/HitGeometry/SphereHitBox.php +++ b/server/src/HitGeometry/SphereHitBox.php @@ -9,33 +9,34 @@ use cs\Core\Util; use cs\Interface\HitIntersect; -class SphereHitBox implements HitIntersect +final class SphereHitBox implements HitIntersect { + private Point $point; - public function __construct(protected Point $relativeCenter, public readonly int $radius) + public function __construct(private readonly Point $relativeCenter, public readonly int $radius) { if ($this->radius <= 0) { throw new GameException("Radius needs to be bigger than zero"); } + $this->point = new Point(); } public function intersect(Player $player, Point $point): bool { - $center = $this->calculateWorldCoordinate($player); - return Collision::pointWithSphere($point, $center, $this->radius); + return Collision::pointWithSphere($point, $this->calculateWorldCoordinate($player), $this->radius); } public function calculateWorldCoordinate(Player $player, ?Point $centerModifier = null): Point { - $angle = $player->getSight()->getRotationHorizontal(); - $center = $player->getPositionClone(); + [$x, $z] = Util::rotatePointY($player->getSight()->getRotationHorizontal(), $this->relativeCenter->x, $this->relativeCenter->z); + $this->point->setFrom($player->getReferenceToPosition()); + $this->point->addPart($x, $this->relativeCenter->y, $z); + if ($centerModifier) { - $center->add($centerModifier); + $this->point->add($centerModifier); } - $point = $center->clone()->add($this->relativeCenter); - [$x, $z] = Util::rotatePointY($angle, $point->x, $point->z, $center->x, $center->z); - return $point->setX($x)->setZ($z); + return $this->point; } } diff --git a/server/src/Map/TestMap.php b/server/src/Map/TestMap.php index ff02905..3d44fc9 100644 --- a/server/src/Map/TestMap.php +++ b/server/src/Map/TestMap.php @@ -13,7 +13,7 @@ class TestMap extends Map public function __construct() { - $this->setAttackersSpawnPositions([new Point()]); + $this->setAttackersSpawnPositions([new Point(), new Point(999, 0, 999)]); $this->setDefendersSpawnPositions([ (new Point())->setZ(50), new Point(9999, 0, 9999), diff --git a/server/src/Net/Server.php b/server/src/Net/Server.php index 678509d..c70e65d 100644 --- a/server/src/Net/Server.php +++ b/server/src/Net/Server.php @@ -47,14 +47,14 @@ public function __construct( $this->tickNanoSeconds = $this->setting->tickMs * (int)1E6; } - public function start(): void + public function start(): int { if (!$this->startWarmup()) { $playerCount = count($this->clients); $this->log("Not all players connected during warmup! Players: {$playerCount}/{$this->setting->playersMax}."); $this->game->quit(GameOverReason::REASON_NOT_ALL_PLAYERS_CONNECTED); $this->sendGameStateToClients(); - return; + return 0; } $this->log("All players connected, starting game."); @@ -63,6 +63,7 @@ public function start(): void } $tickCount = $this->startGame(); $this->log("Game ended! Ticks: {$tickCount}, Lag: {$this->serverLag}."); + return $tickCount; } protected function startWarmup(): bool @@ -75,7 +76,7 @@ protected function startWarmup(): bool if ($this->setting->warmupInstantStart) { return true; } - $gameReady = true; + $gameReady = true; // @codeCoverageIgnore } } @@ -152,7 +153,7 @@ private function receiveClientsCommands(): void } } } else { - $this->playerBlock($address); + $this->playerBlock($address); // @codeCoverageIgnore } } } diff --git a/server/src/Weapon/AmmoBasedWeapon.php b/server/src/Weapon/AmmoBasedWeapon.php index 4e6bd54..7e331bd 100644 --- a/server/src/Weapon/AmmoBasedWeapon.php +++ b/server/src/Weapon/AmmoBasedWeapon.php @@ -120,10 +120,7 @@ public function attackSecondary(Attackable $event): ?AttackResult public function createBullet(): Bullet { $bullet = new Bullet($this, static::range); - $bullet->setProperties( - damage: static::damage, - damageArmor: static::damage * static::armorPenetration, - ); + $bullet->setProperties(static::damage); return $bullet; } diff --git a/test/og/Game/RoundTest.php b/test/og/Game/RoundTest.php index eab9fb0..d79b2f2 100644 --- a/test/og/Game/RoundTest.php +++ b/test/og/Game/RoundTest.php @@ -124,7 +124,8 @@ function (Player $p) use (&$called): void { public function testNoSpawnPosition(): void { $game = $this->createTestGame(); - $player = new Player(2, Color::YELLOW, true); + $game->addPlayer(new Player(2, Color::YELLOW, true)); + $player = new Player(3, Color::GREEN, true); $this->expectExceptionMessage("Cannot find free spawn position for 'attacker' player"); $game->addPlayer($player); } diff --git a/test/og/Inventory/SimpleInventoryTest.php b/test/og/Inventory/SimpleInventoryTest.php index db1c917..73f1c71 100644 --- a/test/og/Inventory/SimpleInventoryTest.php +++ b/test/og/Inventory/SimpleInventoryTest.php @@ -19,6 +19,7 @@ use cs\Weapon\PistolGlock; use cs\Weapon\PistolUsp; use cs\Weapon\RifleAk; +use cs\Weapon\RifleAWP; use cs\Weapon\RifleM4A4; use Test\BaseTestCase; @@ -110,11 +111,48 @@ public function testPlayerBuyAndDropPrimaryWithEnoughMoney(): void $game->getPlayer(1)->dropEquippedItem(); } + public function testPlayerGetPistolOnRoundStartIfHasNone(): void + { + $game = $this->createNoPauseGame(10); + $p2 = new Player(2, Color::GREEN, false); + $game->addPlayer($p2); + $p2->setPosition(new Point(6000, 0, 6000)); + $p2->dropItemFromSlot(InventorySlot::SLOT_SECONDARY->value); + $p2->buyItem(BuyMenuItem::DEFUSE_KIT); + $p2->buyItem(BuyMenuItem::GRENADE_SMOKE); + + $this->playPlayer($game, [ + fn(Player $p) => $this->assertFalse($game->getScore()->attackersIsWinning()), + fn(Player $p) => $this->assertFalse($game->getScore()->defendersIsWinning()), + fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)), + fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_SECONDARY)), + fn(Player $p) => $this->assertInstanceOf(PistolGlock::class, $p->getEquippedItem()), + fn(Player $p) => $p->setPosition(new Point(9999, 0, 9999)), + fn(Player $p) => $this->assertNotNull($p->dropEquippedItem()), + fn(Player $p) => $p->setPosition(new Point(500, 0, 500)), + fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + fn(Player $p) => $game->getPlayer(2)->suicide(), + fn(Player $p) => $this->assertSame(2, $game->getRoundNumber()), + fn(Player $p) => $this->assertTrue($game->getScore()->attackersIsWinning()), + fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)), + fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_SECONDARY)), + fn(Player $p) => $this->assertInstanceOf(PistolGlock::class, $p->getEquippedItem()), + $this->endGame(), + ]); + + $this->assertSame(0, $game->getScore()->getPlayerStat(1)->getDeaths()); + $this->assertSame(0, $game->getScore()->getPlayerStat(1)->getKills()); + $this->assertSame(1, $game->getScore()->getPlayerStat(2)->getDeaths()); + $this->assertSame(-1, $game->getScore()->getPlayerStat(2)->getKills()); + $this->assertTrue($p2->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)); + } + public function testPlayerBuyAndDropAndUseForPickup(): void { $game = $this->createTestGame(); $p = $game->getPlayer(1); - $p->getInventory()->earnMoney(5000); + $p->getInventory()->earnMoney(15000); $this->playPlayer($game, [ fn(Player $p) => $p->getSight()->lookVertical(-60), @@ -133,6 +171,13 @@ public function testPlayerBuyAndDropAndUseForPickup(): void fn(Player $p) => $p->use(), fn(Player $p) => $this->assertEmpty($game->getWorld()->getDropItems()), fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::RIFLE_AWP)), + $this->waitNTicks(200), + fn(Player $p) => $this->assertNotEmpty($game->getWorld()->getDropItems()), + fn(Player $p) => $this->assertInstanceOf(RifleAWP::class, $p->getEquippedItem()), + fn(Player $p) => $p->use(), + $this->waitNTicks(200), + fn(Player $p) => $this->assertInstanceOf(RifleAk::class, $p->getEquippedItem()), $this->endGame(), ]); } @@ -317,6 +362,8 @@ public function testPlayerBuyWeapons(): void $akPrice = 2700; $playerCommands = [ + fn(Player $p) => $this->assertFalse($p->buyItem(BuyMenuItem::RIFLE_M4A4)), + fn(Player $p) => $this->assertFalse($p->buyItem(BuyMenuItem::PISTOL_USP)), fn(Player $p) => $p->buyItem(BuyMenuItem::RIFLE_AK), fn(Player $p) => $p->buyItem(BuyMenuItem::RIFLE_AK), fn(Player $p) => $p->buyItem(BuyMenuItem::RIFLE_AK), @@ -336,6 +383,36 @@ public function testPlayerBuyWeapons(): void $this->assertFalse($game->getPlayer(1)->getInventory()->canBuy($ak)); } + public function testDropFromSlot(): void + { + $game = $this->createTestGame(); + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->lookVertical(90), + fn(Player $p) => $p->getInventory()->earnMoney(5000), + function (Player $p) { + $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_KNIFE->value)); + $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_PRIMARY->value)); + }, + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::RIFLE_AK)), + fn(Player $p) => $p->equipSecondaryWeapon(), + function (Player $p) { + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_KNIFE->value)); + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)); + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)); + + $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_KNIFE->value)); + $this->assertTrue($p->dropItemFromSlot(InventorySlot::SLOT_PRIMARY->value)); + + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_KNIFE->value)); + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)); + $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)); + }, + $this->waitNTicks(1000), + fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)), + $this->endGame(), + ]); + } + public function testPlayerBuyTwoFlashes(): void { $startMoney = 600; @@ -357,12 +434,16 @@ public function testPlayerBuyTwoFlashes(): void $this->assertInstanceOf(Flashbang::class, $item); $this->assertSame($itemPrice, $item->getPrice()); $this->assertFalse($game->getPlayer(1)->getInventory()->canBuy($item)); + $this->assertSame(2, $item->getQuantity()); $flashBang1 = $game->getPlayer(1)->dropEquippedItem(); $this->assertInstanceOf(Flashbang::class, $flashBang1); $flashBang2 = $game->getPlayer(1)->dropEquippedItem(); $this->assertInstanceOf(Flashbang::class, $flashBang2); $this->assertFalse($flashBang1 === $flashBang2); + $this->assertTrue($item === $flashBang2); + $this->assertSame(1, $flashBang1->getQuantity()); + $this->assertSame(1, $flashBang2->getQuantity()); } public function testPlayerBuyMaxFourGrenades(): void @@ -437,6 +518,7 @@ function (Player $p): void { }, fn(Player $p) => $p->buyItem(BuyMenuItem::KEVLAR_BODY), fn(Player $p) => $p->buyItem(BuyMenuItem::KEVLAR_BODY), + fn(Player $p) => $this->assertNull($p->getInventory()->equip(InventorySlot::SLOT_KEVLAR)), function (Player $p): void { $this->assertSame(1651, $p->getMoney()); $this->assertSame(ArmorType::BODY, $p->getArmorType()); diff --git a/test/og/Movement/MovementTest.php b/test/og/Movement/MovementTest.php index ae9fdfe..52b1acc 100644 --- a/test/og/Movement/MovementTest.php +++ b/test/og/Movement/MovementTest.php @@ -163,6 +163,63 @@ public function testPlayerSlowMovementWhenShot(): void $this->assertLessThan(Setting::moveDistancePerTick() * $tickMax, $game->getPlayer(1)->getPositionClone()->z); } + public function testPlayerConsistentMovement(): void + { + $start = new Point(500, 0, 500); + $end = null; + + $game = $this->createTestGame(); + $p = $game->getPlayer(1); + $p->setPosition($start); + $this->playPlayer($game, [ + fn(Player $p) => $p->getInventory()->earnMoney(9000), + fn(Player $p) => $p->getSight()->lookHorizontal(45), + fn(Player $p) => $p->moveForward(), + fn(Player $p) => $p->moveForward(), + function (Player $p) use (&$end) { + $end = $p->getPositionClone(); + }, + fn(Player $p) => $p->getSight()->lookHorizontal(225), + fn(Player $p) => $p->moveForward(), + fn(Player $p) => $p->moveForward(), + $this->endGame(), + ]); + + $this->assertInstanceOf(Point::class, $end); + $this->assertPositionSame($start, $p->getPositionClone()); + $this->assertPositionNotSame($end, $p->getPositionClone()); + } + + public function testPlayerSlowMovementWhenInScope(): void + { + $start = new Point(500, 0, 500); + $end = null; + + $game = $this->createTestGame(); + $p = $game->getPlayer(1); + $p->setPosition($start); + $this->playPlayer($game, [ + fn(Player $p) => $p->getInventory()->earnMoney(9000), + fn(Player $p) => $p->buyItem(BuyMenuItem::RIFLE_AWP), + fn(Player $p) => $p->getSight()->lookHorizontal(45), + fn(Player $p) => $p->moveForward(), + fn(Player $p) => $p->moveForward(), + function (Player $p) use (&$end) { + $end = $p->getPositionClone(); + $p->attackSecondary(); + }, + fn(Player $p) => $p->getSight()->lookHorizontal(225), + fn(Player $p) => $p->moveForward(), + fn(Player $p) => $p->moveForward(), + $this->endGame(), + ]); + + $this->assertNotNull($end); + $this->assertPositionNotSame($start, $p->getPositionClone()); + $this->assertGreaterThan($start->x, $p->getPositionClone()->x); + $this->assertGreaterThan($start->z, $p->getPositionClone()->z); + } + public function testPlayerSlowMovementWhenFlying(): void { $game = $this->createOneRoundGame(); diff --git a/test/og/Shooting/BombTest.php b/test/og/Shooting/BombTest.php index 716f518..e39345a 100644 --- a/test/og/Shooting/BombTest.php +++ b/test/og/Shooting/BombTest.php @@ -2,24 +2,61 @@ namespace Test\Shooting; +use cs\Core\GameProperty; use cs\Core\GameState; use cs\Core\Player; use cs\Core\Point; +use cs\Core\Setting; use cs\Core\Util; use cs\Enum\BuyMenuItem; use cs\Enum\Color; use cs\Enum\InventorySlot; use cs\Enum\RoundEndReason; +use cs\Equipment\Bomb; +use cs\Event\KillEvent; use cs\Event\PlantEvent; use cs\Event\RoundEndEvent; +use cs\Weapon\Knife; use Test\BaseTestCase; class BombTest extends BaseTestCase { + public function testInvalidBombPlantCases(): void + { + $gameProperty = new GameProperty(); + $gameProperty->bomb_plant_time_ms = 1; + $gameProperty->bomb_explode_time_ms = 1; + $gameProperty->freeze_time_sec = 1; + + $game = $this->createTestGame(null, $gameProperty); + $game->getPlayer(1)->setPosition(new Point(500, 0, 500)); + $game->addPlayer(new Player(2, Color::BLUE, true)); + $this->playPlayer($game, [ + fn(Player $p) => $p->equip(InventorySlot::SLOT_BOMB), + fn(Player $p) => $p->attack(), + $this->waitNTicks(1000), + fn(Player $p) => $p->jump(), + fn(Player $p) => $this->assertTrue($p->isFlying()), + fn(Player $p) => $p->attack(), + $this->waitXTicks(Setting::tickCountJump()), + fn(Player $p) => $p->suicide(), + fn(Player $p) => $p->attack(), + $this->waitNTicks(1000), + fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_BOMB->value)), + fn(Player $p) => $this->assertInstanceOf(Bomb::class, $p->getEquippedItem()), + fn(Player $p) => $this->assertNotNull($p->dropEquippedItem()), + fn(Player $p) => $this->assertInstanceOf(Knife::class, $p->getEquippedItem()), + fn(Player $p) => $this->assertFalse($game->getWorld()->canAttack($p)), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + $this->endGame(), + ]); + } + public function testBombPlant(): void { $roundEndEvent = null; + $killEvent = null; $plantEvent = null; $plantCount = 0; @@ -35,15 +72,20 @@ public function testBombPlant(): void } $state->getPlayer(1)->attack(); }); - $game->onEvents(function (array $events) use (&$roundEndEvent, &$plantEvent, &$plantCount) { + $game->onEvents(function (array $events) use (&$roundEndEvent, &$plantEvent, &$killEvent, &$plantCount) { foreach ($events as $event) { if ($event instanceof RoundEndEvent) { + $this->assertNull($roundEndEvent); $roundEndEvent = $event; } if ($event instanceof PlantEvent) { $plantEvent = $event; $plantCount++; } + if ($event instanceof KillEvent) { + $this->assertNull($killEvent); + $killEvent = $event; + } } }); @@ -57,6 +99,9 @@ public function testBombPlant(): void 'attackersWins' => $roundEndEvent->attackersWins, 'score' => $game->getScore()->toArray(), ], $roundEndEvent->serialize()); + $this->assertInstanceOf(KillEvent::class, $killEvent); + $this->assertSame($game->getPlayer(1), $killEvent->getPlayerDead()); + $this->assertSame($game->getPlayer(1), $killEvent->getPlayerCulprit()); $this->assertInstanceOf(PlantEvent::class, $plantEvent); $this->assertSame([ 'timeMs' => 1000, diff --git a/test/og/Shooting/MolotovGrenadeTest.php b/test/og/Shooting/MolotovGrenadeTest.php index 98f3841..a63c33f 100644 --- a/test/og/Shooting/MolotovGrenadeTest.php +++ b/test/og/Shooting/MolotovGrenadeTest.php @@ -11,8 +11,12 @@ use cs\Enum\Color; use cs\Enum\InventorySlot; use cs\Enum\RampDirection; +use cs\Enum\SoundType; +use cs\Equipment\Incendiary; use cs\Equipment\Molotov; use cs\Equipment\Smoke; +use cs\Event\RoundEndEvent; +use cs\Event\SoundEvent; use Test\BaseTestCase; class MolotovGrenadeTest extends BaseTestCase @@ -42,6 +46,41 @@ public function testOwnDamage(): void ]); } + public function testMolotovOnlyTookMaxOneRound(): void + { + $game = $this->createNoPauseGame(3); + $game->getPlayer(1)->setPosition(new Point(Setting::playerBoundingRadius(), 0, Setting::playerBoundingRadius())); + $game->getWorld()->addBox(new Box(new Point(), 1000, 3000, 1000)); + + $roundEndEvent = null; + $game->onEvents(function (array $events) use (&$roundEndEvent): void { + foreach ($events as $event) { + if ($event instanceof RoundEndEvent) { + $roundEndEvent = $event; + return; + } + if ($roundEndEvent !== null && $event instanceof SoundEvent && $event->type === SoundType::FLAME_PLAYER_HIT) { + $this->fail('Molly should not hit after round end'); + } + } + }); + + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->look(0, -90), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_MOLOTOV)), + fn(Player $p) => $this->assertInstanceOf(Molotov::class, $p->getEquippedItem()), + $this->waitNTicks(Molotov::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + $this->waitNTicks(Molotov::MAX_TIME_MS), + fn(Player $p) => $this->assertSame(2, $game->getRoundNumber(), 'Player should not survive molly'), + fn(Player $p) => $this->assertSame(100, $game->getPlayer(1)->getHealth()), + $this->endGame(), + ]); + + $this->assertNotNull($roundEndEvent); + } + public function testWallBlockFire(): void { $game = $this->createNoPauseGame(); @@ -258,4 +297,61 @@ public function testSmokeExtinguishFlames(): void $this->assertSame($health, $game->getPlayer(1)->getHealth()); } + public function testSmokeHeightBoundaryAndShrink(): void + { + $height = 220; + $this->assertLessThan(Smoke::MAX_CORNER_HEIGHT, $height); + $this->assertGreaterThan(Setting::playerHeadHeightStand(), $height); + + $game = $this->createNoPauseGame(); + $game->getWorld()->addBox(new Box(new Point(), 1000, 3000, 1000)); + $game->getWorld()->addRamp(new Ramp(new Point(0, 0, 200), RampDirection::GROW_TO_POSITIVE_Z, 13, 200)); + $game->getWorld()->addBox(new Box(new Point(0, $height, 455), 1000, 10, 400)); + + $p2 = new Player(2, Color::BLUE, false); + $game->addPlayer($p2); + $p2->getSight()->look(0, -90); + $p2->setPosition(new Point(500, 265, 650)); + $p2->buyItem(BuyMenuItem::GRENADE_INCENDIARY); + $p3 = new Player(3, Color::BLUE, false); + $game->addPlayer($p3); + $p3->getSight()->look(0, -90); + $p3->setPosition(new Point(500, 0, 650)); + $p3->buyItem(BuyMenuItem::GRENADE_INCENDIARY); + $game->addPlayer(new Player(4, Color::BLUE, false)); + + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->look(0, -90), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_SMOKE)), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_MOLOTOV)), + fn(Player $p) => $p->setPosition(new Point(500, 0, 650)), + $this->waitNTicks(Molotov::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + fn() => $this->assertInstanceOf(Incendiary::class, $p2->getEquippedItem()), + fn() => $this->assertNotNull($p2->attack()), + $this->waitNTicks(Smoke::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + function (Player $p) { + $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_GRENADE_MOLOTOV->value)); + $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_GRENADE_SMOKE->value)); + }, + fn() => $this->assertTrue($p3->isAlive()), + $this->waitNTicks(Smoke::MAX_TIME_MS), + fn(Player $p) => $this->assertLessThan(100, $p->getHealth()), + fn(Player $p) => $p->setPosition(new Point(500, 265, 660)), + $this->waitNTicks(300), + fn() => $this->assertTrue($p3->isAlive()), + fn() => $this->assertInstanceOf(Incendiary::class, $p3->getEquippedItem()), + fn() => $this->assertNotNull($p3->attack()), + $this->waitNTicks(Incendiary::MAX_TIME_MS), + $this->endGame() + ]); + + $this->assertSame(1, $game->getRoundNumber()); + $this->assertFalse($p2->isAlive()); + $this->assertFalse($p3->isAlive()); + $this->assertTrue($game->getPlayer(1)->isAlive()); + $this->assertTrue($game->getPlayer(4)->isAlive()); + } + } diff --git a/test/og/Shooting/PlayerKillTest.php b/test/og/Shooting/PlayerKillTest.php index 73612ea..0dd89d2 100644 --- a/test/og/Shooting/PlayerKillTest.php +++ b/test/og/Shooting/PlayerKillTest.php @@ -3,6 +3,7 @@ namespace Test\Shooting; use cs\Core\Floor; +use cs\Core\GameException; use cs\Core\GameProperty; use cs\Core\HitBox; use cs\Core\Player; @@ -65,6 +66,9 @@ public function testOnePlayerCanKillOther(): void $this->assertNull($player2->attack()); $this->assertTrue($headHit->getType() === HitBoxType::HEAD); $this->assertSame($startMoney - $gun->getPrice() + $gun->getKillAward(), $player2->getMoney()); + + $this->expectException(GameException::class); + $game->addPlayer(new Player($player2->getId(), Color::BLUE, true)); } public function testOnePlayerCanKillOtherWallBang(): void @@ -247,6 +251,8 @@ public function testUspKillPlayerInThreeBulletsInChestWithNoKevlar(): void $this->assertFalse($game->getScore()->attackersIsWinning()); $this->assertSame(0, $game->getScore()->getScoreAttackers()); $this->assertSame(1, $game->getScore()->getScoreDefenders()); + $this->assertSame(0, $game->getScore()->getNumberOfLossRoundsInRow(false)); + $this->assertSame(1, $game->getScore()->getNumberOfLossRoundsInRow(true)); $this->assertFalse($game->getScore()->isTie()); $this->assertSame(2, $game->getRoundNumber()); } diff --git a/test/og/Shooting/SimpleShootTest.php b/test/og/Shooting/SimpleShootTest.php index 5d406a6..1298224 100644 --- a/test/og/Shooting/SimpleShootTest.php +++ b/test/og/Shooting/SimpleShootTest.php @@ -125,7 +125,7 @@ function (Player $p): void { $this->assertSame(RifleAk::magazineCapacity, $ak->getAmmo()); $this->assertSame(RifleAk::reserveAmmo, $ak->getAmmoReserve()); }, - fn(Player $p) => $p->attack(), + fn(Player $p) => $this->assertNull($p->attack()), function (Player $p): void { $ak = $p->getEquippedItem(); $this->assertInstanceOf(RifleAk::class, $ak); @@ -133,7 +133,7 @@ function (Player $p): void { $this->assertSame(RifleAk::reserveAmmo, $ak->getAmmoReserve()); }, $this->waitNTicks(RifleAk::equipReadyTimeMs) - 1, - fn(Player $p) => $p->attack(), + fn(Player $p) => $this->assertNotNull($p->attack()), function (Player $p): void { $ak = $p->getEquippedItem(); $this->assertInstanceOf(RifleAk::class, $ak); @@ -154,7 +154,25 @@ function (Player $p): void { $this->assertInstanceOf(RifleAk::class, $ak); $this->assertSame(RifleAk::magazineCapacity, $ak->getAmmo()); $this->assertSame(RifleAk::reserveAmmo - 1, $ak->getAmmoReserve()); - $this->assertNotNull($p->attack()); + }, + $this->waitNTicks(RifleAk::fireRateMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + function (Player $p): void { + $ak = $p->getEquippedItem(); + $this->assertInstanceOf(RifleAk::class, $ak); + $this->assertSame(RifleAk::magazineCapacity - 1, $ak->getAmmo()); + }, + fn(Player $p) => $p->reload(), + function (Player $p): void { + $ak = $p->getEquippedItem(); + $this->assertInstanceOf(RifleAk::class, $ak); + $this->assertSame(RifleAk::magazineCapacity - 1, $ak->getAmmo()); + }, + $this->waitNTicks(RifleAk::reloadTimeMs), + function (Player $p): void { + $ak = $p->getEquippedItem(); + $this->assertInstanceOf(RifleAk::class, $ak); + $this->assertSame(RifleAk::magazineCapacity, $ak->getAmmo()); }, ]; diff --git a/test/og/Unit/CollisionTest.php b/test/og/Unit/CollisionTest.php index 0efb0bc..5dbacda 100644 --- a/test/og/Unit/CollisionTest.php +++ b/test/og/Unit/CollisionTest.php @@ -248,7 +248,10 @@ public function testPlaneWithPlane(): void public function testPointWithBoxBoundary(): void { $this->assertTrue(Collision::pointWithBoxBoundary(new Point(), new Point(-5, 0, -5), new Point(5, 4, 5))); + $this->assertTrue(Collision::pointWithBoxBoundary(new Point(4, 2, 5), new Point(-5, 0, -5), new Point(5, 4, 5))); $this->assertFalse(Collision::pointWithBoxBoundary(new Point(-6), new Point(-5, 0, -5), new Point(5, 4, 5))); + $this->assertFalse(Collision::pointWithBoxBoundary(new Point(4, 5, 2), new Point(-5, 0, -5), new Point(5, 4, 5))); + $this->assertFalse(Collision::pointWithBoxBoundary(new Point(4, 2, 6), new Point(-5, 0, -5), new Point(5, 4, 5))); } public function testBoxWithBox(): void diff --git a/test/og/Unit/ProtocolTest.php b/test/og/Unit/ProtocolTest.php index 2077eee..4c58192 100644 --- a/test/og/Unit/ProtocolTest.php +++ b/test/og/Unit/ProtocolTest.php @@ -60,6 +60,7 @@ public function testSerialization(): void $game = new Game(new GameProperty()); $game->loadMap(new TestMap()); $game->addPlayer($player); + $pp = $player->getPositionClone(); $player->getSight()->look(12.45, 1.09); $protocol = new Protocol\TextProtocol(); @@ -90,9 +91,9 @@ public function testSerialization(): void ], 'health' => 100, 'position' => [ - 'x' => 0, - 'y' => 0, - 'z' => 0, + 'x' => $pp->x, + 'y' => $pp->y, + 'z' => $pp->z, ], 'look' => [ 'horizontal' => 12.45, diff --git a/test/og/Unit/ServerTest.php b/test/og/Unit/ServerTest.php index 299ea93..84192ec 100644 --- a/test/og/Unit/ServerTest.php +++ b/test/og/Unit/ServerTest.php @@ -5,14 +5,15 @@ use cs\Core\Game; use cs\Core\GameException; use cs\Core\GameFactory; +use cs\Core\GameProperty; use cs\Core\Setting; +use cs\Core\Util; use cs\Enum\BuyMenuItem; use cs\Enum\GameOverReason; use cs\Enum\InventorySlot; use cs\Equipment\Molotov; use cs\Event\GameOverEvent; use cs\Map\TestMap; -use cs\Net\ProtocolWriter; use cs\Net\Server; use cs\Net\ServerSetting; use cs\Net\TestConnector; @@ -53,6 +54,26 @@ public function testServerNoPlayersConnected(): void $this->assertSame(GameOverReason::REASON_NOT_ALL_PLAYERS_CONNECTED, $gameOver->reason); } + public function testServerGameOver(): void + { + $tickRate = Util::$TICK_RATE; + $roundTimeMs = rand($tickRate + 1, 10 * $tickRate); + $roundTickCount = Util::millisecondsToFrames($roundTimeMs); + $gameProperty = new GameProperty(); + $gameProperty->max_rounds = 1; + $gameProperty->freeze_time_sec = 0; + $gameProperty->half_time_freeze_sec = 0; + $gameProperty->round_end_cool_down_sec = 0; + $gameProperty->round_time_ms = $roundTimeMs; + + $game = new Game($gameProperty); + $game->loadMap(new TestMap()); + $setting = new ServerSetting(1, 0, 'code'); + $testNet = new TestConnector(array_merge(['login code'], array_fill(0, 3 + $roundTickCount, 'stand'))); + $server = new Server($game, $setting, $testNet); + $this->assertSame(2 + $roundTickCount, $server->start()); + } + public function testServer(): void { $game = GameFactory::createDebug(); diff --git a/test/og/Unit/UtilTest.php b/test/og/Unit/UtilTest.php index 17bafcc..69efa2a 100644 --- a/test/og/Unit/UtilTest.php +++ b/test/og/Unit/UtilTest.php @@ -195,6 +195,19 @@ protected function _testWorldAngleUsingMovement(Point $start, float $h, float $v $this->assertSame([$h, $v], [$actualH, $actualV], "{$start}, angle ({$h},{$v})"); } + public function testAngleNormalize(): void + { + $this->assertSame(0.0, Util::normalizeAngle(360.0)); + $this->assertSame(0.0, Util::normalizeAngle(720)); + $this->assertSame(347.8, Util::normalizeAngle(-12.20)); + $this->assertSame(190.3, Util::normalizeAngle(190.3)); + + $this->assertSame(-90.0, Util::normalizeAngleVertical(-91)); + $this->assertSame(-90.0, Util::normalizeAngleVertical(-207.23)); + $this->assertSame(90.0, Util::normalizeAngleVertical(90.1)); + $this->assertSame(43.1, Util::normalizeAngleVertical(43.1)); + } + public function testWorldAngle(): void { $this->assertSame([0.0, 0.0], Util::worldAngle(new Point(10, 10, 20), new Point(10, 10, 10))); @@ -236,9 +249,9 @@ public function testPoint(): void $this->assertSame(2, $point->y); $this->assertSame(3, $point->z); $this->assertSame([ - 'x' => 3, - 'y' => 2, - ], $point->to2D('zy')->toArray()); + 'x' => 2, + 'y' => 4, + ], $point->to2D('zy')->add(-1, 2)->toArray()); $point->setFromArray([1,3,2]); $this->assertTrue((new Point(1,3,2))->equals($point)); } diff --git a/test/og/World/NavigationMeshTest.php b/test/og/World/NavigationMeshTest.php index a99c5a3..1fc7ba3 100644 --- a/test/og/World/NavigationMeshTest.php +++ b/test/og/World/NavigationMeshTest.php @@ -3,6 +3,7 @@ namespace Test\World; use cs\Core\Box; +use cs\Core\GameException; use cs\Core\PathFinder; use cs\Core\Point; use cs\Core\World; @@ -33,7 +34,7 @@ public function testConvertPointToNavMeshPoint(): void ['326,333,326', new Point(333, 333, 333)], ['450,0,295', new Point(450, 0, 285)], ['450,0,295', new Point(461, 0, 285)], - ['1566,50,16', new Point(1570,50,26)], + ['1566,50,16', new Point(1570, 50, 26)], ], ]; $world = new World($this->createTestGame()); @@ -64,6 +65,11 @@ public function testSimple(): void $this->assertCount(3, $path->getGraph()->getNeighbors($startNode)); $path->getGraph()->generateNeighbors(); $this->assertCount(3, $path->getGraph()->getGeneratedNeighbors($startNode->getId())); + foreach ($path->getGraph()->getGeneratedNeighbors($startNode->getId()) as $neighbor) { + $path->getGraph()->removeNodeById($neighbor->getId()); + } + $this->expectException(GameException::class); + $path->getGraph()->getGeneratedNeighbors($startNode->getId()); } public function testBoundary(): void diff --git a/www/demoPlayer.php b/www/demoPlayer.php index be76cff..7343ffa 100644 --- a/www/demoPlayer.php +++ b/www/demoPlayer.php @@ -267,6 +267,7 @@ } if (event.code === || event.code === ) { self.volumetrics[event.data.id] = {} + self.volumetrics[event.data.id]['size'] = event.data.size } if (event.code === ) { if ([value ?>, value ?>, value ?>].includes(event.data.type)) { @@ -281,13 +282,18 @@ const color = event.data.type === value ?> ? new THREE.Color(`hsl(53, 100%, ${Math.random() * 70 + 20}%, 1)`) : new THREE.Color(0x798aa0) - let mesh = self.createVolume(31, event.data.extra.height, 31, color) + let size = self.volumetrics[event.data.extra.id]['size'] + let mesh = self.createVolume(size, event.data.extra.height, size, color) self.volumetrics[event.data.extra.id][`${event.data.position.x}-${event.data.position.y}-${event.data.position.z}`] = mesh mesh.position.set(event.data.position.x, event.data.position.y + (event.data.extra.height / 2), -event.data.position.z) } if (event.data.type === value ?> || event.data.type === value ?>) { const mesh = self.volumetrics[event.data.extra.id][`${event.data.position.x}-${event.data.position.y}-${event.data.position.z}`] mesh.visible = false + if (event.data.type === value ?>) { // single shrink event + delete self.volumetrics[event.data.extra.id]['size'] + Object.keys(self.volumetrics[event.data.extra.id]).forEach((key) => self.volumetrics[event.data.extra.id][key].visible = false) + } } } }) diff --git a/www/playerGenerator.php b/www/playerGenerator.php index 478d1b5..188c1a8 100644 --- a/www/playerGenerator.php +++ b/www/playerGenerator.php @@ -30,22 +30,21 @@ } } +$point = new Point(); $playerParts = []; foreach ($collider->getHitBoxes() as $box) { $geometry = $box->getGeometry(); - if ($geometry instanceof SphereGroupHitBox) { - $modifier = $geometry->centerPointModifier; - $modifier = $modifier === null ? new Point() : $modifier($player); - foreach ($geometry->getParts($player) as $part) { - $playerParts[$box->getType()->value][] = [ - "center" => $part->calculateWorldCoordinate($player, $modifier)->toArray(), - "radius" => $part->radius, - ]; - } - continue; + if (false === ($geometry instanceof SphereGroupHitBox)) { + throw new Exception("Unknown geometry '" . get_class($geometry) . "' given"); } - throw new Exception("Unknown geometry '" . get_class($geometry) . "' given"); + $modifier = $point->setScalar(0)->addY($geometry->usePlayerHeight ? $player->getHeadHeight() : 0); + foreach ($geometry->getParts($player) as $part) { + $playerParts[$box->getType()->value][] = [ + "center" => $part->calculateWorldCoordinate($player, $modifier)->toArray(), + "radius" => $part->radius, + ]; + } } $slots = [