Skip to content

Commit c9ebe3f

Browse files
coopryansolcloud
authored andcommitted
nade: support rotated planes
1 parent d93d4b0 commit c9ebe3f

11 files changed

Lines changed: 220 additions & 40 deletions

File tree

infection.json5

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@
6767
"if\\s*\\(\\$this->ball->getResolutionAngleVertical\\(\\) > 0 && \\(.+",
6868
],
6969
},
70+
"LogicalAndNegation": {
71+
"ignoreSourceCodeByRegex": [
72+
".+\\$moveZ !== 0 && \\$planeCollision = \\$this->world->checkZSideWallCollision\\(\\$this->candidate, 2 \\* \\$r, \\$r.+",
73+
]
74+
},
7075
"LogicalNot": {
7176
"ignoreSourceCodeByRegex": [
7277
".+\\$this->lastMoveX === -\\$moveX.+",

server/src/Core/Floor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public function __construct(Point $start, public readonly int $width = 1, public
1212
throw new GameException("Width and depth cannot be lower than or equal zero");
1313
}
1414
parent::__construct($start, new Point($start->x + $width, $start->y, $start->z + $depth), 'xz');
15+
$this->setNormal(0, 90);
1516
}
1617

1718
public function getY(): int

server/src/Core/Plane.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ abstract class Plane extends SolidSurface
1111
protected int $hitAntiForce = 25123;
1212
protected int $hitAntiForceMargin = 10;
1313
protected int $wallBangEdgeMarginDistance = 8;
14+
protected int $normal;
1415
protected Point2D $point2DStart;
1516
protected Point2D $point2DEnd;
1617

@@ -33,6 +34,40 @@ public function setHitAntiForce(int $hitAntiForceBody, int $hitAntiForceMargin,
3334
$this->wallBangEdgeMarginDistance = max(0, $wallBangEdgeMarginDistance);
3435
}
3536

37+
public function setNormal(int|float|null $angleHorizontal, int|float $angleVertical): static
38+
{
39+
$angleHorizontal = (int)Util::normalizeAngle($angleHorizontal ?? 0);
40+
$angleHorizontal += $angleHorizontal > 180 ? -180 : 0;
41+
$angleVertical = (int)Util::normalizeAngleVertical($angleVertical);
42+
$this->normal = ($angleHorizontal << 8) | abs($angleVertical);
43+
return $this;
44+
}
45+
46+
/** @return array{int, int} **/
47+
public function getNormal(): array
48+
{
49+
return [$this->normal >> 8, $this->normal & 0xFF];
50+
}
51+
52+
/** @return array{float, float, float} **/
53+
public function getNormalizedNormal(float $targetAngleHorizontal, float $targetAngleVertical, int $precision): array
54+
{
55+
[$normalHorizontal, $normalVertical] = $this->getNormal();
56+
$deltaHorizontal = abs(Util::smallestDeltaAngle((int)round($targetAngleHorizontal), $normalHorizontal));
57+
$deltaVertical = abs(Util::smallestDeltaAngle((int)round($targetAngleVertical), $normalVertical));
58+
if ($deltaHorizontal < 90) {
59+
$normalHorizontal = Util::normalizeAngle($normalHorizontal + 180);
60+
}
61+
if ($deltaVertical < 90) {
62+
$normalVertical *= -1;
63+
}
64+
65+
$normalVec = Util::movementXYZ($normalHorizontal, $normalVertical, $precision);
66+
$precision = (float)$precision;
67+
$normalVec = [$normalVec[0] / $precision, $normalVec[1] / $precision, $normalVec[2] / $precision];
68+
return $normalVec;
69+
}
70+
3671
public function getHitAntiForce(Point $point): int
3772
{
3873
if (!$this->penetrable) {

server/src/Core/PlaneBuilder.php

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ final class PlaneBuilder
66
{
77
/** @var array<string,Point> */
88
private array $voxels = [];
9+
/** @var array{?float,float} */
10+
private array $voxelNormal;
911

1012
/** @return list<Plane> */
1113
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
3234

3335
$planes = [];
3436
foreach ($this->voxelizeTriangle($a, $b, $c, $voxelSize, $voxelThreshold, $matchSize) as $voxelPoint) {
35-
$planes[] = new Wall($voxelPoint, true, $voxelSize, $voxelSize);
36-
$planes[] = new Wall($voxelPoint, false, $voxelSize, $voxelSize);
37-
$planes[] = new Wall($voxelPoint->clone()->addX($voxelSize), false, $voxelSize, $voxelSize);
38-
$planes[] = new Floor($voxelPoint->clone()->addY($voxelSize), $voxelSize, $voxelSize);
37+
$planes[] = (new Wall($voxelPoint, true, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]);
38+
$planes[] = (new Wall($voxelPoint, false, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]);
39+
$planes[] = (new Wall($voxelPoint->clone()->addX($voxelSize), false, $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]);
40+
$planes[] = (new Floor($voxelPoint->clone()->addY($voxelSize), $voxelSize, $voxelSize))->setNormal($this->voxelNormal[0], $this->voxelNormal[1]);
3941
}
4042
return $planes;
4143
}
@@ -173,7 +175,7 @@ private function rotatedWall(Point $start, Point $end, int $height, float $jagge
173175
$width = abs($widthOnXAxis ? $previous->x - $current[0] : $previous->z - $current[2]);
174176
$leftPoint = ($direction[1] === 1 || $widthOnXAxis ? $previous->clone() : new Point($current[0], $start->y, $current[2]));
175177

176-
$walls[] = new Wall($leftPoint, $widthOnXAxis, $width, $height);
178+
$walls[] = (new Wall($leftPoint, $widthOnXAxis, $width, $height))->setNormal(90 + $angleH, 0);
177179
$previous->set($current[0], $start->y, $current[2]);
178180
$widthOnXAxis = !$widthOnXAxis;
179181
}
@@ -189,6 +191,8 @@ private function ramp(Point $start, Point $end, int $width, float $jaggedness):
189191
GameException::invalid(); // @codeCoverageIgnore
190192
}
191193
assert($angleV !== 0.0);
194+
$normalH = $angleH + 90; // fixme: embrace jaggedness
195+
$normalV = $angleV + 90; // fixme: embrace jaggedness
192196

193197
$planes = [];
194198
$previous = $start->clone();
@@ -218,13 +222,13 @@ private function ramp(Point $start, Point $end, int $width, float $jaggedness):
218222
$current = $points[$i - 1];
219223
if ($isFloor) {
220224
if ($wallWidthOnXAxis) {
221-
$planes[] = new Floor($previous->clone(), $width, $current[2] - $previous->z);
225+
$planes[] = (new Floor($previous->clone(), $width, $current[2] - $previous->z))->setNormal($normalH, $normalV);
222226
} else {
223-
$planes[] = new Floor($previous->clone(), $current[0] - $previous->x, $width);
227+
$planes[] = (new Floor($previous->clone(), $current[0] - $previous->x, $width))->setNormal($normalH, $normalV);
224228
}
225229
} else {
226230
$wallStart = ($stairsGoingUp ? $previous->clone() : new Point(...$current));
227-
$planes[] = new Wall($wallStart, $wallWidthOnXAxis, $width, abs($current[1] - $previous->y));
231+
$planes[] = (new Wall($wallStart, $wallWidthOnXAxis, $width, abs($current[1] - $previous->y)))->setNormal($normalH, $normalV);
228232
}
229233

230234
$previous->set($current[0], $current[1], $current[2]);
@@ -288,6 +292,14 @@ private function stairs(Point $base, Point $top, int $topSize, int $stepHeight,
288292
*/
289293
private function voxelizeTriangle(Point $a, Point $b, Point $c, int $voxelSize, int $voxelThreshold, bool $matchTriangleSize): array
290294
{
295+
$u = [$b->x - $a->x, $b->y - $a->y, $b->z - $a->z];
296+
$v = [$c->x - $a->x, $c->y - $a->y, $c->z - $a->z];
297+
$this->voxelNormal = Util::worldAngle(new Point(
298+
($u[1] * $v[2]) - ($u[2] * $v[1]),
299+
($u[2] * $v[0]) - ($u[0] * $v[2]),
300+
($u[0] * $v[1]) - ($u[1] * $v[0]),
301+
));
302+
291303
$this->voxels = [];
292304
$this->voxelizeLine($a, $b);
293305
$this->voxelizeLine($b, $c);

server/src/Core/Wall.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ public function __construct(
1818

1919
if ($widthOnXAxis) {
2020
parent::__construct($start, new Point($start->x + $width, $start->y + $height, $start->z), 'xy');
21+
$this->setNormal(0, 0);
2122
} else {
2223
parent::__construct($start, new Point($start->x, $start->y + $height, $start->z + $width), 'zy');
24+
$this->setNormal(90, 0);
2325
}
2426
}
2527

server/src/Event/ThrowEvent.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ public function process(int $tick): void
139139
$pos->z = $z;
140140
}
141141

142-
if (!$this->ball->hasCollision($pos)) {
142+
$ballCollision = $this->ball->hasCollision($pos);
143+
if ($ballCollision === false) {
143144
continue;
144145
}
145146

@@ -162,7 +163,7 @@ public function process(int $tick): void
162163
$this->setAngles($this->ball->getResolutionAngleHorizontal(), $this->ball->getResolutionAngleVertical());
163164
$this->bounceCount++;
164165
$this->velocity = $this->velocity / ($this->bounceCount > 4 ? $this->bounceCount : 1.5);
165-
if ($this->velocity < 1) { // fixme some value based on velocity and gravity that will give lowest possible (angle 0.01/90) distance < 1
166+
if ($ballCollision === null) {
166167
$this->finishLanding($pos);
167168
return;
168169
}

server/src/HitGeometry/BallCollider.php

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ class BallCollider
1414
private Point $lastValidPosition;
1515
private Point $lastExtremePosition;
1616
private int $lastMoveY;
17-
private bool $yGrowing;
17+
18+
private const int angleWorldPrecision = 1_000_000;
19+
private const int angleRoundDecimalPlaces = 2;
1820

1921
public function __construct(
2022
protected World $world,
@@ -29,49 +31,55 @@ public function __construct(
2931
}
3032

3133
$this->candidate = new Point();
32-
$this->yGrowing = $this->angleVertical > 0;
3334
$this->lastMoveY = $this->angleVertical <=> 0;
3435
$this->lastValidPosition = $origin->clone();
3536
$this->lastExtremePosition = $origin->clone();
3637
}
3738

38-
public function hasCollision(Point $point): bool
39+
public function hasCollision(Point $point): ?bool
3940
{
4041
$moveX = $point->x <=> $this->lastValidPosition->x;
4142
$moveY = $point->y <=> $this->lastValidPosition->y;
4243
$moveZ = $point->z <=> $this->lastValidPosition->z;
4344

4445
$r = $this->radius;
45-
$isCollision = false;
46+
$planeCollision = null;
4647
$this->candidate->set($point->x + $r * $moveX, $point->y + $r * $moveY, $point->z + $r * $moveZ);
4748

48-
if ($moveY !== 0 && $this->world->findFloorSquare($this->candidate, $r)) {
49-
$this->angleVertical = Util::nearbyInt(Util::worldAngle($point, $this->lastExtremePosition)[1]);
50-
$this->angleVertical = $moveY > 0 ? -abs($this->angleVertical) : abs($this->angleVertical);
51-
$this->yGrowing = $this->angleVertical > 0;
52-
$isCollision = true;
53-
}
54-
55-
if ($moveX !== 0 && $this->world->checkXSideWallCollision($this->candidate, 2 * $r, $r)) {
56-
$this->angleHorizontal = Util::normalizeAngle(360 - $this->angleHorizontal);
57-
$isCollision = true;
58-
} elseif ($moveZ !== 0 && $this->world->checkZSideWallCollision($this->candidate, 2 * $r, $r)) {
59-
$this->angleHorizontal = Util::normalizeAngle(360 - $this->angleHorizontal + 180);
60-
$isCollision = true;
49+
if ($moveY !== 0 && $planeCollision = $this->world->findFloorSquare($this->candidate, $r)) {
50+
} elseif ($moveX !== 0 && $planeCollision = $this->world->checkXSideWallCollision($this->candidate, 2 * $r, $r)) {
51+
} elseif ($moveZ !== 0 && $planeCollision = $this->world->checkZSideWallCollision($this->candidate, 2 * $r, $r)) {
6152
}
6253

63-
if ($isCollision) {
64-
if ($moveY !== 0 && $this->yGrowing === false && $this->angleVertical > 0 && ($moveX !== 0 || $moveZ !== 0)) {
65-
$this->angleVertical = min(-10, Util::nearbyInt(Util::worldAngle($point, $this->lastExtremePosition)[1]));
54+
if ($planeCollision !== null) {
55+
if ($planeCollision->getNormal()[1] !== 0) {
56+
$this->angleVertical = Util::worldAngle($point, $this->lastExtremePosition)[1];
57+
}
58+
$precision = self::angleWorldPrecision;
59+
$normalVec = $planeCollision->getNormalizedNormal($this->angleHorizontal, $this->angleVertical, $precision);
60+
$directionVec = Util::movementXYZ($this->angleHorizontal, $this->angleVertical, $precision);
61+
$doubleDotProduct = 2 * ($directionVec[0] * $normalVec[0] + $directionVec[1] * $normalVec[1] + $directionVec[2] * $normalVec[2]);
62+
assert($doubleDotProduct <= 0);
63+
if ($doubleDotProduct == 0) {
64+
return null;
6665
}
66+
$reflectionVec = [
67+
Util::nearbyInt($directionVec[0] - ($doubleDotProduct * $normalVec[0])),
68+
Util::nearbyInt($directionVec[1] - ($doubleDotProduct * $normalVec[1])),
69+
Util::nearbyInt($directionVec[2] - ($doubleDotProduct * $normalVec[2])),
70+
];
71+
72+
$this->candidate->setFromArray($reflectionVec);
73+
[$h, $v] = Util::worldAngle($this->candidate);
74+
$this->angleHorizontal = round($h ?? 0, self::angleRoundDecimalPlaces);
75+
$this->angleVertical = round($v, self::angleRoundDecimalPlaces);
6776
$this->lastExtremePosition->setFrom($point);
6877
return true;
6978
}
7079

7180
if ($moveY !== 0 && $this->lastMoveY !== $moveY) {
7281
$this->lastMoveY = $moveY;
7382
$this->lastExtremePosition->setFrom($point);
74-
$this->yGrowing = $moveY === 1;
7583
}
7684

7785
$this->lastValidPosition->setFrom($point);

test/og/Shooting/GrenadeTest.php

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public function testThrow(): void
6161
$this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z);
6262
$this->assertSame($floorY, $bounceEvent->position->y);
6363
$this->assertSame($floorY, $landEvent->position->y);
64-
$this->assertPositionSame(new Point(152, $floorY, 720), $landEvent->position);
64+
$this->assertPositionSame(new Point(153, $floorY, 707), $landEvent->position);
6565
}
6666

6767
public function testThrowRun(): void
@@ -108,7 +108,7 @@ function (Player $p) {
108108
$this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z);
109109
$this->assertSame($floorY, $bounceEvent->position->y);
110110
$this->assertSame($floorY, $landEvent->position->y);
111-
$this->assertPositionSame(new Point(220, $floorY, 1022), $landEvent->position);
111+
$this->assertPositionSame(new Point(220, $floorY, 1008), $landEvent->position);
112112
}
113113

114114
public function testThrow2(): void
@@ -195,7 +195,83 @@ public function testThrow3(): void
195195
$this->assertGreaterThan($bounceEvent->position->z, $landEvent->position->z);
196196
$this->assertSame($y, $bounceEvent->position->y);
197197
$this->assertSame($y, $landEvent->position->y);
198-
$this->assertPositionSame(new Point(470, $y, 470), $landEvent->position);
198+
$this->assertPositionSame(new Point(464, $y, 464), $landEvent->position);
199+
}
200+
201+
public function testThrow4(): void
202+
{
203+
$landEvent = null;
204+
$bounceEvents = null;
205+
$game = $this->createTestGame();
206+
$game->getWorld()->addWall((new Wall(new Point(0, 0, 500), true, 100))->setNormal(45, 45));
207+
$game->onEvents(function (array $events) use (&$landEvent, &$bounceEvent): void {
208+
foreach ($events as $event) {
209+
if (!($event instanceof SoundEvent)) {
210+
continue;
211+
}
212+
if ($event->type === SoundType::GRENADE_LAND) {
213+
$this->assertTrue(is_null($landEvent), 'Only one landEvent please');
214+
$landEvent = $event;
215+
}
216+
if (!$bounceEvent && $event->type === SoundType::GRENADE_BOUNCE) {
217+
$bounceEvent = $event;
218+
}
219+
}
220+
});
221+
$this->playPlayer($game, [
222+
fn(Player $p) => $p->getSight()->look(10, 45),
223+
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)),
224+
fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_GRENADE_DECOY)),
225+
$this->waitNTicks(Decoy::equipReadyTimeMs),
226+
fn(Player $p) => $this->assertNotNull($p->attack()),
227+
$this->waitNTicks(1700),
228+
$this->endGame(),
229+
]);
230+
231+
$y = Decoy::boundingRadius;
232+
$this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value));
233+
$this->assertInstanceOf(SoundEvent::class, $bounceEvent);
234+
$this->assertInstanceOf(SoundEvent::class, $landEvent);
235+
$this->assertPositionSame(new Point(86, 430, 490), $bounceEvent->position);
236+
$this->assertPositionSame(new Point(115, 10, 443), $landEvent->position);
237+
}
238+
239+
public function testThrow5(): void
240+
{
241+
$landEvent = null;
242+
$bounceEvents = null;
243+
$game = $this->createTestGame();
244+
$game->getWorld()->addWall((new Wall(new Point(0, 0, 500), true, 76))->setNormal(135, 45));
245+
$game->onEvents(function (array $events) use (&$landEvent, &$bounceEvent): void {
246+
foreach ($events as $event) {
247+
if (!($event instanceof SoundEvent)) {
248+
continue;
249+
}
250+
if ($event->type === SoundType::GRENADE_LAND) {
251+
$this->assertTrue(is_null($landEvent), 'Only one landEvent please');
252+
$landEvent = $event;
253+
}
254+
if (!$bounceEvent && $event->type === SoundType::GRENADE_BOUNCE) {
255+
$bounceEvent = $event;
256+
}
257+
}
258+
});
259+
$this->playPlayer($game, [
260+
fn(Player $p) => $p->getSight()->look(10, 45),
261+
fn(Player $p) => $this->assertTrue($p->buyItem(BuyMenuItem::GRENADE_DECOY)),
262+
fn(Player $p) => $this->assertTrue($p->equip(InventorySlot::SLOT_GRENADE_DECOY)),
263+
$this->waitNTicks(Decoy::equipReadyTimeMs),
264+
fn(Player $p) => $this->assertNotNull($p->attack()),
265+
$this->waitNTicks(1700),
266+
$this->endGame(),
267+
]);
268+
269+
$y = Decoy::boundingRadius;
270+
$this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value));
271+
$this->assertInstanceOf(SoundEvent::class, $bounceEvent);
272+
$this->assertInstanceOf(SoundEvent::class, $landEvent);
273+
$this->assertPositionSame(new Point(86, 430, 490), $bounceEvent->position);
274+
$this->assertPositionSame(new Point(632, 10, 616), $landEvent->position);
199275
}
200276

201277
public function testThrowFlashBang(): void
@@ -278,7 +354,7 @@ private function _testFullVerticalThrow(int $floorY): void
278354
]);
279355

280356
$this->assertFalse($game->getPlayer(1)->getInventory()->has(InventorySlot::SLOT_GRENADE_DECOY->value));
281-
$this->assertCount(4, $bounceEvents);
357+
$this->assertCount(5, $bounceEvents);
282358
$bounceEvent = array_pop($bounceEvents);
283359
$this->assertInstanceOf(SoundEvent::class, $bounceEvent); // @phpstan-ignore-line
284360
$this->assertInstanceOf(SoundEvent::class, $landEvent);
@@ -324,7 +400,7 @@ public function testThrowAgainstWall(): void
324400
$pp = $game->getPlayer(1)->getPositionClone();
325401
$this->assertGreaterThan($pp->x, $landEvent->position->x);
326402
$this->assertGreaterThan($pp->z, $landEvent->position->z);
327-
$this->assertPositionSame(new Point(703, Decoy::boundingRadius, 374), $landEvent->position);
403+
$this->assertPositionSame(new Point(708, Decoy::boundingRadius, 376), $landEvent->position);
328404
}
329405

330406
public function testMultiThrow(): void
@@ -439,7 +515,7 @@ public function testExtremePosition(): void
439515
$this->assertSame(Decoy::boundingRadius, $test->land->y);
440516
$this->assertSame(1, $test->bounceCount);
441517
$this->assertSame(-1 + Decoy::boundingRadius, $test->bounceX);
442-
$this->assertSame(114, $test->airCount);
518+
$this->assertSame(125, $test->airCount);
443519
$this->assertFalse($test->goingUp);
444520
}
445521

0 commit comments

Comments
 (0)