/* * The ManaPlus Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * Copyright (C) 2011-2014 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 . */ #include "being/being.h" #include "actormanager.h" #include "animatedsprite.h" #include "beingequipbackend.h" #include "client.h" #include "configuration.h" #include "effectmanager.h" #include "guild.h" #include "party.h" #include "soundmanager.h" #include "text.h" #include "being/beingcacheentry.h" #include "being/playerrelations.h" #include "particle/particle.h" #include "particle/particleinfo.h" #include "gui/font.h" #include "gui/gui.h" #include "gui/popups/speechbubble.h" #include "gui/windows/equipmentwindow.h" #include "gui/windows/skilldialog.h" #include "gui/windows/socialwindow.h" #include "net/charserverhandler.h" #include "net/gamehandler.h" #include "net/net.h" #include "net/npchandler.h" #include "net/pethandler.h" #include "net/playerhandler.h" #include "resources/iteminfo.h" #include "resources/resourcemanager.h" #include "resources/db/avatardb.h" #include "resources/db/emotedb.h" #include "resources/db/itemdb.h" #include "resources/db/monsterdb.h" #include "resources/db/npcdb.h" #include "resources/db/petdb.h" #include "gui/widgets/skilldata.h" #include "gui/widgets/skillinfo.h" #include "gui/widgets/tabs/langtab.h" #include "utils/gettext.h" #include "utils/timer.h" #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; bool Being::mUseDiagonal = true; int Being::mAwayEffect = -1; std::list beingInfoCache; typedef std::map::const_iterator GuildsMapCIter; typedef std::map::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(), mSpriteParticles(), mGuilds(), mParty(nullptr), mActionTime(0), mEmotionTime(0), mSpeechTime(0), mAttackSpeed(350), mLevel(0), mAttackRange(1), mLastAttackX(0), mLastAttackY(0), 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()), mSpeed(Net::getPlayerHandler()->getDefaultWalkSpeed().x), mIp(), mSpriteRemap(new int[20]), mSpriteHide(new int[20]), mSpriteDraw(new int[20]), mComment(), mPets(), mOwner(nullptr), mSpecialParticle(nullptr), mX(0), mY(0), mSortOffsetY(0), mOffsetY(0), mOldHeight(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), mLook(0), mUsageCounter(1), mHairColor(0), mErased(false), mEnemy(false), mGotComment(false), mAdvanced(false), mShop(false), mAway(false), mInactive(false) { mSpeechBubble->postInit(); for (int f = 0; f < 20; f ++) { mSpriteRemap[f] = f; mSpriteHide[f] = 0; mSpriteDraw[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); CHECKLISTENERS delete [] mSpriteRemap; mSpriteRemap = nullptr; delete [] mSpriteHide; mSpriteHide = nullptr; delete [] mSpriteDraw; mSpriteDraw = nullptr; delete2(mSpeechBubble); delete2(mDispName); delete2(mText); delete2(mEmotionSprite); delete2(mAnimationEffect); if (mOwner) mOwner->unassignPet(this); FOR_EACH (std::vector::iterator, it, mPets) { Being *pet = *it; if (pet) { pet->setOwner(nullptr); actorManager->erase(pet); delete pet; } } mPets.clear(); removeAllItemsParticles(); } 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) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), false); mYDiff = mInfo->getSortOffsetY(); const int speed = mInfo->getWalkSpeed(); if (!speed) { setWalkSpeed(Net::getPlayerHandler() ->getDefaultWalkSpeed()); } else { setWalkSpeed(Vector(speed, speed, 0)); } } } 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(pos.x), static_cast(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; setPath(mMap->findPath(mX, mY, dstX, dstY, getWalkMask())); } void Being::clearPath() { mPath.clear(); } void Being::setPath(const Path &path) { mPath = path; if (mPath.empty()) return; if (mAction != MOVE && mAction != DEAD) { 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(SPEECH_TIME - 300 + (3 * sz)); } if (time < static_cast(SPEECH_MIN_TIME)) time = static_cast(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(SPEECH_MAX_TIME) ? time : static_cast(SPEECH_MAX_TIME); } const int speech = mSpeechType; if (speech == TEXT_OVERHEAD && userPalette) { delete mText; mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), Graphics::CENTER, &userPalette->getColor(UserPalette::PARTICLE), true); } else { const bool isShowName = (speech == NAME_IN_BUBBLE); mSpeechBubble->setCaption(isShowName ? mName : ""); mSpeechBubble->setText(mSpeech, isShowName); } } void Being::takeDamage(Being *const attacker, const int amount, const AttackType type, const int attackId) { if (!userPalette || !attacker) return; Font *font = nullptr; // TRANSLATORS: hit or miss message in attacks const std::string damage = amount ? toString(amount) : type == FLEE ? _("dodge") : _("miss"); const 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 *const 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 *const 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); mLastAttackX = victim->getTileX(); mLastAttackY = victim->getTileY(); if (mType == PLAYER && mEquippedWeapon) fireMissile(victim, mEquippedWeapon->getMissileParticleFile()); else if (mInfo->getAttack(attackId)) fireMissile(victim, mInfo->getAttack(attackId)->mMissileParticle); 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, 0); 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); const SkillInfo *const skill = skillDialog->getSkill(skillId); const SkillData *const data = skill ? skill->getData1(skillLevel) : nullptr; if (data) fireMissile(victim, data->particle); 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, 0); 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 delete2(mDispName) } 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); } const Guild *Being::getGuild(const std::string &guildName) const { FOR_EACH (GuildsMapCIter, itr, mGuilds) { const Guild *const guild = itr->second; if (guild && guild->getName() == guildName) return guild; } return nullptr; } const Guild *Being::getGuild(const int id) const { const std::map::const_iterator itr = mGuilds.find(id); if (itr != mGuilds.end()) return itr->second; return nullptr; } Guild *Being::getGuild() const { const std::map::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) { 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) { 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) { ItemSoundEvent 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 (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, false, 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(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(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, 0); 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, 0); return; } mActionTime += static_cast(mSpeed / 10); if ((mType != PLAYER || mUseDiagonal) && mX != pos.x && mY != pos.y) mSpeed = mWalkSpeed.x * 1.4; else mSpeed = mWalkSpeed.x; if (mX != pos.x || mY != pos.y) mOldHeight = mMap->getHeightOffset(mX, mY); mX = pos.x; mY = pos.y; const uint8_t height = mMap->getHeightOffset(mX, mY); mOffsetY = height - mOldHeight; setAction(MOVE, 0); } 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) delete2(mText) const int time = tick_time * MILLISECONDS_IN_A_TICK; if (mEmotionSprite) mEmotionSprite->update(time); if (mAnimationEffect) { mAnimationEffect->update(time); if (mAnimationEffect->isTerminated()) delete2(mAnimationEffect) } int frameCount = static_cast(getFrameCount()); switch (mAction) { case STAND: case SIT: case DEAD: case HURT: case SPAWN: default: break; case MOVE: { if (static_cast(get_elapsed_time( mActionTime)) >= mSpeed) { nextTile(); } break; } case ATTACK: { if (!mActionTime) break; int curFrame = 0; if (mAttackSpeed) { curFrame = (get_elapsed_time(mActionTime) * frameCount) / mAttackSpeed; } if (this == player_node && curFrame >= frameCount) nextTile(); break; } } if (mAction == MOVE) { const int xOffset = getXOffset(); const int yOffset = getYOffset(); int offset = xOffset; if (!offset) offset = yOffset; mSortOffsetY = (mOldHeight * mapTileSize / 2) + (mOffsetY * mapTileSize / 2) * (mapTileSize - abs(offset)) / mapTileSize; const int yOffset2 = yOffset - mSortOffsetY; // Update pixel coordinates setPosition(static_cast(mX * mapTileSize + mapTileSize / 2 + xOffset), static_cast( mY * mapTileSize + mapTileSize + yOffset2)); } else { setPosition(static_cast(mX * mapTileSize + mapTileSize / 2), static_cast(mY * mapTileSize + mapTileSize)); } if (mEmotionSprite) { mEmotionTime--; if (mEmotionTime == 0) delete2(mEmotionSprite) } ActorSprite::logic(); if (frameCount < 10) frameCount = 10; if (!isAlive() && mSpeed && Net::getGameHandler()->removeDeadBeings() && static_cast ((static_cast(get_elapsed_time(mActionTime)) / mSpeed)) >= frameCount) { if (mType != PLAYER && actorManager) actorManager->destroy(this); } FOR_EACH (std::vector::iterator, it, mPets) { Being *const pet = *it; if (pet) pet->petLogic(); } 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::petLogic() { if (!mOwner || !mMap || !mInfo) return; const int time = tick_time; const int thinkTime = mInfo->getThinkTime(); if (abs(mMoveTime - time) < thinkTime) return; mMoveTime = time; const int dstX0 = mOwner->getTileX(); const int dstY0 = mOwner->getTileY(); int dstX = dstX0; int dstY = dstY0; const int followDist = mInfo->getStartFollowDist(); const int warpDist = mInfo->getWarpDist(); const int dist = mInfo->getFollowDist(); const int divX = abs(dstX - mX); const int divY = abs(dstY - mY); if (divX >= warpDist || divY >= warpDist) { setAction(Being::STAND, 0); fixPetSpawnPos(dstX, dstY); setTileCoords(dstX, dstY); Net::getPetHandler()->spawn(mOwner, mId, dstX, dstY); } else if (!followDist || divX > followDist || divY > followDist) { if (!dist) { fixPetSpawnPos(dstX, dstY); } else { if (divX > followDist) { if (dstX > mX + dist) dstX -= dist; else if (dstX + dist <= mX) dstX += dist; } else { dstX = mX; } if (divY > followDist) { if (dstY > mY + dist) dstY -= dist; else if (dstX + dist <= mX) dstY += dist; } else { dstY = mY; } } const int walkMask = getWalkMask(); if (!mMap->getWalk(dstX, dstY, walkMask)) { if (dstX != dstX0) { dstX = dstX0; if (!mMap->getWalk(dstX, dstY, walkMask)) dstY = dstY0; } else if (dstY != dstY0) { dstY = dstY0; if (!mMap->getWalk(dstX, dstY, walkMask)) dstX = dstX0; } } if (mX != dstX || mY != dstY) { setPath(mMap->findPath(mX, mY, dstX, dstY, walkMask)); Net::getPetHandler()->move(mOwner, mId, mX, mY, dstX, dstY); return; } } if (mOwner->getCurrentAction() != ATTACK) { if (mAction == ATTACK) setAction(STAND, 0); } else { if (mAction == STAND || mAction == ATTACK) setAction(ATTACK, 0); } if (mAction == STAND || mAction == ATTACK) { int directionType = 0; switch (mOwner->getCurrentAction()) { case STAND: case MOVE: case HURT: case SPAWN: default: directionType = mInfo->getDirectionType(); break; case SIT: directionType = mInfo->getSitDirectionType(); break; case DEAD: directionType = mInfo->getDeadDirectionType(); break; case ATTACK: directionType = mInfo->getAttackDirectionType(); break; } int newDir = 0; switch (directionType) { case 0: default: return; case 1: newDir = mOwner->getDirection(); break; case 2: if (dstX > dstX0) newDir |= LEFT; else if (dstX < dstX0) newDir |= RIGHT; if (dstY > dstY0) newDir |= UP; else if (dstY < dstY0) newDir |= DOWN; break; case 3: if (dstX > dstX0) newDir |= RIGHT; else if (dstX < dstX0) newDir |= LEFT; if (dstY > dstY0) newDir |= DOWN; else if (dstY < dstY0) newDir |= UP; break; case 4: { const int dstX2 = mOwner->getLastAttackX(); const int dstY2 = mOwner->getLastAttackY(); if (dstX > dstX2) newDir |= LEFT; else if (dstX < dstX2) newDir |= RIGHT; if (dstY > dstY2) newDir |= UP; else if (dstY < dstY2) newDir |= DOWN; break; } } if (newDir && newDir != getDirection()) setDirection(newDir); } } void Being::drawEmotion(Graphics *const graphics, const int offsetX, const int offsetY) const { const int px = getPixelX() - offsetX - mapTileSize / 2; const int py = getPixelY() - offsetY - mapTileSize * 2 - mapTileSize; 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->isVisibleLocal()) mSpeechBubble->setVisible(false); } else if (mSpeechTime > 0 && (speech == NAME_IN_BUBBLE || speech == NO_NAME_IN_BUBBLE)) { delete2(mText) mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), py - getHeight() - (mSpeechBubble->getHeight())); mSpeechBubble->setVisible(true); mSpeechBubble->requestMoveToBackground(); } else if (mSpeechTime > 0 && speech == TEXT_OVERHEAD) { mSpeechBubble->setVisible(false); if (!mText && userPalette) { mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), Graphics::CENTER, &theme->getColor( Theme::BUBBLE_TEXT, 255), true); } } else if (speech == NO_SPEECH) { mSpeechBubble->setVisible(false); delete2(mText) } } 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((static_cast(time) * static_cast(mMap->getTileWidth())) / mSpeed) : static_cast((static_cast(time) * static_cast(mMap->getTileHeight())) / mSpeed); } // We calculate the offset _from_ the _target_ location offset -= mapTileSize; if (offset > 0) offset = 0; // Going into negative direction? Invert the offset. if (mDirection & pos) offset = -offset; if (offset > mapTileSize) offset = mapTileSize; if (offset < -mapTileSize) offset = -mapTileSize; 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; delete2(mDispName); 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())); } 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(), Graphics::CENTER, mNameColor, font); } else { mDispName = new FlashText(displayName, getPixelX(), getPixelY(), 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->getColor(Theme::PLAYER, 255); } else { mTextColor = &theme->getColor(Theme::PLAYER, 255); 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 && player_node && 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::updateSprite(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 >= mSpriteIDs.size()) mSpriteIDs.resize(slot + 1, 0); if (slot && mSpriteIDs[slot] == id) return; setSprite(slot, id, color, colorId, isWeapon, isTempSprite); } 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); // disabled for now, because it may broke replace/reorder sprites logic // if (slot && mSpriteIDs[slot] == id) // return; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; if (isWeapon) mEquippedWeapon = nullptr; const int id1 = mSpriteIDs[slot]; if (id1) { const ItemInfo &info = ItemDB::get(id1); if (!isTempSprite && mMap && mType == PLAYER) { const int pet = info.getPet(); if (pet) removePet(pet); } removeItemParticles(id1); } } else { const ItemInfo &info = ItemDB::get(id); const std::string &filename = info.getSprite(mGender, mSubType); AnimatedSprite *equipmentSprite = nullptr; if (!isTempSprite && 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); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); if (isWeapon) mEquippedWeapon = &ItemDB::get(id); setAction(mAction, 0); } 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::const_iterator it1 = mSpriteIDs.begin(); const std::vector::const_iterator it1_end = mSpriteIDs.end(); StringVectCIter it2 = mSpriteColors.begin(); const StringVectCIter it2_end = mSpriteColors.end(); std::vector::const_iterator it3 = mSpriteColorsIds.begin(); const std::vector::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"); mUseDiagonal = config.getBoolValue("useDiagonalSpeed"); 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::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); } void Being::draw(Graphics *const graphics, const int offsetX, const int offsetY) const { if (!mErased) { const int px = getActorX() + offsetX; const int py = getActorY() + offsetY; ActorSprite::draw1(graphics, px, py); drawSpriteAt(graphics, px, py); } } void Being::drawSprites(Graphics *const 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 *const 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); } } void Being::drawSpriteAt(Graphics *const graphics, const int x, const int y) const { CompoundSprite::draw(graphics, x, y); if (!userPalette) return; if (mHighlightMapPortals && mMap && mSubType == 45 && !mMap->getHasWarps()) { graphics->setColor(userPalette-> getColorWithAlpha(UserPalette::PORTAL_HIGHLIGHT)); graphics->fillRectangle(Rect(x, y, mapTileSize, mapTileSize)); if (mDrawHotKeys && !mName.empty()) { 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 = mapTileSize * mAttackRange; else attackRange = mapTileSize; graphics->setColor(userPalette->getColorWithAlpha( UserPalette::MONSTER_ATTACK_RANGE)); graphics->fillRectangle(Rect( x - attackRange, y - attackRange, 2 * attackRange + mapTileSize, 2 * attackRange + mapTileSize)); } 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 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 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 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } 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(maxHP) / static_cast(hp); } else if (maxHP != damage) { p = static_cast(maxHP) / static_cast(maxHP - damage); } else { p = 1; } if (p <= 0 || p > width) return; const int dx = static_cast(static_cast(width) / p); if (serverVersion < 1) { // old servers if ((!damage && (this != player_node || hp == maxHP)) || (!hp && maxHP == damage)) { graphics->setColor(userPalette->getColorWithAlpha(color1)); graphics->fillRectangle(Rect( x, y, dx, height)); return; } else if (width - dx <= 0) { graphics->setColor(userPalette->getColorWithAlpha(color2)); graphics->fillRectangle(Rect( x, y, width, height)); return; } } else { // evol servers if (hp == maxHP) { graphics->setColor(userPalette->getColorWithAlpha(color1)); graphics->fillRectangle(Rect( x, y, dx, height)); return; } else if (width - dx <= 0) { graphics->setColor(userPalette->getColorWithAlpha(color2)); graphics->fillRectangle(Rect( x, y, width, height)); return; } } graphics->setColor(userPalette->getColorWithAlpha(color1)); graphics->fillRectangle(Rect( x, y, dx, height)); graphics->setColor(userPalette->getColorWithAlpha(color2)); graphics->fillRectangle(Rect(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(size()); if (sz < 1) return; std::vector slotRemap; std::map itemSlotRemap; std::vector::iterator it; int oldHide[20]; bool updatedSprite[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; updatedSprite[slot] = false; } 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()) { const SpriteToItemMap *const spriteToItems = info.getSpriteToItemReplaceMap(dir); if (spriteToItems) { FOR_EACHP (SpriteToItemMapCIter, itr, spriteToItems) { const int remSprite = itr->first; const std::map &itemReplacer = itr->second; if (remSprite >= 0) { // slot known if (itemReplacer.empty()) { mSpriteHide[remSprite] = 1; } else if (mSpriteHide[remSprite] != 1) { std::map::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(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); } updatedSprite[remSprite] = 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); } updatedSprite[slot2] = 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(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::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 (mSpriteHide[slot] == 0) { if (oldHide[slot] != 0 && oldHide[slot] != 1) { const int id = mSpriteIDs[slot]; if (!id) continue; updatedSprite[slot] = true; setSprite(slot, id, mSpriteColors[slot], 1, false, true); } } } for (unsigned slot = 0; slot < spriteIdSize; slot ++) { if (mSpriteHide[slot] == 0) { const int id = mSpriteIDs[slot]; if (updatedSprite[slot] == false && mSpriteDraw[slot] != id) setSprite(slot, id, mSpriteColors[slot], 1, false, true); } } } int Being::searchSlotValue(const std::vector &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::iterator &it, int &idx, std::vector &slotRemap, const int val) { // logger->log("searching %d", val); it = slotRemap.begin(); const std::vector::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(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"); const ResourceManager *const resman = ResourceManager::getInstance(); if (ResourceManager::existsLocal(str)) { StringVect lines; resman->loadTextFileLocal(str, lines); if (lines.size() >= 2) return lines[1]; } return ""; } void Being::saveComment(const std::string &restrict name, const std::string &restrict 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)); ResourceManager::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()) { delete2(mEmotionSprite) 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( 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, 0); } } } 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(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; } delete2(mAnimationEffect); } 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 (!actorManager || !config.getBoolValue("usepets")) return; Being *const pet = findChildPet(id); if (pet) { pet->incUsage(); return; } Being *const being = actorManager->createBeing( id, ActorSprite::PET, 0); if (being) { being->setOwner(this); mPets.push_back(being); int dstX = mX; int dstY = mY; being->fixPetSpawnPos(dstX, dstY); being->setTileCoords(dstX, dstY); Net::getPetHandler()->spawn(this, being->mId, dstX, dstY); } } Being *Being::findChildPet(const int id) { FOR_EACH (std::vector::iterator, it, mPets) { Being *const pet = *it; if (pet && pet->mId == id) return pet; } return nullptr; } void Being::removePet(const int id) { if (!actorManager) return; FOR_EACH (std::vector::iterator, it, mPets) { Being *const pet = *it; if (pet && pet->mId == id) { if (!pet->decUsage()) { pet->setOwner(nullptr); actorManager->erase(pet); mPets.erase(it); delete pet; return; } } } } void Being::removeAllPets() { FOR_EACH (std::vector::iterator, it, mPets) { Being *const pet = *it; if (pet) { pet->setOwner(nullptr); actorManager->erase(pet); delete pet; } } mPets.clear(); } void Being::updatePets() { removeAllPets(); FOR_EACH (std::vector::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); } } void Being::unassignPet(const Being *const pet1) { FOR_EACH (std::vector::iterator, it, mPets) { if (*it == pet1) { mPets.erase(it); return; } } } void Being::fixPetSpawnPos(int &dstX, int &dstY) const { if (!mInfo || !mOwner) return; int offsetX1; int offsetY1; switch (mOwner->getCurrentAction()) { case SIT: offsetX1 = mInfo->getSitOffsetX(); offsetY1 = mInfo->getSitOffsetY(); break; case MOVE: offsetX1 = mInfo->getMoveOffsetX(); offsetY1 = mInfo->getMoveOffsetY(); break; case DEAD: offsetX1 = mInfo->getDeadOffsetX(); offsetY1 = mInfo->getDeadOffsetY(); break; case ATTACK: offsetX1 = mInfo->getAttackOffsetX(); offsetY1 = mInfo->getAttackOffsetY(); break; case SPAWN: case HURT: case STAND: default: offsetX1 = mInfo->getTargetOffsetX(); offsetY1 = mInfo->getTargetOffsetY(); break; } int offsetX = offsetX1; int offsetY = offsetY1; switch (mOwner->getDirection()) { case LEFT: offsetX = -offsetY1; offsetY = offsetX1; break; case RIGHT: offsetX = offsetY1; offsetY = -offsetX1; break; case UP: offsetY = -offsetY; offsetX = -offsetX; break; default: case DOWN: break; } dstX += offsetX; dstY += offsetY; if (mMap) { if (!mMap->getWalk(dstX, dstY, getWalkMask())) { dstX = mOwner->getTileX(); dstY = mOwner->getTileY(); } } } void Being::playSfx(const SoundInfo &sound, Being *const being, const bool main, const int x, const int y) const { 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); } void Being::setTileCoords(const int x, const int y) { mX = x; mY = y; if (mMap) mOffsetY = mMap->getHeightOffset(mX, mY); } void Being::setMap(Map *const map) { ActorSprite::setMap(map); if (mMap) mOffsetY = mMap->getHeightOffset(mX, mY); } void Being::removeAllItemsParticles() { FOR_EACH (SpriteParticleInfoIter, it, mSpriteParticles) delete (*it).second; mSpriteParticles.clear(); } void Being::addItemParticles(const int id, const SpriteDisplay &display) { SpriteParticleInfoIter it = mSpriteParticles.find(id); ParticleInfo *pi = nullptr; if (it == mSpriteParticles.end()) { pi = new ParticleInfo(); mSpriteParticles[id] = pi; } else { pi = (*it).second; } if (!pi->particles.empty()) return; // setup particle effects if (Particle::enabled && particleEngine) { FOR_EACH (StringVectCIter, itr, display.particles) { Particle *const p = particleEngine->addEffect(*itr, 0, 0); controlParticle(p); pi->files.push_back(*itr); pi->particles.push_back(p); } } else { FOR_EACH (StringVectCIter, itr, display.particles) pi->files.push_back(*itr); } } void Being::removeItemParticles(const int id) { SpriteParticleInfoIter it = mSpriteParticles.find(id); if (it == mSpriteParticles.end()) return; ParticleInfo *const pi = (*it).second; if (pi) { FOR_EACH (std::vector::const_iterator, itp, pi->particles) mChildParticleEffects.removeLocally(*itp); delete pi; } mSpriteParticles.erase(it); } void Being::recreateItemParticles() { FOR_EACH (SpriteParticleInfoIter, it, mSpriteParticles) { ParticleInfo *const pi = (*it).second; if (pi && !pi->files.empty()) { FOR_EACH (std::vector::const_iterator, itp, pi->particles) { mChildParticleEffects.removeLocally(*itp); } FOR_EACH (std::vector::const_iterator, str, pi->files) { Particle *const p = particleEngine->addEffect(*str, 0, 0); controlParticle(p); pi->particles.push_back(p); } } } }