//------------------------------------------------------------------------- /* 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 "ns.h" // Must come before everything else! #include "build.h" #include "savegamehelp.h" #include "blood.h" BEGIN_BLD_NS void RecoilDude(DBloodActor* actor); AISTATE genIdle = { kAiStateGenIdle, 0, -1, 0, NULL, NULL, NULL, NULL }; AISTATE genRecoil = { kAiStateRecoil, 5, -1, 20, NULL, NULL, NULL, &genIdle }; const int dword_138BB0[5] = { 0x2000, 0x4000, 0x8000, 0xa000, 0xe000 }; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool dudeIsPlayingSeq(DBloodActor* actor, int nSeq) { auto pSprite = &actor->s(); 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(DBloodActor* actor, int a2, AI_SFX_PRIORITY a3, int a4) { DUDEEXTRA* pDudeExtra = &actor->dudeExtra; if (a3 == AI_SFX_PRIORITY_0) sfxPlay3DSound(actor, a2, a4, 2); else if (a3 > pDudeExtra->prio || pDudeExtra->time <= PlayClock) { sfxKill3DSound(actor, -1, -1); sfxPlay3DSound(actor, a2, a4, 0); pDudeExtra->prio = a3; pDudeExtra->time = PlayClock + 120; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiNewState(DBloodActor* actor, AISTATE* pAIState) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); DUDEINFO* pDudeInfo = getDudeInfo(pSprite->type); pXSprite->stateTimer = pAIState->stateTicks; pXSprite->aiState = pAIState; int seqStartId = pDudeInfo->seqStartID; if (pAIState->seqId >= 0) { seqStartId += pAIState->seqId; if (getSequence(seqStartId)) seqSpawn(seqStartId, actor, pAIState->funcId); } if (pAIState->enterFunc) pAIState->enterFunc(actor); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static bool isImmune(DBloodActor* actor, int dmgType, int minScale) { if (dmgType >= kDmgFall && dmgType < kDmgMax && actor->hasX() && actor->x().locked != 1) { int type = actor->s().type; if (type >= kThingBase && type < kThingMax) return (thingInfo[type - kThingBase].dmgControl[dmgType] <= minScale); else if (actor->IsDudeActor()) { if (actor->IsPlayerActor()) return (gPlayer[type - kDudePlayer1].godMode || gPlayer[type - kDudePlayer1].damageControl[dmgType] <= minScale); else return (dudeInfo[type - kDudeBase].damageVal[dmgType] <= minScale); } } return true; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool CanMove(DBloodActor* actor, DBloodActor* target, int nAngle, int nRange) { auto pSprite = &actor->s(); int top, bottom; GetActorExtents(actor, &top, &bottom); int x = pSprite->x; int y = pSprite->y; int z = pSprite->z; HitScan(pSprite, z, CosScale16(nAngle), SinScale16(nAngle), 0, CLIPMASK0, nRange); int nDist = approxDist(x - gHitInfo.hitx, y - gHitInfo.hity); if (nDist - (pSprite->clipdist << 2) < nRange) { if (gHitInfo.hitactor == nullptr || target == nullptr || target != gHitInfo.hitactor) return false; return true; } x += MulScale(nRange, Cos(nAngle), 30); y += MulScale(nRange, Sin(nAngle), 30); int nSector = pSprite->sectnum; assert(nSector >= 0 && nSector < kMaxSectors); if (!FindSector(x, y, z, &nSector)) return false; int floorZ = getflorzofslope(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; } auto Upper = getUpperLink(nSector); auto Lower = getLowerLink(nSector); if (Upper != nullptr) { if (Upper->s().type == kMarkerUpWater || Upper->s().type == kMarkerUpGoo) Water = Depth = 1; } if (Lower != nullptr) { if (Lower->s().type == kMarkerLowWater || Lower->s().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 (!cl_bloodvanillaenemies && !VanillaMode() && Crusher && isImmune(actor, 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(DBloodActor* actor, int a3) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(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 = actor->xvel(); int dy = actor->yvel(); int t1 = DMulScale(dx, nCos, dy, nSin, 30); int vsi = ((t1 * 15) >> 12) / 2; int v8 = 341; if (vc < 0) v8 = -341; if (CanMove(actor, actor->GetTarget(), pSprite->ang + vc, vsi)) pXSprite->goalAng = pSprite->ang + vc; else if (CanMove(actor, actor->GetTarget(), pSprite->ang + vc / 2, vsi)) pXSprite->goalAng = pSprite->ang + vc / 2; else if (CanMove(actor, actor->GetTarget(), pSprite->ang - vc / 2, vsi)) pXSprite->goalAng = pSprite->ang - vc / 2; else if (CanMove(actor, actor->GetTarget(), pSprite->ang + v8, vsi)) pXSprite->goalAng = pSprite->ang + v8; else if (CanMove(actor, actor->GetTarget(), pSprite->ang, vsi)) pXSprite->goalAng = pSprite->ang; else if (CanMove(actor, actor->GetTarget(), 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(actor, actor->GetTarget(), pSprite->ang + pXSprite->dodgeDir * 512, 512)) { pXSprite->dodgeDir = -pXSprite->dodgeDir; if (!CanMove(actor, actor->GetTarget(), pSprite->ang + pXSprite->dodgeDir * 512, 512)) pXSprite->dodgeDir = 0; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiMoveForward(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(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 (abs(nAng) > 341) return; actor->xvel() += MulScale(pDudeInfo->frontSpeed, Cos(pSprite->ang), 30); actor->yvel() += MulScale(pDudeInfo->frontSpeed, Sin(pSprite->ang), 30); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiMoveTurn(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(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(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(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 = actor->xvel(); int dy = actor->yvel(); int t1 = DMulScale(dx, nCos, dy, nSin, 30); int t2 = DMulScale(dx, nSin, -dy, nCos, 30); if (pXSprite->dodgeDir > 0) t2 += pDudeInfo->sideSpeed; else t2 -= pDudeInfo->sideSpeed; actor->xvel() = DMulScale(t1, nCos, t2, nSin, 30); actor->yvel() = DMulScale(t1, nSin, -t2, nCos, 30); } } //--------------------------------------------------------------------------- // // todo: split this up. // //--------------------------------------------------------------------------- void aiActivateDude(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(pSprite->type >= kDudeBase && pSprite->type < kDudeMax); if (!pXSprite->state) { aiChooseDirection(actor, getangle(pXSprite->targetX - pSprite->x, pXSprite->targetY - pSprite->y)); pXSprite->state = 1; } switch (pSprite->type) { case kDudePhantasm: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &ghostSearch); else { aiPlay3DSound(actor, 1600, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &ghostChase); } break; } case kDudeCultistTommy: case kDudeCultistShotgun: case kDudeCultistTesla: case kDudeCultistTNT: case kDudeCultistBeast: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) { switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); if (Chance(0x8000)) { if (pSprite->type == kDudeCultistTommy) aiPlay3DSound(actor, 4008 + Random(5), AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1008 + Random(5), AI_SFX_PRIORITY_1, -1); } break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } } else { if (Chance(0x8000)) { if (pSprite->type == kDudeCultistTommy) aiPlay3DSound(actor, 4003 + Random(4), AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1003 + Random(4), AI_SFX_PRIORITY_1, -1); } switch (pXSprite->medium) { case kMediumNormal: if (pSprite->type == kDudeCultistTommy) aiNewState(actor, &fanaticChase); else aiNewState(actor, &cultistChase); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimChase); break; } } break; } #ifdef NOONE_EXTENSIONS case kDudeModernCustom: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) { 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 (actor->GetTarget() == nullptr) aiGenDudeNewState(pSprite, &genDudeBurnSearch); else aiGenDudeNewState(pSprite, &genDudeBurnChase); break; #endif case kDudeCultistTommyProne: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; pSprite->type = kDudeCultistTommy; if (actor->GetTarget() == nullptr) { switch (pXSprite->medium) { case 0: aiNewState(actor, &cultistSearch); if (Chance(0x8000)) aiPlay3DSound(actor, 4008 + Random(5), AI_SFX_PRIORITY_1, -1); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } } else { if (Chance(0x8000)) aiPlay3DSound(actor, 4008 + Random(5), AI_SFX_PRIORITY_1, -1); switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &cultistProneChase); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimChase); break; } } break; } case kDudeCultistShotgunProne: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; pSprite->type = kDudeCultistShotgun; if (actor->GetTarget() == nullptr) { switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); if (Chance(0x8000)) aiPlay3DSound(actor, 1008 + Random(5), AI_SFX_PRIORITY_1, -1); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } } else { if (Chance(0x8000)) aiPlay3DSound(actor, 1003 + Random(4), AI_SFX_PRIORITY_1, -1); switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &cultistProneChase); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimChase); break; } } break; } case kDudeBurningCultist: if (actor->GetTarget() == nullptr) aiNewState(actor, &cultistBurnSearch); else aiNewState(actor, &cultistBurnChase); break; case kDudeBat: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (!pSprite->flags) pSprite->flags = 9; if (actor->GetTarget() == nullptr) aiNewState(actor, &batSearch); else { if (Chance(0xa000)) aiPlay3DSound(actor, 2000, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &batChase); } break; } case kDudeBoneEel: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &eelSearch); else { if (Chance(0x8000)) aiPlay3DSound(actor, 1501, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1500, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &eelChase); } break; } case kDudeGillBeast: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; XSECTOR* pXSector = NULL; if (sector[pSprite->sectnum].extra > 0) pXSector = &xsector[sector[pSprite->sectnum].extra]; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) { if (pXSector && pXSector->Underwater) aiNewState(actor, &gillBeastSwimSearch); else aiNewState(actor, &gillBeastSearch); } else { if (Chance(0x4000)) aiPlay3DSound(actor, 1701, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1700, AI_SFX_PRIORITY_1, -1); if (pXSector && pXSector->Underwater) aiNewState(actor, &gillBeastSwimChase); else aiNewState(actor, &gillBeastChase); } break; } case kDudeZombieAxeNormal: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &zombieASearch); else { if (Chance(0xa000)) { switch (Random(3)) { default: case 0: case 3: aiPlay3DSound(actor, 1103, AI_SFX_PRIORITY_1, -1); break; case 1: aiPlay3DSound(actor, 1104, AI_SFX_PRIORITY_1, -1); break; case 2: aiPlay3DSound(actor, 1105, AI_SFX_PRIORITY_1, -1); break; } } aiNewState(actor, &zombieAChase); } break; } case kDudeZombieAxeBuried: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (pXSprite->aiState == &zombieEIdle) aiNewState(actor, &zombieEUp); break; } case kDudeZombieAxeLaying: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (pXSprite->aiState == &zombieSIdle) aiNewState(actor, &zombie13AC2C); break; } case kDudeZombieButcher: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &zombieFSearch); else { if (Chance(0x4000)) aiPlay3DSound(actor, 1201, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1200, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &zombieFChase); } break; } case kDudeBurningZombieAxe: if (actor->GetTarget() == nullptr) aiNewState(actor, &zombieABurnSearch); else aiNewState(actor, &zombieABurnChase); break; case kDudeBurningZombieButcher: if (actor->GetTarget() == nullptr) aiNewState(actor, &zombieFBurnSearch); else aiNewState(actor, &zombieFBurnChase); break; case kDudeGargoyleFlesh: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &gargoyleFSearch); else { if (Chance(0x4000)) aiPlay3DSound(actor, 1401, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1400, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &gargoyleFChase); } break; } case kDudeGargoyleStone: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 1; if (actor->GetTarget() == nullptr) aiNewState(actor, &gargoyleFSearch); else { if (Chance(0x4000)) aiPlay3DSound(actor, 1451, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1450, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &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(actor, &statueFBreakSEQ); else aiNewState(actor, &statueSBreakSEQ); } else { if (Chance(0x4000)) aiPlay3DSound(actor, 1401, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1400, AI_SFX_PRIORITY_1, -1); if (pSprite->type == kDudeGargoyleStatueFlesh) aiNewState(actor, &gargoyleFMorph); else aiNewState(actor, &gargoyleSMorph); } #else if (Chance(0x4000)) aiPlay3DSound(actor, 1401, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1400, AI_SFX_PRIORITY_1, -1); if (pSprite->type == kDudeGargoyleStatueFlesh) aiNewState(actor, &gargoyleFMorph); else aiNewState(actor, &gargoyleSMorph); #endif break; case kDudeCerberusTwoHead: if (actor->GetTarget() == nullptr) aiNewState(actor, &cerberusSearch); else { aiPlay3DSound(actor, 2300, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &cerberusChase); } break; case kDudeCerberusOneHead: if (actor->GetTarget() == nullptr) aiNewState(actor, &cerberus2Search); else { aiPlay3DSound(actor, 2300, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &cerberus2Chase); } break; case kDudeHellHound: if (actor->GetTarget() == nullptr) aiNewState(actor, &houndSearch); else { aiPlay3DSound(actor, 1300, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &houndChase); } break; case kDudeHand: if (actor->GetTarget() == nullptr) aiNewState(actor, &handSearch); else { aiPlay3DSound(actor, 1900, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &handChase); } break; case kDudeRat: if (actor->GetTarget() == nullptr) aiNewState(actor, &ratSearch); else { aiPlay3DSound(actor, 2100, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &ratChase); } break; case kDudeInnocent: if (actor->GetTarget() == nullptr) aiNewState(actor, &innocentSearch); else { if (pXSprite->health > 0) aiPlay3DSound(actor, 7000 + Random(6), AI_SFX_PRIORITY_1, -1); aiNewState(actor, &innocentChase); } break; case kDudeTchernobog: if (actor->GetTarget() == nullptr) aiNewState(actor, &tchernobogSearch); else { aiPlay3DSound(actor, 2350 + Random(7), AI_SFX_PRIORITY_1, -1); aiNewState(actor, &tchernobogChase); } break; case kDudeSpiderBrown: case kDudeSpiderRed: case kDudeSpiderBlack: pSprite->flags |= 2; pSprite->cstat &= ~8; if (actor->GetTarget() == nullptr) aiNewState(actor, &spidSearch); else { aiPlay3DSound(actor, 1800, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &spidChase); } break; case kDudeSpiderMother: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; pSprite->flags |= 2; pSprite->cstat &= ~8; if (actor->GetTarget() == nullptr) aiNewState(actor, &spidSearch); else { aiPlay3DSound(actor, 1853 + Random(1), AI_SFX_PRIORITY_1, -1); aiNewState(actor, &spidChase); } break; } case kDudeTinyCaleb: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (actor->GetTarget() == nullptr) { switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &tinycalebSearch); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &tinycalebSwimSearch); break; } } else { switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &tinycalebChase); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &tinycalebSwimChase); break; } } break; } case kDudeBeast: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (actor->GetTarget() == nullptr) { switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &beastSearch); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &beastSwimSearch); break; } } else { aiPlay3DSound(actor, 9009 + Random(2), AI_SFX_PRIORITY_1, -1); switch (pXSprite->medium) { case kMediumNormal: aiNewState(actor, &beastChase); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &beastSwimChase); break; } } break; } case kDudePodGreen: case kDudePodFire: if (actor->GetTarget() == nullptr) aiNewState(actor, &podSearch); else { if (pSprite->type == kDudePodFire) aiPlay3DSound(actor, 2453, AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 2473, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &podChase); } break; case kDudeTentacleGreen: case kDudeTentacleFire: if (actor->GetTarget() == nullptr) aiNewState(actor, &tentacleSearch); else { aiPlay3DSound(actor, 2503, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &tentacleChase); } break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiSetTarget(DBloodActor* actor, int x, int y, int z) { auto pXSprite = &actor->x(); actor->SetTarget(nullptr); pXSprite->targetX = x; pXSprite->targetY = y; pXSprite->targetZ = z; } void aiSetTarget(DBloodActor* actor, DBloodActor* target) { if (target == nullptr) { actor->SetTarget(nullptr); return; } auto pXSprite = &actor->x(); spritetype* pTarget = &target->s(); if (pTarget->type >= kDudeBase && pTarget->type < kDudeMax) { if (actor->GetOwner() != target) { actor->SetTarget(target); DUDEINFO* pDudeInfo = getDudeInfo(pTarget->type); pXSprite->targetX = pTarget->x; pXSprite->targetY = pTarget->y; pXSprite->targetZ = pTarget->z - ((pDudeInfo->eyeHeight * pTarget->yrepeat) << 2); } } } //--------------------------------------------------------------------------- // // todo: split up and put most of its content in tables. // //--------------------------------------------------------------------------- int aiDamageSprite(DBloodActor* source, DBloodActor* actor, DAMAGE_TYPE nDmgType, int nDamage) { auto pSprite = &actor->s(); XSPRITE* pXSprite = &actor->x(); if (!pXSprite->health) return 0; pXSprite->health = ClipLow(pXSprite->health - nDamage, 0); actor->cumulDamage += nDamage; DUDEINFO* pDudeInfo = getDudeInfo(pSprite->type); if (source) { spritetype* pSource = &source->s(); int nSource = pSource->index; if (pSprite == pSource) return 0; else if (actor->GetTarget() == nullptr) // if no target, give the dude a target { aiSetTarget(actor, source); aiActivateDude(actor); } else if (source != actor->GetTarget()) // 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(actor, source); aiActivateDude(actor); } } #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(actor, source, 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) { actor->cumulDamage += nDamage << 4; // to be sure any enemy will play the recoil animation RecoilDude(&bloodActors[pXSprite->reference]); } } DPrintf(DMSG_SPAMMY, "Player #%d does the critical damage to patrol dude #%d!", pPlayer->nPlayer + 1, pSprite->index); } return nDamage; } if (pSprite->type == kDudeModernCustomBurning) { if (Chance(0x2000) && actor->dudeExtra.time < PlayClock) { playGenDudeSound(pSprite, kGenDudeSndBurning); actor->dudeExtra.time = PlayClock + 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 = &actor->genDudeExtra(); if (nDmgType == kDamageBurn) { if (pXSprite->health > (uint32_t)pDudeInfo->fleeHealth) return nDamage; else if (pXSprite->txID <= 0 || getNextIncarnation(pXSprite) == nullptr) { 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(actor, 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(actor, dudeInfo[55].startHealth, dudeInfo[55].startHealth); actor->dudeExtra.time = PlayClock + 360; evKill(actor, kCallbackFXFlameLick); } } else { actKillDude(actor, actor, 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(actor, 14)) aiGenDudeNewState(pSprite, &genDudeDodgeShortL); else aiGenDudeNewState(pSprite, &genDudeDodgeShortD); if (Chance(0x0200)) playGenDudeSound(pSprite, kGenDudeSndGotHit); } else if (dudeIsPlayingSeq(actor, 13)) { aiGenDudeNewState(pSprite, &genDudeDodgeShortW); } } } else if (Chance(0x0200)) { playGenDudeSound(pSprite, kGenDudeSndGotHit); } } return nDamage; } } #endif if (nDmgType == kDamageTesla) { DUDEEXTRA* pDudeExtra = &actor->dudeExtra; pDudeExtra->teslaHit = 1; } else if (!VanillaMode()) // reset tesla hit state if received different type of damage { DUDEEXTRA* pDudeExtra = &actor->dudeExtra; pDudeExtra->teslaHit = 0; } const bool fixRandomCultist = !cl_bloodvanillaenemies && (pSprite->inittype >= kDudeBase) && (pSprite->inittype < kDudeMax) && !VanillaMode(); // fix burning cultists randomly switching types underwater switch (pSprite->type) { case kDudeCultistTommy: case kDudeCultistShotgun: case kDudeCultistTesla: case kDudeCultistTNT: if (nDmgType != kDamageBurn) { if (!dudeIsPlayingSeq(actor, 14) && !pXSprite->medium) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && !pXSprite->medium) aiNewState(actor, &cultistProneDodge); else if (dudeIsPlayingSeq(actor, 13) && (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)) aiNewState(actor, &cultistSwimDodge); } else if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/) { pSprite->type = kDudeBurningCultist; aiNewState(actor, &cultistBurnGoto); aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); aiPlay3DSound(actor, 1031 + Random(2), AI_SFX_PRIORITY_2, -1); actor->dudeExtra.time = PlayClock + 360; actHealDude(actor, dudeInfo[40].startHealth, dudeInfo[40].startHealth); evKill(actor, 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(actor, &cultistBurnGoto); aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); actor->dudeExtra.time = PlayClock + 360; actHealDude(actor, dudeInfo[39].startHealth, dudeInfo[39].startHealth); evKill(actor, kCallbackFXFlameLick); } break; case kDudeBurningCultist: if (Chance(0x4000) && actor->dudeExtra.time < PlayClock) { aiPlay3DSound(actor, 1031 + Random(2), AI_SFX_PRIORITY_2, -1); actor->dudeExtra.time = PlayClock + 360; } if (Chance(0x600) && (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo)) { pSprite->type = kDudeCultistTommy; if (fixRandomCultist) // fix burning cultists randomly switching types underwater pSprite->type = pSprite->inittype; // restore back to spawned cultist type pXSprite->burnTime = 0; aiNewState(actor, &cultistSwimGoto); } else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo) { pSprite->type = kDudeCultistShotgun; if (fixRandomCultist) // fix burning cultists randomly switching types underwater pSprite->type = pSprite->inittype; // restore back to spawned cultist type pXSprite->burnTime = 0; aiNewState(actor, &cultistSwimGoto); } break; case kDudeGargoyleFlesh: aiNewState(actor, &gargoyleFChase); break; case kDudeZombieButcher: if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth) { aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); aiPlay3DSound(actor, 1202, AI_SFX_PRIORITY_2, -1); pSprite->type = kDudeBurningZombieButcher; aiNewState(actor, &zombieFBurnGoto); actHealDude(actor, dudeInfo[42].startHealth, dudeInfo[42].startHealth); evKill(actor, kCallbackFXFlameLick); } break; case kDudeTinyCaleb: if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth/* && (pXSprite->at17_6 != 1 || pXSprite->at17_6 != 2)*/) { if (!cl_bloodvanillaenemies && !VanillaMode()) // fix burning sprite for tiny caleb { pSprite->type = kDudeBurningTinyCaleb; aiNewState(actor, &tinycalebBurnGoto); } else { pSprite->type = kDudeBurningInnocent; aiNewState(actor, &cultistBurnGoto); } aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); actor->dudeExtra.time = PlayClock + 360; actHealDude(actor, dudeInfo[39].startHealth, dudeInfo[39].startHealth); evKill(actor, kCallbackFXFlameLick); } break; case kDudeCultistBeast: if (pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth) { pSprite->type = kDudeBeast; aiPlay3DSound(actor, 9008, AI_SFX_PRIORITY_1, -1); aiNewState(actor, &beastMorphFromCultist); actHealDude(actor, dudeInfo[51].startHealth, dudeInfo[51].startHealth); } break; case kDudeZombieAxeNormal: case kDudeZombieAxeBuried: if (nDmgType == kDamageBurn && pXSprite->health <= (unsigned int)pDudeInfo->fleeHealth) { aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); aiPlay3DSound(actor, 1106, AI_SFX_PRIORITY_2, -1); pSprite->type = kDudeBurningZombieAxe; aiNewState(actor, &zombieABurnGoto); actHealDude(actor, dudeInfo[41].startHealth, dudeInfo[41].startHealth); evKill(actor, kCallbackFXFlameLick); } break; } } return nDamage; } //--------------------------------------------------------------------------- // // todo: split up and put most of its content in tables. // //--------------------------------------------------------------------------- void RecoilDude(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); char v4 = Chance(0x8000); DUDEEXTRA* pDudeExtra = &actor->dudeExtra; 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 = &actor->genDudeExtra(); 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(actor, 4013 + Random(2), AI_SFX_PRIORITY_2, -1); else aiPlay3DSound(actor, 1013 + Random(2), AI_SFX_PRIORITY_2, -1); if (!v4 && pXSprite->medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &cultistTeslaRecoil); else aiNewState(actor, &cultistRecoil); } else if (v4 && pXSprite->medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &cultistTeslaRecoil); else if (gGameOptions.nDifficulty > 0) aiNewState(actor, &cultistProneRecoil); else aiNewState(actor, &cultistRecoil); } else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo) aiNewState(actor, &cultistSwimRecoil); else { if (pDudeExtra->teslaHit) aiNewState(actor, &cultistTeslaRecoil); else aiNewState(actor, &cultistRecoil); } break; case kDudeBurningCultist: aiNewState(actor, &cultistBurnGoto); break; #ifdef NOONE_EXTENSIONS case kDudeModernCustomBurning: aiGenDudeNewState(pSprite, &genDudeBurnGoto); break; #endif case kDudeZombieButcher: aiPlay3DSound(actor, 1202, AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit) aiNewState(actor, &zombieFTeslaRecoil); else aiNewState(actor, &zombieFRecoil); break; case kDudeZombieAxeNormal: case kDudeZombieAxeBuried: aiPlay3DSound(actor, 1106, AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit && pXSprite->data3 > pDudeInfo->startHealth / 3) aiNewState(actor, &zombieATeslaRecoil); else if (pXSprite->data3 > pDudeInfo->startHealth / 3) aiNewState(actor, &zombieARecoil2); else aiNewState(actor, &zombieARecoil); break; case kDudeBurningZombieAxe: aiPlay3DSound(actor, 1106, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &zombieABurnGoto); break; case kDudeBurningZombieButcher: aiPlay3DSound(actor, 1202, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &zombieFBurnGoto); break; case kDudeGargoyleFlesh: case kDudeGargoyleStone: aiPlay3DSound(actor, 1402, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &gargoyleFRecoil); break; case kDudeCerberusTwoHead: aiPlay3DSound(actor, 2302 + Random(2), AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit && pXSprite->data3 > pDudeInfo->startHealth / 3) aiNewState(actor, &cerberusTeslaRecoil); else aiNewState(actor, &cerberusRecoil); break; case kDudeCerberusOneHead: aiPlay3DSound(actor, 2302 + Random(2), AI_SFX_PRIORITY_2, -1); aiNewState(actor, &cerberus2Recoil); break; case kDudeHellHound: aiPlay3DSound(actor, 1302, AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit) aiNewState(actor, &houndTeslaRecoil); else aiNewState(actor, &houndRecoil); break; case kDudeTchernobog: aiPlay3DSound(actor, 2370 + Random(2), AI_SFX_PRIORITY_2, -1); aiNewState(actor, &tchernobogRecoil); break; case kDudeHand: aiPlay3DSound(actor, 1902, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &handRecoil); break; case kDudeRat: aiPlay3DSound(actor, 2102, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &ratRecoil); break; case kDudeBat: aiPlay3DSound(actor, 2002, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &batRecoil); break; case kDudeBoneEel: aiPlay3DSound(actor, 1502, AI_SFX_PRIORITY_2, -1); aiNewState(actor, &eelRecoil); break; case kDudeGillBeast: { XSECTOR* pXSector = NULL; if (sector[pSprite->sectnum].extra > 0) pXSector = &xsector[sector[pSprite->sectnum].extra]; aiPlay3DSound(actor, 1702, AI_SFX_PRIORITY_2, -1); if (pXSector && pXSector->Underwater) aiNewState(actor, &gillBeastSwimRecoil); else aiNewState(actor, &gillBeastRecoil); break; } case kDudePhantasm: aiPlay3DSound(actor, 1602, AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit) aiNewState(actor, &ghostTeslaRecoil); else aiNewState(actor, &ghostRecoil); break; case kDudeSpiderBrown: case kDudeSpiderRed: case kDudeSpiderBlack: aiPlay3DSound(actor, 1802 + Random(1), AI_SFX_PRIORITY_2, -1); aiNewState(actor, &spidDodge); break; case kDudeSpiderMother: aiPlay3DSound(actor, 1851 + Random(1), AI_SFX_PRIORITY_2, -1); aiNewState(actor, &spidDodge); break; case kDudeInnocent: aiPlay3DSound(actor, 7007 + Random(2), AI_SFX_PRIORITY_2, -1); if (pDudeExtra->teslaHit) aiNewState(actor, &innocentTeslaRecoil); else aiNewState(actor, &innocentRecoil); break; case kDudeTinyCaleb: if (pXSprite->medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &tinycalebTeslaRecoil); else aiNewState(actor, &tinycalebRecoil); } else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo) aiNewState(actor, &tinycalebSwimRecoil); else { if (pDudeExtra->teslaHit) aiNewState(actor, &tinycalebTeslaRecoil); else aiNewState(actor, &tinycalebRecoil); } break; case kDudeBeast: aiPlay3DSound(actor, 9004 + Random(2), AI_SFX_PRIORITY_2, -1); if (pXSprite->medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &beastTeslaRecoil); else aiNewState(actor, &beastRecoil); } else if (pXSprite->medium == kMediumWater || pXSprite->medium == kMediumGoo) aiNewState(actor, &beastSwimRecoil); else { if (pDudeExtra->teslaHit) aiNewState(actor, &beastTeslaRecoil); else aiNewState(actor, &beastRecoil); } break; case kDudePodGreen: case kDudePodFire: aiNewState(actor, &podRecoil); break; case kDudeTentacleGreen: case kDudeTentacleFire: aiNewState(actor, &tentacleRecoil); break; default: aiNewState(actor, &genRecoil); break; } pDudeExtra->teslaHit = 0; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiThinkTarget(DBloodActor* actor) { auto pXSprite = &actor->x(); auto pSprite = &actor->s(); assert(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 && abs(nDeltaAngle) <= pDudeInfo->periphery) { aiSetTarget(actor, pPlayer->actor()); aiActivateDude(actor); return; } else if (nDist < pDudeInfo->hearDist) { aiSetTarget(actor, x, y, z); aiActivateDude(actor); return; } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiLookForTarget(DBloodActor* actor) { auto pSprite = &actor->s(); auto pXSprite = &actor->x(); assert(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 && abs(nDeltaAngle) <= pDudeInfo->periphery) { aiSetTarget(actor, pPlayer->actor()); aiActivateDude(actor); return; } else if (nDist < pDudeInfo->hearDist) { aiSetTarget(actor, x, y, z); aiActivateDude(actor); return; } } if (pXSprite->state) { uint8_t sectmap[(kMaxSectors + 7) >> 3]; const bool newSectCheckMethod = !cl_bloodvanillaenemies && !VanillaMode(); // use new sector checking logic GetClosestSpriteSectors(pSprite->sectnum, pSprite->x, pSprite->y, 400, sectmap, nullptr, newSectCheckMethod); BloodStatIterator it(kStatDude); while (DBloodActor* actor2 = it.Next()) { spritetype* pSprite2 = &actor2->s(); 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; aiSetTarget(actor, actor2); aiActivateDude(actor); return; } } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiProcessDudes(void) { BloodStatIterator it(kStatDude); while (auto actor = it.Next()) { auto pSprite = &actor->s(); if (pSprite->flags & 32) continue; auto pXSprite = &actor->x(); DUDEINFO* pDudeInfo = getDudeInfo(pSprite->type); if (IsPlayerSprite(pSprite) || pXSprite->health == 0) continue; pXSprite->stateTimer = ClipLow(pXSprite->stateTimer - 4, 0); if (pXSprite->aiState) { if (pXSprite->aiState->moveFunc) pXSprite->aiState->moveFunc(actor); if (pXSprite->aiState->thinkFunc && (gFrameCount & 3) == (pSprite->index & 3)) // ouch, ouch! :( pXSprite->aiState->thinkFunc(actor); } switch (pSprite->type) { #ifdef NOONE_EXTENSIONS case kDudeModernCustom: case kDudeModernCustomBurning: { GENDUDEEXTRA* pExtra = &actor->genDudeExtra(); if (pExtra->slaveCount > 0) updateTargetOfSlaves(pSprite); if (pExtra->nLifeLeech >= 0) updateTargetOfLeech(pSprite); if (pXSprite->stateTimer == 0 && pXSprite->aiState && 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 > actor->cumulDamage) break; pXSprite->data3 = actor->cumulDamage; RecoilDude(actor); break; } #endif default: if (pXSprite->stateTimer == 0 && pXSprite->aiState && pXSprite->aiState->nextState) { if (pXSprite->aiState->stateTicks > 0) aiNewState(actor, pXSprite->aiState->nextState); else if (seqGetStatus(actor) < 0) aiNewState(actor, pXSprite->aiState->nextState); } if (pXSprite->health > 0 && ((pDudeInfo->hinderDamage << 4) <= actor->cumulDamage)) { pXSprite->data3 = actor->cumulDamage; RecoilDude(actor); } break; } } it.Reset(kStatDude); while (auto actor = it.Next()) { actor->cumulDamage = 0; } } void aiInit(void) { BloodStatIterator it(kStatDude); while (auto actor = it.Next()) { aiInitSprite(actor); } } void aiInitSprite(DBloodActor* actor) { auto pSprite = &actor->s(); auto pXSprite = &actor->x(); int nSector = pSprite->sectnum; int nXSector = sector[nSector].extra; XSECTOR* pXSector = NULL; if (nXSector > 0) pXSector = &xsector[nXSector]; DUDEEXTRA* pDudeExtra = &actor->dudeExtra; DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtra->teslaHit = 0; pDudeExtra->time = 0; pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 0; #ifdef NOONE_EXTENSIONS int stateTimer = -1; int targetX = 0, targetY = 0, targetZ = 0; DBloodActor* pTargetMarker = nullptr; // dude patrol init if (gModernMap) { // must keep it in case of loading save if (pXSprite->dudeFlag4 && actor->GetTarget() && actor->GetTarget()->s().type == kMarkerPath) { stateTimer = pXSprite->stateTimer; pTargetMarker = actor->GetTarget(); 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(actor, &cultistIdle); break; } case kDudeCultistTommyProne: { pDudeExtraE->active = 0; aiNewState(actor, &fanaticProneIdle); break; } case kDudeCultistShotgunProne: { pDudeExtraE->active = 0; aiNewState(actor, &cultistProneIdle); break; } case kDudeZombieButcher: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &zombieFIdle); break; } case kDudeZombieAxeNormal: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &zombieAIdle); break; } case kDudeZombieAxeLaying: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &zombieSIdle); pSprite->flags &= ~1; break; } case kDudeZombieAxeBuried: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &zombieEIdle); break; } case kDudeGargoyleFlesh: case kDudeGargoyleStone: { pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 0; aiNewState(actor, &gargoyleFIdle); break; } case kDudeGargoyleStatueFlesh: case kDudeGargoyleStatueStone: aiNewState(actor, &gargoyleStatueIdle); break; case kDudeCerberusTwoHead: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &cerberusIdle); break; } case kDudeCerberusOneHead: { if (!VanillaMode()) { pDudeExtraE->thinkTime = 0; aiNewState(actor, &cerberus2Idle); break; } aiNewState(actor, &genIdle); break; } case kDudeHellHound: aiNewState(actor, &houndIdle); break; case kDudeHand: aiNewState(actor, &handIdle); break; case kDudePhantasm: { pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 0; aiNewState(actor, &ghostIdle); break; } case kDudeInnocent: aiNewState(actor, &innocentIdle); break; case kDudeRat: aiNewState(actor, &ratIdle); break; case kDudeBoneEel: { pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 0; aiNewState(actor, &eelIdle); break; } case kDudeGillBeast: aiNewState(actor, &gillBeastIdle); break; case kDudeBat: { pDudeExtraE->thinkTime = 0; pDudeExtraE->active = 0; aiNewState(actor, &batIdle); break; } case kDudeSpiderBrown: case kDudeSpiderRed: case kDudeSpiderBlack: { pDudeExtraE->active = 0; pDudeExtraE->thinkTime = 0; aiNewState(actor, &spidIdle); break; } case kDudeSpiderMother: { pDudeExtraE->active = 0; pDudeExtraE->birthCounter = 0; aiNewState(actor, &spidIdle); break; } case kDudeTchernobog: { pDudeExtraE->active = 0; pDudeExtraE->thinkTime = 0; aiNewState(actor, &tchernobogIdle); break; } case kDudeTinyCaleb: aiNewState(actor, &tinycalebIdle); break; case kDudeBeast: aiNewState(actor, &beastIdle); break; case kDudePodGreen: case kDudePodFire: aiNewState(actor, &podIdle); break; case kDudeTentacleGreen: case kDudeTentacleFire: aiNewState(actor, &tentacleIdle); break; #ifdef NOONE_EXTENSIONS case kDudeModernCustom: case kDudeModernCustomBurning: if (!gModernMap) break; aiGenDudeInitSprite(pSprite, pXSprite); genDudePrepare(pSprite, kGenDudePropertyAll); break; #endif default: aiNewState(actor, &genIdle); break; } aiSetTarget(actor, 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)) { if (!(pSprite->flags & kModernTypeFlag1)) // don't add autoaim for player if hitag 1 specified in editor. pSprite->flags = kHitagAutoAim; break; } [[fallthrough]]; // go default #endif default: pSprite->flags = 15; break; } #ifdef NOONE_EXTENSIONS if (gModernMap) { if (pXSprite->dudeFlag4) { // restore dude's path if (pTargetMarker) { actor->SetTarget(pTargetMarker); 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_i <= 0 || sprite[pXSprite->target_i].type != kMarkerPath) { pXSprite->target_i = -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 = stateTimer; // restore state timer } else if (uwater) aiPatrolState(pSprite, kAiStatePatrolMoveW); else if (pXSprite->unused1 & kDudeFlagCrouch) aiPatrolState(pSprite, kAiStatePatrolMoveC); else aiPatrolState(pSprite, kAiStatePatrolMoveL); } } #endif } END_BLD_NS