| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) RedWolf Design |
| 5 | * Copyright (c) 2001, Sven2 |
| 6 | * Copyright (c) 2017-2022, The LegacyClonk Team and contributors |
| 7 | * |
| 8 | * Distributed under the terms of the ISC license; see accompanying file |
| 9 | * "COPYING" for details. |
| 10 | * |
| 11 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 12 | * See accompanying file "TRADEMARK" for details. |
| 13 | * |
| 14 | * To redistribute this file separately, substitute the full license texts |
| 15 | * for the above references. |
| 16 | */ |
| 17 | |
| 18 | // newgfx particle system for smoke, sparks, ... |
| 19 | |
| 20 | #include <C4Include.h> |
| 21 | #include <C4Particles.h> |
| 22 | |
| 23 | #include <C4Physics.h> |
| 24 | #include <C4Object.h> |
| 25 | #include <C4Random.h> |
| 26 | #include <C4Game.h> |
| 27 | #include <C4Components.h> |
| 28 | #include <C4Wrappers.h> |
| 29 | |
| 30 | void C4ParticleDefCore::CompileFunc(StdCompiler *pComp) |
| 31 | { |
| 32 | pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(Name), szName: "Name" , rDefault: "" )); |
| 33 | pComp->Value(rStruct: mkNamingAdapt(rValue&: MaxCount, szName: "MaxCount" , rDefault: C4Px_MaxParticle)); |
| 34 | pComp->Value(rStruct: mkNamingAdapt(rValue&: MinLifetime, szName: "MinLifetime" , rDefault: 0)); |
| 35 | pComp->Value(rStruct: mkNamingAdapt(rValue&: MaxLifetime, szName: "MaxLifetime" , rDefault: 0)); |
| 36 | pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(InitFn), szName: "InitFn" , rDefault: "" )); |
| 37 | pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(ExecFn), szName: "ExecFn" , rDefault: "" )); |
| 38 | pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(CollisionFn), szName: "CollisionFn" , rDefault: "" )); |
| 39 | pComp->Value(rStruct: mkNamingAdapt(toC4CStrBuf(DrawFn), szName: "DrawFn" , rDefault: "" )); |
| 40 | pComp->Value(rStruct: mkNamingAdapt(rValue&: GfxFace, szName: "Face" )); |
| 41 | pComp->Value(rStruct: mkNamingAdapt(rValue&: YOff, szName: "YOff" , rDefault: 0)); |
| 42 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Delay, szName: "Delay" , rDefault: 0)); |
| 43 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Repeats, szName: "Repeats" , rDefault: 0)); |
| 44 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Reverse, szName: "Reverse" , rDefault: 0)); |
| 45 | pComp->Value(rStruct: mkNamingAdapt(rValue&: FadeOutLen, szName: "FadeOutLen" , rDefault: 0)); |
| 46 | pComp->Value(rStruct: mkNamingAdapt(rValue&: FadeOutDelay, szName: "FadeOutDelay" , rDefault: 0)); |
| 47 | pComp->Value(rStruct: mkNamingAdapt(rValue&: RByV, szName: "RByV" , rDefault: 0)); |
| 48 | pComp->Value(rStruct: mkNamingAdapt(rValue&: GravityAcc, szName: "GravityAcc" , rDefault: 0)); |
| 49 | pComp->Value(rStruct: mkNamingAdapt(rValue&: WindDrift, szName: "WindDrift" , rDefault: 0)); |
| 50 | pComp->Value(rStruct: mkNamingAdapt(rValue&: VertexCount, szName: "VertexCount" , rDefault: 0)); |
| 51 | pComp->Value(rStruct: mkNamingAdapt(rValue&: VertexY, szName: "VertexY" , rDefault: 0)); |
| 52 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Additive, szName: "Additive" , rDefault: 0)); |
| 53 | pComp->Value(rStruct: mkNamingAdapt(rValue&: AlphaFade, szName: "AlphaFade" , rDefault: 0)); |
| 54 | pComp->Value(rStruct: mkNamingAdapt(rValue: mkArrayAdapt(array&: Parallaxity, default_: 100), szName: "Parallaxity" )); |
| 55 | pComp->Value(rStruct: mkNamingAdapt(rValue&: Attach, szName: "Attach" , rDefault: 0)); |
| 56 | } |
| 57 | |
| 58 | C4ParticleDefCore::C4ParticleDefCore() : |
| 59 | MaxCount(C4Px_MaxParticle), |
| 60 | MinLifetime(0), MaxLifetime(0), |
| 61 | YOff(0), |
| 62 | Reverse(0), Repeats(0), Delay(0), |
| 63 | FadeOutLen(0), FadeOutDelay(0), |
| 64 | RByV(0), |
| 65 | Placement(0), |
| 66 | GravityAcc(0), |
| 67 | VertexCount(0), VertexY(0), |
| 68 | Additive(0), |
| 69 | Attach(0), |
| 70 | AlphaFade(0) |
| 71 | { |
| 72 | GfxFace.Default(); |
| 73 | Parallaxity[0] = Parallaxity[1] = 100; |
| 74 | } |
| 75 | |
| 76 | bool C4ParticleDefCore::Compile(const char *szSource, const char *szName) |
| 77 | { |
| 78 | return CompileFromBuf_LogWarn<StdCompilerINIRead>(TargetStruct: mkNamingAdapt(rValue&: *this, szName: "Particle" ), |
| 79 | SrcBuf: StdStrBuf::MakeRef(str: szSource), szName); |
| 80 | } |
| 81 | |
| 82 | C4ParticleDef::C4ParticleDef() : |
| 83 | C4ParticleDefCore(), |
| 84 | InitProc(&fxStdInit), |
| 85 | ExecProc(&fxStdExec), |
| 86 | DrawProc(&fxStdDraw), |
| 87 | CollisionProc(nullptr), |
| 88 | Count(0) |
| 89 | { |
| 90 | // zero fields |
| 91 | Gfx.Default(); |
| 92 | // link into list |
| 93 | if (!ParticleSystem.pDef0) |
| 94 | { |
| 95 | pPrev = nullptr; |
| 96 | ParticleSystem.pDef0 = this; |
| 97 | } |
| 98 | else |
| 99 | (pPrev = ParticleSystem.pDefL)->pNext = this; |
| 100 | ParticleSystem.pDefL = this; |
| 101 | pNext = nullptr; |
| 102 | } |
| 103 | |
| 104 | C4ParticleDef::~C4ParticleDef() |
| 105 | { |
| 106 | // clear |
| 107 | Clear(); |
| 108 | // unlink from list |
| 109 | if (pPrev) pPrev->pNext = pNext; else ParticleSystem.pDef0 = pNext; |
| 110 | if (pNext) pNext->pPrev = pPrev; else ParticleSystem.pDefL = pPrev; |
| 111 | } |
| 112 | |
| 113 | void C4ParticleDef::Clear() |
| 114 | { |
| 115 | Name.Clear(); |
| 116 | } |
| 117 | |
| 118 | bool C4ParticleDef::Load(C4Group &rGrp) |
| 119 | { |
| 120 | // store file |
| 121 | Filename.Copy(Buf2: rGrp.GetFullName()); |
| 122 | // load |
| 123 | char *pSource; |
| 124 | if (rGrp.LoadEntry(C4CFN_ParticleCore, lpbpBuf: &pSource, ipSize: nullptr, iAppendZeros: 1)) |
| 125 | { |
| 126 | if (!Compile(szSource: pSource, szName: Filename.getData())) |
| 127 | { |
| 128 | DebugLog(level: spdlog::level::err, fmt: "invalid particle def at '{}'" , args: rGrp.GetFullName().getData()); |
| 129 | delete[] pSource; return false; |
| 130 | } |
| 131 | delete[] pSource; |
| 132 | // load graphics |
| 133 | if (!Gfx.Load(hGroup&: rGrp, C4CFN_DefGraphicsPNG)) |
| 134 | { |
| 135 | DebugLog(level: spdlog::level::err, fmt: "particle {} has no valid graphics defined" , args: Name.getData()); |
| 136 | return false; |
| 137 | } |
| 138 | // set facet, if assigned - otherwise, assume full surface |
| 139 | if (GfxFace.Wdt) Gfx.Set(nsfc: Gfx.Surface, nx: GfxFace.x, ny: GfxFace.y, nwdt: GfxFace.Wdt, nhgt: GfxFace.Hgt, ntx: GfxFace.tx, nty: GfxFace.ty); |
| 140 | // set phase num |
| 141 | int32_t Q; Gfx.GetPhaseNum(rX&: Length, rY&: Q); |
| 142 | if (!Length) |
| 143 | { |
| 144 | DebugLog(level: spdlog::level::err, fmt: "invalid facet for particle '{}'" , args: Name.getData()); |
| 145 | return false; |
| 146 | } |
| 147 | // case fadeout from length |
| 148 | if (FadeOutLen) |
| 149 | { |
| 150 | Length = std::max<int32_t>(a: Length - FadeOutLen, b: 1); |
| 151 | if (!FadeOutDelay) FadeOutDelay = 1; |
| 152 | } |
| 153 | // if phase num is 1, no reverse is allowed |
| 154 | if (Length == 1) Reverse = 0; |
| 155 | // calc aspect |
| 156 | Aspect = static_cast<float>(Gfx.Wdt) / Gfx.Hgt; |
| 157 | // get proc pointers |
| 158 | if (!(InitProc = ParticleSystem.GetProc(szName: InitFn.getData()))) |
| 159 | { |
| 160 | DebugLog(level: spdlog::level::err, fmt: "init proc for particle '{}' not found: '{}'" , args: Name.getData(), args: InitFn.getData()); |
| 161 | return false; |
| 162 | } |
| 163 | if (!(ExecProc = ParticleSystem.GetProc(szName: ExecFn.getData()))) |
| 164 | { |
| 165 | DebugLog(level: spdlog::level::err, fmt: "exec proc for particle '{}' not found: '{}'" , args: Name.getData(), args: ExecFn.getData()); |
| 166 | return false; |
| 167 | } |
| 168 | if (CollisionFn && CollisionFn[0]) if (!(CollisionProc = ParticleSystem.GetProc(szName: CollisionFn.getData()))) |
| 169 | { |
| 170 | DebugLog(level: spdlog::level::err, fmt: "collision proc for particle '{}' not found: '{}'" , args: Name.getData(), args: CollisionFn.getData()); |
| 171 | return false; |
| 172 | } |
| 173 | if (!(DrawProc = ParticleSystem.GetDrawProc(szName: DrawFn.getData()))) |
| 174 | { |
| 175 | DebugLog(level: spdlog::level::err, fmt: "draw proc for particle '{}' not found: '{}'" , args: Name.getData(), args: DrawFn.getData()); |
| 176 | return false; |
| 177 | } |
| 178 | // particle overloading |
| 179 | C4ParticleDef *pDefOverload; |
| 180 | if (pDefOverload = ParticleSystem.GetDef(szName: Name.getData(), pExclude: this)) |
| 181 | { |
| 182 | if (Config.Graphics.VerboseObjectLoading >= 1) |
| 183 | { |
| 184 | Log(id: C4ResStrTableKey::IDS_PRC_DEFOVERLOAD, args: pDefOverload->Name.getData(), args: "<particle>" ); |
| 185 | } |
| 186 | delete pDefOverload; |
| 187 | } |
| 188 | // success |
| 189 | return true; |
| 190 | } |
| 191 | return false; |
| 192 | } |
| 193 | |
| 194 | bool C4ParticleDef::Reload() |
| 195 | { |
| 196 | // no file? |
| 197 | if (!Filename[0]) return false; |
| 198 | // open group |
| 199 | C4Group hGroup; |
| 200 | if (!hGroup.Open(szGroupName: Filename.getData())) return false; |
| 201 | // reset class |
| 202 | Clear(); |
| 203 | // load |
| 204 | return Load(rGrp&: hGroup); |
| 205 | } |
| 206 | |
| 207 | void C4Particle::MoveList(C4ParticleList &rFrom, C4ParticleList &rTo) |
| 208 | { |
| 209 | // remove from current list |
| 210 | if (pPrev) |
| 211 | pPrev->pNext = pNext; |
| 212 | else |
| 213 | // is it the first item in the list? then set this to the next one |
| 214 | if (rFrom.pFirst == this) rFrom.pFirst = pNext; |
| 215 | if (pNext) pNext->pPrev = pPrev; |
| 216 | // add to the other list - insert before first |
| 217 | if (pNext = rTo.pFirst) pNext->pPrev = this; |
| 218 | rTo.pFirst = this; pPrev = nullptr; |
| 219 | } |
| 220 | |
| 221 | C4ParticleChunk::C4ParticleChunk() |
| 222 | { |
| 223 | // zero linked list |
| 224 | pNext = nullptr; |
| 225 | // zero buffer |
| 226 | Clear(); |
| 227 | } |
| 228 | |
| 229 | C4ParticleChunk::~C4ParticleChunk() |
| 230 | { |
| 231 | // list stuff done by C4ParticleSystem |
| 232 | } |
| 233 | |
| 234 | void C4ParticleChunk::Clear() |
| 235 | { |
| 236 | // note that this method is called in ctor with uninitialized data! |
| 237 | // simply clear mem - this won't adjust any counts! |
| 238 | std::memset(s: Data, c: 0, n: sizeof(Data)); |
| 239 | // init list |
| 240 | C4Particle *pPrt = Data; |
| 241 | for (int32_t i = 0; i < C4Px_BufSize; ++i) |
| 242 | { |
| 243 | pPrt->pPrev = pPrt - 1; |
| 244 | pPrt->pNext = pPrt + 1; |
| 245 | ++pPrt; |
| 246 | } |
| 247 | Data[0].pPrev = Data[C4Px_BufSize - 1].pNext = nullptr; |
| 248 | } |
| 249 | |
| 250 | void C4ParticleList::Exec(C4Object *pObj) |
| 251 | { |
| 252 | // execute all particles |
| 253 | C4Particle *pPrtNext = pFirst, *pPrt; |
| 254 | while (pPrt = pPrtNext) |
| 255 | { |
| 256 | // get next now, because destruction could corrupt the list |
| 257 | pPrtNext = pPrt->pNext; |
| 258 | // execute it |
| 259 | if (!pPrt->pDef->ExecProc(pPrt, pObj)) |
| 260 | { |
| 261 | // sorry, life is over for you :P |
| 262 | --pPrt->pDef->Count; |
| 263 | pPrt->MoveList(rFrom&: *this, rTo&: Game.Particles.FreeParticles); |
| 264 | } |
| 265 | } |
| 266 | // done |
| 267 | } |
| 268 | |
| 269 | void C4ParticleList::Draw(C4FacetEx &cgo, C4Object *pObj) |
| 270 | { |
| 271 | // draw all particles |
| 272 | for (C4Particle *pPrt = pFirst; pPrt; pPrt = pPrt->pNext) |
| 273 | pPrt->pDef->DrawProc(pPrt, cgo, pObj); |
| 274 | // done |
| 275 | } |
| 276 | |
| 277 | void C4ParticleList::Clear() |
| 278 | { |
| 279 | // remove all particles |
| 280 | C4Particle *pPrtNext = pFirst, *pPrt; |
| 281 | while (pPrt = pPrtNext) |
| 282 | { |
| 283 | // get next now, because destruction could corrupt the list |
| 284 | pPrtNext = pPrt->pNext; |
| 285 | // sorry, life is over for you :P |
| 286 | --pPrt->pDef->Count; |
| 287 | pPrt->MoveList(rFrom&: *this, rTo&: Game.Particles.FreeParticles); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | int32_t C4ParticleList::Remove(C4ParticleDef *pOfDef) |
| 292 | { |
| 293 | int32_t iNumRemoved = 0; |
| 294 | // check all particles for def |
| 295 | C4Particle *pPrtNext = pFirst, *pPrt; |
| 296 | while (pPrt = pPrtNext) |
| 297 | { |
| 298 | // get next now, because destruction could corrupt the list |
| 299 | pPrtNext = pPrt->pNext; |
| 300 | // execute it |
| 301 | if (!pOfDef || pPrt->pDef == pOfDef) |
| 302 | { |
| 303 | // sorry, life is over for you :P |
| 304 | --pPrt->pDef->Count; |
| 305 | pPrt->MoveList(rFrom&: *this, rTo&: Game.Particles.FreeParticles); |
| 306 | } |
| 307 | } |
| 308 | // done |
| 309 | return iNumRemoved; |
| 310 | } |
| 311 | |
| 312 | C4ParticleSystem::C4ParticleSystem() |
| 313 | { |
| 314 | // zero fields |
| 315 | pDef0 = pDefL = nullptr; |
| 316 | pSmoke = nullptr; |
| 317 | pBlast = nullptr; |
| 318 | pFSpark = nullptr; |
| 319 | pFire1 = nullptr; |
| 320 | pFire2 = nullptr; |
| 321 | } |
| 322 | |
| 323 | C4ParticleSystem::~C4ParticleSystem() |
| 324 | { |
| 325 | // clean up |
| 326 | Clear(); |
| 327 | } |
| 328 | |
| 329 | C4ParticleChunk *C4ParticleSystem::AddChunk() |
| 330 | { |
| 331 | // add another chunk |
| 332 | C4ParticleChunk *pNewChnk = new C4ParticleChunk(); |
| 333 | pNewChnk->pNext = Chunk.pNext; |
| 334 | Chunk.pNext = pNewChnk; |
| 335 | // register into free-particle-list |
| 336 | if (pNewChnk->Data[C4Px_BufSize - 1].pNext = FreeParticles.pFirst) |
| 337 | FreeParticles.pFirst->pPrev = &pNewChnk->Data[C4Px_BufSize - 1]; |
| 338 | FreeParticles.pFirst = &pNewChnk->Data[0]; |
| 339 | // return it |
| 340 | return pNewChnk; |
| 341 | } |
| 342 | |
| 343 | void C4ParticleSystem::ClearParticles() |
| 344 | { |
| 345 | // clear particle lists |
| 346 | C4ObjectLink *pLnk; |
| 347 | for (pLnk = Game.Objects.First; pLnk; pLnk = pLnk->Next) |
| 348 | pLnk->Obj->FrontParticles.pFirst = pLnk->Obj->BackParticles.pFirst = nullptr; |
| 349 | for (pLnk = Game.Objects.InactiveObjects.First; pLnk; pLnk = pLnk->Next) |
| 350 | pLnk->Obj->FrontParticles.pFirst = pLnk->Obj->BackParticles.pFirst = nullptr; |
| 351 | GlobalParticles.pFirst = nullptr; |
| 352 | // reset chunks |
| 353 | C4ParticleChunk *pNextChnk = Chunk.pNext, *pChnk; |
| 354 | while (pChnk = pNextChnk) |
| 355 | { |
| 356 | pNextChnk = pChnk->pNext; |
| 357 | delete pChnk; |
| 358 | } |
| 359 | Chunk.pNext = nullptr; |
| 360 | Chunk.Clear(); |
| 361 | FreeParticles.pFirst = Chunk.Data; |
| 362 | // adjust counts |
| 363 | for (C4ParticleDef *pDef = pDef0; pDef; pDef = pDef->pNext) |
| 364 | pDef->Count = 0; |
| 365 | } |
| 366 | |
| 367 | void C4ParticleSystem::Clear() |
| 368 | { |
| 369 | // clear particles first |
| 370 | ClearParticles(); |
| 371 | // clear defs |
| 372 | while (pDef0) delete pDef0; |
| 373 | // clear system particles |
| 374 | pSmoke = pBlast = pFSpark = pFire1 = pFire2 = nullptr; |
| 375 | // done |
| 376 | } |
| 377 | |
| 378 | C4Particle *C4ParticleSystem::Create(C4ParticleDef *pOfDef, |
| 379 | float x, float y, |
| 380 | float xdir, float ydir, |
| 381 | float a, int32_t b, C4ParticleList *pPxList, |
| 382 | C4Object *pObj) |
| 383 | { |
| 384 | // safety |
| 385 | if (!pOfDef) return nullptr; |
| 386 | // default to global list |
| 387 | if (!pPxList) pPxList = &GlobalParticles; |
| 388 | // check count |
| 389 | int32_t MaxCount = pOfDef->MaxCount * (Config.Graphics.SmokeLevel + 20) / 150; |
| 390 | int32_t iRoom = MaxCount - pOfDef->Count; |
| 391 | if (iRoom <= 0) return nullptr; |
| 392 | // reduce creation if limit is nearly reached |
| 393 | if (iRoom < (MaxCount >> 1)) |
| 394 | if (SafeRandom(range: iRoom) < SafeRandom(range: MaxCount)) return nullptr; |
| 395 | // get free particle |
| 396 | if (!FreeParticles.pFirst) AddChunk(); |
| 397 | C4Particle *pPrt = FreeParticles.pFirst; |
| 398 | if (!pPrt) return nullptr; |
| 399 | // set values |
| 400 | pPrt->x = x; pPrt->y = y; |
| 401 | pPrt->xdir = xdir; pPrt->ydir = ydir; |
| 402 | pPrt->a = a; pPrt->b = b; |
| 403 | pPrt->pDef = pOfDef; |
| 404 | if (pPrt->pDef->Attach && pObj != nullptr) |
| 405 | { |
| 406 | pPrt->x -= pObj->x; |
| 407 | pPrt->y -= pObj->y; |
| 408 | } |
| 409 | // call initialization |
| 410 | if (!pOfDef->InitProc(pPrt, pObj)) |
| 411 | // failed :( |
| 412 | return nullptr; |
| 413 | // count particle |
| 414 | ++pOfDef->Count; |
| 415 | // more to desired list |
| 416 | pPrt->MoveList(rFrom&: Game.Particles.FreeParticles, rTo&: *pPxList); |
| 417 | // return newly created particle |
| 418 | return pPrt; |
| 419 | } |
| 420 | |
| 421 | bool C4ParticleSystem::Cast(C4ParticleDef *pOfDef, int32_t iAmount, |
| 422 | float x, float y, int32_t level, |
| 423 | float a0, uint32_t b0, float a1, uint32_t b1, C4ParticleList *pPxList, C4Object *pObj) |
| 424 | { |
| 425 | // safety |
| 426 | if (!pOfDef) return false; |
| 427 | // get range for a and b |
| 428 | int32_t iA0 = static_cast<int32_t>(a0 * 100), iA1 = static_cast<int32_t>(a1 * 100); |
| 429 | if (iA1 < iA0) std::swap(a&: iA0, b&: iA1); |
| 430 | int32_t iAd = iA1 - iA0 + 1; |
| 431 | if (b1 < b0) { uint32_t dwX = b0; b0 = b1; b1 = dwX; } |
| 432 | uint32_t db = b1 - b0; |
| 433 | uint8_t db1 = uint8_t(db >> 24), db2 = uint8_t(db >> 16), db3 = uint8_t(db >> 8), db4 = uint8_t(db); |
| 434 | // create them |
| 435 | for (int32_t i = iAmount; i > 0; --i) |
| 436 | Create(pOfDef, x, y, |
| 437 | xdir: static_cast<int32_t>(SafeRandom(range: level + 1) - level / 2) / 10.0f, |
| 438 | ydir: static_cast<int32_t>(SafeRandom(range: level + 1) - level / 2) / 10.0f, |
| 439 | a: static_cast<int32_t>(iA0 + SafeRandom(range: iAd)) / 100.0f, |
| 440 | b: b0 + (SafeRandom(range: db1) << 24) + (SafeRandom(range: db2) << 16) + (SafeRandom(range: db3) << 8) + SafeRandom(range: db4), pPxList, pObj); |
| 441 | // success |
| 442 | return true; |
| 443 | } |
| 444 | |
| 445 | C4ParticleProc C4ParticleSystem::GetProc(const char *szName) |
| 446 | { |
| 447 | // seek in map |
| 448 | for (int32_t i = 0; C4ParticleProcMap[i].Name[0]; ++i) |
| 449 | if (SEqual(szStr1: C4ParticleProcMap[i].Name, szStr2: szName)) |
| 450 | return C4ParticleProcMap[i].Proc; |
| 451 | // nothing found... |
| 452 | return nullptr; |
| 453 | } |
| 454 | |
| 455 | C4ParticleDrawProc C4ParticleSystem::GetDrawProc(const char *szName) |
| 456 | { |
| 457 | // seek in map |
| 458 | for (int32_t i = 0; C4ParticleDrawProcMap[i].Name[0]; ++i) |
| 459 | if (SEqual(szStr1: C4ParticleDrawProcMap[i].Name, szStr2: szName)) |
| 460 | return C4ParticleDrawProcMap[i].Proc; |
| 461 | // nothing found... |
| 462 | return nullptr; |
| 463 | } |
| 464 | |
| 465 | C4ParticleDef *C4ParticleSystem::GetDef(const char *szName, C4ParticleDef *pExclude) |
| 466 | { |
| 467 | // seek list |
| 468 | for (C4ParticleDef *pDef = pDef0; pDef; pDef = pDef->pNext) |
| 469 | if (pDef != pExclude && pDef->Name == szName) |
| 470 | return pDef; |
| 471 | // nothing found |
| 472 | return nullptr; |
| 473 | } |
| 474 | |
| 475 | void C4ParticleSystem::SetDefParticles() |
| 476 | { |
| 477 | // get smoke |
| 478 | pSmoke = GetDef(szName: "Smoke" ); |
| 479 | // get blast |
| 480 | pBlast = GetDef(szName: "Blast" ); |
| 481 | pFSpark = GetDef(szName: "FSpark" ); |
| 482 | // get fire, if fire particles are desired |
| 483 | if (Config.Graphics.FireParticles) |
| 484 | { |
| 485 | pFire1 = GetDef(szName: "Fire" ); |
| 486 | pFire2 = GetDef(szName: "Fire2" ); |
| 487 | } |
| 488 | else |
| 489 | pFire1 = pFire2 = nullptr; |
| 490 | // if fire is drawn w/o background fct: unload fire face if both fire particles are assigned |
| 491 | // but this is not done here |
| 492 | } |
| 493 | |
| 494 | int32_t C4ParticleSystem::Push(C4ParticleDef *pOfDef, float dxdir, float dydir) |
| 495 | { |
| 496 | int32_t iNumPushed = 0; |
| 497 | // go through all particle chunks |
| 498 | for (C4ParticleChunk *pChnk = &Chunk; pChnk; pChnk = pChnk->pNext) |
| 499 | { |
| 500 | // go through all particles |
| 501 | C4Particle *pPrt = pChnk->Data; int32_t i = C4Px_BufSize; |
| 502 | while (i--) |
| 503 | { |
| 504 | // def fits? |
| 505 | if (!pOfDef || pPrt->pDef == pOfDef) |
| 506 | { |
| 507 | // push it! |
| 508 | pPrt->xdir += dxdir; |
| 509 | pPrt->ydir += dydir; |
| 510 | // count pushed |
| 511 | ++iNumPushed; |
| 512 | } |
| 513 | // next particle |
| 514 | ++pPrt; |
| 515 | } |
| 516 | } |
| 517 | // done |
| 518 | return iNumPushed; |
| 519 | } |
| 520 | |
| 521 | bool fxSmokeInit(C4Particle *pPrt, C4Object *pTarget) |
| 522 | { |
| 523 | // init lifetime |
| 524 | pPrt->life = pPrt->pDef->MinLifetime; |
| 525 | int32_t iLD = pPrt->pDef->MaxLifetime - pPrt->pDef->MinLifetime; |
| 526 | if (iLD) pPrt->life += SafeRandom(range: iLD); |
| 527 | // use high-word of life to store init-status |
| 528 | pPrt->life |= (pPrt->life / 17) << 16; |
| 529 | // set kind - ydir is unused anyway; set last kind reeeaaally seldom |
| 530 | pPrt->ydir = static_cast<float>(SafeRandom(range: 15)) + SafeRandom(range: 300) / 299; |
| 531 | // set color |
| 532 | if (!pPrt->b) pPrt->b = 0xff4b4b4b; else pPrt->b |= 0xff000000; |
| 533 | // always OK |
| 534 | return true; |
| 535 | } |
| 536 | |
| 537 | bool fxSmokeExec(C4Particle *pPrt, C4Object *pTarget) |
| 538 | { |
| 539 | // lifetime |
| 540 | if (!--pPrt->life) return false; |
| 541 | bool fBuilding = !!(pPrt->life & 0x7fff0000); |
| 542 | // still building? |
| 543 | if (fBuilding) |
| 544 | { |
| 545 | // decrease init-time |
| 546 | pPrt->life -= 0x010000; |
| 547 | // increase color value |
| 548 | pPrt->b -= 0x10000000; |
| 549 | // if full-grown, adjust to lifetime |
| 550 | if (!(pPrt->life & 0x7fff0000)) |
| 551 | pPrt->b = (pPrt->b & 0xffffff) | ((255 - pPrt->life) << 24); |
| 552 | } |
| 553 | // color change |
| 554 | uint32_t dwClr = pPrt->b; |
| 555 | pPrt->b = (LightenClrBy(dst&: dwClr, by: 1) & 0xffffff) | std::min<int32_t>(a: (dwClr >> 24) + 1, b: 255) << 24; |
| 556 | // wind to float |
| 557 | if (!(pPrt->b % 12) || fBuilding) |
| 558 | { |
| 559 | pPrt->xdir = 0.025f * Game.Weather.GetWind(x: int32_t(pPrt->x), y: int32_t(pPrt->y)); |
| 560 | if (pPrt->xdir < -2.0f) pPrt->xdir = -2.0f; else if (pPrt->xdir > 2.0f) pPrt->xdir = 2.0f; |
| 561 | pPrt->xdir += 0.1f * SafeRandom(range: 41) - 2.0f; |
| 562 | } |
| 563 | // float |
| 564 | if (GBackSolid(x: int32_t(pPrt->x), y: int32_t(pPrt->y - pPrt->a))) |
| 565 | { |
| 566 | // if stuck, decay; otherwise, move down |
| 567 | if (!GBackSolid(x: int32_t(pPrt->x), y: int32_t(pPrt->y))) pPrt->y += 0.4f; else pPrt->a -= 2; |
| 568 | } |
| 569 | else |
| 570 | --pPrt->y; |
| 571 | pPrt->x += pPrt->xdir; |
| 572 | // increase in size |
| 573 | pPrt->a *= 1.01f; |
| 574 | // done, keep |
| 575 | return true; |
| 576 | } |
| 577 | |
| 578 | void fxSmokeDraw(C4Particle *pPrt, C4FacetEx &cgo, C4Object *pTarget) |
| 579 | { |
| 580 | C4ParticleDef *pDef = pPrt->pDef; |
| 581 | // apply parallaxity to target pos |
| 582 | int32_t tx = cgo.TargetX * pDef->Parallaxity[0] / 100; |
| 583 | int32_t ty = cgo.TargetY * pDef->Parallaxity[1] / 100; |
| 584 | // check if it's in screen range |
| 585 | if (!Inside(ival: pPrt->x, lbound: tx - pPrt->a, rbound: tx + cgo.Wdt + pPrt->a)) return; |
| 586 | if (!Inside(ival: pPrt->y, lbound: ty - pPrt->a, rbound: ty + cgo.Hgt + pPrt->a)) return; |
| 587 | // get pos |
| 588 | int32_t cx = int32_t(pPrt->x) + cgo.X - tx; |
| 589 | int32_t cy = int32_t(pPrt->y) + cgo.Y - ty; |
| 590 | // get phase by particle index |
| 591 | int32_t i = static_cast<int32_t>(pPrt->ydir); |
| 592 | int32_t ipx = i / 4; |
| 593 | int32_t ipy = i % 4; |
| 594 | // draw at pos |
| 595 | Application.DDraw->ActivateBlitModulation(dwWithClr: pPrt->b); |
| 596 | pDef->Gfx.DrawX(sfcTarget: cgo.Surface, iX: int32_t(cx - pPrt->a), iY: int32_t(cy - pPrt->a), iWdt: int32_t(pPrt->a * 2), iHgt: int32_t(pPrt->a * 2), iPhaseX: ipx, iPhaseY: ipy); |
| 597 | Application.DDraw->DeactivateBlitModulation(); |
| 598 | } |
| 599 | |
| 600 | bool fxStdInit(C4Particle *pPrt, C4Object *pTarget) |
| 601 | { |
| 602 | if (pPrt->pDef->Delay) |
| 603 | // delay given: lifetime starts at zero |
| 604 | pPrt->life = 0; |
| 605 | else |
| 606 | // init lifetime as phase |
| 607 | pPrt->life = SafeRandom(range: pPrt->pDef->Length); |
| 608 | // default color |
| 609 | if (!pPrt->b) pPrt->b = 0xffffff; |
| 610 | // always OK |
| 611 | return true; |
| 612 | } |
| 613 | |
| 614 | bool fxStdExec(C4Particle *pPrt, C4Object *pTarget) |
| 615 | { |
| 616 | float dx = pPrt->x, dy = pPrt->y; |
| 617 | float dxdir = pPrt->xdir, dydir = pPrt->ydir; |
| 618 | // rel. position & movement |
| 619 | if (pPrt->pDef->Attach && pTarget != nullptr) |
| 620 | { |
| 621 | dx += pTarget->x; |
| 622 | dy += pTarget->y; |
| 623 | dxdir += fixtof(x: pTarget->xdir); |
| 624 | dydir += fixtof(x: pTarget->ydir); |
| 625 | } |
| 626 | |
| 627 | // move |
| 628 | if (pPrt->xdir || pPrt->ydir) |
| 629 | { |
| 630 | if (pPrt->pDef->VertexCount && GBackSolid(x: int32_t(dx + pPrt->xdir), y: int32_t(dy + pPrt->ydir + pPrt->pDef->VertexY * pPrt->a / 100.0f))) |
| 631 | { |
| 632 | // collision |
| 633 | if (pPrt->pDef->CollisionProc) |
| 634 | if (!pPrt->pDef->CollisionProc(pPrt, pTarget)) return false; |
| 635 | } |
| 636 | else if (pPrt->pDef->RByV != 2) |
| 637 | { |
| 638 | pPrt->x += pPrt->xdir; |
| 639 | pPrt->y += pPrt->ydir; |
| 640 | } |
| 641 | else |
| 642 | { |
| 643 | // With RByV=2, the V is only used for rotation, not for movement |
| 644 | } |
| 645 | } |
| 646 | // apply gravity |
| 647 | if (pPrt->pDef->GravityAcc) pPrt->ydir += fixtof(GravAccel * pPrt->pDef->GravityAcc) / 100.0f; |
| 648 | // apply WindDrift |
| 649 | if (pPrt->pDef->WindDrift && !GBackSolid(x: int32_t(dx), y: int32_t(dy))) |
| 650 | { |
| 651 | // Air speed: Wind plus some random |
| 652 | int32_t iWind = GBackWind(x: int32_t(dx), y: int32_t(dy)); |
| 653 | float txdir = iWind / 15.0f; |
| 654 | float tydir = 0; |
| 655 | |
| 656 | // Air friction, based on WindDrift. |
| 657 | int32_t iWindDrift = (std::max)(a: pPrt->pDef->WindDrift - 20, b: 0); |
| 658 | pPrt->xdir += ((txdir - dxdir) * iWindDrift) / 800; |
| 659 | pPrt->ydir += ((tydir - dydir) * iWindDrift) / 800; |
| 660 | } |
| 661 | // fade out |
| 662 | int32_t iFade = pPrt->pDef->AlphaFade; |
| 663 | if (iFade < 0) if (Game.FrameCounter % -iFade == 0) iFade = 1; else iFade = 0; |
| 664 | if (iFade) |
| 665 | { |
| 666 | uint32_t dwClr = pPrt->b; |
| 667 | int32_t iAlpha = dwClr >> 24; |
| 668 | iAlpha += pPrt->pDef->AlphaFade; |
| 669 | if (iAlpha >= 0xff) return false; |
| 670 | pPrt->b = (dwClr & 0xffffff) | (iAlpha << 24); |
| 671 | } |
| 672 | // if delay is given, advance lifetime |
| 673 | if (pPrt->pDef->Delay) |
| 674 | { |
| 675 | if (pPrt->life < 0) |
| 676 | { |
| 677 | // decay |
| 678 | return pPrt->life-- >= -pPrt->pDef->FadeOutLen * pPrt->pDef->FadeOutDelay; |
| 679 | } |
| 680 | ++pPrt->life; |
| 681 | // check if still alive |
| 682 | int32_t iPhase = pPrt->life / pPrt->pDef->Delay; |
| 683 | int32_t length = pPrt->pDef->Length - pPrt->pDef->Reverse; |
| 684 | if (iPhase >= length * pPrt->pDef->Repeats + pPrt->pDef->Reverse) |
| 685 | { |
| 686 | // do fadeout, if assigned |
| 687 | if (!pPrt->pDef->FadeOutLen) return false; |
| 688 | pPrt->life = -1; |
| 689 | } |
| 690 | return true; |
| 691 | } |
| 692 | // outside landscape range? |
| 693 | bool kp; |
| 694 | if (dxdir > 0) kp = (dx - pPrt->a < GBackWdt); else kp = (dx + pPrt->a > 0); |
| 695 | if (dydir > 0) kp = kp && (dy - pPrt->a < GBackHgt); else kp = kp && (dy + pPrt->a > pPrt->pDef->YOff); |
| 696 | return kp; |
| 697 | } |
| 698 | |
| 699 | bool fxBounce(C4Particle *pPrt, C4Object *pTarget) |
| 700 | { |
| 701 | // reverse xdir/ydir |
| 702 | pPrt->xdir = -pPrt->xdir; |
| 703 | pPrt->ydir = -pPrt->ydir; |
| 704 | return true; |
| 705 | } |
| 706 | |
| 707 | bool fxBounceY(C4Particle *pPrt, C4Object *pTarget) |
| 708 | { |
| 709 | // reverse ydir only |
| 710 | pPrt->ydir = -pPrt->ydir; |
| 711 | return true; |
| 712 | } |
| 713 | |
| 714 | bool fxStop(C4Particle *pPrt, C4Object *pTarget) |
| 715 | { |
| 716 | // zero xdir/ydir |
| 717 | pPrt->xdir = pPrt->ydir = 0; |
| 718 | return true; |
| 719 | } |
| 720 | |
| 721 | bool fxDie(C4Particle *pPrt, C4Object *pTarget) |
| 722 | { |
| 723 | // DIEEEEEE |
| 724 | return false; |
| 725 | } |
| 726 | |
| 727 | void fxStdDraw(C4Particle *pPrt, C4FacetEx &cgo, C4Object *pTarget) |
| 728 | { |
| 729 | // get def |
| 730 | C4ParticleDef *pDef = pPrt->pDef; |
| 731 | // apply parallaxity to target pos |
| 732 | int32_t tx = cgo.TargetX * pDef->Parallaxity[0] / 100; |
| 733 | int32_t ty = cgo.TargetY * pDef->Parallaxity[1] / 100; |
| 734 | |
| 735 | float dx = pPrt->x, dy = pPrt->y; |
| 736 | float dxdir = pPrt->xdir, dydir = pPrt->ydir; |
| 737 | // relative position & movement |
| 738 | if (pPrt->pDef->Attach && pTarget != nullptr) |
| 739 | { |
| 740 | dx += pTarget->x; |
| 741 | dy += pTarget->y; |
| 742 | dxdir += fixtof(x: pTarget->xdir); |
| 743 | dydir += fixtof(x: pTarget->ydir); |
| 744 | } |
| 745 | |
| 746 | // check if it's in screen range |
| 747 | if (!Inside(ival: dx, lbound: tx - pPrt->a, rbound: tx + cgo.Wdt + pPrt->a)) return; |
| 748 | if (!Inside(ival: dy, lbound: ty - pPrt->a, rbound: ty + cgo.Hgt + pPrt->a)) return; |
| 749 | // get pos |
| 750 | int32_t cgox = cgo.X - tx, cgoy = cgo.Y - ty; |
| 751 | int32_t cx = int32_t(dx + cgox); |
| 752 | int32_t cy = int32_t(dy + cgoy); |
| 753 | // get phase |
| 754 | int32_t iPhase = pPrt->life; |
| 755 | if (pDef->Delay) |
| 756 | if (iPhase >= 0) |
| 757 | { |
| 758 | iPhase /= pDef->Delay; |
| 759 | int32_t length = pDef->Length; |
| 760 | if (pDef->Reverse) |
| 761 | { |
| 762 | --length; iPhase %= length * 2; |
| 763 | if (iPhase > length) iPhase = length * 2 + 1 - iPhase; |
| 764 | } |
| 765 | else iPhase %= length; |
| 766 | } |
| 767 | else iPhase = (iPhase + 1) / -pDef->FadeOutDelay + pDef->Length; |
| 768 | // get rotation |
| 769 | int32_t r = 0; |
| 770 | if ((pDef->RByV == 1) || (pDef->RByV == 2)) // rotation by direction |
| 771 | r = Angle(iX1: 0, iY1: 0, iX2: static_cast<int32_t>(dxdir * 10.0f), iY2: static_cast<int32_t>(dydir * 10.0f)) * 100; |
| 772 | if (pDef->RByV == 3) // random rotation - currently a pseudo random rotation by x/y position |
| 773 | r = (static_cast<int32_t>(pPrt->x * 23 + pPrt->y * 12) % 360) * 100; |
| 774 | // draw at pos |
| 775 | Application.DDraw->ActivateBlitModulation(dwWithClr: pPrt->b); |
| 776 | Application.DDraw->StorePrimaryClipper(); |
| 777 | Application.DDraw->SubPrimaryClipper(iX1: cgox, iY1: cgoy + pDef->YOff, iX2: 100000, iY2: 100000); |
| 778 | if (pDef->Additive) lpDDraw->SetBlitMode(C4GFXBLIT_ADDITIVE); |
| 779 | int32_t iDrawWdt = int32_t(pPrt->a); |
| 780 | int32_t iDrawHgt = int32_t(pDef->Aspect * iDrawWdt); |
| 781 | if (r) |
| 782 | pDef->Gfx.DrawXR(sfcTarget: cgo.Surface, iX: cx - iDrawWdt, iY: cy - iDrawHgt, iWdt: iDrawWdt * 2, iHgt: iDrawHgt * 2, iPhaseX: iPhase, iPhaseY: 0, r); |
| 783 | else |
| 784 | pDef->Gfx.DrawX(sfcTarget: cgo.Surface, iX: cx - iDrawWdt, iY: cy - iDrawHgt, iWdt: iDrawWdt * 2, iHgt: iDrawHgt * 2, iPhaseX: iPhase, iPhaseY: 0); |
| 785 | Application.DDraw->ResetBlitMode(); |
| 786 | Application.DDraw->RestorePrimaryClipper(); |
| 787 | Application.DDraw->DeactivateBlitModulation(); |
| 788 | } |
| 789 | |
| 790 | C4ParticleProcRec C4ParticleProcMap[] = |
| 791 | { |
| 792 | { .Name: "SmokeInit" , .Proc: fxSmokeInit }, |
| 793 | { .Name: "SmokeExec" , .Proc: fxSmokeExec }, |
| 794 | { .Name: "StdInit" , .Proc: fxStdInit }, |
| 795 | { .Name: "StdExec" , .Proc: fxStdExec }, |
| 796 | { .Name: "Bounce" , .Proc: fxBounce }, |
| 797 | { .Name: "BounceY" , .Proc: fxBounceY }, |
| 798 | { .Name: "Stop" , .Proc: fxStop }, |
| 799 | { .Name: "Die" , .Proc: fxDie }, |
| 800 | { .Name: "" , .Proc: nullptr } |
| 801 | }; |
| 802 | |
| 803 | C4ParticleDrawProcRec C4ParticleDrawProcMap[] = |
| 804 | { |
| 805 | { .Name: "Smoke" , .Proc: fxSmokeDraw }, |
| 806 | { .Name: "Std" , .Proc: fxStdDraw }, |
| 807 | { .Name: "" , .Proc: nullptr } |
| 808 | }; |
| 809 | |