Accedi
IT
games/sorcery / src / Game / Game.php · commit 613f5fb
<?phpnamespace App\Game;use App\Data\Assets;use App\Data\Extra;use App\Data\Level;/** * The rules of Sorcery+ (a port of modern/src/game*.cpp). step() is called 50 times per second; the main logic * runs at 37.5 ticks per second (3 ticks every 4 steps), the door animation at 50 steps per second. * All the arithmetic is 8 bit where the original is (see u8/within in Values). */final class Game{    // transition stages    private const WALK_A = 0;    private const TICK_A = 1;    private const WALK_B = 2;    private const WALK_D = 3;    private const WALK_E = 4;    private const TICK_E = 5;    private const PHASE_PLAYING = 0;    private const PHASE_DOOR = 1;    private const PHASE_ENDING = 2;    private const PHASE_VICTORY = 3;    private const PHASE_OVER = 4;    public Assets $A;    public Level $lvl;    private bool $adventure = false;    private bool $specials = true;    private bool $extrasOn = true;    private bool $darkEnabled = false;    private int $lampTicks = 0;    private int $teleCooldown = 0;    private bool $labyrinth = false;    private float $seed;    /** @var array<int, RoomState> */    private array $saved = [];    private RoomState $rs;    private int $room = 0;    /** @var int[] */    private array $map;    /** @var int[] */    private array $cls;    /** @var Entity[] 9 slots (the last one holds the room's magic item) */    private array $slots;    /** @var Particle[] */    private array $bolts;    /** @var Particle[] */    private array $debris;    /** @var Entity[] 8 boss icons */    private array $icons;    public Player $player;    private Player $prevPlayer;    /** @var Entity[] */    private array $prevEnts;    /** @var Entity[] */    private array $prevSlots;    private int $keys = 0;    private int $input = 0;    private bool $actionLatched = false;    private int $phase = self::PHASE_PLAYING;    private bool $paused = false;    private bool $completed = false;    private bool $worldTickPending = false;    private bool $demo = false;    /** @var int[] */    private array $script = [];    private int $scriptPos = 0;    private int $scriptCount = 1;    private int $scriptBits = 0;    private int $endFlags = 0;    public int $score = 0;    private int $magicItems = 0;    private int $rewards = 0;    private int $ticks = 0;    private int $tickAcc = 0;    private int $barSteps = 0;    private int $barCountdown = 0;    private int $animTick = 4;    private int $animStep = 0;    private int $bossGroup = 0;    private int $shotTimer = 0;    private int $freeRun = 0;    private int $walkBase = 0x20;    /** @var string[] sounds queued for the front end */    private array $sounds = [];    /** @var array<int, array{0: string, 1: int, 2: int}> events queued for the front end */    private array $events = [];    public Stats $stats;    /** @var bool[] */    private array $visited = [];    private bool $easy = false;    private bool $noTimer = false;    private int $stage = self::WALK_A;    public bool $trVisible = false;    public int $trY = 0;    public int $trX = 0;    public int $trFrame = 0;    private int $trDest = 0;    private int $trFlags = 0;    private int $trDir = 0;    private int $endPhase = 0;    private int $endTimer = 0;    private int $endReleased = 0;    private int $victoryTimer = 0;    /** @var Overlay[] 4 sorcerers + 1 */    private array $vSprites;    private int $vStage = 0;    private int $vPhase = 0;    private int $vIndex = 0;    private bool $bare = false;    private bool $initiation = false;    private string $override1 = '';    private string $override2 = '';    public function __construct(Assets $assets, int|float $seed = 1)    {        $this->A = $assets;        $this->lvl = $assets->levels[0];        $this->seed = $seed !== 0 ? fmod((float)$seed, 4294967296.0) : 1.0;        $this->map = array_fill(0, Values::MAP_W * Values::MAP_H, 0);        $this->cls = array_fill(0, Values::MAP_W * Values::MAP_H, 0);        $this->rs = new RoomState();        $this->slots = [];        for ($i = 0; $i < 9; $i++) {            $this->slots[] = new Entity();        }        $this->bolts = [];        $this->debris = [];        for ($i = 0; $i < 8; $i++) {            $b = new Particle();            $d = new Particle();            $b->dy = $d->dy = $assets->boltVelocity[$i][0];            $b->dx = $d->dx = $assets->boltVelocity[$i][1];            $this->bolts[] = $b;            $this->debris[] = $d;        }        $this->icons = [];        for ($i = 0; $i < 8; $i++) {            $this->icons[] = new Entity();        }        $this->player = new Player();        $this->player->energy = 99;        $this->player->hurt = 0xFF;        $this->prevPlayer = new Player();        $this->prevEnts = [new Entity(), new Entity()];        $this->prevSlots = [];        for ($i = 0; $i < 9; $i++) {            $this->prevSlots[] = new Entity();        }        $this->vSprites = [];        for ($i = 0; $i < 5; $i++) {            $this->vSprites[] = new Overlay();        }        $this->stats = new Stats();    }    public function setLevel(Level $level, bool $adventure): void    {        $this->lvl = $level;        $this->adventure = $adventure;    }    public function roomCount(): int    {        return count($this->lvl->rooms);    }    public function darkEnabled(): bool    {        return $this->darkEnabled;    }    public function lampTicks(): int    {        return $this->lampTicks;    }    public function lampFactor(): float    {        if (!$this->darkEnabled) {            return 1.0;        }        if ($this->lampTicks <= 0) {            return 0.0;        }        $FADE = 750;        return $this->lampTicks >= $FADE ? 1.0 : 0.3 + 0.7 * $this->lampTicks / $FADE;    }    // ---- helpers ----    /** xorshift32 with float math: the same stream on 32-bit (browser) and 64-bit PHP. */    private static function xor32(float $a, float $b): float    {        $ah = (int)($a / 65536.0) & 0xFFFF;        $al = (int)fmod($a, 65536.0);        $bh = (int)($b / 65536.0) & 0xFFFF;        $bl = (int)fmod($b, 65536.0);        return ($ah ^ $bh) * 65536.0 + ($al ^ $bl);    }    private function rnd(): int    {        $s = $this->seed;        $s = self::xor32($s, fmod($s * 8192.0, 4294967296.0));      // s ^= s << 13        $s = self::xor32($s, (int)($s / 131072.0));                 // s ^= s >> 17        $s = self::xor32($s, fmod($s * 32.0, 4294967296.0));        // s ^= s << 5        $this->seed = $s;        return (int)($s / 256.0) & 0xFF;                            // (s >> 8) & 0xFF    }    private function addScore(int $points): void    {        if ($this->initiation) {            return;        }        $this->score = ($this->score + $points) % 1000000;    }    private function sound(string $s): void    {        $this->sounds[] = $s;    }    private function event(string $kind, int $y, int $x): void    {        $this->events[] = [$kind, $y & 0xFF, $x & 0xFF];    }    private function cellIndex(int $y, int $x): int    {        return ($y >> 3) * Values::MAP_W + ($x >> 2);    }    private function cellAt(int $idx): int    {        return $idx >= 0 && $idx < Values::MAP_W * Values::MAP_H ? $this->cls[$idx] : Values::C_EMPTY;    }    // ---- room records -> RoomState ----    private function parseRecord(array $r): RoomState    {        $s = new RoomState();        $s->title = $r[0];        for ($e = 0; $e < 2; $e++) {            $o = 2 + 10 * $e;            $n = $s->ents[$e];            $n->kind = $r[$o] === Values::NONE ? Values::K_NONE : $r[$o] & 0x7F;            $n->y = $r[$o + 1];            $n->x = $r[$o + 2];            $n->frame = $r[$o + 3];            $n->timer = $r[$o + 4];            $n->rows = $r[$o + 5];            $n->flags = $r[$o + 6];            $n->a7 = $r[$o + 7];            $n->a8 = $r[$o + 8];            $n->next = $r[$o + 9];        }        for ($d = 0; $d < 4; $d++) {            $o = 0x16 + 4 * $d;            $s->doors[$d]->set($r[$o], $r[$o + 1], $r[$o + 2], $r[$o + 3]);        }        $s->heart->set($r[0x26], $r[0x27], $r[0x28]);        $s->hazard->set($r[0x29], $r[0x2A], $r[0x2B]);        $s->lockA->set($r[0x2C], $r[0x2D], $r[0x2E]);        $s->lockB->set($r[0x2F], $r[0x30], $r[0x31]);        $s->pickupY = $r[0x32];        $s->pickupX = $r[0x33];        for ($i = 0; $i < 4; $i++) {            $s->floor[$i]->set($r[0x34 + 3 * $i], $r[0x35 + 3 * $i], $r[0x36 + 3 * $i]);        }        return $s;    }    public function startDemo(array $script): void    {        $this->script = $script;        $this->scriptPos = 3;        $this->scriptCount = 1;        $this->scriptBits = 0;        $this->newGame($script[0] ?? 0, $script[1] ?? 0, $script[2] ?? 0);        $this->demo = true;    }    public function newGame(int $forcedRoom = -1, int $forcedY = 0, int $forcedX = 0, bool $initiation = false): void    {        $this->saved = [];        $this->specials = $this->lvl->original;        $this->extrasOn = !$this->lvl->original || $this->adventure;        $this->darkEnabled = $this->adventure || $this->lvl->dark;        $this->lampTicks = 0;        $this->teleCooldown = 0;        foreach ($this->lvl->rooms as $r) {            $this->saved[] = $this->parseRecord($r->rec);        }        if ($this->extrasOn) {            foreach ($this->lvl->extras as $e) {                $r = $this->saved[$e->room] ?? null;                if ($r !== null && $r->nExtras < 4) {                    $r->extras[$r->nExtras++] = new RoomExtra(                        $e->type === Extra::LANTERN ? RoomExtra::LANTERN : RoomExtra::TELEPORTER,                        $e->room, $e->y, $e->x, $e->toRoom, $e->toY, $e->toX, false                    );                }            }        }        $this->player = new Player();        $this->player->energy = 99;        $this->player->hurt = 0xFF;        $this->labyrinth = false;        $this->visited = array_fill(0, Values::ROOM_COUNT, false);        $this->stats = new Stats();        $this->demo = false;        $this->initiation = $initiation;        $this->bare = false;        $this->override1 = '';        $this->override2 = '';        $this->vStage = 0;        $this->vPhase = 0;        $this->vIndex = 0;        $this->score = 0;        $this->ticks = 0;        $this->magicItems = 0;        $this->rewards = 0;        $this->completed = false;        $this->endFlags = 0;        $this->phase = self::PHASE_PLAYING;        $this->barSteps = 0;        $this->barCountdown = 0x12;        $this->tickAcc = 0;        $this->shotTimer = 0;        $this->actionLatched = false;        $this->trVisible = false;        $this->worldTickPending = false;        $this->victoryTimer = 0;        foreach ($this->bolts as $p) {            $p->y = Values::NONE;        }        foreach ($this->debris as $p) {            $p->y = Values::NONE;        }        foreach ($this->icons as $e) {            $e->clear();        }        foreach ($this->vSprites as $o) {            $o->clear();        }        // the player starts in one of five places; two rooms swap their cauldrons for each place        $idx = $this->rnd() & 7;        if ($idx >= 5) {            $idx -= 5;        }        $sp = $this->A->startPlaces[$idx];        if (!$this->lvl->original) {            $sp = [$this->lvl->startRoom, $this->lvl->startY, $this->lvl->startX];            $idx = 0;        }        for ($k = 0; $k < 2 && $this->lvl->original; $k++) {            $r = $this->A->startSwapRooms[$idx][$k];            if ($r >= Values::ROOM_COUNT) {                continue;            }            [$this->saved[$r]->heart->y, $this->saved[$r]->hazard->y] = [$this->saved[$r]->hazard->y, $this->saved[$r]->heart->y];            [$this->saved[$r]->heart->x, $this->saved[$r]->hazard->x] = [$this->saved[$r]->hazard->x, $this->saved[$r]->heart->x];        }        $this->player->state = Values::P_WAIT;        $this->player->anim = 0x14;        $this->player->frame = 0x4F;        $this->player->y = $sp[1];        $this->player->x = $sp[2];        $this->player->visible = false;        $room = $sp[0];        if ($forcedRoom >= 0 && $forcedRoom < $this->roomCount()) {            $room = $forcedRoom;            $this->player->y = Values::u8($forcedY);            $this->player->x = Values::u8($forcedX);        }        if ($initiation) {            $room = 0x3A;            $this->player->y = 0x10;            $this->player->x = 0x58;            $this->completed = true;        }        $this->enterRoom($room, true);    }    // ---- rooms ----    private function stampBlock(int $y, int $x, array $block): void    {        $base = $this->cellIndex($y, $x);        for ($r = 0; $r < 3; $r++) {            for ($c = 0; $c < 3; $c++) {                $i = $base + $r * Values::MAP_W + $c;                if ($i >= 0 && $i < Values::MAP_W * Values::MAP_H) {                    $this->cls[$i] = $block[$r * 3 + $c] ? Values::C_SOLID : Values::C_EMPTY;                }            }        }    }    private function buildMap(): void    {        $def = $this->lvl->rooms[$this->room];        $this->map = $def->map;        // room 0x3D hides a passage: without the right item one kind of decoration tile is a wall        if ($this->specials && $this->room === Values::ROOM_LABYRINTH && !$this->labyrinth) {            if ($this->player->held === 0x2B) {                $this->labyrinth = true;            } else {                foreach ($this->map as $i => $t) {                    if ($t === 0xF7) {                        $this->map[$i] = 0x01;                    }                }            }        }        $this->cls = array_fill(0, Values::MAP_W * Values::MAP_H, 0);        for ($i = 0; $i < Values::MAP_W * Values::MAP_H; $i++) {            $t = $this->map[$i];            if ($t === 0 || $t > 0xDC) {                continue;            }            $pair = 0;            if ($t === 0x1E || $t === 0x1F) {                $pair = Values::C_RIVER;            } elseif ($t === 0x46 || $t === 0x47) {                $pair = Values::C_ANIM;            } elseif ($t === 0x6E || $t === 0x6F) {                $pair = Values::C_FIRE;            }            if ($pair) {                $this->cls[$i] = $pair;                if ($i + 1 < Values::MAP_W * Values::MAP_H) {                    $this->cls[++$i] = $pair;                }            } else {                $this->cls[$i] = Values::C_SOLID;            }        }    }    private function enterRoom(int $number, bool $fresh = false): void    {        $this->room = $number;        if (!($this->visited[$number] ?? false)) {            $this->visited[$number] = true;            $this->stats->rooms++;        }        $this->rs = RoomState::from($this->saved[$number]);        $this->buildMap();        // doors and objects are solid blocks of 3x3 cells        foreach ($this->rs->doors as $d) {            if ($d->y !== Values::NONE) {                $this->stampBlock($d->y, $d->x, $this->A->doorBlock[1]);            }        }        if ($this->specials && $this->room === Values::ROOM_LABYRINTH) {            for ($i = 0; $i < 3; $i++) {                $this->stampBlock(0x30 + 0x20 * $i, 0, $this->A->doorBlock[1]);            }        }        if ($this->rs->heart->y !== Values::NONE) {            $this->stampBlock($this->rs->heart->y, $this->rs->heart->x, $this->A->doorBlock[0]);        }        if ($this->rs->hazard->y !== Values::NONE) {            $this->stampBlock($this->rs->hazard->y, $this->rs->hazard->x, $this->A->doorBlock[0]);        }        if ($this->rs->lockA->y !== Values::NONE) {            $this->stampBlock($this->rs->lockA->y, $this->rs->lockA->x, $this->A->doorBlock[2]);        }        if ($this->rs->lockB->y !== Values::NONE) {            $this->stampBlock($this->rs->lockB->y, $this->rs->lockB->x, $this->A->stampPlain);        }        $this->animTick = 0;        $this->animStep = 3;        foreach ($this->slots as $e) {            $e->clear();        }        foreach ($this->rs->ents as $e) {            $e->drawn = $e->used();        }        foreach ($this->bolts as $p) {            $p->y = Values::NONE;        }        foreach ($this->debris as $p) {            $p->y = Values::NONE;        }        if ($this->rs->pickupY !== Values::NONE) {            $m = $this->slots[8];            $m->clear();            $m->kind = Values::K_MAGIC_ITEM;            $m->y = $this->rs->pickupY;            $m->x = $this->rs->pickupX;            $m->frame = 0x54;            $m->timer = 0;            $m->rows = 0x18;            $m->drawn = true;        }        foreach ($this->icons as $e) {            $e->clear();        }        if ($this->specials && $this->room === Values::ROOM_BOSS) {            for ($i = 0; $i < $this->magicItems && $i < 8; $i++) {                $t = $this->A->bossRoomItems[$i];                $e = $this->icons[$i];                $e->clear();                $e->kind = 0;                $e->y = $t[1];                $e->x = $t[2];                $e->frame = $t[3];                $e->rows = $t[5];                $e->drawn = true;            }        }        if ($this->specials && $this->room === Values::ROOM_LABYRINTH) {            $this->updateFloatingDoor();        }        $this->snapshot();    }    private function leaveRoom(): void    {        for ($i = 0; $i < 8; $i++) {            $this->settle($this->slots[$i]);        }        foreach ($this->rs->ents as $e) {            if ($e->kind === Values::K_EXPLODING || $e->kind === Values::K_REMAINS) {                $e->clear();            }        }        if ($this->specials && $this->room === Values::ROOM_LABYRINTH) {            $this->rs->doors[0]->y = 0x10;        }        $this->saved[$this->room] = RoomState::from($this->rs);    }    private function updateFloatingDoor(): void    {        if (!($this->specials && $this->room === Values::ROOM_LABYRINTH)) {            return;        }        $this->rs->doors[0]->y = Values::u8((($this->freeRun & 0x30) << 1) | 0x10);    }    public function animFrame(int $group): int    {        return $group === 2 ? $this->animStep & 1 : $this->animStep & 3;    }    public function line1(): string    {        return $this->bare ? $this->override1 : $this->A->roomTitle($this->rs->title);    }    public function line2(): string    {        return $this->bare ? $this->override2 : $this->A->itemName($this->player->held, $this->completed);    }    public function doorStampVisible(int $i): bool    {        $d = $this->rs->doors[$i];        return !($this->trVisible && $this->trY === $d->y && $this->trX === $d->x);    }    // ---- collision queries ----    private function solidRun(int $first, int $step, bool $extra): bool    {        for ($i = 0; $i < 3; $i++) {            if ($this->cellAt($first + $i * $step) & Values::C_SOLID) {                return true;            }        }        return $extra && ($this->cellAt($first + 3 * $step) & Values::C_SOLID) !== 0;    }    private function probeLeft(Entity $e): bool    {        if ($e->x === 0) {            return true;        }        if ($e->x & 3) {            return false;        }        return $this->solidRun($this->cellIndex($e->y, $e->x) - 1, Values::MAP_W, ($e->y & 7) !== 0);    }    private function probeRight(Entity $e): bool    {        if ($e->x === 0x94) {            return true;        }        if ($e->x & 3) {            return false;        }        return $this->solidRun($this->cellIndex($e->y, $e->x) + 3, Values::MAP_W, ($e->y & 7) !== 0);    }    private function probeUp(Entity $e): bool    {        if ($e->y === 0) {            return true;        }        if ($e->y & 7) {            return false;        }        return $this->solidRun($this->cellIndex($e->y, $e->x) - Values::MAP_W, 1, ($e->x & 3) !== 0);    }    private function probeDown(Entity $e): bool    {        if ($e->y === 0x78) {            return true;        }        if ($e->y & 7) {            return false;        }        return $this->solidRun($this->cellIndex($e->y, $e->x) + 3 * Values::MAP_W, 1, ($e->x & 3) !== 0);    }    private function playerBlocked(int $dir): bool    {        $e = new Entity();        $e->y = $this->player->y;        $e->x = $this->player->x;        $test = function (Entity $q) use ($dir): bool {            return match ($dir) {                0 => $this->probeUp($q),                1 => $this->probeLeft($q),                2 => $this->probeRight($q),                default => $this->probeDown($q),            };        };        if ($test($e)) {            return true;        }        match ($dir) {            0 => $e->y--,            1 => $e->x--,            2 => $e->x++,            default => $e->y++,        };        return $test($e);    }    private function playerFeetOnRiver(): bool    {        $y = $this->player->y;        if ($y === 0x78) {            return false;        }        if ($y & 7) {            $y++;            if ($y & 7) {                return false;            }        }        $base = $this->cellIndex($y, $this->player->x) + 3 * Values::MAP_W;        $n = ($this->player->x & 3) ? 4 : 3;        for ($i = 0; $i < $n; $i++) {            if ($this->cellAt($base + $i) !== Values::C_RIVER) {                return false;            }        }        return true;    }    // ---- time ----    public function setInput(int $keys): void    {        $this->keys = $keys;        if (!($keys & Values::IN_ACTION)) {            $this->actionLatched = false;        }    }    public function step(): void    {        if ($this->paused) {            return;        }        switch ($this->phase) {            case self::PHASE_PLAYING:                if (!$this->demo && !$this->noTimer && $this->barSteps < Values::BAR_TOTAL && --$this->barCountdown < 0) {                    $this->barCountdown = $this->easy ? 0x2F : 0x17;                    if (++$this->barSteps >= Values::BAR_TOTAL) {                        $this->endFlags |= Values::END_NO_TIME | Values::END_FINISHED;                    }                }                $this->tickAcc += 3;                if ($this->tickAcc >= 4) {                    $this->tickAcc -= 4;                    $this->tick();                }                break;            case self::PHASE_DOOR:                $this->stepTransition();                break;            case self::PHASE_ENDING:                $this->tickAcc += 3;                if ($this->tickAcc >= 4) {                    $this->tickAcc -= 4;                    $this->stepEnding();                }                break;            case self::PHASE_VICTORY:                $this->tickAcc += 3;                if ($this->tickAcc >= 4) {                    $this->tickAcc -= 4;                    $this->stepVictory();                }                break;        }    }    private function tick(): void    {        $this->ticks++;        $this->snapshot();        $this->input = 0;        if (($this->player->state & 15) === Values::P_FLY) {            if (!$this->demo) {                $this->input = $this->keys;            } elseif (--$this->scriptCount !== 0) {                $this->input = $this->scriptBits;            } else {                $this->scriptCount = $this->scriptByte();                $this->scriptBits = $this->scriptByte();                $this->input = $this->scriptBits;                if ($this->scriptCount === 0) {                    $this->endFlags |= Values::END_FINISHED;                    $this->phase = self::PHASE_OVER;                    return;                }            }        }        if ($this->actionLatched && !$this->demo) {            $this->input &= ~Values::IN_ACTION;        }        $this->updatePlayer();        if ($this->phase === self::PHASE_DOOR) {            $this->worldTickPending = true;            return;        }        $this->tickWorld();    }    private function tickWorld(): void    {        $this->playerAction();        $this->updateEntities();        $this->moveParticles($this->bolts);        $this->moveParticles($this->debris);        $this->animateTiles();        $this->lockedDoors();        $this->cauldrons();        $this->extrasTick();        $this->bossRoom();        $this->endingCheck();        if ($this->phase !== self::PHASE_PLAYING) {            return;        }        if (($this->specials && $this->rewards === 4 && !$this->slots[0]->used())            || ($this->lvl->goal > 0 && $this->magicItems >= $this->lvl->goal && !$this->slots[8]->used())) {            $this->phase = self::PHASE_VICTORY;            $this->vStage = 0;            return;        }        $this->freeRun = Values::u8($this->freeRun + 0x29);        if ($this->endFlags & Values::END_FINISHED) {            $this->phase = self::PHASE_OVER;        }    }    private function scriptByte(): int    {        return $this->scriptPos < count($this->script) ? $this->script[$this->scriptPos++] : 0;    }    private function snapshot(): void    {        $p = new Player();        foreach (['state', 'y', 'x', 'frame', 'anim', 'rows', 'aux', 'visible', 'held', 'energy', 'hurt'] as $f) {            $p->$f = $this->player->$f;        }        $p->items = $this->player->items;        $this->prevPlayer = $p;        foreach ($this->rs->ents as $i => $e) {            $this->prevEnts[$i]->copyFrom($e);        }        foreach ($this->slots as $i => $e) {            $this->prevSlots[$i]->copyFrom($e);        }    }    // ---- state for the front end ----    public function phase(): int    {        return $this->phase;    }    public function phaseOver(): bool    {        return $this->phase === self::PHASE_OVER;    }    public function endFlags(): int    {        return $this->endFlags;    }    public function ticks(): int    {        return $this->ticks;    }    public function demo(): bool    {        return $this->demo;    }    public function room(): int    {        return $this->room;    }    public function roomState(): RoomState    {        return $this->rs;    }    /** @return Entity[] */    public function slots(): array    {        return $this->slots;    }    public function cellClass(int $i): int    {        return $this->cls[$i];    }    public function tileId(int $i): int    {        return $this->map[$i];    }    public function animStepCount(): int    {        return $this->animStep;    }    /** @return Particle[] */    public function bolts(): array    {        return $this->bolts;    }    /** @return Particle[] */    public function debris(): array    {        return $this->debris;    }    public function barSteps(): int    {        return $this->barSteps;    }    public function secondQuest(): bool    {        return $this->completed;    }    /** @return Entity[] */    public function bossIcons(): array    {        return $this->icons;    }    /** @return Overlay[] */    public function victorySprites(): array    {        return $this->vSprites;    }    public function fieldWhite(): bool    {        return $this->victoryTimer > 0;    }    public function bareScene(): bool    {        return $this->bare;    }    public function initiation(): bool    {        return $this->initiation;    }    public function rescued(): int    {        return $this->magicItems;    }    public function visited(int $room): bool    {        return $room >= 0 && $room < Values::ROOM_COUNT && ($this->visited[$room] ?? false);    }    /** @return RoomState[] */    public function savedRooms(): array    {        return $this->saved;    }    public function savedRoom(int $room): ?RoomState    {        return ($room >= 0 && $room < count($this->saved)) ? $this->saved[$room] : null;    }    /** @return string[] */    public function takeSounds(): array    {        $s = $this->sounds;        $this->sounds = [];        return $s;    }    /** @return array<int, array{0: string, 1: int, 2: int}> */    public function takeEvents(): array    {        $e = $this->events;        $this->events = [];        return $e;    }    public function setPaused(bool $p): void    {        $this->paused = $p;    }    public function paused(): bool    {        return $this->paused;    }    public function setAssist(bool $easy, bool $noTimer): void    {        $this->easy = $easy;        $this->noTimer = $noTimer;    }    public function assisted(): bool    {        return $this->easy || $this->noTimer;    }    // ---- test aids ----    public function debugMagicItems(int $n): void    {        $this->magicItems = $n;    }    public function debugRewards(int $n): void    {        $this->rewards = $n;    }    public function debugHeld(int $item): void    {        $this->player->held = $item;    }    // =================================================================================================================    // The player (game_player.cpp)    // =================================================================================================================    private function hurtOnePoint(): void    {        if ($this->player->energy === 0) {            return;        }        $this->player->energy = max(0, $this->player->energy - 1);        $this->sound(Sfx::COUNTER);        $this->event(Event::HURT, $this->player->y, $this->player->x);        $this->stats->hurts++;    }    private function updatePlayer(): void    {        $p = $this->player;        switch ($p->state & 0x0F) {            case Values::P_WAIT:            case Values::P_WAIT2:                if (--$p->anim === 0) {                    $p->state++;                }                break;            case Values::P_MATERIALISE:                if ((++$p->anim & 3) !== 0) {                    break;                }                if (--$p->frame === 0x4B) {                    $p->frame = 0x20;                    $p->state++;                }                $p->visible = true;                break;            case Values::P_FLY:                $this->playerFly();                break;            case Values::P_FALL:                if (!$this->playerBlocked(3)) {                    $p->y += 2;                } else {                    $p->frame = 0x4C;                    $p->anim = 0;                    $p->state++;                    $this->sound(Sfx::HIT);                }                $p->visible = true;                break;            case Values::P_DYING:                if ((++$p->anim & 3) !== 0) {                    break;                }                if (++$p->frame !== 0x50) {                    break;                }                $this->endFlags |= Values::END_FINISHED | Values::END_NO_ENERGY;                $this->event(Event::DEATH, $p->y, $p->x);                $p->visible = false;                break;            case Values::P_SPLASH:                if ((++$p->anim & 3) !== 0) {                    break;                }                if (!($p->state & 0x10)) {                    if (++$p->frame !== 0x4C) {                        break;                    }                    $p->state |= 0x10;                }                if (--$p->frame !== 0x47) {                    break;                }                $p->state &= ~0x10;                $p->frame = 0x5E;                $p->state++;                $p->aux = 0xE0;                $p->y += 2;                break;            case Values::P_SINK:                if ((++$p->anim & 3) !== 0) {                    break;                }                $p->frame ^= 1;                if (++$p->aux !== 0) {                    break;                }                $p->state &= 0xF0;                $this->endFlags |= Values::END_FINISHED | Values::END_DROWNED;                $p->visible = false;                break;        }    }    private function tryDoor(Door $d, int $dx, int $playerFrame, int $doorFrame): bool    {        if ($d->y === Values::NONE || $this->player->y !== $d->y || Values::u8($this->player->x + $dx) !== $d->x) {            return false;        }        if (!$this->lockOpen($d)) {            return false;        }        $this->beginTransition($d, $playerFrame, $doorFrame);        return true;    }    private function playerFly(): void    {        $p = $this->player;        $in = $this->input;        if (($in & Values::IN_UP) && !$this->playerBlocked(0)) {            $p->y -= 2;        }        if ($in & Values::IN_LEFT) {            if (!$this->playerBlocked(1)) {                $p->x -= 2;            } elseif ($this->tryDoor($this->rs->doors[0], -12, 0x1C, 0x31) || $this->tryDoor($this->rs->doors[1], -12, 0x1C, 0x31)) {                return;            }        }        if ($in & Values::IN_RIGHT) {            if (!$this->playerBlocked(2)) {                $p->x += 2;            } elseif ($this->tryDoor($this->rs->doors[2], 12, 0x24, 0x35) || $this->tryDoor($this->rs->doors[3], 12, 0x24, 0x35)) {                return;            }        }        if (!($in & Values::IN_UP)) {            if (!$this->playerBlocked(3)) {                $p->y += 2;            } elseif ($this->playerFeetOnRiver()) {                $p->frame = 0x48;                $p->anim = 0;                $p->state = ($p->state & 0x80) | Values::P_SPLASH;                $this->sound(Sfx::ENEMY_EXPLODE);                $this->event(Event::SPLASH, $p->y + 20, $p->x);                $p->visible = true;                return;            }        }        $this->playerAnimateAndHurt();    }    private function playerAnimateAndHurt(): void    {        $p = $this->player;        if ($p->energy === 0) {            $p->frame = 0x20;            $p->state++;            $p->anim = 0;            $p->visible = true;            return;        }        $p->anim++;        $in = $this->input;        $base = Values::u8(0x20 - ($in & Values::IN_LEFT) + (($in >> 1) & 4));        $p->frame = ($p->anim >> 2 & 3) | $base;        $p->visible = true;        if ($p->x >= 0x0E) {            $this->updateFloatingDoor();        }        // hurting cells below the sprite        $row = $this->cellIndex($p->y, $p->x) + (($p->y & 7) ? 3 * Values::MAP_W : 2 * Values::MAP_W);        $n = ($p->x & 3) ? 4 : 3;        $step = $this->easy ? (int)(Values::ENERGY_STEP / 2) : Values::ENERGY_STEP;        for ($i = 0; $i < $n; $i++) {            if ($this->cellAt($row + $i) === Values::C_FIRE) {                $frac = $p->hurt;                $p->hurt = Values::u8($frac - $step);                if ($frac < $step) {                    $this->hurtOnePoint();                }                return;            }        }        foreach ($this->debris as $d) {            if (!$d->used) {                continue;            }            if (!Values::within($d->y, $p->y, Values::HIT_Y) || !Values::within($d->x, $p->x, Values::HIT_X)) {                continue;            }            $frac = $p->hurt;            $p->hurt = Values::u8($frac - $step);            if ($frac < $step) {                $this->hurtOnePoint();            }            return;        }    }    private function freeSlot(): int    {        for ($i = 0; $i < 8; $i++) {            if (!$this->slots[$i]->used()) {                return $i;            }        }        return -1;    }    private function consumeAction(): void    {        $this->actionLatched = true;        $this->input &= ~Values::IN_ACTION;    }    private function playerAction(): void    {        if (!($this->input & Values::IN_ACTION)) {            return;        }        $p = $this->player;        // 1. swap with a thing lying on the floor close to the player        foreach ($this->rs->floor as $f) {            if ($f->item === Values::NONE) {                continue;            }            if (!Values::within($f->y, $p->y, Values::HIT_Y) || !Values::within($f->x, $p->x, Values::HIT_X)) {                continue;            }            if ($this->specials && $this->room === Values::ROOM_THRONE) {                continue;            }            $this->consumeAction();            $firstTime = !($f->item & 0x80);            $picked = $f->item & 0x7F;            $f->item = Values::u8($p->held | 0x80);            if ($firstTime) {                $this->addScore(50);            }            $this->sound(Sfx::BLIP);            $this->event(Event::PICKUP, $f->y, $f->x);            $this->stats->pickups++;            $p->held = $picked;            return;        }        // 2. use the carried item        foreach ($this->A->itemRules as $r) {            if ($r[0] !== $p->held) {                continue;            }            if ($r[2] !== $this->room && $r[2] !== Values::NONE) {                continue;            }            if ($r[2] !== Values::NONE && !$this->specials) {                continue;            }            $s = $this->freeSlot();            if ($s < 0) {                return;            }            $n = $this->slots[$s];            $n->clear();            $n->frame = $r[0];            $n->kind = $r[1] & 0x7F;            $n->next = $r[3];            $n->a7 = $r[4];            $n->a8 = $r[5];            $n->timer = 0;            $n->rows = 0x18;            $n->flags = 0;            $n->y = $p->y;            $n->x = $p->x;            $p->held = Values::NONE;            $this->consumeAction();            return;        }    }    // ---- doors ----    private function lockOpen(Door $d): bool    {        $lock = $d->flags & 0x3F;        if ($lock === 0) {            return true;        }        if ($lock > 5) {            return false;        }        return $this->player->held === Values::LOCK_KEY[$lock - 1];    }    private function beginTransition(Door $d, int $playerFrame, int $doorFrame): void    {        $this->phase = self::PHASE_DOOR;        $this->trDest = $d->dest;        $this->trFlags = $d->flags & 0xC0;        $d->flags = $this->trFlags;        $this->trVisible = true;        $this->trY = $d->y;        $this->trX = $d->x;        $this->trFrame = $doorFrame;        $this->walkBase = $playerFrame;        $this->trDir = $playerFrame < 0x20 ? -1 : 1;        $this->player->frame = $playerFrame;        $this->player->visible = true;        $this->sound(Sfx::SHOOT);        $this->stage = self::WALK_A;        $this->startWalkSub();        $this->stepTransition();    }    private function startWalkSub(): void    {        $p = $this->player;        $p->frame = Values::u8($this->walkBase | (($p->anim >> 2) & 3));        $p->anim++;        $p->visible = true;    }    private function stepTransition(): void    {        $p = $this->player;        switch ($this->stage) {            case self::WALK_A:            case self::WALK_B:            case self::WALK_D:            case self::WALK_E:                $this->animateTiles();                if ($p->anim & 3) {                    $this->startWalkSub();                    return;                }                $this->walkStepDone();                break;            case self::TICK_A:                $this->stage = ((($this->trFrame - 0x2E) & 3) !== 0) ? self::WALK_A : self::WALK_B;                $this->startWalkSub();                break;            case self::TICK_E:                if ((($this->trFrame - 0x31) & 3) !== 0) {                    $this->stage = self::WALK_E;                    $this->startWalkSub();                } else {                    $this->trVisible = false;                    $this->phase = self::PHASE_PLAYING;                    $this->tickAcc = 1;                    $this->updateFloatingDoor();                    if ($this->worldTickPending) {                        $this->worldTickPending = false;                        $this->tickWorld();                    }                }                break;        }    }    private function walkStepDone(): void    {        $p = $this->player;        switch ($this->stage) {            case self::WALK_A:                $this->trFrame--;                $this->stage = self::TICK_A;                break;            case self::WALK_B:                $p->x = Values::u8($p->x + 2 * $this->trDir);                if ($this->trX !== Values::u8($p->x + 4 * $this->trDir)) {                    $this->startWalkSub();                    break;                }                $this->finishLeaving();                $this->startWalkSub();                break;            case self::WALK_D:                $p->x = Values::u8($p->x + 2 * $this->trDir);                if ($p->x !== $p->aux) {                    $this->startWalkSub();                    break;                }                $this->sound(Sfx::RISE);                $this->sound(Sfx::CLICK);                $this->stage = self::WALK_E;                $this->startWalkSub();                break;            case self::WALK_E:                $this->trFrame++;                $this->stage = self::TICK_E;                break;        }    }    private function finishLeaving(): void    {        $p = $this->player;        $this->leaveRoom();        if ($this->demo) {            $this->trDest = $this->scriptByte();        }        $this->enterRoom($this->trDest);        if ($this->demo) {            $this->scriptCount = 1;            $this->trFlags = $this->scriptByte();        }        if (!($this->trFlags & 0x80)) {            $idx = ($this->trFlags & 0x40) ? 1 : 0;            $d = $this->rs->doors[$idx];            $p->y = $d->y;            $p->x = Values::u8($d->x + 4);            $p->aux = Values::u8($p->x + 8);            $this->walkBase = 0x24;            $baseFrame = 0x31;        } else {            $idx = ($this->trFlags & 0x40) ? 3 : 2;            $d = $this->rs->doors[$idx];            $p->y = $d->y;            $p->x = Values::u8($d->x - 4);            $p->aux = Values::u8($p->x - 8);            $this->walkBase = 0x1C;            $baseFrame = 0x35;        }        $this->trDir = $this->walkBase < 0x20 ? -1 : 1;        $p->visible = false;        $d = $this->rs->doors[$idx];        $this->trVisible = true;        $this->trY = $d->y;        $this->trX = $d->x;        $this->trFrame = Values::u8($baseFrame - 3);        $this->stage = self::WALK_D;    }    // =================================================================================================================    // Creatures, objects, spells and the fixed objects of a room (game_creatures.cpp)    // =================================================================================================================    private function updateEntities(): void    {        foreach ($this->rs->ents as $e) {            $this->runEntity($e);        }        foreach ($this->slots as $e) {            $this->runEntity($e);        }    }    private function touchPlayer(Entity $e, int $damage): void    {        if (!Values::within($this->player->y, $e->y, Values::HIT_Y) || !Values::within($this->player->x, $e->x, Values::HIT_X)) {            return;        }        $this->sound(Sfx::COUNTER);        if ($this->easy) {            $damage = (int)($damage / 2);        }        $before = $this->player->hurt;        $this->player->hurt = Values::u8($before - $damage);        if ($before >= $damage) {            return;        }        $this->hurtOnePoint();    }    private function weaponHit(Entity $e, int $weaponItem): bool    {        $points = 0;        if (($this->input & Values::IN_ACTION) && Values::within($this->player->y, $e->y, Values::HIT_Y)            && Values::within($this->player->x, $e->x, Values::HIT_X) && $this->player->held === $weaponItem) {            $points = 1000;            $this->player->held = Values::NONE;            $e->next = 0x5C;        } else {            $hit = false;            foreach ($this->bolts as $b) {                if ($b->used && Values::within($b->y, $e->y, Values::HIT_Y) && Values::within($b->x, $e->x, Values::HIT_X)) {                    $hit = true;                    break;                }            }            if (!$hit) {                return false;            }            $points = 500;            $e->next = 0x5D;        }        $e->kind = Values::K_EXPLODING;        $e->frame = 0x4C;        $e->timer = 0;        $e->drawn = true;        $this->consumeAction();        $this->addScore($points);        $this->sound(Sfx::PICKUP);        $this->event(Event::KILL, $e->y + 8, $e->x + 4);        $this->stats->kills++;        return true;    }    private function burstDebris(Entity $e): void    {        foreach ($this->debris as $d) {            $d->clear();            $d->y = Values::u8(($e->y & 0xFE) + 9);            $d->x = Values::u8(($e->x & 0xFE) + 4);        }        $this->sound(Sfx::BURST);    }    private function maybeShoot(Entity $e): void    {        if (!$this->completed) {            return;        }        $before = $this->shotTimer;        $this->shotTimer = Values::u8($this->shotTimer - 1);        if ($before >= 1) {            return;        }        $this->shotTimer = $this->rnd() | 0x80;        $this->burstDebris($e);    }    private function clearParticles(array $ps): void    {        foreach ($ps as $p) {            $p->y = Values::NONE;        }    }    private function settle(Entity $e): void    {        if (!$e->used()) {            return;        }        $e->kind = Values::K_NONE;        $e->drawn = false;        foreach ($this->rs->floor as $f) {            if ($f->item !== Values::NONE) {                continue;            }            $drop = Values::u8(0x18 - $e->rows);            if ($e->next & 0x80) {                $f->set(Values::u8($e->y - $drop), $e->x, $e->frame);            } else {                $f->set(Values::u8($e->a7 - $drop), $e->a8, $e->next);            }            return;        }    }    private function runEntity(Entity $e): void    {        if (!$e->used()) {            return;        }        $playing = ($this->player->state & 0x0F) === Values::P_FLY;        switch ($e->kind) {            case Values::K_EMERGE:                if (--$e->timer !== 0) {                    return;                }                $e->timer = 1;                $e->y--;                $e->rows++;                if ($e->rows === 24) {                    $e->kind = $e->next;                }                $e->drawn = true;                break;            case Values::K_CHASER:                if (!$playing) {                    $e->drawn = true;                    return;                }                if ($this->weaponHit($e, 0x2D)) {                    return;                }                $this->touchPlayer($e, 0x4B);                $this->maybeShoot($e);                if ($this->player->x < $e->x && !$this->probeLeft($e)) {                    $e->x--;                }                if ($e->x < $this->player->x && !$this->probeRight($e)) {                    $e->x++;                }                if ($this->player->y < $e->y && !$this->probeUp($e)) {                    $e->y--;                }                if ($e->y < $this->player->y && !$this->probeDown($e)) {                    $e->y++;                }                $e->timer++;                $e->frame = ($e->frame & ~3) | (($e->timer >> 2) & 3);                $e->drawn = true;                break;            case Values::K_WALKER:                if (!$playing) {                    $e->drawn = true;                    return;                }                if ($this->weaponHit($e, 0x41)) {                    return;                }                $this->touchPlayer($e, 0x3C);                $this->maybeShoot($e);                $e->timer++;                if ($this->player->x < $e->x) {                    if (($e->timer & 3) === 0 && --$e->frame < 0x10) {                        $e->frame = 0x13;                    }                    if ($e->frame < 0x15 && !$this->probeLeft($e)) {                        $e->x--;                    }                } else {                    if (($e->timer & 3) === 0 && ++$e->frame >= 0x1C) {                        $e->frame = 0x18;                    }                    if ($e->frame >= 0x17 && !$this->probeRight($e)) {                        $e->x++;                    }                }                $e->drawn = true;                break;            case Values::K_DIVER:                {                    if (!$playing) {                        $e->drawn = true;                        return;                    }                    if ($this->weaponHit($e, 0x2C)) {                        return;                    }                    $this->touchPlayer($e, 0x32);                    $this->maybeShoot($e);                    $RIGHT = 1;                    $TURNING = 2;                    $fl = $e->flags;                    $y = $e->y;                    $x = $e->x;                    $fr = $e->frame;                    $t = Values::u8($e->timer + 1);                    $px = $this->player->x;                    $py = $this->player->y;                    if ($fl & $TURNING) {                        if (!($t & 1)) {                            $fr += ($fl & $RIGHT) ? -1 : 1;                            if (Values::u8($fr - 8) >= 4) {                                $fl &= ~$TURNING;                            }                        }                    } elseif ($x === $px) {                        $fl = ($fl & ~3) | ($fr < 0x0A ? $TURNING : ($TURNING | $RIGHT));                        $fr = ($fl & $RIGHT) ? 0x0B : 0x08;                    } elseif ($x > $px) {                        if ($fr < 8) {                            $x -= 2;                            $fr = (($t >> 2) & 3) | 4;                        } else {                            $fr = 0x0B;                            $fl |= $TURNING | $RIGHT;                        }                    } else {                        if ($fr < 0x0C) {                            $fr = 8;                            $fl = ($fl | $TURNING) & ~$RIGHT;                        } else {                            $x += 2;                            $fr = (($t >> 2) & 3) | 0x0C;                        }                    }                    if ($py !== $y) {                        $y += ($py < $y) ? -1 : 1;                    }                    $e->flags = Values::u8($fl);                    $e->y = Values::u8($y);                    $e->x = Values::u8($x);                    $e->frame = Values::u8($fr);                    $e->timer = $t;                    $e->drawn = true;                    break;                }            case Values::K_PATROL:                $this->touchPlayer($e, 0xFF);                $e->timer++;                $e->frame = (($e->timer >> 2) & 3) | 0x3C;                $next = fn (): int => Values::u8($e->x + (($e->next & 0x80) ? -1 : 1));                $nx = $next();                if ($nx < 0x34 || $nx >= 0x45) {                    $e->next ^= 0x80;                    $nx = $next();                }                $e->x = $nx;                $e->drawn = true;                break;            case Values::K_FOLLOWER:                $this->touchPlayer($e, 0xFF);                $e->timer++;                $e->frame = (($e->timer >> 2) & 3) | 0x58;                $e->x = Values::u8($this->rs->ents[0]->x + 0x1C);                $e->drawn = true;                break;            case Values::K_MAGIC_ITEM:                if (Values::u8($this->player->y - $e->y + 0x17) < 0x2E && Values::u8($this->player->x - $e->x + 0x0B) < 0x17) {                    $e->kind = Values::K_ITEM_RISING;                    $this->magicItems++;                    $e->frame = 0x50;                    $this->rs->pickupY = Values::NONE;                    $this->addScore(2000);                    $this->sound(Sfx::THUD);                    $this->event(Event::PICKUP, $e->y + 8, $e->x + 4);                    $this->stats->pickups++;                    $e->drawn = true;                    return;                }                $e->timer++;                if ($e->timer & 3) {                    return;                }                $e->frame = Values::u8((($e->frame + 1) & 3) | 0x54);                $e->drawn = true;                break;            case Values::K_ITEM_RISING:                $e->y -= 2;                $e->frame = ((($e->y >> 1) ^ 3) & 3) | 0x50;                if ($e->y < 0xC8) {                    $e->drawn = true;                    return;                }                $e->clear();                break;            case Values::K_EXPLODING:                $e->timer++;                if ($e->timer & 3) {                    return;                }                if (++$e->frame === 0x50) {                    $e->kind = Values::K_REMAINS;                    $e->timer = 0xC0;                    $e->frame = $e->next;                }                $e->drawn = true;                break;            case Values::K_REMAINS:                if (++$e->timer !== 0) {                    return;                }                $e->drawn = false;                $e->kind = Values::K_NONE;                break;            case Values::K_FALLING_ITEM:                if ($e->y !== Values::FLOOR_ROWS) {                    if ($this->probeDown($e)) {                        $this->settle($e);                        return;                    }                    $e->y++;                    $e->drawn = true;                    return;                }                $e->kind++;                $e->flags = 0;                $e->frame = 0x48;                $this->sound(Sfx::ENEMY_EXPLODE);                $e->drawn = true;                break;            case Values::K_LANDING:                if (++$e->timer & 3) {                    return;                }                if (!($e->flags & 0x10)) {                    if (++$e->frame !== 0x4C) {                        $e->drawn = true;                        return;                    }                    $e->flags |= 0x10;                }                if (--$e->frame !== 0x47) {                    $e->drawn = true;                    return;                }                $e->flags &= ~0x10;                $e->kind++;                $e->frame = 0x28;                $e->rows = 0;                $e->y += 0x18;                $this->sound(Sfx::WHOOSH);                $e->drawn = true;                break;            case Values::K_GROWING_ITEM:                if (++$e->timer & 3) {                    return;                }                $e->y--;                $e->rows++;                if ($e->rows !== 0x18) {                    $e->drawn = true;                    return;                }                $this->settle($e);                break;            case Values::K_SPELL_RING:            case Values::K_SPELL_CROSS:                {                    $all = $e->kind === Values::K_SPELL_RING;                    $cy = $this->player->y + 9;                    $cx = $this->player->x + 4;                    $e->clear();                    $this->clearParticles($this->bolts);                    for ($i = $all ? 0 : 1; $i < 8; $i += $all ? 1 : 2) {                        $this->bolts[$i]->y = Values::u8($cy);                        $this->bolts[$i]->x = Values::u8($cx);                    }                    $this->sound(Sfx::PLAYER_DEATH);                    $this->event(Event::CAST, $cy, $cx);                    break;                }            case Values::K_REWARD:                {                    $i = min($this->rewards, 3);                    $this->rewards++;                    if ($this->rewards === 4) {                        $this->rs->doors[1]->flags = 0xC6;                    }                    $e->a7 = $e->y = Values::u8($this->A->rewardSpots[$i][0]);                    $e->a8 = $e->x = Values::u8($this->A->rewardSpots[$i][1]);                    $e->frame = 0x4F;                    $e->kind++;                    $this->addScore(2000);                    $this->sound(Sfx::WHOOSH);                    $e->drawn = true;                    break;                }            case Values::K_REWARD_SHRINK:                if (++$e->timer & 3) {                    return;                }                if (--$e->frame !== 0x4B) {                    $e->drawn = true;                    return;                }                $e->kind++;                $e->frame = 0x50;                $e->timer = 0x80;                $e->drawn = true;                break;            case Values::K_REWARD_SPARKLE:                if (++$e->timer & 3) {                    return;                }                $e->frame = (($e->timer >> 2) & 3) | 0x50;                if ($e->timer !== 0) {                    $e->drawn = true;                    return;                }                $this->settle($e);                break;            case Values::K_VANISH:                $e->timer++;                $e->frame = (($e->timer >> 2) & 3) + 0x4C;                if (!($e->timer & 0x10)) {                    $e->drawn = true;                    return;                }                $e->clear();                break;            // ---- the scripted kinds of the victory scene ----            case Values::K_WAIT_SORCERER:                if (++$e->timer & 0x20) {                    $e->kind++;                }                break;            case Values::K_SHOW_SORCERER:                $e->frame = 0x39;                $e->kind++;                $e->drawn = true;                break;            case Values::K_LAST_FRAME:                if (++$e->timer & 3) {                    return;                }                $e->frame++;                $this->burstDebris($e);                $v = $this->vSprites[max(0, $this->vIndex)];                $v->show = 1;                $v->visible = true;                $v->y = $e->y;                $v->x = $e->x;                $v->frame = $e->frame;                $e->clear();                break;            case Values::K_DELAY_ALARM:                if (!(++$e->timer & 8)) {                    return;                }                $e->kind = Values::K_VANISH;                $e->timer = 0;                $this->vSprites[max(0, $this->vIndex)]->visible = false;                $this->sound(Sfx::HIT);                break;            case Values::K_FOLLOW_SCRIPT:                {                    $p = ($e->a8 | $e->next << 8) - 0x9E7C;                    $v = $p >= 0 && $p < count($this->A->victoryPath) ? $this->A->victoryPath[$p] : 0x80;                    $e->a8 = Values::u8($e->a8 + 1);                    if ($e->a8 === 0) {                        $e->next = Values::u8($e->next + 1);                    }                    if ($v === 0x80) {                        $e->kind = Values::K_FRAME_RESTORE;                        return;                    }                    $e->y = Values::u8($e->y + $v);                    $e->drawn = true;                    break;                }            case Values::K_TIMER_RESTORE:                if (++$e->timer !== 0) {                    return;                }                $e->kind = $e->flags & 0x7F;                break;            case Values::K_FRAME_RESTORE:                $e->frame = $e->x;                if ($e->flags === Values::NONE) {                    $v = $this->vSprites[4];                    $v->show = 1;                    $v->visible = true;                    $v->y = $e->y;                    $v->x = $e->x;                    $v->frame = $e->frame;                    $e->clear();                    return;                }                $e->kind = $e->flags & 0x7F;                $e->drawn = true;                break;            case Values::K_SCENE_1:                $e->flags = 0xB1;                $e->timer = 0x9C;                $e->kind = Values::K_TIMER_RESTORE;                break;            case Values::K_SCENE_2:                $e->frame = 0x54;                $e->flags = Values::u8($e->flags + 1);                $e->kind = Values::K_TIMER_RESTORE;                $e->timer = 0xF0;                $e->drawn = true;                break;            case Values::K_SCENE_3:                $e->frame = 0x3C;                $e->kind = Values::K_TIMER_RESTORE;                $e->flags = Values::u8($e->flags + 1);                $e->timer = 0xC8;                $this->override2 = '....CETTE FOIS....';                $e->drawn = true;                break;            case Values::K_SCENE_4:                $e->frame = 0x54;                $this->rs->ents[1]->kind = Values::K_SCENE_5;                $this->rs->ents[0]->timer = 0xC8;                $e->flags = 0xB5;                $e->kind = Values::K_TIMER_RESTORE;                $e->drawn = true;                break;            case Values::K_SCENE_5:                $this->vSprites[4]->visible = false;                $e->frame = 0x55;                $e->flags = 0xB5;                $e->kind = Values::K_TIMER_RESTORE;                $e->timer = 0xC8;                $e->drawn = true;                break;            case Values::K_ADVANCE:                $e->kind++;                $e->timer = 0;                $this->sound(Sfx::PICKUP);                break;            case Values::K_FADE_OUT:                if (++$e->timer & 3) {                    return;                }                $e->frame = (($e->timer >> 2) & 3) | 0x4C;                if (!($e->timer & 0x10)) {                    $e->drawn = true;                    return;                }                $e->clear();                break;            default:                $e->clear();                break;        }    }    private function moveParticles(array $ps): void    {        foreach ($ps as $p) {            if (!$p->used) {                continue;            }            $ny = Values::u8($p->y + $p->dy);            if ($ny >= 0x90) {                $p->y = Values::NONE;                continue;            }            $nx = Values::u8($p->x + $p->dx);            if ($nx >= 0xA0) {                $p->y = Values::NONE;                continue;            }            $p->y = $ny;            $p->x = $nx;        }    }    private function animateTiles(): void    {        if ($this->animTick === 0) {            $this->animTick = 4;            $this->animStep++;        } else {            $this->animTick--;        }    }    private function lockedDoors(): void    {        $p = $this->player;        $a = $this->rs->lockA;        if ($a->y !== Values::NONE && $a->y === $p->y && ((Values::u8($a->x - $p->x + 8)) & 0xEF) === 0 && $a->key === $p->held) {            $y = $a->y;            $x = $a->x;            $a->y = Values::NONE;            for ($r = 0; $r < 3; $r++) {                for ($c = 0; $c < 3; $c++) {                    $i = $this->cellIndex($y, $x) + $r * Values::MAP_W + $c;                    if ($i >= 0 && $i < Values::MAP_W * Values::MAP_H) {                        $this->cls[$i] = 0;                    }                }            }            $p->held = Values::NONE;            $this->sound(Sfx::BLIP);            $this->event(Event::DOOR, $y + 8, $x + 4);        }        $b = $this->rs->lockB;        if ($b->y !== Values::NONE && Values::u8($b->y - 8) === $p->y && $b->x === $p->x && $b->key === $p->held) {            $y = $b->y;            $x = $b->x;            $b->y = Values::NONE;            for ($r = 0; $r < 3; $r++) {                for ($c = 0; $c < 3; $c++) {                    $i = $this->cellIndex($y, $x) + $r * Values::MAP_W + $c;                    if ($i >= 0 && $i < Values::MAP_W * Values::MAP_H) {                        $this->cls[$i] = 0;                    }                }            }            $p->held = Values::NONE;            $this->sound(Sfx::BLIP);            $this->event(Event::DOOR, $y + 8, $x + 4);        }    }    private function cauldrons(): void    {        $p = $this->player;        $h = $this->rs->heart;        if ($h->y !== Values::NONE && Values::u8($h->y - 0x10) === $p->y && Values::within($h->x, $p->x, 4)) {            $p->hurt = 0xFF;            if ($p->energy < 99) {                $p->energy++;                $this->sound(Sfx::COUNTER);                $this->event(Event::HEAL, $p->y + 8, $p->x + 4);            }        }        $z = $this->rs->hazard;        if ($z->y !== Values::NONE && Values::u8($z->y - 0x10) === $p->y && Values::within($z->x, $p->x, 4)) {            $p->hurt = 0xFF;            if ($p->energy > 0) {                $p->energy--;                $this->sound(Sfx::COUNTER);            }        }    }    // =================================================================================================================    // Room 0x1C, the ending of the first quest and the victory (game_story.cpp)    // =================================================================================================================    private function bossRoom(): void    {        if (!$this->specials || $this->room !== Values::ROOM_BOSS) {            return;        }        $this->bossGroup = ($this->bossGroup + 1) & 3;        for ($i = 0; $i < 2; $i++) {            $e = $this->icons[$this->bossGroup * 2 + $i];            if (!$e->used()) {                continue;            }            $e->frame = ($e->frame & ~3) | (($e->frame + 1) & 3);        }    }    private function endingCheck(): void    {        if (!$this->specials || $this->completed || $this->magicItems !== 8 || $this->room !== Values::ROOM_BOSS) {            return;        }        if ($this->player->y !== 0x18 || $this->player->x !== 0x48) {            return;        }        $this->clearParticles($this->bolts);        $this->magicItems = 0;        $this->endTimer = 0x46;        $this->endPhase = 1;        $this->endReleased = 0;        $this->phase = self::PHASE_ENDING;    }    private function stepEnding(): void    {        $this->ticks++;        $p = $this->player;        $this->input = 0;        switch ($this->endPhase) {            case 1:                if (--$this->endTimer === 0) {                    $this->endTimer = 0x32;                    $e = $this->icons[$this->endReleased++];                    $e->frame = ($e->frame & 3) | 0x20;                    $this->sound(Sfx::ZAP);                }                if ($this->endReleased === 8) {                    $this->endPhase = 2;                }                break;            case 2:                $this->addScore(1);                $this->sound(Sfx::LEVEL_DONE);                if ($this->barSteps < Values::BAR_TOTAL) {                    $this->barSteps++;                }                if ($this->barSteps >= Values::BAR_TOTAL) {                    $this->endPhase = 3;                }                break;            case 3:                if ($p->energy >= 99) {                    $this->rs->doors[0]->flags &= 0xC0;                    $this->rs->doors[2]->flags = ($this->rs->doors[2]->flags & 0xC0) | 6;                    $this->completed = true;                    $this->endFlags = 0;                    $this->barSteps = Values::BAR_TOTAL;                    $this->magicItems = 8;                    $this->phase = self::PHASE_PLAYING;                    return;                }                $p->energy++;                break;        }        $p->anim++;        $p->frame = (($p->anim >> 2) & 3) | 0x20;        $this->bossGroup = ($this->bossGroup + 1) & 3;        for ($i = 0; $i < 2; $i++) {            $e = $this->icons[$this->bossGroup * 2 + $i];            if ($e->used()) {                $e->frame = ($e->frame & ~3) | (($e->frame + 1) & 3);            }        }    }    // ---- victory ----    private function victoryWorld(): void    {        $this->updateEntities();        $this->moveParticles($this->debris);        $this->animateTiles();    }    private function stepVictory(): void    {        $s0 = $this->slots[0];        switch ($this->vStage) {            case 0:                $s0->clear();                $s0->kind = Values::K_VANISH;                $s0->y = $this->player->y;                $s0->x = $this->player->x;                $s0->frame = $this->player->frame;                $s0->next = 0xFF;                $s0->drawn = true;                $this->player->visible = false;                $this->sound(Sfx::HIT);                $this->vStage = 1;                $this->vPhase = 0;                $this->vIndex = -1;                break;            case 1:                $this->victoryWorld();                if ($this->vIndex >= 0 && $s0->used()) {                    break;                }                if (++$this->vIndex === 4) {                    $this->vIndex = 0;                    $this->vPhase++;                }                if ($this->vPhase === 2) {                    $this->vStage = 2;                    $this->victoryTimer = 3;                    $this->sound(Sfx::GAME_OVER);                    break;                }                $s0->clear();                $s0->kind = $this->vPhase === 0 ? Values::K_WAIT_SORCERER : Values::K_DELAY_ALARM;                $s0->y = Values::u8($this->A->rewardSpots[$this->vIndex][0]);                $s0->x = Values::u8($this->A->rewardSpots[$this->vIndex][1]);                $s0->frame = $this->vPhase === 0 ? 0x28 : 0x3A;                $s0->drawn = $this->vPhase === 1;                break;            case 2:                $this->victoryWorld();                if (--$this->victoryTimer > 0) {                    break;                }                $this->map = array_fill(0, Values::MAP_W * Values::MAP_H, 0);                $this->cls = array_fill(0, Values::MAP_W * Values::MAP_H, 0);                $this->bare = true;                foreach ($this->vSprites as $o) {                    $o->visible = false;                }                foreach ($this->slots as $e) {                    $e->clear();                }                $this->clearParticles($this->bolts);                $this->clearParticles($this->debris);                $this->override1 = 'VOUS AVEZ VAINCU LE NECROMANCIEN';                for ($i = 0; $i < 2; $i++) {                    $e = $this->rs->ents[$i];                    $e->kind = Values::K_FOLLOW_SCRIPT;                    $e->x = $i ? 0x58 : 0x3C;                    $e->frame = $i ? 0x55 : 0x54;                    $e->rows = 0x18;                    $e->flags = $i ? 0xFF : 0xB0;                    $e->a8 = 0x7C;                    $e->next = 0x9E;                    $e->drawn = true;                }                $this->vStage = 3;                break;            case 3:                $this->victoryWorld();                if ($this->rs->ents[0]->used()) {                    break;                }                $this->addScore(20000);                $this->endFlags |= Values::END_WON | Values::END_FINISHED;                $this->phase = self::PHASE_OVER;                break;        }    }    // =================================================================================================================    // Lanterns and teleporters (game_extras.cpp)    // =================================================================================================================    private function extrasTick(): void    {        if ($this->darkEnabled && $this->lampTicks > 0) {            $this->lampTicks--;        }        if ($this->teleCooldown > 0) {            $this->teleCooldown--;        }        if (!$this->extrasOn || $this->player->state !== Values::P_FLY) {            return;        }        $p = $this->player;        for ($i = 0; $i < $this->rs->nExtras; $i++) {            $x = $this->rs->extras[$i];            if ($x->type === RoomExtra::LANTERN) {                if ($x->taken) {                    continue;                }                if (!Values::within($x->y, $p->y, 14) || !Values::within($x->x, $p->x, 8)) {                    continue;                }                $x->taken = true;                $this->lampTicks = min(Values::LAMP_MAX, $this->lampTicks + Values::LAMP_TICKS);                $this->sound(Sfx::PICKUP);                $this->event(Event::LAMP, $x->y + 12, $x->x + 6);                $this->addScore(100);            } elseif ($x->type === RoomExtra::TELEPORTER) {                if (($this->input & Values::IN_DOWN) && $this->teleCooldown === 0 && Values::within($x->y, $p->y, 8) && Values::within($x->x, $p->x, 6)) {                    $this->teleportTo($x->toRoom, $x->toY, $x->toX);                }            }        }    }    private function teleportTo(int $room, int $y, int $x): void    {        if ($room < 0 || $room >= $this->roomCount()) {            return;        }        $this->event(Event::TELEPORT, $this->player->y + 12, $this->player->x + 6);        $this->leaveRoom();        $this->enterRoom($room);        $this->player->y = Values::u8($y);        $this->player->x = Values::u8($x);        $this->teleCooldown = 45;        $this->sound(Sfx::RISE);        $this->event(Event::TELEPORT, $this->player->y + 12, $this->player->x + 6);        $this->snapshot();    }}