GCC Code Coverage Report
Directory: src/ Exec Total Coverage
File: src/particle/particleemitter.cpp Lines: 1 310 0.3 %
Date: 2021-03-17 Branches: 0 376 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/particleemitter.h"
25
26
#include "logger.h"
27
28
#include "const/resources/map/map.h"
29
30
#include "particle/animationparticle.h"
31
#include "particle/rotationalparticle.h"
32
33
#include "utils/foreach.h"
34
35
#include "resources/imageset.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/imagesetloader.h"
43
#include "resources/loaders/subimageloader.h"
44
#include "resources/loaders/subimagesetloader.h"
45
46
#include "debug.h"
47
48
static const float SIN45 = 0.707106781F;
49
static const float DEG_RAD_FACTOR = 0.017453293F;
50
51
typedef STD_VECTOR<ImageSet*>::const_iterator ImageSetVectorCIter;
52
typedef std::list<ParticleEmitter>::const_iterator ParticleEmitterListCIter;
53
54
ParticleEmitter::ParticleEmitter(XmlNodeConstPtrConst emitterNode,
55
                                 Particle *const target,
56
                                 Map *const map, const int rotation,
57
                                 const std::string& dyePalettes) :
58
    mParticleTarget(target),
59
    mMap(map),
60
    mOutput(),
61
    mOutputPause(),
62
    mParticleImage(nullptr),
63
    mParticleAnimation("particle animation"),
64
    mParticleRotation("particle rotation"),
65
    mParticleAlpha(),
66
    mDeathEffect(),
67
    mParticleChildEmitters(),
68
    mTempSets(),
69
    mOutputPauseLeft(0),
70
    mDeathEffectConditions(0),
71
    mParticleFollow(false)
72
{
73
    // Initializing default values
74
    mParticlePosX.set(0.0F);
75
    mParticlePosY.set(0.0F);
76
    mParticlePosZ.set(0.0F);
77
    mParticleAngleHorizontal.set(0.0F);
78
    mParticleAngleVertical.set(0.0F);
79
    mParticlePower.set(0.0F);
80
    mParticleGravity.set(0.0F);
81
    mParticleRandomness.set(0);
82
    mParticleBounce.set(0.0F);
83
    mParticleAcceleration.set(0.0F);
84
    mParticleDieDistance.set(-1.0F);
85
    mParticleMomentum.set(1.0F);
86
    mParticleLifetime.set(-1);
87
    mParticleFadeOut.set(0);
88
    mParticleFadeIn.set(0);
89
    mOutput.set(1);
90
    mOutputPause.set(0);
91
    mParticleAlpha.set(1.0F);
92
93
    if (emitterNode == nullptr)
94
        return;
95
    for_each_xml_child_node(propertyNode, emitterNode)
96
    {
97
        if (xmlNameEqual(propertyNode, "property"))
98
        {
99
            const std::string name = XML::getProperty(
100
                propertyNode, "name", "");
101
102
            if (name == "position-x")
103
            {
104
                mParticlePosX = readParticleEmitterProp(propertyNode, 0.0F);
105
            }
106
            else if (name == "position-y")
107
            {
108
                mParticlePosY = readParticleEmitterProp(propertyNode, 0.0F);
109
                mParticlePosY.minVal *= SIN45;
110
                mParticlePosY.maxVal *= SIN45;
111
                mParticlePosY.changeAmplitude *= SIN45;
112
            }
113
            else if (name == "position-z")
114
            {
115
                mParticlePosZ = readParticleEmitterProp(propertyNode, 0.0F);
116
                mParticlePosZ.minVal *= SIN45;
117
                mParticlePosZ.maxVal *= SIN45;
118
                mParticlePosZ.changeAmplitude *= SIN45;
119
            }
120
            else if (name == "image")
121
            {
122
                std::string image = XML::getProperty(
123
                    propertyNode, "value", "");
124
                // Don't leak when multiple images are defined
125
                if (!image.empty() && (mParticleImage == nullptr))
126
                {
127
                    if (!dyePalettes.empty())
128
                        Dye::instantiate(image, dyePalettes);
129
                    mParticleImage = Loader::getImage(image);
130
                }
131
            }
132
            else if (name == "subimage")
133
            {
134
                std::string image = XML::getProperty(
135
                    propertyNode, "value", "");
136
                // Don't leak when multiple images are defined
137
                if (!image.empty() && (mParticleImage == nullptr))
138
                {
139
                    if (!dyePalettes.empty())
140
                        Dye::instantiate(image, dyePalettes);
141
                    Image *img = Loader::getImage(image);
142
                    if (img != nullptr)
143
                    {
144
                        mParticleImage = Loader::getSubImage(img,
145
                            XML::getProperty(propertyNode, "x", 0),
146
                            XML::getProperty(propertyNode, "y", 0),
147
                            XML::getProperty(propertyNode, "width", 0),
148
                            XML::getProperty(propertyNode, "height", 0));
149
                        img->decRef();
150
                    }
151
                    else
152
                    {
153
                        mParticleImage = nullptr;
154
                    }
155
                }
156
            }
157
            else if (name == "horizontal-angle")
158
            {
159
                mParticleAngleHorizontal =
160
                    readParticleEmitterProp(propertyNode, 0.0F);
161
                mParticleAngleHorizontal.minVal
162
                    += static_cast<float>(rotation);
163
                mParticleAngleHorizontal.minVal *= DEG_RAD_FACTOR;
164
                mParticleAngleHorizontal.maxVal
165
                    += static_cast<float>(rotation);
166
                mParticleAngleHorizontal.maxVal *= DEG_RAD_FACTOR;
167
                mParticleAngleHorizontal.changeAmplitude *= DEG_RAD_FACTOR;
168
            }
169
            else if (name == "vertical-angle")
170
            {
171
                mParticleAngleVertical =
172
                    readParticleEmitterProp(propertyNode, 0.0F);
173
                mParticleAngleVertical.minVal *= DEG_RAD_FACTOR;
174
                mParticleAngleVertical.maxVal *= DEG_RAD_FACTOR;
175
                mParticleAngleVertical.changeAmplitude *= DEG_RAD_FACTOR;
176
            }
177
            else if (name == "power")
178
            {
179
                mParticlePower = readParticleEmitterProp(propertyNode, 0.0F);
180
            }
181
            else if (name == "gravity")
182
            {
183
                mParticleGravity = readParticleEmitterProp(propertyNode, 0.0F);
184
            }
185
            else if (name == "randomnes"
186
                     || name == "randomness")  // legacy bug
187
            {
188
                mParticleRandomness = readParticleEmitterProp(propertyNode, 0);
189
            }
190
            else if (name == "bounce")
191
            {
192
                mParticleBounce = readParticleEmitterProp(propertyNode, 0.0F);
193
            }
194
            else if (name == "lifetime")
195
            {
196
                mParticleLifetime = readParticleEmitterProp(propertyNode, 0);
197
                mParticleLifetime.minVal += 1;
198
            }
199
            else if (name == "output")
200
            {
201
                mOutput = readParticleEmitterProp(propertyNode, 0);
202
                mOutput.maxVal += 1;
203
            }
204
            else if (name == "output-pause")
205
            {
206
                mOutputPause = readParticleEmitterProp(propertyNode, 0);
207
                mOutputPauseLeft = mOutputPause.value(0);
208
            }
209
            else if (name == "acceleration")
210
            {
211
                mParticleAcceleration = readParticleEmitterProp(
212
                    propertyNode, 0.0F);
213
            }
214
            else if (name == "die-distance")
215
            {
216
                mParticleDieDistance = readParticleEmitterProp(
217
                    propertyNode, 0.0F);
218
            }
219
            else if (name == "momentum")
220
            {
221
                mParticleMomentum = readParticleEmitterProp(
222
                    propertyNode, 1.0F);
223
            }
224
            else if (name == "fade-out")
225
            {
226
                mParticleFadeOut = readParticleEmitterProp(propertyNode, 0);
227
            }
228
            else if (name == "fade-in")
229
            {
230
                mParticleFadeIn = readParticleEmitterProp(propertyNode, 0);
231
            }
232
            else if (name == "alpha")
233
            {
234
                mParticleAlpha = readParticleEmitterProp(propertyNode, 1.0F);
235
            }
236
            else if (name == "follow-parent")
237
            {
238
                const std::string value = XML::getProperty(propertyNode,
239
                    "value", "0");
240
                if (value == "1" || value == "true")
241
                    mParticleFollow = true;
242
            }
243
            else
244
            {
245
                logger->log("Particle Engine: Warning, "
246
                            "unknown emitter property \"%s\"",
247
                            name.c_str());
248
            }
249
        }
250
        else if (xmlNameEqual(propertyNode, "emitter"))
251
        {
252
            ParticleEmitter newEmitter(propertyNode, mParticleTarget, map,
253
                                       rotation, dyePalettes);
254
            mParticleChildEmitters.push_back(newEmitter);
255
        }
256
        else if (xmlNameEqual(propertyNode, "rotation")
257
                 || xmlNameEqual(propertyNode, "animation"))
258
        {
259
            ImageSet *const imageset = getImageSet(propertyNode);
260
            if (imageset == nullptr)
261
            {
262
                logger->log1("Error: no valid imageset");
263
                continue;
264
            }
265
            mTempSets.push_back(imageset);
266
267
            Animation &animation = (xmlNameEqual(propertyNode, "rotation")) !=
268
                0 ? mParticleRotation : mParticleAnimation;
269
270
            // Get animation frames
271
            for_each_xml_child_node(frameNode, propertyNode)
272
            {
273
                const int delay = XML::getIntProperty(
274
                    frameNode, "delay", 0, 0, 100000);
275
                const int offsetX = XML::getProperty(frameNode, "offsetX", 0)
276
                    - imageset->getWidth() / 2 + mapTileSize / 2;
277
                const int offsetY = XML::getProperty(frameNode, "offsetY", 0)
278
                    - imageset->getHeight() + mapTileSize;
279
                const int rand = XML::getIntProperty(
280
                    frameNode, "rand", 100, 0, 100);
281
282
                if (xmlNameEqual(frameNode, "frame"))
283
                {
284
                    const int index = XML::getProperty(frameNode, "index", -1);
285
286
                    if (index < 0)
287
                    {
288
                        logger->log1("No valid value for 'index'");
289
                        continue;
290
                    }
291
292
                    Image *const img = imageset->get(index);
293
294
                    if (img == nullptr)
295
                    {
296
                        logger->log("No image at index %d", index);
297
                        continue;
298
                    }
299
300
                    animation.addFrame(img, delay,
301
                        offsetX, offsetY, rand);
302
                }
303
                else if (xmlNameEqual(frameNode, "sequence"))
304
                {
305
                    int start = XML::getProperty(frameNode, "start", -1);
306
                    const int end = XML::getProperty(frameNode, "end", -1);
307
308
                    if (start < 0 || end < 0)
309
                    {
310
                        logger->log1("No valid value for 'start' or 'end'");
311
                        continue;
312
                    }
313
314
                    while (end >= start)
315
                    {
316
                        Image *const img = imageset->get(start);
317
                        if (img == nullptr)
318
                        {
319
                            logger->log("No image at index %d", start);
320
                            continue;
321
                        }
322
323
                        animation.addFrame(img, delay,
324
                            offsetX, offsetY, rand);
325
                        start ++;
326
                    }
327
                }
328
                else if (xmlNameEqual(frameNode, "end"))
329
                {
330
                    animation.addTerminator(rand);
331
                }
332
            }  // for frameNode
333
        }
334
        else if (xmlNameEqual(propertyNode, "deatheffect"))
335
        {
336
            if (!XmlHaveChildContent(propertyNode))
337
                continue;
338
            mDeathEffect = XmlChildContent(propertyNode);
339
            mDeathEffectConditions = 0x00;
340
            if (XML::getBoolProperty(propertyNode, "on-floor", true))
341
            {
342
                mDeathEffectConditions += CAST_S8(
343
                    AliveStatus::DEAD_FLOOR);
344
            }
345
            if (XML::getBoolProperty(propertyNode, "on-sky", true))
346
            {
347
                mDeathEffectConditions += CAST_S8(
348
                    AliveStatus::DEAD_SKY);
349
            }
350
            if (XML::getBoolProperty(propertyNode, "on-other", false))
351
            {
352
                mDeathEffectConditions += CAST_S8(
353
                    AliveStatus::DEAD_OTHER);
354
            }
355
            if (XML::getBoolProperty(propertyNode, "on-impact", true))
356
            {
357
                mDeathEffectConditions += CAST_S8(
358
                    AliveStatus::DEAD_IMPACT);
359
            }
360
            if (XML::getBoolProperty(propertyNode, "on-timeout", true))
361
            {
362
                mDeathEffectConditions += CAST_S8(
363
                    AliveStatus::DEAD_TIMEOUT);
364
            }
365
        }
366
    }
367
}
368
369
ParticleEmitter::ParticleEmitter(const ParticleEmitter &o)
370
{
371
    *this = o;
372
}
373
374
ImageSet *ParticleEmitter::getImageSet(XmlNodePtrConst node)
375
{
376
    ImageSet *imageset = nullptr;
377
    const int subX = XML::getProperty(node, "subX", -1);
378
    if (subX != -1)
379
    {
380
        Image *const img = Loader::getImage(XML::getProperty(
381
            node, "imageset", ""));
382
        if (img == nullptr)
383
            return nullptr;
384
385
        Image *const img2 = Loader::getSubImage(img, subX,
386
            XML::getProperty(node, "subY", 0),
387
            XML::getProperty(node, "subWidth", 0),
388
            XML::getProperty(node, "subHeight", 0));
389
        if (img2 == nullptr)
390
        {
391
            img->decRef();
392
            return nullptr;
393
        }
394
395
        imageset = Loader::getSubImageSet(img2,
396
            XML::getProperty(node, "width", 0),
397
            XML::getProperty(node, "height", 0));
398
        img2->decRef();
399
        img->decRef();
400
    }
401
    else
402
    {
403
        imageset = Loader::getImageSet(
404
            XML::getProperty(node, "imageset", ""),
405
            XML::getProperty(node, "width", 0),
406
            XML::getProperty(node, "height", 0));
407
    }
408
    return imageset;
409
}
410
411
ParticleEmitter & ParticleEmitter::operator=(const ParticleEmitter &o)
412
{
413
    mParticlePosX = o.mParticlePosX;
414
    mParticlePosY = o.mParticlePosY;
415
    mParticlePosZ = o.mParticlePosZ;
416
    mParticleAngleHorizontal = o.mParticleAngleHorizontal;
417
    mParticleAngleVertical = o.mParticleAngleVertical;
418
    mParticlePower = o.mParticlePower;
419
    mParticleGravity = o.mParticleGravity;
420
    mParticleRandomness = o.mParticleRandomness;
421
    mParticleBounce = o.mParticleBounce;
422
    mParticleFollow = o.mParticleFollow;
423
    mParticleTarget = o.mParticleTarget;
424
    mParticleAcceleration = o.mParticleAcceleration;
425
    mParticleDieDistance = o.mParticleDieDistance;
426
    mParticleMomentum = o.mParticleMomentum;
427
    mParticleLifetime = o.mParticleLifetime;
428
    mParticleFadeOut = o.mParticleFadeOut;
429
    mParticleFadeIn = o.mParticleFadeIn;
430
    mParticleAlpha = o.mParticleAlpha;
431
    mMap = o.mMap;
432
    mOutput = o.mOutput;
433
    mOutputPause = o.mOutputPause;
434
    mParticleImage = o.mParticleImage;
435
    mParticleAnimation = o.mParticleAnimation;
436
    mParticleRotation = o.mParticleRotation;
437
    mParticleChildEmitters = o.mParticleChildEmitters;
438
    mDeathEffectConditions = o.mDeathEffectConditions;
439
    mDeathEffect = o.mDeathEffect;
440
    mTempSets = o.mTempSets;
441
442
    FOR_EACH (ImageSetVectorCIter, i, mTempSets)
443
    {
444
        if (*i != nullptr)
445
            (*i)->incRef();
446
    }
447
448
    mOutputPauseLeft = 0;
449
450
    if (mParticleImage != nullptr)
451
        mParticleImage->incRef();
452
453
    return *this;
454
}
455
456
ParticleEmitter::~ParticleEmitter()
457
{
458
    FOR_EACH (ImageSetVectorCIter, i, mTempSets)
459
    {
460
        if (*i != nullptr)
461
            (*i)->decRef();
462
    }
463
    mTempSets.clear();
464
465
    if (mParticleImage != nullptr)
466
    {
467
        mParticleImage->decRef();
468
        mParticleImage = nullptr;
469
    }
470
}
471
472
template <typename T> ParticleEmitterProp<T>
473
ParticleEmitter::readParticleEmitterProp(XmlNodePtrConst propertyNode, T def)
474
{
475
    ParticleEmitterProp<T> retval;
476
477
    def = static_cast<T>(XML::getDoubleProperty(propertyNode, "value",
478
        static_cast<double>(def)));
479
    retval.set(static_cast<T>(XML::getDoubleProperty(propertyNode, "min",
480
        static_cast<double>(def))), static_cast<T>(XML::getDoubleProperty(
481
        propertyNode, "max", static_cast<double>(def))));
482
483
    const std::string change = XML::getProperty(
484
        propertyNode, "change-func", "none");
485
    T amplitude = static_cast<T>(XML::getDoubleProperty(propertyNode,
486
        "change-amplitude", 0.0));
487
488
    const int period = XML::getProperty(propertyNode, "change-period", 0);
489
    const int phase = XML::getProperty(propertyNode, "change-phase", 0);
490
    if (change == "saw" || change == "sawtooth")
491
    {
492
        retval.setFunction(ParticleChangeFunc::FUNC_SAW,
493
            amplitude, period, phase);
494
    }
495
    else if (change == "sine" || change == "sinewave")
496
    {
497
        retval.setFunction(ParticleChangeFunc::FUNC_SINE,
498
            amplitude, period, phase);
499
    }
500
    else if (change == "triangle")
501
    {
502
        retval.setFunction(ParticleChangeFunc::FUNC_TRIANGLE,
503
            amplitude, period, phase);
504
    }
505
    else if (change == "square")
506
    {
507
        retval.setFunction(ParticleChangeFunc::FUNC_SQUARE,
508
            amplitude, period, phase);
509
    }
510
511
    return retval;
512
}
513
514
void ParticleEmitter::createParticles(const int tick,
515
                                      STD_VECTOR<Particle*> &newParticles)
516
{
517
    if (mOutputPauseLeft > 0)
518
    {
519
        mOutputPauseLeft --;
520
        return;
521
    }
522
    mOutputPauseLeft = mOutputPause.value(tick);
523
524
    for (int i = mOutput.value(tick); i > 0; i--)
525
    {
526
        // Limit maximum particles
527
        if (ParticleEngine::particleCount > ParticleEngine::maxCount)
528
            break;
529
530
        Particle *newParticle = nullptr;
531
        if (mParticleImage != nullptr)
532
        {
533
            const std::string &name = mParticleImage->mIdPath;
534
            if (ImageParticle::imageParticleCountByName.find(name) ==
535
                ImageParticle::imageParticleCountByName.end())
536
            {
537
                ImageParticle::imageParticleCountByName[name] = 0;
538
            }
539
540
            if (ImageParticle::imageParticleCountByName[name] > 200)
541
                break;
542
543
            newParticle = new ImageParticle(mParticleImage);
544
            newParticle->setMap(mMap);
545
        }
546
        else if (!mParticleRotation.mFrames.empty())
547
        {
548
            Animation *const newAnimation = new Animation(mParticleRotation);
549
            newParticle = new RotationalParticle(newAnimation);
550
            newParticle->setMap(mMap);
551
        }
552
        else if (!mParticleAnimation.mFrames.empty())
553
        {
554
            Animation *const newAnimation = new Animation(mParticleAnimation);
555
            newParticle = new AnimationParticle(newAnimation);
556
            newParticle->setMap(mMap);
557
        }
558
        else
559
        {
560
            newParticle = new Particle;
561
            newParticle->setMap(mMap);
562
        }
563
564
        const Vector position(mParticlePosX.value(tick),
565
            mParticlePosY.value(tick),
566
            mParticlePosZ.value(tick));
567
        newParticle->moveTo(position);
568
569
        const float angleH = mParticleAngleHorizontal.value(tick);
570
        const float cosAngleH = static_cast<float>(cos(angleH));
571
        const float sinAngleH = static_cast<float>(sin(angleH));
572
        const float angleV = mParticleAngleVertical.value(tick);
573
        const float cosAngleV = static_cast<float>(cos(angleV));
574
        const float sinAngleV = static_cast<float>(sin(angleV));
575
        const float power = mParticlePower.value(tick);
576
        newParticle->setVelocity(cosAngleH * cosAngleV * power,
577
            sinAngleH * cosAngleV * power,
578
            sinAngleV * power);
579
580
        newParticle->setRandomness(mParticleRandomness.value(tick));
581
        newParticle->setGravity(mParticleGravity.value(tick));
582
        newParticle->setBounce(mParticleBounce.value(tick));
583
        newParticle->setFollow(mParticleFollow);
584
585
        newParticle->setDestination(mParticleTarget,
586
            mParticleAcceleration.value(tick),
587
            mParticleMomentum.value(tick));
588
589
        newParticle->setDieDistance(mParticleDieDistance.value(tick));
590
591
        newParticle->setLifetime(mParticleLifetime.value(tick));
592
        newParticle->setFadeOut(mParticleFadeOut.value(tick));
593
        newParticle->setFadeIn(mParticleFadeIn.value(tick));
594
        newParticle->setAlpha(mParticleAlpha.value(tick));
595
596
        FOR_EACH (ParticleEmitterListCIter, it,  mParticleChildEmitters)
597
            newParticle->addEmitter(new ParticleEmitter(*it));
598
599
        if (!mDeathEffect.empty())
600
            newParticle->setDeathEffect(mDeathEffect, mDeathEffectConditions);
601
602
        newParticles.push_back(newParticle);
603
    }
604
}
605
606
void ParticleEmitter::adjustSize(const int w, const int h)
607
{
608
    if (w == 0 || h == 0)
609
        return;  // new dimensions are illegal
610
611
    // calculate the old rectangle
612
    const int oldArea = CAST_S32(
613
        mParticlePosX.maxVal - mParticlePosX.minVal) * CAST_S32(
614
        mParticlePosX.maxVal - mParticlePosY.minVal);
615
    if (oldArea == 0)
616
    {
617
        // when the effect has no dimension it is
618
        // not designed to be resizeable
619
        return;
620
    }
621
622
    // set the new dimensions
623
    mParticlePosX.set(0, static_cast<float>(w));
624
    mParticlePosY.set(0, static_cast<float>(h));
625
    const int newArea = w * h;
626
    // adjust the output so that the particle density stays the same
627
    const float outputFactor = static_cast<float>(newArea)
628
        / static_cast<float>(oldArea);
629
    mOutput.minVal = CAST_S32(static_cast<float>(
630
        mOutput.minVal) * outputFactor);
631
    mOutput.maxVal = CAST_S32(static_cast<float>(
632
        mOutput.maxVal) * outputFactor);
633
2
}