<?phpnamespace App\Titus;/** * OpenTitus src/enemies.c: handles enemies. * * Global functions: * MOVE_NMI(level): Move enemies, is called by main game loop * SET_NMI(level): Collision detection, animation, is called by main game loop * MOVE_TRASH(level): Move objects thrown by enemies * * Note: inside the switches, the C `continue` (next enemy) is `continue 2` (one switch) or * `continue 3` (two nested switches) in PHP. */trait EnemiesTrait{ /** * C unsigned int comparison a < b (both operands converted to unsigned int, 32 bits). * Used where the C code compares a signed value with an "unsigned int" field (delay, rangeY). */ private static function uint_lt(int $a, int $b): bool { if (($a < 0) !== ($b < 0)) { return $a >= 0; // a negative value is a huge unsigned value } return $a < $b; } public function MOVE_NMI(Level $level): void { $bullet = null; for ($i = 0; $i < $level->enemycount; $i++) { $enemy = $level->enemy[$i]; $player = $level->player; if (!($enemy->sprite->enabled)) continue; //Skip unused enemies switch ($enemy->type) { case 0: case 1: //Noclip walk if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } $enemy->sprite->x -= $enemy->sprite->speedX; //Move the enemy if (abs($enemy->sprite->x - $enemy->centerX) > $enemy->rangeX) { //If the enemy is rangeX from center, turn direction if ($enemy->sprite->x >= $enemy->centerX) { //The enemy is at rightmost edge $enemy->sprite->speedX = abs($enemy->sprite->speedX); } else { //The enemy is at leftmost edge $enemy->sprite->speedX = 0 - abs($enemy->sprite->speedX); } } break; case 2: //Shoot if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } if (!$enemy->visible) { //Skip if not on screen continue 2; } //Give directions! if ($enemy->direction == 0) { //Both ways $enemy->sprite->speedX = 0; if ($enemy->sprite->x < $player->sprite->x) { $enemy->sprite->speedX = -1; } } elseif ($enemy->direction == 2) { //Right only $enemy->sprite->speedX = -1; //Flip the sprite } else { //Left only $enemy->sprite->speedX = 0; //Not flipped (facing left) } switch ($enemy->phase) { //State dependent actions case 0: //Scans the horizon! $this->subto0($enemy->counter); if ($enemy->counter != 0) { //Decrease delay timer continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 24) { continue 3; } if ($enemy->rangeX < abs($player->sprite->x - $enemy->sprite->x)) { //if too far apart continue 3; } if ($enemy->direction != 0) { if ($enemy->direction == 2) { //Right only if ($enemy->sprite->x > $player->sprite->x) { //Skip shooting if player is in opposite direction continue 3; } } else { if ($player->sprite->x > $enemy->sprite->x) { //Skip shooting if player is in opposite direction continue 3; } } } $enemy->phase = 30; //change state $this->UP_ANIMATION($enemy->sprite); break; default: $enemy->phase = self::u8($enemy->phase - 1); if (!$enemy->trigger) { continue 3; } $enemy->sprite->anim += 2; if ($this->FIND_TRASH($level, $bullet)) { $this->PUT_BULLET($level, $enemy, $bullet); //level->enemy[i].counter = NMI_FREQ; //set delay timer $enemy->counter = self::u8($enemy->delay); //set delay timer } $enemy->phase = 0; break; } break; case 3: case 4: //Noclip walk, jump to player (fish) if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: $enemy->sprite->x -= $enemy->sprite->speedX; //Move the enemy if (abs($enemy->sprite->x - $enemy->centerX) > $enemy->rangeX) { //If the enemy is rangeX from center, turn direction if ($enemy->sprite->x >= $enemy->centerX) { //The enemy is at rightmost edge $enemy->sprite->speedX = abs($enemy->sprite->speedX); } else { //The enemy is at leftmost edge $enemy->sprite->speedX = 0 - abs($enemy->sprite->speedX); } } if (!$enemy->visible) { //Is the enemy on the screen? continue 3; } if (($enemy->sprite->y < $player->sprite->y) || ($enemy->sprite->y >= ($player->sprite->y + 256))) { //Skip if player is below or >= 256 pixels above continue 3; } if (self::uint_lt($enemy->rangeY, $enemy->sprite->y - $player->sprite->y)) { //Skip if player is above jump limit continue 3; } //see if the hero is in the direction of movement of fish if ($enemy->sprite->x > $player->sprite->x) { //The enemy is right for the player if ($enemy->sprite->flipped == true) { //The enemy looks right, skip continue 3; } } else { //The enemy is left for the player if ($enemy->sprite->flipped == false) { //The enemy looks left, skip continue 3; } } if (abs($enemy->sprite->x - $player->sprite->x) >= 48) { //Fast calculation continue 3; } //See if the hero is above the area of fish if (abs($player->sprite->x - $enemy->centerX) > $enemy->rangeX) { continue 3; } $enemy->phase = 1; //Change state //Calculation speed to the desired height $enemy->sprite->speedY = 0; $j = 0; do { $enemy->sprite->speedY++; //Set init jump speed $j += $enemy->sprite->speedY; } while (($enemy->sprite->y - $player->sprite->y) > $j); //Make sure the enemy will jump high enough to hit the player $enemy->sprite->speedY = 0 - $enemy->sprite->speedY; //Init speed must be negative $enemy->delay = $enemy->sprite->y; //Delay: Last Y position, reuse of the delay variable $this->UP_ANIMATION($enemy->sprite); break; case 1: if (!$enemy->visible) { //Is the enemy on the screen? continue 3; } $enemy->sprite->x -= $enemy->sprite->speedX << 2; $enemy->sprite->y += $enemy->sprite->speedY; if ($enemy->sprite->speedY + 1 < 0) { $enemy->sprite->speedY++; if (self::uint_lt($enemy->delay - $enemy->rangeY, $enemy->sprite->y)) { //Delay: Last Y position, reuse of the delay variable continue 3; } } $this->UP_ANIMATION($enemy->sprite); $enemy->phase = 2; $enemy->sprite->speedY = 0; if ($enemy->sprite->x <= $enemy->centerX) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); } else { $enemy->sprite->speedX = 0 - abs($enemy->sprite->speedX); } break; case 2: if (!$enemy->visible) { //Is the enemy on the screen? continue 3; } $enemy->sprite->x -= $enemy->sprite->speedX; $enemy->sprite->y += $enemy->sprite->speedY; //2: fall! $enemy->sprite->speedY++; if (self::uint_lt($enemy->sprite->y, $enemy->delay)) { //3: we hit bottom? //Delay: Last Y position, reuse of the delay variable continue 3; } $enemy->sprite->y = self::s16($enemy->delay); //Delay: Last Y position, reuse of the delay variable $enemy->sprite->x -= $enemy->sprite->speedX; $enemy->phase = 0; $this->DOWN_ANIMATION($enemy->sprite); $this->DOWN_ANIMATION($enemy->sprite); break; } break; case 5: case 6: //Noclip walk, move to player (fly) if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } $enemy->sprite->x -= $enemy->sprite->speedX; //Move the enemy if (abs($enemy->sprite->x - $enemy->centerX) > $enemy->rangeX) { //If the enemy is rangeX from center, turn direction if ($enemy->sprite->x >= $enemy->centerX) { //The enemy is at rightmost edge $enemy->sprite->speedX = abs($enemy->sprite->speedX); } else { //The enemy is at leftmost edge $enemy->sprite->speedX = 0 - abs($enemy->sprite->speedX); } } if (!$enemy->visible) { //Is the enemy on the screen? continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //Forward if (abs($enemy->sprite->y - $player->sprite->y) > $enemy->rangeY) { //Too far away continue 3; } if (abs($enemy->sprite->x - $player->sprite->x) > 40) { //Too far away continue 3; } $enemy->delay = $enemy->sprite->y; //Delay: Last Y position, reuse of the delay variable if ($enemy->sprite->y < $player->sprite->y) { //Player is below the enemy $enemy->sprite->speedY = 2; } else { //Player is above the enemy $enemy->sprite->speedY = -2; } $enemy->phase = 1; //Change state $this->UP_ANIMATION($enemy->sprite); break; case 1: //Attack $enemy->sprite->y += $enemy->sprite->speedY; if (abs($enemy->sprite->y - $enemy->delay) < $enemy->rangeY) { //Delay: Last Y position, reuse of the delay variable continue 3; } $enemy->sprite->speedY = 0 - $enemy->sprite->speedY; $this->UP_ANIMATION($enemy->sprite); $enemy->phase = 2; break; case 2: //Back up! $enemy->sprite->y += $enemy->sprite->speedY; if ($enemy->sprite->y != $enemy->delay) { //Delay: Last Y position, reuse of the delay variable continue 3; } $this->DOWN_ANIMATION($enemy->sprite); $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 0; break; } break; case 7: //Gravity walk, hit when near if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //Waiting if ($enemy->sprite->y > $player->sprite->y) { continue 3; } if ($enemy->rangeX < abs($enemy->sprite->x - $player->sprite->x)) { continue 3; } if (abs($enemy->sprite->y - $player->sprite->y) > 200) { continue 3; } $enemy->phase = 1; $this->UP_ANIMATION($enemy->sprite); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } break; case 1: //Gravity walk if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { if ($enemy->sprite->speedY < 16) { //16 = Max yspeed $enemy->sprite->speedY++; } $enemy->sprite->y += $enemy->sprite->speedY; continue 3; } if ($enemy->sprite->speedY != 0) { if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } } $enemy->sprite->speedY = 0; $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); if ($enemy->sprite->speedX > 0) { $j = -1; //moving left } else { $j = 1; //moving right } $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, ($enemy->sprite->x >> 4) + $j)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) > 320 * 2) { //Too far away from the player in X, reset $enemy->phase = 2; continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) >= 200 * 2) { //Too far away from the player in Y, reset $enemy->phase = 2; continue 3; } if (abs($player->sprite->x - $enemy->sprite->x) > $enemy->sprite->spritedata->w + 6) { continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 8) { continue 3; } $enemy->phase = 3; //The player is close to the enemy, strike! $this->UP_ANIMATION($enemy->sprite); break; case 2: //Reset the enemy if ((($enemy->initY >> 4) - $this->BITMAP_Y <= 13) && //13 tiles in Y (($enemy->initY >> 4) - $this->BITMAP_Y >= 0) && (($enemy->initX >> 4) - $this->BITMAP_X < 21) && //21 tiles in X (($enemy->initX >> 4) - $this->BITMAP_X >= 0)) { continue 3; //Player is too close to the enemy's spawning point } $enemy->sprite->y = $enemy->initY; $enemy->sprite->x = $enemy->initX; $enemy->phase = 0; $this->DOWN_ANIMATION($enemy->sprite); break; case 3: //Strike! if ($enemy->trigger) { //End of strike animation (TODO: check if this will ever be executed) $enemy->phase = 1; continue 3; } //Gravity walk (equal to the first part of "case 1:") if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { if ($enemy->sprite->speedY < 16) { //16 = Max yspeed $enemy->sprite->speedY++; } $enemy->sprite->y += $enemy->sprite->speedY; continue 3; } if ($enemy->sprite->speedY != 0) { if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } } $enemy->sprite->speedY = 0; $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); if ($enemy->sprite->speedX > 0) { $j = -1; //moving left } else { $j = 1; //moving right } $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, ($enemy->sprite->x >> 4) + $j)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) > 320 * 2) { //Too far away from the player in X, reset $enemy->phase = 2; continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) >= 200 * 2) { //Too far away from the player in Y, reset $enemy->phase = 2; continue 3; } break; } break; case 8: //Gravity walk when off-screen case 14: //Gravity walk when off-screen (immortal) if ($enemy->type == 14) { $enemy->dying = 0; //Immortal } elseif ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //waiting if ((abs($enemy->sprite->x - $player->sprite->x) > 340) || (abs($enemy->sprite->y - $player->sprite->y) >= 230)) { $enemy->phase = 1; $this->UP_ANIMATION($enemy->sprite); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } } break; case 1: if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { if ($enemy->sprite->speedY < 16) { //16 = Max yspeed $enemy->sprite->speedY++; } $enemy->sprite->y += $enemy->sprite->speedY; continue 3; } if ($enemy->sprite->speedY != 0) { if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } } $enemy->sprite->speedY = 0; $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); if ($enemy->sprite->speedX > 0) { $j = -1; //moving left } else { $j = 1; //moving right } $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, ($enemy->sprite->x >> 4) + $j)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) < 320 * 2) { continue 3; } $enemy->phase = 2; break; case 2: //Reset the enemy if ((($enemy->initY >> 4) - $this->BITMAP_Y < 12) && //12 tiles in Y (($enemy->initY >> 4) - $this->BITMAP_Y >= 0) && (($enemy->initX >> 4) - $this->BITMAP_X < 19) && //19 tiles in X (($enemy->initX >> 4) - $this->BITMAP_X >= 0)) { continue 3; //Player is too close to the enemy's spawning point } $enemy->sprite->y = $enemy->initY; $enemy->sprite->x = $enemy->initX; $enemy->phase = 0; $this->DOWN_ANIMATION($enemy->sprite); break; } break; case 9: //Walk and periodically pop-up if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //wait for its prey! if ($enemy->rangeX < abs($player->sprite->x - $enemy->sprite->x)) { continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 60) { continue 3; } $enemy->phase = 1; $this->UP_ANIMATION($enemy->sprite); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } break; case 1: //Special animation? $this->TAUPE_FLAG = self::u8($this->TAUPE_FLAG + 1); if ((($this->TAUPE_FLAG & 0x04) == 0) && //xxxxx0xx //true 4 times, false 4 times, (($this->IMAGE_COUNTER & 0x01FF) == 0)) { //xxxxxxx0 00000000 //true 1 time, false 511 times $this->UP_ANIMATION($enemy->sprite); } if (($this->IMAGE_COUNTER & 0x007F) == 0) { //xxxxxxxx x0000000 //true 1 time, false 127 times $enemy->phase = 3; $this->UP_ANIMATION($enemy->sprite); //Same as "case 3:" //Remove the head or Periskop! if (!$enemy->visible) { //Is it on the screen? //Give the sequence # 2 and Phase # 1 $this->UP_ANIMATION($enemy->sprite); $enemy->sprite->anim--; //Previous animation frame $this->GAL_FORM($level, $enemy); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } $enemy->phase = 1; } elseif ($enemy->trigger) { if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } $enemy->phase = 1; } continue 3; } if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($enemy->initX > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } } $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); if ($enemy->sprite->speedX > 0) { $j = -1; //moving left } else { $j = 1; //moving right } $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, ($enemy->sprite->x >> 4) + $j)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) < 320 * 4) { continue 3; } $enemy->phase = 2; break; case 2: //Reset, if not visible on the screen if ((($enemy->initY >> 4) - $this->BITMAP_Y <= 12) && //12 tiles in Y (($enemy->initY >> 4) - $this->BITMAP_Y >= 0) && (($enemy->initX >> 4) - $this->BITMAP_X < 25) && //25 tiles in X (($enemy->initX >> 4) - $this->BITMAP_X >= 0)) { continue 3; //Player is too close to the enemy's spawning point } $enemy->sprite->y = $enemy->initY; $enemy->sprite->x = $enemy->initX; $enemy->phase = 0; $this->DOWN_ANIMATION($enemy->sprite); break; case 3: //Remove the head or Periskop! if (!$enemy->visible) { //Is it on the screen? $this->UP_ANIMATION($enemy->sprite); $enemy->sprite->anim--; //Previous animation frame $this->GAL_FORM($level, $enemy); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } $enemy->phase = 1; } elseif ($enemy->trigger) { if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } $enemy->phase = 1; } break; } break; case 10: //Alert when near, walk when nearer if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: if ($this->FURTIF_FLAG != 0) { continue 3; } if ($enemy->rangeX < abs($player->sprite->x - $enemy->sprite->x)) { continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 26) { continue 3; } $enemy->phase = 1; $this->UP_ANIMATION($enemy->sprite); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } // no break: fall through to case 1 (as in the C code) case 1: //wait if ($this->FURTIF_FLAG != 0) { continue 3; } if ($enemy->rangeX < abs($player->sprite->x - $enemy->sprite->x)) { //Switch back to state 0 $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 0; continue 3; } // rangeX - 50 is unsigned in C (huge if rangeX < 50) if ((!self::uint_lt($enemy->rangeX - 50, abs($player->sprite->x - $enemy->sprite->x))) && (abs($player->sprite->y - $enemy->sprite->y) <= 60)) { $enemy->phase = 2; $this->UP_ANIMATION($enemy->sprite); } break; case 2: //run if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($enemy->initX > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } } $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); if ($enemy->sprite->speedX > 0) { $j = -1; //moving left } else { $j = 1; //moving right } $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, ($enemy->sprite->x >> 4) + $j)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($enemy->initX > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) >= 320 * 2) { $enemy->phase = 3; } break; case 3: //Reset, if not visible on the screen if ((($enemy->initY >> 4) - $this->BITMAP_Y <= 13) && //13 tiles in Y (($enemy->initY >> 4) - $this->BITMAP_Y >= 0) && (($enemy->initX >> 4) - $this->BITMAP_X < 21) && //21 tiles in X (($enemy->initX >> 4) - $this->BITMAP_X >= 0)) { continue 3; //Spawning point is visible on screen } $enemy->sprite->y = $enemy->initY; $enemy->sprite->x = $enemy->initX; $this->DOWN_ANIMATION($enemy->sprite); $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 0; break; } break; case 11: //Walk and shoot if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //wait if ($enemy->rangeX < abs($player->sprite->x - $enemy->sprite->x)) { continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 26) { continue 3; } $enemy->phase = 1; $this->UP_ANIMATION($enemy->sprite); if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } break; case 1: if ($this->get_floorflag($level, ($enemy->sprite->y >> 4), ($enemy->sprite->x >> 4)) == self::FFLAG_NOFLOOR) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($enemy->initX > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } } $enemy->sprite->y = self::s16($enemy->sprite->y & 0xFFF0); $hflag = self::u8($this->get_horizflag($level, ($enemy->sprite->y >> 4) - 1, $enemy->sprite->x >> 4)); if (($hflag == self::HFLAG_WALL) || ($hflag == self::HFLAG_DEADLY) || ($hflag == self::HFLAG_PADLOCK)) { //Next tile is wall, change direction $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; if ($enemy->sprite->x < 0) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; $enemy->sprite->x -= $enemy->sprite->speedX; } if (abs($player->sprite->x - $enemy->sprite->x) >= 320 * 2) { $enemy->phase = 2; } $this->subto0($enemy->counter); if ($enemy->counter != 0) { continue 3; } if (abs($player->sprite->x - $enemy->sprite->x) > 64) { continue 3; } if (abs($player->sprite->y - $enemy->sprite->y) > 20) { continue 3; } if ($enemy->sprite->x > $player->sprite->x) { //Enemy is right for the player $enemy->sprite->speedX = $enemy->walkspeedX; //Move left } else { //Enemy is left for the player $enemy->sprite->speedX = 0 - $enemy->walkspeedX; //Move right } $enemy->phase = 3; //phase started! $this->UP_ANIMATION($enemy->sprite); $enemy->counter = 20; break; case 2: //Reset enemy when spawn point isn't visible for the player if ((($enemy->initY >> 4) - $this->BITMAP_Y > 13) || //13 tiles in Y (Spawn is below) (($enemy->initY >> 4) - $this->BITMAP_Y < 0) || // (Spawn is above) (($enemy->initX >> 4) - $this->BITMAP_X >= 21) || //21 tiles in X (Spawn is to the right) (($enemy->initX >> 4) - $this->BITMAP_X < 0)) { //(Spawn is to the right) $enemy->sprite->x = $enemy->initX; $enemy->sprite->y = $enemy->initY; $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 0; } break; case 3: //Shoot if (!$enemy->trigger) { continue 3; } if ($this->FIND_TRASH($level, $bullet)) { $enemy->sprite->anim += 2; $this->PUT_BULLET($level, $enemy, $bullet); } $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 1; break; } break; case 12: //Jump (fireball) (immortal) $enemy->dying = 0; //Immortal switch ($enemy->phase) { //State dependent actions case 0: //init $this->UP_ANIMATION($enemy->sprite); $enemy->sprite->speedY = self::s16($enemy->rangeY); $enemy->initY = $enemy->sprite->y; $enemy->phase = 1; break; case 1: //Fireball moving up $enemy->sprite->y -= $enemy->sprite->speedY; $enemy->sprite->speedY--; if ($enemy->sprite->speedY == 0) { $enemy->phase = 2; } break; case 2: //Fireball falling down $enemy->sprite->y += $enemy->sprite->speedY; $enemy->sprite->speedY++; if ($enemy->sprite->y >= $enemy->initY) { $enemy->sprite->y = $enemy->initY; $enemy->counter = self::u8($enemy->delay); $enemy->phase = 3; $this->DOWN_ANIMATION($enemy->sprite); } break; case 3: //Fireball delay $enemy->counter = self::u8($enemy->counter - 1); if ($enemy->counter == 0) { $enemy->phase = 0; } break; } break; case 13: //Bounce (big baby) if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } switch ($enemy->phase) { //State dependent actions case 0: //remain at rest or attack! if ($player->sprite->x >= $enemy->sprite->x) { $enemy->sprite->speedX = 0 - abs($enemy->sprite->speedX); } else { $enemy->sprite->speedX = abs($enemy->sprite->speedX); } if ((abs($player->sprite->x - $enemy->sprite->x) <= $enemy->rangeX) && (abs($player->sprite->y - $enemy->sprite->y) <= 40)) { $this->UP_ANIMATION($enemy->sprite); $enemy->phase = 1; $enemy->sprite->speedY = 10; } break; case 1: //Jump, move upwards $enemy->sprite->x -= $enemy->sprite->speedX; $enemy->sprite->y -= $enemy->sprite->speedY; $enemy->sprite->speedY--; if ($enemy->sprite->speedY == 0) { $this->UP_ANIMATION($enemy->sprite); $enemy->phase = 2; } break; case 2: //Fall down to the ground $enemy->sprite->x -= $enemy->sprite->speedX; $enemy->sprite->y += $enemy->sprite->speedY; $enemy->sprite->speedY++; if ($enemy->sprite->speedY > 10) { $enemy->phase = 3; $this->UP_ANIMATION($enemy->sprite); $enemy->counter = self::u8($enemy->delay); } break; case 3: //Stay on the ground for a while $enemy->counter = self::u8($enemy->counter - 1); if ($enemy->counter == 0) { $this->DOWN_ANIMATION($enemy->sprite); $this->DOWN_ANIMATION($enemy->sprite); $this->DOWN_ANIMATION($enemy->sprite); $enemy->phase = 0; } break; } break; //case 14: //Gravity walk when off-screen (immortal) //Located at case 8 case 15: //Nothing (immortal) $enemy->dying = 0; //Immortal break; case 16: //Nothing if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } break; case 17: //Drop (immortal) $enemy->dying = 0; //Immortal //Delay if ($enemy->counter + 1 < $enemy->delay) { $enemy->counter = self::u8($enemy->counter + 1); continue 2; } if ($enemy->rangeX < abs($enemy->sprite->x - $player->sprite->x)) { //hero too far! at x $enemy->counter = 0; continue 2; } if (self::uint_lt($enemy->rangeY, $player->sprite->y - $enemy->sprite->y)) { //hero too far! at y continue 2; } //you attack, so finding a free object $j = 0; do { $j++; if ($j > $level->objectcount || !isset($level->object[$j])) { $enemy->counter = 0; // C: "continue" jumps to the while condition and reads past the end of the // object array (undefined behaviour); here: skip to the next enemy continue 3; } } while ($level->object[$j]->sprite->enabled == true); //object[j] is free! $this->UP_ANIMATION($enemy->sprite); $this->updateobjectsprite($level, $level->object[$j], $enemy->sprite->animValue() & 0x1FFF, true); $level->object[$j]->sprite->flipped = true; $level->object[$j]->sprite->x = $enemy->sprite->x; $level->object[$j]->sprite->y = $enemy->sprite->y; $level->object[$j]->sprite->droptobottom = true; $level->object[$j]->sprite->killing = true; $level->object[$j]->sprite->speedY = 0; $this->GRAVITY_FLAG = 4; $this->DOWN_ANIMATION($enemy->sprite); $enemy->counter = 0; break; case 18: //Guard (helicopter guy) if ($enemy->dying != 0) { //If not 0, the enemy is dying or dead, and have special movement $this->DEAD1($level, $enemy); continue 2; } if (($player->sprite->x < self::s16($enemy->initX - $enemy->rangeX)) || //Player is too far left ($player->sprite->x > self::s16($enemy->initX + $enemy->rangeX)) || //Player is too far right ($player->sprite->y < self::s16($enemy->initY - $enemy->rangeY)) || //Player is too high above ($player->sprite->y > self::s16($enemy->initY + $enemy->rangeY))) { //Player is too far below //The player is too far away, move enemy to center if ($enemy->initX != $enemy->sprite->x) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($enemy->initX > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; } if ($enemy->initY != $enemy->sprite->y) { if ($enemy->initY > $enemy->sprite->y) { $enemy->sprite->y += $enemy->sprite->speedY; } else { $enemy->sprite->y -= $enemy->sprite->speedY; } } } else { //The player is inside the guarded area, move enemy to player if ($player->sprite->x != $enemy->sprite->x) { $enemy->sprite->speedX = abs($enemy->sprite->speedX); if ($player->sprite->x > $enemy->sprite->x) { $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } $enemy->sprite->x -= $enemy->sprite->speedX; } if ($player->sprite->y != $enemy->sprite->y) { if ($player->sprite->y > $enemy->sprite->y) { $enemy->sprite->y += $enemy->sprite->speedY; } else { $enemy->sprite->y -= $enemy->sprite->speedY; } } } break; } //switch (level->enemy[i].NMI_ACTION & 0x1FFF) } //for (i = 0; i < NMI_BY_LEVEL; i++) } public function DEAD1(Level $level, Enemy $enemy): void { if ((($enemy->dying & 0x01) != 0) || //00000001 or 00000011 ($enemy->dead_sprite == -1)) { if (($enemy->dying & 0x01) == 0) { $enemy->dying = $enemy->dying | 0x01; $enemy->sprite->speedY = -10; $enemy->phase = 0; } if ($enemy->phase != 0xFF) { $enemy->sprite->y += $enemy->sprite->speedY; if ($this->SEECHOC_FLAG != 0) { $level->player->sprite2->y += $enemy->sprite->speedY; } if ($enemy->sprite->speedY < self::MAX_SPEED_DEAD) { $enemy->sprite->speedY++; } } } else { $enemy->dying = $enemy->dying | 0x01; $this->updateenemysprite($level, $enemy, $enemy->dead_sprite, false); $enemy->sprite->flash = false; $enemy->sprite->visible = false; $enemy->sprite->speedY = 0; $enemy->phase = self::u8(-1); } } public function updateenemysprite(Level $level, Enemy $enemy, int $number, bool $clearflags): int { $this->updatesprite($level, $enemy->sprite, $number, $clearflags); if (($number >= 101) && ($number <= 105)) { //Walking man $enemy->carry_sprite = 105; } elseif (($number >= 126) && ($number <= 130)) { //Fly $enemy->carry_sprite = 130; } elseif (($number >= 149) && ($number <= 153)) { //Skeleton $enemy->carry_sprite = 149; } elseif (($number >= 157) && ($number <= 158)) { //Worm $enemy->carry_sprite = 158; } elseif (($number >= 159) && ($number <= 167)) { //Guy with sword $enemy->carry_sprite = 167; } elseif (($number >= 185) && ($number <= 191)) { //Zombie $enemy->carry_sprite = 186; } elseif (($number >= 197) && ($number <= 203)) { //Woman with pot $enemy->carry_sprite = 203; } else { $enemy->carry_sprite = -1; } if (($number >= 172) && ($number <= 184)) { //Periscope $enemy->dead_sprite = 184; } elseif (($number >= 192) && ($number <= 196)) { //Camel $enemy->dead_sprite = 196; } elseif (($number >= 210) && ($number <= 213)) { //Old man with TV $enemy->dead_sprite = 213; } elseif (($number >= 214) && ($number <= 220)) { //Snake in pot $enemy->dead_sprite = 220; } elseif (($number >= 221) && ($number <= 226)) { //Man throwing knives $enemy->dead_sprite = 226; } elseif (($number >= 242) && ($number <= 247)) { //Carnivorous plant in pot $enemy->dead_sprite = 247; } else { $enemy->dead_sprite = -1; } if ((($number >= 248) && ($number <= 251)) || //Man throwing rocks (3rd level) (($number >= 252) && ($number <= 256)) || //Big baby (11th level) (($number >= 257) && ($number <= 261)) || //Big woman (7th level) (($number >= 263) && ($number <= 267)) || //Big man (15th level on Moktar only) (($number >= 284) && ($number <= 288)) || //Mummy (9th level) (($number >= 329) && ($number <= 332))) { //Ax man (5th level) $enemy->boss = true; } else { $enemy->boss = false; } return 0; } public function SET_NMI(Level $level): void { //Clear enemy sprites //If an enemy is on the screen // - Set bit 13 // - Animate // - Collision with player // - Loose life and fly // - Collision with object // - Decrease enemy's life $k = 0; for ($i = 0; $i < $level->enemycount; $i++) { //50 $enemy = $level->enemy[$i]; if (!($enemy->sprite->enabled)) continue; //Skip unused enemies $enemy->visible = false; //Is the enemy on the screen? if (($enemy->sprite->x + 32 < ($this->BITMAP_X << 4)) || //Left for the screen? ($enemy->sprite->x - 32 > ($this->BITMAP_X << 4) + self::screen_width * 16) || //Right for the screen? ($enemy->sprite->y < ($this->BITMAP_Y << 4)) || //Above the screen? ($enemy->sprite->y - 32 > ($this->BITMAP_Y << 4) + self::screen_height * 16)) { //Below the screen? if (($enemy->dying & 0x03) != 0) { //If the enemy is dying or dead and not on the screen, remove from the list! $enemy->sprite->enabled = false; } continue; } $enemy->visible = true; $this->GAL_FORM($level, $enemy); //Animation if (($enemy->dying & 0x03) != 0) { //If the enemy is dying or dead and not on the screen, remove from the list! continue; } if (($this->KICK_FLAG == 0) && !$this->GODMODE) { //Collision with the hero? if ($enemy->sprite->invisible) { continue; } $this->ACTIONC_NMI($level, $enemy); } $hit = 0; if ($this->GRAVITY_FLAG != 0) { //Collision with a moving object? for ($k = 0; $k < $level->objectcount; $k++) { if ($level->object[$k]->sprite->speedX == 0) { if ($level->object[$k]->sprite->speedY == 0) { continue; } if ($level->object[$k]->mass < 10) { continue; } } if ($level->object[$k]->objectdata->no_damage) { //Is the object a weapon (false) or not (true)? continue; } if ($this->NMI_VS_DROP($enemy->sprite, $level->object[$k]->sprite)) { $hit = 1; break; } } } if (($hit == 0) && $this->DROP_FLAG && !$this->CARRY_FLAG && ($level->player->sprite2->enabled)) { if ($this->NMI_VS_DROP($enemy->sprite, $level->player->sprite2)) { $this->INVULNERABLE_FLAG = 0; $level->player->sprite2->enabled = false; $this->SEE_CHOC($level); $hit = 2; } } if ($hit != 0) { if ($hit == 1) { if ($level->object[$k]->sprite->number != 73) { //Change direction of the object, except if the object is a small iron ball $level->object[$k]->sprite->speedX = 0 - $level->object[$k]->sprite->speedX; } } //If final enemy, remove energy $this->FX_START(1); $this->DROP_FLAG = false; if ($enemy->boss) { if ($this->INVULNERABLE_FLAG != 0) { //j++; continue; } $this->INVULNERABLE_FLAG = 10; $enemy->sprite->flash = true; //flash $this->BIGNMI_POWER = self::u8($this->BIGNMI_POWER - 1); if ($this->BIGNMI_POWER != 0) { //j++; continue; } $this->boss_alive = false; } $enemy->dying = $enemy->dying | 0x02; //Kill the enemy } } } public function GAL_FORM(Level $level, Enemy $enemy): void //Enemy animation { $enemy->sprite->invisible = false; if (($enemy->dying & 0x03) != 0) { $enemy->sprite->visible = false; $enemy->visible = true; return; } $enemy->trigger = false; $table = $enemy->sprite->animTable; $image = $enemy->sprite->anim; //Animation pointer while ($table[$image] < 0) { $image += ($table[$image] >> 1); //jump back to start of animation } if ($table[$image] == 0x55AA) { $enemy->sprite->invisible = true; return; } $enemy->trigger = ($table[$image] & 0x2000) != 0; $this->updateenemysprite($level, $enemy, ($table[$image] & 0x00FF) + self::FIRST_NMI, true); $enemy->sprite->flipped = ($enemy->sprite->speedX < 0) ? true : false; $image++; if ($table[$image] < 0) { $image += ($table[$image] >> 1); //jump back to start of animation } $enemy->sprite->anim = $image; $enemy->visible = true; } public function ACTIONC_NMI(Level $level, Enemy $enemy): void { switch ($enemy->type) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: case 13: case 14: case 18: if ($this->NMI_VS_DROP($enemy->sprite, $level->player->sprite)) { if ($enemy->type != 11) { //Walk and shoot if ($enemy->sprite->number != 178) { //Periscope $enemy->sprite->speedX = 0 - $enemy->sprite->speedX; } } if (($enemy->sprite->number >= self::FIRST_NMI + 53) && ($enemy->sprite->number <= self::FIRST_NMI + 55)) { //Fireball $this->GRANDBRULE_FLAG = true; } if ($enemy->power != 0) { $this->KICK_ASH($level, $enemy->sprite, $enemy->power); } } break; } } public function KICK_ASH(Level $level, Sprite $enemysprite, int $power): void { $this->FX_START(4); $p_sprite = $level->player->sprite; $this->DEC_ENERGY($level); $this->DEC_ENERGY($level); $this->KICK_FLAG = 24; $this->CHOC_FLAG = 0; $this->LAST_ORDER = 0; $p_sprite->speedX = $power; if ($p_sprite->x <= $enemysprite->x) { $p_sprite->speedX = 0 - $p_sprite->speedX; } $p_sprite->speedY = -8 * 16; $this->FORCE_POSE($level); } public function NMI_VS_DROP(Sprite $enemysprite, Sprite $sprite): bool { if (abs($sprite->x - $enemysprite->x) >= 64) { return false; } if (abs($sprite->y - $enemysprite->y) >= 70) { return false; } if ($sprite->y < $enemysprite->y) { //Enemy is below the offending object if ($sprite->y <= $enemysprite->y - $enemysprite->spritedata->collheight + 3) return false; //The offending object is too high for collision } else { //Offending object is below the enemy if ($enemysprite->y <= $sprite->y - $sprite->spritedata->collheight + 3) return false; //The enemy is too high for collision } $enemyleft = self::s16($enemysprite->x - $enemysprite->spritedata->refwidth); $objectleft = self::s16($sprite->x - $sprite->spritedata->refwidth); if ($enemyleft >= $objectleft) { //The object is left for the enemy if (($objectleft + ($sprite->spritedata->collwidth >> 1)) <= $enemyleft) { return false; //The object is too far left } } else { //Enemy is left for the object if (($enemyleft + ($enemysprite->spritedata->collwidth >> 1)) <= $objectleft) { return false; //The enemy is too far left } } return true; //Collision! } public function SEE_CHOC(Level $level): void { $this->updatesprite($level, $level->player->sprite2, self::FIRST_OBJET + 15, true); //Hit (a throw hits an enemy) $level->player->sprite2->speedX = 0; $level->player->sprite2->speedY = 0; $this->SEECHOC_FLAG = 5; } public function MOVE_TRASH(Level $level): void { for ($i = 0; $i < $level->trashcount; $i++) { $trash = $level->trash[$i]; if (!$trash->enabled) continue; if ($trash->speedX != 0) { $trash->x += ($trash->speedX >> 4); $tmp = self::s16(($trash->x >> 4) - $this->BITMAP_X); if (($tmp < 0) || ($tmp > self::screen_width)) { $trash->enabled = false; continue; } if ($tmp != 0) { //Bug in the code $trash->y += ($trash->speedY >> 4); $tmp = self::s16(($trash->y >> 4) - $this->BITMAP_Y); if (($tmp < 0) || ($tmp > self::screen_height * 16)) { //Bug? $trash->enabled = false; continue; } } } if (!$this->GODMODE && $this->NMI_VS_DROP($trash, $level->player->sprite)) { //Trash vs player $trash->x -= $trash->speedX; $this->KICK_ASH($level, $trash, 70); $trash->enabled = false; continue; } } } public function FIND_TRASH(Level $level, ?Sprite &$trash): bool { for ($i = 0; $i < $level->trashcount; $i++) { if ($level->trash[$i]->enabled == false) { $trash = $level->trash[$i]; return true; } } return false; } public function PUT_BULLET(Level $level, Enemy $enemy, Sprite $bullet): void { $bullet->x = $enemy->sprite->x; $bullet->y = $enemy->sprite->y - self::s8($enemy->sprite->animValue(-1) & 0x00FF); $this->updatesprite($level, $bullet, ($enemy->sprite->animValue(-2) & 0x1FFF) + self::FIRST_OBJET, true); if ($enemy->sprite->x < $level->player->sprite->x) { $bullet->speedX = 16 * 11; $bullet->flipped = true; } else { $bullet->speedX = -16 * 11; $bullet->flipped = false; } $bullet->speedY = 0; $bullet->x += $bullet->speedX >> 4; } public function UP_ANIMATION(Sprite $sprite): void { $last = count($sprite->animTable ?? []) - 1; do { $sprite->anim++; } while ($sprite->anim < $last && $sprite->animValue() >= 0); // (bounded: the C code reads outside the table) $sprite->anim++; } public function DOWN_ANIMATION(Sprite $sprite): void { do { $sprite->anim--; } while ($sprite->anim > 0 && $sprite->animValue() >= 0); // (bounded: the C code reads outside the table) $sprite->anim--; }}