type($type); $this->damage = $t[1] & 0x1F; $this->points = $t[2]; if ($t[0] & 0x40) { $this->kind = self::ENEMY; $this->hits = max(1, $t[1] >> 5); } elseif ($t[0] & 0x20) { $this->handler = $t[0] & 0x1F; $this->kind = match ($this->handler) { 0, 1, 2, 3, 4 => self::ITEM, 6 => self::SUIT, 7 => self::TELEPORT, 8 => self::BEAM, 9 => self::CORE, default => self::HAZARD, }; } if ($program > 0) { $this->program = $world->programs[$program] ?? null; } $this->setAnim($type); } public function setAnim(int $id): void { $this->animId = $id; $this->anim = $this->world->anims[$id]; $this->frameIndex = 0; $this->frameTimer = $this->anim['ticks']; $this->frame = $this->anim['frames'][0] ?? 0; } /** Moves in a straight line (shots), instead of a program. */ public function setMotion(int $dirs, int $dx, int $dy): void { $this->program = null; $this->dirs = $dirs; $this->dx = $dx; $this->dy = $dy; } public function width(): int { return intdiv($this->world->sprites[$this->frame & 0x7F]['w'], World::PIXELS_PER_BYTE); } public function height(): int { return $this->world->sprites[$this->frame & 0x7F]['h']; } /** [x, y, w, h] in view pixels. */ public function box(): array { $s = $this->world->sprites[$this->frame & 0x7F]; return [$this->x * World::PIXELS_PER_BYTE, $this->y - World::TOP, $s['w'], $s['h']]; } public function tick(int $heroX, int $heroY): void { if ($this->flash > 0) { $this->flash--; } $this->runProgram(); $this->move($heroX, $heroY); $this->runAnim(); if ($this->kind === self::SHOT && --$this->life <= 0) { $this->dead = true; } } private function runProgram(): void { if ($this->program === null) { return; } if ($this->timer > 0) { $this->timer--; return; } $steps = $this->program['steps']; if ($this->step >= count($steps)) { if ($this->program['end'] === 'loop' && $steps) { $this->step = 0; } else { if ($this->program['end'] === 'kill') { $this->dirs = 0; } return; // 'hold': the last step goes on } } [$this->dirs, $this->timer, $this->dx, $this->dy] = $steps[$this->step++]; } private function move(int $heroX, int $heroY): void { $dirs = $this->dirs; if ($dirs === 0x0F) { // #6CDE: towards Tal $dirs = ($heroX > $this->x ? 8 : ($heroX < $this->x ? 4 : 0)) | ($heroY > $this->y ? 2 : ($heroY < $this->y ? 1 : 0)); } $w = $this->width(); $h = $this->height(); if ($dirs & 1) { $this->y -= $this->dy; } if ($dirs & 2) { $this->y += $this->dy; } if ($dirs & 4) { $this->x -= $this->dx; } if ($dirs & 8) { $this->x += $this->dx; } if ($this->kind === self::SHOT) { if ($this->x < 0 || $this->x + $w > 80 || $this->y >= World::BOTTOM) { $this->dead = true; } return; } // #6C43...: the screen's bytes and the 0-255 lines bound them; what falls off the playfield is gone $this->x = max(0, min(80 - $w, $this->x)); $this->y = max(0, min(255 - $h, $this->y)); } /** On the playfield (objects above or under it are out of play). */ public function visible(): bool { return $this->y + $this->height() > World::TOP && $this->y < World::BOTTOM; } private function runAnim(): void { if (--$this->frameTimer > 0) { return; } $this->frameTimer = $this->anim['ticks']; $frames = $this->anim['frames']; $this->frameIndex++; if ($this->frameIndex >= count($frames)) { switch ($this->anim['end']) { case 'loop': $this->frameIndex = 0; break; case 'next': $this->setAnim($this->anim['next']); return; case 'kill': $this->dead = true; $this->frameIndex = count($frames) - 1; break; default: $this->frameIndex = count($frames) - 1; } } $this->frame = $frames[$this->frameIndex] ?? $this->frame; } public function animId(): int { return $this->animId; } }