/* * The ManaPlus Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * Copyright (C) 2011-2012 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.h" #include "actorspritemanager.h" #include "animatedsprite.h" #include "client.h" #include "configuration.h" #include "effectmanager.h" #include "graphics.h" #include "guild.h" #include "item.h" #include "localplayer.h" #include "particle.h" #include "party.h" #include "playerrelations.h" #include "simpleanimation.h" #include "sound.h" #include "text.h" #include "gui/equipmentwindow.h" #include "gui/gui.h" #include "gui/socialwindow.h" #include "gui/speechbubble.h" #include "gui/sdlfont.h" #include "gui/skilldialog.h" #include "net/charhandler.h" #include "net/gamehandler.h" #include "net/inventoryhandler.h" #include "net/net.h" #include "net/npchandler.h" #include "net/playerhandler.h" #include "resources/colordb.h" #include "resources/emotedb.h" #include "resources/iteminfo.h" #include "resources/monsterdb.h" #include "resources/npcdb.h" #include "resources/resourcemanager.h" #include "gui/widgets/chattab.h" #include "utils/gettext.h" #include <cmath> #include "debug.h" const unsigned int CACHE_SIZE = 50; class BeingCacheEntry final { public: BeingCacheEntry(const int id): mId(id), mName(""), mPartyName(""), mGuildName(""), mLevel(0), mPvpRank(0), mTime(0), mIp(""), mIsAdvanced(false), mFlags(0) { } 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: int mId; /**< Unique sprite id */ std::string mName; /**< Name of character */ std::string mPartyName; std::string mGuildName; int mLevel; unsigned int mPvpRank; int mTime; std::string mIp; bool mIsAdvanced; int mFlags; }; int Being::mNumberOfHairstyles = 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; std::list<BeingCacheEntry*> beingInfoCache; // TODO: mWalkTime used by eAthena only Being::Being(const int id, const Type type, const uint16_t subtype, Map *const map) : ActorSprite(id), mInfo(BeingInfo::unknown), mActionTime(0), mEmotion(0), mEmotionTime(0), mSpeechTime(0), mAttackType(1), mAttackSpeed(350), mAction(STAND), mSubType(0xFFFF), mDirection(DOWN), mDirectionDelayed(0), mSpriteDirection(DIRECTION_DOWN), mDispName(nullptr), mShowName(false), mEquippedWeapon(nullptr), mText(nullptr), mLevel(0), mGender(GENDER_UNSPECIFIED), mParty(nullptr), mIsGM(false), mAttackRange(1), mType(type), mSpeechBubble(new SpeechBubble), mWalkSpeed(Net::getPlayerHandler()->getDefaultWalkSpeed()), mX(0), mY(0), mDamageTaken(0), mHP(0), mMaxHP(0), mDistance(0), mIsReachable(REACH_UNKNOWN), mGoodStatus(-1), mErased(false), mEnemy(false), mIp(""), mAttackDelay(0), mMinHit(0), mMaxHit(0), mCriticalHit(0), mPvpRank(0), mSpriteRemap(new int[20]), mSpriteHide(new int[20]), mComment(""), mGotComment(false), mAdvanced(false), mShop(false), mAway(false), mInactive(false), mNumber(100), mHairColor(0) { for (int f = 0; f < 20; f ++) { mSpriteRemap[f] = f; mSpriteHide[f] = 0; } setMap(map); setSubtype(subtype); 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(); resetCounters(); 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; } void Being::setSubtype(const uint16_t subtype) { if (!mInfo) return; if (subtype == mSubType) return; mSubType = subtype; if (mType == MONSTER) { mInfo = MonsterDB::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay()); mYDiff = mInfo->getSortOffsetY(); } } else if (mType == NPC) { mInfo = NPCDB::get(mSubType); 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; setRaceName(_("Human")); } else { const ItemInfo &info = ItemDB::get(id); setRaceName(info.getName()); } setSprite(Net::getCharHandler()->baseSprite(), id); } } 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); } } 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, int time) { if (!userPalette) 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 && mSpeech.size() < 200) time = static_cast<int>(SPEECH_TIME - 300 + (3 * mSpeech.size())); 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 id) { if (!userPalette || !attacker) return; gcn::Font *font = nullptr; 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) { sound.playSfx(mInfo->getSound(SOUND_EVENT_HURT), attacker->getTileX(), attacker->getTileY()); 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) { // Init the particle effect path based on current // weapon or default. int hitEffectId = 0; if (type != SKILL) { const ItemInfo *const attackerWeapon = attacker->getEquippedWeapon(); if (attacker->mType == PLAYER && attackerWeapon) { if (type != CRITICAL) hitEffectId = attackerWeapon->getHitEffectId(); else hitEffectId = attackerWeapon->getCriticalHitEffectId(); } else { if (type != CRITICAL) hitEffectId = paths.getIntValue("hitEffectId"); else hitEffectId = paths.getIntValue("criticalHitEffectId"); } } else { // move skills effects to +100000 in effects list hitEffectId = id + 100000; } effectManager->trigger(hitEffectId, this); } } } void Being::handleAttack(Being *const victim, const int damage, const AttackType type A_UNUSED) { if (!victim || !mInfo) return; if (this != player_node) setAction(Being::ATTACK, 1); if (mType == PLAYER && mEquippedWeapon) fireMissile(victim, mEquippedWeapon->getMissileParticleFile()); else if (mInfo->getAttack(mAttackType)) fireMissile(victim, mInfo->getAttack(mAttackType)->missileParticle); #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); sound.playSfx(mInfo->getSound((damage > 0) ? SOUND_EVENT_HIT : SOUND_EVENT_MISS), mX, mY); } void Being::handleSkill(Being *const victim, const int damage, const int skillId) { if (!victim || !mInfo || !skillDialog) return; if (this != player_node) setAction(Being::ATTACK, 1); const SkillInfo *const skill = skillDialog->getSkill(skillId); if (skill) fireMissile(victim, skill->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 (skill) { if (damage > 0) sound.playSfx(skill->soundHit, mX, mY); else sound.playSfx(skill->soundMiss, mX, mY); } else { sound.playSfx(mInfo->getSound((damage > 0) ? SOUND_EVENT_HIT : SOUND_EVENT_MISS), 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 (std::map<int, Guild*>::const_iterator itr = mGuilds.begin(), itr_end = mGuilds.end(); itr != itr_end; ++itr) { 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 (std::map<int, Guild*>::const_iterator itr = mGuilds.begin(), itr_end = mGuilds.end(); itr != itr_end; ++itr) { 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 && !mMap->getWalk(mX, mY, Map::BLOCKMASK_GROUNDTOP)) return SpriteAction::SITTOP; return SpriteAction::SIT; } } void Being::setAction(const Action &action, const int attackType A_UNUSED) { std::string currentAction = SpriteAction::INVALID; switch (action) { case MOVE: if (mInfo) sound.playSfx(mInfo->getSound(SOUND_EVENT_MOVE), mX, mY); currentAction = SpriteAction::MOVE; // 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; sound.playSfx(mInfo->getSound(event), mX, mY); } break; case ATTACK: if (mEquippedWeapon) { currentAction = mEquippedWeapon->getAttackAction(); reset(); } else { mAttackType = attackType; if (!mInfo || !mInfo->getAttack(attackType)) break; currentAction = mInfo->getAttack(attackType)->action; reset(); #ifdef MANASERV_SUPPORT if (Net::getNetworkType() == ServerInfo::MANASERV && mInfo->getAttack(attackType)) { //attack particle effect std::string particleEffect = mInfo->getAttack(attackType) ->particleEffect; if (!particleEffect.empty() && Particle::enabled) { int rotation = 0; switch (mSpriteDirection) { case DIRECTION_DOWN: rotation = 0; break; case DIRECTION_LEFT: rotation = 90; break; case DIRECTION_UP: rotation = 180; break; case DIRECTION_RIGHT: rotation = 270; break; default: break; } if (particleEngine) { Particle *p = particleEngine->addEffect( particleEffect, 0, 0, rotation); if (p) controlParticle(p); } } } #endif } break; case HURT: if (mInfo) sound.playSfx(mInfo->getSound(SOUND_EVENT_HURT), mX, mY); //currentAction = SpriteAction::HURT;// Buggy: makes the player stop // attacking and unable to attack // again until he moves. // TODO: fix this! break; case DEAD: currentAction = SpriteAction::DEAD; if (mInfo) sound.playSfx(mInfo->getSound(SOUND_EVENT_DIE), mX, mY); if (mType == MONSTER || mType == NPC) mYDiff = mInfo->getDeadSortOffsetY(); break; case STAND: currentAction = SpriteAction::STAND; break; case SPAWN: if (mInfo) sound.playSfx(mInfo->getSound(SOUND_EVENT_SPAWN), mX, mY); currentAction = SpriteAction::SPAWN; break; default: logger->log("Being::setAction unknown action: " + toString(static_cast<unsigned>(action))); break; } if (currentAction != SpriteAction::INVALID) { play(currentAction); mAction = action; } if (currentAction != SpriteAction::MOVE) 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); 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() { // 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; } 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) 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; } //attack particle effect if (mEquippedWeapon) { particleEffect = mEquippedWeapon->getParticleEffect(); if (!particleEffect.empty() && findSameSubstring(particleEffect, paths.getStringValue("particles")).empty()) { particleEffect = paths.getStringValue("particles") + particleEffect; } } else if (mInfo && mInfo->getAttack(mAttackType)) { particleEffect = mInfo->getAttack(mAttackType) ->particleEffect; } if (particleEngine && !particleEffect.empty() && Particle::enabled && curFrame == 1) { int rotation = 0; switch (mDirection) { case DOWN: rotation = 0; break; case LEFT: rotation = 90; break; case UP: rotation = 180; break; case RIGHT: rotation = 270; break; default: break; } Particle *const p = particleEngine->addEffect( particleEffect, 0, 0, rotation); controlParticle(p); } 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 (mEmotion != 0) { mEmotionTime--; if (mEmotionTime == 0) mEmotion = 0; } ActorSprite::logic(); // int frameCount = static_cast<int>(getFrameCount()); 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); } } void Being::drawEmotion(Graphics *const graphics, const int offsetX, const int offsetY) { if (!mEmotion) return; const int px = getPixelX() - offsetX - 16; const int py = getPixelY() - offsetY - 64 - 32; const int emotionIndex = mEmotion - 1; if (emotionIndex >= 0 && emotionIndex <= EmoteDB::getLast()) { if (EmoteDB::getAnimation2(emotionIndex, true)) EmoteDB::getAnimation2(emotionIndex)->draw(graphics, px, py); else mEmotion = 0; } } 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; } } /** TODO: eAthena only */ int Being::getOffset(const char pos, const 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) { offset = (pos == LEFT && neg == RIGHT) ? static_cast<int>((static_cast<float>(get_elapsed_time(mActionTime)) * static_cast<float>(mMap->getTileWidth())) / static_cast<float>(mWalkSpeed.x)) : static_cast<int>((static_cast<float>(get_elapsed_time(mActionTime)) * 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; // Monster names show above the sprite instead of below it if (mType == MONSTER) { mDispName->adviseXY(getPixelX(), getPixelY() - getHeight() - mDispName->getHeight()); } else { mDispName->adviseXY(getPixelX(), getPixelY()); } } 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 " " + 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 += "$"; if (mAway) { // TRANSLATORS: this away status writed in player nick str += _("A"); } else if (mInactive) { // TRANSLATORS: this inactive status writed in player nick str += _("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 mDisplayName(mName); if (mType != MONSTER && (mShowGender || mShowLevel)) { mDisplayName += " "; if (mShowLevel && getLevel() != 0) mDisplayName += toString(getLevel()); mDisplayName += getGenderSign(); } if (mType == MONSTER) { if (config.getBoolValue("showMonstersTakedDamage")) mDisplayName += ", " + 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(); } mDispName = new FlashText(mDisplayName, 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); mErased = false; 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 (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); mErased = true; } 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::getCharHandler()->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; } else { const ItemInfo &info = ItemDB::get(id); std::string filename = info.getSprite(mGender, mSubType); AnimatedSprite *equipmentSprite = nullptr; if (!filename.empty()) { if (color.empty()) color = info.getDyeColorsString(colorId); filename = combineDye(filename, color); equipmentSprite = AnimatedSprite::delayedLoad( paths.getStringValue("sprites") + filename); } 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() { 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; } void Being::updateName() { if (mShowName) showName(); } void Being::reReadConfig() { if (mUpdateConfigTime + 1 < cur_time) { 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"); mUpdateConfigTime = cur_time; } } 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 (entry->isAdvanced()) { 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; } 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 (std::list<BeingCacheEntry*>::iterator i = beingInfoCache.begin(); i != beingInfoCache.end(); ++ i) { 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() { if (!Client::limitPackets(PACKET_NPC_TALK)) return; Net::getNpcHandler()->talk(mId); } bool Being::draw(Graphics *graphics, int offsetX, int offsetY) const { bool res = true; if (!mErased) res = ActorSprite::draw(graphics, offsetX, offsetY); return res; } void Being::drawSprites(Graphics* graphics, int posX, 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* graphics, int posX, 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 (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, y + 32 - 6, 2 * 50, 4); } if (mShowOwnHP && 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, y + 32 - 6, 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) return; if (!hp && maxHP < hp) 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::getCharHandler()->hairSprite(); for (unsigned slot = 0; slot < sz; slot ++) { oldHide[slot] = mSpriteHide[slot]; mSpriteHide[slot] = 0; } for (unsigned slot = 0; slot < sz; slot ++) { slotRemap.push_back(slot); if (mSpriteIDs.size() <= 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 (SpriteToItemMapCIter itr = spriteToItems->begin(), itr_end = spriteToItems->end(); itr != itr_end; ++ itr) { 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 ((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 (std::map<int, int>::const_iterator repIt = itemReplacer.begin(), repIt_end = itemReplacer.end(); repIt != repIt_end; ++ repIt) { 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]; // logger->log("item slot->slot %d %d->%d", id, slot, itemSlotRemap[id]); } else if (info.mDrawAfter[dir] > 0) { const int id2 = mSpriteIDs[info.mDrawAfter[dir]]; if (itemSlotRemap.find(id2) != itemSlotRemap.end()) { // logger->log("found duplicate (after)"); 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>(mSpriteIDs.size()) > 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(std::vector<int> &slotRemap, const int val) const { for (unsigned slot = 0; slot < size(); 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 += stringToHexPath(name) + "/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 += stringToHexPath(name); const ResourceManager *const resman = ResourceManager::getInstance(); resman->saveTextFile(dir, "comment.txt", name + "\n" + 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; if (needUpdate) { updateName(); addToCache(); } } void Being::setEmote(const uint8_t emotion, const int emote_time) { if ((emotion & FLAG_SPECIAL) == FLAG_SPECIAL) { setState(emotion); mAdvanced = true; } else { mEmotion = emotion; mEmotionTime = emote_time; } } 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]; } BeingEquipBackend::BeingEquipBackend(Being *const being): mBeing(being) { memset(mEquipment, 0, sizeof(mEquipment)); if (being) { const size_t sz = being->mSpriteIDs.size(); for (unsigned f = 0; f < sz; f ++) { const int idx = Net::getInventoryHandler()-> convertFromServerSlot(f); const int id = being->mSpriteIDs[f]; if (id > 0 && idx >= 0 && idx < EQUIPMENT_SIZE) { mEquipment[idx] = new Item(id, 1, 0, being->mSpriteColorsIds[f], true, true); } } } } BeingEquipBackend::~BeingEquipBackend() { clear(); } void BeingEquipBackend::clear() { for (int i = 0; i < EQUIPMENT_SIZE; i++) { delete mEquipment[i]; mEquipment[i] = nullptr; } } void BeingEquipBackend::setEquipment(const int index, Item *const item) { mEquipment[index] = item; } Item *BeingEquipBackend::getEquipment(const int index) const { if (index < 0 || index >= EQUIPMENT_SIZE) return nullptr; return mEquipment[index]; }