diff options
author | Andrei Karas <akaras@inbox.ru> | 2013-08-31 22:42:10 +0300 |
---|---|---|
committer | Andrei Karas <akaras@inbox.ru> | 2013-08-31 22:42:10 +0300 |
commit | 00cda69b883d6354f093be6ee39a7936cb798979 (patch) | |
tree | f1daa290abfb53180bd8420a45fe6dff1c7a2ab3 /src/being | |
parent | 5919cdc663d5f60a8c5cc7e50ad0c43a18cf9829 (diff) | |
download | mv-00cda69b883d6354f093be6ee39a7936cb798979.tar.gz mv-00cda69b883d6354f093be6ee39a7936cb798979.tar.bz2 mv-00cda69b883d6354f093be6ee39a7936cb798979.tar.xz mv-00cda69b883d6354f093be6ee39a7936cb798979.zip |
move being related files into being dir.
Diffstat (limited to 'src/being')
-rw-r--r-- | src/being/actor.cpp | 71 | ||||
-rw-r--r-- | src/being/actor.h | 142 | ||||
-rw-r--r-- | src/being/actorsprite.cpp | 398 | ||||
-rw-r--r-- | src/being/actorsprite.h | 247 | ||||
-rw-r--r-- | src/being/actorspritelistener.h | 44 | ||||
-rw-r--r-- | src/being/being.cpp | 3041 | ||||
-rw-r--r-- | src/being/being.h | 1063 | ||||
-rw-r--r-- | src/being/beingcacheentry.h | 128 | ||||
-rw-r--r-- | src/being/compoundsprite.cpp | 567 | ||||
-rw-r--r-- | src/being/compoundsprite.h | 161 | ||||
-rw-r--r-- | src/being/localplayer.cpp | 4355 | ||||
-rw-r--r-- | src/being/localplayer.h | 652 | ||||
-rw-r--r-- | src/being/playerinfo.cpp | 398 | ||||
-rw-r--r-- | src/being/playerinfo.h | 267 | ||||
-rw-r--r-- | src/being/playerrelations.cpp | 630 | ||||
-rw-r--r-- | src/being/playerrelations.h | 285 |
16 files changed, 12449 insertions, 0 deletions
diff --git a/src/being/actor.cpp b/src/being/actor.cpp new file mode 100644 index 000000000..0efed5d18 --- /dev/null +++ b/src/being/actor.cpp @@ -0,0 +1,71 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/actor.h" + +#include "map.h" + +#include "resources/image.h" +#include "resources/resourcemanager.h" + +#include "debug.h" + +Actor::Actor(): + mMap(nullptr), + mPos(), + mYDiff(0), + mMapActor() +{ +} + +Actor::~Actor() +{ + setMap(nullptr); +} + +void Actor::setMap(Map *const map) +{ + // Remove Actor from potential previous map + if (mMap) + mMap->removeActor(mMapActor); + + mMap = map; + + // Add Actor to potential new map + if (mMap) + mMapActor = mMap->addActor(this); +} + +int Actor::getTileX() const +{ + if (!mMap || !mMap->getTileWidth()) + return 0; + + return getPixelX() / mMap->getTileWidth(); +} + +int Actor::getTileY() const +{ + if (!mMap || !mMap->getTileHeight()) + return 0; + + return getPixelY() / mMap->getTileHeight(); +} diff --git a/src/being/actor.h b/src/being/actor.h new file mode 100644 index 000000000..326b9f40c --- /dev/null +++ b/src/being/actor.h @@ -0,0 +1,142 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_ACTOR_H +#define BEING_ACTOR_H + +#include "vector.h" + +#include <list> + +#include "localconsts.h" + +class Actor; +class Graphics; +class Image; +class Map; + +typedef std::list<Actor*> Actors; +typedef Actors::const_iterator ActorsCIter; + +class Actor +{ +public: + Actor(); + + A_DELETE_COPY(Actor) + + virtual ~Actor(); + + /** + * Draws the Actor to the given graphics context. + * + * Note: this function could be simplified if the graphics context + * would support setting a translation offset. It already does this + * partly with the clipping rectangle support. + */ + virtual bool draw(Graphics *const graphics, + const int offsetX, const int offsetY) const = 0; + + /** + * Returns the horizontal size of the actors graphical representation + * in pixels or 0 when it is undefined. + */ + virtual int getWidth() const A_WARN_UNUSED + { return 0; } + + /** + * Returns the vertical size of the actors graphical representation + * in pixels or 0 when it is undefined. + */ + virtual int getHeight() const A_WARN_UNUSED + { return 0; } + + /** + * Returns the pixel position of this actor. + */ + const Vector &getPosition() const A_WARN_UNUSED + { return mPos; } + + /** + * Sets the pixel position of this actor. + */ + virtual void setPosition(const Vector &pos) + { mPos = pos; } + + /** + * Returns the pixels X coordinate of the actor. + */ + int getPixelX() const A_WARN_UNUSED + { return static_cast<int>(mPos.x); } + + /** + * Returns the pixel Y coordinate of the actor. + */ + virtual int getPixelY() const A_WARN_UNUSED + { return static_cast<int>(mPos.y); } + + /** + * Returns the pixel Y coordinate of the actor for sorting only. + */ + virtual int getSortPixelY() const A_WARN_UNUSED + { return static_cast<int>(mPos.y) - mYDiff; } + + /** + * Returns the x coordinate in tiles of the actor. + */ + virtual int getTileX() const A_WARN_UNUSED; + + /** + * Returns the y coordinate in tiles of the actor. + */ + virtual int getTileY() const A_WARN_UNUSED; + + /** + * Returns the number of Image layers used to draw the actor. + */ + virtual int getNumberOfLayers() const A_WARN_UNUSED + { return 0; } + + /** + * Returns the current alpha value used to draw the actor. + */ + virtual float getAlpha() const A_WARN_UNUSED = 0; + + /** + * Sets the alpha value used to draw the actor. + */ + virtual void setAlpha(float alpha) = 0; + + virtual void setMap(Map *const map); + + const Map* getMap() const A_WARN_UNUSED + { return mMap; } + +protected: + Map *mMap; + Vector mPos; /**< Position in pixels relative to map. */ + int mYDiff; + +private: + Actors::iterator mMapActor; +}; + +#endif // BEING_ACTOR_H diff --git a/src/being/actorsprite.cpp b/src/being/actorsprite.cpp new file mode 100644 index 000000000..9c5ff6def --- /dev/null +++ b/src/being/actorsprite.cpp @@ -0,0 +1,398 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/actorsprite.h" + +#include "client.h" +#include "configuration.h" +#include "effectmanager.h" +#include "imagesprite.h" +#include "simpleanimation.h" +#include "soundmanager.h" +#include "statuseffect.h" + +#include "being/actorspritelistener.h" +#include "being/localplayer.h" + +#include "gui/theme.h" + +#include "net/net.h" + +#include "resources/imageset.h" +#include "resources/resourcemanager.h" + +#include "utils/checkutils.h" + +#include "debug.h" + +AnimatedSprite *ActorSprite::targetCursor[2][NUM_TC]; +bool ActorSprite::loaded = false; + +ActorSprite::ActorSprite(const int id) : + CompoundSprite(), + Actor(), + mStatusEffects(), + mStunParticleEffects(), + mStatusParticleEffects(&mStunParticleEffects, false), + mChildParticleEffects(&mStatusParticleEffects, false), + mId(id), + mStunMode(0), + mUsedTargetCursor(nullptr), + mActorSpriteListeners(), + mCursorPaddingX(0), + mCursorPaddingY(0), + mMustResetParticles(false) +{ +} + +ActorSprite::~ActorSprite() +{ + setMap(nullptr); + + mUsedTargetCursor = nullptr; + + if (player_node && player_node->getTarget() == this) + player_node->setTarget(nullptr); + + // Notify listeners of the destruction. + FOR_EACH (ActorSpriteListenerIterator, iter, mActorSpriteListeners) + { + if (reportFalse(*iter)) + (*iter)->actorSpriteDestroyed(*this); + } +} + +bool ActorSprite::draw(Graphics *const graphics, + const int offsetX, const int offsetY) const +{ + FUNC_BLOCK("ActorSprite::draw", 1) + const int px = getPixelX() + offsetX - 16; + // Temporary fix to the Y offset. +#ifdef MANASERV_SUPPORT + const int py = getPixelY() + offsetY - + ((Net::getNetworkType() == ServerInfo::MANASERV) ? 15 : 32); +#else + const int py = getPixelY() + offsetY - 32; +#endif + + if (mUsedTargetCursor) + { + mUsedTargetCursor->update(tick_time * MILLISECONDS_IN_A_TICK); + mUsedTargetCursor->draw(graphics, + px + getTargetOffsetX() - mCursorPaddingX, + py + getTargetOffsetY() - mCursorPaddingY); + } + + return drawSpriteAt(graphics, px, py); +} + +bool ActorSprite::drawSpriteAt(Graphics *const graphics, + const int x, const int y) const +{ + return CompoundSprite::draw(graphics, x, y); +} + +void ActorSprite::logic() +{ + BLOCK_START("ActorSprite::logic") + // Update sprite animations + update(tick_time * MILLISECONDS_IN_A_TICK); + + // Restart status/particle effects, if needed + if (mMustResetParticles) + { + mMustResetParticles = false; + FOR_EACH (std::set<int>::const_iterator, it, mStatusEffects) + { + const StatusEffect *const effect + = StatusEffect::getStatusEffect(*it, true); + if (effect && effect->particleEffectIsPersistent()) + updateStatusEffect(*it, true); + } + } + + // Update particle effects + mChildParticleEffects.moveTo(mPos.x, mPos.y); + BLOCK_END("ActorSprite::logic") +} + +void ActorSprite::actorLogic() +{ +} + +void ActorSprite::setMap(Map *const 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 *const particle) +{ + mChildParticleEffects.addLocally(particle); +} + +void ActorSprite::setTargetType(const TargetCursorType type) +{ + static const int targetWidths[ActorSprite::NUM_TC] = {0, 0, 0}; + static const int targetHeights[ActorSprite::NUM_TC] = {-16, -16, -32}; + + if (type == TCT_NONE) + { + untarget(); + } + else + { + const TargetCursorSize sz = getTargetCursorSize(); + mUsedTargetCursor = targetCursor[type][sz]; + if (mUsedTargetCursor) + { + mCursorPaddingX = targetWidths[sz]; + mCursorPaddingY = targetHeights[sz]; + } + } +} + +struct EffectDescription final +{ + std::string mGFXEffect; + std::string mSFXEffect; +}; + +void ActorSprite::setStatusEffect(const int index, const 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(const int offset, + const uint16_t newEffects) +{ + for (unsigned i = 0; i < STATUS_EFFECTS; i++) + { + const int index = StatusEffect::blockEffectIndexToEffectIndex( + offset + i); + + if (index != -1) + setStatusEffect(index, (newEffects & (1 << i)) > 0); + } +} + +void ActorSprite::updateStunMode(const int oldMode, const int newMode) +{ + handleStatusEffect(StatusEffect::getStatusEffect(oldMode, false), -1); + handleStatusEffect(StatusEffect::getStatusEffect(newMode, true), -1); +} + +void ActorSprite::updateStatusEffect(const int index, const bool newStatus) +{ + handleStatusEffect(StatusEffect::getStatusEffect(index, newStatus), index); +} + +void ActorSprite::handleStatusEffect(StatusEffect *const effect, + const int effectId) +{ + if (!effect) + return; + + Particle *const particle = effect->getParticle(); + + if (effectId >= 0) + { + mStatusParticleEffects.setLocally(effectId, particle); + } + else + { + mStunParticleEffects.clearLocally(); + if (particle) + mStunParticleEffects.addLocally(particle); + } +} + +void ActorSprite::setupSpriteDisplay(const SpriteDisplay &display, + const bool forceDisplay, + const int imageType, + const std::string &color) +{ + clear(); + + FOR_EACH (SpriteRefs, it, display.sprites) + { + if (!*it) + continue; + const std::string file = paths.getStringValue("sprites").append( + combineDye3((*it)->sprite, color)); + + const int variant = (*it)->variant; + addSprite(AnimatedSprite::delayedLoad(file, variant)); + } + + // Ensure that something is shown, if desired + if (empty() && forceDisplay) + { + if (display.image.empty()) + { + addSprite(AnimatedSprite::delayedLoad( + paths.getStringValue("sprites").append( + paths.getStringValue("spriteErrorFile")))); + } + else + { + ResourceManager *const resman = ResourceManager::getInstance(); + std::string imagePath; + switch (imageType) + { + case 0: + default: + imagePath = paths.getStringValue("itemIcons").append( + display.image); + break; + case 1: + imagePath = paths.getStringValue("itemIcons").append( + display.floor); + break; + } + imagePath = combineDye2(imagePath, color); + + Image *img = resman->getImage(imagePath); + + if (!img) + img = Theme::getImageFromTheme("unknown-item.png"); + + addSprite(new ImageSprite(img)); + if (img) + img->decRef(); + } + } + + mChildParticleEffects.clear(); + + // setup particle effects + if (Particle::enabled && particleEngine) + { + FOR_EACH (StringVectCIter, itr, display.particles) + { + Particle *const p = particleEngine->addEffect(*itr, 0, 0); + controlParticle(p); + } + } + + mMustResetParticles = true; +} + +void ActorSprite::load() +{ + if (loaded) + unload(); + + initTargetCursor(); + + loaded = true; +} + +void ActorSprite::unload() +{ + if (reportTrue(!loaded)) + return; + + cleanupTargetCursors(); + loaded = false; +} + +void ActorSprite::addActorSpriteListener(ActorSpriteListener *const listener) +{ + mActorSpriteListeners.push_front(listener); +} + +void ActorSprite::removeActorSpriteListener(ActorSpriteListener *const + listener) +{ + mActorSpriteListeners.remove(listener); +} + +static const char *cursorType(const int type) +{ + switch (type) + { + case ActorSprite::TCT_IN_RANGE: + return "in-range"; + default: + case ActorSprite::TCT_NORMAL: + return "normal"; + } +} + +static const char *cursorSize(const int size) +{ + switch (size) + { + case ActorSprite::TC_LARGE: + return "l"; + case ActorSprite::TC_MEDIUM: + return "m"; + default: + case ActorSprite::TC_SMALL: + return "s"; + } +} + +void ActorSprite::initTargetCursor() +{ + static const std::string targetCursorFile = "%s/target-cursor-%s-%s.xml"; + + const std::string path = branding.getStringValue("guiPath"); + + // Load target cursors + for (int size = TC_SMALL; size < NUM_TC; size++) + { + for (int type = TCT_NORMAL; type < NUM_TCT; type++) + { + targetCursor[type][size] = AnimatedSprite::load(strprintf( + targetCursorFile.c_str(), path.c_str(), cursorType(type), + cursorSize(size))); + } + } +} + +void ActorSprite::cleanupTargetCursors() +{ + for (int size = TC_SMALL; size < NUM_TC; size++) + { + for (int type = TCT_NORMAL; type < NUM_TCT; type++) + { + if (targetCursor[type][size]) + { + delete targetCursor[type][size]; + targetCursor[type][size] = nullptr; + } + } + } +} diff --git a/src/being/actorsprite.h b/src/being/actorsprite.h new file mode 100644 index 000000000..debdc69d1 --- /dev/null +++ b/src/being/actorsprite.h @@ -0,0 +1,247 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_ACTORSPRITE_H +#define BEING_ACTORSPRITE_H + +#include "localconsts.h" +#include "map.h" + +#include "being/actor.h" +#include "being/compoundsprite.h" + +#include "particle/particlecontainer.h" + +#include <SDL_types.h> + +#include <set> +#include <list> + +#include "localconsts.h" + +class AnimatedSprite; +class StatusEffect; +class ActorSpriteListener; + +class ActorSprite : public CompoundSprite, public Actor +{ +public: + enum Type + { + UNKNOWN = 0, + PLAYER, + NPC, + MONSTER, + FLOOR_ITEM, + PORTAL, + PET, + AVATAR + }; + + enum TargetCursorSize + { + TC_SMALL = 0, + TC_MEDIUM, + TC_LARGE, + NUM_TC + }; + + enum TargetCursorType + { + TCT_NONE = -1, + TCT_NORMAL = 0, + TCT_IN_RANGE, + NUM_TCT + }; + + explicit ActorSprite(const int id); + + A_DELETE_COPY(ActorSprite) + + ~ActorSprite(); + + int getId() const A_WARN_UNUSED + { return mId; } + + void setId(const int id) + { mId = id; } + + /** + * Returns the type of the ActorSprite. + */ + virtual Type getType() const A_WARN_UNUSED + { return UNKNOWN; } + + virtual bool draw(Graphics *const graphics, + const int offsetX, const int offsetY) const override; + + virtual bool drawSpriteAt(Graphics *const graphics, + const int x, const int y) const; + + virtual void logic(); + + static void actorLogic(); + + void setMap(Map *const map) override; + + /** + * Gets the way the object blocks pathfinding for other objects + */ + virtual Map::BlockType getBlockType() const A_WARN_UNUSED + { return Map::BLOCKTYPE_NONE; } + + /** + * Take control of a particle. + */ + void controlParticle(Particle *const particle); + + /** + * Returns the required size of a target cursor for this being. + */ + virtual TargetCursorSize getTargetCursorSize() const A_WARN_UNUSED + { return TC_MEDIUM; } + + virtual int getTargetOffsetX() const A_WARN_UNUSED + { return 0; } + + virtual int getTargetOffsetY() const A_WARN_UNUSED + { return 0; } + + /** + * Sets the target animation for this actor. + */ + void setTargetType(const TargetCursorType type); + + /** + * Untargets the actor. + */ + void untarget() + { mUsedTargetCursor = nullptr; } + + /** + * Sets the actor's stun mode. If zero, the being is `normal', otherwise it + * is `stunned' in some fashion. + */ + void setStunMode(const uint16_t stunMode) + { + if (mStunMode != stunMode) + updateStunMode(mStunMode, stunMode); + mStunMode = stunMode; + } + + void setStatusEffect(const int index, const bool active); + + /** + * A status effect block is a 16 bit mask of status effects. We assign each + * such flag a block ID of offset + bitnr. + * + * These are NOT the same as the status effect indices. + */ + void setStatusEffectBlock(const int offset, const uint16_t flags); + + virtual void setAlpha(const float alpha) override + { CompoundSprite::setAlpha(alpha); } + + virtual float getAlpha() const override A_WARN_UNUSED + { return CompoundSprite::getAlpha(); } + + virtual int getWidth() const override A_WARN_UNUSED + { return CompoundSprite::getWidth(); } + + virtual int getHeight() const override A_WARN_UNUSED + { return CompoundSprite::getHeight(); } + + static void load(); + + static void unload(); + + /** + * Add an ActorSprite listener. + */ + void addActorSpriteListener(ActorSpriteListener *const listener); + + /** + * Remove an ActorSprite listener. + */ + void removeActorSpriteListener(ActorSpriteListener *const listener); + +protected: + /** + * Notify self that the stun mode has been updated. Invoked by + * setStunMode if something changed. + */ + virtual void updateStunMode(const int oldMode, const int newMode); + + /** + * Notify self that a status effect has flipped. + * The new flag is passed. + */ + virtual void updateStatusEffect(const int index, const bool newStatus); + + /** + * Handle an update to a status or stun effect + * + * \param The StatusEffect to effect + * \param effectId -1 for stun, otherwise the effect index + */ + virtual void handleStatusEffect(StatusEffect *const effect, + const int effectId); + + void setupSpriteDisplay(const SpriteDisplay &display, + const bool forceDisplay = true, + const int imageType = 0, + const std::string &color = ""); + + std::set<int> mStatusEffects; /**< set of active status effects */ + + ParticleList mStunParticleEffects; + ParticleVector mStatusParticleEffects; + ParticleList mChildParticleEffects; + int mId; + uint16_t mStunMode; /**< Stun mode; zero if not stunned */ + +private: + /** Load the target cursors into memory */ + static void initTargetCursor(); + + /** Remove the target cursors from memory */ + static void cleanupTargetCursors(); + + /** Animated target cursors. */ + static AnimatedSprite *targetCursor[NUM_TCT][NUM_TC]; + + static bool loaded; + + /** Target cursor being used */ + AnimatedSprite *mUsedTargetCursor; + + typedef std::list<ActorSpriteListener*> ActorSpriteListeners; + typedef ActorSpriteListeners::iterator ActorSpriteListenerIterator; + ActorSpriteListeners mActorSpriteListeners; + + int mCursorPaddingX; + int mCursorPaddingY; + + /** Reset particle status effects on next redraw? */ + bool mMustResetParticles; +}; + +#endif // BEING_ACTORSPRITE_H diff --git a/src/being/actorspritelistener.h b/src/being/actorspritelistener.h new file mode 100644 index 000000000..3d5a48443 --- /dev/null +++ b/src/being/actorspritelistener.h @@ -0,0 +1,44 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_ACTORSPRITELISTENER_H +#define BEING_ACTORSPRITELISTENER_H + +class ActorSprite; + +class ActorSpriteListener +{ + public: + /** + * Destructor. + */ + virtual ~ActorSpriteListener() + { } + + /** + * Called when the ActorSprite has been destroyed. The listener will + * have to be registered first. + * @param actorSprite the ActorSprite being destroyed. + */ + virtual void actorSpriteDestroyed(const ActorSprite &actorSprite) = 0; +}; + +#endif // BEING_ACTORSPRITELISTENER_H diff --git a/src/being/being.cpp b/src/being/being.cpp new file mode 100644 index 000000000..7dfe9d9b6 --- /dev/null +++ b/src/being/being.cpp @@ -0,0 +1,3041 @@ +/* + * The ManaPlus Client + * Copyright (C) 2004-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/being.h" + +#include "actorspritemanager.h" +#include "animatedsprite.h" +#include "beingequipbackend.h" +#include "client.h" +#include "effectmanager.h" +#include "guild.h" +#include "party.h" +#include "soundconsts.h" +#include "soundmanager.h" +#include "text.h" + +#include "being/beingcacheentry.h" +#include "being/playerrelations.h" + +#include "particle/particle.h" + +#include "gui/equipmentwindow.h" +#include "gui/socialwindow.h" +#include "gui/speechbubble.h" +#include "gui/sdlfont.h" +#include "gui/skilldialog.h" + +#include "net/charserverhandler.h" +#include "net/gamehandler.h" +#include "net/inventoryhandler.h" +#include "net/net.h" +#include "net/npchandler.h" +#include "net/playerhandler.h" + +#include "resources/avatardb.h" +#include "resources/emotedb.h" +#include "resources/iteminfo.h" +#include "resources/monsterdb.h" +#include "resources/npcdb.h" +#include "resources/petdb.h" +#include "resources/resourcemanager.h" + +#include "gui/widgets/langtab.h" +#include "gui/widgets/skillinfo.h" + +#include "utils/gettext.h" + +#include <cmath> + +#include "debug.h" + +const unsigned int CACHE_SIZE = 50; + +int Being::mNumberOfHairstyles = 1; +int Being::mNumberOfRaces = 1; + +int Being::mUpdateConfigTime = 0; +unsigned int Being::mConfLineLim = 0; +int Being::mSpeechType = 0; +bool Being::mHighlightMapPortals = false; +bool Being::mHighlightMonsterAttackRange = false; +bool Being::mLowTraffic = true; +bool Being::mDrawHotKeys = true; +bool Being::mShowBattleEvents = false; +bool Being::mShowMobHP = false; +bool Being::mShowOwnHP = false; +bool Being::mShowGender = false; +bool Being::mShowLevel = false; +bool Being::mShowPlayersStatus = false; +bool Being::mEnableReorderSprites = true; +bool Being::mHideErased = false; +bool Being::mMoveNames = false; +int Being::mAwayEffect = -1; + +std::list<BeingCacheEntry*> beingInfoCache; +typedef std::map<int, Guild*>::const_iterator GuildsMapCIter; +typedef std::map<int, int>::const_iterator IntMapCIter; + +Being::Being(const int id, const Type type, const uint16_t subtype, + Map *const map) : + ActorSprite(id), + mNextSound(), + mInfo(BeingInfo::unknown), + mEmotionSprite(nullptr), + mAnimationEffect(nullptr), + mSpriteAction(SpriteAction::STAND), + mName(), + mRaceName(), + mPartyName(), + mGuildName(), + mSpeech(), + mDispName(nullptr), + mNameColor(nullptr), + mEquippedWeapon(nullptr), + mPath(), + mText(nullptr), + mTextColor(nullptr), + mDest(), + mSpriteColors(), + mSpriteIDs(), + mSpriteColorsIds(), + mGuilds(), + mParty(nullptr), + mActionTime(0), + mEmotionTime(0), + mSpeechTime(0), + mAttackSpeed(350), + mLevel(0), + mAttackRange(1), + mGender(GENDER_UNSPECIFIED), + mAction(STAND), + mSubType(0xFFFF), + mDirection(DOWN), + mDirectionDelayed(0), + mSpriteDirection(DIRECTION_DOWN), + mShowName(false), + mIsGM(false), + mType(type), + mSpeechBubble(new SpeechBubble), + mWalkSpeed(Net::getPlayerHandler()->getDefaultWalkSpeed()), + mIp(), + mSpriteRemap(new int[20]), + mSpriteHide(new int[20]), + mComment(), + mPet(nullptr), + mOwner(nullptr), + mSpecialParticle(nullptr), + mX(0), + mY(0), + mDamageTaken(0), + mHP(0), + mMaxHP(0), + mDistance(0), + mIsReachable(REACH_UNKNOWN), + mGoodStatus(-1), + mMoveTime(0), + mAttackTime(0), + mTalkTime(0), + mOtherTime(0), + mTestTime(cur_time), + mAttackDelay(0), + mMinHit(0), + mMaxHit(0), + mCriticalHit(0), + mPvpRank(0), + mNumber(100), + mPetId(0), + mLook(0), + mHairColor(0), + mErased(false), + mEnemy(false), + mGotComment(false), + mAdvanced(false), + mShop(false), + mAway(false), + mInactive(false) +{ + for (int f = 0; f < 20; f ++) + { + mSpriteRemap[f] = f; + mSpriteHide[f] = 0; + } + + setMap(map); + setSubtype(subtype, 0); + + if (mType == PLAYER) + mShowName = config.getBoolValue("visiblenames"); + else if (mType != NPC) + mGotComment = true; + + config.addListener("visiblenames", this); + + reReadConfig(); + + if (mType == NPC) + setShowName(true); + else + setShowName(mShowName); + + updateColors(); + updatePercentHP(); +} + +Being::~Being() +{ + config.removeListener("visiblenames", this); + + delete [] mSpriteRemap; + mSpriteRemap = nullptr; + delete [] mSpriteHide; + mSpriteHide = nullptr; + + delete mSpeechBubble; + mSpeechBubble = nullptr; + delete mDispName; + mDispName = nullptr; + delete mText; + mText = nullptr; + + delete mEmotionSprite; + mEmotionSprite = nullptr; + delete mAnimationEffect; + mAnimationEffect = nullptr; + + if (mOwner) + mOwner->setPet(nullptr); + if (mPet) + mPet->setOwner(nullptr); +} + +void Being::setSubtype(const uint16_t subtype, const uint8_t look) +{ + if (!mInfo) + return; + + if (subtype == mSubType && mLook == look) + return; + + mSubType = subtype; + mLook = look; + + if (mType == MONSTER) + { + mInfo = MonsterDB::get(mSubType); + if (mInfo) + { + setName(mInfo->getName()); + setupSpriteDisplay(mInfo->getDisplay(), true, 0, + mInfo->getColor(mLook)); + mYDiff = mInfo->getSortOffsetY(); + } + } + else if (mType == NPC) + { + mInfo = NPCDB::get(mSubType); + if (mInfo) + { + setupSpriteDisplay(mInfo->getDisplay(), false); + mYDiff = mInfo->getSortOffsetY(); + } + } + else if (mType == AVATAR) + { + mInfo = AvatarDB::get(mSubType); + if (mInfo) + setupSpriteDisplay(mInfo->getDisplay(), false); + } + else if (mType == PET) + { + mInfo = PETDB::get(mId); + if (mInfo) + { + setupSpriteDisplay(mInfo->getDisplay(), false); + mYDiff = mInfo->getSortOffsetY(); + } + } + else if (mType == PLAYER) + { + int id = -100 - subtype; + + // Prevent showing errors when sprite doesn't exist + if (!ItemDB::exists(id)) + { + id = -100; + // TRANSLATORS: default race name + setRaceName(_("Human")); + if (Net::getCharServerHandler()) + setSprite(Net::getCharServerHandler()->baseSprite(), id); + } + else + { + const ItemInfo &info = ItemDB::get(id); + setRaceName(info.getName()); + if (Net::getCharServerHandler()) + { + setSprite(Net::getCharServerHandler()->baseSprite(), + id, info.getColor(mLook)); + } + } + } +} + +ActorSprite::TargetCursorSize Being::getTargetCursorSize() const +{ + if (!mInfo) + return ActorSprite::TC_SMALL; + + return mInfo->getTargetCursorSize(); +} + +void Being::setPosition(const Vector &pos) +{ + Actor::setPosition(pos); + + updateCoords(); + + if (mText) + { + mText->adviseXY(static_cast<int>(pos.x), static_cast<int>(pos.y) + - getHeight() - mText->getHeight() - 6, mMoveNames); + } +} + +void Being::setDestination(const int dstX, const int dstY) +{ + // We can't calculate anything without a map anyway. + if (!mMap) + return; + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + setPath(mMap->findPath(mX, mY, dstX, dstY, getWalkMask())); + return; + } + +#ifdef MANASERV_SUPPORT + // Don't handle flawed destinations from server... + if (dstX == 0 || dstY == 0) + return; + + // If the destination is unwalkable, don't bother trying to get there + if (!mMap->getWalk(dstX / 32, dstY / 32)) + return; + + Position dest = mMap->checkNodeOffsets(getCollisionRadius(), getWalkMask(), + dstX, dstY); + Path thisPath = mMap->findPixelPath(static_cast<int>(mPos.x), + static_cast<int>(mPos.y), dest.x, dest.y, + static_cast<int>(getCollisionRadius()), + static_cast<unsigned char>(getWalkMask())); + + if (thisPath.empty()) + { + // If there is no path but the destination is on the same walkable tile, + // we accept it. + if (static_cast<int>(mPos.x) / 32 == dest.x / 32 + && static_cast<int>(mPos.y) / 32 == dest.y / 32) + { + mDest.x = static_cast<float>(dest.x); + mDest.y = static_cast<float>(dest.y); + } + setPath(Path()); + return; + } + + // The destination is valid, so we set it. + mDest.x = static_cast<float>(dest.x); + mDest.y = static_cast<float>(dest.y); + + setPath(thisPath); +#endif +} + +void Being::clearPath() +{ + mPath.clear(); +} + +void Being::setPath(const Path &path) +{ + mPath = path; + if (mPath.empty()) + return; + +#ifdef MANASERV_SUPPORT + if ((Net::getNetworkType() != ServerInfo::MANASERV) && + mAction != MOVE && mAction != DEAD) +#else + if (mAction != MOVE && mAction != DEAD) +#endif + { + nextTile(); + mActionTime = tick_time; + } +} + +void Being::setSpeech(const std::string &text, const std::string &channel, + int time) +{ + if (!userPalette) + return; + + if (!channel.empty() && (langChatTab && langChatTab->getChannelName() + != channel)) + { + return; + } + + // Remove colors + mSpeech = removeColors(text); + + // Trim whitespace + trim(mSpeech); + + const unsigned int lineLim = mConfLineLim; + if (lineLim > 0 && mSpeech.length() > lineLim) + mSpeech = mSpeech.substr(0, lineLim); + + trim(mSpeech); + if (mSpeech.empty()) + return; + + if (!time) + { + const size_t sz = mSpeech.size(); + if (sz < 200) + time = static_cast<int>(SPEECH_TIME - 300 + (3 * sz)); + } + + if (time < static_cast<int>(SPEECH_MIN_TIME)) + time = static_cast<int>(SPEECH_MIN_TIME); + + // Check for links + size_t start = mSpeech.find('['); + size_t e = mSpeech.find(']', start); + + while (start != std::string::npos && e != std::string::npos) + { + // Catch multiple embeds and ignore them so it doesn't crash the client. + while ((mSpeech.find('[', start + 1) != std::string::npos) && + (mSpeech.find('[', start + 1) < e)) + { + start = mSpeech.find('[', start + 1); + } + + size_t position = mSpeech.find('|'); + if (mSpeech[start + 1] == '@' && mSpeech[start + 2] == '@') + { + mSpeech.erase(e, 1); + mSpeech.erase(start, (position - start) + 1); + } + position = mSpeech.find('@'); + + while (position != std::string::npos) + { + mSpeech.erase(position, 2); + position = mSpeech.find('@'); + } + + start = mSpeech.find('[', start + 1); + e = mSpeech.find(']', start); + } + + if (!mSpeech.empty()) + { + mSpeechTime = time <= static_cast<int>(SPEECH_MAX_TIME) + ? time : static_cast<int>(SPEECH_MAX_TIME); + } + + const int speech = mSpeechType; + if (speech == TEXT_OVERHEAD && userPalette) + { + delete mText; + + mText = new Text(mSpeech, + getPixelX(), getPixelY() - getHeight(), + gcn::Graphics::CENTER, + &userPalette->getColor(UserPalette::PARTICLE), + true); + } +} + +void Being::takeDamage(Being *const attacker, const int amount, + const AttackType type, const int attackId) +{ + if (!userPalette || !attacker) + return; + + gcn::Font *font = nullptr; + // TRANSLATORS: hit or miss message in attacks + const std::string damage = amount ? toString(amount) : type == FLEE ? + _("dodge") : _("miss"); + const gcn::Color *color; + + if (gui) + font = gui->getInfoParticleFont(); + + // Selecting the right color + if (type == CRITICAL || type == FLEE) + { + if (type == CRITICAL) + attacker->setCriticalHit(amount); + + if (attacker == player_node) + { + color = &userPalette->getColor( + UserPalette::HIT_LOCAL_PLAYER_CRITICAL); + } + else + { + color = &userPalette->getColor(UserPalette::HIT_CRITICAL); + } + } + else if (!amount) + { + if (attacker == player_node) + { + // This is intended to be the wrong direction to visually + // differentiate between hits and misses + color = &userPalette->getColor(UserPalette::HIT_LOCAL_PLAYER_MISS); + } + else + { + color = &userPalette->getColor(UserPalette::MISS); + } + } + else if (mType == MONSTER) + { + if (attacker == player_node) + { + color = &userPalette->getColor( + UserPalette::HIT_LOCAL_PLAYER_MONSTER); + } + else + { + color = &userPalette->getColor( + UserPalette::HIT_PLAYER_MONSTER); + } + } + else if (mType == PLAYER && attacker != player_node + && this == player_node) + { + // here player was attacked by other player. mark him as enemy. + color = &userPalette->getColor(UserPalette::HIT_PLAYER_PLAYER); + attacker->setEnemy(true); + attacker->updateColors(); + } + else + { + color = &userPalette->getColor(UserPalette::HIT_MONSTER_PLAYER); + } + + if (chatWindow && mShowBattleEvents) + { + if (this == player_node) + { + if (attacker->mType == PLAYER || amount) + { + chatWindow->battleChatLog(strprintf("%s : Hit you -%d", + attacker->getName().c_str(), amount), BY_OTHER); + } + } + else if (attacker == player_node && amount) + { + chatWindow->battleChatLog(strprintf("%s : You hit %s -%d", + attacker->getName().c_str(), getName().c_str(), amount), + BY_PLAYER); + } + } + if (font && particleEngine) + { + // Show damage number + particleEngine->addTextSplashEffect(damage, + getPixelX(), getPixelY() - 16, color, font, true); + } + + if (type != SKILL) + attacker->updateHit(amount); + + if (amount > 0) + { + if (player_node && player_node == this) + player_node->setLastHitFrom(attacker->getName()); + + mDamageTaken += amount; + if (mInfo) + { + playSfx(mInfo->getSound(SOUND_EVENT_HURT), this, false, mX, mY); + + if (!mInfo->isStaticMaxHP()) + { + if (!mHP && mInfo->getMaxHP() < mDamageTaken) + mInfo->setMaxHP(mDamageTaken); + } + } + if (mHP && isAlive()) + { + mHP -= amount; + if (mHP < 0) + mHP = 0; + } + + if (mType == MONSTER) + { + updatePercentHP(); + updateName(); + } + else if (mType == PLAYER && socialWindow && getName() != "") + { + socialWindow->updateAvatar(getName()); + } + + if (effectManager) + { + const int hitEffectId = getHitEffect(attacker, type, attackId); + if (hitEffectId >= 0) + effectManager->trigger(hitEffectId, this); + } + } + else + { + if (effectManager) + { + const int hitEffectId = getHitEffect(attacker, + MISS, attackId); + if (hitEffectId >= 0) + effectManager->trigger(hitEffectId, this); + } + } +} + +int Being::getHitEffect(const Being *const attacker, + const AttackType type, const int attackId) const +{ + if (!effectManager) + return 0; + + // Init the particle effect path based on current + // weapon or default. + int hitEffectId = 0; + if (type != SKILL) + { + if (attacker) + { + const ItemInfo *attackerWeapon = attacker->getEquippedWeapon(); + if (attackerWeapon && attacker->getType() == PLAYER) + { + if (type == MISS) + hitEffectId = attackerWeapon->getMissEffectId(); + else if (type != CRITICAL) + hitEffectId = attackerWeapon->getHitEffectId(); + else + hitEffectId = attackerWeapon->getCriticalHitEffectId(); + } + else if (attacker->getType() == MONSTER) + { + const BeingInfo *const info = attacker->getInfo(); + if (info) + { + const Attack *atk = info->getAttack(attackId); + if (atk) + { + if (type == MISS) + hitEffectId = atk->mMissEffectId; + else if (type != CRITICAL) + hitEffectId = atk->mHitEffectId; + else + hitEffectId = atk->mCriticalHitEffectId; + } + else + { + hitEffectId = getDefaultEffectId(type); + } + } + } + else + { + hitEffectId = getDefaultEffectId(type); + } + } + else + { + hitEffectId = getDefaultEffectId(type); + } + } + else + { + // move skills effects to +100000 in effects list + hitEffectId = attackId + 100000; + } + return hitEffectId; +} + +int Being::getDefaultEffectId(const int type) +{ + if (type == MISS) + return paths.getIntValue("missEffectId"); + else if (type != CRITICAL) + return paths.getIntValue("hitEffectId"); + else + return paths.getIntValue("criticalHitEffectId"); +} + +void Being::handleAttack(Being *const victim, const int damage, + const int attackId) +{ + if (!victim || !mInfo) + return; + + if (this != player_node) + setAction(Being::ATTACK, attackId); + + if (mType == PLAYER && mEquippedWeapon) + fireMissile(victim, mEquippedWeapon->getMissileParticleFile()); + else if (mInfo->getAttack(attackId)) + fireMissile(victim, mInfo->getAttack(attackId)->mMissileParticle); + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + reset(); + mActionTime = tick_time; + } + + if (this != player_node) + { + const uint8_t dir = calcDirection(victim->getTileX(), + victim->getTileY()); + if (dir) + setDirection(dir); + } + if (damage && victim->mType == PLAYER && victim->mAction == SIT) + victim->setAction(STAND); + + if (mType == PLAYER) + { + if (mSpriteIDs.size() >= 10) + { + // here 10 is weapon slot + int weaponId = mSpriteIDs[10]; + if (!weaponId) + weaponId = -100 - mSubType; + const ItemInfo &info = ItemDB::get(weaponId); + playSfx(info.getSound((damage > 0) ? + SOUND_EVENT_HIT : SOUND_EVENT_MISS), victim, true, mX, mY); + } + } + else + { + playSfx(mInfo->getSound((damage > 0) ? + SOUND_EVENT_HIT : SOUND_EVENT_MISS), victim, true, mX, mY); + } +} + +void Being::handleSkill(Being *const victim, const int damage, + const int skillId, const int skillLevel) +{ + if (!victim || !mInfo || !skillDialog) + return; + + if (this != player_node) + setAction(Being::ATTACK, 1); + + SkillInfo *const skill = skillDialog->getSkill(skillId); + const SkillData *const data = skill + ? skill->getData1(skillLevel) : nullptr; + if (data) + fireMissile(victim, data->particle); + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + reset(); + mActionTime = tick_time; + } + + if (this != player_node) + { + const uint8_t dir = calcDirection(victim->getTileX(), + victim->getTileY()); + if (dir) + setDirection(dir); + } + if (damage && victim->mType == PLAYER && victim->mAction == SIT) + victim->setAction(STAND); + if (data) + { + if (damage > 0) + playSfx(data->soundHit, victim, true, mX, mY); + else + playSfx(data->soundMiss, victim, true, mX, mY); + } + else + { + playSfx(mInfo->getSound((damage > 0) ? + SOUND_EVENT_HIT : SOUND_EVENT_MISS), victim, true, mX, mY); + } +} + +void Being::setName(const std::string &name) +{ + if (mType == NPC) + { + mName = name.substr(0, name.find('#', 0)); + showName(); + } + else + { + mName = name; + + if (mType == PLAYER && getShowName()) + showName(); + } +} + +void Being::setShowName(const bool doShowName) +{ + if (mShowName == doShowName) + return; + + mShowName = doShowName; + + if (doShowName) + { + showName(); + } + else + { + delete mDispName; + mDispName = nullptr; + } +} + +void Being::setGuildName(const std::string &name) +{ + mGuildName = name; +} + +void Being::setGuildPos(const std::string &pos A_UNUSED) +{ +} + +void Being::addGuild(Guild *const guild) +{ + if (!guild) + return; + + mGuilds[guild->getId()] = guild; + + if (this == player_node && socialWindow) + socialWindow->addTab(guild); +} + +void Being::removeGuild(const int id) +{ + if (this == player_node && socialWindow) + socialWindow->removeTab(mGuilds[id]); + + if (mGuilds[id]) + mGuilds[id]->removeMember(getName()); + mGuilds.erase(id); +} + +Guild *Being::getGuild(const std::string &guildName) const +{ + FOR_EACH (GuildsMapCIter, itr, mGuilds) + { + Guild *const guild = itr->second; + if (guild && guild->getName() == guildName) + return guild; + } + + return nullptr; +} + +Guild *Being::getGuild(const int id) const +{ + const std::map<int, Guild*>::const_iterator itr = mGuilds.find(id); + if (itr != mGuilds.end()) + return itr->second; + + return nullptr; +} + +Guild *Being::getGuild() const +{ + const std::map<int, Guild*>::const_iterator itr = mGuilds.begin(); + if (itr != mGuilds.end()) + return itr->second; + + return nullptr; +} + +void Being::clearGuilds() +{ + FOR_EACH (GuildsMapCIter, itr, mGuilds) + { + Guild *const guild = itr->second; + + if (guild) + { + if (this == player_node && socialWindow) + socialWindow->removeTab(guild); + + guild->removeMember(mId); + } + } + + mGuilds.clear(); +} + +void Being::setParty(Party *const party) +{ + if (party == mParty) + return; + + Party *const old = mParty; + mParty = party; + + if (old) + old->removeMember(mId); + + if (party) + party->addMember(mId, mName); + + updateColors(); + + if (this == player_node && socialWindow) + { + if (old) + socialWindow->removeTab(old); + + if (party) + socialWindow->addTab(party); + } +} + +void Being::updateGuild() +{ + if (!player_node) + return; + + Guild *const guild = player_node->getGuild(); + if (!guild) + { + clearGuilds(); + updateColors(); + return; + } + if (guild->getMember(getName())) + { + setGuild(guild); + if (!guild->getName().empty()) + mGuildName = guild->getName(); + } + updateColors(); +} + +void Being::setGuild(Guild *const guild) +{ + Guild *const old = getGuild(); + if (guild == old) + return; + + clearGuilds(); + addGuild(guild); + + if (old) + old->removeMember(mName); + + updateColors(); + + if (this == player_node && socialWindow) + { + if (old) + socialWindow->removeTab(old); + + if (guild) + socialWindow->addTab(guild); + } +} + +void Being::fireMissile(Being *const victim, const std::string &particle) const +{ + if (!victim || particle.empty() || !particleEngine) + return; + + Particle *const target = particleEngine->createChild(); + + if (!target) + return; + + Particle *const missile = target->addEffect( + particle, getPixelX(), getPixelY()); + + if (missile) + { + target->moveBy(Vector(0.0f, 0.0f, 32.0f)); + target->setLifetime(1000); + victim->controlParticle(target); + + missile->setDestination(target, 7, 0); + missile->setDieDistance(8); + missile->setLifetime(900); + } +} + +std::string Being::getSitAction() const +{ + if (serverVersion < 0) + { + return SpriteAction::SIT; + } + else + { + if (mMap) + { + const unsigned char mask = mMap->getBlockMask(mX, mY); + if (mask & Map::BLOCKMASK_GROUNDTOP) + return SpriteAction::SITTOP; + else if (mask & Map::BLOCKMASK_AIR) + return SpriteAction::SITSKY; + else if (mask & Map::BLOCKMASK_WATER) + return SpriteAction::SITWATER; + } + return SpriteAction::SIT; + } +} + + +std::string Being::getMoveAction() const +{ + if (serverVersion < 0) + { + return SpriteAction::MOVE; + } + else + { + if (mMap) + { + const unsigned char mask = mMap->getBlockMask(mX, mY); + if (mask & Map::BLOCKMASK_AIR) + return SpriteAction::FLY; + else if (mask & Map::BLOCKMASK_WATER) + return SpriteAction::SWIM; + } + return SpriteAction::MOVE; + } +} + +std::string Being::getWeaponAttackAction(const ItemInfo *const weapon) const +{ + if (!weapon) + return SpriteAction::ATTACK; + + if (serverVersion < 0 || !weapon) + { + return weapon->getAttackAction(); + } + else + { + if (mMap) + { + const unsigned char mask = mMap->getBlockMask(mX, mY); + if (mask & Map::BLOCKMASK_AIR) + return weapon->getSkyAttackAction(); + else if (mask & Map::BLOCKMASK_WATER) + return weapon->getWaterAttackAction(); + } + return weapon->getAttackAction(); + } +} + +std::string Being::getAttackAction(const Attack *const attack1) const +{ + if (!attack1) + return SpriteAction::ATTACK; + + if (serverVersion < 0 || !attack1) + { + return attack1->mAction; + } + else + { + if (mMap) + { + const unsigned char mask = mMap->getBlockMask(mX, mY); + if (mask & Map::BLOCKMASK_AIR) + return attack1->mSkyAction; + else if (mask & Map::BLOCKMASK_WATER) + return attack1->mWaterAction; + } + return attack1->mAction; + } +} + +#define getSpriteAction(func, action) \ + std::string Being::get##func##Action() const \ +{ \ + if (serverVersion < 0) \ + { \ + return SpriteAction::action; \ + } \ + else \ + { \ + if (mMap) \ + { \ + const unsigned char mask = mMap->getBlockMask(mX, mY); \ + if (mask & Map::BLOCKMASK_AIR) \ + return SpriteAction::action##SKY; \ + else if (mask & Map::BLOCKMASK_WATER) \ + return SpriteAction::action##WATER; \ + } \ + return SpriteAction::action; \ + } \ +} + +getSpriteAction(Dead, DEAD) +getSpriteAction(Stand, STAND) +getSpriteAction(Spawn, SPAWN) + +void Being::setAction(const Action &action, const int attackId) +{ + std::string currentAction = SpriteAction::INVALID; + + switch (action) + { + case MOVE: + if (mInfo) + { + playSfx(mInfo->getSound( + SOUND_EVENT_MOVE), nullptr, true, mX, mY); + } + currentAction = getMoveAction(); + // Note: When adding a run action, + // Differentiate walk and run with action name, + // while using only the ACTION_MOVE. + break; + case SIT: + currentAction = getSitAction(); + if (mInfo) + { + SoundEvent event; + if (currentAction == SpriteAction::SITTOP) + event = SOUND_EVENT_SITTOP; + else + event = SOUND_EVENT_SIT; + playSfx(mInfo->getSound(event), nullptr, true, mX, mY); + } + break; + case ATTACK: + if (mEquippedWeapon) + { + currentAction = getWeaponAttackAction(mEquippedWeapon); + reset(); + } + else + { + if (!mInfo || !mInfo->getAttack(attackId)) + break; + + currentAction = getAttackAction(mInfo->getAttack(attackId)); + reset(); + + // attack particle effect + if (Particle::enabled) + { + const int effectId = mInfo->getAttack(attackId)->mEffectId; + + int rotation; + switch (mSpriteDirection) + { + case DIRECTION_DOWN: + default: + rotation = 0; + break; + case DIRECTION_LEFT: + rotation = 90; + break; + case DIRECTION_UP: + rotation = 180; + break; + case DIRECTION_RIGHT: + rotation = 270; + break; + } + if (Particle::enabled && effectManager && effectId >= 0) + effectManager->trigger(effectId, this, rotation); + } + } + break; + case HURT: + if (mInfo) + { + playSfx(mInfo->getSound(SOUND_EVENT_HURT), + this, false, mX, mY); + } + break; + case DEAD: + currentAction = getDeadAction(); + if (mInfo) + { + playSfx(mInfo->getSound(SOUND_EVENT_DIE), this, true, mX, mY); + if (mType == MONSTER || mType == NPC) + mYDiff = mInfo->getDeadSortOffsetY(); + } + break; + case STAND: + currentAction = getStandAction(); + break; + case SPAWN: + if (mInfo) + { + playSfx(mInfo->getSound(SOUND_EVENT_SPAWN), + nullptr, true, mX, mY); + } + currentAction = getSpawnAction(); + break; + default: + logger->log("Being::setAction unknown action: " + + toString(static_cast<unsigned>(action))); + break; + } + + if (currentAction != SpriteAction::INVALID) + { + mSpriteAction = currentAction; + play(currentAction); + if (mEmotionSprite) + mEmotionSprite->play(currentAction); + if (mAnimationEffect) + mAnimationEffect->play(currentAction); + mAction = action; + } + + if (currentAction != SpriteAction::MOVE + && currentAction != SpriteAction::FLY + && currentAction != SpriteAction::SWIM) + { + mActionTime = tick_time; + } +} + +void Being::setDirection(const uint8_t direction) +{ + if (mDirection == direction) + return; + + mDirection = direction; + + mDirectionDelayed = 0; + + // if the direction does not change much, keep the common component + int mFaceDirection = mDirection & direction; + if (!mFaceDirection) + mFaceDirection = direction; + + SpriteDirection dir; + if (mFaceDirection & UP) + { + if (mFaceDirection & LEFT) + dir = DIRECTION_UPLEFT; + else if (mFaceDirection & RIGHT) + dir = DIRECTION_UPRIGHT; + else + dir = DIRECTION_UP; + } + else if (mFaceDirection & DOWN) + { + if (mFaceDirection & LEFT) + dir = DIRECTION_DOWNLEFT; + else if (mFaceDirection & RIGHT) + dir = DIRECTION_DOWNRIGHT; + else + dir = DIRECTION_DOWN; + } + else if (mFaceDirection & RIGHT) + { + dir = DIRECTION_RIGHT; + } + else + { + dir = DIRECTION_LEFT; + } + mSpriteDirection = static_cast<uint8_t>(dir); + + CompoundSprite::setSpriteDirection(dir); + if (mEmotionSprite) + mEmotionSprite->setSpriteDirection(dir); + if (mAnimationEffect) + mAnimationEffect->setSpriteDirection(dir); + recalcSpritesOrder(); +} + +uint8_t Being::calcDirection() const +{ + uint8_t dir = 0; + if (mDest.x > mX) + dir |= RIGHT; + else if (mDest.x < mX) + dir |= LEFT; + if (mDest.y > mY) + dir |= DOWN; + else if (mDest.y < mY) + dir |= UP; + return dir; +} + +uint8_t Being::calcDirection(const int dstX, const int dstY) const +{ + uint8_t dir = 0; + if (dstX > mX) + dir |= RIGHT; + else if (dstX < mX) + dir |= LEFT; + if (dstY > mY) + dir |= DOWN; + else if (dstY < mY) + dir |= UP; + return dir; +} + +void Being::nextTile() +{ + if (mPath.empty()) + { + setAction(STAND); + return; + } + + const Position pos = mPath.front(); + mPath.pop_front(); + + const uint8_t dir = calcDirection(pos.x, pos.y); + if (dir) + setDirection(dir); + + if (!mMap || !mMap->getWalk(pos.x, pos.y, getWalkMask())) + { + setAction(STAND); + return; + } + + mX = pos.x; + mY = pos.y; + setAction(MOVE); + mActionTime += static_cast<int>(mWalkSpeed.x / 10); +} + +void Being::logic() +{ + BLOCK_START("Being::logic") + // Reduce the time that speech is still displayed + if (mSpeechTime > 0) + mSpeechTime--; + + // Remove text and speechbubbles if speech boxes aren't being used + if (mSpeechTime == 0 && mText) + { + delete mText; + mText = nullptr; + } + + const int time = tick_time * MILLISECONDS_IN_A_TICK; + if (mEmotionSprite) + mEmotionSprite->update(time); + + if (mAnimationEffect) + { + mAnimationEffect->update(time); + if (mAnimationEffect->isTerminated()) + { + delete mAnimationEffect; + mAnimationEffect = nullptr; + } + } + + int frameCount = static_cast<int>(getFrameCount()); +#ifdef MANASERV_SUPPORT + if ((Net::getNetworkType() == ServerInfo::MANASERV) && (mAction != DEAD)) + { + const Vector dest = (mPath.empty()) ? + mDest : Vector(static_cast<float>(mPath.front().x), + static_cast<float>(mPath.front().y)); + + // This is a hack that stops NPCs from running off the map... + if (mDest.x <= 0 && mDest.y <= 0) + { + BLOCK_END("Being::logic") + return; + } + + // The Vector representing the difference between current position + // and the next destination path node. + Vector dir = dest - mPos; + + const float nominalLength = dir.length(); + + // When we've not reached our destination, move to it. + if (nominalLength > 0.0f && !mWalkSpeed.isNull()) + { + // The deplacement of a point along a vector is calculated + // using the Unit Vector (â) multiplied by the point speed. + // â = a / ||a|| (||a|| is the a length.) + // Then, diff = (dir/||dir||) * speed. + const Vector normalizedDir = dir.normalized(); + Vector diff(normalizedDir.x * mWalkSpeed.x, + normalizedDir.y * mWalkSpeed.y); + + // Test if we don't miss the destination by a move too far: + if (diff.length() > nominalLength) + { + setPosition(mPos + dir); + + // Also, if the destination is reached, try to get the next + // path point, if existing. + if (!mPath.empty()) + mPath.pop_front(); + } + // Otherwise, go to it using the nominal speed. + else + { + setPosition(mPos + diff); + } + + if (mAction != MOVE) + setAction(MOVE); + + // Update the player sprite direction. + // N.B.: We only change this if the distance is more than one pixel. + if (nominalLength > 1.0f) + { + int direction = 0; + const float dx = std::abs(dir.x); + float dy = std::abs(dir.y); + + // When not using mouse for the player, we slightly prefer + // UP and DOWN position, especially when walking diagonally. + if (player_node && this == player_node && + !player_node->isPathSetByMouse()) + { + dy = dy + 2; + } + + if (dx > dy) + direction |= (dir.x > 0) ? RIGHT : LEFT; + else + direction |= (dir.y > 0) ? DOWN : UP; + + setDirection(static_cast<uint8_t>(direction)); + } + } + else if (!mPath.empty()) + { + // If the current path node has been reached, + // remove it and go to the next one. + mPath.pop_front(); + } + else if (mAction == MOVE) + { + setAction(STAND); + } + } + else + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + switch (mAction) + { + case STAND: + case SIT: + case DEAD: + case HURT: + case SPAWN: + default: + break; + + case MOVE: + { + if (getWalkSpeed().x && static_cast<int> ((static_cast<float>( + get_elapsed_time(mActionTime)) * static_cast<float>( + frameCount)) / getWalkSpeed().x) + >= frameCount) + { + nextTile(); + } + break; + } + + case ATTACK: + { +// std::string particleEffect(""); + + if (!mActionTime) + break; + + int curFrame = 0; + if (mAttackSpeed) + { + curFrame = (get_elapsed_time(mActionTime) * frameCount) + / mAttackSpeed; + } + + if (this == player_node && curFrame >= frameCount) + nextTile(); + + break; + } + } + + // Update pixel coordinates + setPosition(static_cast<float>(mX * 32 + 16 + getXOffset()), + static_cast<float>(mY * 32 + 32 + getYOffset())); + } + + if (mEmotionSprite) + { + mEmotionTime--; + if (mEmotionTime == 0) + { + delete mEmotionSprite; + mEmotionSprite = nullptr; + } + } + + ActorSprite::logic(); + + if (frameCount < 10) + frameCount = 10; + + if (!isAlive() && getWalkSpeed().x + && Net::getGameHandler()->removeDeadBeings() + && static_cast<int> ((static_cast<float>(get_elapsed_time(mActionTime)) + / static_cast<float>(getWalkSpeed().x))) >= frameCount) + { + if (mType != PLAYER && actorSpriteManager) + actorSpriteManager->destroy(this); + } + + const SoundInfo *const sound = mNextSound.sound; + if (sound) + { + const int time2 = tick_time; + if (time2 > mNextSound.time) + { + soundManager.playSfx(sound->sound, mNextSound.x, mNextSound.y); + + mNextSound.sound = nullptr; + mNextSound.time = time2 + sound->delay; + } + } + + BLOCK_END("Being::logic") +} + +void Being::drawEmotion(Graphics *const graphics, const int offsetX, + const int offsetY) +{ + const int px = getPixelX() - offsetX - 16; + const int py = getPixelY() - offsetY - 64 - 32; + if (mEmotionSprite) + mEmotionSprite->draw(graphics, px, py); + if (mAnimationEffect) + mAnimationEffect->draw(graphics, px, py); +} + +void Being::drawSpeech(const int offsetX, const int offsetY) +{ + if (!mSpeechBubble || mSpeech.empty()) + return; + + const int px = getPixelX() - offsetX; + const int py = getPixelY() - offsetY; + const int speech = mSpeechType; + + // Draw speech above this being + if (mSpeechTime == 0) + { + if (mSpeechBubble->isVisible()) + mSpeechBubble->setVisible(false); + } + else if (mSpeechTime > 0 && (speech == NAME_IN_BUBBLE || + speech == NO_NAME_IN_BUBBLE)) + { + const bool isShowName = (speech == NAME_IN_BUBBLE); + + delete mText; + mText = nullptr; + + mSpeechBubble->setCaption(isShowName ? mName : ""); + + mSpeechBubble->setText(mSpeech, isShowName); + mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), + py - getHeight() - (mSpeechBubble->getHeight())); + mSpeechBubble->setVisible(true); + } + else if (mSpeechTime > 0 && speech == TEXT_OVERHEAD) + { + mSpeechBubble->setVisible(false); + + if (!mText && userPalette) + { + mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), + gcn::Graphics::CENTER, &Theme::getThemeColor( + Theme::BUBBLE_TEXT), true); + } + } + else if (speech == NO_SPEECH) + { + mSpeechBubble->setVisible(false); + + delete mText; + mText = nullptr; + } +} + +int Being::getOffset(const signed char pos, const signed char neg) const +{ + // Check whether we're walking in the requested direction + if (mAction != MOVE || !(mDirection & (pos | neg))) + return 0; + + int offset = 0; + + if (mMap) + { + const int time = get_elapsed_time(mActionTime); + offset = (pos == LEFT && neg == RIGHT) ? + static_cast<int>((static_cast<float>(time) + * static_cast<float>(mMap->getTileWidth())) + / static_cast<float>(mWalkSpeed.x)) : + static_cast<int>((static_cast<float>(time) + * static_cast<float>(mMap->getTileHeight())) + / static_cast<float>(mWalkSpeed.y)); + } + + // We calculate the offset _from_ the _target_ location + offset -= 32; + if (offset > 0) + offset = 0; + + // Going into negative direction? Invert the offset. + if (mDirection & pos) + offset = -offset; + + if (offset > 32) + offset = 32; + if (offset < -32) + offset = -32; + + return offset; +} + +void Being::updateCoords() +{ + if (!mDispName) + return; + + int offsetX = getPixelX(); + int offsetY = getPixelY(); + if (mInfo) + { + offsetX += mInfo->getNameOffsetX(); + offsetY += mInfo->getNameOffsetY(); + } + // Monster names show above the sprite instead of below it + if (mType == MONSTER) + offsetY += - getHeight() - mDispName->getHeight(); + + mDispName->adviseXY(offsetX, offsetY, mMoveNames); +} + +void Being::optionChanged(const std::string &value) +{ + if (mType == PLAYER && value == "visiblenames") + setShowName(config.getBoolValue("visiblenames")); +} + +void Being::flashName(const int time) +{ + if (mDispName) + mDispName->flash(time); +} + +std::string Being::getGenderSignWithSpace() const +{ + const std::string &str = getGenderSign(); + if (str.empty()) + return str; + else + return std::string(" ").append(str); +} + +std::string Being::getGenderSign() const +{ + std::string str; + if (mShowGender) + { + if (getGender() == GENDER_FEMALE) + str = "\u2640"; + else if (getGender() == GENDER_MALE) + str = "\u2642"; + } + if (mShowPlayersStatus && mAdvanced) + { + if (mShop) + str.append("$"); + if (mAway) + { + // TRANSLATORS: this away status writed in player nick + str.append(_("A")); + } + else if (mInactive) + { + // TRANSLATORS: this inactive status writed in player nick + str.append(_("I")); + } + } + return str; +} + +void Being::showName() +{ + if (mName.empty()) + return; + + delete mDispName; + mDispName = nullptr; + + if (mHideErased && player_relations.getRelation(mName) == + PlayerRelation::ERASED) + { + return; + } + + std::string displayName(mName); + + if (mType != MONSTER && (mShowGender || mShowLevel)) + { + displayName.append(" "); + if (mShowLevel && getLevel() != 0) + displayName.append(toString(getLevel())); + + displayName.append(getGenderSign()); + } + + if (mType == MONSTER) + { + if (config.getBoolValue("showMonstersTakedDamage")) + displayName.append(", ").append(toString(getDamageTaken())); + } + + gcn::Font *font = nullptr; + if (player_node && player_node->getTarget() == this + && mType != MONSTER) + { + font = boldFont; + } + else if (mType == PLAYER && !player_relations.isGoodName(this) && gui) + { + font = gui->getSecureFont(); + } + + if (mInfo) + { + mDispName = new FlashText(displayName, + getPixelX() + mInfo->getNameOffsetX(), + getPixelY() + mInfo->getNameOffsetY(), + gcn::Graphics::CENTER, mNameColor, font); + } + else + { + mDispName = new FlashText(displayName, getPixelX(), getPixelY(), + gcn::Graphics::CENTER, mNameColor, font); + } + + updateCoords(); +} + +void Being::updateColors() +{ + if (userPalette) + { + if (mType == MONSTER) + { + mNameColor = &userPalette->getColor(UserPalette::MONSTER); + mTextColor = &userPalette->getColor(UserPalette::MONSTER); + } + else if (mType == NPC) + { + mNameColor = &userPalette->getColor(UserPalette::NPC); + mTextColor = &userPalette->getColor(UserPalette::NPC); + } + else if (this == player_node) + { + mNameColor = &userPalette->getColor(UserPalette::SELF); + mTextColor = &Theme::getThemeColor(Theme::PLAYER); + } + else + { + mTextColor = &Theme::getThemeColor(Theme::PLAYER); + + if (player_relations.getRelation(mName) != PlayerRelation::ERASED) + mErased = false; + else + mErased = true; + + if (mIsGM) + { + mTextColor = &userPalette->getColor(UserPalette::GM); + mNameColor = &userPalette->getColor(UserPalette::GM); + } + else if (mEnemy) + { + mNameColor = &userPalette->getColor(UserPalette::MONSTER); + } + else if (mParty && mParty == player_node->getParty()) + { + mNameColor = &userPalette->getColor(UserPalette::PARTY); + } + else if (player_node && getGuild() + && getGuild() == player_node->getGuild()) + { + mNameColor = &userPalette->getColor(UserPalette::GUILD); + } + else if (player_relations.getRelation(mName) == + PlayerRelation::FRIEND) + { + mNameColor = &userPalette->getColor(UserPalette::FRIEND); + } + else if (player_relations.getRelation(mName) == + PlayerRelation::DISREGARDED + || player_relations.getRelation(mName) == + PlayerRelation::BLACKLISTED) + { + mNameColor = &userPalette->getColor(UserPalette::DISREGARDED); + } + else if (player_relations.getRelation(mName) == + PlayerRelation::IGNORED + || player_relations.getRelation(mName) == + PlayerRelation::ENEMY2) + { + mNameColor = &userPalette->getColor(UserPalette::IGNORED); + } + else if (player_relations.getRelation(mName) == + PlayerRelation::ERASED) + { + mNameColor = &userPalette->getColor(UserPalette::ERASED); + } + else + { + mNameColor = &userPalette->getColor(UserPalette::PC); + } + } + + if (mDispName) + mDispName->setColor(mNameColor); + } +} + +void Being::setSprite(const unsigned int slot, const int id, + std::string color, const unsigned char colorId, + const bool isWeapon, const bool isTempSprite) +{ + if (slot >= Net::getCharServerHandler()->maxSprite()) + return; + + if (slot >= size()) + ensureSize(slot + 1); + + if (slot >= mSpriteIDs.size()) + mSpriteIDs.resize(slot + 1, 0); + + if (slot >= mSpriteColors.size()) + mSpriteColors.resize(slot + 1, ""); + + if (slot >= mSpriteColorsIds.size()) + mSpriteColorsIds.resize(slot + 1, 1); + + // id = 0 means unequip + if (id == 0) + { + removeSprite(slot); + + if (isWeapon) + mEquippedWeapon = nullptr; + const int id1 = mSpriteIDs[slot]; + if (id1) + { + const ItemInfo &info = ItemDB::get(id1); + if (mMap) + { + const int pet = info.getPet(); + if (pet) + removePet(); + } + } + } + else + { + const ItemInfo &info = ItemDB::get(id); + const std::string filename = info.getSprite(mGender, mSubType); + AnimatedSprite *equipmentSprite = nullptr; + + if (mType == PLAYER) + { + const int pet = info.getPet(); + if (pet) + addPet(pet); + } + + if (!filename.empty()) + { + if (color.empty()) + color = info.getDyeColorsString(colorId); + + equipmentSprite = AnimatedSprite::delayedLoad( + paths.getStringValue("sprites").append( + combineDye(filename, color))); + } + + if (equipmentSprite) + equipmentSprite->setSpriteDirection(getSpriteDirection()); + + CompoundSprite::setSprite(slot, equipmentSprite); + + if (isWeapon) + mEquippedWeapon = &ItemDB::get(id); + + setAction(mAction); + } + + if (!isTempSprite) + { + mSpriteIDs[slot] = id; + mSpriteColors[slot] = color; + mSpriteColorsIds[slot] = colorId; + recalcSpritesOrder(); + if (beingEquipmentWindow) + beingEquipmentWindow->updateBeing(this); + } +} + +void Being::setSpriteID(const unsigned int slot, const int id) +{ + setSprite(slot, id, mSpriteColors[slot]); +} + +void Being::setSpriteColor(const unsigned int slot, const std::string &color) +{ + setSprite(slot, mSpriteIDs[slot], color); +} + +void Being::setHairStyle(const unsigned int slot, const int id) +{ +// dumpSprites(); + setSprite(slot, id, ItemDB::get(id).getDyeColorsString(mHairColor)); +// dumpSprites(); +} + +void Being::setHairColor(const unsigned int slot, const unsigned char color) +{ + mHairColor = color; + setSprite(slot, mSpriteIDs[slot], ItemDB::get( + getSpriteID(slot)).getDyeColorsString(color)); +} + +void Being::dumpSprites() const +{ + std::vector<int>::const_iterator it1 = mSpriteIDs.begin(); + const std::vector<int>::const_iterator it1_end = mSpriteIDs.end(); + StringVectCIter it2 = mSpriteColors.begin(); + const StringVectCIter it2_end = mSpriteColors.end(); + std::vector<int>::const_iterator it3 = mSpriteColorsIds.begin(); + const std::vector<int>::const_iterator it3_end = mSpriteColorsIds.end(); + + logger->log("sprites"); + for (; it1 != it1_end && it2 != it2_end && it3 != it3_end; + ++ it1, ++ it2, ++ it3) + { + logger->log("%d,%s,%d", *it1, (*it2).c_str(), *it3); + } +} + +void Being::load() +{ + // Hairstyles are encoded as negative numbers. Count how far negative + // we can go. + int hairstyles = 1; + while (ItemDB::get(-hairstyles).getSprite(GENDER_MALE, 0) != + paths.getStringValue("spriteErrorFile")) + { + hairstyles ++; + } + mNumberOfHairstyles = hairstyles; + + int races = 100; + while (ItemDB::get(-races).getSprite(GENDER_MALE, 0) != + paths.getStringValue("spriteErrorFile")) + { + races ++; + } + mNumberOfRaces = races - 100; +} + +void Being::updateName() +{ + if (mShowName) + showName(); +} + +void Being::reReadConfig() +{ + BLOCK_START("Being::reReadConfig") + if (mUpdateConfigTime + 1 < cur_time) + { + mAwayEffect = paths.getIntValue("afkEffectId"); + mHighlightMapPortals = config.getBoolValue("highlightMapPortals"); + mConfLineLim = config.getIntValue("chatMaxCharLimit"); + mSpeechType = config.getIntValue("speech"); + mHighlightMonsterAttackRange = + config.getBoolValue("highlightMonsterAttackRange"); + mLowTraffic = config.getBoolValue("lowTraffic"); + mDrawHotKeys = config.getBoolValue("drawHotKeys"); + mShowBattleEvents = config.getBoolValue("showBattleEvents"); + mShowMobHP = config.getBoolValue("showMobHP"); + mShowOwnHP = config.getBoolValue("showOwnHP"); + mShowGender = config.getBoolValue("showgender"); + mShowLevel = config.getBoolValue("showlevel"); + mShowPlayersStatus = config.getBoolValue("showPlayersStatus"); + mEnableReorderSprites = config.getBoolValue("enableReorderSprites"); + mHideErased = config.getBoolValue("hideErased"); + mMoveNames = config.getBoolValue("moveNames"); + + mUpdateConfigTime = cur_time; + } + BLOCK_END("Being::reReadConfig") +} + +bool Being::updateFromCache() +{ + const BeingCacheEntry *const entry = Being::getCacheEntry(getId()); + + if (entry && entry->getTime() + 120 >= cur_time) + { + if (!entry->getName().empty()) + setName(entry->getName()); + setPartyName(entry->getPartyName()); + setGuildName(entry->getGuildName()); + setLevel(entry->getLevel()); + setPvpRank(entry->getPvpRank()); + setIp(entry->getIp()); + + mAdvanced = entry->isAdvanced(); + if (mAdvanced) + { + const int flags = entry->getFlags(); + mShop = ((flags & FLAG_SHOP) != 0); + mAway = ((flags & FLAG_AWAY) != 0); + mInactive = ((flags & FLAG_INACTIVE) != 0); + if (mShop || mAway || mInactive) + updateName(); + } + else + { + mShop = false; + mAway = false; + mInactive = false; + } + + updateAwayEffect(); + if (mType == PLAYER) + updateColors(); + return true; + } + return false; +} + +void Being::addToCache() const +{ + if (player_node == this) + return; + + BeingCacheEntry *entry = Being::getCacheEntry(getId()); + if (!entry) + { + entry = new BeingCacheEntry(getId()); + beingInfoCache.push_front(entry); + + if (beingInfoCache.size() >= CACHE_SIZE) + { + delete beingInfoCache.back(); + beingInfoCache.pop_back(); + } + } + if (!mLowTraffic) + return; + + entry->setName(getName()); + entry->setLevel(getLevel()); + entry->setPartyName(getPartyName()); + entry->setGuildName(getGuildName()); + entry->setTime(cur_time); + entry->setPvpRank(getPvpRank()); + entry->setIp(getIp()); + entry->setAdvanced(isAdvanced()); + if (isAdvanced()) + { + int flags = 0; + if (mShop) + flags += FLAG_SHOP; + if (mAway) + flags += FLAG_AWAY; + if (mInactive) + flags += FLAG_INACTIVE; + entry->setFlags(flags); + } + else + { + entry->setFlags(0); + } +} + +BeingCacheEntry* Being::getCacheEntry(const int id) +{ + FOR_EACH (std::list<BeingCacheEntry*>::iterator, i, beingInfoCache) + { + if (!*i) + continue; + + if (id == (*i)->getId()) + { + // Raise priority: move it to front + if ((*i)->getTime() + 120 < cur_time) + { + beingInfoCache.splice(beingInfoCache.begin(), + beingInfoCache, i); + } + return *i; + } + } + return nullptr; +} + + +void Being::setGender(const Gender gender) +{ + if (gender != mGender) + { + mGender = gender; + + // Reload all subsprites + for (unsigned int i = 0; i < mSpriteIDs.size(); i++) + { + if (mSpriteIDs.at(i) != 0) + setSprite(i, mSpriteIDs.at(i), mSpriteColors.at(i)); + } + + updateName(); + } +} + +void Being::setGM(const bool gm) +{ + mIsGM = gm; + + updateColors(); +} + +void Being::talkTo() const +{ + if (!client->limitPackets(PACKET_NPC_TALK)) + return; + + Net::getNpcHandler()->talk(mId); +} + +bool Being::draw(Graphics *const graphics, + const int offsetX, const int offsetY) const +{ + bool res = true; + if (!mErased) + res = ActorSprite::draw(graphics, offsetX, offsetY); + + return res; +} + +void Being::drawSprites(Graphics *const graphics, + const int posX, const int posY) const +{ + const int sz = getNumberOfLayers(); + for (int f = 0; f < sz; f ++) + { + const int rSprite = mSpriteHide[mSpriteRemap[f]]; + if (rSprite == 1) + continue; + + Sprite *const sprite = getSprite(mSpriteRemap[f]); + if (sprite) + { + sprite->setAlpha(mAlpha); + sprite->draw(graphics, posX, posY); + } + } +} + +void Being::drawSpritesSDL(Graphics *const graphics, + const int posX, const int posY) const +{ + const size_t sz = size(); + for (unsigned f = 0; f < sz; f ++) + { + const int rSprite = mSpriteHide[mSpriteRemap[f]]; + if (rSprite == 1) + continue; + + const Sprite *const sprite = getSprite(mSpriteRemap[f]); + if (sprite) + sprite->draw(graphics, posX, posY); + } +} + +bool Being::drawSpriteAt(Graphics *const graphics, + const int x, const int y) const +{ + bool res = true; + + if (!mErased) + res = ActorSprite::drawSpriteAt(graphics, x, y); + + if (!userPalette) + return res; + + if (mHighlightMapPortals && mMap && mSubType == 45 && !mMap->getHasWarps()) + { + graphics->setColor(userPalette-> + getColorWithAlpha(UserPalette::PORTAL_HIGHLIGHT)); + + graphics->fillRectangle(gcn::Rectangle(x, y, 32, 32)); + + if (mDrawHotKeys && !mName.empty()) + { + gcn::Font *const font = gui->getFont(); + if (font) + { + graphics->setColor(userPalette->getColor(UserPalette::BEING)); + font->drawString(graphics, mName, x, y); + } + } + } + + if (mHighlightMonsterAttackRange && mType == ActorSprite::MONSTER + && isAlive()) + { + int attackRange; + if (mAttackRange) + attackRange = 32 * mAttackRange; + else + attackRange = 32; + + graphics->setColor(userPalette->getColorWithAlpha( + UserPalette::MONSTER_ATTACK_RANGE)); + + graphics->fillRectangle(gcn::Rectangle( + x - attackRange, y - attackRange, + 2 * attackRange + 32, 2 * attackRange + 32)); + } + + if (mShowMobHP && mInfo && player_node && player_node->getTarget() == this + && mType == MONSTER) + { + // show hp bar here + int maxHP = mMaxHP; + if (!maxHP) + maxHP = mInfo->getMaxHP(); + + drawHpBar(graphics, maxHP, mHP, mDamageTaken, + UserPalette::MONSTER_HP, UserPalette::MONSTER_HP2, + x - 50 + 16 + mInfo->getHpBarOffsetX(), + y + 32 - 6 + mInfo->getHpBarOffsetY(), + 2 * 50, 4); + } + if (mShowOwnHP && mInfo && player_node == this && mAction != DEAD) + { + drawHpBar(graphics, PlayerInfo::getAttribute(PlayerInfo::MAX_HP), + PlayerInfo::getAttribute(PlayerInfo::HP), 0, + UserPalette::PLAYER_HP, UserPalette::PLAYER_HP2, + x - 50 + 16 + mInfo->getHpBarOffsetX(), + y + 32 - 6 + mInfo->getHpBarOffsetY(), + 2 * 50, 4); + } + return res; +} + +void Being::drawHpBar(Graphics *const graphics, const int maxHP, const int hp, + const int damage, const int color1, const int color2, + const int x, const int y, const int width, + const int height) const +{ + if (maxHP <= 0 || !userPalette) + return; + + float p; + + if (hp) + { + p = static_cast<float>(maxHP) / static_cast<float>(hp); + } + else if (maxHP != damage) + { + p = static_cast<float>(maxHP) + / static_cast<float>(maxHP - damage); + } + else + { + p = 1; + } + + if (p <= 0 || p > width) + return; + + const int dx = static_cast<const int>(static_cast<float>(width) / p); + + if (serverVersion < 1) + { // old servers + if ((!damage && (this != player_node || hp == maxHP)) + || (!hp && maxHP == damage)) + { + graphics->setColor(userPalette->getColorWithAlpha(color1)); + graphics->fillRectangle(gcn::Rectangle( + x, y, dx, height)); + return; + } + else if (width - dx <= 0) + { + graphics->setColor(userPalette->getColorWithAlpha(color2)); + graphics->fillRectangle(gcn::Rectangle( + x, y, width, height)); + return; + } + } + else + { // evol servers + if (hp == maxHP) + { + graphics->setColor(userPalette->getColorWithAlpha(color1)); + graphics->fillRectangle(gcn::Rectangle( + x, y, dx, height)); + return; + } + else if (width - dx <= 0) + { + graphics->setColor(userPalette->getColorWithAlpha(color2)); + graphics->fillRectangle(gcn::Rectangle( + x, y, width, height)); + return; + } + } + + graphics->setColor(userPalette->getColorWithAlpha(color1)); + graphics->fillRectangle(gcn::Rectangle( + x, y, dx, height)); + + graphics->setColor(userPalette->getColorWithAlpha(color2)); + graphics->fillRectangle(gcn::Rectangle( + x + dx, y, width - dx, height)); +} + +void Being::setHP(const int hp) +{ + mHP = hp; + if (mMaxHP < mHP) + mMaxHP = mHP; + if (mType == MONSTER) + updatePercentHP(); +} + +void Being::setMaxHP(const int hp) +{ + mMaxHP = hp; + if (mMaxHP < mHP) + mMaxHP = mHP; +} + +void Being::resetCounters() +{ + mMoveTime = 0; + mAttackTime = 0; + mTalkTime = 0; + mOtherTime = 0; + mTestTime = cur_time; +} + +void Being::recalcSpritesOrder() +{ + if (!mEnableReorderSprites) + return; + +// logger->log("recalcSpritesOrder"); + const unsigned sz = static_cast<unsigned>(size()); + if (sz < 1) + return; + + std::vector<int> slotRemap; + std::map<int, int> itemSlotRemap; + + std::vector<int>::iterator it; + int oldHide[20]; + int dir = mSpriteDirection; + if (dir < 0 || dir >= 9) + dir = 0; + // hack for allow different logic in dead player + if (mAction == DEAD) + dir = 9; + + const unsigned int hairSlot = Net::getCharServerHandler()->hairSprite(); + + for (unsigned slot = 0; slot < sz; slot ++) + { + oldHide[slot] = mSpriteHide[slot]; + mSpriteHide[slot] = 0; + } + + const size_t spriteIdSize = mSpriteIDs.size(); + for (unsigned slot = 0; slot < sz; slot ++) + { + slotRemap.push_back(slot); + + if (spriteIdSize <= slot) + continue; + + const int id = mSpriteIDs[slot]; + if (!id) + continue; + + const ItemInfo &info = ItemDB::get(id); + + if (info.isRemoveSprites()) + { + SpriteToItemMap *const spriteToItems + = info.getSpriteToItemReplaceMap(dir); + + if (spriteToItems) + { + FOR_EACHP (SpriteToItemMapCIter, itr, spriteToItems) + { + const int remSprite = itr->first; + const std::map<int, int> &itemReplacer = itr->second; + if (remSprite >= 0) + { // slot known + if (itemReplacer.empty()) + { + mSpriteHide[remSprite] = 1; + } + else + { + std::map<int, int>::const_iterator repIt + = itemReplacer.find(mSpriteIDs[remSprite]); + if (repIt == itemReplacer.end()) + { + repIt = itemReplacer.find(0); + if (repIt->second == 0) + repIt = itemReplacer.end(); + } + if (repIt != itemReplacer.end()) + { + mSpriteHide[remSprite] = repIt->second; + if (repIt->second != 1) + { + if (static_cast<unsigned>(remSprite) + != hairSlot) + { + setSprite(remSprite, repIt->second, + mSpriteColors[remSprite], + 1, false, true); + } + else + { + setSprite(remSprite, repIt->second, + ItemDB::get(repIt->second) + .getDyeColorsString(mHairColor), + 1, false, true); + } + } + } + } + } + else + { // slot unknown. Search for real slot, this can be slow + FOR_EACH (IntMapCIter, repIt, itemReplacer) + { + for (unsigned slot2 = 0; slot2 < sz; slot2 ++) + { + if (mSpriteIDs[slot2] == repIt->first) + { + mSpriteHide[slot2] = repIt->second; + if (repIt->second != 1) + { + if (slot2 != hairSlot) + { + setSprite(slot2, repIt->second, + mSpriteColors[slot2], + 1, false, true); + } + else + { + setSprite(slot2, repIt->second, + ItemDB::get(repIt->second) + .getDyeColorsString( + mHairColor), + 1, false, true); + } + } + } + } + } + } + } + } + } + + if (info.mDrawBefore[dir] > 0) + { + const int id2 = mSpriteIDs[info.mDrawBefore[dir]]; + if (itemSlotRemap.find(id2) != itemSlotRemap.end()) + { +// logger->log("found duplicate (before)"); + const ItemInfo &info2 = ItemDB::get(id2); + if (info.mDrawPriority[dir] < info2.mDrawPriority[dir]) + { +// logger->log("old more priority"); + continue; + } + else + { +// logger->log("new more priority"); + itemSlotRemap.erase(id2); + } + } + + itemSlotRemap[id] = -info.mDrawBefore[dir]; + } + else if (info.mDrawAfter[dir] > 0) + { + const int id2 = mSpriteIDs[info.mDrawAfter[dir]]; + if (itemSlotRemap.find(id2) != itemSlotRemap.end()) + { + const ItemInfo &info2 = ItemDB::get(id2); + if (info.mDrawPriority[dir] < info2.mDrawPriority[dir]) + { +// logger->log("old more priority"); + continue; + } + else + { +// logger->log("new more priority"); + itemSlotRemap.erase(id2); + } + } + + itemSlotRemap[id] = info.mDrawAfter[dir]; +// logger->log("item slot->slot %d %d->%d", id, slot, itemSlotRemap[id]); + } + } +// logger->log("preparation end"); + + int lastRemap = 0; + unsigned cnt = 0; + + while (cnt < 15 && lastRemap >= 0) + { + lastRemap = -1; + cnt ++; +// logger->log("iteration"); + + for (unsigned slot0 = 0; slot0 < sz; slot0 ++) + { + const int slot = searchSlotValue(slotRemap, slot0); + const int val = slotRemap.at(slot); + int id = 0; + + if (static_cast<int>(spriteIdSize) > val) + id = mSpriteIDs[val]; + + int idx = -1; + int idx1 = -1; +// logger->log("item %d, id=%d", slot, id); + int reorder = 0; + const std::map<int, int>::const_iterator + orderIt = itemSlotRemap.find(id); + if (orderIt != itemSlotRemap.end()) + reorder = orderIt->second; + + if (reorder < 0) + { +// logger->log("move item %d before %d", slot, -reorder); + searchSlotValueItr(it, idx, slotRemap, -reorder); + if (it == slotRemap.end()) + return; + searchSlotValueItr(it, idx1, slotRemap, val); + if (it == slotRemap.end()) + return; + lastRemap = idx1; + if (idx1 + 1 != idx) + { + slotRemap.erase(it); + searchSlotValueItr(it, idx, slotRemap, -reorder); + slotRemap.insert(it, val); + } + } + else if (reorder > 0) + { +// logger->log("move item %d after %d", slot, reorder); + searchSlotValueItr(it, idx, slotRemap, reorder); + searchSlotValueItr(it, idx1, slotRemap, val); + if (it == slotRemap.end()) + return; + lastRemap = idx1; + if (idx1 != idx + 1) + { + slotRemap.erase(it); + searchSlotValueItr(it, idx, slotRemap, reorder); + if (it != slotRemap.end()) + { + ++ it; + if (it != slotRemap.end()) + slotRemap.insert(it, val); + else + slotRemap.push_back(val); + } + else + { + slotRemap.push_back(val); + } + } + } + } + } + +// logger->log("after remap"); + for (unsigned slot = 0; slot < sz; slot ++) + { + mSpriteRemap[slot] = slotRemap[slot]; + if (oldHide[slot] != 0 && oldHide[slot] != 1 && mSpriteHide[slot] == 0) + { + const int id = mSpriteIDs[slot]; + if (!id) + continue; + + setSprite(slot, id, mSpriteColors[slot], 1, false, true); + } +// logger->log("slot %d = %d", slot, mSpriteRemap[slot]); + } +} + +int Being::searchSlotValue(const std::vector<int> &slotRemap, + const int val) const +{ + const size_t sz = size(); + for (size_t slot = 0; slot < sz; slot ++) + { + if (slotRemap[slot] == val) + return slot; + } + return getNumberOfLayers() - 1; +} + +void Being::searchSlotValueItr(std::vector<int>::iterator &it, int &idx, + std::vector<int> &slotRemap, + const int val) const +{ +// logger->log("searching %d", val); + it = slotRemap.begin(); + const std::vector<int>::iterator it_end = slotRemap.end(); + idx = 0; + while (it != it_end) + { +// logger->log("testing %d", *it); + if (*it == val) + { +// logger->log("found at %d", idx); + return; + } + ++ it; + idx ++; + } +// logger->log("not found"); + idx = -1; + return; +} + +void Being::updateHit(const int amount) +{ + if (amount > 0) + { + if (!mMinHit || amount < mMinHit) + mMinHit = amount; + if (amount != mCriticalHit && (!mMaxHit || amount > mMaxHit)) + mMaxHit = amount; + } +} + +Equipment *Being::getEquipment() +{ + Equipment *const eq = new Equipment(); + Equipment::Backend *const bk = new BeingEquipBackend(this); + eq->setBackend(bk); + return eq; +} + +void Being::undressItemById(const int id) +{ + const size_t sz = mSpriteIDs.size(); + + for (size_t f = 0; f < sz; f ++) + { + if (id == mSpriteIDs[f]) + { + setSprite(static_cast<unsigned int>(f), 0); + break; + } + } +} + +void Being::clearCache() +{ + delete_all(beingInfoCache); + beingInfoCache.clear(); +} + +void Being::updateComment() +{ + if (mGotComment || mName.empty()) + return; + + mGotComment = true; + mComment = loadComment(mName, mType); +} + +std::string Being::loadComment(const std::string &name, const int type) +{ + std::string str; + switch (type) + { + case PLAYER: + str = client->getUsersDirectory(); + break; + case NPC: + str = client->getNpcsDirectory(); + break; + default: + return ""; + } + + str.append(stringToHexPath(name)).append("/comment.txt"); + logger->log("load from: %s", str.c_str()); + StringVect lines; + + const ResourceManager *const resman = ResourceManager::getInstance(); + if (resman->existsLocal(str)) + { + lines = resman->loadTextFileLocal(str); + if (lines.size() >= 2) + return lines[1]; + } + return ""; +} + +void Being::saveComment(const std::string &name, + const std::string &comment, const int type) +{ + std::string dir; + switch (type) + { + case PLAYER: + dir = client->getUsersDirectory(); + break; + case NPC: + dir = client->getNpcsDirectory(); + break; + default: + return; + } + dir.append(stringToHexPath(name)); + const ResourceManager *const resman = ResourceManager::getInstance(); + resman->saveTextFile(dir, "comment.txt", + (name + "\n").append(comment)); +} + +void Being::setState(const uint8_t state) +{ + const bool shop = ((state & FLAG_SHOP) != 0); + const bool away = ((state & FLAG_AWAY) != 0); + const bool inactive = ((state & FLAG_INACTIVE) != 0); + const bool needUpdate = (shop != mShop || away != mAway + || inactive != mInactive); + + mShop = shop; + mAway = away; + mInactive = inactive; + updateAwayEffect(); + + if (needUpdate) + { + if (shop || away || inactive) + mAdvanced = true; + updateName(); + addToCache(); + } +} + +void Being::setEmote(const uint8_t emotion, const int emote_time) +{ + if ((emotion & FLAG_SPECIAL) == FLAG_SPECIAL) + { + setState(emotion); + mAdvanced = true; + } + else + { + const int emotionIndex = emotion - 1; + if (emotionIndex >= 0 && emotionIndex <= EmoteDB::getLast()) + { + delete mEmotionSprite; + mEmotionSprite = nullptr; + const EmoteInfo *const info = EmoteDB::get2(emotionIndex, true); + if (info) + { + const EmoteSprite *const sprite = info->sprites.front(); + if (sprite) + { + mEmotionSprite = AnimatedSprite::clone(sprite->sprite); + if (mEmotionSprite) + mEmotionTime = info->time; + else + mEmotionTime = emote_time; + } + } + } + + if (mEmotionSprite) + { + mEmotionSprite->play(mSpriteAction); + mEmotionSprite->setSpriteDirection(static_cast<SpriteDirection>( + mSpriteDirection)); + } + else + { + mEmotionTime = 0; + } + } +} + +void Being::updatePercentHP() +{ + if (!mMaxHP || !serverVersion) + return; + if (mHP) + { + const unsigned num = mHP * 100 / mMaxHP; + if (num != mNumber) + { + mNumber = num; + if (updateNumber(mNumber)) + setAction(mAction); + } + } +} + +uint8_t Being::genderToInt(const Gender sex) +{ + switch (sex) + { + case GENDER_FEMALE: + case GENDER_UNSPECIFIED: + default: + return 0; + case GENDER_MALE: + return 1; + case GENDER_OTHER: + return 3; + } +} + +Gender Being::intToGender(const uint8_t sex) +{ + switch (sex) + { + case 0: + default: + return GENDER_FEMALE; + case 1: + return GENDER_MALE; + case 3: + return GENDER_OTHER; + } +} + +int Being::getSpriteID(const int slot) const +{ + if (slot < 0 || static_cast<unsigned>(slot) >= mSpriteIDs.size()) + return -1; + + return mSpriteIDs[slot]; +} + +void Being::addAfkEffect() +{ + addSpecialEffect(mAwayEffect); +} + +void Being::removeAfkEffect() +{ + removeSpecialEffect(); +} + +void Being::addSpecialEffect(const int effect) +{ + if (effectManager && Particle::enabled + && !mSpecialParticle && effect != -1) + { + mSpecialParticle = effectManager->triggerReturn(effect, this); + } +} + +void Being::removeSpecialEffect() +{ + if (effectManager && mSpecialParticle) + { + mChildParticleEffects.removeLocally(mSpecialParticle); + mSpecialParticle = nullptr; + } + delete mAnimationEffect; + mAnimationEffect = nullptr; +} + +void Being::updateAwayEffect() +{ + if (mAway) + addAfkEffect(); + else + removeAfkEffect(); +} + +void Being::addEffect(const std::string &name) +{ + delete mAnimationEffect; + mAnimationEffect = AnimatedSprite::load( + paths.getStringValue("sprites") + name); +} + +void Being::addPet(const int id) +{ + if (!actorSpriteManager) + return; + + removePet(); + Being *const being = actorSpriteManager->createBeing( + id, ActorSprite::PET, 0); + if (being) + { + being->setTileCoords(getTileX(), getTileY()); + being->setOwner(this); + mPetId = id; + mPet = being; + } +} + +void Being::removePet() +{ + if (!actorSpriteManager) + return; + + mPetId = 0; + if (mPet) + { + mPet->setOwner(nullptr); + actorSpriteManager->destroy(mPet); + mPet = nullptr; + } +} + +void Being::updatePets() +{ + removePet(); + FOR_EACH (std::vector<int>::const_iterator, it, mSpriteIDs) + { + const int id = *it; + if (!id) + continue; + const ItemInfo &info = ItemDB::get(id); + const int pet = info.getPet(); + if (pet) + { + addPet(pet); + return; + } + } +} + +void Being::playSfx(const SoundInfo &sound, Being *const being, + const bool main, const int x, const int y) +{ + if (being) + { + // here need add timer and delay sound + const int time = tick_time; + if (main) + { + being->mNextSound.sound = nullptr; + being->mNextSound.time = time + sound.delay; + soundManager.playSfx(sound.sound, x, y); + } + else if (mNextSound.time <= time) + { // old event sound time is gone. we can play new sound + being->mNextSound.sound = nullptr; + being->mNextSound.time = time + sound.delay; + soundManager.playSfx(sound.sound, x, y); + } + else + { // old event sound in progress. need save sound and wait + being->mNextSound.sound = &sound; + being->mNextSound.x = x; + being->mNextSound.y = y; + } + } + else + { + soundManager.playSfx(sound.sound, x, y); + } +} + +void Being::setLook(const int look) +{ + if (mType == PLAYER) + setSubtype(mSubType, look); +} diff --git a/src/being/being.h b/src/being/being.h new file mode 100644 index 000000000..29b735956 --- /dev/null +++ b/src/being/being.h @@ -0,0 +1,1063 @@ +/* + * The ManaPlus Client + * Copyright (C) 2004-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_BEING_H +#define BEING_BEING_H + +#include "equipment.h" + +#include "resources/beinginfo.h" + +#include <guichan/color.hpp> + +#include <SDL_types.h> + +#include <map> +#include <set> + +#include "localconsts.h" + +static const unsigned int FIRST_IGNORE_EMOTE = 14; +static const unsigned int STATUS_EFFECTS = 32; + +static const unsigned int SPEECH_TIME = 500; +static const unsigned int SPEECH_MIN_TIME = 200; +static const unsigned int SPEECH_MAX_TIME = 800; + +static const int DEFAULT_BEING_WIDTH = 32; +static const int DEFAULT_BEING_HEIGHT = 32; + +class AnimatedSprite; +class BeingCacheEntry; +class Being; +class FlashText; +class Guild; +class Inventory; +class ItemInfo; +class Item; +class Particle; +class Party; +class SpeechBubble; +class Text; + +struct Position; + +extern volatile int cur_time; + +enum Gender +{ + GENDER_MALE = 0, + GENDER_FEMALE = 1, + GENDER_UNSPECIFIED = 2, + GENDER_OTHER = 3 +}; + + +struct NextSoundInfo +{ + NextSoundInfo() : + sound(nullptr), + x(0), + y(0), + time(0) + { + } + + const SoundInfo *sound; + int x; + int y; + int time; +}; + +class Being : public ActorSprite, public ConfigListener +{ + public: + friend class BeingEquipBackend; + friend class LocalPlayer; + + enum FLAGS + { + FLAG_SHOP = 1, + FLAG_AWAY = 2, + FLAG_INACTIVE = 4, + FLAG_GENDER_OTHER = 32, + FLAG_GM = 64, + FLAG_GENDER_MALE = 128, + FLAG_SPECIAL = 128 + 64 + }; + + /** + * Action the being is currently performing + * WARNING: Has to be in sync with the same enum in the Being class + * of the server! + */ + enum Action + { + STAND = 0, + MOVE, + ATTACK, + SIT, + DEAD, + HURT, + SPAWN + }; + + enum Speech + { + NO_SPEECH = 0, + TEXT_OVERHEAD, + NO_NAME_IN_BUBBLE, + NAME_IN_BUBBLE + }; + + enum AttackType + { + HIT = 0x00, + CRITICAL = 0x0a, + MULTI = 0x08, + REFLECT = 0x04, + FLEE = 0x0b, + SKILL = 0xff, + MISS = 0xffff // pseudo value for miss attacks + }; + + enum Reachable + { + REACH_UNKNOWN = 0, + REACH_YES = 1, + REACH_NO = 2 + }; + + /** + * Directions, to be used as bitmask values + */ + enum BeingDirection + { + DOWN = 1, + LEFT = 2, + UP = 4, + RIGHT = 8 + }; + + /** + * Constructor. + * + * @param id a unique being id + * @param subtype partly determines the type of the being + * @param map the map the being is on + */ + Being(const int id, const Type type, const uint16_t subtype, + Map *const map); + + A_DELETE_COPY(Being) + + virtual ~Being(); + + Type getType() const A_WARN_UNUSED + { return mType; } + + /** + * Removes all path nodes from this being. + */ + void clearPath(); + + /** + * Returns the time spent in the current action. + */ + int getActionTime() const A_WARN_UNUSED + { return mActionTime; } + + /** + * Set the current action time. + * @see Ea::BeingHandler that set it to tick time. + */ + void setActionTime(const int actionTime) + { mActionTime = actionTime; } + + /** + * Makes this being take the next tile of its path. + * TODO: Used by eAthena only? + */ + virtual void nextTile(); + + /** + * Get the current X pixel offset. + * TODO: Used by eAthena only? + */ + int getXOffset() const A_WARN_UNUSED + { return getOffset(LEFT, RIGHT); } + + /** + * Get the current Y pixel offset. + * TODO: Used by eAthena only? + */ + int getYOffset() const A_WARN_UNUSED + { return getOffset(UP, DOWN); } + + /** + * Creates a path for the being from current position to ex and ey + */ + void setDestination(const int dstX, const int dstY); + + /** + * Returns the destination for this being. + */ + const Vector &getDestination() const A_WARN_UNUSED + { return mDest; } + + /** + * Returns the tile x coord + */ + int getTileX() const A_WARN_UNUSED + { return mX; } + + /** + * Returns the tile y coord + */ + int getTileY() const A_WARN_UNUSED + { return mY; } + + /** + * Sets the tile x and y coord + */ + void setTileCoords(const int x, const int y) + { mX = x; mY = y; } + + /** + * Puts a "speech balloon" above this being for the specified amount + * of time. + * + * @param text The text that should appear. + * @param time The amount of time the text should stay in milliseconds. + */ + void setSpeech(const std::string &text, + const std::string &channel = "", + int time = 0); + + /** + * Puts a damage bubble above this being. + * + * @param attacker the attacking being + * @param damage the amount of damage recieved (0 means miss) + * @param type the attack type + * @param id skill id + */ + void takeDamage(Being *const attacker, const int damage, + const AttackType type, const int attackId = 1); + + /** + * Handles an attack of another being by this being. + * + * @param victim the victim being + * @param damage the amount of damage dealt (0 means miss) + * @param type the attack type + */ + void handleAttack(Being *const victim, const int damage, + const int attackId = 1); + + virtual void handleSkill(Being *const victim, const int damage, + const int skillId, const int skillLevel); + + const ItemInfo *getEquippedWeapon() const A_WARN_UNUSED + { return mEquippedWeapon; } + + /** + * Returns the name of the being. + */ + const std::string &getName() const A_WARN_UNUSED + { return mName; } + + /** + * Sets the name for the being. + * + * @param name The name that should appear. + */ + void setName(const std::string &name); + + bool getShowName() const A_WARN_UNUSED + { return mShowName; } + + void setShowName(const bool doShowName); + + /** + * Sets the name of the party the being is in. Shown in BeingPopup. + */ + void setPartyName(const std::string &name) + { mPartyName = name; } + + const std::string &getPartyName() const A_WARN_UNUSED + { return mPartyName; } + + const std::string &getGuildName() const A_WARN_UNUSED + { return mGuildName; } + + /** + * Sets the name of the primary guild the being is in. Shown in + * BeingPopup (eventually). + */ + void setGuildName(const std::string &name); + + void setGuildPos(const std::string &pos); + + /** + * Adds a guild to the being. + */ + void addGuild(Guild *const guild); + + /** + * Removers a guild from the being. + */ + void removeGuild(const int id); + + /** + * Returns a pointer to the specified guild that the being is in. + */ + Guild *getGuild(const std::string &guildName) const A_WARN_UNUSED; + + /** + * Returns a pointer to the specified guild that the being is in. + */ + Guild *getGuild(const int id) const A_WARN_UNUSED; + + /** + * Returns a pointer to the specified guild that the being is in. + */ + Guild *getGuild() const A_WARN_UNUSED; + + /** + * Returns all guilds the being is in. + */ + const std::map<int, Guild*> &getGuilds() const A_WARN_UNUSED + { return mGuilds; } + + /** + * Removes all guilds the being is in. + */ + void clearGuilds(); + + /** + * Get number of guilds the being belongs to. + */ + int16_t getNumberOfGuilds() const A_WARN_UNUSED + { return static_cast<int16_t>(mGuilds.size()); } + + bool isInParty() const A_WARN_UNUSED + { return mParty; } + + void setParty(Party *const party); + + void setGuild(Guild *const guild); + + void updateGuild(); + + Party *getParty() const A_WARN_UNUSED + { return mParty; } + + int getSpritesCount() const A_WARN_UNUSED + { return static_cast<int>(size()); } + + /** + * Sets visible equipments for this being. + */ + void setSprite(const unsigned int slot, const int id, + std::string color = "", + const unsigned char colorId = 1, + const bool isWeapon = false, + const bool isTempSprite = false); + + void setSpriteID(const unsigned int slot, const int id); + + void setSpriteColor(const unsigned int slot, + const std::string &color = ""); + + /** + * Get the number of hairstyles implemented + */ + static int getNumOfHairstyles() A_WARN_UNUSED + { return mNumberOfHairstyles; } + + /** + * Get the number of races implemented + */ + static int getNumOfRaces() A_WARN_UNUSED + { return mNumberOfRaces; } + + /** + * Get the number of layers used to draw the being + */ + int getNumberOfLayers() const A_WARN_UNUSED + { return CompoundSprite::getNumberOfLayers(); } + + /** + * Performs being logic. + */ + virtual void logic() override; + + /** + * Draws the speech text above the being. + */ + void drawSpeech(const int offsetX, const int offsetY); + + /** + * Draws the emotion picture above the being. + */ + void drawEmotion(Graphics *const graphics, const int offsetX, + const int offsetY); + + uint16_t getSubType() const + { return mSubType; } + + /** + * Set Being's subtype (mostly for view for monsters and NPCs) + */ + void setSubtype(const uint16_t subtype, const uint8_t look); + + const BeingInfo *getInfo() const A_WARN_UNUSED + { return mInfo; } + + TargetCursorSize getTargetCursorSize() const A_WARN_UNUSED; + + int getTargetOffsetX() const A_WARN_UNUSED + { + if (!mInfo) + return 0; + return mInfo->getTargetOffsetX(); + } + + int getTargetOffsetY() const A_WARN_UNUSED + { + if (!mInfo) + return 0; + return mInfo->getTargetOffsetY(); + } + + /** + * Gets the way the object is blocked by other objects. + */ + virtual unsigned char getWalkMask() const A_WARN_UNUSED + { + if (!mInfo) + return 0; + return mInfo->getWalkMask(); + } + + /** + * Gets the way the monster blocks pathfinding for other objects + */ + Map::BlockType getBlockType() const A_WARN_UNUSED + { + if (!mInfo) + return Map::BLOCKTYPE_NONE; + return mInfo->getBlockType(); + } + + /** + * Sets the walk speed. + * in pixels per second for eAthena, + * in tiles per second for Manaserv. + */ + void setWalkSpeed(const Vector &speed) + { mWalkSpeed = speed; } + + /** + * Gets the walk speed. + * in pixels per second for eAthena, + * in tiles per second for Manaserv (0.1 precision). + */ + Vector getWalkSpeed() const A_WARN_UNUSED + { return mWalkSpeed; } + + /** + * Sets the attack speed. + * @todo In what unit? + */ + void setAttackSpeed(const int speed) + { mAttackSpeed = speed; } + + /** + * Gets the attack speed. + * @todo In what unit? + */ + int getAttackSpeed() const A_WARN_UNUSED + { return mAttackSpeed; } + + /** + * Sets the current action. + */ + virtual void setAction(const Action &action, const int attackType = 0); + + /** + * Get the being's action currently performed. + */ + Action getCurrentAction() const A_WARN_UNUSED + { return mAction; } + + /** + * Returns whether this being is still alive. + */ + bool isAlive() const A_WARN_UNUSED + { return mAction != DEAD; } + + /** + * Returns the current direction. + */ + uint8_t getDirection() const A_WARN_UNUSED + { return mDirection; } + + /** + * Sets the current direction. + */ + virtual void setDirection(const uint8_t direction); + + virtual void setDirectionDelayed(const uint8_t direction) + { mDirectionDelayed = direction; } + + uint8_t getDirectionDelayed() const A_WARN_UNUSED + { return mDirectionDelayed; } + + /** + * Returns the direction the being is facing. + */ + SpriteDirection getSpriteDirection() const A_WARN_UNUSED + { return static_cast<SpriteDirection>(mSpriteDirection); } + + void setPosition(const Vector &pos); + + /** + * Overloaded method provided for convenience. + * + * @see setPosition(const Vector &pos) + */ + inline void setPosition(const float x, const float y, + const float z = 0.0f) + { setPosition(Vector(x, y, z)); } + + /** + * Returns the horizontal size of the current base sprite of the being. + */ + virtual int getWidth() const override A_WARN_UNUSED + { return std::max(CompoundSprite::getWidth(), DEFAULT_BEING_WIDTH); } + + /** + * Returns the vertical size of the current base sprite of the being. + */ + virtual int getHeight() const override A_WARN_UNUSED + { return std::max(CompoundSprite::getHeight(), DEFAULT_BEING_HEIGHT); } + + /** + * Returns the being's pixel radius used to detect collisions. + */ + virtual int getCollisionRadius() const A_WARN_UNUSED + { return 16; } + + /** + * Shoots a missile particle from this being, to target being + */ + void fireMissile(Being *const target, + const std::string &particle) const; + + /** + * Returns the path this being is following. An empty path is returned + * when this being isn't following any path currently. + */ + const Path &getPath() const A_WARN_UNUSED + { return mPath; } + + int getDistance() const A_WARN_UNUSED + { return mDistance; } + + void setDistance(const int n) + { mDistance = n; } + + /** + * Set the Emoticon type and time displayed above + * the being. + */ + void setEmote(const uint8_t emotion, const int emote_time); + + void setState(const uint8_t state); + + virtual void drawSprites(Graphics *const graphics, + int posX, int posY) const override; + + virtual void drawSpritesSDL(Graphics *const graphics, + int posX, int posY) const override; + + void drawHpBar(Graphics *const graphics, const int x, const int y, + const int maxHP, const int hp, const int damage, + const int color1, const int color2, const int width, + const int height) const; + + static void load(); + + virtual void optionChanged(const std::string &value) override; + + void flashName(const int time); + + int getDamageTaken() const A_WARN_UNUSED + { return mDamageTaken; } + + void setDamageTaken(const int damage) + { mDamageTaken = damage; } + + void updateName(); + + void setLevel(const int n) + { mLevel = n; } + + virtual int getLevel() const A_WARN_UNUSED + { return mLevel; } + + void setIsReachable(const int n) + { mIsReachable = n; } + + int isReachable() const A_WARN_UNUSED + { return mIsReachable; } + + static void reReadConfig(); + + static BeingCacheEntry* getCacheEntry(const int id) A_WARN_UNUSED; + + void addToCache() const; + + bool updateFromCache(); + + /** + * Sets the gender of this being. + */ + virtual void setGender(const Gender gender); + + Gender getGender() const A_WARN_UNUSED + { return mGender; } + + /** + * Return sprite sit action for current environment. + */ + std::string getSitAction() const A_WARN_UNUSED; + + std::string getMoveAction() const A_WARN_UNUSED; + + std::string getDeadAction() const A_WARN_UNUSED; + + std::string getStandAction() const A_WARN_UNUSED; + + std::string getSpawnAction() const A_WARN_UNUSED; + + std::string getWeaponAttackAction(const ItemInfo *const weapon) const; + + std::string getAttackAction(const Attack *const attack) const; + + /** + * Whether or not this player is a GM. + */ + bool isGM() const A_WARN_UNUSED + { return mIsGM; } + + /** + * Triggers whether or not to show the name as a GM name. + */ + void setGM(const bool gm); + + bool canTalk() const A_WARN_UNUSED + { return mType == NPC; } + + void talkTo() const; + + bool draw(Graphics *const graphics, + const int offsetX, const int offsetY) const override; + + bool drawSpriteAt(Graphics *const graphics, + const int x, const int y) const; + + void setMoveTime() + { mMoveTime = cur_time; } + + void setAttackTime() + { mAttackTime = cur_time; } + + void setTalkTime() + { mTalkTime = cur_time; } + + void setTestTime() + { mTestTime = cur_time; } + + void setOtherTime() + { mOtherTime = cur_time; } + + unsigned int getMoveTime() const + { return mMoveTime; } + + unsigned int getAttackTime() const + { return mAttackTime; } + + unsigned int getTalkTime() const + { return mTalkTime; } + + unsigned int getTestTime() const + { return mTestTime; } + + unsigned int getOtherTime() const + { return mOtherTime; } + + void resetCounters(); + + virtual void updateColors(); + + void setEnemy(const bool n) + { mEnemy = n; } + + const std::string &getIp() const A_WARN_UNUSED + { return mIp; } + + void setIp(std::string ip) + { mIp = ip; } + + unsigned int getPvpRank() const A_WARN_UNUSED + { return mPvpRank; } + + void setPvpRank(const unsigned int rank) + { mPvpRank = rank; } + + void setHP(const int n); + + void setMaxHP(const int hp); + + int getHP() const A_WARN_UNUSED + { return mHP; } + + uint8_t calcDirection(const int dstX, + const int dstY) const A_WARN_UNUSED; + + uint8_t calcDirection() const A_WARN_UNUSED; + + void setAttackDelay(const int n) + { mAttackDelay = n; } + + int getAttackDelay() const A_WARN_UNUSED + { return mAttackDelay; } + + int getMinHit() const A_WARN_UNUSED + { return mMinHit; } + + void setMinHit(const int n) + { mMinHit = n; } + + int getMaxHit() const A_WARN_UNUSED + { return mMaxHit; } + + void setMaxHit(const int n) + { mMaxHit = n; } + + int getCriticalHit() const A_WARN_UNUSED + { return mCriticalHit; } + + void setCriticalHit(const int n) + { mCriticalHit = n; } + + void updateHit(const int amount); + + Equipment *getEquipment() A_WARN_UNUSED; + + void undressItemById(const int id); + + int getGoodStatus() const A_WARN_UNUSED + { return mGoodStatus; } + + void setGoodStatus(const int n) + { mGoodStatus = n; } + + std::string getGenderSign() const A_WARN_UNUSED; + + std::string getGenderSignWithSpace() const A_WARN_UNUSED; + + void updateComment(); + + const std::string getComment() const A_WARN_UNUSED + { return mComment; } + + void setComment(std::string n) + { mComment = n; } + + static void clearCache(); + + static std::string loadComment(const std::string &name, + const int type) A_WARN_UNUSED; + + static void saveComment(const std::string &name, + const std::string &comment, const int type); + + bool isAdvanced() const A_WARN_UNUSED + { return mAdvanced; } + + void setAdvanced(const bool n) + { mAdvanced = n; addToCache(); } + + bool isShopEnabled() const A_WARN_UNUSED + { return mShop; } + + void enableShop(const bool b) + { mShop = b; } + + /** + * Sets the attack range. + */ + void setAttackRange(const int range) + { mAttackRange = range; } + + void attack(Being *target = nullptr, bool keep = false, + bool dontChangeEquipment = false); + + void attack2(Being *target = nullptr, bool keep = false, + bool dontChangeEquipment = false); + + void updatePercentHP(); + + void setRaceName(std::string name) + { mRaceName = name; } + + std::string getRaceName() const A_WARN_UNUSED + { return mRaceName; } + + int getSpriteID(const int slot) const A_WARN_UNUSED; + + void setHairStyle(const unsigned int slot, const int id); + + void setHairColor(const unsigned int slot, + const unsigned char color); + + void setHairColor(const unsigned char color) + { mHairColor = color; } + + unsigned char getHairColor() const A_WARN_UNUSED + { return mHairColor; } + + void recalcSpritesOrder(); + + int getHitEffect(const Being *const attacker, + const AttackType type, + const int attackId) const A_WARN_UNUSED; + + Cursor::Cursor getHoverCursor() const A_WARN_UNUSED + { return mInfo ? mInfo->getHoverCursor() : Cursor::CURSOR_POINTER; } + + void addAfkEffect(); + + void removeAfkEffect(); + + void updateAwayEffect(); + + void addSpecialEffect(const int effect); + + void removeSpecialEffect(); + + void addEffect(const std::string &name); + + void addPet(const int id); + + void removePet(); + + void updatePets(); + + Being *getPet() + { return mPet; } + + void setPet(Being *const pet) + { mPet = pet; } + + void setOwner(Being *const owner) + { mOwner = owner; } + + void playSfx(const SoundInfo &sound, Being *const being, + const bool main, const int x, const int y); + + int getLook() + { return mLook; } + + void setLook(const int look); + + static uint8_t genderToInt(const Gender sex) A_WARN_UNUSED; + + static Gender intToGender(uint8_t sex) A_WARN_UNUSED; + + NextSoundInfo mNextSound; + + protected: + /** + * Sets the new path for this being. + */ + void setPath(const Path &path); + + /** + * Updates name's location. + */ + virtual void updateCoords(); + + void showName(); + + static int getDefaultEffectId(const int type); + + BeingInfo *mInfo; + AnimatedSprite *mEmotionSprite; + AnimatedSprite* mAnimationEffect; + + std::string mSpriteAction; + std::string mName; /**< Name of character */ + std::string mRaceName; + std::string mPartyName; + std::string mGuildName; + std::string mSpeech; + + /** + * Holds a text object when the being displays it's name, 0 otherwise + */ + FlashText *mDispName; + const gcn::Color *mNameColor; + + /** Engine-related infos about weapon. */ + const ItemInfo *mEquippedWeapon; + + static int mNumberOfHairstyles; /** Number of hair styles in use */ + static int mNumberOfRaces; /** Number of races in use */ + + Path mPath; + Text *mText; + const gcn::Color *mTextColor; + + Vector mDest; /**< destination coordinates. */ + + StringVect mSpriteColors; + std::vector<int> mSpriteIDs; + std::vector<int> mSpriteColorsIds; + + // Character guild information + std::map<int, Guild*> mGuilds; + Party *mParty; + + int mActionTime; /**< Time spent in current action */ + int mEmotionTime; /**< Time until emotion disappears */ + + /** Time until the last speech sentence disappears */ + int mSpeechTime; + int mAttackSpeed; /**< Attack speed */ + + int mLevel; + int mAttackRange; + Gender mGender; + Action mAction; /**< Action the being is performing */ + uint16_t mSubType; /**< Subtype (graphical view, basically) */ + uint8_t mDirection; /**< Facing direction */ + uint8_t mDirectionDelayed; /**< Facing direction */ + uint8_t mSpriteDirection; /**< Facing direction */ + bool mShowName; + bool mIsGM; + + private: + /** + * Calculates the offset in the given directions. + * If walking in direction 'neg' the value is negated. + * TODO: Used by eAthena only? + */ + int getOffset(const signed char pos, + const signed char neg) const A_WARN_UNUSED; + + int searchSlotValue(const std::vector<int> &slotRemap, + const int val) const A_WARN_UNUSED; + + void searchSlotValueItr(std::vector<int>::iterator &it, int &idx, + std::vector<int> &slotRemap, + const int val) const; + + void dumpSprites() const; + + const Type mType; + + /** Speech Bubble components */ + SpeechBubble *mSpeechBubble; + + /** + * Walk speed for x and y movement values. + * In pixels per second for eAthena, + * In pixels per ticks for Manaserv. + * @see MILLISECONDS_IN_A_TICK + */ + Vector mWalkSpeed; + std::string mIp; + int *mSpriteRemap; + int *mSpriteHide; + std::string mComment; + Being *mPet; + Being *mOwner; + Particle *mSpecialParticle; + + int mX, mY; /**< Position in tile */ + + int mDamageTaken; + int mHP; + int mMaxHP; + int mDistance; + int mIsReachable; /**< 0 - unknown, 1 - reachable, 2 - not reachable*/ + int mGoodStatus; + + static int mUpdateConfigTime; + static unsigned int mConfLineLim; + static int mSpeechType; + static bool mHighlightMapPortals; + static bool mHighlightMonsterAttackRange; + static bool mLowTraffic; + static bool mDrawHotKeys; + static bool mShowBattleEvents; + static bool mShowMobHP; + static bool mShowOwnHP; + static bool mShowGender; + static bool mShowLevel; + static bool mShowPlayersStatus; + static bool mEnableReorderSprites; + static bool mHideErased; + static bool mMoveNames; + static int mAwayEffect; + + unsigned int mMoveTime; + unsigned int mAttackTime; + unsigned int mTalkTime; + unsigned int mOtherTime; + unsigned int mTestTime; + int mAttackDelay; + int mMinHit; + int mMaxHit; + int mCriticalHit; + unsigned int mPvpRank; + unsigned int mNumber; + int mPetId; + int mLook; + unsigned char mHairColor; + bool mErased; + bool mEnemy; + bool mGotComment; + bool mAdvanced; + bool mShop; + bool mAway; + bool mInactive; +}; + +extern std::list<BeingCacheEntry*> beingInfoCache; + +#endif // BEING_BEING_H diff --git a/src/being/beingcacheentry.h b/src/being/beingcacheentry.h new file mode 100644 index 000000000..2890d44a6 --- /dev/null +++ b/src/being/beingcacheentry.h @@ -0,0 +1,128 @@ +/* + * The ManaPlus Client + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_BEINGCACHEENTRY_H +#define BEING_BEINGCACHEENTRY_H + +#include "localconsts.h" + +#include <string> + +class BeingCacheEntry final +{ + public: + explicit BeingCacheEntry(const int id): + mName(), + mPartyName(), + mGuildName(), + mIp(), + mId(id), + mLevel(0), + mPvpRank(0), + mTime(0), + mFlags(0), + mIsAdvanced(false) + { + } + + A_DELETE_COPY(BeingCacheEntry) + + int getId() const + { return mId; } + + /** + * Returns the name of the being. + */ + const std::string &getName() const + { return mName; } + + /** + * Sets the name for the being. + * + * @param name The name that should appear. + */ + void setName(const std::string &name) + { mName = name; } + + /** + * Following are set from the server (mainly for players) + */ + void setPartyName(const std::string &name) + { mPartyName = name; } + + void setGuildName(const std::string &name) + { mGuildName = name; } + + const std::string &getPartyName() const + { return mPartyName; } + + const std::string &getGuildName() const + { return mGuildName; } + + void setLevel(const int n) + { mLevel = n; } + + int getLevel() const + { return mLevel; } + + void setTime(const int n) + { mTime = n; } + + int getTime() const + { return mTime; } + + unsigned getPvpRank() const + { return mPvpRank; } + + void setPvpRank(const int r) + { mPvpRank = r; } + + std::string getIp() const + { return mIp; } + + void setIp(std::string ip) + { mIp = ip; } + + bool isAdvanced() const + { return mIsAdvanced; } + + void setAdvanced(const bool a) + { mIsAdvanced = a; } + + int getFlags() const + { return mFlags; } + + void setFlags(const int flags) + { mFlags = flags; } + + protected: + std::string mName; /**< Name of character */ + std::string mPartyName; + std::string mGuildName; + std::string mIp; + int mId; /**< Unique sprite id */ + int mLevel; + unsigned int mPvpRank; + int mTime; + int mFlags; + bool mIsAdvanced; +}; + +#endif // BEING_BEINGCACHEENTRY_H diff --git a/src/being/compoundsprite.cpp b/src/being/compoundsprite.cpp new file mode 100644 index 000000000..68a0d42e3 --- /dev/null +++ b/src/being/compoundsprite.cpp @@ -0,0 +1,567 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/compoundsprite.h" + +#include "client.h" +#include "configuration.h" +#include "game.h" + +#ifdef USE_OPENGL +#include "main.h" +#endif + +#include "map.h" +#include "sdlshared.h" + +#include "render/surfacegraphics.h" + +#include "resources/image.h" +#include "resources/imagehelper.h" + +#include "utils/dtor.h" +#include "utils/sdlcheckutils.h" + +#include <SDL.h> + +#include "debug.h" + +static const int BUFFER_WIDTH = 100; +static const int BUFFER_HEIGHT = 100; + +static const unsigned cache_max_size = 10; +static const unsigned cache_clean_part = 3; +bool CompoundSprite::mEnableDelay = true; + +CompoundSprite::CompoundSprite() : + imagesCache(), + mCacheItem(nullptr), + mImage(nullptr), + mAlphaImage(nullptr), + mOffsetX(0), + mOffsetY(0), + mSprites(), + mNextRedrawTime(0), + mNeedsRedraw(false), + mEnableAlphaFix(config.getBoolValue("enableAlphaFix")), + mDisableAdvBeingCaching(config.getBoolValue("disableAdvBeingCaching")), + mDisableBeingCaching(config.getBoolValue("disableBeingCaching")) +{ + mAlpha = 1.0f; +} + +CompoundSprite::~CompoundSprite() +{ + clear(); + mImage = nullptr; + mAlphaImage = nullptr; +} + +bool CompoundSprite::reset() +{ + bool ret = false; + FOR_EACH (SpriteIterator, it, mSprites) + { + if (*it) + ret |= (*it)->reset(); + } + mNeedsRedraw |= ret; + return ret; +} + +bool CompoundSprite::play(const std::string &action) +{ + bool ret = false; + FOR_EACH (SpriteIterator, it, mSprites) + { + if (*it) + ret |= (*it)->play(action); + } + mNeedsRedraw |= ret; + return ret; +} + +bool CompoundSprite::update(const int time) +{ + bool ret = false; + FOR_EACH (SpriteIterator, it, mSprites) + { + if (*it) + ret |= (*it)->update(time); + } + mNeedsRedraw |= ret; + return ret; +} + +bool CompoundSprite::draw(Graphics *const graphics, + const int posX, const int posY) const +{ + FUNC_BLOCK("CompoundSprite::draw", 1) + if (mNeedsRedraw) + updateImages(); + + if (mSprites.empty()) // Nothing to draw + return false; + + if (mAlpha == 1.0f && mImage) + { + return graphics->drawImage(mImage, posX + mOffsetX, posY + mOffsetY); + } + else if (mAlpha && mAlphaImage) + { + mAlphaImage->setAlpha(mAlpha); + return graphics->drawImage(mAlphaImage, + posX + mOffsetX, posY + mOffsetY); + } + else + { + drawSprites(graphics, posX, posY); + } + return false; +} + +void CompoundSprite::drawSprites(Graphics *const graphics, + const int posX, const int posY) const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + { + (*it)->setAlpha(mAlpha); + (*it)->draw(graphics, posX, posY); + } + } +} + +void CompoundSprite::drawSpritesSDL(Graphics *const graphics, + const int posX, const int posY) const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + (*it)->draw(graphics, posX, posY); + } +} + +int CompoundSprite::getWidth() const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + const Sprite *const base = *it; + if (base) + return base->getWidth(); + } + + return 0; +} + +int CompoundSprite::getHeight() const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + const Sprite *const base = *it; + if (base) + return base->getHeight(); + } + + return 0; +} + +const Image *CompoundSprite::getImage() const +{ + return mImage; +} + +bool CompoundSprite::setSpriteDirection(const SpriteDirection direction) +{ + bool ret = false; + FOR_EACH (SpriteIterator, it, mSprites) + { + if (*it) + ret |= (*it)->setSpriteDirection(direction); + } + mNeedsRedraw |= ret; + return ret; +} + +int CompoundSprite::getNumberOfLayers() const +{ + if (mImage || mAlphaImage) + return 1; + else + return static_cast<int>(size()); +} + +unsigned int CompoundSprite::getCurrentFrame() const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + return (*it)->getCurrentFrame(); + } + return 0; +} + +unsigned int CompoundSprite::getFrameCount() const +{ + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + return (*it)->getFrameCount(); + } + return 0; +} + +void CompoundSprite::addSprite(Sprite *const sprite) +{ + mSprites.push_back(sprite); + mNeedsRedraw = true; +} + +void CompoundSprite::setSprite(const int layer, Sprite *const sprite) +{ + // Skip if it won't change anything + if (mSprites.at(layer) == sprite) + return; + + delete mSprites.at(layer); + mSprites[layer] = sprite; + mNeedsRedraw = true; +} + +void CompoundSprite::removeSprite(const int layer) +{ + // Skip if it won't change anything + if (!mSprites.at(layer)) + return; + + delete mSprites.at(layer); + mSprites.at(layer) = nullptr; + mNeedsRedraw = true; +} + +void CompoundSprite::clear() +{ + // Skip if it won't change anything + if (!mSprites.empty()) + { + delete_all(mSprites); + mSprites.clear(); + } + mNeedsRedraw = true; + delete_all(imagesCache); + imagesCache.clear(); + delete mCacheItem; + mCacheItem = nullptr; +} + +void CompoundSprite::ensureSize(size_t layerCount) +{ + // Skip if it won't change anything + if (mSprites.size() >= layerCount) + return; + +// resize(layerCount, nullptr); + mSprites.resize(layerCount); +} + +/** + * Returns the curent frame in the current animation of the given layer. + */ +unsigned int CompoundSprite::getCurrentFrame(unsigned int layer) const +{ + if (layer >= mSprites.size()) + return 0; + + const Sprite *const s = getSprite(layer); + if (s) + return s->getCurrentFrame(); + + return 0; +} + +/** + * Returns the frame count in the current animation of the given layer. + */ +unsigned int CompoundSprite::getFrameCount(unsigned int layer) +{ + if (layer >= mSprites.size()) + return 0; + + const Sprite *const s = getSprite(layer); + if (s) + return s->getFrameCount(); + + return 0; +} + +void CompoundSprite::redraw() const +{ +#ifndef USE_SDL2 + +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + const int rmask = 0xff000000; + const int gmask = 0x00ff0000; + const int bmask = 0x0000ff00; + const int amask = 0x000000ff; +#else + const int rmask = 0x000000ff; + const int gmask = 0x0000ff00; + const int bmask = 0x00ff0000; + const int amask = 0xff000000; +#endif + + SDL_Surface *const surface = MSDL_CreateRGBSurface(SDL_HWSURFACE, + BUFFER_WIDTH, BUFFER_HEIGHT, 32, rmask, gmask, bmask, amask); + + if (!surface) + return; + + SurfaceGraphics *graphics = new SurfaceGraphics(); + graphics->setBlitMode(SurfaceGraphics::BLIT_GFX); + graphics->setTarget(surface); + graphics->_beginDraw(); + + int tileX = 32 / 2; + int tileY = 32; + + const Game *const game = Game::instance(); + if (game) + { + const Map *const map = game->getCurrentMap(); + if (map) + { + tileX = map->getTileWidth() / 2; + tileY = map->getTileWidth(); + } + } + + const int posX = BUFFER_WIDTH / 2 - tileX; + const int posY = BUFFER_HEIGHT - tileY; + + mOffsetX = tileX - BUFFER_WIDTH / 2; + mOffsetY = tileY - BUFFER_HEIGHT; + + drawSpritesSDL(graphics, posX, posY); + + delete graphics; + graphics = nullptr; + + SDL_Surface *const surfaceA = MSDL_CreateRGBSurface(SDL_HWSURFACE, + BUFFER_WIDTH, BUFFER_HEIGHT, 32, rmask, gmask, bmask, amask); + +#ifdef USE_SDL2 + SDL_SetSurfaceAlphaMod(surface, 255); +#else + SDL_SetAlpha(surface, 0, SDL_ALPHA_OPAQUE); +#endif + SDL_BlitSurface(surface, nullptr, surfaceA, nullptr); + + delete mImage; + delete mAlphaImage; + + mImage = imageHelper->load(surface); + MSDL_FreeSurface(surface); + + if (ImageHelper::mEnableAlpha) + { + mAlphaImage = imageHelper->load(surfaceA); + MSDL_FreeSurface(surfaceA); + } + else + { + mAlphaImage = nullptr; + } +#endif +} + +void CompoundSprite::setAlpha(float alpha) +{ + if (alpha != mAlpha) + { +#ifdef USE_OPENGL + if (mEnableAlphaFix && imageHelper->useOpenGL() == 0 + && size() > 3) +#else + if (mEnableAlphaFix && size() > 3) +#endif + { + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + (*it)->setAlpha(alpha); + } + } + mAlpha = alpha; + } +} + +void CompoundSprite::updateImages() const +{ +#ifndef USE_SDL2 +#ifdef USE_OPENGL + if (imageHelper->useOpenGL()) + return; +#endif + + if (mEnableDelay) + { + if (get_elapsed_time1(mNextRedrawTime) < 10) + return; + mNextRedrawTime = tick_time; + } + mNeedsRedraw = false; + + if (!mDisableBeingCaching) + { + if (size() <= 3) + return; + + if (!mDisableAdvBeingCaching) + { + if (updateFromCache()) + return; + + redraw(); + + if (mImage) + initCurrentCacheItem(); + } + else + { + redraw(); + } + } +#endif +} + +bool CompoundSprite::updateFromCache() const +{ +#ifndef USE_SDL2 +// static int hits = 0; +// static int miss = 0; + + if (mCacheItem && mCacheItem->image) + { + imagesCache.push_front(mCacheItem); + mCacheItem = nullptr; + if (imagesCache.size() > cache_max_size) + { + for (unsigned f = 0; f < cache_clean_part; f ++) + { + CompoundItem *item = imagesCache.back(); + imagesCache.pop_back(); + delete item; + } + } + } + +// logger->log("cache size: %d, hit %d, miss %d", +// (int)imagesCache.size(), hits, miss); + + const size_t sz = size(); + FOR_EACH (ImagesCache::iterator, it, imagesCache) + { + CompoundItem *const ic = *it; + if (ic && ic->data.size() == sz) + { + bool fail(false); + VectorPointers::const_iterator it2 = ic->data.begin(); + const VectorPointers::const_iterator it2_end = ic->data.end(); + + for (SpriteConstIterator it1 = mSprites.begin(), + it1_end = mSprites.end(); + it1 != it1_end && it2 != it2_end; + ++ it1, ++ it2) + { + const void *ptr1 = nullptr; + const void *ptr2 = nullptr; + if (*it1) + ptr1 = (*it1)->getHash(); + if (*it2) + ptr2 = *it2; + if (ptr1 != ptr2) + { + fail = true; + break; + } + } + if (!fail) + { +// hits ++; + mImage = (*it)->image; + mAlphaImage = (*it)->alphaImage; + imagesCache.erase(it); + mCacheItem = ic; + return true; + } + } + } + mImage = nullptr; + mAlphaImage = nullptr; +// miss++; +#endif + return false; +} + +void CompoundSprite::initCurrentCacheItem() const +{ + delete mCacheItem; + mCacheItem = new CompoundItem(); + mCacheItem->image = mImage; + mCacheItem->alphaImage = mAlphaImage; +// mCacheItem->alpha = mAlpha; + + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + mCacheItem->data.push_back((*it)->getHash()); + else + mCacheItem->data.push_back(nullptr); + } +} + +bool CompoundSprite::updateNumber(unsigned num) +{ + bool res(false); + FOR_EACH (SpriteConstIterator, it, mSprites) + { + if (*it) + { + if ((*it)->updateNumber(num)) + res = true; + } + } + return res; +} + +CompoundItem::CompoundItem() : + data(), + image(nullptr), + alphaImage(nullptr) +{ +} + +CompoundItem::~CompoundItem() +{ + delete image; + delete alphaImage; +} diff --git a/src/being/compoundsprite.h b/src/being/compoundsprite.h new file mode 100644 index 000000000..5024f0122 --- /dev/null +++ b/src/being/compoundsprite.h @@ -0,0 +1,161 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_COMPOUNDSPRITE_H +#define BEING_COMPOUNDSPRITE_H + +#include "sprite.h" + +#include <list> +#include <vector> + +#include "localconsts.h" + +class Image; + +typedef std::list <void*> VectorPointers; + +class CompoundItem final +{ + public: + CompoundItem(); + + A_DELETE_COPY(CompoundItem) + + ~CompoundItem(); + + VectorPointers data; + Image *image; + Image *alphaImage; +}; + +class CompoundSprite : public Sprite +{ +public: + typedef std::vector<Sprite*>::iterator SpriteIterator; + typedef std::vector<Sprite*>::const_iterator SpriteConstIterator; + + CompoundSprite(); + + A_DELETE_COPY(CompoundSprite) + + ~CompoundSprite(); + + virtual bool reset() override; + + virtual bool play(const std::string &action) override; + + virtual bool update(const int time) override; + + virtual bool draw(Graphics *const graphics, + const int posX, const int posY) const override; + + /** + * Gets the width in pixels of the first sprite in the list. + */ + virtual int getWidth() const override A_WARN_UNUSED; + + /** + * Gets the height in pixels of the first sprite in the list. + */ + virtual int getHeight() const override A_WARN_UNUSED; + + virtual const Image *getImage() const override A_WARN_UNUSED; + + virtual bool setSpriteDirection(const SpriteDirection direction) override; + + int getNumberOfLayers() const A_WARN_UNUSED; + + unsigned int getCurrentFrame() const override A_WARN_UNUSED; + + unsigned int getFrameCount() const override A_WARN_UNUSED; + + size_t size() const A_WARN_UNUSED + { return mSprites.size(); } + + bool empty() const A_WARN_UNUSED + { return mSprites.empty(); } + + void addSprite(Sprite *const sprite); + + void setSprite(const int layer, Sprite *const sprite); + + Sprite *getSprite(int layer) const A_WARN_UNUSED + { return mSprites.at(layer); } + + void removeSprite(const int layer); + + void clear(); + + void ensureSize(size_t layerCount); + + virtual void drawSprites(Graphics *const graphics, + int posX, int posY) const; + + virtual void drawSpritesSDL(Graphics *const graphics, + int posX, int posY) const; + + /** + * Returns the curent frame in the current animation of the given layer. + */ + virtual unsigned int getCurrentFrame(unsigned int layer) + const A_WARN_UNUSED; + + /** + * Returns the frame count in the current animation of the given layer. + */ + virtual unsigned int getFrameCount(unsigned int layer) A_WARN_UNUSED; + + virtual void setAlpha(float alpha) override; + + bool updateNumber(const unsigned num) override; + + static void setEnableDelay(bool b) + { mEnableDelay = b; } + +private: + void redraw() const; + + void updateImages() const; + + bool updateFromCache() const; + + void initCurrentCacheItem() const; + + typedef std::list<CompoundItem*> ImagesCache; + mutable ImagesCache imagesCache; + mutable CompoundItem *mCacheItem; + + mutable Image *mImage; + mutable Image *mAlphaImage; + + mutable int mOffsetX; + mutable int mOffsetY; + std::vector<Sprite*> mSprites; + mutable int mNextRedrawTime; + static bool mEnableDelay; + mutable bool mNeedsRedraw; + bool mEnableAlphaFix; + bool mDisableAdvBeingCaching; + bool mDisableBeingCaching; +}; + +#endif // BEING_COMPOUNDSPRITE_H diff --git a/src/being/localplayer.cpp b/src/being/localplayer.cpp new file mode 100644 index 000000000..6a0aa1b19 --- /dev/null +++ b/src/being/localplayer.cpp @@ -0,0 +1,4355 @@ +/* + * The ManaPlus Client + * Copyright (C) 2004-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/localplayer.h" + +#include "actorspritemanager.h" +#include "client.h" +#include "configuration.h" +#include "dropshortcut.h" +#include "effectmanager.h" +#include "guild.h" +#include "item.h" +#include "maplayer.h" +#include "party.h" +#include "simpleanimation.h" +#include "soundconsts.h" +#include "soundmanager.h" +#include "statuseffect.h" +#include "walklayer.h" + +#include "being/playerinfo.h" +#include "being/playerrelations.h" + +#include "particle/particle.h" + +#include "input/keyboardconfig.h" + +#include "gui/chatwindow.h" +#include "gui/gui.h" +#include "gui/ministatuswindow.h" +#include "gui/okdialog.h" +#include "gui/outfitwindow.h" +#include "gui/shopwindow.h" +#include "gui/sdlfont.h" +#include "gui/skilldialog.h" +#include "gui/socialwindow.h" +#include "gui/updaterwindow.h" +#include "gui/viewport.h" + +#include "gui/widgets/gmtab.h" +#include "gui/widgets/whispertab.h" + +#include "render/graphics.h" + +#include "net/beinghandler.h" +#include "net/chathandler.h" +#include "net/guildhandler.h" +#include "net/inventoryhandler.h" +#include "net/net.h" +#include "net/partyhandler.h" +#include "net/playerhandler.h" +#include "net/skillhandler.h" +#include "net/tradehandler.h" + +#include "resources/imageset.h" +#include "resources/iteminfo.h" +#include "resources/resourcemanager.h" + +#include "utils/gettext.h" + +#include "mumblemanager.h" + +#include <climits> + +#include "debug.h" + +static const int16_t awayLimitTimer = 60; +static const int MAX_TICK_VALUE = INT_MAX / 2; + +typedef std::map<int, Guild*>::const_iterator GuildMapCIter; + +LocalPlayer *player_node = nullptr; + +extern std::list<BeingCacheEntry*> beingInfoCache; +extern OkDialog *weightNotice; +extern int weightNoticeTime; +extern MiniStatusWindow *miniStatusWindow; +extern SkillDialog *skillDialog; + +LocalPlayer::LocalPlayer(const int id, const int subtype) : + Being(id, PLAYER, subtype, nullptr), + mGMLevel(0), + mInvertDirection(0), + mCrazyMoveType(config.getIntValue("crazyMoveType")), + mCrazyMoveState(0), + mAttackWeaponType(config.getIntValue("attackWeaponType")), + mQuickDropCounter(config.getIntValue("quickDropCounter")), + mMoveState(0), + mPickUpType(config.getIntValue("pickUpType")), + mMagicAttackType(config.getIntValue("magicAttackType")), + mPvpAttackType(config.getIntValue("pvpAttackType")), + mMoveToTargetType(config.getIntValue("moveToTargetType")), + mAttackType(config.getIntValue("attackType")), + mFollowMode(config.getIntValue("followMode")), + mImitationMode(config.getIntValue("imitationMode")), + mLastTargetX(0), + mLastTargetY(0), + mHomes(), + mTarget(nullptr), + mPlayerFollowed(), + mPlayerImitated(), + mNextDestX(0), + mNextDestY(0), + mPickUpTarget(nullptr), + mLastAction(-1), + mStatusEffectIcons(), + mLocalWalkTime(-1), + mMessages(), + mMessageTime(0), + mAwayListener(new AwayListener), + mAwayDialog(nullptr), + mPingSendTick(0), + mPingTime(0), + mAfkTime(0), + mActivityTime(0), + mNavigateX(0), + mNavigateY(0), + mNavigateId(0), + mCrossX(0), + mCrossY(0), + mOldX(0), + mOldY(0), + mOldTileX(0), + mOldTileY(0), + mNavigatePath(), + mLastHitFrom(), + mWaitFor(), + mAdvertTime(0), + mTestParticle(nullptr), + mTestParticleName(), + mTestParticleTime(0), + mTestParticleHash(0l), + mWalkingDir(0), + mUpdateName(true), + mBlockAdvert(false), + mTargetDeadPlayers(config.getBoolValue("targetDeadPlayers")), + mServerAttack(config.getBoolValue("serverAttack")), + mEnableAdvert(config.getBoolValue("enableAdvert")), + mTradebot(config.getBoolValue("tradebot")), + mTargetOnlyReachable(config.getBoolValue("targetOnlyReachable")), + mDisableGameModifiers(config.getBoolValue("disableGameModifiers")), + mIsServerBuggy(serverConfig.getValueBool("enableBuggyServers", true)), + mSyncPlayerMove(config.getBoolValue("syncPlayerMove")), + mDrawPath(config.getBoolValue("drawPath")), + mAttackMoving(config.getBoolValue("attackMoving")), + mAttackNext(config.getBoolValue("attackNext")), + mShowJobExp(config.getBoolValue("showJobExp")), + mNextStep(false), + mDisableCrazyMove(false), + mGoingToTarget(false), + mKeepAttacking(false), + mPathSetByMouse(false), + mWaitPing(false), + mAwayMode(false), + mPseudoAwayMode(false), + mShowNavigePath(false) +{ + logger->log1("LocalPlayer::LocalPlayer"); + + listen(CHANNEL_ATTRIBUTES); + + mAttackRange = 0; + mLevel = 1; + mAdvanced = true; + mTextColor = &Theme::getThemeColor(Theme::PLAYER); + if (userPalette) + mNameColor = &userPalette->getColor(UserPalette::SELF); + else + mNameColor = nullptr; + + PlayerInfo::setStatBase(PlayerInfo::WALK_SPEED, + static_cast<int>(getWalkSpeed().x)); + PlayerInfo::setStatMod(PlayerInfo::WALK_SPEED, 0); + + loadHomes(); + + config.addListener("showownname", this); + config.addListener("targetDeadPlayers", this); + serverConfig.addListener("enableBuggyServers", this); + config.addListener("syncPlayerMove", this); + config.addListener("drawPath", this); + config.addListener("serverAttack", this); + config.addListener("attackMoving", this); + config.addListener("attackNext", this); + config.addListener("showJobExp", this); + config.addListener("enableAdvert", this); + config.addListener("tradebot", this); + config.addListener("targetOnlyReachable", this); + setShowName(config.getBoolValue("showownname")); +} + +LocalPlayer::~LocalPlayer() +{ + logger->log1("LocalPlayer::~LocalPlayer"); + + config.removeListeners(this); + serverConfig.removeListener("enableBuggyServers", this); + + if (mAwayDialog) + { + soundManager.volumeRestore(); + delete mAwayDialog; + mAwayDialog = nullptr; + } + delete mAwayListener; + mAwayListener = nullptr; +} + +void LocalPlayer::logic() +{ + BLOCK_START("LocalPlayer::logic") +#ifdef USE_MUMBLE + if (mumbleManager) + mumbleManager->setPos(mX, mY, mDirection); +#endif + + // Actions are allowed once per second + if (get_elapsed_time(mLastAction) >= 1000) + mLastAction = -1; + + if (mActivityTime == 0 || mLastAction != -1) + mActivityTime = cur_time; + + if ((mAction != MOVE || mNextStep) && !mNavigatePath.empty()) + { + mNextStep = false; + int dist = 5; + if (!mSyncPlayerMove) + dist = 20; + + if ((mNavigateX || mNavigateY) && + ((mCrossX + dist >= mX && mCrossX <= mX + dist + && mCrossY + dist >= mY && mCrossY <= mY + dist) + || (!mCrossX && !mCrossY))) + { + const Path::const_iterator i = mNavigatePath.begin(); + if ((*i).x == mX && (*i).y == mY) + mNavigatePath.pop_front(); + else + moveTo((*i).x, (*i).y); + } + } + + // Show XP messages + if (!mMessages.empty()) + { + if (mMessageTime == 0) + { + MessagePair info = mMessages.front(); + + if (particleEngine) + { + particleEngine->addTextRiseFadeOutEffect( + info.first, + getPixelX(), + getPixelY() - 48, + &userPalette->getColor(info.second), + gui->getInfoParticleFont(), true); + } + + mMessages.pop_front(); + mMessageTime = 30; + } + mMessageTime--; + } + +#ifdef MANASERV_SUPPORT + PlayerInfo::logic(); +#endif + + if (mTarget) + { + if (mTarget->getType() == ActorSprite::NPC) + { + // NPCs are always in range + mTarget->setTargetType(TCT_IN_RANGE); + } + else + { + // Find whether target is in range +#ifdef MANASERV_SUPPORT + const int rangeX = + (Net::getNetworkType() == ServerInfo::MANASERV) ? + static_cast<int>(abs(static_cast<int>(mTarget->getPosition().x + - getPosition().x))) : + static_cast<int>(abs(mTarget->getTileX() - getTileX())); + const int rangeY = + (Net::getNetworkType() == ServerInfo::MANASERV) ? + static_cast<int>(abs(static_cast<int>(mTarget->getPosition().y + - getPosition().y))) : + static_cast<int>(abs(mTarget->getTileY() - getTileY())); +#else + const int rangeX = static_cast<int>( + abs(mTarget->getTileX() - getTileX())); + const int rangeY = static_cast<int>( + abs(mTarget->getTileY() - getTileY())); +#endif + const int attackRange = getAttackRange(); + const TargetCursorType targetType = rangeX > attackRange || + rangeY > attackRange ? + TCT_NORMAL : TCT_IN_RANGE; + mTarget->setTargetType(targetType); + + if (!mTarget->isAlive() && (!mTargetDeadPlayers + || mTarget->getType() != Being::PLAYER)) + { + stopAttack(true); + } + + if (mKeepAttacking && mTarget) + attack(mTarget, true); + } + } + + Being::logic(); + BLOCK_END("LocalPlayer::logic") +} + +void LocalPlayer::slowLogic() +{ + BLOCK_START("LocalPlayer::slowLogic") + const int time = cur_time; + if (weightNotice && weightNoticeTime < time) + { + weightNotice->scheduleDelete(); + weightNotice = nullptr; + weightNoticeTime = 0; + } + + if (serverVersion < 4 && mEnableAdvert && !mBlockAdvert + && mAdvertTime < cur_time) + { + uint8_t smile = FLAG_SPECIAL; + if (mTradebot && shopWindow && !shopWindow->isShopEmpty()) + smile |= FLAG_SHOP; + + if (mAwayMode || mPseudoAwayMode) + smile |= FLAG_AWAY; + + if (mInactive) + smile |= FLAG_INACTIVE; + + if (emote(smile)) + mAdvertTime = time + 60; + else + mAdvertTime = time + 30; + } + + if (mTestParticleTime != time && !mTestParticleName.empty()) + { + unsigned long hash = UpdaterWindow::getFileHash(mTestParticleName); + if (hash != mTestParticleHash) + { + setTestParticle(mTestParticleName, false); + mTestParticleHash = hash; + } + mTestParticleTime = time; + } + + BLOCK_END("LocalPlayer::slowLogic") +} + +void LocalPlayer::setAction(const Action &action, const int attackType) +{ + if (action == DEAD) + { + if (!mLastHitFrom.empty()) + { + // TRANSLATORS: chat message after death + debugMsg(strprintf(_("You were killed by %s"), + mLastHitFrom.c_str())); + mLastHitFrom.clear(); + } + setTarget(nullptr); + } + + Being::setAction(action, attackType); +#ifdef USE_MUMBLE + if (mumbleManager) + mumbleManager->setAction(static_cast<int>(action)); +#endif +} + +void LocalPlayer::setGMLevel(const int level) +{ + mGMLevel = level; + + if (level > 0) + { + setGM(true); + if (chatWindow) + { + chatWindow->loadGMCommands(); + if (!gmChatTab && config.getBoolValue("enableGmTab")) + gmChatTab = new GmTab(chatWindow); + } + } +} + +#ifdef MANASERV_SUPPORT +Position LocalPlayer::getNextWalkPosition(const unsigned char dir) const +{ + // Compute where the next tile will be set. + int dx = 0, dy = 0; + if (dir & Being::UP) + dy--; + if (dir & Being::DOWN) + dy++; + if (dir & Being::LEFT) + dx--; + if (dir & Being::RIGHT) + dx++; + + const Vector &pos = getPosition(); + + // If no map or no direction is given, give back the current player position + if (!mMap || (!dx && !dy)) + return Position(static_cast<int>(pos.x), static_cast<int>(pos.y)); + + const int posX = static_cast<int>(pos.x); + const int posY = static_cast<int>(pos.y); + // Get the current tile pos and its offset + const int tileX = posX / mMap->getTileWidth(); + const int tileY = posY / mMap->getTileHeight(); + const int offsetX = posX % mMap->getTileWidth(); + const int offsetY = posY % mMap->getTileHeight(); + const unsigned char walkMask = getWalkMask(); + const int radius = getCollisionRadius(); + + // Get the walkability of every surrounding tiles. + bool wTopLeft = mMap->getWalk(tileX - 1, tileY - 1, walkMask); + const bool wTop = mMap->getWalk(tileX, tileY - 1, walkMask); + bool wTopRight = mMap->getWalk(tileX + 1, tileY - 1, walkMask); + const bool wLeft = mMap->getWalk(tileX - 1, tileY, walkMask); + const bool wRight = mMap->getWalk(tileX + 1, tileY, walkMask); + bool wBottomLeft = mMap->getWalk(tileX - 1, tileY + 1, walkMask); + const bool wBottom = mMap->getWalk(tileX, tileY + 1, walkMask); + bool wBottomRight = mMap->getWalk(tileX + 1, tileY + 1, walkMask); + + // Make diagonals unwalkable when both straight directions are blocking + if (!wTop) + { + if (!wRight) + wTopRight = false; + if (!wLeft) + wTopLeft = false; + } + if (!wBottom) + { + if (!wRight) + wBottomRight = false; + if (!wLeft) + wBottomLeft = false; + } + + // We'll make tests for each desired direction + + // Handle diagonal cases by setting the way back to a straight direction + // when necessary. + if (dx && dy) + { + // Going top-right + if (dx > 0 && dy < 0) + { + if (!wTopRight) + { + // Choose a straight direction when diagonal target is blocked + if (!wTop && wRight) + { + dy = 0; + } + else if (wTop && !wRight) + { + dx = 0; + } + else if (!wTop && !wRight) + { + return Position(tileX * 32 + 32 - radius, + tileY * 32 + getCollisionRadius()); + } + else // Both straight direction are walkable + { + // Go right when below the corner + if (offsetY >= (offsetX / mMap->getTileHeight() + - (offsetX / mMap->getTileWidth() + * mMap->getTileHeight()) )) + { + dy = 0; + } + else // Go up otherwise + { + dx = 0; + } + } + } + else // The diagonal is walkable + { + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX + 32, posY - 32)); + } + } + + // Going top-left + if (dx < 0 && dy < 0) + { + if (!wTopLeft) + { + // Choose a straight direction when diagonal target is blocked + if (!wTop && wLeft) + { + dy = 0; + } + else if (wTop && !wLeft) + { + dx = 0; + } + else if (!wTop && !wLeft) + { + return Position(tileX * 32 + radius, + tileY * 32 + radius); + } + else // Both straight direction are walkable + { + // Go left when below the corner + if (offsetY >= (offsetX / mMap->getTileWidth() + * mMap->getTileHeight())) + { + dy = 0; + } + else // Go up otherwise + { + dx = 0; + } + } + } + else // The diagonal is walkable + { + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX - 32, posY - 32)); + } + } + + // Going bottom-left + if (dx < 0 && dy > 0) + { + if (!wBottomLeft) + { + // Choose a straight direction when diagonal target is blocked + if (!wBottom && wLeft) + { + dy = 0; + } + else if (wBottom && !wLeft) + { + dx = 0; + } + else if (!wBottom && !wLeft) + { + return Position(tileX * 32 + radius, + tileY * 32 + 32 - radius); + } + else // Both straight direction are walkable + { + // Go down when below the corner + if (offsetY >= (offsetX / mMap->getTileHeight() + - (offsetX / mMap->getTileWidth() + * mMap->getTileHeight()))) + { + dx = 0; + } + else // Go left otherwise + { + dy = 0; + } + } + } + else // The diagonal is walkable + { + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX - 32, posY + 32)); + } + } + + // Going bottom-right + if (dx > 0 && dy > 0) + { + if (!wBottomRight) + { + // Choose a straight direction when diagonal target is blocked + if (!wBottom && wRight) + { + dy = 0; + } + else if (wBottom && !wRight) + { + dx = 0; + } + else if (!wBottom && !wRight) + { + return Position(tileX * 32 + 32 - radius, + tileY * 32 + 32 - radius); + } + else // Both straight direction are walkable + { + // Go down when below the corner + if (offsetY >= (offsetX / mMap->getTileWidth() + * mMap->getTileHeight())) + { + dx = 0; + } + else // Go right otherwise + { + dy = 0; + } + } + } + else // The diagonal is walkable + { + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX + 32, posY + 32)); + } + } + } // End of diagonal cases + + // Straight directions + // Right direction + if (dx > 0 && !dy) + { + // If the straight destination is blocked, + // Make the player go the closest possible. + if (!wRight) + { + return Position(tileX * 32 + 32 - radius, posY); + } + else + { + if (!wTopRight) + { + // If we're going to collide with the top-right corner + if (offsetY - radius < 0) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + 32 - radius, + tileY * 32 + radius); + } + } + + if (!wBottomRight) + { + // If we're going to collide with the bottom-right corner + if (offsetY + radius > 32) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + 32 - radius, + tileY * 32 + 32 - radius); + } + } + // If the way is clear, step up one checked tile ahead. + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX + 32, posY)); + } + } + + // Left direction + if (dx < 0 && !dy) + { + // If the straight destination is blocked, + // Make the player go the closest possible. + if (!wLeft) + { + return Position(tileX * 32 + radius, posY); + } + else + { + if (!wTopLeft) + { + // If we're going to collide with the top-left corner + if (offsetY - radius < 0) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + radius, + tileY * 32 + radius); + } + } + + if (!wBottomLeft) + { + // If we're going to collide with the bottom-left corner + if (offsetY + radius > 32) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + radius, + tileY * 32 + 32 - radius); + } + } + // If the way is clear, step up one checked tile ahead. + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX - 32, posY)); + } + } + + // Up direction + if (!dx && dy < 0) + { + // If the straight destination is blocked, + // Make the player go the closest possible. + if (!wTop) + { + return Position(posX, tileY * 32 + radius); + } + else + { + if (!wTopLeft) + { + // If we're going to collide with the top-left corner + if (offsetX - radius < 0) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + radius, + tileY * 32 + radius); + } + } + + if (!wTopRight) + { + // If we're going to collide with the top-right corner + if (offsetX + radius > 32) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + 32 - radius, + tileY * 32 + radius); + } + } + // If the way is clear, step up one checked tile ahead. + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX, posY - 32)); + } + } + + // Down direction + if (!dx && dy > 0) + { + // If the straight destination is blocked, + // Make the player go the closest possible. + if (!wBottom) + { + return Position(posX, tileY * 32 + 32 - radius); + } + else + { + if (!wBottomLeft) + { + // If we're going to collide with the bottom-left corner + if (offsetX - radius < 0) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + radius, + tileY * 32 + 32 - radius); + } + } + + if (!wBottomRight) + { + // If we're going to collide with the bottom-right corner + if (offsetX + radius > 32) + { + // We make the player corrects its offset + // before going further + return Position(tileX * 32 + 32 - radius, + tileY * 32 + 32 - radius); + } + } + // If the way is clear, step up one checked tile ahead. + return mMap->checkNodeOffsets(radius, + walkMask, Position(posX, posY + 32)); + } + } + + // Return the current position if everything else has failed. + return Position(posX, posY); +} +#endif + +void LocalPlayer::nextTile(unsigned char dir A_UNUSED = 0) +{ +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + Party *const party = Party::getParty(1); + if (party) + { + PartyMember *const pm = party->getMember(getName()); + if (pm) + { + pm->setX(mX); + pm->setY(mY); + } + } + + if (mPath.empty()) + { + if (mPickUpTarget) + pickUp(mPickUpTarget); + + if (mWalkingDir) + startWalking(mWalkingDir); + } + else if (mPath.size() == 1) + { + if (mPickUpTarget) + pickUp(mPickUpTarget); + } + + if (mGoingToTarget && mTarget && withinAttackRange(mTarget)) + { + mAction = Being::STAND; + attack(mTarget, true); + mGoingToTarget = false; + mPath.clear(); + return; + } + else if (mGoingToTarget && !mTarget) + { + mGoingToTarget = false; + mPath.clear(); + } + + if (mPath.empty()) + { + if (mNavigatePath.empty() || mAction != MOVE) + setAction(STAND); + else + mNextStep = true; + } + else + { + Being::nextTile(); + } + } +#ifdef MANASERV_SUPPORT + else + { + if (!mMap || !dir) + return; + + const Vector &pos = getPosition(); + const Position destination = getNextWalkPosition(dir); + + if (static_cast<int>(pos.x) != destination.x + || static_cast<int>(pos.y) != destination.y) + { + setDestination(destination.x, destination.y); + } + else if (dir != mDirection) + { + Net::getPlayerHandler()->setDirection(dir); + setDirection(dir); + } + } +#endif +} + +bool LocalPlayer::pickUp(FloorItem *const item) +{ + if (!item) + return false; + + if (!client->limitPackets(PACKET_PICKUP)) + return false; + + const int dx = item->getTileX() - mX; + const int dy = item->getTileY() - mY; + int dist = 6; + + if (mPickUpType >= 4 && mPickUpType <= 6) + dist = 4; + + if (dx * dx + dy * dy < dist) + { + if (actorSpriteManager && actorSpriteManager->checkForPickup(item)) + { + Net::getPlayerHandler()->pickUp(item); + mPickUpTarget = nullptr; + } + } + else if (mPickUpType >= 4 && mPickUpType <= 6) + { +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + setDestination(item->getPixelX() + 16, item->getPixelY() + 16); + mPickUpTarget = item; + mPickUpTarget->addActorSpriteListener(this); + } + else +#endif + { + const Vector &playerPos = getPosition(); + const Path debugPath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + item->getTileX(), item->getTileY(), getWalkMask(), 0); + if (!debugPath.empty()) + navigateTo(item->getTileX(), item->getTileY()); + else + setDestination(item->getTileX(), item->getTileY()); + + mPickUpTarget = item; + mPickUpTarget->addActorSpriteListener(this); + } + } + return true; +} + +void LocalPlayer::actorSpriteDestroyed(const ActorSprite &actorSprite) +{ + if (mPickUpTarget == &actorSprite) + mPickUpTarget = nullptr; +} + +Being *LocalPlayer::getTarget() const +{ + return mTarget; +} + +void LocalPlayer::setTarget(Being *const target) +{ + if (target == this && target) + return; + + if (target == mTarget) + return; + + Being *oldTarget = nullptr; + if (mTarget) + { + mTarget->untarget(); + oldTarget = mTarget; + } + + if (mTarget && mTarget->getType() == ActorSprite::MONSTER) + mTarget->setShowName(false); + + mTarget = target; + + if (oldTarget) + oldTarget->updateName(); + + if (mTarget) + { + mLastTargetX = mTarget->getTileX(); + mLastTargetY = mTarget->getTileY(); + mTarget->updateName(); + } + + if (target && target->getType() == ActorSprite::MONSTER) + target->setShowName(true); +} + +void LocalPlayer::setDestination(const int x, const int y) +{ + mActivityTime = cur_time; + + if (getAttackType() == 0 || !mAttackMoving) + mKeepAttacking = false; + + // Only send a new message to the server when destination changes + if (x != mDest.x || y != mDest.y) + { + if (mInvertDirection != 1) + { + Net::getPlayerHandler()->setDestination(x, y, mDirection); + Being::setDestination(x, y); + } + else if (mInvertDirection == 1) + { + uint8_t newDir = 0; + if (mDirection & UP) + newDir |= DOWN; + if (mDirection & LEFT) + newDir |= RIGHT; + if (mDirection & DOWN) + newDir |= UP; + if (mDirection & RIGHT) + newDir |= LEFT; + + Net::getPlayerHandler()->setDestination(x, y, newDir); + +// if (client->limitPackets(PACKET_DIRECTION)) + { + setDirection(newDir); + Net::getPlayerHandler()->setDirection(newDir); + } + + Being::setDestination(x, y); + } + else + { +#ifdef MANASERV_SUPPORT + // Manaserv: + // If the destination given to being class is accepted, + // we inform the Server. + if ((x == mDest.x && y == mDest.y) + || Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + Net::getPlayerHandler()->setDestination(x, y, mDirection); + } + } + } +} + +void LocalPlayer::setWalkingDir(const unsigned char dir) +{ + // This function is called by Game::handleInput() + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + // First if player is pressing key for the direction he is already + // going, do nothing more... + + // Else if he is pressing a key, and its different from what he has + // been pressing, stop (do not send this stop to the server) and + // start in the new direction + if (dir && (dir != getWalkingDir())) + stopWalking(false); + + // Else, he is not pressing a key, + // and the current path hasn't been sent by mouse, + // then, stop (sending to server). + else if (!dir) + { + if (!mPathSetByMouse) + stopWalking(true); + return; + } + + // If the delay to send another walk message to the server hasn't + // expired, don't do anything or we could get disconnected for + // spamming the server + if (get_elapsed_time(mLocalWalkTime) < walkingKeyboardDelay) + return; + } +#endif + + mWalkingDir = dir; + + // If we're not already walking, start walking. + if (mAction != MOVE && dir) + { + startWalking(dir); + } +#ifdef MANASERV_SUPPORT + else if (mAction == MOVE && (Net::getNetworkType() + == ServerInfo::MANASERV)) + { + nextTile(dir); + } +#endif +} + +void LocalPlayer::startWalking(const unsigned char dir) +{ + // This function is called by setWalkingDir(), + // but also by nextTile() for TMW-Athena... + if (!mMap || !dir) + return; + + mPickUpTarget = nullptr; + if (mAction == MOVE && !mPath.empty()) + { + // Just finish the current action, otherwise we get out of sync +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + const Vector &pos = getPosition(); + Being::setDestination(static_cast<int>(pos.x), + static_cast<int>(pos.y)); + } + else +#endif + { + Being::setDestination(mX, mY); + } + return; + } + + int dx = 0, dy = 0; + if (dir & UP) + dy--; + if (dir & DOWN) + dy++; + if (dir & LEFT) + dx--; + if (dir & RIGHT) + dx++; + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() != ServerInfo::MANASERV) +#endif + { + const unsigned char walkMask = getWalkMask(); + // Prevent skipping corners over colliding tiles + if (dx && !mMap->getWalk(mX + dx, mY, walkMask)) + dx = 0; + if (dy && !mMap->getWalk(mX, mY + dy, walkMask)) + dy = 0; + + // Choose a straight direction when diagonal target is blocked + if (dx && dy && !mMap->getWalk(mX + dx, mY + dy, walkMask)) + dx = 0; + + // Walk to where the player can actually go + if ((dx || dy) && mMap->getWalk(mX + dx, mY + dy, walkMask)) + { + setDestination(mX + dx, mY + dy); + } + else if (dir != mDirection) + { + // If the being can't move, just change direction + +// if (client->limitPackets(PACKET_DIRECTION)) + { + Net::getPlayerHandler()->setDirection(dir); + setDirection(dir); + } + } + } +#ifdef MANASERV_SUPPORT + else + { + nextTile(dir); + } +#endif +} + +void LocalPlayer::stopWalking(const bool sendToServer) +{ + if (mAction == MOVE && mWalkingDir) + { + mWalkingDir = 0; + mLocalWalkTime = 0; + mPickUpTarget = nullptr; + + setDestination(static_cast<int>(getPosition().x), + static_cast<int>(getPosition().y)); + if (sendToServer) + { + Net::getPlayerHandler()->setDestination( + static_cast<int>(getPosition().x), + static_cast<int>(getPosition().y), -1); + } + setAction(STAND); + } + + // No path set anymore, so we reset the path by mouse flag + mPathSetByMouse = false; + + clearPath(); + navigateClean(); +} + +bool LocalPlayer::toggleSit() const +{ + if (!client->limitPackets(PACKET_SIT)) + return false; + + Being::Action newAction; + switch (mAction) + { + case STAND: + case SPAWN: + newAction = SIT; + break; + case SIT: + newAction = STAND; + break; + case MOVE: + case ATTACK: + case DEAD: + case HURT: + default: + return true; + } + + Net::getPlayerHandler()->changeAction(newAction); + return true; +} + +bool LocalPlayer::updateSit() const +{ + if (!client->limitPackets(PACKET_SIT)) + return false; + + Net::getPlayerHandler()->changeAction(mAction); + return true; +} + +bool LocalPlayer::emote(const uint8_t emotion) +{ + if (!client->limitPackets(PACKET_EMOTE)) + return false; + + Net::getPlayerHandler()->emote(emotion); + return true; +} + +void LocalPlayer::attack(Being *const target, const bool keep, + const bool dontChangeEquipment) +{ +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + if (mLastAction != -1) + return; + + // Can only attack when standing still + if (mAction != STAND && mAction != ATTACK) + return; + } +#endif + + mKeepAttacking = keep; + + if (!target || target->getType() == ActorSprite::NPC) + return; + + if (mTarget != target || !mTarget) + setTarget(target); + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + const Vector &plaPos = this->getPosition(); + const Vector &tarPos = mTarget->getPosition(); + const int dist_x = static_cast<int>(plaPos.x - tarPos.x); + const int dist_y = static_cast<int>(plaPos.y - tarPos.y); + + if (abs(dist_y) >= abs(dist_x)) + { + if (dist_y < 0) + setDirection(DOWN); + else + setDirection(UP); + } + else + { + if (dist_x < 0) + setDirection(RIGHT); + else + setDirection(LEFT); + } + + mLastAction = tick_time; + } + else +#endif + { + const int dist_x = target->getTileX() - mX; + const int dist_y = target->getTileY() - mY; + + // Must be standing or sitting to attack + if (mAction != STAND && mAction != SIT) + return; + + if (abs(dist_y) >= abs(dist_x)) + { + if (dist_y > 0) + setDirection(DOWN); + else + setDirection(UP); + } + else + { + if (dist_x > 0) + setDirection(RIGHT); + else + setDirection(LEFT); + } + + mActionTime = tick_time; + } + + if (target->getType() != Being::PLAYER || checAttackPermissions(target)) + { + setAction(ATTACK); + + if (!client->limitPackets(PACKET_ATTACK)) + return; + + if (!dontChangeEquipment) + changeEquipmentBeforeAttack(target); + + Net::getPlayerHandler()->attack(target->getId(), mServerAttack); + } + +#ifdef MANASERV_SUPPORT + if ((Net::getNetworkType() != ServerInfo::MANASERV) && !keep) +#else + if (!keep) +#endif + stopAttack(); +} + +void LocalPlayer::stopAttack(const bool keepAttack) +{ + if (!client->limitPackets(PACKET_STOPATTACK)) + return; + + if (mServerAttack && mAction == ATTACK) + Net::getPlayerHandler()->stopAttack(); + + untarget(); + if (!keepAttack || !mAttackNext) + mKeepAttacking = false; +} + +void LocalPlayer::untarget() +{ + if (mAction == ATTACK) + setAction(STAND); + + if (mTarget) + setTarget(nullptr); +} + +void LocalPlayer::pickedUp(const ItemInfo &itemInfo, const int amount, + const unsigned char color, const int floorItemId, + const unsigned char fail) +{ + if (fail) + { + if (actorSpriteManager && floorItemId) + { + FloorItem *const item = actorSpriteManager->findItem(floorItemId); + if (item) + { + if (!item->getShowMsg()) + return; + item->setShowMsg(false); + } + } + const char* msg; + switch (fail) + { + case PICKUP_BAD_ITEM: + // TRANSLATORS: pickup error message + msg = N_("Tried to pick up nonexistent item."); + break; + case PICKUP_TOO_HEAVY: + // TRANSLATORS: pickup error message + msg = N_("Item is too heavy."); + break; + case PICKUP_TOO_FAR: + // TRANSLATORS: pickup error message + msg = N_("Item is too far away."); + break; + case PICKUP_INV_FULL: + // TRANSLATORS: pickup error message + msg = N_("Inventory is full."); + break; + case PICKUP_STACK_FULL: + // TRANSLATORS: pickup error message + msg = N_("Stack is too big."); + break; + case PICKUP_DROP_STEAL: + // TRANSLATORS: pickup error message + msg = N_("Item belongs to someone else."); + break; + default: + // TRANSLATORS: pickup error message + msg = N_("Unknown problem picking up item."); + break; + } + if (localChatTab && config.getBoolValue("showpickupchat")) + localChatTab->chatLog(gettext(msg), BY_SERVER); + + if (mMap && config.getBoolValue("showpickupparticle")) + { + // Show pickup notification + addMessageToQueue(gettext(msg), UserPalette::PICKUP_INFO); + } + } + else + { + std::string str; + if (serverVersion > 0) + str = itemInfo.getName(color); + else + str = itemInfo.getName(); + + if (config.getBoolValue("showpickupchat") && localChatTab) + { + // TRANSLATORS: %d is number, + // [@@%d|%s@@] - here player can see link to item + localChatTab->chatLog(strprintf(ngettext("You picked up %d " + "[@@%d|%s@@].", "You picked up %d [@@%d|%s@@].", amount), + amount, itemInfo.getId(), str.c_str()), BY_SERVER); + } + + if (mMap && config.getBoolValue("showpickupparticle")) + { + // Show pickup notification + if (amount > 1) + { + addMessageToQueue(strprintf("%d x %s", amount, + str.c_str()), UserPalette::PICKUP_INFO); + } + else + { + addMessageToQueue(str, UserPalette::PICKUP_INFO); + } + } + } +} + +int LocalPlayer::getAttackRange() const +{ + if (mAttackRange > -1) + { + return mAttackRange; + } + else + { + const Item *const weapon = PlayerInfo::getEquipment(EQUIP_FIGHT1_SLOT); + if (weapon) + { + const ItemInfo &info = weapon->getInfo(); + return info.getAttackRange(); + } + return 48; // unarmed range + } +} + +bool LocalPlayer::withinAttackRange(const Being *const target, + const bool fixDistance, + const int addRange) const +{ + if (!target) + return false; + + int range = getAttackRange() + addRange; + int dx; + int dy; + + if (fixDistance && range == 1) + range = 2; + +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + const Vector &targetPos = target->getPosition(); + const Vector &pos = getPosition(); + dx = static_cast<int>(abs(static_cast<int>(targetPos.x - pos.x))); + dy = static_cast<int>(abs(static_cast<int>(targetPos.y - pos.y))); + } + else +#endif + { + dx = static_cast<int>(abs(target->getTileX() - mX)); + dy = static_cast<int>(abs(target->getTileY() - mY)); + } + return !(dx > range || dy > range); +} + +void LocalPlayer::setGotoTarget(Being *const target) +{ + if (!target) + return; + + mPickUpTarget = nullptr; +#ifdef MANASERV_SUPPORT + if (Net::getNetworkType() == ServerInfo::MANASERV) + { + mTarget = target; + mGoingToTarget = true; + const Vector &targetPos = target->getPosition(); + setDestination(static_cast<int>(targetPos.x), + static_cast<int>(targetPos.y)); + } + else +#endif + { + setTarget(target); + mGoingToTarget = true; + setDestination(target->getTileX(), target->getTileY()); + } +} + +void LocalPlayer::handleStatusEffect(StatusEffect *const effect, + const int effectId) +{ + Being::handleStatusEffect(effect, effectId); + + if (effect) + { + effect->deliverMessage(); + effect->playSFX(); + + AnimatedSprite *const sprite = effect->getIcon(); + + if (!sprite) + { + // delete sprite, if necessary + for (unsigned int i = 0; i < mStatusEffectIcons.size(); ) + { + if (mStatusEffectIcons[i] == effectId) + { + mStatusEffectIcons.erase(mStatusEffectIcons.begin() + i); + if (miniStatusWindow) + miniStatusWindow->eraseIcon(i); + } + else + { + i++; + } + } + } + else + { + // replace sprite or append + bool found = false; + const unsigned int sz = mStatusEffectIcons.size(); + for (unsigned int i = 0; i < sz; i++) + { + if (mStatusEffectIcons[i] == effectId) + { + if (miniStatusWindow) + miniStatusWindow->setIcon(i, sprite); + found = true; + break; + } + } + + if (!found) + { // add new + const int offset = static_cast<int>(mStatusEffectIcons.size()); + if (miniStatusWindow) + miniStatusWindow->setIcon(offset, sprite); + mStatusEffectIcons.push_back(effectId); + } + } + } +} + +void LocalPlayer::addMessageToQueue(const std::string &message, + const int color) +{ + if (mMessages.size() < 20) + mMessages.push_back(MessagePair(message, color)); +} + +void LocalPlayer::optionChanged(const std::string &value) +{ + if (value == "showownname") + setShowName(config.getBoolValue("showownname")); + else if (value == "targetDeadPlayers") + mTargetDeadPlayers = config.getBoolValue("targetDeadPlayers"); + else if (value == "enableBuggyServers") + mIsServerBuggy = serverConfig.getBoolValue("enableBuggyServers"); + else if (value == "syncPlayerMove") + mSyncPlayerMove = config.getBoolValue("syncPlayerMove"); + else if (value == "drawPath") + mDrawPath = config.getBoolValue("drawPath"); + else if (value == "serverAttack") + mServerAttack = config.getBoolValue("serverAttack"); + else if (value == "attackMoving") + mAttackMoving = config.getBoolValue("attackMoving"); + else if (value == "attackNext") + mAttackNext = config.getBoolValue("attackNext"); + else if (value == "showJobExp") + mShowJobExp = config.getBoolValue("showJobExp"); + else if (value == "enableAdvert") + mEnableAdvert = config.getBoolValue("enableAdvert"); + else if (value == "tradebot") + mTradebot = config.getBoolValue("tradebot"); + else if (value == "targetOnlyReachable") + mTargetOnlyReachable = config.getBoolValue("targetOnlyReachable"); +} + +void LocalPlayer::processEvent(Channels channel, + const DepricatedEvent &event) +{ + if (channel == CHANNEL_ATTRIBUTES) + { + if (event.getName() == EVENT_UPDATEATTRIBUTE) + { + switch (event.getInt("id")) + { + case PlayerInfo::EXP: + { + if (event.getInt("oldValue") > event.getInt("newValue")) + break; + + const int change = event.getInt("newValue") + - event.getInt("oldValue"); + + if (change != 0) + { + // TRANSLATORS: get xp message + addMessageToQueue(strprintf("%d %s", change, _("xp"))); + } + break; + } + case PlayerInfo::LEVEL: + mLevel = event.getInt("newValue"); + break; + default: + break; + }; + } + else if (event.getName() == EVENT_UPDATESTAT) + { + if (!mShowJobExp) + return; + + const int id = event.getInt("id"); + if (id == Net::getPlayerHandler()->getJobLocation()) + { + const std::pair<int, int> exp + = PlayerInfo::getStatExperience(id); + if (event.getInt("oldValue1") > exp.first + || !event.getInt("oldValue2")) + { + return; + } + + const int change = exp.first - event.getInt("oldValue1"); + if (change != 0 && mMessages.size() < 20) + { + if (!mMessages.empty()) + { + MessagePair pair = mMessages.back(); + // TRANSLATORS: this is normal experience + if (pair.first.find(strprintf(" %s", + _("xp"))) == pair.first.size() + - strlen(_("xp")) - 1) + { + mMessages.pop_back(); + // TRANSLATORS: this is job experience + pair.first.append(strprintf(", %d %s", + change, _("job"))); + mMessages.push_back(pair); + } + else + { + // TRANSLATORS: this is job experience + addMessageToQueue(strprintf("%d %s", + change, _("job"))); + } + } + else + { + // TRANSLATORS: this is job experience + addMessageToQueue(strprintf( + "%d %s", change, _("job"))); + } + } + } + } + } +} + +void LocalPlayer::moveTo(const int x, const int y) +{ + setDestination(x, y); +} + +void LocalPlayer::move(const int dX, const int dY) +{ + mPickUpTarget = nullptr; + moveTo(mX + dX, mY + dY); +} + +void LocalPlayer::moveToTarget(int dist) +{ + bool gotPos(false); + Path debugPath; + + const Vector &playerPos = getPosition(); + unsigned int limit(0); + + if (dist == -1) + { + dist = mMoveToTargetType; + if (mMoveToTargetType == 0) + { + dist = 0; + } + else + { + switch (mMoveToTargetType) + { + case 1: + dist = 1; + break; + case 2: + dist = 2; + break; + case 3: + dist = 3; + break; + case 4: + dist = 5; + break; + case 5: + dist = 7; + break; + case 6: + case 7: + dist = mAttackRange; + if (dist == 1 && serverVersion < 1) + dist = 2; + break; + case 8: + dist = mAttackRange - 1; + if (dist < 1) + dist = 1; + if (dist == 1 && serverVersion < 1) + dist = 2; + break; + default: + break; + } + } + } + + if (mTarget) + { + if (mMap) + { + debugPath = mMap->findPath(static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + mTarget->getTileX(), mTarget->getTileY(), getWalkMask(), 0); + } + + const unsigned int sz = debugPath.size(); + if (sz < static_cast<unsigned int>(dist)) + return; + limit = static_cast<int>(sz) - dist; + gotPos = true; + } + else if (mNavigateX || mNavigateY) + { + debugPath = mNavigatePath; + limit = dist; + gotPos = true; + } + + if (gotPos) + { + if (dist == 0) + { + if (mTarget) + navigateTo(mTarget); + } + else + { + Position pos(0, 0); + unsigned int f = 0; + + for (Path::const_iterator i = debugPath.begin(), + i_end = debugPath.end(); + i != i_end && f < limit; ++i, f++) + { + pos = (*i); + } + navigateTo(pos.x, pos.y); + } + } + else if (mLastTargetX || mLastTargetY) + { + navigateTo(mLastTargetX, mLastTargetY); + } +} + +void LocalPlayer::moveToHome() +{ + mPickUpTarget = nullptr; + if ((mX != mCrossX || mY != mCrossY) && mCrossX && mCrossY) + { + moveTo(mCrossX, mCrossY); + } + else if (mMap) + { + const std::map<std::string, Vector>::const_iterator iter = + mHomes.find(mMap->getProperty("_realfilename")); + + if (iter != mHomes.end()) + { + const Vector pos = mHomes[(*iter).first]; + if (mX == pos.x && mY == pos.y) + { + Net::getPlayerHandler()->setDestination( + static_cast<int>(pos.x), + static_cast<int>(pos.y), + static_cast<int>(mDirection)); + } + else + { + navigateTo(static_cast<int>(pos.x), static_cast<int>(pos.y)); + } + } + } +} + +static const unsigned invertDirectionSize = 5; + +void LocalPlayer::changeMode(unsigned *const var, const unsigned limit, + const char *const conf, + std::string (LocalPlayer::*const func)(), + const unsigned def, + const bool save) +{ + if (!var) + return; + + (*var) ++; + if (*var >= limit) + *var = def; + if (save) + config.setValue(conf, *var); + if (miniStatusWindow) + miniStatusWindow->updateStatus(); + const std::string str = (this->*func)(); + if (str.size() > 4) + debugMsg(str.substr(4)); +} + +void LocalPlayer::invertDirection() +{ + mMoveState = 0; + changeMode(&mInvertDirection, invertDirectionSize, "invertMoveDirection", + &LocalPlayer::getInvertDirectionString, 0, false); +} + +static const char *const invertDirectionStrings[] = +{ + // TRANSLATORS: move type in status bar + N_("(D) default moves"), + // TRANSLATORS: move type in status bar + N_("(I) invert moves"), + // TRANSLATORS: move type in status bar + N_("(c) moves with some crazy moves"), + // TRANSLATORS: move type in status bar + N_("(C) moves with crazy moves"), + // TRANSLATORS: move type in status bar + N_("(d) double normal + crazy"), + // TRANSLATORS: move type in status bar + N_("(?) unknown move") +}; + +std::string LocalPlayer::getInvertDirectionString() +{ + return gettext(getVarItem(&invertDirectionStrings[0], + mInvertDirection, invertDirectionSize)); +} + +static const unsigned crazyMoveTypeSize = 11; + +void LocalPlayer::changeCrazyMoveType() +{ + mCrazyMoveState = 0; + changeMode(&mCrazyMoveType, crazyMoveTypeSize, "crazyMoveType", + &LocalPlayer::getCrazyMoveTypeString, 1); +} + +std::string LocalPlayer::getCrazyMoveTypeString() +{ + if (mCrazyMoveType < crazyMoveTypeSize - 1) + { + // TRANSLATORS: crazy move type in status bar + return strprintf(_("(%u) crazy move number %u"), + mCrazyMoveType, mCrazyMoveType); + } + else if (mCrazyMoveType == crazyMoveTypeSize - 1) + { + // TRANSLATORS: crazy move type in status bar + return _("(a) custom crazy move"); + } + else + { + // TRANSLATORS: crazy move type in status bar + return _("(?) crazy move"); + } +} + +static const unsigned moveToTargetTypeSize = 9; + +void LocalPlayer::changeMoveToTargetType() +{ + changeMode(&mMoveToTargetType, moveToTargetTypeSize, "moveToTargetType", + &LocalPlayer::getMoveToTargetTypeString); +} + +static const char *const moveToTargetTypeStrings[] = +{ + // TRANSLATORS: move to target type in status bar + N_("(0) default moves to target"), + // TRANSLATORS: move to target type in status bar + N_("(1) moves to target in distance 1"), + // TRANSLATORS: move to target type in status bar + N_("(2) moves to target in distance 2"), + // TRANSLATORS: move to target type in status bar + N_("(3) moves to target in distance 3"), + // TRANSLATORS: move to target type in status bar + N_("(5) moves to target in distance 5"), + // TRANSLATORS: move to target type in status bar + N_("(7) moves to target in distance 7"), + // TRANSLATORS: move to target type in status bar + N_("(A) moves to target in attack range"), + // TRANSLATORS: move to target type in status bar + N_("(a) archer attack range"), + // TRANSLATORS: move to target type in status bar + N_("(B) moves to target in attack range - 1"), + // TRANSLATORS: move to target type in status bar + N_("(?) move to target") +}; + +std::string LocalPlayer::getMoveToTargetTypeString() +{ + return gettext(getVarItem(&moveToTargetTypeStrings[0], + mMoveToTargetType, moveToTargetTypeSize)); +} + +static const unsigned followModeSize = 4; + +void LocalPlayer::changeFollowMode() +{ + changeMode(&mFollowMode, followModeSize, "followMode", + &LocalPlayer::getFollowModeString); +} + +static const char *const followModeStrings[] = +{ + // TRANSLATORS: folow mode in status bar + N_("(D) default follow"), + // TRANSLATORS: folow mode in status bar + N_("(R) relative follow"), + // TRANSLATORS: folow mode in status bar + N_("(M) mirror follow"), + // TRANSLATORS: folow mode in status bar + N_("(P) pet follow"), + // TRANSLATORS: folow mode in status bar + N_("(?) unknown follow") +}; + +std::string LocalPlayer::getFollowModeString() +{ + return gettext(getVarItem(&followModeStrings[0], + mFollowMode, followModeSize)); +} + +const unsigned attackWeaponTypeSize = 4; + +void LocalPlayer::changeAttackWeaponType() +{ + changeMode(&mAttackWeaponType, attackWeaponTypeSize, "attackWeaponType", + &LocalPlayer::getAttackWeaponTypeString, 1); +} + +static const char *attackWeaponTypeStrings[] = +{ + // TRANSLATORS: switch attack type in status bar + N_("(?) attack"), + // TRANSLATORS: switch attack type in status bar + N_("(D) default attack"), + // TRANSLATORS: switch attack type in status bar + N_("(s) switch attack without shield"), + // TRANSLATORS: switch attack type in status bar + N_("(S) switch attack with shield"), + // TRANSLATORS: switch attack type in status bar + N_("(?) attack") +}; + +std::string LocalPlayer::getAttackWeaponTypeString() +{ + return gettext(getVarItem(&attackWeaponTypeStrings[0], + mAttackWeaponType, attackWeaponTypeSize)); +} + +const unsigned attackTypeSize = 4; + +void LocalPlayer::changeAttackType() +{ + changeMode(&mAttackType, attackTypeSize, "attackType", + &LocalPlayer::getAttackTypeString); +} + +static const char *const attackTypeStrings[] = +{ + // TRANSLATORS: attack type in status bar + N_("(D) default attack"), + // TRANSLATORS: attack type in status bar + N_("(G) go and attack"), + // TRANSLATORS: attack type in status bar + N_("(A) go, attack, pickup"), + // TRANSLATORS: attack type in status bar + N_("(d) without auto attack"), + // TRANSLATORS: attack type in status bar + N_("(?) attack") +}; + +std::string LocalPlayer::getAttackTypeString() +{ + return gettext(getVarItem(&attackTypeStrings[0], + mAttackType, attackTypeSize)); +} + +const unsigned quickDropCounterSize = 31; + +void LocalPlayer::changeQuickDropCounter() +{ + changeMode(&mQuickDropCounter, quickDropCounterSize, "quickDropCounter", + &LocalPlayer::getQuickDropCounterString, 1); +} + +std::string LocalPlayer::getQuickDropCounterString() +{ + if (mQuickDropCounter > 9) + { + return strprintf("(%c) drop counter %u", static_cast<signed char>( + 'a' + mQuickDropCounter - 10), mQuickDropCounter); + } + else + { + return strprintf("(%u) drop counter %u", + mQuickDropCounter, mQuickDropCounter); + } +} + +void LocalPlayer::setQuickDropCounter(const int n) +{ + if (n < 1 || n >= static_cast<signed>(quickDropCounterSize)) + return; + mQuickDropCounter = n; + config.setValue("quickDropCounter", mQuickDropCounter); + if (miniStatusWindow) + miniStatusWindow->updateStatus(); +} + +const unsigned pickUpTypeSize = 7; + +void LocalPlayer::changePickUpType() +{ + changeMode(&mPickUpType, pickUpTypeSize, "pickUpType", + &LocalPlayer::getPickUpTypeString); +} + +static const char *const pickUpTypeStrings[] = +{ + // TRANSLATORS: pickup size in status bar + N_("(S) small pick up 1x1 cells"), + // TRANSLATORS: pickup size in status bar + N_("(D) default pick up 2x1 cells"), + // TRANSLATORS: pickup size in status bar + N_("(F) forward pick up 2x3 cells"), + // TRANSLATORS: pickup size in status bar + N_("(3) pick up 3x3 cells"), + // TRANSLATORS: pickup size in status bar + N_("(g) go and pick up in distance 4"), + // TRANSLATORS: pickup size in status bar + N_("(G) go and pick up in distance 8"), + // TRANSLATORS: pickup size in status bar + N_("(A) go and pick up in max distance"), + // TRANSLATORS: pickup size in status bar + N_("(?) pick up") +}; + +std::string LocalPlayer::getPickUpTypeString() +{ + return gettext(getVarItem(&pickUpTypeStrings[0], + mPickUpType, pickUpTypeSize)); +} + +const unsigned debugPathSize = 5; + +static const char *const debugPathStrings[] = +{ + // TRANSLATORS: map view type in status bar + N_("(N) normal map view"), + // TRANSLATORS: map view type in status bar + N_("(D) debug map view"), + // TRANSLATORS: map view type in status bar + N_("(u) ultra map view"), + // TRANSLATORS: map view type in status bar + N_("(U) ultra map view 2"), + // TRANSLATORS: map view type in status bar + N_("(e) empty map view"), + // TRANSLATORS: map view type in status bar + N_("(b) black & white map view") +}; + +std::string LocalPlayer::getDebugPathString() const +{ + return gettext(getVarItem(&debugPathStrings[0], + viewport->getDebugPath(), debugPathSize)); +} + +const unsigned magicAttackSize = 5; + +void LocalPlayer::switchMagicAttack() +{ + changeMode(&mMagicAttackType, magicAttackSize, "magicAttackType", + &LocalPlayer::getMagicAttackString); +} + +static const char *const magicAttackStrings[] = +{ + // TRANSLATORS: magic attack in status bar + N_("(f) use #flar for magic attack"), + // TRANSLATORS: magic attack in status bar + N_("(c) use #chiza for magic attack"), + // TRANSLATORS: magic attack in status bar + N_("(I) use #ingrav for magic attack"), + // TRANSLATORS: magic attack in status bar + N_("(F) use #frillyar for magic attack"), + // TRANSLATORS: magic attack in status bar + N_("(U) use #upmarmu for magic attack"), + // TRANSLATORS: magic attack in status bar + N_("(?) magic attack") +}; + +std::string LocalPlayer::getMagicAttackString() +{ + return gettext(getVarItem(&magicAttackStrings[0], + mMagicAttackType, magicAttackSize)); +} + +const unsigned pvpAttackSize = 4; + +void LocalPlayer::switchPvpAttack() +{ + changeMode(&mPvpAttackType, pvpAttackSize, "pvpAttackType", + &LocalPlayer::getPvpAttackString); +} + +static const char *const pvpAttackStrings[] = +{ + // TRANSLATORS: player attack type in status bar + N_("(a) attack all players"), + // TRANSLATORS: player attack type in status bar + N_("(f) attack all except friends"), + // TRANSLATORS: player attack type in status bar + N_("(b) attack bad relations"), + // TRANSLATORS: player attack type in status bar + N_("(d) don't attack players"), + // TRANSLATORS: player attack type in status bar + N_("(?) pvp attack") +}; + +std::string LocalPlayer::getPvpAttackString() +{ + return gettext(getVarItem(&pvpAttackStrings[0], + mPvpAttackType, pvpAttackSize)); +} + +const unsigned imitationModeSize = 2; + +void LocalPlayer::changeImitationMode() +{ + changeMode(&mImitationMode, imitationModeSize, "imitationMode", + &LocalPlayer::getImitationModeString); +} + +static const char *const imitationModeStrings[] = +{ + // TRANSLATORS: imitation type in status bar + N_("(D) default imitation"), + // TRANSLATORS: imitation type in status bar + N_("(O) outfits imitation"), + // TRANSLATORS: imitation type in status bar + N_("(?) imitation") +}; + +std::string LocalPlayer::getImitationModeString() +{ + return gettext(getVarItem(&imitationModeStrings[0], + mImitationMode, imitationModeSize)); +} + +const unsigned awayModeSize = 2; + +void LocalPlayer::changeAwayMode() +{ + mAwayMode = !mAwayMode; + mAfkTime = 0; + mInactive = false; + updateName(); + if (miniStatusWindow) + miniStatusWindow->updateStatus(); + if (mAwayMode) + { + if (chatWindow) + chatWindow->clearAwayLog(); + + cancelFollow(); + navigateClean(); + if (outfitWindow) + outfitWindow->wearAwayOutfit(); + // TRANSLATORS: away message box header + mAwayDialog = new OkDialog(_("Away"), + config.getStringValue("afkMessage"), + DIALOG_SILENCE, true, false); + mAwayDialog->addActionListener(mAwayListener); + soundManager.volumeOff(); + addAfkEffect(); + } + else + { + mAwayDialog = nullptr; + soundManager.volumeRestore(); + if (chatWindow) + { + chatWindow->displayAwayLog(); + chatWindow->clearAwayLog(); + } + removeAfkEffect(); + } +} + +static const char *awayModeStrings[] = +{ + // TRANSLATORS: away type in status bar + N_("(O) on keyboard"), + // TRANSLATORS: away type in status bar + N_("(A) away"), + // TRANSLATORS: away type in status bar + N_("(?) away") +}; + +std::string LocalPlayer::getAwayModeString() +{ + return gettext(getVarItem(&awayModeStrings[0], + mAwayMode, awayModeSize)); +} + +const unsigned cameraModeSize = 2; + +static const char *cameraModeStrings[] = +{ + // TRANSLATORS: camera mode in status bar + N_("(G) game camera mode"), + // TRANSLATORS: camera mode in status bar + N_("(F) free camera mode"), + // TRANSLATORS: camera mode in status bar + N_("(?) away") +}; + +std::string LocalPlayer::getCameraModeString() const +{ + return gettext(getVarItem(&cameraModeStrings[0], + viewport->getCameraMode(), cameraModeSize)); +} + +const unsigned gameModifiersSize = 2; + +void LocalPlayer::switchGameModifiers() +{ + mDisableGameModifiers = !mDisableGameModifiers; + config.setValue("disableGameModifiers", mDisableGameModifiers); + miniStatusWindow->updateStatus(); + + const std::string str = getGameModifiersString(); + if (str.size() > 4) + debugMsg(str.substr(4)); +} + +static const char *const gameModifiersStrings[] = +{ + // TRANSLATORS: game modifiers state in status bar + N_("Game modifiers are enabled"), + // TRANSLATORS: game modifiers state in status bar + N_("Game modifiers are disabled"), + // TRANSLATORS: game modifiers state in status bar + N_("Game modifiers are unknown") +}; + +std::string LocalPlayer::getGameModifiersString() +{ + return gettext(getVarItem(&gameModifiersStrings[0], + mDisableGameModifiers, gameModifiersSize)); +} + + +void LocalPlayer::changeEquipmentBeforeAttack(const Being *const target) const +{ + if (mAttackWeaponType == 1 || !target || !PlayerInfo::getInventory()) + return; + + bool allowSword = false; + const int dx = target->getTileX() - mX; + const int dy = target->getTileY() - mY; + const Item *item = nullptr; + + if (dx * dx + dy * dy > 80) + return; + + if (dx * dx + dy * dy < 8) + allowSword = true; + + const Inventory *const inv = PlayerInfo::getInventory(); + if (!inv) + return; + + // if attack distance for sword + if (allowSword) + { + // finding sword + item = inv->findItem(571, 0); + + if (!item) + item = inv->findItem(570, 0); + + if (!item) + item = inv->findItem(579, 0); + + if (!item) + item = inv->findItem(867, 0); + + if (!item) + item = inv->findItem(536, 0); + + if (!item) + item = inv->findItem(758, 0); + + // no swords + if (!item) + return; + + // if sword not equiped + if (!item->isEquipped()) + Net::getInventoryHandler()->equipItem(item); + + // if need equip shield too + if (mAttackWeaponType == 3) + { + // finding shield + item = inv->findItem(601, 0); + if (!item) + item = inv->findItem(602, 0); + if (item && !item->isEquipped()) + Net::getInventoryHandler()->equipItem(item); + } + } + // big distance. allowed only bow + else + { + // finding bow + item = inv->findItem(545, 0); + + if (!item) + item = inv->findItem(530, 0); + + // no bow + if (!item) + return; + + if (!item->isEquipped()) + Net::getInventoryHandler()->equipItem(item); + } +} + +void LocalPlayer::crazyMove() +{ + const bool oldDisableCrazyMove = mDisableCrazyMove; + mDisableCrazyMove = true; + switch (mCrazyMoveType) + { + case 1: + crazyMove1(); + break; + case 2: + crazyMove2(); + break; + case 3: + crazyMove3(); + break; + case 4: + crazyMove4(); + break; + case 5: + crazyMove5(); + break; + case 6: + crazyMove6(); + break; + case 7: + crazyMove7(); + break; + case 8: + crazyMove8(); + break; + case 9: + crazyMove9(); + break; + case 10: + crazyMoveA(); + break; + default: + break; + } + mDisableCrazyMove = oldDisableCrazyMove; +} + +void LocalPlayer::crazyMove1() +{ + if (mAction == MOVE) + return; + +// if (!client->limitPackets(PACKET_DIRECTION)) +// return; + + if (mDirection == Being::UP) + { + setWalkingDir(Being::UP); + setDirection(Being::LEFT); + Net::getPlayerHandler()->setDirection(Being::LEFT); + } + else if (mDirection == Being::LEFT) + { + setWalkingDir(Being::LEFT); + setDirection(Being::DOWN); + Net::getPlayerHandler()->setDirection(Being::DOWN); + } + else if (mDirection == Being::DOWN) + { + setWalkingDir(Being::DOWN); + setDirection(Being::RIGHT); + Net::getPlayerHandler()->setDirection(Being::RIGHT); + } + else if (mDirection == Being::RIGHT) + { + setWalkingDir(Being::RIGHT); + setDirection(Being::UP); + Net::getPlayerHandler()->setDirection(Being::UP); + } +} + +void LocalPlayer::crazyMove2() +{ + if (mAction == MOVE) + return; + +// if (!client->limitPackets(PACKET_DIRECTION)) +// return; + + if (mDirection == Being::UP) + { + setWalkingDir(Being::UP | Being::LEFT); + setDirection(Being::RIGHT); + Net::getPlayerHandler()->setDirection(Being::DOWN | Being::RIGHT); + } + else if (mDirection == Being::RIGHT) + { + setWalkingDir(Being::UP | Being::RIGHT); + setDirection(Being::DOWN); + Net::getPlayerHandler()->setDirection(Being::DOWN | Being::LEFT); + } + else if (mDirection == Being::DOWN) + { + setWalkingDir(Being::DOWN | Being::RIGHT); + setDirection(Being::LEFT); + Net::getPlayerHandler()->setDirection(Being::UP | Being::LEFT); + } + else if (mDirection == Being::LEFT) + { + setWalkingDir(Being::DOWN | Being::LEFT); + setDirection(Being::UP); + Net::getPlayerHandler()->setDirection(Being::UP | Being::RIGHT); + } +} + +void LocalPlayer::crazyMove3() +{ + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + move(1, 1); + mCrazyMoveState = 1; + break; + case 1: + move(1, -1); + mCrazyMoveState = 2; + break; + case 2: + move(-1, -1); + mCrazyMoveState = 3; + break; + case 3: + move(-1, 1); + mCrazyMoveState = 0; + break; + default: + break; + } + +// if (!client->limitPackets(PACKET_DIRECTION)) +// return; + + setDirection(Being::DOWN); + Net::getPlayerHandler()->setDirection(Being::DOWN); +} + +void LocalPlayer::crazyMove4() +{ + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + move(7, 0); + mCrazyMoveState = 1; + break; + case 1: + move(-7, 0); + mCrazyMoveState = 0; + break; + default: + break; + } +} + +void LocalPlayer::crazyMove5() +{ + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + move(0, 7); + mCrazyMoveState = 1; + break; + case 1: + move(0, -7); + mCrazyMoveState = 0; + break; + default: + break; + } +} + +void LocalPlayer::crazyMove6() +{ + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + move(3, 0); + mCrazyMoveState = 1; + break; + case 1: + move(2, -2); + mCrazyMoveState = 2; + break; + case 2: + move(0, -3); + mCrazyMoveState = 3; + break; + case 3: + move(-2, -2); + mCrazyMoveState = 4; + break; + case 4: + move(-3, 0); + mCrazyMoveState = 5; + break; + case 5: + move(-2, 2); + mCrazyMoveState = 6; + break; + case 6: + move(0, 3); + mCrazyMoveState = 7; + break; + case 7: + move(2, 2); + mCrazyMoveState = 0; + break; + default: + break; + } +} + +void LocalPlayer::crazyMove7() +{ + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + move(1, 1); + mCrazyMoveState = 1; + break; + case 1: + move(-1, 1); + mCrazyMoveState = 2; + break; + case 2: + move(-1, -1); + mCrazyMoveState = 3; + break; + case 3: + move(1, -1); + mCrazyMoveState = 0; + break; + default: + break; + } +} + +void LocalPlayer::crazyMove8() +{ + if (mAction == MOVE || !mMap) + return; + int idx = 0; + const int dist = 1; + +// look +// up, ri,do,le + static const int movesX[][4] = + { + {-1, 0, 1, 0}, // move left + { 0, 1, 0, -1}, // move up + { 1, 0, -1, 0}, // move right + { 0, -1, 0, 1} // move down + }; + +// look +// up, ri,do,le + static const int movesY[][4] = + { + { 0, -1, 0, 1}, // move left + {-1, 0, 1, 0}, // move up + { 0, 1, 0, -1}, // move right + { 1, 0, -1, 0} // move down + }; + + if (mDirection == Being::UP) + idx = 0; + else if (mDirection == Being::RIGHT) + idx = 1; + else if (mDirection == Being::DOWN) + idx = 2; + else if (mDirection == Being::LEFT) + idx = 3; + + + int mult = 1; + const unsigned char walkMask = getWalkMask(); + if (mMap->getWalk(mX + movesX[idx][0], + mY + movesY[idx][0], walkMask)) + { + while (mMap->getWalk(mX + movesX[idx][0] * mult, + mY + movesY[idx][0] * mult, + getWalkMask()) && mult <= dist) + { + mult ++; + } + move(movesX[idx][0] * (mult - 1), movesY[idx][0] * (mult - 1)); + } + else if (mMap->getWalk(mX + movesX[idx][1], + mY + movesY[idx][1], walkMask)) + { + while (mMap->getWalk(mX + movesX[idx][1] * mult, + mY + movesY[idx][1] * mult, + getWalkMask()) && mult <= dist) + { + mult ++; + } + move(movesX[idx][1] * (mult - 1), movesY[idx][1] * (mult - 1)); + } + else if (mMap->getWalk(mX + movesX[idx][2], + mY + movesY[idx][2], walkMask)) + { + while (mMap->getWalk(mX + movesX[idx][2] * mult, + mY + movesY[idx][2] * mult, + getWalkMask()) && mult <= dist) + { + mult ++; + } + move(movesX[idx][2] * (mult - 1), movesY[idx][2] * (mult - 1)); + } + else if (mMap->getWalk(mX + movesX[idx][3], + mY + movesY[idx][3], walkMask)) + { + while (mMap->getWalk(mX + movesX[idx][3] * mult, + mY + movesY[idx][3] * mult, + getWalkMask()) && mult <= dist) + { + mult ++; + } + move(movesX[idx][3] * (mult - 1), movesY[idx][3] * (mult - 1)); + } +} + +void LocalPlayer::crazyMove9() +{ + int dx = 0; + int dy = 0; + + if (mAction == MOVE) + return; + + switch (mCrazyMoveState) + { + case 0: + switch (mDirection) + { + case UP : dy = -1; break; + case DOWN : dy = 1; break; + case LEFT : dx = -1; break; + case RIGHT: dx = 1; break; + default: break; + } + move(dx, dy); + mCrazyMoveState = 1; + break; + case 1: + mCrazyMoveState = 2; + if (!allowAction()) + return; + Net::getPlayerHandler()->changeAction(SIT); + break; + case 2: + mCrazyMoveState = 3; + break; + case 3: + mCrazyMoveState = 0; + break; + default: + break; + } +} + +void LocalPlayer::crazyMoveA() +{ + const std::string mMoveProgram(config.getStringValue("crazyMoveProgram")); + + if (mAction == MOVE) + return; + + if (mMoveProgram.empty()) + return; + + if (mCrazyMoveState >= mMoveProgram.length()) + mCrazyMoveState = 0; + + // move command + if (mMoveProgram[mCrazyMoveState] == 'm') + { + mCrazyMoveState ++; + if (mCrazyMoveState < mMoveProgram.length()) + { + int dx = 0; + int dy = 0; + + signed char param = mMoveProgram[mCrazyMoveState++]; + if (param == '?') + { + const char cmd[] = {'l', 'r', 'u', 'd'}; + srand(tick_time); + param = cmd[rand() % 4]; + } + switch (param) + { + case 'd': + move(0, 1); + break; + case 'u': + move(0, -1); + break; + case 'l': + move(-1, 0); + break; + case 'r': + move(1, 0); + break; + case 'f': + switch (mDirection) + { + case UP : dy = -1; break; + case DOWN : dy = 1; break; + case LEFT : dx = -1; break; + case RIGHT: dx = 1; break; + default: break; + } + move(dx, dy); + break; + case 'b': + switch (mDirection) + { + case UP : dy = 1; break; + case DOWN : dy = -1; break; + case LEFT : dx = 1; break; + case RIGHT: dx = -1; break; + default: break; + } + move(dx, dy); + break; + default: + break; + } + } + } + // direction command + else if (mMoveProgram[mCrazyMoveState] == 'd') + { + mCrazyMoveState ++; + + if (mCrazyMoveState < mMoveProgram.length()) + { + signed char param = mMoveProgram[mCrazyMoveState++]; + if (param == '?') + { + const char cmd[] = {'l', 'r', 'u', 'd'}; + srand(tick_time); + param = cmd[rand() % 4]; + } + switch (param) + { + case 'd': + +// if (client->limitPackets(PACKET_DIRECTION)) + { + setDirection(Being::DOWN); + Net::getPlayerHandler()->setDirection(Being::DOWN); + } + break; + case 'u': +// if (client->limitPackets(PACKET_DIRECTION)) + { + setDirection(Being::UP); + Net::getPlayerHandler()->setDirection(Being::UP); + } + break; + case 'l': +// if (client->limitPackets(PACKET_DIRECTION)) + { + setDirection(Being::LEFT); + Net::getPlayerHandler()->setDirection(Being::LEFT); + } + break; + case 'r': +// if (client->limitPackets(PACKET_DIRECTION)) + { + setDirection(Being::RIGHT); + Net::getPlayerHandler()->setDirection(Being::RIGHT); + } + break; + case 'L': +// if (client->limitPackets(PACKET_DIRECTION)) + { + uint8_t dir = 0; + switch (mDirection) + { + case UP : dir = Being::LEFT; break; + case DOWN : dir = Being::RIGHT; break; + case LEFT : dir = Being::DOWN; break; + case RIGHT : dir = Being::UP; break; + default: break; + } + setDirection(dir); + Net::getPlayerHandler()->setDirection(dir); + } + break; + case 'R': +// if (client->limitPackets(PACKET_DIRECTION)) + { + uint8_t dir = 0; + switch (mDirection) + { + case UP : dir = Being::RIGHT; break; + case DOWN : dir = Being::LEFT; break; + case LEFT : dir = Being::UP; break; + case RIGHT : dir = Being::DOWN; break; + default: break; + } + setDirection(dir); + Net::getPlayerHandler()->setDirection(dir); + } + break; + case 'b': +// if (client->limitPackets(PACKET_DIRECTION)) + { + uint8_t dir = 0; + switch (mDirection) + { + case UP : dir = Being::DOWN; break; + case DOWN : dir = Being::UP; break; + case LEFT : dir = Being::RIGHT; break; + case RIGHT : dir = Being::LEFT; break; + default: break; + } + setDirection(dir); + Net::getPlayerHandler()->setDirection(dir); + } + break; + case '0': + dropShortcut->dropFirst(); + break; + case 'a': + dropShortcut->dropItems(); + break; + default: + break; + } + } + } + // sit command + else if (mMoveProgram[mCrazyMoveState] == 's') + { + mCrazyMoveState ++; + if (toggleSit()) + mCrazyMoveState ++; + } + // wear outfits + else if (mMoveProgram[mCrazyMoveState] == 'o') + { + mCrazyMoveState ++; + if (mCrazyMoveState < mMoveProgram.length()) + { + // wear next outfit + if (mMoveProgram[mCrazyMoveState] == 'n') + { + mCrazyMoveState ++; + outfitWindow->wearNextOutfit(); + } + // wear previous outfit + else if (mMoveProgram[mCrazyMoveState] == 'p') + { + mCrazyMoveState ++; + outfitWindow->wearPreviousOutfit(); + } + } + } + // pause + else if (mMoveProgram[mCrazyMoveState] == 'w') + { + mCrazyMoveState ++; + } + // pick up + else if (mMoveProgram[mCrazyMoveState] == 'p') + { + mCrazyMoveState ++; + pickUpItems(); + } + // emote + else if (mMoveProgram[mCrazyMoveState] == 'e') + { + mCrazyMoveState ++; + const signed char emo = mMoveProgram[mCrazyMoveState]; + if (emo == '?') + { + srand(tick_time); + emote(static_cast<unsigned char>(1 + (rand() % 13))); + } + else + { + if (emo >= '0' && emo <= '9') + emote(static_cast<unsigned char>(emo - '0' + 1)); + else if (emo >= 'a' && emo <= 'd') + emote(static_cast<unsigned char>(emo - 'a' + 11)); + } + + mCrazyMoveState ++; + } + else + { + mCrazyMoveState ++; + } + + if (mCrazyMoveState >= mMoveProgram.length()) + mCrazyMoveState = 0; +} + +bool LocalPlayer::isReachable(Being *const being, + const int maxCost) +{ + if (!being || !mMap) + return false; + + if (being->isReachable() == Being::REACH_NO) + return false; + + if (being->getTileX() == mX + && being->getTileY() == mY) + { + being->setDistance(0); + being->setIsReachable(Being::REACH_YES); + return true; + } + else if (being->getTileX() - 1 <= mX + && being->getTileX() + 1 >= mX + && being->getTileY() - 1 <= mY + && being->getTileY() + 1 >= mY) + { + being->setDistance(1); + being->setIsReachable(Being::REACH_YES); + return true; + } + + const Vector &playerPos = getPosition(); + + const Path debugPath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + being->getTileX(), being->getTileY(), getWalkMask(), maxCost); + + being->setDistance(static_cast<int>(debugPath.size())); + if (!debugPath.empty()) + { + being->setIsReachable(Being::REACH_YES); + return true; + } + else + { + being->setIsReachable(Being::REACH_NO); + return false; + } +} + +bool LocalPlayer::isReachable(const int x, const int y, + const bool allowCollision) const +{ + const WalkLayer *const walk = mMap->getWalkLayer(); + if (!walk) + return false; + int num = walk->getDataAt(x, y); + if (allowCollision && num < 0) + num = -num; + + return walk->getDataAt(mX, mY) == num; +} + +bool LocalPlayer::pickUpItems(int pickUpType) +{ + if (!actorSpriteManager) + return false; + + bool status = false; + int x = mX; + int y = mY; + + // first pick up item on player position + FloorItem *item = + actorSpriteManager->findItem(x, y); + if (item) + status = pickUp(item); + + if (pickUpType == 0) + pickUpType = mPickUpType; + + if (pickUpType == 0) + return status; + + int x1, y1, x2, y2; + switch (pickUpType) + { + case 1: + switch (mDirection) + { + case UP : --y; break; + case DOWN : ++y; break; + case LEFT : --x; break; + case RIGHT: ++x; break; + default: break; + } + item = actorSpriteManager->findItem(x, y); + if (item) + status = pickUp(item); + break; + case 2: + switch (mDirection) + { + case UP : x1 = x - 1; y1 = y - 1; x2 = x + 1; y2 = y; break; + case DOWN : x1 = x - 1; y1 = y; x2 = x + 1; y2 = y + 1; break; + case LEFT : x1 = x - 1; y1 = y - 1; x2 = x; y2 = y + 1; break; + case RIGHT: x1 = x; y1 = y - 1; x2 = x + 1; y2 = y + 1; break; + default: x1 = x; x2 = x; y1 = y; y2 = y; break; + } + if (actorSpriteManager->pickUpAll(x1, y1, x2, y2)) + status = true; + break; + case 3: + if (actorSpriteManager->pickUpAll(x - 1, y - 1, x + 1, y + 1)) + status = true; + break; + + case 4: + if (!actorSpriteManager->pickUpAll(x - 1, y - 1, x + 1, y + 1)) + { + if (actorSpriteManager->pickUpNearest(x, y, 4)) + status = true; + } + else + { + status = true; + } + break; + + case 5: + if (!actorSpriteManager->pickUpAll(x - 1, y - 1, x + 1, y + 1)) + { + if (actorSpriteManager->pickUpNearest(x, y, 8)) + status = true; + } + else + { + status = true; + } + break; + + case 6: + if (!actorSpriteManager->pickUpAll(x - 1, y - 1, x + 1, y + 1)) + { + if (actorSpriteManager->pickUpNearest(x, y, 90)) + status = true; + } + else + { + status = true; + } + break; + + default: + break; + } + return status; +} + + +void LocalPlayer::moveByDirection(const unsigned char dir) +{ + int dx = 0, dy = 0; +#ifdef MANASERV_SUPPORT + if (dir & UP) + dy -= 32; + if (dir & DOWN) + dy += 32; + if (dir & LEFT) + dx -= 32; + if (dir & RIGHT) + dx += 32; +#else + if (dir & UP) + dy--; + if (dir & DOWN) + dy++; + if (dir & LEFT) + dx--; + if (dir & RIGHT) + dx++; +#endif + + move(dx, dy); +} + +void LocalPlayer::specialMove(const unsigned char direction) +{ + if (direction && (mNavigateX || mNavigateY)) + navigateClean(); + + if (direction && (mInvertDirection >= 2 + && mInvertDirection <= 4) + && !mIsServerBuggy) + { + if (mAction == MOVE) + return; + + int max; + + if (mInvertDirection == 2) + max = 5; + else if (mInvertDirection == 4) + max = 1; + else + max = 3; + + if (getMoveState() < max) + { + moveByDirection(direction); + mMoveState ++; + } + else + { + mMoveState = 0; + crazyMove(); + } + } + else + { + setWalkingDir(direction); + } +} + +void LocalPlayer::debugMsg(const std::string &str) const +{ + if (debugChatTab) + debugChatTab->chatLog(str); +} + +void LocalPlayer::magicAttack() const +{ + if (!chatWindow || !isAlive() + || !Net::getPlayerHandler()->canUseMagic()) + { + return; + } + + switch (mMagicAttackType) + { + // flar W00 + case 0: + tryMagic("#flar", 1, 0, 10); + break; + // chiza W01 + case 1: + tryMagic("#chiza", 1, 0, 9); + break; + // ingrav W10 + case 2: + tryMagic("#ingrav", 2, 2, 20); + break; + // frillyar W11 + case 3: + tryMagic("#frillyar", 2, 2, 25); + break; + // upmarmu W12 + case 4: + tryMagic("#upmarmu", 2, 2, 20); + break; + default: + break; + } +} + +void LocalPlayer::tryMagic(const std::string &spell, const int baseMagic, + const int schoolMagic, const int mana) +{ + if (!chatWindow) + return; + + if (PlayerInfo::getSkillLevel(340) >= baseMagic + && PlayerInfo::getSkillLevel(342) >= schoolMagic) + { + if (PlayerInfo::getAttribute(PlayerInfo::MP) >= mana) + { + if (!client->limitPackets(PACKET_CHAT)) + return; + + chatWindow->localChatInput(spell); + } + } +} + +void LocalPlayer::loadHomes() +{ + if (serverVersion > 0) + return; + std::string buf; + std::stringstream ss(serverConfig.getValue("playerHomes", + "maps/018-1.tmx 71 76 maps/013-3.tmx 71 24")); + + while (ss >> buf) + { + Vector pos; + ss >> pos.x; + ss >> pos.y; + mHomes[buf] = pos; + } +} + +void LocalPlayer::setMap(Map *const map) +{ + if (map) + { + if (socialWindow) + socialWindow->updateActiveList(); + } + navigateClean(); + mCrossX = 0; + mCrossY = 0; + + Being::setMap(map); + updateNavigateList(); +} + +void LocalPlayer::setHome() +{ + if (!mMap || !socialWindow) + return; + + SpecialLayer *const specialLayer = mMap->getSpecialLayer(); + + if (!specialLayer) + return; + + const std::string key = mMap->getProperty("_realfilename"); + Vector pos = mHomes[key]; + + if (mAction == SIT) + { + const std::map<std::string, Vector>::const_iterator + iter = mHomes.find(key); + + if (iter != mHomes.end()) + { + socialWindow->removePortal(static_cast<int>(pos.x), + static_cast<int>(pos.y)); + } + + if (iter != mHomes.end() && mX == static_cast<int>(pos.x) + && mY == static_cast<int>(pos.y)) + { + mMap->updatePortalTile("", MapItem::EMPTY, + static_cast<int>(pos.x), static_cast<int>(pos.y)); + + mHomes.erase(key); + socialWindow->removePortal(static_cast<int>(pos.x), + static_cast<int>(pos.y)); + } + else + { + if (specialLayer && iter != mHomes.end()) + { + specialLayer->setTile(static_cast<int>(pos.x), + static_cast<int>(pos.y), MapItem::EMPTY); + } + + pos.x = static_cast<float>(mX); + pos.y = static_cast<float>(mY); + mHomes[key] = pos; + mMap->updatePortalTile("home", MapItem::HOME, + mX, mY); + socialWindow->addPortal(mX, mY); + } + MapItem *const mapItem = specialLayer->getTile(mX, mY); + if (mapItem) + { + const int idx = socialWindow->getPortalIndex(mX, mY); + mapItem->setName(keyboard.getKeyShortString( + outfitWindow->keyName(idx))); + } + saveHomes(); + } + else + { + MapItem *mapItem = specialLayer->getTile(mX, mY); + int type = 0; + + const std::map<std::string, Vector>::iterator iter = mHomes.find(key); + if (iter != mHomes.end() && mX == pos.x && mY == pos.y) + { + mHomes.erase(key); + saveHomes(); + } + + if (!mapItem || mapItem->getType() == MapItem::EMPTY) + { + if (mDirection & UP) + type = MapItem::ARROW_UP; + else if (mDirection & LEFT) + type = MapItem::ARROW_LEFT; + else if (mDirection & DOWN) + type = MapItem::ARROW_DOWN; + else if (mDirection & RIGHT) + type = MapItem::ARROW_RIGHT; + } + else + { + type = MapItem::EMPTY; + } + mMap->updatePortalTile("", type, mX, mY); + + if (type != MapItem::EMPTY) + { + socialWindow->addPortal(mX, mY); + mapItem = specialLayer->getTile(mX, mY); + if (mapItem) + { + const int idx = socialWindow->getPortalIndex(mX, mY); + mapItem->setName(keyboard.getKeyShortString( + outfitWindow->keyName(idx))); + } + } + else + { + specialLayer->setTile(mX, mY, MapItem::EMPTY); + socialWindow->removePortal(mX, mY); + } + } +} + +void LocalPlayer::saveHomes() +{ + std::stringstream ss; + + for (std::map<std::string, Vector>::const_iterator iter = mHomes.begin(), + iter_end = mHomes.end(); iter != iter_end; ++iter ) + { + const Vector &pos = (*iter).second; + + if (iter != mHomes.begin()) + ss << " "; + ss << (*iter).first << " " << pos.x << " " << pos.y; + } + + serverConfig.setValue("playerHomes", ss.str()); +} + +void LocalPlayer::pingRequest() +{ + const int time = tick_time; + if (mWaitPing == true && mPingSendTick != 0) + { + if (time >= mPingSendTick && (time - mPingSendTick) > 1000) + return; + } + + mPingSendTick = time; + mWaitPing = true; + Net::getBeingHandler()->requestNameById(getId()); +} + +std::string LocalPlayer::getPingTime() const +{ + std::string str; + if (!mWaitPing) + { + if (!mPingTime) + str = "?"; + else + str = toString(mPingTime); + } + else + { + int time = tick_time; + if (time > mPingSendTick) + time -= mPingSendTick; + else + time += MAX_TICK_VALUE - mPingSendTick; + if (time <= mPingTime) + time = mPingTime; + if (mPingTime != time) + str = strprintf("%d (%d)", mPingTime, time); + else + str = toString(time); + } + return str; +} + +void LocalPlayer::pingResponse() +{ + if (mWaitPing == true && mPingSendTick > 0) + { + mWaitPing = false; + const int time = tick_time; + if (time < mPingSendTick) + { + mPingSendTick = 0; + mPingTime = 0; + } + else + { + mPingTime = (time - mPingSendTick) * 10; + } + } +} + +void LocalPlayer::tryPingRequest() +{ + if (mPingSendTick == 0 || tick_time < mPingSendTick + || (tick_time - mPingSendTick) > 200) + { + pingRequest(); + } +} + + +void LocalPlayer::setAway(const std::string &message) +{ + if (!message.empty()) + config.setValue("afkMessage", message); + changeAwayMode(); + updateStatus(); +} + +void LocalPlayer::setPseudoAway(const std::string &message) +{ + if (!message.empty()) + config.setValue("afkMessage", message); + mPseudoAwayMode = !mPseudoAwayMode; +} + +void LocalPlayer::afkRespond(ChatTab *const tab, const std::string &nick) +{ + if (mAwayMode) + { + const int time = cur_time; + if (mAfkTime == 0 || time < mAfkTime + || time - mAfkTime > awayLimitTimer) + { + const std::string msg = "*AFK*: " + + config.getStringValue("afkMessage"); + + if (!tab) + { + Net::getChatHandler()->privateMessage(nick, msg); + if (localChatTab) + { + localChatTab->chatLog(std::string(getName()).append( + " : ").append(msg), ACT_WHISPER, false); + } + } + else + { + if (tab->getNoAway()) + return; + Net::getChatHandler()->privateMessage(nick, msg); + tab->chatLog(getName(), msg); + } + mAfkTime = time; + } + } +} + +bool LocalPlayer::navigateTo(const int x, const int y) +{ + if (!mMap) + return false; + + SpecialLayer *const tmpLayer = mMap->getTempLayer(); + if (!tmpLayer) + return false; + + const Vector &playerPos = getPosition(); + mShowNavigePath = true; + mOldX = static_cast<int>(playerPos.x); + mOldY = static_cast<int>(playerPos.y); + mOldTileX = mX; + mOldTileY = mY; + mNavigateX = x; + mNavigateY = y; + mNavigateId = 0; + + mNavigatePath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + x, y, getWalkMask(), 0); + + if (mDrawPath) + tmpLayer->addRoad(mNavigatePath); + return !mNavigatePath.empty(); +} + +void LocalPlayer::navigateTo(const Being *const being) +{ + if (!mMap || !being) + return; + + SpecialLayer *const tmpLayer = mMap->getTempLayer(); + if (!tmpLayer) + return; + + const Vector &playerPos = getPosition(); + mShowNavigePath = true; + mOldX = static_cast<int>(playerPos.x); + mOldY = static_cast<int>(playerPos.y); + mOldTileX = mX; + mOldTileY = mY; + mNavigateX = being->getTileX(); + mNavigateY = being->getTileY(); + + mNavigatePath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + being->getTileX(), being->getTileY(), + getWalkMask(), 0); + + if (mDrawPath) + tmpLayer->addRoad(mNavigatePath); +} + +void LocalPlayer::navigateClean() +{ + if (!mMap) + return; + + mShowNavigePath = false; + mOldX = 0; + mOldY = 0; + mOldTileX = 0; + mOldTileY = 0; + mNavigateX = 0; + mNavigateY = 0; + mNavigateId = 0; + + mNavigatePath.clear(); + + const SpecialLayer *const tmpLayer = mMap->getTempLayer(); + if (!tmpLayer) + return; + + tmpLayer->clean(); +} + +void LocalPlayer::updateCoords() +{ + Being::updateCoords(); + + const Vector &playerPos = getPosition(); + // probably map not loaded. + if (!playerPos.x || !playerPos.y) + return; + + if (mX != mOldTileX || mY != mOldTileY) + { + if (socialWindow) + socialWindow->updatePortals(); + if (viewport) + viewport->hideBeingPopup(); + if (mMap) + { + std::string str = mMap->getObjectData(mX, mY, + MapItem::MUSIC); + if (str.empty()) + str = mMap->getMusicFile(); + if (str != soundManager.getCurrentMusicFile()) + { + if (str.empty()) + soundManager.fadeOutMusic(); + else + soundManager.fadeOutAndPlayMusic(str); + } + } + } + + if (mShowNavigePath) + { + if (mMap && (mX != mOldTileX || mY != mOldTileY)) + { + SpecialLayer *const tmpLayer = mMap->getTempLayer(); + if (!tmpLayer) + return; + + const int x = static_cast<int>(playerPos.x - 16) / 32; + const int y = static_cast<int>(playerPos.y - 32) / 32; + if (mNavigateId) + { + if (!actorSpriteManager) + { + navigateClean(); + return; + } + + const Being *const being = actorSpriteManager + ->findBeing(mNavigateId); + if (!being) + { + navigateClean(); + return; + } + mNavigateX = being->getTileX(); + mNavigateY = being->getTileY(); + } + + if (mNavigateX == x && mNavigateY == y) + { + navigateClean(); + return; + } + else + { + for (Path::const_iterator i = mNavigatePath.begin(), + i_end = mNavigatePath.end(); i != i_end; ++ i) + { + if ((*i).x == mX && (*i).y == mY) + { + mNavigatePath.pop_front(); + break; + } + } + + if (mDrawPath) + { + tmpLayer->clean(); + tmpLayer->addRoad(mNavigatePath); + } + } + } + } + mOldX = static_cast<int>(playerPos.x); + mOldY = static_cast<int>(playerPos.y); + mOldTileX = mX; + mOldTileY = mY; +} + +void LocalPlayer::targetMoved() const +{ +/* + if (mKeepAttacking) + { + if (mTarget && mServerAttack) + { + logger->log("LocalPlayer::targetMoved0"); + if (!client->limitPackets(PACKET_ATTACK)) + return; + logger->log("LocalPlayer::targetMoved"); + Net::getPlayerHandler()->attack(mTarget->getId(), mServerAttack); + } + } +*/ +} + +int LocalPlayer::getPathLength(const Being *const being) const +{ + if (!mMap || !being) + return 0; + + const Vector &playerPos = getPosition(); + + if (being->mX == mX && being->mY == mY) + return 0; + + if (being->mX - 1 <= mX && being->mX + 1 >= mX + && being->mY - 1 <= mY && being->mY + 1 >= mY) + { + return 1; + } + + if (mTargetOnlyReachable) + { + const Path debugPath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + being->getTileX(), being->getTileY(), + getWalkMask(), 0); + return static_cast<int>(debugPath.size()); + } + else + { + const int dx = static_cast<int>(abs(being->mX - mX)); + const int dy = static_cast<int>(abs(being->mY - mY)); + if (dx > dy) + return dx; + return dy; + } +} + +int LocalPlayer::getAttackRange2() const +{ + int range = getAttackRange(); + if (range == 1) + range = 2; + return range; +} + +void LocalPlayer::attack2(Being *const target, const bool keep, + const bool dontChangeEquipment) +{ + if (!dontChangeEquipment && target) + changeEquipmentBeforeAttack(target); + + // probably need cache getPathLength(target) + if ((!target || mAttackType == 0 || mAttackType == 3) + || (withinAttackRange(target, serverVersion < 1, + serverVersion < 1 ? 1 : 0) + && getPathLength(target) <= getAttackRange2())) + { + attack(target, keep); + if (mAttackType == 2) + { + if (!target) + { + if (pickUpItems()) + return; + } + else + { + pickUpItems(3); + } + } + } + else if (!mPickUpTarget) + { + if (mAttackType == 2) + { + if (pickUpItems()) + return; + } + setTarget(target); + if (target && target->getType() != Being::NPC) + { + mKeepAttacking = true; + moveToTarget(); + } + } +} + +void LocalPlayer::setFollow(const std::string &player) +{ + mPlayerFollowed = player; + if (!mPlayerFollowed.empty()) + { + // TRANSLATORS: follow command message + std::string msg = strprintf(_("Follow: %s"), player.c_str()); + debugMsg(msg); + } + else + { + // TRANSLATORS: follow command message + debugMsg(_("Follow canceled")); + } +} + +void LocalPlayer::setImitate(const std::string &player) +{ + mPlayerImitated = player; + if (!mPlayerImitated.empty()) + { + // TRANSLATORS: imitate command message + std::string msg = strprintf(_("Imitation: %s"), player.c_str()); + debugMsg(msg); + } + else + { + // TRANSLATORS: imitate command message + debugMsg(_("Imitation canceled")); + } +} + +void LocalPlayer::cancelFollow() +{ + if (!mPlayerFollowed.empty()) + { + // TRANSLATORS: cancel follow message + debugMsg(_("Follow canceled")); + } + if (!mPlayerImitated.empty()) + { + // TRANSLATORS: cancel follow message + debugMsg(_("Imitation canceled")); + } + mPlayerFollowed.clear(); + mPlayerImitated.clear(); +} + +void LocalPlayer::imitateEmote(const Being *const being, + const unsigned char action) const +{ + if (!being) + return; + + std::string player_imitated = getImitate(); + if (!player_imitated.empty() && being->getName() == player_imitated) + emote(action); +} + +void LocalPlayer::imitateAction(const Being *const being, + const Being::Action &action) +{ + if (!being) + return; + + if (!mPlayerImitated.empty() && being->getName() == mPlayerImitated) + { + setAction(action); + Net::getPlayerHandler()->changeAction(action); + } +} + +void LocalPlayer::imitateDirection(const Being *const being, + const unsigned char dir) +{ + if (!being) + return; + + if (!mPlayerImitated.empty() && being->getName() == mPlayerImitated) + { + if (!client->limitPackets(PACKET_DIRECTION)) + return; + + if (mFollowMode == 2) + { + uint8_t dir2 = 0; + if (dir & Being::LEFT) + dir2 |= Being::RIGHT; + else if (dir & Being::RIGHT) + dir2 |= Being::LEFT; + if (dir & Being::UP) + dir2 |= Being::DOWN; + else if (dir & Being::DOWN) + dir2 |= Being::UP; + + setDirection(dir2); + Net::getPlayerHandler()->setDirection(dir2); + } + else + { + setDirection(dir); + Net::getPlayerHandler()->setDirection(dir); + } + } +} + +void LocalPlayer::imitateOutfit(Being *const player, const int sprite) const +{ + if (!player) + return; + + if (mImitationMode == 1 && !mPlayerImitated.empty() + && player->getName() == mPlayerImitated) + { + if (sprite < 0 || sprite >= player->getNumberOfLayers()) + return; + + const AnimatedSprite *const equipmentSprite + = dynamic_cast<const AnimatedSprite *const>( + player->getSprite(sprite)); + + if (equipmentSprite) + { +// logger->log("have equipmentSprite"); + const Inventory *const inv = PlayerInfo::getInventory(); + if (!inv) + return; + + const std::string &path = equipmentSprite->getIdPath(); + if (path.empty()) + return; + +// logger->log("idPath: " + path); + const Item *const item = inv->findItemBySprite(path, + player->getGender(), player->getSubType()); + if (item && !item->isEquipped()) + Net::getInventoryHandler()->equipItem(item); + } + else + { +// logger->log("have unequip %d", sprite); + const int equipmentSlot = Net::getInventoryHandler() + ->convertFromServerSlot(sprite); +// logger->log("equipmentSlot: " + toString(equipmentSlot)); + if (equipmentSlot == EQUIP_PROJECTILE_SLOT) + return; + + const Item *const item = PlayerInfo::getEquipment(equipmentSlot); + if (item) + { +// logger->log("unequiping"); + Net::getInventoryHandler()->unequipItem(item); + } + } + } +} + +void LocalPlayer::followMoveTo(const Being *const being, + const int x, const int y) +{ + if (being && !mPlayerFollowed.empty() + && being->getName() == mPlayerFollowed) + { + mPickUpTarget = nullptr; + setDestination(x, y); + } +} + +void LocalPlayer::followMoveTo(const Being *const being, + const int x1, const int y1, + const int x2, const int y2) +{ + if (!being) + return; + + mPickUpTarget = nullptr; + if (!mPlayerFollowed.empty() && being->getName() == mPlayerFollowed) + { + switch (mFollowMode) + { + case 0: + setDestination(x1, y1); + setNextDest(x2, y2); + break; + case 1: + if (x1 != x2 || y1 != y2) + { + setDestination(mX + x2 - x1, mY + y2 - y1); + setNextDest(mX + x2 - x1, mY + y2 - y1); + } + break; + case 2: + if (x1 != x2 || y1 != y2) + { + setDestination(mX + x1 - x2, mY + y1 - y2); + setNextDest(mX + x1 - x2, mY + y1 - y2); + } + break; + case 3: + if (!mTarget || mTarget->getName() != mPlayerFollowed) + { + if (actorSpriteManager) + { + Being *const b = actorSpriteManager->findBeingByName( + mPlayerFollowed, Being::PLAYER); + setTarget(b); + } + } + moveToTarget(); + setNextDest(x2, y2); + break; + default: + break; + } + } +} + +void LocalPlayer::setNextDest(const int x, const int y) +{ + mNextDestX = x; + mNextDestY = y; +} + +bool LocalPlayer::allowAction() +{ + if (mIsServerBuggy) + { + if (mLastAction != -1) + return false; + mLastAction = tick_time; + } + return true; +} + +/* +bool LocalPlayer::allowMove() const +{ + if (mIsServerBuggy) + { + if (mAction == MOVE) + return false; + } + return true; +} +*/ + +void LocalPlayer::fixPos(const int maxDist) +{ + if (!mCrossX && !mCrossY) + return; + + const int dx = abs(mX - mCrossX); + const int dy = abs(mY - mCrossY); + const int dest = (dx * dx) + (dy * dy); + const int time = cur_time; + + if (dest > maxDist && mActivityTime + && (time < mActivityTime || time - mActivityTime > 2)) + { + mActivityTime = time; + moveTo(mCrossX, mCrossY); + } +} + +void LocalPlayer::setRealPos(const int x, const int y) +{ + if (!mMap) + return; + + SpecialLayer *const layer = mMap->getTempLayer(); + if (layer) + { + fixPos(1); + + if ((mCrossX || mCrossY) && layer->getTile(mCrossX, mCrossY) + && layer->getTile(mCrossX, mCrossY)->getType() == MapItem::CROSS) + { + layer->setTile(mCrossX, mCrossY, MapItem::EMPTY); + } + + if (!layer->getTile(x, y) + || layer->getTile(x, y)->getType() == MapItem::EMPTY) + { + if (getTileX() != x && getTileY() != y) + layer->setTile(x, y, MapItem::CROSS); + } + + mCrossX = x; + mCrossY = y; + } + if (mMap->isCustom()) + mMap->setWalk(x, y, true); +} +void LocalPlayer::fixAttackTarget() +{ + if (!mMap || !mTarget) + return; + + if (mMoveToTargetType == 7 || !mAttackType + || !config.getBoolValue("autofixPos")) + { + return; + } + + const Vector &playerPos = getPosition(); + const Path debugPath = mMap->findPath( + static_cast<int>(playerPos.x - 16) / 32, + static_cast<int>(playerPos.y - 32) / 32, + mTarget->getTileX(), mTarget->getTileY(), + getWalkMask(), 0); + + if (!debugPath.empty()) + { + const Path::const_iterator i = debugPath.begin(); + moveTo((*i).x, (*i).y); + } +} + +void LocalPlayer::respawn() +{ + navigateClean(); +} + +int LocalPlayer::getLevel() const +{ + return PlayerInfo::getAttribute(PlayerInfo::LEVEL); +} + +void LocalPlayer::updateNavigateList() +{ + if (mMap) + { + const std::map<std::string, Vector>::const_iterator iter = + mHomes.find(mMap->getProperty("_realfilename")); + + if (iter != mHomes.end()) + { + const Vector &pos = mHomes[(*iter).first]; + if (pos.x && pos.y) + { + mMap->addPortalTile("home", MapItem::HOME, + static_cast<int>(pos.x), static_cast<int>(pos.y)); + } + } + } +} + +void LocalPlayer::waitFor(const std::string &nick) +{ + mWaitFor = nick; +} + +void LocalPlayer::checkNewName(Being *const being) +{ + if (!being) + return; + + const std::string nick = being->getName(); + if (being->getType() == ActorSprite::PLAYER) + { + const Guild *const guild = getGuild(); + if (guild) + { + const GuildMember *const gm = guild->getMember(nick); + if (gm) + { + const int level = gm->getLevel(); + if (level > 1 && being->getLevel() != level) + { + being->setLevel(level); + being->updateName(); + } + } + } + if (chatWindow) + { + WhisperTab *const tab = chatWindow->getWhisperTab(nick); + if (tab) + tab->setWhisperTabColors(); + } + } + + if (!mWaitFor.empty() && mWaitFor == nick) + { + // TRANSLATORS: wait player/monster message + debugMsg(strprintf(_("You see %s"), mWaitFor.c_str())); + soundManager.playGuiSound(SOUND_INFO); + mWaitFor.clear(); + } +} + +void LocalPlayer::resetYellowBar() +{ + mInvertDirection = 0; + mCrazyMoveType = config.resetIntValue("crazyMoveType"); + mMoveToTargetType = config.resetIntValue("moveToTargetType"); + mFollowMode = config.resetIntValue("followMode"); + mAttackWeaponType = config.resetIntValue("attackWeaponType"); + mAttackType = config.resetIntValue("attackType"); + mMagicAttackType = config.resetIntValue("magicAttackType"); + mPvpAttackType = config.resetIntValue("pvpAttackType"); + mQuickDropCounter = config.resetIntValue("quickDropCounter"); + mPickUpType = config.resetIntValue("pickUpType"); + if (viewport) + { + viewport->setDebugPath(0); + if (viewport->getCameraMode()) + viewport->toggleCameraMode(); + } + if (mMap) + mMap->setDebugFlags(0); + mImitationMode = config.resetIntValue("imitationMode"); + mDisableGameModifiers = config.resetBoolValue("disableGameModifiers"); + + if (miniStatusWindow) + miniStatusWindow->updateStatus(); +} + +unsigned char LocalPlayer::getWalkMask() const +{ + // for now blocking all types of collisions + return Map::BLOCKMASK_WALL | Map::BLOCKMASK_AIR | Map::BLOCKMASK_WATER; +} + +void LocalPlayer::removeHome() +{ + if (!mMap) + return; + + const std::string key = mMap->getProperty("_realfilename"); + const std::map<std::string, Vector>::iterator iter = mHomes.find(key); + + if (iter != mHomes.end()) + mHomes.erase(key); +} + +void LocalPlayer::stopAdvert() +{ + mBlockAdvert = true; +} + +bool LocalPlayer::checAttackPermissions(const Being *const target) const +{ + if (!target) + return false; + + switch (mPvpAttackType) + { + case 0: + return true; + case 1: + return !(player_relations.getRelation(target->getName()) + == PlayerRelation::FRIEND); + case 2: + return player_relations.checkBadRelation(target->getName()); + default: + case 3: + return false; + } +} + + +const char *LocalPlayer::getVarItem(const char *const *const arr, + const unsigned index, + const unsigned sz) const +{ + if (index < sz) + return arr[index]; + return arr[sz]; +} + +void LocalPlayer::updateStatus() const +{ + if (serverVersion >= 4 && mEnableAdvert) + { + uint8_t status = 0; + if (mTradebot && shopWindow && !shopWindow->isShopEmpty()) + status |= FLAG_SHOP; + + if (mAwayMode || mPseudoAwayMode) + status |= FLAG_AWAY; + + if (mInactive) + status |= FLAG_INACTIVE; + + Net::getPlayerHandler()->updateStatus(status); + } +} + +void LocalPlayer::setTestParticle(const std::string &fileName, bool updateHash) +{ + mTestParticleName = fileName; + mTestParticleTime = cur_time; + if (mTestParticle) + { + mChildParticleEffects.removeLocally(mTestParticle); + mTestParticle = nullptr; + } + if (!fileName.empty()) + { + mTestParticle = particleEngine->addEffect(fileName, 0, 0, false); + controlParticle(mTestParticle); + if (updateHash) + mTestParticleHash = UpdaterWindow::getFileHash(mTestParticleName); + } +} + +void AwayListener::action(const gcn::ActionEvent &event) +{ + if (event.getId() == "ok" && player_node && player_node->getAway()) + { + player_node->changeAwayMode(); + player_node->updateStatus(); + if (outfitWindow) + outfitWindow->unwearAwayOutfit(); + if (miniStatusWindow) + miniStatusWindow->updateStatus(); + } +} diff --git a/src/being/localplayer.h b/src/being/localplayer.h new file mode 100644 index 000000000..4f57df0d7 --- /dev/null +++ b/src/being/localplayer.h @@ -0,0 +1,652 @@ +/* + * The ManaPlus Client + * Copyright (C) 2004-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_LOCALPLAYER_H +#define BEING_LOCALPLAYER_H + +#include "depricatedlistener.h" + +#include "being/actorspritelistener.h" +#include "being/being.h" + +#include "gui/userpalette.h" + +#include <guichan/actionlistener.hpp> + +#include <memory> +#include <vector> + +#include "localconsts.h" + +class ChatTab; +class FloorItem; +class ImageSet; +class Item; +class Map; +class OkDialog; + +class AwayListener final : public gcn::ActionListener +{ + public: + void action(const gcn::ActionEvent &event) override; +}; + +/** + * Reasons an item can fail to be picked up. + */ +enum +{ + PICKUP_OKAY = 0, + PICKUP_BAD_ITEM, + PICKUP_TOO_HEAVY, + PICKUP_TOO_FAR, + PICKUP_INV_FULL, + PICKUP_STACK_FULL, + PICKUP_DROP_STEAL +}; + +/** + * The local player character. + */ +class LocalPlayer final : public Being, + public ActorSpriteListener, + public DepricatedListener +{ + public: + /** + * Constructor. + */ + explicit LocalPlayer(const int id = 65535, const int subtype = 0); + + A_DELETE_COPY(LocalPlayer) + + /** + * Destructor. + */ + ~LocalPlayer(); + + virtual void logic() override; + + void slowLogic(); + + virtual void setAction(const Action &action, + const int attackType = 0) override; + + /** + * Compute the next pathnode location when walking using keyboard. + * used by nextTile(). + */ + Position getNextWalkPosition(const unsigned char dir) + const A_WARN_UNUSED; + + /** + * Adds a new tile to the path when walking. + * @note Eathena + * Also, when specified, it picks up an item at the end of a path + * or attack target. + */ + virtual void nextTile() override + { nextTile(0); } + + virtual void nextTile(unsigned char dir); + + bool pickUp(FloorItem *const item); + + /** + * Called when an ActorSprite has been destroyed. + * @param actorSprite the ActorSprite being destroyed. + */ + void actorSpriteDestroyed(const ActorSprite &actorSprite) override; + + /** + * Gets the attack range. + */ + int getAttackRange() const A_WARN_UNUSED; + + int getAttackRange2() const A_WARN_UNUSED; + + void attack(Being *const target = nullptr, const bool keep = false, + const bool dontChangeEquipment = false); + + void attack2(Being *const target = nullptr, const bool keep = false, + const bool dontChangeEquipment = false); + + void setGMLevel(const int level); + + int getGMLevel() const A_WARN_UNUSED + { return mGMLevel; } + + void stopAttack(const bool keepAttack = false); + + void untarget(); + + /** + * Returns the current target of the player. Returns 0 if no being is + * currently targeted. + */ + Being *getTarget() const A_WARN_UNUSED; + + /** + * Sets the target being of the player. + */ + void setTarget(Being *const target); + + /** + * Sets a new destination for this being to walk to. + */ + virtual void setDestination(const int x, const int y); + + /** + * Sets a new direction to keep walking in. + */ + void setWalkingDir(const unsigned char dir); + + /** + * Gets the walking direction + */ + unsigned char getWalkingDir() const A_WARN_UNUSED + { return mWalkingDir; } + + /** + * Sets going to being to attack + */ + void setGotoTarget(Being *const target); + + /** + * Returns whether the target is in range to attack + */ + bool withinAttackRange(const Being *const target, + const bool fixDistance = false, + const int addRange = 0) const A_WARN_UNUSED; + + /** + * Stops the player dead in his tracks + */ + void stopWalking(const bool sendToServer = true); + + bool toggleSit() const; + + bool updateSit() const; + + static bool emote(const uint8_t emotion); + + /** + * Shows item pickup notifications. + */ + void pickedUp(const ItemInfo &itemInfo, const int amount, + const unsigned char color, const int floorItemId, + const unsigned char fail); + + int getLevel() const override A_WARN_UNUSED; + + /** Tells that the path has been set by mouse. */ + void pathSetByMouse() + { mPathSetByMouse = true; } + + /** Tells if the path has been set by mouse. */ + bool isPathSetByMouse() const A_WARN_UNUSED + { return mPathSetByMouse; } + + int getInvertDirection() const A_WARN_UNUSED + { return mInvertDirection; } + + void setInvertDirection(const int n) + { mInvertDirection = n; } + + void invertDirection(); + + int getAttackWeaponType() const A_WARN_UNUSED + { return mAttackWeaponType; } + + int getAttackType() const A_WARN_UNUSED + { return mAttackType; } + + int getFollowMode() const A_WARN_UNUSED + { return mFollowMode; } + + int getImitationMode() const A_WARN_UNUSED + { return mImitationMode; } + + void changeAttackWeaponType(); + + void changeAttackType(); + + void changeFollowMode(); + + void changeImitationMode(); + + void changePickUpType(); + + int getCrazyMoveType() const A_WARN_UNUSED + { return mCrazyMoveType; } + + int getPickUpType() const A_WARN_UNUSED + { return mPickUpType; } + + int getQuickDropCounter() const A_WARN_UNUSED + { return mQuickDropCounter; } + + void setQuickDropCounter(const int n); + + void changeQuickDropCounter(); + + int getMoveState() const A_WARN_UNUSED + { return mMoveState; } + + void setMoveState(const int n) + { mMoveState = n; } + + void switchMagicAttack(); + + void switchPvpAttack(); + + int getMagicAttackType() const A_WARN_UNUSED + { return mMagicAttackType; } + + int getPvpAttackType() const A_WARN_UNUSED + { return mPvpAttackType; } + + int getMoveToTargetType() const A_WARN_UNUSED + { return mMoveToTargetType; } + + int getDisableGameModifiers() const A_WARN_UNUSED + { return mDisableGameModifiers; } + + std::string getPingTime() const A_WARN_UNUSED; + + void tryPingRequest(); + + void changeMoveToTargetType(); + + void switchGameModifiers(); + + void magicAttack() const; + + void specialMove(const unsigned char direction); + + void moveByDirection(const unsigned char dir); + + bool pickUpItems(int pickUpType = 0); + + void changeCrazyMoveType(); + + void crazyMove(); + + void moveTo(const int x, const int y); + + void move(const int dX, const int dY); + + void moveToTarget(int dist = -1); + + void moveToHome(); + + void debugMsg(const std::string &str) const; + + bool isReachable(Being *const being, + const int maxCost = 0) A_WARN_UNUSED; + + bool isReachable(const int x, const int y, + const bool allowCollision) const A_WARN_UNUSED; + + void setHome(); + + void pingRequest(); + + void pingResponse(); + + void changeAwayMode(); + + void setAway(const std::string &message); + + void setPseudoAway(const std::string &message); + + bool getAway() const A_WARN_UNUSED + { return mAwayMode; } + + bool getPseudoAway() const A_WARN_UNUSED + { return mPseudoAwayMode; } + + void setHalfAway(const bool n) + { mInactive = n; } + + bool getHalfAway() const A_WARN_UNUSED + { return mInactive; } + + void afkRespond(ChatTab *const tab, const std::string &nick); + + bool navigateTo(const int x, const int y); + + void navigateTo(const Being *const being); + + void navigateClean(); + + void imitateEmote(const Being *const being, + const unsigned char emote) const; + + void imitateAction(const Being *const being, + const Being::Action &action); + + void imitateDirection(const Being *const being, + const unsigned char dir); + + void imitateOutfit(Being *const player, const int sprite = -1) const; + + void followMoveTo(const Being *const being, const int x, const int y); + + void followMoveTo(const Being *const being, const int x1, const int y1, + const int x2, const int y2); + + bool allowAction() A_WARN_UNUSED; + + void setRealPos(const int x, const int y); + + bool isServerBuggy() const A_WARN_UNUSED + { return mIsServerBuggy; } + + void fixPos(const int maxDist = 1); + + /** + * Sets the map the being is on + */ + void setMap(Map *const map); + + void addMessageToQueue(const std::string &message, + const int color = UserPalette::EXP_INFO); + + /** + * Called when a option (set with config.addListener()) is changed + */ + void optionChanged(const std::string &value) override; + + void processEvent(Channels channel, + const DepricatedEvent &event) override; + + /** + * set a following player. + */ + void setFollow(const std::string &player); + + /** + * set an imitation player. + */ + void setImitate(const std::string &player); + + /** + * setting the next destination of the following, in case of warp + */ + void setNextDest(const int x, const int y); + + int getNextDestX() const A_WARN_UNUSED + { return mNextDestX; } + + int getNextDestY() const A_WARN_UNUSED + { return mNextDestY; } + + void respawn(); + + const FloorItem *getPickUpTarget() const A_WARN_UNUSED + { return mPickUpTarget; } + + void unSetPickUpTarget() + { mPickUpTarget = nullptr; } + + /** + * Stop following a player. + */ + void cancelFollow(); + + /** + * Get the playername followed by the current player. + */ + const std::string &getFollow() const A_WARN_UNUSED + { return mPlayerFollowed; } + + /** + * Get the playername imitated by the current player. + */ + const std::string &getImitate() const A_WARN_UNUSED + { return mPlayerImitated; } + + /** + * Tells the engine whether to check + * if the Player Name is to be displayed. + */ + void setCheckNameSetting(const bool checked) + { mUpdateName = checked; } + + /** + * Gets if the engine has to check + * if the Player Name is to be displayed. + */ + bool getCheckNameSetting() const A_WARN_UNUSED + { return mUpdateName; } + + void fixAttackTarget(); + + void updateNavigateList(); + + int getPathLength(const Being *const being) const A_WARN_UNUSED; + + void targetMoved() const; + + void setLastHitFrom(const std::string &n) + { mLastHitFrom = n; } + + void waitFor(const std::string &nick); + + void checkNewName(Being *const being); + + void resetYellowBar(); + + unsigned char getWalkMask() const override A_WARN_UNUSED; + + void saveHomes(); + + void removeHome(); + + void stopAdvert(); + + bool checAttackPermissions(const Being *const target) + const A_WARN_UNUSED; + + void updateStatus() const; + + void setTestParticle(const std::string &fileName, + bool updateHash = true); + + std::string getInvertDirectionString(); + + std::string getCrazyMoveTypeString(); + + std::string getMoveToTargetTypeString(); + + std::string getFollowModeString(); + + std::string getAttackWeaponTypeString(); + + std::string getAttackTypeString(); + + std::string getQuickDropCounterString(); + + std::string getPickUpTypeString(); + + std::string getDebugPathString() const; + + std::string getMagicAttackString(); + + std::string getPvpAttackString(); + + std::string getImitationModeString(); + + std::string getAwayModeString(); + + std::string getCameraModeString() const; + + std::string getGameModifiersString(); + + protected: + void updateCoords() override; + + + virtual void handleStatusEffect(StatusEffect *const effect, + const int effectId); + + void startWalking(const unsigned char dir); + + void changeEquipmentBeforeAttack(const Being *const target) const; + + static void tryMagic(const std::string &spell, const int baseMagic, + const int schoolMagic, const int mana); + + const char *getVarItem(const char *const *const arr, + const unsigned index, + const unsigned sz) const A_WARN_UNUSED; + + void changeMode(unsigned *const var, const unsigned limit, + const char *const conf, + std::string (LocalPlayer::*const func)(), + const unsigned def = 0, + const bool save = true); + + void crazyMove1(); + void crazyMove2(); + void crazyMove3(); + void crazyMove4(); + void crazyMove5(); + void crazyMove6(); + void crazyMove7(); + void crazyMove8(); + void crazyMove9(); + void crazyMoveA(); + + void loadHomes(); + + int mGMLevel; + + // move type + unsigned int mInvertDirection; + // crazy move type + unsigned int mCrazyMoveType; + // crazy move state + unsigned int mCrazyMoveState; + // attack weapon type + unsigned int mAttackWeaponType; + // quick drop counter + unsigned int mQuickDropCounter; + // move state. used if mInvertDirection == 2 + unsigned int mMoveState; + // pick up type 1x1, normal aka 2x1, forward aka 2x3, 3x3, 3x3 + 1 + unsigned int mPickUpType; + // magic attack type + unsigned int mMagicAttackType; + // pvp attack type + unsigned int mPvpAttackType; + // type how move to target + unsigned int mMoveToTargetType; + unsigned int mAttackType; + unsigned int mFollowMode; + unsigned int mImitationMode; + + int mLastTargetX; + int mLastTargetY; + + std::map<std::string, Vector> mHomes; + + Being *mTarget; + + /** Follow system **/ + std::string mPlayerFollowed; + std::string mPlayerImitated; + int mNextDestX; + int mNextDestY; + + FloorItem *mPickUpTarget; + + int mLastAction; // Time stamp of the last action, -1 if none. + + std::vector<int> mStatusEffectIcons; + + int mLocalWalkTime; // Timestamp used to control keyboard walk + // messages flooding + + typedef std::pair<std::string, int> MessagePair; + /** Queued messages*/ + std::list<MessagePair> mMessages; + int mMessageTime; + AwayListener *mAwayListener; + OkDialog *mAwayDialog; + + int mPingSendTick; + int mPingTime; + int mAfkTime; + int mActivityTime; + int mNavigateX; + int mNavigateY; + int mNavigateId; + int mCrossX; + int mCrossY; + int mOldX; + int mOldY; + int mOldTileX; + int mOldTileY; + Path mNavigatePath; + + std::string mLastHitFrom; + std::string mWaitFor; + int mAdvertTime; + Particle *mTestParticle; + std::string mTestParticleName; + int mTestParticleTime; + unsigned long mTestParticleHash; + unsigned char mWalkingDir; // The direction the player is walking in. + /** Whether or not the name settings have changed */ + bool mUpdateName; + bool mBlockAdvert; + bool mTargetDeadPlayers; + bool mServerAttack; + bool mEnableAdvert; + bool mTradebot; + bool mTargetOnlyReachable; + bool mDisableGameModifiers; + bool mIsServerBuggy; + bool mSyncPlayerMove; + bool mDrawPath; + bool mAttackMoving; + bool mAttackNext; + bool mShowJobExp; + bool mNextStep; + // temporary disable crazy moves in moves + bool mDisableCrazyMove; + bool mGoingToTarget; + // Whether or not to continue to attack + bool mKeepAttacking; + // Tells if the path was set using mouse + bool mPathSetByMouse; + bool mWaitPing; + bool mAwayMode; + bool mPseudoAwayMode; + bool mShowNavigePath; +}; + +extern LocalPlayer *player_node; + +#endif // BEING_LOCALPLAYER_H diff --git a/src/being/playerinfo.cpp b/src/being/playerinfo.cpp new file mode 100644 index 000000000..9abd617a5 --- /dev/null +++ b/src/being/playerinfo.cpp @@ -0,0 +1,398 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/playerinfo.h" + +#include "client.h" +#include "configuration.h" +#include "depricatedevent.h" +#include "inventory.h" +#include "depricatedlistener.h" +#include "logger.h" + +#include "gui/inventorywindow.h" +#include "gui/npcdialog.h" +#include "gui/npcpostdialog.h" + +#include "resources/iteminfo.h" + +#include "net/inventoryhandler.h" +#include "net/playerhandler.h" + +#include "debug.h" + +namespace PlayerInfo +{ + +PlayerInfoBackend mData; +int mCharId = 0; + +Inventory *mInventory = nullptr; +Equipment *mEquipment = nullptr; + +#ifdef MANASERV_SUPPORT +std::map<int, Special> mSpecials; +signed char mSpecialRechargeUpdateNeeded = 0; +#endif + +bool mTrading = false; +int mLevelProgress = 0; +std::set<int> mProtectedItems; + +// --- Triggers --------------------------------------------------------------- + +void triggerAttr(const int id, const int old) +{ + DepricatedEvent event(EVENT_UPDATEATTRIBUTE); + event.setInt("id", id); + event.setInt("oldValue", old); + event.setInt("newValue", mData.mAttributes.find(id)->second); + DepricatedEvent::trigger(CHANNEL_ATTRIBUTES, event); +} + +void triggerStat(const int id, const std::string &changed, + const int old1, const int old2) +{ + const StatMap::const_iterator it = mData.mStats.find(id); + if (it == mData.mStats.end()) + return; + + DepricatedEvent event(EVENT_UPDATESTAT); + event.setInt("id", id); + const Stat &stat = it->second; + event.setInt("base", stat.base); + event.setInt("mod", stat.mod); + event.setInt("exp", stat.exp); + event.setInt("expNeeded", stat.expNeed); + event.setString("changed", changed); + event.setInt("oldValue1", old1); + event.setInt("oldValue2", old2); + DepricatedEvent::trigger(CHANNEL_ATTRIBUTES, event); +} + +// --- Attributes ------------------------------------------------------------- + +int getAttribute(const int id) +{ + const IntMap::const_iterator it = mData.mAttributes.find(id); + if (it != mData.mAttributes.end()) + return it->second; + else + return 0; +} + +void setAttribute(const int id, const int value, const bool notify) +{ + const int old = mData.mAttributes[id]; + mData.mAttributes[id] = value; + if (notify) + triggerAttr(id, old); +} + +int getSkillLevel(const int id) +{ + const IntMap::const_iterator it = mData.mSkills.find(id); + if (it != mData.mSkills.end()) + return it->second; + else + return 0; +} + +void setSkillLevel(const int id, const int value) +{ + mData.mSkills[id] = value; +} + +// --- Stats ------------------------------------------------------------------ + +int getStatBase(const int id) +{ + const StatMap::const_iterator it = mData.mStats.find(id); + if (it != mData.mStats.end()) + return it->second.base; + else + return 0; +} + +void setStatBase(const int id, const int value, const bool notify) +{ + const int old = mData.mStats[id].base; + mData.mStats[id].base = value; + if (notify) + triggerStat(id, "base", old); +} + +int getStatMod(const int id) +{ + const StatMap::const_iterator it = mData.mStats.find(id); + if (it != mData.mStats.end()) + return it->second.mod; + else + return 0; +} + +void setStatMod(const int id, const int value, const bool notify) +{ + const int old = mData.mStats[id].mod; + mData.mStats[id].mod = value; + if (notify) + triggerStat(id, "mod", old); +} + +int getStatEffective(const int id) +{ + const StatMap::const_iterator it = mData.mStats.find(id); + if (it != mData.mStats.end()) + return it->second.base + it->second.mod; + else + return 0; +} + +const std::pair<int, int> getStatExperience(const int id) +{ + const StatMap::const_iterator it = mData.mStats.find(id); + int a, b; + if (it != mData.mStats.end()) + { + a = it->second.exp; + b = it->second.expNeed; + } + else + { + a = 0; + b = 0; + } + return std::pair<int, int>(a, b); +} + +void setStatExperience(const int id, const int have, + const int need, const bool notify) +{ + Stat &stat = mData.mStats[id]; + + const int oldExp = stat.exp; + const int oldExpNeed = stat.expNeed; + stat.exp = have; + stat.expNeed = need; + if (notify) + triggerStat(id, "exp", oldExp, oldExpNeed); +} + +// --- Inventory / Equipment -------------------------------------------------- + +Inventory *getInventory() +{ + return mInventory; +} + +Inventory *getStorageInventory() +{ + return Net::getInventoryHandler()->getStorage(); +} + +void clearInventory() +{ + if (mEquipment) + mEquipment->clear(); + if (mInventory) + mInventory->clear(); +} + +void setInventoryItem(const int index, const int id, + const int amount, const int refine) +{ + bool equipment = false; + const int itemType = ItemDB::get(id).getType(); + if (itemType != ITEM_UNUSABLE && itemType != ITEM_USABLE) + equipment = true; + if (mInventory) + mInventory->setItem(index, id, amount, refine, equipment); +} + +Equipment *getEquipment() +{ + return mEquipment; +} + +Item *getEquipment(const unsigned int slot) +{ + if (mEquipment) + return mEquipment->getEquipment(slot); + else + return nullptr; +} + +void setEquipmentBackend(Equipment::Backend *const backend) +{ + if (mEquipment) + mEquipment->setBackend(backend); +} + +// --- Misc ------------------------------------------------------------------- + +void setBackend(const PlayerInfoBackend &backend) +{ + mData = backend; +} + +void setCharId(const int charId) +{ + mCharId = charId; +} + +int getCharId() +{ + return mCharId; +} + +void logic() +{ +#ifdef MANASERV_SUPPORT + if ((mSpecialRechargeUpdateNeeded % 11) == 0) + { + mSpecialRechargeUpdateNeeded = 0; + FOR_EACH (SpecialsMap::iterator, it, mSpecials) + { + Special &special = it->second; + special.currentMana += special.recharge; + if (special.currentMana > special.neededMana) + special.currentMana = special.neededMana; + } + } + mSpecialRechargeUpdateNeeded++; +#endif +} + +bool isTrading() +{ + return mTrading; +} + +void setTrading(const bool trading) +{ + mTrading = trading; +} + +void updateAttrs() +{ + const Net::PlayerHandler *const handler = Net::getPlayerHandler(); + if (!handler) + return; + const int attr = handler->getAttackLocation(); + const int attackDelay = getStatBase(ATTACK_DELAY); + if (attr != -1 && attackDelay) + { + setStatBase(ATTACK_SPEED, getStatBase(attr) * 1000 + / attackDelay, false); + setStatMod(ATTACK_SPEED, getStatMod(attr) * 1000 + / attackDelay, true); + } + else + { + setStatBase(ATTACK_SPEED, 0, false); + setStatMod(ATTACK_SPEED, 0, true); + } +} + +void init() +{ +} + +void deinit() +{ + clearInventory(); +} + +void loadData() +{ + mProtectedItems.clear(); + splitToIntSet(mProtectedItems, + serverConfig.getStringValue("protectedItems"), ','); +} + +void clear() +{ + mData.mSkills.clear(); +} + +bool isTalking() +{ + return NpcDialog::isActive() || NpcPostDialog::isActive() + || InventoryWindow::isStorageActive(); +} + +void gameDestroyed() +{ + delete mInventory; + mInventory = nullptr; + delete mEquipment; + mEquipment = nullptr; +} + +void stateChange(const int state) +{ + if (state == STATE_GAME) + { + if (!mInventory) + { + mInventory = new Inventory(Inventory::INVENTORY); + mEquipment = new Equipment(); + } + } +} + +static void saveProtectedItems() +{ + std::string str; + std::set<int>::const_iterator it = mProtectedItems.begin(); + std::set<int>::const_iterator it_end = mProtectedItems.end(); + if (it != it_end) + { + str.append(toString(*it)); + ++ it; + } + while (it != it_end) + { + str.append(",").append(toString(*it)); + ++ it; + } + serverConfig.setValue("protectedItems", str); + serverConfig.write(); +} + +void protectItem(const int id) +{ + mProtectedItems.insert(id); + saveProtectedItems(); +} + +void unprotectItem(const int id) +{ + mProtectedItems.erase(id); + saveProtectedItems(); +} + +bool isItemProtected(const int id) +{ + return mProtectedItems.find(id) != mProtectedItems.end(); +} + +} // namespace PlayerInfo diff --git a/src/being/playerinfo.h b/src/being/playerinfo.h new file mode 100644 index 000000000..37aa9736c --- /dev/null +++ b/src/being/playerinfo.h @@ -0,0 +1,267 @@ +/* + * The ManaPlus Client + * Copyright (C) 2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_PLAYERINFO_H +#define BEING_PLAYERINFO_H + +#include "equipment.h" + +#include <map> +#include <string> + + +/** + * Stat information storage structure. + */ +struct Stat +{ + int base; + int mod; + int exp; + int expNeed; +}; + +typedef std::map<int, int> IntMap; +typedef std::map<int, Stat> StatMap; + +/** + * Backend for core player information. + */ +struct PlayerInfoBackend final +{ + PlayerInfoBackend() : + mAttributes(), + mStats(), + mSkills() + { + } + + IntMap mAttributes; + StatMap mStats; + IntMap mSkills; +}; + +class Equipment; +class Inventory; +class Item; + +/** + * Special information storage structure. + */ +struct Special final +{ + int currentMana; + int neededMana; + int recharge; +}; + +typedef std::map<int, Special> SpecialsMap; + +/** + * A database like namespace which holds global info about the localplayer + * + * NOTE: 'bool notify' is used to determine if a event is to be triggered. + */ +namespace PlayerInfo +{ + /** + * Standard attributes for players. + */ + enum Attribute + { + LEVEL = 0, + HP, + MAX_HP, + MP, + MAX_MP, + EXP, + EXP_NEEDED, + MONEY, + TOTAL_WEIGHT, + MAX_WEIGHT, + SKILL_POINTS, + CHAR_POINTS, + CORR_POINTS, + ATTACK_DELAY = 100, + ATTACK_RANGE = 101, + WALK_SPEED = 102, + ATTACK_SPEED = 103 + }; + +// --- Attributes ------------------------------------------------------------- + + /** + * Returns the value of the given attribute. + */ + int getAttribute(const int id) A_WARN_UNUSED; + + /** + * Changes the value of the given attribute. + */ + void setAttribute(const int id, const int value, + const bool notify = true); + + int getSkillLevel(const int id) A_WARN_UNUSED; + + void setSkillLevel(const int id, const int value); + +// --- Stats ------------------------------------------------------------------ + + /** + * Returns the base value of the given stat. + */ + int getStatBase(const int id) A_WARN_UNUSED; + + /** + * Changes the base value of the given stat. + */ + void setStatBase(const int id, const int value, + const bool notify = true); + + /** + * Returns the modifier for the given stat. + */ + int getStatMod(const int id) A_WARN_UNUSED; + + /** + * Changes the modifier for the given stat. + */ + void setStatMod(const int id, const int value, + const bool notify = true); + + /** + * Returns the current effective value of the given stat. Effective is base + * + mod + */ + int getStatEffective(const int id) A_WARN_UNUSED; + + /** + * Changes the level of the given stat. + */ + void setStatLevel(int id, int value, bool notify = true); + + /** + * Returns the experience of the given stat. + */ + const std::pair<int, int> getStatExperience(const int id) A_WARN_UNUSED; + + /** + * Changes the experience of the given stat. + */ + void setStatExperience(const int id, const int have, + const int need, const bool notify = true); + +// --- Inventory / Equipment -------------------------------------------------- + + /** + * Returns the player's inventory. + */ + Inventory *getInventory() A_WARN_UNUSED; + + Inventory *getStorageInventory() A_WARN_UNUSED; + + /** + * Clears the player's inventory and equipment. + */ + void clearInventory(); + + void clear(); + + /** + * Changes the inventory item at the given slot. + */ + void setInventoryItem(const int index, const int id, + const int amount, const int refine); + + /** + * Returns the player's equipment. + */ + Equipment *getEquipment() A_WARN_UNUSED; + + /** + * Returns the player's equipment at the given slot. + */ + Item *getEquipment(const unsigned int slot) A_WARN_UNUSED; + +// --- Misc ------------------------------------------------------------------- + + /** + * Changes the internal PlayerInfoBackend reference; + */ + void setBackend(const PlayerInfoBackend &backend); + + void setCharId(const int charId); + + int getCharId(); + + /** + * Does necessary updates every tick. + */ + void logic(); + + /** + * Returns true if the player is involved in a trade at the moment, false + * otherwise. + */ + bool isTrading(); + + /** + * Sets whether the player is currently involved in trade or not. + */ + void setTrading(const bool trading); + + void updateAttrs(); + + /** + * Initializes some internals. + */ + void init(); + + void deinit(); + + void loadData(); + + bool isTalking(); + + void gameDestroyed(); + + void stateChange(const int state); + + void triggerAttr(int id); + + void triggerAttr(const int id, const int old); + + void triggerStat(int id); + + void triggerStat(const int id, const std::string &changed, + const int old1, const int old2 = 0); + + void setEquipmentBackend(Equipment::Backend *const backend); + + void protectItem(const int id); + + void unprotectItem(const int id); + + bool isItemProtected(const int id); + +} // namespace PlayerInfo + +#endif // BEING_PLAYERINFO_H diff --git a/src/being/playerrelations.cpp b/src/being/playerrelations.cpp new file mode 100644 index 000000000..cdd9ca4bc --- /dev/null +++ b/src/being/playerrelations.cpp @@ -0,0 +1,630 @@ +/* + * The ManaPlus Client + * Copyright (C) 2008-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#include "being/playerrelations.h" + +#include "actorspritemanager.h" +#include "configuration.h" + +#include "being/localplayer.h" + +#include "render/graphics.h" + +#include "utils/dtor.h" +#include "utils/gettext.h" + +#include <algorithm> + +#include "debug.h" + +static const char *const PLAYER_IGNORE_STRATEGY_NOP = "nop"; +static const char *const PLAYER_IGNORE_STRATEGY_EMOTE0 = "emote0"; +static const char *const DEFAULT_IGNORE_STRATEGY + = PLAYER_IGNORE_STRATEGY_EMOTE0; + +static const char *const NAME = "name"; +static const char *const RELATION = "relation"; + +static const unsigned int IGNORE_EMOTE_TIME = 100; + +typedef std::map<std::string, PlayerRelation *> PlayerRelations; +typedef PlayerRelations::const_iterator PlayerRelationsCIter; +typedef std::list<PlayerRelationsListener *> PlayerRelationListeners; +typedef PlayerRelationListeners::const_iterator PlayerRelationListenersCIter; + +class SortPlayersFunctor final +{ + public: + bool operator() (const std::string &str1, + const std::string &str2) const + { + std::string s1 = str1; + std::string s2 = str2; + toLower(s1); + toLower(s2); + if (s1 == s2) + return str1 < str2; + return s1 < s2; + } +} playersRelSorter; + +// (De)serialisation class +class PlayerConfSerialiser final : + public ConfigurationListManager<std::pair<std::string, PlayerRelation *>, + std::map<std::string, PlayerRelation *> *> +{ +public: + virtual ConfigurationObject *writeConfigItem( + const std::pair<std::string, PlayerRelation *> &value, + ConfigurationObject *const cobj) const override + { + if (!cobj || !value.second) + return nullptr; + cobj->setValue(NAME, value.first); + cobj->setValue(RELATION, toString( + static_cast<int>(value.second->mRelation))); + + return cobj; + } + + virtual std::map<std::string, PlayerRelation *> * + readConfigItem(const ConfigurationObject *const cobj, + std::map<std::string, PlayerRelation *> + *const container) const override + { + if (!cobj) + return container; + const std::string name = cobj->getValue(NAME, ""); + if (name.empty()) + return container; + + if (!(*container)[name]) + { + const int v = cobj->getValueInt(RELATION, + static_cast<int>(PlayerRelation::NEUTRAL)); + + (*container)[name] = new PlayerRelation( + static_cast<PlayerRelation::Relation>(v)); + } + // otherwise ignore the duplicate entry + + return container; + } +}; + +static PlayerConfSerialiser player_conf_serialiser; // stateless singleton + +const unsigned int PlayerRelation::RELATION_PERMISSIONS[RELATIONS_NR] = +{ + /* NEUTRAL */ 0, // we always fall back to the defaults anyway + /* FRIEND */ EMOTE | SPEECH_FLOAT | SPEECH_LOG | WHISPER | TRADE, + /* DISREGARDED*/ EMOTE | SPEECH_FLOAT, + /* IGNORED */ 0, + /* ERASED */ INVISIBLE, + /* BLACKLISTED */ SPEECH_LOG | WHISPER, + /* ENEMY2 */ EMOTE | SPEECH_FLOAT | SPEECH_LOG | WHISPER | TRADE +}; + +PlayerRelation::PlayerRelation(const Relation relation) : + mRelation(relation) +{ +} + +PlayerRelationsManager::PlayerRelationsManager() : + mPersistIgnores(false), + mDefaultPermissions(PlayerRelation::DEFAULT), + mIgnoreStrategy(nullptr), + mRelations(), + mListeners(), + mIgnoreStrategies() +{ +} + +PlayerRelationsManager::~PlayerRelationsManager() +{ + delete_all(mIgnoreStrategies); + + FOR_EACH (PlayerRelationsCIter, it, mRelations) + delete it->second; + mRelations.clear(); +} + +void PlayerRelationsManager::clear() +{ + StringVect *const names = getPlayers(); + FOR_EACHP (StringVectCIter, it, names) + removePlayer(*it); + delete names; +} + +static const char *const PERSIST_IGNORE_LIST = "persistent-player-list"; +static const char *const PLAYER_IGNORE_STRATEGY = "player-ignore-strategy"; +static const char *const DEFAULT_PERMISSIONS = "default-player-permissions"; + +int PlayerRelationsManager::getPlayerIgnoreStrategyIndex( + const std::string &name) +{ + const std::vector<PlayerIgnoreStrategy *> *const strategies + = getPlayerIgnoreStrategies(); + + if (!strategies) + return -1; + + const size_t sz = strategies->size(); + for (size_t i = 0; i < sz; i++) + { + if ((*strategies)[i]->mShortName == name) + return i; + } + + return -1; +} + +void PlayerRelationsManager::load(const bool oldConfig) +{ + Configuration *cfg; + if (oldConfig) + cfg = &config; + else + cfg = &serverConfig; + clear(); + + mPersistIgnores = cfg->getValue(PERSIST_IGNORE_LIST, 1); + mDefaultPermissions = static_cast<int>(cfg->getValue(DEFAULT_PERMISSIONS, + mDefaultPermissions)); + + const std::string ignore_strategy_name = cfg->getValue( + PLAYER_IGNORE_STRATEGY, DEFAULT_IGNORE_STRATEGY); + const int ignore_strategy_index = getPlayerIgnoreStrategyIndex( + ignore_strategy_name); + + if (ignore_strategy_index >= 0) + { + setPlayerIgnoreStrategy((*getPlayerIgnoreStrategies()) + [ignore_strategy_index]); + } + + cfg->getList<std::pair<std::string, PlayerRelation *>, + std::map<std::string, PlayerRelation *> *> + ("player", &(mRelations), &player_conf_serialiser); +} + + +void PlayerRelationsManager::init() +{ + load(); + + if (!mPersistIgnores) + { + clear(); // Yes, we still keep them around in the config file + // until the next update. + } + + FOR_EACH (PlayerRelationListenersCIter, it, mListeners) + (*it)->updateAll(); +} + +void PlayerRelationsManager::store() const +{ + serverConfig.setList<std::map<std::string, + PlayerRelation *>::const_iterator, + std::pair<std::string, PlayerRelation *>, + std::map<std::string, PlayerRelation *> *> + ("player", mRelations.begin(), mRelations.end(), + &player_conf_serialiser); + + serverConfig.setValue(DEFAULT_PERMISSIONS, mDefaultPermissions); + serverConfig.setValue(PERSIST_IGNORE_LIST, mPersistIgnores); + serverConfig.setValue(PLAYER_IGNORE_STRATEGY, + mIgnoreStrategy ? mIgnoreStrategy->mShortName + : DEFAULT_IGNORE_STRATEGY); + + serverConfig.write(); +} + +void PlayerRelationsManager::signalUpdate(const std::string &name) +{ + FOR_EACH (PlayerRelationListenersCIter, it, mListeners) + (*it)->updatedPlayer(name); + + if (actorSpriteManager) + { + Being *const being = actorSpriteManager->findBeingByName( + name, Being::PLAYER); + + if (being && being->getType() == Being::PLAYER) + being->updateColors(); + } +} + +unsigned int PlayerRelationsManager::checkPermissionSilently( + const std::string &player_name, const unsigned int flags) const +{ + const std::map<std::string, PlayerRelation *>::const_iterator + it = mRelations.find(player_name); + if (it == mRelations.end()) + { + return mDefaultPermissions & flags; + } + else + { + const PlayerRelation *const r = (*it).second; + unsigned int permissions = + PlayerRelation::RELATION_PERMISSIONS[r->mRelation]; + + switch (r->mRelation) + { + case PlayerRelation::NEUTRAL: + permissions = mDefaultPermissions; + break; + + case PlayerRelation::FRIEND: + permissions |= mDefaultPermissions; // widen + break; + + case PlayerRelation::DISREGARDED: + case PlayerRelation::IGNORED: + case PlayerRelation::ERASED: + case PlayerRelation::BLACKLISTED: + case PlayerRelation::ENEMY2: + default: + permissions &= mDefaultPermissions; // narrow + } + + return permissions & flags; + } +} + +bool PlayerRelationsManager::hasPermission(const Being *const being, + const unsigned int flags) const +{ + if (!being) + return false; + + if (being->getType() == ActorSprite::PLAYER) + return hasPermission(being->getName(), flags) == flags; + return true; +} + +bool PlayerRelationsManager::hasPermission(const std::string &name, + const unsigned int flags) const +{ + if (!actorSpriteManager) + return false; + + const unsigned int rejections = flags + & ~checkPermissionSilently(name, flags); + const bool permitted = (rejections == 0); + + if (!permitted) + { + // execute `ignore' strategy, if possible + if (mIgnoreStrategy) + { + Being *const b = actorSpriteManager->findBeingByName( + name, ActorSprite::PLAYER); + + if (b && b->getType() == ActorSprite::PLAYER) + mIgnoreStrategy->ignore(b, rejections); + } + } + + return permitted; +} + +void PlayerRelationsManager::setRelation(const std::string &player_name, + const PlayerRelation::Relation + relation) +{ + if (!player_node || (relation != PlayerRelation::NEUTRAL + && player_node->getName() == player_name)) + { + return; + } + + PlayerRelation *const r = mRelations[player_name]; + if (!r) + mRelations[player_name] = new PlayerRelation(relation); + else + r->mRelation = relation; + + signalUpdate(player_name); +} + +StringVect *PlayerRelationsManager::getPlayers() const +{ + StringVect *const retval = new StringVect(); + + FOR_EACH (PlayerRelationsCIter, it, mRelations) + { + if (it->second) + retval->push_back(it->first); + } + + std::sort(retval->begin(), retval->end(), playersRelSorter); + + return retval; +} + +StringVect *PlayerRelationsManager::getPlayersByRelation( + const PlayerRelation::Relation rel) const +{ + StringVect *const retval = new StringVect(); + + FOR_EACH (PlayerRelationsCIter, it, mRelations) + { + if (it->second && it->second->mRelation == rel) + retval->push_back(it->first); + } + + std::sort(retval->begin(), retval->end(), playersRelSorter); + + return retval; +} + +void PlayerRelationsManager::removePlayer(const std::string &name) +{ + delete mRelations[name]; + mRelations.erase(name); + signalUpdate(name); +} + + +PlayerRelation::Relation PlayerRelationsManager::getRelation( + const std::string &name) const +{ + const std::map<std::string, PlayerRelation *>::const_iterator + it = mRelations.find(name); + if (it != mRelations.end()) + return (*it).second->mRelation; + + return PlayerRelation::NEUTRAL; +} + +//////////////////////////////////////// +// defaults + +unsigned int PlayerRelationsManager::getDefault() const +{ + return mDefaultPermissions; +} + +void PlayerRelationsManager::setDefault(const unsigned int permissions) +{ + mDefaultPermissions = permissions; + + store(); + signalUpdate(""); +} + +void PlayerRelationsManager::ignoreTrade(const std::string &name) +{ + if (name.empty()) + return; + + const PlayerRelation::Relation relation = getRelation(name); + + if (relation == PlayerRelation::IGNORED + || relation == PlayerRelation::DISREGARDED + || relation == PlayerRelation::BLACKLISTED + || relation == PlayerRelation::ERASED) + { + return; + } + else + { + player_relations.setRelation(name, PlayerRelation::BLACKLISTED); + } +} + +bool PlayerRelationsManager::checkBadRelation(const std::string &name) const +{ + if (name.empty()) + return true; + + const PlayerRelation::Relation relation = getRelation(name); + + if (relation == PlayerRelation::IGNORED + || relation == PlayerRelation::DISREGARDED + || relation == PlayerRelation::BLACKLISTED + || relation == PlayerRelation::ERASED + || relation == PlayerRelation::ENEMY2) + { + return true; + } + return false; +} + +//////////////////////////////////////// +// ignore strategies + + +class PIS_nothing final : public PlayerIgnoreStrategy +{ +public: + PIS_nothing() : + PlayerIgnoreStrategy() + { + // TRANSLATORS: ignore/unignore action + mDescription = _("Completely ignore"); + mShortName = PLAYER_IGNORE_STRATEGY_NOP; + } + + virtual void ignore(Being *const being A_UNUSED, + const unsigned int flags A_UNUSED) const override + { + } +}; + +class PIS_dotdotdot final : public PlayerIgnoreStrategy +{ +public: + PIS_dotdotdot() : + PlayerIgnoreStrategy() + { + // TRANSLATORS: ignore/unignore action + mDescription = _("Print '...'"); + mShortName = "dotdotdot"; + } + + virtual void ignore(Being *const being, + const unsigned int flags A_UNUSED) const override + { + if (!being) + return; + + logger->log("ignoring: " + being->getName()); + being->setSpeech("..."); + } +}; + + +class PIS_blinkname final : public PlayerIgnoreStrategy +{ +public: + PIS_blinkname() : + PlayerIgnoreStrategy() + { + // TRANSLATORS: ignore/unignore action + mDescription = _("Blink name"); + mShortName = "blinkname"; + } + + virtual void ignore(Being *const being, + const unsigned int flags A_UNUSED) const override + { + if (!being) + return; + + logger->log("ignoring: " + being->getName()); + being->flashName(200); + } +}; + +class PIS_emote final : public PlayerIgnoreStrategy +{ +public: + PIS_emote(const uint8_t emote_nr, const std::string &description, + const std::string &shortname) : + PlayerIgnoreStrategy(), + mEmotion(emote_nr) + { + mDescription = description; + mShortName = shortname; + } + + virtual void ignore(Being *const being, + const unsigned int flags A_UNUSED) const override + { + if (!being) + return; + + being->setEmote(mEmotion, IGNORE_EMOTE_TIME); + } + uint8_t mEmotion; +}; + +std::vector<PlayerIgnoreStrategy *> * +PlayerRelationsManager::getPlayerIgnoreStrategies() +{ + if (mIgnoreStrategies.empty()) + { + // not initialised yet? + mIgnoreStrategies.push_back(new PIS_emote(FIRST_IGNORE_EMOTE, + // TRANSLATORS: ignore strategi + _("Floating '...' bubble"), + PLAYER_IGNORE_STRATEGY_EMOTE0)); + mIgnoreStrategies.push_back(new PIS_emote(FIRST_IGNORE_EMOTE + 1, + // TRANSLATORS: ignore strategi + _("Floating bubble"), + "emote1")); + mIgnoreStrategies.push_back(new PIS_nothing); + mIgnoreStrategies.push_back(new PIS_dotdotdot); + mIgnoreStrategies.push_back(new PIS_blinkname); + } + return &mIgnoreStrategies; +} + +bool PlayerRelationsManager::isGoodName(const std::string &name) const +{ + const size_t size = name.size(); + + if (size < 3) + return true; + + const std::map<std::string, PlayerRelation *>::const_iterator + it = mRelations.find(name); + if (it != mRelations.end()) + return true; + + return checkName(name); +} + +bool PlayerRelationsManager::isGoodName(Being *const being) const +{ + if (!being) + return false; + if (being->getGoodStatus() != -1) + return (being->getGoodStatus() == 1); + + const std::string name = being->getName(); + const size_t size = name.size(); + + if (size < 3) + return true; + + const std::map<std::string, PlayerRelation *>::const_iterator + it = mRelations.find(name); + if (it != mRelations.end()) + return true; + + const bool status = checkName(name); + being->setGoodStatus(status ? 1 : 0); + return status; +} + +bool PlayerRelationsManager::checkName(const std::string &name) const +{ + const size_t size = name.size(); + const std::string check = config.getStringValue("unsecureChars"); + const std::string lastChar = name.substr(size - 1, 1); + + if (name.substr(0, 1) == " " || lastChar == " " || lastChar == "." + || name.find(" ") != std::string::npos) + { + return false; + } + else if (check.empty()) + { + return true; + } + else if (name.find_first_of(check) != std::string::npos) + { + return false; + } + else + { + return true; + } +} + +PlayerRelationsManager player_relations; diff --git a/src/being/playerrelations.h b/src/being/playerrelations.h new file mode 100644 index 000000000..f7e9eba60 --- /dev/null +++ b/src/being/playerrelations.h @@ -0,0 +1,285 @@ +/* + * The ManaPlus Client + * Copyright (C) 2008-2009 The Mana World Development Team + * Copyright (C) 2009-2010 The Mana Developers + * Copyright (C) 2011-2013 The ManaPlus 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 <http://www.gnu.org/licenses/>. + */ + +#ifndef BEING_PLAYERRELATIONS_H +#define BEING_PLAYERRELATIONS_H + +#include "utils/stringvector.h" + +#include <list> +#include <map> + +#include "localconsts.h" + +class Being; + +struct PlayerRelation final +{ + static const unsigned int EMOTE = (1 << 0); + static const unsigned int SPEECH_FLOAT = (1 << 1); + static const unsigned int SPEECH_LOG = (1 << 2); + static const unsigned int WHISPER = (1 << 3); + static const unsigned int TRADE = (1 << 4); + static const unsigned int INVISIBLE = (1 << 5); + static const unsigned int BLACKLIST = (1 << 6); + static const unsigned int ENEMY = (1 << 7); + + static const unsigned int RELATIONS_NR = 7; + static const unsigned int RELATION_PERMISSIONS[RELATIONS_NR]; + + static const unsigned int DEFAULT = EMOTE + | SPEECH_FLOAT + | SPEECH_LOG + | WHISPER + | TRADE; + enum Relation + { + NEUTRAL = 0, + FRIEND = 1, + DISREGARDED = 2, + IGNORED = 3, + ERASED = 4, + BLACKLISTED = 5, + ENEMY2 = 6 + }; + + explicit PlayerRelation(const Relation relation); + + A_DELETE_COPY(PlayerRelation) + + Relation mRelation; // bitmask for all of the above +}; + + +/** + * Ignore strategy: describes how we should handle ignores. + */ +class PlayerIgnoreStrategy +{ + public: + std::string mDescription; + std::string mShortName; + + PlayerIgnoreStrategy() : + mDescription(), + mShortName() + { + } + + A_DELETE_COPY(PlayerIgnoreStrategy) + + virtual ~PlayerIgnoreStrategy() + { } + + /** + * Handle the ignoring of the indicated action by the indicated player. + */ + virtual void ignore(Being *const being, + const unsigned int flags) const = 0; +}; + +class PlayerRelationsListener +{ + public: + PlayerRelationsListener() + { } + + virtual ~PlayerRelationsListener() + { } + + virtual void updatedPlayer(const std::string &name) = 0; + + virtual void updateAll() = 0; +}; + +/** + * Player relations class, represents any particular relations and/or + * preferences the user of the local client has wrt other players (identified + * by std::string). + */ +class PlayerRelationsManager final +{ + public: + PlayerRelationsManager(); + + A_DELETE_COPY(PlayerRelationsManager) + + ~PlayerRelationsManager(); + + /** + * Initialise player relations manager (load config file etc.) + */ + void init(); + + /** + * Load configuration from our config file, or substitute defaults. + */ + void load(const bool oldConfig = false); + + /** + * Save configuration to our config file. + */ + void store() const; + + /** + * Determines whether the player in question is being ignored, filtered by + * the specified flags. + */ + unsigned int checkPermissionSilently(const std::string &player_name, + const unsigned int flags) + const A_WARN_UNUSED; + + /** + * Tests whether the player in question is being ignored for any of the + * actions in the specified flags. If so, trigger appropriate side effects + * if requested by the player. + */ + bool hasPermission(const Being *const being, + const unsigned int flags) const A_WARN_UNUSED; + + bool hasPermission(const std::string &being, + const unsigned int flags) const A_WARN_UNUSED; + + /** + * Updates the relationship with this player. + */ + void setRelation(const std::string &name, + const PlayerRelation::Relation relation); + + /** + * Updates the relationship with this player. + */ + PlayerRelation::Relation getRelation(const std::string &name) + const A_WARN_UNUSED; + + /** + * Deletes the information recorded for a player. + */ + void removePlayer(const std::string &name); + + /** + * Retrieves the default permissions. + */ + unsigned int getDefault() const A_WARN_UNUSED; + + /** + * Sets the default permissions. + */ + void setDefault(const unsigned int permissions); + + /** + * Retrieves all known player ignore strategies. + * + * The player ignore strategies are allocated statically and must + * not be deleted. + */ + std::vector<PlayerIgnoreStrategy *> *getPlayerIgnoreStrategies() + A_WARN_UNUSED; + + /** + * Return the current player ignore strategy. + * + * \return A player ignore strategy, or nullptr + */ + const PlayerIgnoreStrategy *getPlayerIgnoreStrategy() const + A_WARN_UNUSED + { return mIgnoreStrategy; } + + /** + * Sets the strategy to call when ignoring players. + */ + void setPlayerIgnoreStrategy(PlayerIgnoreStrategy *const strategy) + { mIgnoreStrategy = strategy; } + + /** + * For a given ignore strategy short name, find the appropriate index + * in the ignore strategies vector. + * + * \param The short name of the ignore strategy to look up + * \return The appropriate index, or -1 + */ + int getPlayerIgnoreStrategyIndex(const std::string &shortname) + A_WARN_UNUSED; + + /** + * Retrieves a sorted vector of all players for which we have any + * relations recorded. + */ + StringVect *getPlayers() const A_WARN_UNUSED; + + StringVect *getPlayersByRelation(const PlayerRelation::Relation rel) + const A_WARN_UNUSED; + + /** + * Removes all recorded player info. + */ + void clear(); + + /** + * Do we persist our `ignore' setup? + */ + bool getPersistIgnores() const + { return mPersistIgnores; } + + void ignoreTrade(const std::string &name); + + bool isGoodName(Being *const being) const A_WARN_UNUSED; + + bool isGoodName(const std::string &name) const A_WARN_UNUSED; + + /** + * Change the `ignore persist' flag. + * + * @param value Whether to persist ignores + */ + void setPersistIgnores(const bool value) + { mPersistIgnores = value; } + + void addListener(PlayerRelationsListener *const listener) + { mListeners.push_back(listener); } + + void removeListener(PlayerRelationsListener *const listener) + { mListeners.remove(listener); } + + bool checkBadRelation(const std::string &name) const A_WARN_UNUSED; + + private: + void signalUpdate(const std::string &name); + + bool mPersistIgnores; // If NOT set, we delete the + // ignored data upon reloading + unsigned int mDefaultPermissions; + + bool checkName(const std::string &name) const A_WARN_UNUSED; + + PlayerIgnoreStrategy *mIgnoreStrategy; + std::map<std::string, PlayerRelation *> mRelations; + std::list<PlayerRelationsListener *> mListeners; + std::vector<PlayerIgnoreStrategy *> mIgnoreStrategies; +}; + + +extern PlayerRelationsManager player_relations; // singleton representation + // of player relations + + +#endif // BEING_PLAYERRELATIONS_H |