//------------------------------------------------------------------------- /* Copyright (C) 2010-2019 EDuke32 developers and contributors Copyright (C) 2019 Nuke.YKT Copyright (C) NoOne 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. */ //------------------------------------------------------------------------- /////////////////////////////////////////////////////////////////// // This file provides modern features for mappers. // For full documentation please visit http://cruo.bloodgame.ru/xxsystem /////////////////////////////////////////////////////////////////// #include "ns.h" #ifdef NOONE_EXTENSIONS #include #include "blood.h" #include "savegamehelp.h" BEGIN_BLD_NS bool gAllowTrueRandom = false; bool gEventRedirectsUsed = false; CVARD(Bool, nnext_showconditionsprites, false, 0, "makes kModernCondition sprites visable") short gEffectGenCallbacks[] = { kCallbackFXFlameLick, kCallbackFXFlareSpark, kCallbackFXFlareSparkLite, kCallbackFXZombieSpurt, kCallbackFXBloodSpurt, kCallbackFXArcSpark, kCallbackFXTeslaAlt, }; TRPLAYERCTRL gPlayerCtrl[kMaxPlayers]; TArray gConditions; std::default_random_engine gStdRandom; const VECTORINFO_EXTRA gVectorInfoExtra[] = { 1207,1207, 1001,1001, 4001,4002, 431,431, 1002,1002, 359,359, 521,521, 513,513, 499,499, 9012,9014, 1101,1101, 1207,1207, 499,495, 495,496, 9013,499, 1307,1308, 499,499, 499,499, 499,499, 499,499, 351,351, 0,0, 357,499 }; const MISSILEINFO_EXTRA gMissileInfoExtra[] = { 1207, 1207, false, false, false, false, false, true, false, true, 420, 420, false, true, true, false, false, false, false, true, 471, 471, false, false, false, false, false, false, true, false, 421, 421, false, true, false, true, false, false, false, false, 1309, 351, false, true, false, false, false, false, false, true, 480, 480, false, true, false, true, false, false, false, false, 470, 470, false, false, false, false, false, false, true, true, 489, 490, false, false, false, false, false, true, false, true, 462, 351, false, true, false, false, false, false, false, true, 1203, 172, false, false, true, false, false, false, false, true, 0,0, false, false, true, false, false, false, false, true, 1457, 249, false, false, false, false, false, true, false, true, 480, 489, false, true, false, true, false, false, false, false, 480, 489, false, false, false, true, false, false, false, false, 480, 489, false, false, false, true, false, false, false, false, 491, 491, true, true, true, true, true, true, true, true, 520, 520, false, false, false, false, false, true, false, true, 520, 520, false, false, false, false, false, true, false, true, }; const THINGINFO_EXTRA gThingInfoExtra[] = { true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, true, false, false, true, true, true, true, false, false, false, false, false, true, true, true, true, true, true, true, true, true, }; const DUDEINFO_EXTRA gDudeInfoExtra[] = { { false, false, -1, -1, -1, -1, -1, -1 }, // 200 { false, false, 0, 9, 13, 13, 17, 14 }, // 201 { false, false, 0, 9, 13, 13, 17, 14 }, // 202 { false, true, 0, 8, 0, 8, -1, -1 }, // 203 { false, false, 0, 8, 0, 8, -1, -1 }, // 204 { false, true, 1, -1, -1, -1, -1, -1 }, // 205 { true, true, 0, 0, 0, 0, -1, -1 }, // 206 { true, false, 0, 0, 0, 0, -1, -1 }, // 207 { true, false, 1, -1, -1, -1, -1, -1 }, // 208 { true, false, 1, -1, -1, -1, -1, -1 }, // 209 { true, true, 0, 0, 0, 0, -1, -1 }, // 210 { false, true, 0, 8, 0, 8, -1, -1 }, // 211 { false, true, 0, 6, 0, 6, -1, -1 }, // 212 { false, true, 0, 7, 0, 7, -1, -1 }, // 213 { false, true, 0, 7, 0, 7, -1, -1 }, // 214 { false, true, 0, 7, 0, 7, -1, -1 }, // 215 { false, true, 0, 7, 0, 7, -1, -1 }, // 216 { false, true, 0, 9, 10, 10, -1, -1 }, // 217 { false, true, 0, 0, 0, 0, -1, -1 }, // 218 { true, false, 7, 7, 7, 7, -1, -1 }, // 219 { false, true, 0, 7, 0, 7, -1, -1 }, // 220 { false, false, -1, -1, -1, -1, -1, -1 }, // 221 { false, true, -1, -1, -1, -1, -1, -1 }, // 222 { false, false, -1, -1, -1, -1, -1, -1 }, // 223 { false, true, -1, -1, -1, -1, -1, -1 }, // 224 { false, false, -1, -1, -1, -1, -1, -1 }, // 225 { false, false, -1, -1, -1, -1, -1, -1 }, // 226 { false, false, 0, 7, 0, 7, -1, -1 }, // 227 { false, false, 0, 7, 0, 7, -1, -1 }, // 228 { false, false, 0, 8, 0, 8, -1, -1 }, // 229 { false, false, 0, 9, 13, 13, 17, 14 }, // 230 { false, false, -1, -1, -1, -1, -1, -1 }, // 231 { false, false, -1, -1, -1, -1, -1, -1 }, // 232 { false, false, -1, -1, -1, -1, -1, -1 }, // 233 { false, false, -1, -1, -1, -1, -1, -1 }, // 234 { false, false, -1, -1, -1, -1, -1, -1 }, // 235 { false, false, -1, -1, -1, -1, -1, -1 }, // 236 { false, false, -1, -1, -1, -1, -1, -1 }, // 237 { false, false, -1, -1, -1, -1, -1, -1 }, // 238 { false, false, -1, -1, -1, -1, -1, -1 }, // 239 { false, false, -1, -1, -1, -1, -1, -1 }, // 240 { false, false, -1, -1, -1, -1, -1, -1 }, // 241 { false, false, -1, -1, -1, -1, -1, -1 }, // 242 { false, false, -1, -1, -1, -1, -1, -1 }, // 243 { false, true, -1, -1, -1, -1, -1, -1 }, // 244 { false, true, 0, 6, 0, 6, -1, -1 }, // 245 { false, false, 0, 9, 13, 13, 17, 14 }, // 246 { false, false, 0, 9, 13, 13, 14, 14 }, // 247 { false, false, 0, 9, 13, 13, 14, 14 }, // 248 { false, false, 0, 9, 13, 13, 17, 14 }, // 249 { false, true, 0, 6, 8, 8, 10, 9 }, // 250 { false, true, 0, 8, 9, 9, 11, 10 }, // 251 { false, false, -1, -1, -1, -1, -1, -1 }, // 252 { false, false, -1, -1, -1, -1, -1, -1 }, // 253 { false, false, 0, 9, 17, 13, 17, 14 }, // 254 { false, false, -1, -1, -1, -1, -1, -1 }, // 255 }; AISTATE genPatrolStates[] = { //------------------------------------------------------------------------------- { kAiStatePatrolWaitL, 0, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitL, 7, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolMoveL, 9, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveL, 8, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveL, 0, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveL, 6, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveL, 7, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolTurnL, 9, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnL, 8, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnL, 0, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnL, 6, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnL, 7, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, //------------------------------------------------------------------------------- { kAiStatePatrolWaitW, 0, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitW, 10, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitW, 13, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitW, 17, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitW, 8, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitW, 9, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 0, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 10, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 13, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 8, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 9, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 7, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveW, 6, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 0, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 10, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 13, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 8, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 9, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 7, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnW, 6, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, //------------------------------------------------------------------------------- { kAiStatePatrolWaitC, 17, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitC, 11, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitC, 10, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolWaitC, 14, -1, 0, NULL, NULL, aiPatrolThink, NULL }, { kAiStatePatrolMoveC, 14, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveC, 10, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolMoveC, 9, -1, 0, NULL, aiPatrolMove, aiPatrolThink, NULL }, { kAiStatePatrolTurnC, 14, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnC, 10, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, { kAiStatePatrolTurnC, 9, -1, 0, aiPatrolRandGoalAng, aiPatrolTurn, aiPatrolThink, NULL }, //------------------------------------------------------------------------------- }; CONDITION_TYPE_NAMES gCondTypeNames[7] = { {kCondGameBase, kCondGameMax, "Game"}, {kCondMixedBase, kCondMixedMax, "Mixed"}, {kCondWallBase, kCondWallMax, "Wall"}, {kCondSectorBase, kCondSectorMax, "Sector"}, {kCondPlayerBase, kCondPlayerMax, "Player"}, {kCondDudeBase, kCondDudeMax, "Enemy"}, {kCondSpriteBase, kCondSpriteMax, "Sprite"}, }; // for actor.cpp //------------------------------------------------------------------------- //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static DBloodActor* nnExtSpawnDude(DBloodActor* sourceactor, DBloodActor* origin, int nType, double dist, double zadd) { DBloodActor* pDudeActor = nullptr; if (nType < kDudeBase || nType >= kDudeMax || (pDudeActor = actSpawnSprite(origin, kStatDude)) == NULL) return NULL; DAngle angle = origin->spr.Angles.Yaw; auto pos = origin->spr.pos.plusZ(zadd); if (dist >= 0) { pos += angle.ToVector() * dist; } SetActor(pDudeActor, pos); pDudeActor->spr.type = nType; pDudeActor->spr.Angles.Yaw = angle; pDudeActor->spr.cstat |= CSTAT_SPRITE_BLOOD_BIT1 | CSTAT_SPRITE_BLOCK_ALL; pDudeActor->clipdist = getDudeInfo(nType)->fClipdist(); pDudeActor->xspr.respawn = 1; pDudeActor->xspr.health = getDudeInfo(nType)->startHealth << 4; if (fileSystem.FindResource(getDudeInfo(nType)->seqStartID, "SEQ")) seqSpawn(getDudeInfo(nType)->seqStartID, pDudeActor, -1); // add a way to inherit some values of spawner by dude. if (sourceactor->spr.flags & kModernTypeFlag1) { //inherit pal? if (pDudeActor->spr.pal <= 0) pDudeActor->spr.pal = sourceactor->spr.pal; // inherit spawn sprite trigger settings, so designer can count monsters. pDudeActor->xspr.txID = sourceactor->xspr.txID; pDudeActor->xspr.command = sourceactor->xspr.command; pDudeActor->xspr.triggerOn = sourceactor->xspr.triggerOn; pDudeActor->xspr.triggerOff = sourceactor->xspr.triggerOff; // inherit drop items pDudeActor->xspr.dropMsg = sourceactor->xspr.dropMsg; // inherit dude flags pDudeActor->xspr.dudeDeaf = sourceactor->xspr.dudeDeaf; pDudeActor->xspr.dudeGuard = sourceactor->xspr.dudeGuard; pDudeActor->xspr.dudeAmbush = sourceactor->xspr.dudeAmbush; pDudeActor->xspr.dudeFlag4 = sourceactor->xspr.dudeFlag4; pDudeActor->xspr.unused1 = sourceactor->xspr.unused1; } aiInitSprite(pDudeActor); if (AllowedKillType(pDudeActor)) Level.addKillCount(); bool burning = IsBurningDude(pDudeActor); if (burning) { pDudeActor->xspr.burnTime = 10; pDudeActor->SetTarget(nullptr); } if ((burning || (sourceactor->spr.flags & kModernTypeFlag3)) && !pDudeActor->xspr.dudeFlag4) aiActivateDude(pDudeActor); return pDudeActor; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool nnExtIsImmune(DBloodActor* actor, int dmgType, int minScale) { if (dmgType >= kDmgFall && dmgType < kDmgMax && actor->hasX() && actor->xspr.locked != 1) { if (actor->spr.type >= kThingBase && actor->spr.type < kThingMax) { return (thingInfo[actor->spr.type - kThingBase].dmgControl[dmgType] <= minScale); } else if (actor->IsDudeActor()) { if (actor->IsPlayerActor()) return (getPlayer(actor->spr.type - kDudePlayer1)->damageControl[dmgType]); else if (actor->spr.type == kDudeModernCustom) return (actor->genDudeExtra.dmgControl[dmgType] <= minScale); else return (getDudeInfo(actor->spr.type)->damageVal[dmgType] <= minScale); } } return true; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool nnExtEraseModernStuff(DBloodActor* actor) { bool erased = false; switch (actor->spr.type) { // erase all modern types if the map is not extended case kModernSpriteDamager: case kModernCustomDudeSpawn: case kModernRandomTX: case kModernSequentialTX: case kModernSeqSpawner: case kModernObjPropertiesChanger: case kModernObjPicnumChanger: case kModernObjSizeChanger: case kModernDudeTargetChanger: case kModernSectorFXChanger: case kModernObjDataChanger: case kModernObjDataAccumulator: case kModernEffectSpawner: case kModernWindGenerator: case kModernPlayerControl: case kModernCondition: case kModernConditionFalse: case kModernSlopeChanger: case kModernStealthRegion: actor->spr.type = kSpriteDecoration; erased = true; break; case kItemModernMapLevel: case kDudeModernCustom: case kDudeModernCustomBurning: case kModernThingTNTProx: case kModernThingEnemyLifeLeech: actor->spr.type = kSpriteDecoration; ChangeActorStat(actor, kStatDecoration); erased = true; break; // also erase some modernized vanilla types which was not active case kMarkerWarpDest: if (actor->spr.statnum == kStatMarker) break; actor->spr.type = kSpriteDecoration; erased = true; break; } if (actor->xspr.Sight) { actor->xspr.Sight = false; // it does not work in vanilla at all erased = true; } if (actor->xspr.Proximity) { // proximity works only for things and dudes in vanilla switch (actor->spr.statnum) { case kStatThing: case kStatDude: break; default: actor->xspr.Proximity = false; erased = true; break; } } return erased; } //--------------------------------------------------------------------------- // // //--------------------------------------------------------------------------- void nnExtTriggerObject(EventObject& eob, int command, DBloodActor* initiator) { if (eob.isSector()) { trTriggerSector(eob.sector(), command, initiator); } else if (eob.isWall()) { trTriggerWall(eob.wall(), command, initiator); } else if (eob.isActor()) { auto objActor = eob.actor(); if (!objActor || !objActor->hasX()) return; trTriggerSprite(objActor, command, initiator); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void nnExtResetGlobals() { gAllowTrueRandom = gEventRedirectsUsed = false; // reset counters gProxySpritesCount = gSightSpritesCount = gPhysSpritesCount = gImpactSpritesCount = 0; // fill arrays with negative values to avoid index 0 situation memset(gSightSpritesList, 0, sizeof(gSightSpritesList)); memset(gProxySpritesList, 0, sizeof(gProxySpritesList)); memset(gPhysSpritesList, 0, sizeof(gPhysSpritesList)); memset(gImpactSpritesList, 0, sizeof(gImpactSpritesList)); gConditions.Clear(); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- CCMD(nnext_ifshow) { if (!isBlood()) return; BloodSpriteIterator it; int cnt = 0; while (auto actor = it.Next()) { if (actor->spr.type == kModernCondition || actor->spr.type == kModernConditionFalse) { if (actor->spr.cstat & CSTAT_SPRITE_INVISIBLE) { actor->spr.cstat &= ~CSTAT_SPRITE_INVISIBLE; cnt++; } } } if (cnt <= 0) Printf("No condition sprites found!\n"); Printf("%d sprites are visible now.\n", cnt); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void nnExtInitModernStuff(TArray& actors) { nnExtResetGlobals(); // use true random only for single player mode, otherwise use Blood's default one. if (gGameOptions.nGameType == 0 && !VanillaMode()) { gStdRandom.seed(std::random_device()()); // since true random is not working if compiled with old mingw versions, we should // check if it works in game and if not - switch to using in-game random function. for (int i = kMaxRandomizeRetries; i >= 0; i--) { std::uniform_int_distribution dist_a_b(0, 100); if (gAllowTrueRandom || i <= 0) break; else if (dist_a_b(gStdRandom) != 0) gAllowTrueRandom = true; } } for (auto actor : actors) { if (!actor->exists() || !actor->hasX()) continue; switch (actor->spr.type) { case kModernRandomTX: case kModernSequentialTX: if (actor->xspr.command == kCmdLink) gEventRedirectsUsed = true; break; case kDudeModernCustom: case kDudeModernCustomBurning: getSpriteMassBySize(actor); // create mass cache break; case kModernCondition: case kModernConditionFalse: if (!actor->xspr.rxID && actor->xspr.data1 > kCondGameMax) condError(actor, "\nThe condition must have RX ID!\nSPRITE #%d", actor->GetIndex()); else if (!actor->xspr.txID && !actor->spr.flags) { Printf(PRINT_HIGH, "The condition must have TX ID or hitag to be set: RX ID %d, SPRITE #%d", actor->xspr.rxID, actor->GetIndex()); } break; } // auto set going On and going Off if both are empty if (actor->xspr.txID && !actor->xspr.triggerOn && !actor->xspr.triggerOff) actor->xspr.triggerOn = actor->xspr.triggerOff = true; // copy custom start health to avoid overwrite by kThingBloodChunks if (actor->IsDudeActor()) actor->xspr.sysData2 = actor->xspr.data4; // check reserved statnums if (actor->spr.statnum >= kStatModernBase && actor->spr.statnum < kStatModernMax) { bool sysStat = true; switch (actor->spr.statnum) { case kStatModernStealthRegion: sysStat = (actor->spr.type != kModernStealthRegion); break; case kStatModernDudeTargetChanger: sysStat = (actor->spr.type != kModernDudeTargetChanger); break; case kStatModernCondition: sysStat = (actor->spr.type != kModernCondition && actor->spr.type != kModernConditionFalse); break; case kStatModernEventRedirector: sysStat = (actor->spr.type != kModernRandomTX && actor->spr.type != kModernSequentialTX); break; case kStatModernWindGen: sysStat = (actor->spr.type != kModernWindGenerator); break; case kStatModernPlayerLinker: case kStatModernQavScene: sysStat = (actor->spr.type != kModernPlayerControl); break; } if (sysStat) I_Error("Sprite statnum %d on sprite #%d is in a range of reserved (%d - %d)!", actor->spr.statnum, actor->GetIndex(), kStatModernBase, kStatModernMax); } switch (actor->spr.type) { case kModernRandomTX: case kModernSequentialTX: if (actor->xspr.command != kCmdLink) break; // add statnum for faster redirects search ChangeActorStat(actor, kStatModernEventRedirector); break; case kModernWindGenerator: actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK; ChangeActorStat(actor, kStatModernWindGen); break; case kModernDudeTargetChanger: case kModernObjDataAccumulator: case kModernRandom: case kModernRandom2: case kModernStealthRegion: actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK; actor->spr.cstat |= CSTAT_SPRITE_INVISIBLE; switch (actor->spr.type) { // stealth regions for patrolling enemies case kModernStealthRegion: ChangeActorStat(actor, kStatModernStealthRegion); break; // add statnum for faster dude searching case kModernDudeTargetChanger: ChangeActorStat(actor, kStatModernDudeTargetChanger); if (actor->xspr.busyTime <= 0) actor->xspr.busyTime = 5; actor->xspr.command = kCmdLink; break; // remove kStatItem status from random item generators case kModernRandom: case kModernRandom2: ChangeActorStat(actor, kStatDecoration); actor->xspr.sysData1 = actor->xspr.command; // save the command so spawned item can inherit it actor->xspr.command = kCmdLink; // generator itself can't send commands break; } break; case kModernThingTNTProx: actor->xspr.Proximity = true; break; case kDudeModernCustom: { if (actor->xspr.txID <= 0) break; int found = 0; BloodStatIterator it(kStatDude); while (DBloodActor* iactor = it.Next()) { if (iactor->xspr.rxID != actor->xspr.txID) continue; else if (found) I_Error("\nCustom dude (TX ID %d):\nOnly one incarnation allowed per channel!", actor->xspr.txID); ChangeActorStat(iactor, kStatInactive); found++; } break; } case kDudePodMother: case kDudeTentacleMother: actor->xspr.state = 1; break; case kModernPlayerControl: switch (actor->xspr.command) { case kCmdLink: { if (actor->xspr.data1 < 0 || actor->xspr.data1 > kMaxPlayers) I_Error("\nPlayer Control (SPRITE #%d):\nPlayer out of a range (data1 = %d)", actor->GetIndex(), actor->xspr.data1); //if (numplayers < actor->xspr.data1) //I_Error("\nPlayer Control (SPRITE #%d):\n There is no player #%d", actor->GetIndex(), actor->xspr.data1); if (actor->xspr.rxID && actor->xspr.rxID != kChannelLevelStart) I_Error("\nPlayer Control (SPRITE #%d) with Link command should have no RX ID!", actor->GetIndex()); if (actor->xspr.txID && actor->xspr.txID < kChannelUser) I_Error("\nPlayer Control (SPRITE #%d):\nTX ID should be in range of %d and %d!", actor->GetIndex(), kChannelUser, kChannelMax); // only one linker per player allowed BloodStatIterator it(kStatModernPlayerLinker); while (auto iactor = it.Next()) { if (actor != iactor && actor->xspr.data1 == iactor->xspr.data1) I_Error("\nPlayer Control (SPRITE #%d):\nPlayer %d already linked with different player control sprite #%d!", actor->GetIndex(), actor->xspr.data1, iactor->GetIndex()); } actor->xspr.sysData1 = -1; actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK; ChangeActorStat(actor, kStatModernPlayerLinker); break; } case 67: // play qav animation if (actor->xspr.txID >= kChannelUser && !actor->xspr.waitTime) actor->xspr.waitTime = 1; ChangeActorStat(actor, kStatModernQavScene); break; } break; case kModernCondition: case kModernConditionFalse: if (actor->xspr.busyTime > 0) { if (actor->xspr.waitTime > 0) { actor->xspr.busyTime = ClipHigh(actor->xspr.busyTime + ((actor->xspr.waitTime * 120) / 10), 4095); actor->xspr.waitTime = 0; Printf(PRINT_HIGH, "Summing busyTime and waitTime for tracking condition #%d, RX ID %d. Result = %d ticks", actor->GetIndex(), actor->xspr.rxID, actor->xspr.busyTime); } actor->xspr.busy = actor->xspr.busyTime; } if (actor->xspr.waitTime && actor->xspr.command >= kCmdNumberic) condError(actor, "Delay is not available when using numberic commands (%d - %d)", kCmdNumberic, 255); actor->xspr.Decoupled = false; // must go through operateSprite always actor->xspr.Sight = actor->xspr.Impact = actor->xspr.Touch = false; actor->xspr.Proximity = actor->xspr.Push = actor->xspr.Vector = false; actor->xspr.state = actor->xspr.restState = 0; if (gModernMap == 2 && actor->xspr.triggerOn && !actor->xspr.triggerOff) actor->spr.flags |= kModernTypeFlag64; else actor->spr.flags &= ~kModernTypeFlag64; actor->xspr.triggerOn = actor->xspr.triggerOff = false; actor->xspr.TargetPos = {-1, -1, -1}; actor->xspr.sysData2 = -1; actor->SetTarget(nullptr); ChangeActorStat(actor, kStatModernCondition); auto oldStat = actor->spr.cstat; actor->spr.cstat = 0; if (oldStat & CSTAT_SPRITE_BLOCK) actor->spr.cstat |= CSTAT_SPRITE_BLOCK; if (oldStat & CSTAT_SPRITE_MOVE_FORWARD) actor->spr.cstat |= CSTAT_SPRITE_MOVE_FORWARD; else if (oldStat & CSTAT_SPRITE_MOVE_REVERSE) actor->spr.cstat |= CSTAT_SPRITE_MOVE_REVERSE; if (!nnext_showconditionsprites) actor->spr.cstat |= CSTAT_SPRITE_INVISIBLE; break; } // the following trigger flags are senseless to have together if ((actor->xspr.Touch && (actor->xspr.Proximity || actor->xspr.Sight) && actor->xspr.DudeLockout) || (actor->xspr.Touch && actor->xspr.Proximity && !actor->xspr.Sight)) actor->xspr.Touch = false; if (actor->xspr.Proximity && actor->xspr.Sight && actor->xspr.DudeLockout) actor->xspr.Proximity = false; // very quick fix for floor sprites with Touch trigger flag if their Z is equals sector florz / ceilgz if (actor->insector() && actor->xspr.Touch && (actor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)) { if (actor->spr.pos.Z == actor->sector()->floorz) actor->spr.pos.Z -= zmaptoworld; else if (actor->spr.pos.Z == actor->sector()->ceilingz) actor->spr.pos.Z += zmaptoworld; } // make Proximity flag work not just for dudes and things... if (actor->xspr.Proximity && gProxySpritesCount < kMaxSuperXSprites) { switch (actor->spr.statnum) { case kStatFX: case kStatExplosion: case kStatItem: case kStatPurge: case kStatSpares: case kStatFlare: case kStatInactive: case kStatFree: case kStatMarker: case kStatThing: case kStatDude: case kStatModernPlayerLinker: break; default: gProxySpritesList[gProxySpritesCount++] = actor; if (gProxySpritesCount == kMaxSuperXSprites) I_Error("Max (%d) *additional* Proximity sprites reached!", kMaxSuperXSprites); break; } } // make Sight, Screen, Aim flags work not just for dudes and things... if ((actor->xspr.Sight || actor->xspr.unused3) && gSightSpritesCount < kMaxSuperXSprites) { switch (actor->spr.statnum) { case kStatFX: case kStatExplosion: case kStatItem: case kStatPurge: case kStatSpares: case kStatFlare: case kStatInactive: case kStatFree: case kStatMarker: case kStatModernPlayerLinker: break; default: gSightSpritesList[gSightSpritesCount++] = actor; if (gSightSpritesCount == kMaxSuperXSprites) I_Error("Max (%d) Sight sprites reached!", kMaxSuperXSprites); break; } } // make Impact flag work for sprites that affected by explosions... if (actor->xspr.Impact && gImpactSpritesCount < kMaxSuperXSprites) { switch (actor->spr.statnum) { case kStatFX: case kStatExplosion: case kStatItem: case kStatPurge: case kStatSpares: case kStatFlare: case kStatInactive: case kStatFree: case kStatMarker: case kStatModernPlayerLinker: break; default: gImpactSpritesList[gImpactSpritesCount++] = actor; if (gImpactSpritesCount == kMaxSuperXSprites) I_Error("Max (%d) *additional* Impact sprites reached!", kMaxSuperXSprites); break; } } } // collect objects for tracking conditions BloodStatIterator it2(kStatModernCondition); while (auto iactor = it2.Next()) { if (iactor->xspr.busyTime <= 0 || iactor->xspr.isTriggered) continue; TRCONDITION* pCond = &gConditions[gConditions.Reserve(1)]; for (auto iactor2 : actors) { if (!iactor->exists() || !iactor2->hasX() || iactor2->xspr.txID != iactor->xspr.rxID || iactor2 == iactor) continue; switch (iactor2->spr.type) { case kSwitchToggle: // exceptions case kSwitchOneWay: // exceptions continue; } if (iactor2->spr.type == kModernCondition || iactor2->spr.type == kModernConditionFalse) condError(iactor, "Tracking condition always must be first in condition sequence!"); pCond->objects.Reserve(1); pCond->objects.Last().obj = EventObject(iactor2); pCond->objects.Last().cmd = (uint8_t)iactor2->xspr.command; } for (auto& sect : sector) { if (!sect.hasX() || sect.xs().txID != iactor->xspr.rxID) continue; pCond->objects.Reserve(1); pCond->objects.Last().obj = EventObject(§); pCond->objects.Last().cmd = sect.xs().command; } for (auto& wal : wall) { if (!wal.hasX() || wal.xw().txID != iactor->xspr.rxID) continue; switch (wal.type) { case kSwitchToggle: // exceptions case kSwitchOneWay: // exceptions continue; } pCond->objects.Reserve(1); pCond->objects.Last().obj = EventObject(&wal); pCond->objects.Last().cmd = wal.xw().command; } if (iactor->xspr.data1 > kCondGameMax && pCond->objects.Size() == 0) Printf(PRINT_HIGH, "No objects to track found for condition #%d, RXID: %d!", iactor->GetIndex(), iactor->xspr.rxID); pCond->actor = iactor; } } // The following functions required for random event features //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int nnExtRandom(int a, int b) { if (!gAllowTrueRandom) return Random(((b + 1) - a)) + a; // used for better randomness in single player std::uniform_int_distribution dist_a_b(a, b); return dist_a_b(gStdRandom); } static int GetDataVal(DBloodActor* actor, int data) { if (!actor->hasX()) return -1; switch (data) { case 0: return actor->xspr.data1; case 1: return actor->xspr.data2; case 2: return actor->xspr.data3; case 3: return actor->xspr.data4; } return -1; } //--------------------------------------------------------------------------- // // tries to get random data field of sprite // //--------------------------------------------------------------------------- static int randomGetDataValue(DBloodActor* actor, int randType) { if (actor == NULL || !actor->hasX()) return -1; int random = 0; int bad = 0; int maxRetries = kMaxRandomizeRetries; int rData[4]; rData[0] = actor->xspr.data1; rData[2] = actor->xspr.data3; rData[1] = actor->xspr.data2; rData[3] = actor->xspr.data4; // randomize only in case if at least 2 data fields fits. for (int i = 0; i < 4; i++) { switch (randType) { case kRandomizeItem: if (rData[i] >= kItemWeaponBase && rData[i] < kItemMax) break; else bad++; break; case kRandomizeDude: if (rData[i] >= kDudeBase && rData[i] < kDudeMax) break; else bad++; break; case kRandomizeTX: if (rData[i] > kChannelZero && rData[i] < kChannelUserMax) break; else bad++; break; default: bad++; break; } } if (bad < 3) { // try randomize few times while (maxRetries > 0) { random = nnExtRandom(0, 3); if (rData[random] > 0) return rData[random]; else maxRetries--; } } return -1; } //--------------------------------------------------------------------------- // // this function drops random item using random pickup generator(s) // //--------------------------------------------------------------------------- static DBloodActor* randomDropPickupObject(DBloodActor* sourceactor, int prevItem) { DBloodActor* spawned = nullptr; int selected = -1; int maxRetries = 9; if (sourceactor->hasX()) { while ((selected = randomGetDataValue(sourceactor, kRandomizeItem)) == prevItem) if (maxRetries-- <= 0) break; if (selected > 0) { spawned = actDropObject(sourceactor, selected); if (spawned) { sourceactor->xspr.dropMsg = uint8_t(spawned->spr.type); // store dropped item type in dropMsg spawned->spr.pos = sourceactor->spr.pos; if ((sourceactor->spr.flags & kModernTypeFlag1) && (sourceactor->xspr.txID > 0 || (sourceactor->xspr.txID != 3 && sourceactor->xspr.lockMsg > 0))) { spawned->addX(); // inherit spawn sprite trigger settings, so designer can send command when item picked up. spawned->xspr.txID = sourceactor->xspr.txID; spawned->xspr.command = sourceactor->xspr.sysData1; spawned->xspr.triggerOn = sourceactor->xspr.triggerOn; spawned->xspr.triggerOff = sourceactor->xspr.triggerOff; spawned->xspr.Pickup = true; } } } } return spawned; } //--------------------------------------------------------------------------- // // this function spawns random dude using dudeSpawn // //--------------------------------------------------------------------------- DBloodActor* randomSpawnDude(DBloodActor* sourceactor, DBloodActor* origin, double dist, double zadd) { DBloodActor* spawned = NULL; int selected = -1; if ((selected = randomGetDataValue(sourceactor, kRandomizeDude)) > 0) spawned = nnExtSpawnDude(sourceactor, origin, selected, dist, zadd); return spawned; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static void windGenDoVerticalWind(int factor, sectortype* pSector) { double maxZ = 0, zdiff; bool maxZfound = false; // find maxz marker first BloodSectIterator it(pSector); while (auto actor = it.Next()) { if (actor->spr.type == kMarkerOn && actor->spr.statnum != kStatMarker) { maxZ = actor->spr.pos.Z; maxZfound = true; break; } } it.Reset(pSector); while (auto actor = it.Next()) { switch (actor->spr.statnum) { case kStatFree: continue; case kStatFX: if (actor->vel.Z) break; continue; case kStatThing: case kStatDude: if (actor->spr.flags & kPhysGravity) break; continue; default: if (actor->hasX() && actor->xspr.physAttr & kPhysGravity) break; continue; } if (maxZfound && actor->spr.pos.Z <= maxZ) { zdiff = actor->spr.pos.Z - maxZ; if (actor->vel.Z < 0) actor->vel.Z += actor->vel.Z * zdiff / 4096; continue; } double val = -factor / 1024.; if (actor->vel.Z >= 0) actor->vel.Z += val; else actor->vel.Z = val; actor->spr.pos.Z += actor->vel.Z / 16.; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void nnExtProcessSuperSprites() { // process tracking conditions if (gConditions.Size() > 0) { for (unsigned i = 0; i < gConditions.Size(); i++) { TRCONDITION const* pCond = &gConditions[i]; auto aCond = pCond->actor; if (aCond->xspr.locked || aCond->xspr.isTriggered || ++aCond->xspr.busy < aCond->xspr.busyTime) continue; if (pCond->objects.Size() > 0) { aCond->xspr.busy = 0; for (unsigned k = 0; k < pCond->objects.Size(); k++) { EVENT evn; evn.target = pCond->objects[k].obj; evn.cmd = pCond->objects[k].cmd; evn.funcID = kCallbackMax; evn.initiator = nullptr; useCondition(pCond->actor, evn); } } else if (aCond->xspr.data1 >= kCondGameBase && aCond->xspr.data1 < kCondGameMax) { EVENT evn; evn.target = EventObject(pCond->actor); evn.cmd = (int8_t)aCond->xspr.command; evn.funcID = kCallbackMax; evn.initiator = nullptr; useCondition(pCond->actor, evn); } } } // process floor oriented kModernWindGenerator to create a vertical wind in the sectors BloodStatIterator it(kStatModernWindGen); while (auto windactor = it.Next()) { if (!(windactor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR) || windactor->spr.statnum >= kMaxStatus || !windactor->hasX()) continue; if (!windactor->xspr.state || windactor->xspr.locked) continue; int j, rx; bool fWindAlways = (windactor->spr.flags & kModernTypeFlag1); if (windactor->xspr.txID) { rx = windactor->xspr.txID; for (j = bucketHead[rx]; j < bucketHead[rx + 1]; j++) { if (!rxBucket[j].isSector()) continue; auto pSector = rxBucket[j].sector(); XSECTOR* pXSector = &pSector->xs(); if ((!pXSector->locked) && (fWindAlways || pXSector->windAlways || pXSector->busy)) windGenDoVerticalWind(windactor->xspr.sysData2, pSector); } DBloodActor* pXRedir = nullptr; // check redirected TX buckets while ((pXRedir = evrListRedirectors(OBJ_SPRITE, nullptr, nullptr, windactor, pXRedir, &rx)) != nullptr) { for (j = bucketHead[rx]; j < bucketHead[rx + 1]; j++) { if (!rxBucket[j].isSector()) continue; auto pSector = rxBucket[j].sector(); XSECTOR* pXSector = &pSector->xs(); if ((!pXSector->locked) && (fWindAlways || pXSector->windAlways || pXSector->busy)) windGenDoVerticalWind(windactor->xspr.sysData2, pSector); } } } else if (windactor->insector()) { sectortype* pSect = windactor->sector(); XSECTOR* pXSector = (pSect->hasX()) ? &pSect->xs() : nullptr; if ((fWindAlways) || (pXSector && !pXSector->locked && (pXSector->windAlways || pXSector->busy))) windGenDoVerticalWind(windactor->xspr.sysData2, windactor->sector()); } } // process additional proximity sprites if (gProxySpritesCount > 0) { for (int i = 0; i < gProxySpritesCount; i++) { DBloodActor* pProx = gProxySpritesList[i]; if (!pProx || !xsprIsFine(pProx)) continue; if (!pProx->xspr.Proximity || (!pProx->xspr.Interrutable && pProx->xspr.state != pProx->xspr.restState) || pProx->xspr.locked == 1 || pProx->xspr.isTriggered) continue; // don't process locked or triggered sprites int okDist = (pProx->IsDudeActor()) ? 96 : max(int(pProx->clipdist * 12), 32); auto pos = pProx->spr.pos; auto pSect = pProx->sector(); if (!pProx->xspr.DudeLockout) { BloodStatIterator itr(kStatDude); while (auto affected = itr.Next()) { if (!xsprIsFine(affected) || affected->xspr.health <= 0) continue; else if (CheckProximity(affected, pos, pSect, okDist)) { trTriggerSprite(pProx, kCmdSpriteProximity, affected); break; } } } else { for (int a = connecthead; a >= 0; a = connectpoint2[a]) { DBloodPlayer* pPlayer = getPlayer(a); if (!pPlayer || !xsprIsFine(pPlayer->GetActor()) || pPlayer->GetActor()->xspr.health <= 0) continue; if (pPlayer->GetActor()->xspr.health > 0 && CheckProximity(getPlayer(0)->GetActor(), pos, pSect, okDist)) { trTriggerSprite(pProx, kCmdSpriteProximity, pPlayer->GetActor()); } } } } } // process sight sprites (for players only) if (gSightSpritesCount > 0) { for (int i = 0; i < gSightSpritesCount; i++) { DBloodActor* pSight = gSightSpritesList[i]; if (!pSight || !xsprIsFine(pSight)) continue; if ((!pSight->xspr.Interrutable && pSight->xspr.state != pSight->xspr.restState) || pSight->xspr.locked == 1 || pSight->xspr.isTriggered) continue; // don't process locked or triggered sprites // sprite is drawn for one of players if ((pSight->xspr.unused3 & kTriggerSpriteScreen) && (pSight->spr.cstat2 & CSTAT2_SPRITE_MAPPED)) { trTriggerSprite(pSight, kCmdSpriteSight); pSight->spr.cstat2 &= ~CSTAT2_SPRITE_MAPPED; continue; } auto pSightSect = pSight->sector(); double ztop2, zbot2; for (int a = connecthead; a >= 0; a = connectpoint2[a]) { DBloodPlayer* pPlayer = getPlayer(a); if (!pPlayer || !xsprIsFine(pPlayer->GetActor()) || pPlayer->GetActor()->xspr.health <= 0) continue; auto plActor = pPlayer->GetActor(); GetActorExtents(plActor, &ztop2, &zbot2); if (cansee(pSight->spr.pos, pSightSect, DVector3(plActor->spr.pos.XY(), ztop2), plActor->sector())) { if (pSight->xspr.Sight) { trTriggerSprite(pSight, kCmdSpriteSight, plActor); } if (pSight->xspr.unused3 & kTriggerSpriteAim) { bool vector = (pSight->spr.cstat & CSTAT_SPRITE_BLOCK_HITSCAN); if (!vector) pSight->spr.cstat |= CSTAT_SPRITE_BLOCK_HITSCAN; HitScan(pPlayer->GetActor(), pPlayer->zWeapon, pPlayer->aim, CLIPMASK0 | CLIPMASK1, 0); if (!vector) pSight->spr.cstat &= ~CSTAT_SPRITE_BLOCK_HITSCAN; if (gHitInfo.actor() == pSight) { trTriggerSprite(gHitInfo.actor(), kCmdSpriteSight, plActor); } } } } } } // process Debris sprites for movement if (gPhysSpritesCount > 0) { for (int i = 0; i < gPhysSpritesCount; i++) { DBloodActor* debrisactor = gPhysSpritesList[i]; if (debrisactor == nullptr || !debrisactor->hasX()) continue; if (debrisactor->spr.statnum == kStatFree || (debrisactor->spr.flags & kHitagFree) != 0) { gPhysSpritesList[i] = nullptr; continue; } if (!(debrisactor->xspr.physAttr & kPhysMove) && !(debrisactor->xspr.physAttr & kPhysGravity)) { gPhysSpritesList[i] = nullptr; continue; } XSECTOR* pXSector = (debrisactor->sector()->hasX()) ? &debrisactor->sector()->xs() : nullptr; viewBackupSpriteLoc(debrisactor); bool uwater = false; int mass = debrisactor->spriteMass.mass; int airVel = debrisactor->spriteMass.airVel; double top, bottom; GetActorExtents(debrisactor, &top, &bottom); if (pXSector != nullptr) { if ((uwater = pXSector->Underwater) != 0) airVel <<= 6; if (pXSector->panVel != 0 && getflorzofslopeptr(debrisactor->sector(), debrisactor->spr.pos) <= bottom) { DAngle angle = pXSector->panAngle; double speed = 0; if (pXSector->panAlways || pXSector->state || pXSector->busy) { speed = pXSector->panVel / 128.; if (!pXSector->panAlways && pXSector->busy) speed *= FixedToFloat(pXSector->busy); } if (debrisactor->sector()->floorstat & CSTAT_SECTOR_ALIGN) angle += debrisactor->sector()->walls[0].normalAngle(); debrisactor->vel += angle.ToVector() * speed; } } actAirDrag(debrisactor, airVel); if (debrisactor->xspr.physAttr & kPhysDebrisTouch) { DBloodPlayer* pPlayer = NULL; for (int a = connecthead; a != -1; a = connectpoint2[a]) { pPlayer = getPlayer(a); DBloodActor* pact = pPlayer->GetActor(); if (pact && pact->hit.hit.type == kHitSprite && pact->hit.hit.actor() == debrisactor) { double nSpeed = pact->vel.XY().Length(); nSpeed = max(nSpeed - nSpeed * FixedToFloat<6>(mass), FixedToFloat(0x9000 - (mass << 3))); // very messy math (TM)... debrisactor->vel += pPlayer->GetActor()->spr.Angles.Yaw.ToVector() * nSpeed; debrisactor->hit.hit.setSprite(pPlayer->GetActor()); } } } if (debrisactor->xspr.physAttr & kPhysGravity) debrisactor->xspr.physAttr |= kPhysFalling; if ((debrisactor->xspr.physAttr & kPhysFalling) || !debrisactor->vel.isZero() || debrisactor->sector()->velFloor || debrisactor->sector()->velCeil) debrisMove(i); if (!debrisactor->vel.XY().isZero()) debrisactor->xspr.goalAng = debrisactor->vel.Angle(); debrisactor->norm_ang(); DAngle ang = debrisactor->spr.Angles.Yaw; if ((uwater = spriteIsUnderwater(debrisactor)) == false) evKillActor(debrisactor, kCallbackEnemeyBubble); else if (Chance(0x1000 - mass)) { if (debrisactor->vel.Z > 0x100) debrisBubble(debrisactor); if (absangle(ang, debrisactor->xspr.goalAng) < minAngle) // need to be very careful with comparing angles for equality! { debrisactor->xspr.goalAng += RandomAngle(kAng60); debrisactor->norm_ang(); debrisBubble(debrisactor); } } int vdist = max((int)(abs(debrisactor->vel.X) + abs(debrisactor->vel.Y) * 2048.), (uwater) ? 1 : 0); auto angStep = DAngle::fromBuild(vdist); if (ang < debrisactor->xspr.goalAng) debrisactor->spr.Angles.Yaw = min(ang + angStep, debrisactor->xspr.goalAng); else if (ang > debrisactor->xspr.goalAng) debrisactor->spr.Angles.Yaw = max(ang - angStep, debrisactor->xspr.goalAng); auto pSector = debrisactor->sector(); double fz, cz; calcSlope(pSector, debrisactor->spr.pos, &cz, &fz); GetActorExtents(debrisactor, &top, &bottom); if (fz >= bottom && pSector->lowerLink == nullptr && !(pSector->ceilingstat & CSTAT_SECTOR_SKY)) debrisactor->spr.pos.Z += max(cz - top, 0.); if (cz <= top && pSector->upperLink == nullptr && !(pSector->floorstat & CSTAT_SECTOR_SKY)) debrisactor->spr.pos.Z += min(fz - bottom, 0.); } } } //--------------------------------------------------------------------------- // // this function plays sound predefined in missile info // //--------------------------------------------------------------------------- void sfxPlayMissileSound(DBloodActor* actor, int missileId) { const MISSILEINFO_EXTRA* pMissType = &gMissileInfoExtra[missileId - kMissileBase]; sfxPlay3DSound(actor, Chance(0x5000) ? pMissType->fireSound[0] : pMissType->fireSound[1], -1, 0); } //--------------------------------------------------------------------------- // // this function plays sound predefined in vector info // //--------------------------------------------------------------------------- void sfxPlayVectorSound(DBloodActor* actor, int vectorId) { const VECTORINFO_EXTRA* pVectorData = &gVectorInfoExtra[vectorId]; sfxPlay3DSound(actor, Chance(0x5000) ? pVectorData->fireSound[0] : pVectorData->fireSound[1], -1, 0); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int getSpriteMassBySize(DBloodActor* actor) { int mass = 0; int seqId = -1; double clipDist = actor->clipdist; if (!actor->hasX()) { I_Error("getSpriteMassBySize: actor->spr.hasX == false"); } else if (actor->IsDudeActor()) { switch (actor->spr.type) { case kDudePodMother: // fake dude, no seq break; case kDudeModernCustom: case kDudeModernCustomBurning: seqId = actor->xspr.data2; clipDist = actor->genDudeExtra.clipdist; break; default: seqId = getDudeInfo(actor->spr.type)->seqStartID; break; } } else { seqId = seqGetID(actor); } SPRITEMASS* cached = &actor->spriteMass; if (((seqId >= 0 && seqId == cached->seqId) || actor->spr.spritetexture() == cached->texid) && actor->spr.scale.X == cached->scale.X && actor->spr.scale.Y == cached->scale.Y && clipDist == cached->clipDist) { return cached->mass; } auto texid = actor->spr.spritetexture(); int massDiv = 30; int addMul = 2; int subMul = 2; if (seqId >= 0) { auto pSeq = getSequence(seqId); if (pSeq) { texid = seqGetTexture(&pSeq->frames[0]); } } auto tex = TexMan.GetGameTexture(texid); if (!tex) return 0; clipDist = max(actor->clipdist, 0.25); int x = (int)tex->GetDisplayWidth(); int y = (int)tex->GetDisplayHeight(); int xscale = int(actor->spr.scale.X * 64); int yscale = int(actor->spr.scale.Y * 64); // take surface type into account switch (GetExtInfo(actor->spr.spritetexture()).surftype) { case 1: massDiv = 16; break; // stone case 2: massDiv = 18; break; // metal case 3: massDiv = 21; break; // wood case 4: massDiv = 25; break; // flesh case 5: massDiv = 28; break; // water case 6: massDiv = 26; break; // dirt case 7: massDiv = 27; break; // clay case 8: massDiv = 35; break; // snow case 9: massDiv = 22; break; // ice case 10: massDiv = 37; break; // leaves case 11: massDiv = 33; break; // cloth case 12: massDiv = 36; break; // plant case 13: massDiv = 24; break; // goo case 14: massDiv = 23; break; // lava } mass = ((x + y) * int(clipDist * 2)) / massDiv; if (xscale > 64) mass += ((xscale - 64) * addMul); else if (xscale < 64 && mass > 0) { for (int i = 64 - xscale; i > 0; i--) { if ((mass -= subMul) <= 100 && subMul-- <= 1) { mass -= i; break; } } } if (yscale > 64) mass += ((yscale - 64) * addMul); else if (yscale < 64 && mass > 0) { for (int i = 64 - yscale; i > 0; i--) { if ((mass -= subMul) <= 100 && subMul-- <= 1) { mass -= i; break; } } } if (mass <= 0) cached->mass = 1 + Random(10); else cached->mass = ClipRange(mass, 1, 65535); cached->airVel = ClipRange(400 - cached->mass, 32, 400); cached->fraction = ClipRange(60000 - (cached->mass << 7), 8192, 60000); cached->scale.X = actor->spr.scale.X; cached->scale.Y = actor->spr.scale.Y; cached->texid = actor->spr.spritetexture(); cached->seqId = seqId; cached->clipDist = actor->clipdist; return cached->mass; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static int debrisGetIndex(DBloodActor* actor) { if (!actor->hasX() || actor->xspr.physAttr == 0) return -1; for (int i = 0; i < gPhysSpritesCount; i++) { if (gPhysSpritesList[i] != actor) continue; return i; } return -1; } int debrisGetFreeIndex(void) { for (int i = 0; i < kMaxSuperXSprites; i++) { if (gPhysSpritesList[i] == nullptr) return i; auto actor = gPhysSpritesList[i]; if (actor->spr.statnum == kStatFree) return i; else if ((actor->spr.flags & kHitagFree) || !gPhysSpritesList[i]->hasX()) return i; else if (gPhysSpritesList[i]->xspr.physAttr == 0) return i; } return -1; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void debrisConcuss(DBloodActor* owneractor, int listIndex, const DVector3& pos, int dmg) { DBloodActor* actor = gPhysSpritesList[listIndex]; if (actor != nullptr && actor->hasX()) { auto dv = actor->spr.pos - pos; dmg = int(dmg * (0x4000 / (0x4000 + dv.LengthSquared()))); bool thing = (actor->spr.type >= kThingBase && actor->spr.type < kThingMax); auto tex = TexMan.GetGameTexture(actor->spr.spritetexture()); double size = (tex->GetDisplayWidth() * actor->spr.scale.X * tex->GetDisplayHeight() * actor->spr.scale.Y) * 2048; if (actor->xspr.physAttr & kPhysDebrisExplode) { if (actor->spriteMass.mass > 0) { double t = double(dmg) * size / actor->spriteMass.mass; actor->vel += dv * t / (1 << 24); } if (thing) actor->spr.statnum = kStatThing; // temporary change statnum property } actDamageSprite(owneractor, actor, kDamageExplode, dmg); if (thing) actor->spr.statnum = kStatDecoration; // return statnum property back } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void debrisBubble(DBloodActor* actor) { double top, bottom; GetActorExtents(actor, &top, &bottom); auto tex = TexMan.GetGameTexture(actor->spr.spritetexture()); double nDist = (actor->spr.scale.X * tex->GetDisplayWidth()) * (1. / 2); for (unsigned int i = 0; i < 1 + Random(5); i++) { DAngle nAngle = RandomAngle(); DVector3 pos; pos.XY() = actor->spr.pos.XY() + nAngle.ToVector() * nDist; pos.Z = bottom - RandomD(bottom - top, 8); auto pFX = gFX.fxSpawnActor((FX_ID)(FX_23 + Random(3)), actor->sector(), pos, nullAngle); if (pFX) { pFX->vel.X = actor->vel.X + Random2F(0x1aaaa); pFX->vel.Y = actor->vel.Y + Random2F(0x1aaaa); pFX->vel.Z = actor->vel.Z + Random2F(0x1aaaa); } } if (Chance(0x2000)) evPostActor(actor, 0, kCallbackEnemeyBubble); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void debrisMove(int listIndex) { DBloodActor* actor = gPhysSpritesList[listIndex]; auto pSector = actor->sector(); if (!actor->hasX() || !pSector) { gPhysSpritesList[listIndex] = nullptr; return; } double top, bottom; GetActorExtents(actor, &top, &bottom); Collision moveHit; moveHit.setNone(); double floorDist = (bottom - actor->spr.pos.Z) * 0.25; double ceilDist = (actor->spr.pos.Z - top) * 0.25; double clipDistf = actor->clipdist; int mass = actor->spriteMass.mass; bool uwater = false; int tmpFraction = actor->spriteMass.fraction; if (pSector->hasX() && pSector->xs().Underwater) { tmpFraction >>= 1; uwater = true; } if (actor->vel.X != 0 || actor->vel.Y != 0) { auto oldcstat = actor->spr.cstat; actor->spr.cstat &= ~(CSTAT_SPRITE_BLOCK | CSTAT_SPRITE_BLOCK_HITSCAN); ClipMove(actor->spr.pos, &pSector, actor->vel.XY(), clipDistf, ceilDist, floorDist, CLIPMASK0, moveHit); actor->hit.hit = moveHit; actor->spr.cstat = oldcstat; if (actor->sector() != pSector) { if (!pSector) return; else ChangeActorSect(actor, pSector); } if (pSector->type >= kSectorPath && pSector->type <= kSectorRotate) { auto pSector2 = pSector; if (pushmove(actor->spr.pos, &pSector2, clipDistf, ceilDist, floorDist, CLIPMASK0) != -1) pSector = pSector2; } if (actor->hit.hit.type == kHitWall) { moveHit = actor->hit.hit; actWallBounceVector(actor, moveHit.hitWall, FixedToFloat(tmpFraction)); } } else { updatesectorz(actor->spr.pos, &pSector); if (!pSector) return; } if (actor->sector() != pSector) { assert(pSector); ChangeActorSect(actor, pSector); pSector = actor->sector(); } if (pSector->hasX()) uwater = pSector->xs().Underwater; actor->spr.pos.Z += actor->vel.Z; double ceilZ, floorZ; Collision ceilColl, floorColl; GetZRange(actor, &ceilZ, &ceilColl, &floorZ, &floorColl, clipDistf, CLIPMASK0, PARALLAXCLIP_CEILING | PARALLAXCLIP_FLOOR); GetActorExtents(actor, &top, &bottom); if ((actor->xspr.physAttr & kPhysDebrisSwim) && uwater) { double vc = 0; double cz = getceilzofslopeptr(pSector, actor->spr.pos); double fz = getflorzofslopeptr(pSector, actor->spr.pos); double div = max(bottom - top, 1 / 256.); if (pSector->lowerLink) cz += (cz < 0) ? 5. : -5.; if (top > cz && (!(actor->xspr.physAttr & kPhysDebrisFloat) || fz <= bottom * 4)) actor->vel.Z -= (bottom - ceilZ) * mass / 64.; if (fz < bottom) vc = 0.888888 + ((bottom - fz) * -1.222222) / div; if (vc) { actor->spr.pos.Z += vc * 2; actor->vel.Z = vc; } } else if ((actor->xspr.physAttr & kPhysGravity) && bottom < floorZ) { actor->spr.pos.Z += 1.777; actor->vel.Z += 0.888888; } int i; if ((i = CheckLink(actor)) != 0) { GetZRange(actor, &ceilZ, &ceilColl, &floorZ, &floorColl, clipDistf, CLIPMASK0, PARALLAXCLIP_CEILING | PARALLAXCLIP_FLOOR); if (!(actor->spr.cstat & CSTAT_SPRITE_INVISIBLE)) { switch (i) { case kMarkerUpWater: case kMarkerUpGoo: int pitch = (150000 - (actor->spriteMass.mass << 9)) + Random3(8192); sfxPlay3DSoundVolume(actor, 720, -1, 0, pitch, 75 - Random(40)); if (!spriteIsUnderwater(actor)) { evKillActor(actor, kCallbackEnemeyBubble); } else { evPostActor(actor, 0, kCallbackEnemeyBubble); for (int ii = 2; ii <= 5; ii++) { if (Chance(0x5000 * ii)) evPostActor(actor, Random(5), kCallbackEnemeyBubble); } } break; } } } GetActorExtents(actor, &top, &bottom); if (floorZ <= bottom) { actor->hit.florhit = floorColl; double veldiff = actor->vel.Z - actor->sector()->velFloor; if (veldiff > 0) { actor->xspr.physAttr |= kPhysFalling; auto vec4 = actFloorBounceVector(actor, veldiff, actor->sector(), FixedToFloat(tmpFraction)); actor->vel = vec4.XYZ(); veldiff = actor->vel.Z; if (abs(actor->vel.Z) < 1) { actor->vel.Z = actor->sector()->velFloor; actor->xspr.physAttr &= ~kPhysFalling; } moveHit = floorColl; DBloodActor* pFX = NULL, * pFX2 = NULL; switch (tileGetSurfType(floorColl)) { case kSurfLava: if ((pFX = gFX.fxSpawnActor(FX_10, actor->sector(), DVector3(actor->spr.pos.XY(), floorZ))) == NULL) break; for (i = 0; i < 7; i++) { if ((pFX2 = gFX.fxSpawnActor(FX_14, pFX->sector(), pFX->spr.pos)) == NULL) continue; pFX2->vel.X = Random2F(0x6aaaa); pFX2->vel.Y = Random2F(0x6aaaa); pFX2->vel.Z = -Random2F(0xd5555); } break; case kSurfWater: gFX.fxSpawnActor(FX_9, actor->sector(), DVector3(actor->spr.pos.XY(), floorZ)); break; } } else if (actor->vel.Z == 0) { actor->xspr.physAttr &= ~kPhysFalling; } } else { actor->hit.florhit.setNone(); if (actor->xspr.physAttr & kPhysGravity) actor->xspr.physAttr |= kPhysFalling; } if (top <= ceilZ) { actor->hit.ceilhit = moveHit = ceilColl; actor->spr.pos.Z += max(ceilZ - top, 0.); if (actor->vel.Z <= 0 && (actor->xspr.physAttr & kPhysFalling)) actor->vel.Z *= 0.875; } else { actor->hit.ceilhit.setNone(); GetActorExtents(actor, &top, &bottom); } if (moveHit.type != kHitNone && actor->xspr.Impact && !actor->xspr.locked && !actor->xspr.isTriggered && (actor->xspr.state == actor->xspr.restState || actor->xspr.Interrutable)) { if (actor->spr.type >= kThingBase && actor->spr.type < kThingMax) ChangeActorStat(actor, kStatThing); trTriggerSprite(actor, kCmdToggle, actor); } if (actor->vel.X == 0 && actor->vel.Y == 0) return; else if (floorColl.type == kHitSprite) { if ((floorColl.actor()->spr.cstat & CSTAT_SPRITE_ALIGNMENT_MASK) == 0) { actor->vel.XY() += (actor->spr.pos.XY() - floorColl.actor()->spr.pos.XY()) / 4096.; return; } } actor->xspr.height = int(max(floorZ - bottom, 0.)); if (uwater || actor->xspr.height >= 0x100) return; double nDrag = 0.1640625; if (actor->xspr.height > 0) nDrag *= 1 - actor->xspr.height / 256.; actor->vel.XY() *= 1 - nDrag; if (actor->vel.XY().LengthSquared() < 1 / 256.) actor->vel.XY().Zero(); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool ceilIsTooLow(DBloodActor* actor) { if (actor != nullptr) { sectortype* pSector = actor->sector(); double a = pSector->ceilingz - pSector->floorz; double top, bottom; GetActorExtents(actor, &top, &bottom); if (a > top - bottom) return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiSetGenIdleState(DBloodActor* actor) { switch (actor->spr.type) { case kDudeModernCustom: case kDudeModernCustomBurning: aiGenDudeNewState(actor, &genIdle); break; default: aiNewState(actor, &genIdle); break; } } //--------------------------------------------------------------------------- // // this function stops wind on all TX sectors affected by WindGen after it goes off state. // //--------------------------------------------------------------------------- void windGenStopWindOnSectors(DBloodActor* sourceactor) { if (sourceactor->xspr.txID <= 0 && sourceactor->sector()->hasX()) { sourceactor->sector()->xs().windVel = 0; return; } for (int i = bucketHead[sourceactor->xspr.txID]; i < bucketHead[sourceactor->xspr.txID + 1]; i++) { if (!rxBucket[i].isSector()) continue; auto pSector = rxBucket[i].sector(); XSECTOR* pXSector = &pSector->xs(); if ((pXSector->state == 1 && !pXSector->windAlways) || ((sourceactor->spr.flags & kModernTypeFlag1) && !(sourceactor->spr.flags & kModernTypeFlag2))) { pXSector->windVel = 0; } } // check redirected TX buckets int rx = -1; DBloodActor* pXRedir = nullptr; while ((pXRedir = evrListRedirectors(OBJ_SPRITE, nullptr, nullptr, sourceactor, pXRedir, &rx)) != nullptr) { for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { if (!rxBucket[i].isSector()) continue; auto pSector = rxBucket[i].sector(); XSECTOR* pXSector = &pSector->xs(); if ((pXSector->state == 1 && !pXSector->windAlways) || (sourceactor->spr.flags & kModernTypeFlag2)) pXSector->windVel = 0; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlStartScene(DBloodActor* sourceactor, DBloodPlayer* pPlayer, bool force) { TRPLAYERCTRL* pCtrl = &gPlayerCtrl[pPlayer->pnum]; if (pCtrl->qavScene.initiator != nullptr && !force) return; QAV* pQav = playerQavSceneLoad(sourceactor->xspr.data2); if (pQav != nullptr) { // save current weapon sourceactor->xspr.dropMsg = pPlayer->curWeapon; auto initiator = pCtrl->qavScene.initiator; if (initiator != nullptr && initiator != sourceactor && initiator->hasX()) sourceactor->xspr.dropMsg = initiator->xspr.dropMsg; if (initiator == nullptr) WeaponLower(pPlayer); sourceactor->xspr.sysData1 = ClipLow((pQav->duration * sourceactor->xspr.waitTime) / 4, 0); // how many times animation should be played pCtrl->qavScene.initiator = sourceactor; pCtrl->qavScene.qavResrc = pQav; pCtrl->qavScene.dummy = -1; //pCtrl->qavScene.qavResrc->Preload(); pPlayer->sceneQav = sourceactor->xspr.data2; pPlayer->weaponTimer = pCtrl->qavScene.qavResrc->duration; pPlayer->qavCallback = (sourceactor->xspr.data3 > 0) ? ClipRange(sourceactor->xspr.data3 - 1, 0, 32) : -1; pPlayer->qavLoop = false; pPlayer->qavLastTick = I_GetTime(pCtrl->qavScene.qavResrc->ticrate); pPlayer->qavTimer = pCtrl->qavScene.qavResrc->duration; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlStopScene(DBloodPlayer* pPlayer) { TRPLAYERCTRL* pCtrl = &gPlayerCtrl[pPlayer->pnum]; auto initiator = pCtrl->qavScene.initiator; if (initiator->hasX()) { initiator->xspr.sysData1 = 0; } if (pCtrl->qavScene.initiator != nullptr) { pCtrl->qavScene.initiator = nullptr; pCtrl->qavScene.qavResrc = nullptr; pPlayer->sceneQav = -1; // restore weapon if (pPlayer->GetActor()->xspr.health > 0) { int oldWeapon = (initiator->hasX() && initiator->xspr.dropMsg != 0) ? initiator->xspr.dropMsg : 1; pPlayer->newWeapon = pPlayer->curWeapon = oldWeapon; WeaponRaise(pPlayer); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlLink(DBloodActor* sourceactor, DBloodPlayer* pPlayer, bool checkCondition) { // save player's sprite index to let the tracking condition know it after savegame loading... auto actor = pPlayer->GetActor(); sourceactor->prevmarker = actor; actor->xspr.txID = sourceactor->xspr.txID; actor->xspr.command = kCmdToggle; actor->xspr.triggerOn = sourceactor->xspr.triggerOn; actor->xspr.triggerOff = sourceactor->xspr.triggerOff; actor->xspr.busyTime = sourceactor->xspr.busyTime; actor->xspr.waitTime = sourceactor->xspr.waitTime; actor->xspr.restState = sourceactor->xspr.restState; actor->xspr.Push = sourceactor->xspr.Push; actor->xspr.Impact = sourceactor->xspr.Impact; actor->xspr.Vector = sourceactor->xspr.Vector; actor->xspr.Touch = sourceactor->xspr.Touch; actor->xspr.Sight = sourceactor->xspr.Sight; actor->xspr.Proximity = sourceactor->xspr.Proximity; actor->xspr.Decoupled = sourceactor->xspr.Decoupled; actor->xspr.Interrutable = sourceactor->xspr.Interrutable; actor->xspr.DudeLockout = sourceactor->xspr.DudeLockout; actor->xspr.data1 = sourceactor->xspr.data1; actor->xspr.data2 = sourceactor->xspr.data2; actor->xspr.data3 = sourceactor->xspr.data3; actor->xspr.data4 = sourceactor->xspr.data4; actor->xspr.key = sourceactor->xspr.key; actor->xspr.dropMsg = sourceactor->xspr.dropMsg; // let's check if there is tracking condition expecting objects with this TX id if (checkCondition && sourceactor->xspr.txID >= kChannelUser) { for (unsigned i = 0; i < gConditions.Size(); i++) { TRCONDITION* pCond = &gConditions[i]; if (pCond->actor->xspr.rxID != sourceactor->xspr.txID) continue; // search for player control sprite and replace it with actual player sprite for (unsigned k = 0; k < pCond->objects.Size(); k++) { if (!pCond->objects[k].obj.isActor() || pCond->objects[k].obj.actor() != sourceactor) continue; pCond->objects[k].obj = EventObject(pPlayer->GetActor()); pCond->objects[k].cmd = (uint8_t)pPlayer->GetActor()->xspr.command; break; } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlSetRace(int value, DBloodPlayer* pPlayer) { playerSetRace(pPlayer, value); switch (pPlayer->lifeMode) { case kModeHuman: case kModeBeast: playerSizeReset(pPlayer); break; case kModeHumanShrink: playerSizeShrink(pPlayer, 2); break; case kModeHumanGrown: playerSizeGrow(pPlayer, 2); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlSetMoveSpeed(int value, DBloodPlayer* pPlayer) { int speed = ClipRange(value, 0, 500); for (int i = 0; i < kModeMax; i++) { for (int a = 0; a < kPostureMax; a++) { POSTURE* curPosture = &pPlayer->pPosture[i][a]; POSTURE* defPosture = &gPostureDefaults[i][a]; curPosture->frontAccel = (defPosture->frontAccel * speed) / kPercFull; curPosture->sideAccel = (defPosture->sideAccel * speed) / kPercFull; curPosture->backAccel = (defPosture->backAccel * speed) / kPercFull; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlSetJumpHeight(int value, DBloodPlayer* pPlayer) { int jump = ClipRange(value, 0, 500); for (int i = 0; i < kModeMax; i++) { POSTURE* curPosture = &pPlayer->pPosture[i][kPostureStand]; POSTURE* defPosture = &gPostureDefaults[i][kPostureStand]; curPosture->normalJumpZ = (defPosture->normalJumpZ * jump) / kPercFull; curPosture->pwupJumpZ = (defPosture->pwupJumpZ * jump) / kPercFull; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlSetScreenEffect(int value, int timeval, DBloodPlayer* pPlayer) { int eff = ClipLow(value, 0); int time = (eff > 0) ? timeval : 0; switch (eff) { case 0: // clear all case 1: // tilting pPlayer->tiltEffect = ClipRange(time, 0, 220); if (eff) break; [[fallthrough]]; case 2: // pain pPlayer->painEffect = ClipRange(time, 0, 2048); if (eff) break; [[fallthrough]]; case 3: // blind pPlayer->blindEffect = ClipRange(time, 0, 2048); if (eff) break; [[fallthrough]]; case 4: // pickup pPlayer->pickupEffect = ClipRange(time, 0, 2048); if (eff) break; [[fallthrough]]; case 5: // quakeEffect pPlayer->quakeEffect = ClipRange(time, 0, 2048); if (eff) break; [[fallthrough]]; case 6: // visibility pPlayer->visibility = ClipRange(time, 0, 2048); if (eff) break; [[fallthrough]]; case 7: // delirium pPlayer->pwUpTime[kPwUpDeliriumShroom] = ClipHigh(time << 1, 432000); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlSetLookAngle(int value, DBloodPlayer* pPlayer) { static constexpr double upAngle = 289; static constexpr double downAngle = -347; static constexpr double lookStepUp = 4.0 * upAngle / 60.0; static constexpr double lookStepDown = -4.0 * downAngle / 60.0; if (const double adjustment = clamp(value * 0.125 * (value > 0 ? lookStepUp : lookStepDown), downAngle, upAngle)) { gameInput.ForceInputSync(pPlayer->pnum); pPlayer->GetActor()->spr.Angles.Pitch = maphoriz(-100. * tan(adjustment * pi::pi() * (1. / 1024.))); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlEraseStuff(int value, DBloodPlayer* pPlayer) { switch (value) { case 0: // erase all [[fallthrough]]; case 1: // erase weapons WeaponLower(pPlayer); for (int i = 0; i < 14; i++) { pPlayer->hasWeapon[i] = false; // also erase ammo if (i < 12) pPlayer->ammoCount[i] = 0; } pPlayer->hasWeapon[kWeapPitchFork] = true; pPlayer->curWeapon = kWeapNone; pPlayer->nextWeapon = kWeapPitchFork; WeaponRaise(pPlayer); if (value) break; [[fallthrough]]; case 2: // erase all armor for (int i = 0; i < 3; i++) pPlayer->armor[i] = 0; if (value) break; [[fallthrough]]; case 3: // erase all pack items for (int i = 0; i < 5; i++) { pPlayer->packSlots[i].isActive = false; pPlayer->packSlots[i].curAmount = 0; } pPlayer->packItemId = -1; if (value) break; [[fallthrough]]; case 4: // erase all keys for (int i = 0; i < 8; i++) pPlayer->hasKey[i] = false; if (value) break; [[fallthrough]]; case 5: // erase powerups for (int i = 0; i < kMaxPowerUps; i++) pPlayer->pwUpTime[i] = 0; break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlGiveStuff(int data2, int weapon, int data4, DBloodPlayer* pPlayer, TRPLAYERCTRL* pCtrl) { switch (data2) { case 1: // give N weapon and default ammo for it case 2: // give just N ammo for selected weapon if (weapon <= 0 || weapon > 13) { Printf(PRINT_HIGH, "Weapon #%d is out of a weapons range!", weapon); break; } else if (data2 == 2 && data4 == 0) { Printf(PRINT_HIGH, "Zero ammo for weapon #%d is specified!", weapon); break; } switch (weapon) { case kWeapProximity: // remote bomb case kWeapRemote: // prox bomb pPlayer->hasWeapon[weapon] = true; weapon--; pPlayer->ammoCount[weapon] = ClipHigh(pPlayer->ammoCount[weapon] + ((data2 == 2) ? data4 : 1), gAmmoInfo[weapon].max); weapon++; break; default: for (int i = 0; i < 11; i++) { if (gWeaponItemData[i].type != weapon) continue; const WEAPONITEMDATA* pWeaponData = &gWeaponItemData[i]; int nAmmoType = pWeaponData->ammoType; switch (data2) { case 1: pPlayer->hasWeapon[weapon] = true; if (pPlayer->ammoCount[nAmmoType] >= pWeaponData->count) break; pPlayer->ammoCount[nAmmoType] = ClipHigh(pPlayer->ammoCount[nAmmoType] + pWeaponData->count, gAmmoInfo[nAmmoType].max); break; case 2: pPlayer->ammoCount[nAmmoType] = ClipHigh(pPlayer->ammoCount[nAmmoType] + data4, gAmmoInfo[nAmmoType].max); break; } break; } break; } if (pPlayer->hasWeapon[weapon] && data4 == 0) // switch on it { pPlayer->nextWeapon = kWeapNone; if (pPlayer->sceneQav >= 0 && pCtrl->qavScene.initiator && pCtrl->qavScene.initiator->hasX()) { pCtrl->qavScene.initiator->xspr.dropMsg = weapon; } else if (pPlayer->curWeapon != weapon) { pPlayer->newWeapon = weapon; WeaponRaise(pPlayer); } } break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlUsePackItem(int data2, int data3, int data4, DBloodPlayer* pPlayer, int evCmd) { unsigned int invItem = data2 - 1; switch (evCmd) { case kCmdOn: if (!pPlayer->packSlots[invItem].isActive) packUseItem(pPlayer, invItem); break; case kCmdOff: if (pPlayer->packSlots[invItem].isActive) packUseItem(pPlayer, invItem); break; default: packUseItem(pPlayer, invItem); break; } switch (data4) { case 2: // both case 0: // switch on it if (pPlayer->packSlots[invItem].curAmount > 0) pPlayer->packItemId = invItem; if (!data4) break; [[fallthrough]]; case 1: // force remove after use pPlayer->packSlots[invItem].isActive = false; pPlayer->packSlots[invItem].curAmount = 0; break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void trPlayerCtrlUsePowerup(DBloodActor* sourceactor, DBloodPlayer* pPlayer, int evCmd) { bool relative = (sourceactor->spr.flags & kModernTypeFlag1); int nPower = (kMinAllowedPowerup + sourceactor->xspr.data2) - 1; int nTime = ClipRange(abs(sourceactor->xspr.data3) * 100, -gPowerUpInfo[nPower].maxTime, gPowerUpInfo[nPower].maxTime); if (sourceactor->xspr.data3 < 0) nTime = -nTime; if (pPlayer->pwUpTime[nPower]) { if (!relative && nTime <= 0) powerupDeactivate(pPlayer, nPower); } if (nTime != 0) { if (pPlayer->pwUpTime[nPower] <= 0) powerupActivate(pPlayer, nPower); // MUST activate first for powerups like kPwUpDeathMask // ...so we able to change time amount if (relative) pPlayer->pwUpTime[nPower] += nTime; else pPlayer->pwUpTime[nPower] = nTime; } if (pPlayer->pwUpTime[nPower] <= 0) powerupDeactivate(pPlayer, nPower); return; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useObjResizer(DBloodActor* sourceactor, int targType, sectortype* targSect, walltype* targWall, DBloodActor* targetactor) { switch (targType) { // for sectors case OBJ_SECTOR: if (!targSect) return; if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) targSect->floorxpan_ = (float)ClipRange(sourceactor->xspr.data1, 0, 255); if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) targSect->floorypan_ = (float)ClipRange(sourceactor->xspr.data2, 0, 255); if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) targSect->ceilingxpan_ = (float)ClipRange(sourceactor->xspr.data3, 0, 255); if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) targSect->ceilingypan_ = (float)ClipRange(sourceactor->xspr.data4, 0, 255); break; // for sprites case OBJ_SPRITE: { bool fit = false; // resize by seq scaling if (sourceactor->spr.flags & kModernTypeFlag1) { if (valueIsBetween(sourceactor->xspr.data1, -255, 32767)) { int mulDiv = (valueIsBetween(sourceactor->xspr.data2, 0, 257)) ? sourceactor->xspr.data2 : 256; if (sourceactor->xspr.data1 > 0) targetactor->xspr.scale = mulDiv * ClipHigh(sourceactor->xspr.data1, 25); else if (sourceactor->xspr.data1 < 0) targetactor->xspr.scale = mulDiv / ClipHigh(abs(sourceactor->xspr.data1), 25); else targetactor->xspr.scale = 0; fit = true; } // resize by repeats } else { if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) { targetactor->spr.scale.X = (ClipRange(sourceactor->xspr.data1, 0, 255) * REPEAT_SCALE); fit = true; } if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) { targetactor->spr.scale.Y = (ClipRange(sourceactor->xspr.data2, 0, 255) * REPEAT_SCALE); fit = true; } } if (fit && (targetactor->spr.type == kDudeModernCustom || targetactor->spr.type == kDudeModernCustomBurning)) { // request properties update for custom dude targetactor->genDudeExtra.updReq[kGenDudePropertySpriteSize] = true; targetactor->genDudeExtra.updReq[kGenDudePropertyAttack] = true; targetactor->genDudeExtra.updReq[kGenDudePropertyMass] = true; targetactor->genDudeExtra.updReq[kGenDudePropertyDmgScale] = true; evPostActor(targetactor, kGenDudeUpdTimeRate, kCallbackGenDudeUpdate); } if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) targetactor->spr.xoffset = ClipRange(sourceactor->xspr.data3, 0, 255); if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) targetactor->spr.yoffset = ClipRange(sourceactor->xspr.data4, 0, 255); break; } case OBJ_WALL: if (!targWall) return; if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) targWall->xrepeat = ClipRange(sourceactor->xspr.data1, 0, 255); if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) targWall->yrepeat = ClipRange(sourceactor->xspr.data2, 0, 255); if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) targWall->xpan_ = (float)ClipRange(sourceactor->xspr.data3, 0, 255); if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) targWall->ypan_ = (float)ClipRange(sourceactor->xspr.data4, 0, 255); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void usePropertiesChanger(DBloodActor* sourceactor, int objType, sectortype* pSector, walltype* pWall, DBloodActor* targetactor) { bool flag1 = (sourceactor->spr.flags & kModernTypeFlag1); bool flag2 = (sourceactor->spr.flags & kModernTypeFlag2); bool flag4 = (sourceactor->spr.flags & kModernTypeFlag4); short data3 = sourceactor->xspr.data3; short data4 = sourceactor->xspr.data4; switch (objType) { case OBJ_WALL: { if (!pWall) return; // data1 = set this wall as first wall of sector or as sector.alignto wall if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) { if (pWall->sectorp()) { switch (sourceactor->xspr.data1) { case 1: // You got to be f***ing kidding... //setfirstwall(nSector, objIndex); Printf("Setting first wall not supported!"); // this will require a lot of mess... pWall->allocX(); break; case 2: pWall->sectorp()->slopewallofs = max(int(pWall - pWall->sectorp()->walls.Data()), 0); break; } } } // data3 = set wall hitag if (valueIsBetween(data3, -1, 32767)) { if (flag1) { if (flag4) pWall->hitag &= ~data3; else pWall->hitag |= data3; pWall->hitag |= sourceactor->xspr.data3; } else pWall->hitag = sourceactor->xspr.data3; } // data4 = set wall cstat if (valueIsBetween(data4, -1, 65535)) { auto old = pWall->cstat; // relative if (flag1) { if (flag4) pWall->cstat &= ~EWallFlags::FromInt(data4); else pWall->cstat |= EWallFlags::FromInt(data4); } // absolute else { pWall->cstat = EWallFlags::FromInt(data4); if (!flag2) { // check for exceptions pWall->cstat |= old & (CSTAT_WALL_BOTTOM_SWAP | CSTAT_WALL_ALIGN_BOTTOM | CSTAT_WALL_1WAY); pWall->cstat = (pWall->cstat & ~CSTAT_WALL_MOVE_MASK) | (old & CSTAT_WALL_MOVE_MASK); } } } } break; case OBJ_SPRITE: { bool thing2debris = false; int old = -1; // data3 = set sprite hitag if (valueIsBetween(data3, -1, 32767)) { old = targetactor->spr.flags; // relative if (flag1) { if (flag4) targetactor->spr.flags &= ~data3; else targetactor->spr.flags |= data3; } // absolute else { targetactor->spr.flags = data3; } // and handle exceptions if ((old & kHitagFree) && !(targetactor->spr.flags & kHitagFree)) targetactor->spr.flags |= kHitagFree; if ((old & kHitagRespawn) && !(targetactor->spr.flags & kHitagRespawn)) targetactor->spr.flags |= kHitagRespawn; // prepare things for different (debris) physics. thing2debris = (targetactor->spr.statnum == kStatThing && debrisGetFreeIndex() >= 0); } // data2 = sprite physics settings if (valueIsBetween(sourceactor->xspr.data2, -1, 32767) || thing2debris) { switch (targetactor->spr.statnum) { case kStatDude: // dudes already treating in game case kStatFree: case kStatMarker: case kStatPathMarker: break; default: // store physics attributes in xsprite to avoid setting hitag for modern types! int flags = (targetactor->xspr.physAttr != 0) ? targetactor->xspr.physAttr : 0; int oldFlags = flags; if (thing2debris) { // converting thing to debris if ((targetactor->spr.flags & kPhysMove) != 0) flags |= kPhysMove; else flags &= ~kPhysMove; if ((targetactor->spr.flags & kPhysGravity) != 0) flags |= (kPhysGravity | kPhysFalling); else flags &= ~(kPhysGravity | kPhysFalling); targetactor->spr.flags &= ~(kPhysMove | kPhysGravity | kPhysFalling); targetactor->vel.Zero(); targetactor->xspr.restState = targetactor->xspr.state; } else { // WTF is this?!? char digits[6] = {}; snprintf(digits, 6, "%d", sourceactor->xspr.data2); for (unsigned int i = 0; i < sizeof(digits); i++) digits[i] = (digits[i] >= 48 && digits[i] <= 57) ? (digits[i] - 57) + 9 : 0; // first digit of data2: set main physics attributes switch (digits[0]) { case 0: flags &= ~kPhysMove; flags &= ~(kPhysGravity | kPhysFalling); break; case 1: flags |= kPhysMove; flags &= ~(kPhysGravity | kPhysFalling); break; case 2: flags &= ~kPhysMove; flags |= (kPhysGravity | kPhysFalling); break; case 3: flags |= kPhysMove; flags |= (kPhysGravity | kPhysFalling); break; } // second digit of data2: touch physics flags switch (digits[1]) { case 0: flags &= ~kPhysDebrisTouch; break; case 1: flags |= kPhysDebrisTouch; break; } // third digit of data2: weapon physics flags switch (digits[2]) { case 0: flags &= ~kPhysDebrisVector; flags &= ~kPhysDebrisExplode; break; case 1: flags |= kPhysDebrisVector; flags &= ~kPhysDebrisExplode; break; case 2: flags &= ~kPhysDebrisVector; flags |= kPhysDebrisExplode; break; case 3: flags |= kPhysDebrisVector; flags |= kPhysDebrisExplode; break; } // fourth digit of data2: swimming / flying physics flags switch (digits[3]) { case 0: flags &= ~kPhysDebrisSwim; flags &= ~kPhysDebrisFly; flags &= ~kPhysDebrisFloat; break; case 1: flags |= kPhysDebrisSwim; flags &= ~kPhysDebrisFly; flags &= ~kPhysDebrisFloat; break; case 2: flags |= kPhysDebrisSwim; flags |= kPhysDebrisFloat; flags &= ~kPhysDebrisFly; break; case 3: flags |= kPhysDebrisFly; flags &= ~kPhysDebrisSwim; flags &= ~kPhysDebrisFloat; break; case 4: flags |= kPhysDebrisFly; flags |= kPhysDebrisFloat; flags &= ~kPhysDebrisSwim; break; case 5: flags |= kPhysDebrisSwim; flags |= kPhysDebrisFly; flags &= ~kPhysDebrisFloat; break; case 6: flags |= kPhysDebrisSwim; flags |= kPhysDebrisFly; flags |= kPhysDebrisFloat; break; } } int nIndex = debrisGetIndex(targetactor); // check if there is no sprite in list // adding physics sprite in list if ((flags & kPhysGravity) != 0 || (flags & kPhysMove) != 0) { if (oldFlags == 0) targetactor->vel.Zero(); if (nIndex != -1) { targetactor->xspr.physAttr = flags; // just update physics attributes } else if ((nIndex = debrisGetFreeIndex()) < 0) { viewSetSystemMessage("Max (%d) Physics affected sprites reached!", kMaxSuperXSprites); } else { targetactor->xspr.physAttr = flags; // update physics attributes // allow things to became debris, so they use different physics... if (targetactor->spr.statnum == kStatThing) ChangeActorStat(targetactor, 0); // set random goal ang for swimming so they start turning if ((flags & kPhysDebrisSwim) && targetactor->vel.isZero()) targetactor->xspr.goalAng = (targetactor->spr.Angles.Yaw + DAngle::fromBuild(Random3(kAng45))).Normalized360(); if (targetactor->xspr.physAttr & kPhysDebrisVector) targetactor->spr.cstat |= CSTAT_SPRITE_BLOCK_HITSCAN; gPhysSpritesList[nIndex] = targetactor; if (nIndex >= gPhysSpritesCount) gPhysSpritesCount++; getSpriteMassBySize(targetactor); // create physics cache } // removing physics from sprite in list (don't remove sprite from list) } else if (nIndex != -1) { targetactor->xspr.physAttr = flags; targetactor->vel.Zero(); if (targetactor->spr.lotag >= kThingBase && targetactor->spr.lotag < kThingMax) ChangeActorStat(targetactor, kStatThing); // if it was a thing - restore statnum } break; } } // data4 = sprite cstat if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) { // relative if (flag1) { if (flag4) targetactor->spr.cstat &= ~ESpriteFlags::FromInt(data4); else targetactor->spr.cstat |= ESpriteFlags::FromInt(data4); } // absolute else { auto oldstat = targetactor->spr.cstat; // set new cstat if ((sourceactor->spr.flags & kModernTypeFlag1)) targetactor->spr.cstat |= ESpriteFlags::FromInt(sourceactor->xspr.data4); // relative else targetactor->spr.cstat = ESpriteFlags::FromInt(sourceactor->xspr.data4 & 0xffff); // absolute // and handle exceptions if ((oldstat & CSTAT_SPRITE_BLOOD_BIT1)) targetactor->spr.cstat |= CSTAT_SPRITE_BLOOD_BIT1; //kSpritePushable if ((oldstat & CSTAT_SPRITE_YCENTER)) targetactor->spr.cstat |= CSTAT_SPRITE_YCENTER; targetactor->spr.cstat = (targetactor->spr.cstat & ~CSTAT_SPRITE_MOVE_MASK) | (oldstat & CSTAT_SPRITE_MOVE_MASK); #if 0 // looks very broken. if (old & 0x6000) { if (!(targetactor->spr.cstat & 0x6000)) targetactor->spr.cstat |= 0x6000; // kSpriteMoveMask if ((old & 0x0) && !(targetactor->spr.cstat & 0x0)) targetactor->spr.cstat |= 0x0; // kSpriteMoveNone else if ((old & 0x2000) && !(targetactor->spr.cstat & 0x2000)) targetactor->spr.cstat |= 0x2000; // kSpriteMoveForward, kSpriteMoveFloor else if ((old & 0x4000) && !(targetactor->spr.cstat & 0x4000)) targetactor->spr.cstat |= 0x4000; // kSpriteMoveReverse, kSpriteMoveCeiling } #endif } } } break; case OBJ_SECTOR: { if (!pSector) return; XSECTOR* pXSector = &pSector->xs(); // data1 = sector underwater status and depth level if (sourceactor->xspr.data1 >= 0 && sourceactor->xspr.data1 < 2) { pXSector->Underwater = (sourceactor->xspr.data1) ? true : false; auto aLower = barrier_cast(pSector->lowerLink); DBloodActor* aUpper = nullptr; if (aLower) { // must be sure we found exact same upper link for (auto& sec : sector) { aUpper = barrier_cast(sec.upperLink); if (aUpper == nullptr || aUpper->xspr.data1 != aLower->xspr.data1) { aUpper = nullptr; continue; } break; } } // treat sectors that have links, so warp can detect underwater status properly if (aLower) { if (pXSector->Underwater) { switch (aLower->spr.type) { case kMarkerLowStack: case kMarkerLowLink: aLower->xspr.sysData1 = aLower->spr.type; aLower->spr.type = kMarkerLowWater; break; default: if (pSector->portalflags != PORTAL_SECTOR_CEILING) aLower->xspr.sysData1 = kMarkerLowLink; else aLower->xspr.sysData1 = kMarkerLowStack; break; } } else if (aLower->xspr.sysData1 > 0) aLower->spr.type = aLower->xspr.sysData1; else if (pSector->portalflags != PORTAL_SECTOR_CEILING) aLower->spr.type = kMarkerLowLink; else aLower->spr.type = kMarkerLowStack; } if (aUpper) { if (pXSector->Underwater) { switch (aUpper->spr.type) { case kMarkerUpStack: case kMarkerUpLink: aUpper->xspr.sysData1 = aUpper->spr.type; aUpper->spr.type = kMarkerUpWater; break; default: if (pSector->portalflags != PORTAL_SECTOR_FLOOR) aUpper->xspr.sysData1 = kMarkerUpLink; else aUpper->xspr.sysData1 = kMarkerUpStack; break; } } else if (aUpper->xspr.sysData1 > 0) aUpper->spr.type = aUpper->xspr.sysData1; else if (pSector->portalflags != PORTAL_SECTOR_FLOOR) aUpper->spr.type = kMarkerUpLink; else aUpper->spr.type = kMarkerUpStack; } // search for dudes in this sector and change their underwater status BloodSectIterator it(pSector); while (auto iactor = it.Next()) { if (iactor->spr.statnum != kStatDude || !iactor->IsDudeActor() || !iactor->hasX()) continue; DBloodPlayer* pPlayer = getPlayerById(iactor->spr.type); if (pXSector->Underwater) { if (aLower) iactor->xspr.medium = (aLower->spr.type == kMarkerUpGoo) ? kMediumGoo : kMediumWater; if (pPlayer) { int waterPal = kMediumWater; if (aLower) { if (aLower->xspr.data2 > 0) waterPal = aLower->xspr.data2; else if (aLower->spr.type == kMarkerUpGoo) waterPal = kMediumGoo; } pPlayer->nWaterPal = waterPal; pPlayer->posture = kPostureSwim; pPlayer->GetActor()->xspr.burnTime = 0; } } else { iactor->xspr.medium = kMediumNormal; if (pPlayer) { pPlayer->posture = (!(pPlayer->cmd.ucmd.actions & SB_CROUCH)) ? kPostureStand : kPostureCrouch; pPlayer->nWaterPal = 0; } } } } else if (sourceactor->xspr.data1 > 9) pXSector->Depth = 7; else if (sourceactor->xspr.data1 > 1) pXSector->Depth = sourceactor->xspr.data1 - 2; // data2 = sector visibility if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) pSector->visibility = ClipRange(sourceactor->xspr.data2, 0, 234); // data3 = sector ceil cstat if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) { // relative if (flag1) { if (flag4) pSector->ceilingstat &= ~ESectorFlags::FromInt(data3); else pSector->ceilingstat |= ESectorFlags::FromInt(data3); } // absolute else pSector->ceilingstat = ESectorFlags::FromInt(data3); } // data4 = sector floor cstat if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) { // relative if (flag1) { if (flag4) pSector->floorstat &= ~ESectorFlags::FromInt(data4); else pSector->floorstat |= ESectorFlags::FromInt(data4); } // absolute else pSector->floorstat = ESectorFlags::FromInt(data4); } } break; // no TX id case -1: // data2 = global visibility if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) gVisibility = ClipRange(sourceactor->xspr.data2, 0, 4096); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useVelocityChanger(DBloodActor* actor, sectortype* sect, DBloodActor* initiator, DBloodActor* pSprite) //void useVelocityChanger(XSPRITE* pXSource, int causerID, short objType, int objIndex) { const double kVelScale = 1 / 64.; // scaled 8 left, 14 right, i.e. 6 right altogether. const int kVelShift = 8; const int kScaleVal = 0x10000; int r = 0, t = 0; DAngle nAng = nullAngle; DVector3 vv(0, 0, 0); bool relative = (actor->spr.flags & kModernTypeFlag1); bool toDstAng = (actor->spr.flags & kModernTypeFlag2); bool toSrcAng = (actor->spr.flags & kModernTypeFlag4); bool toRndAng = (actor->spr.flags & kModernTypeFlag8); bool chgDstAng = !(actor->spr.flags & kModernTypeFlag16); bool toEvnAng = (toDstAng && toSrcAng && initiator != NULL); bool toAng = (toDstAng || toSrcAng || toEvnAng || toRndAng); bool toAng180 = (toRndAng && (toDstAng || toSrcAng || toEvnAng)); if (actor) { if ((r = MulScale(actor->xspr.data4 << kVelShift, kScaleVal, 14)) != 0) r = nnExtRandom(-r, r); double rr = FixedToFloat(r); if (valueIsBetween(actor->xspr.data3, -32767, 32767)) { vv.Z = actor->xspr.data3 * kVelScale; if (vv.Z != 0) vv.Z += rr; } if (!toAng) { if (valueIsBetween(actor->xspr.data1, -32767, 32767)) { vv.X = actor->xspr.data1 * kVelScale; if (vv.X != 0) vv.X += rr; } if (valueIsBetween(actor->xspr.data2, -32767, 32767)) { vv.Y = actor->xspr.data2 * kVelScale; if (vv.Y != 0) vv.Y += rr; } } else { if (toEvnAng) nAng = initiator->spr.Angles.Yaw; else if (toSrcAng) nAng = actor->spr.Angles.Yaw; else nAng = pSprite->spr.Angles.Yaw; if (!toAng180 && toRndAng) { auto tempang = nAng; while (tempang == nAng) nAng = RandomAngle(); } if (chgDstAng) changeSpriteAngle(pSprite, nAng); double v = actor->xspr.data1 * kVelScale; if (v != 0) v += rr; vv.XY() += nAng.ToVector() * v; } if (actor->xspr.physAttr) { t = 1; switch (pSprite->spr.statnum) { case kStatThing: break; case kStatFX: t = 0; [[fallthrough]]; case kStatDude: case kStatProjectile: if (actor->xspr.physAttr & kPhysMove) pSprite->spr.flags |= kPhysMove; else pSprite->spr.flags &= ~kPhysMove; if (actor->xspr.physAttr & kPhysGravity) pSprite->spr.flags |= kPhysGravity; else pSprite->spr.flags &= ~kPhysGravity; if (actor->xspr.physAttr & kPhysFalling) pSprite->spr.flags |= kPhysFalling; else pSprite->spr.flags &= ~kPhysFalling; break; } // debris physics for sprites that is allowed if (t && ((t = debrisGetIndex(pSprite)) >= 0 || (t = debrisGetFreeIndex()) >= 0)) { pSprite->addX(); if (pSprite->spr.statnum == kStatThing) { pSprite->spr.flags &= ~(kPhysMove | kPhysGravity | kPhysFalling); ChangeActorStat(pSprite, 0); } pSprite->xspr.physAttr = actor->xspr.physAttr, gPhysSpritesList[t] = pSprite; getSpriteMassBySize(pSprite); if (t >= gPhysSpritesCount) gPhysSpritesCount++; } } if (relative) { pSprite->vel += vv; } else { pSprite->vel == vv; } auto vAng = pSprite->vel.Angle(); if (toAng) { DAngle angl; if (toAng180) angl = DAngle180; else angl = nAng - vAng; auto velv = pSprite->vel.XY(); auto pt = rotatepoint(pSprite->spr.pos.XY(), velv, angl); pSprite->vel.XY() = pt; vAng = pSprite->vel.Angle(); } if (chgDstAng) changeSpriteAngle(pSprite, vAng); if (pSprite->ownerActor) { // hack to make player projectiles damage it's owner if (pSprite->spr.statnum == kStatProjectile && pSprite->IsPlayerActor()) pSprite->ownerActor = pSprite; } viewCorrectPrediction(); //if (pXSource->rxID == 157) //viewSetSystemMessage("%d: %d / %d / %d, C: %d", pSprite->spr.sectnum, pSprite->vel.X, pSprite->vel.Y, pSprite->vel.Z, sprite[causerID].type); } else if (sect) { BloodSectIterator it(sect); while(auto act = it.Next()) { useVelocityChanger(actor, nullptr, initiator, act); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useTeleportTarget(DBloodActor* sourceactor, DBloodActor* actor) { DBloodPlayer* pPlayer = getPlayerById(actor->spr.type); XSECTOR* pXSector = (sourceactor->sector()->hasX()) ? &sourceactor->sector()->xs() : nullptr; bool isDude = (!pPlayer && actor->IsDudeActor()); if (actor->sector() != sourceactor->sector()) ChangeActorSect(actor, sourceactor->sector()); actor->spr.pos.XY() =sourceactor->spr.pos.XY(); double zTop, zBot; GetActorExtents(sourceactor, &zTop, &zBot); actor->spr.pos.Z = zBot; clampSprite(actor, 0x01); if (sourceactor->spr.flags & kModernTypeFlag1) // force telefrag TeleFrag(actor, sourceactor->sector()); if (actor->spr.flags & kPhysGravity) actor->spr.flags |= kPhysFalling; if (pXSector) { if (pXSector->Enter && (pPlayer || (isDude && !pXSector->dudeLockout))) trTriggerSector(sourceactor->sector(), kCmdSectorEnter, actor); if (pXSector->Underwater) { DBloodActor* aUpper = nullptr; auto aLink = barrier_cast(sourceactor->sector()->lowerLink); if (aLink) { // must be sure we found exact same upper link for (auto& sec : sector) { aUpper = barrier_cast(sec.upperLink); if (aUpper == nullptr || aUpper->xspr.data1 != aLink->xspr.data1) { aUpper = nullptr; continue; } break; } } if (aUpper) actor->xspr.medium = (aLink->spr.type == kMarkerUpGoo) ? kMediumGoo : kMediumWater; if (pPlayer) { int waterPal = kMediumWater; if (aUpper) { if (aLink->xspr.data2 > 0) waterPal = aLink->xspr.data2; else if (aLink->spr.type == kMarkerUpGoo) waterPal = kMediumGoo; } pPlayer->nWaterPal = waterPal; pPlayer->posture = kPostureSwim; pPlayer->GetActor()->xspr.burnTime = 0; } } else { actor->xspr.medium = kMediumNormal; if (pPlayer) { pPlayer->posture = (!(pPlayer->cmd.ucmd.actions & SB_CROUCH)) ? kPostureStand : kPostureCrouch; pPlayer->nWaterPal = 0; } } } if (actor->spr.statnum == kStatDude && actor->IsDudeActor() && !actor->IsPlayerActor()) { auto pos = actor->xspr.TargetPos; auto target = actor->GetTarget(); aiInitSprite(actor); if (target != nullptr && target->IsDudeActor()) { actor->xspr.TargetPos = pos; actor->SetTarget(target); aiActivateDude(actor); } } #if 0 if (sourceactor->xspr.data2 == 1) { if (pPlayer) { pPlayer->angle.settarget(sourceactor->spr.angle); pPlayer->angle.lockinput(); } else if (isDude) sourceactor->xspr.goalAng = actor->spr.angle = sourceactor->spr.angle; else actor->spr.__int_angle = sourceactor->spr.angle; } #endif if (sourceactor->xspr.data3 == 1) { actor->vel.Zero(); } else if (sourceactor->xspr.data3 > 0) { // change movement direction according source angle if (sourceactor->xspr.data3 & kModernTypeFlag2) { auto velv = actor->vel.XY(); auto pt = rotatepoint(actor->spr.pos.XY(), velv, sourceactor->spr.Angles.Yaw - velv.Angle()); actor->vel.XY() = pt; } if (sourceactor->xspr.data3 & kModernTypeFlag4) actor->vel.Z = 0; } if (sourceactor->xspr.data2 == 1) changeSpriteAngle(actor, sourceactor->spr.Angles.Yaw); viewBackupSpriteLoc(actor); if (sourceactor->xspr.data4 > 0) sfxPlay3DSound(sourceactor, sourceactor->xspr.data4, -1, 0); if (pPlayer) { playerResetInertia(pPlayer); if (sourceactor->xspr.data2 == 1) pPlayer->zViewVel = pPlayer->zWeaponVel = 0; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useEffectGen(DBloodActor* sourceactor, DBloodActor* actor) { if (!actor) actor = sourceactor; int fxId = (sourceactor->xspr.data3 <= 0) ? sourceactor->xspr.data2 : sourceactor->xspr.data2 + Random(sourceactor->xspr.data3 + 1); if (!actor->hasX()) return; else if (fxId >= kEffectGenCallbackBase) { int length = sizeof(gEffectGenCallbacks) / sizeof(gEffectGenCallbacks[0]); if (fxId < kEffectGenCallbackBase + length) { fxId = gEffectGenCallbacks[fxId - kEffectGenCallbackBase]; evKillActor(actor, (CALLBACK_ID)fxId); evPostActor(actor, 0, (CALLBACK_ID)fxId); } } else if (valueIsBetween(fxId, 0, kFXMax)) { double pos, top, bottom; GetActorExtents(actor, &top, &bottom); DBloodActor* pEffect = nullptr; // select where exactly effect should be spawned switch (sourceactor->xspr.data4) { case 1: pos = bottom; break; case 2: // middle { auto tex = TexMan.GetGameTexture(actor->spr.spritetexture()); double center = tex->GetDisplayHeight() / 2 + tex->GetDisplayTopOffset(); pos = actor->spr.pos.Z + center * actor->spr.scale.Y; break; } case 3: case 4: if (actor->insector()) { if (sourceactor->xspr.data4 == 3) pos = getflorzofslopeptr(actor->sector(), actor->spr.pos.X, actor->spr.pos.Y); else pos = getceilzofslopeptr(actor->sector(), actor->spr.pos.X, actor->spr.pos.Y); break; } [[fallthrough]]; default: pos = top; break; } if ((pEffect = gFX.fxSpawnActor((FX_ID)fxId, actor->sector(), DVector3(actor->spr.pos.XY(), pos))) != nullptr) { pEffect->SetOwner(sourceactor); if (sourceactor->spr.flags & kModernTypeFlag1) { pEffect->spr.pal = sourceactor->spr.pal; pEffect->spr.xoffset = sourceactor->spr.xoffset; pEffect->spr.yoffset = sourceactor->spr.yoffset; pEffect->spr.scale = sourceactor->spr.scale; pEffect->spr.shade = sourceactor->spr.shade; } if (sourceactor->spr.flags & kModernTypeFlag2) { pEffect->spr.cstat = sourceactor->spr.cstat; if (pEffect->spr.cstat & CSTAT_SPRITE_INVISIBLE) pEffect->spr.cstat &= ~CSTAT_SPRITE_INVISIBLE; } if (sourceactor->spr.flags & kModernTypeFlag4) { pEffect->spr.Angles.Yaw = sourceactor->spr.Angles.Yaw; } if (pEffect->spr.cstat & CSTAT_SPRITE_ONE_SIDE) pEffect->spr.cstat &= ~CSTAT_SPRITE_ONE_SIDE; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSectorWindGen(DBloodActor* sourceactor, sectortype* pSector) { XSECTOR* pXSector = nullptr; if (pSector != nullptr) { pXSector = &pSector->xs(); } else if (sourceactor->sector()->hasX()) { pSector = sourceactor->sector(); pXSector = &pSector->xs(); } else { pSector = sourceactor->sector(); pSector->allocX(); pXSector = &pSector->xs(); pXSector->windAlways = 1; } int windVel = ClipRange(sourceactor->xspr.data2, 0, 32767); if ((sourceactor->xspr.data1 & 0x0001)) windVel = nnExtRandom(0, windVel); // process vertical wind in nnExtProcessSuperSprites(); if ((sourceactor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)) { sourceactor->xspr.sysData2 = windVel << 1; return; } pXSector->windVel = windVel; if ((sourceactor->spr.flags & kModernTypeFlag1)) pXSector->panAlways = pXSector->windAlways = 1; DAngle angle = sourceactor->spr.Angles.Yaw; if (sourceactor->xspr.data4 <= 0) { if ((sourceactor->xspr.data1 & 0x0002)) { while (sourceactor->spr.Angles.Yaw == angle) sourceactor->spr.Angles.Yaw = RandomAngle(); } } else if (sourceactor->spr.cstat & CSTAT_SPRITE_MOVE_FORWARD) sourceactor->spr.Angles.Yaw += mapangle(sourceactor->xspr.data4); else if (sourceactor->spr.cstat & CSTAT_SPRITE_MOVE_REVERSE) sourceactor->spr.Angles.Yaw -= mapangle(sourceactor->xspr.data4); else if (sourceactor->xspr.sysData1 == 0) { angle += mapangle(sourceactor->xspr.data4); if (angle >= DAngle180) sourceactor->xspr.sysData1 = 1; sourceactor->spr.Angles.Yaw = min(angle, DAngle180); } else { angle -= mapangle(sourceactor->xspr.data4); if (angle <= -DAngle180) sourceactor->xspr.sysData1 = 0; sourceactor->spr.Angles.Yaw = max(angle, -DAngle180); } pXSector->windAng = sourceactor->spr.Angles.Yaw; if (sourceactor->xspr.data3 > 0 && sourceactor->xspr.data3 < 4) { switch (sourceactor->xspr.data3) { case 1: pXSector->panFloor = true; pXSector->panCeiling = false; break; case 2: pXSector->panFloor = false; pXSector->panCeiling = true; break; case 3: pXSector->panFloor = true; pXSector->panCeiling = true; break; } if (pXSector->panCeiling) { StartInterpolation(pSector, Interp_Sect_CeilingPanX); StartInterpolation(pSector, Interp_Sect_CeilingPanY); } if (pXSector->panFloor) { StartInterpolation(pSector, Interp_Sect_FloorPanX); StartInterpolation(pSector, Interp_Sect_FloorPanY); } int oldPan = pXSector->panVel; pXSector->panAngle = pXSector->windAng; pXSector->panVel = pXSector->windVel; // add to panList if panVel was set to 0 previously if (oldPan == 0 && pXSector->panVel != 0) { if (!panList.Contains(pSector)) panList.Push(pSector); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSpriteDamager(DBloodActor* sourceactor, int objType, sectortype* targSect, DBloodActor* targetactor) { sectortype* pSector = sourceactor->sector(); double top, bottom; bool floor, ceil, wall, enter; switch (objType) { case OBJ_SPRITE: damageSprites(sourceactor, targetactor); break; case OBJ_SECTOR: { GetActorExtents(sourceactor, &top, &bottom); floor = (bottom >= pSector->floorz); ceil = (top <= pSector->ceilingz); wall = (sourceactor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_WALL); enter = (!floor && !ceil && !wall); BloodSectIterator it(targSect); while (auto iactor = it.Next()) { auto& hit = iactor->hit; if (!iactor->IsDudeActor() || !iactor->hasX()) continue; else if (enter) damageSprites(sourceactor, iactor); else if (floor && hit.florhit.type == kHitSector && hit.florhit.hitSector == targSect) damageSprites(sourceactor, iactor); else if (ceil && hit.ceilhit.type == kHitSector && hit.ceilhit.hitSector == targSect) damageSprites(sourceactor, iactor); else if (wall && hit.hit.type == kHitWall && hit.hit.hitWall->sectorp() == targSect) damageSprites(sourceactor, iactor); } break; } case -1: { BloodStatIterator it(kStatDude); while (auto iactor = it.Next()) { if (iactor->spr.statnum != kStatDude) continue; switch (sourceactor->xspr.data1) { case 667: if (iactor->IsPlayerActor()) continue; damageSprites(sourceactor, iactor); break; case 668: if (iactor->IsPlayerActor()) continue; damageSprites(sourceactor, iactor); break; default: damageSprites(sourceactor, iactor); break; } } break; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void damageSprites(DBloodActor* sourceactor, DBloodActor* actor) { if (!actor->IsDudeActor() || !actor->hasX() || actor->xspr.health <= 0 || sourceactor->xspr.data3 < 0) return; int health = 0; DBloodPlayer* pPlayer = getPlayerById(actor->spr.type); int dmgType = (sourceactor->xspr.data2 >= kDmgFall) ? ClipHigh(sourceactor->xspr.data2, kDmgElectric) : -1; int dmg = actor->xspr.health << 4; int armor[3]; bool godMode = (pPlayer && ((dmgType >= 0 && pPlayer->damageControl[dmgType]) || powerupCheck(pPlayer, kPwUpDeathMask) || pPlayer->godMode)); // kneeling if (godMode || actor->xspr.locked) return; else if (sourceactor->xspr.data3) { if (sourceactor->spr.flags & kModernTypeFlag1) dmg = ClipHigh(sourceactor->xspr.data3 << 1, 65535); else if (actor->xspr.sysData2 > 0) dmg = (ClipHigh(actor->xspr.sysData2 << 4, 65535) * sourceactor->xspr.data3) / kPercFull; else dmg = ((getDudeInfo(actor->spr.type)->startHealth << 4) * sourceactor->xspr.data3) / kPercFull; health = actor->xspr.health - dmg; } if (dmgType >= kDmgFall) { if (dmg < (int)actor->xspr.health << 4) { if (!nnExtIsImmune(actor, dmgType, 0)) { if (pPlayer) { playerDamageArmor(pPlayer, (DAMAGE_TYPE)dmgType, dmg); for (int i = 0; i < 3; armor[i] = pPlayer->armor[i], pPlayer->armor[i] = 0, i++); actDamageSprite(sourceactor, actor, (DAMAGE_TYPE)dmgType, dmg); for (int i = 0; i < 3; pPlayer->armor[i] = armor[i], i++); } else { actDamageSprite(sourceactor, actor, (DAMAGE_TYPE)dmgType, dmg); } } else { //Printf(PRINT_HIGH, "Dude type %d is immune to damage type %d!", actor->spr.type, dmgType); } } else if (!pPlayer) actKillDude(sourceactor, actor, (DAMAGE_TYPE)dmgType, dmg); else playerDamageSprite(sourceactor, pPlayer, (DAMAGE_TYPE)dmgType, dmg); } else if ((actor->xspr.health = ClipLow(health, 1)) > 16); else if (!pPlayer) actKillDude(sourceactor, actor, kDamageBullet, dmg); else playerDamageSprite(sourceactor, pPlayer, kDamageBullet, dmg); if (actor->xspr.health > 0) { if (!(sourceactor->spr.flags & kModernTypeFlag8)) actor->xspr.health = health; bool showEffects = !(sourceactor->spr.flags & kModernTypeFlag2); // show it by default bool forceRecoil = (sourceactor->spr.flags & kModernTypeFlag4); if (showEffects) { switch (dmgType) { case kDmgBurn: if (actor->xspr.burnTime > 0) break; actBurnSprite(sourceactor, actor, ClipLow(dmg >> 1, 128)); evKillActor(actor, kCallbackFXFlameLick); evPostActor(actor, 0, kCallbackFXFlameLick); // show flames break; case kDmgElectric: forceRecoil = true; // show tesla recoil animation break; case kDmgBullet: evKillActor(actor, kCallbackFXBloodSpurt); for (int i = 1; i < 6; i++) { if (Chance(0x16000 >> i)) fxSpawnBlood(actor, dmg << 4); } break; case kDmgChoke: if (!pPlayer || !Chance(0x2000)) break; else pPlayer->blindEffect += dmg << 2; } } if (forceRecoil && !pPlayer) { actor->xspr.data3 = 32767; actor->dudeExtra.teslaHit = (dmgType == kDmgElectric) ? 1 : 0; if (actor->xspr.aiState->stateType != kAiStateRecoil) RecoilDude(actor); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSeqSpawnerGen(DBloodActor* sourceactor, int objType, sectortype* pSector, walltype* pWall, DBloodActor* iactor) { if (sourceactor->xspr.data2 > 0 && !getSequence(sourceactor->xspr.data2)) { Printf(PRINT_HIGH, "Missing sequence #%d", sourceactor->xspr.data2); return; } switch (objType) { case OBJ_SECTOR: { if (sourceactor->xspr.data2 <= 0) { if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 1) seqKill(SS_FLOOR, pSector); if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 2) seqKill(SS_CEILING, pSector); } else { if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 1) seqSpawn(sourceactor->xspr.data2, SS_FLOOR, pSector, -1); if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 2) seqSpawn(sourceactor->xspr.data2, SS_CEILING, pSector, -1); } return; } case OBJ_WALL: { if (sourceactor->xspr.data2 <= 0) { if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 1) seqKill(SS_WALL, pWall); if ((sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 2) && (pWall->cstat & CSTAT_WALL_MASKED)) seqKill(SS_MASKED, pWall); } else { if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 1) seqSpawn(sourceactor->xspr.data2, SS_WALL, pWall, -1); if (sourceactor->xspr.data3 == 3 || sourceactor->xspr.data3 == 2) { if (!pWall->twoSided()) { if (sourceactor->xspr.data3 == 3) seqSpawn(sourceactor->xspr.data2, SS_WALL, pWall, -1); } else { if (!(pWall->cstat & CSTAT_WALL_MASKED)) pWall->cstat |= CSTAT_WALL_MASKED; seqSpawn(sourceactor->xspr.data2, SS_MASKED, pWall, -1); } } if (sourceactor->xspr.data4 > 0) { DVector3 cpos; cpos.XY() = pWall->center(); auto pMySector = pWall->sectorp(); double ceilZ, floorZ; calcSlope(pSector, cpos, &ceilZ, &floorZ); double ceilZ2, floorZ2; calcSlope(pWall->nextSector(), cpos, &ceilZ2, &floorZ2); ceilZ = max(ceilZ, ceilZ2); floorZ = min(floorZ, floorZ2); cpos.Z = (ceilZ + floorZ) * 0.5; sfxPlay3DSound(cpos, sourceactor->xspr.data4, pSector); } } return; } case OBJ_SPRITE: { if (sourceactor->xspr.data2 <= 0) seqKill(iactor); else if (iactor->insector()) { if (sourceactor->xspr.data3 > 0) { auto spawned = InsertSprite(iactor->sector(), kStatDecoration); if (spawned != nullptr) { double top, bottom; GetActorExtents(spawned, &top, &bottom); DVector3 pos = iactor->spr.pos; switch (sourceactor->xspr.data3) { default: break; case 2: pos.Z = bottom; break; case 3: pos.Z = top; break; case 4: { // this had no value shift and no repeat handling, which looks like a bug. auto tex = TexMan.GetGameTexture(iactor->spr.spritetexture()); double center = tex->GetDisplayHeight() / 2 + tex->GetDisplayTopOffset(); pos.Z += center * iactor->spr.scale.Y; break; } case 5: case 6: if (!iactor->insector()) pos.Z = top; else pos.Z = ((sourceactor->xspr.data3 == 5) ? getflorzofslopeptr(spawned->sector(), spawned->spr.pos) : getceilzofslopeptr(spawned->sector(), spawned->spr.pos)); break; } spawned->spr.pos = pos; spawned->addX(); seqSpawn(sourceactor->xspr.data2, spawned, -1); if (sourceactor->spr.flags & kModernTypeFlag1) { spawned->spr.pal = sourceactor->spr.pal; spawned->spr.shade = sourceactor->spr.shade; spawned->spr.scale = sourceactor->spr.scale; spawned->spr.xoffset = sourceactor->spr.xoffset; spawned->spr.yoffset = sourceactor->spr.yoffset; } if (sourceactor->spr.flags & kModernTypeFlag2) { spawned->spr.cstat |= sourceactor->spr.cstat; } if (sourceactor->spr.flags & kModernTypeFlag4) { spawned->spr.Angles.Yaw = sourceactor->spr.Angles.Yaw; } // should be: the more is seqs, the shorter is timer evPostActor(spawned, 1000, kCallbackRemove); } } else { seqSpawn(sourceactor->xspr.data2, iactor, -1); } if (sourceactor->xspr.data4 > 0) sfxPlay3DSound(iactor, sourceactor->xspr.data4, -1, 0); } return; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void condPush(DBloodActor* actor, const EventObject& iactor) { actor->condition[0] = iactor; } void condPush(DBloodActor* actor, DBloodActor* iactor) { actor->condition[0] = EventObject(iactor); } void condPush(DBloodActor* actor, walltype* iactor) { actor->condition[0] = EventObject(iactor); } void condPush(DBloodActor* actor, sectortype* iactor) { actor->condition[0] = EventObject(iactor); } EventObject condGet(DBloodActor* actor) { return actor->condition[0]; } void condBackup(DBloodActor* actor) { actor->condition[1] = actor->condition[0]; } void condRestore(DBloodActor* actor) { actor->condition[0] = actor->condition[1]; } // normal comparison bool condCmp(int val, int arg1, int arg2, int comOp) { if (comOp & 0x2000) return (comOp & CSTAT_SPRITE_BLOCK) ? (val > arg1) : (val >= arg1); // blue sprite else if (comOp & 0x4000) return (comOp & CSTAT_SPRITE_BLOCK) ? (val < arg1) : (val <= arg1); // green sprite else if (comOp & CSTAT_SPRITE_BLOCK) { if (arg1 > arg2) I_Error("Value of argument #1 (%d) must be less than value of argument #2 (%d)", arg1, arg2); return (val >= arg1 && val <= arg2); } else return (val == arg1); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void condError(DBloodActor* aCond, const char* pzFormat, ...) { char buffer[256]; char buffer2[512]; FString condType = "Unknown"; for (int i = 0; i < 7; i++) { if (aCond->xspr.data1 < gCondTypeNames[i].rng1 || aCond->xspr.data1 >= gCondTypeNames[i].rng2) continue; condType = gCondTypeNames[i].name; condType.ToUpper(); break; } snprintf(buffer, 256, "\n\n%s CONDITION RX: %d, TX: %d, SPRITE: #%d RETURNS:\n", condType.GetChars(), aCond->xspr.rxID, aCond->xspr.txID, aCond->GetIndex()); va_list args; va_start(args, pzFormat); vsnprintf(buffer2, 512, pzFormat, args); I_Error("%s%s", buffer, buffer2); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckGame(DBloodActor* aCond, const EVENT& event, int cmpOp, bool PUSH) { int cond = aCond->xspr.data1; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; int arg3 = aCond->xspr.data4; switch (cond) { case 1: return condCmp(gFrameCount / (kTicsPerSec * 60), arg1, arg2, cmpOp); // compare level minutes case 2: return condCmp((gFrameCount / kTicsPerSec) % 60, arg1, arg2, cmpOp); // compare level seconds case 3: return condCmp(((gFrameCount % kTicsPerSec) * 33) / 10, arg1, arg2, cmpOp); // compare level mseconds case 4: return condCmp(gFrameCount, arg1, arg2, cmpOp); // compare level time (unsafe) case 5: return condCmp(Level.kills.got, arg1, arg2, cmpOp); // compare current global kills counter case 6: return condCmp(Level.kills.max, arg1, arg2, cmpOp); // compare total global kills counter case 7: return condCmp(Level.secrets.got, arg1, arg2, cmpOp); // compare how many secrets found case 8: return condCmp(Level.secrets.max, arg1, arg2, cmpOp); // compare total secrets /*----------------------------------------------------------------------------------------------------------------------------------*/ case 20: return condCmp(gVisibility, arg1, arg2, cmpOp); // compare global visibility value /*----------------------------------------------------------------------------------------------------------------------------------*/ case 30: return Chance((0x10000 * arg3) / kPercFull); // check chance case 31: return condCmp(nnExtRandom(arg1, arg2), arg1, arg2, cmpOp); /*----------------------------------------------------------------------------------------------------------------------------------*/ case 47: { BloodStatIterator it(ClipRange(arg3, 0, kMaxStatus)); int c = 0; while (it.Next()) c++; return condCmp(c, arg1, arg2, cmpOp); // compare counter of specific statnum sprites } case 48: return condCmp(Numsprites, arg1, arg2, cmpOp); // compare counter of total sprites } condError(aCond, "Unexpected condition id (%d)!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckMixed(DBloodActor* aCond, const EVENT& event, int cmpOp, bool PUSH) { //int var = -1; int cond = aCond->xspr.data1 - kCondMixedBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); switch (cond) { case 0: return (eob.isSector()); case 5: return (eob.isWall()); case 10: return (eob.isActor() && eob.actor()); case 15: // x-index is fine? if (eob.isWall()) return eob.wall()->hasX(); if (eob.isSector()) return eob.sector()->hasX(); if (eob.isActor()) return eob.actor() && eob.actor()->hasX(); break; case 20: // type in a range? if (eob.isWall()) return condCmp(eob.wall()->type, arg1, arg2, cmpOp); if (eob.isSector()) return condCmp(eob.sector()->type, arg1, arg2, cmpOp); if (eob.isActor()) return eob.actor() && condCmp(eob.actor()->spr.type, arg1, arg2, cmpOp); break; case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31: case 32: case 33: if (eob.isWall()) { walltype* pObj = eob.wall(); switch (cond) { case 24: return condCmp(GetExtInfo(pObj->walltexture).surftype, arg1, arg2, cmpOp); case 25: return condCmp(legacyTileNum(pObj->walltexture), arg1, arg2, cmpOp); case 26: return condCmp(pObj->pal, arg1, arg2, cmpOp); case 27: return condCmp(pObj->shade, arg1, arg2, cmpOp); case 28: return (arg3) ? condCmp((pObj->cstat & EWallFlags::FromInt(arg3)), arg1, arg2, cmpOp) : (pObj->cstat & EWallFlags::FromInt(arg1)); case 29: return (arg3) ? condCmp((pObj->hitag & arg3), arg1, arg2, cmpOp) : (pObj->hitag & arg1); case 30: return condCmp(pObj->xrepeat, arg1, arg2, cmpOp); case 31: return condCmp(pObj->xpan(), arg1, arg2, cmpOp); case 32: return condCmp(pObj->yrepeat, arg1, arg2, cmpOp); case 33: return condCmp(pObj->ypan(), arg1, arg2, cmpOp); } } else if (eob.isActor()) { auto actor = eob.actor(); if (!actor) break; switch (cond) { case 24: return condCmp(GetExtInfo(actor->spr.spritetexture()).surftype, arg1, arg2, cmpOp); case 25: return condCmp(legacyTileNum(actor->spr.spritetexture()), arg1, arg2, cmpOp); case 26: return condCmp(actor->spr.pal, arg1, arg2, cmpOp); case 27: return condCmp(actor->spr.shade, arg1, arg2, cmpOp); case 28: return (arg3) ? condCmp((actor->spr.cstat & ESpriteFlags::FromInt(arg3)), arg1, arg2, cmpOp) : (actor->spr.cstat & ESpriteFlags::FromInt(arg1)); case 29: return (arg3) ? condCmp((actor->spr.hitag & arg3), arg1, arg2, cmpOp) : (actor->spr.hitag & arg1); case 30: return condCmp(int(actor->spr.scale.X * INV_REPEAT_SCALE), arg1, arg2, cmpOp); case 31: return condCmp(actor->spr.xoffset, arg1, arg2, cmpOp); case 32: return condCmp(int(actor->spr.scale.Y * INV_REPEAT_SCALE), arg1, arg2, cmpOp); case 33: return condCmp(actor->spr.yoffset, arg1, arg2, cmpOp); } } else if (eob.sector()) { sectortype* pObj = eob.sector(); switch (cond) { case 24: switch (arg3) { default: return (condCmp(GetExtInfo(pObj->floortexture).surftype, arg1, arg2, cmpOp) || condCmp(GetExtInfo(pObj->ceilingtexture).surftype, arg1, arg2, cmpOp)); case 1: return condCmp(GetExtInfo(pObj->floortexture).surftype, arg1, arg2, cmpOp); case 2: return condCmp(GetExtInfo(pObj->ceilingtexture).surftype, arg1, arg2, cmpOp); } break; case 25: switch (arg3) { default: return (condCmp(legacyTileNum(pObj->floortexture), arg1, arg2, cmpOp) || condCmp(legacyTileNum(pObj->ceilingtexture), arg1, arg2, cmpOp)); case 1: return condCmp(legacyTileNum(pObj->floortexture), arg1, arg2, cmpOp); case 2: return condCmp(legacyTileNum(pObj->ceilingtexture), arg1, arg2, cmpOp); } break; case 26: switch (arg3) { default: return (condCmp(pObj->floorpal, arg1, arg2, cmpOp) || condCmp(pObj->ceilingpal, arg1, arg2, cmpOp)); case 1: return condCmp(pObj->floorpal, arg1, arg2, cmpOp); case 2: return condCmp(pObj->ceilingpal, arg1, arg2, cmpOp); } break; case 27: switch (arg3) { default: return (condCmp(pObj->floorshade, arg1, arg2, cmpOp) || condCmp(pObj->ceilingshade, arg1, arg2, cmpOp)); case 1: return condCmp(pObj->floorshade, arg1, arg2, cmpOp); case 2: return condCmp(pObj->ceilingshade, arg1, arg2, cmpOp); } break; case 28: { auto a = ESectorFlags::FromInt(arg1); switch (arg3) { default: return ((pObj->floorstat & a) || (pObj->ceilingstat & a)); case 1: return (pObj->floorstat & a); case 2: return (pObj->ceilingstat & a); } break; } case 29: return (arg3) ? condCmp((pObj->hitag & arg3), arg1, arg2, cmpOp) : (pObj->hitag & arg1); case 30: return condCmp(pObj->floorxpan(), arg1, arg2, cmpOp); case 31: return condCmp(pObj->ceilingxpan(), arg1, arg2, cmpOp); case 32: return condCmp(pObj->floorypan(), arg1, arg2, cmpOp); case 33: return condCmp(pObj->ceilingypan(), arg1, arg2, cmpOp); } } break; case 41: case 42: case 43: case 44: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: case 58: case 59: case 70: case 71: if (eob.isWall()) { auto pObj = eob.wall(); if (!pObj->hasX()) return condCmp(0, arg1, arg2, cmpOp); XWALL* pXObj = &pObj->xw(); switch (cond) { case 41: return condCmp(pXObj->data, arg1, arg2, cmpOp); case 50: return condCmp(pXObj->rxID, arg1, arg2, cmpOp); case 51: return condCmp(pXObj->txID, arg1, arg2, cmpOp); case 52: return pXObj->locked; case 53: return pXObj->triggerOn; case 54: return pXObj->triggerOff; case 55: return pXObj->triggerOnce; case 56: return pXObj->isTriggered; case 57: return pXObj->state; case 58: return condCmp((kPercFull * pXObj->busy) / 65536, arg1, arg2, cmpOp); case 59: return pXObj->dudeLockout; case 70: switch (arg3) { default: return (condCmp(seqGetID(SS_WALL, pObj), arg1, arg2, cmpOp) || condCmp(seqGetID(SS_MASKED, pObj), arg1, arg2, cmpOp)); case 1: return condCmp(seqGetID(SS_WALL, pObj), arg1, arg2, cmpOp); case 2: return condCmp(seqGetID(SS_MASKED, pObj), arg1, arg2, cmpOp); } break; case 71: switch (arg3) { default: return (condCmp(seqGetStatus(SS_WALL, pObj), arg1, arg2, cmpOp) || condCmp(seqGetStatus(SS_MASKED, pObj), arg1, arg2, cmpOp)); case 1: return condCmp(seqGetStatus(SS_WALL, pObj), arg1, arg2, cmpOp); case 2: return condCmp(seqGetStatus(SS_MASKED, pObj), arg1, arg2, cmpOp); } break; } } else if (eob.isActor()) { auto objActor = eob.actor(); if (!objActor) break; if (!objActor->hasX()) return condCmp(0, arg1, arg2, cmpOp); switch (cond) { case 41: case 42: case 43: case 44: return condCmp(getDataFieldOfObject(eob, 1 + cond - 41), arg1, arg2, cmpOp); case 50: return condCmp(objActor->xspr.rxID, arg1, arg2, cmpOp); case 51: return condCmp(objActor->xspr.txID, arg1, arg2, cmpOp); case 52: return objActor->xspr.locked; case 53: return objActor->xspr.triggerOn; case 54: return objActor->xspr.triggerOff; case 55: return objActor->xspr.triggerOnce; case 56: return objActor->xspr.isTriggered; case 57: return objActor->xspr.state; case 58: return condCmp((kPercFull * objActor->xspr.busy) / 65536, arg1, arg2, cmpOp); case 59: return objActor->xspr.DudeLockout; case 70: return condCmp(seqGetID(objActor), arg1, arg2, cmpOp); case 71: return condCmp(seqGetStatus(objActor), arg1, arg2, cmpOp); } } else if (eob.isSector()) { auto pObj = eob.sector(); if (!pObj->hasX()) return condCmp(0, arg1, arg2, cmpOp); XSECTOR* pXObj = &pObj->xs(); switch (cond) { case 41: return condCmp(pXObj->data, arg1, arg2, cmpOp); case 50: return condCmp(pXObj->rxID, arg1, arg2, cmpOp); case 51: return condCmp(pXObj->txID, arg1, arg2, cmpOp); case 52: return pXObj->locked; case 53: return pXObj->triggerOn; case 54: return pXObj->triggerOff; case 55: return pXObj->triggerOnce; case 56: return pXObj->isTriggered; case 57: return pXObj->state; case 58: return condCmp((kPercFull * pXObj->busy) / 65536, arg1, arg2, cmpOp); case 59: return pXObj->dudeLockout; case 70: // wall??? switch (arg3) { default: return (condCmp(seqGetID(SS_CEILING, pObj), arg1, arg2, cmpOp) || condCmp(seqGetID(SS_FLOOR, pObj), arg1, arg2, cmpOp)); case 1: return condCmp(seqGetID(SS_CEILING, pObj), arg1, arg2, cmpOp); case 2: return condCmp(seqGetID(SS_FLOOR, pObj), arg1, arg2, cmpOp); } break; case 71: switch (arg3) { default: return (condCmp(seqGetStatus(SS_CEILING, pObj), arg1, arg2, cmpOp) || condCmp(seqGetStatus(SS_FLOOR, pObj), arg1, arg2, cmpOp)); case 1: return condCmp(seqGetStatus(SS_CEILING, pObj), arg1, arg2, cmpOp); case 2: return condCmp(seqGetStatus(SS_FLOOR, pObj), arg1, arg2, cmpOp); } break; } } break; case 99: return condCmp(event.cmd, arg1, arg2, cmpOp); // this codition received specified command? } condError(aCond, "Unexpected condition id (%d)!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckSector(DBloodActor* aCond, int cmpOp, bool PUSH) { int cond = aCond->xspr.data1 - kCondSectorBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; //int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); if (!eob.isSector()) condError(aCond, "Sector expected, got %s", eob.description().GetChars()); sectortype* pSect = eob.sector(); XSECTOR* pXSect = pSect->hasX() ? &pSect->xs() : nullptr; if (cond < (kCondRange >> 1)) { switch (cond) { default: break; case 0: return condCmp(pSect->visibility, arg1, arg2, cmpOp); case 5: return condCmp(pSect->floorheinum, arg1, arg2, cmpOp); case 6: return condCmp(pSect->ceilingheinum, arg1, arg2, cmpOp); case 10: // required sprite type is in current sector? BloodSectIterator it(pSect); while (auto iactor = it.Next()) { if (!condCmp(iactor->spr.type, arg1, arg2, cmpOp)) continue; else if (PUSH) condPush(aCond, iactor); return true; } return false; } } else if (pXSect) { switch (cond) { default: break; case 50: return pXSect->Underwater; case 51: return condCmp(pXSect->Depth, arg1, arg2, cmpOp); case 55: // compare floor height (in %) case 56: { // compare ceil height (in %) double h = 0, curH = 0; switch (pSect->type) { case kSectorZMotion: case kSectorRotate: case kSectorSlide: if (cond == 55)// 60) { h = max(abs(pXSect->onFloorZ - pXSect->offFloorZ), 1 / 256.); curH = abs(pSect->floorz - pXSect->offFloorZ); } else { h = max(abs(pXSect->onCeilZ - pXSect->offCeilZ), 1 / 256.); curH = abs(pSect->ceilingz - pXSect->offCeilZ); } return condCmp(int((kPercFull * curH) / h), arg1, arg2, cmpOp); default: condError(aCond, "Usupported sector type %d", pSect->type); return false; } } case 57: // this sector in movement? return !pXSect->unused1; } } else { switch (cond) { default: return false; case 55: case 56: return condCmp(0, arg1, arg2, cmpOp); } } condError(aCond, "Unexpected condition id (%d)!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckWall(DBloodActor* aCond, int cmpOp, bool PUSH) { int var = -1; int cond = aCond->xspr.data1 - kCondWallBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; //int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); if (!eob.isWall()) condError(aCond, "Wall expected, got %s", eob.description().GetChars()); walltype* pWall = eob.wall(); if (cond < (kCondRange >> 1)) { switch (cond) { default: break; case 0: return condCmp(legacyTileNum(pWall->overtexture), arg1, arg2, cmpOp); case 5: if (PUSH) condPush(aCond, pWall->sectorp()); return true; case 10: // this wall is a mirror? // must be as constants here return (pWall->portalflags == PORTAL_WALL_MIRROR); case 15: if (!pWall->twoSided()) return false; else if (PUSH) condPush(aCond, pWall->nextSector()); return true; case 20: if (!pWall->twoSided()) return false; else if (PUSH) condPush(aCond, pWall->nextWall()); return true; case 25: // next wall belongs to sector? (Note: This was 'sector of next wall' which is same as case 15 because we do not allow bad links!) if (!pWall->twoSided()) return false; else if (PUSH) condPush(aCond, pWall->nextSector()); return true; } } condError(aCond, "Unexpected condition id (%d)!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckPlayer(DBloodActor* aCond, int cmpOp, bool PUSH) { int var = -1; DBloodPlayer* pPlayer = NULL; int cond = aCond->xspr.data1 - kCondPlayerBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); if (!eob.isActor() || !eob.actor()) condError(aCond, "Sprite expected, got %s", eob.description().GetChars()); auto objActor = eob.actor(); for (int i = 0; i < kMaxPlayers; i++) { if (objActor != getPlayer(i)->GetActor()) continue; pPlayer = getPlayer(i); break; } if (!pPlayer) { condError(aCond, "Player expected, got %s", eob.description().GetChars()); return false; } switch (cond) { case 0: // check if this player is connected if (!condCmp(pPlayer->pnum + 1, arg1, arg2, cmpOp) || pPlayer->GetActor() == nullptr) return false; else if (PUSH) condPush(aCond, pPlayer->GetActor()); return true; case 1: return condCmp((gGameOptions.nGameType != 3) ? 0 : pPlayer->teamId + 1, arg1, arg2, cmpOp); // compare team case 2: return (arg1 > 0 && arg1 < 8 && pPlayer->hasKey[arg1 - 1]); case 3: return (arg1 > 0 && arg1 < 15 && pPlayer->hasWeapon[arg1 - 1]); case 4: return condCmp(pPlayer->curWeapon, arg1, arg2, cmpOp); case 5: return (arg1 > 0 && arg1 < 6 && condCmp(pPlayer->packSlots[arg1 - 1].curAmount, arg2, arg3, cmpOp)); case 6: return (arg1 > 0 && arg1 < 6 && pPlayer->packSlots[arg1 - 1].isActive); case 7: return condCmp(pPlayer->packItemId + 1, arg1, arg2, cmpOp); case 8: // check for powerup amount in seconds if (arg3 > 0 && arg3 <= (kMaxAllowedPowerup - (kMinAllowedPowerup << 1) + 1)) { var = (kMinAllowedPowerup + arg3) - 1; // allowable powerups return condCmp(pPlayer->pwUpTime[var] / 100, arg1, arg2, cmpOp); } condError(aCond, "Unexpected powerup #%d", arg3); return false; case 9: if (!pPlayer->fragger) return false; else if (PUSH) condPush(aCond, pPlayer->fragger); return true; case 10: // check keys pressed switch (arg1) { case 1: return (pPlayer->cmd.ucmd.vel.X > 0); // forward case 2: return (pPlayer->cmd.ucmd.vel.X < 0); // backward case 3: return (pPlayer->cmd.ucmd.vel.Y < 0); // left case 4: return (pPlayer->cmd.ucmd.vel.Y > 0); // right case 5: return !!(pPlayer->cmd.ucmd.actions & SB_JUMP); // jump case 6: return !!(pPlayer->cmd.ucmd.actions & SB_CROUCH); // crouch case 7: return !!(pPlayer->cmd.ucmd.actions & SB_FIRE); // normal fire weapon case 8: return !!(pPlayer->cmd.ucmd.actions & SB_ALTFIRE); // alt fire weapon case 9: return !!(pPlayer->cmd.ucmd.actions & SB_OPEN); // use default: condError(aCond, "Specify a correct key!"); break; } return false; case 11: return (pPlayer->isRunning); case 12: return (pPlayer->fallScream); // falling in abyss? case 13: return condCmp(pPlayer->lifeMode + 1, arg1, arg2, cmpOp); case 14: return condCmp(pPlayer->posture + 1, arg1, arg2, cmpOp); case 46: return condCmp(pPlayer->sceneQav, arg1, arg2, cmpOp); case 47: return (pPlayer->godMode || powerupCheck(pPlayer, kPwUpDeathMask)); case 48: return isShrunk(pPlayer->GetActor()); case 49: return isGrown(pPlayer->GetActor()); } condError(aCond, "Unexpected condition #%d!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckDude(DBloodActor* aCond, int cmpOp, bool PUSH) { int var = -1; int cond = aCond->xspr.data1 - kCondDudeBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); if (!eob.isActor() || !eob.actor()) condError(aCond, "Sprite expected, got %s", eob.description().GetChars()); auto objActor = eob.actor(); if (!objActor->hasX() || objActor->spr.type == kThingBloodChunks) condError(aCond, "Sprite #%d is dead!", objActor->GetIndex()); if (!objActor->IsDudeActor() || objActor->IsPlayerActor()) condError(aCond, "Object #%d is not an enemy!", objActor->GetIndex()); auto targ = objActor->GetTarget(); switch (cond) { default: break; case 0: // dude have any targets? if (!targ) return false; else if (!targ->IsDudeActor() && targ->spr.type != kMarkerPath) return false; else if (PUSH) condPush(aCond, targ); return true; case 1: return aiFightDudeIsAffected(objActor); // dude affected by ai fight? case 2: // distance to the target in a range? case 3: // is the target visible? case 4: // is the target visible with periphery? { if (!targ) condError(aCond, "Dude #%d has no target!", objActor->GetIndex()); DUDEINFO* pInfo = getDudeInfo(objActor->spr.type); double height = (pInfo->eyeHeight * objActor->spr.scale.Y); auto delta = targ->spr.pos.XY() - objActor->spr.pos.XY(); switch (cond) { case 2: var = condCmp(int(delta.Length() * 16), arg1 * 512, arg2 * 512, cmpOp); break; case 3: case 4: var = cansee(objActor->spr.pos, objActor->sector(), targ->spr.pos.plusZ(-height), targ->sector()); if (cond == 4 && var > 0) { DAngle absang = absangle(delta.Angle(), objActor->spr.Angles.Yaw); var = absang < (arg1 <= 0 ? pInfo->Periphery() : min(mapangle(arg1), DAngle360)); } break; } if (var <= 0) return false; else if (PUSH) condPush(aCond, targ); return true; } case 5: return objActor->xspr.dudeFlag4; case 6: return objActor->xspr.dudeDeaf; case 7: return objActor->xspr.dudeGuard; case 8: return objActor->xspr.dudeAmbush; case 9: return (objActor->xspr.unused1 & kDudeFlagStealth); case 10: // check if the marker is busy with another dude case 11: // check if the marker is reached if (!objActor->xspr.dudeFlag4 || !targ || targ->spr.type != kMarkerPath) return false; switch (cond) { case 10: { auto check = aiPatrolMarkerBusy(objActor, targ); if (!check) return false; else if (PUSH) condPush(aCond, check); break; } case 11: if (!aiPatrolMarkerReached(objActor)) return false; else if (PUSH) condPush(aCond, targ); break; } return true; case 12: // compare spot progress value in % if (!objActor->xspr.dudeFlag4 || !targ || targ->spr.type != kMarkerPath) var = 0; else if (!(objActor->xspr.unused1 & kDudeFlagStealth) || objActor->xspr.data3 < 0 || objActor->xspr.data3 > kMaxPatrolSpotValue) var = 0; else var = (kPercFull * objActor->xspr.data3) / kMaxPatrolSpotValue; return condCmp(var, arg1, arg2, cmpOp); case 15: return getDudeInfo(objActor->spr.type)->lockOut; // dude allowed to interact with objects? case 16: return condCmp(objActor->xspr.aiState->stateType, arg1, arg2, cmpOp); case 17: return condCmp(objActor->xspr.stateTimer, arg1, arg2, cmpOp); case 20: // kDudeModernCustom conditions case 21: case 22: case 23: case 24: switch (objActor->spr.type) { case kDudeModernCustom: case kDudeModernCustomBurning: switch (cond) { case 20: // life leech is thrown? { DBloodActor* act = objActor->genDudeExtra.pLifeLeech; if (!act) return false; else if (PUSH) condPush(aCond, act); return true; } case 21: // life leech is destroyed? { DBloodActor* act = objActor->genDudeExtra.pLifeLeech; if (!act) return false; if (objActor->GetSpecialOwner()) return true; else if (PUSH) condPush(aCond, act); return false; } case 22: // are required amount of dudes is summoned? return condCmp(objActor->genDudeExtra.slaveCount, arg1, arg2, cmpOp); case 23: // check if dude can... switch (arg3) { case 1: return objActor->genDudeExtra.canAttack; case 2: return objActor->genDudeExtra.canBurn; case 3: return objActor->genDudeExtra.canDuck; case 4: return objActor->genDudeExtra.canElectrocute; case 5: return objActor->genDudeExtra.canFly; case 6: return objActor->genDudeExtra.canRecoil; case 7: return objActor->genDudeExtra.canSwim; case 8: return objActor->genDudeExtra.canWalk; default: condError(aCond, "Invalid argument %d", arg3); break; } break; case 24: // compare weapon dispersion return condCmp(objActor->genDudeExtra.baseDispersion, arg1, arg2, cmpOp); } break; default: condError(aCond, "Dude #%d is not a Custom Dude!", objActor->GetIndex()); return false; } } condError(aCond, "Unexpected condition #%d!", cond); return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool condCheckSprite(DBloodActor* aCond, int cmpOp, bool PUSH) { int var = -1, var2 = -1, var3 = -1; DBloodPlayer* pPlayer = NULL; bool retn = false; int cond = aCond->xspr.data1 - kCondSpriteBase; int arg1 = aCond->xspr.data2; int arg2 = aCond->xspr.data3; int arg3 = aCond->xspr.data4; auto eob = condGet(aCond); if (!eob.isActor() || !eob.actor()) condError(aCond, "Sprite expected, got %s", eob.description().GetChars()); auto objActor = eob.actor(); if (cond < (kCondRange >> 1)) { switch (cond) { default: break; case 0: return condCmp((arg3 == 0) ? (objActor->spr.Angles.Yaw.Normalized360().Buildang()) : objActor->spr.Angles.Yaw.Buildang(), arg1, arg2, cmpOp); case 5: return condCmp(objActor->spr.statnum, arg1, arg2, cmpOp); case 6: return ((objActor->spr.flags & kHitagRespawn) || objActor->spr.statnum == kStatRespawn); case 7: return condCmp(spriteGetSlope(objActor), arg1, arg2, cmpOp); case 10: return condCmp(int(objActor->clipdist * 4), arg1, arg2, cmpOp); case 15: if (!objActor->GetOwner()) return false; else if (PUSH) condPush(aCond, objActor->GetOwner()); return true; case 20: // stays in a sector? if (!objActor->insector()) return false; else if (PUSH) condPush(aCond, objActor->sector()); return true; case 25: if (arg3 == 1) { if (arg1 == 0) { if ((var = condCmp(FloatToFixed(objActor->vel.X), arg1, arg2, cmpOp)) == true) return var; if ((var = condCmp(FloatToFixed(objActor->vel.Y), arg1, arg2, cmpOp)) == true) return var; if ((var = condCmp(FloatToFixed(objActor->vel.Z), arg1, arg2, cmpOp)) == true) return var; } else if (arg1 == 1) return condCmp(FloatToFixed(objActor->vel.X), arg1, arg2, cmpOp); else if (arg1 == 2) return condCmp(FloatToFixed(objActor->vel.Y), arg1, arg2, cmpOp); else if (arg1 == 3) return condCmp(FloatToFixed(objActor->vel.Z), arg1, arg2, cmpOp); } else if (arg1 == 0) return (!objActor->vel.isZero()); else if (arg1 == 1) return (FloatToFixed(objActor->vel.X)); else if (arg1 == 2) return (FloatToFixed(objActor->vel.Y)); else if (arg1 == 3) return (FloatToFixed(objActor->vel.Z)); break; case 30: if (!spriteIsUnderwater(objActor) && !spriteIsUnderwater(objActor, true)) return false; else if (PUSH) condPush(aCond, objActor->sector()); return true; case 31: if (arg1 == -1) { for (var = 0; var < kDmgMax; var++) { if (!nnExtIsImmune(objActor, arg1, 0)) return false; } return true; } return nnExtIsImmune(objActor, arg1, 0); case 35: // hitscan: ceil? case 36: // hitscan: floor? case 37: // hitscan: wall? case 38: // hitscan: sprite? { switch (arg1) { case 0: arg1 = CLIPMASK0 | CLIPMASK1; break; case 1: arg1 = CLIPMASK0; break; case 2: arg1 = CLIPMASK1; break; } double range = arg3 * 2; if ((pPlayer = getPlayerById(objActor->spr.type)) != NULL) var = HitScan(objActor, pPlayer->zWeapon, pPlayer->aim, arg1, range); else if (objActor->IsDudeActor()) var = HitScan(objActor, objActor->spr.pos.Z, DVector3(objActor->spr.Angles.Yaw.ToVector(), (!objActor->hasX()) ? 0 : objActor->dudeSlope), arg1, range); else if ((objActor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_MASK) == CSTAT_SPRITE_ALIGNMENT_FLOOR) { var3 = (objActor->spr.cstat & CSTAT_SPRITE_YFLIP) ? 8 : -8; // was 0x20000 - HitScan uses Q28.4 for dz! var = HitScan(objActor, objActor->spr.pos.Z, DVector3(objActor->spr.Angles.Yaw.ToVector(), var3), arg1, range); } else { var = HitScan(objActor, objActor->spr.pos.Z, DVector3(objActor->spr.Angles.Yaw.ToVector(), 0), arg1, range); } if (var < 0) return false; switch (cond) { case 35: if (var != 1) return false; else if (PUSH) condPush(aCond, gHitInfo.hitSector); return true; case 36: if (var != 2) return false; else if (PUSH) condPush(aCond, gHitInfo.hitSector); return true; case 37: if (var != 0 && var != 4) return false; else if (PUSH) condPush(aCond, gHitInfo.hitWall); return true; case 38: if (var != 3) return false; else if (PUSH) condPush(aCond, gHitInfo.actor()); return true; } break; } case 45: // this sprite is a target of some dude? BloodStatIterator it(kStatDude); while (auto iactor = it.Next()) { if (objActor == iactor) continue; if (iactor->IsDudeActor() && iactor->hasX()) { if (iactor->xspr.health <= 0 || iactor->GetTarget() != objActor) continue; else if (PUSH) condPush(aCond, iactor); return true; } } return false; } } else if (objActor->hasX()) { switch (cond) { default: break; case 50: // compare hp (in %) if (objActor->IsDudeActor()) var = (objActor->xspr.sysData2 > 0) ? ClipRange(objActor->xspr.sysData2 << 4, 1, 65535) : getDudeInfo(objActor->spr.type)->startHealth << 4; else if (objActor->spr.type == kThingBloodChunks) return condCmp(0, arg1, arg2, cmpOp); else if (objActor->spr.type >= kThingBase && objActor->spr.type < kThingMax) var = thingInfo[objActor->spr.type - kThingBase].startHealth << 4; return condCmp((kPercFull * objActor->xspr.health) / ClipLow(var, 1), arg1, arg2, cmpOp); case 55: // touching ceil of sector? if (objActor->hit.ceilhit.type != kHitSector) return false; else if (PUSH) condPush(aCond, objActor->hit.ceilhit.hitSector); return true; case 56: // touching floor of sector? if (objActor->hit.florhit.type != kHitSector) return false; else if (PUSH) condPush(aCond, objActor->hit.florhit.hitSector); return true; case 57: // touching walls of sector? if (objActor->hit.hit.type != kHitWall) return false; else if (PUSH) condPush(aCond, objActor->hit.hit.hitWall); return true; case 58: // touching another sprite? { DBloodActor* actorvar = nullptr; // Caution: The hit pointers here may be stale, so be careful with them. switch (arg3) { case 0: case 1: if (objActor->hit.florhit.type == kHitSprite) actorvar = objActor->hit.florhit.safeActor(); if (arg3 || var >= 0) break; [[fallthrough]]; case 2: if (objActor->hit.hit.type == kHitSprite) actorvar = objActor->hit.hit.safeActor(); if (arg3 || var >= 0) break; [[fallthrough]]; case 3: if (objActor->hit.ceilhit.type == kHitSprite) actorvar = objActor->hit.ceilhit.safeActor(); break; } if (actorvar == nullptr && objActor->insector()) { // check if something is touching this sprite BloodSectIterator it(objActor->sector()); while (auto iactor = it.Next()) { if (iactor->spr.flags & kHitagRespawn) continue; auto& hit = iactor->hit; switch (arg3) { case 0: case 1: if (hit.ceilhit.type == kHitSprite && hit.ceilhit.safeActor() == objActor) actorvar = iactor; if (arg3 || actorvar) break; [[fallthrough]]; case 2: if (hit.hit.type == kHitSprite && hit.hit.safeActor() == objActor) actorvar = iactor; if (arg3 || actorvar) break; [[fallthrough]]; case 3: if (hit.florhit.type == kHitSprite && hit.florhit.safeActor() == objActor) actorvar = iactor; break; } } } if (actorvar == nullptr) return false; else if (PUSH) condPush(aCond, actorvar); return true; } case 65: // compare burn time (in %) var = (objActor->IsDudeActor()) ? 2400 : 1200; if (!condCmp((kPercFull * objActor->xspr.burnTime) / var, arg1, arg2, cmpOp)) return false; else if (PUSH && objActor->GetBurnSource()) condPush(aCond, objActor->GetBurnSource()); return true; case 66: // any flares stuck in this sprite? { BloodStatIterator it(kStatFlare); while (auto flareactor = it.Next()) { if (!flareactor->hasX() || (flareactor->spr.flags & kHitagFree)) continue; if (flareactor->GetTarget() != objActor) continue; else if (PUSH) condPush(aCond, flareactor); return true; } return false; } case 70: return condCmp(getSpriteMassBySize(objActor), arg1, arg2, cmpOp); // mass of the sprite in a range? } } else { switch (cond) { default: return false; case 50: case 65: case 70: return condCmp(0, arg1, arg2, cmpOp); } } condError(aCond, "Unexpected condition id (%d)!", cond); return false; } //--------------------------------------------------------------------------- // // this updates index of object in all conditions // only used when spawning players // //--------------------------------------------------------------------------- void conditionsUpdateIndex(DBloodActor* oldActor, DBloodActor* newActor) { // update index in tracking conditions first for (unsigned i = 0; i < gConditions.Size(); i++) { TRCONDITION* pCond = &gConditions[i]; for (unsigned k = 0; k < pCond->objects.Size(); k++) { if (!pCond->objects[k].obj.isActor() || pCond->objects[k].obj.actor() != oldActor) continue; pCond->objects[k].obj = EventObject(newActor); break; } } // puke... auto oldSerial = EventObject(oldActor); auto newSerial = EventObject(newActor); // then update serials BloodStatIterator it(kStatModernCondition); while (auto iActor = it.Next()) { if (iActor->condition[0] == oldSerial) iActor->condition[0] = newSerial; if (iActor->condition[1] == oldSerial) iActor->condition[1] = newSerial; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool modernTypeSetSpriteState(DBloodActor* actor, int nState, DBloodActor* initiator) { if ((actor->xspr.busy & 0xffff) == 0 && actor->xspr.state == nState) return false; actor->xspr.busy = IntToFixed(nState); actor->xspr.state = nState; evKillActor(actor, initiator); if (actor->xspr.restState != nState && actor->xspr.waitTime > 0) evPostActor(actor, (actor->xspr.waitTime * 120) / 10, actor->xspr.restState ? kCmdOn : kCmdOff, initiator); if (actor->xspr.txID != 0 && ((actor->xspr.triggerOn && actor->xspr.state) || (actor->xspr.triggerOff && !actor->xspr.state))) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); return true; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void modernTypeSendCommand(DBloodActor* actor, int destChannel, COMMAND_ID command, DBloodActor* initiator) { switch (command) { case kCmdLink: evSendActor(actor, destChannel, kCmdModernUse, initiator); // just send command to change properties return; case kCmdUnlock: evSendActor(actor, destChannel, command, initiator); // send normal command first evSendActor(actor, destChannel, kCmdModernUse, initiator); // then send command to change properties return; default: evSendActor(actor, destChannel, kCmdModernUse, initiator); // send first command to change properties evSendActor(actor, destChannel, command, initiator); // then send normal command return; } } //--------------------------------------------------------------------------- // // this function used by various new modern types. // //--------------------------------------------------------------------------- void modernTypeTrigger(int destObjType, sectortype* destSect, walltype* destWall, DBloodActor* destactor, EVENT& event) { if (!event.isActor()) return; auto sourceactor = event.getActor(); if (!sourceactor || !sourceactor->hasX()) return; switch (destObjType) { case OBJ_SECTOR: if (!destSect || !destSect->hasX()) return; break; case OBJ_WALL: if (!destWall || !destWall->hasX()) return; break; case OBJ_SPRITE: { if (!destactor) return; if (destactor->spr.flags & kHitagFree) return; // allow redirect events received from some modern types. // example: it allows to spawn FX effect if event was received from kModernEffectGen // on many TX channels instead of just one. DBloodActor* initiator = event.initiator; switch (destactor->spr.type) { case kModernRandomTX: case kModernSequentialTX: if (destactor->xspr.command != kCmdLink || destactor->xspr.locked) break; // no redirect mode detected switch (destactor->spr.type) { case kModernRandomTX: useRandomTx(destactor, (COMMAND_ID)sourceactor->xspr.command, false, initiator); // set random TX id break; case kModernSequentialTX: if (destactor->spr.flags & kModernTypeFlag1) { seqTxSendCmdAll(destactor, sourceactor, (COMMAND_ID)sourceactor->xspr.command, true, initiator); return; } useSequentialTx(destactor, (COMMAND_ID)sourceactor->xspr.command, false, initiator); // set next TX id break; } if (destactor->xspr.txID <= 0 || destactor->xspr.txID >= kChannelUserMax) return; modernTypeSendCommand(sourceactor, destactor->xspr.txID, (COMMAND_ID)sourceactor->xspr.command, initiator); return; } break; } default: return; } switch (sourceactor->spr.type) { // allows teleport any sprite from any location to the source destination case kMarkerWarpDest: if (destObjType != OBJ_SPRITE) break; useTeleportTarget(sourceactor, destactor); break; // changes slope of sprite or sector case kModernSlopeChanger: switch (destObjType) { case OBJ_SPRITE: case OBJ_SECTOR: useSlopeChanger(sourceactor, destObjType, destSect, destactor); break; } break; case kModernSpriteDamager: // damages xsprite via TX ID or xsprites in a sector switch (destObjType) { case OBJ_SPRITE: case OBJ_SECTOR: useSpriteDamager(sourceactor, destObjType, destSect, destactor); break; } break; // can spawn any effect passed in data2 on it's or txID sprite case kModernEffectSpawner: if (destObjType != OBJ_SPRITE) break; useEffectGen(sourceactor, destactor); break; // takes data2 as SEQ ID and spawns it on it's or TX ID object case kModernSeqSpawner: useSeqSpawnerGen(sourceactor, destObjType, destSect, destWall, destactor); break; // creates wind on TX ID sector case kModernWindGenerator: if (destObjType != OBJ_SECTOR || sourceactor->xspr.data2 < 0) break; useSectorWindGen(sourceactor, destSect); break; // size and pan changer of sprite/wall/sector via TX ID case kModernObjSizeChanger: useObjResizer(sourceactor, destObjType, destSect, destWall, destactor); break; // iterate data filed value of destination object case kModernObjDataAccumulator: useIncDecGen(sourceactor, destObjType, destSect, destWall, destactor); break; // change data field value of destination object case kModernObjDataChanger: useDataChanger(sourceactor, destObjType, destSect, destWall, destactor); break; // change sector lighting dynamically case kModernSectorFXChanger: if (destObjType != OBJ_SECTOR) break; useSectorLightChanger(sourceactor, destSect); break; // change target of dudes and make it fight case kModernDudeTargetChanger: if (destObjType != OBJ_SPRITE) break; useTargetChanger(sourceactor, destactor); break; // change picture and palette of TX ID object case kModernObjPicnumChanger: usePictureChanger(sourceactor, destObjType, destSect, destWall, destactor); break; // change various properties case kModernObjPropertiesChanger: usePropertiesChanger(sourceactor, destObjType, destSect, destWall, destactor); break; // updated vanilla sound gen that now allows to play sounds on TX ID sprites // change velocity of the sprite case kModernVelocityChanger: switch (destObjType) { case OBJ_SPRITE: case OBJ_SECTOR: useVelocityChanger(sourceactor, destSect, event.initiator, destactor); break; } break; case kGenModernSound: if (destObjType != OBJ_SPRITE) break; useSoundGen(sourceactor, destactor); break; // updated ecto skull gen that allows to fire missile from TX ID sprites case kGenModernMissileUniversal: if (destObjType != OBJ_SPRITE) break; useUniMissileGen(sourceactor, destactor); break; // spawn enemies on TX ID sprites case kMarkerDudeSpawn: if (destObjType != OBJ_SPRITE) break; useDudeSpawn(sourceactor, destactor); break; // spawn custom dude on TX ID sprites case kModernCustomDudeSpawn: if (destObjType != OBJ_SPRITE) break; useCustomDudeSpawn(sourceactor, destactor); break; } } //--------------------------------------------------------------------------- // // the following functions required for kModernDudeTargetChanger // //--------------------------------------------------------------------------- DBloodActor* aiFightGetTargetInRange(DBloodActor* actor, int minDist, int maxDist, int data, int teamMode) { DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); BloodStatIterator it(kStatDude); while (auto targactor = it.Next()) { if (!aiFightDudeCanSeeTarget(actor, pDudeInfo, targactor)) continue; int dist = aiFightGetTargetDist(actor, pDudeInfo, targactor); if (dist < minDist || dist > maxDist) continue; else if (actor->GetTarget() == targactor) return targactor; else if (!targactor->IsDudeActor() || targactor == actor || targactor->IsPlayerActor()) continue; else if (IsBurningDude(targactor) || !IsKillableDude(targactor) || targactor->GetOwner() == actor) continue; else if ((teamMode == 1 && actor->xspr.rxID == targactor->xspr.rxID) || aiFightMatesHaveSameTarget(actor, targactor, 1)) continue; else if (data == 666 || targactor->xspr.data1 == data) { if (actor->GetTarget()) { double fineDist1 = aiFightGetFineTargetDist(actor, actor->GetTarget()); double fineDist2 = aiFightGetFineTargetDist(actor, targactor); if (fineDist1 < fineDist2) continue; } return targactor; } } return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* aiFightTargetIsPlayer(DBloodActor* actor) { auto targ = actor->GetTarget(); if (targ && targ->IsPlayerActor()) return targ; return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* aiFightGetMateTargets(DBloodActor* actor) { int rx = actor->xspr.rxID; for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { if (rxBucket[i].isActor()) { auto mate = rxBucket[i].actor(); if (!mate || !mate->hasX() || mate == actor || !mate->IsDudeActor()) continue; if (mate->GetTarget()) { if (!mate->GetTarget()->IsPlayerActor()) return mate->GetTarget(); } } } return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiFightMatesHaveSameTarget(DBloodActor* leaderactor, DBloodActor* targetactor, int allow) { int rx = leaderactor->xspr.rxID; for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto mate = rxBucket[i].actor(); if (!mate || !mate->hasX() || mate == leaderactor || !mate->IsDudeActor()) continue; if (mate->GetTarget() == targetactor && allow-- <= 0) return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiFightDudeCanSeeTarget(DBloodActor* dudeactor, DUDEINFO* pDudeInfo, DBloodActor* targetactor) { auto dv = targetactor->spr.pos.XY() - dudeactor->spr.pos.XY(); // check target if (dv.Length() < pDudeInfo->SeeDist()) { double height = (pDudeInfo->eyeHeight * dudeactor->spr.scale.Y); // is there a line of sight to the target? if (cansee(dudeactor->spr.pos, dudeactor->sector(), targetactor->spr.pos.plusZ(-height), targetactor->sector())) { return true; } } return false; } //--------------------------------------------------------------------------- // // this function required if monsters in genIdle ai state. It wakes up monsters // when kModernDudeTargetChanger goes to off state, so they won't ignore the world. // //--------------------------------------------------------------------------- void aiFightActivateDudes(int rx) { for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto dudeactor = rxBucket[i].actor(); if (!dudeactor || !dudeactor->hasX() || !dudeactor->IsDudeActor() || dudeactor->xspr.aiState->stateType != kAiStateGenIdle) continue; aiInitSprite(dudeactor); } } //--------------------------------------------------------------------------- // // this function sets target to -1 for all dudes that hunting for nSprite // //--------------------------------------------------------------------------- void aiFightFreeTargets(DBloodActor* actor) { BloodStatIterator it(kStatDude); while (auto targetactor = it.Next()) { if (!targetactor->IsDudeActor() || !targetactor->hasX()) continue; else if (targetactor->GetTarget() == actor) aiSetTarget(targetactor, targetactor->spr.pos); } } //--------------------------------------------------------------------------- // // this function sets target to -1 for all targets that hunting for dudes affected by selected kModernDudeTargetChanger // //--------------------------------------------------------------------------- void aiFightFreeAllTargets(DBloodActor* sourceactor) { auto txID = sourceactor->xspr.txID; if (txID <= 0) return; for (int i = bucketHead[txID]; i < bucketHead[txID + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto actor = rxBucket[i].actor(); if (actor && actor->hasX()) aiFightFreeTargets(actor); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiFightDudeIsAffected(DBloodActor* dudeactor) { if (dudeactor->xspr.rxID <= 0 || dudeactor->xspr.locked == 1) return false; BloodStatIterator it(kStatModernDudeTargetChanger); while (auto actor = it.Next()) { if (!actor->hasX()) continue; if (actor->xspr.txID <= 0 || actor->xspr.state != 1) continue; for (int i = bucketHead[actor->xspr.txID]; i < bucketHead[actor->xspr.txID + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto rxactor = rxBucket[i].actor(); if (!rxactor || !rxactor->hasX() || !rxactor->IsDudeActor()) continue; else if (rxactor == dudeactor) return true; } } return false; } //--------------------------------------------------------------------------- // // this function tells if there any dude found for kModernDudeTargetChanger // //--------------------------------------------------------------------------- bool aiFightGetDudesForBattle(DBloodActor* actor) { auto txID = actor->xspr.txID; for (int i = bucketHead[txID]; i < bucketHead[txID + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto rxactor = rxBucket[i].actor(); if (!rxactor || !rxactor->hasX() || !rxactor->IsDudeActor()) continue; if (rxactor->xspr.health > 0) return true; } // check redirected TX buckets int rx = -1; DBloodActor* pXRedir = nullptr; while ((pXRedir = evrListRedirectors(OBJ_SPRITE, nullptr, nullptr, actor, pXRedir, &rx)) != nullptr) { for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto rxactor = rxBucket[i].actor(); if (!rxactor || !rxactor->hasX() || !rxactor->IsDudeActor()) continue; if (rxactor->xspr.health > 0) return true; } } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiFightAlarmDudesInSight(DBloodActor* actor, int max) { DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); BloodStatIterator it(kStatDude); while (auto dudeactor = it.Next()) { if (dudeactor == actor || !dudeactor->IsDudeActor() || !dudeactor->hasX()) continue; if (aiFightDudeCanSeeTarget(actor, pDudeInfo, dudeactor)) { if (dudeactor->GetTarget() != nullptr || dudeactor->xspr.rxID > 0) continue; aiSetTarget(dudeactor, dudeactor->spr.pos); aiActivateDude(dudeactor); if (max-- < 1) break; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiFightUnitCanFly(DBloodActor* dude) { return (dude->IsDudeActor() && gDudeInfoExtra[dude->spr.type - kDudeBase].flying); } bool aiFightIsMeleeUnit(DBloodActor* dude) { if (dude->spr.type == kDudeModernCustom) return (dude->hasX() && dudeIsMelee(dude)); else return (dude->IsDudeActor() && gDudeInfoExtra[dude->spr.type - kDudeBase].melee); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int aiFightGetTargetDist(DBloodActor* actor, DUDEINFO* pDudeInfo, DBloodActor* target) { auto dvec = target->spr.pos.XY() - actor->spr.pos.XY(); double dist = dvec.Length(); if (dist <= pDudeInfo->MeleeDist()) return 0; double seeDist = pDudeInfo->SeeDist(); if (dist >= seeDist) return 13; if (dist <= seeDist / 12) return 1; if (dist <= seeDist / 11) return 2; if (dist <= seeDist / 10) return 3; if (dist <= seeDist / 9) return 4; if (dist <= seeDist / 8) return 5; if (dist <= seeDist / 7) return 6; if (dist <= seeDist / 6) return 7; if (dist <= seeDist / 5) return 8; if (dist <= seeDist / 4) return 9; if (dist <= seeDist / 3) return 10; if (dist <= seeDist / 2) return 11; return 12; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- double aiFightGetFineTargetDist(DBloodActor* actor, DBloodActor* target) { auto dvec = target->spr.pos.XY() - actor->spr.pos.XY(); return (dvec).LengthSquared(); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void sectorKillSounds(sectortype* pSector) { BloodSectIterator it(pSector); while (auto actor = it.Next()) { if (actor->spr.type != kSoundSector) continue; sfxKill3DSound(actor); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void sectorPauseMotion(sectortype* pSector, DBloodActor* initiator) { if (!pSector->hasX()) return; XSECTOR* pXSector = &pSector->xs(); pXSector->unused1 = 1; evKillSector(pSector, initiator); sectorKillSounds(pSector); if ((pXSector->busy == 0 && !pXSector->state) || (pXSector->busy == 65536 && pXSector->state)) SectorEndSound(pSector, pXSector->state); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void sectorContinueMotion(sectortype* pSector, EVENT event) { if (!pSector->hasX()) return; XSECTOR* pXSector = &pSector->xs(); pXSector->unused1 = 0; int busyTimeA = pXSector->busyTimeA; int waitTimeA = pXSector->waitTimeA; int busyTimeB = pXSector->busyTimeB; int waitTimeB = pXSector->waitTimeB; if (pSector->type == kSectorPath) { if (!pXSector->marker0) return; busyTimeA = busyTimeB = pXSector->marker0->xspr.busyTime; waitTimeA = waitTimeB = pXSector->marker0->xspr.waitTime; } if (!pXSector->interruptable && event.cmd != kCmdSectorMotionContinue && ((!pXSector->state && pXSector->busy) || (pXSector->state && pXSector->busy != 65536))) { event.cmd = kCmdSectorMotionContinue; } else if (event.cmd == kCmdToggle) { event.cmd = (pXSector->state) ? kCmdOn : kCmdOff; } int nDelta = 1; switch (event.cmd) { case kCmdOff: if (pXSector->busy == 0) { if (pXSector->reTriggerB && waitTimeB) evPostSector(pSector, (waitTimeB * 120) / 10, kCmdOff, event.initiator); return; } pXSector->state = 1; nDelta = 65536 / ClipLow((busyTimeB * 120) / 10, 1); break; case kCmdOn: if (pXSector->busy == 65536) { if (pXSector->reTriggerA && waitTimeA) evPostSector(pSector, (waitTimeA * 120) / 10, kCmdOn, event.initiator); return; } pXSector->state = 0; nDelta = 65536 / ClipLow((busyTimeA * 120) / 10, 1); break; case kCmdSectorMotionContinue: nDelta = 65536 / ClipLow((((pXSector->state) ? busyTimeB : busyTimeA) * 120) / 10, 1); break; } //bool crush = pXSector->Crush; int busyFunc = BUSYID_0; switch (pSector->type) { case kSectorZMotion: busyFunc = BUSYID_2; break; case kSectorZMotionSprite: busyFunc = BUSYID_1; break; case kSectorSlideMarked: case kSectorSlide: busyFunc = BUSYID_3; break; case kSectorRotateMarked: case kSectorRotate: busyFunc = BUSYID_4; break; case kSectorRotateStep: busyFunc = BUSYID_5; break; case kSectorPath: busyFunc = BUSYID_7; break; default: I_Error("Unsupported sector type %d", pSector->type); break; } SectorStartSound(pSector, pXSector->state); nDelta = (pXSector->state) ? -nDelta : nDelta; BUSY b = { pSector, nDelta, (int)pXSector->busy, (BUSYID)busyFunc }; gBusy.Push(b); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool modernTypeOperateSector(sectortype* pSector, const EVENT& event) { auto pXSector = &pSector->xs(); if (event.cmd >= kCmdLock && event.cmd <= kCmdToggleLock) { switch (event.cmd) { case kCmdLock: pXSector->locked = 1; break; case kCmdUnlock: pXSector->locked = 0; break; case kCmdToggleLock: pXSector->locked = pXSector->locked ^ 1; break; } switch (pSector->type) { case kSectorCounter: if (pXSector->locked != 1) break; SetSectorState(pSector, 0, event.initiator.Get()); evPostSector(pSector, 0, kCallbackCounterCheck); break; } return true; // continue motion of the paused sector } else if (pXSector->unused1) { switch (event.cmd) { case kCmdOff: case kCmdOn: case kCmdToggle: case kCmdSectorMotionContinue: sectorContinueMotion(pSector, event); return true; } // pause motion of the sector } else if (event.cmd == kCmdSectorMotionPause) { sectorPauseMotion(pSector, event.initiator.Get()); return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useCustomDudeSpawn(DBloodActor* pSource, DBloodActor* pActor) { genDudeSpawn(pSource, pActor, pActor->clipdist * 0.5); } void useDudeSpawn(DBloodActor* pSource, DBloodActor* pActor) { if (randomSpawnDude(pSource, pActor, pActor->clipdist * 0.5, 0) == nullptr) nnExtSpawnDude(pSource, pActor, pActor->xspr.data1, pActor->clipdist * 0.5, 0); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool modernTypeOperateSprite(DBloodActor* actor, EVENT& event) { DBloodActor* initiator = event.initiator; if (event.cmd >= kCmdLock && event.cmd <= kCmdToggleLock) { switch (event.cmd) { case kCmdLock: actor->xspr.locked = 1; break; case kCmdUnlock: actor->xspr.locked = 0; break; case kCmdToggleLock: actor->xspr.locked = actor->xspr.locked ^ 1; break; } switch (actor->spr.type) { case kModernCondition: case kModernConditionFalse: actor->xspr.restState = 0; if (actor->xspr.busyTime <= 0) break; else if (!actor->xspr.locked) actor->xspr.busy = 0; break; } return true; } else if (event.cmd == kCmdDudeFlagsSet) { if (!event.isActor()) { viewSetSystemMessage("Only sprites can use command #%d", event.cmd); return true; } else { auto pEvActor = event.getActor(); if (pEvActor && pEvActor->hasX()) { // copy dude flags from the source to destination sprite aiPatrolFlagsMgr(pEvActor, actor, true, false); } } } if (actor->spr.statnum == kStatDude && actor->IsDudeActor()) { switch (event.cmd) { case kCmdOff: if (actor->xspr.state) SetSpriteState(actor, 0, initiator); break; case kCmdOn: if (!actor->xspr.state) SetSpriteState(actor, 1, initiator); if (!actor->IsDudeActor() || actor->IsPlayerActor() || actor->xspr.health <= 0) break; else if (actor->xspr.aiState->stateType >= kAiStatePatrolBase && actor->xspr.aiState->stateType < kAiStatePatrolMax) break; switch (actor->xspr.aiState->stateType) { case kAiStateIdle: case kAiStateGenIdle: aiActivateDude(actor); break; } break; case kCmdDudeFlagsSet: if (event.isActor()) { auto pEvActor = event.getActor(); if (!pEvActor || !pEvActor->hasX()) break; else aiPatrolFlagsMgr(pEvActor, actor, false, true); // initialize patrol dude with possible new flags } break; default: if (!actor->xspr.state) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; } switch (actor->spr.type) { default: return false; // no modern type found to work with, go normal OperateSprite(); case kThingBloodBits: case kThingBloodChunks: // dude to thing morphing causing a lot of problems since it continues receiving commands after dude is dead. // this leads to weird stuff like exploding with gargoyle gib or corpse disappearing immediately. // let's allow only specific commands here to avoid this. if (actor->spr.inittype < kDudeBase || actor->spr.inittype >= kDudeMax) return false; else if (event.cmd != kCmdToggle && event.cmd != kCmdOff && event.cmd != kCmdSpriteImpact) return true; DudeToGibCallback1(0, actor); // set proper gib type just in case DATAs was changed from the outside. return false; case kModernCondition: case kModernConditionFalse: if (!actor->xspr.isTriggered) useCondition(actor, event); return true; // add spawn random dude feature - works only if at least 2 data fields are not empty. case kMarkerDudeSpawn: if (!gGameOptions.nMonsterSettings) return true; else if (!(actor->spr.flags & kModernTypeFlag4)) useDudeSpawn(actor, actor); else if (actor->xspr.txID) evSendActor(actor, actor->xspr.txID, kCmdModernUse, initiator); return true; case kModernCustomDudeSpawn: if (!gGameOptions.nMonsterSettings) return true; else if (!(actor->spr.flags & kModernTypeFlag4)) useCustomDudeSpawn(actor, actor); else if (actor->xspr.txID) evSendActor(actor, actor->xspr.txID, kCmdModernUse, initiator); return true; case kModernRandomTX: // random Event Switch takes random data field and uses it as TX ID case kModernSequentialTX: // sequential Switch takes values from data fields starting from data1 and uses it as TX ID if (actor->xspr.command == kCmdLink) return true; // work as event redirector switch (actor->spr.type) { case kModernRandomTX: useRandomTx(actor, (COMMAND_ID)actor->xspr.command, true, initiator); break; case kModernSequentialTX: if (!(actor->spr.flags & kModernTypeFlag1)) useSequentialTx(actor, (COMMAND_ID)actor->xspr.command, true, initiator); else seqTxSendCmdAll(actor, actor, (COMMAND_ID)actor->xspr.command, false, initiator); break; } return true; case kModernSpriteDamager: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID > 0) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else if (actor->xspr.data1 == 0 && actor->insector()) useSpriteDamager(actor, OBJ_SECTOR, actor->sector(), nullptr); else if (actor->xspr.data1 >= 666 && actor->xspr.data1 < 669) useSpriteDamager(actor, -1, nullptr, nullptr); else { DBloodPlayer* pPlayer = getPlayerById(actor->xspr.data1); if (pPlayer != NULL) useSpriteDamager(actor, OBJ_SPRITE, 0, pPlayer->GetActor()); } if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kMarkerWarpDest: if (actor->xspr.txID <= 0) { DBloodPlayer* pPlayer = getPlayerById(actor->xspr.data1); if (pPlayer != NULL && SetSpriteState(actor, actor->xspr.state ^ 1, initiator) == 1) useTeleportTarget(actor, pPlayer->GetActor()); return true; } [[fallthrough]]; case kModernObjPropertiesChanger: if (actor->xspr.txID <= 0) { if (SetSpriteState(actor, actor->xspr.state ^ 1, initiator) == 1) usePropertiesChanger(actor, -1, nullptr, nullptr, nullptr); return true; } [[fallthrough]]; case kModernSlopeChanger: case kModernObjSizeChanger: case kModernObjPicnumChanger: case kModernSectorFXChanger: case kModernObjDataChanger: modernTypeSetSpriteState(actor, actor->xspr.state ^ 1, initiator); return true; case kModernSeqSpawner: case kModernEffectSpawner: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) { SetSpriteState(actor, 0, initiator); if (actor->spr.type == kModernEffectSpawner) killEffectGenCallbacks(actor); } break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); if (actor->spr.type == kModernSeqSpawner) seqSpawnerOffSameTx(actor); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID > 0) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else if (actor->spr.type == kModernSeqSpawner) useSeqSpawnerGen(actor, OBJ_SPRITE, nullptr, nullptr, actor); else useEffectGen(actor, nullptr); if (actor->xspr.busyTime > 0) evPostActor(actor, ClipLow((int(actor->xspr.busyTime) + Random2(actor->xspr.data1)) * 120 / 10, 0), kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernWindGenerator: switch (event.cmd) { case kCmdOff: windGenStopWindOnSectors(actor); if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID > 0) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else useSectorWindGen(actor, nullptr); if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernVelocityChanger: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID > 0) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else useVelocityChanger(actor, actor->sector(), initiator, nullptr); if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernDudeTargetChanger: // this one is required if data4 of generator was dynamically changed // it turns monsters in normal idle state instead of genIdle, so they not ignore the world. if (actor->xspr.dropMsg == 3 && 3 != actor->xspr.data4) aiFightActivateDudes(actor->xspr.txID); switch (event.cmd) { case kCmdOff: if (actor->xspr.data4 == 3) aiFightActivateDudes(actor->xspr.txID); if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID <= 0 || !aiFightGetDudesForBattle(actor)) { aiFightFreeAllTargets(actor); evPostActor(actor, 0, kCmdOff, initiator); break; } else { modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); } if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } actor->xspr.dropMsg = uint8_t(actor->xspr.data4); return true; case kGenTrigger: if (!(actor->spr.flags & kModernTypeFlag1)) return false; // work as vanilla generator switch (event.cmd) { // work as fast generator case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID) evSendActor(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernObjDataAccumulator: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: // force OFF after *all* TX objects reach the goal value if (actor->spr.flags == kModernTypeFlag0 && incDecGoalValueIsReached(actor)) { evPostActor(actor, 0, kCmdOff, initiator); break; } modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); if (actor->xspr.busyTime > 0) evPostActor(actor, actor->xspr.busyTime, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernRandom: case kModernRandom2: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: useRandomItemGen(actor); if (actor->xspr.busyTime > 0) evPostActor(actor, (120 * actor->xspr.busyTime) / 10, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kModernThingTNTProx: if (actor->spr.statnum != kStatRespawn) { switch (event.cmd) { case kCmdSpriteProximity: if (actor->xspr.state) break; sfxPlay3DSound(actor, 452, 0, 0); evPostActor(actor, 30, kCmdOff, nullptr); actor->xspr.state = 1; [[fallthrough]]; case kCmdOn: sfxPlay3DSound(actor, 451, 0, 0); actor->xspr.Proximity = 1; break; default: actExplodeSprite(actor); break; } } return true; case kModernThingEnemyLifeLeech: dudeLeechOperate(actor, event); return true; case kModernPlayerControl: { // WIP DBloodPlayer* pPlayer = NULL; int cmd = (event.cmd >= kCmdNumberic) ? event.cmd : actor->xspr.command; int playerID; if ((actor->xspr.txID == kChannelEventCauser || actor->xspr.data1 == 0) && initiator && initiator->IsPlayerActor()) playerID = initiator->spr.type; else playerID = actor->xspr.data1; if ((pPlayer = getPlayerById(playerID)) == NULL || ((cmd < 67 || cmd > 68) && !modernTypeSetSpriteState(actor, actor->xspr.state ^ 1, initiator))) return true; TRPLAYERCTRL* pCtrl = &gPlayerCtrl[pPlayer->pnum]; /// !!! COMMANDS OF THE CURRENT SPRITE, NOT OF THE EVENT !!! /// if ((cmd -= kCmdNumberic) < 0) return true; else if (pPlayer->GetActor()->xspr.health <= 0) { switch (cmd) { case 36: actHealDude(pPlayer->GetActor(), ((actor->xspr.data2 > 0) ? ClipHigh(actor->xspr.data2, 200) : getDudeInfo(pPlayer->GetActor()->spr.type)->startHealth), 200); pPlayer->curWeapon = kWeapPitchFork; break; } return true; } switch (cmd) { case 0: // 64 (player life form) if (actor->xspr.data2 < kModeHuman || actor->xspr.data2 > kModeHumanGrown) break; else trPlayerCtrlSetRace(actor->xspr.data2, pPlayer); break; case 1: // 65 (move speed and jump height) // player movement speed (for all races and postures) if (valueIsBetween(actor->xspr.data2, -1, 32767)) trPlayerCtrlSetMoveSpeed(actor->xspr.data2, pPlayer); // player jump height (for all races and stand posture only) if (valueIsBetween(actor->xspr.data3, -1, 32767)) trPlayerCtrlSetJumpHeight(actor->xspr.data3, pPlayer); break; case 2: // 66 (player screen effects) if (actor->xspr.data3 < 0) break; else trPlayerCtrlSetScreenEffect(actor->xspr.data2, actor->xspr.data3, pPlayer); break; case 3: // 67 (start playing qav scene) trPlayerCtrlStartScene(actor, pPlayer, (actor->xspr.data4 == 1) ? true : false); break; case 4: // 68 (stop playing qav scene) if (actor->xspr.data2 > 0 && actor->xspr.data2 != pPlayer->sceneQav) break; else trPlayerCtrlStopScene(pPlayer); break; case 5: // 69 (set player look angle, TO-DO: if tx > 0, take a look on TX ID sprite) //data4 is reserved if (actor->xspr.data4 != 0) break; else if (valueIsBetween(actor->xspr.data2, -128, 128)) trPlayerCtrlSetLookAngle(actor->xspr.data2, pPlayer); break; case 6: // 70 (erase player stuff...) if (actor->xspr.data2 < 0) break; else trPlayerCtrlEraseStuff(actor->xspr.data2, pPlayer); break; case 7: // 71 (give something to player...) if (actor->xspr.data2 <= 0) break; else trPlayerCtrlGiveStuff(actor->xspr.data2, actor->xspr.data3, actor->xspr.data4, pPlayer, pCtrl); break; case 8: // 72 (use inventory item) if (actor->xspr.data2 < 1 || actor->xspr.data2 > 5) break; else trPlayerCtrlUsePackItem(actor->xspr.data2, actor->xspr.data3, actor->xspr.data4, pPlayer, event.cmd); break; case 9: // 73 (set player's sprite angle, TO-DO: if tx > 0, take a look on TX ID sprite) //data4 is reserved if (actor->xspr.data4 != 0) break; else if (actor->spr.flags & kModernTypeFlag1) { gameInput.ForceInputSync(pPlayer->pnum); pPlayer->GetActor()->spr.Angles.Yaw = actor->spr.Angles.Yaw; } else if (valueIsBetween(actor->xspr.data2, -kAng360, kAng360)) { gameInput.ForceInputSync(pPlayer->pnum); pPlayer->GetActor()->spr.Angles.Yaw = mapangle(actor->xspr.data2); } break; case 10: // 74 (de)activate powerup if (actor->xspr.data2 <= 0 || actor->xspr.data2 > (kMaxAllowedPowerup - (kMinAllowedPowerup << 1) + 1)) break; trPlayerCtrlUsePowerup(actor, pPlayer, event.cmd); break; // case 11: // 75 (print the book) // data2: RFF TXT id // data3: background tile // data4: font base tile // pal: font / background palette // hitag: // d1: 0: print whole text at a time, 1: print line by line, 2: word by word, 3: letter by letter // d2: 1: force pause the game (sp only) // d3: 1: inherit palette for font, 2: inherit palette for background, 3: both // busyTime: speed of word/letter/line printing // waitTime: if TX ID > 0 and TX ID object is book reader, trigger it? //break; } } return true; case kGenModernSound: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else useSoundGen(actor, actor); if (actor->xspr.busyTime > 0) evPostActor(actor, (120 * actor->xspr.busyTime) / 10, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; case kGenModernMissileUniversal: switch (event.cmd) { case kCmdOff: if (actor->xspr.state == 1) SetSpriteState(actor, 0, initiator); break; case kCmdOn: evKillActor(actor, initiator); // queue overflow protect if (actor->xspr.state == 0) SetSpriteState(actor, 1, initiator); [[fallthrough]]; case kCmdRepeat: if (actor->xspr.txID) modernTypeSendCommand(actor, actor->xspr.txID, (COMMAND_ID)actor->xspr.command, initiator); else useUniMissileGen(actor, actor); if (actor->xspr.busyTime > 0) evPostActor(actor, (120 * actor->xspr.busyTime) / 10, kCmdRepeat, initiator); break; default: if (actor->xspr.state == 0) evPostActor(actor, 0, kCmdOn, initiator); else evPostActor(actor, 0, kCmdOff, initiator); break; } return true; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool modernTypeOperateWall(walltype* pWall, const EVENT& event) { DBloodActor* initiator = event.initiator.Get(); switch (pWall->type) { case kSwitchOneWay: switch (event.cmd) { case kCmdOff: SetWallState(pWall, 0, initiator); break; case kCmdOn: SetWallState(pWall, 1, initiator); break; default: SetWallState(pWall, pWall->xw().restState ^ 1, initiator); break; } return true; default: return false; // no modern type found to work with, go normal OperateWall(); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool txIsRanged(DBloodActor* sourceactor) { if (!sourceactor->hasX()) return false; if (sourceactor->xspr.data1 > 0 && sourceactor->xspr.data2 <= 0 && sourceactor->xspr.data3 <= 0 && sourceactor->xspr.data4 > 0) { if (sourceactor->xspr.data1 > sourceactor->xspr.data4) { // data1 must be less than data4 int tmp = sourceactor->xspr.data1; sourceactor->xspr.data1 = sourceactor->xspr.data4; sourceactor->xspr.data4 = tmp; } return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void seqTxSendCmdAll(DBloodActor* sourceactor, DBloodActor* actor, COMMAND_ID cmd, bool modernSend, DBloodActor* initiator) { bool ranged = txIsRanged(sourceactor); if (ranged) { for (sourceactor->xspr.txID = sourceactor->xspr.data1; sourceactor->xspr.txID <= sourceactor->xspr.data4; sourceactor->xspr.txID++) { if (sourceactor->xspr.txID <= 0 || sourceactor->xspr.txID >= kChannelUserMax) continue; else if (!modernSend) evSendActor(actor, sourceactor->xspr.txID, cmd, initiator); else modernTypeSendCommand(actor, sourceactor->xspr.txID, cmd, initiator); } } else { for (int i = 0; i <= 3; i++) { sourceactor->xspr.txID = GetDataVal(sourceactor, i); if (sourceactor->xspr.txID <= 0 || sourceactor->xspr.txID >= kChannelUserMax) continue; else if (!modernSend) evSendActor(actor, sourceactor->xspr.txID, cmd, initiator); else modernTypeSendCommand(actor, sourceactor->xspr.txID, cmd, initiator); } } sourceactor->xspr.txID = sourceactor->xspr.sysData1 = 0; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useRandomTx(DBloodActor* sourceactor, COMMAND_ID cmd, bool setState, DBloodActor* initiator) { int tx = 0; int maxRetries = kMaxRandomizeRetries; if (txIsRanged(sourceactor)) { while (maxRetries-- >= 0) { if ((tx = nnExtRandom(sourceactor->xspr.data1, sourceactor->xspr.data4)) != sourceactor->xspr.txID) break; } } else { while (maxRetries-- >= 0) { if ((tx = randomGetDataValue(sourceactor, kRandomizeTX)) > 0 && tx != sourceactor->xspr.txID) break; } } sourceactor->xspr.txID = (tx > 0 && tx < kChannelUserMax) ? tx : 0; if (setState) SetSpriteState(sourceactor, sourceactor->xspr.state ^ 1, initiator); //evSendActor(sourceactor->spr.index, sourceactor->xspr.txID, (COMMAND_ID)sourceactor->xspr.command); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSequentialTx(DBloodActor* sourceactor, COMMAND_ID cmd, bool setState, DBloodActor* initiator) { bool range = txIsRanged(sourceactor); int cnt = 3; int tx = 0; if (range) { // make sure sysData is correct as we store current index of TX ID here. if (sourceactor->xspr.sysData1 < sourceactor->xspr.data1) sourceactor->xspr.sysData1 = sourceactor->xspr.data1; else if (sourceactor->xspr.sysData1 > sourceactor->xspr.data4) sourceactor->xspr.sysData1 = sourceactor->xspr.data4; } else { // make sure sysData is correct as we store current index of data field here. if (sourceactor->xspr.sysData1 > 3) sourceactor->xspr.sysData1 = 0; else if (sourceactor->xspr.sysData1 < 0) sourceactor->xspr.sysData1 = 3; } switch (cmd) { case kCmdOff: if (!range) { while (cnt-- >= 0) // skip empty data fields { if (sourceactor->xspr.sysData1-- < 0) sourceactor->xspr.sysData1 = 3; if ((tx = GetDataVal(sourceactor, sourceactor->xspr.sysData1)) <= 0) continue; else break; } } else { if (--sourceactor->xspr.sysData1 < sourceactor->xspr.data1) sourceactor->xspr.sysData1 = sourceactor->xspr.data4; tx = sourceactor->xspr.sysData1; } break; default: if (!range) { while (cnt-- >= 0) // skip empty data fields { if (sourceactor->xspr.sysData1 > 3) sourceactor->xspr.sysData1 = 0; if ((tx = GetDataVal(sourceactor, sourceactor->xspr.sysData1++)) <= 0) continue; else break; } } else { tx = sourceactor->xspr.sysData1; if (sourceactor->xspr.sysData1 >= sourceactor->xspr.data4) { sourceactor->xspr.sysData1 = sourceactor->xspr.data1; break; } sourceactor->xspr.sysData1++; } break; } sourceactor->xspr.txID = (tx > 0 && tx < kChannelUserMax) ? tx : 0; if (setState) SetSpriteState(sourceactor, sourceactor->xspr.state ^ 1, initiator); //evSendActor(sourceactor->spr.index, sourceactor->xspr.txID, (COMMAND_ID)sourceactor->xspr.command); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int useCondition(DBloodActor* sourceactor, EVENT& event) { bool srcIsCondition = false; auto const pActor = event.isActor() ? event.getActor() : nullptr; if (event.isActor() && pActor == nullptr) return -1; if (event.isActor() && pActor != sourceactor) srcIsCondition = (pActor->spr.type == kModernCondition || pActor->spr.type == kModernConditionFalse); // if it's a tracking condition, it must ignore all the commands sent from objects if (sourceactor->xspr.busyTime > 0 && event.funcID != kCallbackMax) return -1; else if (!srcIsCondition) // save object serials in the stack and make copy of initial object { condPush(sourceactor, event.target); condBackup(sourceactor); } else // or grab serials of objects from previous conditions { sourceactor->condition[0] = pActor->condition[0]; sourceactor->condition[1] = pActor->condition[1]; } int cond = sourceactor->xspr.data1; bool ok = false; bool RVRS = (sourceactor->spr.type == kModernConditionFalse); bool RSET = (sourceactor->xspr.command == kCmdNumberic + 36); bool PUSH = (sourceactor->xspr.command == kCmdNumberic); int comOp = sourceactor->spr.cstat; // comparison operator if (sourceactor->xspr.restState == 0) { if (cond == 0) ok = true; // dummy else if (cond >= kCondGameBase && cond < kCondGameMax) ok = condCheckGame(sourceactor, event, comOp, PUSH); else if (cond >= kCondMixedBase && cond < kCondMixedMax) ok = condCheckMixed(sourceactor, event, comOp, PUSH); else if (cond >= kCondWallBase && cond < kCondWallMax) ok = condCheckWall(sourceactor, comOp, PUSH); else if (cond >= kCondSectorBase && cond < kCondSectorMax) ok = condCheckSector(sourceactor, comOp, PUSH); else if (cond >= kCondPlayerBase && cond < kCondPlayerMax) ok = condCheckPlayer(sourceactor, comOp, PUSH); else if (cond >= kCondDudeBase && cond < kCondDudeMax) ok = condCheckDude(sourceactor, comOp, PUSH); else if (cond >= kCondSpriteBase && cond < kCondSpriteMax) ok = condCheckSprite(sourceactor, comOp, PUSH); else condError(sourceactor, "Unexpected condition id %d!", cond); sourceactor->xspr.state = (ok ^ RVRS); if (sourceactor->xspr.waitTime > 0 && sourceactor->xspr.state > 0) { sourceactor->xspr.restState = 1; evKillActor(sourceactor); evPostActor(sourceactor, (sourceactor->xspr.waitTime * 120) / 10, kCmdRepeat, event.initiator); return -1; } } else if (event.cmd == kCmdRepeat) { sourceactor->xspr.restState = 0; } else { return -1; } int retn = sourceactor->xspr.state; if (retn) { sourceactor->xspr.isTriggered = sourceactor->xspr.triggerOnce; if (RSET) condRestore(sourceactor); // reset focus to the initial object // send command to rx bucket if (sourceactor->xspr.txID) { evSendActor(sourceactor, sourceactor->xspr.txID, (COMMAND_ID)sourceactor->xspr.command, sourceactor->condition[0].isActor()? sourceactor->condition[0].actor() : nullptr); } if (sourceactor->spr.flags) { // send it for object currently in the focus if (sourceactor->spr.flags & kModernTypeFlag1) { nnExtTriggerObject(event.target, sourceactor->xspr.command, sourceactor); } // send it for initial object if ((sourceactor->spr.flags & kModernTypeFlag2) && (sourceactor->condition[0] != sourceactor->condition[1] || !(sourceactor->spr.hitag & kModernTypeFlag1))) { auto co = condGet(sourceactor); nnExtTriggerObject(co, sourceactor->xspr.command, sourceactor); } } } return retn; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useRandomItemGen(DBloodActor* actor) { // let's first search for previously dropped items and remove it if (actor->xspr.dropMsg > 0) { BloodStatIterator it(kStatItem); while (auto iactor = it.Next()) { if ((unsigned int)iactor->spr.type == actor->xspr.dropMsg && iactor->spr.pos == actor->spr.pos) { gFX.fxSpawnActor((FX_ID)29, actor->sector(), actor->spr.pos); iactor->spr.type = kSpriteDecoration; actPostSprite(iactor, kStatFree); break; } } } // then drop item auto dropactor = randomDropPickupObject(actor, actor->xspr.dropMsg); if (dropactor != nullptr) { clampSprite(dropactor); // check if generator affected by physics if (debrisGetIndex(actor) != -1) { dropactor->addX(); int nIndex = debrisGetFreeIndex(); if (nIndex >= 0) { dropactor->xspr.physAttr |= kPhysMove | kPhysGravity | kPhysFalling; // must fall always actor->spr.cstat &= ~CSTAT_SPRITE_BLOCK; gPhysSpritesList[nIndex] = dropactor; if (nIndex >= gPhysSpritesCount) gPhysSpritesCount++; getSpriteMassBySize(dropactor); // create mass cache } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useUniMissileGen(DBloodActor* sourceactor, DBloodActor* actor) { if (actor == nullptr) actor = sourceactor; DVector3 dv(0, 0, 0); if (sourceactor->xspr.data1 < kMissileBase || sourceactor->xspr.data1 >= kMissileMax) return; if (actor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR) { if (actor->spr.cstat & CSTAT_SPRITE_YFLIP) dv.Z = 1; else dv.Z = -1; } else { dv.XY() = actor->spr.Angles.Yaw.ToVector(); dv.Z = clamp(sourceactor->xspr.data3 / 256., -4., 4.); // add slope controlling } auto missileactor = actFireMissile(actor, 0, 0, dv, actor->xspr.data1); if (missileactor) { int from; // inherit some properties of the generator if ((from = (sourceactor->spr.flags & kModernTypeFlag3)) > 0) { int canInherit = 0xF; if (missileactor->hasX() && seqGetStatus(missileactor) >= 0) { canInherit &= ~0x8; SEQINST* pInst = GetInstance(missileactor); Seq* pSeq = pInst->pSequence; for (int i = 0; i < pSeq->nFrames; i++) { if ((canInherit & 0x4) && pSeq->frames[i].palette != 0) canInherit &= ~0x4; if ((canInherit & 0x2) && pSeq->frames[i].scalex != 0) canInherit &= ~0x2; if ((canInherit & 0x1) && pSeq->frames[i].scaley != 0) canInherit &= ~0x1; } } if (canInherit != 0) { if (canInherit & 0x2) missileactor->spr.scale.X = ((from == kModernTypeFlag1) ? sourceactor->spr.scale.X : actor->spr.scale.X); if (canInherit & 0x1) missileactor->spr.scale.Y = ((from == kModernTypeFlag1) ? sourceactor->spr.scale.Y : actor->spr.scale.Y); if (canInherit & 0x4) missileactor->spr.pal = (from == kModernTypeFlag1) ? sourceactor->spr.pal : actor->spr.pal; if (canInherit & 0x8) missileactor->spr.shade = (from == kModernTypeFlag1) ? sourceactor->spr.shade : actor->spr.shade; } } // add velocity controlling if (sourceactor->xspr.data2 > 0) { missileactor->vel = dv * sourceactor->xspr.data2 / 16.; } // add bursting for missiles if (missileactor->spr.type != kMissileFlareAlt && sourceactor->xspr.data4 > 0) evPostActor(missileactor, ClipHigh(sourceactor->xspr.data4, 500), kCallbackMissileBurst); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSoundGen(DBloodActor* sourceactor, DBloodActor* actor) { int pitch = sourceactor->xspr.data4 << 1; if (pitch < 2000) pitch = 0; sfxPlay3DSoundVolume(actor, sourceactor->xspr.data2, -1, 0, pitch, sourceactor->xspr.data3); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useIncDecGen(DBloodActor* sourceactor, int objType, sectortype* destSect, walltype* destWall, DBloodActor* objactor) { char buffer[7]; int data = -65535; short tmp = 0; int dataIndex = 0; snprintf(buffer, 7, "%d", abs(sourceactor->xspr.data1)); int len = int(strlen(buffer)); for (int i = 0; i < len; i++) { dataIndex = (buffer[i] - 52) + 4; if ((data = getDataFieldOfObject(objType, destSect, destWall, objactor, dataIndex)) == -65535) { Printf(PRINT_HIGH, "\nWrong index of data (%c) for IncDec Gen! Only 1, 2, 3 and 4 indexes allowed!\n", buffer[i]); continue; } if (sourceactor->xspr.data2 < sourceactor->xspr.data3) { data = ClipRange(data, sourceactor->xspr.data2, sourceactor->xspr.data3); if ((data += sourceactor->xspr.data4) >= sourceactor->xspr.data3) { switch (sourceactor->spr.flags) { case kModernTypeFlag0: case kModernTypeFlag1: if (data > sourceactor->xspr.data3) data = sourceactor->xspr.data3; break; case kModernTypeFlag2: if (data > sourceactor->xspr.data3) data = sourceactor->xspr.data3; if (!incDecGoalValueIsReached(sourceactor)) break; tmp = sourceactor->xspr.data3; sourceactor->xspr.data3 = sourceactor->xspr.data2; sourceactor->xspr.data2 = tmp; break; case kModernTypeFlag3: if (data > sourceactor->xspr.data3) data = sourceactor->xspr.data2; break; } } } else if (sourceactor->xspr.data2 > sourceactor->xspr.data3) { data = ClipRange(data, sourceactor->xspr.data3, sourceactor->xspr.data2); if ((data -= sourceactor->xspr.data4) <= sourceactor->xspr.data3) { switch (sourceactor->spr.flags) { case kModernTypeFlag0: case kModernTypeFlag1: if (data < sourceactor->xspr.data3) data = sourceactor->xspr.data3; break; case kModernTypeFlag2: if (data < sourceactor->xspr.data3) data = sourceactor->xspr.data3; if (!incDecGoalValueIsReached(sourceactor)) break; tmp = sourceactor->xspr.data3; sourceactor->xspr.data3 = sourceactor->xspr.data2; sourceactor->xspr.data2 = tmp; break; case kModernTypeFlag3: if (data < sourceactor->xspr.data3) data = sourceactor->xspr.data2; break; } } } sourceactor->xspr.sysData1 = data; setDataValueOfObject(objType, destSect, destWall, objactor, dataIndex, data); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void sprite2sectorSlope(DBloodActor* actor, sectortype* pSector, int rel, bool forcez) { int slope = 0; double z = 0; switch (rel) { default: z = getflorzofslopeptr(actor->sector(), actor->spr.pos); if ((actor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR) && actor->hasX() && actor->xspr.Touch) z--; slope = pSector->floorheinum; break; case 1: z = getceilzofslopeptr(actor->sector(), actor->spr.pos); if ((actor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR) && actor->hasX() && actor->xspr.Touch) z++; slope = pSector->ceilingheinum; break; } spriteSetSlope(actor, slope); if (forcez) actor->spr.pos.Z = z; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSlopeChanger(DBloodActor* sourceactor, int objType, sectortype* pSect, DBloodActor* objActor) { int slope, oslope; bool flag2 = (sourceactor->spr.flags & kModernTypeFlag2); if (sourceactor->spr.flags & kModernTypeFlag1) slope = ClipRange(sourceactor->xspr.data2, -32767, 32767); else slope = (32767 / kPercFull) * ClipRange(sourceactor->xspr.data2, -kPercFull, kPercFull); if (objType == OBJ_SECTOR) { switch (sourceactor->xspr.data1) { case 2: case 0: // just set floor slope if (flag2) { pSect->setfloorslope(slope); } else { // force closest floor aligned sprites to inherit slope of the sector's floor oslope = pSect->floorheinum; BloodSectIterator it(pSect); while (auto iactor = it.Next()) { if (!(iactor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)) continue; else if (getflorzofslopeptr(pSect, iactor->spr.pos) - kSlopeDist <= iactor->spr.pos.Z) { sprite2sectorSlope(iactor, pSect, 0, true); // set temporary slope of floor pSect->floorheinum = slope; // force sloped sprites to be on floor slope z sprite2sectorSlope(iactor, pSect, 0, true); // restore old slope for next sprite pSect->floorheinum = oslope; } } // finally set new slope of floor pSect->setfloorslope(slope); } if (sourceactor->xspr.data1 == 0) break; [[fallthrough]]; case 1: // just set ceiling slope if (flag2) { pSect->setceilingslope(slope); } else { oslope = pSect->ceilingheinum; BloodSectIterator it(pSect); while (auto iactor = it.Next()) { if (!(iactor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)) continue; else if (getceilzofslopeptr(pSect, iactor->spr.pos) + kSlopeDist >= iactor->spr.pos.Z) { sprite2sectorSlope(iactor, pSect, 1, true); // set new slope of ceiling pSect->ceilingheinum = slope; // force sloped sprites to be on ceiling slope z sprite2sectorSlope(iactor, pSect, 1, true); // restore old slope for next sprite pSect->ceilingheinum = oslope; } } // finally set new slope of ceiling pSect->setceilingslope(slope); } break; } // let's give a little impulse to the physics sprites... BloodSectIterator it(pSect); while (auto iactor = it.Next()) { if (iactor->hasX() && iactor->xspr.physAttr > 0) { iactor->xspr.physAttr |= kPhysFalling; iactor->vel.Z += FixedToFloat(1); } else if ((iactor->spr.statnum == kStatThing || iactor->spr.statnum == kStatDude) && (iactor->spr.flags & kPhysGravity)) { iactor->spr.flags |= kPhysFalling; iactor->vel.Z += FixedToFloat(1); } } } else if (objType == OBJ_SPRITE) { if (!(objActor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_FLOOR)) objActor->spr.cstat |= CSTAT_SPRITE_ALIGNMENT_FLOOR; if ((objActor->spr.cstat & CSTAT_SPRITE_ALIGNMENT_SLOPE) != CSTAT_SPRITE_ALIGNMENT_SLOPE) objActor->spr.cstat |= CSTAT_SPRITE_ALIGNMENT_SLOPE; switch (sourceactor->xspr.data4) { case 1: case 2: case 3: if (!objActor->insector()) break; switch (sourceactor->xspr.data4) { case 1: sprite2sectorSlope(objActor, objActor->sector(), 0, flag2); break; case 2: sprite2sectorSlope(objActor, objActor->sector(), 1, flag2); break; case 3: if (getflorzofslopeptr(objActor->sector(), objActor->spr.pos) - kSlopeDist <= objActor->spr.pos.Z) sprite2sectorSlope(objActor, objActor->sector(), 0, flag2); if (getceilzofslopeptr(objActor->sector(), objActor->spr.pos) + kSlopeDist >= objActor->spr.pos.Z) sprite2sectorSlope(objActor, objActor->sector(), 1, flag2); break; } break; default: spriteSetSlope(objActor, slope); break; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useDataChanger(DBloodActor* sourceactor, int objType, sectortype* pSector, walltype* pWall, DBloodActor* objActor) { bool flag1 = (sourceactor->spr.flags & kModernTypeFlag1); switch (objType) { case OBJ_SECTOR: if (flag1 || valueIsBetween(sourceactor->xspr.data1, -1, 32767)) setDataValueOfObject(objType, pSector, pWall, nullptr, 1, sourceactor->xspr.data1); break; case OBJ_SPRITE: if (flag1 || valueIsBetween(sourceactor->xspr.data1, -1, 32767)) setDataValueOfObject(objType, pSector, pWall, objActor, 1, sourceactor->xspr.data1); if (flag1 || valueIsBetween(sourceactor->xspr.data2, -1, 32767)) setDataValueOfObject(objType, pSector, pWall, objActor, 2, sourceactor->xspr.data1); if (flag1 || valueIsBetween(sourceactor->xspr.data3, -1, 32767)) setDataValueOfObject(objType, pSector, pWall, objActor, 3, sourceactor->xspr.data1); if (flag1 || valueIsBetween(sourceactor->xspr.data4, -1, 65535)) setDataValueOfObject(objType, pSector, pWall, objActor, 4, sourceactor->xspr.data1); break; case OBJ_WALL: if (flag1 || valueIsBetween(sourceactor->xspr.data1, -1, 32767)) setDataValueOfObject(objType, pSector, pWall, nullptr, 1, sourceactor->xspr.data1); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useSectorLightChanger(DBloodActor* sourceactor, sectortype* pSector) { bool relative = (sourceactor->spr.flags & kModernTypeFlag16); auto pXSector = &pSector->xs(); if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) { if (relative) pXSector->wave = ClipHigh(pXSector->wave + sourceactor->xspr.data1, 11); else pXSector->wave = ClipHigh(sourceactor->xspr.data1, 11); } if (valueIsBetween(sourceactor->xspr.data2, -128, 128)) { if (relative) pXSector->amplitude = ClipRange(pXSector->amplitude + sourceactor->xspr.data2, -127, 127); else pXSector->amplitude = (int8_t)sourceactor->xspr.data2; } if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) { if (relative) pXSector->freq = ClipHigh(pXSector->freq + sourceactor->xspr.data3, 255); else pXSector->freq = ClipHigh(sourceactor->xspr.data3, 255); } if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) { if (relative) pXSector->phase = ClipHigh(pXSector->phase + sourceactor->xspr.data4, 255); else pXSector->phase = ClipHigh(sourceactor->xspr.data4, 255); } if (sourceactor->spr.flags) { if (sourceactor->spr.flags != kModernTypeFlag1) { pXSector->shadeAlways = (sourceactor->spr.flags & kModernTypeFlag1) ? true : false; pXSector->shadeFloor = (sourceactor->spr.flags & kModernTypeFlag2) ? true : false; pXSector->shadeCeiling = (sourceactor->spr.flags & kModernTypeFlag4) ? true : false; pXSector->shadeWalls = (sourceactor->spr.flags & kModernTypeFlag8) ? true : false; pXSector->color = (sourceactor->spr.pal) ? true : false; auto cstat = sourceactor->spr.cstat; if ((cstat & CSTAT_SPRITE_ALIGNMENT_MASK) == CSTAT_SPRITE_ALIGNMENT_FLOOR) { // !!! xsector pal bits must be extended if (cstat & CSTAT_SPRITE_ONE_SIDE) { if (cstat & CSTAT_SPRITE_YFLIP) pXSector->ceilpal = sourceactor->spr.pal; else pXSector->floorpal = sourceactor->spr.pal; } else { pXSector->floorpal = sourceactor->spr.pal; pXSector->ceilpal = sourceactor->spr.pal; } } } else { pXSector->shadeAlways = true; } } // add to shadeList if (!shadeList.Contains(pSector)) shadeList.Push(pSector); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void useTargetChanger(DBloodActor* sourceactor, DBloodActor* actor) { if (!actor->IsDudeActor() || actor->spr.statnum != kStatDude) { switch (actor->spr.type) // can be dead dude turned in gib { // make current target and all other dudes not attack this dude anymore case kThingBloodBits: case kThingBloodChunks: aiFightFreeTargets(actor); return; default: return; } } int receiveHp = 33 + Random(33); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type); int matesPerEnemy = 1; // dude is burning? if (actor->xspr.burnTime > 0 && actor->GetBurnSource()) { if (IsBurningDude(actor)) return; else { auto burnactor = actor->GetBurnSource(); if (burnactor->hasX()) { if (sourceactor->xspr.data2 == 1 && actor->xspr.rxID == burnactor->xspr.rxID) { actor->xspr.burnTime = 0; // heal dude a bit in case of friendly fire int startHp = (actor->xspr.sysData2 > 0) ? ClipRange(actor->xspr.sysData2 << 4, 1, 65535) : pDudeInfo->startHealth << 4; if (actor->xspr.health < (unsigned)startHp) actHealDude(actor, receiveHp, startHp); } else if (burnactor->xspr.health <= 0) { actor->xspr.burnTime = 0; } } } } auto playeractor = aiFightTargetIsPlayer(actor); // special handling for player(s) if target changer data4 > 2. if (playeractor != nullptr) { auto actLeech = leechIsDropped(actor); if (sourceactor->xspr.data4 == 3) { aiSetTarget(actor, actor->spr.pos); aiSetGenIdleState(actor); if (actor->spr.type == kDudeModernCustom && actLeech) removeLeech(actLeech); } else if (sourceactor->xspr.data4 == 4) { aiSetTarget(actor, playeractor->spr.pos); if (actor->spr.type == kDudeModernCustom && actLeech) removeLeech(actLeech); } } int maxAlarmDudes = 8 + Random(8); auto targetactor = actor->GetTarget(); if (targetactor && targetactor->hasX() && playeractor == nullptr) { if (aiFightUnitCanFly(actor) && aiFightIsMeleeUnit(targetactor) && !aiFightUnitCanFly(targetactor)) actor->spr.flags |= 0x0002; else if (aiFightUnitCanFly(actor)) actor->spr.flags &= ~0x0002; if (!targetactor->IsDudeActor() || targetactor->xspr.health < 1 || !aiFightDudeCanSeeTarget(actor, pDudeInfo, targetactor)) { aiSetTarget(actor, actor->spr.pos); } // dude attack or attacked by target that does not fit by data id? else if (sourceactor->xspr.data1 != 666 && targetactor->xspr.data1 != sourceactor->xspr.data1) { if (aiFightDudeIsAffected(targetactor)) { // force stop attack target aiSetTarget(actor, actor->spr.pos); if (actor->GetBurnSource() == targetactor) { actor->xspr.burnTime = 0; actor->SetBurnSource(nullptr); } // force stop attack dude aiSetTarget(targetactor, targetactor->spr.pos); if (targetactor->GetBurnSource() == actor) { targetactor->xspr.burnTime = 0; targetactor->SetBurnSource(nullptr); } } } else if (sourceactor->xspr.data2 == 1 && actor->xspr.rxID == targetactor->xspr.rxID) { auto mateactor = targetactor; // heal dude int startHp = (actor->xspr.sysData2 > 0) ? ClipRange(actor->xspr.sysData2 << 4, 1, 65535) : pDudeInfo->startHealth << 4; if (actor->xspr.health < (unsigned)startHp) actHealDude(actor, receiveHp, startHp); // heal mate startHp = (mateactor->xspr.sysData2 > 0) ? ClipRange(mateactor->xspr.sysData2 << 4, 1, 65535) : getDudeInfo(mateactor->spr.type)->startHealth << 4; if (mateactor->xspr.health < (unsigned)startHp) actHealDude(mateactor, receiveHp, startHp); auto matetarget = mateactor->GetTarget(); if (matetarget != nullptr && matetarget->hasX()) { // force mate stop attack dude, if he does if (matetarget == actor) { aiSetTarget(mateactor, mateactor->spr.pos); } else if (actor->xspr.rxID != matetarget->xspr.rxID) { // force dude to attack same target that mate have aiSetTarget(actor, matetarget); return; } else { // force mate to stop attack another mate aiSetTarget(mateactor, mateactor->spr.pos); } } // force dude stop attack mate, if target was not changed previously if (actor == mateactor) aiSetTarget(actor, actor->spr.pos); } // check if targets aims player then force this target to fight with dude else if (aiFightTargetIsPlayer(actor) != nullptr) { aiSetTarget(targetactor, actor); } int mDist = 3; if (aiFightIsMeleeUnit(actor)) mDist = 2; if (targetactor != nullptr && aiFightGetTargetDist(actor, pDudeInfo, targetactor) < mDist) { if (!isActive(actor)) aiActivateDude(actor); return; } // lets try to look for target that fits better by distance else if ((PlayClock & 256) != 0 && (targetactor == nullptr || aiFightGetTargetDist(actor, pDudeInfo, targetactor) >= mDist)) { auto newtargactor = aiFightGetTargetInRange(actor, 0, mDist, sourceactor->xspr.data1, sourceactor->xspr.data2); if (newtargactor != nullptr) { // Make prev target not aim in dude if (targetactor) { aiSetTarget(targetactor, targetactor->spr.pos); if (!isActive(newtargactor)) aiActivateDude(newtargactor); } // Change target for dude aiSetTarget(actor, newtargactor); if (!isActive(actor)) aiActivateDude(actor); // ...and change target of target to dude to force it fight if (sourceactor->xspr.data3 > 0 && newtargactor->GetTarget() != actor) { aiSetTarget(newtargactor, actor); if (!isActive(newtargactor)) aiActivateDude(newtargactor); } return; } } } if ((targetactor == nullptr || playeractor != nullptr) && (PlayClock & 32) != 0) { // try find first target that dude can see BloodStatIterator it(kStatDude); while (auto newtargactor = it.Next()) { if (newtargactor->GetTarget() == actor) { aiSetTarget(actor, newtargactor); return; } // skip non-dudes and players if (!newtargactor->IsDudeActor() || (newtargactor->IsPlayerActor() && sourceactor->xspr.data4 > 0) || newtargactor->GetOwner() == actor) continue; // avoid self aiming, those who dude can't see, and those who dude own else if (!aiFightDudeCanSeeTarget(actor, pDudeInfo, newtargactor) || actor == newtargactor) continue; // if Target Changer have data1 = 666, everyone can be target, except AI team mates. else if (sourceactor->xspr.data1 != 666 && sourceactor->xspr.data1 != newtargactor->xspr.data1) continue; // don't attack immortal, burning dudes and mates if (IsBurningDude(newtargactor) || !IsKillableDude(newtargactor) || (sourceactor->xspr.data2 == 1 && actor->xspr.rxID == newtargactor->xspr.rxID)) continue; if (sourceactor->xspr.data2 == 0 || (sourceactor->xspr.data2 == 1 && !aiFightMatesHaveSameTarget(actor, newtargactor, matesPerEnemy))) { // Change target for dude aiSetTarget(actor, newtargactor); if (!isActive(actor)) aiActivateDude(actor); // ...and change target of target to dude to force it fight if (sourceactor->xspr.data3 > 0 && newtargactor->GetTarget() != actor) { aiSetTarget(newtargactor, actor); if (playeractor == nullptr && !isActive(newtargactor)) aiActivateDude(newtargactor); if (sourceactor->xspr.data3 == 2) aiFightAlarmDudesInSight(newtargactor, maxAlarmDudes); } return; } break; } } // got no target - let's ask mates if they have targets if ((actor->GetTarget() == nullptr || playeractor != nullptr) && sourceactor->xspr.data2 == 1 && (PlayClock & 64) != 0) { DBloodActor* pMateTargetActor = aiFightGetMateTargets(actor); if (pMateTargetActor != nullptr && pMateTargetActor->hasX()) { if (aiFightDudeCanSeeTarget(actor, pDudeInfo, pMateTargetActor)) { if (pMateTargetActor->GetTarget() == nullptr) { aiSetTarget(pMateTargetActor, actor); if (pMateTargetActor->IsDudeActor() && !isActive(pMateTargetActor)) aiActivateDude(pMateTargetActor); } aiSetTarget(actor, pMateTargetActor); if (!isActive(actor)) aiActivateDude(actor); return; // try walk in mate direction in case if not see the target } else if (pMateTargetActor->GetTarget() && aiFightDudeCanSeeTarget(actor, pDudeInfo, pMateTargetActor->GetTarget())) { actor->SetTarget(pMateTargetActor); auto pMate = pMateTargetActor->GetTarget(); actor->xspr.TargetPos = pMate->spr.pos; if (!isActive(actor)) aiActivateDude(actor); return; } } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void usePictureChanger(DBloodActor* sourceactor, int objType, sectortype* targSect, walltype* targWall, DBloodActor* objActor) { switch (objType) { case OBJ_SECTOR: if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) targSect->setfloortexture(tileGetTextureID(sourceactor->xspr.data1)); if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) targSect->setceilingtexture(tileGetTextureID(sourceactor->xspr.data2)); if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) targSect->floorpal = uint8_t(sourceactor->xspr.data3); if (valueIsBetween(sourceactor->xspr.data4, -1, 65535)) targSect->ceilingpal = uint8_t(sourceactor->xspr.data4); break; case OBJ_SPRITE: if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) objActor->spr.setspritetexture(tileGetTextureID(sourceactor->xspr.data1)); if (sourceactor->xspr.data2 >= 0) objActor->spr.shade = (sourceactor->xspr.data2 > 127) ? 127 : sourceactor->xspr.data2; else if (sourceactor->xspr.data2 < -1) objActor->spr.shade = (sourceactor->xspr.data2 < -127) ? -127 : sourceactor->xspr.data2; if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) objActor->spr.pal = uint8_t(sourceactor->xspr.data3); break; case OBJ_WALL: if (valueIsBetween(sourceactor->xspr.data1, -1, 32767)) targWall->setwalltexture(tileGetTextureID(sourceactor->xspr.data1)); if (valueIsBetween(sourceactor->xspr.data2, -1, 32767)) targWall->setovertexture(tileGetTextureID(sourceactor->xspr.data2)); if (valueIsBetween(sourceactor->xspr.data3, -1, 32767)) targWall->pal = uint8_t(sourceactor->xspr.data3); break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- QAV* playerQavSceneLoad(int qavId) { QAV* pQav = getQAV(qavId); if (!pQav) viewSetSystemMessage("Failed to load QAV animation #%d", qavId); return pQav; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void playerQavSceneProcess(DBloodPlayer* pPlayer, QAVSCENE* pQavScene) { auto initiator = pQavScene->initiator; if (initiator->hasX()) { if (initiator->xspr.waitTime > 0 && --initiator->xspr.sysData1 <= 0) { if (initiator->xspr.txID >= kChannelUser) { for (int i = bucketHead[initiator->xspr.txID]; i < bucketHead[initiator->xspr.txID + 1]; i++) { if (rxBucket[i].isActor()) { auto rxactor = rxBucket[i].actor(); if (!rxactor || !rxactor->hasX() || rxactor == initiator) continue; if (rxactor->spr.type == kModernPlayerControl && rxactor->xspr.command == 67) { if (rxactor->xspr.data2 == initiator->xspr.data2 || rxactor->xspr.locked) continue; else trPlayerCtrlStartScene(rxactor, pPlayer, true); return; } } nnExtTriggerObject(rxBucket[i], initiator->xspr.command, pPlayer->GetActor()); } } trPlayerCtrlStopScene(pPlayer); } else { playerQavScenePlay(pPlayer); pPlayer->weaponTimer = ClipLow(pPlayer->weaponTimer -= 4, 0); } } else { pQavScene->initiator = nullptr; pPlayer->sceneQav = -1; pQavScene->qavResrc = NULL; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void playerQavSceneDraw(DBloodPlayer* pPlayer, int shade, double xpos, double ypos, int palnum, DAngle angle) { if (pPlayer == NULL || pPlayer->sceneQav == -1) return; QAVSCENE* pQavScene = &gPlayerCtrl[pPlayer->pnum].qavScene; auto actor = pQavScene->initiator; if (pQavScene->qavResrc != NULL) { QAV* pQAV = pQavScene->qavResrc; int duration; double interpfrac; qavProcessTimer(pPlayer, pQAV, &duration, &interpfrac); int flags = 2; int nInv = powerupCheck(pPlayer, kPwUpShadowCloak); if (nInv >= 120 * 8 || (nInv != 0 && (PlayClock & 32))) { shade = -128; flags |= 1; } // draw as weapon if (!(actor->spr.flags & kModernTypeFlag1)) { pQAV->x = xpos; pQAV->y = ypos; pQAV->Draw(duration, flags, shade, palnum, true, interpfrac, angle); // draw fullscreen (currently 4:3 only) } else { // What an awful hack. This throws proper ordering out of the window, but there is no way to reproduce this better with strict layering of elements. // From the above commit it seems to be incomplete anyway... pQAV->Draw(duration, flags, shade, palnum, false, interpfrac, angle); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void playerQavScenePlay(DBloodPlayer* pPlayer) { if (pPlayer == NULL) return; QAVSCENE* pQavScene = &gPlayerCtrl[pPlayer->pnum].qavScene; if (pPlayer->sceneQav == -1 && pQavScene->initiator != nullptr) pPlayer->sceneQav = pQavScene->initiator->xspr.data2; if (pQavScene->qavResrc != NULL) { QAV* pQAV = pQavScene->qavResrc; int nTicks = pQAV->duration - pPlayer->weaponTimer; pQAV->Play(nTicks - 4, nTicks, pPlayer->qavCallback, pPlayer); } } void playerQavSceneReset(DBloodPlayer* pPlayer) { QAVSCENE* pQavScene = &gPlayerCtrl[pPlayer->pnum].qavScene; pQavScene->initiator = nullptr; pQavScene->dummy = pPlayer->sceneQav = -1; pQavScene->qavResrc = NULL; } bool playerSizeShrink(DBloodPlayer* pPlayer, int divider) { pPlayer->GetActor()->xspr.scale = 256 / divider; playerSetRace(pPlayer, kModeHumanShrink); return true; } bool playerSizeGrow(DBloodPlayer* pPlayer, int multiplier) { pPlayer->GetActor()->xspr.scale = 256 * multiplier; playerSetRace(pPlayer, kModeHumanGrown); return true; } bool playerSizeReset(DBloodPlayer* pPlayer) { playerSetRace(pPlayer, kModeHuman); pPlayer->GetActor()->xspr.scale = 0; return true; } void playerDeactivateShrooms(DBloodPlayer* pPlayer) { powerupDeactivate(pPlayer, kPwUpGrowShroom); pPlayer->pwUpTime[kPwUpGrowShroom] = 0; powerupDeactivate(pPlayer, kPwUpShrinkShroom); pPlayer->pwUpTime[kPwUpShrinkShroom] = 0; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodPlayer* getPlayerById(int id) { // relative to connected players if (id >= 1 && id <= kMaxPlayers) { id = id - 1; for (int i = connecthead; i >= 0; i = connectpoint2[i]) { if (id == getPlayer(i)->pnum) return getPlayer(i); } // absolute sprite type } else if (id >= kDudePlayer1 && id <= kDudePlayer8) { for (int i = connecthead; i >= 0; i = connectpoint2[i]) { if (id == getPlayer(i)->GetActor()->spr.type) return getPlayer(i); } } //viewSetSystemMessage("There is no player id #%d", id); return NULL; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool IsBurningDude(DBloodActor* actor) { if (actor == NULL) return false; switch (actor->spr.type) { case kDudeBurningInnocent: case kDudeBurningCultist: case kDudeBurningZombieAxe: case kDudeBurningZombieButcher: case kDudeBurningTinyCaleb: case kDudeBurningBeast: case kDudeModernCustomBurning: return true; } return false; } bool IsKillableDude(DBloodActor* actor) { switch (actor->spr.type) { case kDudeGargoyleStatueFlesh: case kDudeGargoyleStatueStone: return false; default: if (!actor->IsDudeActor() || actor->xspr.locked == 1) return false; return true; } } bool isGrown(DBloodActor* actor) { if (powerupCheck(getPlayer(actor->spr.type - kDudePlayer1), kPwUpGrowShroom) > 0) return true; else if (actor->hasX() && actor->xspr.scale >= 512) return true; else return false; } bool isShrunk(DBloodActor* actor) { if (powerupCheck(getPlayer(actor->spr.type - kDudePlayer1), kPwUpShrinkShroom) > 0) return true; else if (actor->hasX() && actor->xspr.scale > 0 && actor->xspr.scale <= 128) return true; else return false; } bool isActive(DBloodActor* actor) { if (!actor->hasX()) return false; switch (actor->xspr.aiState->stateType) { case kAiStateIdle: case kAiStateGenIdle: case kAiStateSearch: case kAiStateMove: case kAiStateOther: return false; default: return true; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int getDataFieldOfObject(EventObject& eob, int dataIndex) { int data = -65535; if (eob.isActor()) { auto actor = eob.actor(); if (actor) { switch (dataIndex) { case 1: return actor->xspr.data1; case 2: return actor->xspr.data2; case 3: switch (actor->spr.type) { case kDudeModernCustom: return actor->xspr.sysData1; default: return actor->xspr.data3; } case 4: return actor->xspr.data4; default: return data; } } } else if (eob.isSector()) { return eob.sector()->xs().data; } else if (eob.isWall()) { return eob.wall()->xw().data; } return data; } int getDataFieldOfObject(int objType, sectortype* sect, walltype* wal, DBloodActor* actor, int dataIndex) { int data = -65535; switch (objType) { case OBJ_SPRITE: switch (dataIndex) { case 1: return actor->xspr.data1; case 2: return actor->xspr.data2; case 3: switch (actor->spr.type) { case kDudeModernCustom: return actor->xspr.sysData1; default: return actor->xspr.data3; } case 4: return actor->xspr.data4; default: return data; } case OBJ_SECTOR: return sect->xs().data; case OBJ_WALL: return wal->xw().data; default: return data; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool setDataValueOfObject(int objType, sectortype* sect, walltype* wal, DBloodActor* objActor, int dataIndex, int value) { switch (objType) { case OBJ_SPRITE: { int type = objActor->spr.type; // exceptions if (objActor->IsDudeActor() && objActor->xspr.health <= 0) return true; switch (type) { case kThingBloodBits: case kThingBloodChunks: case kThingZombieHead: return true; break; } switch (dataIndex) { case 1: objActor->xspr.data1 = value; switch (type) { case kSwitchCombo: if (value == objActor->xspr.data2) SetSpriteState(objActor, 1, nullptr); else SetSpriteState(objActor, 0, nullptr); break; case kDudeModernCustom: case kDudeModernCustomBurning: objActor->genDudeExtra.updReq[kGenDudePropertyWeapon] = true; objActor->genDudeExtra.updReq[kGenDudePropertyDmgScale] = true; evPostActor(objActor, kGenDudeUpdTimeRate, kCallbackGenDudeUpdate); break; } return true; case 2: objActor->xspr.data2 = value; switch (type) { case kDudeModernCustom: case kDudeModernCustomBurning: objActor->genDudeExtra.updReq[kGenDudePropertySpriteSize] = true; objActor->genDudeExtra.updReq[kGenDudePropertyMass] = true; objActor->genDudeExtra.updReq[kGenDudePropertyDmgScale] = true; objActor->genDudeExtra.updReq[kGenDudePropertyStates] = true; objActor->genDudeExtra.updReq[kGenDudePropertyAttack] = true; evPostActor(objActor, kGenDudeUpdTimeRate, kCallbackGenDudeUpdate); break; } return true; case 3: objActor->xspr.data3 = value; switch (type) { case kDudeModernCustom: case kDudeModernCustomBurning: objActor->xspr.sysData1 = value; break; } return true; case 4: objActor->xspr.data4 = value; return true; default: return false; } } case OBJ_SECTOR: sect->xs().data = value; return true; case OBJ_WALL: wal->xw().data = value; return true; default: return false; } } //--------------------------------------------------------------------------- // // a replacement of vanilla CanMove for patrol dudes // //--------------------------------------------------------------------------- bool nnExtCanMove(DBloodActor* actor, DBloodActor* target, DAngle nAngle, double nRange) { DVector3 pos = actor->spr.pos; DVector3 nAngVect(nAngle.ToVector(), 0); auto pSector = actor->sector(); HitScan(actor, pos.Z, nAngVect, CLIPMASK0, nRange); double nDist = (actor->spr.pos.XY() - gHitInfo.hitpos.XY()).Length(); if (target != nullptr && nDist - actor->clipdist < nRange) return (target == gHitInfo.actor()); pos += nAngVect * nRange; updatesectorz(pos, &pSector); if (!pSector) return false; if (pSector->hasX()) { XSECTOR* pXSector = &pSector->xs(); return !((pSector->type == kSectorDamage || pXSector->damageType > 0) && pXSector->state && !nnExtIsImmune(actor, pXSector->damageType, 16)); } return true; } //--------------------------------------------------------------------------- // // a replacement of vanilla aiChooseDirection for patrol dudes // //--------------------------------------------------------------------------- void nnExtAiSetDirection(DBloodActor* actor, DAngle direction) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DAngle vc = deltaangle(actor->spr.Angles.Yaw, direction); DAngle v8 = vc > nullAngle ? DAngle180 / 3 : -DAngle180 / 3; double range = actor->vel.XY().dot(actor->spr.Angles.Yaw.ToVector()) * 120; if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw + vc, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw + vc; else if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw + vc / 2, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw + vc / 2; else if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw - vc / 2, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw - vc / 2; else if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw + v8, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw + v8; else if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw; else if (nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw - v8, range)) actor->xspr.goalAng = actor->spr.Angles.Yaw - v8; else actor->xspr.goalAng = actor->spr.Angles.Yaw + DAngle180 / 3; if (actor->xspr.dodgeDir) { if (!nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw + DAngle90 * actor->xspr.dodgeDir, 512)) { actor->xspr.dodgeDir = -actor->xspr.dodgeDir; if (!nnExtCanMove(actor, actor->GetTarget(), actor->spr.Angles.Yaw + DAngle90 * actor->xspr.dodgeDir, 512)) actor->xspr.dodgeDir = 0; } } } //--------------------------------------------------------------------------- // /// patrol functions // //--------------------------------------------------------------------------- void aiPatrolState(DBloodActor* actor, int state) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax&& actor->hasX()); assert(actor->GetTarget()); auto markeractor = actor->GetTarget(); assert(markeractor->spr.type == kMarkerPath); bool nSeqOverride = false, crouch = false; int i, seq = -1, start = 0, end = kPatrolStateSize; const DUDEINFO_EXTRA* pExtra = &gDudeInfoExtra[actor->spr.type - kDudeBase]; switch (state) { case kAiStatePatrolWaitL: seq = pExtra->idlgseqofs; start = 0; end = 2; break; case kAiStatePatrolMoveL: seq = pExtra->mvegseqofs; start = 2, end = 7; break; case kAiStatePatrolTurnL: seq = pExtra->mvegseqofs; start = 7, end = 12; break; case kAiStatePatrolWaitW: seq = pExtra->idlwseqofs; start = 12; end = 18; break; case kAiStatePatrolMoveW: seq = pExtra->mvewseqofs; start = 18; end = 25; break; case kAiStatePatrolTurnW: seq = pExtra->mvewseqofs; start = 25; end = 32; break; case kAiStatePatrolWaitC: seq = pExtra->idlcseqofs; start = 32; end = 36; crouch = true; break; case kAiStatePatrolMoveC: seq = pExtra->mvecseqofs; start = 36; end = 39; crouch = true; break; case kAiStatePatrolTurnC: seq = pExtra->mvecseqofs; start = 39; end = kPatrolStateSize; crouch = true; break; } if (markeractor->xspr.data4 > 0) seq = markeractor->xspr.data4, nSeqOverride = true; else if (!nSeqOverride && state == kAiStatePatrolWaitC && (actor->spr.type == kDudeCultistTesla || actor->spr.type == kDudeCultistTNT)) seq = 11537, nSeqOverride = true; // these don't have idle crouch seq for some reason... if (seq < 0) return aiPatrolStop(actor, nullptr); for (i = start; i < end; i++) { AISTATE* newState = &genPatrolStates[i]; if (newState->stateType != state || (!nSeqOverride && seq != newState->seqId)) continue; if (actor->spr.type == kDudeModernCustom) aiGenDudeNewState(actor, newState); else aiNewState(actor, newState); if (crouch) actor->xspr.unused1 |= kDudeFlagCrouch; else actor->xspr.unused1 &= ~kDudeFlagCrouch; if (nSeqOverride) seqSpawn(seq, actor); return; } if (i == end) { viewSetSystemMessage("No patrol state #%d found for dude #%d (type = %d)", state, actor->GetIndex(), actor->spr.type); aiPatrolStop(actor, nullptr); } } //--------------------------------------------------------------------------- // // check if some dude already follows the given marker // //--------------------------------------------------------------------------- DBloodActor* aiPatrolMarkerBusy(DBloodActor* except, DBloodActor* marker) { BloodStatIterator it(kStatDude); while (auto actor = it.Next()) { if (!actor->IsDudeActor() || actor == except || !actor->hasX()) continue; auto targ = actor->GetTarget(); if (actor->xspr.health > 0 && targ != nullptr && targ->spr.type == kMarkerPath && targ == marker) return actor; } return nullptr; } //--------------------------------------------------------------------------- // // check if some dude already follows the given marker // //--------------------------------------------------------------------------- bool aiPatrolMarkerReached(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); const DUDEINFO_EXTRA* pExtra = &gDudeInfoExtra[actor->spr.type - kDudeBase]; auto markeractor = actor->GetTarget(); if (markeractor && markeractor->spr.type == kMarkerPath) { double okDist = max(markeractor->clipdist * 8, 4.); auto ov = markeractor->spr.pos.XY() - actor->spr.pos.XY(); // this was already shifted right by 4 in the old code. if (ov.Length() <= okDist) { if (spriteIsUnderwater(actor) || pExtra->flying) { okDist = markeractor->clipdist * 16; double ztop, zbot, ztop2, zbot2; GetActorExtents(actor, &ztop, &zbot); GetActorExtents(markeractor, &ztop2, &zbot2); double oZ1 = abs(zbot - ztop2); double oZ2 = abs(ztop - zbot2); if (oZ1 > okDist && oZ2 > okDist) return false; } return true; } } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* findNextMarker(DBloodActor* mark, bool back) { BloodStatIterator it(kStatPathMarker); while (auto next = it.Next()) { if (!next->hasX() || next == mark) continue; if ((next->xspr.locked || next->xspr.isTriggered || next->xspr.DudeLockout) || (back && next->xspr.data2 != mark->xspr.data1) || (!back && next->xspr.data1 != mark->xspr.data2)) continue; return next; } return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool markerIsNode(DBloodActor* mark, bool back) { int cnt = 0; BloodStatIterator it(kStatPathMarker); while (auto next = it.Next()) { if (!next->hasX() || next == mark) continue; if ((next->xspr.locked || next->xspr.isTriggered || next->xspr.DudeLockout) || (back && next->xspr.data2 != mark->xspr.data1) || (!back && next->xspr.data1 != mark->xspr.data2)) continue; if (++cnt > 1) return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolSetMarker(DBloodActor* actor) { auto targetactor = actor->GetTarget(); DBloodActor* selected = nullptr; double closest = DBL_MAX; // select closest marker that dude can see if (targetactor == nullptr) { double zt1, zb1, zt2, zb2; double dist; GetActorExtents(actor, &zt2, &zb2); BloodStatIterator it(kStatPathMarker); while (auto nextactor = it.Next()) { if (!nextactor->hasX()) continue; if (nextactor->xspr.locked || nextactor->xspr.isTriggered || nextactor->xspr.DudeLockout || (dist = (nextactor->spr.pos.XY() - actor->spr.pos.XY()).LengthSquared()) > closest) continue; GetActorExtents(nextactor, &zt1, &zb1); if (cansee(DVector3(nextactor->spr.pos.XY(), zt1), nextactor->sector(), DVector3(actor->spr.pos.XY(), zt2), actor->sector())) { closest = dist; selected = nextactor; } } } // set next marker else if (targetactor->spr.type == kMarkerPath && targetactor->hasX()) { // idea: which one of next (allowed) markers are closer to the potential target? // idea: -3 select random next marker that dude can see in radius of reached marker // if reached marker is in radius of another marker with -3, but greater radius, use that marker // idea: for nodes only flag32 = specify if enemy must return back to node or allowed to select // another marker which belongs that node? DBloodActor* prevactor = nullptr; DBloodActor* firstFinePath = nullptr; int next; int breakChance = 0; if (actor->prevmarker) { prevactor = actor->prevmarker; } bool node = markerIsNode(targetactor, false); actor->xspr.unused2 = aiPatrolGetPathDir(actor, targetactor); // decide if it should go back or forward if (actor->xspr.unused2 == kPatrolMoveBackward && Chance(0x8000) && node) actor->xspr.unused2 = kPatrolMoveForward; bool back = (actor->xspr.unused2 == kPatrolMoveBackward); next = (back) ? targetactor->xspr.data1 : targetactor->xspr.data2; BloodStatIterator it(kStatPathMarker); while (auto nextactor = it.Next()) { if (nextactor == targetactor || !nextactor->hasX()) continue; else if (actor->xspr.TargetPos.X >= 0 && nextactor == prevactor && node) { if (targetactor->xspr.data2 == prevactor->xspr.data1) continue; } if ((nextactor->xspr.locked || nextactor->xspr.isTriggered || nextactor->xspr.DudeLockout) || (back && nextactor->xspr.data2 != next) || (!back && nextactor->xspr.data1 != next)) continue; if (firstFinePath == nullptr) firstFinePath = nextactor; if (aiPatrolMarkerBusy(actor, nextactor) && !Chance(0x0010)) continue; else selected = nextactor; breakChance += nnExtRandom(1, 5); if (breakChance >= 5) break; } if (firstFinePath == nullptr) { viewSetSystemMessage("No markers with id #%d found for dude #%d! (back = %d)", next, actor->GetIndex(), back); return; } if (selected == nullptr) selected = firstFinePath; } if (!selected) return; actor->SetTarget(selected); selected->SetOwner(actor); actor->prevmarker = targetactor; // keep previous marker index here, use actual sprite coords when selecting direction } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolStop(DBloodActor* actor, DBloodActor* targetactor, bool alarm) { if (actor->hasX()) { actor->xspr.data3 = 0; // reset spot progress actor->xspr.unused1 &= ~kDudeFlagCrouch; // reset the crouch status actor->xspr.unused2 = kPatrolMoveForward; // reset path direction actor->prevmarker = nullptr; actor->xspr.TargetPos.X = -1; // reset the previous marker index if (actor->xspr.health <= 0) return; auto mytarget = actor->GetTarget(); if (mytarget && mytarget->spr.type == kMarkerPath) { if (targetactor == nullptr) actor->spr.Angles.Yaw = mytarget->spr.Angles.Yaw; actor->SetTarget(nullptr); } bool patrol = actor->xspr.dudeFlag4; actor->xspr.dudeFlag4 = 0; if (targetactor && targetactor->hasX() && targetactor->IsDudeActor()) { aiSetTarget(actor, targetactor); aiActivateDude(actor); // alarm only when in non-recoil state? //if (((actor->xspr.unused1 & kDudeFlagStealth) && stype != kAiStateRecoil) || !(actor->xspr.unused1 & kDudeFlagStealth)) { //if (alarm) aiPatrolAlarmFull(actor, targetactor, Chance(0x0100)); if (alarm) aiPatrolAlarmLite(actor, targetactor); //} } else { aiInitSprite(actor); aiSetTarget(actor, actor->xspr.TargetPos); } actor->xspr.dudeFlag4 = patrol; // this must be kept so enemy can patrol after respawn again } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolRandGoalAng(DBloodActor* actor) { DAngle goal = DAngle90; if (Chance(0x4000)) goal = DAngle360 / 3; if (Chance(0x4000)) goal = DAngle180; if (Chance(0x8000)) goal = -goal; actor->xspr.goalAng = (actor->spr.Angles.Yaw + goal).Normalized360(); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolTurn(DBloodActor* actor) { DAngle nTurnRange = mapangle((getDudeInfo(actor->spr.type)->angSpeed << 1) >> 4); DAngle nAng = deltaangle(actor->spr.Angles.Yaw, actor->xspr.goalAng); actor->spr.Angles.Yaw += clamp(nAng, -nTurnRange, nTurnRange); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolMove(DBloodActor* actor) { auto targetactor = actor->GetTarget(); if (!(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax) || !targetactor) return; int dudeIdx = actor->spr.type - kDudeBase; switch (actor->spr.type) { case kDudeCultistShotgunProne: dudeIdx = kDudeCultistShotgun - kDudeBase; break; case kDudeCultistTommyProne: dudeIdx = kDudeCultistTommy - kDudeBase; break; } DUDEINFO* pDudeInfo = &dudeInfo[dudeIdx]; const DUDEINFO_EXTRA* pExtra = &gDudeInfoExtra[dudeIdx]; DVector3 dv = targetactor->spr.pos - actor->spr.pos.plusZ(-pDudeInfo->eyeHeight); // eyeHeight is in map units! DAngle goalAng = DAngle180 / 3; if (pExtra->flying || spriteIsUnderwater(actor)) { goalAng *= 0.5; actor->vel.Z = dv.Z * 6; if (actor->spr.flags & kPhysGravity) actor->spr.flags &= ~kPhysGravity; } else if (!pExtra->flying) { actor->spr.flags |= kPhysGravity | kPhysFalling; } DAngle nTurnRange = pDudeInfo->TurnRange() / 64; DAngle nAng = deltaangle(actor->spr.Angles.Yaw, actor->xspr.goalAng); actor->spr.Angles.Yaw += clamp(nAng, -nTurnRange, nTurnRange); if (abs(nAng) > goalAng || ((targetactor->xspr.waitTime > 0 || targetactor->xspr.data1 == targetactor->xspr.data2) && aiPatrolMarkerReached(actor))) { actor->vel.XY().Zero(); return; } if (actor->hit.hit.type == kHitSprite) { actor->xspr.dodgeDir = Chance(0x5000) ? 1 : -1; auto hitactor = actor->hit.hit.actor(); if (hitactor) { if (xsprIsFine(hitactor) && hitactor->xspr.health) { hitactor->xspr.dodgeDir = (actor->xspr.dodgeDir > 0) ? -1 : 1; if (!hitactor->vel.XY().isZero()) aiMoveDodge(hitactor); } } aiMoveDodge(actor); } else { int frontSpeed = pDudeInfo->frontSpeed; switch (actor->spr.type) { case kDudeModernCustom: case kDudeModernCustomBurning: frontSpeed = actor->genDudeExtra.moveSpeed; break; } frontSpeed = aiPatrolGetVelocity(pDudeInfo->frontSpeed, targetactor->xspr.busyTime); actor->vel += actor->spr.Angles.Yaw.ToVector() * FixedToFloat(frontSpeed); } double vel = (actor->xspr.unused1 & kDudeFlagCrouch) ? kMaxPatrolCrouchVelocity : kMaxPatrolVelocity; vel *= dv.XY().Length() / 1024; // was: MulScale16 with length << 6, effectively resulting in >> 10. actor->vel.X = clamp(actor->vel.X, -vel, vel); actor->vel.Y = clamp(actor->vel.Y, -vel, vel); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolAlarmLite(DBloodActor* actor, DBloodActor* targetactor) { if (!xsprIsFine(actor) || !actor->IsDudeActor()) return; if (actor->xspr.health <= 0) return; double zt1, zb1, zt2, zb2; GetActorExtents(actor, &zt1, &zb1); GetActorExtents(targetactor, &zt2, &zb2); BloodStatIterator it(kStatDude); while (auto dudeactor = it.Next()) { if (dudeactor == actor || !dudeactor->IsDudeActor() || dudeactor->IsPlayerActor() || !dudeactor->hasX()) continue; if (dudeactor->xspr.health <= 0) continue; double eaz2 = (getDudeInfo(targetactor->spr.type)->eyeHeight * targetactor->spr.scale.Y); double nDist = (dudeactor->spr.pos.XY() - actor->spr.pos.XY()).LengthSquared(); if (nDist >= kPatrolAlarmSeeDistSq || !cansee(DVector3(actor->spr.pos.XY(), zt1), actor->sector(), dudeactor->spr.pos.plusZ(-eaz2), dudeactor->sector())) { nDist = (dudeactor->spr.pos.XY() - targetactor->spr.pos.XY()).LengthSquared(); if (nDist >= kPatrolAlarmSeeDistSq || !cansee(DVector3(targetactor->spr.pos.XY(), zt2), targetactor->sector(), dudeactor->spr.pos.plusZ(-eaz2), dudeactor->sector())) continue; } if (aiInPatrolState(dudeactor->xspr.aiState)) aiPatrolStop(dudeactor, dudeactor->GetTarget()); if (dudeactor->GetTarget() && dudeactor->GetTarget() == actor->GetTarget()) continue; aiSetTarget(dudeactor, targetactor); aiActivateDude(dudeactor); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolAlarmFull(DBloodActor* actor, DBloodActor* targetactor, bool chain) { if (!xsprIsFine(actor) || !actor->IsDudeActor()) return; if (actor->xspr.health <= 0) return; double eaz2 = (getDudeInfo(actor->spr.type)->eyeHeight * actor->spr.scale.Y); auto pos2 = actor->spr.pos.plusZ(-eaz2); auto pSect2 = actor->sector(); double tzt, tzb; GetActorExtents(targetactor, &tzt, &tzb); DVector3 pos3(targetactor->spr.pos.XY(), tzt); auto pSect3 = targetactor->sector(); BloodStatIterator it(kStatDude); while (auto dudeactor = it.Next()) { if (dudeactor == actor || !dudeactor->IsDudeActor() || dudeactor->IsPlayerActor() || !dudeactor->hasX()) continue; if (dudeactor->xspr.health <= 0) continue; double eaz1 = (getDudeInfo(dudeactor->spr.type)->eyeHeight * dudeactor->spr.scale.Y); auto pos1 = dudeactor->spr.pos.plusZ(-eaz1); auto pSect1 = dudeactor->sector(); double nDist1 = (pos1 - pos2).Length(); double nDist2 = (pos1 - pos3).Length(); //double hdist = (dudeactor->xspr.dudeDeaf) ? 0 : getDudeInfo(dudeactor->spr.type)->HearDist() / 4; double sdist = (dudeactor->xspr.dudeGuard) ? 0 : getDudeInfo(dudeactor->spr.type)->SeeDist() / 2; if (//(nDist1 < hdist || nDist2 < hdist) || ((nDist1 < sdist && cansee(pos1, pSect1, pos2, pSect2)) || (nDist2 < sdist && cansee(pos1, pSect1, pos3, pSect3)))) { if (aiInPatrolState(dudeactor->xspr.aiState)) aiPatrolStop(dudeactor, dudeactor->GetTarget()); if (dudeactor->GetTarget() && dudeactor->GetTarget() == actor->GetTarget()) continue; if (actor->GetTarget()) aiSetTarget(dudeactor, actor->GetTarget()); else aiSetTarget(dudeactor, actor->spr.pos); aiActivateDude(dudeactor); if (chain) aiPatrolAlarmFull(dudeactor, targetactor, Chance(0x0010)); //Printf("Dude #%d alarms dude #%d", actor->GetIndex(), dudeactor->spr.index); } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool spritesTouching(DBloodActor* actor1, DBloodActor* actor2) { if (!actor1->hasX() || !actor2->hasX()) return false; auto hit = &actor1->hit; DBloodActor* hitactor = nullptr; if (hit->hit.type == kHitSprite) hitactor = hit->hit.actor(); else if (hit->florhit.type == kHitSprite) hitactor = hit->florhit.actor(); else if (hit->ceilhit.type == kHitSprite) hitactor = hit->ceilhit.actor(); else return false; return hitactor->hasX() && hitactor == actor2; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiCanCrouch(DBloodActor* actor) { if (actor->spr.type >= kDudeBase && actor->spr.type < kDudeVanillaMax) return (gDudeInfoExtra[actor->spr.type - kDudeBase].idlcseqofs >= 0 && gDudeInfoExtra[actor->spr.type - kDudeBase].mvecseqofs >= 0); else if (actor->spr.type == kDudeModernCustom || actor->spr.type == kDudeModernCustomBurning) return actor->genDudeExtra.canDuck; return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool readyForCrit(DBloodActor* hunter, DBloodActor* victim) { if (!(hunter->spr.type >= kDudeBase && hunter->spr.type < kDudeMax) || !(victim->spr.type >= kDudeBase && victim->spr.type < kDudeMax)) return false; auto dvect = victim->spr.pos.XY() - hunter->spr.pos.XY(); if (dvect.Length() >= (437.5 / max(gGameOptions.nDifficulty >> 1, 1))) return false; return absangle(victim->spr.Angles.Yaw, dvect.Angle()) <= DAngle45; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* aiPatrolSearchTargets(DBloodActor* actor) { enum { kMaxPatrolFoundSounds = 256 }; // should be the maximum amount of sound channels the engine can play at the same time. PATROL_FOUND_SOUNDS patrolBonkles[kMaxPatrolFoundSounds]; assert(actor->IsDudeActor()); DUDEINFO* pDudeInfo = getDudeInfo(actor->spr.type - kDudeBase); DBloodPlayer* pPlayer = NULL; for (int i = 0; i < kMaxPatrolFoundSounds; i++) { patrolBonkles[i].snd = NO_SOUND; patrolBonkles[i].cur = 0; patrolBonkles[i].max = ClipLow((gGameOptions.nDifficulty + 1) >> 1, 1); } int i, mod, sndCnt = 0, seeChance, hearChance; bool stealth = (actor->xspr.unused1 & kDudeFlagStealth); bool blind = (actor->xspr.dudeGuard); bool deaf = (actor->xspr.dudeDeaf); int nRandomSkill = Random(gGameOptions.nDifficulty); DBloodActor* newtarget = nullptr; // search for player targets for (i = connecthead; i != -1; i = connectpoint2[i]) { pPlayer = getPlayer(i); if (!xsprIsFine(pPlayer->GetActor())) continue; auto plActor = pPlayer->GetActor(); if (plActor->xspr.health <= 0) continue; newtarget = nullptr; seeChance = hearChance = 0x0000; auto pos = plActor->spr.pos; auto dv = pos.XY() - actor->spr.pos.XY(); double nDistf = dv.Length(); double seeDistf = (stealth) ? pDudeInfo->SeeDist() / 3 : pDudeInfo->SeeDist() / 4; double hearDistf = pDudeInfo->HearDist(); double feelDistf = hearDistf / 2; // TO-DO: is there any dudes that sees this patrol dude and sees target? if (nDistf <= seeDistf) { double scratch; double eyeAboveZ = (pDudeInfo->eyeHeight * actor->spr.scale.Y); if (nDistf < seeDistf / 8) GetActorExtents(pPlayer->GetActor(), &pos.Z, &scratch); //use ztop of the target sprite if (!cansee(pos, plActor->sector(), actor->spr.pos - eyeAboveZ, actor->sector())) continue; } else continue; bool invisible = (powerupCheck(pPlayer, kPwUpShadowCloak) > 0); if (spritesTouching(actor, pPlayer->GetActor()) || spritesTouching(pPlayer->GetActor(), actor)) { DPrintf(DMSG_SPAMMY, "Patrol dude #%d spot the Player #%d via touch.", actor->GetIndex(), pPlayer->pnum + 1); if (invisible) pPlayer->pwUpTime[kPwUpShadowCloak] = 0; newtarget = pPlayer->GetActor(); break; } if (!deaf) { soundEngine->EnumerateChannels([&](FSoundChan* chan) { DVector2 sndv; sectortype* searchsect = nullptr; if (chan->SourceType == SOURCE_Actor) { auto emitterActor = (DBloodActor*)chan->Source; if (emitterActor == nullptr) return false; // not a valid source. sndv = emitterActor->spr.pos.XY(); // sound attached to the sprite if (pPlayer->GetActor() != emitterActor && emitterActor->GetOwner() != actor) { if (!emitterActor->insector()) return false; searchsect = emitterActor->sector(); } } else if (chan->SourceType == SOURCE_Unattached) { if (chan->UserData < 0 || !validSectorIndex(chan->UserData)) return false; // not a vaild sector sound. sndv.X = chan->Point[0]; sndv.Y = -chan->Point[1]; searchsect = §or[chan->UserData]; } if (searchsect == nullptr) return false; double nsDist = (sndv - actor->spr.pos.XY()).Length(); if (nsDist > hearDistf) return false; auto sndnum = chan->OrgID; // N same sounds per single enemy for (int f = 0; f < sndCnt; f++) { if (patrolBonkles[f].snd != sndnum) continue; else if (++patrolBonkles[f].cur >= patrolBonkles[f].max) return false; } if (sndCnt < kMaxPatrolFoundSounds - 1) patrolBonkles[sndCnt++].snd = sndnum; bool found = false; BloodSectIterator it(searchsect); while (auto act = it.Next()) { if (act->GetOwner() == pPlayer->GetActor()) { found = true; break; } } if (!found) return false; int f = max(int((hearDistf - nsDist) * 16), 0); int sndvol = int(chan->Volume * (80.f / 0.8f)); hearChance += sndvol * f + nRandomSkill; return (hearChance >= kMaxPatrolSpotValue); }); /* if (invisible && hearChance >= kMaxPatrolSpotValue >> 2) { newtarget = pPlayer->actor; pPlayer->pwUpTime[kPwUpShadowCloak] = 0; invisible = false; break; } */ } if (!invisible && (!deaf || !blind)) { if (stealth) { switch (pPlayer->lifeMode) { case kModeHuman: case kModeHumanShrink: if (pPlayer->lifeMode == kModeHumanShrink) { seeDistf *= 92. / 256; feelDistf *= 92. / 256; } if (pPlayer->posture == kPostureCrouch) { seeDistf *= 0.75; feelDistf *= 0.5; } break; case kModeHumanGrown: if (pPlayer->posture != kPostureCrouch) { seeDistf *= 328. / 256; feelDistf *= 320. / 256; } else { seeDistf *= 304. / 256; } break; } } bool itCanHear = false; bool itCanSee = false; feelDistf = max(feelDistf, 0.); seeDistf = max(seeDistf, 0.); if (hearDistf) { DBloodActor* act = pPlayer->GetActor(); itCanHear = (!deaf && (nDistf < hearDistf || hearChance > 0)); if (act && itCanHear && nDistf < feelDistf && (!act->vel.isZero())) hearChance += (int)max(((feelDistf - nDistf) + act->vel.Sum() * 64, 0.) / 256, 0.); } if (seeDistf) { DAngle periphery = max(pDudeInfo->Periphery(), DAngle60); DAngle nDeltaAngle = absangle(actor->spr.Angles.Yaw, dv.Angle()); if ((itCanSee = (!blind && nDistf < seeDistf && nDeltaAngle < periphery)) == true) { int base = 100 + ((20 * gGameOptions.nDifficulty) - (nDeltaAngle.Buildang() / 5)); //seeChance = base - MulScale(ClipRange(5 - gGameOptions.nDifficulty, 1, 4), nDist >> 1, 16); //scale(0x40000, a6, dist2); int d = int(nDistf * 4); int m = DivScale(d, 0x2000, 8); int t = MulScale(d, m, 8); seeChance = ClipRange(DivScale(base, t, 8), 0, kMaxPatrolSpotValue >> 1); } } if (!itCanSee && !itCanHear) continue; if (stealth) { // search in stealth regions to modify spot chances BloodStatIterator it(kStatModernStealthRegion); while (auto steal = it.Next()) { if (!steal->hasX()) continue; if (steal->xspr.locked) // ignore locked regions continue; bool fixd = (steal->spr.flags & kModernTypeFlag1); // fixed percent value bool both = (steal->spr.flags & kModernTypeFlag4); // target AND dude must be in this region bool dude = (both || (steal->spr.flags & kModernTypeFlag2)); // dude must be in this region bool trgt = (both || !dude); // target must be in this region bool crouch = (steal->spr.flags & kModernTypeFlag8); // target must crouch //bool floor = (iactor->spr.cstat & CSTAT_SPRITE_BLOCK); // target (or dude?) must touch floor of the sector if (trgt) { if (steal->xspr.data1 > 0) { if ((steal->spr.pos.XY() - plActor->spr.pos.XY()).Length() >= steal->xspr.data1) continue; } else if (plActor->sector() != steal->sector()) continue; if (crouch && pPlayer->posture == kPostureStand) continue; } if (dude) { if (steal->xspr.data1 > 0) { if ((steal->spr.pos.XY() - plActor->spr.pos.XY()).Length() >= steal->xspr.data1) continue; } else if (plActor->sector() != steal->sector()) continue; } if (itCanHear) { if (fixd) hearChance = ClipLow(hearChance, steal->xspr.data2); mod = (hearChance * steal->xspr.data2) / kPercFull; if (fixd) hearChance = mod; else hearChance += mod; hearChance = ClipRange(hearChance, -kMaxPatrolSpotValue, kMaxPatrolSpotValue); } if (itCanSee) { if (fixd) seeChance = ClipLow(seeChance, steal->xspr.data3); mod = (seeChance * steal->xspr.data3) / kPercFull; if (fixd) seeChance = mod; else seeChance += mod; seeChance = ClipRange(seeChance, -kMaxPatrolSpotValue, kMaxPatrolSpotValue); } // trigger this region if target gonna be spot if (steal->xspr.txID && actor->xspr.data3 + hearChance + seeChance >= kMaxPatrolSpotValue) trTriggerSprite(steal, kCmdToggle, pPlayer->GetActor()); // continue search another stealth regions to affect chances } } if (itCanHear && hearChance > 0) { DPrintf(DMSG_SPAMMY, "Patrol dude #%d hearing the Player #%d.", actor->GetIndex(), pPlayer->pnum + 1); actor->xspr.data3 = ClipRange(actor->xspr.data3 + hearChance, -kMaxPatrolSpotValue, kMaxPatrolSpotValue); if (!stealth) { newtarget = pPlayer->GetActor(); break; } } if (itCanSee && seeChance > 0) { //DPrintf(DMSG_SPAMMY, "Patrol dude #%d seeing the Player #%d.", actor->GetIndex(), pPlayer->nPlayer + 1); //actor->xspr.data3 += seeChance; actor->xspr.data3 = ClipRange(actor->xspr.data3 + seeChance, -kMaxPatrolSpotValue, kMaxPatrolSpotValue); if (!stealth) { newtarget = pPlayer->GetActor(); break; } } } // add check for corpses? if ((actor->xspr.data3 = ClipRange(actor->xspr.data3, 0, kMaxPatrolSpotValue)) == kMaxPatrolSpotValue) { newtarget = pPlayer->GetActor(); break; } //int perc = (100 * ClipHigh(actor->xspr.data3, kMaxPatrolSpotValue)) / kMaxPatrolSpotValue; //viewSetSystemMessage("%d / %d / %d / %d", hearChance, seeDist, seeChance, perc); } if (newtarget) return newtarget; actor->xspr.data3 -= ClipLow(((kPercFull * actor->xspr.data3) / kMaxPatrolSpotValue) >> 2, 3); return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolFlagsMgr(DBloodActor* sourceactor, DBloodActor* destactor, bool copy, bool init) { // copy flags if (copy) { destactor->xspr.dudeFlag4 = sourceactor->xspr.dudeFlag4; destactor->xspr.dudeAmbush = sourceactor->xspr.dudeAmbush; destactor->xspr.dudeGuard = sourceactor->xspr.dudeGuard; destactor->xspr.dudeDeaf = sourceactor->xspr.dudeDeaf; destactor->xspr.unused1 = sourceactor->xspr.unused1; if (sourceactor->xspr.unused1 & kDudeFlagStealth) destactor->xspr.unused1 |= kDudeFlagStealth; else destactor->xspr.unused1 &= ~kDudeFlagStealth; } // do init if (init && destactor->IsDudeActor() && !destactor->IsPlayerActor()) { if (!destactor->xspr.dudeFlag4) { if (aiInPatrolState(destactor->xspr.aiState)) aiPatrolStop(destactor, nullptr); } else { if (aiInPatrolState(destactor->xspr.aiState)) return; destactor->SetTarget(nullptr); destactor->xspr.stateTimer = 0; aiPatrolSetMarker(destactor); if (spriteIsUnderwater(destactor)) aiPatrolState(destactor, kAiStatePatrolWaitW); else aiPatrolState(destactor, kAiStatePatrolWaitL); destactor->xspr.data3 = 0; // reset the spot progress } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool aiPatrolGetPathDir(DBloodActor* actor, DBloodActor* marker) { if (actor->xspr.unused2 == kPatrolMoveForward) return (marker->xspr.data2 == -2) ? (bool)kPatrolMoveBackward : (bool)kPatrolMoveForward; else return (findNextMarker(marker, kPatrolMoveBackward) != nullptr) ? (bool)kPatrolMoveBackward : (bool)kPatrolMoveForward; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void aiPatrolThink(DBloodActor* actor) { assert(actor->spr.type >= kDudeBase && actor->spr.type < kDudeMax); DBloodActor* targetactor; unsigned int stateTimer; auto markeractor = actor->GetTarget(); if ((targetactor = aiPatrolSearchTargets(actor)) != nullptr) { aiPatrolStop(actor, targetactor, actor->xspr.dudeAmbush); return; } bool crouch = (actor->xspr.unused1 & kDudeFlagCrouch), uwater = spriteIsUnderwater(actor); if (markeractor == nullptr || (actor->spr.type == kDudeModernCustom && ((uwater && !canSwim(actor)) || !canWalk(actor)))) { aiPatrolStop(actor, nullptr); return; } const DUDEINFO_EXTRA* pExtra = &gDudeInfoExtra[actor->spr.type - kDudeBase]; bool isFinal = ((!actor->xspr.unused2 && markeractor->xspr.data2 == -1) || (actor->xspr.unused2 && markeractor->xspr.data1 == -1)); bool reached = false; if (aiPatrolWaiting(actor->xspr.aiState)) { //viewSetSystemMessage("WAIT %d / %d", actor->xspr.targetY, actor->xspr.stateTimer); if (actor->xspr.stateTimer > 0 || markeractor->xspr.data1 == markeractor->xspr.data2) { if (pExtra->flying) actor->vel.Z = Random2F(0x8000); // turn while waiting if (markeractor->spr.flags & kModernTypeFlag16) { stateTimer = actor->xspr.stateTimer; if (--actor->xspr.unused4 <= 0) { if (uwater) aiPatrolState(actor, kAiStatePatrolTurnW); else if (crouch) aiPatrolState(actor, kAiStatePatrolTurnC); else aiPatrolState(actor, kAiStatePatrolTurnL); actor->xspr.unused4 = kMinPatrolTurnDelay + Random(kPatrolTurnDelayRange); } // must restore stateTimer for waiting actor->xspr.stateTimer = stateTimer; } return; } // trigger at departure if (markeractor->xspr.triggerOff) { // send command if (markeractor->xspr.txID) { evSendActor(markeractor, markeractor->xspr.txID, (COMMAND_ID)markeractor->xspr.command, actor); // copy dude flags for current dude } else if (markeractor->xspr.command == kCmdDudeFlagsSet) { aiPatrolFlagsMgr(markeractor, actor, true, true); if (!actor->xspr.dudeFlag4) // this dude is not in patrol anymore return; } } // release the enemy if (isFinal) { aiPatrolStop(actor, nullptr); return; } // move next marker aiPatrolSetMarker(actor); } else if (aiPatrolTurning(actor->xspr.aiState)) { //viewSetSystemMessage("TURN"); if (absangle(actor->spr.Angles.Yaw, actor->xspr.goalAng) < minAngle) { // save imer for waiting stateTimer = actor->xspr.stateTimer; if (uwater) aiPatrolState(actor, kAiStatePatrolWaitW); else if (crouch) aiPatrolState(actor, kAiStatePatrolWaitC); else aiPatrolState(actor, kAiStatePatrolWaitL); // must restore it actor->xspr.stateTimer = stateTimer; } return; } else if ((reached = aiPatrolMarkerReached(actor)) == true) { markeractor->xspr.isTriggered = markeractor->xspr.triggerOnce; // can't select this marker for path anymore if true if (markeractor->spr.flags > 0) { if ((markeractor->spr.flags & kModernTypeFlag2) && (markeractor->spr.flags & kModernTypeFlag1)) crouch = !crouch; else if (markeractor->spr.flags & kModernTypeFlag2) crouch = false; else if ((markeractor->spr.flags & kModernTypeFlag1) && aiCanCrouch(actor)) crouch = true; } if (markeractor->xspr.waitTime > 0 || markeractor->xspr.data1 == markeractor->xspr.data2) { // take marker's angle if (!(markeractor->spr.flags & kModernTypeFlag4)) { actor->xspr.goalAng = ((!(markeractor->spr.flags & kModernTypeFlag8) && actor->xspr.unused2) ? markeractor->spr.Angles.Yaw+ DAngle180 : markeractor->spr.Angles.Yaw).Normalized360(); if (absangle(actor->spr.Angles.Yaw, actor->xspr.goalAng) > minAngle) // let the enemy play move animation while turning return; } if (markeractor->GetOwner() == actor) markeractor->SetOwner(aiPatrolMarkerBusy(actor, markeractor)); // trigger at arrival if (markeractor->xspr.triggerOn) { // send command if (markeractor->xspr.txID) { evSendActor(markeractor, markeractor->xspr.txID, (COMMAND_ID)markeractor->xspr.command, actor); } else if (markeractor->xspr.command == kCmdDudeFlagsSet) { // copy dude flags for current dude aiPatrolFlagsMgr(markeractor, actor, true, true); if (!actor->xspr.dudeFlag4) // this dude is not in patrol anymore return; } } if (uwater) aiPatrolState(actor, kAiStatePatrolWaitW); else if (crouch) aiPatrolState(actor, kAiStatePatrolWaitC); else aiPatrolState(actor, kAiStatePatrolWaitL); if (markeractor->xspr.waitTime) actor->xspr.stateTimer = (markeractor->xspr.waitTime * 120) / 10; if (markeractor->spr.flags & kModernTypeFlag16) actor->xspr.unused4 = kMinPatrolTurnDelay + Random(kPatrolTurnDelayRange); return; } else { if (markeractor->GetOwner() == actor) markeractor->SetOwner(aiPatrolMarkerBusy(actor, markeractor)); if (markeractor->xspr.triggerOn || markeractor->xspr.triggerOff) { if (markeractor->xspr.txID) { // send command at arrival if (markeractor->xspr.triggerOn) evSendActor(markeractor, markeractor->xspr.txID, (COMMAND_ID)markeractor->xspr.command, actor); // send command at departure if (markeractor->xspr.triggerOff) evSendActor(markeractor, markeractor->xspr.txID, (COMMAND_ID)markeractor->xspr.command, actor); // copy dude flags for current dude } else if (markeractor->xspr.command == kCmdDudeFlagsSet) { aiPatrolFlagsMgr(markeractor, actor, true, true); if (!actor->xspr.dudeFlag4) // this dude is not in patrol anymore return; } } // release the enemy if (isFinal) { aiPatrolStop(actor, nullptr); return; } // move the next marker aiPatrolSetMarker(actor); } } nnExtAiSetDirection(actor, (markeractor->spr.pos - actor->spr.pos).Angle()); if (aiPatrolMoving(actor->xspr.aiState) && !reached) return; else if (uwater) aiPatrolState(actor, kAiStatePatrolMoveW); else if (crouch) aiPatrolState(actor, kAiStatePatrolMoveC); else aiPatrolState(actor, kAiStatePatrolMoveL); return; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- int listTx(DBloodActor* actor, int tx) { if (txIsRanged(actor)) { if (tx == -1) tx = actor->xspr.data1; else if (tx < actor->xspr.data4) tx++; else tx = -1; } else { if (tx == -1) { for (int i = 0; i <= 3; i++) { if ((tx = GetDataVal(actor, i)) <= 0) continue; else return tx; } } else { int saved = tx; bool savedFound = false; for (int i = 0; i <= 3; i++) { tx = GetDataVal(actor, i); if (savedFound && tx > 0) return tx; else if (tx != saved) continue; else savedFound = true; } } tx = -1; } return tx; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* evrIsRedirector(DBloodActor* actor) { if (actor) { switch (actor->spr.type) { case kModernRandomTX: case kModernSequentialTX: if (actor->hasX() && actor->xspr.command == kCmdLink && !actor->xspr.locked) return actor; break; } } return nullptr; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- DBloodActor* evrListRedirectors(int objType, sectortype* pSector, walltype* pWall, DBloodActor* objActor, DBloodActor* pXRedir, int* tx) { if (!gEventRedirectsUsed) return nullptr; else if (pXRedir && (*tx = listTx(pXRedir, *tx)) != -1) return pXRedir; int id = 0; switch (objType) { case OBJ_SECTOR: { if (!pSector->hasX()) return nullptr; id = pSector->xs().txID; break; } case OBJ_SPRITE: if (!objActor) return nullptr; id = objActor->xspr.txID; break; case OBJ_WALL: { if (!pWall->hasX()) return nullptr; id = pWall->xw().txID; break; } default: return nullptr; } bool prevFound = false; for (int i = bucketHead[id]; i < bucketHead[id + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto rxactor = rxBucket[i].actor(); auto pXSpr = evrIsRedirector(rxactor); if (!pXSpr) continue; else if (prevFound || pXRedir == nullptr) { *tx = listTx(pXSpr, *tx); return pXSpr; } else if (pXRedir != pXSpr) continue; else prevFound = true; } *tx = -1; return NULL; } //--------------------------------------------------------------------------- // // this function checks if all TX objects have the same value // //--------------------------------------------------------------------------- bool incDecGoalValueIsReached(DBloodActor* actor) { if (actor->xspr.data3 != actor->xspr.sysData1) return false; char buffer[7]; snprintf(buffer, 7, "%d", abs(actor->xspr.data1)); int len = int(strlen(buffer)); int rx = -1; for (int i = bucketHead[actor->xspr.txID]; i < bucketHead[actor->xspr.txID + 1]; i++) { if (!rxBucket[i].isActor()) continue; auto rxactor = rxBucket[i].actor(); if (evrIsRedirector(rxactor)) continue; for (int a = 0; a < len; a++) { if (getDataFieldOfObject(rxBucket[i], (buffer[a] - 52) + 4) != actor->xspr.data3) return false; } } DBloodActor* pXRedir = nullptr; // check redirected TX buckets while ((pXRedir = evrListRedirectors(OBJ_SPRITE, nullptr, nullptr, actor, pXRedir, &rx)) != nullptr) { for (int i = bucketHead[rx]; i < bucketHead[rx + 1]; i++) { for (int a = 0; a < len; a++) { if (getDataFieldOfObject(rxBucket[i], (buffer[a] - 52) + 4) != actor->xspr.data3) return false; } } } return true; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void seqSpawnerOffSameTx(DBloodActor* actor) { for (auto§ : sector) { BloodSectIterator it(§); while (auto iactor = it.Next()) { if (iactor->spr.type != kModernSeqSpawner || !iactor->hasX() || iactor == actor) continue; if (/*iactor->xspr.txID == actor->xspr.txID &&*/ iactor->xspr.state == 1) { evKillActor(iactor); iactor->xspr.state = 0; } } } } //--------------------------------------------------------------------------- // // this function can be called via sending numbered command to TX kChannelModernEndLevelCustom // it allows to set custom next level instead of taking it from INI file. // //--------------------------------------------------------------------------- void levelEndLevelCustom(int nLevel) { gGameOptions.uGameFlags |= GF_AdvanceLevel; gNextLevel = FindMapByIndex(currentLevel->cluster, nLevel + 1); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void callbackUniMissileBurst(DBloodActor* actor, sectortype*) // 22 { if (!actor) return; if (actor->spr.statnum != kStatProjectile) return; auto nAngVec = actor->vel.XY().Angle().ToVector(); double nRadius = FixedToFloat(0x55555); for (int i = 0; i < 8; i++) { auto burstactor = actSpawnSprite(actor, 5); if (!burstactor) break; burstactor->spr.type = actor->spr.type; burstactor->spr.shade = actor->spr.shade; burstactor->spr.setspritetexture(actor->spr.spritetexture()); burstactor->spr.cstat = actor->spr.cstat; if ((burstactor->spr.cstat & CSTAT_SPRITE_BLOCK)) { burstactor->spr.cstat &= ~CSTAT_SPRITE_BLOCK; // we don't want missiles impact each other evPostActor(burstactor, 100, kCallbackMissileSpriteBlock); // so set blocking flag a bit later } burstactor->spr.pal = actor->spr.pal; burstactor->clipdist = actor->clipdist * 0.25; burstactor->spr.flags = actor->spr.flags; burstactor->spr.scale = actor->spr.scale; burstactor->spr.scale *= 0.5; burstactor->spr.Angles.Yaw = actor->spr.Angles.Yaw + mapangle(missileInfo[actor->spr.type - kMissileBase].angleOfs); burstactor->SetOwner(actor); actBuildMissile(burstactor, actor); auto spAngVec = DAngle::fromBam(i << 29).ToVector().Rotated90CW() * nRadius; if (i & 1) spAngVec *= 0.5; burstactor->vel += DVector3(DVector2(0, spAngVec.X).Rotated(nAngVec.X, nAngVec.Y), spAngVec.Y); evPostActor(burstactor, 960, kCallbackRemove); } evPostActor(actor, 0, kCallbackRemove); } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void callbackMakeMissileBlocking(DBloodActor* actor, sectortype*) // 23 { if (!actor || actor->spr.statnum != kStatProjectile) return; actor->spr.cstat |= CSTAT_SPRITE_BLOCK; } void callbackGenDudeUpdate(DBloodActor* actor, sectortype*) // 24 { if (actor) genDudeUpdate(actor); } void clampSprite(DBloodActor* actor, int which) { double zTop, zBot; if (actor->insector()) { GetActorExtents(actor, &zTop, &zBot); if (which & 0x01) actor->spr.pos.Z += min(getflorzofslopeptr(actor->sector(), actor->spr.pos) - zBot, 0.); if (which & 0x02) actor->spr.pos.Z += max(getceilzofslopeptr(actor->sector(), actor->spr.pos) - zTop, 0.); } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- void killEvents(int nRx, int nCmd) { for (int i = bucketHead[nRx]; i < bucketHead[nRx + 1]; i++) { if (nCmd == kCmdEventKillFull) evKill_(rxBucket[i]); } } void triggerTouchSprite(DBloodActor* actor, DBloodActor* hActor) { if (hActor && hActor->hasX()) { if (hActor->xspr.Touch && !hActor->xspr.isTriggered && (!hActor->xspr.DudeLockout || actor->IsPlayerActor())) trTriggerSprite(hActor, kCmdSpriteTouch, actor); // enough to reset gSpriteHit values actor->vel.X += FixedToFloat(5); } } void triggerTouchWall(DBloodActor* actor, walltype* pHWall) { if (pHWall && pHWall->hasX()) { if (pHWall->xw().triggerTouch && !pHWall->xw().isTriggered && (!pHWall->xw().dudeLockout || actor->IsPlayerActor())) trTriggerWall(pHWall, kCmdWallTouch, actor); // enough to reset gSpriteHit values actor->vel.X += FixedToFloat(5); } } void changeSpriteAngle(DBloodActor* pSpr, DAngle nAng) { if (!pSpr->IsDudeActor()) pSpr->spr.Angles.Yaw = nAng; else { DBloodPlayer* pPlayer = getPlayerById(pSpr->spr.type); if (pPlayer) pPlayer->GetActor()->spr.Angles.Yaw = nAng; else { pSpr->spr.Angles.Yaw = nAng; if (xsprIsFine(pSpr)) pSpr->xspr.goalAng = pSpr->spr.Angles.Yaw; } } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- static void dokillEffectGenCallbacks(DBloodActor* actor) { int l = sizeof(gEffectGenCallbacks) / sizeof(gEffectGenCallbacks[0]); while (--l >= 0) evKillActor(actor, (CALLBACK_ID)gEffectGenCallbacks[l]); } void killEffectGenCallbacks(DBloodActor* actor) { int i; if (actor->xspr.data2 < kEffectGenCallbackBase) return; switch (actor->xspr.txID) { case kChannelZero: // self dokillEffectGenCallbacks(actor); break; case kChannelAllPlayers: // player sprites for (i = connecthead; i >= 0; i = connectpoint2[i]) { if (getPlayer(i)->GetActor() != nullptr) dokillEffectGenCallbacks(getPlayer(i)->GetActor()); } break; case kChannelEventCauser: // worst case... for(auto& sec : sector) { BloodSectIterator it(&sec); while (auto act = it.Next()) dokillEffectGenCallbacks(act); } break; default: if (actor->xspr.txID >= kChannelUser) // TX ID scope { for (i = bucketHead[actor->xspr.txID]; i < bucketHead[actor->xspr.txID + 1]; i++) { if (rxBucket[i].isActor()) dokillEffectGenCallbacks(rxBucket[i].actor()); } } else if (actor->xspr.txID >= kChannelPlayer0 && actor->xspr.txID < kChannelPlayer0 + kMaxPlayers) { // player sprites DBloodPlayer* pPlayer = getPlayerById(actor->xspr.txID - kChannelPlayer0); if (pPlayer && pPlayer->GetActor() != nullptr) dokillEffectGenCallbacks(pPlayer->GetActor()); } break; } } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- bool xsprIsFine(DBloodActor* pSpr) { if (pSpr && pSpr->hasX() && !(pSpr->spr.flags & kHitagFree)) { if (!(pSpr->spr.flags & kHitagRespawn) || (pSpr->spr.statnum != kStatThing && pSpr->spr.statnum != kStatDude)) return true; } return false; } //--------------------------------------------------------------------------- // // // //--------------------------------------------------------------------------- FSerializer& Serialize(FSerializer& arc, const char* keyname, GENDUDEEXTRA& w, GENDUDEEXTRA* def) { if (arc.BeginObject(keyname)) { arc ("clipdist", w.clipdist) .Array("availdeaths", w.availDeaths, kDamageMax) ("movespeed", w.moveSpeed) ("firedist", w.fireDist) ("throwdist", w.throwDist) ("curweapon", w.curWeapon) ("weapontype", w.weaponType) ("basedispersion", w.baseDispersion) ("slavecount", w.slaveCount) ("lifeleech", w.pLifeLeech) .Array("slaves", w.slave, w.slaveCount) .Array("dmgcontrol", w.dmgControl, kDamageMax) .Array("updreq", w.updReq, kGenDudePropertyMax) ("flags", w.flags) .EndObject(); } return arc; } FSerializer& Serialize(FSerializer& arc, const char* keyname, SPRITEMASS& w, SPRITEMASS* def) { static SPRITEMASS nul; if (arc.isReading()) w = {}; if (arc.BeginObject(keyname)) { arc("seq", w.seqId, &nul.seqId) ("texid", w.texid, &nul.texid) ("scale", w.scale, &nul.scale) ("clipdist", w.clipDist) ("mass", w.mass) ("airvel", w.airVel) ("fraction", w.fraction) .EndObject(); } return arc; } FSerializer& Serialize(FSerializer& arc, const char* keyname, OBJECTS_TO_TRACK& w, OBJECTS_TO_TRACK* def) { static OBJECTS_TO_TRACK nul; if (arc.isReading()) w = {}; if (arc.BeginObject(keyname)) { arc("obj", w.obj, &nul.obj) ("cmd", w.cmd, &nul.cmd) .EndObject(); } return arc; } FSerializer& Serialize(FSerializer& arc, const char* keyname, TRCONDITION& w, TRCONDITION* def) { static TRCONDITION nul; if (arc.isReading()) w = {}; if (arc.BeginObject(keyname)) { arc("xindex", w.actor, &nul.actor) ("obj", w.objects) .EndObject(); } return arc; } void SerializeNNExts(FSerializer& arc) { if (arc.BeginObject("nnexts")) { arc("proxyspritescount", gProxySpritesCount) .Array("proxyspriteslist", gProxySpritesList, gProxySpritesCount) ("sightspritescount", gSightSpritesCount) .Array("sightspriteslist", gSightSpritesList, gSightSpritesCount) ("physspritescount", gPhysSpritesCount) .Array("physspriteslist", gPhysSpritesList, gPhysSpritesCount) ("impactspritescount", gImpactSpritesCount) .Array("impactspriteslist", gImpactSpritesList, gImpactSpritesCount) ("eventredirects", gEventRedirectsUsed) ("trcondition", gConditions); gSprNSect.Serialize(arc); arc.EndObject(); } } /////////////////////////////////////////////////////////////////// // This file provides modern features for mappers. // For full documentation please visit http://cruo.bloodgame.ru/xxsystem /////////////////////////////////////////////////////////////////// END_BLD_NS #endif