diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d70388e..b862e61 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,9 +41,9 @@ jobs: composer check - name: "Check code coverage min percentage" - timeout-minutes: 5 + timeout-minutes: 4 run: | - echo ' cc.php + echo ' 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 diff --git a/README.md b/README.md index 0b30025..1ad673d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/composer.json b/composer.json index c56bb91..e7d7564 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/infection.json5 b/infection.json5 index d4c7896..f618b55 100644 --- a/infection.json5 +++ b/infection.json5 @@ -5,45 +5,68 @@ "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": { + "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": { + "ignore": [ + "cs\\Event\\*::serialize", + ], + }, + "Coalesce": { + "ignoreSourceCodeByRegex": [ + ".+\\(\\$skipPlayerIds\\[\\$playerId\\].+", + ".+SpeedMultiplier-\\{\\$itemId\\}.+", + ], + }, + "Division": { ignoreSourceCodeByRegex: [ - "public function processFlammableExplosion\\(.+", + ".+rand\\(.+", ], }, - "@identical": true, - "@number": false, - "@operator": false, - "@regex": true, - "@removal": true, "MatchArmRemoval": { "ignoreSourceCodeByRegex": [ ".+GameException::invalid\\(.+", ], }, - "ArrayItemRemoval": { - "ignore": [ - "cs\\Event\\*::serialize", - ], - }, "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\\(.+", + ], + }, }, } diff --git a/server/src/Core/Game.php b/server/src/Core/Game.php index a79418b..b3940b0 100644 --- a/server/src/Core/Game.php +++ b/server/src/Core/Game.php @@ -451,19 +451,18 @@ private function roundReset(bool $firstRound, RoundEndEvent $roundEndEvent): voi 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 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $ // 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 diff --git a/server/src/Core/GameFactory.php b/server/src/Core/GameFactory.php index d870c21..339f806 100644 --- a/server/src/Core/GameFactory.php +++ b/server/src/Core/GameFactory.php @@ -16,6 +16,7 @@ public static function createDefaultCompetitive(): Game return new Game($properties); } + /** @infection-ignore-all */ public static function createDebug(): Game { $properties = new GameProperty(); diff --git a/server/src/Core/GameProperty.php b/server/src/Core/GameProperty.php index f07d6f6..0a530ad 100644 --- a/server/src/Core/GameProperty.php +++ b/server/src/Core/GameProperty.php @@ -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"); } /** diff --git a/server/src/Core/HitBox.php b/server/src/Core/HitBox.php index 3a7a1b1..11caeb5 100644 --- a/server/src/Core/HitBox.php +++ b/server/src/Core/HitBox.php @@ -93,8 +93,7 @@ private function calculateArmorDamage(BaseWeapon $shootItem, ArmorType $armorTyp 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; } diff --git a/server/src/Core/PathFinder.php b/server/src/Core/PathFinder.php index 44c1993..65eb497 100644 --- a/server/src/Core/PathFinder.php +++ b/server/src/Core/PathFinder.php @@ -122,7 +122,7 @@ public function findTile(Point $pointOnFloor, int $radius): Point 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 @@ public function findTile(Point $pointOnFloor, int $radius): Point $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 @@ public function buildNavigationMesh(Point $start, int $objectHeight): void if (array_key_exists($currentKey, $this->visited)) { continue; } + $this->visited[$currentKey] = true; - $currentNodeOrNull = $this->graph->getNodeById($currentKey); - $currentNode = $currentNodeOrNull ?? new Node($currentKey, $current); + $currentNode = $this->graph->getNodeById($currentKey); + if ($currentNode === null) { + $currentNode = new Node($currentKey, $current); + $this->graph->addNode($currentNode); + } - $hasNeighbour = false; 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 @@ public function buildNavigationMesh(Point $start, int $objectHeight): void $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 } diff --git a/server/src/Core/Player.php b/server/src/Core/Player.php index 57c3533..d75f249 100644 --- a/server/src/Core/Player.php +++ b/server/src/Core/Player.php @@ -271,11 +271,6 @@ public function getHeadFloor(): Floor return $this->headFloor; } - public function getCentrePoint(): Point - { - return $this->getPositionClone()->addY((int) ceil($this->headHeight / 2)); - } - /** * @return list */ diff --git a/server/src/Core/Score.php b/server/src/Core/Score.php index 5c8f513..8a63113 100644 --- a/server/src/Core/Score.php +++ b/server/src/Core/Score.php @@ -45,8 +45,8 @@ public function swapTeams(): void $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 @@ public function roundEnd(RoundEndEvent $event): void $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 @@ public function getScoreDefenders(): int 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 diff --git a/server/src/Core/Setting.php b/server/src/Core/Setting.php index de91028..9c63d37 100644 --- a/server/src/Core/Setting.php +++ b/server/src/Core/Setting.php @@ -17,6 +17,7 @@ final class Setting 'flyingMovementSpeedMultiplier' => 0.8, 'throwSpeed' => 40, + 'playerVelocity' => 0, 'playerHeadRadius' => 10, 'playerBoundingRadius' => 60, 'playerJumpHeight' => 150, @@ -51,8 +52,6 @@ public static function loadConstants(array $constants): void */ 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 @@ public static function playerBoundingRadius(): int 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 diff --git a/server/src/Core/Wall.php b/server/src/Core/Wall.php index 2563f0d..1ac35a1 100644 --- a/server/src/Core/Wall.php +++ b/server/src/Core/Wall.php @@ -28,11 +28,6 @@ public function getBase(): int 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; diff --git a/server/src/Core/World.php b/server/src/Core/World.php index 968e9a5..a41b246 100644 --- a/server/src/Core/World.php +++ b/server/src/Core/World.php @@ -88,9 +88,7 @@ public function loadMap(Map $map): void 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 @@ public function tryPickDropItems(Player $player): void 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 @@ private function processSmokeExpansion(Player $initiator, Point $epicentre, Smok $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 @@ public function checkFlameDamage(GrillEvent $fire, int $tickId): void $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 @@ private function processHighExplosiveBlast(Player $thrower, Point $epicentre, Hi if (!$player->isAlive()) { continue; // @codeCoverageIgnore } - if (Util::distanceSquared($epicentre, $player->getCentrePoint()) > $maxBlastDistanceSquared) { + if (Util::distanceSquared($epicentre, $player->getCentrePointClone()) > $maxBlastDistanceSquared) { continue; } diff --git a/server/src/Equipment/Grenade.php b/server/src/Equipment/Grenade.php index a512420..e5dfc87 100644 --- a/server/src/Equipment/Grenade.php +++ b/server/src/Equipment/Grenade.php @@ -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 @@ public function attackSecondary(Attackable $event): ?AttackResult public function getDamageValue(HitBoxType $hitBox, ArmorType $armor): int { - GameException::invalid('Should not be called'); + GameException::invalid(); } public function getKillAward(): int @@ -54,4 +55,9 @@ public function getSpeedMultiplier(): float return ($this->primaryAttack ? 1.0 : 0.5); } + public function createBullet(): Bullet + { + GameException::invalid(get_class($this)); + } + } diff --git a/server/src/Event/AttackEvent.php b/server/src/Event/AttackEvent.php index 6fff068..1566740 100644 --- a/server/src/Event/AttackEvent.php +++ b/server/src/Event/AttackEvent.php @@ -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 @@ public function __construct( 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 @@ public function fire(): AttackResult 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; diff --git a/server/src/Event/DropEvent.php b/server/src/Event/DropEvent.php index c3ca31c..751a28f 100644 --- a/server/src/Event/DropEvent.php +++ b/server/src/Event/DropEvent.php @@ -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 @@ public function __construct(private readonly Player $player, private readonly It } } + /** @param Closure(DropItem): void $callback function(DropItem $dropItem):void{} */ public function onFloorLand(Closure $callback): void { $this->onLand = $callback; @@ -53,7 +55,7 @@ private function finish(): void 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 @@ public function process(int $tick): void 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 @@ public function process(int $tick): void $this->world->makeSound($soundEvent->setPlayer($player)->setItem($item)->addExtra('id', $this->id)); } - public function getDropItem(): DropItem - { - return $this->dropItem; - } - /** @codeCoverageIgnore */ public function serialize(): array { diff --git a/server/src/Event/ThrowEvent.php b/server/src/Event/ThrowEvent.php index 3f39de2..8a4c44b 100644 --- a/server/src/Event/ThrowEvent.php +++ b/server/src/Event/ThrowEvent.php @@ -55,7 +55,7 @@ public function __construct( $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 @@ private function makeEvent(Point $point, SoundType $type): Event 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; } diff --git a/server/src/Interface/AttackEnable.php b/server/src/Interface/AttackEnable.php index 9a19464..bb38031 100644 --- a/server/src/Interface/AttackEnable.php +++ b/server/src/Interface/AttackEnable.php @@ -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 @@ public function getType(): ItemType; public function getId(): int; + public function createBullet(): Bullet; + } diff --git a/server/src/Map/Map.php b/server/src/Map/Map.php index 85ccf0e..f59f464 100644 --- a/server/src/Map/Map.php +++ b/server/src/Map/Map.php @@ -4,7 +4,6 @@ use cs\Core\Box; use cs\Core\Floor; -use cs\Core\PathFinder; use cs\Core\Point; use cs\Core\Wall; @@ -90,11 +89,6 @@ public function getBombMaxBlastDistance(): int return 1000; } - public function getNavigationMesh(string $key): ?PathFinder - { - return null; - } - public abstract function getBuyArea(bool $forAttackers): Box; public abstract function getPlantArea(): Box; diff --git a/server/src/Net/Server.php b/server/src/Net/Server.php index 4600e0e..35190df 100644 --- a/server/src/Net/Server.php +++ b/server/src/Net/Server.php @@ -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 [ipAddress-port => playerId] */ private array $loggedPlayers = []; @@ -93,6 +93,7 @@ private function startGame(): int $this->sendGameStateToClients(); $tickId = 0; + $noSleep = ($this->tickNanoSeconds === 0); // no sleep inside tests $nextNsGoal = hrtime(true) + $this->tickNanoSeconds; while (true) { @@ -105,15 +106,20 @@ private function startGame(): int $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 @@ private function receiveClientsCommands(): void { $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; } diff --git a/server/src/Traits/Player/AttackTrait.php b/server/src/Traits/Player/AttackTrait.php index e0b65df..c16192b 100644 --- a/server/src/Traits/Player/AttackTrait.php +++ b/server/src/Traits/Player/AttackTrait.php @@ -62,6 +62,7 @@ public function attackSecondary(): ?AttackResult $item = $this->getEquippedItem(); if ($item instanceof ScopeItem) { $item->scope(); + return null; } if (!($item instanceof AttackEnable)) { return null; // @codeCoverageIgnore @@ -76,7 +77,7 @@ public function attackSecondary(): ?AttackResult } } - return null; // @codeCoverageIgnore + return null; } private function processAttackResult(AttackResult $result): AttackResult @@ -97,7 +98,7 @@ private function getThrowSpeed(): int return (int)ceil($base); } - protected function createAttackEvent(AttackEnable $item): Attackable + private function createAttackEvent(AttackEnable $item): Attackable { $origin = $this->getSightPositionClone(); diff --git a/server/src/Traits/Player/CrouchTrait.php b/server/src/Traits/Player/CrouchTrait.php index ac49997..b75bc7b 100644 --- a/server/src/Traits/Player/CrouchTrait.php +++ b/server/src/Traits/Player/CrouchTrait.php @@ -19,10 +19,11 @@ protected function createCrouchEvent(bool $directionDown): void } $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 @@ protected function createCrouchEvent(bool $directionDown): void } if ($this->world->isCollisionWithOtherPlayers($this->getId(), $candidate, $this->getBoundingRadius(), 2)) { $event->restartTimer(); + $this->headHeight = $headHeightCrouch; break; } $this->headHeight = $h; diff --git a/server/src/Traits/Player/MovementTrait.php b/server/src/Traits/Player/MovementTrait.php index 85ebe4f..fe9792f 100644 --- a/server/src/Traits/Player/MovementTrait.php +++ b/server/src/Traits/Player/MovementTrait.php @@ -53,6 +53,11 @@ public function getPositionClone(): Point 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 @@ private function processMovement(int $moveX, int $moveZ, Point $current): Point 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)) { diff --git a/test/og/Game/RoundTest.php b/test/og/Game/RoundTest.php index 96ea8ae..a5d1034 100644 --- a/test/og/Game/RoundTest.php +++ b/test/og/Game/RoundTest.php @@ -267,6 +267,48 @@ function () use ($enemy) { $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 @@ function () use ($p1, $p2) { $expectedScoreBoard = [ 'score' => [2, 2], - 'lossBonus' => [1400, 1900], + 'lossBonus' => [1400, 1400], 'history' => [ 1 => [ 'attackersWins' => false, @@ -410,7 +452,7 @@ public function testBombExplodeMoney(): void $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 @@ public function testBombExplodeMoney(): void $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()); + } + } diff --git a/test/og/Inventory/InventoryTest.php b/test/og/Inventory/InventoryTest.php index de4d6cd..1991fc0 100644 --- a/test/og/Inventory/InventoryTest.php +++ b/test/og/Inventory/InventoryTest.php @@ -14,6 +14,7 @@ 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 @@ public function testPlayerBuyAndDropAndUseForPickup(): void $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 @@ public function testPlayerBuyAndDropAndUseForPickup(): void 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 @@ public function testPlayerBuyAndDropAndUseForPickup(): void 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->assertFalse($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)), - fn(Player $p) => $p->moveForward(), + 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)), - $this->waitNTicks(400), + $this->waitNTicks(200), + fn() => $this->assertCount(2, $game->getWorld()->getDropItems()), 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 @@ public function testDropToOtherPlayer(): void $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()), diff --git a/test/og/Movement/MovementTest.php b/test/og/Movement/MovementTest.php index d7ce8de..fe7d664 100644 --- a/test/og/Movement/MovementTest.php +++ b/test/og/Movement/MovementTest.php @@ -129,7 +129,7 @@ public function testPlayerIncrementYBySteppingOnSmallWall(): void $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 @@ function (Player $p) use (&$end) { $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 @@ function (Player $p) use ($origin) { 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(), + ]); + } + + 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->simulateGame($playerCommands); + $this->assertSame(Setting::playerHeadHeightCrouch(), $game->getPlayer(1)->getHeadHeight()); + $this->assertSame(Setting::playerHeadHeightCrouch() + 1, $game->getPlayer(2)->getPositionClone()->y); } public function testPlayerMouseOrthogonalMovement1(): void diff --git a/test/og/Shooting/HighExplosiveGrenadeTest.php b/test/og/Shooting/HighExplosiveGrenadeTest.php index 548018a..04993ef 100644 --- a/test/og/Shooting/HighExplosiveGrenadeTest.php +++ b/test/og/Shooting/HighExplosiveGrenadeTest.php @@ -8,7 +8,9 @@ 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 @@ public function testDistanceLowerDamage(): void $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()); } } diff --git a/test/og/Shooting/MolotovGrenadeTest.php b/test/og/Shooting/MolotovGrenadeTest.php index c6a6a8f..2a83831 100644 --- a/test/og/Shooting/MolotovGrenadeTest.php +++ b/test/og/Shooting/MolotovGrenadeTest.php @@ -63,6 +63,12 @@ public function testShrinkPhaseDoDamage(): void $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 @@ function (Player $p) { 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()); diff --git a/test/og/Shooting/PlayerKillTest.php b/test/og/Shooting/PlayerKillTest.php index cfc1662..8a995e6 100644 --- a/test/og/Shooting/PlayerKillTest.php +++ b/test/og/Shooting/PlayerKillTest.php @@ -16,8 +16,10 @@ 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 @@ public function testBulletHitOnePlayerOnlyOneHitBox(): void $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 @@ function (Player $p) { ], $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 @@ function (Player $p) { $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 diff --git a/test/og/Shooting/ShootTest.php b/test/og/Shooting/ShootTest.php index 082eb13..26c529c 100644 --- a/test/og/Shooting/ShootTest.php +++ b/test/og/Shooting/ShootTest.php @@ -28,7 +28,8 @@ public function testOneTapAmmoMagazine(): void 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 @@ function () use ($game) { $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 @@ public function testDamageLowOnRangeMaxDamage(): void $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 @@ public function testDamageLowOnRangeMaxDamage(): void ]); $this->assertSame(99, $game->getPlayer(2)->getHealth()); + $this->assertSame(1, $bulletHitHeadShotsCount); } } diff --git a/test/og/Unit/CollisionTest.php b/test/og/Unit/CollisionTest.php index 5dbacda..fe6aa99 100644 --- a/test/og/Unit/CollisionTest.php +++ b/test/og/Unit/CollisionTest.php @@ -12,6 +12,16 @@ 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 @@ public function testCircleWithPlaneFalse(): void 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 @@ 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->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 @@ public function testPointWithBoxBoundary(): void 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), + )); } diff --git a/test/og/Unit/PerformanceTest.php b/test/og/Unit/PerformanceTest.php index 0334ebe..82957f8 100644 --- a/test/og/Unit/PerformanceTest.php +++ b/test/og/Unit/PerformanceTest.php @@ -22,6 +22,7 @@ use SebastianBergmann\Timer\Timer; use Test\BaseTest; +/** @coversNothing */ class PerformanceTest extends BaseTest { private static float $timeScale; @@ -245,6 +246,7 @@ public function testMolotov(): void $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 @@ public function testMolotov(): void } $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 diff --git a/test/og/Unit/ProtocolTest.php b/test/og/Unit/ProtocolTest.php index e0675e1..9f7f259 100644 --- a/test/og/Unit/ProtocolTest.php +++ b/test/og/Unit/ProtocolTest.php @@ -27,6 +27,13 @@ public function testPlayerControlMethods(): void } } + 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(); diff --git a/test/og/Unit/ServerTest.php b/test/og/Unit/ServerTest.php index 99e65f9..54a1d4f 100644 --- a/test/og/Unit/ServerTest.php +++ b/test/og/Unit/ServerTest.php @@ -101,6 +101,7 @@ public function testServerGameOver(): void $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()); diff --git a/test/og/World/NavigationMeshTest.php b/test/og/World/NavigationMeshTest.php index 1d05ff4..56976a2 100644 --- a/test/og/World/NavigationMeshTest.php +++ b/test/og/World/NavigationMeshTest.php @@ -3,6 +3,7 @@ namespace Test\World; use cs\Core\Box; +use cs\Core\Floor; use cs\Core\GameException; use cs\Core\PathFinder; use cs\Core\Point; @@ -127,6 +128,78 @@ public function testBoundaryAbove(): void $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(); diff --git a/test/og/World/PlayerBoostTest.php b/test/og/World/PlayerBoostTest.php index 04f4bb9..7967e16 100644 --- a/test/og/World/PlayerBoostTest.php +++ b/test/og/World/PlayerBoostTest.php @@ -69,7 +69,8 @@ public function testPlayerFallDownWhenBoosterMoveAway(): void $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()); diff --git a/test/og/World/WallTest.php b/test/og/World/WallTest.php index 736ebbf..5947a0b 100644 --- a/test/og/World/WallTest.php +++ b/test/og/World/WallTest.php @@ -7,7 +7,6 @@ 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 @@ public function testPlayerRunningStairsToDeathBoundingRadius(): void $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(); diff --git a/www/assets/js/Setting.js b/www/assets/js/Setting.js index 9bf1cde..2ab73e7 100644 --- a/www/assets/js/Setting.js +++ b/www/assets/js/Setting.js @@ -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,