/*
* The ManaPlus Client
* Copyright (C) 2006-2009 The Mana World Development Team
* Copyright (C) 2009-2010 The Mana Developers
* Copyright (C) 2011-2020 The ManaPlus Developers
* Copyright (C) 2020-2023 The ManaVerse Developers
*
* This file is part of The ManaPlus Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "particle/particle.h"
#include "actormanager.h"
#include "logger.h"
#include "being/actorsprite.h"
#include "particle/animationparticle.h"
#include "particle/particleemitter.h"
#include "particle/rotationalparticle.h"
#include "resources/animation/simpleanimation.h"
#include "resources/dye/dye.h"
#include "resources/image/image.h"
#include "resources/loaders/imageloader.h"
#include "resources/loaders/xmlloader.h"
#include "utils/delete2.h"
#include "utils/dtor.h"
#include "utils/foreach.h"
#include "utils/likely.h"
#include "utils/mathutils.h"
#include "utils/mrand.h"
#include "debug.h"
static const float SIN45 = 0.707106781F;
static const double PI = M_PI;
static const float PI2 = 2 * M_PI;
class Graphics;
Particle::Particle() :
Actor(),
mAlpha(1.0F),
mLifetimeLeft(-1),
mLifetimePast(0),
mFadeOut(0),
mFadeIn(0),
mVelocity(),
mAlive(AliveStatus::ALIVE),
mType(ParticleType::Normal),
mAnimation(nullptr),
mImage(nullptr),
mActor(BeingId_zero),
mChildEmitters(),
mChildParticles(),
mChildMoveParticles(),
mDeathEffect(),
mGravity(0.0F),
mBounce(0.0F),
mAcceleration(0.0F),
mInvDieDistance(-1.0F),
mMomentum(1.0F),
mTarget(nullptr),
mRandomness(0),
mDeathEffectConditions(0x00),
mAutoDelete(true),
mAllowSizeAdjust(false),
mFollow(false)
{
ParticleEngine::particleCount++;
}
Particle::~Particle()
{
if (mActor != BeingId_zero &&
(actorManager != nullptr))
{
ActorSprite *const actor = actorManager->findActor(mActor);
if (actor != nullptr)
actor->controlParticleDeleted(this);
}
// Delete child emitters and child particles
clear();
delete2(mAnimation)
if (mImage != nullptr)
{
if (mType == ParticleType::Image)
{
const std::string &restrict name = mImage->mIdPath;
StringIntMapIter it
= ImageParticle::imageParticleCountByName.find(name);
if (it != ImageParticle::imageParticleCountByName.end())
{
int &cnt = (*it).second;
if (cnt > 0)
cnt --;
}
mImage->decRef();
}
mImage = nullptr;
}
ParticleEngine::particleCount--;
}
void Particle::draw(Graphics *restrict const graphics A_UNUSED,
const int offsetX A_UNUSED,
const int offsetY A_UNUSED) const restrict2
{
}
void Particle::updateSelf() restrict2
{
// calculate particle movement
if (A_LIKELY(mMomentum != 1.0F))
mVelocity *= mMomentum;
if ((mTarget != nullptr) && mAcceleration != 0.0F)
{
Vector dist = mPos - mTarget->mPos;
dist.x *= SIN45;
float invHypotenuse;
switch (ParticleEngine::fastPhysics)
{
case ParticlePhysics::Normal:
invHypotenuse = fastInvSqrt(
dist.x * dist.x + dist.y * dist.y + dist.z * dist.z);
break;
case ParticlePhysics::Fast:
if (dist.x == 0.0F)
{
invHypotenuse = 0;
break;
}
invHypotenuse = 2.0F / (static_cast(fabs(dist.x))
+ static_cast(fabs(dist.y))
+ static_cast(fabs(dist.z)));
break;
case ParticlePhysics::Best:
default:
invHypotenuse = 1.0F / static_cast(sqrt(
dist.x * dist.x + dist.y * dist.y + dist.z * dist.z));
break;
}
if (invHypotenuse != 0.0F)
{
if (mInvDieDistance > 0.0F && invHypotenuse > mInvDieDistance)
mAlive = AliveStatus::DEAD_IMPACT;
const float accFactor = invHypotenuse * mAcceleration;
mVelocity -= dist * accFactor;
}
}
if (A_LIKELY(mRandomness >= 10)) // reduce useless calculations
{
const int rand2 = mRandomness * 2;
mVelocity.x += static_cast(mrand() % rand2 - mRandomness)
/ 1000.0F;
mVelocity.y += static_cast(mrand() % rand2 - mRandomness)
/ 1000.0F;
mVelocity.z += static_cast(mrand() % rand2 - mRandomness)
/ 1000.0F;
}
mVelocity.z -= mGravity;
// Update position
mPos.x += mVelocity.x;
mPos.y += mVelocity.y * SIN45;
mPos.z += mVelocity.z * SIN45;
// Update other stuff
if (A_LIKELY(mLifetimeLeft > 0))
mLifetimeLeft--;
mLifetimePast++;
if (mPos.z < 0.0F)
{
if (mBounce > 0.0F)
{
mPos.z *= -mBounce;
mVelocity *= mBounce;
mVelocity.z = -mVelocity.z;
}
else
{
mAlive = AliveStatus::DEAD_FLOOR;
}
}
else if (mPos.z > ParticleEngine::PARTICLE_SKY)
{
mAlive = AliveStatus::DEAD_SKY;
}
// Update child emitters
if ((ParticleEngine::emitterSkip != 0) &&
(mLifetimePast - 1) % ParticleEngine::emitterSkip == 0)
{
FOR_EACH (EmitterConstIterator, e, mChildEmitters)
{
STD_VECTOR newParticles;
(*e)->createParticles(mLifetimePast, newParticles);
FOR_EACH (STD_VECTOR::const_iterator,
it,
newParticles)
{
Particle *const p = *it;
p->moveBy(mPos);
mChildParticles.push_back(p);
if (p->mFollow)
mChildMoveParticles.push_back(p);
}
}
}
// create death effect when the particle died
if (A_UNLIKELY(mAlive != AliveStatus::ALIVE &&
mAlive != AliveStatus::DEAD_LONG_AGO))
{
if ((CAST_U32(mAlive) & mDeathEffectConditions)
> 0x00 && !mDeathEffect.empty())
{
Particle *restrict const deathEffect = particleEngine->addEffect(
mDeathEffect, 0, 0, 0);
if (deathEffect != nullptr)
deathEffect->moveBy(mPos);
}
mAlive = AliveStatus::DEAD_LONG_AGO;
}
}
bool Particle::update() restrict2
{
if (A_LIKELY(mAlive == AliveStatus::ALIVE))
{
if (A_UNLIKELY(mLifetimeLeft == 0))
{
mAlive = AliveStatus::DEAD_TIMEOUT;
if (mChildParticles.empty())
{
if (mAutoDelete)
return false;
return true;
}
}
else
{
if (mAnimation != nullptr)
{
if (mType == ParticleType::Animation)
{
// particle engine is updated every 10ms
mAnimation->update(10);
}
else // ParticleType::Rotational
{
// TODO: cache velocities to avoid spamming atan2()
const int size = mAnimation->getLength();
if (size == 0)
return false;
float rad = static_cast(atan2(mVelocity.x,
mVelocity.y));
if (rad < 0)
rad = PI2 + rad;
const float range = static_cast(PI / size);
// Determines which frame the particle should play
if (A_UNLIKELY(rad < range || rad > PI2 - range))
{
mAnimation->setFrame(0);
}
else
{
const float range2 = 2 * range;
// +++ need move condition outside of for
for (int c = 1; c < size; c++)
{
const float cRange = static_cast(c) *
range2;
if (cRange - range < rad &&
rad < cRange + range)
{
mAnimation->setFrame(c);
break;
}
}
}
}
mImage = mAnimation->getCurrentImage();
}
const Vector oldPos = mPos;
updateSelf();
const Vector change = mPos - oldPos;
if (mChildParticles.empty())
{
if (mAlive != AliveStatus::ALIVE &&
mAutoDelete)
{
return false;
}
return true;
}
for (ParticleIterator p = mChildMoveParticles.begin(),
fp2 = mChildMoveParticles.end(); p != fp2; )
{
// move particle with its parent if desired
(*p)->moveBy(change);
++p;
}
}
// Update child particles
for (ParticleIterator p = mChildParticles.begin(),
fp2 = mChildParticles.end(); p != fp2; )
{
Particle *restrict const particle = *p;
// update particle
if (A_LIKELY(particle->update()))
{
++p;
}
else
{
mChildMoveParticles.remove(*p);
delete particle;
p = mChildParticles.erase(p);
}
}
if (A_UNLIKELY(mAlive != AliveStatus::ALIVE &&
mChildParticles.empty() &&
mAutoDelete))
{
return false;
}
}
else
{
if (mChildParticles.empty())
{
if (mAutoDelete)
return false;
return true;
}
// Update child particles
for (ParticleIterator p = mChildParticles.begin(),
fp2 = mChildParticles.end(); p != fp2; )
{
Particle *restrict const particle = *p;
// update particle
if (A_LIKELY(particle->update()))
{
++p;
}
else
{
mChildMoveParticles.remove(*p);
delete particle;
p = mChildParticles.erase(p);
}
}
if (A_UNLIKELY(mChildParticles.empty() &&
mAutoDelete))
{
return false;
}
}
return true;
}
void Particle::moveBy(const Vector &restrict change) restrict2
{
mPos += change;
FOR_EACH (ParticleConstIterator, p, mChildMoveParticles)
{
(*p)->moveBy(change);
}
}
void Particle::moveTo(const float x, const float y) restrict2
{
moveTo(Vector(x, y, mPos.z));
}
Particle *Particle::addEffect(const std::string &restrict particleEffectFile,
const int pixelX, const int pixelY,
const int rotation) restrict2
{
Particle *newParticle = nullptr;
const size_t pos = particleEffectFile.find('|');
const std::string dyePalettes = (pos != std::string::npos)
? particleEffectFile.substr(pos + 1) : "";
XML::Document *doc = Loader::getXml(particleEffectFile.substr(0, pos),
UseVirtFs_true,
SkipError_false);
if (doc == nullptr)
return nullptr;
XmlNodeConstPtrConst rootNode = doc->rootNode();
if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "effect"))
{
logger->log("Error loading particle: %s", particleEffectFile.c_str());
doc->decRef();
return nullptr;
}
// Parse particles
for_each_xml_child_node(effectChildNode, rootNode)
{
// We're only interested in particles
if (!xmlNameEqual(effectChildNode, "particle"))
continue;
// Determine the exact particle type
XmlNodePtr node;
// Animation
if ((node = XML::findFirstChildByName(effectChildNode, "animation")) !=
nullptr)
{
newParticle = new AnimationParticle(node, dyePalettes);
newParticle->setMap(mMap);
}
// Rotational
else if ((node = XML::findFirstChildByName(
effectChildNode, "rotation")) != nullptr)
{
newParticle = new RotationalParticle(node, dyePalettes);
newParticle->setMap(mMap);
}
// Image
else if ((node = XML::findFirstChildByName(effectChildNode,
"image")) != nullptr)
{
std::string imageSrc;
if (XmlHaveChildContent(node))
imageSrc = XmlChildContent(node);
if (!imageSrc.empty() && !dyePalettes.empty())
Dye::instantiate(imageSrc, dyePalettes);
Image *const img = Loader::getImage(imageSrc);
newParticle = new ImageParticle(img);
newParticle->setMap(mMap);
}
// Other
else
{
newParticle = new Particle;
newParticle->setMap(mMap);
}
// Read and set the basic properties of the particle
const float offsetX = XML::getFloatProperty(
effectChildNode, "position-x", 0);
const float offsetY = XML::getFloatProperty(
effectChildNode, "position-y", 0);
const float offsetZ = XML::getFloatProperty(
effectChildNode, "position-z", 0);
const Vector position(mPos.x + static_cast(pixelX) + offsetX,
mPos.y + static_cast(pixelY) + offsetY,
mPos.z + offsetZ);
newParticle->moveTo(position);
const int lifetime = XML::getProperty(effectChildNode, "lifetime", -1);
newParticle->setLifetime(lifetime);
const bool resizeable = "false" != XML::getProperty(effectChildNode,
"size-adjustable", "false");
newParticle->setAllowSizeAdjust(resizeable);
// Look for additional emitters for this particle
for_each_xml_child_node(emitterNode, effectChildNode)
{
if (xmlNameEqual(emitterNode, "emitter"))
{
ParticleEmitter *restrict const newEmitter =
new ParticleEmitter(
emitterNode,
newParticle,
mMap,
rotation,
dyePalettes);
newParticle->addEmitter(newEmitter);
}
else if (xmlNameEqual(emitterNode, "deatheffect"))
{
std::string deathEffect;
if ((node != nullptr) && XmlHaveChildContent(node))
deathEffect = XmlChildContent(emitterNode);
char deathEffectConditions = 0x00;
if (XML::getBoolProperty(emitterNode, "on-floor", true))
{
deathEffectConditions += CAST_S8(
AliveStatus::DEAD_FLOOR);
}
if (XML::getBoolProperty(emitterNode, "on-sky", true))
{
deathEffectConditions += CAST_S8(
AliveStatus::DEAD_SKY);
}
if (XML::getBoolProperty(emitterNode, "on-other", false))
{
deathEffectConditions += CAST_S8(
AliveStatus::DEAD_OTHER);
}
if (XML::getBoolProperty(emitterNode, "on-impact", true))
{
deathEffectConditions += CAST_S8(
AliveStatus::DEAD_IMPACT);
}
if (XML::getBoolProperty(emitterNode, "on-timeout", true))
{
deathEffectConditions += CAST_S8(
AliveStatus::DEAD_TIMEOUT);
}
newParticle->setDeathEffect(
deathEffect, deathEffectConditions);
}
}
mChildParticles.push_back(newParticle);
}
doc->decRef();
return newParticle;
}
void Particle::adjustEmitterSize(const int w, const int h) restrict2
{
if (mAllowSizeAdjust)
{
FOR_EACH (EmitterConstIterator, e, mChildEmitters)
(*e)->adjustSize(w, h);
}
}
void Particle::prepareToDie() restrict2
{
FOR_EACH (ParticleIterator, p, mChildParticles)
{
Particle *restrict const particle = *p;
if (particle == nullptr)
continue;
particle->prepareToDie();
if (particle->isAlive() &&
particle->mLifetimeLeft == -1 &&
particle->mAutoDelete)
{
particle->kill();
}
}
}
void Particle::clear() restrict2
{
delete_all(mChildEmitters);
mChildEmitters.clear();
delete_all(mChildParticles);
mChildParticles.clear();
mChildMoveParticles.clear();
}