mirror of
https://github.com/solcloud/Counter-Strike.git
synced 2025-01-16 22:28:15 +01:00
Stability++
This commit is contained in:
parent
5677c7bf55
commit
f06f871921
2
.github/workflows/electron.yml
vendored
2
.github/workflows/electron.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@ -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
5
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/vendor/
|
||||
/electron/package-lock.json
|
||||
/electron/build/
|
||||
/electron/node_modules/
|
||||
/electron/package-lock.json
|
||||
/vendor/
|
||||
/www/coverage/
|
||||
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -42,6 +42,7 @@ final class Graph extends DiGraph
|
||||
/**
|
||||
* @return array<string,string[]>
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function internalGetGeneratedNeighbors(): array
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -131,6 +131,7 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
return $this->dropItem;
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public function serialize(): array
|
||||
{
|
||||
return [
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 [
|
||||
|
@ -47,7 +47,7 @@ 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();
|
||||
@ -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 [
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -13,7 +13,8 @@ class HitBoxLegs extends SphereGroupHitBox
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
parent::__construct(false);
|
||||
|
||||
$this->createLeftLimb();
|
||||
$this->createRightLimb();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
[$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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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);
|
||||
if (false === ($geometry instanceof SphereGroupHitBox)) {
|
||||
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,
|
||||
];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception("Unknown geometry '" . get_class($geometry) . "' given");
|
||||
}
|
||||
|
||||
$slots = [
|
||||
|
Loading…
x
Reference in New Issue
Block a user