diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f312f4..d70388e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ on: - 'www/' jobs: - composer-check: + server-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: - name: "Check code coverage min percentage" timeout-minutes: 5 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: 10 + timeout-minutes: 8 run: | export XDEBUG_MODE=off grep '"timeout": 20,' infection.json5 diff --git a/README.md b/README.md index 3bb252f..0b30025 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) +# 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) 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 857998d..c56bb91 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=87", + "infection": "php -d memory_limit=180M vendor/bin/infection --only-covered --threads=6 --min-covered-msi=99", "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 a2ed0d6..d4c7896 100644 --- a/infection.json5 +++ b/infection.json5 @@ -34,8 +34,19 @@ "@operator": false, "@regex": true, "@removal": true, + "MatchArmRemoval": { + "ignoreSourceCodeByRegex": [ + ".+GameException::invalid\\(.+", + ], + }, + "ArrayItemRemoval": { + "ignore": [ + "cs\\Event\\*::serialize", + ], + }, "MethodCallRemoval": { "ignoreSourceCodeByRegex": [ + "\\$this->setActiveFloor\\(.+\\);", "\\$prevPos->setFrom\\(\\$candidate\\);", "\\$prevPos->setFrom\\(\\$newPos\\);", "\\$this->makeSound\\(.+\\);", @@ -44,6 +55,7 @@ "\\$soundEvent->setSurface\\(.+\\);", "\\$soundEvent->addExtra\\(.+\\);", "\\$this->addSoundEvent\\(.+\\);", + "\\$bullet->addPlayerIdSkip\\(\\$playerId\\);", ] }, "@return_value": true, diff --git a/server/src/Core/Game.php b/server/src/Core/Game.php index bd5250b..a79418b 100644 --- a/server/src/Core/Game.php +++ b/server/src/Core/Game.php @@ -108,17 +108,15 @@ public function tick(int $tickId): ?GameOverEvent } } $this->backtrack->finishState(); - $this->checkRoundEnd($alivePlayers[0], $alivePlayers[1]); + if (!$this->roundEndCoolDown) { + $this->checkRoundEnd($alivePlayers[0], $alivePlayers[1]); + } $this->processEvents($tickId); return null; } private function checkRoundEnd(int $defendersAlive, int $attackersAlive): void { - if ($this->roundEndCoolDown) { - return; - } - if ($this->playersCountAttackers > 0 && $attackersAlive === 0) { $this->roundEnd(false, RoundEndReason::ALL_ENEMIES_ELIMINATED); return; @@ -372,10 +370,6 @@ public function bombPlanted(Player $planter): void public function roundEnd(bool $attackersWins, RoundEndReason $reason): void { - if ($this->roundEndCoolDown) { - return; - } - $this->roundEndCoolDown = true; $roundEndEvent = new RoundEndEvent($this, $attackersWins, $reason); $roundEndEvent->onComplete[] = fn() => $this->endRound($roundEndEvent); @@ -467,8 +461,7 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $ $amount += match ($roundEndEvent->reason) { RoundEndReason::ALL_ENEMIES_ELIMINATED => 3250, RoundEndReason::BOMB_EXPLODED => 3500, - RoundEndReason::TIME_RUNS_OUT, - RoundEndReason::BOMB_DEFUSED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore + RoundEndReason::TIME_RUNS_OUT, RoundEndReason::BOMB_DEFUSED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore }; } elseif (!$player->isAlive()) { $amount += $this->score->getMoneyLossBonus(true); @@ -482,7 +475,7 @@ private function calculateRoundMoneyAward(RoundEndEvent $roundEndEvent, Player $ $amount += match ($roundEndEvent->reason) { RoundEndReason::ALL_ENEMIES_ELIMINATED, RoundEndReason::TIME_RUNS_OUT => 3250, RoundEndReason::BOMB_DEFUSED => 3500, - RoundEndReason::BOMB_EXPLODED => throw new GameException('Invalid? ' . $roundEndEvent->reason->value), // @codeCoverageIgnore + RoundEndReason::BOMB_EXPLODED => GameException::invalid((string)$roundEndEvent->reason->value), // @codeCoverageIgnore }; } else { $amount += $this->score->getMoneyLossBonus(false); diff --git a/server/src/Core/GameException.php b/server/src/Core/GameException.php index 735b967..56411f3 100644 --- a/server/src/Core/GameException.php +++ b/server/src/Core/GameException.php @@ -15,4 +15,9 @@ public static function notImplementedYet(string $msg = ''): never throw new self("Not implemented yet! " . $msg); } + public static function invalid(string $msg = ''): never + { + throw new self("This should not be called! " . $msg); + } + } diff --git a/server/src/Core/Inventory.php b/server/src/Core/Inventory.php index 78cd254..db5fa41 100644 --- a/server/src/Core/Inventory.php +++ b/server/src/Core/Inventory.php @@ -39,10 +39,7 @@ public function reset(bool $isAttackerSide, bool $respawn): void ]; $this->equippedSlot = InventorySlot::SLOT_SECONDARY->value; $this->lastEquippedSlotId = InventorySlot::SLOT_KNIFE->value; - $this->lastEquippedGrenadeSlots = [ - InventorySlot::SLOT_GRENADE_SMOKE->value, InventorySlot::SLOT_GRENADE_MOLOTOV->value, InventorySlot::SLOT_GRENADE_HE->value, - InventorySlot::SLOT_GRENADE_FLASH->value, InventorySlot::SLOT_GRENADE_DECOY->value, - ]; + $this->lastEquippedGrenadeSlots = InventorySlot::getGrenadeSlotIds(); } else { foreach ($this->items as $item) { $item->reset(); @@ -52,12 +49,15 @@ public function reset(bool $isAttackerSide, bool $respawn): void } } - $this->removeBomb(); + if ($this->has(InventorySlot::SLOT_BOMB->value)) { + $this->removeBomb(); + } $this->store->reset($isAttackerSide, $this->items); } - private function updateEquippedSlot(): int + private function updateEquippedSlot(Item $item): int { + $this->tryRemoveLastEquippedGrenade($item); if (isset($this->items[$this->equippedSlot])) { return $this->equippedSlot; } @@ -72,8 +72,13 @@ private function updateEquippedSlot(): int public function removeBomb(): InventorySlot { - unset($this->items[InventorySlot::SLOT_BOMB->value]); - return InventorySlot::from($this->updateEquippedSlot()); + $bomb = $this->items[InventorySlot::SLOT_BOMB->value] ?? null; + if ($bomb) { + unset($this->items[InventorySlot::SLOT_BOMB->value]); + return InventorySlot::from($this->updateEquippedSlot($bomb)); + } + + GameException::invalid('You do not have bomb!'); // @codeCoverageIgnore } public function getEquipped(): Item @@ -81,6 +86,15 @@ public function getEquipped(): Item return $this->items[$this->equippedSlot]; } + private function tryRemoveLastEquippedGrenade(Item $item): void + { + if ($item instanceof Grenade) { + $index = array_search($item->getSlot()->value, $this->lastEquippedGrenadeSlots, true); + assert(is_int($index)); + unset($this->lastEquippedGrenadeSlots[$index]); + } + } + public function removeEquipped(): ?Item { if (!$this->getEquipped()->isUserDroppable()) { @@ -90,10 +104,7 @@ public function removeEquipped(): ?Item $item = $this->items[$this->equippedSlot]; if ($item->getQuantity() === 1) { unset($this->items[$this->equippedSlot]); - if ($item instanceof Grenade) { - unset($this->lastEquippedGrenadeSlots[$this->equippedSlot]); - } - $this->updateEquippedSlot(); + $this->updateEquippedSlot($item); $item->unEquip(); return $item; @@ -116,10 +127,7 @@ public function removeSlot(int $slot): void } unset($this->items[$slot]); - if ($item instanceof Grenade) { - unset($this->lastEquippedGrenadeSlots[$slot]); - } - $this->updateEquippedSlot(); + $this->updateEquippedSlot($item); } public function canBuy(Item $item): bool @@ -176,7 +184,7 @@ public function equip(InventorySlot $slot): ?EquipEvent $this->lastEquippedSlotId = $this->equippedSlot; $this->equippedSlot = $slot->value; if ($item instanceof Grenade) { - unset($this->lastEquippedGrenadeSlots[$slot->value]); + $this->tryRemoveLastEquippedGrenade($item); array_unshift($this->lastEquippedGrenadeSlots, $slot->value); } return $item->equip(); diff --git a/server/src/Core/Item.php b/server/src/Core/Item.php index d3fb3e1..56a167a 100644 --- a/server/src/Core/Item.php +++ b/server/src/Core/Item.php @@ -123,7 +123,7 @@ public function canPurchaseMultipleTime(self $newSlotItem): bool { return match ($this->getType()) { ItemType::TYPE_WEAPON_PRIMARY, ItemType::TYPE_WEAPON_SECONDARY => true, - default => GameException::notImplementedYet('New item? ' . get_class($this)) // @codeCoverageIgnore + default => GameException::invalid('New item? ' . get_class($this)) // @codeCoverageIgnore }; } diff --git a/server/src/Core/Plane.php b/server/src/Core/Plane.php index 2375707..737506e 100644 --- a/server/src/Core/Plane.php +++ b/server/src/Core/Plane.php @@ -42,7 +42,7 @@ public function getHitAntiForce(Point $point): int $hit = $point->to2D($this->axis2d); if ($hit->x < $this->point2DStart->x || $hit->x > $this->point2DEnd->x || $hit->y < $this->point2DStart->y || $hit->y > $this->point2DEnd->y) { - throw new GameException("Hit '{$hit}' out of plane boundary '{$this}'"); + throw new GameException("Hit '{$hit}' ({$point}) out of plane boundary '{$this}'"); } $margin = $this->wallBangEdgeMarginDistance; diff --git a/server/src/Core/Score.php b/server/src/Core/Score.php index 8dc8b6a..5c8f513 100644 --- a/server/src/Core/Score.php +++ b/server/src/Core/Score.php @@ -146,7 +146,7 @@ public function getPlayerStat(int $playerId): PlayerStat } /** - * @return array + * @return array */ public function toArray(): array { diff --git a/server/src/Core/World.php b/server/src/Core/World.php index b201605..968e9a5 100644 --- a/server/src/Core/World.php +++ b/server/src/Core/World.php @@ -48,6 +48,7 @@ final class World private Bomb $bomb; private int $lastBombActionTick = -1; private int $lastBombPlayerId = -1; + private int $bombActionTickBuffer = 1; private int $playerPotentialDistanceSquared; private ?PathFinder $grenadeNavMesh = null; @@ -314,20 +315,21 @@ public function playerUse(Player $player): void && $this->canBeSeen($player, $this->bomb->getPosition(), self::BOMB_RADIUS, self::BOMB_DEFUSE_MAX_DISTANCE) ) { $bomb = $this->bomb; - if ($this->lastBombActionTick + Util::millisecondsToFrames(50) < $this->getTickId()) { - $bomb->reset(); + $tickId = $this->getTickId(); + $playerId = $player->getId(); + if ($playerId !== $this->lastBombPlayerId || $this->lastBombActionTick + $this->bombActionTickBuffer < $tickId) { $player->stop(); + $bomb->startDefusing($tickId, $player->hasDefuseKit()); $soundEvent = new SoundEvent($player->getPositionClone()->addY(10), SoundType::BOMB_DEFUSING); $this->makeSound($soundEvent->setPlayer($player)->setItem($bomb)); } - $this->lastBombActionTick = $this->getTickId(); - $this->lastBombPlayerId = $player->getId(); + $this->lastBombActionTick = $tickId; + $this->lastBombPlayerId = $playerId; - $defused = $this->bomb->defuse($player->hasDefuseKit()); - if ($defused) { - $this->game->bombDefused($player); + if ($bomb->isDefused($tickId)) { $this->lastBombActionTick = -1; $this->lastBombPlayerId = -1; + $this->game->bombDefused($player); } return; } @@ -827,25 +829,24 @@ private function playerHit(Point $hitPoint, Player $playerHit, Player $playerCul } } - public function tryPlantBomb(Player $player): void + public function tryPlantBomb(Player $player, Bomb $bomb): void { if (!$this->canPlant($player)) { return; } - /** @var Bomb $bomb */ - $bomb = $player->getEquippedItem(); - if ($this->lastBombActionTick + Util::millisecondsToFrames(200) < $this->getTickId()) { - $bomb->reset(); + $tickId = $this->getTickId(); + $playerId = $player->getId(); + if ($playerId !== $this->lastBombPlayerId || $this->lastBombActionTick + $this->bombActionTickBuffer < $tickId) { $player->stop(); + $bomb->startPlanting($tickId); $soundEvent = new SoundEvent($player->getPositionClone()->addY(10), SoundType::BOMB_PLANTING); $this->makeSound($soundEvent->setPlayer($player)->setItem($bomb)); } $this->lastBombActionTick = $this->getTickId(); - $this->lastBombPlayerId = $player->getId(); + $this->lastBombPlayerId = $playerId; - $planted = $bomb->plant(); - if ($planted) { + if ($bomb->isPlanted($tickId)) { $player->equip($player->getInventory()->removeBomb()); $bomb->setPosition($player->getPositionClone()); $this->game->bombPlanted($player); @@ -858,10 +859,7 @@ public function tryPlantBomb(Player $player): void public function isPlantingOrDefusing(Player $player): bool { - return ( - $this->lastBombPlayerId === $player->getId() && - ($this->lastBombActionTick === $this->getTickId() || $this->lastBombActionTick + 1 === $this->getTickId()) - ); + return ($this->lastBombPlayerId === $player->getId() && $this->bombActionTickBuffer >= $this->getTickId() - $this->lastBombActionTick); } public function isWallOrFloorCollision(Point $start, Point $candidate, int $radius): bool @@ -896,7 +894,7 @@ public function isCollisionWithOtherPlayers(int $playerIdSkip, Point $point, int } if ($collider->collide($point, $radius, $height)) { - return $this->game->getPlayer($collider->playerId); + return $collider->getPlayer(); } } diff --git a/server/src/Enum/InventorySlot.php b/server/src/Enum/InventorySlot.php index 8998846..399017d 100644 --- a/server/src/Enum/InventorySlot.php +++ b/server/src/Enum/InventorySlot.php @@ -20,4 +20,16 @@ enum InventorySlot: int case SLOT_KEVLAR = 10; case SLOT_KIT = 11; + /** @return list */ + public static function getGrenadeSlotIds(): array + { + return [ + self::SLOT_GRENADE_SMOKE->value, + self::SLOT_GRENADE_MOLOTOV->value, + self::SLOT_GRENADE_HE->value, + self::SLOT_GRENADE_FLASH->value, + self::SLOT_GRENADE_DECOY->value, + ]; + } + } diff --git a/server/src/Equipment/Bomb.php b/server/src/Equipment/Bomb.php index a81808d..8bf6824 100644 --- a/server/src/Equipment/Bomb.php +++ b/server/src/Equipment/Bomb.php @@ -11,11 +11,13 @@ class Bomb extends BaseEquipment { + public const equipReadyTimeMs = 80; private Point $position; - private int $plantTickCount = 0; + private int $plantTickStart = 0; private int $plantTickCountMax; - private int $defuseTickCount = 0; + private int $defuseTickStart = 0; private int $defuseTickCountMax; + private int $tickToDefuseCount; public function __construct(int $plantTimeMs, int $defuseTimeMs, private int $maxBlastDistance = 1000) { @@ -42,8 +44,8 @@ public function getSlot(): InventorySlot public function reset(): void { parent::reset(); - $this->plantTickCount = 0; - $this->defuseTickCount = 0; + $this->plantTickStart = 0; + $this->defuseTickStart = 0; } public function unEquip(): void @@ -52,16 +54,25 @@ public function unEquip(): void $this->reset(); } - public function plant(): bool + public function startPlanting(int $tickStart): void { - $this->plantTickCount++; - return ($this->plantTickCount >= $this->plantTickCountMax); + $this->plantTickStart = $tickStart; } - public function defuse(bool $hasKit): bool + public function isPlanted(int $tickId): bool { - $this->defuseTickCount += $hasKit ? 2 : 1; - return ($this->defuseTickCount >= $this->defuseTickCountMax); + return ($tickId - $this->plantTickStart >= $this->plantTickCountMax); + } + + public function startDefusing(int $tickId, bool $hasDefuseKit): void + { + $this->defuseTickStart = $tickId; + $this->tickToDefuseCount = $hasDefuseKit ? (int)ceil($this->defuseTickCountMax / 2) : $this->defuseTickCountMax;; + } + + public function isDefused(int $tickId): bool + { + return ($tickId - $this->defuseTickStart >= $this->tickToDefuseCount); } public function setPosition(Point $position): void diff --git a/server/src/Equipment/Grenade.php b/server/src/Equipment/Grenade.php index 086b8e1..a512420 100644 --- a/server/src/Equipment/Grenade.php +++ b/server/src/Equipment/Grenade.php @@ -36,7 +36,7 @@ public function attackSecondary(Attackable $event): ?AttackResult public function getDamageValue(HitBoxType $hitBox, ArmorType $armor): int { - GameException::notImplementedYet('Should not be called'); + GameException::invalid('Should not be called'); } public function getKillAward(): int diff --git a/server/src/Event/ThrowEvent.php b/server/src/Event/ThrowEvent.php index 2c5d097..3f39de2 100644 --- a/server/src/Event/ThrowEvent.php +++ b/server/src/Event/ThrowEvent.php @@ -27,7 +27,6 @@ final class ThrowEvent extends Event implements Attackable, ForOneRoundMax private float $time = 0.0; private float $timeIncrement; private Point $position; - private Point $lastEventPosition; private Point $floorCandidate; private BallCollider $ball; private int $bounceCount = 0; @@ -52,7 +51,6 @@ public function __construct( $this->id = Sequence::next(); $this->position = $origin->clone(); - $this->lastEventPosition = $origin->clone(); $this->floorCandidate = $origin->clone(); $this->ball = new BallCollider($this->world, $origin, $radius, $this->angleHorizontal, $this->angleVertical); $this->needsToLandOnFloor = !($this->item instanceof Flashbang || $this->item instanceof HighExplosive); @@ -70,7 +68,6 @@ private function makeEvent(Point $point, SoundType $type): Event ->addExtra('id', $this->id) ; $this->world->makeSound($event); - $this->lastEventPosition->setFrom($point); return $event; } diff --git a/server/src/Map/TestMap.php b/server/src/Map/TestMap.php index 3d44fc9..a85f71d 100644 --- a/server/src/Map/TestMap.php +++ b/server/src/Map/TestMap.php @@ -16,10 +16,10 @@ public function __construct() $this->setAttackersSpawnPositions([new Point(), new Point(999, 0, 999)]); $this->setDefendersSpawnPositions([ (new Point())->setZ(50), - new Point(9999, 0, 9999), - new Point(9999, 0, 9999), - new Point(9999, 0, 9999), - new Point(9999, 0, 9999), + new Point(9991, 0, 9991), + new Point(9992, 0, 9992), + new Point(9993, 0, 9993), + new Point(9994, 0, 9994), ]); $this->buyArea = new Box(new Point(), 99999, 999, 99999); diff --git a/server/src/Net/Server.php b/server/src/Net/Server.php index 64316d8..4600e0e 100644 --- a/server/src/Net/Server.php +++ b/server/src/Net/Server.php @@ -253,6 +253,11 @@ private function gameTick(int $tickId): ?GameOverEvent return $this->game->tick($tickId); } + public function getBlockedPlayersCount(): int + { + return count($this->blockList); + } + /** * @codeCoverageIgnore */ diff --git a/server/src/Traits/Player/AttackTrait.php b/server/src/Traits/Player/AttackTrait.php index 6b8c77e..e0b65df 100644 --- a/server/src/Traits/Player/AttackTrait.php +++ b/server/src/Traits/Player/AttackTrait.php @@ -30,7 +30,9 @@ public function attack(): ?AttackResult $item = $this->getEquippedItem(); if ($item instanceof Bomb) { - $this->world->tryPlantBomb($this); + if ($item->canAttack($this->world->getTickId())) { + $this->world->tryPlantBomb($this, $item); + } return null; } diff --git a/server/src/Traits/Player/CrouchTrait.php b/server/src/Traits/Player/CrouchTrait.php index 3293eaf..ac49997 100644 --- a/server/src/Traits/Player/CrouchTrait.php +++ b/server/src/Traits/Player/CrouchTrait.php @@ -72,7 +72,7 @@ public function isCrouching(): bool return ($this->getHeadHeight() !== Setting::playerHeadHeightStand()); } - private function canCrouch(): bool + public function canCrouch(): bool { return (!isset($this->events[$this->eventIdCrouch])); } diff --git a/server/src/Traits/Player/GravityTrait.php b/server/src/Traits/Player/GravityTrait.php index c7e6737..63f3bd4 100644 --- a/server/src/Traits/Player/GravityTrait.php +++ b/server/src/Traits/Player/GravityTrait.php @@ -66,7 +66,7 @@ private function checkFallDamage(Floor $floor): void $fallHeight = $this->fallHeight - $floorHeight; if ($fallHeight > 3 * Setting::playerObstacleOvercomeHeight()) { $soundEvent = new SoundEvent($this->getPositionClone()->setY($floorHeight), SoundType::PLAYER_GROUND_TOUCH); - $this->world->makeSound($soundEvent->setPlayer($this)->setSurface($floor)); + $this->world->makeSound($soundEvent->setPlayer($this)); } $threshold = Setting::playerFallDamageThreshold(); diff --git a/server/src/Traits/Player/MovementTrait.php b/server/src/Traits/Player/MovementTrait.php index 6888dba..85ebe4f 100644 --- a/server/src/Traits/Player/MovementTrait.php +++ b/server/src/Traits/Player/MovementTrait.php @@ -35,7 +35,7 @@ public function speedWalk(): void public function isMoving(): bool { - return ($this->moveX <> 0 || $this->moveZ <> 0); + return ($this->moveX !== 0 || $this->moveZ !== 0); } public function isWalking(): bool @@ -131,7 +131,7 @@ private function getMoveAngle(): float $moveZ = $this->moveZ; $angle = $this->sight->getRotationHorizontal(); - if ($moveX <> 0 && $moveZ <> 0) { // diagonal move + if ($moveX !== 0 && $moveZ !== 0) { // diagonal move if ($moveZ === 1) { $angle += $moveX * 45; } else { @@ -186,7 +186,7 @@ private function getMoveSpeed(): int private function processMovement(int $moveX, int $moveZ, Point $current): Point { // If single direction move in opposite direction than previous (counter strafing) we stop - if (!($moveX <> 0 && $moveZ <> 0) && (($moveX !== 0 && $this->lastMoveX === -$moveX) || ($moveZ !== 0 && $this->lastMoveZ === -$moveZ))) { + if (!($moveX !== 0 && $moveZ !== 0) && (($moveX !== 0 && $this->lastMoveX === -$moveX) || ($moveZ !== 0 && $this->lastMoveZ === -$moveZ))) { $this->velocityPermil = 0; return $current; } diff --git a/test/og/BaseTestCase.php b/test/og/BaseTestCase.php index 5f77b27..bc528f3 100644 --- a/test/og/BaseTestCase.php +++ b/test/og/BaseTestCase.php @@ -17,7 +17,8 @@ abstract class BaseTestCase extends BaseTest { - private int $testTickRateMs = 10; + protected const TEST_TICK_RATE = 10; + private int $testTickRateMs = self::TEST_TICK_RATE; /** @var array */ private array $defaultTestAction = [ 'moveOneMs' => 5, diff --git a/test/og/Game/RoundTest.php b/test/og/Game/RoundTest.php index 42a4f93..96ea8ae 100644 --- a/test/og/Game/RoundTest.php +++ b/test/og/Game/RoundTest.php @@ -7,6 +7,7 @@ use cs\Core\Player; use cs\Core\Point; use cs\Core\Util; +use cs\Core\Wall; use cs\Enum\BuyMenuItem; use cs\Enum\Color; use cs\Enum\InventorySlot; @@ -16,6 +17,7 @@ use cs\Event\KillEvent; use cs\Event\PauseEndEvent; use cs\Event\PauseStartEvent; +use cs\Event\PlantEvent; use cs\Event\RoundEndCoolDownEvent; use cs\Event\RoundEndEvent; use cs\Event\RoundStartEvent; @@ -78,10 +80,10 @@ public function testRoundEndWhenNoPlayersAreAlive(): void $killEvent = $killEvents[0]; $this->assertInstanceOf(KillEvent::class, $killEvent); $this->assertSame([ - 'playerDead' => $killEvent->getPlayerDead()->getId(), + 'playerDead' => $killEvent->getPlayerDead()->getId(), 'playerCulprit' => $killEvent->getPlayerCulprit()->getId(), - 'itemId' => $killEvent->getAttackItemId(), - 'headshot' => $killEvent->wasHeadShot(), + 'itemId' => $killEvent->getAttackItemId(), + 'headshot' => $killEvent->wasHeadShot(), ], $killEvent->serialize()); $this->assertFalse($killEvent->wasHeadShot()); $this->assertSame(1, $killEvent->getPlayerDead()->getId()); @@ -166,7 +168,7 @@ public function testRoundEndEventFiredOncePerRoundEndActually(): void { $maxRounds = 5; $game = $this->createGame([ - GameProperty::MAX_ROUNDS => $maxRounds, + GameProperty::MAX_ROUNDS => $maxRounds, GameProperty::ROUND_TIME_MS => 1, ]); @@ -196,10 +198,10 @@ public function testHalfTimeSwitch(): void { $maxRounds = 5; $game = $this->createGame([ - GameProperty::MAX_ROUNDS => $maxRounds, - GameProperty::ROUND_TIME_MS => 1, + GameProperty::MAX_ROUNDS => $maxRounds, + GameProperty::ROUND_TIME_MS => 1, GameProperty::HALF_TIME_FREEZE_SEC => 0, - GameProperty::START_MONEY => 3000, + GameProperty::START_MONEY => 3000, ]); $playerAttackerSpawnPosition = $game->getPlayer(1)->getPositionClone(); $game->setTickMax($maxRounds * 2); @@ -220,6 +222,207 @@ public function testHalfTimeSwitch(): void $this->assertSame(9500, $game->getPlayer(1)->getMoney()); $this->assertPositionNotSame($playerAttackerSpawnPosition, $game->getPlayer(1)->getPositionClone()); $this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_BOMB->value)); + $this->assertSame(9500, $game->getPlayer(1)->getMoney()); + $this->assertSame([2, 0], $game->getScore()->toArray()['firstHalfScore']); + $this->assertSame([3, 0], $game->getScore()->toArray()['secondHalfScore']); + $this->assertSame(2, $game->getScore()->toArray()['halfTimeRoundNumber']); + } + + public function testRoundEndCoolDown(): void + { + $gameProperty = $this->createNoPauseGameProperty(); + $gameProperty->round_end_cool_down_sec = 1; + $gameProperty->max_rounds = 6; + $game = $this->createTestGame(null, $gameProperty); + $pos = new Point(501, 0, 502); + $enemy = new Player(2, Color::BLUE, false); + $game->addPlayer($enemy); + $enemy->setPosition($pos->clone()->addX(-300)); + + $this->playPlayer($game, [ + fn() => $enemy->getSight()->look(-90, 0), + fn() => $enemy->equipSecondaryWeapon(), + fn() => $this->assertSame(1, $game->getRoundNumber()), + fn(Player $p) => $p->setPosition($pos), + fn(Player $p) => $p->suicide(), + fn() => $this->assertSame(2, $game->getRoundNumber()), + fn(Player $p) => $this->assertFalse($p->isAlive()), + $this->waitNTicks(500), + fn(Player $p) => $this->assertFalse($p->isAlive()), + fn(Player $p) => $this->assertPositionSame($pos, $p->getPositionClone()), + function () use ($enemy) { + $result = $this->assertPlayerNotHit($enemy->attack()); + $hits = $result->getHits(); + $this->assertCount(1, $hits); + $wall = $hits[0]; + $this->assertInstanceOf(Wall::class, $wall); + $this->assertPositionSame($enemy->getSightPositionClone()->setX(-1), $result->getBullet()->getPosition()); + }, + $this->waitNTicks(500), + $this->endGame(), + ]); + + $this->assertSame(2, $game->getRoundNumber()); + $this->assertTrue($game->getPlayer(1)->isAlive()); + $this->assertTrue($game->getPlayer(1)->isPlayingOnAttackerSide()); + } + + public function testMultipleRoundsScoreAndEvents(): void + { + $maxRounds = 4; + $gameProperty = $this->createNoPauseGameProperty($maxRounds); + $gameProperty->bomb_plant_time_ms = 0; + $gameProperty->bomb_defuse_time_ms = 0; + $gameProperty->bomb_explode_time_ms = 200; + $gameProperty->round_time_ms = 500; + $game = $this->createTestGame(null, $gameProperty); + $p1 = $game->getPlayer(1); + $p2 = new Player(2, Color::BLUE, false); + $game->addPlayer($p2); + $start = new Point(500, 0, 500); + $p2->setPosition($start); + $p1->setPosition($p2->getPositionClone()); + $p1->getSight()->look(0, -90); + $p2->getSight()->look(0, -90); + + $eventCounts = []; + $eventObjects = []; + $game->onEvents(function (array $events) use (&$eventCounts, &$eventObjects): void { + foreach ($events as $event) { + if (!isset($eventCounts[$event::class])) { + $eventCounts[$event::class] = 0; + } + $eventCounts[$event::class]++; + $eventObjects[$event::class] = $event; + } + }); + + $this->playPlayer($game, [ + fn() => $this->assertTrue($p1->equip(InventorySlot::SLOT_BOMB)), + $this->waitNTicks(Bomb::equipReadyTimeMs), + fn() => $p1->attack(), + fn() => $this->assertPositionSame($start, $p2->getPositionClone()), + function () use ($p2) { + $p2->moveForward(); + $p2->use(); + $this->assertFalse($p2->isMoving()); + }, + fn() => $this->assertPositionSame($start, $p2->getPositionClone()), + fn() => $this->assertSame(2, $game->getRoundNumber()), + fn() => $this->assertTrue($p1->equip(InventorySlot::SLOT_BOMB)), + $this->waitNTicks(Bomb::equipReadyTimeMs), + fn() => $p1->attack(), + $this->waitNTicks(220), + fn() => $this->assertSame(3, $game->getRoundNumber()), + fn() => $p1->setPosition(new Point(500, 0, 500)), + fn() => $p2->setPosition(new Point(500, 0, 500)), + fn() => $this->assertTrue($p1->buyItem(BuyMenuItem::GRENADE_FLASH)), + fn() => $p1->buyItem(BuyMenuItem::DEFUSE_KIT), + fn() => $this->assertTrue($p1->getInventory()->has(InventorySlot::SLOT_KIT->value)), + fn() => $p2->buyItem(BuyMenuItem::DEFUSE_KIT), + fn() => $this->assertFalse($p2->getInventory()->has(InventorySlot::SLOT_KIT->value)), + fn() => $this->assertTrue($p2->buyItem(BuyMenuItem::GRENADE_FLASH)), + fn() => $this->assertTrue($p2->buyItem(BuyMenuItem::GRENADE_FLASH)), + fn() => $this->assertTrue($p2->buyItem(BuyMenuItem::GRENADE_DECOY)), + fn() => $p1->suicide(), + fn() => $this->assertSame(4, $game->getRoundNumber()), + function () use ($p1, $p2) { + $this->assertFalse($p1->hasDefuseKit()); + $this->assertFalse($p2->hasDefuseKit()); + $this->assertFalse($p1->getInventory()->has(InventorySlot::SLOT_BOMB->value)); + $this->assertTrue($p2->getInventory()->has(InventorySlot::SLOT_BOMB->value)); + $this->assertTrue($p2->getInventory()->has(InventorySlot::SLOT_GRENADE_FLASH->value)); + $this->assertTrue($p2->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); + $this->assertFalse($p1->getInventory()->has(InventorySlot::SLOT_GRENADE_FLASH->value)); + }, + $this->waitNTicks(800), + ]); + + $this->assertNotEmpty($eventCounts); + $this->assertSame($maxRounds + 1, $game->getRoundNumber()); + $this->assertSame($maxRounds + 1, $eventCounts[PauseStartEvent::class]); + $this->assertSame($maxRounds, $eventCounts[PauseEndEvent::class]); + $this->assertSame($maxRounds, $eventCounts[RoundStartEvent::class]); + $this->assertSame($maxRounds, $eventCounts[RoundEndEvent::class]); + $this->assertSame(2, $eventCounts[PlantEvent::class]); + $this->assertSame($maxRounds - 2, $eventCounts[RoundEndCoolDownEvent::class]); + $this->assertTrue($game->getScore()->isTie()); + + $expectedScoreBoard = [ + 'score' => [2, 2], + 'lossBonus' => [1400, 1900], + 'history' => [ + 1 => [ + 'attackersWins' => false, + 'reason' => 2, + 'scoreAttackers' => 0, + 'scoreDefenders' => 1, + ], + 2 => [ + 'attackersWins' => true, + 'reason' => 3, + 'scoreAttackers' => 1, + 'scoreDefenders' => 1, + ], + 3 => [ + 'attackersWins' => true, + 'reason' => 0, + 'scoreAttackers' => 2, + 'scoreDefenders' => 1, + ], + 4 => [ + 'attackersWins' => false, + 'reason' => 1, + 'scoreAttackers' => 2, + 'scoreDefenders' => 2, + ], + ], + 'firstHalfScore' => [1, 1], + 'secondHalfScore' => [1, 1], + 'halfTimeRoundNumber' => 2, + 'scoreboard' => [ + [ + [ + 'id' => 1, + 'kills' => -1, + 'deaths' => 1, + 'damage' => 0, + ], + ], + [ + [ + 'id' => 2, + 'kills' => 0, + 'deaths' => 0, + 'damage' => 0, + ], + ], + ], + ]; + $this->assertSame($expectedScoreBoard, $game->getScore()->toArray()); + } + + public function testBombExplodeMoney(): void + { + $maxRounds = 4; + $gameProperty = $this->createNoPauseGameProperty($maxRounds); + $gameProperty->bomb_plant_time_ms = 0; + $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); + $game = $this->createTestGame(null, $gameProperty); + + $this->playPlayer($game, [ + fn(Player $p) => $p->equip(InventorySlot::SLOT_BOMB), + $this->waitNTicks(Bomb::equipReadyTimeMs), + fn(Player $p) => $p->setPosition(new Point(500, 0, 500)), + fn(Player $p) => $p->attack(), + $this->waitNTicks(3000), + ]); + + $this->assertSame($maxRounds + 1, $game->getRoundNumber()); + $this->assertSame(4050, $game->getPlayer(1)->getMoney()); } } diff --git a/test/og/Inventory/SimpleInventoryTest.php b/test/og/Inventory/InventoryTest.php similarity index 95% rename from test/og/Inventory/SimpleInventoryTest.php rename to test/og/Inventory/InventoryTest.php index d469898..de4d6cd 100644 --- a/test/og/Inventory/SimpleInventoryTest.php +++ b/test/og/Inventory/InventoryTest.php @@ -8,6 +8,7 @@ use cs\Core\Player; use cs\Core\Point; use cs\Core\Wall; +use cs\Core\World; use cs\Enum\ArmorType; use cs\Enum\BuyMenuItem; use cs\Enum\Color; @@ -23,9 +24,10 @@ use cs\Weapon\RifleAk; use cs\Weapon\RifleAWP; use cs\Weapon\RifleM4A4; +use ReflectionProperty; use Test\BaseTestCase; -class SimpleInventoryTest extends BaseTestCase +class InventoryTest extends BaseTestCase { public function testPlayerInventory(): void { @@ -257,6 +259,9 @@ public function testDropAndInstantPickupItem(): void fn(Player $p) => $this->assertInstanceOf(PistolGlock::class, $p->getEquippedItem()), $this->endGame(), ]); + + $reflection = new ReflectionProperty(World::class, 'dropItems'); + $this->assertSame([], $reflection->getValue($game->getWorld())); } public function testDropToOtherPlayer(): void @@ -441,19 +446,27 @@ function (Player $p) { $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_PRIMARY->value)); }, fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::RIFLE_AK)), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)), fn(Player $p) => $p->equipSecondaryWeapon(), function (Player $p) { $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_KNIFE->value)); $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)); $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)); + $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); + $grenadeSlots = $p->getInventory()->getLastEquippedGrenadeSlots(); + $this->assertSame(InventorySlot::SLOT_GRENADE_DECOY->value, array_shift($grenadeSlots)); $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_KIT->value)); $this->assertFalse($p->dropItemFromSlot(InventorySlot::SLOT_KNIFE->value)); $this->assertTrue($p->dropItemFromSlot(InventorySlot::SLOT_PRIMARY->value)); + $this->assertTrue($p->dropItemFromSlot(InventorySlot::SLOT_GRENADE_DECOY->value)); $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_KNIFE->value)); $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_SECONDARY->value)); $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)); + $this->assertFalse($p->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); + $grenadeSlots = $p->getInventory()->getLastEquippedGrenadeSlots(); + $this->assertSame(InventorySlot::SLOT_GRENADE_SMOKE->value, array_shift($grenadeSlots)); }, $this->waitNTicks(1000), fn(Player $p) => $this->assertTrue($p->getInventory()->has(InventorySlot::SLOT_PRIMARY->value)), @@ -544,6 +557,16 @@ public function testPlayerBuyMaxFourGrenades(): void public function testCancelReload(): void { $game = $this->createNoPauseGame(); + + $reloadEventCount = 0; + $game->onEvents(function (array $events) use (&$reloadEventCount): void { + foreach ($events as $event) { + if ($event instanceof SoundEvent && $event->type === SoundType::ITEM_RELOAD) { + $reloadEventCount++; + } + } + }); + $this->playPlayer($game, [ fn(Player $p) => $p->getInventory()->earnMoney(6123), fn(Player $p) => $p->getSight()->lookVertical(-90), @@ -574,6 +597,7 @@ function (Player $p): void { $ak = $game->getPlayer(1)->getEquippedItem(); $this->assertInstanceOf(RifleAk::class, $ak); $this->assertSame(RifleAk::magazineCapacity - 1, $ak->getAmmo()); + $this->assertSame(1, $reloadEventCount); } public function testKevlarBuy(): void diff --git a/test/og/Movement/MovementTest.php b/test/og/Movement/MovementTest.php index 70ea629..d7ce8de 100644 --- a/test/og/Movement/MovementTest.php +++ b/test/og/Movement/MovementTest.php @@ -194,7 +194,11 @@ public function testPlayerConsistentMovement(): void $this->playPlayer($game, [ fn(Player $p) => $p->getInventory()->earnMoney(9000), fn(Player $p) => $p->getSight()->lookHorizontal(45), - fn(Player $p) => $p->moveForward(), + function (Player $p) { + $p->moveForward(); + $this->assertFalse($p->isWalking()); + $this->assertTrue($p->isRunning()); + }, fn(Player $p) => $p->moveForward(), function (Player $p) use (&$end) { $end = $p->getPositionClone(); diff --git a/test/og/Movement/SimpleMovementTest.php b/test/og/Movement/SimpleMovementTest.php index 8a11a8a..9bc5690 100644 --- a/test/og/Movement/SimpleMovementTest.php +++ b/test/og/Movement/SimpleMovementTest.php @@ -66,6 +66,21 @@ public function testPlayerCrouch(): void $this->assertSame(Setting::playerHeadHeightCrouch(), $game->getPlayer(1)->getHeadHeight()); } + public function testPlayerCrouchStand(): void + { + $game = $this->createOneRoundGame(Setting::tickCountCrouch() * 3); + $game->onTick(function (GameState $state): void { + $state->getPlayer(1)->crouch(); + if ($state->getTickId() > Setting::tickCountCrouch()) { + $state->getPlayer(1)->stand(); + } + }); + + $game->start(); + $this->assertTrue($game->getPlayer(1)->canCrouch()); + $this->assertSame(Setting::playerHeadHeightStand(), $game->getPlayer(1)->getHeadHeight()); + } + public function testPlayerCrouchSpeed(): void { $game = $this->createTestGame(); diff --git a/test/og/Shooting/BacktrackTest.php b/test/og/Shooting/BacktrackShootingTest.php similarity index 99% rename from test/og/Shooting/BacktrackTest.php rename to test/og/Shooting/BacktrackShootingTest.php index 745f357..efcafd7 100644 --- a/test/og/Shooting/BacktrackTest.php +++ b/test/og/Shooting/BacktrackShootingTest.php @@ -16,7 +16,7 @@ use Test\BaseTestCase; use Test\TestGame; -class BacktrackTest extends BaseTestCase +class BacktrackShootingTest extends BaseTestCase { private function _setupGame1(int $backtrackTickCount): TestGame diff --git a/test/og/Shooting/BombTest.php b/test/og/Shooting/BombTest.php index e39345a..7a8d92e 100644 --- a/test/og/Shooting/BombTest.php +++ b/test/og/Shooting/BombTest.php @@ -33,8 +33,11 @@ public function testInvalidBombPlantCases(): void $game->getPlayer(1)->setPosition(new Point(500, 0, 500)); $game->addPlayer(new Player(2, Color::BLUE, true)); $this->playPlayer($game, [ + fn() => $this->assertTrue($game->isPaused()), fn(Player $p) => $p->equip(InventorySlot::SLOT_BOMB), fn(Player $p) => $p->attack(), + $this->waitNTicks(Bomb::equipReadyTimeMs), + fn(Player $p) => $p->attack(), $this->waitNTicks(1000), fn(Player $p) => $p->jump(), fn(Player $p) => $this->assertTrue($p->isFlying()), @@ -94,24 +97,24 @@ public function testBombPlant(): void $this->assertLessThan(Util::millisecondsToFrames($properties->round_time_ms), $game->getTickId()); $this->assertInstanceOf(RoundEndEvent::class, $roundEndEvent); $this->assertSame([ - 'roundNumber' => $roundEndEvent->roundNumberEnded, + 'roundNumber' => $roundEndEvent->roundNumberEnded, 'newRoundNumber' => $roundEndEvent->roundNumberEnded + 1, - 'attackersWins' => $roundEndEvent->attackersWins, - 'score' => $game->getScore()->toArray(), + 'attackersWins' => $roundEndEvent->attackersWins, + 'score' => $game->getScore()->toArray(), ], $roundEndEvent->serialize()); $this->assertInstanceOf(KillEvent::class, $killEvent); $this->assertSame($game->getPlayer(1), $killEvent->getPlayerDead()); $this->assertSame($game->getPlayer(1), $killEvent->getPlayerCulprit()); $this->assertInstanceOf(PlantEvent::class, $plantEvent); $this->assertSame([ - 'timeMs' => 1000, + 'timeMs' => 1000, 'position' => (new Point())->toArray(), ], $plantEvent->serialize()); $this->assertSame(1, $plantCount); $this->assertSame(RoundEndReason::BOMB_EXPLODED, $roundEndEvent->reason); $this->assertSame( - Util::millisecondsToFrames($properties->bomb_plant_time_ms) + Util::millisecondsToFrames($properties->bomb_explode_time_ms), - $game->getTickId() - 3 + Util::millisecondsToFrames(Bomb::equipReadyTimeMs) + Util::millisecondsToFrames($properties->bomb_plant_time_ms) + Util::millisecondsToFrames($properties->bomb_explode_time_ms), + $game->getTickId() - 4 ); } @@ -154,6 +157,8 @@ protected function _testBombPlantRound(bool $shouldDefuse): void if ($shouldDefuse) { $this->assertTrue($game->getScore()->defendersIsWinning()); $this->assertFalse($game->getScore()->attackersIsWinning()); + $this->assertSame(700, $defender->getMoney()); + $this->assertFalse($game->isBombActive()); } else { $this->assertTrue($game->getScore()->attackersIsWinning()); $this->assertFalse($game->getScore()->defendersIsWinning()); @@ -167,5 +172,56 @@ public function testBombPlantDefuse(): void $this->_testBombPlantRound(true); } + public function testBombPlantReset(): void + { + $gameProperty = $this->createNoPauseGameProperty(); + $gameProperty->bomb_plant_time_ms = self::TEST_TICK_RATE * 2; + $gameProperty->bomb_explode_time_ms = 100; + $game = $this->createTestGame(null, $gameProperty); + $start = new Point(321, 0, 300); + + $this->playPlayer($game, [ + fn(Player $p) => $p->setPosition($start), + fn(Player $p) => $p->equip(InventorySlot::SLOT_BOMB), + $this->waitNTicks(Bomb::equipReadyTimeMs), + function (Player $p) { + $p->moveForward(); + $p->attack(); + $this->assertFalse($p->isMoving()); + }, + fn(Player $p) => $this->assertPositionSame($start, $p->getPositionClone()), + fn(Player $p) => $this->assertFalse($game->isBombActive()), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + $this->waitNTicks(500), + fn(Player $p) => $this->assertFalse($game->isBombActive()), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + function (Player $p) use ($game) { + $this->assertFalse($game->isBombActive()); + $p->attack(); + $this->assertFalse($game->isBombActive()); + }, + function (Player $p) use ($game) { + $this->assertFalse($game->isBombActive()); + $p->attack(); + $this->assertFalse($game->isBombActive()); + }, + fn(Player $p) => $p->attack(), + fn(Player $p) => $this->assertTrue($game->isBombActive()), + fn(Player $p) => $this->assertSame(1, $game->getRoundNumber()), + $this->waitNTicks(300), + ]); + + $this->assertSame(2, $game->getRoundNumber()); + $this->assertSame(1100, $game->getPlayer(1)->getMoney()); + $score = $game->getScore()->toArray(); + $history = $score['history'] ?? false; + $this->assertIsArray($history); + $this->assertSame(RoundEndReason::BOMB_EXPLODED->value, $history[1]['reason'] ?? false); + $this->assertSame([0, 1], $score['score'] ?? false); + $this->assertSame([0, 1], $score['firstHalfScore'] ?? false); + $this->assertSame([], $score['secondHalfScore'] ?? false); + $this->assertSame(1100, $game->getPlayer(1)->getMoney()); + } + } diff --git a/test/og/Shooting/KnifeAttackTest.php b/test/og/Shooting/KnifeAttackTest.php index 6703b17..d13a69c 100644 --- a/test/og/Shooting/KnifeAttackTest.php +++ b/test/og/Shooting/KnifeAttackTest.php @@ -5,7 +5,10 @@ use cs\Core\HitBox; use cs\Core\Player; use cs\Enum\Color; +use cs\Enum\HitBoxType; +use cs\Enum\SoundType; use cs\Event\AttackResult; +use cs\Event\SoundEvent; use cs\Weapon\Knife; use Test\BaseTestCase; @@ -46,6 +49,16 @@ public function testBackStab(): void $p2 = new Player(2, Color::GREEN, false); $game->addPlayer($p2); $p2->setPosition($game->getPlayer(1)->getPositionClone()->addZ($p2->getBoundingRadius() * 3)); + + $itemAttack2EventCounts = 0; + $game->onEvents(function (array $events) use (&$itemAttack2EventCounts): void { + foreach ($events as $event) { + if ($event instanceof SoundEvent && $event->type === SoundType::ITEM_ATTACK2) { + $itemAttack2EventCounts++; + } + } + }); + $this->playPlayer($game, [ fn(Player $p) => $p->reload(), $this->waitNTicks(Knife::equipReadyTimeMs), @@ -70,10 +83,12 @@ function () use ($game) { $this->assertCount(1, $hits); $hitBox = $hits[0]; $this->assertInstanceOf(HitBox::class, $hitBox); - //$this->assertSame(HitBoxType::BACK, $hitBox->getType()); once there is HitBoxBack geometry + $this->assertSame(HitBoxType::BACK, $hitBox->getType()); }, $this->endGame(), ]); + + $this->assertSame(1, $itemAttack2EventCounts); } } diff --git a/test/og/Shooting/MolotovGrenadeTest.php b/test/og/Shooting/MolotovGrenadeTest.php index f4c824a..c6a6a8f 100644 --- a/test/og/Shooting/MolotovGrenadeTest.php +++ b/test/og/Shooting/MolotovGrenadeTest.php @@ -293,6 +293,19 @@ public function testFlameDoNotLikeSmoke(): void $game->getWorld()->addBox(new Box(new Point(), 1000, 2000, 1000)); $health = 100; + $flameSpawnEventCount = 0; + $flameExtinguishEventCount = 0; + $game->onEvents(function (array $events) use (&$flameSpawnEventCount, &$flameExtinguishEventCount): void { + foreach ($events as $event) { + if ($event instanceof SoundEvent && $event->type === SoundType::FLAME_SPAWN) { + $flameSpawnEventCount++; + } + if ($event instanceof SoundEvent && $event->type === SoundType::FLAME_EXTINGUISH) { + $flameExtinguishEventCount++; + } + } + }); + $this->playPlayer($game, [ fn(Player $p) => $p->setPosition(new Point(500, 0, 500)), fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_MOLOTOV)), @@ -316,6 +329,8 @@ function (Player $p) use (&$health) { $this->assertSame(1, $game->getRoundNumber()); $this->assertSame($health, $game->getPlayer(1)->getHealth()); $this->assertGreaterThan(72, $game->getPlayer(1)->getHealth()); + $this->assertGreaterThan(10, $flameSpawnEventCount); + $this->assertSame($flameSpawnEventCount, $flameExtinguishEventCount); } public function testSmokeExtinguishFlames(): void @@ -387,6 +402,19 @@ public function testSmokeHeightBoundaryAndShrink(): void $p3->buyItem(BuyMenuItem::GRENADE_INCENDIARY); $game->addPlayer(new Player(4, Color::BLUE, false)); + $smokeSpawnCount = 0; + $smokeFadeCount = 0; + $game->onEvents(function (array $events) use (&$smokeSpawnCount, &$smokeFadeCount): void { + foreach ($events as $event) { + if ($event instanceof SoundEvent && $event->type === SoundType::SMOKE_SPAWN) { + $smokeSpawnCount++; + } + if ($event instanceof SoundEvent && $event->type === SoundType::SMOKE_FADE) { + $smokeFadeCount++; + } + } + }); + $this->playPlayer($game, [ fn(Player $p) => $p->getSight()->look(0, -90), fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_SMOKE)), @@ -419,6 +447,8 @@ function (Player $p) { $this->assertFalse($p3->isAlive()); $this->assertTrue($game->getPlayer(1)->isAlive()); $this->assertTrue($game->getPlayer(4)->isAlive()); + $this->assertGreaterThan(10, $smokeSpawnCount); + $this->assertSame(1, $smokeFadeCount, 'Smoke fade - single shrink event'); } } diff --git a/test/og/Shooting/SimpleShootTest.php b/test/og/Shooting/ShootTest.php similarity index 94% rename from test/og/Shooting/SimpleShootTest.php rename to test/og/Shooting/ShootTest.php index 0585eb4..082eb13 100644 --- a/test/og/Shooting/SimpleShootTest.php +++ b/test/og/Shooting/ShootTest.php @@ -19,7 +19,7 @@ use cs\Weapon\RifleAk; use Test\BaseTestCase; -class SimpleShootTest extends BaseTestCase +class ShootTest extends BaseTestCase { public function testOneTapAmmoMagazine(): void @@ -263,11 +263,20 @@ public function testTeamDamageIsLowerThanOpponent(): void $this->waitNTicks(PistolGlock::fireRateMs), fn(Player $p) => $p->getSight()->look(180, -10), fn(Player $p) => $this->assertPlayerHit($p->attack()), + function () use ($game) { + $this->assertLessThan(100, $game->getPlayer(3)->getHealth()); + $this->assertLessThan($game->getPlayer(2)->getHealth(), $game->getPlayer(3)->getHealth()); + }, + fn(Player $p) => $p->getSight()->look(180, 0), + $this->waitNTicks(PistolGlock::fireRateMs), + fn(Player $p) => $this->assertPlayerHit($p->attack()), + $this->waitNTicks(PistolGlock::fireRateMs), + fn(Player $p) => $this->assertPlayerHit($p->attack()), $this->endGame(), ]); - $this->assertLessThan(100, $game->getPlayer(3)->getHealth()); - $this->assertLessThan($game->getPlayer(2)->getHealth(), $game->getPlayer(3)->getHealth()); + $this->assertCount(2, $game->getAlivePlayers()); + $this->assertSame(-1, $game->getScore()->getPlayerStat(1)->getKills()); } public function testDamageLowOnRangeMaxDamage(): void diff --git a/test/og/Unit/BacktrackTest.php b/test/og/Unit/BacktrackTest.php new file mode 100644 index 0000000..c457dee --- /dev/null +++ b/test/og/Unit/BacktrackTest.php @@ -0,0 +1,133 @@ +loadMap(new TestMap()); + $game->addPlayer($player); + + return $game; + } + + public function test1(): void + { + $game = $this->createGame(); + $player = $game->getPlayer(1); + $player->getSight()->look(123, -9); + $origPosition = $player->getPositionClone(); + $origPlayerHeadHeight = $player->getHeadHeight(); + $modifiedHeadHeight = $origPlayerHeadHeight - 3; + + $backtrack = new Backtrack($game, 123); + $this->assertCount(1, $backtrack->getStates()); + $this->assertSame([0], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertCount(1, $backtrack->getStates()); + $this->assertSame([0], $backtrack->getStates()); + + $player->setPosition($origPosition->clone()->addX(10)); + $player->setHeadHeight($modifiedHeadHeight); + $player->getSight()->look(12, 13); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + + $this->assertCount(1, $backtrack->getStates()); + $this->assertSame([1], $backtrack->getStates()); + + $this->assertSame($modifiedHeadHeight, $player->getHeadHeight()); + $this->assertSame(12.0, $player->getSight()->getRotationHorizontal()); + $this->assertPositionSame($origPosition->clone()->addX(10), $player->getPositionClone()); + + $backtrack->saveState(); + $states = $backtrack->getStates(); + $this->assertCount(1, $states); + $backtrack->apply($states[0], $player->getId()); + $this->assertSame($origPlayerHeadHeight, $player->getHeadHeight()); + $this->assertSame(123.0, $player->getSight()->getRotationHorizontal()); + $this->assertSame(-9.0, $player->getSight()->getRotationVertical()); + $this->assertPositionSame($origPosition, $player->getPositionClone()); + $backtrack->restoreState(); + + $this->assertSame($modifiedHeadHeight, $player->getHeadHeight()); + $this->assertSame(12.0, $player->getSight()->getRotationHorizontal()); + $this->assertSame(13.0, $player->getSight()->getRotationVertical()); + $this->assertPositionSame($origPosition->clone()->addX(10), $player->getPositionClone()); + } + + public function testEmptyStateIsSkippedInGetStates(): void + { + $game = $this->createGame(); + $player = $game->getPlayer(1); + $backtrack = new Backtrack($game, 5); + $this->assertSame([0], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertSame([0], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([1], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertSame([2], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([1, 3], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertSame([2, 4], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertSame([1, 3, 5], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([1, 2, 4], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([2, 3, 5], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([3, 4], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->addStateData($player); + $backtrack->finishState(); + $this->assertSame([4, 5], $backtrack->getStates()); + + $backtrack->startState(); + $backtrack->finishState(); + $this->assertSame([1, 5], $backtrack->getStates()); + + $backtrack->apply(2, $player->getId()); + $backtrack->apply(1, -1); + } +} diff --git a/test/og/Unit/InventoryTest.php b/test/og/Unit/InventoryTest.php new file mode 100644 index 0000000..568fa77 --- /dev/null +++ b/test/og/Unit/InventoryTest.php @@ -0,0 +1,39 @@ +assertSame(InventorySlot::getGrenadeSlotIds(), $inventory->getLastEquippedGrenadeSlots()); + + $lastGrenadeEquippedSlots = $inventory->getLastEquippedGrenadeSlots(); + $this->assertSame(InventorySlot::SLOT_GRENADE_SMOKE->value, array_shift($lastGrenadeEquippedSlots)); + + $this->assertTrue($inventory->pickup(new Incendiary())); + $this->assertNotNull($inventory->equip(InventorySlot::SLOT_GRENADE_MOLOTOV)); + $lastGrenadeEquippedSlots = $inventory->getLastEquippedGrenadeSlots(); + $this->assertNotSame(InventorySlot::getGrenadeSlotIds(), $lastGrenadeEquippedSlots); + $expectedSlots = [InventorySlot::SLOT_GRENADE_MOLOTOV->value]; + foreach (InventorySlot::getGrenadeSlotIds() as $slotId) { + if ($slotId === InventorySlot::SLOT_GRENADE_MOLOTOV->value) { + continue; + } + $expectedSlots[] = $slotId; + } + $this->assertSame($expectedSlots, $lastGrenadeEquippedSlots); + + $inventory->removeSlot(InventorySlot::SLOT_GRENADE_MOLOTOV->value); + $lastGrenadeEquippedSlots = $inventory->getLastEquippedGrenadeSlots(); + $this->assertSame(InventorySlot::SLOT_GRENADE_SMOKE->value, array_shift($lastGrenadeEquippedSlots)); + } + +} diff --git a/test/og/Unit/ProtocolTest.php b/test/og/Unit/ProtocolTest.php index 4c58192..e0675e1 100644 --- a/test/og/Unit/ProtocolTest.php +++ b/test/og/Unit/ProtocolTest.php @@ -7,11 +7,12 @@ use cs\Core\GameState; use cs\Core\Player; use cs\Enum\Color; +use cs\Enum\GameOverReason; +use cs\Event\GameOverEvent; use cs\Map\TestMap; use cs\Net\PlayerControl; use cs\Net\Protocol; -use cs\Net\ProtocolReader; -use cs\Net\ProtocolWriter; +use cs\Net\ServerSetting; use Test\BaseTest; class ProtocolTest extends BaseTest @@ -110,14 +111,64 @@ public function testSerialization(): void ]; $this->assertSame($playerSerializedExpected, $player->serialize()); + $event = new GameOverEvent(GameOverReason::REASON_NOT_ALL_PLAYERS_CONNECTED); $expected = [ 'players' => [ $playerSerializedExpected, ], - 'events' => [], + 'events' => [ + [ + 'code' => $event->getCode(), + 'data' => [ + 'reason' => $event->reason->value, + ], + ], + ], ]; - $actual = $protocol->serialize($game->getPlayers(), []); + $actual = $protocol->serialize($game->getPlayers(), [$event]); $this->assertSame($expected, json_decode($actual, true)); + + $playerJsonSerialized = json_encode($playerSerializedExpected); + $expectedGameSettingsSerialized = <<serializeGameSetting($player, new ServerSetting(9, 20), $game); + $this->assertJsonStringEqualsJsonString($expectedGameSettingsSerialized, $actualGameSettingsSerialized); } } diff --git a/test/og/Unit/ScoreTest.php b/test/og/Unit/ScoreTest.php new file mode 100644 index 0000000..69af189 --- /dev/null +++ b/test/og/Unit/ScoreTest.php @@ -0,0 +1,66 @@ +addPlayer(new Player(1, Color::BLUE, false)); + $score->addPlayer(new Player(2, Color::ORANGE, false)); + $score->addPlayer(new Player(3, Color::GREEN, true)); + $score->addPlayer(new Player(4, Color::PURPLE, true)); + + $score->getPlayerStat(1)->addDeath(); + $score->getPlayerStat(2)->addDamage(987); + $score->getPlayerStat(2)->addKill(false); + $score->getPlayerStat(3)->addDeath(); + $score->getPlayerStat(4)->addDamage(21); + $score->getPlayerStat(4)->addKill(true); + $scoreBoard = $score->toArray(); + $this->assertIsArray($scoreBoard['score'] ?? false); + $this->assertIsArray($scoreBoard['lossBonus'] ?? false); + $this->assertIsArray($scoreBoard['history'] ?? false); + $this->assertIsArray($scoreBoard['firstHalfScore'] ?? false); + $this->assertIsArray($scoreBoard['secondHalfScore'] ?? false); + $this->assertIsArray($scoreBoard['scoreboard'] ?? false); + $this->assertSame([ + [ + [ + 'id' => 2, + 'kills' => 1, + 'deaths' => 0, + 'damage' => 100, + ], + [ + 'id' => 1, + 'kills' => 0, + 'deaths' => 1, + 'damage' => 0, + ], + ], + [ + [ + 'id' => 4, + 'kills' => 1, + 'deaths' => 0, + 'damage' => 21, + ], + [ + 'id' => 3, + 'kills' => 0, + 'deaths' => 1, + 'damage' => 0, + ], + ], + ], $scoreBoard['scoreboard']); + } + +} diff --git a/test/og/Unit/ServerTest.php b/test/og/Unit/ServerTest.php index 84192ec..99e65f9 100644 --- a/test/og/Unit/ServerTest.php +++ b/test/og/Unit/ServerTest.php @@ -12,7 +12,9 @@ use cs\Enum\GameOverReason; use cs\Enum\InventorySlot; use cs\Equipment\Molotov; +use cs\Event\EventList; use cs\Event\GameOverEvent; +use cs\Event\GameStartEvent; use cs\Map\TestMap; use cs\Net\Server; use cs\Net\ServerSetting; @@ -42,16 +44,50 @@ private function runTestServer(Game $game, array $clientRequests): array return $testNet->getResponses(); } - public function testServerNoPlayersConnected(): void + public function testServerInvalidLoginCodeBlocked(): void { $game = GameFactory::createDebug(); $setting = new ServerSetting(1, 0, 'a', 'd', false, 0); - $testNet = new TestConnector(['']); + $testNet = new TestConnector(['login some-invalid-code']); $server = new Server($game, $setting, $testNet); + $this->assertSame(0, $server->getBlockedPlayersCount()); $server->start(); $gameOver = $game->tick($game->getTickId() + 1); $this->assertInstanceOf(GameOverEvent::class, $gameOver); $this->assertSame(GameOverReason::REASON_NOT_ALL_PLAYERS_CONNECTED, $gameOver->reason); + $this->assertSame(1, $server->getBlockedPlayersCount()); + $this->assertCount(0, $testNet->getResponses()); + } + + public function testServerNotAllPlayersConnected(): void + { + $game = GameFactory::createDebug(); + $game->loadMap(new TestMap()); + $setting = new ServerSetting(2, 0, 'code', 'd', true, 0); + $testNet = new TestConnector(['login code']); + $server = new Server($game, $setting, $testNet); + $server->start(); + + $this->assertSame(0, $server->getBlockedPlayersCount()); + $serverResponses = $testNet->getResponses(); + $this->assertCount(2, $serverResponses); + + $gameStartMsg = $serverResponses[0]; + $gameStartEvent = json_decode($gameStartMsg, true); + $this->assertIsArray($gameStartEvent); + $this->assertCount(0, $gameStartEvent['players'] ?? false); + $this->assertCount(1, $gameStartEvent['events']); + $this->assertSame(EventList::map[GameStartEvent::class], $gameStartEvent['events'][0]['code'] ?? false); + $this->assertSame($game->getProperties()->max_rounds, $gameStartEvent['events'][0]['data']['setting']['max_rounds'] ?? false); + $this->assertSame(2, $gameStartEvent['events'][0]['data']['playersCount'] ?? false); + + $gameOverMsg = $serverResponses[1]; + $gameOverEvent = json_decode($gameOverMsg, true); + $this->assertIsArray($gameOverEvent); + $this->assertCount(1, $gameOverEvent['players'] ?? false); + $this->assertCount(1, $gameOverEvent['events']); + $this->assertSame(EventList::map[GameOverEvent::class], $gameOverEvent['events'][0]['code'] ?? false); + $this->assertSame(GameOverReason::REASON_NOT_ALL_PLAYERS_CONNECTED->value, $gameOverEvent['events'][0]['data']['reason'] ?? false); } public function testServerGameOver(): void diff --git a/test/og/World/FloorTest.php b/test/og/World/FloorTest.php index 41cadb1..302f0cf 100644 --- a/test/og/World/FloorTest.php +++ b/test/og/World/FloorTest.php @@ -7,6 +7,8 @@ use cs\Core\GameState; use cs\Core\Point; use cs\Core\Setting; +use cs\Enum\SoundType; +use cs\Event\SoundEvent; use Test\BaseTestCase; class FloorTest extends BaseTestCase @@ -102,4 +104,39 @@ public function testPlayerBoxTunnel(): void $this->assertSame(20 * Setting::moveDistancePerTick(), $p->getPositionClone()->z); } + public function testPlayerMakeNoiseWhenFallingOnFloorEdgeBoundingRadiusSerialize(): void + { + $floor = new Floor(new Point(500, 10, 500)); + $game = $this->createTestGame(5); + $game->getWorld()->addFloor($floor); + + $player = $game->getPlayer(1); + $player->setPosition($floor->getStart()->clone()->addPart( + -$player->getBoundingRadius(), Setting::playerObstacleOvercomeHeight() * 5, $player->getBoundingRadius(), + )); + + $groundTouch = null; + $game->onEvents(function (array $events) use (&$groundTouch): void { + foreach ($events as $event) { + if ($event instanceof SoundEvent && $event->type === SoundType::PLAYER_GROUND_TOUCH) { + $this->assertNull($groundTouch); + $groundTouch = $event; + } + } + }); + + $game->start(); + $this->assertSame(1, $game->getRoundNumber()); + $this->assertInstanceOf(SoundEvent::class, $groundTouch); + $this->assertSame($floor->getY(), $player->getPositionClone()->y); + $this->assertSame([ + 'position' => $player->getPositionClone()->setY($floor->getY())->toArray(), + 'item' => null, + 'player' => $player->getId(), + 'surface' => null, + 'type' => SoundType::PLAYER_GROUND_TOUCH->value, + 'extra' => [], + ], $groundTouch->serialize()); + } + } diff --git a/test/og/World/NavigationMeshTest.php b/test/og/World/NavigationMeshTest.php index bc0f9b5..1d05ff4 100644 --- a/test/og/World/NavigationMeshTest.php +++ b/test/og/World/NavigationMeshTest.php @@ -106,6 +106,27 @@ public function testBoundary(): void $this->assertPositionNotSame($orig, $candidate); } + public function testBoundaryAbove(): void + { + $game = $this->createTestGame(); + $game->getWorld()->addBox(new Box(new Point(), 10, 1000, 10)); + $start = new Point(1, 1, 1); + $game->getWorld()->addBox(new Box($start->clone(), 1, 1, 10)); + $game->getTestMap()->startPointForNavigationMesh->setFrom($start); + $path = $game->getWorld()->buildNavigationMesh(3, 100); + + $candidate = $start->clone()->setY(0); + $this->assertNull($path->getGraph()->getNodeById($candidate->hash())); + + $closestCandidate = $candidate->clone(); + $path->convertToNavMeshNode($closestCandidate); + $this->assertNull($path->getGraph()->getNodeById($closestCandidate->hash())); + + $validPoint = $path->findTile($candidate, 1); + $this->assertNotNull($path->getGraph()->getNodeById($validPoint->hash())); + $this->assertSame('2,1,2', $validPoint->hash()); + } + public function testOneWayDirection(): void { $game = $this->createTestGame(); diff --git a/test/og/World/WorldTest.php b/test/og/World/WorldTest.php index fbc4de2..bef62fd 100644 --- a/test/og/World/WorldTest.php +++ b/test/og/World/WorldTest.php @@ -80,6 +80,28 @@ public function testCanBeSeen(): void $this->assertFalse($game->getWorld()->canBeSeen($player, $player->getPositionClone()->setY(-1), 10, 999)); } + public function testRandomSpawnPosition(): void + { + $game = $this->createGame(); + $map = new TestMap(); + $game->loadMap($map); + $firstSpawnPosition = $map->getSpawnPositionDefender()[0]; + $this->assertPositionSame($firstSpawnPosition, $game->getWorld()->getPlayerSpawnPosition(false, false)); + $game->getWorld()->roundReset(); + $this->assertPositionSame($firstSpawnPosition, $game->getWorld()->getPlayerSpawnPosition(false, false)); + $game->getWorld()->roundReset(); + $this->assertPositionSame($firstSpawnPosition, $game->getWorld()->getPlayerSpawnPosition(false, false)); + + $game->getWorld()->roundReset(); + if ($game->getWorld()->getPlayerSpawnPosition(false, true)->equals($firstSpawnPosition)) { + $game->getWorld()->roundReset(); + if ($game->getWorld()->getPlayerSpawnPosition(false, true)->equals($firstSpawnPosition)) { + $game->getWorld()->roundReset(); + $this->assertPositionNotSame($firstSpawnPosition, $game->getWorld()->getPlayerSpawnPosition(false, true)); + } + } + } + public function testStairCaseUp(): void { $steps = 20; diff --git a/www/assets/js/ModelRepository.js b/www/assets/js/ModelRepository.js index 9573c9f..8cbae70 100644 --- a/www/assets/js/ModelRepository.js +++ b/www/assets/js/ModelRepository.js @@ -76,6 +76,7 @@ export class ModelRepository { const bomb = this.#models[ItemId.Bomb] bomb.children.forEach((root) => root.visible = false) bomb.getObjectByName('item').visible = true + bomb.rotation.set(0, 0, 0) bomb.position.setScalar(0) return bomb } diff --git a/www/assets/js/World.js b/www/assets/js/World.js index 6c4cbb5..8ce798d 100644 --- a/www/assets/js/World.js +++ b/www/assets/js/World.js @@ -121,7 +121,6 @@ export class World { spawnBomb(position) { const bomb = this.#modelRepository.getBomb() this.#scene.add(bomb) - bomb.rotation.set(0, 0, 0) bomb.position.set(position.x, position.y, -position.z) bomb.visible = true }