<?phpnamespace App\Game;use App\Engine\Map\Level;use App\Engine\Map\Line;use App\Engine\Map\Sector;/** * Movement, collisions and traces (p_map.c, p_maputl.c, p_mobj.c, p_sight.c), in floats. */trait Physics{ // results of checkPosition (tm* globals of the original) private float $tmFloorz = 0.0; private float $tmCeilingz = 0.0; private float $tmDropoffz = 0.0; private ?Line $ceilingLine = null; /** @var Line[] */ private array $specHit = []; /** P_BoxOnLineSide: 0 / 1 when the whole box is on one side, -1 when the line crosses it. */ public function boxOnLineSide(float $top, float $bottom, float $left, float $right, Line $l): int { if ($l->dx == 0) { $p1 = $right < $l->x1 ? 1 : 0; $p2 = $left < $l->x1 ? 1 : 0; if ($l->dy < 0) { $p1 ^= 1; $p2 ^= 1; } } elseif ($l->dy == 0) { $p1 = $top > $l->y1 ? 1 : 0; $p2 = $bottom > $l->y1 ? 1 : 0; if ($l->dx < 0) { $p1 ^= 1; $p2 ^= 1; } } elseif (($l->dy > 0) === ($l->dx > 0)) { $p1 = $l->side($left, $top); $p2 = $l->side($right, $bottom); } else { $p1 = $l->side($right, $top); $p2 = $l->side($left, $bottom); } return $p1 === $p2 ? $p1 : -1; } /** P_LineOpening: [top, bottom, lowfloor] of a two-sided line. */ private function opening(Line $l): array { $f = $l->frontSector; $b = $l->backSector; if ($b === null) { return [0.0, 0.0, 0.0]; } return [min($f->ceil, $b->ceil), max($f->floor, $b->floor), min($f->floor, $b->floor)]; } /** P_CheckPosition: can $mo stand at (x, y)? Fills tmFloorz / tmCeilingz / tmDropoffz. */ public function checkPosition(Mobj $mo, float $x, float $y): bool { $r = $mo->radius; $top = $y + $r; $bottom = $y - $r; $left = $x - $r; $right = $x + $r; $sector = $this->level->sectorAt($x, $y); $this->tmFloorz = $this->tmDropoffz = $sector->floor; $this->tmCeilingz = $sector->ceil; $this->ceilingLine = null; $this->specHit = []; if ($mo->is(Info::NOCLIP)) { return true; } // things $lv = $this->level; $bx0 = (int)floor(($left - self::MAXRADIUS - $lv->bmx) / Level::BLOCK); $bx1 = (int)floor(($right + self::MAXRADIUS - $lv->bmx) / Level::BLOCK); $by0 = (int)floor(($bottom - self::MAXRADIUS - $lv->bmy) / Level::BLOCK); $by1 = (int)floor(($top + self::MAXRADIUS - $lv->bmy) / Level::BLOCK); for ($by = $by0; $by <= $by1; $by++) { for ($bx = $bx0; $bx <= $bx1; $bx++) { foreach ($this->blockThings[$by * $lv->bmw + $bx] ?? [] as $th) { if (!$this->checkThing($mo, $th, $x, $y)) { return false; } } } } // lines foreach ($lv->linesInBox($left, $bottom, $right, $top) as $l) { if ($right <= $l->bbox[2] || $left >= $l->bbox[3] || $top <= $l->bbox[1] || $bottom >= $l->bbox[0]) { continue; } if ($this->boxOnLineSide($top, $bottom, $left, $right, $l) !== -1) { continue; } if ($l->backSector === null) { return false; } if (!$mo->is(Info::MISSILE)) { if ($l->flags & Line::BLOCKING) { return false; } if (($l->flags & Line::BLOCK_MONSTERS) && $mo->player === null) { return false; } } [$otop, $obottom, $olow] = $this->opening($l); if ($otop < $this->tmCeilingz) { $this->tmCeilingz = $otop; $this->ceilingLine = $l; } if ($obottom > $this->tmFloorz) { $this->tmFloorz = $obottom; } if ($olow < $this->tmDropoffz) { $this->tmDropoffz = $olow; } if ($l->special !== '') { $this->specHit[] = $l; } } return true; } /** PIT_CheckThing */ private function checkThing(Mobj $mo, Mobj $th, float $x, float $y): bool { if (!($th->flags & (Info::SOLID | Info::SPECIAL | Info::SHOOTABLE)) || $th === $mo || $th->removed) { return true; } $block = $th->radius + $mo->radius; if (abs($th->x - $x) >= $block || abs($th->y - $y) >= $block) { return true; } if ($mo->is(Info::MISSILE)) { if ($mo->z > $th->z + $th->height || $mo->z + $mo->height < $th->z) { return true; } if ($th === $mo->target) { return true; } // monsters of a kind do not hurt each other with projectiles if ($mo->target !== null && $mo->target->type === $th->type && $th->player === null) { return false; } if (!$th->is(Info::SHOOTABLE)) { return !$th->is(Info::SOLID); } $damage = (($this->random() % 8) + 1) * $mo->info['damage']; $this->damage($th, $mo, $mo->target, $damage); return false; } if ($th->is(Info::SPECIAL)) { $solid = $th->is(Info::SOLID); if ($mo->is(Info::PICKUP)) { $this->touchSpecial($th, $mo); } return !$solid; } return !$th->is(Info::SOLID); } /** P_TryMove */ public function tryMove(Mobj $mo, float $x, float $y): bool { if (!$this->checkPosition($mo, $x, $y)) { return false; } if (!$mo->is(Info::NOCLIP)) { if ($this->tmCeilingz - $this->tmFloorz < $mo->height) { return false; } if ($this->tmCeilingz - $mo->z < $mo->height) { return false; } if ($this->tmFloorz - $mo->z > self::MAXSTEP) { return false; } if (!$mo->is(Info::DROPOFF) && $this->tmFloorz - $this->tmDropoffz > self::MAXSTEP) { return false; } } $mo->floorz = $this->tmFloorz; $mo->ceilingz = $this->tmCeilingz; $mo->x = $x; $mo->y = $y; $this->link($mo); return true; } /** P_XYMovement */ private function xyMovement(Mobj $mo): void { $mo->momx = max(-self::MAXMOVE, min(self::MAXMOVE, $mo->momx)); $mo->momy = max(-self::MAXMOVE, min(self::MAXMOVE, $mo->momy)); $xmove = $mo->momx; $ymove = $mo->momy; do { if (abs($xmove) > self::MAXMOVE / 2 || abs($ymove) > self::MAXMOVE / 2) { $xmove /= 2; $ymove /= 2; $tx = $mo->x + $xmove; $ty = $mo->y + $ymove; } else { $tx = $mo->x + $xmove; $ty = $mo->y + $ymove; $xmove = $ymove = 0.0; } if (!$this->tryMove($mo, $tx, $ty)) { if ($mo->player !== null) { $this->slideMove($mo, $tx - $mo->x, $ty - $mo->y); $xmove = $ymove = 0.0; } elseif ($mo->is(Info::MISSILE)) { $l = $this->ceilingLine; if ($l !== null && $l->backSector !== null && $l->backSector->ceilPic === 'SKY' && $mo->z > $l->backSector->ceil) { $this->remove($mo); // into the sky: gone return; } $this->explodeMissile($mo); return; } else { $mo->momx = $mo->momy = 0.0; return; } } } while ($xmove != 0 || $ymove != 0); if ($mo->is(Info::MISSILE) || $mo->z > $mo->floorz) { return; } if (abs($mo->momx) < self::STOPSPEED && abs($mo->momy) < self::STOPSPEED) { $mo->momx = $mo->momy = 0.0; } else { $mo->momx *= self::FRICTION; $mo->momy *= self::FRICTION; } } /** Sliding along walls: the move split into its two axes (the original's fallback "stairstep"). */ private function slideMove(Mobj $mo, float $dx, float $dy): void { if ($dx != 0 && $this->tryMove($mo, $mo->x + $dx, $mo->y)) { $mo->momy = 0.0; return; } if ($dy != 0 && $this->tryMove($mo, $mo->x, $mo->y + $dy)) { $mo->momx = 0.0; return; } $mo->momx = $mo->momy = 0.0; } /** P_ZMovement */ private function zMovement(Mobj $mo): void { $p = $mo->player; if ($p !== null && $mo->z < $mo->floorz) { $p->viewheight -= $mo->floorz - $mo->z; $p->deltaviewheight = (Player::VIEWHEIGHT - $p->viewheight) / 8; } $mo->z += $mo->momz; if ($mo->z <= $mo->floorz) { if ($mo->momz < 0) { if ($p !== null && $mo->momz < -self::GRAVITY * 8) { $p->deltaviewheight = $mo->momz / 8; $this->sound('oof', $mo); } $mo->momz = 0.0; } $mo->z = $mo->floorz; if ($mo->is(Info::MISSILE)) { $this->explodeMissile($mo); return; } } elseif (!$mo->is(Info::NOGRAVITY)) { $mo->momz = $mo->momz == 0 ? -self::GRAVITY * 2 : $mo->momz - self::GRAVITY; } if ($mo->z + $mo->height > $mo->ceilingz) { if ($mo->momz > 0) { $mo->momz = 0.0; } $mo->z = $mo->ceilingz - $mo->height; if ($mo->is(Info::MISSILE)) { if ($mo->sector !== null && $mo->sector->ceilPic === 'SKY') { $this->remove($mo); return; } $this->explodeMissile($mo); } } } /** After a floor / ceiling moved: does $mo still fit? (P_ThingHeightClip) */ public function heightClip(Mobj $mo): bool { $onFloor = $mo->z == $mo->floorz; $this->checkPosition($mo, $mo->x, $mo->y); $mo->floorz = $this->tmFloorz; $mo->ceilingz = $this->tmCeilingz; if ($onFloor) { $mo->z = $mo->floorz; } elseif ($mo->z + $mo->height > $mo->ceilingz) { $mo->z = $mo->ceilingz - $mo->height; } return $mo->ceilingz - $mo->floorz >= $mo->height; } // -------------------------------------------------------------------------------------- traces /** * Everything a horizontal trace from (x, y) along $angle over $range crosses, nearest first: * ['frac' => 0..1, 'line' => Line] or ['frac', 'thing' => Mobj]. */ private function intercepts(float $x, float $y, float $angle, float $range, bool $things = true): array { $dx = cos($angle) * $range; $dy = sin($angle) * $range; $x2 = $x + $dx; $y2 = $y + $dy; $out = []; foreach ($this->level->linesInBox(min($x, $x2), min($y, $y2), max($x, $x2), max($y, $y2)) as $l) { $den = $dx * $l->dy - $dy * $l->dx; if (abs($den) < 1e-9) { continue; } $t = (($l->x1 - $x) * $l->dy - ($l->y1 - $y) * $l->dx) / $den; $u = (($l->x1 - $x) * $dy - ($l->y1 - $y) * $dx) / $den; if ($t >= 0 && $t <= 1 && $u >= 0 && $u <= 1) { $out[] = ['frac' => $t, 'line' => $l]; } } if ($things) { foreach ($this->mobjs as $th) { // the trace against the thing's box $r = $th->radius; $t0 = 0.0; $t1 = 1.0; foreach ([[$x, $dx, $th->x - $r, $th->x + $r], [$y, $dy, $th->y - $r, $th->y + $r]] as [$o, $d, $lo, $hi]) { if (abs($d) < 1e-9) { if ($o < $lo || $o > $hi) { continue 2; } continue; } $a = ($lo - $o) / $d; $b = ($hi - $o) / $d; if ($a > $b) { [$a, $b] = [$b, $a]; } $t0 = max($t0, $a); $t1 = min($t1, $b); if ($t0 > $t1) { continue 2; } } $out[] = ['frac' => $t0, 'thing' => $th]; } } usort($out, fn ($a, $b) => $a['frac'] <=> $b['frac']); return $out; } /** * P_AimLineAttack: the vertical slope to the first shootable thing along $angle (null: none). * @return array{0: float, 1: ?Mobj} slope, target */ public function aimLineAttack(Mobj $t1, float $angle, float $range): array { $shootz = $t1->z + $t1->height / 2 + 8; $topslope = 100 / 160; $bottomslope = -100 / 160; foreach ($this->intercepts($t1->x, $t1->y, $angle, $range) as $in) { $dist = $in['frac'] * $range; if (isset($in['line'])) { $l = $in['line']; if ($l->backSector === null) { break; } [$otop, $obottom] = $this->opening($l); if ($obottom >= $otop || $dist < 0.01) { break; } if ($l->frontSector->floor != $l->backSector->floor) { $bottomslope = max($bottomslope, ($obottom - $shootz) / $dist); } if ($l->frontSector->ceil != $l->backSector->ceil) { $topslope = min($topslope, ($otop - $shootz) / $dist); } if ($topslope <= $bottomslope) { break; } continue; } $th = $in['thing']; if ($th === $t1 || !$th->is(Info::SHOOTABLE) || $dist < 0.01) { continue; } $thTop = ($th->z + $th->height - $shootz) / $dist; if ($thTop < $bottomslope) { continue; } $thBottom = ($th->z - $shootz) / $dist; if ($thBottom > $topslope) { continue; } return [(min($thTop, $topslope) + max($thBottom, $bottomslope)) / 2, $th]; } return [0.0, null]; } /** P_LineAttack: a bullet (hitscan). */ public function lineAttack(Mobj $t1, float $angle, float $range, float $slope, int $damage): void { $shootz = $t1->z + $t1->height / 2 + 8; foreach ($this->intercepts($t1->x, $t1->y, $angle, $range) as $in) { $dist = $in['frac'] * $range; if (isset($in['line'])) { $l = $in['line']; if ($l->backSector !== null) { [$otop, $obottom] = $this->opening($l); $pass = true; if ($dist > 0.01) { if ($l->frontSector->floor != $l->backSector->floor && ($obottom - $shootz) / $dist > $slope) { $pass = false; } if ($l->frontSector->ceil != $l->backSector->ceil && ($otop - $shootz) / $dist < $slope) { $pass = false; } } if ($pass) { continue; } } // hit the wall, a bit in front of it $f = max(0.0, $in['frac'] - 4 / $range); $x = $t1->x + cos($angle) * $range * $f; $y = $t1->y + sin($angle) * $range * $f; $z = $shootz + $slope * $range * $f; $side = $l->side($t1->x, $t1->y) === 0 ? $l->frontSector : ($l->backSector ?? $l->frontSector); if ($side->ceilPic === 'SKY' && $z > $side->ceil) { return; } $other = $side === $l->frontSector ? $l->backSector : $l->frontSector; if ($other !== null && $other->ceilPic === 'SKY' && $z > $other->ceil) { return; } $this->spawnPuff($x, $y, $z); return; } $th = $in['thing']; if ($th === $t1 || !$th->is(Info::SHOOTABLE) || $dist < 0.01) { continue; } if (($th->z + $th->height - $shootz) / $dist < $slope || ($th->z - $shootz) / $dist > $slope) { continue; } $f = max(0.0, $in['frac'] - 10 / $range); $x = $t1->x + cos($angle) * $range * $f; $y = $t1->y + sin($angle) * $range * $f; $z = $shootz + $slope * $range * $f; if ($th->is(Info::NOBLOOD)) { $this->spawnPuff($x, $y, $z); } else { $this->spawnBlood($x, $y, $z, $damage); } $this->damage($th, $t1, $t1, $damage); return; } } /** P_CheckSight: is there a line of sight from t1's eyes to t2? */ public function checkSight(Mobj $t1, Mobj $t2): bool { $sz = $t1->z + $t1->height - $t1->height / 4; $dx = $t2->x - $t1->x; $dy = $t2->y - $t1->y; $total = sqrt($dx * $dx + $dy * $dy); if ($total < 1) { return true; } $topslope = ($t2->z + $t2->height - $sz) / $total; $bottomslope = ($t2->z - $sz) / $total; foreach ($this->level->linesInBox(min($t1->x, $t2->x), min($t1->y, $t2->y), max($t1->x, $t2->x), max($t1->y, $t2->y)) as $l) { $den = $dx * $l->dy - $dy * $l->dx; if (abs($den) < 1e-9) { continue; } $t = (($l->x1 - $t1->x) * $l->dy - ($l->y1 - $t1->y) * $l->dx) / $den; $u = (($l->x1 - $t1->x) * $dy - ($l->y1 - $t1->y) * $dx) / $den; if ($t < 0 || $t > 1 || $u < 0 || $u > 1) { continue; } if ($l->backSector === null) { return false; } [$otop, $obottom] = $this->opening($l); if ($obottom >= $otop) { return false; } $d = max(0.01, $t * $total); if ($l->frontSector->floor != $l->backSector->floor) { $bottomslope = max($bottomslope, ($obottom - $sz) / $d); } if ($l->frontSector->ceil != $l->backSector->ceil) { $topslope = min($topslope, ($otop - $sz) / $d); } if ($topslope <= $bottomslope) { return false; } } return true; } /** P_RadiusAttack: explosion damage around $spot. */ public function radiusAttack(Mobj $spot, ?Mobj $source, int $damage): void { foreach ($this->mobjs as $th) { if (!$th->is(Info::SHOOTABLE) || $th === $spot) { continue; } $dist = max(abs($th->x - $spot->x), abs($th->y - $spot->y)) - $th->radius; if ($dist < 0) { $dist = 0; } if ($dist >= $damage) { continue; } if ($this->checkSight($th, $spot) || $dist < 16) { $this->damage($th, $spot, $source, (int)($damage - $dist)); } } } /** P_NoiseAlert: the sound of a shot wakes the monsters of every sector it reaches. */ public function noiseAlert(Mobj $target): void { $this->validCount++; $this->recursiveSound($target->sector, 0, $target); } private function recursiveSound(Sector $sec, int $blocks, Mobj $target): void { if ($sec->soundValid === $this->validCount && $sec->soundTraversed <= $blocks + 1) { return; } $sec->soundValid = $this->validCount; $sec->soundTraversed = $blocks + 1; $sec->soundTarget = $target; foreach ($sec->lines as $l) { if ($l->backSector === null) { continue; } [$otop, $obottom] = $this->opening($l); if ($otop - $obottom <= 0) { continue; } $other = $l->frontSector === $sec ? $l->backSector : $l->frontSector; if ($l->flags & Line::SOUND_BLOCK) { if ($blocks === 0) { $this->recursiveSound($other, 1, $target); } } else { $this->recursiveSound($other, $blocks, $target); } } } /** P_UseLines: the player presses "use" on what is in front of him. */ private function useLines(): void { $mo = $this->player->mo; foreach ($this->intercepts($mo->x, $mo->y, $mo->angle, self::USERANGE, false) as $in) { $l = $in['line']; if ($l->special === '') { [$otop, $obottom] = $this->opening($l); if ($l->backSector === null || $otop - $obottom <= 0) { $this->sound('noway', $mo); return; } continue; } if ($l->side($mo->x, $mo->y) === 1) { return; } $this->useSpecialLine($mo, $l); return; } }}