//------------------------------------------------------------------------- /* 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 gCultTeslaFireChance[5] = { 0x2000, 0x4000, 0x8000, 0xa000, 0xe000 }; //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool dudeIsPlayingSeq(DBloodActor* actor, int nSeq) { if (actor->spr.statnum == kStatDude && actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax) { DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); if (seqGetID(actor) == pDudeInfo->seqStartID + nSeq && seqGetStatus(actor) >= 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) { DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); actor->xspr.stateTimer = pAIState->stateTicks; actor->xspr.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->xspr.locked != 1) { int type = actor->spr.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) { int top, bottom; GetActorExtents(actor, &top, &bottom); int x = actor->int_pos().X; int y = actor->int_pos().Y; int z = actor->int_pos().Z; HitScan(actor, z, bcos(nAngle), bsin(nAngle), 0, CLIPMASK0, nRange); int nDist = approxDist(actor->spr.pos.XY() - gHitInfo.hitpos.XY()); if (nDist - (actor->int_clipdist()) < nRange) { if (gHitInfo.actor() == nullptr || target == nullptr || target != gHitInfo.actor()) return false; return true; } x += MulScale(nRange, Cos(nAngle), 30); y += MulScale(nRange, Sin(nAngle), 30); auto pSector = actor->sector(); assert(pSector); updatesectorz(x, y, z, &pSector); if (!pSector) return false; int floorZ = getflorzofslopeptr(pSector, x, y); auto pXSector = pSector->hasX()? &pSector->xs() : nullptr; bool Underwater = 0; bool Water = 0; bool Depth = 0; bool Crusher = 0; if (pXSector) { if (pXSector->Underwater) Underwater = 1; if (pXSector->Depth) Depth = 1; if (pSector->type == kSectorDamage || pXSector->damageType > 0) Crusher = 1; } auto Upper = pSector->upperLink; auto Lower = pSector->lowerLink; if (Upper != nullptr) { if (Upper->spr.type == kMarkerUpWater || Upper->spr.type == kMarkerUpGoo) Water = Depth = 1; } if (Lower != nullptr) { if (Lower->spr.type == kMarkerLowWater || Lower->spr.type == kMarkerLowGoo) Depth = 1; } switch (actor->spr.type) { case kDudeGargoyleFlesh: case kDudeGargoyleStone: case kDudeBat: if (actor->native_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(actor, pXSector->damageType)) || ((Water || Underwater) && !canSwim(actor))) return false; return true; [[fallthrough]]; #endif case kDudeZombieAxeNormal: case kDudePhantasm: case kDudeGillBeast: default: if (Crusher) return false; if ((pXSector == nullptr || (!pXSector->Underwater && !pXSector->Depth)) && floorZ - bottom > 0x2000) return false; break; } return 1; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiChooseDirection(DBloodActor* actor, DAngle direction) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DAngle vc = deltaangle(actor->spr.angle, direction); double nCos = actor->spr.angle.Cos(); double nSin = actor->spr.angle.Sin(); double t1 = actor->vel.X * nCos + actor->vel.Y * nSin; auto almost60deg = DAngle::fromBuild(341); // 60° does not work correctly - this is a little bit less, actually. int range = FloatToFixed(t1 * (15 / 8192.)); DAngle v8 = vc.Sgn() == -1 ? -almost60deg : almost60deg; if (CanMove(actor, actor->GetTarget(), actor->spr.angle + vc, range)) actor->xspr.goalAng = actor->spr.angle + vc; else if (CanMove(actor, actor->GetTarget(), actor->spr.angle + vc / 2, range)) actor->xspr.goalAng = actor->spr.angle + vc / 2; else if (CanMove(actor, actor->GetTarget(), actor->spr.angle - vc / 2, range)) actor->xspr.goalAng = actor->spr.angle - vc / 2; else if (CanMove(actor, actor->GetTarget(), actor->spr.angle + v8, range)) actor->xspr.goalAng = actor->spr.angle + v8; else if (CanMove(actor, actor->GetTarget(), actor->spr.angle, range)) actor->xspr.goalAng = actor->spr.angle; else if (CanMove(actor, actor->GetTarget(), actor->spr.angle - v8, range)) actor->xspr.goalAng = actor->spr.angle - v8; //else if (actor->spr.flags&2) //actor->xspr.goalAng = actor->spr.angle+341; else // Weird.. actor->xspr.goalAng = actor->spr.angle + almost60deg; if (Chance(0x8000)) actor->xspr.dodgeDir = 1; else actor->xspr.dodgeDir = -1; actor->xspr.goalAng = actor->xspr.goalAng.Normalized360(); if (!CanMove(actor, actor->GetTarget(), actor->spr.angle + DAngle90 * actor->xspr.dodgeDir, 512)) { actor->xspr.dodgeDir = -actor->xspr.dodgeDir; if (!CanMove(actor, actor->GetTarget(), actor->spr.angle + DAngle90 * actor->xspr.dodgeDir, 512)) actor->xspr.dodgeDir = 0; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiMoveForward(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); auto nAng = deltaangle(actor->spr.angle, actor->xspr.goalAng); auto nTurnRange = DAngle::fromQ16(pDudeInfo->angSpeed << 3); actor->spr.angle += clamp(nAng, -nTurnRange, nTurnRange); if (abs(nAng) > DAngle60) return; actor->add_int_bvel_x(MulScale(pDudeInfo->frontSpeed, Cos(actor->int_ang()), 30)); actor->add_int_bvel_y(MulScale(pDudeInfo->frontSpeed, Sin(actor->int_ang()), 30)); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiMoveTurn(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); auto nAng = deltaangle(actor->spr.angle, actor->xspr.goalAng); auto nTurnRange = DAngle::fromQ16(pDudeInfo->angSpeed << 3); actor->spr.angle += clamp(nAng, -nTurnRange, nTurnRange); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiMoveDodge(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); auto nAng = deltaangle(actor->spr.angle, actor->xspr.goalAng); auto nTurnRange = DAngle::fromQ16(pDudeInfo->angSpeed << 3); actor->spr.angle += clamp(nAng, -nTurnRange, nTurnRange); if (actor->xspr.dodgeDir) { AdjustVelocity(actor, ADJUSTER{ if (actor->xspr.dodgeDir > 0) t2 += FixedToFloat(pDudeInfo->sideSpeed); else t2 -= FixedToFloat(pDudeInfo->sideSpeed); }); } } //--------------------------------------------------------------------------- // // todo: split this up. // //--------------------------------------------------------------------------- void aiActivateDude(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); if (!actor->xspr.state) { aiChooseDirection(actor, VecToAngle(actor->xspr.TargetPos - actor->spr.pos)); actor->xspr.state = 1; } switch (actor->spr.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 (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); if (Chance(0x8000)) { if (actor->spr.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 (actor->spr.type == kDudeCultistTommy) aiPlay3DSound(actor, 4003 + Random(4), AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1003 + Random(4), AI_SFX_PRIORITY_1, -1); } switch (actor->xspr.medium) { case kMediumNormal: if (actor->spr.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(actor, false)) aiGenDudeNewState(actor, &genDudeSearchW); else aiGenDudeNewState(actor, &genDudeSearchL); } else { if (Chance(0x4000)) playGenDudeSound(actor,kGenDudeSndTargetSpot); if (spriteIsUnderwater(actor, false)) aiGenDudeNewState(actor, &genDudeChaseW); else aiGenDudeNewState(actor, &genDudeChaseL); } break; } case kDudeModernCustomBurning: if (actor->GetTarget() == nullptr) aiGenDudeNewState(actor, &genDudeBurnSearch); else aiGenDudeNewState(actor, &genDudeBurnChase); break; #endif case kDudeCultistTommyProne: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->active = 1; actor->spr.type = kDudeCultistTommy; if (actor->GetTarget() == nullptr) { switch (actor->xspr.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 (actor->xspr.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; actor->spr.type = kDudeCultistShotgun; if (actor->GetTarget() == nullptr) { switch (actor->xspr.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 (actor->xspr.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 (!actor->spr.flags) actor->spr.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 = actor->sector()->hasX()? &actor->sector()->xs() : nullptr; 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 (actor->xspr.aiState == &zombieEIdle) aiNewState(actor, &zombieEUp); break; } case kDudeZombieAxeLaying: { DUDEEXTRA_STATS* pDudeExtraE = &actor->dudeExtra.stats; pDudeExtraE->thinkTime = 1; if (actor->xspr.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 && actor->xspr.data1 == 1) { if (actor->spr.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 (actor->spr.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 (actor->spr.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 (actor->xspr.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: actor->spr.flags |= 2; actor->spr.cstat &= ~CSTAT_SPRITE_YFLIP; 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; actor->spr.flags |= 2; actor->spr.cstat &= ~CSTAT_SPRITE_YFLIP; 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 (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &tinycalebSearch); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &tinycalebSwimSearch); break; } } else { switch (actor->xspr.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 (actor->xspr.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 (actor->xspr.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 (actor->spr.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) { actor->SetTarget(nullptr); actor->xspr.TargetPos = {x * maptoworld, y * maptoworld, z * zmaptoworld }; } void aiSetTarget(DBloodActor* actor, DBloodActor* target) { if (target == nullptr) { actor->SetTarget(nullptr); return; } if (target->spr.type >= kDudeBase && target->spr.type < kDudeMax) { if (actor->GetOwner() != target) { actor->SetTarget(target); DUDEINFO* pDudeInfo = getDudeInfo(target->spr.type); double eyeHeight = ((pDudeInfo->eyeHeight * target->spr.yrepeat) << 2) * inttoworld; actor->xspr.TargetPos = target->spr.pos.plusZ(-eyeHeight); } } } //--------------------------------------------------------------------------- // // todo: split up and put most of its content in tables. // //--------------------------------------------------------------------------- int aiDamageSprite(DBloodActor* source, DBloodActor* actor, DAMAGE_TYPE nDmgType, int nDamage) { if (!actor->xspr.health) return 0; actor->xspr.health = ClipLow(actor->xspr.health - nDamage, 0); actor->cumulDamage += nDamage; DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); if (source) { if (actor == source) 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 (actor->spr.type == source->spr.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(actor->xspr.aiState)) { aiPatrolStop(actor, source, actor->xspr.dudeAmbush); PLAYER* pPlayer = getPlayerById(source->spr.type); if (!pPlayer) return nDamage; //if (powerupCheck(pPlayer, kPwUpShadowCloak)) pPlayer->pwUpTime[kPwUpShadowCloak] = 0; if (readyForCrit(source, actor)) { nDamage += aiDamageSprite(actor, source, nDmgType, nDamage * (10 - gGameOptions.nDifficulty)); if (actor->xspr.health > 0) { int fullHp = (actor->xspr.sysData2 > 0) ? ClipRange(actor->xspr.sysData2 << 4, 1, 65535) : getDudeInfo(actor->spr.type)->startHealth << 4; if (((100 * actor->xspr.health) / fullHp) <= 75) { actor->cumulDamage += nDamage << 4; // to be sure any enemy will play the recoil animation RecoilDude(actor); } } DPrintf(DMSG_SPAMMY, "Player #%d does the critical damage to patrol dude #%d!", pPlayer->nPlayer + 1, actor->GetIndex()); } return nDamage; } if (actor->spr.type == kDudeModernCustomBurning) { if (Chance(0x2000) && actor->dudeExtra.time < PlayClock) { playGenDudeSound(actor,kGenDudeSndBurning); actor->dudeExtra.time = PlayClock + 360; } if (actor->xspr.burnTime == 0) actor->xspr.burnTime = 2400; if (spriteIsUnderwater(actor, false)) { actor->spr.type = kDudeModernCustom; actor->xspr.burnTime = 0; actor->xspr.health = 1; // so it can be killed with flame weapons while underwater and if already was burning dude before. aiGenDudeNewState(actor, &genDudeGotoW); } return nDamage; } if (actor->spr.type == kDudeModernCustom) { GENDUDEEXTRA* pExtra = &actor->genDudeExtra; if (nDmgType == kDamageBurn) { if (actor->xspr.health > (uint32_t)pDudeInfo->fleeHealth) return nDamage; else if (actor->xspr.txID <= 0 || getNextIncarnation(actor) == nullptr) { removeDudeStuff(actor); if (pExtra->weaponType == kGenDudeWeaponKamikaze) doExplosion(actor, actor->xspr.data1 - kTrapExploder); if (spriteIsUnderwater(actor)) { actor->xspr.health = 0; return nDamage; } if (actor->xspr.burnTime <= 0) actor->xspr.burnTime = 1200; if (pExtra->canBurn && pExtra->availDeaths[kDamageBurn] > 0) { aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); playGenDudeSound(actor,kGenDudeSndBurning); actor->spr.type = kDudeModernCustomBurning; if (actor->xspr.data2 == kGenDudeDefaultSeq) // don't inherit palette for burning if using default animation actor->spr.pal = 0; aiGenDudeNewState(actor, &genDudeBurnGoto); actHealDude(actor, dudeInfo[55].startHealth, dudeInfo[55].startHealth); actor->dudeExtra.time = PlayClock + 360; evKillActor(actor, kCallbackFXFlameLick); } } else { actKillDude(actor, actor, kDamageFall, 65535); } } else if (canWalk(actor) && !inDodge(actor->xspr.aiState) && !inRecoil(actor->xspr.aiState)) { if (!dudeIsMelee(actor)) { if (inIdle(actor->xspr.aiState) || Chance(getDodgeChance(actor))) { if (!spriteIsUnderwater(actor)) { if (!canDuck(actor) || !dudeIsPlayingSeq(actor, 14)) aiGenDudeNewState(actor, &genDudeDodgeShortL); else aiGenDudeNewState(actor, &genDudeDodgeShortD); if (Chance(0x0200)) playGenDudeSound(actor,kGenDudeSndGotHit); } else if (dudeIsPlayingSeq(actor, 13)) { aiGenDudeNewState(actor, &genDudeDodgeShortW); } } } else if (Chance(0x0200)) { playGenDudeSound(actor,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 && (actor->spr.inittype >= kDudeBase) && (actor->spr.inittype < kDudeMax) && (actor->spr.inittype != actor->spr.type) && !VanillaMode(); // fix burning cultists randomly switching types underwater switch (actor->spr.type) { case kDudeCultistTommy: case kDudeCultistShotgun: case kDudeCultistTesla: case kDudeCultistTNT: if (nDmgType != kDamageBurn) { if (!dudeIsPlayingSeq(actor, 14) && !actor->xspr.medium) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && !actor->xspr.medium) aiNewState(actor, &cultistProneDodge); else if (dudeIsPlayingSeq(actor, 13) && (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo)) aiNewState(actor, &cultistSwimDodge); } else if (nDmgType == kDamageBurn && actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth/* && (actor->xspr.at17_6 != 1 || actor->xspr.at17_6 != 2)*/) { actor->spr.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); evKillActor(actor, kCallbackFXFlameLick); } break; case kDudeInnocent: if (nDmgType == kDamageBurn && actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth/* && (actor->xspr.at17_6 != 1 || actor->xspr.at17_6 != 2)*/) { actor->spr.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); evKillActor(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) && (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo)) { actor->spr.type = kDudeCultistTommy; if (fixRandomCultist) // fix burning cultists randomly switching types underwater actor->spr.type = actor->spr.inittype; // restore back to spawned cultist type actor->xspr.burnTime = 0; aiNewState(actor, &cultistSwimGoto); } else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) { actor->spr.type = kDudeCultistShotgun; if (fixRandomCultist) // fix burning cultists randomly switching types underwater actor->spr.type = actor->spr.inittype; // restore back to spawned cultist type actor->xspr.burnTime = 0; aiNewState(actor, &cultistSwimGoto); } break; case kDudeGargoyleFlesh: aiNewState(actor, &gargoyleFChase); break; case kDudeZombieButcher: if (nDmgType == kDamageBurn && actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth) { aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); aiPlay3DSound(actor, 1202, AI_SFX_PRIORITY_2, -1); actor->spr.type = kDudeBurningZombieButcher; aiNewState(actor, &zombieFBurnGoto); actHealDude(actor, dudeInfo[42].startHealth, dudeInfo[42].startHealth); evKillActor(actor, kCallbackFXFlameLick); } break; case kDudeTinyCaleb: if (nDmgType == kDamageBurn && actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth/* && (actor->xspr.at17_6 != 1 || actor->xspr.at17_6 != 2)*/) { if (!cl_bloodvanillaenemies && !VanillaMode()) // fix burning sprite for tiny caleb { actor->spr.type = kDudeBurningTinyCaleb; aiNewState(actor, &tinycalebBurnGoto); } else { actor->spr.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); evKillActor(actor, kCallbackFXFlameLick); } break; case kDudeCultistBeast: if (actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth) { actor->spr.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 && actor->xspr.health <= (unsigned int)pDudeInfo->fleeHealth) { aiPlay3DSound(actor, 361, AI_SFX_PRIORITY_0, -1); aiPlay3DSound(actor, 1106, AI_SFX_PRIORITY_2, -1); actor->spr.type = kDudeBurningZombieAxe; aiNewState(actor, &zombieABurnGoto); actHealDude(actor, dudeInfo[41].startHealth, dudeInfo[41].startHealth); evKillActor(actor, kCallbackFXFlameLick); } break; } } return nDamage; } //--------------------------------------------------------------------------- // // todo: split up and put most of its content in tables. // //--------------------------------------------------------------------------- void RecoilDude(DBloodActor* actor) { uint8_t v4 = Chance(0x8000); DUDEEXTRA* pDudeExtra = &actor->dudeExtra; if (actor->spr.statnum == kStatDude && (actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax)) { DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); switch (actor->spr.type) { #ifdef NOONE_EXTENSIONS case kDudeModernCustom: { GENDUDEEXTRA* pExtra = &actor->genDudeExtra; int rChance = getRecoilChance(actor); if (pExtra->canElectrocute && pDudeExtra->teslaHit && !spriteIsUnderwater(actor, false)) { if (Chance(rChance << 3) || (dudeIsMelee(actor) && Chance(rChance << 4))) aiGenDudeNewState(actor, &genDudeRecoilTesla); else if (pExtra->canRecoil && Chance(rChance)) aiGenDudeNewState(actor, &genDudeRecoilL); else if (canWalk(actor)) { if (Chance(rChance >> 2)) aiGenDudeNewState(actor, &genDudeDodgeL); else if (Chance(rChance >> 1)) aiGenDudeNewState(actor, &genDudeDodgeShortL); } } else if (pExtra->canRecoil && Chance(rChance)) { if (inDuck(actor->xspr.aiState) && Chance(rChance >> 2)) aiGenDudeNewState(actor, &genDudeRecoilD); else if (spriteIsUnderwater(actor, false)) aiGenDudeNewState(actor, &genDudeRecoilW); else aiGenDudeNewState(actor, &genDudeRecoilL); } int rState = inRecoil(actor->xspr.aiState); if (rState > 0) { if (!canWalk(actor)) { if (rState == 1) actor->xspr.aiState->nextState = &genDudeChaseNoWalkL; else if (rState == 2) actor->xspr.aiState->nextState = &genDudeChaseNoWalkD; else actor->xspr.aiState->nextState = &genDudeChaseNoWalkW; } else if (!dudeIsMelee(actor) || Chance(rChance >> 2)) { if (rState == 1) actor->xspr.aiState->nextState = (Chance(rChance) ? &genDudeDodgeL : &genDudeDodgeShortL); else if (rState == 2) actor->xspr.aiState->nextState = (Chance(rChance) ? &genDudeDodgeD : &genDudeDodgeShortD); else if (rState == 3) actor->xspr.aiState->nextState = (Chance(rChance) ? &genDudeDodgeW : &genDudeDodgeShortW); } else if (rState == 1) actor->xspr.aiState->nextState = &genDudeChaseL; else if (rState == 2) actor->xspr.aiState->nextState = &genDudeChaseD; else actor->xspr.aiState->nextState = &genDudeChaseW; playGenDudeSound(actor,kGenDudeSndGotHit); } pDudeExtra->teslaHit = 0; break; } #endif case kDudeCultistTommy: case kDudeCultistShotgun: case kDudeCultistTesla: case kDudeCultistTNT: case kDudeCultistBeast: if (actor->spr.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 && actor->xspr.medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &cultistTeslaRecoil); else aiNewState(actor, &cultistRecoil); } else if (v4 && actor->xspr.medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &cultistTeslaRecoil); else if (gGameOptions.nDifficulty > 0) aiNewState(actor, &cultistProneRecoil); else aiNewState(actor, &cultistRecoil); } else if (actor->xspr.medium == kMediumWater || actor->xspr.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(actor, &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 && actor->xspr.data3 > pDudeInfo->startHealth / 3) aiNewState(actor, &zombieATeslaRecoil); else if (actor->xspr.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 && actor->xspr.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 = actor->sector()->hasX() ? &actor->sector()->xs() : nullptr; 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 (actor->xspr.medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &tinycalebTeslaRecoil); else aiNewState(actor, &tinycalebRecoil); } else if (actor->xspr.medium == kMediumWater || actor->xspr.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 (actor->xspr.medium == kMediumNormal) { if (pDudeExtra->teslaHit) aiNewState(actor, &beastTeslaRecoil); else aiNewState(actor, &beastRecoil); } else if (actor->xspr.medium == kMediumWater || actor->xspr.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) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); if (Chance(pDudeInfo->alertChance)) { for (int p = connecthead; p >= 0; p = connectpoint2[p]) { PLAYER* pPlayer = &gPlayer[p]; if (actor->GetOwner() == pPlayer->actor || pPlayer->actor->xspr.health == 0 || powerupCheck(pPlayer, kPwUpShadowCloak) > 0) continue; auto ppos = pPlayer->actor->spr.pos; auto dvec = ppos - actor->spr.pos.XY(); auto pSector = pPlayer->actor->sector(); int nDist = approxDist(dvec); if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist) continue; double height = (pDudeInfo->eyeHeight * actor->spr.yrepeat) * REPEAT_SCALE; if (!cansee(ppos, pSector, actor->spr.pos.plusZ(-height), actor->sector())) continue; int nDeltaAngle = getincangle(actor->int_ang(), getangle(dvec)); if (nDist < pDudeInfo->seeDist && abs(nDeltaAngle) <= pDudeInfo->periphery) { aiSetTarget(actor, pPlayer->actor); aiActivateDude(actor); return; } else if (nDist < pDudeInfo->hearDist) { aiSetTarget(actor, ppos); aiActivateDude(actor); return; } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiLookForTarget(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); if (Chance(pDudeInfo->alertChance)) { for (int p = connecthead; p >= 0; p = connectpoint2[p]) { PLAYER* pPlayer = &gPlayer[p]; if (actor->GetOwner() == pPlayer->actor || pPlayer->actor->xspr.health == 0 || powerupCheck(pPlayer, kPwUpShadowCloak) > 0) continue; auto ppos = pPlayer->actor->spr.pos; auto dvec = ppos - actor->spr.pos.XY(); auto pSector = pPlayer->actor->sector(); int nDist = approxDist(dvec); if (nDist > pDudeInfo->seeDist && nDist > pDudeInfo->hearDist) continue; double height = (pDudeInfo->eyeHeight * actor->spr.yrepeat) * REPEAT_SCALE; if (!cansee(ppos, pSector, actor->spr.pos.plusZ(-height), actor->sector())) continue; int nDeltaAngle = getincangle(actor->int_ang(), getangle(dvec)); if (nDist < pDudeInfo->seeDist && abs(nDeltaAngle) <= pDudeInfo->periphery) { aiSetTarget(actor, pPlayer->actor); aiActivateDude(actor); return; } else if (nDist < pDudeInfo->hearDist) { aiSetTarget(actor, ppos); aiActivateDude(actor); return; } } if (actor->xspr.state) { const bool newSectCheckMethod = !cl_bloodvanillaenemies && !VanillaMode(); // use new sector checking logic GetClosestSpriteSectors(actor->sector(), actor->spr.pos.XY(), 400, nullptr, newSectCheckMethod); BloodStatIterator it(kStatDude); while (DBloodActor* actor2 = it.Next()) { int nDist = approxDist(actor2->spr.pos.XY() - actor->spr.pos.XY()); if (actor2->spr.type == kDudeInnocent) { pDudeInfo = getDudeInfo(actor2->spr.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()) { if (actor->spr.flags & 32) continue; DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); if (actor->IsPlayerActor() || actor->xspr.health == 0) continue; actor->xspr.stateTimer = ClipLow(actor->xspr.stateTimer - 4, 0); if (actor->xspr.aiState) { if (actor->xspr.aiState->moveFunc) actor->xspr.aiState->moveFunc(actor); if (actor->xspr.aiState->thinkFunc && (gFrameCount & 3) == (actor->GetIndex() & 3)) actor->xspr.aiState->thinkFunc(actor); } switch (actor->spr.type) { #ifdef NOONE_EXTENSIONS case kDudeModernCustom: case kDudeModernCustomBurning: { GENDUDEEXTRA* pExtra = &actor->genDudeExtra; if (pExtra->slaveCount > 0) updateTargetOfSlaves(actor); if (pExtra->pLifeLeech != nullptr) updateTargetOfLeech(actor); if (actor->xspr.stateTimer == 0 && actor->xspr.aiState && actor->xspr.aiState->nextState && (actor->xspr.aiState->stateTicks > 0 || seqGetStatus(actor) < 0)) { aiGenDudeNewState(actor, actor->xspr.aiState->nextState); } int hinder = ((pExtra->isMelee) ? 25 : 5) << 4; if (actor->xspr.health <= 0 || hinder > actor->cumulDamage) break; actor->xspr.data3 = actor->cumulDamage; RecoilDude(actor); break; } #endif default: if (actor->xspr.stateTimer == 0 && actor->xspr.aiState && actor->xspr.aiState->nextState) { if (actor->xspr.aiState->stateTicks > 0) aiNewState(actor, actor->xspr.aiState->nextState); else if (seqGetStatus(actor) < 0) aiNewState(actor, actor->xspr.aiState->nextState); } if (actor->xspr.health > 0 && ((pDudeInfo->hinderDamage << 4) <= actor->cumulDamage)) { actor->xspr.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) { XSECTOR* pXSector = actor->sector()->hasX() ? &actor->sector()->xs() : nullptr; 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 unsigned int stateTimer = 0; DVector3 targetV(0,0,0); DBloodActor* pTargetMarker = nullptr; // dude patrol init if (gModernMap) { // must keep it in case of loading save if (actor->xspr.dudeFlag4 && actor->GetTarget() && actor->GetTarget()->spr.type == kMarkerPath) { stateTimer = actor->xspr.stateTimer; pTargetMarker = actor->GetTarget(); targetV = actor->xspr.TargetPos; } } #endif switch (actor->spr.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); actor->spr.flags &= ~1; break; } case kDudeZombieAxeBuried: { pDudeExtraE->thinkTime = 0; aiNewState(actor, &zombieEIdle); actor->spr.flags &= ~1; 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(actor); genDudePrepare(actor, kGenDudePropertyAll); break; #endif default: aiNewState(actor, &genIdle); break; } aiSetTarget(actor, 0, 0, 0); actor->xspr.stateTimer = 0; switch (actor->spr.type) { case kDudeSpiderBrown: case kDudeSpiderRed: case kDudeSpiderBlack: if (actor->spr.cstat & CSTAT_SPRITE_YFLIP) actor->spr.flags |= 9; else actor->spr.flags = 15; break; case kDudeGargoyleFlesh: case kDudeGargoyleStone: case kDudePhantasm: case kDudeBoneEel: case kDudeBat: actor->spr.flags |= 9; break; case kDudeGillBeast: if (pXSector && pXSector->Underwater) actor->spr.flags |= 9; else actor->spr.flags = 15; break; case kDudeZombieAxeBuried: case kDudeZombieAxeLaying: actor->spr.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 && (actor->spr.cstat & CSTAT_SPRITE_YFLIP)) { if (!(actor->spr.flags & kModernTypeFlag1)) // don't add autoaim for player if hitag 1 specified in editor. actor->spr.flags = kHitagAutoAim; break; } [[fallthrough]]; // go default #endif default: actor->spr.flags = 15; break; } #ifdef NOONE_EXTENSIONS if (gModernMap) { if (actor->xspr.dudeFlag4) { // restore dude's path if (pTargetMarker) { actor->SetTarget(pTargetMarker); actor->xspr.TargetPos = targetV; } // reset target spot progress actor->xspr.data3 = 0; // make dude follow the markers bool uwater = spriteIsUnderwater(actor); if (actor->GetTarget() == nullptr || actor->GetTarget()->spr.type != kMarkerPath) { actor->SetTarget(nullptr); aiPatrolSetMarker(actor); } if (stateTimer > 0) { if (uwater) aiPatrolState(actor, kAiStatePatrolWaitW); else if (actor->xspr.unused1 & kDudeFlagCrouch) aiPatrolState(actor, kAiStatePatrolWaitC); else aiPatrolState(actor, kAiStatePatrolWaitL); actor->xspr.stateTimer = stateTimer; // restore state timer } else if (uwater) aiPatrolState(actor, kAiStatePatrolMoveW); else if (actor->xspr.unused1 & kDudeFlagCrouch) aiPatrolState(actor, kAiStatePatrolMoveC); else aiPatrolState(actor, kAiStatePatrolMoveL); } } #endif } END_BLD_NS