= 3 */ public int $SAUT_COUNT = 0; public bool $NOSCROLL_FLAG = false; /** Finish a level */ public bool $NEWLEVEL_FLAG = false; /** Number of bosses that needs to be killed to finish */ public int $BIGNMI_NBR = 0; /** Used for enemies walking and popping up */ public int $TAUPE_FLAG = 0; /** Flying carpet state */ public int $TAPISWAIT_FLAG = 0; /** Counter when hit */ public int $SEECHOC_FLAG = 0; /** Skip NO_FAST_CPU */ public bool $NFC_FLAG = false; /** Lives of the boss */ public int $BIGNMI_POWER = 0; /** True if the boss is alive */ public bool $boss_alive = false; public int $AUDIOMODE = 1; /** If true, the player will not interfere with the enemies */ public bool $GODMODE = false; /** If true, the player will move noclip */ public bool $NOCLIP = false; public bool $DISPLAYLOOPTIME = false; /** cheat keys (G, N, S) */ public int $devmode = 0; // --- port state ---------------------------------------------------------------------------------- public Level $level; public Screen $screen; /** @var SpriteData[] */ private array $sprites = []; /** @var ObjectData[] */ private array $objects = []; /** @var int[][] the C anim_player table (row 1 = the Moktar walk) */ public array $anim_player = []; /** Game time in ms (SDL_GetTicks) */ private float $ms = 0.0; /** First level of a demo / test run (enter() params) */ public ?int $startLevel = null; /** Called when the player quits from the menu (desktop: closes the game) */ public ?\Closure $onQuit = null; public function __construct( public readonly Sdl $sdl, public readonly Input $input, public readonly string $dataDir, public readonly ?\JumPHP\Audio\SoundManager $sound = null, ) { $this->level = new Level(); $this->screen = new Screen(); $this->anim_player = OriginalData::ANIM_PLAYER; $this->anim_player[1] = OriginalData::ANIM_PLAYER_MOKTAR; } /** The data files are there (the game cannot run without them). */ public static function hasData(string $dataDir): bool { foreach (['SPRITES.SQZ', 'FONTS.SQZ', 'MENU.SQZ', 'LEVEL0.SQZ'] as $file) { if (!is_file($dataDir . '/' . $file)) { return false; } } return true; } public function file(string $name): string { return $this->dataDir . '/' . $name; } /** GameScene: time of the frame. */ public function advanceTime(float $ms): void { $this->ms += $ms; } /** SDL_GetTicks() */ public function SDL_GetTicks(): int { return (int)$this->ms; } // --- opentitus.c -------------------------------------------------------------------------------- /** main(): intro text, logo, intro picture, then menu <-> game forever. */ public function main(): \Generator { $this->initoriginal(); $this->loadfonts(); $this->initaudio(); if ($this->startLevel !== null) { // demo / tests: straight into a level yield from $this->playtitus($this->startLevel); } $state = true; $retval = yield from $this->viewintrotext(); if ($retval < 0) { $state = false; } if ($state) { $retval = yield from $this->viewimage('TITUS.SQZ', 2, 0, 4000); } $this->SELECT_MUSIC(15); if ($state && $retval >= 0) { yield from $this->viewimage('TITRE.SQZ', 2, 0, 6500); } while (true) { $retval = yield from $this->viewmenu('MENU.SQZ', 2); if ($retval <= 0) { // Escape in the menu: the original quits the program if ($this->onQuit !== null) { ($this->onQuit)(); } yield 0; continue; } if ($retval <= self::LEVELCOUNT) { yield from $this->playtitus($retval - 1); $this->SELECT_MUSIC(15); } } } /** original.c initoriginal(): object data, sprites */ private function initoriginal(): void { $this->objects = []; foreach (OriginalData::OBJECT_FLAG as $i => $flag) { $o = new ObjectData(); $o->maxspeedY = OriginalData::OBJECT_MAXSPEEDY[$i]; $o->support = ($flag & 0x01) !== 0; $o->bounce = ($flag & 0x02) !== 0; $o->gravity = ($flag & 0x04) !== 0; $o->droptobottom = ($flag & 0x08) !== 0; $o->no_damage = ($flag & 0x10) !== 0; $this->objects[$i] = $o; } $this->loadsprites(); } /** sprites.c loadsprites() */ private function loadsprites(): void { $b = Sqz::load($this->file('SPRITES.SQZ')); $offset = 0; $this->sprites = []; for ($i = 0; $i < self::SPRITECOUNT; $i++) { $w = OriginalData::SPRITE_WIDTH[$i]; $h = OriginalData::SPRITE_HEIGHT[$i]; $this->sprites[$i] = new SpriteData($i, $w, $h, OriginalData::SPRITE_COLL[$i * 2], OriginalData::SPRITE_COLL[$i * 2 + 1], OriginalData::SPRITE_REF[$i * 2], -(OriginalData::SPRITE_REF[$i * 2 + 1] - $h), Gfx::planar($b, $offset, $w, $h)); $offset += ($w * $h) >> 1; } } /** level number (0-15) -> index in the original tables (getlevelid) */ public function getlevelid(int $levelnumber): int { foreach (OriginalData::LEVEL_ID as $i => $id) { if ($id === $levelnumber + 1) { return $i; } } return 0; } /** engine.c playtitus(): plays from $firstlevel to the end (or game over / quit). */ private function playtitus(int $firstlevel): \Generator { $level = $this->level; $level->lives = 2; $level->extrabonus = 0; for ($n = $firstlevel; $n < self::LEVELCOUNT; $n++) { $level->load(Sqz::load($this->file(self::LEVEL_FILES[$n] . '.SQZ')), $n, $this->sprites, $this->objects); $level->levelid = $this->getlevelid($n); $level->levelnumber = $n; $this->PERMUT_FLAG = $level->permut; $this->ALTITUDE_ZERO = $level->altitudeZero & 0xFF; $this->XLIMIT = $level->xLimit; $this->screen->loadLevelGraphics($this->sdl, $level); $first = true; while (true) { $this->SELECT_MUSIC(0); $this->CLEAR_DATA($level); $this->GODMODE = false; $this->NOCLIP = false; $this->DISPLAYLOOPTIME = false; $retval = yield from $this->viewstatus($level, $first); $first = false; if ($retval < 0) { return $retval; } $this->SELECT_MUSIC(OriginalData::LEVEL_MUSIC[$level->levelid]); yield from $this->INIT_SCREENM($level); $this->TFR_SCREENM(); yield from $this->flip_screen(true); $retval = yield from $this->playlevel($level); if ($retval < 0) { return $retval; } if ($this->NEWLEVEL_FLAG) { break; } if ($this->RESETLEVEL_FLAG === 1) { if ($level->lives === 0) { $this->GAMEOVER_FLAG = true; } else { $level->lives--; yield from $this->death($level); } } if ($this->GAMEOVER_FLAG) { yield from $this->gameover($level); return 0; } } } return 0; } /** engine.c playlevel(): the game loop */ private function playlevel(Level $level): \Generator { $firstrun = true; do { if (!$firstrun) { $this->DISPLAY_COUNT($level); $this->RETURN_MUSIC(); yield from $this->flip_screen(true); } $firstrun = false; $this->IMAGE_COUNTER = ($this->IMAGE_COUNTER + 1) & 0x0FFF; $this->MOVE_TRP($level); $this->move_objects($level); $retval = yield from $this->move_player($level); if ($retval === self::TITUS_ERROR_QUIT) { return $retval; } $this->MOVE_NMI($level); $this->MOVE_TRASH($level); $this->SET_NMI($level); yield from $this->CROSSING_GATE($level); $this->SPRITES_ANIMATION($level); $this->scroll($level); $this->BLOC_ANIMATION($level); $this->TFR_SCREENM(); $this->DISPLAY_SPRITES($level); $retval = yield from $this->RESET_LEVEL($level); if ($retval < 0) { return $retval; } } while ($retval === 0); return 0; } /** engine.c death() */ private function death(Level $level): \Generator { $player = $level->player; $this->SELECT_MUSIC(1); $this->FORCE_POSE($level); $this->updatesprite($level, $player->sprite, 13, true); // death $player->sprite->speedY = 15; for ($i = 0; $i < 60; $i++) { $this->TFR_SCREENM(); $this->DISPLAY_SPRITES($level); yield from $this->flip_screen(true); $player->sprite->speedY--; if ($player->sprite->speedY < -16) { $player->sprite->speedY = -16; } $player->sprite->y -= $player->sprite->speedY; } yield from $this->WAIT_SONG(); $this->SELECT_MUSIC(0); yield from $this->CLOSE_SCREEN(); } /** engine.c gameover() */ private function gameover(Level $level): \Generator { $player = $level->player; $this->SELECT_MUSIC(2); $this->updatesprite($level, $player->sprite, 13, true); // death $this->updatesprite($level, $player->sprite2, 333, true); // game $player->sprite2->x = ($this->BITMAP_X << 4) - (120 - 2); $player->sprite2->y = ($this->BITMAP_Y << 4) + 100; $this->updatesprite($level, $player->sprite3, 334, true); // over $player->sprite3->x = ($this->BITMAP_X << 4) + (320 + 120 - 2); $player->sprite3->y = ($this->BITMAP_Y << 4) + 100; for ($i = 0; $i < 31; $i++) { $this->TFR_SCREENM(); $this->DISPLAY_SPRITES($level); yield from $this->flip_screen(true); $player->sprite2->x += 8; $player->sprite3->x -= 8; } $retval = yield from $this->waitforbutton(); if ($retval < 0) { return $retval; } yield from $this->fadeout(); return 0; } // --- common.c / keyboard.c ------------------------------------------------------------------------ /** Decreases a value with 1 until it's zero */ public function subto0(int &$number): void { if ($number > 0) { $number--; } } /** keystate[key]: is the key held now */ public function keystate(string $key): bool { return $this->input->held($key); } /** * SDL_PollEvent: the key events since the last step. * @return array ['down'|'up', key] */ public function pollEvents(): array { return $this->input->takeEvents(); } /** waitforbutton(): 0 = Return / Enter / Space, -1 = Escape */ public function waitforbutton(): \Generator { while (true) { foreach ($this->pollEvents() as [$type, $key]) { if ($type !== 'down') { continue; } if ($key === self::KEY_RETURN || $key === self::KEY_SPACE) { return 0; } if ($key === self::KEY_ESC) { return -1; } $this->musicKey($key); } yield 0; } } /** KEY_MUSIC handling, common to every waiting loop */ public function musicKey(string $key): void { if ($key === self::KEY_MUSIC) { $this->AUDIOMODE++; if ($this->AUDIOMODE > 1) { $this->AUDIOMODE = 0; } if ($this->AUDIOMODE === 1) { $this->startmusic(); } else { $this->stopmusic(); } } } // --- C integer helpers --------------------------------------------------------------------------- /** (uint8) */ public static function u8(int $v): int { return $v & 0xFF; } /** (int8) */ public static function s8(int $v): int { $v &= 0xFF; return $v >= 0x80 ? $v - 0x100 : $v; } /** (uint16) */ public static function u16(int $v): int { return $v & 0xFFFF; } /** (int16) */ public static function s16(int $v): int { $v &= 0xFFFF; return $v >= 0x8000 ? $v - 0x10000 : $v; } /** C integer division (truncates toward zero) */ public static function div(int $a, int $b): int { return intdiv($a, $b); } }