GCC Code Coverage Report
Directory: src/ Exec Total Coverage
File: src/particle/particle.cpp Lines: 1 241 0.4 %
Date: 2021-03-17 Branches: 0 283 0.0 %

Line Branch Exec Source
1
/*
2
 *  The ManaPlus Client
3
 *  Copyright (C) 2006-2009  The Mana World Development Team
4
 *  Copyright (C) 2009-2010  The Mana Developers
5
 *  Copyright (C) 2011-2019  The ManaPlus Developers
6
 *  Copyright (C) 2019-2021  Andrei Karas
7
 *
8
 *  This file is part of The ManaPlus Client.
9
 *
10
 *  This program is free software; you can redistribute it and/or modify
11
 *  it under the terms of the GNU General Public License as published by
12
 *  the Free Software Foundation; either version 2 of the License, or
13
 *  any later version.
14
 *
15
 *  This program is distributed in the hope that it will be useful,
16
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 *  GNU General Public License for more details.
19
 *
20
 *  You should have received a copy of the GNU General Public License
21
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
#include "particle/particle.h"
25
26
#include "actormanager.h"
27
#include "logger.h"
28
29
#include "being/actorsprite.h"
30
31
#include "particle/animationparticle.h"
32
#include "particle/particleemitter.h"
33
#include "particle/rotationalparticle.h"
34
35
#include "resources/animation/simpleanimation.h"
36
37
#include "resources/dye/dye.h"
38
39
#include "resources/image/image.h"
40
41
#include "resources/loaders/imageloader.h"
42
#include "resources/loaders/xmlloader.h"
43
44
#include "utils/delete2.h"
45
#include "utils/dtor.h"
46
#include "utils/foreach.h"
47
#include "utils/likely.h"
48
#include "utils/mathutils.h"
49
#include "utils/mrand.h"
50
51
#include "debug.h"
52
53
static const float SIN45 = 0.707106781F;
54
static const double PI = M_PI;
55
static const float PI2 = 2 * M_PI;
56
57
class Graphics;
58
59
Particle::Particle() :
60
    Actor(),
61
    mAlpha(1.0F),
62
    mLifetimeLeft(-1),
63
    mLifetimePast(0),
64
    mFadeOut(0),
65
    mFadeIn(0),
66
    mVelocity(),
67
    mAlive(AliveStatus::ALIVE),
68
    mType(ParticleType::Normal),
69
    mAnimation(nullptr),
70
    mImage(nullptr),
71
    mActor(BeingId_zero),
72
    mChildEmitters(),
73
    mChildParticles(),
74
    mChildMoveParticles(),
75
    mDeathEffect(),
76
    mGravity(0.0F),
77
    mBounce(0.0F),
78
    mAcceleration(0.0F),
79
    mInvDieDistance(-1.0F),
80
    mMomentum(1.0F),
81
    mTarget(nullptr),
82
    mRandomness(0),
83
    mDeathEffectConditions(0x00),
84
    mAutoDelete(true),
85
    mAllowSizeAdjust(false),
86
    mFollow(false)
87
{
88
    ParticleEngine::particleCount++;
89
}
90
91
Particle::~Particle()
92
{
93
    if (mActor != BeingId_zero &&
94
        (actorManager != nullptr))
95
    {
96
        ActorSprite *const actor = actorManager->findActor(mActor);
97
        if (actor != nullptr)
98
            actor->controlParticleDeleted(this);
99
    }
100
    // Delete child emitters and child particles
101
    clear();
102
    delete2(mAnimation)
103
    if (mImage != nullptr)
104
    {
105
        if (mType == ParticleType::Image)
106
        {
107
            const std::string &restrict name = mImage->mIdPath;
108
            StringIntMapIter it
109
                = ImageParticle::imageParticleCountByName.find(name);
110
            if (it != ImageParticle::imageParticleCountByName.end())
111
            {
112
                int &cnt = (*it).second;
113
                if (cnt > 0)
114
                    cnt --;
115
            }
116
            mImage->decRef();
117
        }
118
        mImage = nullptr;
119
    }
120
121
    ParticleEngine::particleCount--;
122
}
123
124
void Particle::draw(Graphics *restrict const graphics A_UNUSED,
125
                    const int offsetX A_UNUSED,
126
                    const int offsetY A_UNUSED) const restrict2
127
{
128
}
129
130
void Particle::updateSelf() restrict2
131
{
132
    // calculate particle movement
133
    if (A_LIKELY(mMomentum != 1.0F))
134
        mVelocity *= mMomentum;
135
136
    if ((mTarget != nullptr) && mAcceleration != 0.0F)
137
    {
138
        Vector dist = mPos - mTarget->mPos;
139
        dist.x *= SIN45;
140
        float invHypotenuse;
141
142
        switch (ParticleEngine::fastPhysics)
143
        {
144
            case ParticlePhysics::Normal:
145
                invHypotenuse = fastInvSqrt(
146
                    dist.x * dist.x + dist.y * dist.y + dist.z * dist.z);
147
                break;
148
            case ParticlePhysics::Fast:
149
                if (dist.x == 0.0F)
150
                {
151
                    invHypotenuse = 0;
152
                    break;
153
                }
154
155
                invHypotenuse = 2.0F / (static_cast<float>(fabs(dist.x))
156
                                + static_cast<float>(fabs(dist.y))
157
                                + static_cast<float>(fabs(dist.z)));
158
                break;
159
            case ParticlePhysics::Best:
160
            default:
161
                invHypotenuse = 1.0F / static_cast<float>(sqrt(
162
                    dist.x * dist.x + dist.y * dist.y + dist.z * dist.z));
163
                break;
164
        }
165
166
        if (invHypotenuse != 0.0F)
167
        {
168
            if (mInvDieDistance > 0.0F && invHypotenuse > mInvDieDistance)
169
                mAlive = AliveStatus::DEAD_IMPACT;
170
            const float accFactor = invHypotenuse * mAcceleration;
171
            mVelocity -= dist * accFactor;
172
        }
173
    }
174
175
    if (A_LIKELY(mRandomness >= 10))  // reduce useless calculations
176
    {
177
        const int rand2 = mRandomness * 2;
178
        mVelocity.x += static_cast<float>(mrand() % rand2 - mRandomness)
179
            / 1000.0F;
180
        mVelocity.y += static_cast<float>(mrand() % rand2 - mRandomness)
181
            / 1000.0F;
182
        mVelocity.z += static_cast<float>(mrand() % rand2 - mRandomness)
183
            / 1000.0F;
184
    }
185
186
    mVelocity.z -= mGravity;
187
188
    // Update position
189
    mPos.x += mVelocity.x;
190
    mPos.y += mVelocity.y * SIN45;
191
    mPos.z += mVelocity.z * SIN45;
192
193
    // Update other stuff
194
    if (A_LIKELY(mLifetimeLeft > 0))
195
        mLifetimeLeft--;
196
197
    mLifetimePast++;
198
199
    if (mPos.z < 0.0F)
200
    {
201
        if (mBounce > 0.0F)
202
        {
203
            mPos.z *= -mBounce;
204
            mVelocity *= mBounce;
205
            mVelocity.z = -mVelocity.z;
206
        }
207
        else
208
        {
209
            mAlive = AliveStatus::DEAD_FLOOR;
210
        }
211
    }
212
    else if (mPos.z > ParticleEngine::PARTICLE_SKY)
213
    {
214
        mAlive = AliveStatus::DEAD_SKY;
215
    }
216
217
    // Update child emitters
218
    if ((ParticleEngine::emitterSkip != 0) &&
219
        (mLifetimePast - 1) % ParticleEngine::emitterSkip == 0)
220
    {
221
        FOR_EACH (EmitterConstIterator, e, mChildEmitters)
222
        {
223
            STD_VECTOR<Particle*> newParticles;
224
            (*e)->createParticles(mLifetimePast, newParticles);
225
            FOR_EACH (STD_VECTOR<Particle*>::const_iterator,
226
                      it,
227
                      newParticles)
228
            {
229
                Particle *const p = *it;
230
                p->moveBy(mPos);
231
                mChildParticles.push_back(p);
232
                if (p->mFollow)
233
                    mChildMoveParticles.push_back(p);
234
            }
235
        }
236
    }
237
238
    // create death effect when the particle died
239
    if (A_UNLIKELY(mAlive != AliveStatus::ALIVE &&
240
        mAlive != AliveStatus::DEAD_LONG_AGO))
241
    {
242
        if ((CAST_U32(mAlive) & mDeathEffectConditions)
243
            > 0x00  && !mDeathEffect.empty())
244
        {
245
            Particle *restrict const deathEffect = particleEngine->addEffect(
246
                mDeathEffect, 0, 0, 0);
247
            if (deathEffect != nullptr)
248
                deathEffect->moveBy(mPos);
249
        }
250
        mAlive = AliveStatus::DEAD_LONG_AGO;
251
    }
252
}
253
254
bool Particle::update() restrict2
255
{
256
    if (A_LIKELY(mAlive == AliveStatus::ALIVE))
257
    {
258
        if (A_UNLIKELY(mLifetimeLeft == 0))
259
        {
260
            mAlive = AliveStatus::DEAD_TIMEOUT;
261
            if (mChildParticles.empty())
262
            {
263
                if (mAutoDelete)
264
                    return false;
265
                return true;
266
            }
267
        }
268
        else
269
        {
270
            if (mAnimation != nullptr)
271
            {
272
                if (mType == ParticleType::Animation)
273
                {
274
                    // particle engine is updated every 10ms
275
                    mAnimation->update(10);
276
                }
277
                else  // ParticleType::Rotational
278
                {
279
                    // TODO: cache velocities to avoid spamming atan2()
280
                    const int size = mAnimation->getLength();
281
                    if (size == 0)
282
                        return false;
283
284
                    float rad = static_cast<float>(atan2(mVelocity.x,
285
                        mVelocity.y));
286
                    if (rad < 0)
287
                        rad = PI2 + rad;
288
289
                    const float range = static_cast<float>(PI / size);
290
291
                    // Determines which frame the particle should play
292
                    if (A_UNLIKELY(rad < range || rad > PI2 - range))
293
                    {
294
                        mAnimation->setFrame(0);
295
                    }
296
                    else
297
                    {
298
                        const float range2 = 2 * range;
299
                        // +++ need move condition outside of for
300
                        for (int c = 1; c < size; c++)
301
                        {
302
                            const float cRange = static_cast<float>(c) *
303
                                range2;
304
                            if (cRange - range < rad &&
305
                                rad < cRange + range)
306
                            {
307
                                mAnimation->setFrame(c);
308
                                break;
309
                            }
310
                        }
311
                    }
312
                }
313
                mImage = mAnimation->getCurrentImage();
314
            }
315
            const Vector oldPos = mPos;
316
317
            updateSelf();
318
319
            const Vector change = mPos - oldPos;
320
            if (mChildParticles.empty())
321
            {
322
                if (mAlive != AliveStatus::ALIVE &&
323
                    mAutoDelete)
324
                {
325
                    return false;
326
                }
327
                return true;
328
            }
329
            for (ParticleIterator p = mChildMoveParticles.begin(),
330
                 fp2 = mChildMoveParticles.end(); p != fp2; )
331
            {
332
                // move particle with its parent if desired
333
                (*p)->moveBy(change);
334
                ++p;
335
            }
336
        }
337
338
        // Update child particles
339
        for (ParticleIterator p = mChildParticles.begin(),
340
             fp2 = mChildParticles.end(); p != fp2; )
341
        {
342
            Particle *restrict const particle = *p;
343
            // update particle
344
            if (A_LIKELY(particle->update()))
345
            {
346
                ++p;
347
            }
348
            else
349
            {
350
                mChildMoveParticles.remove(*p);
351
                delete particle;
352
                p = mChildParticles.erase(p);
353
            }
354
        }
355
        if (A_UNLIKELY(mAlive != AliveStatus::ALIVE &&
356
            mChildParticles.empty() &&
357
            mAutoDelete))
358
        {
359
            return false;
360
        }
361
    }
362
    else
363
    {
364
        if (mChildParticles.empty())
365
        {
366
            if (mAutoDelete)
367
                return false;
368
            return true;
369
        }
370
        // Update child particles
371
        for (ParticleIterator p = mChildParticles.begin(),
372
             fp2 = mChildParticles.end(); p != fp2; )
373
        {
374
            Particle *restrict const particle = *p;
375
            // update particle
376
            if (A_LIKELY(particle->update()))
377
            {
378
                ++p;
379
            }
380
            else
381
            {
382
                mChildMoveParticles.remove(*p);
383
                delete particle;
384
                p = mChildParticles.erase(p);
385
            }
386
        }
387
        if (A_UNLIKELY(mChildParticles.empty() &&
388
            mAutoDelete))
389
        {
390
            return false;
391
        }
392
    }
393
394
    return true;
395
}
396
397
void Particle::moveBy(const Vector &restrict change) restrict2
398
{
399
    mPos += change;
400
    FOR_EACH (ParticleConstIterator, p, mChildMoveParticles)
401
    {
402
        (*p)->moveBy(change);
403
    }
404
}
405
406
void Particle::moveTo(const float x, const float y) restrict2
407
{
408
    moveTo(Vector(x, y, mPos.z));
409
}
410
411
Particle *Particle::addEffect(const std::string &restrict particleEffectFile,
412
                              const int pixelX, const int pixelY,
413
                              const int rotation) restrict2
414
{
415
    Particle *newParticle = nullptr;
416
417
    const size_t pos = particleEffectFile.find('|');
418
    const std::string dyePalettes = (pos != std::string::npos)
419
        ? particleEffectFile.substr(pos + 1) : "";
420
    XML::Document *doc = Loader::getXml(particleEffectFile.substr(0, pos),
421
        UseVirtFs_true,
422
        SkipError_false);
423
    if (doc == nullptr)
424
        return nullptr;
425
    XmlNodeConstPtrConst rootNode = doc->rootNode();
426
427
    if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "effect"))
428
    {
429
        logger->log("Error loading particle: %s", particleEffectFile.c_str());
430
        doc->decRef();
431
        return nullptr;
432
    }
433
434
    // Parse particles
435
    for_each_xml_child_node(effectChildNode, rootNode)
436
    {
437
        // We're only interested in particles
438
        if (!xmlNameEqual(effectChildNode, "particle"))
439
            continue;
440
441
        // Determine the exact particle type
442
        XmlNodePtr node;
443
444
        // Animation
445
        if ((node = XML::findFirstChildByName(effectChildNode, "animation")) !=
446
            nullptr)
447
        {
448
            newParticle = new AnimationParticle(node, dyePalettes);
449
            newParticle->setMap(mMap);
450
        }
451
        // Rotational
452
        else if ((node = XML::findFirstChildByName(
453
                 effectChildNode, "rotation")) != nullptr)
454
        {
455
            newParticle = new RotationalParticle(node, dyePalettes);
456
            newParticle->setMap(mMap);
457
        }
458
        // Image
459
        else if ((node = XML::findFirstChildByName(effectChildNode,
460
                 "image")) != nullptr)
461
        {
462
            std::string imageSrc;
463
            if (XmlHaveChildContent(node))
464
                imageSrc = XmlChildContent(node);
465
            if (!imageSrc.empty() && !dyePalettes.empty())
466
                Dye::instantiate(imageSrc, dyePalettes);
467
            Image *const img = Loader::getImage(imageSrc);
468
469
            newParticle = new ImageParticle(img);
470
            newParticle->setMap(mMap);
471
        }
472
        // Other
473
        else
474
        {
475
            newParticle = new Particle;
476
            newParticle->setMap(mMap);
477
        }
478
479
        // Read and set the basic properties of the particle
480
        const float offsetX = XML::getFloatProperty(
481
            effectChildNode, "position-x", 0);
482
        const float offsetY = XML::getFloatProperty(
483
            effectChildNode, "position-y", 0);
484
        const float offsetZ = XML::getFloatProperty(
485
            effectChildNode, "position-z", 0);
486
        const Vector position(mPos.x + static_cast<float>(pixelX) + offsetX,
487
            mPos.y + static_cast<float>(pixelY) + offsetY,
488
            mPos.z + offsetZ);
489
        newParticle->moveTo(position);
490
491
        const int lifetime = XML::getProperty(effectChildNode, "lifetime", -1);
492
        newParticle->setLifetime(lifetime);
493
        const bool resizeable = "false" != XML::getProperty(effectChildNode,
494
            "size-adjustable", "false");
495
496
        newParticle->setAllowSizeAdjust(resizeable);
497
498
        // Look for additional emitters for this particle
499
        for_each_xml_child_node(emitterNode, effectChildNode)
500
        {
501
            if (xmlNameEqual(emitterNode, "emitter"))
502
            {
503
                ParticleEmitter *restrict const newEmitter =
504
                    new ParticleEmitter(
505
                    emitterNode,
506
                    newParticle,
507
                    mMap,
508
                    rotation,
509
                    dyePalettes);
510
                newParticle->addEmitter(newEmitter);
511
            }
512
            else if (xmlNameEqual(emitterNode, "deatheffect"))
513
            {
514
                std::string deathEffect;
515
                if ((node != nullptr) && XmlHaveChildContent(node))
516
                    deathEffect = XmlChildContent(emitterNode);
517
518
                char deathEffectConditions = 0x00;
519
                if (XML::getBoolProperty(emitterNode, "on-floor", true))
520
                {
521
                    deathEffectConditions += CAST_S8(
522
                        AliveStatus::DEAD_FLOOR);
523
                }
524
                if (XML::getBoolProperty(emitterNode, "on-sky", true))
525
                {
526
                    deathEffectConditions += CAST_S8(
527
                        AliveStatus::DEAD_SKY);
528
                }
529
                if (XML::getBoolProperty(emitterNode, "on-other", false))
530
                {
531
                    deathEffectConditions += CAST_S8(
532
                        AliveStatus::DEAD_OTHER);
533
                }
534
                if (XML::getBoolProperty(emitterNode, "on-impact", true))
535
                {
536
                    deathEffectConditions += CAST_S8(
537
                        AliveStatus::DEAD_IMPACT);
538
                }
539
                if (XML::getBoolProperty(emitterNode, "on-timeout", true))
540
                {
541
                    deathEffectConditions += CAST_S8(
542
                        AliveStatus::DEAD_TIMEOUT);
543
                }
544
                newParticle->setDeathEffect(
545
                    deathEffect, deathEffectConditions);
546
            }
547
        }
548
549
        mChildParticles.push_back(newParticle);
550
    }
551
552
    doc->decRef();
553
    return newParticle;
554
}
555
556
void Particle::adjustEmitterSize(const int w, const int h) restrict2
557
{
558
    if (mAllowSizeAdjust)
559
    {
560
        FOR_EACH (EmitterConstIterator, e, mChildEmitters)
561
            (*e)->adjustSize(w, h);
562
    }
563
}
564
565
void Particle::prepareToDie() restrict2
566
{
567
    FOR_EACH (ParticleIterator, p, mChildParticles)
568
    {
569
        Particle *restrict const particle = *p;
570
        if (particle == nullptr)
571
            continue;
572
        particle->prepareToDie();
573
        if (particle->isAlive() &&
574
            particle->mLifetimeLeft == -1 &&
575
            particle->mAutoDelete)
576
        {
577
            particle->kill();
578
        }
579
    }
580
}
581
582
void Particle::clear() restrict2
583
{
584
    delete_all(mChildEmitters);
585
    mChildEmitters.clear();
586
587
    delete_all(mChildParticles);
588
    mChildParticles.clear();
589
590
    mChildMoveParticles.clear();
591
2
}