If you have comments or questions concerning this source file, discuss them in the forum.
/*
Copyright (c) 2002 Nicolai Haehnle

See the license.txt for details. If that file was not included in the
source distributions, please email <prefect@rtts.org>
*/
// ge_air.c -- airbound entities

#include "game.h"

/*
==============================================================================

Enemy Type Management

==============================================================================
*/

airunit_type_t *airtypes = 0;

/*
==============
Air_FreeType

Frees a given airtype
==============
*/
void Air_FreeType(airunit_type_t *type)
{
    lassert(type->used > 0);

    type->used--;
    if (type->used)
        return;

    if (type->sprite)
        SPR_Free(type->sprite);

    if (type->next)
        type->next->pprev = type->pprev;
    *type->pprev = type->next;
    L_Free(type);
}

/*
==============
AttackTypeForString

Returns the appropriate EW constant
==============
*/
static int AttackTypeForString(const char *sztype, LParser *p)
{
    if (!strcmp(sztype, "shot"))
        return EW_SHOT;
    if (!strcmp(sztype, "homing-shot"))
        return EW_HOMINGSHOT;
    if (!strcmp(sztype, "left-shot"))
        return EW_LEFTSHOT;
    if (!strcmp(sztype, "right-shot"))
        return EW_RIGHTSHOT;
    if (!strcmp(sztype, "missile"))
        return EW_MISSILE;
    if (!strcmp(sztype, "fireball"))
        return EW_FIREBALL;

    if (p)
        throw p->Error("unknown attack type %s", sztype);
    else
        throw LError("unknown attack type %s", sztype);
}

/*
==============
ParseAttack

Parse an attack definition
==============
*/
static void ParseAttack(const char *name, au_attack_t *attack, LParser *p)
{
    p->NextToken();

    attack->wait = 0;
    attack->repeat = 1;
    attack->delay = 0;

    if (p->tok.type == TOK_STRING || p->tok.type == TOK_QUOTED)
    {
        attack->type = AttackTypeForString(p->tok.string, p);
        attack->attach = p->Integer();
    }
    else if (p->tok.type == '{')
    {
        // fire { "xxx" attach wait [ * repeat delay ] }
        attack->type = AttackTypeForString(p->AnyString(), p);
        attack->attach = p->Integer();
        attack->wait = p->Float();

        p->NextToken();

        if (p->tok.type == '*')
        {
            attack->repeat = p->Integer();
            attack->delay = p->Float();
            p->NextToken();
        }

        if (p->tok.type != '}')
            throw p->Error("'}' expected after fire definition");
    }
    else
        throw p->Error("syntax error in fire definition");
}

/*
==============
Air_GetType

Parse an enemy type definition
==============
*/
airunit_type_t *Air_GetType(const char *name)
{
    airunit_type_t *type;
    char path[MAX_OSPATH];

    // Check if it's already loaded
    for(type = airtypes; type; type = type->next) {
        if (!strcmp(type->name, name)) {
            type->used++;
            return type;
        }
    }

    // Load the description file
    type = 0;

    try
    {
        LParser p;
        const char *keyword;
        const char *parm;

        snprintf(path, sizeof(path), "air/%s.txt", name);
        p.AddFile(path);

        type = (airunit_type_t *)L_Malloc(sizeof(airunit_type_t), TAG_UNIT);
        memset(type, 0, sizeof(airunit_type_t));

        type->used = 1;
        xstrcpy(type->name, sizeof(type->name), name);

        for(;;) {
            keyword = p.TryString();
            if (!keyword)
                break;

            if (!strcmp(keyword, "boss"))
            {
                type->bBoss = true;
            }
            else if (!strcmp(keyword, "sprite"))
            {
                char buf[MAX_OSPATH];
                parm = p.AnyString();
                p.RelativePath(buf, sizeof(buf), parm);
                type->sprite = SPR_Get(buf);
            }
            else if (!strcmp(keyword, "movement"))
            {
                keyword = p.AnyString();

                if (!strcmp(keyword, "dumb"))
                    type->iMovement = AIRMOVE_DUMB;
                else if (!strcmp(keyword, "chase"))
                    type->iMovement = AIRMOVE_CHASE;
                else
                    throw p.Error("unknown movement type %s", keyword);
            }
            else if (!strcmp(keyword, "mparm1"))
            {
                type->flMParm1 = p.Float();
            }
            else if (!strcmp(keyword, "mparm2"))
            {
                type->flMParm2 = p.Float();
            }
            else if (!strcmp(keyword, "speed"))
            {
                type->flSpeed = p.Float();
            }
            else if (!strcmp(keyword, "health"))
            {
                type->flHealth = p.Float();
            }
            else if (!strcmp(keyword, "money"))
            {
                type->iMoney = p.Integer();
            }
            else if (!strcmp(keyword, "attackcycle"))
            {
                type->flAttackCycle = p.Float();
            }
            else if (!strcmp(keyword, "fire"))
            {
                if (type->numattacks >= MAX_ATTACKS)
                    throw p.Error("too many attacks");

                ParseAttack(name, &type->attacks[type->numattacks], &p);
                type->numattacks++;
            }
            else if (!strcmp(keyword, "trail"))
            {
                type->iTrailAttach = p.Integer();;
                xstrcpy(type->szTrailSprite, sizeof(type->szTrailSprite), p.AnyString());
                type->iTrailTime = p.Integer();
            }
            else
                throw p.Error("unknown keyword %s", keyword);
        }

        if (!type->sprite)
            throw LError("%s: no sprite given", name);
    }
    catch(...)
    {
        if (type)
            L_Free(type);

        throw;
    }

    type->pprev = &airtypes;
    type->next = airtypes;
    if (type->next)
        type->next->pprev = &type->next;
    airtypes = type;

    return type;
}

/*
==============================================================================

AirSprite IMPLEMENTATION

==============================================================================
*/

class AirSprite : public AirBase {
public:
    int     m_iDieTime;

public:
    AirSprite(const char *pszName, int iLoops, float x, float y, int iAnimTime = 0);
    AirSprite(sprite_t *pSprite, int iLoops, float x, float y, int iAnimTime = 0);

    virtual void Think();
    virtual bool IsGhost() { return true; }

private:
    void Init(int iLoops);
};

/*
==============
AirSprite::AirSprite

Load the requested sprite
==============
*/
AirSprite::AirSprite(const char *pszName, int iLoops, float x, float y, int iAnimTime)
    : AirBase(LYR_SPRITE, pszName, x, y)
{
    m_iAnimTime = iAnimTime;

    Init(iLoops);
}

AirSprite::AirSprite(sprite_t *pSprite, int iLoops, float x, float y, int iAnimTime)
    : AirBase(LYR_SPRITE, pSprite, x, y)
{
    m_iAnimTime = iAnimTime;

    Init(iLoops);
}

/*
==============
AirSprite::Init

Set the death timer
==============
*/
void AirSprite::Init(int iLoops)
{
    if (iLoops <= 0)
        m_iDieTime = -1;
    else {
        float time;

        time = (float)m_pSprite->base.numframes / (float)m_pSprite->base.speed;
        time *= iLoops;

        m_iDieTime = (int)(time * 1000);
    }
}

/*
==============
AirSprite::Think

Advance the animation, kill the sprite after too many loops
==============
*/
void AirSprite::Think()
{
    m_iAnimTime += LOGICTIME;

    if (m_iDieTime >= 0 && m_iAnimTime >= m_iDieTime)
        m_bRemove = true;
}

/*
==============================================================================

MISSILES

==============================================================================
*/

#define MM_NORMAL       0 // fly straight down or up
#define MM_TARGET       1 // target an enemy
#define MM_DFIRE        2 // dumbfire missile

#define MAX_MISS_SPRITES    6

typedef struct missile_type_s {
    struct missile_type_s **pprev, *next;
    int             used;
    char            name[64];

    int             layer;
    int             numsprites;
    sprite_t        *sprites[MAX_MISS_SPRITES];
    int             movement;
    float           speed;
    float           damage;
    int             numtrails;
    sprite_t        *trails[MAX_MISS_SPRITES];
    int             trailtime;
} missile_type_t;

missile_type_t  *missile_types = 0;


/*
==============
Air_DoFreeMissileType

Actually free a missile
==============
*/
void Air_DoFreeMissileType(missile_type_t *type)
{
    int i;

    if (type->next)
        type->next->pprev = type->pprev;
    if (type->pprev)
        *type->pprev = type->next;

    for(i = 0; i < type->numsprites; i++)
        SPR_Free(type->sprites[i]);

    for(i = 0; i < type->numtrails; i++)
        SPR_Free(type->trails[i]);

    L_Free(type);
}

/*
==============
Air_GetMissileType

Find the missile data and sprites and load them
==============
*/
static void ParseSpriteList(int *pnumsprites, sprite_t **sprites, int maxsprites,
                            LParser *p)
{
    p->Expect("[");

    for(;;) {
        p->NextToken();
        if (p->tok.type == ']')
            break;

        if (p->tok.type != TOK_STRING && p->tok.type != TOK_QUOTED)
            throw p->Error("] or sprite name expected");

        if (*pnumsprites >= maxsprites)
            throw p->Error("too many sprites");

        sprites[*pnumsprites] = SPR_Get(p->tok.string);
        (*pnumsprites)++;
    }
}

missile_type_t *Air_GetMissileType(const char *name)
{
    missile_type_t *type;
    char path[MAX_OSPATH];

    // Lookup the missile
    for(type = missile_types; type; type = type->next) {
        if (!strcmp(type->name, name)) {
            type->used++;
            return type;
        }
    }

    // Allocate a new missile type
    type = 0;

    try
    {
        LParser p;
        const char *keyword;

        snprintf(path, sizeof(path), "missiles/%s.txt", name);
        p.AddFile(path);

        type = (missile_type_t *)L_Malloc(sizeof(missile_type_t), TAG_UNIT);
        memset(type, 0, sizeof(missile_type_t));

        type->used = 1;
        type->layer = LYR_MISS;
        xstrcpy(type->name, sizeof(type->name), name);

        for(;;) {
            keyword = p.TryString();
            if (!keyword)
                break;

            if (!strcmp(keyword, "sprite"))
            {
                if (type->numsprites)
                    throw p.Error("duplicate sprite");

                ParseSpriteList(&type->numsprites, type->sprites, MAX_MISS_SPRITES, &p);
            }
            else if (!strcmp(keyword, "speed"))
            {
                type->speed = p.Float();
            }
            else if (!strcmp(keyword, "damage"))
            {
                type->damage = p.Float();
            }
            else if (!strcmp(keyword, "low"))
            {
                type->layer = LYR_BOMB;
            }
            else if (!strcmp(keyword, "trail"))
            {
                if (type->numtrails)
                    throw p.Error("duplicate trail");

                ParseSpriteList(&type->numtrails, type->trails, MAX_MISS_SPRITES, &p);

                type->trailtime = p.Integer();
            }
            else if (!strcmp(keyword, "move"))
            {
                keyword = p.AnyString();

                if (!strcmp(keyword, "normal"))
                    type->movement = MM_NORMAL;
                else if (!strcmp(keyword, "target"))
                    type->movement = MM_TARGET;
                else if (!strcmp(keyword, "dfire"))
                    type->movement = MM_DFIRE;
                else
                    throw p.Error("unknown movement type %s", keyword);
            }
            else
                throw p.Error("unknown keyword %s", keyword);
        }

        if (!type->numsprites)
            throw LError("%s: no sprite(s) defined", path);
    }
    catch(...)
    {
        if (type)
            Air_DoFreeMissileType(type);

        throw;
    }

    type->pprev = &missile_types;
    type->next = missile_types;
    if (type->next)
        type->next->pprev = &type->next;
    missile_types = type;

    return type;
}

/*
==============
Air_FreeMissileType

Reduce the missile use counter - don't actually free though,
to avoid reloading missile types all the time
==============
*/
void Air_FreeMissileType(missile_type_t *type)
{
    type->used--;
}


class AirMissile : public AirBase {
public:
    missile_type_t  *m_pType;

    bool            m_bGoodGuy;
    int             m_iCycle;
    int             m_iFuse;
    sprite_t        *m_pTrail;
    int             m_iTrailTimer;

    int             m_iLogicTime; // logic time to be used by the missile

public:
    virtual ~AirMissile();

    virtual void Think();
    virtual bool IsGoodGuy() { return m_bGoodGuy; }
    virtual void Collide(AirBase *pOther);

    static AirMissile *Spawn(bool bGoodGuy, const char *name, int iCycle,
            float x, float y, bool bOverride, float vx, float vy, AirBase *pEnemy);

private:
    AirMissile(bool bGoodGuy, missile_type_t *pType, sprite_t *pSprite, sprite_t *pTrail,
            float x, float y, bool bOverride, float vx, float vy, AirBase *pEnemy);

    void Init(bool bOverride, float vx, float vy, AirBase *pEnemy);
};

/*
==============
AirMissile::AirMissile

Missile constructor
==============
*/
AirMissile::AirMissile(bool bGoodGuy, missile_type_t *pType, sprite_t *pSprite, sprite_t *pTrail,
            float x, float y, bool bOverride, float vx, float vy, AirBase *pEnemy)
    : AirBase(pType->layer, pSprite, x, y)
{
    m_bGoodGuy = bGoodGuy;

    m_pType = pType;
    m_pType->used++;

    m_pTrail = pTrail;
    if (m_pTrail)
        m_pTrail->used++;
    m_iTrailTimer = pType->trailtime;

    m_iFuse = m_iCycle = 0;

    Init(bOverride, vx, vy, pEnemy);
}

/*
==============
AirMissile::Init

Initialize velocity etc..
==============
*/
void AirMissile::Init(bool bOverride, float vx, float vy, AirBase *pEnemy)
{
    AirBase *pOther;
    float f;

    if (m_bGoodGuy)
        m_iLogicTime = ge->m_iFrameTime;
    else
        m_iLogicTime = LOGICTIME; // badguy missiles accelerate with level

    if (bOverride)
    {
        m_vel[0] = vx;
        m_vel[1] = vy;

        f = sqrt(m_vel[0]*m_vel[0] + m_vel[1]*m_vel[1]);
        if (!f) f = 1;
        f = m_pType->speed / f;

        m_vel[0] *= f;
        m_vel[1] *= f;
    }
    else
    {
        switch(m_pType->movement) {
        default:
        case MM_NORMAL:
            m_vel[0] = 0;

            if (m_bGoodGuy)
                m_vel[1] = -m_pType->speed;
            else
                m_vel[1] = m_pType->speed;

            m_vel[0] += vx;
            m_vel[1] += vy;
            break;

        case MM_TARGET:
            if (pEnemy)
                pOther = pEnemy;
            else
                pOther = ge->Air_FindNearest(m_x, m_y, !m_bGoodGuy, 0, 0);
            if (pOther)
            {
                float ox, oy;

                ox = pOther->m_x + 24.0 * (2.0*RandFloat() - 1.0);
                oy = pOther->m_y + 24.0 * (2.0*RandFloat() - 1.0);

                m_vel[0] = ox - m_x;
                m_vel[1] = oy - m_y;

                f = sqrt(m_vel[0]*m_vel[0] + m_vel[1]*m_vel[1]);
                if (!f) f = 1;
                f = m_pType->speed / f;

                m_vel[0] *= f;
                m_vel[1] *= f;
            }
            else
                m_bRemove = true;

            m_vel[0] += vx;
            m_vel[1] += vy;
            break;

        case MM_DFIRE:
            // dumbfire missiles are special: they're launched horizontally up to
            // a certain distance, and then start off vertically later
            m_vel[0] = vx * 100.0 * (0.3 + 0.7 * RandFloat());
            m_vel[1] = vy;

            m_iFuse = 500;
            break;
        }
    }
}

/*
==============
AirMissile::~AirMissile

Clean up allocated resources
==============
*/
AirMissile::~AirMissile()
{
    Air_FreeMissileType(m_pType);

    if (m_pTrail)
        SPR_Free(m_pTrail);
}

/*
==============
AirMissile::Think

Moves the missile; remove if out of screen
==============
*/
void AirMissile::Think()
{
    float dx, dy;
    float t;
    float x, y;
    bool bTrail;

    if (m_pType->movement == MM_DFIRE && !m_iCycle)
    {
        bTrail = false;

        t = m_vel[0] * -0.9;
        m_vel[0] += t * m_iLogicTime / 1000.0;

//      if (m_x <= 12 || m_x >= 628)
//          m_vel[0] = 0;

        m_iFuse -= m_iLogicTime;
        if (m_iFuse <= 0) {
            m_vel[0] = 0;
            if (m_bGoodGuy)
                m_vel[1] = -m_pType->speed;
            else
                m_vel[1] = m_pType->speed;

            bTrail = true;
            m_iCycle = 1;
        }
    }
    else
        bTrail = true;

    if (bTrail && m_pTrail) {
        m_iTrailTimer -= m_iLogicTime;
        while(m_iTrailTimer <= 0) {
            t = (float)-m_iTrailTimer / m_iLogicTime;
            x = m_lastx * t + m_x * (1.0 - t);
            y = m_lasty * t + m_y * (1.0 - t);

            new AirSprite(m_pTrail, 1, m_x, m_y, -m_iTrailTimer);

            m_iTrailTimer += m_pType->trailtime;
        }
    }

    m_iAnimTime += m_iLogicTime;

    dx = m_vel[0] * m_iLogicTime / 1000.0;
    dy = m_vel[1] * m_iLogicTime / 1000.0;

    Move(dx, dy);

    if (SHADOWY(m_y)+m_pSprite->maxs[1] < -2 || SHADOWY(m_y)+m_pSprite->mins[1] > 482)
        m_bRemove = true;
}

/*
==============
AirMissile::Collide

Collided with something: kill self, deal damage and show hit sprite.
Note that the latter is _not_ done if we're out of the screen
==============
*/
void AirMissile::Collide(AirBase *pOther)
{
    if (m_y+m_pSprite->maxs[1] >= 0 && m_y+m_pSprite->mins[1] <= 480) {
        pOther->Damage(this, m_pType->damage);

        if (m_bGoodGuy)
            new AirSprite("effects/hit0", 1, m_x, m_y);
        else
            new AirSprite("effects/exp1", 1, m_x, m_y);
    }

    m_bRemove = true;
}

/*
==============
AirMissile::Spawn

Create a missile out of thin air.
If bOverride is true, vx/vy are used as velocity. Otherwise, the
velocity is determined automatically,
pEnemy overrides the target for targeting missiles.
==============
*/
AirMissile *AirMissile::Spawn(bool bGoodGuy, const char *name, int iCycle,
                    float x, float y, bool bOverride, float vx, float vy,
                    AirBase *pEnemy)
{
    AirMissile *pMiss;
    missile_type_t *type;
    sprite_t *sprite;
    sprite_t *trail;

    type = Air_GetMissileType(name);
    sprite = type->sprites[iCycle % type->numsprites];
    if (!type->numtrails)
        trail = 0;
    else
        trail = type->trails[iCycle % type->numtrails];
    pMiss = new AirMissile(bGoodGuy, type, sprite, trail, x, y, bOverride, vx, vy,
        pEnemy);
    Air_FreeMissileType(type);

    return pMiss;
}


/*
==============================================================================

AirUnit IMPLEMENTATION

==============================================================================
*/

/*
==============
AirUnit::AirUnit

Initialize using default values
==============
*/
AirUnit::AirUnit(const char *pszSprite, float flSpeed, float x, float y, float health)
    : AirBase(LYR_UNIT, pszSprite, x, y)
{
    m_flHealth = health;
    m_flLowHealth = health / 20;
    m_iDeathTime = 0;
    m_flDmgExplosions = 10;

    m_iDmgExplosionTimer = 0;

    m_flSpeed = flSpeed;

    m_flDeathMoveAngle = 2*M_PI*RandFloat();
    m_flDeathAngleSpeed = 10.0 + 10.0*RandFloat();
    if (rand() % 2)
        m_flDeathAngleSpeed = -m_flDeathAngleSpeed;
}

AirUnit::AirUnit(sprite_t *sprite, float flSpeed, float x, float y, float health)
    : AirBase(LYR_UNIT, sprite, x, y)
{
    m_flHealth = health;
    m_flLowHealth = health / 20;
    m_iDeathTime = 0;
    m_flDmgExplosions = 10;

    m_iDmgExplosionTimer = 0;

    m_flSpeed = flSpeed;

    m_flDeathMoveAngle = 2*M_PI*RandFloat();
    m_flDeathAngleSpeed = 10.0 + 10.0*RandFloat();
    if (rand() % 2)
        m_flDeathAngleSpeed = -m_flDeathAngleSpeed;
}

/*
==============
AirUnit::Think

Thinking hub; don't override
==============
*/
void AirUnit::Think()
{
    float fract;
    int frametime = IsGoodGuy() ? ge->m_iFrameTime : LOGICTIME;

    if (m_flHealth <= 0) {
        float mx, my;

        m_flDeathMoveAngle += m_flDeathAngleSpeed * frametime / 1000.0;
        m_flDeathMoveAngle *= 0.96;
        m_vel[0] = cos(m_flDeathMoveAngle) * m_flSpeed * 2;
        m_vel[1] = sin(m_flDeathMoveAngle) * m_flSpeed * 2;

        mx = m_vel[0] * frametime / 1000.0;
        my = m_vel[1] * frametime / 1000.0;
        Move(mx, my);

        m_iDeathTime -= frametime;
        if (m_iDeathTime <= 0) {
            FinalDeath();
            m_bRemove = true;

            new AirSprite("effects/exp0", 1, m_x, m_y); // final explosion
        }
    } else
        Run();

    // lowhealth explosions
    if (m_flLowHealth > 0 && m_flHealth < m_flLowHealth)
    {
        m_iDmgExplosionTimer -= frametime;
        if (m_iDmgExplosionTimer <= 0) {
            float freq, x, y, t;

            fract = m_flHealth;
            if (fract < 0)
                fract = 0;
            fract /= m_flLowHealth;

            freq = m_flDmgExplosions * (1.0 - (fract*0.6));
            if (m_flHealth <= 0)
                freq *= 3;

            do {
                t = RandFloat();
                x = m_pSprite->mins[0] * t + m_pSprite->maxs[0] * (1.0-t);
                t = RandFloat();
                y = m_pSprite->mins[1] * t + m_pSprite->maxs[1] * (1.0-t);

                x += m_x;
                y += m_y;

                new AirSprite("effects/exp1", 1, x, y);

                m_iDmgExplosionTimer += (int)(1000.0 / freq);
            } while(m_iDmgExplosionTimer <= 0);
        }
    }
}

/*
==============
AirUnit::Damage

Apply damage
==============
*/
void AirUnit::Damage(AirBase *pInflictor, float flAmt)
{
    float flOldHealth;

    if (ge->m_iState == ge_win || ge->m_iState == ge_abort)
        return;

    flOldHealth = m_flHealth;
    m_flHealth -= flAmt;

    DamageNotice(flAmt);
}

/*
==============================================================================

AirEnemy IMPLEMENTATION

==============================================================================
*/

typedef struct {
    float x, y;
} mnode_t;

class AirEnemy : public AirUnit {
public:
    airunit_type_t  *m_pType;

    int         m_iAttackTime;  // time the current attack lasts
    int         m_iAttackRepeats[MAX_ATTACKS];

    int         m_iMovement;
    bool        m_bDieIfOffscreen;

    bool        m_bFollowNodes;
    int         m_iNumNodes;
    mnode_t     *m_pNodes;
    int         m_iNodeIdx;

    int         m_iChaseState; // 0 = innocent, 1 = chasing
    int         m_iChaseDelay; // ms until chase

    int         m_iTrailTimer;

public:
    AirEnemy(airjob_t *aj);
    virtual ~AirEnemy();

    virtual void FinalDeath();

    virtual void Draw();
    virtual void MoveAI();
    virtual void Run();
    virtual void Collide(AirBase *pOther);

    void Fire(int type, float x, float y);
    void BounceX();

    void FollowNodes();
    void InitMoveLogic();

    void MoveChase();
};

/*
==============
AirEnemy::AirEnemy

Initialize the enemy data. Note that pType ownership is transferred.
==============
*/
AirEnemy::AirEnemy(airjob_t *aj)
    : AirUnit(aj->type->sprite, aj->type->flSpeed, aj->x,
                FIRSTSHADOWY(aj->type->sprite->maxs[1]),
                aj->type->flHealth * ge->m_flHealthFactor)
{
    int i;
    float x, y;

    m_pType = aj->type;
    m_pType->used++;

    m_iAttackTime = -99999;
    memset(m_iAttackRepeats, 0, sizeof(m_iAttackRepeats));

    m_iMovement = m_pType->iMovement;
    m_bDieIfOffscreen = false;

    m_bFollowNodes = true;
    m_iNodeIdx = 0;
    m_iNumNodes = aj->nodes.size();
    if (m_iNumNodes) {
        m_pNodes = (mnode_t *)L_Malloc(sizeof(mnode_t)*m_iNumNodes, TAG_UNIT);

        x = m_x;
        y = m_y;
        for(i = 0; i < m_iNumNodes; i++) {
            x += aj->nodes[i].dx;
            y += aj->nodes[i].dy;
            m_pNodes[i].x = x;
            m_pNodes[i].y = y;
        }
    } else
        m_pNodes = 0;

    FollowNodes();

    m_iTrailTimer = m_pType->iTrailTime;

    ge->m_player.stats.moneymax += m_pType->iMoney;
}

/*
==============
AirEnemy::~AirEnemy

Free associated data
==============
*/
AirEnemy::~AirEnemy()
{
    if (m_pNodes)
        L_Free(m_pNodes);

    Air_FreeType(m_pType);
}

/*
==============
AirEnemy::Draw

Special draw (engine)
==============
*/
void AirEnemy::Draw()
{
    float x, y;
    float time;

    x = LERP(m_lastx, m_x);
    y = LERP(m_lasty, m_y);

    time = LERP(m_iAnimTime-LOGICTIME, m_iAnimTime);
    SPR_Draw(m_pSprite, x, y, -1, time);
    SPR_Draw(m_pSprite, x, y, 1001, time); // engine
}


/*
==============
AirEnemy::FinalDeath

Called just before the remove flag is set
==============
*/
void AirEnemy::FinalDeath()
{
    ge->m_player.data.money += m_pType->iMoney;
    ge->m_player.stats.money += m_pType->iMoney;
}

/*
==============
AirEnemy::Fire

Fire a single bullet or rocket
==============
*/
void AirEnemy::Fire(int type, float x, float y)
{
    switch(type) {
    case EW_SHOT:
        AirMissile::Spawn(false, "shot", 0, x, y, false, 0, 0, 0);
        break;

    case EW_HOMINGSHOT:
        AirMissile::Spawn(false, "homingshot", 0, x, y, false, 0, 0, 0);
        break;

    case EW_LEFTSHOT:
        AirMissile::Spawn(false, "shot", 0, x, y, true, -1, 1, 0);
        break;

    case EW_RIGHTSHOT:
        AirMissile::Spawn(false, "shot", 0, x, y, true, 1, 1, 0);
        break;

    case EW_MISSILE:
        AirMissile::Spawn(false, "missile", 0, x, y, false, 0, 0, 0);
        break;

    case EW_FIREBALL:
        AirMissile::Spawn(false, "fireball", 0, x, y, false, 0, 0, 0);
        break;
    }
}

/*
==============
AirEnemy::BounceX

Bounce off the screen boundaries
==============
*/
void AirEnemy::BounceX()
{
    if ((m_x < 12-m_pSprite->mins[0] && m_vel[0] < 0) ||
        (m_x > 628-m_pSprite->maxs[0] && m_vel[0] > 0))
        m_vel[0] = -m_vel[0];
}

/*
==============
AirEnemy::FollowNodes

Logic for following nodes. Every enemy first follows the nodes set by the
level designer. Once the last node is reached, the autonomous move logic
begins to act.
==============
*/
void AirEnemy::FollowNodes()
{
    if (!m_bFollowNodes)
        return;

restart:
    if (m_iNodeIdx < m_iNumNodes)
        {
        float dx, dy;

        dx = m_pNodes[m_iNodeIdx].x - m_x;
        dy = m_pNodes[m_iNodeIdx].y - m_y;

        if (dx < 0.1 && dy < 0.1)
            {
            m_iNodeIdx++;
            goto restart;
            }

        if (fabs(dx) > fabs(dy))
            {
            m_vel[0] = m_pType->flSpeed * dx / fabs(dx);
            m_vel[1] = m_pType->flSpeed * dy / fabs(dx);
            }
        else
            {
            m_vel[1] = m_pType->flSpeed * dy / fabs(dy);
            m_vel[0] = m_pType->flSpeed * dx / fabs(dy);
            }
        }
    else
        {
        m_bFollowNodes = false;

        InitMoveLogic();
        }
}


/*
===============
AirEnemy::InitMoveLogic

Initialize the autonomous move logic once the last node has been reached.
===============
*/
void AirEnemy::InitMoveLogic()
{
    if (m_iMovement == AIRMOVE_CHASE)
        {
        float dx = (float)rand() * (0.2/RAND_MAX); // 0..0.2
        m_vel[0] = m_pType->flSpeed * (dx - 0.1);
        m_vel[1] = m_pType->flSpeed;

        m_iChaseState = 0;
        m_iChaseDelay = rand() % (int)(m_pType->flMParm1 * 1000);
        }
    else
        {
        m_vel[0] = 0;
        m_vel[1] = m_pType->flSpeed;
        }
}


/*
===============
AirEnemy::MoveChase

Move logic for chase movement type
===============
*/
void AirEnemy::MoveChase()
{
    if (m_iChaseState == 0)
        {
        m_iChaseDelay -= LOGICTIME;

        if (m_iChaseDelay < 0)
            {
            AirBase* pOther;

            pOther = ge->Air_FindNearest(m_x, m_y, true, 0, 0);
            if (!pOther)
                return;

            float speed = (1.0 + RandFloat() * (m_pType->flMParm2-1.0)) * m_pType->flSpeed;
            float ox, oy;
            float f;

            ox = pOther->m_x + 24.0 * (2.0*RandFloat() - 1.0);
            oy = pOther->m_y + 24.0 * (2.0*RandFloat() - 1.0);

            m_vel[0] = ox - m_x;
            m_vel[1] = oy - m_y;

            f = sqrt(m_vel[0]*m_vel[0] + m_vel[1]*m_vel[1]);
            if (!f) f = 1;
            f = speed / f;

            m_vel[0] *= f;
            m_vel[1] *= f;

            m_iChaseState = 1;
            }
        }
}


/*
==============
AirEnemy::MoveAI

Move "intelligently"
==============
*/
void AirEnemy::MoveAI()
{
    float dx, dy;

    dx = m_vel[0] * LOGICTIME / 1000.0;
    dy = m_vel[1] * LOGICTIME / 1000.0;

    Move(dx, dy);

    if (m_bFollowNodes)
        {
        if (dx > dy)
            {
            if ((dx > 0 && m_x >= m_pNodes[m_iNodeIdx].x) ||
                (dx < 0 && m_x <= m_pNodes[m_iNodeIdx].x))
                {
                m_iNodeIdx++;
                FollowNodes();
                }
            }
        else
            {
            if ((dy > 0 && m_y >= m_pNodes[m_iNodeIdx].y) ||
                (dy < 0 && m_y <= m_pNodes[m_iNodeIdx].y))
                {
                m_iNodeIdx++;
                FollowNodes();
                }
            }
        }
    else
        {
        if (m_iMovement == AIRMOVE_CHASE)
            {
            MoveChase();
            }
        else
            {
            // dumb movement, simply move downwards
            if (m_y > 0)
                m_bDieIfOffscreen = true;
            }

        // remove when out of screen
        bool onscreen = IsInScreen();

        if (onscreen)
            m_bDieIfOffscreen = true;
        else if (m_bDieIfOffscreen)
            m_bRemove = true;
        }
}

/*
==============
AirEnemy::Run

Move the enemy, run AI, ...
==============
*/
void AirEnemy::Run()
{
    au_attack_t *attack;
    bool finished;
    int i;

    // Trails
    if (m_pType->szTrailSprite[0]) {
        spr_attach_t *attach;
        float t, x, y;

        m_iTrailTimer -= LOGICTIME;
        if (m_iTrailTimer <= 0) {
            attach = SPR_GetAttach(m_pSprite, m_pType->iTrailAttach);

            do {
                t = (float)-m_iTrailTimer / LOGICTIME;
                //t = 0;
                x = m_lastx * t + m_x * (1.0 - t) + attach->offset[0];
                y = m_lasty * t + m_y * (1.0 - t) + attach->offset[1];

                new AirSprite(m_pType->szTrailSprite, 1, x, y, -m_iTrailTimer);

                m_iTrailTimer += m_pType->iTrailTime;
            } while(m_iTrailTimer <= 0);
        }
    }

    // Attack
    if (m_y > 0) {
        if (m_iAttackTime <= -99999) {
            m_iAttackTime = (int)(- m_pType->flAttackCycle * (1000 / 4));
            if (m_iAttackTime < -500)
                m_iAttackTime = -500;
        }

        m_iAttackTime += LOGICTIME;
        if (m_iAttackTime >= 0)
        {
            finished = true;
            for(i = 0, attack = m_pType->attacks; i < m_pType->numattacks; i++, attack++)
            {
                spr_attach_t *attach;
                float ax, ay;
                float t;

                while(m_iAttackRepeats[i] < attack->repeat) {
                    finished = false;
                    t = attack->wait + m_iAttackRepeats[i] * attack->delay;

                    if (m_iAttackTime < t*1000)
                        break;

                    attach = SPR_GetAttach(m_pSprite, m_pType->attacks[i].attach);
                    ax = m_x + attach->offset[0];
                    ay = m_y + attach->offset[1];

                    Fire(attack->type, ax, ay);

                    m_iAttackRepeats[i]++;
                }
            }

            if (finished) {
                m_iAttackTime -= (int)(m_pType->flAttackCycle * 1000);
                memset(m_iAttackRepeats, 0, sizeof(m_iAttackRepeats));
            }
        }
    }

    // Animation and movement
    m_iAnimTime += LOGICTIME;

    MoveAI();
}

/*
==============
AirEnemy::Collide

Deal some damage
==============
*/
void AirEnemy::Collide(AirBase *pOther)
{
    pOther->Damage(this, m_pType->flHealth);
}


/*
==============================================================================

AirBoss IMPLEMENTATION

==============================================================================
*/

class AirBoss : public AirEnemy {
public:
    static AirBoss  *m_pBossList;
    AirBoss         **m_ppPrev;
    AirBoss         *m_pNext;

    int             m_iDirChangeDelay;

public:
    AirBoss(airjob_t *aj);
    virtual ~AirBoss();

    virtual void MoveAI();
};

AirBoss *AirBoss::m_pBossList = 0;

/*
==============
AirBoss::AirBoss

Create the boss unit
==============
*/
AirBoss::AirBoss(airjob_t *aj)
    : AirEnemy(aj)
{
    m_ppPrev = &m_pBossList;
    m_pNext = m_pBossList;
    if (m_pNext)
        m_pNext->m_ppPrev = &m_pNext;
    m_pBossList = this;

    m_vel[0] = 0.3;
    m_vel[1] = 0.9;

    m_iDirChangeDelay = 4000;

    m_flLowHealth = m_flHealth * 0.2;
    m_iDeathTime = 500;
    m_flDmgExplosions *= 2.0; // bigger surface -> more explosions
}

/*
==============
AirBoss::~AirBoss

Remove the boss; if we died and were the last finalboss, set gamestate
to bossdestroyed
==============
*/
AirBoss::~AirBoss()
{
    // first unlink self
    if (m_pNext)
        m_pNext->m_ppPrev = m_ppPrev;
    *m_ppPrev = m_pNext;

    // did we die?
    ge->m_iBossDestroyed++;
}

/*
==============
AirBoss::MoveAI

Bosses move differently
==============
*/
void AirBoss::MoveAI()
{
    float dx, dy;
    int i;
    float f;

    m_iDirChangeDelay -= LOGICTIME;
    if (m_iDirChangeDelay < 0) {
        m_iDirChangeDelay += 4500 + rand() % 1000;

        i = rand() % 2;
        m_vel[i] = -m_vel[i];
        m_vel[i] += 0.3 * (2*RandFloat() - 1.0);
        if (m_vel[i] < -1.0)
            m_vel[i]= -1.0;
        else if (m_vel[i] > 1.0)
            m_vel[i] = 1.0;

        f = sqrt(m_vel[0]*m_vel[0] + m_vel[1]*m_vel[1]);
        if (!f)
            m_vel[0] = m_vel[1] = 0.2;
        else {
            m_vel[0] /= f;
            m_vel[1] /= f;
        }
    }

    dx = m_pType->flSpeed * m_vel[0] * LOGICTIME / 1000.0;
    dy = m_pType->flSpeed * m_vel[1] * LOGICTIME / 1000.0;

    Move(dx, dy);

    if ((m_x < 12-m_pSprite->mins[0] && m_vel[0] < 0) ||
        (m_x > 628-m_pSprite->maxs[0] && m_vel[0] > 0))
        m_vel[0] = -m_vel[0];
    if ((m_y < 12-m_pSprite->mins[1] && m_vel[1] < 0) ||
        (m_y > 240-m_pSprite->maxs[1] && m_vel[1] > 0))
        m_vel[1] = -m_vel[1];
}

/*
==============================================================================

AirPlayer IMPLEMENTATION

==============================================================================
*/

/*
==============
AirPlayer::AirPlayer

Spawn the player ship
==============
*/
AirPlayer::AirPlayer(eplayer_t *pPlayer, float x, float y)
    : AirUnit("ship", 320, x, y, pPlayer->data.health)
{
    m_pPlayer = pPlayer;
    m_pPlayer->ship = this;

    m_flLowHealth = 10;
    m_iDeathTime = 2000;

    m_iFireMGDelay = 0;
    m_iFirePrimaryDelay = 0;
    m_iMGCycle = 0;
    m_iPrimaryCycle = 0;
    m_bInFire = false;

    SelectBestWeapon();
}

/*
==============
AirPlayer::~AirPlayer
==============
*/
AirPlayer::~AirPlayer()
{
    m_flHealth = (int)m_flHealth;

    if (m_flHealth <= 0)
    {
        ge->m_iState = ge_loose;
    }
    else if (ge->m_iPrewinTime < 0 && m_y < 0)
    {
        ge->m_iState = ge_win;
    }

    m_pPlayer->data.health = (int)m_flHealth;
    m_pPlayer->ship = 0;
}

/*
==============
AirPlayer::Draw

Special draw for the player (muzzleflashes)
==============
*/
void AirPlayer::Draw()
{
    float x, y;
    float time;

    x = LERP(m_lastx, m_x);
    y = LERP(m_lasty, m_y);

    time = LERP(m_iAnimTime-ge->m_iFrameTime, m_iAnimTime);
    SPR_Draw(m_pSprite, x, y, -1, time);
    SPR_Draw(m_pSprite, x, y, 1001, time); // engine

    if (m_bInFire)
        SPR_Draw(m_pSprite, x, y, 1, time);
}

/*
==============
AirPlayer::RunWeapons

Shoot if it's time
==============
*/
void AirPlayer::RunWeapons()
{
    AirBase *pOther;
    spr_attach_t *attach;
    int att1, att2;
    float x, y;
    float vx;

    if (m_iFireMGDelay < 0)
        m_iFireMGDelay = 0;
    else
        m_iFireMGDelay -= ge->m_iFrameTime;

    if (m_iFirePrimaryDelay < 0)
        m_iFirePrimaryDelay = 0;
    else
        m_iFirePrimaryDelay -= ge->m_iFrameTime;

    if (!m_bInFire)
        return;

    if (m_iFireMGDelay <= 0) {
        m_iFireMGDelay += 120; // 1 shot / X ms

        m_iMGCycle++;

        attach = SPR_GetAttach(m_pSprite, 1);
        x = m_x + attach->offset[0];
        y = m_y + attach->offset[1];
        AirMissile::Spawn(true, "mg", m_iMGCycle, x, y, false, 0, 0, 0);

        attach = SPR_GetAttach(m_pSprite, 2);
        x = m_x + attach->offset[0];
        y = m_y + attach->offset[1];
        AirMissile::Spawn(true, "mg", m_iMGCycle, x, y, false, 0, 0, 0);
    }

    if (m_iFirePrimaryDelay <= 0) {
        switch(m_iCurWeapon) {
        case WEAP_ROCKET:
            m_iFirePrimaryDelay += 300;

            m_iPrimaryCycle++;
            if (m_iPrimaryCycle % 2) {
                att1 = 3;
                att2 = 4;
            } else {
                att1 = 1;
                att2 = 2;
            }

            attach = SPR_GetAttach(m_pSprite, att1);
            x = m_x + attach->offset[0] + 2;
            y = m_y + attach->offset[1];
            AirMissile::Spawn(true, "rocket", m_iPrimaryCycle, x, y, false, 0, 0, 0);

            attach = SPR_GetAttach(m_pSprite, att2);
            x = m_x + attach->offset[0] - 2;
            y = m_y + attach->offset[1];
            AirMissile::Spawn(true, "rocket", m_iPrimaryCycle, x, y, false, 0, 0, 0);
            break;

        case WEAP_DUMBFIRE:
            m_iFirePrimaryDelay += 200;

            m_iPrimaryCycle++;
            if (m_iPrimaryCycle % 2) {
                attach = SPR_GetAttach(m_pSprite, 3);
                vx = -1;
            } else {
                attach = SPR_GetAttach(m_pSprite, 4);
                vx = 1;
            }

            x = m_x + attach->offset[0];
            y = m_y + attach->offset[1];
            AirMissile::Spawn(true, "dfire", m_iPrimaryCycle, x, y, false, vx, 0, 0);
            break;

        case WEAP_ATG:
            m_iFirePrimaryDelay += 120;

            m_iPrimaryCycle++;

            attach = SPR_GetAttach(m_pSprite, 3);
            x = m_x + attach->offset[0];
            y = m_y + attach->offset[1];
            pOther = ge->Air_FindNearest(x, y, false, -1, -1);
            if (pOther)
                AirMissile::Spawn(true, "atg", m_iPrimaryCycle, m_x, m_y,
                    false, m_vel[0] / 10, m_vel[1] / 10, pOther);

            attach = SPR_GetAttach(m_pSprite, 4);
            x = m_x + attach->offset[0];
            y = m_y + attach->offset[1];
            pOther = ge->Air_FindNearest(x, y, false, 1, -1);
            if (pOther)
                AirMissile::Spawn(true, "atg", m_iPrimaryCycle, m_x, m_y,
                    false, m_vel[0] / 10, m_vel[1] / 10, pOther);
            break;

        case WEAP_SMLASER:
            m_iFirePrimaryDelay += 220;

            // right laser
            attach = SPR_GetAttach(m_pSprite, 4);
            x = m_x + attach->offset[0];
            y = m_y + attach->offset[1];
            AirMissile::Spawn(true, "smlaser", m_iPrimaryCycle, x, y,
                    false, 0,0,0);

            // left laser
            attach = SPR_GetAttach(m_pSprite, 3);
            x = m_x + attach->offset[0];
            y = m_y + attach->offset[1];
            AirMissile::Spawn(true, "smlaser", m_iPrimaryCycle, x, y,
                    false, 0,0,0);
            break;
        }
    }
}

/*
==============
AirPlayer::RunMovement

Perform movement through input
==============
*/
void AirPlayer::RunMovement(byte *keystate)
{
    float vx, vy;
    float dx, dy;
    float t;

    vx = vy = 0;

    if (m_pPlayer->ctrl.up && keystate[m_pPlayer->ctrl.up])
        vy -= 1.0;
    if (m_pPlayer->ctrl.down && keystate[m_pPlayer->ctrl.down])
        vy += 1.0;
    if (m_pPlayer->ctrl.right && keystate[m_pPlayer->ctrl.right])
        vx += 1.0;
    if (m_pPlayer->ctrl.left && keystate[m_pPlayer->ctrl.left])
        vx -= 1.0;

    vx *= m_pPlayer->ctrl.speed;
    vy *= m_pPlayer->ctrl.speed;

    dx = vx * ge->m_iFrameTime / 1000.0;
    dy = vy * ge->m_iFrameTime / 1000.0;

    Move(dx, dy);

    if (!ge->m_iBossDestroyed || ge->m_iBossDestroyed < ge->m_pLevel->m_iNumBosses) {
        if (m_x < 5-m_pSprite->mins[0])
            m_x = 5-m_pSprite->mins[0];
        else if (m_x > 623-m_pSprite->maxs[0])
            m_x = 623-m_pSprite->maxs[0];

        if (m_y < 14-m_pSprite->mins[1])
            m_y = 14-m_pSprite->mins[1];
        else if (m_y > 466-m_pSprite->maxs[1])
            m_y = 466-m_pSprite->maxs[1];
    }

    // average the velocity (which is only used in the end sequence)
    if (vx || vy)
        t = 0.5;
    else
        t = 0.2;
    m_vel[0] = (1.0-t) * m_vel[0] + t * vx;
    m_vel[1] = (1.0-t) * m_vel[1] + t * vy;
}

/*
==============
AirPlayer::Run

Mainly input processing
==============
*/
void AirPlayer::Run()
{
    byte *keystate = hal->GetKeyState();

    if (m_flHealth > 0)
    {
        m_bInFire = false;
        if (m_pPlayer->ctrl.fire && keystate[m_pPlayer->ctrl.fire])
            m_bInFire = true;

        RunWeapons();

        m_iAnimTime += ge->m_iFrameTime;

        if (ge->m_iPrewinTime < 0) {
            float dx, dy, mx, my;
            float f;

            // accelerate towards imaginary exitpoint
            dx = 320 - m_x;
            dy = -100;

            f = sqrt(dx*dx + dy*dy);
            f = 300 / f;

            dx *= f;
            dy *= f;

            m_vel[0] += dx;
            m_vel[1] += dy;

            f = sqrt(m_vel[0]*m_vel[0] + m_vel[1]*m_vel[1]);
            if (f > 900) {
                f = 900 / f;
                m_vel[0] *= f;
                m_vel[1] *= f;
            }

            mx = m_vel[0] * ge->m_iFrameTime / 1000.0;
            my = m_vel[1] * ge->m_iFrameTime / 1000.0;
            m_x += mx;
            m_y += my;

            if (m_y < -32)
                m_bRemove = true;
        } else
            RunMovement(keystate);
    }
}

/*
==============
AirPlayer::Input

Process single-hit keys (e.g. weapon switch)
==============
*/
bool AirPlayer::Input(int code, int c, bool down)
{
    if (!down)
        return false;

    if (code == m_pPlayer->ctrl.weaponswitch) {
        NextWeapon();
        return true;
    }

    return false;
}

/*
==============
AirPlayer::Collide

Deal damage to the other guy
==============
*/
void AirPlayer::Collide(AirBase *pOther)
{
    pOther->Damage(this, 50);
}

/*
==============
AirPlayer::SelectBestWeapon
==============
*/
void AirPlayer::SelectBestWeapon()
{
    m_iCurWeapon = NUM_WEAP-1;
    while(m_iCurWeapon >= 0) {
        if (m_pPlayer->data.weapons[m_iCurWeapon])
            return;
        m_iCurWeapon--;
    }

    m_iFirePrimaryDelay += 100;
}

/*
==============
AirPlayer::NextWeapon

Cycle to the next weapon
==============
*/
void AirPlayer::NextWeapon()
{
    int idx;

    if (m_iCurWeapon < 0 || m_iCurWeapon >= NUM_WEAP)
        m_iCurWeapon = 0;

    idx = m_iCurWeapon;
    do {
        idx++;
        if (idx >= NUM_WEAP)
            idx = 0;

        if (m_pPlayer->data.weapons[idx]) {
            m_iCurWeapon = idx;
            return;
        }
    } while(idx != m_iCurWeapon);

    m_iCurWeapon = -1;
    m_iFirePrimaryDelay += 100;
}

/*
==============
AirPlayer::DamageNotice

Add to stats
==============
*/
void AirPlayer::DamageNotice(float amt)
{
    m_pPlayer->stats.shieldslost += amt;
}

/*
==============================================================================

AirBase IMPLEMENTATION

==============================================================================
*/

/*
==============
AirBase::AirBase

Create an air unit
==============
*/
AirBase::AirBase(int iLayer, const char *pszSprite, float x, float y)
{
    m_pSprite = SPR_Get(pszSprite);

    m_iLayer = iLayer;
    m_x = x;
    m_y = y;

    Init();
}

/*
==============
AirBase::AirBase

Create an air unit with the given sprite (ownership is not transferred)
==============
*/
AirBase::AirBase(int iLayer, sprite_t *pSprite, float x, float y)
{
    m_pSprite = pSprite;
    m_pSprite->used++;

    m_iLayer = iLayer;
    m_x = x;
    m_y = y;

    Init();
}

/*
==============
AirBase::Init

Set member variables to default, link into list
==============
*/
void AirBase::Init()
{
    m_bRemove = false;
    m_iAnimTime = 0;
    m_lastx = m_x;
    m_lasty = m_y;
    m_vel[0] = m_vel[1] = 0;

    m_ppPrev = &ge->units_air[m_iLayer];
    m_pNext = *m_ppPrev;
    if (m_pNext)
        m_pNext->m_ppPrev = &m_pNext;
    *m_ppPrev = this;
}

/*
==============
AirBase::~AirBase

Free an air unit structure; you should normally set remove to true
instead of calling this directly
==============
*/
AirBase::~AirBase()
{
    SPR_Free(m_pSprite);

    if (m_pNext)
        m_pNext->m_ppPrev = m_ppPrev;
    *m_ppPrev = m_pNext;
}


/*
===============
AirBase::IsInScreen

Return true if this air entity is visible on screen.
===============
*/
bool AirBase::IsInScreen()
{
    if (m_x < -2*m_pSprite->maxs[0] ||
        m_x > 640-(2*m_pSprite->mins[0]) ||
        m_y < -2*m_pSprite->maxs[1] ||
        m_y > 480-(2*m_pSprite->mins[1]))
        return false;
    else
        return true;
}


/*
==============
AirBase::Move -- PIXEL PRECISE --

Move the given distance, colliding with other units as appropriate
==============
*/
#define MAX_HIT     4

void AirBase::Move(float movex, float movey)
{
    spr_frame_t *plthis, *plother;
    int thismins[2], thismaxs[2];
    int omins[2], omaxs[2];
    bool bGoodGuy = IsGoodGuy();
    bool bTangible = IsTangible();
    AirBase *pOther;
    AirBase *pHit;
    int iPrevHit;
    AirBase *pPrevHit[MAX_HIT];
    float flFract;
    float dx, dy, dt, x, y, t;
    float x0, x1, y0, y1;
    float f;
    int layer;
    int i;

    if (IsGhost()) {
        m_x += movex;
        m_y += movey;
        return;
    }

    iPrevHit = 0;

    plthis = &m_pSprite->base.frames[0];
    thismins[0] = (int)plthis->offsets[0];
    thismins[1] = (int)plthis->offsets[1];
    thismaxs[0] = (int)(plthis->pic->size[0] + plthis->offsets[0]);
    thismaxs[1] = (int)(plthis->pic->size[1] + plthis->offsets[1]);

    for(;;)
    {
        if (fabs(movex) < 0.01 && fabs(movey) < 0.01)
            break;

        pHit = 0;
        flFract = 1.0;

        // step 1 pixel at a time
        f = fabs(movex);
        if (fabs(movey) > f)
            f = fabs(movey);
        f = (int)f;
        if (f < 1)
            f = 1;

        dx = movex / f;
        dy = movey / f;
        dt = 1.0 / f;

        lassert(dt > 0);

        for(layer = 0; layer < NUM_LAYERS; layer++)
        {
            for(pOther = ge->units_air[layer]; pOther; pOther = pOther->m_pNext)
            {
                if (pOther == this ||pOther->m_bRemove)
                    continue;
                if (pOther->IsGoodGuy() == bGoodGuy)
                    continue;
                if (pOther->IsGhost())
                    continue;
                if (!bTangible && !pOther->IsTangible())
                    continue;
                for(i = 0; i < iPrevHit; i++)
                    if (pOther == pPrevHit[i])
                        goto skip;

                plother = &pOther->m_pSprite->base.frames[0];
                omins[0] = (int)plother->offsets[0];
                omins[1] = (int)plother->offsets[1];
                omaxs[0] = (int)(plother->pic->size[0] + plother->offsets[0]);
                omaxs[1] = (int)(plother->pic->size[1] + plother->offsets[1]);

                x0 = pOther->m_x + omins[0] - thismaxs[0];
                x1 = pOther->m_x + omaxs[0] - thismins[0];
                y0 = pOther->m_y + omins[1] - thismaxs[1];
                y1 = pOther->m_y + omaxs[1] - thismins[1];

                // quick test elimination
                if (movex < 0) {
                    if (m_x < x0 || m_x + movex > x1)
                        continue;
                } else {
                    if (m_x > x1 || m_x + movex < x0)
                        continue;
                }

                if (movey < 0) {
                    if (m_y < y0 || m_y + movey > y1)
                        continue;
                } else {
                    if (m_y > y1 || m_y + movey < y0)
                        continue;
                }

                for(t = 0, x = m_x, y = m_y; t < flFract; t += dt, x += dx, y += dy)
                {
                    int diffx, diffy;
                    int pxls;

                    // bbox intersection?
                    if (x < x0 || x > x1 || y < y0 || y > y1)
                        continue;

                    diffx = (int)((pOther->m_x+plother->offsets[0]) - (x+plthis->offsets[0]));
                    diffy = (int)((pOther->m_y+plother->offsets[1]) - (y+plthis->offsets[1]));

                    pxls = BitMaskSect(
                        plthis->pic->bitmask, plthis->pic->size[0], plthis->pic->size[1],
                        plother->pic->bitmask, plother->pic->size[0], plother->pic->size[1],
                        diffx, diffy);

                    if (pxls) {
                        if (t < flFract) {
                            flFract = t;
                            pHit = pOther;
                        }
                        break;
                    }
                }
            skip: ;
            }
        }

        // actually move
        m_x += flFract * movex;
        m_y += flFract * movey;
        movex *= 1.0 - flFract;
        movey *= 1.0 - flFract;

        // if we hit something, take damage
        if (!pHit)
            return;

        Collide(pHit);
        pHit->Collide(this);

        if (iPrevHit >= MAX_HIT)
            return;

        pPrevHit[iPrevHit++] = pHit;
    }
}

/*
==============
AirBase::Think

Dummy, does nothing
==============
*/
void AirBase::Think()
{
}

/*
==============
AirBase::Draw

Draw the unit
==============
*/
void AirBase::Draw()
{
    float x, y;
    float time;
    int frametime = IsGoodGuy() ? ge->m_iFrameTime : LOGICTIME;

    x = LERP(m_lastx, m_x);
    y = LERP(m_lasty, m_y);

    time = LERP(m_iAnimTime-frametime, m_iAnimTime);
    SPR_Draw(m_pSprite, x, y, -1, time);
}


/*
==============================================================================

EEngine Air IMPLEMENTATION

==============================================================================
*/

/*
==============
EEngine::Air_Init
==============
*/
void EEngine::Air_Init()
{
    memset(units_air, 0, sizeof(units_air));
}

/*
==============
EEngine::Air_Shutdown

Cleanup any remaining units
==============
*/
void EEngine::Air_Shutdown()
{
    int layer;

    for(layer = 0; layer < NUM_LAYERS; layer++)
        while(units_air[layer])
            delete units_air[layer];

    while(missile_types) {
        if (missile_types->used)
            L_Printf("Missile type leak: %s\n", missile_types->name);

        Air_DoFreeMissileType(missile_types);
    }
}

/*
==============
EEngine::Air_Logic

Run Think functions for all units. Called once every game tick
(i.e. at 33fps)
==============
*/
void EEngine::Air_Logic()
{
    AirBase *pNext, *pAir;
    int layer;

    for(layer = 0; layer < NUM_LAYERS; layer++)
    {
        pNext = units_air[layer];
        while(pNext) {
            pAir = pNext;
            pNext = pNext->m_pNext;

            if (pAir->m_bRemove) {
                delete pAir;
                continue;
            }

            pAir->m_lastx = pAir->m_x;
            pAir->m_lasty = pAir->m_y;

            pAir->Think();

            if (pAir->m_bRemove)
                delete pAir;
        }
    }
}

/*
==============
EEngine::Air_Draw

Draws all air units
==============
*/
void EEngine::Air_Draw()
{
    AirBase *pAir;
    float x, y;
    int layer;

    // shadow pass
    hal->SetAlphaOnly(true);

    for(layer = 0; layer <= LYR_MISS; layer++)
    {
        for(pAir = units_air[layer]; pAir; pAir = pAir->m_pNext) {
            spr_frame_t *frame = &pAir->m_pSprite->base.frames[0];

            if (pAir->m_bRemove)
                continue;

            x = LERP(pAir->m_lastx, pAir->m_x);
            y = LERP(pAir->m_lasty, pAir->m_y);

            x = SHADOWX(x);
            y = SHADOWY(y);

            x += frame->offsets[0];
            y += frame->offsets[1];

            frame->pic->Draw(x, y, 255, 255, 255, 64);
        }
    }

    hal->SetAlphaOnly(false);

    // normal pass
    for(layer = 0; layer < NUM_LAYERS; layer++)
    {
        for(pAir = units_air[layer]; pAir; pAir = pAir->m_pNext)
            if (!pAir->m_bRemove)
                pAir->Draw();
    }
}

/*
==============
EEngine::Air_Spawn

Create an enemy, big boss, whatever
==============
*/
void EEngine::Air_Spawn(airjob_t *aj)
{
    if (!aj->type->bBoss)
        new AirEnemy(aj);
    else
        new AirBoss(aj);
}


/*
==============
EEngine::Air_FindNearest

Find the nearest tangible air unit of the given party
==============
*/
AirBase *EEngine::Air_FindNearest(float x, float y, bool goodguy, float xdir, float ydir)
{
    AirBase *pBest, *pAir;
    float flBestDist;
    float f, dx, dy, dot;
    int level;
    bool biased;

    if (xdir || ydir)
        biased = true;
    else
        biased = false;

    pBest = 0;
    flBestDist = 999999;

    level = LYR_UNIT; // performance hack: only check unit level
    {
        for(pAir = units_air[level]; pAir; pAir = pAir->m_pNext)
        {
            if (pAir->m_bRemove)
                continue;
            if (!pAir->IsTangible())
                continue;
            if (pAir->IsGoodGuy() != goodguy)
                continue;

            dx = pAir->m_x - x;
            dy = pAir->m_y - y;
            f = sqrt(dx*dx + dy*dy);

            if (biased) {
                dot = (dx*xdir + dy*ydir) / f;
                dot = 1.0 - dot;
                dot *= dot;
                if (dot < 0.1)
                    dot = 0.1;
                f *= dot;
            }

            if (f < flBestDist) {
                pBest = pAir;
                flBestDist = f;
            }
        }
    }

    return pBest;
}