Entrar
PT
games/doom-jumphp / src / Game / Physics.php · commit 8e1cb9f
<?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;        }    }}