/* * The Mana Client * Copyright (C) 2006-2009 The Mana World Development Team * Copyright (C) 2009-2012 The Mana Developers * * This file is part of The Mana 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 "animationparticle.h" #include "imageparticle.h" #include "log.h" #include "map.h" #include "particle.h" #include "particleemitter.h" #include "rotationalparticle.h" #include "resources/dye.h" #include "resources/image.h" #include "resources/imageset.h" #include "resources/resourcemanager.h" #include "utils/stringutils.h" #include #define SIN45 0.707106781f #define DEG_RAD_FACTOR 0.017453293f ParticleEmitter::ParticleEmitter(xmlNodePtr emitterNode, Particle *target, Map *map, int rotation, const std::string &dyePalettes) { mMap = map; mParticleTarget = target; // Initializing default values mParticlePosX.set(0.0f); mParticlePosY.set(0.0f); mParticlePosZ.set(0.0f); mParticleAngleHorizontal.set(0.0f); mParticleAngleVertical.set(0.0f); mParticlePower.set(0.0f); mParticleGravity.set(0.0f); mParticleRandomness.set(0); mParticleBounce.set(0.0f); mParticleFollow = false; mParticleAcceleration.set(0.0f); mParticleDieDistance.set(-1.0f); mParticleMomentum.set(1.0f); mParticleLifetime.set(-1); mParticleFadeOut.set(0); mParticleFadeIn.set(0); mOutput.set(1); mOutputPause.set(0); mParticleAlpha.set(1.0f); for (auto propertyNode : XML::Children(emitterNode)) { if (xmlStrEqual(propertyNode->name, BAD_CAST "property")) { std::string name = XML::getProperty(propertyNode, "name", ""); if (name == "position-x") { mParticlePosX = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "position-y") { mParticlePosY = readParticleEmitterProp(propertyNode, 0.0f); mParticlePosY.minVal *= SIN45; mParticlePosY.maxVal *= SIN45; mParticlePosY.changeAmplitude *= SIN45; } else if (name == "position-z") { mParticlePosZ = readParticleEmitterProp(propertyNode, 0.0f); mParticlePosZ.minVal *= SIN45; mParticlePosZ.maxVal *= SIN45; mParticlePosZ.changeAmplitude *= SIN45; } else if (name == "image") { std::string image = XML::getProperty(propertyNode, "value", ""); // Don't leak when multiple images are defined if (!image.empty() && !mParticleImage) { if (!dyePalettes.empty()) Dye::instantiate(image, dyePalettes); ResourceManager *resman = ResourceManager::getInstance(); mParticleImage = resman->getImageRef(image); } } else if (name == "horizontal-angle") { mParticleAngleHorizontal = readParticleEmitterProp(propertyNode, 0.0f); mParticleAngleHorizontal.minVal += rotation; mParticleAngleHorizontal.minVal *= DEG_RAD_FACTOR; mParticleAngleHorizontal.maxVal += rotation; mParticleAngleHorizontal.maxVal *= DEG_RAD_FACTOR; mParticleAngleHorizontal.changeAmplitude *= DEG_RAD_FACTOR; } else if (name == "vertical-angle") { mParticleAngleVertical = readParticleEmitterProp(propertyNode, 0.0f); mParticleAngleVertical.minVal *= DEG_RAD_FACTOR; mParticleAngleVertical.maxVal *= DEG_RAD_FACTOR; mParticleAngleVertical.changeAmplitude *= DEG_RAD_FACTOR; } else if (name == "power") { mParticlePower = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "gravity") { mParticleGravity = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "randomnes" || name == "randomness") // legacy bug { mParticleRandomness = readParticleEmitterProp(propertyNode, 0); } else if (name == "bounce") { mParticleBounce = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "lifetime") { mParticleLifetime = readParticleEmitterProp(propertyNode, 0); mParticleLifetime.minVal += 1; } else if (name == "output") { mOutput = readParticleEmitterProp(propertyNode, 0); mOutput.maxVal +=1; } else if (name == "output-pause") { mOutputPause = readParticleEmitterProp(propertyNode, 0); mOutputPauseLeft = mOutputPause.value(0); } else if (name == "acceleration") { mParticleAcceleration = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "die-distance") { mParticleDieDistance = readParticleEmitterProp(propertyNode, 0.0f); } else if (name == "momentum") { mParticleMomentum = readParticleEmitterProp(propertyNode, 1.0f); } else if (name == "fade-out") { mParticleFadeOut = readParticleEmitterProp(propertyNode, 0); } else if (name == "fade-in") { mParticleFadeIn = readParticleEmitterProp(propertyNode, 0); } else if (name == "alpha") { mParticleAlpha = readParticleEmitterProp(propertyNode, 1.0f); } else if (name == "follow-parent") { mParticleFollow = true; } else { logger->log("Particle Engine: Warning, unknown emitter property \"%s\"", name.c_str() ); } } else if (xmlStrEqual(propertyNode->name, BAD_CAST "emitter")) { ParticleEmitter newEmitter(propertyNode, mParticleTarget, map, rotation, dyePalettes); mParticleChildEmitters.push_back(newEmitter); } else if (xmlStrEqual(propertyNode->name, BAD_CAST "rotation")) { ImageSet *imageset = ResourceManager::getInstance()->getImageSet( XML::getProperty(propertyNode, "imageset", ""), XML::getProperty(propertyNode, "width", 0), XML::getProperty(propertyNode, "height", 0) ); // Get animation frames for (auto frameNode : XML::Children(propertyNode)) { int delay = XML::getProperty(frameNode, "delay", 0); int offsetX = XML::getProperty(frameNode, "offsetX", 0); int offsetY = XML::getProperty(frameNode, "offsetY", 0); if (mMap) { offsetX -= imageset->getWidth() / 2 - mMap->getTileWidth() / 2; offsetY -= imageset->getHeight() - mMap->getTileHeight(); } if (xmlStrEqual(frameNode->name, BAD_CAST "frame")) { int index = XML::getProperty(frameNode, "index", -1); if (index < 0) { logger->log("No valid value for 'index'"); continue; } Image *img = imageset->get(index); if (!img) { logger->log("No image at index %d", index); continue; } mParticleRotation.addFrame(img, delay, offsetX, offsetY); } else if (xmlStrEqual(frameNode->name, BAD_CAST "sequence")) { int start = XML::getProperty(frameNode, "start", -1); int end = XML::getProperty(frameNode, "end", -1); if (start < 0 || end < 0) { logger->log("No valid value for 'start' or 'end'"); continue; } while (end >= start) { Image *img = imageset->get(start); if (!img) { logger->log("No image at index %d", start); continue; } mParticleRotation.addFrame(img, delay, offsetX, offsetY); start++; } } else if (xmlStrEqual(frameNode->name, BAD_CAST "end")) { mParticleRotation.addTerminator(); } } // for frameNode } else if (xmlStrEqual(propertyNode->name, BAD_CAST "animation")) { std::string imagesetPath = XML::getProperty(propertyNode, "imageset", ""); ImageSet *imageset = ResourceManager::getInstance()->getImageSet( imagesetPath, XML::getProperty(propertyNode, "width", 0), XML::getProperty(propertyNode, "height", 0) ); if (!imageset) logger->error(strprintf("Failed to load \"%s\"", imagesetPath.c_str())); // Get animation frames for (auto frameNode : XML::Children(propertyNode)) { int delay = XML::getProperty(frameNode, "delay", 0); int offsetX = XML::getProperty(frameNode, "offsetX", 0); int offsetY = XML::getProperty(frameNode, "offsetY", 0); offsetY -= imageset->getHeight() - 32; offsetX -= imageset->getWidth() / 2 - 16; if (xmlStrEqual(frameNode->name, BAD_CAST "frame")) { int index = XML::getProperty(frameNode, "index", -1); if (index < 0) { logger->log("No valid value for 'index'"); continue; } Image *img = imageset->get(index); if (!img) { logger->log("No image at index %d", index); continue; } mParticleAnimation.addFrame(img, delay, offsetX, offsetY); } else if (xmlStrEqual(frameNode->name, BAD_CAST "sequence")) { int start = XML::getProperty(frameNode, "start", -1); int end = XML::getProperty(frameNode, "end", -1); if (start < 0 || end < 0) { logger->log("No valid value for 'start' or 'end'"); continue; } while (end >= start) { Image *img = imageset->get(start); if (!img) { logger->log("No image at index %d", start); continue; } mParticleAnimation.addFrame(img, delay, offsetX, offsetY); start++; } } else if (xmlStrEqual(frameNode->name, BAD_CAST "end")) { mParticleAnimation.addTerminator(); } } // for frameNode } else if (xmlStrEqual(propertyNode->name, BAD_CAST "deatheffect")) { mDeathEffect = (const char*)propertyNode->children->content; mDeathEffectConditions = 0x00; if (XML::getBoolProperty(propertyNode, "on-floor", true)) { mDeathEffectConditions += Particle::DEAD_FLOOR; } if (XML::getBoolProperty(propertyNode, "on-sky", true)) { mDeathEffectConditions += Particle::DEAD_SKY; } if (XML::getBoolProperty(propertyNode, "on-other", false)) { mDeathEffectConditions += Particle::DEAD_OTHER; } if (XML::getBoolProperty(propertyNode, "on-impact", true)) { mDeathEffectConditions += Particle::DEAD_IMPACT; } if (XML::getBoolProperty(propertyNode, "on-timeout", true)) { mDeathEffectConditions += Particle::DEAD_TIMEOUT; } } } } ParticleEmitter::ParticleEmitter(const ParticleEmitter &o) { *this = o; } ParticleEmitter & ParticleEmitter::operator=(const ParticleEmitter &o) { mParticlePosX = o.mParticlePosX; mParticlePosY = o.mParticlePosY; mParticlePosZ = o.mParticlePosZ; mParticleAngleHorizontal = o.mParticleAngleHorizontal; mParticleAngleVertical = o.mParticleAngleVertical; mParticlePower = o.mParticlePower; mParticleGravity = o.mParticleGravity; mParticleRandomness = o.mParticleRandomness; mParticleBounce = o.mParticleBounce; mParticleFollow = o.mParticleFollow; mParticleTarget = o.mParticleTarget; mParticleAcceleration = o.mParticleAcceleration; mParticleDieDistance = o.mParticleDieDistance; mParticleMomentum = o.mParticleMomentum; mParticleLifetime = o.mParticleLifetime; mParticleFadeOut = o.mParticleFadeOut; mParticleFadeIn = o.mParticleFadeIn; mParticleAlpha = o.mParticleAlpha; mMap = o.mMap; mOutput = o.mOutput; mOutputPause = o.mOutputPause; mParticleImage = o.mParticleImage; mParticleAnimation = o.mParticleAnimation; mParticleRotation = o.mParticleRotation; mParticleChildEmitters = o.mParticleChildEmitters; mOutputPauseLeft = 0; return *this; } ParticleEmitter::~ParticleEmitter() = default; template ParticleEmitterProp ParticleEmitter::readParticleEmitterProp(xmlNodePtr propertyNode, T def) { ParticleEmitterProp retval; def = (T) XML::getFloatProperty(propertyNode, "value", (double) def); retval.set((T) XML::getFloatProperty(propertyNode, "min", (double) def), (T) XML::getFloatProperty(propertyNode, "max", (double) def)); std::string change = XML::getProperty(propertyNode, "change-func", "none"); T amplitude = (T) XML::getFloatProperty(propertyNode, "change-amplitude", 0.0); int period = XML::getProperty(propertyNode, "change-period", 0); int phase = XML::getProperty(propertyNode, "change-phase", 0); if (change == "saw" || change == "sawtooth") retval.setFunction(FUNC_SAW, amplitude, period, phase); else if (change == "sine" || change == "sinewave") retval.setFunction(FUNC_SINE, amplitude, period, phase); else if (change == "triangle") retval.setFunction(FUNC_TRIANGLE, amplitude, period, phase); else if (change == "square") retval.setFunction(FUNC_SQUARE, amplitude, period, phase); return retval; } std::list ParticleEmitter::createParticles(int tick) { std::list newParticles; if (mOutputPauseLeft > 0) { mOutputPauseLeft--; return newParticles; } mOutputPauseLeft = mOutputPause.value(tick); for (int i = mOutput.value(tick); i > 0; i--) { // Limit maximum particles if (Particle::particleCount > Particle::maxCount) break; Particle *newParticle; if (mParticleImage) { newParticle = new ImageParticle(mMap, mParticleImage); } else if (mParticleRotation.getLength() > 0) { newParticle = new RotationalParticle(mMap, mParticleRotation); } else if (mParticleAnimation.getLength() > 0) { newParticle = new AnimationParticle(mMap, mParticleAnimation); } else { newParticle = new Particle(mMap); } Vector position(mParticlePosX.value(tick), mParticlePosY.value(tick), mParticlePosZ.value(tick)); newParticle->moveTo(position); float angleH = mParticleAngleHorizontal.value(tick); float angleV = mParticleAngleVertical.value(tick); float power = mParticlePower.value(tick); newParticle->setVelocity( cos(angleH) * cos(angleV) * power, sin(angleH) * cos(angleV) * power, sin(angleV) * power); newParticle->setRandomness(mParticleRandomness.value(tick)); newParticle->setGravity(mParticleGravity.value(tick)); newParticle->setBounce(mParticleBounce.value(tick)); newParticle->setFollow(mParticleFollow); newParticle->setDestination(mParticleTarget, mParticleAcceleration.value(tick), mParticleMomentum.value(tick) ); newParticle->setDieDistance(mParticleDieDistance.value(tick)); newParticle->setLifetime(mParticleLifetime.value(tick)); newParticle->setFadeOut(mParticleFadeOut.value(tick)); newParticle->setFadeIn(mParticleFadeIn.value(tick)); newParticle->setAlpha(mParticleAlpha.value(tick)); for (auto &particleChildEmitter : mParticleChildEmitters) { newParticle->addEmitter(new ParticleEmitter(particleChildEmitter)); } if (!mDeathEffect.empty()) { newParticle->setDeathEffect(mDeathEffect, mDeathEffectConditions); } newParticles.push_back(newParticle); } return newParticles; } void ParticleEmitter::adjustSize(int w, int h) { if (w == 0 || h == 0) return; // new dimensions are illegal // calculate the old rectangle int oldWidth = mParticlePosX.maxVal - mParticlePosX.minVal; int oldHeight = mParticlePosX.maxVal - mParticlePosY.minVal; int oldArea = oldWidth * oldHeight; if (oldArea == 0) { //when the effect has no dimension it is //not designed to be resizeable return; } // set the new dimensions mParticlePosX.set(0, w); mParticlePosY.set(0, h); int newArea = w * h; // adjust the output so that the particle density stays the same float outputFactor = (float)newArea / (float)oldArea; mOutput.minVal *= outputFactor; mOutput.maxVal *= outputFactor; }