| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design) |
| 5 | * Copyright (c) 2017-2022, The LegacyClonk Team and contributors |
| 6 | * |
| 7 | * Distributed under the terms of the ISC license; see accompanying file |
| 8 | * "COPYING" for details. |
| 9 | * |
| 10 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 11 | * See accompanying file "TRADEMARK" for details. |
| 12 | * |
| 13 | * To redistribute this file separately, substitute the full license texts |
| 14 | * for the above references. |
| 15 | */ |
| 16 | |
| 17 | // C4AulFun-based effects assigned to an object |
| 18 | /* Also contains some helper functions for various landscape effects */ |
| 19 | |
| 20 | #include <C4Include.h> |
| 21 | |
| 22 | #include <C4Object.h> |
| 23 | #include <C4Random.h> |
| 24 | #include <C4Log.h> |
| 25 | #include <C4Game.h> |
| 26 | #include <C4Wrappers.h> |
| 27 | |
| 28 | #include <format> |
| 29 | #include <numbers> |
| 30 | |
| 31 | void C4Effect::AssignCallbackFunctions() |
| 32 | { |
| 33 | C4AulScript *pSrcScript = GetCallbackScript(); |
| 34 | // compose function names and search them |
| 35 | pFnStart = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxStart, args: +Name).c_str()); |
| 36 | pFnStop = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxStop, args: +Name).c_str()); |
| 37 | pFnTimer = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxTimer, args: +Name).c_str()); |
| 38 | pFnEffect = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxEffect, args: +Name).c_str()); |
| 39 | pFnDamage = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxDamage, args: +Name).c_str()); |
| 40 | } |
| 41 | |
| 42 | C4AulScript *C4Effect::GetCallbackScript() |
| 43 | { |
| 44 | // def script or global only? |
| 45 | C4AulScript *pSrcScript; C4Def *pDef; |
| 46 | if (pCommandTarget) |
| 47 | { |
| 48 | pSrcScript = &pCommandTarget->Def->Script; |
| 49 | // overwrite ID for sync safety in runtime join |
| 50 | idCommandTarget = pCommandTarget->id; |
| 51 | } |
| 52 | else if (idCommandTarget && (pDef = Game.Defs.ID2Def(id: idCommandTarget))) |
| 53 | pSrcScript = &pDef->Script; |
| 54 | else |
| 55 | pSrcScript = &Game.ScriptEngine; |
| 56 | return pSrcScript; |
| 57 | } |
| 58 | |
| 59 | C4Effect::C4Effect(C4Object *pForObj, const char *szName, int32_t iPrio, int32_t iTimerIntervall, C4Object *pCmdTarget, C4ID idCmdTarget, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, bool fDoCalls, int32_t &riStoredAsNumber, bool passErrors) |
| 60 | : EffectVars(0) |
| 61 | { |
| 62 | C4Effect *pPrev, *pCheck; |
| 63 | // assign values |
| 64 | SCopy(szSource: szName, sTarget: Name, iMaxL: C4MaxDefString); |
| 65 | iPriority = 0; // effect is not yet valid; some callbacks to other effects are done before |
| 66 | riStoredAsNumber = 0; |
| 67 | iIntervall = iTimerIntervall; |
| 68 | iTime = 0; |
| 69 | pCommandTarget = pCmdTarget; |
| 70 | pCommandTarget.Enumerate(); |
| 71 | idCommandTarget = idCmdTarget; |
| 72 | AssignCallbackFunctions(); |
| 73 | // get effect target |
| 74 | C4Effect **ppEffectList = pForObj ? &pForObj->pEffects : &Game.pGlobalEffects; |
| 75 | // assign a unique number for that object |
| 76 | iNumber = 1; |
| 77 | for (pCheck = *ppEffectList; pCheck; pCheck = pCheck->pNext) |
| 78 | if (pCheck->iNumber >= iNumber) iNumber = pCheck->iNumber + 1; |
| 79 | // register into object |
| 80 | pPrev = *ppEffectList; |
| 81 | if (pPrev && Abs(val: pPrev->iPriority) < iPrio) |
| 82 | { |
| 83 | while (pCheck = pPrev->pNext) |
| 84 | if (Abs(val: pCheck->iPriority) >= iPrio) break; else pPrev = pCheck; |
| 85 | // insert after previous |
| 86 | pNext = pPrev->pNext; |
| 87 | pPrev->pNext = this; |
| 88 | } |
| 89 | else |
| 90 | { |
| 91 | // insert as first effect |
| 92 | pNext = *ppEffectList; |
| 93 | *ppEffectList = this; |
| 94 | } |
| 95 | // no calls to be done: finished here |
| 96 | if (!fDoCalls) return; |
| 97 | // ask all effects with higher priority first - except for prio 1 effects, which are considered out of the priority call chain (as per doc) |
| 98 | try |
| 99 | { |
| 100 | bool fRemoveUpper = (iPrio != 1); |
| 101 | // note that apart from denying the creation of this effect, higher priority effects may also remove themselves |
| 102 | // or do other things with the effect list |
| 103 | // (which does not quite make sense, because the effect might be denied by another effect) |
| 104 | // so the priority is assigned after this call, marking this effect dead before it's definitely valid |
| 105 | if (fRemoveUpper && pNext) |
| 106 | { |
| 107 | int32_t iResult = pNext->Check(pForObj, szCheckEffect: Name, iPrio, iTimer: iIntervall, rVal1, rVal2, rVal3, rVal4, passErrors); |
| 108 | if (iResult) |
| 109 | { |
| 110 | // effect denied (iResult = -1), added to an effect (iResult = Number of that effect) |
| 111 | // or added to an effect that destroyed itself (iResult = -2) |
| 112 | if (iResult != C4Fx_Effect_Deny) riStoredAsNumber = iResult; |
| 113 | // effect is still marked dead |
| 114 | return; |
| 115 | } |
| 116 | } |
| 117 | // init effect |
| 118 | // higher-priority effects must be deactivated temporarily, and then reactivated regarding the new effect |
| 119 | // higher-level effects should not be inserted during the process of removing or adding a lower-level effect |
| 120 | // because that would cause a wrong initialization order |
| 121 | // (hardly ever causing trouble, however...) |
| 122 | C4Effect *pLastRemovedEffect = nullptr; |
| 123 | if (fRemoveUpper && pNext && pFnStart) |
| 124 | TempRemoveUpperEffects(pObj: pForObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect); |
| 125 | // bad things may happen |
| 126 | if (pForObj && !pForObj->Status) return; // this will be invalid! |
| 127 | iPriority = iPrio; // validate effect now |
| 128 | if (pFnStart) |
| 129 | if (pFnStart->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj: pForObj), C4VInt(iVal: iNumber), C4VInt(iVal: 0), rVal1, rVal2, rVal3, rVal4}, fPassErrors: true, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Start_Deny) |
| 130 | // the effect denied to start: assume it hasn't, and mark it dead |
| 131 | SetDead(); |
| 132 | if (fRemoveUpper && pNext && pFnStart) |
| 133 | TempReaddUpperEffects(pObj: pForObj, pLastReaddEffect: pLastRemovedEffect); |
| 134 | if (pForObj && !pForObj->Status) return; // this will be invalid! |
| 135 | // this effect has been created; hand back the number |
| 136 | riStoredAsNumber = iNumber; |
| 137 | } |
| 138 | catch (...) |
| 139 | { |
| 140 | if (*ppEffectList == this) |
| 141 | { |
| 142 | *ppEffectList = pNext; |
| 143 | } |
| 144 | else |
| 145 | { |
| 146 | pPrev->pNext = pNext; |
| 147 | } |
| 148 | pNext = nullptr; |
| 149 | throw; |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | C4Effect::C4Effect(StdCompiler *pComp) : EffectVars(0) |
| 154 | { |
| 155 | // defaults |
| 156 | iNumber = iPriority = iTime = iIntervall = 0; |
| 157 | pNext = nullptr; |
| 158 | // compile |
| 159 | pComp->Value(rStruct&: *this); |
| 160 | } |
| 161 | |
| 162 | C4Effect::~C4Effect() |
| 163 | { |
| 164 | // del following effects (not recursively) |
| 165 | C4Effect *pEffect; |
| 166 | while (pEffect = pNext) |
| 167 | { |
| 168 | pNext = pEffect->pNext; |
| 169 | pEffect->pNext = nullptr; |
| 170 | delete pEffect; |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | void C4Effect::EnumeratePointers() |
| 175 | { |
| 176 | // enum in all effects |
| 177 | C4Effect *pEff = this; |
| 178 | do |
| 179 | { |
| 180 | // command target |
| 181 | pEff->pCommandTarget.Enumerate(); |
| 182 | // effect var denumeration: not necessary, because this is done while saving |
| 183 | } while (pEff = pEff->pNext); |
| 184 | } |
| 185 | |
| 186 | void C4Effect::DenumeratePointers() |
| 187 | { |
| 188 | // denum in all effects |
| 189 | C4Effect *pEff = this; |
| 190 | do |
| 191 | { |
| 192 | // command target |
| 193 | pEff->pCommandTarget.Denumerate(); |
| 194 | // variable pointers |
| 195 | pEff->EffectVars.DenumeratePointers(); |
| 196 | // assign any callback functions |
| 197 | pEff->AssignCallbackFunctions(); |
| 198 | } while (pEff = pEff->pNext); |
| 199 | } |
| 200 | |
| 201 | void C4Effect::ClearPointers(C4Object *pObj) |
| 202 | { |
| 203 | // clear pointers in all effects |
| 204 | C4Effect *pEff = this; |
| 205 | do |
| 206 | // command target lost: effect dead w/o callback |
| 207 | if (pEff->pCommandTarget == pObj) |
| 208 | { |
| 209 | pEff->SetDead(); |
| 210 | pEff->pCommandTarget = nullptr; |
| 211 | } |
| 212 | while (pEff = pEff->pNext); |
| 213 | } |
| 214 | |
| 215 | C4Effect *C4Effect::Get(const char *szName, int32_t iIndex, int32_t iMaxPriority) |
| 216 | { |
| 217 | // safety |
| 218 | if (!szName) return nullptr; |
| 219 | // check all effects |
| 220 | C4Effect *pEff = this; |
| 221 | do |
| 222 | { |
| 223 | // skip dead |
| 224 | if (pEff->IsDead()) continue; |
| 225 | // skip effects with too high priority |
| 226 | if (iMaxPriority && pEff->iPriority > iMaxPriority) continue; |
| 227 | // wildcard compare name |
| 228 | const char *szEffectName = pEff->Name; |
| 229 | if (!SWildcardMatchEx(szString: szEffectName, szWildcard: szName)) continue; |
| 230 | // effect name matches |
| 231 | // check index |
| 232 | if (iIndex--) continue; |
| 233 | // effect found |
| 234 | return pEff; |
| 235 | } while (pEff = pEff->pNext); |
| 236 | // nothing found |
| 237 | return nullptr; |
| 238 | } |
| 239 | |
| 240 | C4Effect *C4Effect::Get(int32_t iNumber, bool fIncludeDead, int32_t iMaxPriority) |
| 241 | { |
| 242 | // check all effects |
| 243 | C4Effect *pEff = this; |
| 244 | do |
| 245 | if (pEff->iNumber == iNumber) |
| 246 | { |
| 247 | if (!pEff->IsDead() || fIncludeDead) |
| 248 | if (!iMaxPriority || pEff->iPriority <= iMaxPriority) |
| 249 | return pEff; |
| 250 | // effect found but denied |
| 251 | return nullptr; |
| 252 | } |
| 253 | while (pEff = pEff->pNext); |
| 254 | // nothing found |
| 255 | return nullptr; |
| 256 | } |
| 257 | |
| 258 | int32_t C4Effect::GetCount(const char *szMask, int32_t iMaxPriority) |
| 259 | { |
| 260 | // count all matching effects |
| 261 | int32_t iCnt = 0; C4Effect *pEff = this; |
| 262 | do if (!pEff->IsDead()) |
| 263 | if (!szMask || SWildcardMatchEx(szString: pEff->Name, szWildcard: szMask)) |
| 264 | if (!iMaxPriority || pEff->iPriority <= iMaxPriority) |
| 265 | ++iCnt; |
| 266 | while (pEff = pEff->pNext); |
| 267 | // return count |
| 268 | return iCnt; |
| 269 | } |
| 270 | |
| 271 | int32_t C4Effect::Check(C4Object *pForObj, const char *szCheckEffect, int32_t iPrio, int32_t iTimer, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, bool passErrors) |
| 272 | { |
| 273 | // priority=1: always OK; no callbacks |
| 274 | if (iPrio == 1) return 0; |
| 275 | // check this and other effects |
| 276 | C4Effect *pAddToEffect = nullptr; bool fDoTempCallsForAdd = false; |
| 277 | C4Effect *pLastRemovedEffect = nullptr; |
| 278 | for (C4Effect *pCheck = this; pCheck; pCheck = pCheck->pNext) |
| 279 | { |
| 280 | if (!pCheck->IsDead() && pCheck->pFnEffect && pCheck->iPriority >= iPrio) |
| 281 | { |
| 282 | int32_t iResult = pCheck->pFnEffect->Exec(pObj: pCheck->pCommandTarget, pPars: {C4VString(strString: szCheckEffect), C4VObj(pObj: pForObj), C4VInt(iVal: pCheck->iNumber), C4Value(), rVal1, rVal2, rVal3, rVal4}, fPassErrors: passErrors, nonStrict3WarnConversionOnly: true).getInt(); |
| 283 | if (iResult == C4Fx_Effect_Deny) |
| 284 | // effect denied |
| 285 | return C4Fx_Effect_Deny; |
| 286 | // add to other effect |
| 287 | if (iResult == C4Fx_Effect_Annul || iResult == C4Fx_Effect_AnnulCalls) |
| 288 | { |
| 289 | pAddToEffect = pCheck; |
| 290 | fDoTempCallsForAdd = (iResult == C4Fx_Effect_AnnulCalls); |
| 291 | } |
| 292 | } |
| 293 | } |
| 294 | // adding to other effect? |
| 295 | if (pAddToEffect) |
| 296 | { |
| 297 | // do temp remove calls if desired |
| 298 | if (pAddToEffect->pNext && fDoTempCallsForAdd) |
| 299 | pAddToEffect->TempRemoveUpperEffects(pObj: pForObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect); |
| 300 | C4Value Par1 = C4VString(strString: szCheckEffect), Par2 = C4VInt(iVal: iTimer); |
| 301 | int32_t iResult = pAddToEffect->DoCall(pObj: pForObj, PSFS_FxAdd, rVal1: Par1, rVal2: Par2, rVal3: rVal1, rVal4: rVal2, rVal5: rVal3, rVal6: rVal4).getInt(); |
| 302 | // do temp readd calls if desired |
| 303 | if (pAddToEffect->pNext && fDoTempCallsForAdd) |
| 304 | pAddToEffect->TempReaddUpperEffects(pObj: pForObj, pLastReaddEffect: pLastRemovedEffect); |
| 305 | // effect removed by this call? |
| 306 | if (iResult == C4Fx_Start_Deny) |
| 307 | { |
| 308 | pAddToEffect->Kill(pObj: pForObj); |
| 309 | return C4Fx_Effect_Annul; |
| 310 | } |
| 311 | else |
| 312 | // other effect is the target effect number |
| 313 | return pAddToEffect->iNumber; |
| 314 | } |
| 315 | // added to no effect and not denied |
| 316 | return 0; |
| 317 | } |
| 318 | |
| 319 | void C4Effect::Execute(C4Object *pObj) |
| 320 | { |
| 321 | // get effect list |
| 322 | C4Effect **ppEffectList = pObj ? &pObj->pEffects : &Game.pGlobalEffects; |
| 323 | // execute all effects not marked as dead |
| 324 | C4Effect *pEffect = this, **ppPrevEffect = ppEffectList; |
| 325 | do |
| 326 | { |
| 327 | // effect dead? |
| 328 | if (pEffect->IsDead()) |
| 329 | { |
| 330 | // delete it, then |
| 331 | C4Effect *pNextEffect = pEffect->pNext; |
| 332 | pEffect->pNext = nullptr; |
| 333 | delete pEffect; |
| 334 | // next effect |
| 335 | *ppPrevEffect = pEffect = pNextEffect; |
| 336 | } |
| 337 | else |
| 338 | { |
| 339 | // execute effect: time elapsed |
| 340 | ++pEffect->iTime; |
| 341 | // check timer execution |
| 342 | if (pEffect->iIntervall && !(pEffect->iTime % pEffect->iIntervall)) |
| 343 | if (pEffect->pFnTimer) |
| 344 | { |
| 345 | if (pEffect->pFnTimer->Exec(pObj: pEffect->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEffect->iNumber), C4VInt(iVal: pEffect->iTime)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Execute_Kill) |
| 346 | { |
| 347 | // safety: this class got deleted! |
| 348 | if (pObj && !pObj->Status) return; |
| 349 | // timer function decided to finish it |
| 350 | pEffect->Kill(pObj); |
| 351 | } |
| 352 | // safety: this class got deleted! |
| 353 | if (pObj && !pObj->Status) return; |
| 354 | } |
| 355 | else |
| 356 | // no timer function: mark dead after time elapsed |
| 357 | pEffect->Kill(pObj); |
| 358 | // next effect |
| 359 | ppPrevEffect = &pEffect->pNext; |
| 360 | pEffect = pEffect->pNext; |
| 361 | } |
| 362 | } while (pEffect); |
| 363 | } |
| 364 | |
| 365 | void C4Effect::Kill(C4Object *pObj) |
| 366 | { |
| 367 | const auto deletionTracker = TrackDeletion(); |
| 368 | // active? |
| 369 | C4Effect *pLastRemovedEffect = nullptr; |
| 370 | if (IsActive()) |
| 371 | { |
| 372 | // then temp remove all higher priority effects |
| 373 | TempRemoveUpperEffects(pObj, fTempRemoveThis: false, ppLastRemovedEffect: &pLastRemovedEffect); |
| 374 | } |
| 375 | else |
| 376 | { |
| 377 | // otherwise: temp reactivate before real removal |
| 378 | // this happens only if a lower priority effect removes an upper priority effect in its add- or removal-call |
| 379 | if (pFnStart && iPriority != 1) |
| 380 | { |
| 381 | pFnStart->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(C4FxCall_TempAddForRemoval)}, fPassErrors: false, nonStrict3WarnConversionOnly: true); |
| 382 | if (deletionTracker.IsDeleted()) |
| 383 | { |
| 384 | return; |
| 385 | } |
| 386 | } |
| 387 | } |
| 388 | // remove this effect |
| 389 | int32_t iPrevPrio = iPriority; SetDead(); |
| 390 | if (pFnStop) |
| 391 | { |
| 392 | if (pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Stop_Deny) |
| 393 | { |
| 394 | // effect denied to be removed: recover |
| 395 | iPriority = iPrevPrio; |
| 396 | } |
| 397 | |
| 398 | if (deletionTracker.IsDeleted()) |
| 399 | { |
| 400 | return; |
| 401 | } |
| 402 | } |
| 403 | // reactivate other effects |
| 404 | TempReaddUpperEffects(pObj, pLastReaddEffect: pLastRemovedEffect); |
| 405 | } |
| 406 | |
| 407 | void C4Effect::ClearAll(C4Object *pObj, int32_t iClearFlag) |
| 408 | { |
| 409 | // simply remove access all effects recursively, and do removal calls |
| 410 | // this does not regard lower-level effects being added in the removal calls, |
| 411 | // because this could hang the engine with poorly coded effects |
| 412 | if (pNext) pNext->ClearAll(pObj, iClearFlag); |
| 413 | if ((pObj && !pObj->Status) || IsDead()) return; |
| 414 | int32_t iPrevPrio = iPriority; |
| 415 | SetDead(); |
| 416 | if (pFnStop) |
| 417 | if (pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(iVal: iClearFlag)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt() == C4Fx_Stop_Deny) |
| 418 | { |
| 419 | // this stop-callback might have deleted the object and then denied its own removal |
| 420 | // must not modify self in this case... |
| 421 | if (pObj && !pObj->Status) return; |
| 422 | // effect denied to be removed: recover it |
| 423 | iPriority = iPrevPrio; |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | void C4Effect::DoDamage(C4Object *pObj, int32_t &riDamage, int32_t iDamageType, int32_t iCausePlr) |
| 428 | { |
| 429 | // ask all effects for damage adjustments |
| 430 | C4Effect *pEff = this; |
| 431 | do |
| 432 | { |
| 433 | if (!pEff->IsDead() && pEff->pFnDamage) |
| 434 | riDamage = pEff->pFnDamage->Exec(pObj: pEff->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEff->iNumber), C4VInt(iVal: riDamage), C4VInt(iVal: iDamageType), C4VInt(iVal: iCausePlr)}, fPassErrors: false, nonStrict3WarnConversionOnly: true).getInt(); |
| 435 | if (pObj && !pObj->Status) return; |
| 436 | } while ((pEff = pEff->pNext) && riDamage); |
| 437 | } |
| 438 | |
| 439 | C4Value C4Effect::DoCall(C4Object *pObj, const char *szFn, const C4Value &rVal1, const C4Value &rVal2, const C4Value &rVal3, const C4Value &rVal4, const C4Value &rVal5, const C4Value &rVal6, const C4Value &rVal7, bool passErrors, bool convertNilToIntBool) |
| 440 | { |
| 441 | // def script or global only? |
| 442 | C4AulScript *pSrcScript; C4Def *pDef; |
| 443 | if (pCommandTarget) |
| 444 | { |
| 445 | pSrcScript = &pCommandTarget->Def->Script; |
| 446 | // overwrite ID for sync safety in runtime join |
| 447 | idCommandTarget = pCommandTarget->id; |
| 448 | } |
| 449 | else if (idCommandTarget && (pDef = Game.Defs.ID2Def(id: idCommandTarget))) |
| 450 | pSrcScript = &pDef->Script; |
| 451 | else |
| 452 | pSrcScript = &Game.ScriptEngine; |
| 453 | // call it |
| 454 | C4AulFunc *pFn = pSrcScript->GetFuncRecursive(pIdtf: std::format(PSF_FxCustom, args: +Name, args&: szFn).c_str()); |
| 455 | if (!pFn) return C4Value(); |
| 456 | return pFn->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), rVal1, rVal2, rVal3, rVal4, rVal5, rVal6, rVal7}, fPassErrors: passErrors, nonStrict3WarnConversionOnly: true, convertNilToIntBool); |
| 457 | } |
| 458 | |
| 459 | void C4Effect::OnObjectChangedDef(C4Object *pObj) |
| 460 | { |
| 461 | // safety |
| 462 | if (!pObj) return; |
| 463 | // check all effects for reassignment |
| 464 | C4Effect *pCheck = this; |
| 465 | while (pCheck) |
| 466 | { |
| 467 | if (pCheck->pCommandTarget == pObj) |
| 468 | pCheck->ReAssignCallbackFunctions(); |
| 469 | pCheck = pCheck->pNext; |
| 470 | } |
| 471 | } |
| 472 | |
| 473 | void C4Effect::TempRemoveUpperEffects(C4Object *pObj, bool fTempRemoveThis, C4Effect **ppLastRemovedEffect) |
| 474 | { |
| 475 | if (pObj && !pObj->Status) return; // this will be invalid! |
| 476 | // priority=1: no callbacks |
| 477 | if (iPriority == 1) return; |
| 478 | // remove from high to low priority |
| 479 | // recursive implementation... |
| 480 | C4Effect *pEff = pNext; |
| 481 | while (pEff) if (pEff->IsActive()) break; else pEff = pEff->pNext; |
| 482 | // temp remove active effects with higher priority |
| 483 | if (pEff) pEff->TempRemoveUpperEffects(pObj, fTempRemoveThis: true, ppLastRemovedEffect); |
| 484 | // temp remove this |
| 485 | if (fTempRemoveThis) |
| 486 | { |
| 487 | FlipActive(); |
| 488 | // temp callbacks only for higher priority effects |
| 489 | if (pFnStop && iPriority != 1) pFnStop->Exec(pObj: pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: iNumber), C4VInt(C4FxCall_Temp), C4VBool(fVal: true)}, fPassErrors: false, nonStrict3WarnConversionOnly: true); |
| 490 | if (!*ppLastRemovedEffect) *ppLastRemovedEffect = this; |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | void C4Effect::TempReaddUpperEffects(C4Object *pObj, C4Effect *pLastReaddEffect) |
| 495 | { |
| 496 | // nothing to do? - this will also happen if TempRemoveUpperEffects did nothing due to priority==1 |
| 497 | if (!pLastReaddEffect) return; |
| 498 | if (pObj && !pObj->Status) return; // this will be invalid! |
| 499 | // simply activate all following, inactive effects |
| 500 | for (C4Effect *pEff = pNext; pEff; pEff = pEff->pNext) |
| 501 | { |
| 502 | if (pEff->IsInactiveAndNotDead()) |
| 503 | { |
| 504 | pEff->FlipActive(); |
| 505 | if (pEff->pFnStart && pEff->iPriority != 1) pEff->pFnStart->Exec(pObj: pEff->pCommandTarget, pPars: {C4VObj(pObj), C4VInt(iVal: pEff->iNumber), C4VInt(C4FxCall_Temp)}, fPassErrors: false, nonStrict3WarnConversionOnly: true); |
| 506 | } |
| 507 | // done? |
| 508 | if (pEff == pLastReaddEffect) break; |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | void C4Effect::CompileFunc(StdCompiler *pComp) |
| 513 | { |
| 514 | // read name |
| 515 | pComp->Value(mkStringAdaptMI(Name)); |
| 516 | pComp->Separator(eSep: StdCompiler::SEP_START); // '(' |
| 517 | // read number |
| 518 | pComp->Value(rInt&: iNumber); pComp->Separator(); |
| 519 | // read priority |
| 520 | pComp->Value(rInt&: iPriority); pComp->Separator(); |
| 521 | // read time and intervall |
| 522 | pComp->Value(rInt&: iTime); pComp->Separator(); |
| 523 | pComp->Value(rInt&: iIntervall); pComp->Separator(); |
| 524 | // read object number |
| 525 | pComp->Value(rStruct&: pCommandTarget); pComp->Separator(); |
| 526 | // read ID |
| 527 | pComp->Value(rStruct: mkC4IDAdapt(rValue&: idCommandTarget)); |
| 528 | pComp->Separator(eSep: StdCompiler::SEP_END); // ')' |
| 529 | // read variables |
| 530 | if (pComp->isCompiler() || EffectVars.GetSize() > 0) |
| 531 | if (pComp->Separator(eSep: StdCompiler::SEP_START2)) // '[' |
| 532 | { |
| 533 | pComp->Value(rStruct&: EffectVars); |
| 534 | pComp->Separator(eSep: StdCompiler::SEP_END2); // ']' |
| 535 | } |
| 536 | else |
| 537 | EffectVars.Reset(); |
| 538 | // is there a next effect? |
| 539 | bool fNext = !!pNext; |
| 540 | if (pComp->hasNaming()) |
| 541 | { |
| 542 | if (fNext || pComp->isCompiler()) |
| 543 | fNext = pComp->Separator(); |
| 544 | } |
| 545 | else |
| 546 | pComp->Value(rBool&: fNext); |
| 547 | if (!fNext) return; |
| 548 | // read next |
| 549 | pComp->Value(rStruct: mkPtrAdapt(rpObj&: pNext, fAllowNull: false)); |
| 550 | // denumeration and callback assignment will be done later |
| 551 | } |
| 552 | |
| 553 | // Internal fire effect |
| 554 | |
| 555 | C4Value &FxFireVarMode (C4Effect *pEffect) { return pEffect->EffectVars[0]; } |
| 556 | C4Value &FxFireVarCausedBy (C4Effect *pEffect) { return pEffect->EffectVars[1]; } |
| 557 | C4Value &FxFireVarBlasted (C4Effect *pEffect) { return pEffect->EffectVars[2]; } |
| 558 | C4Value &FxFireVarIncineratingObj(C4Effect *pEffect) { return pEffect->EffectVars[3]; } |
| 559 | |
| 560 | int32_t FnFxFireStart(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iTemp, int32_t iCausedBy, bool fBlasted, C4Object *pIncineratingObject) |
| 561 | { |
| 562 | // safety |
| 563 | if (!pObj) return -1; |
| 564 | // temp readd |
| 565 | if (iTemp) { pObj->SetOnFire(true); return 1; } |
| 566 | // fail if already on fire |
| 567 | if (pObj->GetOnFire()) return -1; |
| 568 | // get associated effect |
| 569 | C4Effect *pEffect; |
| 570 | if (!(pEffect = pObj->pEffects)) return -1; |
| 571 | if (!(pEffect = pEffect->Get(iNumber, fIncludeDead: true))) return -1; |
| 572 | // structures must eject contents now, because DoCon is not guaranteed to be executed! |
| 573 | // In extinguishing material |
| 574 | bool fFireCaused = true; |
| 575 | int32_t iMat; |
| 576 | if (MatValid(mat: iMat = GBackMat(x: pObj->x, y: pObj->y))) |
| 577 | if (Game.Material.Map[iMat].Extinguisher) |
| 578 | { |
| 579 | // blasts should changedef in water, too! |
| 580 | if (fBlasted) if (pObj->Def->BurnTurnTo != C4ID_None) pObj->ChangeDef(idNew: pObj->Def->BurnTurnTo); |
| 581 | // no fire caused |
| 582 | fFireCaused = false; |
| 583 | } |
| 584 | // BurnTurnTo |
| 585 | if (fFireCaused) if (pObj->Def->BurnTurnTo != C4ID_None) pObj->ChangeDef(idNew: pObj->Def->BurnTurnTo); |
| 586 | // eject contents |
| 587 | C4Object *cobj; |
| 588 | if (!pObj->Def->IncompleteActivity && !pObj->Def->NoBurnDecay) |
| 589 | while (cobj = pObj->Contents.GetObject()) |
| 590 | { |
| 591 | cobj->Controller = iCausedBy; // update controller, so incinerating a hut full of flints attributes the damage to the incinerator |
| 592 | if (pObj->Contained) cobj->Enter(pTarget: pObj->Contained); |
| 593 | else cobj->Exit(iX: cobj->x, iY: cobj->y); |
| 594 | } |
| 595 | // Detach attached objects |
| 596 | cobj = nullptr; |
| 597 | if (!pObj->Def->IncompleteActivity && !pObj->Def->NoBurnDecay) |
| 598 | while (cobj = Game.FindObject(id: 0, iX: 0, iY: 0, iWdt: 0, iHgt: 0, ocf: OCF_All, szAction: nullptr, pActionTarget: pObj, pExclude: nullptr, pContainer: nullptr, iOwner: ANY_OWNER, pFindNext: cobj)) |
| 599 | if ((cobj->Action.Act > ActIdle) && (cobj->Def->ActMap[cobj->Action.Act].Procedure == DFA_ATTACH)) |
| 600 | cobj->SetAction(iAct: ActIdle); |
| 601 | // fire caused? |
| 602 | if (!fFireCaused) |
| 603 | { |
| 604 | // if object was blasted but not incinerated (i.e., inside extinguisher) |
| 605 | // do a script callback |
| 606 | if (fBlasted) pObj->Call(PSF_IncinerationEx, pPars: {C4VInt(iVal: iCausedBy)}); |
| 607 | return -1; |
| 608 | } |
| 609 | // determine fire appearance |
| 610 | int32_t iFireMode; |
| 611 | if (!(iFireMode = pObj->Call(PSF_FireMode).getInt())) |
| 612 | { |
| 613 | // set default fire modes |
| 614 | uint32_t dwCat = pObj->Category; |
| 615 | if (dwCat & (C4D_Living | C4D_StaticBack)) // Tiere, Bäume |
| 616 | iFireMode = C4Fx_FireMode_LivingVeg; |
| 617 | else if (dwCat & (C4D_Structure | C4D_Vehicle)) // Gebäude und Fahrzeuge sind unten meist kantig |
| 618 | iFireMode = C4Fx_FireMode_StructVeh; |
| 619 | else |
| 620 | iFireMode = C4Fx_FireMode_Object; |
| 621 | } |
| 622 | else if (!Inside<int32_t>(ival: iFireMode, lbound: 1, C4Fx_FireMode_Last)) |
| 623 | { |
| 624 | DebugLog(level: spdlog::level::warn, fmt: "FireMode {} of object {} ({}) is invalid!" , args&: iFireMode, args: pObj->GetName(), args: pObj->Def->GetName()); |
| 625 | iFireMode = C4Fx_FireMode_Object; |
| 626 | } |
| 627 | // store causes in effect vars |
| 628 | FxFireVarMode(pEffect).SetInt(iFireMode); |
| 629 | FxFireVarCausedBy(pEffect).SetInt(iCausedBy); // used in C4Object::GetFireCause and timer! |
| 630 | FxFireVarBlasted(pEffect).SetBool(fBlasted); |
| 631 | FxFireVarIncineratingObj(pEffect).SetObject(pIncineratingObject); |
| 632 | // Set values |
| 633 | pObj->SetOnFire(true); |
| 634 | pObj->FirePhase = Random(MaxFirePhase); |
| 635 | if (pObj->Shape.Wdt * pObj->Shape.Hgt > 500) StartSoundEffect(name: "Inflame" , loop: false, volume: 100, obj: pObj); |
| 636 | if (pObj->Def->Mass >= 100) StartSoundEffect(name: "Fire" , loop: true, volume: 100, obj: pObj); |
| 637 | // Engine script call |
| 638 | pObj->Call(PSF_Incineration, pPars: {C4VInt(iVal: iCausedBy)}); |
| 639 | // Done, success |
| 640 | return C4Fx_OK; |
| 641 | } |
| 642 | |
| 643 | int32_t FnFxFireTimer(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iTime) |
| 644 | { |
| 645 | // safety |
| 646 | if (!pObj) return C4Fx_Execute_Kill; |
| 647 | |
| 648 | // get cause |
| 649 | int32_t iCausedByPlr = NO_OWNER; C4Effect *pEffect; |
| 650 | if (pEffect = pObj->pEffects) |
| 651 | if (pEffect = pEffect->Get(iNumber, fIncludeDead: true)) |
| 652 | { |
| 653 | iCausedByPlr = FxFireVarCausedBy(pEffect).getInt(); |
| 654 | if (!ValidPlr(plr: iCausedByPlr)) iCausedByPlr = NO_OWNER; |
| 655 | } |
| 656 | |
| 657 | // causes on object |
| 658 | pObj->ExecFire(iIndex: iNumber, iCausedByPlr); |
| 659 | |
| 660 | // special effects only if loaded |
| 661 | if (!Game.Particles.IsFireParticleLoaded()) return C4Fx_OK; |
| 662 | |
| 663 | // get effect: May be nullptr after object fire execution, in which case the fire has been extinguished |
| 664 | if (!pObj->GetOnFire()) return C4Fx_Execute_Kill; |
| 665 | if (!(pEffect = pObj->pEffects)) return C4Fx_Execute_Kill; |
| 666 | if (!(pEffect = pEffect->Get(iNumber, fIncludeDead: true))) return C4Fx_Execute_Kill; |
| 667 | |
| 668 | /* Fire execution behaviour transferred from script (FIRE) */ |
| 669 | |
| 670 | // get fire mode |
| 671 | int32_t iFireMode = FxFireVarMode(pEffect).getInt(); |
| 672 | |
| 673 | // special effects only each four frames, except for objects (e.g.: Projectiles) |
| 674 | if (iTime % 4 && iFireMode != C4Fx_FireMode_Object) return C4Fx_OK; |
| 675 | |
| 676 | // no gfx for contained |
| 677 | if (pObj->Contained) return C4Fx_OK; |
| 678 | |
| 679 | // some constant effect parameters for this object |
| 680 | int32_t iWidth = std::max<int32_t>(a: pObj->Def->Shape.Wdt, b: 1), |
| 681 | iHeight = pObj->Def->Shape.Hgt, |
| 682 | iYOff = iHeight / 2 - pObj->Def->Shape.FireTop; |
| 683 | |
| 684 | int32_t iCount = int32_t(sqrt(x: double(iWidth * iHeight)) / 4); // Number of particles per execution |
| 685 | const int32_t iBaseParticleSize = 30; // With of particles in pixels/10, w/o add of values below |
| 686 | const int32_t iParticleSizeDiff = 10; // Size variation among particles |
| 687 | const int32_t iRelParticleSize = 12; // Influence of object size on particle size |
| 688 | |
| 689 | // some varying effect parameters |
| 690 | int32_t iX = pObj->x, iY = pObj->y; |
| 691 | int32_t iXDir, iYDir, iCon, iWdtCon, iA, iSize; |
| 692 | |
| 693 | // get remainign size (%) |
| 694 | iCon = iWdtCon = std::max<int32_t>(a: (100 * pObj->GetCon()) / FullCon, b: 1); |
| 695 | if (!pObj->Def->GrowthType) |
| 696 | // fixed width for not-stretched-objects |
| 697 | if (iWdtCon < 100) iWdtCon = 100; |
| 698 | |
| 699 | // regard non-center object offsets |
| 700 | iX += pObj->Shape.x + pObj->Shape.Wdt / 2; |
| 701 | iY += pObj->Shape.y + pObj->Shape.Hgt / 2; |
| 702 | |
| 703 | // apply rotation |
| 704 | float fRot[4] = { 1.0f, 0.0f, 0.0f, 1.0f }; |
| 705 | if (pObj->r && pObj->Def->Rotateable) |
| 706 | { |
| 707 | fRot[0] = cosf(x: static_cast<float>(pObj->r * std::numbers::pi_v<float> / 180.0)); |
| 708 | fRot[1] = -sinf(x: static_cast<float>(pObj->r * std::numbers::pi_v<float> / 180.0)); |
| 709 | fRot[2] = -fRot[1]; |
| 710 | fRot[3] = fRot[0]; |
| 711 | // rotated objects usually better burn from the center |
| 712 | if (iYOff > 0) iYOff = 0; |
| 713 | } |
| 714 | |
| 715 | // Adjust particle number by con |
| 716 | iCount = (std::max)(a: 2, b: iCount * iWdtCon / 100); |
| 717 | |
| 718 | // calc base for particle size parameter |
| 719 | iA = static_cast<int32_t>(sqrt(x: sqrt(x: double(iWidth * iHeight)) * (iCon + 20) / 120) * iRelParticleSize); |
| 720 | |
| 721 | // create a double set of particles; first quarter normal (Fire); remaining three quarters additive (Fire2) |
| 722 | for (int32_t i = 0; i < iCount * 2; ++i) |
| 723 | { |
| 724 | // calc actual size to be used in this frame |
| 725 | // Using Random instead of SafeRandom would be safe here |
| 726 | // However, since it's just affecting particles there's no need to use synchronized random values |
| 727 | iSize = SafeRandom(range: iParticleSizeDiff + 1) + iBaseParticleSize - iParticleSizeDiff / 2 - 1 + iA; |
| 728 | |
| 729 | // get particle target list |
| 730 | C4ParticleList *pParticleList = SafeRandom(range: 4) ? &(pObj->BackParticles) : &(pObj->FrontParticles); |
| 731 | |
| 732 | // get particle def and color |
| 733 | C4ParticleDef *pPartDef; uint32_t dwClr; |
| 734 | if (i < iCount / 2) |
| 735 | { |
| 736 | dwClr = 0x32004000 + ((SafeRandom(range: 59) + 196) << 16); |
| 737 | pPartDef = Game.Particles.pFire1; |
| 738 | } |
| 739 | else |
| 740 | { |
| 741 | dwClr = 0xffffff; |
| 742 | pPartDef = Game.Particles.pFire2; |
| 743 | } |
| 744 | if (iFireMode == C4Fx_FireMode_Object) dwClr += 0x62000000; |
| 745 | |
| 746 | // get particle creation pos... |
| 747 | int32_t iRandX = SafeRandom(range: iWidth + 1) - iWidth / 2 - 1; |
| 748 | |
| 749 | int32_t iPx = iRandX * iWdtCon / 100; |
| 750 | int32_t iPy = iYOff * iCon / 100; |
| 751 | if (iFireMode == C4Fx_FireMode_LivingVeg) iPy -= iPx * iPx * 100 / iWidth / iWdtCon; // parable form particle pos on livings |
| 752 | |
| 753 | // ...and movement speed |
| 754 | if (iFireMode != C4Fx_FireMode_Object) |
| 755 | { |
| 756 | // ...for normal fire proc |
| 757 | iXDir = iRandX * iCon / 400 - (iPx / 3) - int32_t(fixtof(x: pObj->xdir) * 3); |
| 758 | iYDir = -SafeRandom(range: 15 + iHeight * iCon / 300) - 1 - int32_t(fixtof(x: pObj->ydir) * 3); |
| 759 | } |
| 760 | else |
| 761 | { |
| 762 | // ...for objects |
| 763 | iXDir = -int32_t(fixtof(x: pObj->xdir) * 3); |
| 764 | iYDir = -int32_t(fixtof(x: pObj->ydir) * 3); |
| 765 | if (!iYDir) iYDir = -SafeRandom(range: 13 + iHeight / 4) - 1; |
| 766 | } |
| 767 | |
| 768 | // OK; create it! |
| 769 | Game.Particles.Create(pOfDef: pPartDef, x: float(iX) + fRot[0] * iPx + fRot[1] * iPy, y: float(iY) + fRot[2] * iPx + fRot[3] * iPy, xdir: iXDir / 10.0f, ydir: iYDir / 10.0f, a: iSize / 10.0f, b: dwClr, pPxList: pParticleList, pObj); |
| 770 | } |
| 771 | |
| 772 | return C4Fx_OK; |
| 773 | } |
| 774 | |
| 775 | int32_t FnFxFireStop(C4AulContext *ctx, C4Object *pObj, int32_t iNumber, int32_t iReason, bool fTemp) |
| 776 | { |
| 777 | // safety |
| 778 | if (!pObj) return false; |
| 779 | // only if real removal is done |
| 780 | if (fTemp) |
| 781 | { |
| 782 | // but fake being not on fire, so higher-priority effects get the status right |
| 783 | pObj->SetOnFire(false); |
| 784 | return true; |
| 785 | } |
| 786 | // alter OnFire-flag |
| 787 | pObj->SetOnFire(false); |
| 788 | // stop sound |
| 789 | if (pObj->Def->Mass >= 100) StopSoundEffect(name: "Fire" , obj: pObj); |
| 790 | // done, success |
| 791 | return true; |
| 792 | } |
| 793 | |
| 794 | C4String *FnFxFireInfo(C4AulContext *ctx, C4Object *pObj, int32_t iNumber) |
| 795 | { |
| 796 | return new C4String(LoadResStr(id: C4ResStrTableKey::IDS_OBJ_BURNS), &Game.ScriptEngine.Strings); |
| 797 | } |
| 798 | |
| 799 | // Some other, internal effects |
| 800 | |
| 801 | void Splash(int32_t tx, int32_t ty, int32_t amt, C4Object *pByObj) |
| 802 | { |
| 803 | // Splash only if there is free space above |
| 804 | if (GBackSemiSolid(x: tx, y: ty - 15)) return; |
| 805 | // get back mat |
| 806 | int32_t iMat = GBackMat(x: tx, y: ty); |
| 807 | // check liquid |
| 808 | if (MatValid(mat: iMat)) |
| 809 | if (DensityLiquid(dens: Game.Material.Map[iMat].Density) && Game.Material.Map[iMat].Instable) |
| 810 | { |
| 811 | int32_t sy = ty; |
| 812 | while (GBackLiquid(x: tx, y: sy) && sy > ty - 20 && sy >= 0) sy--; |
| 813 | // Splash bubbles and liquid |
| 814 | for (int32_t cnt = 0; cnt < amt; cnt++) |
| 815 | { |
| 816 | // force argument evaluation order |
| 817 | const auto r2 = Random(iRange: 16); |
| 818 | const auto r1 = Random(iRange: 16); |
| 819 | BubbleOut(tx: tx + r1 - 8, ty: ty + r2 - 6); |
| 820 | if (GBackLiquid(x: tx, y: ty) && !GBackSemiSolid(x: tx, y: sy)) |
| 821 | { |
| 822 | // force argument evaluation order |
| 823 | const auto r2 = FIXED100(x: -Random(iRange: 200)); |
| 824 | const auto r1 = FIXED100(x: Random(iRange: 151) - 75); |
| 825 | Game.PXS.Create(mat: Game.Landscape.ExtractMaterial(fx: tx, fy: ty), |
| 826 | ix: itofix(x: tx), iy: itofix(x: sy), |
| 827 | ixdir: r1, |
| 828 | iydir: r2); |
| 829 | } |
| 830 | } |
| 831 | } |
| 832 | // Splash sound |
| 833 | if (amt >= 20) |
| 834 | StartSoundEffect(name: "Splash2" , loop: false, volume: 100, obj: pByObj); |
| 835 | else if (amt > 1) StartSoundEffect(name: "Splash1" , loop: false, volume: 100, obj: pByObj); |
| 836 | } |
| 837 | |
| 838 | int32_t GetSmokeLevel() |
| 839 | { |
| 840 | // Network active: enforce fixed smoke level |
| 841 | if (Game.Control.SyncMode()) |
| 842 | return 150; |
| 843 | // User-defined smoke level |
| 844 | return Config.Graphics.SmokeLevel; |
| 845 | } |
| 846 | |
| 847 | void BubbleOut(int32_t tx, int32_t ty) |
| 848 | { |
| 849 | // No bubbles from nowhere |
| 850 | if (!GBackSemiSolid(x: tx, y: ty)) return; |
| 851 | // User-defined smoke level |
| 852 | int32_t SmokeLevel = GetSmokeLevel(); |
| 853 | // Enough bubbles out there already |
| 854 | if (Game.Objects.ObjectCount(id: C4Id(str: "FXU1" )) >= SmokeLevel) return; |
| 855 | // Create bubble |
| 856 | Game.CreateObject(type: C4Id(str: "FXU1" ), pCreator: nullptr, owner: NO_OWNER, x: tx, y: ty); |
| 857 | } |
| 858 | |
| 859 | void Smoke(int32_t tx, int32_t ty, int32_t level, uint32_t dwClr) |
| 860 | { |
| 861 | if (Game.Particles.pSmoke) |
| 862 | { |
| 863 | Game.Particles.Create(pOfDef: Game.Particles.pSmoke, x: float(tx), y: float(ty) - level / 2, xdir: 0.0f, ydir: 0.0f, a: float(level), b: dwClr); |
| 864 | return; |
| 865 | } |
| 866 | // User-defined smoke level |
| 867 | int32_t SmokeLevel = GetSmokeLevel(); |
| 868 | // Enough smoke out there already |
| 869 | if (Game.Objects.ObjectCount(id: C4Id(str: "FXS1" )) >= SmokeLevel) return; |
| 870 | // Create smoke |
| 871 | level = BoundBy<int32_t>(bval: level, lbound: 3, rbound: 32); |
| 872 | C4Object *pObj; |
| 873 | if (pObj = Game.CreateObjectConstruction(type: C4Id(str: "FXS1" ), pCreator: nullptr, owner: NO_OWNER, ctx: tx, bty: ty, con: FullCon * level / 32)) |
| 874 | pObj->Call(PSF_Activate); |
| 875 | } |
| 876 | |
| 877 | void Explosion(int32_t tx, int32_t ty, int32_t level, C4Object *inobj, int32_t iCausedBy, C4Object *pByObj, C4ID idEffect, const char *szEffect) |
| 878 | { |
| 879 | int32_t grade = BoundBy(bval: (level / 10) - 1, lbound: 1, rbound: 3); |
| 880 | // Sound |
| 881 | StartSoundEffect(name: std::format(fmt: "Blast{}" , args: '0' + grade).c_str(), loop: false, volume: 100, obj: pByObj); |
| 882 | // Check blast containment |
| 883 | C4Object *container = inobj; |
| 884 | while (container && !container->Def->ContainBlast) container = container->Contained; |
| 885 | // Uncontained blast effects |
| 886 | if (!container) |
| 887 | { |
| 888 | // Incinerate landscape |
| 889 | if (!Game.Landscape.Incinerate(x: tx, y: ty)) |
| 890 | if (!Game.Landscape.Incinerate(x: tx, y: ty - 10)) |
| 891 | if (!Game.Landscape.Incinerate(x: tx - 5, y: ty - 5)) |
| 892 | Game.Landscape.Incinerate(x: tx + 5, y: ty - 5); |
| 893 | // Create blast object or particle |
| 894 | C4Object *pBlast; |
| 895 | C4ParticleDef *pPrtDef = Game.Particles.pBlast; |
| 896 | // particle override |
| 897 | if (szEffect) |
| 898 | { |
| 899 | C4ParticleDef *pPrtDef2 = Game.Particles.GetDef(szName: szEffect); |
| 900 | if (pPrtDef2) pPrtDef = pPrtDef2; |
| 901 | } |
| 902 | else if (idEffect) pPrtDef = nullptr; |
| 903 | // create particle |
| 904 | if (pPrtDef) |
| 905 | { |
| 906 | Game.Particles.Create(pOfDef: pPrtDef, x: static_cast<float>(tx), y: static_cast<float>(ty), xdir: 0.0f, ydir: 0.0f, a: static_cast<float>(level), b: 0); |
| 907 | if (SEqual2(szStr1: pPrtDef->Name.getData(), szStr2: "Blast" )) |
| 908 | Game.Particles.Cast(pOfDef: Game.Particles.pFSpark, iAmount: level / 5 + 1, x: static_cast<float>(tx), y: static_cast<float>(ty), level, a0: level / 2 + 1.0f, b0: 0x00ef0000, a1: level + 1.0f, b1: 0xffff1010); |
| 909 | } |
| 910 | else if (pBlast = Game.CreateObjectConstruction(type: idEffect ? idEffect : C4Id(str: "FXB1" ), pCreator: pByObj, owner: iCausedBy, ctx: tx, bty: ty + level, con: FullCon * level / 20)) |
| 911 | pBlast->Call(PSF_Activate); |
| 912 | } |
| 913 | // Blast objects |
| 914 | Game.BlastObjects(tx, ty, level, inobj, iCausedBy, pByObj); |
| 915 | if (container != inobj) Game.BlastObjects(tx, ty, level, inobj: container, iCausedBy, pByObj); |
| 916 | if (!container) |
| 917 | { |
| 918 | // Blast free landscape. After blasting objects so newly mined materials don't get flinged |
| 919 | Game.Landscape.BlastFree(tx, ty, rad: level, grade, iByPlayer: iCausedBy); |
| 920 | } |
| 921 | } |
| 922 | |