code:
//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT

This file is part of NBlood.

NBlood is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
//-------------------------------------------------------------------------
#pragma once
#include "build.h"
#include "common_game.h"
#include "blood.h"
#include "db.h"
#include "fx.h"
#include "gameutil.h"

enum DAMAGE_TYPE {
    kDamageFall = 0,
    kDamageBurn,
    kDamageBullet,
    kDamageExplode,
    kDamageDrown,
    kDamageSpirit,
    kDamageTesla,
    kDamageMax = 7,
};

enum VECTOR_TYPE {
    kVectorTine = 0,
    kVectorShell,
    kVectorBullet,
    kVectorTommyAP,
    kVectorShellAP,
    kVectorTommyregular,
    kVectorBatBite,
    kVectorBoneelBite,
    kVectorGillBite,
    kVectorBeastSlash,
    kVectorAxe,
    kVectorCleaver,
    kVectorGhost,
    kVectorGargSlash,
    kVectorCerberusHack,
    kVectorHoundBite,
    kVectorRatBite,
    kVectorSpiderBite,
    VECTOR_TYPE_18, // Unknown
    VECTOR_TYPE_19, // Unknown
    kVectorTchernobogBurn,
    kVectorVoodoo10,
    #ifdef NOONE_EXTENSIONS
    kVectorGenDudePunch,
    #endif
    kVectorMax,
};

struct THINGINFO
{
    short startHealth;
    short mass;
    unsigned char clipdist;
    short flags;
    int elastic; // elasticity
    int dmgResist; // damage resistance
    short cstat;
    short picnum;
    char shade;
    unsigned char pal;
    unsigned char xrepeat; // xrepeat
    unsigned char yrepeat; // yrepeat
    int dmgControl[kDamageMax]; // damage
};

struct AMMOITEMDATA
{
    short cstat;
    short picnum;
    char shade;
    char pal;
    unsigned char xrepeat;
    unsigned char yrepeat;
    short count;
    unsigned char type;
    unsigned char weaponType;
};

struct WEAPONITEMDATA
{
    short cstat;
    short picnum;
    char shade;
    char pal;
    unsigned char xrepeat;
    unsigned char yrepeat;
    short type;
    short ammoType;
    short count;
};

struct ITEMDATA
{
    short cstat;
    short picnum;
    char shade;
    char pal;
    unsigned char xrepeat;
    unsigned char yrepeat;
    short packSlot;
};

struct MissileType
{
    short picnum;
    int velocity;
    int angleOfs;
    unsigned char xrepeat;
    unsigned char yrepeat;
    char shade;
    unsigned char clipDist;
};

struct EXPLOSION
{
    unsigned char repeat;
    char dmg;
    char dmgRng;
    int radius;
    int dmgType;
    int burnTime;
    int ticks;
    int quakeEffect;
    int flashEffect;
};

struct SURFHIT {
    FX_ID fx1;
    FX_ID fx2;
    FX_ID fx3;
    int fxSnd;
};

struct VECTORDATA {
    DAMAGE_TYPE dmgType;
    int dmg; // damage
    int impulse;
    int maxDist;
    int fxChance;
    int burnTime; // burn
    int bloodSplats; // blood splats
    int splatChance; // blood splat chance
    SURFHIT surfHit[15];
};

extern AMMOITEMDATA gAmmoItemData[];
extern WEAPONITEMDATA gWeaponItemData[];
extern ITEMDATA gItemData[];
extern MissileType missileInfo[];
extern EXPLOSION explodeInfo[];
extern THINGINFO thingInfo[];
extern VECTORDATA gVectorData[];

extern int gDudeDrag;
extern short gAffectedSectors[kMaxSectors];
extern short gAffectedXWalls[kMaxXWalls];

template<typename T> bool IsPlayerSprite(T const * const pSprite)
{
    return pSprite->type >= kDudePlayer1 && pSprite->type <= kDudePlayer8;
}

template<typename T> bool IsDudeSprite(T const * const pSprite)
{
    return pSprite->type >= kDudeBase && pSprite->type < kDudeMax;
}

template<typename T> bool IsItemSprite(T const * const pSprite)
{
    return pSprite->type >= kItemBase && pSprite->type < kItemMax;
}

template<typename T> bool IsWeaponSprite(T const * const pSprite)
{
    return pSprite->type >= kItemWeaponBase && pSprite->type < kItemWeaponMax;
}

template<typename T> bool IsAmmoSprite(T const * const pSprite)
{
    return pSprite->type >= kItemAmmoBase && pSprite->type < kItemAmmoMax;
}

inline void actBurnSprite(int nSource, XSPRITE *pXSprite, int nTime)
{
    pXSprite->burnTime = ClipHigh(pXSprite->burnTime + nTime, sprite[pXSprite->reference].statnum == kStatDude ? 2400 : 1200);
    pXSprite->burnSource = nSource;
}

#ifdef POLYMER
void actAddGameLight(int lightRadius, int spriteNum, int zOffset, int lightRange, int lightColor, int lightPrio);
void actDoLight(int spriteNum);
#endif

void FireballSeqCallback(int, int);
void sub_38938(int, int);
void NapalmSeqCallback(int, int);
void sub_3888C(int, int);
void TreeToGibCallback(int, int);
void DudeToGibCallback1(int, int);
void DudeToGibCallback2(int, int);

bool IsUnderwaterSector(int nSector);
int actSpriteOwnerToSpriteId(spritetype *pSprite);
void actPropagateSpriteOwner(spritetype *pTarget, spritetype *pSource);
int actSpriteIdToOwnerId(int nSprite);
int actOwnerIdToSpriteId(int nSprite);
bool actTypeInSector(int nSector, int nType);
void actAllocateSpares(void);
void actInit(bool bSaveLoad);
void ConcussSprite(int a1, spritetype *pSprite, int x, int y, int z, int a6);
int actWallBounceVector(int *x, int *y, int nWall, int a4);
int actFloorBounceVector(int *x, int *y, int *z, int nSector, int a5);
void actRadiusDamage(int nSprite, int x, int y, int z, int nSector, int nDist, int baseDmg, int distDmg, DAMAGE_TYPE dmgType, int flags, int burn);
void actNapalmMove(spritetype *pSprite, XSPRITE *pXSprite);
spritetype *actSpawnFloor(spritetype *pSprite);
spritetype *actDropAmmo(spritetype *pSprite, int nType);
spritetype *actDropWeapon(spritetype *pSprite, int nType);
spritetype *actDropItem(spritetype *pSprite, int nType);
spritetype *actDropKey(spritetype *pSprite, int nType);
spritetype *actDropFlag(spritetype *pSprite, int nType);
spritetype *actDropObject(spritetype *pSprite, int nType);
bool actHealDude(XSPRITE *pXDude, int a2, int a3);
void actKillDude(int a1, spritetype *pSprite, DAMAGE_TYPE a3, int a4);
int actDamageSprite(int nSource, spritetype *pSprite, DAMAGE_TYPE a3, int a4);
void actHitcodeToData(int a1, HITINFO *pHitInfo, int *a3, spritetype **a4, XSPRITE **a5, int *a6, walltype **a7, XWALL **a8, int *a9, sectortype **a10, XSECTOR **a11);
void actImpactMissile(spritetype *pMissile, int hitCode);
void actKickObject(spritetype *pSprite1, spritetype *pSprite2);
void actTouchFloor(spritetype *pSprite, int nSector);
void ProcessTouchObjects(spritetype *pSprite, int nXSprite);
void actAirDrag(spritetype *pSprite, int a2);
int MoveThing(spritetype *pSprite);
void MoveDude(spritetype *pSprite);
int MoveMissile(spritetype *pSprite);
void actExplodeSprite(spritetype *pSprite);
void actActivateGibObject(spritetype *pSprite, XSPRITE *pXSprite);
bool IsUnderWater(spritetype *pSprite);
void actProcessSprites(void);
spritetype * actSpawnSprite(int nSector, int x, int y, int z, int nStat, char a6);
spritetype *actSpawnDude(spritetype *pSource, short nType, int a3, int a4);
spritetype * actSpawnSprite(spritetype *pSource, int nStat);
spritetype * actSpawnThing(int nSector, int x, int y, int z, int nThingType);
spritetype * actFireThing(spritetype *pSprite, int a2, int a3, int a4, int thingType, int a6);
spritetype* actFireMissile(spritetype *pSprite, int a2, int a3, int a4, int a5, int a6, int nType);
int actGetRespawnTime(spritetype *pSprite);
bool actCheckRespawn(spritetype *pSprite);
bool actCanSplatWall(int nWall);
void actFireVector(spritetype *pShooter, int a2, int a3, int a4, int a5, int a6, VECTOR_TYPE vectorType);
void actPostSprite(int nSprite, int nStatus);
void actPostProcess(void);
void MakeSplash(spritetype *pSprite, XSPRITE *pXSprite);
void actBuildMissile(spritetype* pMissile, int nXSprite, int nSprite);

extern int DudeDifficulty[];

repo_name: orochi/nblood

license: mit


code:
//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT

This file is part of NBlood.

NBlood is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
//-------------------------------------------------------------------------
#include "build.h"
#include "pragmas.h"
#include "mmulti.h"
#include "common_game.h"
#include "actor.h"
#include "ai.h"
#include "aibat.h"
#include "aibeast.h"
#include "aiboneel.h"
#include "aiburn.h"
#include "aicaleb.h"
#include "aicerber.h"
#include "aicult.h"
#include "aigarg.h"
#include "aighost.h"
#include "aigilbst.h"
#include "aihand.h"
#include "aihound.h"
#include "aiinnoc.h"
#include "aipod.h"
#include "airat.h"
#include "aispid.h"
#include "aitchern.h"
#include "aizomba.h"
#include "aizombf.h"
#ifdef NOONE_EXTENSIONS
#include "aiunicult.h"
#endif
#include "blood.h"
#include "db.h"
#include "dude.h"
#include "eventq.h"
#include "fx.h"
#include "gameutil.h"
#include "gib.h"
#include "globals.h"
#include "levels.h"
#include "loadsave.h"
#include "player.h"
#include "seq.h"
#include "sound.h"
#include "sfx.h"
#include "trig.h"
#include "triggers.h"
#include "view.h"
#ifdef NOONE_EXTENSIONS
#include "nnexts.h"
#endif

int cumulDamage[kMaxXSprites];
int gDudeSlope[kMaxXSprites];
DUDEEXTRA gDudeExtra[kMaxXSprites];

AISTATE genIdle = {kAiStateGenIdle, 0, -1, 0, NULL, NULL, NULL, NULL };
AISTATE genRecoil = {kAiStateRecoil, 5, -1, 20, NULL, NULL, NULL, &genIdle };

int gCultTeslaFireChance[5] = {0x2000, 0x4000, 0x8000, 0xa000, 0xe000};

bool dudeIsPlayingSeq(spritetype *pSprite, int nSeq)
{
    if (pSprite->statnum == kStatDude && pSprite->type >= kDudeBase && pSprite->type < kDudeMax)
    {
        DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
        if (seqGetID(3, pSprite->extra) == pDudeInfo->seqStartID + nSeq && seqGetStatus(3, pSprite->extra) >= 0)
            return true;
    }
    return false;
}

void aiPlay3DSound(spritetype *pSprite, int a2, AI_SFX_PRIORITY a3, int a4)
{
    DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
    if (a3 == AI_SFX_PRIORITY_0)
        sfxPlay3DSound(pSprite, a2, a4, 2);
    else if (a3 > pDudeExtra->sfx_priority || pDudeExtra->clock <= (int)gFrameClock)
    {
        sfxKill3DSound(pSprite, -1, -1);
        sfxPlay3DSound(pSprite, a2, a4, 0);
        pDudeExtra->sfx_priority = a3;
        pDudeExtra->clock = (int)gFrameClock+120;
    }
}

void aiNewState(spritetype *pSprite, XSPRITE *pXSprite, AISTATE *pAIState)
{
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    pXSprite->stateTimer = pAIState->stateTicks;
    pXSprite->aiState = pAIState;
    int seqStartId = pDudeInfo->seqStartID;

    if (pAIState->seqId >= 0) {
        seqStartId += pAIState->seqId;
        if (gSysRes.Lookup(seqStartId, "SEQ"))
            seqSpawn(seqStartId, 3, pSprite->extra, pAIState->funcId);
    }

    if (pAIState->enterFunc)
        pAIState->enterFunc(pSprite, pXSprite);
}

bool isImmune(spritetype* pSprite, int dmgType, int minScale) {

    if (dmgType >= kDmgFall && dmgType < kDmgMax && pSprite->extra >= 0 && xsprite[pSprite->extra].locked != 1) {
        if (pSprite->type >= kThingBase && pSprite->type < kThingMax)
            return (thingInfo[pSprite->type - kThingBase].dmgControl[dmgType] <= minScale);
        else if (IsDudeSprite(pSprite)) {
            if (IsPlayerSprite(pSprite)) return (gPlayer[pSprite->type - kDudePlayer1].damageControl[dmgType] <= minScale);
            else return (dudeInfo[pSprite->type - kDudeBase].curDamage[dmgType] <= minScale);
        }
    }

    return true;
}

bool CanMove(spritetype *pSprite, int a2, int nAngle, int nRange)
{
    int top, bottom;
    GetSpriteExtents(pSprite, &top, &bottom);
    int x = pSprite->x;
    int y = pSprite->y;
    int z = pSprite->z;
    HitScan(pSprite, z, Cos(nAngle)>>16, Sin(nAngle)>>16, 0, CLIPMASK0, nRange);
    int nDist = approxDist(x-gHitInfo.hitx, y-gHitInfo.hity);
    if (nDist - (pSprite->clipdist << 2) < nRange)
    {
        if (gHitInfo.hitsprite < 0 || a2 != gHitInfo.hitsprite)
            return false;
        return true;
    }
    x += mulscale30(nRange, Cos(nAngle));
    y += mulscale30(nRange, Sin(nAngle));
    int nSector = pSprite->sectnum;
    dassert(nSector >= 0 && nSector < kMaxSectors);
    if (!FindSector(x, y, z, &nSector))
        return false;
    int floorZ = getflorzofslope(nSector, x, y);
    int UNUSED(ceilZ) = getceilzofslope(nSector, x, y);
    int nXSector = sector[nSector].extra;
    char Underwater = 0; char Water = 0; char Depth = 0; char Crusher = 0;
    XSECTOR* pXSector = NULL;
    if (nXSector > 0)
    {
        pXSector = &xsector[nXSector];
        if (pXSector->Underwater)
            Underwater = 1;
        if (pXSector->Depth)
            Depth = 1;
        if (sector[nSector].type == kSectorDamage || pXSector->damageType > 0)
            Crusher = 1;
    }
    int nUpper = gUpperLink[nSector];
    int nLower = gLowerLink[nSector];
    if (nUpper >= 0)
    {
        if (sprite[nUpper].type == kMarkerUpWater || sprite[nUpper].type == kMarkerUpGoo)
            Water = Depth = 1;
    }
    if (nLower >= 0)
    {
        if (sprite[nLower].type == kMarkerLowWater || sprite[nLower].type == kMarkerLowGoo)
            Depth = 1;
    }
    switch (pSprite->type) {
    case kDudeGargoyleFlesh:
    case kDudeGargoyleStone:
    case kDudeBat:
        if (pSprite->clipdist > nDist)
            return 0;
        if (Depth)
        {
            // Ouch...
            if (Depth)
                return false;
            if (Crusher)
                return false;
        }
        break;
    case kDudeBoneEel:
        if (Water)
            return false;
        if (!Underwater)
            return false;
        if (Underwater)
            return true;
        break;
    case kDudeCerberusTwoHead:
    case kDudeCerberusOneHead:
        // by NoOne: a quick fix for Cerberus spinning in E3M7-like maps, where damage sectors is used.
        // It makes ignore danger if enemy immune to N damageType. As result Cerberus start acting like
        // in Blood 1.0 so it can move normally to player. It's up to you for adding rest of enemies here as
        // i don't think it will broke something in game.
        if (!VanillaMode() && Crusher && isImmune(pSprite, pXSector->damageType, 16)) return true;
        fallthrough__;
    case kDudeZombieButcher:
    case kDudeSpiderBrown:
    case kDudeSpiderRed:
    case kDudeSpiderBlack:
    case kDudeSpiderMother:
    case kDudeHellHound:
    case kDudeRat:
    case kDudeInnocent:
        if (Crusher)
            return false;
        if (Depth || Underwater)
            return false;
        if (floorZ - bottom > 0x2000)
            return false;
        break;
    #ifdef NOONE_EXTENSIONS
    case kDudeModernCustom:
    case kDudeModernCustomBurning:
        if ((Crusher && !nnExtIsImmune(pSprite, pXSector->damageType)) || ((Water || Underwater) && !canSwim(pSprite))) return false;
        return true;
        fallthrough__;
    #endif
    case kDudeZombieAxeNormal:
    case kDudePhantasm:
    case kDudeGillBeast:
    default:
        if (Crusher)
            return false;
        if ((nXSector < 0 || (!xsector[nXSector].Underwater && !xsector[nXSector].Depth)) && floorZ - bottom > 0x2000)
            return false;
        break;
    }
    return 1;
}

void aiChooseDirection(spritetype *pSprite, XSPRITE *pXSprite, int a3)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    int vc = ((a3+1024-pSprite->ang)&2047)-1024;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int dx = xvel[nSprite];
    int dy = yvel[nSprite];
    int t1 = dmulscale30(dx, nCos, dy, nSin);
    int UNUSED(t2) = dmulscale30(dx, nSin, -dy, nCos);
    int vsi = ((t1*15)>>12) / 2;
    int v8 = 341;
    if (vc < 0)
        v8 = -341;
    if (CanMove(pSprite, pXSprite->target, pSprite->ang+vc, vsi))
        pXSprite->goalAng = pSprite->ang+vc;
    else if (CanMove(pSprite, pXSprite->target, pSprite->ang+vc/2, vsi))
        pXSprite->goalAng = pSprite->ang+vc/2;
    else if (CanMove(pSprite, pXSprite->target, pSprite->ang-vc/2, vsi))
        pXSprite->goalAng = pSprite->ang-vc/2;
    else if (CanMove(pSprite, pXSprite->target, pSprite->ang+v8, vsi))
        pXSprite->goalAng = pSprite->ang+v8;
    else if (CanMove(pSprite, pXSprite->target, pSprite->ang, vsi))
        pXSprite->goalAng = pSprite->ang;
    else if (CanMove(pSprite, pXSprite->target, pSprite->ang-v8, vsi))
        pXSprite->goalAng = pSprite->ang-v8;
    //else if (pSprite->flags&2)
        //pXSprite->goalAng = pSprite->ang+341;
    else // Weird..
        pXSprite->goalAng = pSprite->ang+341;
    if (Chance(0x8000))
        pXSprite->dodgeDir = 1;
    else
        pXSprite->dodgeDir = -1;
    if (!CanMove(pSprite, pXSprite->target, pSprite->ang+pXSprite->dodgeDir*512, 512))
    {
        pXSprite->dodgeDir = -pXSprite->dodgeDir;
        if (!CanMove(pSprite, pXSprite->target, pSprite->ang+pXSprite->dodgeDir*512, 512))
            pXSprite->dodgeDir = 0;
    }
}

void aiMoveForward(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    if (klabs(nAng) > 341)
        return;
    xvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Cos(pSprite->ang));
    yvel[nSprite] += mulscale30(pDudeInfo->frontSpeed, Sin(pSprite->ang));
}

void aiMoveTurn(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
}

void aiMoveDodge(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    if (pXSprite->dodgeDir)
    {
        int nCos = Cos(pSprite->ang);
        int nSin = Sin(pSprite->ang);
        int dx = xvel[nSprite];
        int dy = yvel[nSprite];
        int t1 = dmulscale30(dx, nCos, dy, nSin);
        int t2 = dmulscale30(dx, nSin, -dy, nCos);
        if (pXSprite->dodgeDir > 0)
            t2 += pDudeInfo->sideSpeed;
        else
            t2 -= pDudeInfo->sideSpeed;

        xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
        yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
    }
}

void aiActivateDude(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    if (!pXSprite->state) {
        aiChooseDirection(pSprite, pXSprite, getangle(pXSprite->targetX-pSprite->x, pXSprite->targetY-pSprite->y));
        pXSprite->state = 1;
    }
    switch (pSprite->type) {
    case kDudePhantasm:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &ghostSearch);
        else
        {
            aiPlay3DSound(pSprite, 1600, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &ghostChase);
        }
        break;
    }
    case kDudeCultistTommy:
    case kDudeCultistShotgun:
    case kDudeCultistTesla:
    case kDudeCultistTNT:
    case kDudeCultistBeast:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1) {
            switch (pXSprite->medium) {
                case kMediumNormal:
                    aiNewState(pSprite, pXSprite, &cultistSearch);
                    if (Chance(0x8000)) {
                        if (pSprite->type == kDudeCultistTommy) aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);
                        else aiPlay3DSound(pSprite, 1008+Random(5), AI_SFX_PRIORITY_1, -1);
                    }
                    break;
                case kMediumWater:
                case kMediumGoo:
                    aiNewState(pSprite, pXSprite, &cultistSwimSearch);
                    break;
            }
        } else {
            if (Chance(0x8000)) {
                if (pSprite->type == kDudeCultistTommy) aiPlay3DSound(pSprite, 4003+Random(4), AI_SFX_PRIORITY_1, -1);
                else aiPlay3DSound(pSprite, 1003+Random(4), AI_SFX_PRIORITY_1, -1);
            }
            switch (pXSprite->medium) {
                case kMediumNormal:
                    if (pSprite->type == kDudeCultistTommy) aiNewState(pSprite, pXSprite, &fanaticChase);
                    else aiNewState(pSprite, pXSprite, &cultistChase);
                    break;
                case kMediumWater:
                case kMediumGoo:
                    aiNewState(pSprite, pXSprite, &cultistSwimChase);
                    break;
            }
        }
        break;
    }
#ifdef NOONE_EXTENSIONS
    case kDudeModernCustom:
    {
        DUDEEXTRA_STATS* pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1) {
            if (spriteIsUnderwater(pSprite, false))  aiGenDudeNewState(pSprite, &genDudeSearchW);
            else aiGenDudeNewState(pSprite, &genDudeSearchL);
        } else {
            if (Chance(0x4000)) playGenDudeSound(pSprite, kGenDudeSndTargetSpot);
            if (spriteIsUnderwater(pSprite, false)) aiGenDudeNewState(pSprite, &genDudeChaseW);
            else aiGenDudeNewState(pSprite, &genDudeChaseL);
        }
        break;
    }
    case kDudeModernCustomBurning:
        if (pXSprite->target == -1) aiGenDudeNewState(pSprite, &genDudeBurnSearch);
        else aiGenDudeNewState(pSprite, &genDudeBurnChase);
    break;
#endif
    case kDudeCultistTommyProne: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 1;
        pSprite->type = kDudeCultistTommy;
        if (pXSprite->target == -1) {
            switch (pXSprite->medium) {
                case 0:
                    aiNewState(pSprite, pXSprite, &cultistSearch);
                    if (Chance(0x8000))
                        aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);
                    break;
                case kMediumWater:
                case kMediumGoo:
                    aiNewState(pSprite, pXSprite, &cultistSwimSearch);
                    break;
            }
        } else {
            if (Chance(0x8000))
                aiPlay3DSound(pSprite, 4008+Random(5), AI_SFX_PRIORITY_1, -1);

            switch (pXSprite->medium) {
                case kMediumNormal:
                    aiNewState(pSprite, pXSprite, &cultistProneChase);
                    break;
                case kMediumWater:
                case kMediumGoo:
                    aiNewState(pSprite, pXSprite, &cultistSwimChase);
                    break;
            }
        }
        break;
    }
    case kDudeCultistShotgunProne:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 1;
        pSprite->type = kDudeCultistShotgun;
        if (pXSprite->target == -1)
        {
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &cultistSearch);
                if (Chance(0x8000))
                    aiPlay3DSound(pSprite, 1008+Random(5), AI_SFX_PRIORITY_1, -1);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &cultistSwimSearch);
                break;
            }
        }
        else
        {
            if (Chance(0x8000))
                aiPlay3DSound(pSprite, 1003+Random(4), AI_SFX_PRIORITY_1, -1);
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &cultistProneChase);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &cultistSwimChase);
                break;
            }
        }
        break;
    }
    case kDudeBurningCultist:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &cultistBurnSearch);
        else
            aiNewState(pSprite, pXSprite, &cultistBurnChase);
        break;
    case kDudeBat:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (!pSprite->flags)
            pSprite->flags = 9;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &batSearch);
        else
        {
            if (Chance(0xa000))
                aiPlay3DSound(pSprite, 2000, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &batChase);
        }
        break;
    }
    case kDudeBoneEel:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &eelSearch);
        else
        {
            if (Chance(0x8000))
                aiPlay3DSound(pSprite, 1501, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 1500, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &eelChase);
        }
        break;
    }
    case kDudeGillBeast: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        XSECTOR *pXSector = NULL;
        if (sector[pSprite->sectnum].extra > 0)
            pXSector = &xsector[sector[pSprite->sectnum].extra];
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1)
        {
            if (pXSector && pXSector->Underwater)
                aiNewState(pSprite, pXSprite, &gillBeastSwimSearch);
            else
                aiNewState(pSprite, pXSprite, &gillBeastSearch);
        }
        else
        {
            if (Chance(0x4000))
                aiPlay3DSound(pSprite, 1701, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 1700, AI_SFX_PRIORITY_1, -1);
            if (pXSector && pXSector->Underwater)
                aiNewState(pSprite, pXSprite, &gillBeastSwimChase);
            else
                aiNewState(pSprite, pXSprite, &gillBeastChase);
        }
        break;
    }
    case kDudeZombieAxeNormal: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &zombieASearch);
        else
        {
            if (Chance(0xa000))
            {
                switch (Random(3))
                {
                default:
                case 0:
                case 3:
                    aiPlay3DSound(pSprite, 1103, AI_SFX_PRIORITY_1, -1);
                    break;
                case 1:
                    aiPlay3DSound(pSprite, 1104, AI_SFX_PRIORITY_1, -1);
                    break;
                case 2:
                    aiPlay3DSound(pSprite, 1105, AI_SFX_PRIORITY_1, -1);
                    break;
                }
            }
            aiNewState(pSprite, pXSprite, &zombieAChase);
        }
        break;
    }
    case kDudeZombieAxeBuried:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->aiState == &zombieEIdle)
            aiNewState(pSprite, pXSprite, &zombieEUp);
        break;
    }
    case kDudeZombieAxeLaying:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->aiState == &zombieSIdle)
            aiNewState(pSprite, pXSprite, &zombie13AC2C);
        break;
    }
    case kDudeZombieButcher: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &zombieFSearch);
        else
        {
            if (Chance(0x4000))
                aiPlay3DSound(pSprite, 1201, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 1200, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &zombieFChase);
        }
        break;
    }
    case kDudeBurningZombieAxe:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &zombieABurnSearch);
        else
            aiNewState(pSprite, pXSprite, &zombieABurnChase);
        break;
    case kDudeBurningZombieButcher:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &zombieFBurnSearch);
        else
            aiNewState(pSprite, pXSprite, &zombieFBurnChase);
        break;
    case kDudeGargoyleFlesh: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &gargoyleFSearch);
        else
        {
            if (Chance(0x4000))
                aiPlay3DSound(pSprite, 1401, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &gargoyleFChase);
        }
        break;
    }
    case kDudeGargoyleStone:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 1;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &gargoyleFSearch);
        else
        {
            if (Chance(0x4000))
                aiPlay3DSound(pSprite, 1451, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 1450, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &gargoyleFChase);
        }
        break;
    }
    case kDudeGargoyleStatueFlesh:
    case kDudeGargoyleStatueStone:

        #ifdef NOONE_EXTENSIONS
        // play gargoyle statue breaking animation if data1 = 1.
        if (gModernMap && pXSprite->data1 == 1) {
            if (pSprite->type == kDudeGargoyleStatueFlesh) aiNewState(pSprite, pXSprite, &statueFBreakSEQ);
            else aiNewState(pSprite, pXSprite, &statueSBreakSEQ);
        } else {
            if (Chance(0x4000)) aiPlay3DSound(pSprite, 1401, AI_SFX_PRIORITY_1, -1);
            else aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);

            if (pSprite->type == kDudeGargoyleStatueFlesh) aiNewState(pSprite, pXSprite, &gargoyleFMorph);
            else aiNewState(pSprite, pXSprite, &gargoyleSMorph);
        }
        #else
        if (Chance(0x4000)) aiPlay3DSound(pSprite, 1401, AI_SFX_PRIORITY_1, -1);
        else aiPlay3DSound(pSprite, 1400, AI_SFX_PRIORITY_1, -1);

        if (pSprite->type == kDudeGargoyleStatueFlesh) aiNewState(pSprite, pXSprite, &gargoyleFMorph);
        else aiNewState(pSprite, pXSprite, &gargoyleSMorph);
        #endif
        break;
    case kDudeCerberusTwoHead:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &cerberusSearch);
        else
        {
            aiPlay3DSound(pSprite, 2300, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &cerberusChase);
        }
        break;
    case kDudeCerberusOneHead:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &cerberus2Search);
        else
        {
            aiPlay3DSound(pSprite, 2300, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &cerberus2Chase);
        }
        break;
    case kDudeHellHound:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &houndSearch);
        else
        {
            aiPlay3DSound(pSprite, 1300, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &houndChase);
        }
        break;
    case kDudeHand:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &handSearch);
        else
        {
            aiPlay3DSound(pSprite, 1900, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &handChase);
        }
        break;
    case kDudeRat:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &ratSearch);
        else
        {
            aiPlay3DSound(pSprite, 2100, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &ratChase);
        }
        break;
    case kDudeInnocent:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &innocentSearch);
        else
        {
            if (pXSprite->health > 0)
                aiPlay3DSound(pSprite, 7000+Random(6), AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &innocentChase);
        }
        break;
    case kDudeTchernobog:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &tchernobogSearch);
        else
        {
            aiPlay3DSound(pSprite, 2350+Random(7), AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &tchernobogChase);
        }
        break;
    case kDudeSpiderBrown:
    case kDudeSpiderRed:
    case kDudeSpiderBlack:
        pSprite->flags |= 2;
        pSprite->cstat &= ~8;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &spidSearch);
        else
        {
            aiPlay3DSound(pSprite, 1800, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &spidChase);
        }
        break;
    case kDudeSpiderMother: {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 1;
        pSprite->flags |= 2;
        pSprite->cstat &= ~8;
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &spidSearch);
        else
        {
            aiPlay3DSound(pSprite, 1853+Random(1), AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &spidChase);
        }
        break;
    }
    case kDudeTinyCaleb:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->target == -1)
        {
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &tinycalebSearch);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &tinycalebSwimSearch);
                break;
            }
        }
        else
        {
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &tinycalebChase);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &tinycalebSwimChase);
                break;
            }
        }
        break;
    }
    case kDudeBeast:
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->thinkTime = 1;
        if (pXSprite->target == -1)
        {
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &beastSearch);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &beastSwimSearch);
                break;
            }
        }
        else
        {
            aiPlay3DSound(pSprite, 9009+Random(2), AI_SFX_PRIORITY_1, -1);
            switch (pXSprite->medium)
            {
            case kMediumNormal:
                aiNewState(pSprite, pXSprite, &beastChase);
                break;
            case kMediumWater:
            case kMediumGoo:
                aiNewState(pSprite, pXSprite, &beastSwimChase);
                break;
            }
        }
        break;
    }
    case kDudePodGreen:
    case kDudePodFire:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &podSearch);
        else
        {
            if (pSprite->type == kDudePodFire)
                aiPlay3DSound(pSprite, 2453, AI_SFX_PRIORITY_1, -1);
            else
                aiPlay3DSound(pSprite, 2473, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &podChase);
        }
        break;
    case kDudeTentacleGreen:
    case kDudeTentacleFire:
        if (pXSprite->target == -1)
            aiNewState(pSprite, pXSprite, &tentacleSearch);
        else
        {
            aiPlay3DSound(pSprite, 2503, AI_SFX_PRIORITY_1, -1);
            aiNewState(pSprite, pXSprite, &tentacleChase);
        }
        break;
    }
}

void aiSetTarget(XSPRITE *pXSprite, int x, int y, int z)
{
    pXSprite->target = -1;
    pXSprite->targetX = x;
    pXSprite->targetY = y;
    pXSprite->targetZ = z;
}

void aiSetTarget(XSPRITE *pXSprite, int nTarget)
{
    dassert(nTarget >= 0 && nTarget < kMaxSprites);
    spritetype *pTarget = &sprite[nTarget];
    if (pTarget->type >= kDudeBase && pTarget->type < kDudeMax)
    {
        if (actSpriteOwnerToSpriteId(&sprite[pXSprite->reference]) != nTarget)
        {
            pXSprite->target = nTarget;
            DUDEINFO *pDudeInfo = getDudeInfo(pTarget->type);
            pXSprite->targetX = pTarget->x;
            pXSprite->targetY = pTarget->y;
            pXSprite->targetZ = pTarget->z-((pDudeInfo->eyeHeight*pTarget->yrepeat)<<2);
        }
    }
}

int aiDamageSprite(spritetype *pSprite, XSPRITE *pXSprite, int nSource, DAMAGE_TYPE nDmgType, int nDamage)
{
    dassert(nSource < kMaxSprites);
    if (!pXSprite->health)
        return 0;
    pXSprite->health = ClipLow(pXSprite->health - nDamage, 0);
    cumulDamage[pSprite->extra] += nDamage;
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nSprite = pXSprite->reference;
    if (nSource >= 0) {
        spritetype *pSource = &sprite[nSource];
        if (pSprite == pSource) return 0;
        else if (pXSprite->target == -1) // if no target, give the dude a target
        {
            aiSetTarget(pXSprite, nSource);
            aiActivateDude(pSprite, pXSprite);
        }
        else if (nSource != pXSprite->target) // if found a new target, retarget
        {
            int nThresh = nDamage;
            if (pSprite->type == pSource->type)
                nThresh *= pDudeInfo->changeTargetKin;
            else
                nThresh *= pDudeInfo->changeTarget;

            if (Chance(nThresh))
            {
                aiSetTarget(pXSprite, nSource);
                aiActivateDude(pSprite, pXSprite);
            }
        }

        #ifdef NOONE_EXTENSIONS
        if (gModernMap) {

            // for enemies in patrol mode
            if (aiInPatrolState(pXSprite->aiState)) {

                aiPatrolStop(pSprite, pSource->index, pXSprite->dudeAmbush);

                PLAYER* pPlayer = getPlayerById(pSource->type);
                if (!pPlayer) return nDamage;
                //if (powerupCheck(pPlayer, kPwUpShadowCloak)) pPlayer->pwUpTime[kPwUpShadowCloak] = 0;
                if (readyForCrit(pSource, pSprite)) {
                    nDamage += aiDamageSprite(pSprite, pXSprite, pSource->index, nDmgType, nDamage * (10 - gGameOptions.nDifficulty));
                    if (pXSprite->health > 0) {
                        int fullHp = (pXSprite->sysData2 > 0) ? ClipRange(pXSprite->sysData2 << 4, 1, 65535) : getDudeInfo(pSprite->type)->startHealth << 4;
                        if (((100 * pXSprite->health) / fullHp) <= 75) {
                            cumulDamage[pSprite->extra] += nDamage << 4; // to be sure any enemy will play the recoil animation
                            RecoilDude(pSprite, pXSprite);
                        }
                    }

                    consoleSysMsg("Player #%d does the critical damage to patrol dude #%d!", pPlayer->nPlayer + 1, pSprite->index);
                }

                return nDamage;
            }

            if (pSprite->type == kDudeModernCustomBurning) {

                if (Chance(0x2000) && gDudeExtra[pSprite->extra].clock < (int)gFrameClock) {
                    playGenDudeSound(pSprite, kGenDudeSndBurning);
                    gDudeExtra[pSprite->extra].clock = (int)gFrameClock + 360;
                }

                if (pXSprite->burnTime == 0) pXSprite->burnTime = 2400;
                if (spriteIsUnderwater(pSprite, false)) {
                    pSprite->type = kDudeModernCustom;
                    pXSprite->burnTime = 0;
                    pXSprite->health = 1; // so it can be killed with flame weapons while underwater and if already was burning dude before.
                    aiGenDudeNewState(pSprite, &genDudeGotoW);
                }

                return nDamage;

            }

            if (pSprite->type == kDudeModernCustom) {

                GENDUDEEXTRA* pExtra = genDudeExtra(pSprite);
                if (nDmgType == kDamageBurn) {

                    if (pXSprite->health > pDudeInfo->fleeHealth) return nDamage;
                    else if (pXSprite->txID <= 0 || getNextIncarnation(pXSprite) == NULL) {
                        removeDudeStuff(pSprite);

                        if (pExtra->weaponType == kGenDudeWeaponKamikaze)
                            doExplosion(pSprite, pXSprite->data1 - kTrapExploder);

                        if (spriteIsUnderwater(pSprite)) {
                            pXSprite->health = 0;
                            return nDamage;
                        }

                        if (pXSprite->burnTime <= 0)
                            pXSprite->burnTime = 1200;

                        if (pExtra->canBurn && pExtra->availDeaths[kDamageBurn] > 0) {

                            aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                            playGenDudeSound(pSprite, kGenDudeSndBurning);
                            pSprite->type = kDudeModernCustomBurning;

                            if (pXSprite->data2 == kGenDudeDefaultSeq) // don't inherit palette for burning if using default animation
                                pSprite->pal = 0;

                            aiGenDudeNewState(pSprite, &genDudeBurnGoto);
                            actHealDude(pXSprite, dudeInfo[55].startHealth, dudeInfo[55].startHealth);
                            gDudeExtra[pSprite->extra].clock = (int)gFrameClock + 360;
                            evKill(nSprite, 3, kCallbackFXFlameLick);

                        }

                    } else {
                        actKillDude(nSource, pSprite, kDamageFall, 65535);
                    }

                } else if (canWalk(pSprite) && !inDodge(pXSprite->aiState) && !inRecoil(pXSprite->aiState)) {

                    if (!dudeIsMelee(pXSprite)) {
                        if (inIdle(pXSprite->aiState) || Chance(getDodgeChance(pSprite))) {
                            if (!spriteIsUnderwater(pSprite)) {
                                if (!canDuck(pSprite) || !dudeIsPlayingSeq(pSprite, 14))  aiGenDudeNewState(pSprite, &genDudeDodgeShortL);
                                else aiGenDudeNewState(pSprite, &genDudeDodgeShortD);

                                if (Chance(0x0200))
                                    playGenDudeSound(pSprite, kGenDudeSndGotHit);

                            } else if (dudeIsPlayingSeq(pSprite, 13)) {
                                aiGenDudeNewState(pSprite, &genDudeDodgeShortW);
                            }
                        }
                    } else if (Chance(0x0200)) {
                        playGenDudeSound(pSprite, kGenDudeSndGotHit);
                    }

                }

                return nDamage;

            }
        }
        #endif

        if (nDmgType == kDamageTesla)
        {
            DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
            pDudeExtra->teslaHit = 1;
        }
        else if (!VanillaMode()) // reset tesla hit state if received different type of damage
        {
            DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
            pDudeExtra->teslaHit = 0;
        }
        const bool fixRandomCultist = (pSprite->inittype >= kDudeBase) && (pSprite->inittype < kDudeMax) && (pSprite->inittype != pSprite->type) && !VanillaMode(); // fix burning cultists randomly switching types underwater
        switch (pSprite->type)
        {
        case kDudeCultistTommy:
        case kDudeCultistShotgun:
        case kDudeCultistTesla:
        case kDudeCultistTNT:
            if (nDmgType != kDamageBurn)
            {
                if (!dudeIsPlayingSeq(pSprite, 14) && !pXSprite->medium)
                    aiNewState(pSprite, pXSprite, &cultistDodge);
                else if (dudeIsPlayingSeq(pSprite, 14) && !pXSprite->medium)
                    aiNewState(pSprite, pXSprite, &cultistProneDodge);
                else if (dudeIsPlayingSeq(pSprite, 13) && (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo))
                    aiNewState(pSprite, pXSprite, &cultistSwimDodge);
            }
            else if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
            {
                pSprite->type = kDudeBurningCultist;
                aiNewState(pSprite, pXSprite, &cultistBurnGoto);
                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                aiPlay3DSound(pSprite, 1031+Random(2), AI_SFX_PRIORITY_2, -1);
                gDudeExtra[pSprite->extra].clock = (int)gFrameClock+360;
                actHealDude(pXSprite, dudeInfo[40].startHealth, dudeInfo[40].startHealth);
                evKill(nSprite, 3, kCallbackFXFlameLick);
            }
            break;
        case kDudeInnocent:
            if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
            {
                pSprite->type = kDudeBurningInnocent;
                aiNewState(pSprite, pXSprite, &cultistBurnGoto);
                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                gDudeExtra[pSprite->extra].clock = (int)gFrameClock+360;
                actHealDude(pXSprite, dudeInfo[39].startHealth, dudeInfo[39].startHealth);
                evKill(nSprite, 3, kCallbackFXFlameLick);
            }
            break;
        case kDudeBurningCultist:
            if (Chance(0x4000) && gDudeExtra[pSprite->extra].clock < gFrameClock)
            {
                aiPlay3DSound(pSprite, 1031+Random(2), AI_SFX_PRIORITY_2, -1);
                gDudeExtra[pSprite->extra].clock = (int)gFrameClock+360;
            }
            if (Chance(0x600) && (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo))
            {
                if (fixRandomCultist)
                    pSprite->type = pSprite->inittype; // restore back to spawned cultist type
                else
                    pSprite->type = kDudeCultistTommy; // vanilla behavior
                pXSprite->burnTime = 0;
                aiNewState(pSprite, pXSprite, &cultistSwimGoto);
            }
            else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)
            {
                if (fixRandomCultist)
                    pSprite->type = pSprite->inittype; // restore back to spawned cultist type
                else
                    pSprite->type = kDudeCultistShotgun; // vanilla behavior
                pXSprite->burnTime = 0;
                aiNewState(pSprite, pXSprite, &cultistSwimGoto);
            }
            break;
        case kDudeGargoyleFlesh:
            aiNewState(pSprite, pXSprite, &gargoyleFChase);
            break;
        case kDudeZombieButcher:
            if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth) {
                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
                pSprite->type = kDudeBurningZombieButcher;
                aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
                actHealDude(pXSprite, dudeInfo[42].startHealth, dudeInfo[42].startHealth);
                evKill(nSprite, 3, kCallbackFXFlameLick);
            }
            break;
        case kDudeTinyCaleb:
            if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/)
            {
                if (!VanillaMode()) // fix burning sprite for tiny caleb
                {
                    pSprite->type = kDudeBurningTinyCaleb;
                    aiNewState(pSprite, pXSprite, &tinycalebBurnGoto);
                }
                else
                {
                    pSprite->type = kDudeBurningInnocent;
                    aiNewState(pSprite, pXSprite, &cultistBurnGoto);
                }
                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                gDudeExtra[pSprite->extra].clock = (int)gFrameClock+360;
                actHealDude(pXSprite, dudeInfo[39].startHealth, dudeInfo[39].startHealth);
                evKill(nSprite, 3, kCallbackFXFlameLick);
            }
            break;
        case kDudeCultistBeast:
            if (pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth)
            {
                pSprite->type = kDudeBeast;
                aiPlay3DSound(pSprite, 9008, AI_SFX_PRIORITY_1, -1);
                aiNewState(pSprite, pXSprite, &beastMorphFromCultist);
                actHealDude(pXSprite, dudeInfo[51].startHealth, dudeInfo[51].startHealth);
            }
            break;
        case kDudeZombieAxeNormal:
        case kDudeZombieAxeBuried:
            if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth)
            {
                aiPlay3DSound(pSprite, 361, AI_SFX_PRIORITY_0, -1);
                aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
                pSprite->type = kDudeBurningZombieAxe;
                aiNewState(pSprite, pXSprite, &zombieABurnGoto);
                actHealDude(pXSprite, dudeInfo[41].startHealth, dudeInfo[41].startHealth);
                evKill(nSprite, 3, kCallbackFXFlameLick);
            }
            break;
        }
    }
    return nDamage;
}

void RecoilDude(spritetype *pSprite, XSPRITE *pXSprite)
{
    char v4 = Chance(0x8000);
    DUDEEXTRA *pDudeExtra = &gDudeExtra[pSprite->extra];
    if (pSprite->statnum == kStatDude && (pSprite->type >= kDudeBase && pSprite->type < kDudeMax)) {
        DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
        switch (pSprite->type) {
#ifdef NOONE_EXTENSIONS
        case kDudeModernCustom: {
            GENDUDEEXTRA* pExtra = genDudeExtra(pSprite); int rChance = getRecoilChance(pSprite);
            if (pExtra->canElectrocute && pDudeExtra->teslaHit && !spriteIsUnderwater(pSprite, false)) {

                if (Chance(rChance << 3) || (dudeIsMelee(pXSprite) && Chance(rChance << 4))) aiGenDudeNewState(pSprite, &genDudeRecoilTesla);
                else if (pExtra->canRecoil && Chance(rChance)) aiGenDudeNewState(pSprite, &genDudeRecoilL);
                else if (canWalk(pSprite)) {

                    if (Chance(rChance >> 2)) aiGenDudeNewState(pSprite, &genDudeDodgeL);
                    else if (Chance(rChance >> 1)) aiGenDudeNewState(pSprite, &genDudeDodgeShortL);

                }

            } else if (pExtra->canRecoil && Chance(rChance)) {

                if (inDuck(pXSprite->aiState) && Chance(rChance >> 2)) aiGenDudeNewState(pSprite, &genDudeRecoilD);
                else if (spriteIsUnderwater(pSprite, false)) aiGenDudeNewState(pSprite, &genDudeRecoilW);
                else aiGenDudeNewState(pSprite, &genDudeRecoilL);

            }

            short rState = inRecoil(pXSprite->aiState);
            if (rState > 0) {

                if (!canWalk(pSprite)) {
                    if (rState == 1) pXSprite->aiState->nextState = &genDudeChaseNoWalkL;
                    else if (rState == 2) pXSprite->aiState->nextState = &genDudeChaseNoWalkD;
                    else pXSprite->aiState->nextState = &genDudeChaseNoWalkW;

                } else if (!dudeIsMelee(pXSprite) || Chance(rChance >> 2)) {
                    if (rState == 1) pXSprite->aiState->nextState = (Chance(rChance) ? &genDudeDodgeL : &genDudeDodgeShortL);
                    else if (rState == 2) pXSprite->aiState->nextState = (Chance(rChance) ? &genDudeDodgeD : &genDudeDodgeShortD);
                    else if (rState == 3) pXSprite->aiState->nextState = (Chance(rChance) ? &genDudeDodgeW : &genDudeDodgeShortW);

                }
                else if (rState == 1) pXSprite->aiState->nextState = &genDudeChaseL;
                else if (rState == 2) pXSprite->aiState->nextState = &genDudeChaseD;
                else pXSprite->aiState->nextState = &genDudeChaseW;

                playGenDudeSound(pSprite, kGenDudeSndGotHit);

            }

            pDudeExtra->teslaHit = 0;
            break;
        }
#endif
        case kDudeCultistTommy:
        case kDudeCultistShotgun:
        case kDudeCultistTesla:
        case kDudeCultistTNT:
        case kDudeCultistBeast:
            if (pSprite->type == kDudeCultistTommy) aiPlay3DSound(pSprite, 4013+Random(2), AI_SFX_PRIORITY_2, -1);
            else aiPlay3DSound(pSprite, 1013+Random(2), AI_SFX_PRIORITY_2, -1);

            if (!v4 && pXSprite->medium == kMediumNormal) {
                if (pDudeExtra->teslaHit) aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
                else aiNewState(pSprite, pXSprite, &cultistRecoil);

            } else if (v4 && pXSprite->medium == kMediumNormal) {
                if (pDudeExtra->teslaHit) aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
                else if (gGameOptions.nDifficulty > 0) aiNewState(pSprite, pXSprite, &cultistProneRecoil);
                else aiNewState(pSprite, pXSprite, &cultistRecoil);
            }
            else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)
                aiNewState(pSprite, pXSprite, &cultistSwimRecoil);
            else
            {
                if (pDudeExtra->teslaHit)
                    aiNewState(pSprite, pXSprite, &cultistTeslaRecoil);
                else
                    aiNewState(pSprite, pXSprite, &cultistRecoil);
            }
            break;
        case kDudeBurningCultist:
            aiNewState(pSprite, pXSprite, &cultistBurnGoto);
            break;
#ifdef NOONE_EXTENSIONS
        case kDudeModernCustomBurning:
            aiGenDudeNewState(pSprite, &genDudeBurnGoto);
            break;
#endif
        case kDudeZombieButcher:
            aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit)
                aiNewState(pSprite, pXSprite, &zombieFTeslaRecoil);
            else
                aiNewState(pSprite, pXSprite, &zombieFRecoil);
            break;
        case kDudeZombieAxeNormal:
        case kDudeZombieAxeBuried:
            aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit && pXSprite->data3 > pDudeInfo->startHealth/3)
                aiNewState(pSprite, pXSprite, &zombieATeslaRecoil);
            else if (pXSprite->data3 > pDudeInfo->startHealth/3)
                aiNewState(pSprite, pXSprite, &zombieARecoil2);
            else
                aiNewState(pSprite, pXSprite, &zombieARecoil);
            break;
        case kDudeBurningZombieAxe:
            aiPlay3DSound(pSprite, 1106, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &zombieABurnGoto);
            break;
        case kDudeBurningZombieButcher:
            aiPlay3DSound(pSprite, 1202, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &zombieFBurnGoto);
            break;
        case kDudeGargoyleFlesh:
        case kDudeGargoyleStone:
            aiPlay3DSound(pSprite, 1402, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &gargoyleFRecoil);
            break;
        case kDudeCerberusTwoHead:
            aiPlay3DSound(pSprite, 2302+Random(2), AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit && pXSprite->data3 > pDudeInfo->startHealth/3)
                aiNewState(pSprite, pXSprite, &cerberusTeslaRecoil);
            else
                aiNewState(pSprite, pXSprite, &cerberusRecoil);
            break;
        case kDudeCerberusOneHead:
            aiPlay3DSound(pSprite, 2302+Random(2), AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &cerberus2Recoil);
            break;
        case kDudeHellHound:
            aiPlay3DSound(pSprite, 1302, AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit)
                aiNewState(pSprite, pXSprite, &houndTeslaRecoil);
            else
                aiNewState(pSprite, pXSprite, &houndRecoil);
            break;
        case kDudeTchernobog:
            aiPlay3DSound(pSprite, 2370+Random(2), AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &tchernobogRecoil);
            break;
        case kDudeHand:
            aiPlay3DSound(pSprite, 1902, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &handRecoil);
            break;
        case kDudeRat:
            aiPlay3DSound(pSprite, 2102, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &ratRecoil);
            break;
        case kDudeBat:
            aiPlay3DSound(pSprite, 2002, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &batRecoil);
            break;
        case kDudeBoneEel:
            aiPlay3DSound(pSprite, 1502, AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &eelRecoil);
            break;
        case kDudeGillBeast: {
            XSECTOR *pXSector = NULL;
            if (sector[pSprite->sectnum].extra > 0)
                pXSector = &xsector[sector[pSprite->sectnum].extra];
            aiPlay3DSound(pSprite, 1702, AI_SFX_PRIORITY_2, -1);
            if (pXSector && pXSector->Underwater)
                aiNewState(pSprite, pXSprite, &gillBeastSwimRecoil);
            else
                aiNewState(pSprite, pXSprite, &gillBeastRecoil);
            break;
        }
        case kDudePhantasm:
            aiPlay3DSound(pSprite, 1602, AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit)
                aiNewState(pSprite, pXSprite, &ghostTeslaRecoil);
            else
                aiNewState(pSprite, pXSprite, &ghostRecoil);
            break;
        case kDudeSpiderBrown:
        case kDudeSpiderRed:
        case kDudeSpiderBlack:
            aiPlay3DSound(pSprite, 1802+Random(1), AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &spidDodge);
            break;
        case kDudeSpiderMother:
            aiPlay3DSound(pSprite, 1851+Random(1), AI_SFX_PRIORITY_2, -1);
            aiNewState(pSprite, pXSprite, &spidDodge);
            break;
        case kDudeInnocent:
            aiPlay3DSound(pSprite, 7007+Random(2), AI_SFX_PRIORITY_2, -1);
            if (pDudeExtra->teslaHit)
                aiNewState(pSprite, pXSprite, &innocentTeslaRecoil);
            else
                aiNewState(pSprite, pXSprite, &innocentRecoil);
            break;
        case kDudeTinyCaleb:
            if (pXSprite->medium == kMediumNormal)
            {
                if (pDudeExtra->teslaHit)
                    aiNewState(pSprite, pXSprite, &tinycalebTeslaRecoil);
                else
                    aiNewState(pSprite, pXSprite, &tinycalebRecoil);
            }
            else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)
                aiNewState(pSprite, pXSprite, &tinycalebSwimRecoil);
            else
            {
                if (pDudeExtra->teslaHit)
                    aiNewState(pSprite, pXSprite, &tinycalebTeslaRecoil);
                else
                    aiNewState(pSprite, pXSprite, &tinycalebRecoil);
            }
            break;
        case kDudeBeast:
            aiPlay3DSound(pSprite, 9004+Random(2), AI_SFX_PRIORITY_2, -1);
            if (pXSprite->medium == kMediumNormal)
            {
                if (pDudeExtra->teslaHit)
                    aiNewState(pSprite, pXSprite, &beastTeslaRecoil);
                else
                    aiNewState(pSprite, pXSprite, &beastRecoil);
            }
            else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)
                aiNewState(pSprite, pXSprite, &beastSwimRecoil);
            else
            {
                if (pDudeExtra->teslaHit)
                    aiNewState(pSprite, pXSprite, &beastTeslaRecoil);
                else
                    aiNewState(pSprite, pXSprite, &beastRecoil);
            }
            break;
        case kDudePodGreen:
        case kDudePodFire:
            aiNewState(pSprite, pXSprite, &podRecoil);
            break;
        case kDudeTentacleGreen:
        case kDudeTentacleFire:
            aiNewState(pSprite, pXSprite, &tentacleRecoil);
            break;
        default:
            aiNewState(pSprite, pXSprite, &genRecoil);
            break;
        }
        pDudeExtra->teslaHit = 0;
    }
}

void aiThinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    if (Chance(pDudeInfo->alertChance))
    {
        for (int p = connecthead; p >= 0; p = connectpoint2[p])
        {
            PLAYER *pPlayer = &gPlayer[p];
            if (pSprite->owner == pPlayer->nSprite || pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, kPwUpShadowCloak) > 0)
                continue;
            int x = pPlayer->pSprite->x;
            int y = pPlayer->pSprite->y;
            int z = pPlayer->pSprite->z;
            int nSector = pPlayer->pSprite->sectnum;
            int dx = x-pSprite->x;
            int dy = y-pSprite->y;
            int nDist = approxDist(dx, dy);
            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
                continue;
            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
                continue;
            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
            {
                aiSetTarget(pXSprite, pPlayer->nSprite);
                aiActivateDude(pSprite, pXSprite);
                return;
            }
            else if (nDist < pDudeInfo->hearDist)
            {
                aiSetTarget(pXSprite, x, y, z);
                aiActivateDude(pSprite, pXSprite);
                return;
            }
        }
    }
}

void aiLookForTarget(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    if (Chance(pDudeInfo->alertChance))
    {
        for (int p = connecthead; p >= 0; p = connectpoint2[p])
        {
            PLAYER *pPlayer = &gPlayer[p];
            if (pSprite->owner == pPlayer->nSprite || pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, kPwUpShadowCloak) > 0)
                continue;
            int x = pPlayer->pSprite->x;
            int y = pPlayer->pSprite->y;
            int z = pPlayer->pSprite->z;
            int nSector = pPlayer->pSprite->sectnum;
            int dx = x-pSprite->x;
            int dy = y-pSprite->y;
            int nDist = approxDist(dx, dy);
            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
                continue;
            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
                continue;
            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
            {
                aiSetTarget(pXSprite, pPlayer->nSprite);
                aiActivateDude(pSprite, pXSprite);
                return;
            }
            else if (nDist < pDudeInfo->hearDist)
            {
                aiSetTarget(pXSprite, x, y, z);
                aiActivateDude(pSprite, pXSprite);
                return;
            }
        }
        if (pXSprite->state)
        {
            char sectmap[(kMaxSectors+7)>>3];
            gAffectedSectors[0] = 0;
            gAffectedXWalls[0] = 0;
            GetClosestSpriteSectors(pSprite->sectnum, pSprite->x, pSprite->y, 400, gAffectedSectors, sectmap, gAffectedXWalls);
            for (int nSprite2 = headspritestat[kStatDude]; nSprite2 >= 0; nSprite2 = nextspritestat[nSprite2])
            {
                spritetype *pSprite2 = &sprite[nSprite2];
                int dx = pSprite2->x-pSprite->x;
                int dy = pSprite2->y-pSprite->y;
                int nDist = approxDist(dx, dy);
                if (pSprite2->type == kDudeInnocent)
                {
                    DUDEINFO *pDudeInfo = getDudeInfo(pSprite2->type);
                    if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
                        continue;
                    int UNUSED(nAngle) = getangle(dx,dy);
                    aiSetTarget(pXSprite, pSprite2->index);
                    aiActivateDude(pSprite, pXSprite);
                    return;
                }
            }
        }
    }
}

void aiProcessDudes(void) {
    for (int nSprite = headspritestat[kStatDude]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
        spritetype *pSprite = &sprite[nSprite];
        if (pSprite->flags & 32) continue;
        int nXSprite = pSprite->extra;
        XSPRITE *pXSprite = &xsprite[nXSprite]; DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
        if (IsPlayerSprite(pSprite) || pXSprite->health == 0) continue;
        pXSprite->stateTimer = ClipLow(pXSprite->stateTimer-4, 0);

        if (pXSprite->aiState->moveFunc)
            pXSprite->aiState->moveFunc(pSprite, pXSprite);

        if (pXSprite->aiState->thinkFunc && (gFrame & 3) == (nSprite & 3))
            pXSprite->aiState->thinkFunc(pSprite, pXSprite);

        switch (pSprite->type) {
            #ifdef NOONE_EXTENSIONS
            case kDudeModernCustom:
            case kDudeModernCustomBurning: {
                GENDUDEEXTRA* pExtra = &gGenDudeExtra[pSprite->index];
                if (pExtra->slaveCount > 0) updateTargetOfSlaves(pSprite);
                if (pExtra->nLifeLeech >= 0) updateTargetOfLeech(pSprite);
                if (pXSprite->stateTimer == 0 && pXSprite->aiState->nextState
                    && (pXSprite->aiState->stateTicks > 0 || seqGetStatus(3, pSprite->extra) < 0)) {
                    aiGenDudeNewState(pSprite, pXSprite->aiState->nextState);
                }
                int hinder = ((pExtra->isMelee) ? 25 : 5) << 4;
                if (pXSprite->health <= 0 || hinder > cumulDamage[pSprite->extra]) break;
                pXSprite->data3 = cumulDamage[pSprite->extra];
                RecoilDude(pSprite, pXSprite);
                break;
            }
            #endif
            default:
                if (pXSprite->stateTimer == 0 && pXSprite->aiState->nextState) {
                    if (pXSprite->aiState->stateTicks > 0)
                        aiNewState(pSprite, pXSprite, pXSprite->aiState->nextState);
                    else if (seqGetStatus(3, nXSprite) < 0)
                        aiNewState(pSprite, pXSprite, pXSprite->aiState->nextState);
                }

                if (pXSprite->health > 0 && ((pDudeInfo->hinderDamage << 4) <= cumulDamage[nXSprite])) {
                    pXSprite->data3 = cumulDamage[nXSprite];
                    RecoilDude(pSprite, pXSprite);
                }
                break;
        }
    }
    memset(cumulDamage, 0, sizeof(cumulDamage));
}

void aiInit(void) {
    memset(cumulDamage, 0, sizeof(cumulDamage));
    memset(gDudeSlope, 0, sizeof(gDudeSlope));
    memset(gDudeExtra, 0, sizeof(gDudeExtra));
    for (int nSprite = headspritestat[kStatDude]; nSprite >= 0; nSprite = nextspritestat[nSprite]) {
        aiInitSprite(&sprite[nSprite]);
    }
}

void aiInitSprite(spritetype *pSprite)
{
    int nXSprite = pSprite->extra;
    XSPRITE *pXSprite = &xsprite[nXSprite];
    int nSector = pSprite->sectnum;
    int nXSector = sector[nSector].extra;
    XSECTOR *pXSector = NULL;
    if (nXSector > 0)
        pXSector = &xsector[nXSector];
    DUDEEXTRA *pDudeExtra = &gDudeExtra[nXSprite];
    DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[nXSprite].stats;
    pDudeExtra->clock = 0;
    pDudeExtra->teslaHit = 0;
    if (!VanillaMode()) // clear these stats for non-vanilla mode
    {
        pDudeExtra->sfx_priority = AI_SFX_PRIORITY_0;
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 0;
    }

    #ifdef NOONE_EXTENSIONS
    unsigned int stateTimer = 0;
    int targetMarker = -1, targetX = 0, targetY = 0, targetZ = 0;

    // dude patrol init
    if (gModernMap)
    {
        // must keep it in case of loading save
        if (pXSprite->dudeFlag4 && pXSprite->target > 0 && pXSprite->target < kMaxSprites && sprite[pXSprite->target].type == kMarkerPath)
        {
            stateTimer = pXSprite->stateTimer;
            targetMarker = pXSprite->target;
            targetX = pXSprite->targetX;
            targetY = pXSprite->targetY;
            targetZ = pXSprite->targetZ;
        }
    }
    #endif

    switch (pSprite->type) {
    case kDudeCultistTommy:
    case kDudeCultistShotgun:
    case kDudeCultistTesla:
    case kDudeCultistTNT:
    case kDudeCultistBeast:
    {
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &cultistIdle);
        break;
    }
    case kDudeCultistTommyProne:
    {
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &fanaticProneIdle);
        break;
    }
    case kDudeCultistShotgunProne:
    {
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &cultistProneIdle);
        break;
    }
    case kDudeZombieButcher: {
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &zombieFIdle);
        break;
    }
    case kDudeZombieAxeNormal: {
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &zombieAIdle);
        break;
    }
    case kDudeZombieAxeLaying:
    {
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &zombieSIdle);
        pSprite->flags &= ~1;
        break;
    }
    case kDudeZombieAxeBuried: {
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &zombieEIdle);
        pSprite->flags &= ~1;
        break;
    }
    case kDudeGargoyleFlesh:
    case kDudeGargoyleStone: {
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &gargoyleFIdle);
        break;
    }
    case kDudeGargoyleStatueFlesh:
    case kDudeGargoyleStatueStone:
        aiNewState(pSprite, pXSprite, &gargoyleStatueIdle);
        break;
    case kDudeCerberusTwoHead: {
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &cerberusIdle);
        break;
    }
    case kDudeCerberusOneHead: {
        if (!VanillaMode()) {
            pDudeExtraE->thinkTime = 0;
            aiNewState(pSprite, pXSprite, &cerberus2Idle);
            break;
        }
        aiNewState(pSprite, pXSprite, &genIdle);
        break;
    }
    case kDudeHellHound:
        aiNewState(pSprite, pXSprite, &houndIdle);
        break;
    case kDudeHand:
        aiNewState(pSprite, pXSprite, &handIdle);
        break;
    case kDudePhantasm:
    {
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &ghostIdle);
        break;
    }
    case kDudeInnocent:
        aiNewState(pSprite, pXSprite, &innocentIdle);
        break;
    case kDudeRat:
        aiNewState(pSprite, pXSprite, &ratIdle);
        break;
    case kDudeBoneEel:
    {
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &eelIdle);
        break;
    }
    case kDudeGillBeast:
        aiNewState(pSprite, pXSprite, &gillBeastIdle);
        break;
    case kDudeBat:
    {
        pDudeExtraE->thinkTime = 0;
        pDudeExtraE->active = 0;
        aiNewState(pSprite, pXSprite, &batIdle);
        break;
    }
    case kDudeSpiderBrown:
    case kDudeSpiderRed:
    case kDudeSpiderBlack:
    {
        pDudeExtraE->active = 0;
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &spidIdle);
        break;
    }
    case kDudeSpiderMother:
    {
        pDudeExtraE->active = 0;
        pDudeExtraE->birthCounter = 0;
        aiNewState(pSprite, pXSprite, &spidIdle);
        break;
    }
    case kDudeTchernobog:
    {
        pDudeExtraE->active = 0;
        pDudeExtraE->thinkTime = 0;
        aiNewState(pSprite, pXSprite, &tchernobogIdle);
        break;
    }
    case kDudeTinyCaleb:
        aiNewState(pSprite, pXSprite, &tinycalebIdle);
        break;
    case kDudeBeast:
        aiNewState(pSprite, pXSprite, &beastIdle);
        break;
    case kDudePodGreen:
    case kDudePodFire:
        aiNewState(pSprite, pXSprite, &podIdle);
        break;
    case kDudeTentacleGreen:
    case kDudeTentacleFire:
        aiNewState(pSprite, pXSprite, &tentacleIdle);
        break;
    #ifdef NOONE_EXTENSIONS
    case kDudeModernCustom:
    case kDudeModernCustomBurning:
        if (!gModernMap) break;
        aiGenDudeInitSprite(pSprite, pXSprite);
        genDudePrepare(pSprite, kGenDudePropertyAll);
        break;
    #endif
    default:
        aiNewState(pSprite, pXSprite, &genIdle);
        break;
    }
    aiSetTarget(pXSprite, 0, 0, 0);
    pXSprite->stateTimer = 0;
    switch (pSprite->type)
    {
    case kDudeSpiderBrown:
    case kDudeSpiderRed:
    case kDudeSpiderBlack:
        if (pSprite->cstat&8) pSprite->flags |= 9;
        else pSprite->flags = 15;
        break;
    case kDudeGargoyleFlesh:
    case kDudeGargoyleStone:
    case kDudePhantasm:
    case kDudeBoneEel:
    case kDudeBat:
        pSprite->flags |= 9;
        break;
    case kDudeGillBeast:
        if (pXSector && pXSector->Underwater) pSprite->flags |= 9;
        else pSprite->flags = 15;
        break;
    case kDudeZombieAxeBuried:
    case kDudeZombieAxeLaying:
        pSprite->flags = 7;
        break;
    #ifdef NOONE_EXTENSIONS
    case kDudePodMother: // FakeDude type
        if (gModernMap) break;
        fallthrough__;
    // Allow put pods and tentacles on ceilings if sprite is y-flipped.
    case kDudePodGreen:
    case kDudeTentacleGreen:
    case kDudePodFire:
    case kDudeTentacleFire:
    case kDudeTentacleMother:
        if (gModernMap && (pSprite->cstat & CSTAT_SPRITE_YFLIP)) break;
        fallthrough__;
    // go default
    #endif
    default:
        pSprite->flags = 15;
        break;
    }

    #ifdef NOONE_EXTENSIONS
    if (gModernMap)
    {
        if (pXSprite->dudeFlag4)
        {
            // restore dude's path
            if (spriRangeIsFine(targetMarker))
            {
                pXSprite->target = targetMarker;
                pXSprite->targetX = targetX;
                pXSprite->targetY = targetY;
                pXSprite->targetZ = targetZ;
            }

            // reset target spot progress
            pXSprite->data3 = 0;

            // make dude follow the markers
            bool uwater = spriteIsUnderwater(pSprite);
            if (pXSprite->target <= 0 || sprite[pXSprite->target].type != kMarkerPath)
            {
                pXSprite->target = -1; aiPatrolSetMarker(pSprite, pXSprite);
            }

            if (stateTimer > 0)
            {
                if (uwater) aiPatrolState(pSprite, kAiStatePatrolWaitW);
                else if (pXSprite->unused1 & kDudeFlagCrouch) aiPatrolState(pSprite, kAiStatePatrolWaitC);
                else aiPatrolState(pSprite, kAiStatePatrolWaitL);
                pXSprite->stateTimer = (unsigned int)stateTimer; // restore state timer
            }
            else if (uwater) aiPatrolState(pSprite, kAiStatePatrolMoveW);
            else if (pXSprite->unused1 & kDudeFlagCrouch) aiPatrolState(pSprite, kAiStatePatrolMoveC);
            else aiPatrolState(pSprite, kAiStatePatrolMoveL);
        }
    }
    #endif

}

class AILoadSave : public LoadSave
{
    virtual void Load(void);
    virtual void Save(void);
};

void AILoadSave::Load(void)
{
    Read(cumulDamage, sizeof(cumulDamage));
    Read(gDudeSlope, sizeof(gDudeSlope));
    Read(gDudeExtra, sizeof(gDudeExtra));
}

void AILoadSave::Save(void)
{
    Write(cumulDamage, sizeof(cumulDamage));
    Write(gDudeSlope, sizeof(gDudeSlope));
    Write(gDudeExtra, sizeof(gDudeExtra));
}

static AILoadSave *myLoadSave;

void AILoadSaveConstruct(void)
{
    myLoadSave = new AILoadSave();
}


code:
//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT

This file is part of NBlood.

NBlood is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
//-------------------------------------------------------------------------
#pragma once

#include "compat.h"
#include "common_game.h"
#include "actor.h"
#include "db.h"

struct AISTATE {
    int stateType; // By NoOne: current type of state. Basically required for kModernDudeTargetChanger, but can be used for something else.
    int seqId;
    int funcId; // seq callback
    int stateTicks;
    void(*enterFunc)(spritetype *, XSPRITE *);
    void(*moveFunc)(spritetype *, XSPRITE *);
    void(*thinkFunc)(spritetype *, XSPRITE *);
    AISTATE *nextState;
};
extern AISTATE aiState[];
extern AISTATE genIdle;
extern AISTATE genRecoil;

enum AI_SFX_PRIORITY {
    AI_SFX_PRIORITY_0 = 0,
    AI_SFX_PRIORITY_1,
    AI_SFX_PRIORITY_2,
};

struct DUDEEXTRA_STATS
{
    union {
        int thinkTime;
        int birthCounter;
    };
    char active;
};

struct DUDEEXTRA
{
    int clock;
    char teslaHit;
    AI_SFX_PRIORITY sfx_priority;
    DUDEEXTRA_STATS stats;
};

struct TARGETTRACK {
    int at0;
    int at4;
    int at8; // view angle
    int atc;
    int at10; // Move predict
};

extern int gCultTeslaFireChance[5];
extern DUDEEXTRA gDudeExtra[];
extern int gDudeSlope[];
extern int cumulDamage[];

bool dudeIsPlayingSeq(spritetype *pSprite, int nSeq);
void aiPlay3DSound(spritetype *pSprite, int a2, AI_SFX_PRIORITY a3, int a4);
void aiNewState(spritetype *pSprite, XSPRITE *pXSprite, AISTATE *pAIState);
void aiChooseDirection(spritetype *pSprite, XSPRITE *pXSprite, int a3);
void aiMoveForward(spritetype *pSprite, XSPRITE *pXSprite);
void aiMoveTurn(spritetype *pSprite, XSPRITE *pXSprite);
void aiMoveDodge(spritetype *pSprite, XSPRITE *pXSprite);
void aiActivateDude(spritetype *pSprite, XSPRITE *pXSprite);
void aiSetTarget(XSPRITE *pXSprite, int x, int y, int z);
void aiSetTarget(XSPRITE *pXSprite, int nTarget);
int aiDamageSprite(spritetype *pSprite, XSPRITE *pXSprite, int nSource, DAMAGE_TYPE nDmgType, int nDamage);
void aiThinkTarget(spritetype *pSprite, XSPRITE *pXSprite);
void aiLookForTarget(spritetype *pSprite, XSPRITE *pXSprite);
void aiProcessDudes(void);
void aiInit(void);
void aiInitSprite(spritetype *pSprite);
bool CanMove(spritetype* pSprite, int a2, int nAngle, int nRange);
void RecoilDude(spritetype* pSprite, XSPRITE* pXSprite);


code:
//-------------------------------------------------------------------------
/*
Copyright (C) 2010-2019 EDuke32 developers and contributors
Copyright (C) 2019 Nuke.YKT

This file is part of NBlood.

NBlood is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
//-------------------------------------------------------------------------
#include "compat.h"
#include "build.h"
#include "pragmas.h"
#include "mmulti.h"
#include "common_game.h"

#include "actor.h"
#include "ai.h"
#include "aibat.h"
#include "blood.h"
#include "db.h"
#include "dude.h"
#include "player.h"
#include "seq.h"
#include "trig.h"

static void BiteSeqCallback(int, int);
static void thinkTarget(spritetype *, XSPRITE *);
static void thinkSearch(spritetype *, XSPRITE *);
static void thinkGoto(spritetype *, XSPRITE *);
static void thinkPonder(spritetype *, XSPRITE *);
static void MoveDodgeUp(spritetype *, XSPRITE *);
static void MoveDodgeDown(spritetype *, XSPRITE *);
static void thinkChase(spritetype *, XSPRITE *);
static void MoveForward(spritetype *, XSPRITE *);
static void MoveSwoop(spritetype *, XSPRITE *);
static void MoveFly(spritetype *, XSPRITE *);
static void MoveToCeil(spritetype *, XSPRITE *);

static int nBiteClient = seqRegisterClient(BiteSeqCallback);

AISTATE batIdle = {kAiStateIdle, 0, -1, 0, NULL, NULL, thinkTarget, NULL };
AISTATE batFlyIdle = {kAiStateIdle, 6, -1, 0, NULL, NULL, thinkTarget, NULL };
AISTATE batChase = {kAiStateChase, 6, -1, 0, NULL, MoveForward, thinkChase, &batFlyIdle };
AISTATE batPonder = {kAiStateOther, 6, -1, 0, NULL, NULL, thinkPonder, NULL };
AISTATE batGoto = {kAiStateMove, 6, -1, 600, NULL, MoveForward, thinkGoto, &batFlyIdle };
AISTATE batBite = {kAiStateChase, 7, nBiteClient, 60, NULL, NULL, NULL, &batPonder };
AISTATE batRecoil = {kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &batChase };
AISTATE batSearch = {kAiStateSearch, 6, -1, 120, NULL, MoveForward, thinkSearch, &batFlyIdle };
AISTATE batSwoop = {kAiStateOther, 6, -1, 60, NULL, MoveSwoop, thinkChase, &batChase };
AISTATE batFly = { kAiStateMove, 6, -1, 0, NULL, MoveFly, thinkChase, &batChase };
AISTATE batTurn = {kAiStateMove, 6, -1, 60, NULL, aiMoveTurn, NULL, &batChase };
AISTATE batHide = {kAiStateOther, 6, -1, 0, NULL, MoveToCeil, MoveForward, NULL };
AISTATE batDodgeUp = {kAiStateMove, 6, -1, 120, NULL, MoveDodgeUp, 0, &batChase };
AISTATE batDodgeUpRight = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeUp, 0, &batChase };
AISTATE batDodgeUpLeft = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeUp, 0, &batChase };
AISTATE batDodgeDown = {kAiStateMove, 6, -1, 120, NULL, MoveDodgeDown, 0, &batChase };
AISTATE batDodgeDownRight = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeDown, 0, &batChase };
AISTATE batDodgeDownLeft = {kAiStateMove, 6, -1, 90, NULL, MoveDodgeDown, 0, &batChase };

static void BiteSeqCallback(int, int nXSprite)
{
    XSPRITE *pXSprite = &xsprite[nXSprite];
    spritetype *pSprite = &sprite[pXSprite->reference];
    spritetype *pTarget = &sprite[pXSprite->target];
    int dx = Cos(pSprite->ang) >> 16;
    int dy = Sin(pSprite->ang) >> 16;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    DUDEINFO *pDudeInfoT = getDudeInfo(pTarget->type);
    int height = (pSprite->yrepeat*pDudeInfo->eyeHeight)<<2;
    int height2 = (pTarget->yrepeat*pDudeInfoT->eyeHeight)<<2;
    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
    actFireVector(pSprite, 0, 0, dx, dy, height2-height, kVectorBatBite);
}

static void thinkTarget(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
    if (pDudeExtraE->active && pDudeExtraE->thinkTime < 10)
        pDudeExtraE->thinkTime++;
    else if (pDudeExtraE->thinkTime >= 10 && pDudeExtraE->active)
    {
        pDudeExtraE->thinkTime = 0;
        pXSprite->goalAng += 256;
        POINT3D *pTarget = &baseSprite[pSprite->index];
        aiSetTarget(pXSprite, pTarget->x, pTarget->y, pTarget->z);
        aiNewState(pSprite, pXSprite, &batTurn);
        return;
    }
    if (Chance(pDudeInfo->alertChance))
    {
        for (int p = connecthead; p >= 0; p = connectpoint2[p])
        {
            PLAYER *pPlayer = &gPlayer[p];
            if (pPlayer->pXSprite->health == 0 || powerupCheck(pPlayer, kPwUpShadowCloak) > 0)
                continue;
            int x = pPlayer->pSprite->x;
            int y = pPlayer->pSprite->y;
            int z = pPlayer->pSprite->z;
            int nSector = pPlayer->pSprite->sectnum;
            int dx = x-pSprite->x;
            int dy = y-pSprite->y;
            int nDist = approxDist(dx, dy);
            if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist)
                continue;
            if (!cansee(x, y, z, nSector, pSprite->x, pSprite->y, pSprite->z-((pDudeInfo->eyeHeight*pSprite->yrepeat)<<2), pSprite->sectnum))
                continue;
            int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
            {
                aiSetTarget(pXSprite, pPlayer->nSprite);
                aiActivateDude(pSprite, pXSprite);
            }
            else if (nDist < pDudeInfo->hearDist)
            {
                aiSetTarget(pXSprite, x, y, z);
                aiActivateDude(pSprite, pXSprite);
            }
            else
                continue;
            break;
        }
    }
}

static void thinkSearch(spritetype *pSprite, XSPRITE *pXSprite)
{
    aiChooseDirection(pSprite, pXSprite, pXSprite->goalAng);
    thinkTarget(pSprite, pXSprite);
}

static void thinkGoto(spritetype *pSprite, XSPRITE *pXSprite)
{
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int dx = pXSprite->targetX-pSprite->x;
    int dy = pXSprite->targetY-pSprite->y;
    int nAngle = getangle(dx, dy);
    int nDist = approxDist(dx, dy);
    aiChooseDirection(pSprite, pXSprite, nAngle);
    if (nDist < 512 && klabs(pSprite->ang - nAngle) < pDudeInfo->periphery)
        aiNewState(pSprite, pXSprite, &batSearch);
    thinkTarget(pSprite, pXSprite);
}

static void thinkPonder(spritetype *pSprite, XSPRITE *pXSprite)
{
    if (pXSprite->target == -1)
    {
        aiNewState(pSprite, pXSprite, &batSearch);
        return;
    }
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
    spritetype *pTarget = &sprite[pXSprite->target];
    XSPRITE *pXTarget = &xsprite[pTarget->extra];
    int dx = pTarget->x-pSprite->x;
    int dy = pTarget->y-pSprite->y;
    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
    if (pXTarget->health == 0)
    {
        aiNewState(pSprite, pXSprite, &batSearch);
        return;
    }
    int nDist = approxDist(dx, dy);
    if (nDist <= pDudeInfo->seeDist)
    {
        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
        int height2 = (getDudeInfo(pTarget->type)->eyeHeight*pTarget->yrepeat)<<2;
        int top, bottom;
        GetSpriteExtents(pSprite, &top, &bottom);
        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
        {
            aiSetTarget(pXSprite, pXSprite->target);
            if (height2-height < 0x3000 && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
                aiNewState(pSprite, pXSprite, &batDodgeUp);
            else if (height2-height > 0x5000 && nDist < 0x1800 && nDist > 0xc00 && klabs(nDeltaAngle) < 85)
                aiNewState(pSprite, pXSprite, &batDodgeDown);
            else if (height2-height < 0x2000 && nDist < 0x200 && klabs(nDeltaAngle) < 85)
                aiNewState(pSprite, pXSprite, &batDodgeUp);
            else if (height2-height > 0x6000 && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
                aiNewState(pSprite, pXSprite, &batDodgeDown);
            else if (height2-height < 0x2000 && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
                aiNewState(pSprite, pXSprite, &batDodgeUp);
            else if (height2-height < 0x2000 && klabs(nDeltaAngle) < 85 && nDist > 0x1400)
                aiNewState(pSprite, pXSprite, &batDodgeUp);
            else if (height2-height > 0x4000)
                aiNewState(pSprite, pXSprite, &batDodgeDown);
            else
                aiNewState(pSprite, pXSprite, &batDodgeUp);
            return;
        }
    }
    aiNewState(pSprite, pXSprite, &batGoto);
    pXSprite->target = -1;
}

static void MoveDodgeUp(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int dx = xvel[nSprite];
    int dy = yvel[nSprite];
    int t1 = dmulscale30(dx, nCos, dy, nSin);
    int t2 = dmulscale30(dx, nSin, -dy, nCos);
    if (pXSprite->dodgeDir > 0)
        t2 += pDudeInfo->sideSpeed;
    else
        t2 -= pDudeInfo->sideSpeed;

    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
    zvel[nSprite] = -0x52aaa;
}

static void MoveDodgeDown(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    if (pXSprite->dodgeDir == 0)
        return;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int dx = xvel[nSprite];
    int dy = yvel[nSprite];
    int t1 = dmulscale30(dx, nCos, dy, nSin);
    int t2 = dmulscale30(dx, nSin, -dy, nCos);
    if (pXSprite->dodgeDir > 0)
        t2 += pDudeInfo->sideSpeed;
    else
        t2 -= pDudeInfo->sideSpeed;

    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
    zvel[nSprite] = 0x44444;
}

static void thinkChase(spritetype *pSprite, XSPRITE *pXSprite)
{
    if (pXSprite->target == -1)
    {
        aiNewState(pSprite, pXSprite, &batGoto);
        return;
    }
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    dassert(pXSprite->target >= 0 && pXSprite->target < kMaxSprites);
    spritetype *pTarget = &sprite[pXSprite->target];
    XSPRITE *pXTarget = &xsprite[pTarget->extra];
    int dx = pTarget->x-pSprite->x;
    int dy = pTarget->y-pSprite->y;
    aiChooseDirection(pSprite, pXSprite, getangle(dx, dy));
    if (pXTarget->health == 0)
    {
        aiNewState(pSprite, pXSprite, &batSearch);
        return;
    }
    if (IsPlayerSprite(pTarget) && powerupCheck(&gPlayer[pTarget->type - kDudePlayer1], kPwUpShadowCloak) > 0)
    {
        aiNewState(pSprite, pXSprite, &batSearch);
        return;
    }
    int nDist = approxDist(dx, dy);
    if (nDist <= pDudeInfo->seeDist)
    {
        int nDeltaAngle = ((getangle(dx,dy)+1024-pSprite->ang)&2047)-1024;
        int height = (pDudeInfo->eyeHeight*pSprite->yrepeat)<<2;
        // Should be dudeInfo[pTarget->type-kDudeBase]
        int height2 = (pDudeInfo->eyeHeight*pTarget->yrepeat)<<2;
        int top, bottom;
        GetSpriteExtents(pSprite, &top, &bottom);
        if (cansee(pTarget->x, pTarget->y, pTarget->z, pTarget->sectnum, pSprite->x, pSprite->y, pSprite->z - height, pSprite->sectnum))
        {
            if (nDist < pDudeInfo->seeDist && klabs(nDeltaAngle) <= pDudeInfo->periphery)
            {
                aiSetTarget(pXSprite, pXSprite->target);
                int floorZ = getflorzofslope(pSprite->sectnum, pSprite->x, pSprite->y);
                if (height2-height < 0x2000 && nDist < 0x200 && klabs(nDeltaAngle) < 85)
                    aiNewState(pSprite, pXSprite, &batBite);
                else if ((height2-height > 0x5000 || floorZ-bottom > 0x5000) && nDist < 0x1400 && nDist > 0x800 && klabs(nDeltaAngle) < 85)
                    aiNewState(pSprite, pXSprite, &batSwoop);
                else if ((height2-height < 0x3000 || floorZ-bottom < 0x3000) && klabs(nDeltaAngle) < 85)
                    aiNewState(pSprite, pXSprite, &batFly);
                return;
            }
        }
        else
        {
            aiNewState(pSprite, pXSprite, &batFly);
            return;
        }
    }

    pXSprite->target = -1;
    aiNewState(pSprite, pXSprite, &batHide);
}

static void MoveForward(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    int nAccel = pDudeInfo->frontSpeed<<2;
    if (klabs(nAng) > 341)
        return;
    if (pXSprite->target == -1)
        pSprite->ang = (pSprite->ang+256)&2047;
    int dx = pXSprite->targetX-pSprite->x;
    int dy = pXSprite->targetY-pSprite->y;
    int UNUSED(nAngle) = getangle(dx, dy);
    int nDist = approxDist(dx, dy);
    if ((unsigned int)Random(64) < 32 && nDist <= 0x200)
        return;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int vx = xvel[nSprite];
    int vy = yvel[nSprite];
    int t1 = dmulscale30(vx, nCos, vy, nSin);
    int t2 = dmulscale30(vx, nSin, -vy, nCos);
    if (pXSprite->target == -1)
        t1 += nAccel;
    else
        t1 += nAccel>>1;
    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
}

static void MoveSwoop(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    int nAccel = pDudeInfo->frontSpeed<<2;
    if (klabs(nAng) > 341)
    {
        pXSprite->goalAng = (pSprite->ang+512)&2047;
        return;
    }
    int dx = pXSprite->targetX-pSprite->x;
    int dy = pXSprite->targetY-pSprite->y;
    int UNUSED(nAngle) = getangle(dx, dy);
    int nDist = approxDist(dx, dy);
    if (Chance(0x600) && nDist <= 0x200)
        return;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int vx = xvel[nSprite];
    int vy = yvel[nSprite];
    int t1 = dmulscale30(vx, nCos, vy, nSin);
    int t2 = dmulscale30(vx, nSin, -vy, nCos);
    t1 += nAccel>>1;
    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
    zvel[nSprite] = 0x44444;
}

static void MoveFly(spritetype *pSprite, XSPRITE *pXSprite)
{
    int nSprite = pSprite->index;
    dassert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax);
    DUDEINFO *pDudeInfo = getDudeInfo(pSprite->type);
    int nAng = ((pXSprite->goalAng+1024-pSprite->ang)&2047)-1024;
    int nTurnRange = (pDudeInfo->angSpeed<<2)>>4;
    pSprite->ang = (pSprite->ang+ClipRange(nAng, -nTurnRange, nTurnRange))&2047;
    int nAccel = pDudeInfo->frontSpeed<<2;
    if (klabs(nAng) > 341)
    {
        pSprite->ang = (pSprite->ang+512)&2047;
        return;
    }
    int dx = pXSprite->targetX-pSprite->x;
    int dy = pXSprite->targetY-pSprite->y;
    int UNUSED(nAngle) = getangle(dx, dy);
    int nDist = approxDist(dx, dy);
    if (Chance(0x4000) && nDist <= 0x200)
        return;
    int nCos = Cos(pSprite->ang);
    int nSin = Sin(pSprite->ang);
    int vx = xvel[nSprite];
    int vy = yvel[nSprite];
    int t1 = dmulscale30(vx, nCos, vy, nSin);
    int t2 = dmulscale30(vx, nSin, -vy, nCos);
    t1 += nAccel>>1;
    xvel[nSprite] = dmulscale30(t1, nCos, t2, nSin);
    yvel[nSprite] = dmulscale30(t1, nSin, -t2, nCos);
    zvel[nSprite] = -0x2d555;
}

void MoveToCeil(spritetype *pSprite, XSPRITE *pXSprite)
{
    int x = pSprite->x;
    int y = pSprite->y;
    int z = pSprite->z;
    int nSector = pSprite->sectnum;
    if (z - pXSprite->targetZ < 0x1000)
    {
        DUDEEXTRA_STATS *pDudeExtraE = &gDudeExtra[pSprite->extra].stats;
        pDudeExtraE->active = 0;
        pSprite->flags = 0;
        aiNewState(pSprite, pXSprite, &batIdle);
    }
    else
        aiSetTarget(pXSprite, x, y, sector[nSector].ceilingz);
}