/* * The Mana Client * Copyright (C) 2010-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 "actorsprite.h" #include "client.h" #include "configuration.h" #include "event.h" #include "imagesprite.h" #include "localplayer.h" #include "log.h" #include "simpleanimation.h" #include "sound.h" #include "statuseffect.h" #include "net/net.h" #include "resources/image.h" #include "resources/imageset.h" #include "resources/resourcemanager.h" #include "resources/theme.h" #include #define EFFECTS_FILE "effects.xml" ImageSet *ActorSprite::targetCursorImages[2][NUM_TC]; SimpleAnimation *ActorSprite::targetCursor[2][NUM_TC]; bool ActorSprite::loaded = false; ActorSprite::ActorSprite(int id): mId(id), mStatusParticleEffects(&mStunParticleEffects, false), mChildParticleEffects(&mStatusParticleEffects, false) {} ActorSprite::~ActorSprite() { setMap(nullptr); mUsedTargetCursor = nullptr; // Notify listeners of the destruction. Event event(Event::Destroyed); event.setActor("source", this); event.trigger(Event::ActorSpriteChannel); } int ActorSprite::getDrawOrder() const { int drawOrder = Actor::getDrawOrder(); // See note at ActorSprite::draw if (mMap) drawOrder += mMap->getTileHeight() / 2; return drawOrder; } bool ActorSprite::draw(Graphics *graphics, int offsetX, int offsetY) const { int px = getPixelX() + offsetX; int py = getPixelY() + offsetY; if (mUsedTargetCursor) { mUsedTargetCursor->reset(); mUsedTargetCursor->update(tick_time * MILLISECONDS_IN_A_TICK); mUsedTargetCursor->draw(graphics, px, py); } // This is makes sure that actors positioned on the center of a tile have // their sprite aligned to the bottom of that tile, mainly to maintain // compatibility with older clients. if (mMap) py += mMap->getTileHeight() / 2; return drawSpriteAt(graphics, px, py); } bool ActorSprite::drawSpriteAt(Graphics *graphics, int x, int y) const { return CompoundSprite::draw(graphics, x, y); } void ActorSprite::logic() { // Update sprite animations update(tick_time * MILLISECONDS_IN_A_TICK); // Restart status/particle effects, if needed if (mMustResetParticles) { mMustResetParticles = false; for (int statusEffect : mStatusEffects) { const StatusEffect *effect = StatusEffect::getStatusEffect(statusEffect, true); if (effect && effect->particleEffectIsPersistent()) updateStatusEffect(statusEffect, true); } } // See note at ActorSprite::draw float py = mPos.y; if (mMap) py += mMap->getTileHeight() / 2; // Update particle effects mChildParticleEffects.moveTo(mPos.x, py); } void ActorSprite::actorLogic() { } void ActorSprite::setMap(Map* map) { Actor::setMap(map); // Clear particle effect list because child particles became invalid mChildParticleEffects.clear(); mMustResetParticles = true; // Reset status particles on next redraw } void ActorSprite::controlParticle(Particle *particle) { mChildParticleEffects.addLocally(particle); } void ActorSprite::setTargetType(TargetCursorType type) { if (type == TCT_NONE) untarget(); else mUsedTargetCursor = targetCursor[type][getTargetCursorSize()]; } struct EffectDescription { std::string mGFXEffect; std::string mSFXEffect; }; static EffectDescription *default_effect = nullptr; static std::map effects; static bool effects_initialized = false; static EffectDescription *getEffectDescription(xmlNodePtr node, int *id) { auto *ed = new EffectDescription; *id = atoi(XML::getProperty(node, "id", "-1").c_str()); ed->mSFXEffect = XML::getProperty(node, "audio", ""); ed->mGFXEffect = XML::getProperty(node, "particle", ""); return ed; } static EffectDescription *getEffectDescription(int effectId) { if (!effects_initialized) { XML::Document doc(EFFECTS_FILE); xmlNodePtr root = doc.rootNode(); if (!root || !xmlStrEqual(root->name, BAD_CAST "effects")) { logger->log("Error loading being effects file: " EFFECTS_FILE); return nullptr; } for_each_xml_child_node(node, root) { int id; if (xmlStrEqual(node->name, BAD_CAST "effect")) { EffectDescription *EffectDescription = getEffectDescription(node, &id); effects[id] = EffectDescription; } else if (xmlStrEqual(node->name, BAD_CAST "default")) { EffectDescription *effectDescription = getEffectDescription(node, &id); delete default_effect; default_effect = effectDescription; } } effects_initialized = true; } // done initializing EffectDescription *ed = effects[effectId]; return ed ? ed : default_effect; } void ActorSprite::setStatusEffect(int index, bool active) { const bool wasActive = mStatusEffects.find(index) != mStatusEffects.end(); if (active != wasActive) { updateStatusEffect(index, active); if (active) mStatusEffects.insert(index); else mStatusEffects.erase(index); } } void ActorSprite::setStatusEffectBlock(int offset, uint16_t newEffects) { for (int i = 0; i < STATUS_EFFECTS; i++) { int index = StatusEffect::blockEffectIndexToEffectIndex(offset + i); if (index != -1) setStatusEffect(index, (newEffects & (1 << i)) > 0); } } void ActorSprite::internalTriggerEffect(int effectId, bool sfx, bool gfx) { logger->log("Special effect #%d on %s", effectId, getId() == local_player->getId() ? "self" : "other"); EffectDescription *ed = getEffectDescription(effectId); if (!ed) { logger->log("Unknown special effect and no default recorded"); return; } if (gfx && !ed->mGFXEffect.empty()) { Particle *selfFX; selfFX = particleEngine->addEffect(ed->mGFXEffect, 0, 0); controlParticle(selfFX); } if (sfx && !ed->mSFXEffect.empty()) sound.playSfx(ed->mSFXEffect); } void ActorSprite::updateStunMode(int oldMode, int newMode) { if (this == local_player) { Event event(Event::Stun); event.setInt("oldMode", oldMode); event.setInt("newMode", newMode); event.trigger(Event::ActorSpriteChannel); } handleStatusEffect(StatusEffect::getStatusEffect(oldMode, false), -1); handleStatusEffect(StatusEffect::getStatusEffect(newMode, true), -1); } void ActorSprite::updateStatusEffect(int index, bool newStatus) { if (this == local_player) { Event event(Event::UpdateStatusEffect); event.setInt("index", index); event.setBool("newStatus", newStatus); event.trigger(Event::ActorSpriteChannel); } handleStatusEffect(StatusEffect::getStatusEffect(index, newStatus), index); } void ActorSprite::handleStatusEffect(StatusEffect *effect, int effectId) { if (!effect) return; // TODO: Find out how this is meant to be used // (SpriteAction != Being::Action) //SpriteAction action = effect->getAction(); //if (action != ACTION_INVALID) // setAction(action); Particle *particle = effect->getParticle(); if (effectId >= 0) { mStatusParticleEffects.setLocally(effectId, particle); } else { mStunParticleEffects.clearLocally(); if (particle) mStunParticleEffects.addLocally(particle); } } void ActorSprite::setupSpriteDisplay(const SpriteDisplay &display, bool forceDisplay) { clear(); for (const auto &sprite : display.sprites) { std::string file = paths.getStringValue("sprites") + sprite.sprite; int variant = sprite.variant; addSprite(AnimatedSprite::load(file, variant)); } // Ensure that something is shown, if desired if (size() == 0 && forceDisplay) { if (display.image.empty()) { addSprite(AnimatedSprite::load(paths.getStringValue("sprites") + paths.getStringValue("spriteErrorFile"))); } else { ResourceManager *resman = ResourceManager::getInstance(); std::string imagePath = paths.getStringValue("itemIcons") + display.image; Image *img = resman->getImage(imagePath); if (!img) img = Theme::getImageFromTheme( paths.getStringValue("unknownItemFile")); addSprite(new ImageSprite(img)); } } mChildParticleEffects.clear(); //setup particle effects if (Particle::enabled) { for (const auto &particle : display.particles) { Particle *p = particleEngine->addEffect(particle, 0, 0); controlParticle(p); } } mMustResetParticles = true; } void ActorSprite::load() { if (loaded) unload(); initTargetCursor(); loaded = true; } void ActorSprite::unload() { if (!loaded) return; cleanupTargetCursors(); loaded = false; } void ActorSprite::initTargetCursor() { static const std::string targetCursor = "graphics/target-cursor-%s-%s.png"; static const char * const cursorTypeStr[NUM_TCT] = { "normal", "in-range" }; static const int targetWidths[NUM_TC] = { 44, 62, 82 }; static const int targetHeights[NUM_TC] = { 35, 44, 60 }; static const char * const cursorSizeStr[NUM_TC] = { "s", "m", "l" }; // Load target cursors for (int size = 0; size < NUM_TC; size++) { for (int type = 0; type < NUM_TCT; type++) { loadTargetCursor(strprintf(targetCursor.c_str(), cursorTypeStr[type], cursorSizeStr[size]), targetWidths[size], targetHeights[size], type, size); } } } void ActorSprite::cleanupTargetCursors() { for (int size = 0; size < NUM_TC; size++) { for (int type = 0; type < NUM_TCT; type++) { delete targetCursor[type][size]; if (targetCursorImages[type][size]) targetCursorImages[type][size]->decRef(); } } } void ActorSprite::loadTargetCursor(const std::string &filename, int width, int height, int type, int size) { assert(size > -1); assert(size < 3); ResourceManager *resman = ResourceManager::getInstance(); ImageSet *currentImageSet = resman->getImageSet(filename, width, height); if (!currentImageSet) { logger->log("Error loading target cursor: %s", filename.c_str()); return; } auto *anim = new Animation; for (unsigned int i = 0; i < currentImageSet->size(); ++i) { anim->addFrame(currentImageSet->get(i), DEFAULT_FRAME_DELAY, -(currentImageSet->getWidth() / 2), -(currentImageSet->getHeight() / 2)); } auto *currentCursor = new SimpleAnimation(anim); targetCursorImages[type][size] = currentImageSet; targetCursor[type][size] = currentCursor; }