//------------------------------------------------------------------------- /* 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 "blood.h" BEGIN_BLD_NS static void cultThinkSearch(DBloodActor*); static void cultThinkGoto(DBloodActor*); static void cultThinkChase(DBloodActor*); AISTATE cultistIdle = { kAiStateIdle, 0, -1, 0, NULL, NULL, aiThinkTarget, NULL }; AISTATE cultistProneIdle = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL }; AISTATE fanaticProneIdle = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL }; AISTATE cultistProneIdle3 = { kAiStateIdle, 17, -1, 0, NULL, NULL, aiThinkTarget, NULL }; AISTATE cultistChase = { kAiStateChase, 9, -1, 0, NULL, aiMoveForward, cultThinkChase, NULL }; AISTATE fanaticChase = { kAiStateChase, 0, -1, 0, NULL, aiMoveTurn, cultThinkChase, NULL }; AISTATE cultistDodge = { kAiStateMove, 9, -1, 90, NULL, aiMoveDodge, NULL, &cultistChase }; AISTATE cultistGoto = { kAiStateMove, 9, -1, 600, NULL, aiMoveForward, cultThinkGoto, &cultistIdle }; AISTATE cultistProneChase = { kAiStateChase, 14, -1, 0, NULL, aiMoveForward, cultThinkChase, NULL }; AISTATE cultistProneDodge = { kAiStateMove, 14, -1, 90, NULL, aiMoveDodge, NULL, &cultistProneChase }; AISTATE cultistTThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistTFire }; AISTATE cultistSThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistSFire }; AISTATE cultistTsThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistTsFire }; AISTATE cultistDThrow = { kAiStateChase, 7, nThrowClient, 120, NULL, NULL, NULL, &cultistChase }; AISTATE cultist139A78 = { kAiStateChase, 7, n68170Client, 120, NULL, NULL, NULL, &cultistChase }; AISTATE cultist139A94 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, NULL, &cultistIdle }; AISTATE cultist139AB0 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, cultThinkSearch, &cultist139A94 }; AISTATE cultist139ACC = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, cultThinkSearch, &cultist139AB0 }; AISTATE cultist139AE8 = { kAiStateChase, 7, n68230Client, 120, NULL, NULL, cultThinkSearch, &cultist139AE8 }; AISTATE cultistSearch = { kAiStateSearch, 9, -1, 1800, NULL, aiMoveForward, cultThinkSearch, &cultistIdle }; AISTATE cultistSFire = { kAiStateChase, 6, nShotClient, 60, NULL, NULL, NULL, &cultistChase }; AISTATE cultistTFire = { kAiStateChase, 6, nTommyClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistTFire }; AISTATE cultistTsFire = { kAiStateChase, 6, nTeslaClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistChase }; AISTATE cultistSProneFire = { kAiStateChase, 8, nShotClient, 60, NULL, NULL, NULL, &cultistProneChase }; AISTATE cultistTProneFire = { kAiStateChase, 8, nTommyClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistTProneFire }; AISTATE cultistTsProneFire = { kAiStateChase, 8, nTeslaClient, 0, NULL, aiMoveTurn, NULL, &cultistTsProneFire }; // vanilla, broken AISTATE cultistTsProneFireFixed = { kAiStateChase, 8, nTeslaClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistTsProneFireFixed }; AISTATE cultistRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistDodge }; AISTATE cultistProneRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistProneDodge }; AISTATE cultistTeslaRecoil = { kAiStateRecoil, 4, -1, 0, NULL, NULL, NULL, &cultistDodge }; AISTATE cultistSwimIdle = { kAiStateIdle, 13, -1, 0, NULL, NULL, aiThinkTarget, NULL }; AISTATE cultistSwimChase = { kAiStateChase, 13, -1, 0, NULL, aiMoveForward, cultThinkChase, NULL }; AISTATE cultistSwimDodge = { kAiStateMove, 13, -1, 90, NULL, aiMoveDodge, NULL, &cultistSwimChase }; AISTATE cultistSwimGoto = { kAiStateMove, 13, -1, 600, NULL, aiMoveForward, cultThinkGoto, &cultistSwimIdle }; AISTATE cultistSwimSearch = { kAiStateSearch, 13, -1, 1800, NULL, aiMoveForward, cultThinkSearch, &cultistSwimIdle }; AISTATE cultistSSwimFire = { kAiStateChase, 8, nShotClient, 60, NULL, NULL, NULL, &cultistSwimChase }; AISTATE cultistTSwimFire = { kAiStateChase, 8, nTommyClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistTSwimFire }; AISTATE cultistTsSwimFire = { kAiStateChase, 8, nTeslaClient, 0, NULL, aiMoveTurn, cultThinkChase, &cultistTsSwimFire }; AISTATE cultistSwimRecoil = { kAiStateRecoil, 5, -1, 0, NULL, NULL, NULL, &cultistSwimDodge }; void TommySeqCallback(int, DBloodActor* actor) { DVector3 vect(actor->spr.angle.ToVector(), actor->_dudeSlope / 16384); vect.X += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Y += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Z += Random3F((5 - gGameOptions.nDifficulty) * 500, 14); actFireVector(actor, 0, 0, vect, kVectorBullet); sfxPlay3DSound(actor, 4001, -1, 0); } void TeslaSeqCallback(int, DBloodActor* actor) { if (Chance(gCultTeslaFireChance[gGameOptions.nDifficulty])) { DVector3 vect(actor->spr.angle.ToVector(), actor->_dudeSlope / 16384); vect.X += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Y += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Z += Random3F((5 - gGameOptions.nDifficulty) * 500, 14); actFireMissile(actor, 0, 0, vect, kMissileTeslaRegular); sfxPlay3DSound(actor, 470, -1, 0); } } void ShotSeqCallback(int, DBloodActor* actor) { DVector3 vect(actor->spr.angle.ToVector(), actor->_dudeSlope / 16384); vect.X += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Y += Random3F((5 - gGameOptions.nDifficulty) * 1000, 14); vect.Z += Random3F((5 - gGameOptions.nDifficulty) * 500, 14); for (int i = 0; i < 8; i++) { double r1 = Random3F(500 , 14); double r2 = Random3F(1000, 14); double r3 = Random3F(1000, 14); actFireVector(actor, 0, 0, vect + DVector3(r1, r2, r3), kVectorShell); } if (Chance(0x8000)) sfxPlay3DSound(actor, 1001, -1, 0); else sfxPlay3DSound(actor, 1002, -1, 0); } void cultThrowSeqCallback(int, DBloodActor* actor) { int nMissile = kThingArmedTNTStick; if (gGameOptions.nDifficulty > 2) nMissile = kThingArmedTNTBundle; uint8_t v4 = Chance(0x6000); sfxPlay3DSound(actor, 455, -1, 0); if (!actor->ValidateTarget(__FUNCTION__)) return; auto target = actor->GetTarget(); assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); auto dv = target->spr.pos - actor->spr.pos; double nDist = dv.XY().Length(); if (nDist > 0x1e0) v4 = 0; auto* pMissile = actFireThing(actor, 0., 0., dv.Z / 32768 - FixedToFloat(14500), nMissile, nDist * (2048. / 64800)); if (v4) pMissile->xspr.Impact = 1; else evPostActor(pMissile, 120 * (1 + Random(2)), kCmdOn, actor); } void cultThrowSeqCallback2(int, DBloodActor* actor) { int nMissile = kThingArmedTNTStick; if (gGameOptions.nDifficulty > 2) nMissile = kThingArmedTNTBundle; sfxPlay3DSound(actor, 455, -1, 0); auto pMissile = actFireThing(actor, 0, 0, actor->_dudeSlope - 9460, nMissile, 0x133333); evPostActor(pMissile, 120 * (2 + Random(2)), kCmdOn, actor); } void cultThrowSeqCallback3(int, DBloodActor* actor) { int nMissile = kThingArmedTNTStick; if (gGameOptions.nDifficulty > 2) nMissile = kThingArmedTNTBundle; sfxPlay3DSound(actor, 455, -1, 0); if (!actor->ValidateTarget(__FUNCTION__)) return; auto target = actor->GetTarget(); assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); auto dv = target->spr.pos - actor->spr.pos; double nDist = dv.XY().Length(); auto* pMissile = actFireThing(actor, 0., 0., dv.Z / 32768 - FixedToFloat(14500), nMissile, nDist * (32. / 64800)); pMissile->xspr.Impact = 1; } static bool TargetNearExplosion(sectortype* sector) { BloodSectIterator it(sector); while (auto actor = it.Next()) { if (actor->spr.type == kThingArmedTNTStick || actor->spr.statnum == kStatExplosion) return true; } return false; } static void cultThinkSearch(DBloodActor* actor) { aiChooseDirection(actor, actor->xspr.goalAng); aiLookForTarget(actor); } static void cultThinkGoto(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); auto dvec = actor->xspr.TargetPos.XY() - actor->spr.pos.XY(); DAngle nAngle = VecToAngle(dvec); double nDist = dvec.Length(); aiChooseDirection(actor, nAngle); if (nDist < 320 && absangle(actor->spr.angle, nAngle) < pDudeInfo->Periphery()) { switch (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } } aiThinkTarget(actor); } static void cultThinkChase(DBloodActor* actor) { if (actor->GetTarget() == nullptr) { switch (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistGoto); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimGoto); break; } return; } assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); auto target = actor->GetTarget(); auto dvec = target->spr.pos - actor->spr.pos; double nDist = dvec.XY().Length(); DAngle nAngle = VecToAngle(dvec); aiChooseDirection(actor, nAngle); if (target->xspr.health == 0) { switch (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); if (actor->spr.type == kDudeCultistTommy) aiPlay3DSound(actor, 4021 + Random(4), AI_SFX_PRIORITY_1, -1); else aiPlay3DSound(actor, 1021 + Random(4), AI_SFX_PRIORITY_1, -1); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } return; } if (target->IsPlayerActor() && powerupCheck(&gPlayer[target->spr.type - kDudePlayer1], kPwUpShadowCloak) > 0) { switch (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistSearch); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimSearch); break; } return; } if (nDist > 0 && nDist <= pDudeInfo->SeeDist()) { DAngle nDeltaAngle = absangle(actor->spr.angle, nAngle); double height = (pDudeInfo->eyeHeight * actor->spr.yrepeat) * REPEAT_SCALE; if (cansee(target->spr.pos, target->sector(), actor->spr.pos.plusZ(-height), actor->sector())) { if (nDist < pDudeInfo->SeeDist() && nDeltaAngle <= pDudeInfo->Periphery()) { aiSetTarget(actor, actor->GetTarget()); actor->_dudeSlope = nDist == 0 ? 0 : (target->spr.pos.Z - actor->spr.pos.Z) / nDist * 16384; switch (actor->spr.type) { case kDudeCultistTommy: if (nDist < 0x1e0 && nDist > 0xe0 && nDeltaAngle < DAngle15 && !TargetNearExplosion(target->sector()) && (target->spr.flags & 2) && gGameOptions.nDifficulty > 2 && target->IsPlayerActor() && gPlayer[target->spr.type - kDudePlayer1].isRunning && Chance(0x8000)) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistTThrow); break; case 0: case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistTThrow); break; default: aiNewState(actor, &cultistTThrow); break; } } else if (nDist < 0x460 && nDeltaAngle < DAngle1 * 5) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTProneFire); else if (dudeIsPlayingSeq(actor, 13) && (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo)) aiNewState(actor, &cultistTSwimFire); break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun) { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistTSwimFire); } else { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistProneDodge); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSwimDodge); } break; default: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistTSwimFire); break; } } break; case kDudeCultistShotgun: if (nDist < 0x2c0 && nDist > 0x140 && !TargetNearExplosion(target->sector()) && (target->spr.flags & 2) && gGameOptions.nDifficulty >= 2 && target->IsPlayerActor() && !gPlayer[target->spr.type - kDudePlayer1].isRunning && Chance(0x8000)) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistSThrow); break; case 0: case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistSThrow); break; default: aiNewState(actor, &cultistSThrow); break; } } else if (nDist < 0x320 && nDeltaAngle < DAngle1 * 5) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistTommy) { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); } else { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistProneDodge); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSwimDodge); } break; default: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); break; } } break; case kDudeCultistTesla: if (nDist < 0x1e0 && nDist > 0xe0 && !TargetNearExplosion(target->sector()) && (target->spr.flags & 2) && gGameOptions.nDifficulty > 2 && target->IsPlayerActor() && gPlayer[target->spr.type - kDudePlayer1].isRunning && Chance(0x8000)) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistTsThrow); break; case 0: case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistTsThrow); break; default: aiNewState(actor, &cultistTsThrow); break; } } else if (nDist < 0x320 && nDeltaAngle < DAngle1 * 5) { AISTATE *pCultistTsProneFire = !cl_bloodvanillaenemies && !VanillaMode() ? &cultistTsProneFireFixed : &cultistTsProneFire; int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTsFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, pCultistTsProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistTsSwimFire); break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistTommy) { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTsFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, pCultistTsProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistTsSwimFire); } else { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistProneDodge); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSwimDodge); } break; default: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistTsFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, pCultistTsProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistTsSwimFire); break; } } break; case kDudeCultistTNT: if (nDist < 0x2c0 && nDist > 0x140 && nDeltaAngle < DAngle15 && (target->spr.flags & 2) && target->IsPlayerActor()) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistDThrow); break; case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistDThrow); break; default: aiNewState(actor, &cultistDThrow); break; } } else if (nDist < 0x140 && nDeltaAngle < DAngle15 && (target->spr.flags & 2) && target->IsPlayerActor()) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != 1 && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultist139A78); break; case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultist139A78); break; default: aiNewState(actor, &cultist139A78); break; } } break; case kDudeCultistBeast: if (nDist < 0x1e0 && nDist > 0xe0 && !TargetNearExplosion(target->sector()) && (target->spr.flags & 2) && gGameOptions.nDifficulty > 2 && target->IsPlayerActor() && gPlayer[target->spr.type - kDudePlayer1].isRunning && Chance(0x8000)) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistSThrow); break; case 0: case 4: break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistShotgun && actor->xspr.medium != kMediumWater && actor->xspr.medium != kMediumGoo) aiNewState(actor, &cultistSThrow); break; default: aiNewState(actor, &cultistSThrow); break; } } else if (nDist < 0x320 && abs(nDeltaAngle) < DAngle1 * 5) { int hit = HitScan(actor, actor->spr.pos.Z, DVector3(dvec, 0), CLIPMASK1, 0); switch (hit) { case -1: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); break; case 3: if (actor->spr.type != gHitInfo.actor()->spr.type && gHitInfo.actor()->spr.type != kDudeCultistTommy) { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); } else { if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistDodge); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistProneDodge); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSwimDodge); } break; default: if (!dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSFire); else if (dudeIsPlayingSeq(actor, 14) && actor->xspr.medium == kMediumNormal) aiNewState(actor, &cultistSProneFire); else if (actor->xspr.medium == kMediumWater || actor->xspr.medium == kMediumGoo) aiNewState(actor, &cultistSSwimFire); break; } } break; } return; } } } switch (actor->xspr.medium) { case kMediumNormal: aiNewState(actor, &cultistGoto); break; case kMediumWater: case kMediumGoo: aiNewState(actor, &cultistSwimGoto); break; } actor->SetTarget(nullptr); } END_BLD_NS