<?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(); }}