#include "ballos.h"

#include "../../ObjManager.h"
#include "../../autogen/sprites.h"
#include "../../caret.h"
#include "../../common/misc.h"
#include "../../Utils/Logger.h"
#include "../../game.h"
#include "../../graphics/Renderer.h"
#include "../../map.h"
#include "../../player.h"
#include "../../screeneffect.h"
#include "../../sound/SoundManager.h"
#include "../../trig.h"
#include "../ai.h"
#include "../stdai.h"
#include "../sym/smoke.h"

using namespace NXE::Graphics;

static int platform_speed;
static int rotators_left;
#define FLOOR_Y 0x26000                 // Y coord of floor
#define CRASH_Y (FLOOR_Y - (40 * CSFI)) // Y coord of main when body hits floor

enum EYE_STATES
{
  EYE_OPENING   = 10,
  EYE_CLOSING   = 20,
  EYE_INVISIBLE = 30,
  EYE_EXPLODING = 40
};

enum BALLOS_STATES
{
  // Form 1 states
  BALLOSA_COME_DOWN    = 100, // scripted
  BALLOSA_BEGIN_FIGHT  = 200, // scripted
  BALLOSA_PREPARE_JUMP = 210,
  BALLOSA_JUMPING      = 215,
  BALLOSA_DEFEATED     = 220, // scripted

  // Form 2 states.
  BALLOSB_ENTER_FORM  = 300, // scripted
  BALLOSB_FIGHT_BEGIN = 311, // scripted
  BALLOSB_LEFT        = 320,
  BALLOSB_UP          = 330,
  BALLOSB_RIGHT       = 340,
  BALLOSB_DOWN        = 350,

  // Form 3 states
  BALLOSC_ENTER_FORM     = 400,
  BALLOSC_SPAWN_SPIKES   = 410,
  BALLOSC_EXPLODE_BLOODY = 420,
  BALLOSC_SPIN_PLATFORMS = 430
};

INITFUNC(AIRoutines)
{
  ONDEATH(OBJ_BALLOS_MAIN, ondeath_ballos);

  ONTICK(OBJ_BALLOS_ROTATOR, ai_ballos_rotator);
  AFTERMOVE(OBJ_BALLOS_ROTATOR, aftermove_ballos_rotator);

  ONTICK(OBJ_BALLOS_PLATFORM, ai_ballos_platform);
}

void BallosBoss::OnMapEntry(void)
{
  // create (invisible) main controller object
  main                  = CreateObject(0, 0, OBJ_BALLOS_MAIN);
  game.stageboss.object = main;

  main->id2   = 1000; // defeated script (has a flagjump in it to handle each form)
  main->flags = (FLAG_SHOW_FLOATTEXT | FLAG_SCRIPTONDEATH | FLAG_SOLID_BRICK | FLAG_IGNORE_SOLID);

  main->x = ((map.xsize / 2) * TILE_W) * CSFI;
  main->y = -(64 * CSFI);

  main->damage = 0;
  main->hp     = 800;

  objprop[main->type].hurt_sound = NXE::Sound::SFX::SND_ENEMY_HURT_COOL;
  main->invisible                = true;

  // create body (the big rock)
  body        = CreateObject(0, 0, OBJ_BALLOS_BODY);
  body->hp    = 1000; // not his real HP, we're using damage transfer
  body->flags = (FLAG_SOLID_MUSHY | FLAG_SHOOTABLE | FLAG_INVULNERABLE | FLAG_IGNORE_SOLID);

  // create eyes (open/close animations)
  for (int i = 0; i < NUM_EYES; i++)
  {
    eye[i]      = CreateObject(0, 0, OBJ_BALLOS_EYE);
    eye[i]->dir = i;
    eye[i]->hp  = 1000;
  }

  // create a top shield to cover eyes from above
  shield            = CreateObject(0, 0, OBJ_BBOX_PUPPET);
  shield->sprite    = SPR_BBOX_PUPPET_1;
  shield->invisible = true;
  shield->hp        = 1000;
  shield->flags     = (FLAG_SOLID_MUSHY | FLAG_SHOOTABLE | FLAG_INVULNERABLE | FLAG_IGNORE_SOLID);

  // initilize bboxes
  Renderer::getInstance()->sprites.sprites[body->sprite].bbox[0].set(-48, -24, 48, 32);
  Renderer::getInstance()->sprites.sprites[shield->sprite].bbox[0].set(-32, -8, 32, 8);
  Renderer::getInstance()->sprites.sprites[main->sprite].bbox[0].set(-32, -48, 32, 48);

  Renderer::getInstance()->sprites.sprites[main->sprite].solidbox   = Renderer::getInstance()->sprites.sprites[main->sprite].bbox[0];
  Renderer::getInstance()->sprites.sprites[body->sprite].solidbox   = Renderer::getInstance()->sprites.sprites[body->sprite].bbox[0];
  Renderer::getInstance()->sprites.sprites[shield->sprite].solidbox = Renderer::getInstance()->sprites.sprites[shield->sprite].bbox[0];

  // body and eyes are both directly shootable during one form or another
  // but should not shake as their damage is to be transferred to main object.
  objprop[OBJ_BALLOS_MAIN].shaketime = 8;
  objprop[OBJ_BALLOS_BODY].shaketime = 0;
  objprop[OBJ_BALLOS_EYE].shaketime  = 0;

  // initilize parameters
  LOG_DEBUG("BallosBoss::OnMapEntry()");
}

/*
void c------------------------------() {}
*/

void BallosBoss::Run()
{
  if (!main)
    return;
  // AIDEBUG;

  transfer_damage(body, main);
  transfer_damage(eye[LEFT], main);
  transfer_damage(eye[RIGHT], main);
  transfer_damage(shield, main);

  RunForm1(main);
  RunForm2(main);
  RunForm3(main);
  RunDefeated(main);

  run_eye(LEFT);
  run_eye(RIGHT);

  // flash red when hurt
  if (main->shaketime & 2)
    body->frame |= 1;
  else
    body->frame &= ~1;
}

void BallosBoss::RunAftermove()
{
  if (!main)
    return;

  // place eyes
  place_eye(LEFT);
  place_eye(RIGHT);

  // place body
  body->x = main->x;
  body->y = main->y;

  // place shield
  shield->x = main->x;
  shield->y = main->y - (44 * CSFI);

  // riding on platform by eye? Player can sort of stay on this platform
  // when he jumps. We don't do this for the shield up top though, in order that
  // he gets kind of slid off--what happens is he'll fall through the shield
  // onto the main body (a SOLID_BRICK), and then now that he's embedded in
  // the shield (a SOLID_MUSHY), it'll repel him to the side.
  if (player->riding == body)
  {
    player->apply_xinertia(main->xinertia);
    player->apply_yinertia(main->yinertia);
  }
}

/*
void c------------------------------() {}
*/

// left and right maximums during form 1
static const int F1_LEFT  = (88 * CSFI);
static const int F1_RIGHT = (552 * CSFI);

// runs arrival of first form as a stage-boss
void BallosBoss::RunComeDown(Object *o)
{
  switch (o->state)
  {
    case BALLOSA_COME_DOWN:
    {
      o->savedhp = o->hp;

      o->x     = player->CenterX();
      o->y     = -(64 * CSFI);
      o->frame = 0;

      // create the targeter
      // setting dir to right tells it don't spawn any lightning
      CreateObject(o->x, FLOOR_Y, OBJ_BALLOS_TARGET)->dir = RIGHT;
      o->timer                                            = 0;

      if (o->x < F1_LEFT)
        o->x = F1_LEFT;
      if (o->x > F1_RIGHT)
        o->x = F1_RIGHT;

      o->state++;
    }
    case BALLOSA_COME_DOWN + 1:
    {
      if (++o->timer > 30)
        o->state++;
    }
    break;

    // falling
    case BALLOSA_COME_DOWN + 2:
    {
      o->yinertia += 0x40;
      LIMITY(0xC00);

      if (passed_ycoord(GREATER_THAN, CRASH_Y))
      {
        o->yinertia = 0;
        o->timer    = 0;
        o->state++;

        megaquake(30, NXE::Sound::SFX::SND_MISSILE_HIT);

        // player smush damage
        // (he could only get that low if he had been pushed into the floor)
        if (player->y > (o->y + (48 * CSFI)))
          hurtplayer(16);

        SmokeXY(o->x, o->y + (40 * CSFI), 16, 40, 0);

        if (player->blockd)
          player->yinertia = -0x200;
      }
    }
    break;

    case BALLOSA_COME_DOWN + 3:
    {
      if (++o->timer > 31)
      {
        SetEyeStates(EYE_OPENING);
        o->state++;
      }
    }
    break;
  }
}

// 1st form as a stageboss.
// the one where he jumps around as a rock.
void BallosBoss::RunForm1(Object *o)
{
  RunComeDown(o);

  switch (o->state)
  {
    case BALLOSA_BEGIN_FIGHT:
    {
      // can be damaged between eyes opening and boss bar appearing,
      // but it is not counted.
      o->hp    = o->savedhp;
      o->state = BALLOSA_PREPARE_JUMP;
    }
    case BALLOSA_PREPARE_JUMP: // delay, then jump at player
    {
      o->xinertia = 0;
      o->damage   = 0;
      o->state++;

      // he makes two jumps then a pause,
      // after that it's three jumps before pausing.
      // this corresponds to:
      if ((++o->timer2 % 3) == 0)
        o->timer = 150;
      else
        o->timer = 50;
    }
    case BALLOSA_PREPARE_JUMP + 1:
    {
      if (--o->timer <= 0)
      {
        o->yinertia = -0xC00;
        o->xinertia = (o->x < player->x) ? 0x200 : -0x200;
        o->state    = BALLOSA_JUMPING;
      }
    }
    break;

    case BALLOSA_JUMPING:
    {
      o->yinertia += 0x55;
      LIMITY(0xC00);

      if (passed_xcoord(LESS_THAN, F1_LEFT))
        o->xinertia = 0x200;
      if (passed_xcoord(GREATER_THAN, F1_RIGHT))
        o->xinertia = -0x200;

      if (passed_ycoord(GREATER_THAN, CRASH_Y))
      {
        // player smush damage
        if (player->y > (o->y + (48 * CSFI)))
          hurtplayer(16);

        // player hopping from the vibration
        if (player->blockd)
          player->yinertia = -0x200;

        megaquake(30, NXE::Sound::SFX::SND_MISSILE_HIT);

        CreateObject(o->x - (12 * CSFI), o->y + (52 * CSFI), OBJ_BALLOS_BONE_SPAWNER)->dir = LEFT;
        CreateObject(o->x + (12 * CSFI), o->y + (52 * CSFI), OBJ_BALLOS_BONE_SPAWNER)->dir = RIGHT;

        SmokeXY(o->x, o->y + (40 * CSFI), 16, 40, 0);

        o->yinertia = 0;
        o->state    = BALLOSA_PREPARE_JUMP;
      }
    }
    break;

    // 1st form defeated
    case BALLOSA_DEFEATED:
    {
      SetEyeStates(EYE_CLOSING);
      game.bossbar.defeated = true;
      o->hp                 = 1200;

      o->state++;

      o->xinertia  = 0;
      o->shaketime = 0;
    }
    case BALLOSA_DEFEATED + 1:
    {
      o->yinertia += 0x40;
      LIMITY(0xC00);

      if (passed_ycoord(GREATER_THAN, CRASH_Y))
      {
        o->yinertia = 0;
        o->state++;

        megaquake(30, NXE::Sound::SFX::SND_MISSILE_HIT);
        SmokeXY(o->x, o->y + 0x5000, 16, 40, 0);

        if (player->blockd)
          player->yinertia = -0x200;

        // ... and wait for script to trigger form 2
      }
    }
    break;
  }
}

static void SetRotatorStates(int newstate)
{
  Object *o;
  FOREACH_OBJECT(o)
  {
    if (o->type == OBJ_BALLOS_ROTATOR)
      o->state = newstate;
  }
}

// 2nd form as a stageboss.
// the one where he spawns spiky rotators and circles around the room.
void BallosBoss::RunForm2(Object *o)
{
  static const int BALLOSB_SPEED     = 0x3AA;
  static const int ARENA_LEFT   = (119 * CSFI);
  static const int ARENA_TOP    = (119 * CSFI);
  static const int ARENA_RIGHT  = (521 * CSFI);
  static const int ARENA_BOTTOM = (233 * CSFI);

  switch (o->state)
  {
    // enter 2nd form (script-triggered)
    case BALLOSB_ENTER_FORM:
    {
      o->timer = 0;
      o->state++;

      rotators_left = 0;
      for (int angle = 0; angle <= 0x100; angle += 0x20)
      {
        Object *r = CreateObject(o->x, o->y, OBJ_BALLOS_ROTATOR);
        r->angle  = angle;
        r->dir    = (rotators_left & 1) ? RIGHT : LEFT;

        rotators_left++;
      }
    }
    case BALLOSB_ENTER_FORM + 1:
    {
      o->y += (ARENA_BOTTOM - o->y) / 8;

      if (passed_xcoord(LESS_THAN, ARENA_LEFT, false))
        o->x += (ARENA_LEFT - o->x) / 8;

      if (passed_xcoord(GREATER_THAN, ARENA_RIGHT, false))
        o->x += (ARENA_RIGHT - o->x) / 8;
    }
    break;

    case BALLOSB_FIGHT_BEGIN: // script-triggered
    {
      SetRotatorStates(10); // spin CCW, work as treads
      o->state = BALLOSB_LEFT;
      o->timer = 0;
    }
    case BALLOSB_LEFT: // left on floor
    {
      o->xinertia = -BALLOSB_SPEED;
      o->yinertia = 0;
      o->dirparam = LEFT;

      if (passed_xcoord(LESS_THAN, ARENA_LEFT))
        o->state = BALLOSB_UP;
    }
    break;

    // up on wall
    case BALLOSB_UP:
    {
      o->xinertia = 0;
      o->yinertia = -BALLOSB_SPEED;
      o->dirparam = UP;

      if (passed_ycoord(LESS_THAN, ARENA_TOP))
        o->state = BALLOSB_RIGHT;
    }
    break;

    // right on ceiling
    case BALLOSB_RIGHT:
    {
      o->xinertia = BALLOSB_SPEED;
      o->yinertia = 0;
      o->dirparam = RIGHT;

      // all rotators destroyed?
      if (rotators_left <= 0 && ++o->timer > 3)
      {
        // center of room
        if (o->x >= (312 * CSFI) && o->x <= (344 * CSFI))
        {
          o->state = BALLOSC_ENTER_FORM;
        }
      }

      if (passed_xcoord(GREATER_THAN, ARENA_RIGHT))
        o->state = BALLOSB_DOWN;
    }
    break;

    // down on wall
    case BALLOSB_DOWN:
    {
      o->xinertia = 0;
      o->yinertia = BALLOSB_SPEED;
      o->dirparam = DOWN;

      if (passed_ycoord(GREATER_THAN, ARENA_BOTTOM))
      {
        o->state = BALLOSB_LEFT;
      }
    }
    break;
  }
}

// form 3 as a stageboss, the final form.
// he reaches the center of the room, platforms come out, spikes go up,
// and he explodes into a really bloody version of himself.
//
// then the platforms spin in various speeds and directions while he
// spawns red butes from the sides and his top.
void BallosBoss::RunForm3(Object *o)
{
  static const int YPOSITION = (167 * CSFI);

  // platform spin speeds and how long they travel at each speed.
  // it's a repeating pattern.
  static const struct
  {
    int length, speed;
  } platform_pattern[] = {{500, 2}, {200, 1}, {20, 0}, {200, -1}, {500, -2}, {200, -1}, {20, 0}, {200, 1}, {0, 0}};

  switch (o->state)
  {
    // enter form 3
    case BALLOSC_ENTER_FORM:
    {
      o->timer    = 0;
      o->xinertia = 0;
      o->yinertia = 0;
      o->state++;

      DeleteObjectsOfType(OBJ_GREEN_DEVIL_SPAWNER);
      SetRotatorStates(20); // fast spin CCW
    }
    case BALLOSC_ENTER_FORM + 1:
    {
      // come down into center of room
      o->y += (YPOSITION - o->y) / 8;
      o->timer++;

      if (o->timer == 50)
      {
        // create platforms
        platform_speed = 0;

        for (int angle = 0; angle < 0x100; angle += 0x20)
        {
          Object *p   = CreateObject(o->x, o->y, OBJ_BALLOS_PLATFORM);
          p->dirparam = angle;
        }
      }

      if (o->timer > 100)
      {
        platform_speed = -1;

        o->state = BALLOSC_SPAWN_SPIKES;
        o->timer = 0;
      }
    }
    break;

    case BALLOSC_SPAWN_SPIKES:
    {
      o->timer = 0;
      o->xmark = 0;
      o->state++;
    }
    case BALLOSC_SPAWN_SPIKES + 1:
    {
      o->timer++;

      if ((o->timer % 3) == 0)
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_QUAKE);

      if ((o->timer % 30) == 1)
      {
        o->xmark += 2;
        CreateObject((o->xmark * TILE_W) * CSFI, FLOOR_Y + (48 * CSFI), OBJ_BALLOS_SPIKES);

        if (o->xmark == 38)
          o->state = BALLOSC_EXPLODE_BLOODY;
      }
    }
    break;

    // explode into all bloody
    case BALLOSC_EXPLODE_BLOODY:
    {
      SetEyeStates(EYE_INVISIBLE);
      SetRotatorStates(30); // slow spin CW, alternate open/closed

      SmokeClouds(o, 256, 60, 60); // ka boom!
      NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_EXPLOSION1);
      megaquake(30);

      body->frame |= 2; // go all bloody
      body->flags &= ~FLAG_INVULNERABLE;
      shield->flags &= ~FLAG_INVULNERABLE;

      o->state = BALLOSC_SPIN_PLATFORMS;
    }
    // fall-through
    case BALLOSC_SPIN_PLATFORMS:
    {
      o->state++;
      o->timer  = 0;
      o->timer2 = 0;
      o->timer3 = 0;

      platform_speed = platform_pattern[o->timer2].speed;
    }
    case BALLOSC_SPIN_PLATFORMS + 1:
    {
      // spin platforms
      if (++o->timer3 > platform_pattern[o->timer2].length)
      {
        o->timer3 = 0;
        o->timer2++;

        if (!platform_pattern[o->timer2].length)
          o->timer2 = 0;

        platform_speed = platform_pattern[o->timer2].speed;
      }

      // spawn butes
      switch (++o->timer)
      {
        case 270: // spawn swordsmen from face
        case 280:
        case 290:
        {
          SmokeXY(o->x, o->y - (52 * CSFI), 4);
          CreateObject(o->x, o->y - (52 * CSFI), OBJ_BUTE_SWORD_RED)->dir = UP;
          NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_EM_FIRE);
        }
        break;

        case 300: // spawn archers on side
        {
          o->timer = 0;
          // direction butes will be facing, not side of screen
          int dir = (player->CenterX() > o->x) ? LEFT : RIGHT;

          for (int i = 0; i < 8; i++)
          {
            // give some granularity to the coords,
            // so that they can't overlap too closely.
            int x = (random(-TILE_W, TILE_W) & ~3) * CSFI;
            int y = (random(2 * TILE_H, 17 * TILE_H) & ~3) * CSFI;
            if (dir == LEFT)
              x += MAPX(map.xsize - 1);

            CreateObject(x, y, OBJ_BUTE_ARCHER_RED)->dir = dir;
          }
        }
        break;
      }

      // spawn blood
      int prob = (o->hp <= 500) ? 4 : 10;
      if (!random(0, prob))
      {
        CreateObject(o->x + random(-40, 40) * CSFI, o->y + random(0, 40) * CSFI, OBJ_RED_ENERGY)->angle = DOWN;
      }
    }
    break;
  }
}

void BallosBoss::RunDefeated(Object *o)
{
  switch (o->state)
  {
    case 1000:
    {
      o->state = 1001;
      o->timer = 0;

      SetEyeStates(EYE_EXPLODING); // blow out eyes
      SetRotatorStates(1000);      // explode rotators

      uint32_t mask = ~(FLAG_SOLID_BRICK | FLAG_SOLID_MUSHY | FLAG_SHOOTABLE | FLAG_INVULNERABLE);
      main->flags &= mask;
      body->flags &= mask;
      shield->flags &= mask;
      eye[LEFT]->flags &= mask;
      eye[RIGHT]->flags &= mask;
    }
    case 1001:
    {
      int x = o->x + random(-60, 60) * CSFI;
      int y = o->y + random(-60, 60) * CSFI;
      SmokePuff(x, y);
      effect(x, y, EFFECT_BOOMFLASH);

      o->timer++;

      if ((o->timer % 12) == 0)
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_MISSILE_HIT);

      if (o->timer > 150)
      {
        o->timer = 0;
        o->state = 1002;

        starflash.Start(o->x, o->y);
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_EXPLOSION1);
      }
    }
    break;

    case 1002:
    {
      megaquake(40);

      if (++o->timer >= 50)
      {
        KillObjectsOfType(OBJ_BUTE_ARCHER_RED);
        KillObjectsOfType(OBJ_BALLOS_SPIKES);

        body->invisible       = true;
        eye[LEFT]->invisible  = true;
        eye[RIGHT]->invisible = true;
        o->state              = 1003;
      }
    }
    break;
  }
}

void ondeath_ballos(Object *o)
{
  // as soon as one of his forms is defeated make him non-killable
  // until the init for the next form runs and makes him killable again.
  // intended to fix the extremely rare possibility of killing him completely
  // after his 1st form instead of moving on to the spiky rotators like he should.
  o->hp = 999999;
}

/*
void c------------------------------() {}
*/

// Handles his eyes.
//
// When closed, the eyes are like "overlay" objects that replace the open eyes
// drawn on the body, and allow animating them seperately from the body.
//
// When open, the eyes turn invisible and are used as shoot-points to detect shots
// hitting the eyes drawn on the body.
void BallosBoss::run_eye(int index)
{
  Object *o = eye[index];

  switch (o->state)
  {
    case 0:
    {
      o->flags = (FLAG_SHOOTABLE | FLAG_INVULNERABLE);
      o->state = 1;
    }
    break;

    // open eyes
    case EYE_OPENING:
    {
      o->frame     = 0;
      o->animtimer = 0;
      o->state++;
    }
    case EYE_OPENING + 1:
    {
      if (++o->animtimer > 2)
      {
        o->animtimer = 0;
        if (++o->frame >= 3)
        {
          o->flags &= ~FLAG_INVULNERABLE;
          o->invisible = true;
          o->state++;
        }
      }
    }
    break;

    // close eyes
    case EYE_CLOSING:
    {
      o->frame     = 3;
      o->invisible = false;
      o->flags |= FLAG_INVULNERABLE;

      o->animtimer = 0;
      o->state++;
    }
    case EYE_CLOSING + 1:
    {
      if (++o->animtimer > 2)
      {
        o->animtimer = 0;
        if (--o->frame <= 0)
        {
          o->frame = 0;
          o->state++;
        }
      }
    }
    break;

    // invisible (the underlying eyes drawn on the body are what are seen)
    case EYE_INVISIBLE:
    {
      o->flags &= ~FLAG_INVULNERABLE;
      o->invisible = true;
      o->state++;
    }
    break;

    // explode eyes (final defeat sequence)
    case EYE_EXPLODING:
    {
      o->frame     = 4; // empty eyes
      o->invisible = false;

      o->flags &= ~(FLAG_SHOOTABLE | FLAG_INVULNERABLE);
      o->state++;

      if (o->dir == LEFT)
        SmokeXY(o->x - (4 * CSFI), o->y, 10, 4, 4);
      else
        SmokeXY(o->x + (4 * CSFI), o->y, 10, 4, 4);
    }
    break;
  }
}

void BallosBoss::place_eye(int index)
{
  Object *o = eye[index];

  if (o->dir == LEFT)
    o->x = main->x - (24 * CSFI);
  else
    o->x = main->x + (24 * CSFI);

  o->y = main->y - (36 * CSFI);
}

void BallosBoss::SetEyeStates(int newstate)
{
  eye[LEFT]->state  = newstate;
  eye[RIGHT]->state = newstate;
}

/*
void c------------------------------() {}
*/

static void make_puff(int x, int y, int bd)
{
  Object *o = SmokePuff(x, y);

  // make sure the smoke puff is traveling away from floor/wall
  switch (bd)
  {
    case LEFT:
      o->yinertia = -abs(o->yinertia);
      break;
    case UP:
      o->xinertia = abs(o->xinertia);
      break;
    case RIGHT:
      o->yinertia = abs(o->yinertia);
      break;
    case DOWN:
      o->xinertia = -abs(o->xinertia);
      break;
  }
}

// spawns impact smokeclouds/skulls as the rotators hit the ground/walls
static void spawn_impact_puffs(Object *o)
{
  Object *ballos = game.stageboss.object;

#define SHORT (8 * CSFI)
#define LONG (12 * CSFI)
#define HITANGLE 12
  static const struct
  {
    int wallangle;
    int xoffs1, xoffs2;
    int yoffs1, yoffs2;
  } hitdata[] = {
      {0x180, SHORT, -SHORT, -LONG, -LONG}, // RIGHT on ceiling
      {0x80, SHORT, -SHORT, LONG, LONG},    // LEFT on floor
      {0x100, -LONG, -LONG, SHORT, -SHORT}, // UP left wall
      {0x00, LONG, LONG, SHORT, -SHORT}     // DOWN right wall
  };

  int bd = ballos->dirparam;
  if (o->timer2 == hitdata[bd].wallangle + HITANGLE)
  {
    make_puff(o->x + hitdata[bd].xoffs1, o->y + hitdata[bd].yoffs1, bd);
    make_puff(o->x + hitdata[bd].xoffs2, o->y + hitdata[bd].yoffs2, bd);
    NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_QUAKE);

    if (bd == RIGHT) // on ceiling
    {
      CreateObject(o->x - SHORT, o->y - LONG, OBJ_BALLOS_SKULL);
    }
  }
}

void ai_ballos_rotator(Object *o)
{
  switch (o->state)
  {
    case 0: // just spawned
    {
      o->state  = 1;
      o->timer2 = o->angle * 2;

      o->timer3 = 0xC0;
      o->damage = 14;
    }
    case 1: // expanding outward; overshoot a bit
    {
      if (o->timer3 < 0x140)
        o->timer3 += 0x08;
      else
        o->state = 2;
    }
    break;
    case 2: // come back in to correct distance
    {
      if (o->timer3 > 0x130)
        o->timer3 -= 0x04;
      else
        o->state = 3;
    }
    break;

    // spinning CCW during form 2 (working like treads)
    case 10:
    {
      o->state = 11;

      o->flags |= FLAG_SHOOTABLE;
      o->flags &= ~FLAG_INVULNERABLE;
      o->hp = 1000;
    }
    case 11: // spinning during phase 2, alive
    {
      o->timer2 -= 2;
      if (o->timer2 < 0)
        o->timer2 += 0x200;

      if (o->frame != 2) // still undestroyed?
      {
        o->frame = (o->shaketime & 2) ? 1 : 0;

        if (o->hp <= (1000 - 100))
        {
          o->flags &= ~FLAG_SHOOTABLE;
          o->frame = 2; // close eye

          SmokeClouds(o, 32, 16, 16);
          NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_LITTLE_CRASH);

          rotators_left--;
        }
      }

      spawn_impact_puffs(o);
    }
    break;

    case 20: // spinning fast CCW while spikes come up
    {
      o->frame = 2;

      o->timer2 -= 4;
      if (o->timer2 < 0)
        o->timer2 += 0x200;
    }
    break;

    case 30: // beginning form 3
    {
      o->state  = 31;
      o->hp     = 1000;
      o->damage = 10;

      // this dir was set when they were created and
      // alternates left/right around the circle
      if (o->dir == LEFT)
      {
        o->flags |= FLAG_SHOOTABLE;
        o->frame = 0;
      }
      else
      {
        o->flags |= FLAG_INVULNERABLE;
        o->frame = 2;
      }
    }
    case 31: // form 3 CW slow spin
    {
      // come in closer to main object
      if (o->timer3 > 0x100)
        o->timer3--;

      // spin CW
      if (++o->timer2 > 0x200)
        o->timer2 -= 0x200;

      if (o->flags & FLAG_SHOOTABLE)
      {
        o->frame = (o->shaketime & 2) ? 1 : 0;

        if (o->hp < (1000 - 100))
        {
          o->xinertia = 0;
          o->yinertia = 0;

          o->flags &= ~(FLAG_SHOOTABLE | FLAG_IGNORE_SOLID);
          SmokeClouds(o, 32, 16, 16);
          NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_LITTLE_CRASH);

          o->frame  = 2;
          o->state  = 40;
          o->damage = 5;

          // blow up immediately if Ballos is defeated
          o->timer2 = 0;
        }
      }
    }
    break;

    case 40: // destroyed during phase 3, bouncing
    {
      o->yinertia += 0x20;
      LIMITY(0x5ff);

      if (o->blockl)
        o->xinertia = 0x100;
      if (o->blockr)
        o->xinertia = -0x100;

      if (o->blockd && o->yinertia >= 0)
      {
        // first time they hit they head toward player, after that
        // they keep going in same direction until hit wall
        if (o->xinertia == 0)
          o->xinertia = (o->CenterX() < player->CenterX()) ? 0x100 : -0x100;

        o->yinertia = -0x800;
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_QUAKE);
      }
    }
    break;

    case 1000: // Ballos was defeated
    {
      o->state    = 1001;
      o->xinertia = 0;
      o->yinertia = 0;

      o->frame = 2;
      o->flags &= ~(FLAG_SHOOTABLE | FLAG_IGNORE_SOLID);
      o->damage = 0;

      o->timer2 /= 4;
    }
    case 1001:
    {
      // explode one by one going clockwise
      if (o->timer2 <= 0)
      {
        SmokeClouds(o, 32, 16, 16);
        NXE::Sound::SoundManager::getInstance()->playSfx(NXE::Sound::SFX::SND_LITTLE_CRASH);
        effect(o->CenterX(), o->CenterY(), EFFECT_BOOMFLASH);
        o->Delete();
      }
      else
      {
        o->timer2--;
        o->frame = (o->timer2 & 2) ? 1 : 0;
      }
    }
    break;
  }
}

void aftermove_ballos_rotator(Object *o)
{
  if (o->state < 40)
  {
    Object *ballos = game.stageboss.object;
    if (!ballos)
      return;

    uint8_t angle = (o->timer2 / 2);
    int dist      = (o->timer3 / 4) * CSFI;

    o->x = ballos->x + (xinertia_from_angle(angle, dist));
    o->y = ballos->y + (yinertia_from_angle(angle, dist));
  }
}

/*
void c------------------------------() {}
*/

void ai_ballos_platform(Object *o)
{
  Object *ballos = game.stageboss.object;
  if (!ballos)
    return;

  if (ballos->state >= 1000) // defeated
  {
    if (o->state < 1000)
      o->state = 1000;
  }

  switch (o->state)
  {
    case 0: // just spawned
    {
      o->timer2 = (o->dirparam * 4);
      o->timer3 = 0xC0;
      o->state  = 1;
    }
    case 1: // expanding outward
    {
      if (o->timer3 < 0x1C0)
        o->timer3 += 8;
      else
        o->state = 2;
    }
    break;

    // running - the direction/speed is set by the global variable,
    // controlled by Ballos.
    case 2:
    {
      o->timer2 += platform_speed;
      if (o->timer2 < 0)
        o->timer2 += 0x400;
      if (o->timer2 >= 0x400)
        o->timer2 -= 0x400;
    }
    break;

    case 1000: // ballos defeated!
    {
      o->state    = 1001;
      o->xinertia = 0;
      o->yinertia = 0;
      o->flags &= ~FLAG_SOLID_BRICK;
    }
    case 1001:
    {
      o->yinertia += 0x40;

      if (o->Top() > (map.ysize * TILE_H) * CSFI)
        o->Delete();
    }
    break;
  }

  if (o->state >= 1000)
    return;

  // let player jump up through platforms, but be solid when he is standing on them
  if (player->yinertia < 0 || player->CenterY() > o->Top())
  {
    o->flags &= ~FLAG_SOLID_BRICK;
  }
  else
  {
    o->flags |= FLAG_SOLID_BRICK;
  }

  // spin
  uint8_t angle = o->timer2 / 4;
  int xoff, yoff;

  xoff = xinertia_from_angle(angle, o->timer3 * CSFI);
  yoff = yinertia_from_angle(angle, o->timer3 * CSFI);

  o->xmark = (xoff / 4) + ballos->x;
  o->ymark = ((yoff / 4) + (16 * CSFI)) + ballos->y;

  switch (abs(platform_speed))
  {
    case 1:
      if ((o->timer2 % 4) == 0)
      {
        o->speed = (o->ymark - o->y) / 4;
      }
      break;

    case 2:
      if ((o->timer2 & 2) == 0)
      {
        o->speed = (o->ymark - o->y) / 2;
      }
      break;

    default:
      o->speed = (o->ymark - o->y);
      break;
  }

  o->xinertia = (o->xmark - o->x);
  o->yinertia = o->speed;
}

/*
void c------------------------------() {}
*/

bool BallosBoss::passed_xcoord(bool ltgt, int xcoord, bool reset)
{
  int next_x = main->x + main->xinertia;
  bool result;

  if (ltgt == LESS_THAN)
    result = (next_x <= xcoord);
  else
    result = (next_x >= xcoord);

  if (result && reset)
  {
    main->x        = xcoord;
    main->xinertia = 0;
  }

  return result;
}

bool BallosBoss::passed_ycoord(bool ltgt, int ycoord, bool reset)
{
  int next_y = main->y + main->yinertia;
  bool result;

  if (ltgt == LESS_THAN)
    result = (next_y <= ycoord);
  else
    result = (next_y >= ycoord);

  if (result && reset)
  {
    main->y        = ycoord;
    main->yinertia = 0;
  }

  return result;
}
