diff --git a/cli/udp-ws-bridge.php b/cli/udp-ws-bridge.php index a90e9493..201b37a8 100644 --- a/cli/udp-ws-bridge.php +++ b/cli/udp-ws-bridge.php @@ -11,7 +11,7 @@ $addressUdp = "udp://localhost:{$portUdp}"; /// -$udp = (new Factory())->createClient($addressUdp, 4); +$udp = (new Factory())->createClient($addressUdp, 15); $ws = new Server([ 'filter' => ['text'], 'fragment_size' => '8192', diff --git a/infection.json5 b/infection.json5 index eac77c49..c687310c 100644 --- a/infection.json5 +++ b/infection.json5 @@ -67,6 +67,11 @@ "if\\s*\\(\\$this->ball->getResolutionAngleVertical\\(\\) > 0 && \\(.+", ], }, + "LogicalAndNegation": { + "ignoreSourceCodeByRegex": [ + ".+\\$moveZ !== 0 && \\$planeCollision = \\$this->world->checkZSideWallCollision\\(\\$this->candidate, 2 \\* \\$r, \\$r.+", + ] + }, "LogicalNot": { "ignoreSourceCodeByRegex": [ ".+\\$this->lastMoveX === -\\$moveX.+", diff --git a/server/src/Core/Floor.php b/server/src/Core/Floor.php index f6174f0f..d401d5dd 100644 --- a/server/src/Core/Floor.php +++ b/server/src/Core/Floor.php @@ -12,6 +12,7 @@ public function __construct(Point $start, public readonly int $width = 1, public throw new GameException("Width and depth cannot be lower than or equal zero"); } parent::__construct($start, new Point($start->x + $width, $start->y, $start->z + $depth), 'xz'); + $this->setNormal(0, 90); } public function getY(): int diff --git a/server/src/Core/Plane.php b/server/src/Core/Plane.php index fafbbd30..0f391ac7 100644 --- a/server/src/Core/Plane.php +++ b/server/src/Core/Plane.php @@ -11,6 +11,7 @@ abstract class Plane extends SolidSurface protected int $hitAntiForce = 25123; protected int $hitAntiForceMargin = 10; protected int $wallBangEdgeMarginDistance = 8; + protected int $normal; protected Point2D $point2DStart; protected Point2D $point2DEnd; @@ -33,6 +34,40 @@ public function setHitAntiForce(int $hitAntiForceBody, int $hitAntiForceMargin, $this->wallBangEdgeMarginDistance = max(0, $wallBangEdgeMarginDistance); } + public function setNormal(int|float|null $angleHorizontal, int|float $angleVertical): static + { + $angleHorizontal = (int)Util::normalizeAngle($angleHorizontal ?? 0); + $angleHorizontal += $angleHorizontal > 180 ? -180 : 0; + $angleVertical = (int)Util::normalizeAngleVertical($angleVertical); + $this->normal = ($angleHorizontal << 8) | abs($angleVertical); + return $this; + } + + /** @return array{int, int} **/ + public function getNormal(): array + { + return [$this->normal >> 8, $this->normal & 0xFF]; + } + + /** @return array{float, float, float} **/ + public function getNormalizedNormal(float $targetAngleHorizontal, float $targetAngleVertical, int $precision): array + { + [$normalHorizontal, $normalVertical] = $this->getNormal(); + $deltaHorizontal = abs(Util::smallestDeltaAngle((int)round($targetAngleHorizontal), $normalHorizontal)); + $deltaVertical = abs(Util::smallestDeltaAngle((int)round($targetAngleVertical), $normalVertical)); + if ($deltaHorizontal < 90) { + $normalHorizontal = Util::normalizeAngle($normalHorizontal + 180); + } + if ($deltaVertical < 90) { + $normalVertical *= -1; + } + + $normalVec = Util::movementXYZ($normalHorizontal, $normalVertical, $precision); + $precision = (float)$precision; + $normalVec = [$normalVec[0] / $precision, $normalVec[1] / $precision, $normalVec[2] / $precision]; + return $normalVec; + } + public function getHitAntiForce(Point $point): int { if (!$this->penetrable) { diff --git a/server/src/Core/PlaneBuilder.php b/server/src/Core/PlaneBuilder.php index 28454ba7..cb77431f 100644 --- a/server/src/Core/PlaneBuilder.php +++ b/server/src/Core/PlaneBuilder.php @@ -6,6 +6,8 @@ final class PlaneBuilder { /** @var array */ private array $voxels = []; + /** @var array{?float,float} */ + private array $voxelNormal; /** @return list */ public function create(Point $a, Point $b, Point $c, ?Point $d = null, ?float $jaggedness = null): array @@ -32,10 +34,10 @@ public function fromTriangle(Point $a, Point $b, Point $c, float $voxelSizeDotTh $planes = []; foreach ($this->voxelizeTriangle($a, $b, $c, $voxelSize, $voxelThreshold, $matchSize) as $voxelPoint) { - $planes[] = new Wall($voxelPoint, true, $voxelSize, $voxelSize); - $planes[] = new Wall($voxelPoint, false, $voxelSize, $voxelSize); - $planes[] = new Wall($voxelPoint->clone()->addX($voxelSize), false, $voxelSize, $voxelSize); - $planes[] = new Floor($voxelPoint->clone()->addY($voxelSize), $voxelSize, $voxelSize); + $planes[] = (new Wall($voxelPoint, true, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]); + $planes[] = (new Wall($voxelPoint, false, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]); + $planes[] = (new Wall($voxelPoint->clone()->addX($voxelSize), false, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]); + $planes[] = (new Floor($voxelPoint->clone()->addY($voxelSize), $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]); } return $planes; } @@ -173,7 +175,7 @@ private function rotatedWall(Point $start, Point $end, int $height, float $jagge $width = abs($widthOnXAxis ? $previous->x - $current[0] : $previous->z - $current[2]); $leftPoint = ($direction[1] === 1 || $widthOnXAxis ? $previous->clone() : new Point($current[0], $start->y, $current[2])); - $walls[] = new Wall($leftPoint, $widthOnXAxis, $width, $height); + $walls[] = (new Wall($leftPoint, $widthOnXAxis, $width, $height))->setNormal(90 + $angleH, 0); $previous->set($current[0], $start->y, $current[2]); $widthOnXAxis = !$widthOnXAxis; } @@ -189,6 +191,8 @@ private function ramp(Point $start, Point $end, int $width, float $jaggedness): GameException::invalid(); // @codeCoverageIgnore } assert($angleV !== 0.0); + $normalH = $angleH + 90; // fixme: embrace jaggedness + $normalV = $angleV + 90; // fixme: embrace jaggedness $planes = []; $previous = $start->clone(); @@ -218,13 +222,13 @@ private function ramp(Point $start, Point $end, int $width, float $jaggedness): $current = $points[$i - 1]; if ($isFloor) { if ($wallWidthOnXAxis) { - $planes[] = new Floor($previous->clone(), $width, $current[2] - $previous->z); + $planes[] = (new Floor($previous->clone(), $width, $current[2] - $previous->z))->setNormal($normalH, $normalV); } else { - $planes[] = new Floor($previous->clone(), $current[0] - $previous->x, $width); + $planes[] = (new Floor($previous->clone(), $current[0] - $previous->x, $width))->setNormal($normalH, $normalV); } } else { $wallStart = ($stairsGoingUp ? $previous->clone() : new Point(...$current)); - $planes[] = new Wall($wallStart, $wallWidthOnXAxis, $width, abs($current[1] - $previous->y)); + $planes[] = (new Wall($wallStart, $wallWidthOnXAxis, $width, abs($current[1] - $previous->y)))->setNormal($normalH, $normalV); } $previous->set($current[0], $current[1], $current[2]); @@ -288,6 +292,14 @@ private function stairs(Point $base, Point $top, int $topSize, int $stepHeight, */ private function voxelizeTriangle(Point $a, Point $b, Point $c, int $voxelSize, int $voxelThreshold, bool $matchTriangleSize): array { + $u = [$b->x - $a->x, $b->y - $a->y, $b->z - $a->z]; + $v = [$c->x - $a->x, $c->y - $a->y, $c->z - $a->z]; + $this->voxelNormal = Util::worldAngle(new Point( + ($u[1] * $v[2]) - ($u[2] * $v[1]), + ($u[2] * $v[0]) - ($u[0] * $v[2]), + ($u[0] * $v[1]) - ($u[1] * $v[0]), + )); + $this->voxels = []; $this->voxelizeLine($a, $b); $this->voxelizeLine($b, $c); diff --git a/server/src/Core/Wall.php b/server/src/Core/Wall.php index 1ac35a17..0fa33704 100644 --- a/server/src/Core/Wall.php +++ b/server/src/Core/Wall.php @@ -18,8 +18,10 @@ public function __construct( if ($widthOnXAxis) { parent::__construct($start, new Point($start->x + $width, $start->y + $height, $start->z), 'xy'); + $this->setNormal(0, 0); } else { parent::__construct($start, new Point($start->x, $start->y + $height, $start->z + $width), 'zy'); + $this->setNormal(90, 0); } } diff --git a/server/src/Event/ThrowEvent.php b/server/src/Event/ThrowEvent.php index ab801746..a253dc0d 100644 --- a/server/src/Event/ThrowEvent.php +++ b/server/src/Event/ThrowEvent.php @@ -139,7 +139,8 @@ public function process(int $tick): void $pos->z = $z; } - if (!$this->ball->hasCollision($pos)) { + $ballCollision = $this->ball->hasCollision($pos); + if ($ballCollision === false) { continue; } @@ -162,7 +163,7 @@ public function process(int $tick): void $this->setAngles($this->ball->getResolutionAngleHorizontal(), $this->ball->getResolutionAngleVertical()); $this->bounceCount++; $this->velocity = $this->velocity / ($this->bounceCount > 4 ? $this->bounceCount : 1.5); - if ($this->velocity < 1) { // fixme some value based on velocity and gravity that will give lowest possible (angle 0.01/90) distance < 1 + if ($ballCollision === null) { $this->finishLanding($pos); return; } diff --git a/server/src/HitGeometry/BallCollider.php b/server/src/HitGeometry/BallCollider.php index 7fe62e67..ac15a2e7 100644 --- a/server/src/HitGeometry/BallCollider.php +++ b/server/src/HitGeometry/BallCollider.php @@ -14,7 +14,9 @@ class BallCollider private Point $lastValidPosition; private Point $lastExtremePosition; private int $lastMoveY; - private bool $yGrowing; + + private const int angleWorldPrecision = 1_000_000; + private const int angleRoundDecimalPlaces = 2; public function __construct( protected World $world, @@ -29,41 +31,48 @@ public function __construct( } $this->candidate = new Point(); - $this->yGrowing = $this->angleVertical > 0; $this->lastMoveY = $this->angleVertical <=> 0; $this->lastValidPosition = $origin->clone(); $this->lastExtremePosition = $origin->clone(); } - public function hasCollision(Point $point): bool + public function hasCollision(Point $point): ?bool { $moveX = $point->x <=> $this->lastValidPosition->x; $moveY = $point->y <=> $this->lastValidPosition->y; $moveZ = $point->z <=> $this->lastValidPosition->z; $r = $this->radius; - $isCollision = false; + $planeCollision = null; $this->candidate->set($point->x + $r * $moveX, $point->y + $r * $moveY, $point->z + $r * $moveZ); - if ($moveY !== 0 && $this->world->findFloorSquare($this->candidate, $r)) { - $this->angleVertical = Util::nearbyInt(Util::worldAngle($point, $this->lastExtremePosition)[1]); - $this->angleVertical = $moveY > 0 ? -abs($this->angleVertical) : abs($this->angleVertical); - $this->yGrowing = $this->angleVertical > 0; - $isCollision = true; - } - - if ($moveX !== 0 && $this->world->checkXSideWallCollision($this->candidate, 2 * $r, $r)) { - $this->angleHorizontal = Util::normalizeAngle(360 - $this->angleHorizontal); - $isCollision = true; - } elseif ($moveZ !== 0 && $this->world->checkZSideWallCollision($this->candidate, 2 * $r, $r)) { - $this->angleHorizontal = Util::normalizeAngle(360 - $this->angleHorizontal + 180); - $isCollision = true; + if ($moveY !== 0 && $planeCollision = $this->world->findFloorSquare($this->candidate, $r)) { + } elseif ($moveX !== 0 && $planeCollision = $this->world->checkXSideWallCollision($this->candidate, 2 * $r, $r)) { + } elseif ($moveZ !== 0 && $planeCollision = $this->world->checkZSideWallCollision($this->candidate, 2 * $r, $r)) { } - if ($isCollision) { - if ($moveY !== 0 && $this->yGrowing === false && $this->angleVertical > 0 && ($moveX !== 0 || $moveZ !== 0)) { - $this->angleVertical = min(-10, Util::nearbyInt(Util::worldAngle($point, $this->lastExtremePosition)[1])); + if ($planeCollision !== null) { + if ($planeCollision->getNormal()[1] !== 0) { + $this->angleVertical = Util::worldAngle($point, $this->lastExtremePosition)[1]; + } + $precision = self::angleWorldPrecision; + $normalVec = $planeCollision->getNormalizedNormal($this->angleHorizontal, $this->angleVertical, $precision); + $directionVec = Util::movementXYZ($this->angleHorizontal, $this->angleVertical, $precision); + $doubleDotProduct = 2 * ($directionVec[0] * $normalVec[0] + $directionVec[1] * $normalVec[1] + $directionVec[2] * $normalVec[2]); + assert($doubleDotProduct <= 0); + if ($doubleDotProduct == 0) { + return null; } + $reflectionVec = [ + Util::nearbyInt($directionVec[0] - ($doubleDotProduct * $normalVec[0])), + Util::nearbyInt($directionVec[1] - ($doubleDotProduct * $normalVec[1])), + Util::nearbyInt($directionVec[2] - ($doubleDotProduct * $normalVec[2])), + ]; + + $this->candidate->setFromArray($reflectionVec); + [$h, $v] = Util::worldAngle($this->candidate); + $this->angleHorizontal = round($h ?? 0, self::angleRoundDecimalPlaces); + $this->angleVertical = round($v, self::angleRoundDecimalPlaces); $this->lastExtremePosition->setFrom($point); return true; } @@ -71,7 +80,6 @@ public function hasCollision(Point $point): bool if ($moveY !== 0 && $this->lastMoveY !== $moveY) { $this->lastMoveY = $moveY; $this->lastExtremePosition->setFrom($point); - $this->yGrowing = $moveY === 1; } $this->lastValidPosition->setFrom($point); diff --git a/server/src/Map/DefaultMap.php b/server/src/Map/DefaultMap.php index 730de2e9..bb985783 100644 --- a/server/src/Map/DefaultMap.php +++ b/server/src/Map/DefaultMap.php @@ -6795,52 +6795,52 @@ public function __construct() // t spawn walls.001 $add( - new Point(5871, 1210, 1068), - new Point(5871, 1210, 1232), - new Point(5871, 1960, 1232), - new Point(5871, 1960, 1068), + new Point(5867, 1210, 1068), + new Point(5867, 1210, 1222), + new Point(5867, 1960, 1222), + new Point(5867, 1960, 1068), ); $add( - new Point(5871, 1960, 1232), - new Point(5871, 1210, 1232), - new Point(5015, 1210, 1232), - new Point(5015, 1960, 1232), + new Point(5867, 1960, 1222), + new Point(5867, 1210, 1222), + new Point(5003, 1210, 1222), + new Point(5003, 1960, 1222), ); $add( - new Point(5015, 1960, 1232), - new Point(5015, 1210, 1232), - new Point(5015, 1210, 1404), - new Point(5015, 1960, 1404), + new Point(5003, 1960, 1222), + new Point(5003, 1210, 1222), + new Point(5003, 1210, 1361), + new Point(5003, 1960, 1361), ); $add( - new Point(4809, 1960, 1404), - new Point(4809, 1210, 1404), - new Point(4809, 1210, 1080), - new Point(4809, 1960, 1080), + new Point(4811, 1960, 1361), + new Point(4811, 1210, 1361), + new Point(4811, 1210, 1080), + new Point(4811, 1960, 1080), ); $add( - new Point(5015, 1960, 1368), - new Point(5015, 1780, 1368), - new Point(5015, 1780, 2062), - new Point(5015, 1960, 2062), + new Point(5003, 1960, 1368), + new Point(5003, 1780, 1368), + new Point(5003, 1780, 2062), + new Point(5003, 1960, 2062), ); $add( - new Point(4809, 1780, 1368), - new Point(4809, 1960, 1368), - new Point(4809, 1960, 2062), - new Point(4809, 1780, 2062), + new Point(4811, 1780, 1368), + new Point(4811, 1960, 1368), + new Point(4811, 1960, 2062), + new Point(4811, 1780, 2062), ); $add( - new Point(4809, 1781, 1327), - new Point(5015, 1781, 1327), - new Point(5015, 1781, 2044), - new Point(4809, 1781, 2044), + new Point(4811, 1781, 1327), + new Point(5003, 1781, 1327), + new Point(5003, 1781, 2044), + new Point(4811, 1781, 2044), ); $add( - new Point(5015, 1960, 1404), - new Point(5015, 1210, 1404), - new Point(4810, 1210, 1404), - new Point(4810, 1960, 1404), + new Point(5003, 1960, 1361), + new Point(5003, 1210, 1361), + new Point(4811, 1210, 1361), + new Point(4811, 1960, 1361), ); // t spawn walls.002 diff --git a/test/og/Shooting/GrenadeTest.php b/test/og/Shooting/GrenadeTest.php index 94dfa2ef..a4f3cf20 100644 --- a/test/og/Shooting/GrenadeTest.php +++ b/test/og/Shooting/GrenadeTest.php @@ -61,7 +61,7 @@ public function testThrow(): void $this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z); $this->assertSame($floorY, $bounceEvent->position->y); $this->assertSame($floorY, $landEvent->position->y); - $this->assertPositionSame(new Point(152, $floorY, 720), $landEvent->position); + $this->assertPositionSame(new Point(153, $floorY, 707), $landEvent->position); } public function testThrowRun(): void @@ -108,7 +108,7 @@ function (Player $p) { $this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z); $this->assertSame($floorY, $bounceEvent->position->y); $this->assertSame($floorY, $landEvent->position->y); - $this->assertPositionSame(new Point(220, $floorY, 1022), $landEvent->position); + $this->assertPositionSame(new Point(220, $floorY, 1008), $landEvent->position); } public function testThrow2(): void @@ -195,7 +195,83 @@ public function testThrow3(): void $this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z); $this->assertSame($y, $bounceEvent->position->y); $this->assertSame($y, $landEvent->position->y); - $this->assertPositionSame(new Point(470, $y, 470), $landEvent->position); + $this->assertPositionSame(new Point(464, $y, 464), $landEvent->position); + } + + public function testThrow4(): void + { + $landEvent = null; + $bounceEvents = null; + $game = $this->createTestGame(); + $game->getWorld()->addWall((new Wall(new Point(0, 0, 500), true, 100))->setNormal(45, 45)); + $game->onEvents(function (array $events) use (&$landEvent, &$bounceEvent): void { + foreach ($events as $event) { + if (!($event instanceof SoundEvent)) { + continue; + } + if ($event->type === SoundType::GRENADE_LAND) { + $this->assertTrue(is_null($landEvent), 'Only one landEvent please'); + $landEvent = $event; + } + if (!$bounceEvent && $event->type === SoundType::GRENADE_BOUNCE) { + $bounceEvent = $event; + } + } + }); + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->look(10, 45), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)), + fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_GRENADE_DECOY)), + $this->waitNTicks(Decoy::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + $this->waitNTicks(1700), + $this->endGame(), + ]); + + $y = Decoy::boundingRadius; + $this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); + $this->assertInstanceOf(SoundEvent::class, $bounceEvent); + $this->assertInstanceOf(SoundEvent::class, $landEvent); + $this->assertPositionSame(new Point(86, 430, 490), $bounceEvent->position); + $this->assertPositionSame(new Point(115, 10, 443), $landEvent->position); + } + + public function testThrow5(): void + { + $landEvent = null; + $bounceEvents = null; + $game = $this->createTestGame(); + $game->getWorld()->addWall((new Wall(new Point(0, 0, 500), true, 76))->setNormal(135, 45)); + $game->onEvents(function (array $events) use (&$landEvent, &$bounceEvent): void { + foreach ($events as $event) { + if (!($event instanceof SoundEvent)) { + continue; + } + if ($event->type === SoundType::GRENADE_LAND) { + $this->assertTrue(is_null($landEvent), 'Only one landEvent please'); + $landEvent = $event; + } + if (!$bounceEvent && $event->type === SoundType::GRENADE_BOUNCE) { + $bounceEvent = $event; + } + } + }); + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->look(10, 45), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)), + fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_GRENADE_DECOY)), + $this->waitNTicks(Decoy::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + $this->waitNTicks(1700), + $this->endGame(), + ]); + + $y = Decoy::boundingRadius; + $this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); + $this->assertInstanceOf(SoundEvent::class, $bounceEvent); + $this->assertInstanceOf(SoundEvent::class, $landEvent); + $this->assertPositionSame(new Point(86, 430, 490), $bounceEvent->position); + $this->assertPositionSame(new Point(632, 10, 616), $landEvent->position); } public function testThrowFlashBang(): void @@ -278,7 +354,7 @@ private function _testFullVerticalThrow(int $floorY): void ]); $this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value)); - $this->assertCount(4, $bounceEvents); + $this->assertCount(5, $bounceEvents); $bounceEvent = array_pop($bounceEvents); $this->assertInstanceOf(SoundEvent::class, $bounceEvent); // @phpstan-ignore-line $this->assertInstanceOf(SoundEvent::class, $landEvent); @@ -324,7 +400,7 @@ public function testThrowAgainstWall(): void $pp = $game->getPlayer(1)->getPositionClone(); $this->assertGreaterThan($pp->x, $landEvent->position->x); $this->assertGreaterThan($pp->z, $landEvent->position->z); - $this->assertPositionSame(new Point(703, Decoy::boundingRadius, 374), $landEvent->position); + $this->assertPositionSame(new Point(708, Decoy::boundingRadius, 376), $landEvent->position); } public function testMultiThrow(): void @@ -439,7 +515,7 @@ public function testExtremePosition(): void $this->assertSame(Decoy::boundingRadius, $test->land->y); $this->assertSame(1, $test->bounceCount); $this->assertSame(-1 + Decoy::boundingRadius, $test->bounceX); - $this->assertSame(114, $test->airCount); + $this->assertSame(125, $test->airCount); $this->assertFalse($test->goingUp); } diff --git a/test/og/Shooting/MolotovGrenadeTest.php b/test/og/Shooting/MolotovGrenadeTest.php index f01cac72..d893c741 100644 --- a/test/og/Shooting/MolotovGrenadeTest.php +++ b/test/og/Shooting/MolotovGrenadeTest.php @@ -8,6 +8,7 @@ use cs\Core\Point; use cs\Core\Ramp; use cs\Core\Setting; +use cs\Core\Wall; use cs\Enum\BuyMenuItem; use cs\Enum\Color; use cs\Enum\InventorySlot; @@ -460,4 +461,32 @@ function (Player $p) { $this->assertSame(1, $smokeFadeCount, 'Smoke fade - single shrink event'); } + public function testRotatedWallBounce(): void + { + $game = $this->createNoPauseGame(); + $game->getWorld()->addBox(new Box(new Point(), 500, 1000, 500)); + $game->getWorld()->addWall(new Wall(new Point(-10, 0, 200), true, 999, 500)); + $game->getWorld()->addWall(new Wall(new Point(-20, 550, 200), true, 999, 500)); + $rotatedWall = new Wall(new Point(250, 400, 0), false, 999, 200); + $rotatedWall->setNormal(290, -25); + $game->getWorld()->addWall($rotatedWall); + $game->getPlayer(1)->setPosition(new Point(100, 0, 150)); + $game->addPlayer(new Player(2, Color::BLUE, false)); + $game->getPlayer(2)->setPosition(new Point(150, 0, 400)); + $game->getTestMap()->startPointForNavigationMesh->setFrom($game->getPlayer(2)->getPositionClone()); + + $this->playPlayer($game, [ + fn(Player $p) => $p->getSight()->look(100, 75), + fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_MOLOTOV)), + $this->waitNTicks(Molotov::equipReadyTimeMs), + fn(Player $p) => $this->assertNotNull($p->attack()), + $this->waitNTicks(2000), + fn() => $this->assertTrue($game->getWorld()->activeMolotovExists()), + $this->waitNTicks(Molotov::MAX_TIME_MS), + ]); + + $this->assertSame(2, $game->getRoundNumber()); + $this->assertSame(1, $game->getScore()->getPlayerStat(1)->getKills()); + } + } diff --git a/test/og/TestGame.php b/test/og/TestGame.php index 1972acb3..36c10f49 100644 --- a/test/og/TestGame.php +++ b/test/og/TestGame.php @@ -53,7 +53,7 @@ public function start(bool $debug = false): void call_user_func($this->afterTickCallback, $this->getState()); } if ($debug) { - $this->gameStates[$this->tick + 1] = json_decode($protocol->serialize($this->getPlayers(), $events)); + $this->gameStates[$this->tick] = json_decode($protocol->serialize($this->getPlayers(), $events)); } if ($gameOverEventOrNull) { break; diff --git a/test/og/Unit/BallColliderTest.php b/test/og/Unit/BallColliderTest.php index e3a310d5..0ecf19ef 100644 --- a/test/og/Unit/BallColliderTest.php +++ b/test/og/Unit/BallColliderTest.php @@ -70,8 +70,7 @@ public function testResolution2(): void $this->assertTrue($ball->hasCollision($start->addPart(-1, -1, 0))); $this->assertPositionSame($start, $ball->getLastExtremePosition()); $this->assertPositionSame(new Point($radius, $radius + 6, 0), $ball->getLastValidPosition()); - $this->assertSame(360.0 - $angleHorizontal, $ball->getResolutionAngleHorizontal()); - $this->assertLessThan(0, $ball->getResolutionAngleVertical()); + $this->assertSame(360.0 - $angleHorizontal, round($ball->getResolutionAngleHorizontal())); } protected function runCollision(BallCollider $ball, Point $start, float $angleHorizontal, float $angleVertical): void @@ -88,6 +87,12 @@ protected function runCollision(BallCollider $ball, Point $start, float $angleHo $this->fail("No '{$start}' collision detected"); } + public function testSingleWallAngledBounce(): void + { + $this->_testSingleWallBounce(new Point(5, 15, 5), 1, 180, 10, (new Wall(new Point(1, 1, 1), true, 100))->setNormal(0, 40), 0, -62, new Point(5, 16, 2)); + $this->_testSingleWallBounce(new Point(5, 15, 5), 1, 180, 70, (new Wall(new Point(1, 1, 1), true, 100))->setNormal(0, -10), 0, 49, new Point(5, 23, 2)); + } + public function testSingleWallBounce(): void { $this->_testSingleWallBounce(new Point(5, 5, 11), 4, 0, 90, new Floor(new Point(5, 16, 11)), 0, -90); @@ -108,7 +113,8 @@ public function testSingleWallBounce(): void private function _testSingleWallBounce( Point $ballCenter, int $ballRadius, float $angleHorizontal, float $angleVertical, - Plane $plane, float $expectedAngleHorizontal, float $expectedAngleVertical, int $maxDistance = 16 + Plane $plane, float $expectedAngleHorizontal, float $expectedAngleVertical, + ?Point $expectedCollisionPoint = null, int $maxDistance = 16 ): void { $world = $this->createWorld(); @@ -131,6 +137,7 @@ private function _testSingleWallBounce( continue; } + $expectedCollisionPoint && $this->assertPositionSame($expectedCollisionPoint, $candidate); $this->assertSame($expectedAngleHorizontal, round($ball->getResolutionAngleHorizontal())); $this->assertSame($expectedAngleVertical, round($ball->getResolutionAngleVertical())); diff --git a/test/og/World/WallTest.php b/test/og/World/WallTest.php index 88960841..79e83c3f 100644 --- a/test/og/World/WallTest.php +++ b/test/og/World/WallTest.php @@ -22,6 +22,10 @@ public function testIsWallAt(): void $wallHorizontal = new Wall(new Point(0, 0, 0), true, 2, 1); $wallVertical = new Wall(new Point(0, 0, 0), false, 2, 1); $wallHorizontal->setHitAntiForce(123, 10, 1); + $this->assertSame([0, 0], $wallHorizontal->getNormal()); + $this->assertSame([90, 0], $wallVertical->getNormal()); + $this->assertSame([+1.0, 0.0, 0.0], $wallVertical->getNormalizedNormal(270, 0, 1000)); + $this->assertSame([-1.0, 0.0, 0.0], $wallVertical->getNormalizedNormal(90, 0, 1000)); $world = new World(new Game()); $world->addWall($wallHorizontal); $world->addWall($wallVertical);