mirror of
https://github.com/solcloud/Counter-Strike.git
synced 2025-01-16 14:18:15 +01:00
Stability++
This commit is contained in:
parent
45f5093d49
commit
205450dbc2
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@ -41,9 +41,9 @@ jobs:
|
||||
composer check
|
||||
|
||||
- name: "Check code coverage min percentage"
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 4
|
||||
run: |
|
||||
echo '<?php preg_match("~Lines:\s+([\d.]+)%~", stream_get_contents(STDIN), $m);exit((int)((float)$m[1] < 99.87));' > cc.php
|
||||
echo '<?php preg_match("~Lines:\s+([\d.]+)%~", stream_get_contents(STDIN), $m);exit((int)((float)$m[1] < 100));' > cc.php
|
||||
export XDEBUG_MODE=coverage
|
||||
composer unit -- --stderr --no-progress --colors=never \
|
||||
--coverage-xml=www/coverage/coverage-xml --log-junit=www/coverage/junit.xml \
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
grep 'Lines: ' cc.txt | php -d error_reporting=E_ALL cc.php
|
||||
|
||||
- name: "Check infection mutation framework min percentage"
|
||||
timeout-minutes: 8
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
export XDEBUG_MODE=off
|
||||
grep '"timeout": 20,' infection.json5
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Counter-Strike: Football [![Tests](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml/badge.svg)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml) [![Code coverage](https://img.shields.io/badge/Code%20coverage-100%25-green?style=flat)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml)
|
||||
# Counter-Strike: Football [![Tests](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml/badge.svg)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml) [![Code coverage](https://img.shields.io/badge/Code%20coverage-100%25-green?style=flat)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml) [![Mutation score](https://img.shields.io/badge/Mutation%20score-100%25-green?style=flat)](https://github.com/solcloud/Counter-Strike/actions/workflows/test.yml)
|
||||
|
||||
Competitive multiplayer FPS game where two football fan teams fight with the goal of winning more rounds than the opponent team.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"scripts": {
|
||||
"stan": "php vendor/bin/phpstan --memory-limit=300M analyze",
|
||||
"unit": "php vendor/bin/phpunit -d memory_limit=70M",
|
||||
"infection": "php -d memory_limit=180M vendor/bin/infection --only-covered --threads=6 --min-covered-msi=99",
|
||||
"infection": "php -d memory_limit=180M vendor/bin/infection --show-mutations --only-covered --threads=6 --min-covered-msi=100",
|
||||
"infection-cache": "@infection --coverage=www/coverage/",
|
||||
"dev": "php cli/server.php 1 8080 --debug & php cli/udp-ws-bridge.php",
|
||||
"dev2": "php cli/server.php 2 8080 --debug & php cli/udp-ws-bridge.php & php cli/udp-ws-bridge.php 8082",
|
||||
|
@ -5,38 +5,45 @@
|
||||
"server/src/",
|
||||
],
|
||||
},
|
||||
"logs": {
|
||||
"text": "/tmp/infection.log",
|
||||
},
|
||||
"timeout": 20,
|
||||
"testFramework": "phpunit",
|
||||
"mutators": {
|
||||
"global-ignoreSourceCodeByRegex": [
|
||||
"\\$this->log\\(.*\\);",
|
||||
"throw new GameException\\(.+\\);",
|
||||
"GameException::invalid\\(.*\\);",
|
||||
"GameException::notImplementedYet\\(.*\\);",
|
||||
],
|
||||
"@default": true,
|
||||
"@arithmetic": false,
|
||||
"@boolean": false,
|
||||
"@cast": false,
|
||||
"@conditional_boundary": false,
|
||||
"@conditional_negotiation": false,
|
||||
"@equal": false,
|
||||
"@function_signature": true,
|
||||
"PublicVisibility": {
|
||||
ignoreSourceCodeByRegex: [
|
||||
"public function processFlammableExplosion\\(.+",
|
||||
],
|
||||
},
|
||||
"@identical": true,
|
||||
"@number": false,
|
||||
"@operator": false,
|
||||
"@regex": true,
|
||||
"@removal": true,
|
||||
"MatchArmRemoval": {
|
||||
"ignoreSourceCodeByRegex": [
|
||||
".+GameException::invalid\\(.+",
|
||||
"Break_": false,
|
||||
"CastInt": false,
|
||||
"Continue_": false,
|
||||
"DecrementInteger": false,
|
||||
"FalseValue": false,
|
||||
"IfNegation": false,
|
||||
"Increment": false,
|
||||
"IncrementInteger": false,
|
||||
"InstanceOf_": false,
|
||||
"IntegerNegation": false,
|
||||
"LogicalAnd": false,
|
||||
"LogicalAndAllSubExprNegation": false,
|
||||
"LogicalAndNegation": false,
|
||||
"LogicalAndSingleSubExprNegation": false,
|
||||
"LogicalOrAllSubExprNegation": false,
|
||||
"LogicalOr": false,
|
||||
"Minus": false,
|
||||
"Modulus": false,
|
||||
"MulEqual": false,
|
||||
"Multiplication": false,
|
||||
"Plus": false,
|
||||
"PlusEqual": false,
|
||||
"RoundingFamily": false,
|
||||
"TrueValue": false,
|
||||
"ArrayItem": {
|
||||
"ignore": [
|
||||
"cs\\Event\\*::serialize",
|
||||
],
|
||||
},
|
||||
"ArrayItemRemoval": {
|
||||
@ -44,6 +51,22 @@
|
||||
"cs\\Event\\*::serialize",
|
||||
],
|
||||
},
|
||||
"Coalesce": {
|
||||
"ignoreSourceCodeByRegex": [
|
||||
".+\\(\\$skipPlayerIds\\[\\$playerId\\].+",
|
||||
".+SpeedMultiplier-\\{\\$itemId\\}.+",
|
||||
],
|
||||
},
|
||||
"Division": {
|
||||
ignoreSourceCodeByRegex: [
|
||||
".+rand\\(.+",
|
||||
],
|
||||
},
|
||||
"MatchArmRemoval": {
|
||||
"ignoreSourceCodeByRegex": [
|
||||
".+GameException::invalid\\(.+",
|
||||
],
|
||||
},
|
||||
"MethodCallRemoval": {
|
||||
"ignoreSourceCodeByRegex": [
|
||||
"\\$this->setActiveFloor\\(.+\\);",
|
||||
@ -56,13 +79,16 @@
|
||||
"\\$soundEvent->addExtra\\(.+\\);",
|
||||
"\\$this->addSoundEvent\\(.+\\);",
|
||||
"\\$bullet->addPlayerIdSkip\\(\\$playerId\\);",
|
||||
"\\$this->convertToNavMeshNode\\(\\$navmesh\\);",
|
||||
]
|
||||
},
|
||||
"@return_value": true,
|
||||
"IntegerNegation": false,
|
||||
"@sort": true,
|
||||
"@unwrap": true,
|
||||
"For_": true,
|
||||
"Foreach_": true,
|
||||
"Ternary": {
|
||||
"ignore": [
|
||||
"cs\\Core\\Player::serialize",
|
||||
],
|
||||
ignoreSourceCodeByRegex: [
|
||||
".+rand\\(.+",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -451,19 +451,18 @@ class Game
|
||||
|
||||
private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $player): int
|
||||
{
|
||||
$amount = 0;
|
||||
$attackersWins = $roundEndEvent->attackersWins;
|
||||
|
||||
// Attacker side checks
|
||||
if ($player->isPlayingOnAttackerSide()) {
|
||||
$amount += $this->bombPlanted ? 800 : 0;
|
||||
$amount = $this->bombPlanted ? 800 : 0;
|
||||
if ($attackersWins) {
|
||||
$amount += match ($roundEndEvent->reason) {
|
||||
RoundEndReason::ALL_ENEMIES_ELIMINATED => 3250,
|
||||
RoundEndReason::BOMB_EXPLODED => 3500,
|
||||
RoundEndReason::TIME_RUNS_OUT, RoundEndReason::BOMB_DEFUSED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore
|
||||
};
|
||||
} elseif (!$player->isAlive()) {
|
||||
} elseif ($this->bombPlanted || !$player->isAlive()) {
|
||||
$amount += $this->score->getMoneyLossBonus(true);
|
||||
}
|
||||
|
||||
@ -472,16 +471,14 @@ class Game
|
||||
|
||||
// Defender side checks
|
||||
if (!$attackersWins) {
|
||||
$amount += match ($roundEndEvent->reason) {
|
||||
return match ($roundEndEvent->reason) {
|
||||
RoundEndReason::ALL_ENEMIES_ELIMINATED, RoundEndReason::TIME_RUNS_OUT => 3250,
|
||||
RoundEndReason::BOMB_DEFUSED => 3500,
|
||||
RoundEndReason::BOMB_EXPLODED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore
|
||||
};
|
||||
} else {
|
||||
$amount += $this->score->getMoneyLossBonus(false);
|
||||
}
|
||||
|
||||
return $amount;
|
||||
return $this->score->getMoneyLossBonus(false);
|
||||
}
|
||||
|
||||
public function getState(): GameState
|
||||
|
@ -16,6 +16,7 @@ class GameFactory
|
||||
return new Game($properties);
|
||||
}
|
||||
|
||||
/** @infection-ignore-all */
|
||||
public static function createDebug(): Game
|
||||
{
|
||||
$properties = new GameProperty();
|
||||
|
@ -35,12 +35,12 @@ class GameProperty
|
||||
|
||||
public function __set(string $name, mixed $value): void
|
||||
{
|
||||
throw new GameException("Invalid field '{$name}' given");
|
||||
GameException::invalid("Invalid field '{$name}' given");
|
||||
}
|
||||
|
||||
public function __get(string $name): never
|
||||
{
|
||||
throw new GameException("Invalid field '{$name}' given");
|
||||
GameException::invalid("Invalid field '{$name}' given");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,8 +93,7 @@ final class HitBox implements Hittable
|
||||
return 0;
|
||||
}
|
||||
|
||||
$armorDamage = 0;
|
||||
$armorDamage += ($shootItem->getType() === ItemType::TYPE_WEAPON_PRIMARY ? 20 : 10);
|
||||
$armorDamage = ($shootItem->getType() === ItemType::TYPE_WEAPON_PRIMARY ? 20 : 10);
|
||||
if ($armorType === ArmorType::BODY_AND_HEAD && $hitBoxType === HitBoxType::HEAD) {
|
||||
$armorDamage += 30;
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ final class PathFinder
|
||||
for ($i = 1; $i <= $maxY; $i++) {
|
||||
$yCandidate->addY(1);
|
||||
if ($this->world->findFloorSquare($yCandidate, $radius - 1)) {
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
if ($this->getGraph()->getNodeById($navMeshCenter->setY($yCandidate->y)->hash())) {
|
||||
return $navMeshCenter;
|
||||
@ -153,14 +153,16 @@ final class PathFinder
|
||||
$prevNavmesh = $navmesh->hash();
|
||||
$navmesh->setFrom($candidate);
|
||||
$this->convertToNavMeshNode($navmesh);
|
||||
if ($prevNavmesh === $navmesh->hash()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->getGraph()->getNodeById($navmesh->hash())) {
|
||||
return $navmesh;
|
||||
}
|
||||
if ($prevNavmesh !== $navmesh->hash()) {
|
||||
$above = $checkAbove($candidate, $maxY, $radius);
|
||||
if ($above) {
|
||||
return $above;
|
||||
}
|
||||
$above = $checkAbove($candidate, $maxY, $radius);
|
||||
if ($above) {
|
||||
return $above;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,18 +203,20 @@ final class PathFinder
|
||||
if (array_key_exists($currentKey, $this->visited)) {
|
||||
continue;
|
||||
}
|
||||
$this->visited[$currentKey] = true;
|
||||
$currentNodeOrNull = $this->graph->getNodeById($currentKey);
|
||||
$currentNode = $currentNodeOrNull ?? new Node($currentKey, $current);
|
||||
|
||||
$hasNeighbour = false;
|
||||
$this->visited[$currentKey] = true;
|
||||
$currentNode = $this->graph->getNodeById($currentKey);
|
||||
if ($currentNode === null) {
|
||||
$currentNode = new Node($currentKey, $current);
|
||||
$this->graph->addNode($currentNode);
|
||||
}
|
||||
|
||||
foreach ($this->moves as $angle => $move) {
|
||||
$candidate->setFrom($current);
|
||||
if (!$this->canFullyMoveTo($candidate, $angle, $this->tileSize, $this->tileSizeHalf, $objectHeight)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasNeighbour = true;
|
||||
$newNeighbour = $candidate->clone();
|
||||
$newNode = $this->graph->getNodeById($newNeighbour->hash());
|
||||
if ($newNode === null) {
|
||||
@ -222,9 +226,6 @@ final class PathFinder
|
||||
$this->graph->addEdge(new DirectedEdge($currentNode, $newNode, 1));
|
||||
$queue->enqueue($newNeighbour);
|
||||
}
|
||||
if ($hasNeighbour && $currentNodeOrNull === null) {
|
||||
$this->graph->addNode($currentNode);
|
||||
}
|
||||
if (++$this->iterationCount === 10_000) {
|
||||
GameException::notImplementedYet('New map, tileSize or bad test (no boundary box, bad starting point)?'); // @codeCoverageIgnore
|
||||
}
|
||||
|
@ -271,11 +271,6 @@ final class Player
|
||||
return $this->headFloor;
|
||||
}
|
||||
|
||||
public function getCentrePoint(): Point
|
||||
{
|
||||
return $this->getPositionClone()->addY((int) ceil($this->headHeight / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Point>
|
||||
*/
|
||||
|
@ -45,8 +45,8 @@ class Score
|
||||
$this->scoreAttackers = $this->scoreDefenders;
|
||||
$this->scoreDefenders = $attackerScore;
|
||||
|
||||
$this->lossBonusAttackers = 1;
|
||||
$this->lossBonusDefenders = 1;
|
||||
$this->lossBonusAttackers = 0;
|
||||
$this->lossBonusDefenders = 0;
|
||||
$this->lastRoundAttackerWins = null;
|
||||
}
|
||||
|
||||
@ -67,20 +67,9 @@ class Score
|
||||
$this->lossBonusAttackers++;
|
||||
}
|
||||
} else {
|
||||
if ($this->lastRoundAttackerWins === true && $attackersWins) {
|
||||
$this->lossBonusDefenders++;
|
||||
}
|
||||
if ($this->lastRoundAttackerWins === true && !$attackersWins) {
|
||||
$this->lossBonusDefenders = 0;
|
||||
$this->lossBonusAttackers = 1;
|
||||
}
|
||||
if ($this->lastRoundAttackerWins === false && !$attackersWins) {
|
||||
$this->lossBonusAttackers++;
|
||||
}
|
||||
if ($this->lastRoundAttackerWins === false && $attackersWins) {
|
||||
$this->lossBonusDefenders = 1;
|
||||
$this->lossBonusAttackers = 0;
|
||||
}
|
||||
$attackersOnStreak = ($attackersWins && $this->lastRoundAttackerWins);
|
||||
$this->lossBonusDefenders = $attackersOnStreak ? $this->lossBonusDefenders + 1 : 0;
|
||||
$this->lossBonusAttackers = $attackersOnStreak ? 0 : $this->lossBonusAttackers + 1;
|
||||
}
|
||||
|
||||
if ($this->secondHalfScore !== []) {
|
||||
@ -128,7 +117,10 @@ class Score
|
||||
|
||||
public function getMoneyLossBonus(bool $isAttacker): int
|
||||
{
|
||||
return $this->lossBonuses[min(count($this->lossBonuses) - 1, $this->getNumberOfLossRoundsInRow($isAttacker))];
|
||||
return $this->lossBonuses[min(
|
||||
count($this->lossBonuses) - 1,
|
||||
max(0, $this->getNumberOfLossRoundsInRow($isAttacker) - 1),
|
||||
)];
|
||||
}
|
||||
|
||||
public function getNumberOfLossRoundsInRow(bool $isAttacker): int
|
||||
|
@ -17,6 +17,7 @@ final class Setting
|
||||
'flyingMovementSpeedMultiplier' => 0.8,
|
||||
'throwSpeed' => 40,
|
||||
|
||||
'playerVelocity' => 0,
|
||||
'playerHeadRadius' => 10,
|
||||
'playerBoundingRadius' => 60,
|
||||
'playerJumpHeight' => 150,
|
||||
@ -51,8 +52,6 @@ final class Setting
|
||||
*/
|
||||
private static function fixBackwardCompatible(array &$constants): void
|
||||
{
|
||||
// BC code
|
||||
$constants['playerVelocity'] = ($constants['playerVelocity'] ?? 0);
|
||||
foreach (self::defaultConstant as $key => $defaultValue) {
|
||||
if (isset($constants[$key])) {
|
||||
continue;
|
||||
@ -175,7 +174,7 @@ final class Setting
|
||||
|
||||
public static function playerVelocity(): int
|
||||
{
|
||||
return self::$data['playerVelocity'] ?? ((int)ceil(Util::$TICK_RATE * 1.7)); // @phpstan-ignore-line
|
||||
return self::$data['playerVelocity']; // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
public static function playerJumpHeight(): int
|
||||
|
@ -28,11 +28,6 @@ class Wall extends Plane
|
||||
return ($this->widthOnXAxis ? $this->getStart()->z : $this->getStart()->x);
|
||||
}
|
||||
|
||||
public function getOther(): int
|
||||
{
|
||||
return ($this->widthOnXAxis ? $this->getStart()->x : $this->getStart()->z);
|
||||
}
|
||||
|
||||
public function isWidthOnXAxis(): bool
|
||||
{
|
||||
return $this->widthOnXAxis;
|
||||
|
@ -88,9 +88,7 @@ final class World
|
||||
|
||||
public function regenerateNavigationMeshes(): void
|
||||
{
|
||||
$key = sprintf('%d-%d', self::GRENADE_NAVIGATION_MESH_TILE_SIZE, self::GRENADE_NAVIGATION_MESH_OBJECT_HEIGHT);
|
||||
$this->grenadeNavMesh = $this->getMap()->getNavigationMesh($key)
|
||||
?? $this->buildNavigationMesh(self::GRENADE_NAVIGATION_MESH_TILE_SIZE, self::GRENADE_NAVIGATION_MESH_OBJECT_HEIGHT);
|
||||
$this->grenadeNavMesh = $this->buildNavigationMesh(self::GRENADE_NAVIGATION_MESH_TILE_SIZE, self::GRENADE_NAVIGATION_MESH_OBJECT_HEIGHT);
|
||||
}
|
||||
|
||||
public function addRamp(Ramp $ramp): void
|
||||
@ -302,8 +300,8 @@ final class World
|
||||
public function dropItem(Player $player, Item $item): void
|
||||
{
|
||||
$dropEvent = new DropEvent($player, $item, $this);
|
||||
$dropEvent->onFloorLand(function (DropEvent $event): void {
|
||||
$this->dropItems[] = $event->getDropItem();
|
||||
$dropEvent->onFloorLand(function (DropItem $dropItem): void {
|
||||
$this->dropItems[] = $dropItem;
|
||||
});
|
||||
$this->game->addDropEvent($dropEvent);
|
||||
}
|
||||
@ -519,7 +517,7 @@ final class World
|
||||
$this->game->addSmokeEvent($event);
|
||||
}
|
||||
|
||||
public function processFlammableExplosion(Player $thrower, Point $epicentre, Flammable $item): void
|
||||
private function processFlammableExplosion(Player $thrower, Point $epicentre, Flammable $item): void
|
||||
{
|
||||
if ($this->grenadeNavMesh === null) {
|
||||
$this->regenerateNavigationMeshes();
|
||||
@ -606,7 +604,7 @@ final class World
|
||||
$damage = $flammableItem->calculateDamage($player->getArmorType() !== ArmorType::NONE);
|
||||
assert($fire->item instanceof Item);
|
||||
$this->playerHit(
|
||||
$player->getCentrePoint(), $player, $fire->initiator, SoundType::FLAME_PLAYER_HIT,
|
||||
$player->getCentrePointClone(), $player, $fire->initiator, SoundType::FLAME_PLAYER_HIT,
|
||||
$fire->item, $flame->center, $damage
|
||||
);
|
||||
$player->lowerHealth($damage);
|
||||
@ -645,7 +643,7 @@ final class World
|
||||
if (!$player->isAlive()) {
|
||||
continue; // @codeCoverageIgnore
|
||||
}
|
||||
if (Util::distanceSquared($epicentre, $player->getCentrePoint()) > $maxBlastDistanceSquared) {
|
||||
if (Util::distanceSquared($epicentre, $player->getCentrePointClone()) > $maxBlastDistanceSquared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace cs\Equipment;
|
||||
|
||||
use cs\Core\Bullet;
|
||||
use cs\Core\GameException;
|
||||
use cs\Enum\ArmorType;
|
||||
use cs\Enum\HitBoxType;
|
||||
@ -36,7 +37,7 @@ abstract class Grenade extends BaseEquipment implements AttackEnable
|
||||
|
||||
public function getDamageValue(HitBoxType $hitBox, ArmorType $armor): int
|
||||
{
|
||||
GameException::invalid('Should not be called');
|
||||
GameException::invalid();
|
||||
}
|
||||
|
||||
public function getKillAward(): int
|
||||
@ -54,4 +55,9 @@ abstract class Grenade extends BaseEquipment implements AttackEnable
|
||||
return ($this->primaryAttack ? 1.0 : 0.5);
|
||||
}
|
||||
|
||||
public function createBullet(): Bullet
|
||||
{
|
||||
GameException::invalid(get_class($this));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,14 +2,10 @@
|
||||
|
||||
namespace cs\Event;
|
||||
|
||||
use cs\Core\Bullet;
|
||||
use cs\Core\GameException;
|
||||
use cs\Core\Point;
|
||||
use cs\Core\World;
|
||||
use cs\Interface\Attackable;
|
||||
use cs\Interface\AttackEnable;
|
||||
use cs\Weapon\AmmoBasedWeapon;
|
||||
use cs\Weapon\Knife;
|
||||
|
||||
final class AttackEvent implements Attackable
|
||||
{
|
||||
@ -28,7 +24,7 @@ final class AttackEvent implements Attackable
|
||||
|
||||
public function fire(): AttackResult
|
||||
{
|
||||
$bullet = $this->createBullet();
|
||||
$bullet = $this->item->createBullet();
|
||||
$bullet->setOriginPlayer($this->playerId, $this->playingOnAttackerSide, $this->origin->clone());
|
||||
$result = new AttackResult($bullet);
|
||||
$checkDistance = $bullet->getDistanceTraveled();
|
||||
@ -75,19 +71,6 @@ final class AttackEvent implements Attackable
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function createBullet(): Bullet
|
||||
{
|
||||
if ($this->item instanceof AmmoBasedWeapon) {
|
||||
return $this->item->createBullet();
|
||||
}
|
||||
|
||||
if ($this->item instanceof Knife) {
|
||||
return $this->item->createBullet();
|
||||
}
|
||||
|
||||
GameException::notImplementedYet("No bullet for item: " . get_class($this->item)); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
public function applyRecoil(float $offsetHorizontal, float $offsetVertical): void
|
||||
{
|
||||
$this->angleHorizontal += $offsetHorizontal;
|
||||
|
@ -17,8 +17,9 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
{
|
||||
|
||||
private string $id;
|
||||
private Point $origin;
|
||||
private Point $dropPosition;
|
||||
private DropItem $dropItem;
|
||||
/** @var null|Closure(DropItem):void */
|
||||
private ?Closure $onLand = null;
|
||||
private float $angleHorizontal;
|
||||
private float $angleVertical;
|
||||
@ -29,8 +30,8 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
public function __construct(private readonly Player $player, private readonly Item $item, private readonly World $world)
|
||||
{
|
||||
$this->id = Sequence::next();
|
||||
$this->origin = $this->player->getSightPositionClone();
|
||||
$this->dropItem = new DropItem($this->id, $this->item, $this->origin->clone());
|
||||
$this->dropPosition = $this->player->getSightPositionClone();
|
||||
$this->dropItem = new DropItem($this->id, $this->item, $this->dropPosition);
|
||||
$this->angleHorizontal = $player->getSight()->getRotationHorizontal();
|
||||
$this->angleVertical = $player->getSight()->getRotationVertical();
|
||||
$this->velocity = ($player->isMoving() || $player->isJumping()) ? 30.0 : 20.0;
|
||||
@ -41,6 +42,7 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
}
|
||||
}
|
||||
|
||||
/** @param Closure(DropItem): void $callback function(DropItem $dropItem):void{} */
|
||||
public function onFloorLand(Closure $callback): void
|
||||
{
|
||||
$this->onLand = $callback;
|
||||
@ -53,7 +55,7 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
|
||||
public function process(int $tick): void
|
||||
{
|
||||
$dropPosition = $this->dropItem->getPosition();
|
||||
$dropPosition = $this->dropPosition;
|
||||
$this->time += $this->timeIncrement;
|
||||
$directionX = Util::directionX($this->angleHorizontal);
|
||||
$directionZ = Util::directionZ($this->angleHorizontal);
|
||||
@ -108,7 +110,7 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
if ($floorCandidate) {
|
||||
$dropPosition->setFrom($pos);
|
||||
if ($this->onLand) {
|
||||
call_user_func($this->onLand, $this);
|
||||
call_user_func($this->onLand, $this->dropItem);
|
||||
}
|
||||
$soundEvent = new SoundEvent($pos->clone(), SoundType::ITEM_DROP_LAND);
|
||||
$this->world->makeSound($soundEvent->setPlayer($player)->setItem($item)->addExtra('id', $this->id));
|
||||
@ -126,11 +128,6 @@ class DropEvent extends Event implements ForOneRoundMax
|
||||
$this->world->makeSound($soundEvent->setPlayer($player)->setItem($item)->addExtra('id', $this->id));
|
||||
}
|
||||
|
||||
public function getDropItem(): DropItem
|
||||
{
|
||||
return $this->dropItem;
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public function serialize(): array
|
||||
{
|
||||
|
@ -55,7 +55,7 @@ final class ThrowEvent extends Event implements Attackable, ForOneRoundMax
|
||||
$this->ball = new BallCollider($this->world, $origin, $radius, $this->angleHorizontal, $this->angleVertical);
|
||||
$this->needsToLandOnFloor = !($this->item instanceof Flashbang || $this->item instanceof HighExplosive);
|
||||
$this->timeIncrement = 1 / $this->timeMsToTick(150); // fixme some good value or velocity or gravity :)
|
||||
$this->tickMax = $this->getTickId() + $this->timeMsToTick($this->needsToLandOnFloor ? 99999 : 1200);
|
||||
$this->tickMax = $this->getTickId() + $this->timeMsToTick($this->needsToLandOnFloor ? 30_000 : 1200);
|
||||
}
|
||||
|
||||
private function makeEvent(Point $point, SoundType $type): Event
|
||||
@ -73,13 +73,13 @@ final class ThrowEvent extends Event implements Attackable, ForOneRoundMax
|
||||
|
||||
private function finishLanding(Point $point): void
|
||||
{
|
||||
if (!$this->needsToLandOnFloor) {
|
||||
if ($this->needsToLandOnFloor === false) {
|
||||
$this->makeEvent($point, SoundType::GRENADE_LAND);
|
||||
$this->runOnCompleteHooks();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->tickMax > 0) {
|
||||
if ($this->tickMax > 0) { // @infection-ignore-all
|
||||
$point->addY(-$this->radius);
|
||||
$this->tickMax = 0;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace cs\Interface;
|
||||
|
||||
use cs\Core\Bullet;
|
||||
use cs\Enum\ArmorType;
|
||||
use cs\Enum\HitBoxType;
|
||||
use cs\Enum\ItemType;
|
||||
@ -21,4 +22,6 @@ interface AttackEnable {
|
||||
|
||||
public function getId(): int;
|
||||
|
||||
public function createBullet(): Bullet;
|
||||
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ namespace cs\Map;
|
||||
|
||||
use cs\Core\Box;
|
||||
use cs\Core\Floor;
|
||||
use cs\Core\PathFinder;
|
||||
use cs\Core\Point;
|
||||
use cs\Core\Wall;
|
||||
|
||||
@ -90,11 +89,6 @@ abstract class Map
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function getNavigationMesh(string $key): ?PathFinder
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public abstract function getBuyArea(bool $forAttackers): Box;
|
||||
|
||||
public abstract function getPlantArea(): Box;
|
||||
|
@ -28,7 +28,7 @@ final class Server
|
||||
private int $countDefenders = 0;
|
||||
private int $blockListMax = 500;
|
||||
private int $serverLag = 0;
|
||||
private int $tickNanoSeconds;
|
||||
private readonly int $tickNanoSeconds;
|
||||
|
||||
/** @var array<string,int> [ipAddress-port => playerId] */
|
||||
private array $loggedPlayers = [];
|
||||
@ -93,6 +93,7 @@ final class Server
|
||||
$this->sendGameStateToClients();
|
||||
|
||||
$tickId = 0;
|
||||
$noSleep = ($this->tickNanoSeconds === 0); // no sleep inside tests
|
||||
$nextNsGoal = hrtime(true) + $this->tickNanoSeconds;
|
||||
|
||||
while (true) {
|
||||
@ -105,15 +106,20 @@ final class Server
|
||||
$tickId++;
|
||||
|
||||
// Sleep time
|
||||
if ($noSleep) {
|
||||
continue;
|
||||
}
|
||||
// @codeCoverageIgnoreStart
|
||||
$nsCurrent = hrtime(true);
|
||||
$nsDelta = $nextNsGoal - $nsCurrent;
|
||||
$nextNsGoal += $this->tickNanoSeconds;
|
||||
if ($nsDelta > 1024) {
|
||||
time_nanosleep(0, $nsDelta); // @codeCoverageIgnore
|
||||
time_nanosleep(0, $nsDelta);
|
||||
} else {
|
||||
$this->log('Server lag detected on tick ' . ($tickId - 1), LogLevel::WARNING);
|
||||
$this->serverLag++;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $tickId;
|
||||
@ -138,7 +144,7 @@ final class Server
|
||||
{
|
||||
$playersRequest = [];
|
||||
for ($i = 1; $i <= $this->setting->playersMax * 2; $i++) {
|
||||
if (!$this->pollClient($address, $port, $msg)) {
|
||||
if (!$this->pollClient($address, $port, $msg)) { // @infection-ignore-all
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ trait AttackTrait
|
||||
$item = $this->getEquippedItem();
|
||||
if ($item instanceof ScopeItem) {
|
||||
$item->scope();
|
||||
return null;
|
||||
}
|
||||
if (!($item instanceof AttackEnable)) {
|
||||
return null; // @codeCoverageIgnore
|
||||
@ -76,7 +77,7 @@ trait AttackTrait
|
||||
}
|
||||
}
|
||||
|
||||
return null; // @codeCoverageIgnore
|
||||
return null;
|
||||
}
|
||||
|
||||
private function processAttackResult(AttackResult $result): AttackResult
|
||||
@ -97,7 +98,7 @@ trait AttackTrait
|
||||
return (int)ceil($base);
|
||||
}
|
||||
|
||||
protected function createAttackEvent(AttackEnable $item): Attackable
|
||||
private function createAttackEvent(AttackEnable $item): Attackable
|
||||
{
|
||||
$origin = $this->getSightPositionClone();
|
||||
|
||||
|
@ -19,10 +19,11 @@ trait CrouchTrait
|
||||
}
|
||||
|
||||
$event = new CrouchEvent($directionDown, function (CrouchEvent $event): void {
|
||||
$headHeightCrouch = Setting::playerHeadHeightCrouch();
|
||||
if ($event->directionDown) {
|
||||
$this->headHeight -= $event->moveOffset;
|
||||
if ($this->headHeight < Setting::playerHeadHeightCrouch()) {
|
||||
$this->headHeight = Setting::playerHeadHeightCrouch();
|
||||
if ($this->headHeight < $headHeightCrouch) {
|
||||
$this->headHeight = $headHeightCrouch;
|
||||
}
|
||||
} else {
|
||||
$targetHeadHeight = min(Setting::playerHeadHeightStand(), $this->headHeight + $event->moveOffset);
|
||||
@ -35,6 +36,7 @@ trait CrouchTrait
|
||||
}
|
||||
if ($this->world->isCollisionWithOtherPlayers($this->getId(), $candidate, $this->getBoundingRadius(), 2)) {
|
||||
$event->restartTimer();
|
||||
$this->headHeight = $headHeightCrouch;
|
||||
break;
|
||||
}
|
||||
$this->headHeight = $h;
|
||||
|
@ -53,6 +53,11 @@ trait MovementTrait
|
||||
return $this->position->clone();
|
||||
}
|
||||
|
||||
public function getCentrePointClone(): Point
|
||||
{
|
||||
return $this->getPositionClone()->addY((int) ceil($this->headHeight / 2));
|
||||
}
|
||||
|
||||
public function getSightPositionClone(): Point
|
||||
{
|
||||
return $this->position->clone()->addY($this->getSightHeight());
|
||||
@ -225,16 +230,14 @@ trait MovementTrait
|
||||
break;
|
||||
}
|
||||
|
||||
$target->setFrom($candidate);
|
||||
if ($this->activeFloor && !$this->world->isOnFloor($this->activeFloor, $target, $this->getBoundingRadius())) {
|
||||
$this->setActiveFloor(null);
|
||||
}
|
||||
if (!$looseFloor && !$this->activeFloor && !$this->isJumping()) { // do initial (one-shot) gravity bump
|
||||
$newY = $this->calculateGravity($target, 1);
|
||||
$candidate->setY($newY);
|
||||
$target->setY($newY);
|
||||
$candidate->setY($this->calculateGravity($candidate, 1));
|
||||
$looseFloor = true;
|
||||
}
|
||||
$target->setFrom($candidate);
|
||||
}
|
||||
|
||||
if ($this->isRunning() && !$this->isCrouching() && !$this->isFlying() && !$orig->equals($target)) {
|
||||
|
@ -267,6 +267,48 @@ class RoundTest extends BaseTestCase
|
||||
$this->assertTrue($game->getPlayer(1)->isPlayingOnAttackerSide());
|
||||
}
|
||||
|
||||
public function testKillInRoundEndCoolDown(): void
|
||||
{
|
||||
$gameProperty = $this->createNoPauseGameProperty();
|
||||
$gameProperty->start_money = 0;
|
||||
$gameProperty->freeze_time_sec = 1;
|
||||
$gameProperty->round_end_cool_down_sec = 1;
|
||||
$gameProperty->bomb_plant_time_ms = 0;
|
||||
$gameProperty->bomb_defuse_time_ms = 0;
|
||||
$gameProperty->max_rounds = 6;
|
||||
$game = $this->createTestGame(null, $gameProperty);
|
||||
$game->getPlayer(1)->setPosition(new Point(500, 0, 500));
|
||||
$enemy = new Player(2, Color::BLUE, false);
|
||||
$game->addPlayer($enemy);
|
||||
$enemy->setPosition($game->getPlayer(1)->getPositionClone());
|
||||
$enemy->getSight()->look(0, -90);
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->equip(InventorySlot::SLOT_BOMB),
|
||||
$this->waitNTicks(max(1000, Bomb::equipReadyTimeMs)),
|
||||
fn(Player $p) => $p->attack(),
|
||||
fn() => $this->assertSame(1, $game->getRoundNumber()),
|
||||
fn() => $this->assertSame(0, $enemy->getMoney()),
|
||||
fn() => $enemy->use(),
|
||||
fn() => $this->assertSame(300, $enemy->getMoney()),
|
||||
fn() => $this->assertSame(2, $game->getRoundNumber()),
|
||||
function () use ($enemy) {
|
||||
$enemy->setPosition($enemy->getPositionClone()->addX(500));
|
||||
$enemy->getSight()->look(-90, 0);
|
||||
$result = $this->assertPlayerHit($enemy->attack());
|
||||
$this->assertCount(2, $result->getHits());
|
||||
$this->assertSame(300, $result->getMoneyAward());
|
||||
$this->assertSame(600, $enemy->getMoney());
|
||||
},
|
||||
$this->waitNTicks(1000),
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$this->assertSame(2, $game->getRoundNumber());
|
||||
$this->assertSame(300 + 800 + 1400, $game->getPlayer(1)->getMoney());
|
||||
$this->assertSame(300 + 300 + 3500, $game->getPlayer(2)->getMoney());
|
||||
}
|
||||
|
||||
public function testMultipleRoundsScoreAndEvents(): void
|
||||
{
|
||||
$maxRounds = 4;
|
||||
@ -350,7 +392,7 @@ class RoundTest extends BaseTestCase
|
||||
|
||||
$expectedScoreBoard = [
|
||||
'score' => [2, 2],
|
||||
'lossBonus' => [1400, 1900],
|
||||
'lossBonus' => [1400, 1400],
|
||||
'history' => [
|
||||
1 => [
|
||||
'attackersWins' => false,
|
||||
@ -410,7 +452,7 @@ class RoundTest extends BaseTestCase
|
||||
$gameProperty->bomb_defuse_time_ms = 0;
|
||||
$gameProperty->bomb_explode_time_ms = 1;
|
||||
$gameProperty->round_time_ms = Bomb::equipReadyTimeMs * 2;
|
||||
$this->assertGreaterThan(1, $gameProperty->round_time_ms);
|
||||
$this->assertGreaterThan(Util::$TICK_RATE, $gameProperty->round_time_ms);
|
||||
$game = $this->createTestGame(null, $gameProperty);
|
||||
|
||||
$this->playPlayer($game, [
|
||||
@ -425,4 +467,17 @@ class RoundTest extends BaseTestCase
|
||||
$this->assertSame(4050, $game->getPlayer(1)->getMoney());
|
||||
}
|
||||
|
||||
public function testNoMoneyForAttackerIfSurvivedRoundWithoutBombPlant(): void
|
||||
{
|
||||
$maxRounds = 4;
|
||||
$game = $this->createNoPauseGame($maxRounds);
|
||||
$game->addPlayer(new Player(2, Color::GREEN, false));
|
||||
|
||||
$game->start();
|
||||
|
||||
$this->assertSame($maxRounds + 1, $game->getRoundNumber());
|
||||
$this->assertSame(4050, $game->getPlayer(1)->getMoney());
|
||||
$this->assertSame(800, $game->getPlayer(2)->getMoney());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ use cs\Enum\BuyMenuItem;
|
||||
use cs\Enum\Color;
|
||||
use cs\Enum\InventorySlot;
|
||||
use cs\Enum\SoundType;
|
||||
use cs\Equipment\Bomb;
|
||||
use cs\Equipment\Decoy;
|
||||
use cs\Equipment\Flashbang;
|
||||
use cs\Equipment\HighExplosive;
|
||||
@ -182,6 +183,7 @@ class InventoryTest extends BaseTestCase
|
||||
$p->getInventory()->earnMoney(15000);
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->getSight()->lookHorizontal(0),
|
||||
fn(Player $p) => $this->assertEmpty($game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $p->getSight()->lookVertical(-60),
|
||||
fn(Player $p) => $this->assertInstanceOf(Knife::class, $p->getEquippedItem()),
|
||||
@ -209,11 +211,22 @@ class InventoryTest extends BaseTestCase
|
||||
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->assertCount(1, $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()),
|
||||
fn(Player $p) => $p->dropEquippedItem(),
|
||||
fn(Player $p) => $p->getSight()->lookHorizontal(45),
|
||||
fn(Player $p) => $p->equipSecondaryWeapon(),
|
||||
fn(Player $p) => $p->dropEquippedItem(),
|
||||
$this->waitNTicks(200),
|
||||
fn(Player $p) => $this->assertCount(3, $game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)),
|
||||
fn(Player $p) => $p->getSight()->lookHorizontal(45),
|
||||
fn(Player $p) => $p->use(),
|
||||
fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
$this->endGame(),
|
||||
]);
|
||||
}
|
||||
@ -221,25 +234,76 @@ class InventoryTest extends BaseTestCase
|
||||
public function testDropAndPickupItem(): void
|
||||
{
|
||||
$game = $this->createNoPauseGame();
|
||||
$game->getPlayer(1)->equipSecondaryWeapon();
|
||||
$glock = $game->getPlayer(1)->getEquippedItem();
|
||||
$this->assertInstanceOf(PistolGlock::class, $glock);
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn() => $this->assertCount(0, $game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)),
|
||||
fn(Player $p) => $p->getSight()->look(90, -10),
|
||||
fn(Player $p) => $p->dropEquippedItem(),
|
||||
fn(Player $p) => $p->moveForward(),
|
||||
fn(Player $p) => $p->equipSecondaryWeapon(),
|
||||
fn(Player $p) => $p->getSight()->lookVertical(-30),
|
||||
$this->waitNTicks(PistolGlock::equipReadyTimeMs),
|
||||
fn(Player $p) => $this->assertInstanceOf(PistolGlock::class, $p->dropEquippedItem()),
|
||||
fn(Player $p) => $this->assertNotNull($p->attack()),
|
||||
fn(Player $p) => $p->getSight()->look(0, -90),
|
||||
function (Player $p) use ($glock) {
|
||||
$dropItem = $p->dropEquippedItem();
|
||||
$this->assertInstanceOf(PistolGlock::class, $dropItem);
|
||||
$this->assertSame(PistolGlock::magazineCapacity - 1, $dropItem->getAmmo());
|
||||
$this->assertSame($glock, $dropItem);
|
||||
},
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
fn(Player $p) => $p->moveForward(),
|
||||
$this->waitNTicks(200),
|
||||
fn() => $this->assertCount(2, $game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
$this->waitNTicks(400),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
fn(Player $p) => $p->moveForward(),
|
||||
fn(Player $p) => $p->moveForward(),
|
||||
fn(Player $p) => $p->moveForward(),
|
||||
fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)),
|
||||
fn() => $this->assertCount(1, $game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $this->assertInstanceOf(Knife::class, $p->getEquippedItem()),
|
||||
fn(Player $p) => $p->equipSecondaryWeapon(),
|
||||
fn(Player $p) => $this->assertInstanceOf(PistolGlock::class, $p->getEquippedItem()),
|
||||
function (Player $p) use ($glock) {
|
||||
$equippedItem = $p->getEquippedItem();
|
||||
$this->assertInstanceOf(PistolGlock::class, $equippedItem);
|
||||
$this->assertSame(PistolGlock::magazineCapacity - 1, $equippedItem->getAmmo());
|
||||
$this->assertSame($glock, $equippedItem);
|
||||
},
|
||||
fn(Player $p) => $p->getSight()->look(90, -10),
|
||||
fn(Player $p) => $p->dropEquippedItem(),
|
||||
fn(Player $p) => $p->moveLeft(),
|
||||
$this->waitNTicks(200),
|
||||
fn() => $this->assertCount(2, $game->getWorld()->getDropItems()),
|
||||
fn(Player $p) => $p->moveLeft(),
|
||||
fn() => $this->assertCount(2, $game->getWorld()->getDropItems()),
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$this->assertSame(PistolGlock::magazineCapacity - 1, $glock->getAmmo());
|
||||
}
|
||||
|
||||
public function testDropOnlyLastEquippedGrenadeOnDead(): void
|
||||
{
|
||||
$game = $this->createNoPauseGame();
|
||||
$game->addPlayer(new Player(2, Color::GREEN, false));
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->getInventory()->earnMoney(9000),
|
||||
fn(Player $p) => $p->setPosition(new Point(500, 0, 500)),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_MOLOTOV)),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_SMOKE)),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_HE)),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)),
|
||||
fn(Player $p) => $p->suicide(),
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$dropItems = $game->getWorld()->getDropItems();
|
||||
$this->assertCount(3, $dropItems);
|
||||
$this->assertInstanceOf(PistolGlock::class, $dropItems[0]->getItem());
|
||||
$this->assertInstanceOf(Bomb::class, $dropItems[1]->getItem());
|
||||
$this->assertInstanceOf(Decoy::class, $dropItems[2]->getItem());
|
||||
}
|
||||
|
||||
public function testDropAndInstantPickupItem(): void
|
||||
@ -286,6 +350,7 @@ class InventoryTest extends BaseTestCase
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->getInventory()->earnMoney(5000),
|
||||
fn(Player $p) => $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)),
|
||||
fn(Player $p) => $this->assertFalse($p->buyItem(BuyMenuItem::RIFLE_AK)),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::RIFLE_M4A4)),
|
||||
fn(Player $p) => $p->getSight()->look(220, -15),
|
||||
fn(Player $p) => $this->assertInstanceOf(RifleM4A4::class, $p->dropEquippedItem()),
|
||||
|
@ -129,7 +129,7 @@ class MovementTest extends BaseTestCase
|
||||
$this->assertPlayerPosition($game, new Point(0, 0, $wall->getBase() - 1));
|
||||
|
||||
$game->getPlayer(1)->setPosition(new Point());
|
||||
$game->getWorld()->addFloor(new Floor(new Point(0, $wall->getCeiling(), $wall->getOther()), 0, Setting::moveDistancePerTick()));
|
||||
$game->getWorld()->addFloor(new Floor(new Point(0, $wall->getCeiling(), $wall->getStart()->z), 0, Setting::moveDistancePerTick()));
|
||||
$game->start();
|
||||
$this->assertPlayerPosition($game, new Point(0, $wall->getCeiling(), Setting::moveDistancePerTick()));
|
||||
}
|
||||
@ -242,6 +242,7 @@ class MovementTest extends BaseTestCase
|
||||
$this->assertPositionNotSame($start, $p->getPositionClone());
|
||||
$this->assertGreaterThan($start->x, $p->getPositionClone()->x);
|
||||
$this->assertGreaterThan($start->z, $p->getPositionClone()->z);
|
||||
$this->assertPositionSame(new Point(520, 0, 520), $p->getPositionClone());
|
||||
}
|
||||
|
||||
public function testPlayerSlowMovementWhenFlying(): void
|
||||
@ -453,33 +454,46 @@ class MovementTest extends BaseTestCase
|
||||
|
||||
public function testPlayerCrouchingStanding(): void
|
||||
{
|
||||
$playerCommands = [
|
||||
function (Player $p) {
|
||||
$this->assertFalse($p->isCrouching());
|
||||
},
|
||||
$this->simulateGame([
|
||||
fn(Player $p) => $this->assertFalse($p->isCrouching()),
|
||||
fn(Player $p) => $p->crouch(),
|
||||
function (Player $p) {
|
||||
$this->assertTrue($p->isCrouching());
|
||||
},
|
||||
fn(Player $p) => $this->assertTrue($p->isCrouching()),
|
||||
$this->waitXTicks(Setting::tickCountCrouch()),
|
||||
function (Player $p) {
|
||||
$this->assertTrue($p->isCrouching());
|
||||
},
|
||||
fn(Player $p) => $this->assertTrue($p->isCrouching()),
|
||||
function (Player $p) {
|
||||
$p->stand();
|
||||
$p->stand();
|
||||
},
|
||||
function (Player $p) {
|
||||
$this->assertTrue($p->isCrouching());
|
||||
},
|
||||
fn(Player $p) => $this->assertTrue($p->isCrouching()),
|
||||
$this->waitXTicks(Setting::tickCountCrouch()),
|
||||
function (Player $p) {
|
||||
$this->assertFalse($p->isCrouching());
|
||||
},
|
||||
fn(Player $p) => $this->assertFalse($p->isCrouching()),
|
||||
$this->endGame(),
|
||||
];
|
||||
]);
|
||||
}
|
||||
|
||||
$this->simulateGame($playerCommands);
|
||||
public function testPlayerCrouchingAndCannotStandWhenOtherPlayerChillingOnTop(): void
|
||||
{
|
||||
$game = $this->createNoPauseGame();
|
||||
$game->addPlayer(new Player(2, Color::GREEN, false));
|
||||
$p2 = $game->getPlayer(2);
|
||||
$p2->setHeadHeight(2); // for continue/break infection detection in isCollisionWithOtherPlayers
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $this->assertSame(Setting::playerHeadHeightStand(), $p->getHeadHeight()),
|
||||
fn(Player $p) => $p->crouch(),
|
||||
$this->waitXTicks(Setting::tickCountCrouch()),
|
||||
fn(Player $p) => $this->assertSame(Setting::playerHeadHeightCrouch(), $p->getHeadHeight()),
|
||||
function (Player $p) use ($p2) {
|
||||
$p2->setPosition($p->getPositionClone()->addY($p->getHeadHeight() + Setting::crouchDistancePerTick() * 2));
|
||||
$p->stand();
|
||||
},
|
||||
fn(Player $p) => $this->assertGreaterThan(Setting::playerHeadHeightCrouch(), $p->getHeadHeight()),
|
||||
$this->waitXTicks(Setting::tickCountCrouch()),
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$this->assertSame(Setting::playerHeadHeightCrouch(), $game->getPlayer(1)->getHeadHeight());
|
||||
$this->assertSame(Setting::playerHeadHeightCrouch() + 1, $game->getPlayer(2)->getPositionClone()->y);
|
||||
}
|
||||
|
||||
public function testPlayerMouseOrthogonalMovement1(): void
|
||||
|
@ -8,7 +8,9 @@ use cs\Core\Setting;
|
||||
use cs\Core\Wall;
|
||||
use cs\Enum\BuyMenuItem;
|
||||
use cs\Enum\Color;
|
||||
use cs\Enum\SoundType;
|
||||
use cs\Equipment\HighExplosive;
|
||||
use cs\Event\SoundEvent;
|
||||
use cs\Weapon\PistolGlock;
|
||||
use Test\BaseTestCase;
|
||||
|
||||
@ -18,7 +20,7 @@ class HighExplosiveGrenadeTest extends BaseTestCase
|
||||
public function testOwnDamage(): void
|
||||
{
|
||||
$game = $this->createNoPauseGame();
|
||||
$game->getPlayer(1)->setPosition(new Point(500,0, 500));
|
||||
$game->getPlayer(1)->setPosition(new Point(500, 0, 500));
|
||||
$health = $game->getPlayer(1)->getHealth();
|
||||
|
||||
$this->playPlayer($game, [
|
||||
@ -174,11 +176,40 @@ class HighExplosiveGrenadeTest extends BaseTestCase
|
||||
$this->assertSame(1, $game->getRoundNumber());
|
||||
$this->assertCount(3, $game->getAlivePlayers());
|
||||
$this->assertLessThan(100, $health);
|
||||
$this->assertLessThan(100, $game->getPlayer(2)->getHealth());
|
||||
$this->assertLessThan(100, $game->getPlayer(3)->getHealth());
|
||||
$this->assertGreaterThan($health, $game->getPlayer(2)->getHealth());
|
||||
$this->assertLessThan(100, $game->getPlayer(2)->getHealth());
|
||||
$this->assertLessThan(100, $game->getPlayer(3)->getHealth());
|
||||
$this->assertGreaterThan($health, $game->getPlayer(2)->getHealth());
|
||||
$health = $game->getPlayer(2)->getHealth();
|
||||
$this->assertGreaterThan($health, $game->getPlayer(3)->getHealth());
|
||||
$this->assertGreaterThan($health, $game->getPlayer(3)->getHealth());
|
||||
}
|
||||
|
||||
public function testExplodeMidAir(): void
|
||||
{
|
||||
$game = $this->createNoPauseGame();
|
||||
|
||||
$landed = false;
|
||||
$game->onEvents(function (array $events) use (&$landed): void {
|
||||
foreach ($events as $event) {
|
||||
if ($event instanceof SoundEvent && $event->type === SoundType::GRENADE_LAND) {
|
||||
$landed = true;
|
||||
$this->assertGreaterThan(HighExplosive::boundingRadius * 3, $event->position->y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->getSight()->look(45, 90),
|
||||
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_HE)),
|
||||
$this->waitNTicks(HighExplosive::equipReadyTimeMs),
|
||||
fn(Player $p) => $this->assertInstanceOf(HighExplosive::class, $p->getEquippedItem()),
|
||||
fn(Player $p) => $this->assertNotNull($p->attack()),
|
||||
$this->waitNTicks(1200),
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($landed);
|
||||
$this->assertSame(1, $game->getRoundNumber());
|
||||
$this->assertLessThan(100, $game->getPlayer(1)->getHealth());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -63,6 +63,12 @@ class MolotovGrenadeTest extends BaseTestCase
|
||||
$p = $game->getPlayer(1);
|
||||
$this->assertSame(100, $p->getHealth());
|
||||
$this->assertTrue($game->getWorld()->activeMolotovExists());
|
||||
$this->assertNull($event->getPlayerId());
|
||||
$this->assertNull($event->getItem());
|
||||
$eventSerialized = $event->serialize();
|
||||
$this->assertIsArray($eventSerialized);
|
||||
$this->assertIsArray($eventSerialized['extra']);
|
||||
$this->assertNotEmpty($eventSerialized['extra']['id'] ?? false);
|
||||
$p->setPosition(new Point(500, 0, 500));
|
||||
}
|
||||
}
|
||||
@ -439,7 +445,7 @@ class MolotovGrenadeTest extends BaseTestCase
|
||||
fn() => $this->assertInstanceOf(Incendiary::class, $p3->getEquippedItem()),
|
||||
fn() => $this->assertNotNull($p3->attack()),
|
||||
$this->waitNTicks(Incendiary::MAX_TIME_MS),
|
||||
$this->endGame()
|
||||
$this->endGame(),
|
||||
]);
|
||||
|
||||
$this->assertSame(1, $game->getRoundNumber());
|
||||
|
@ -16,8 +16,10 @@ use cs\Enum\BuyMenuItem;
|
||||
use cs\Enum\Color;
|
||||
use cs\Enum\HitBoxType;
|
||||
use cs\Enum\ItemId;
|
||||
use cs\Enum\SoundType;
|
||||
use cs\Event\AttackResult;
|
||||
use cs\Event\KillEvent;
|
||||
use cs\Event\SoundEvent;
|
||||
use cs\Weapon\PistolGlock;
|
||||
use cs\Weapon\PistolUsp;
|
||||
use cs\Weapon\RifleAk;
|
||||
@ -144,6 +146,7 @@ class PlayerKillTest extends BaseTestCase
|
||||
$this->assertSame(0, $result->getMoneyAward());
|
||||
$this->assertSame(1, $game->getRoundNumber());
|
||||
$this->assertTrue($player1->isAlive());
|
||||
$this->assertSame(100 - 10 - 30, $player1->getArmorValue());
|
||||
}
|
||||
|
||||
public function testM4KillPlayerInFourBulletsInChestWithNoKevlar(): void
|
||||
@ -390,6 +393,48 @@ class PlayerKillTest extends BaseTestCase
|
||||
], $player2->getId());
|
||||
}
|
||||
|
||||
public function testArmorShooting(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
$p2 = new Player(2, Color::GREEN, false);
|
||||
$game->addPlayer($p2);
|
||||
$p1 = $game->getPlayer(1);
|
||||
|
||||
$p1->getSight()->look(-90, -10);
|
||||
$p1->setPosition(new Point(500, 0, 500));
|
||||
|
||||
$p2->getSight()->look(-90, -90);
|
||||
$p2->setPosition(new Point(300, 0, 500));
|
||||
$p2->buyItem(BuyMenuItem::KEVLAR_BODY);
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->equipSecondaryWeapon(),
|
||||
$this->waitNTicks(PistolGlock::equipReadyTimeMs),
|
||||
function (Player $p) use ($p2) {
|
||||
$this->assertSame(100, $p2->getHealth());
|
||||
$this->assertSame(100, $p2->getArmorValue());
|
||||
|
||||
$result = $this->assertPlayerHit($p->attack());
|
||||
$hits = $result->getHits();
|
||||
$this->assertCount(2, $hits);
|
||||
|
||||
$bodyShot = $hits[0];
|
||||
$this->assertInstanceOf(HitBox::class, $bodyShot);
|
||||
$this->assertSame(HitBoxType::BACK, $bodyShot->getType());
|
||||
|
||||
$this->assertLessThan(100, $p2->getHealth());
|
||||
$this->assertLessThan(100, $p2->getArmorValue());
|
||||
$this->assertTrue($p->isAlive());
|
||||
$this->assertTrue($p2->isAlive());
|
||||
},
|
||||
fn() => $this->assertSame(1, $game->getRoundNumber()),
|
||||
$this->waitNTicks(PistolGlock::recoilResetMs),
|
||||
fn(Player $p) => $p->getSight()->look(-90, 0),
|
||||
fn(Player $p) => $this->assertPlayerHit($p->attack()),
|
||||
$this->endGame(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function testPlayerHorizontalVerticalBullet(): void
|
||||
{
|
||||
$player2 = new Player(2, Color::GREEN, false);
|
||||
@ -531,6 +576,8 @@ class PlayerKillTest extends BaseTestCase
|
||||
$this->assertSame(3, $killEventsCount);
|
||||
$this->assertSame(3, $game->getScore()->getScoreDefenders());
|
||||
$this->assertSame(3 + 1, $game->getRoundNumber());
|
||||
$this->assertSame(6500, $p1->getMoney());
|
||||
$this->assertSame(11450, $player2->getMoney());
|
||||
}
|
||||
|
||||
public function testPlayerCannotKillDeadPlayer(): void
|
||||
|
@ -28,7 +28,8 @@ class ShootTest extends BaseTestCase
|
||||
fn(Player $p) => $p->buyItem(BuyMenuItem::RIFLE_AK),
|
||||
$this->waitNTicks(RifleAk::equipReadyTimeMs),
|
||||
fn(Player $p) => $p->getSight()->lookVertical(-91),
|
||||
fn(Player $p) => $p->attack(),
|
||||
fn(Player $p) => $this->assertNull($p->attackSecondary()),
|
||||
fn(Player $p) => $this->assertPlayerNotHit($p->attack()),
|
||||
];
|
||||
|
||||
$game = $this->simulateGame($playerCommands, [GameProperty::START_MONEY => 16000]);
|
||||
@ -277,6 +278,7 @@ class ShootTest extends BaseTestCase
|
||||
|
||||
$this->assertCount(2, $game->getAlivePlayers());
|
||||
$this->assertSame(-1, $game->getScore()->getPlayerStat(1)->getKills());
|
||||
$this->assertSame(500, $game->getPlayer(1)->getMoney());
|
||||
}
|
||||
|
||||
public function testDamageLowOnRangeMaxDamage(): void
|
||||
@ -284,6 +286,15 @@ class ShootTest extends BaseTestCase
|
||||
$game = $this->createTestGame();
|
||||
$game->addPlayer(new Player(2, Color::ORANGE, true));
|
||||
|
||||
$bulletHitHeadShotsCount = 0;
|
||||
$game->onEvents(function (array $events) use (&$bulletHitHeadShotsCount): void {
|
||||
foreach ($events as $event) {
|
||||
if ($event instanceof SoundEvent && $event->type === SoundType::BULLET_HIT_HEADSHOT) {
|
||||
$bulletHitHeadShotsCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->playPlayer($game, [
|
||||
fn(Player $p) => $p->setPosition(new Point(500)),
|
||||
fn(Player $p) => $game->getPlayer(2)->setPosition(new Point(500, 0, PistolGlock::rangeMaxDamage + $p->getBoundingRadius())),
|
||||
@ -295,6 +306,7 @@ class ShootTest extends BaseTestCase
|
||||
]);
|
||||
|
||||
$this->assertSame(99, $game->getPlayer(2)->getHealth());
|
||||
$this->assertSame(1, $bulletHitHeadShotsCount);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,6 +12,16 @@ use Test\BaseTest;
|
||||
class CollisionTest extends BaseTest
|
||||
{
|
||||
|
||||
public function testPointWithCircle(): void
|
||||
{
|
||||
$this->assertTrue(Collision::pointWithCircle(10, 10, 10, 10, 1));
|
||||
$this->assertTrue(Collision::pointWithCircle(11, 10, 10, 10, 1));
|
||||
$this->assertTrue(Collision::pointWithCircle(10, 11, 10, 10, 1));
|
||||
|
||||
$this->assertFalse(Collision::pointWithCircle(10, 13, 10, 10, 2));
|
||||
$this->assertFalse(Collision::pointWithCircle(13, 10, 10, 10, 2));
|
||||
}
|
||||
|
||||
public function testCircleWithPlaneFalse(): void
|
||||
{
|
||||
$radius = 2;
|
||||
@ -25,6 +35,7 @@ class CollisionTest extends BaseTest
|
||||
new Point2D(8, 4),
|
||||
new Point2D(8, 4),
|
||||
new Point2D(6, 6),
|
||||
new Point2D(-1, 0),
|
||||
];
|
||||
foreach ($circles as $circleCenter) {
|
||||
$this->assertFalse(Collision::circleWithPlane($circleCenter, $radius, $floor), "Circle: {$circleCenter} x Floor: {$floor}");
|
||||
@ -249,6 +260,11 @@ class CollisionTest extends BaseTest
|
||||
{
|
||||
$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->assertTrue(Collision::pointWithBoxBoundary(new Point(0, 1, 1), new Point(), new Point(1, 1, 1)));
|
||||
$this->assertTrue(Collision::pointWithBoxBoundary(new Point(0, 1, 1), new Point(), new Point(1, 8, 1)));
|
||||
$this->assertTrue(Collision::pointWithBoxBoundary(new Point(1, 1, 1), new Point(), new Point(1, 8, 1)));
|
||||
$this->assertTrue(Collision::pointWithBoxBoundary(new Point(1, 1, 0), new Point(), new Point(1, 8, 1)));
|
||||
|
||||
$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)));
|
||||
@ -256,35 +272,60 @@ class CollisionTest extends BaseTest
|
||||
|
||||
public function testBoxWithBox(): void
|
||||
{
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5, 0, -5), new Point(5, 4, 5), new Point(1, 0, -1), new Point(3, 3, 1))
|
||||
);
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,4,-1), new Point(3,7,1))
|
||||
);
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,-2,-1), new Point(3,1,1))
|
||||
);
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,2,-3), new Point(3,5,-1))
|
||||
);
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(-3,3,2), new Point(-1,6,4))
|
||||
);
|
||||
$this->assertTrue(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,2,-7), new Point(3,5,-5))
|
||||
);
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 0, -1), new Point(3, 3, 1),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 4, -1), new Point(3, 7, 1),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, -2, -1), new Point(3, 1, 1),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 2, -3), new Point(3, 5, -1),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(-3, 3, 2), new Point(-1, 6, 4),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 2, -7), new Point(3, 5, -5),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(5, 2, -7), new Point(6, 5, -5),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 2, 5), new Point(3, 5, -5),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, -2, 5), new Point(3, 0, -5),
|
||||
));
|
||||
$this->assertTrue(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(-6, 0, 5), new Point(-5, 2, -5),
|
||||
));
|
||||
|
||||
$this->assertFalse(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,5,2), new Point(3,8,4))
|
||||
);
|
||||
$this->assertFalse(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,2,-8), new Point(3,5,-6))
|
||||
);
|
||||
$this->assertFalse(
|
||||
Collision::boxWithBox(new Point(-5,0,-5), new Point(5,4,5), new Point(1,-6,-5), new Point(3,-3,-3))
|
||||
);
|
||||
|
||||
$this->assertFalse(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 5, 2), new Point(3, 8, 4),
|
||||
));
|
||||
$this->assertFalse(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, 2, -8), new Point(3, 5, -6),
|
||||
));
|
||||
$this->assertFalse(Collision::boxWithBox(
|
||||
new Point(-5, 0, -5), new Point(5, 4, 5),
|
||||
new Point(1, -6, -5), new Point(3, -3, -3),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@ use cs\Weapon\PistolGlock;
|
||||
use SebastianBergmann\Timer\Timer;
|
||||
use Test\BaseTest;
|
||||
|
||||
/** @coversNothing */
|
||||
class PerformanceTest extends BaseTest
|
||||
{
|
||||
private static float $timeScale;
|
||||
@ -245,6 +246,7 @@ class PerformanceTest extends BaseTest
|
||||
$timer->start();
|
||||
$game->getWorld()->regenerateNavigationMeshes();
|
||||
$took = $timer->stop();
|
||||
$this->assertGreaterThan(10, $took->asMilliseconds());
|
||||
$this->assertLessThan(120 * self::$timeScale, $took->asMilliseconds());
|
||||
|
||||
$player = new Player(1, Color::GREEN, true);
|
||||
@ -256,36 +258,25 @@ class PerformanceTest extends BaseTest
|
||||
}
|
||||
$flammableItem = $player->getEquippedItem();
|
||||
$this->assertInstanceOf(Flammable::class, $flammableItem);
|
||||
$player->getSight()->look(0, -90);
|
||||
|
||||
$timer->start();
|
||||
$this->assertNotNull($player->attack());
|
||||
$attackResult = $player->attack();
|
||||
$took = $timer->stop();
|
||||
$this->assertLessThan(0.6 * self::$timeScale, $took->asMilliseconds());
|
||||
$this->assertNotNull($attackResult);
|
||||
$this->assertLessThan(0.8 * self::$timeScale, $took->asMilliseconds());
|
||||
|
||||
$epicentre = $player->getPositionClone()->addY($flammableItem->getBoundingRadius());
|
||||
$timer->start();
|
||||
$game->getWorld()->processFlammableExplosion($player, $epicentre, $flammableItem);
|
||||
$took = $timer->stop();
|
||||
$this->assertLessThan(0.6 * self::$timeScale, $took->asMilliseconds());
|
||||
|
||||
$samplesCount = 1;
|
||||
$timer->start();
|
||||
foreach (range(1, $samplesCount) as $i) {
|
||||
foreach (range(1, Util::millisecondsToFrames(Molotov::MAX_TIME_MS)) as $i) {
|
||||
$timer->start();
|
||||
$game->tick(++$tickId);
|
||||
}
|
||||
$took = $timer->stop();
|
||||
$this->assertLessThan(100, $player->getHealth());
|
||||
$this->assertLessThan(0.3 * self::$timeScale, $took->asMilliseconds() / $samplesCount);
|
||||
$took = $timer->stop();
|
||||
$this->assertLessThan(0.8 * self::$timeScale, $took->asMilliseconds(), "Tick {$tickId}");
|
||||
|
||||
$health = $player->getHealth();
|
||||
$samplesCount = 10;
|
||||
$timer->start();
|
||||
foreach (range(1, $samplesCount) as $i) {
|
||||
$game->tick(++$tickId);
|
||||
if ($game->getRoundNumber() === 2) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$took = $timer->stop();
|
||||
$this->assertLessThan($health, $player->getHealth());
|
||||
$this->assertLessThan(0.4 * self::$timeScale, $took->asMilliseconds() / $samplesCount);
|
||||
$this->assertSame(2, $game->getRoundNumber());
|
||||
}
|
||||
|
||||
private function createMolotovMap(): Map
|
||||
|
@ -27,6 +27,13 @@ class ProtocolTest extends BaseTest
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidCommandsWhenExtendingMaxCallPerTick(): void
|
||||
{
|
||||
$protocol = new Protocol\TextProtocol();
|
||||
$this->assertSame([['attack']], $protocol->parsePlayerControlCommands(implode($protocol::separator, ['attack'])));
|
||||
$this->assertSame([], $protocol->parsePlayerControlCommands(implode($protocol::separator, ['attack', 'attack'])));
|
||||
}
|
||||
|
||||
public function testTextProtocol(): void
|
||||
{
|
||||
$protocol = new Protocol\TextProtocol();
|
||||
|
@ -101,6 +101,7 @@ class ServerTest extends BaseTest
|
||||
$gameProperty->half_time_freeze_sec = 0;
|
||||
$gameProperty->round_end_cool_down_sec = 0;
|
||||
$gameProperty->round_time_ms = $roundTimeMs;
|
||||
$this->assertSame(1, $gameProperty->toArray()[GameProperty::MAX_ROUNDS] ?? false);
|
||||
|
||||
$game = new Game($gameProperty);
|
||||
$game->loadMap(new TestMap());
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace Test\World;
|
||||
|
||||
use cs\Core\Box;
|
||||
use cs\Core\Floor;
|
||||
use cs\Core\GameException;
|
||||
use cs\Core\PathFinder;
|
||||
use cs\Core\Point;
|
||||
@ -127,6 +128,78 @@ final class NavigationMeshTest extends BaseTestCase
|
||||
$this->assertSame('2,1,2', $validPoint->hash());
|
||||
}
|
||||
|
||||
public function testNeighboursShareSameNodeReference(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
$game->getTestMap()->startPointForNavigationMesh->set(1, 0, 1);
|
||||
$game->getWorld()->addBox(new Box(new Point(), 7, 1000, 4));
|
||||
$path = $game->getWorld()->buildNavigationMesh(3, 10);
|
||||
$graph = $path->getGraph();
|
||||
$this->assertSame(2, $graph->getNodesCount());
|
||||
$this->assertSame(2, $graph->getEdgeCount());
|
||||
$node1 = $graph->getNodeById('2,0,2');
|
||||
$this->assertNotNull($node1);
|
||||
$node2 = $graph->getNodeById('5,0,2');
|
||||
$this->assertNotNull($node2);
|
||||
$node1neighbours = $graph->getNeighbors($node1);
|
||||
$node2neighbours = $graph->getNeighbors($node2);
|
||||
$this->assertCount(1, $node1neighbours);
|
||||
$this->assertCount(1, $node2neighbours);
|
||||
$this->assertSame($node1, $node2neighbours[0]);
|
||||
$this->assertSame($node2, $node1neighbours[0]);
|
||||
}
|
||||
|
||||
public function testWallBoundary(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
$game->getTestMap()->startPointForNavigationMesh->set(1, 0, 1);
|
||||
$game->getWorld()->addBox(new Box(new Point(), 5, 1000, 4));
|
||||
$path = $game->getWorld()->buildNavigationMesh(3, 10);
|
||||
$graph = $path->getGraph();
|
||||
$this->assertSame(1, $graph->getNodesCount());
|
||||
$this->assertSame(0, $graph->getEdgeCount());
|
||||
|
||||
$node = $graph->getNodeById('2,0,2');
|
||||
$this->assertNotNull($node);
|
||||
$this->assertCount(0, $graph->getNeighbors($node));
|
||||
}
|
||||
|
||||
public function testUnderNavMesh(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
$expectedPoint = new Point(2, 2, 2);
|
||||
$game->getTestMap()->startPointForNavigationMesh->set(1, 2, 1);
|
||||
$game->getWorld()->addBox(new Box(new Point(), 5, 1000, 4));
|
||||
$game->getWorld()->addFloor(new Floor(new Point(2, 2, 0), 10, 10));
|
||||
|
||||
$path = $game->getWorld()->buildNavigationMesh(3, 10);
|
||||
$graph = $path->getGraph();
|
||||
|
||||
$nodes = $graph->getNodes();
|
||||
$this->assertCount(1, $nodes);
|
||||
$node = $graph->getNodeById('2,2,2');
|
||||
$this->assertNotNull($node);
|
||||
$this->assertSame($node, array_shift($nodes));
|
||||
$nodePosition = $node->getData();
|
||||
$this->assertInstanceOf(Point::class, $nodePosition);
|
||||
$this->assertPositionSame($expectedPoint, $nodePosition);
|
||||
|
||||
$tilePoint = $path->findTile(new Point(1, 0, 1), 1);
|
||||
$this->assertPositionSame($expectedPoint, $tilePoint);
|
||||
}
|
||||
|
||||
public function testDeepHole(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
$game->getTestMap()->startPointForNavigationMesh->set(1, 1000, 1);
|
||||
$game->getWorld()->addBox(new Box(new Point(), 10, 2000, 10));
|
||||
$game->getWorld()->addFloor(new Floor(new Point(1, 1000, 1), 1, 1));
|
||||
|
||||
$path = $game->getWorld()->buildNavigationMesh(3, 10);
|
||||
$this->assertSame(1, $path->getGraph()->getNodesCount());
|
||||
$this->assertSame(0, $path->getGraph()->getEdgeCount());
|
||||
}
|
||||
|
||||
public function testOneWayDirection(): void
|
||||
{
|
||||
$game = $this->createTestGame();
|
||||
|
@ -69,7 +69,8 @@ class PlayerBoostTest extends BaseTestCase
|
||||
$game->start();
|
||||
$p2pos = $game->getPlayer(2)->getPositionClone();
|
||||
$this->assertGreaterThan(0, $p2pos->y);
|
||||
$this->assertPositionSame(new Point(0, $player1->getHeadHeight() + 1, 0), $p2pos);
|
||||
$this->assertSame($player1->getHeadFloor()->getY(), $player1->getHeadHeight() + 1);
|
||||
$this->assertPositionSame(new Point(0, $player1->getHeadFloor()->getY(), 0), $p2pos);
|
||||
$this->assertFalse($player2->isFlying());
|
||||
$this->assertTrue($player2->canJump());
|
||||
|
||||
|
@ -7,7 +7,6 @@ use cs\Core\Game;
|
||||
use cs\Core\GameException;
|
||||
use cs\Core\GameState;
|
||||
use cs\Core\Point;
|
||||
use cs\Core\Point2D;
|
||||
use cs\Core\Ramp;
|
||||
use cs\Core\Setting;
|
||||
use cs\Core\Wall;
|
||||
@ -248,8 +247,13 @@ class WallTest extends BaseTestCase
|
||||
|
||||
$game->onTick(function (GameState $state) use ($numOfBoxes) {
|
||||
$state->getPlayer(1)->moveForward();
|
||||
if ($state->getTickId() === $numOfBoxes) {
|
||||
if ($state->getTickId() === $numOfBoxes + 1) {
|
||||
$this->assertGreaterThan(0, $state->getPlayer(1)->getPositionClone()->y);
|
||||
$this->assertSame(Setting::playerObstacleOvercomeHeight() * $numOfBoxes, $state->getPlayer(1)->getPositionClone()->y);
|
||||
}
|
||||
if ($state->getTickId() === $numOfBoxes + 2) {
|
||||
$this->assertLessThan(Setting::playerObstacleOvercomeHeight() * $numOfBoxes, $state->getPlayer(1)->getPositionClone()->y);
|
||||
$this->assertSame(Setting::playerObstacleOvercomeHeight() * $numOfBoxes - Setting::fallAmountPerTick() - 1, $state->getPlayer(1)->getPositionClone()->y); // test for initial (one-shot) gravity bump
|
||||
}
|
||||
});
|
||||
$game->start();
|
||||
|
@ -33,7 +33,9 @@ export class Setting {
|
||||
'KeyE': Action.USE,
|
||||
'Space': Action.JUMP,
|
||||
'ControlLeft': Action.CROUCH,
|
||||
'ControlRight': Action.CROUCH,
|
||||
'ShiftLeft': Action.WALK,
|
||||
'ShiftRight': Action.WALK,
|
||||
'KeyR': Action.RELOAD,
|
||||
'KeyG': Action.DROP,
|
||||
'KeyQ': Action.EQUIP_KNIFE,
|
||||
|
Loading…
x
Reference in New Issue
Block a user