Stability++

This commit is contained in:
Andy Kernel 2024-09-13 15:41:10 +02:00 committed by solcloud
parent 5677c7bf55
commit f06f871921
42 changed files with 472 additions and 206 deletions

View File

@ -10,7 +10,7 @@ jobs:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
persist-credentials: false

View File

@ -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 '<?php preg_match("~Lines:\s+([\d.]+)%~", stream_get_contents(STDIN), $m);exit((int)((float)$m[1] < 98.5));' > 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

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
/vendor/
/electron/package-lock.json
/electron/build/
/electron/node_modules/
/electron/package-lock.json
/vendor/
/www/coverage/

View File

@ -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": "*"
}
}

View File

@ -13,7 +13,6 @@ class Bullet
private bool $originPlayerIsAttacker;
private int $distanceTraveled;
private int $damage = 1;
private int $damageArmor = 1;
/** @var array<int,bool> [playerId => true] */
private array $playerSkipIds = [];
@ -22,10 +21,9 @@ class Bullet
$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 @@ class Bullet
return $this->damage;
}
public function getDamageArmor(): int
{
return $this->damageArmor;
}
public function isActive(): bool
{
return ($this->damage > 0 && $this->distanceTraveled < $this->distanceMax);

View File

@ -244,6 +244,7 @@ class Game
return $this->roundNumber;
}
/** @infection-ignore-all */
public function addSoundEvent(SoundEvent $event): void
{
$this->addEvent($event);
@ -308,7 +309,7 @@ class Game
$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 @@ class Game
$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 @@ class Game
$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);

View File

@ -42,6 +42,7 @@ final class Graph extends DiGraph
/**
* @return array<string,string[]>
* @internal
* @codeCoverageIgnore
*/
public function internalGetGeneratedNeighbors(): array
{

View File

@ -70,16 +70,19 @@ abstract class Item
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 @@ abstract class Item
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

View File

@ -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 @@ final class PathFinder
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 @@ final class PathFinder
}
}
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 @@ final class PathFinder
$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<Point> $queue */
@ -226,7 +226,7 @@ final class PathFinder
$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
}
}
}

View File

@ -137,7 +137,7 @@ class Point
return new Point2D($this->z, $this->y);
}
GameException::notImplementedYet("New axis '$XYaxis'?");
GameException::notImplementedYet("New axis '$XYaxis'?"); // @codeCoverageIgnore
}
/**
@ -160,12 +160,4 @@ class Point
];
}
/**
* @return int[]
*/
public function toFlatArray(): array
{
return [$this->x, $this->y, $this->z];
}
}

View File

@ -9,11 +9,6 @@ class Point2D
{
}
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 @@ class Point2D
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<string,int>
*/

View File

@ -541,8 +541,7 @@ class World
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 @@ class World
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 @@ class World
}
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 @@ class World
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 @@ class World
$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 @@ class World
{
$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 @@ class World
$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 @@ class World
/**
* @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 @@ class World
/**
* @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 @@ class World
return $output;
}
/**
* @return Floor[]
*/
public function getYFloors(int $y): array
{
return ($this->floors[$y] ?? []);
}
/**
* @return DropItem[]
* @internal

View File

@ -7,6 +7,8 @@ use cs\Interface\Volumetric;
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;

View File

@ -131,6 +131,7 @@ class DropEvent extends Event implements ForOneRoundMax
return $this->dropItem;
}
/** @codeCoverageIgnore */
public function serialize(): array
{
return [

View File

@ -49,10 +49,6 @@ final class GrillEvent extends VolumetricEvent
public function extinguish(Column $flame): void
{
if (!$flame->active) {
return;
}
$flame->active = false;
$this->shrinkPart($flame);
}

View File

@ -6,17 +6,16 @@ use cs\Core\Column;
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 @@ final class SmokeEvent extends VolumetricEvent
{
$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;

View File

@ -196,20 +196,19 @@ final class ThrowEvent extends Event implements Attackable, ForOneRoundMax
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 [

View File

@ -47,14 +47,14 @@ abstract class VolumetricEvent extends Event implements ForOneRoundMax
{
$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 @@ abstract class VolumetricEvent extends Event implements ForOneRoundMax
$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 @@ abstract class VolumetricEvent extends Event implements ForOneRoundMax
return $this->item;
}
/** @codeCoverageIgnore */
public function serialize(): array
{
return [

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -13,7 +13,8 @@ class HitBoxLegs extends SphereGroupHitBox
public function __construct()
{
parent::__construct();
parent::__construct(false);
$this->createLeftLimb();
$this->createRightLimb();

View File

@ -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();

View File

@ -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;
}
}

View File

@ -9,33 +9,34 @@ use cs\Core\Point;
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();
if ($centerModifier) {
$center->add($centerModifier);
}
$point = $center->clone()->add($this->relativeCenter);
[$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);
[$x, $z] = Util::rotatePointY($angle, $point->x, $point->z, $center->x, $center->z);
return $point->setX($x)->setZ($z);
if ($centerModifier) {
$this->point->add($centerModifier);
}
return $this->point;
}
}

View File

@ -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),

View File

@ -47,14 +47,14 @@ class Server
$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 @@ class Server
}
$tickCount = $this->startGame();
$this->log("Game ended! Ticks: {$tickCount}, Lag: {$this->serverLag}.");
return $tickCount;
}
protected function startWarmup(): bool
@ -75,7 +76,7 @@ class Server
if ($this->setting->warmupInstantStart) {
return true;
}
$gameReady = true;
$gameReady = true; // @codeCoverageIgnore
}
}
@ -152,7 +153,7 @@ class Server
}
}
} else {
$this->playerBlock($address);
$this->playerBlock($address); // @codeCoverageIgnore
}
}
}

View File

@ -120,10 +120,7 @@ abstract class AmmoBasedWeapon extends BaseWeapon implements Reloadable, AttackE
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;
}

View File

@ -124,7 +124,8 @@ class RoundTest extends BaseTestCase
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);
}

View File

@ -19,6 +19,7 @@ use cs\Weapon\Knife;
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 @@ class SimpleInventoryTest extends BaseTestCase
$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 @@ class SimpleInventoryTest extends BaseTestCase
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 @@ class SimpleInventoryTest extends BaseTestCase
$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 @@ class SimpleInventoryTest extends BaseTestCase
$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 @@ class SimpleInventoryTest extends BaseTestCase
$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 @@ class SimpleInventoryTest extends BaseTestCase
},
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());

View File

@ -163,6 +163,63 @@ class MovementTest extends BaseTestCase
$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();

View File

@ -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 @@ class BombTest extends BaseTestCase
}
$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 @@ class BombTest extends BaseTestCase
'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,

View File

@ -11,8 +11,12 @@ use cs\Enum\BuyMenuItem;
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 @@ class MolotovGrenadeTest extends BaseTestCase
]);
}
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 @@ class MolotovGrenadeTest extends BaseTestCase
$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());
}
}

View File

@ -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 @@ class PlayerKillTest extends BaseTestCase
$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 @@ class PlayerKillTest extends BaseTestCase
$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());
}

View File

@ -125,7 +125,7 @@ class SimpleShootTest extends BaseTestCase
$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 @@ class SimpleShootTest extends BaseTestCase
$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 @@ class SimpleShootTest extends BaseTestCase
$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());
},
];

View File

@ -248,7 +248,10 @@ class CollisionTest extends BaseTest
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

View File

@ -60,6 +60,7 @@ class ProtocolTest extends BaseTest
$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 @@ class ProtocolTest extends BaseTest
],
'health' => 100,
'position' => [
'x' => 0,
'y' => 0,
'z' => 0,
'x' => $pp->x,
'y' => $pp->y,
'z' => $pp->z,
],
'look' => [
'horizontal' => 12.45,

View File

@ -5,14 +5,15 @@ namespace Test\Unit;
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 @@ class ServerTest extends BaseTest
$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();

View File

@ -195,6 +195,19 @@ class UtilTest extends BaseTest
$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 @@ class UtilTest extends BaseTest
$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));
}

View File

@ -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 @@ final class NavigationMeshTest extends BaseTestCase
['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 @@ final class NavigationMeshTest extends BaseTestCase
$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

View File

@ -267,6 +267,7 @@ $frameIdEnd = null;
}
if (event.code === <?= EventList::map[GrillEvent::class] ?> || event.code === <?= EventList::map[SmokeEvent::class] ?>) {
self.volumetrics[event.data.id] = {}
self.volumetrics[event.data.id]['size'] = event.data.size
}
if (event.code === <?= EventList::map[SoundEvent::class] ?>) {
if ([<?= SoundType::GRENADE_LAND->value ?>, <?= SoundType::GRENADE_BOUNCE->value ?>, <?= SoundType::GRENADE_AIR->value ?>].includes(event.data.type)) {
@ -281,13 +282,18 @@ $frameIdEnd = null;
const color = event.data.type === <?= SoundType::FLAME_SPAWN->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 === <?= SoundType::FLAME_EXTINGUISH->value ?> || event.data.type === <?= SoundType::SMOKE_FADE->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 === <?= SoundType::SMOKE_FADE->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)
}
}
}
})

View File

@ -30,22 +30,21 @@ if (is_numeric($_GET['crouch'] ?? false)) {
}
}
$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 = [