/* * The ManaPlus Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * Copyright (C) 2011-2016 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 "beingequipbackend.h" #include "configuration.h" #include "effectmanager.h" #include "guild.h" #include "party.h" #include "settings.h" #include "soundmanager.h" #include "text.h" #include "being/beingcacheentry.h" #include "being/beingflag.h" #include "being/beingspeech.h" #include "being/playerinfo.h" #include "being/playerrelations.h" #include "const/utils/timer.h" #include "const/resources/spriteaction.h" #include "enums/being/beingdirection.h" #include "particle/particleinfo.h" #include "gui/gui.h" #include "gui/userpalette.h" #include "gui/fonts/font.h" #include "gui/popups/speechbubble.h" #include "gui/windows/chatwindow.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/npchandler.h" #include "net/packetlimiter.h" #include "net/playerhandler.h" #include "net/serverfeatures.h" #include "resources/attack.h" #include "resources/chatobject.h" #include "resources/emoteinfo.h" #include "resources/emotesprite.h" #include "resources/horseinfo.h" #include "resources/iteminfo.h" #include "resources/db/avatardb.h" #include "resources/db/badgesdb.h" #include "resources/db/emotedb.h" #include "resources/db/homunculusdb.h" #include "resources/db/horsedb.h" #include "resources/db/itemdb.h" #include "resources/db/mercenarydb.h" #include "resources/db/monsterdb.h" #include "resources/db/npcdb.h" #include "resources/db/petdb.h" #include "resources/db/skillunitdb.h" #include "resources/map/map.h" #include "resources/sprite/animatedsprite.h" #include "gui/widgets/createwidget.h" #include "gui/widgets/skilldata.h" #include "gui/widgets/skillinfo.h" #include "gui/widgets/tabs/chat/langtab.h" #include "utils/delete2.h" #include "utils/files.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; Move Being::mMoveNames = Move_false; bool Being::mUseDiagonal = true; uint8_t Being::mShowBadges = 1; int Being::mAwayEffect = -1; std::list beingInfoCache; typedef std::map::const_iterator GuildsMapCIter; typedef std::map::const_iterator IntMapCIter; static const unsigned int SPEECH_TIME = 500; static const unsigned int SPEECH_MIN_TIME = 200; static const unsigned int SPEECH_MAX_TIME = 800; #define for_each_badges() \ for (int f = 0; f < BadgeIndex::BadgeIndexSize; f++) #define for_each_horses(name) \ FOR_EACH (std::vector::const_iterator, it, name) Being::Being(const BeingId id, const ActorTypeT type, const BeingTypeId subtype, Map *const map) : ActorSprite(id), mNextSound(), mInfo(BeingInfo::unknown), mEmotionSprite(nullptr), mAnimationEffect(nullptr), mBadges(), 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), mPreStandTime(0), mGender(Gender::UNSPECIFIED), mAction(BeingAction::STAND), mSubType(fromInt(0xFFFF, BeingTypeId)), mDirection(BeingDirection::DOWN), mDirectionDelayed(0), mSpriteDirection(SpriteDirection::DOWN), mShowName(false), mIsGM(false), mType(type), mSpeechBubble(nullptr), mWalkSpeed(playerHandler ? playerHandler->getDefaultWalkSpeed() : Vector(1, 1, 1)), mSpeed(playerHandler ? playerHandler->getDefaultWalkSpeed().x : 0), mIp(), mSpriteRemap(new int[20]), mSpriteHide(new int[20]), mSpriteDraw(new int[20]), mComment(), #ifdef EATHENA_SUPPORT mBuyBoard(), mSellBoard(), #endif mPets(), mOwner(nullptr), mSpecialParticle(nullptr), #ifdef EATHENA_SUPPORT mChat(nullptr), mHorseInfo(nullptr), mDownHorseSprites(), mUpHorseSprites(), mSpiritParticles(), #endif mX(0), mY(0), mSortOffsetY(0), mOffsetY(0), mFixedOffsetY(0), mOldHeight(0), mDamageTaken(0), mHP(0), mMaxHP(0), mDistance(0), mReachable(Reachable::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), mSpiritBalls(0U), mUsageCounter(1), mKarma(0), mManner(0), mAreaSize(11), mTeamId(0U), mLook(0U), mBadgesCount(0U), mHairColor(ItemColor_zero), mErased(false), mEnemy(false), mGotComment(false), mAdvanced(false), mShop(false), mAway(false), mInactive(false), mNeedPosUpdate(true), mPetAi(true) { for (int f = 0; f < 20; f ++) { mSpriteRemap[f] = f; mSpriteHide[f] = 0; mSpriteDraw[f] = 0; } for_each_badges() mBadges[f] = nullptr; setMap(map); setSubtype(subtype, 0); if (mType == ActorType::Player #ifdef EATHENA_SUPPORT || mType == ActorType::Mercenary || mType == ActorType::Pet || mType == ActorType::Homunculus #endif ) { mShowName = config.getBoolValue("visiblenames"); } else if (mType != ActorType::Npc) { mGotComment = true; } #ifdef EATHENA_SUPPORT if (mType == ActorType::Portal || mType == ActorType::SkillUnit) #else if (mType == ActorType::Portal) #endif { mShowName = false; } config.addListener("visiblenames", this); reReadConfig(); if (mType == ActorType::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; for_each_badges() delete2(mBadges[f]); delete2(mSpeechBubble); delete2(mDispName); delete2(mText); delete2(mEmotionSprite); delete2(mAnimationEffect); mBadgesCount = 0; #ifdef EATHENA_SUPPORT delete2(mChat); removeHorse(); #endif if (mOwner) { if (mType == ActorType::LocalPet) mOwner->unassignPet(this); mOwner = nullptr; } FOR_EACH (std::vector::iterator, it, mPets) { Being *pet = *it; if (pet) { pet->setOwner(nullptr); actorManager->erase(pet); delete pet; } } mPets.clear(); removeAllItemsParticles(); #ifdef EATHENA_SUPPORT mSpiritParticles.clear(); #endif } void Being::createSpeechBubble() restrict2 { CREATEWIDGETV0(mSpeechBubble, SpeechBubble); } void Being::setSubtype(const BeingTypeId subtype, const uint16_t look) restrict2 { if (!mInfo) return; if (subtype == mSubType && mLook == look) return; mSubType = subtype; mLook = look; if (mType == ActorType::Monster) { mInfo = MonsterDB::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, 0, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } } #ifdef EATHENA_SUPPORT if (mType == ActorType::Pet) { mInfo = PETDB::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, 0, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } } else if (mType == ActorType::Mercenary) { mInfo = MercenaryDB::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, 0, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } } if (mType == ActorType::Homunculus) { mInfo = HomunculusDB::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, 0, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } } if (mType == ActorType::SkillUnit) { mInfo = SkillUnitDb::get(mSubType); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false, 0, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } } #endif else if (mType == ActorType::Npc) { mInfo = NPCDB::get(mSubType); if (mInfo) { setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false); mYDiff = mInfo->getSortOffsetY(); } } else if (mType == ActorType::Avatar) { mInfo = AvatarDB::get(mSubType); if (mInfo) setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false); } else if (mType == ActorType::LocalPet) { mInfo = PETDB::get(fromInt(mId, BeingTypeId)); if (mInfo) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false); mYDiff = mInfo->getSortOffsetY(); const int speed = mInfo->getWalkSpeed(); if (!speed) { if (playerHandler) setWalkSpeed(playerHandler->getDefaultWalkSpeed()); else setWalkSpeed(Vector(1, 1, 0)); } else { setWalkSpeed(Vector(speed, speed, 0)); } } } else if (mType == ActorType::Player) { int id = -100 - toInt(subtype, int); // Prevent showing errors when sprite doesn't exist if (!ItemDB::exists(id)) { id = -100; // TRANSLATORS: default race name setRaceName(_("Human")); if (charServerHandler) setSprite(charServerHandler->baseSprite(), id); } else { const ItemInfo &restrict info = ItemDB::get(id); setRaceName(info.getName()); if (charServerHandler) { setSprite(charServerHandler->baseSprite(), id, info.getColor(fromInt(mLook, ItemColor))); } } } } TargetCursorSizeT Being::getTargetCursorSize() const restrict2 { if (!mInfo) return TargetCursorSize::SMALL; return mInfo->getTargetCursorSize(); } void Being::setPosition(const Vector &restrict pos) restrict2 { 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) restrict2 { // We can't calculate anything without a map anyway. if (!mMap) return; setPath(mMap->findPath(mX, mY, dstX, dstY, getBlockWalkMask())); } void Being::clearPath() restrict2 { mPath.clear(); } void Being::setPath(const Path &restrict path) restrict2 { mPath = path; if (mPath.empty()) return; if (mAction != BeingAction::MOVE && mAction != BeingAction::DEAD) { nextTile(); mActionTime = tick_time; } } void Being::setSpeech(const std::string &restrict text, const std::string &restrict channel, int time) restrict2 { 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 == BeingSpeech::TEXT_OVERHEAD) { delete mText; mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), Graphics::CENTER, &userPalette->getColor(UserColorId::PARTICLE), Speech_true); } else { const bool isShowName = (speech == BeingSpeech::NAME_IN_BUBBLE); if (!mSpeechBubble) createSpeechBubble(); if (mSpeechBubble) { mSpeechBubble->setCaption(isShowName ? mName : ""); mSpeechBubble->setText(mSpeech, isShowName); } } } void Being::takeDamage(Being *restrict const attacker, const int amount, const AttackTypeT type, const int attackId, const int level) restrict2 { if (!userPalette || !attacker) return; BLOCK_START("Being::takeDamage1") Font *font = nullptr; // TRANSLATORS: hit or miss message in attacks const std::string damage = amount ? toString(amount) : type == AttackType::FLEE ? _("dodge") : _("miss"); const Color *color; if (gui) font = gui->getInfoParticleFont(); // Selecting the right color if (type == AttackType::CRITICAL || type == AttackType::FLEE) { if (type == AttackType::CRITICAL) attacker->setCriticalHit(amount); if (attacker == localPlayer) { color = &userPalette->getColor( UserColorId::HIT_LOCAL_PLAYER_CRITICAL); } else { color = &userPalette->getColor(UserColorId::HIT_CRITICAL); } } else if (!amount) { if (attacker == localPlayer) { // This is intended to be the wrong direction to visually // differentiate between hits and misses color = &userPalette->getColor(UserColorId::HIT_LOCAL_PLAYER_MISS); } else { color = &userPalette->getColor(UserColorId::MISS); } } else if (mType == ActorType::Monster #ifdef EATHENA_SUPPORT || mType == ActorType::Mercenary || mType == ActorType::Pet || mType == ActorType::Homunculus #endif ) { if (attacker == localPlayer) { color = &userPalette->getColor( UserColorId::HIT_LOCAL_PLAYER_MONSTER); } else { color = &userPalette->getColor( UserColorId::HIT_PLAYER_MONSTER); } } else if (mType == ActorType::Player && attacker != localPlayer && this == localPlayer) { // here player was attacked by other player. mark him as enemy. color = &userPalette->getColor(UserColorId::HIT_PLAYER_PLAYER); attacker->setEnemy(true); attacker->updateColors(); } else { color = &userPalette->getColor(UserColorId::HIT_MONSTER_PLAYER); } if (chatWindow && mShowBattleEvents) { if (this == localPlayer) { if (attacker->mType == ActorType::Player || amount) { chatWindow->battleChatLog(strprintf("%s : Hit you -%d", attacker->getName().c_str(), amount), ChatMsgType::BY_OTHER); } } else if (attacker == localPlayer && amount) { chatWindow->battleChatLog(strprintf("%s : You hit %s -%d", attacker->getName().c_str(), getName().c_str(), amount), ChatMsgType::BY_PLAYER); } } if (font && particleEngine) { // Show damage number particleEngine->addTextSplashEffect(damage, getPixelX(), getPixelY() - 16, color, font, true); } BLOCK_END("Being::takeDamage1") BLOCK_START("Being::takeDamage2") if (type != AttackType::SKILL) attacker->updateHit(amount); if (amount > 0) { if (localPlayer && localPlayer == this) localPlayer->setLastHitFrom(attacker->getName()); mDamageTaken += amount; if (mInfo) { playSfx(mInfo->getSound(ItemSoundEvent::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 == ActorType::Monster) { updatePercentHP(); updateName(); } else if (mType == ActorType::Player && socialWindow && getName() != "") { socialWindow->updateAvatar(getName()); } if (effectManager) { const int hitEffectId = getHitEffect(attacker, type, attackId, level); if (hitEffectId >= 0) effectManager->trigger(hitEffectId, this); } } else { if (effectManager) { int hitEffectId = -1; if (type == AttackType::SKILL) { hitEffectId = getHitEffect(attacker, AttackType::SKILLMISS, attackId, level); } else { hitEffectId = getHitEffect(attacker, AttackType::MISS, attackId, level); } if (hitEffectId >= 0) effectManager->trigger(hitEffectId, this); } } BLOCK_END("Being::takeDamage2") } int Being::getHitEffect(const Being *restrict const attacker, const AttackTypeT type, const int attackId, const int level) const restrict2 { if (!effectManager) return 0; BLOCK_START("Being::getHitEffect") // Init the particle effect path based on current // weapon or default. int hitEffectId = 0; if (type == AttackType::SKILL || type == AttackType::SKILLMISS) { SkillData *restrict const data = skillDialog->getSkillDataByLevel( attackId, level); if (!data) return -1; if (type == AttackType::SKILL) { hitEffectId = data->hitEffectId; if (hitEffectId == -1) hitEffectId = paths.getIntValue("skillHitEffectId"); } else { hitEffectId = data->missEffectId; if (hitEffectId == -1) hitEffectId = paths.getIntValue("skillMissEffectId"); } } else { if (attacker) { const ItemInfo *restrict const attackerWeapon = attacker->getEquippedWeapon(); if (attackerWeapon && attacker->getType() == ActorType::Player) { if (type == AttackType::MISS) hitEffectId = attackerWeapon->getMissEffectId(); else if (type != AttackType::CRITICAL) hitEffectId = attackerWeapon->getHitEffectId(); else hitEffectId = attackerWeapon->getCriticalHitEffectId(); } else if (attacker->getType() == ActorType::Monster) { const BeingInfo *restrict const info = attacker->getInfo(); if (info) { const Attack *restrict const atk = info->getAttack(attackId); if (atk) { if (type == AttackType::MISS) hitEffectId = atk->mMissEffectId; else if (type != AttackType::CRITICAL) hitEffectId = atk->mHitEffectId; else hitEffectId = atk->mCriticalHitEffectId; } else { hitEffectId = getDefaultEffectId(type); } } } else { hitEffectId = getDefaultEffectId(type); } } else { hitEffectId = getDefaultEffectId(type); } } BLOCK_END("Being::getHitEffect") return hitEffectId; } int Being::getDefaultEffectId(const AttackTypeT &restrict type) { if (type == AttackType::MISS) return paths.getIntValue("missEffectId"); else if (type != AttackType::CRITICAL) return paths.getIntValue("hitEffectId"); else return paths.getIntValue("criticalHitEffectId"); } void Being::handleAttack(Being *restrict const victim, const int damage, const int attackId) restrict2 { if (!victim || !mInfo) return; BLOCK_START("Being::handleAttack") if (this != localPlayer) setAction(BeingAction::ATTACK, attackId); mLastAttackX = victim->getTileX(); mLastAttackY = victim->getTileY(); if (mType == ActorType::Player && mEquippedWeapon) fireMissile(victim, mEquippedWeapon->getMissileParticleFile()); else if (mInfo->getAttack(attackId)) fireMissile(victim, mInfo->getAttack(attackId)->mMissileParticle); reset(); mActionTime = tick_time; if (serverFeatures && !serverFeatures->haveAttackDirections() && this != localPlayer) { const uint8_t dir = calcDirection(victim->getTileX(), victim->getTileY()); if (dir) setDirection(dir); } if (damage && victim->mType == ActorType::Player && victim->mAction == BeingAction::SIT) { victim->setAction(BeingAction::STAND, 0); } if (mType == ActorType::Player) { if (mSpriteIDs.size() >= 10) { // here 10 is weapon slot int weaponId = mSpriteIDs[10]; if (!weaponId) weaponId = -100 - toInt(mSubType, int); const ItemInfo &info = ItemDB::get(weaponId); playSfx(info.getSound( (damage > 0) ? ItemSoundEvent::HIT : ItemSoundEvent::MISS), victim, true, mX, mY); } } else { playSfx(mInfo->getSound((damage > 0) ? ItemSoundEvent::HIT : ItemSoundEvent::MISS), victim, true, mX, mY); } BLOCK_END("Being::handleAttack") } void Being::handleSkill(Being *restrict const victim, const int damage, const int skillId, const int skillLevel) restrict2 { if (!victim || !mInfo || !skillDialog) return; const SkillInfo *restrict const skill = skillDialog->getSkill(skillId); const SkillData *restrict const data = skill ? skill->getData1(skillLevel) : nullptr; if (data) fireMissile(victim, data->particle); if (this != localPlayer && skill) { const SkillType::SkillType type = skill->type; if (type == SkillType::Attack || type == SkillType::Ground) setAction(BeingAction::ATTACK, 1); else setAction(BeingAction::STAND, 1); } reset(); mActionTime = tick_time; if (!serverFeatures->haveAttackDirections() && this != localPlayer) { const uint8_t dir = calcDirection(victim->getTileX(), victim->getTileY()); if (dir) setDirection(dir); } if (damage && victim->mType == ActorType::Player && victim->mAction == BeingAction::SIT) { victim->setAction(BeingAction::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) ? ItemSoundEvent::HIT : ItemSoundEvent::MISS), victim, true, mX, mY); } } void Being::showNameBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Name]); if (show && !mName.empty() && mShowBadges) { const std::string badge = BadgesDB::getNameBadge(mName); if (!badge.empty()) { mBadges[BadgeIndex::Name] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } } void Being::setName(const std::string &restrict name) restrict2 { if (mType == ActorType::Npc) { mName = name.substr(0, name.find('#', 0)); showName(); } else if (mType == ActorType::Player) { if (mName != name) { mName = name; showNameBadge(!mName.empty()); } if (getShowName()) showName(); } else { if (mType == ActorType::Portal) mName = name.substr(0, name.find('#', 0)); else mName = name; if (getShowName()) showName(); } } void Being::setShowName(const bool doShowName) restrict2 { if (mShowName == doShowName) return; mShowName = doShowName; if (doShowName) showName(); else delete2(mDispName) } void Being::showGuildBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Guild]); if (show && !mGuildName.empty() && mShowBadges) { const std::string badge = BadgesDB::getGuildBadge(mGuildName); if (!badge.empty()) { mBadges[BadgeIndex::Guild] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } } void Being::setGuildName(const std::string &restrict name) restrict2 { if (mGuildName != name) { mGuildName = name; showGuildBadge(!mGuildName.empty()); updateBadgesCount(); } } void Being::setGuildPos(const std::string &restrict pos A_UNUSED) restrict2 { } void Being::addGuild(Guild *restrict const guild) restrict2 { if (!guild) return; mGuilds[guild->getId()] = guild; if (this == localPlayer && socialWindow) socialWindow->addTab(guild); } void Being::removeGuild(const int id) restrict2 { if (this == localPlayer && socialWindow) socialWindow->removeTab(mGuilds[id]); if (mGuilds[id]) mGuilds[id]->removeMember(getName()); mGuilds.erase(id); } const Guild *Being::getGuild(const std::string &restrict guildName) const restrict2 { FOR_EACH (GuildsMapCIter, itr, mGuilds) { const Guild *restrict const guild = itr->second; if (guild && guild->getName() == guildName) return guild; } return nullptr; } const Guild *Being::getGuild(const int id) const restrict2 { const std::map::const_iterator itr = mGuilds.find(id); if (itr != mGuilds.end()) return itr->second; return nullptr; } Guild *Being::getGuild() const restrict2 { const std::map::const_iterator itr = mGuilds.begin(); if (itr != mGuilds.end()) return itr->second; return nullptr; } void Being::clearGuilds() restrict2 { FOR_EACH (GuildsMapCIter, itr, mGuilds) { Guild *const guild = itr->second; if (guild) { if (this == localPlayer && socialWindow) socialWindow->removeTab(guild); guild->removeMember(mId); } } mGuilds.clear(); } void Being::setParty(Party *restrict const party) restrict2 { if (party == mParty) return; Party *const old = mParty; mParty = party; if (old) old->removeMember(mId); if (party) party->addMember(mId, mName); updateColors(); if (this == localPlayer && socialWindow) { if (old) socialWindow->removeTab(old); if (party) socialWindow->addTab(party); } } void Being::updateGuild() restrict2 { if (!localPlayer) return; Guild *restrict const guild = localPlayer->getGuild(); if (!guild) { clearGuilds(); updateColors(); return; } if (guild->getMember(getName())) { setGuild(guild); if (!guild->getName().empty()) setGuildName(guild->getName()); } updateColors(); } void Being::setGuild(Guild *restrict const guild) restrict2 { Guild *restrict const old = getGuild(); if (guild == old) return; clearGuilds(); addGuild(guild); if (old) old->removeMember(mName); updateColors(); if (this == localPlayer && socialWindow) { if (old) socialWindow->removeTab(old); if (guild) socialWindow->addTab(guild); } } void Being::fireMissile(Being *restrict const victim, const std::string &restrict particle) const restrict2 { if (!victim || particle.empty() || !particleEngine) return; BLOCK_START("Being::fireMissile") Particle *restrict const target = particleEngine->createChild(); if (!target) { BLOCK_END("Being::fireMissile") return; } Particle *restrict 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); } BLOCK_END("Being::fireMissile") } std::string Being::getSitAction() const restrict2 { if (mHorseId != 0) return SpriteAction::SITRIDE; if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mask & BlockMask::GROUNDTOP) return SpriteAction::SITTOP; else if (mask & BlockMask::AIR) return SpriteAction::SITSKY; else if (mask & BlockMask::WATER) return SpriteAction::SITWATER; } return SpriteAction::SIT; } std::string Being::getMoveAction() const restrict2 { if (mHorseId != 0) return SpriteAction::RIDE; if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mask & BlockMask::AIR) return SpriteAction::FLY; else if (mask & BlockMask::WATER) return SpriteAction::SWIM; } return SpriteAction::MOVE; } std::string Being::getWeaponAttackAction(const ItemInfo *restrict const weapon) const restrict2 { if (!weapon) return getAttackAction(); if (mHorseId != 0) return weapon->getRideAttackAction(); if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mask & BlockMask::AIR) return weapon->getSkyAttackAction(); else if (mask & BlockMask::WATER) return weapon->getWaterAttackAction(); } return weapon->getAttackAction(); } std::string Being::getAttackAction(const Attack *restrict const attack1) const restrict2 { if (!attack1) return getAttackAction(); if (mHorseId != 0) return attack1->mRideAction; if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mask & BlockMask::AIR) return attack1->mSkyAction; else if (mask & BlockMask::WATER) return attack1->mWaterAction; } return attack1->mAction; } std::string Being::getCastAction(const SkillInfo *restrict const skill) const restrict2 { if (!skill) return getCastAction(); if (mHorseId != 0) return skill->castingRideAction; if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mask & BlockMask::AIR) return skill->castingSkyAction; else if (mask & BlockMask::WATER) return skill->castingWaterAction; } return skill->castingAction; } #define getSpriteAction(func, action) \ std::string Being::get##func##Action() const restrict2\ { \ if (mHorseId != 0) \ return SpriteAction::action##RIDE; \ if (mMap) \ { \ const unsigned char mask = mMap->getBlockMask(mX, mY); \ if (mask & BlockMask::AIR) \ return SpriteAction::action##SKY; \ else if (mask & BlockMask::WATER) \ return SpriteAction::action##WATER; \ } \ return SpriteAction::action; \ } getSpriteAction(Attack, ATTACK) getSpriteAction(Cast, CAST) getSpriteAction(Dead, DEAD) getSpriteAction(Spawn, SPAWN) std::string Being::getStandAction() const restrict2 { if (mHorseId != 0) return SpriteAction::STANDRIDE; if (mMap) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mTrickDead) { if (mask & BlockMask::AIR) return SpriteAction::DEADSKY; else if (mask & BlockMask::WATER) return SpriteAction::DEADWATER; else return SpriteAction::DEAD; } else { if (mask & BlockMask::AIR) return SpriteAction::STANDSKY; else if (mask & BlockMask::WATER) return SpriteAction::STANDWATER; } } return SpriteAction::STAND; } void Being::setAction(const BeingActionT &restrict action, const int attackId) restrict2 { std::string currentAction = SpriteAction::INVALID; switch (action) { case BeingAction::MOVE: if (mInfo) { playSfx(mInfo->getSound( ItemSoundEvent::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 BeingAction::SIT: currentAction = getSitAction(); if (mInfo) { ItemSoundEvent::Type event; if (currentAction == SpriteAction::SITTOP) event = ItemSoundEvent::SITTOP; else event = ItemSoundEvent::SIT; playSfx(mInfo->getSound(event), nullptr, true, mX, mY); } break; case BeingAction::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 && effectManager) { const int effectId = mInfo->getAttack(attackId)->mEffectId; if (effectId >= 0) { effectManager->triggerDirection(effectId, this, mSpriteDirection); } } } break; case BeingAction::CAST: if (skillDialog) { const SkillInfo *restrict const info = skillDialog->getSkill(attackId); currentAction = getCastAction(info); } break; case BeingAction::HURT: if (mInfo) { playSfx(mInfo->getSound(ItemSoundEvent::HURT), this, false, mX, mY); } break; case BeingAction::DEAD: currentAction = getDeadAction(); if (mInfo) { playSfx(mInfo->getSound(ItemSoundEvent::DIE), this, false, mX, mY); #ifdef EATHENA_SUPPORT if (mType == ActorType::Monster || mType == ActorType::Npc || mType == ActorType::SkillUnit) #else if (mType == ActorType::Monster || mType == ActorType::Npc) #endif { mYDiff = mInfo->getDeadSortOffsetY(); } } break; case BeingAction::STAND: currentAction = getStandAction(); break; case BeingAction::SPAWN: if (mInfo) { playSfx(mInfo->getSound(ItemSoundEvent::SPAWN), nullptr, true, mX, mY); } currentAction = getSpawnAction(); break; case BeingAction::PRESTAND: 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); for_each_badges() { AnimatedSprite *const sprite = mBadges[f]; if (sprite) sprite->play(currentAction); } #ifdef EATHENA_SUPPORT for_each_horses(mDownHorseSprites) (*it)->play(currentAction); for_each_horses(mUpHorseSprites) (*it)->play(currentAction); #endif mAction = action; } if (currentAction != SpriteAction::MOVE && currentAction != SpriteAction::FLY && currentAction != SpriteAction::SWIM) { mActionTime = tick_time; } } void Being::setDirection(const uint8_t direction) restrict2 { 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::Type dir; if (mFaceDirection & BeingDirection::UP) { if (mFaceDirection & BeingDirection::LEFT) dir = SpriteDirection::UPLEFT; else if (mFaceDirection & BeingDirection::RIGHT) dir = SpriteDirection::UPRIGHT; else dir = SpriteDirection::UP; } else if (mFaceDirection & BeingDirection::DOWN) { if (mFaceDirection & BeingDirection::LEFT) dir = SpriteDirection::DOWNLEFT; else if (mFaceDirection & BeingDirection::RIGHT) dir = SpriteDirection::DOWNRIGHT; else dir = SpriteDirection::DOWN; } else if (mFaceDirection & BeingDirection::RIGHT) { dir = SpriteDirection::RIGHT; } else { dir = SpriteDirection::LEFT; } mSpriteDirection = dir; CompoundSprite::setSpriteDirection(dir); if (mEmotionSprite) mEmotionSprite->setSpriteDirection(dir); if (mAnimationEffect) mAnimationEffect->setSpriteDirection(dir); for_each_badges() { AnimatedSprite *const sprite = mBadges[f]; if (sprite) sprite->setSpriteDirection(dir); } #ifdef EATHENA_SUPPORT for_each_horses(mDownHorseSprites) (*it)->setSpriteDirection(dir); for_each_horses(mUpHorseSprites) (*it)->setSpriteDirection(dir); #endif recalcSpritesOrder(); } uint8_t Being::calcDirection() const restrict2 { uint8_t dir = 0; if (mDest.x > mX) dir |= BeingDirection::RIGHT; else if (mDest.x < mX) dir |= BeingDirection::LEFT; if (mDest.y > mY) dir |= BeingDirection::DOWN; else if (mDest.y < mY) dir |= BeingDirection::UP; return dir; } uint8_t Being::calcDirection(const int dstX, const int dstY) const restrict2 { uint8_t dir = 0; if (dstX > mX) dir |= BeingDirection::RIGHT; else if (dstX < mX) dir |= BeingDirection::LEFT; if (dstY > mY) dir |= BeingDirection::DOWN; else if (dstY < mY) dir |= BeingDirection::UP; return dir; } void Being::nextTile() restrict2 { if (mPath.empty()) { mAction = BeingAction::PRESTAND; mPreStandTime = tick_time; 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, getBlockWalkMask())) { setAction(BeingAction::STAND, 0); return; } mActionTime += static_cast(mSpeed / 10); if ((mType != ActorType::Player || mUseDiagonal) && mX != pos.x && mY != pos.y) { mSpeed = static_cast(mWalkSpeed.x * 1.4F); } 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; mFixedOffsetY = height; mNeedPosUpdate = true; setAction(BeingAction::MOVE, 0); } void Being::logic() restrict2 { 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); #ifdef EATHENA_SUPPORT for_each_horses(mDownHorseSprites) (*it)->update(time); for_each_horses(mUpHorseSprites) (*it)->update(time); #endif if (mAnimationEffect) { mAnimationEffect->update(time); if (mAnimationEffect->isTerminated()) delete2(mAnimationEffect) } for_each_badges() { AnimatedSprite *restrict const sprite = mBadges[f]; if (sprite) sprite->update(time); } int frameCount = static_cast(getFrameCount()); switch (mAction) { case BeingAction::STAND: case BeingAction::SIT: case BeingAction::DEAD: case BeingAction::HURT: case BeingAction::SPAWN: case BeingAction::CAST: default: break; case BeingAction::MOVE: { if (static_cast(get_elapsed_time( mActionTime)) >= mSpeed) { nextTile(); } break; } case BeingAction::ATTACK: { if (!mActionTime) break; int curFrame = 0; if (mAttackSpeed) { curFrame = (get_elapsed_time(mActionTime) * frameCount) / mAttackSpeed; } if (this == localPlayer && curFrame >= frameCount) nextTile(); break; } case BeingAction::PRESTAND: { if (get_elapsed_time1(mPreStandTime) > 10) setAction(BeingAction::STAND, 0); break; } } if (mAction == BeingAction::MOVE || mNeedPosUpdate) { const int xOffset = getOffset( BeingDirection::LEFT, BeingDirection::RIGHT); const int yOffset = getOffset( BeingDirection::UP, BeingDirection::DOWN); int offset = xOffset; if (!offset) offset = yOffset; if (!xOffset && !yOffset) mNeedPosUpdate = false; /* mSortOffsetY = (mOldHeight * mapTileSize / 2) + (mOffsetY * mapTileSize / 2) * (mapTileSize - abs(offset)) / mapTileSize - mFixedOffsetY * mapTileSize / 2; const int yOffset2 = yOffset - mSortOffsetY - mFixedOffsetY * mapTileSize / 2; */ const int halfTile = mapTileSize / 2; const float offset2 = static_cast(mOffsetY * abs(offset)) / 2; mSortOffsetY = (mOldHeight - mFixedOffsetY + mOffsetY) * halfTile - offset2; const float yOffset3 = (mY + 1) * mapTileSize + yOffset - (mOldHeight + mOffsetY) * halfTile + offset2; // Update pixel coordinates setPosition(static_cast(mX * mapTileSize + mapTileSize / 2 + xOffset), yOffset3); } if (mEmotionSprite) { mEmotionTime--; if (mEmotionTime == 0) delete2(mEmotionSprite) } ActorSprite::logic(); if (frameCount < 10) frameCount = 10; if (!isAlive() && mSpeed && gameHandler->removeDeadBeings() && static_cast ((static_cast(get_elapsed_time(mActionTime)) / mSpeed)) >= frameCount) { if (mType != ActorType::Player && actorManager) actorManager->destroy(this); } FOR_EACH (std::vector::iterator, it, mPets) { Being *const pet = *it; if (pet) pet->petLogic(); } const SoundInfo *restrict 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() restrict2 { if (!mOwner || !mMap || !mInfo) return; const int time = tick_time; const int thinkTime = mInfo->getThinkTime(); if (abs(static_cast(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(BeingAction::STAND, 0); fixPetSpawnPos(dstX, dstY); setTileCoords(dstX, dstY); mPetAi = true; } else if (!followDist || divX > followDist || divY > followDist) { if (!mPetAi) return; 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 unsigned char blockWalkMask = getBlockWalkMask(); if (!mMap->getWalk(dstX, dstY, blockWalkMask)) { if (dstX != dstX0) { dstX = dstX0; if (!mMap->getWalk(dstX, dstY, blockWalkMask)) dstY = dstY0; } else if (dstY != dstY0) { dstY = dstY0; if (!mMap->getWalk(dstX, dstY, blockWalkMask)) dstX = dstX0; } } if (mX != dstX || mY != dstY) { setPath(mMap->findPath(mX, mY, dstX, dstY, blockWalkMask)); return; } } if (!mPetAi) return; if (mOwner->getCurrentAction() != BeingAction::ATTACK) { if (mAction == BeingAction::ATTACK) setAction(BeingAction::STAND, 0); } else { if (mAction == BeingAction::STAND || mAction == BeingAction::ATTACK) setAction(BeingAction::ATTACK, 0); } if (mAction == BeingAction::STAND || mAction == BeingAction::ATTACK) { int directionType = 0; switch (mOwner->getCurrentAction()) { case BeingAction::STAND: case BeingAction::MOVE: case BeingAction::HURT: case BeingAction::SPAWN: case BeingAction::CAST: case BeingAction::PRESTAND: default: directionType = mInfo->getDirectionType(); break; case BeingAction::SIT: directionType = mInfo->getSitDirectionType(); break; case BeingAction::DEAD: directionType = mInfo->getDeadDirectionType(); break; case BeingAction::ATTACK: directionType = mInfo->getAttackDirectionType(); break; } uint8_t newDir = 0; switch (directionType) { case 0: default: return; case 1: newDir = mOwner->getDirection(); break; case 2: if (dstX > dstX0) newDir |= BeingDirection::LEFT; else if (dstX < dstX0) newDir |= BeingDirection::RIGHT; if (dstY > dstY0) newDir |= BeingDirection::UP; else if (dstY < dstY0) newDir |= BeingDirection::DOWN; break; case 3: if (dstX > dstX0) newDir |= BeingDirection::RIGHT; else if (dstX < dstX0) newDir |= BeingDirection::LEFT; if (dstY > dstY0) newDir |= BeingDirection::DOWN; else if (dstY < dstY0) newDir |= BeingDirection::UP; break; case 4: { const int dstX2 = mOwner->getLastAttackX(); const int dstY2 = mOwner->getLastAttackY(); if (dstX > dstX2) newDir |= BeingDirection::LEFT; else if (dstX < dstX2) newDir |= BeingDirection::RIGHT; if (dstY > dstY2) newDir |= BeingDirection::UP; else if (dstY < dstY2) newDir |= BeingDirection::DOWN; break; } } if (newDir && newDir != getDirection()) setDirection(newDir); } } void Being::drawEmotion(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { const int px = getPixelX() - offsetX - mapTileSize / 2; const int py = getPixelY() - offsetY - mapTileSize * 2 - mapTileSize; if (mAnimationEffect) mAnimationEffect->draw(graphics, px, py); if (mShowBadges && mBadgesCount) { int x; int y; if (mShowBadges == 2 && mDispName && gui) { Font *restrict const font = gui->getFont(); x = mDispName->getX() - offsetX + mDispName->getWidth(); y = mDispName->getY() - offsetY - font->getHeight(); } else if (mShowBadges == 3 && mDispName && gui) { x = px + 8 - mBadgesCount * 8; y = mDispName->getY() - offsetY; } else { x = px + 8 - mBadgesCount * 8; y = py; } for_each_badges() { AnimatedSprite *restrict const sprite = mBadges[f]; if (sprite) { sprite->draw(graphics, x, y); x += 16; } } } if (mEmotionSprite) mEmotionSprite->draw(graphics, px, py); } void Being::drawSpeech(const int offsetX, const int offsetY) restrict2 { if (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 && mSpeechBubble->isVisibleLocal()) mSpeechBubble->setVisible(Visible_false); mSpeech.clear(); } else if (mSpeechTime > 0 && (speech == BeingSpeech::NAME_IN_BUBBLE || speech == BeingSpeech::NO_NAME_IN_BUBBLE)) { delete2(mText) if (mSpeechBubble) { mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), py - getHeight() - (mSpeechBubble->getHeight())); mSpeechBubble->setVisible(Visible_true); mSpeechBubble->requestMoveToBackground(); } } else if (mSpeechTime > 0 && speech == BeingSpeech::TEXT_OVERHEAD) { if (mSpeechBubble) mSpeechBubble->setVisible(Visible_false); if (!mText && userPalette) { mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), Graphics::CENTER, &theme->getColor(ThemeColorId::BUBBLE_TEXT, 255), Speech_true); } } else if (speech == BeingSpeech::NO_SPEECH) { if (mSpeechBubble) mSpeechBubble->setVisible(Visible_false); delete2(mText) } } int Being::getOffset(const signed char pos, const signed char neg) const restrict2 { // Check whether we're walking in the requested direction if (mAction != BeingAction::MOVE || !(mDirection & (pos | neg))) return 0; int offset = 0; if (mMap) { const int time = get_elapsed_time(mActionTime); offset = (pos == BeingDirection::LEFT && neg == BeingDirection::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() restrict2 { 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 == ActorType::Monster) offsetY += - getHeight() - mDispName->getHeight(); mDispName->adviseXY(offsetX, offsetY, mMoveNames); } void Being::optionChanged(const std::string &restrict value) restrict2 { if (mType == ActorType::Player && value == "visiblenames") setShowName(config.getBoolValue("visiblenames")); } void Being::flashName(const int time) restrict2 { if (mDispName) mDispName->flash(time); } std::string Being::getGenderSignWithSpace() const restrict2 { const std::string &restrict str = getGenderSign(); if (str.empty()) return str; else return std::string(" ").append(str); } std::string Being::getGenderSign() const restrict2 { std::string str; if (mShowGender) { if (getGender() == Gender::FEMALE) str = "\u2640"; else if (getGender() == Gender::MALE) str = "\u2642"; } if (mShowPlayersStatus && !mShowBadges) { 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() restrict2 { if (mName.empty()) return; delete2(mDispName); if (mHideErased && player_relations.getRelation(mName) == Relation::ERASED) return; std::string displayName(mName); if (mType != ActorType::Monster && (mShowGender || mShowLevel)) { displayName.append(" "); if (mShowLevel && getLevel() != 0) displayName.append(toString(getLevel())); displayName.append(getGenderSign()); } if (mType == ActorType::Monster) { if (config.getBoolValue("showMonstersTakedDamage")) displayName.append(", ").append(toString(getDamageTaken())); } Font *font = nullptr; if (localPlayer && localPlayer->getTarget() == this && mType != ActorType::Monster) { font = boldFont; } else if (mType == ActorType::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::setDefaultNameColor(const UserColorIdT defaultColor) restrict2 { switch (mTeamId) { case 0: default: mNameColor = &userPalette->getColor(defaultColor); break; case 1: mNameColor = &userPalette->getColor(UserColorId::TEAM1); break; case 2: mNameColor = &userPalette->getColor(UserColorId::TEAM2); break; case 3: mNameColor = &userPalette->getColor(UserColorId::TEAM3); break; } } void Being::updateColors() { if (userPalette) { if (mType == ActorType::Monster) { setDefaultNameColor(UserColorId::MONSTER); mTextColor = &userPalette->getColor(UserColorId::MONSTER); } else if (mType == ActorType::Npc) { setDefaultNameColor(UserColorId::NPC); mTextColor = &userPalette->getColor(UserColorId::NPC); } #ifdef EATHENA_SUPPORT else if (mType == ActorType::Pet) { setDefaultNameColor(UserColorId::PET); mTextColor = &userPalette->getColor(UserColorId::PET); } else if (mType == ActorType::Homunculus) { setDefaultNameColor(UserColorId::HOMUNCULUS); mTextColor = &userPalette->getColor(UserColorId::HOMUNCULUS); } else if (mType == ActorType::SkillUnit) { setDefaultNameColor(UserColorId::SKILLUNIT); mTextColor = &userPalette->getColor(UserColorId::SKILLUNIT); } #endif #ifdef TMWA_SUPPORT else if (mType == ActorType::LocalPet) { setDefaultNameColor(UserColorId::PET); mTextColor = &userPalette->getColor(UserColorId::PET); } #endif else if (this == localPlayer) { mNameColor = &userPalette->getColor(UserColorId::SELF); mTextColor = &theme->getColor(ThemeColorId::PLAYER, 255); } else { mTextColor = &theme->getColor(ThemeColorId::PLAYER, 255); if (player_relations.getRelation(mName) != Relation::ERASED) mErased = false; else mErased = true; if (mIsGM) { mTextColor = &userPalette->getColor(UserColorId::GM); mNameColor = &userPalette->getColor(UserColorId::GM); } else if (mEnemy) { mNameColor = &userPalette->getColor(UserColorId::ENEMY); } else if (mParty && localPlayer && mParty == localPlayer->getParty()) { mNameColor = &userPalette->getColor(UserColorId::PARTY); } else if (localPlayer && getGuild() && getGuild() == localPlayer->getGuild()) { mNameColor = &userPalette->getColor(UserColorId::GUILD); } else if (player_relations.getRelation(mName) == Relation::FRIEND) { mNameColor = &userPalette->getColor(UserColorId::FRIEND); } else if (player_relations.getRelation(mName) == Relation::DISREGARDED || player_relations.getRelation(mName) == Relation::BLACKLISTED) { mNameColor = &userPalette->getColor(UserColorId::DISREGARDED); } else if (player_relations.getRelation(mName) == Relation::IGNORED || player_relations.getRelation(mName) == Relation::ENEMY2) { mNameColor = &userPalette->getColor(UserColorId::IGNORED); } else if (player_relations.getRelation(mName) == Relation::ERASED) { mNameColor = &userPalette->getColor(UserColorId::ERASED); } else { setDefaultNameColor(UserColorId::PC); } } if (mDispName) mDispName->setColor(mNameColor); } } void Being::updateSprite(const unsigned int slot, const int id, std::string color, const ItemColor colorId, const bool isWeapon, const bool isTempSprite) restrict2 { if (!charServerHandler || slot >= charServerHandler->maxSprite()) return; if (slot >= static_cast(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 ItemColor colorId, const bool isWeapon, const bool isTempSprite) restrict2 { if (!charServerHandler || slot >= charServerHandler->maxSprite()) return; if (slot >= static_cast(size())) ensureSize(slot + 1); if (slot >= static_cast(mSpriteIDs.size())) mSpriteIDs.resize(slot + 1, 0); if (slot >= static_cast(mSpriteColors.size())) mSpriteColors.resize(slot + 1, ""); if (slot >= static_cast(mSpriteColorsIds.size())) mSpriteColorsIds.resize(slot + 1, ItemColor_one); // 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 == ActorType::Player) { const BeingId pet = fromInt(info.getPet(), BeingId); if (pet != BeingId_zero) removePet(pet); } removeItemParticles(id1); } } else { const ItemInfo &info = ItemDB::get(id); const std::string &restrict filename = info.getSprite( mGender, mSubType); int lastTime = 0; int startTime = 0; AnimatedSprite *restrict equipmentSprite = nullptr; if (!isTempSprite && mType == ActorType::Player) { const BeingId pet = fromInt(info.getPet(), BeingId); if (pet != BeingId_zero) 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()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); if (isWeapon) mEquippedWeapon = &ItemDB::get(id); setAction(mAction, 0); if (equipmentSprite) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } 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) restrict2 { setSprite(slot, id, mSpriteColors[slot]); } void Being::setSpriteColor(const unsigned int slot, const std::string &restrict color) restrict2 { setSprite(slot, mSpriteIDs[slot], color); } void Being::setHairStyle(const unsigned int slot, const int id) restrict2 { // dumpSprites(); setSprite(slot, id, ItemDB::get(id).getDyeColorsString(mHairColor)); // dumpSprites(); } void Being::setHairColor(const unsigned int slot, const ItemColor color) restrict2 { mHairColor = color; setSprite(slot, mSpriteIDs[slot], ItemDB::get( getSpriteID(slot)).getDyeColorsString(color)); } void Being::dumpSprites() const restrict2 { 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(), toInt(*it3, int)); } } 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, BeingTypeId_zero) != paths.getStringValue("spriteErrorFile")) { hairstyles ++; } mNumberOfHairstyles = hairstyles; int races = 100; while (ItemDB::get(-races).getSprite(Gender::MALE, BeingTypeId_zero) != paths.getStringValue("spriteErrorFile")) { races ++; } mNumberOfRaces = races - 100; } void Being::updateName() restrict2 { 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 = fromBool(config.getBoolValue("moveNames"), Move); mUseDiagonal = config.getBoolValue("useDiagonalSpeed"); mShowBadges = static_cast(config.getIntValue("showBadges")); mUpdateConfigTime = cur_time; } BLOCK_END("Being::reReadConfig") } bool Being::updateFromCache() restrict2 { const BeingCacheEntry *restrict 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()); setTeamId(entry->getTeamId()); mAdvanced = entry->isAdvanced(); if (mAdvanced) { const int flags = entry->getFlags(); if (serverFeatures && !serverFeatures->haveVending()) mShop = ((flags & BeingFlag::SHOP) != 0); mAway = ((flags & BeingFlag::AWAY) != 0); mInactive = ((flags & BeingFlag::INACTIVE) != 0); if (mShop || mAway || mInactive) updateName(); } else { if (serverFeatures && !serverFeatures->haveVending()) mShop = false; mAway = false; mInactive = false; } showShopBadge(mShop); showInactiveBadge(mInactive); showAwayBadge(mAway); updateAwayEffect(); if (mType == ActorType::Player || mTeamId) updateColors(); return true; } return false; } void Being::addToCache() const restrict2 { if (localPlayer == 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()); entry->setTeamId(getTeamId()); if (isAdvanced()) { int flags = 0; if (serverFeatures && !serverFeatures->haveVending() && mShop) flags += BeingFlag::SHOP; if (mAway) flags += BeingFlag::AWAY; if (mInactive) flags += BeingFlag::INACTIVE; entry->setFlags(flags); } else { entry->setFlags(0); } } BeingCacheEntry* Being::getCacheEntry(const BeingId 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 GenderT gender) restrict2 { if (gender != mGender) { mGender = gender; // Reload all subsprites for (unsigned int i = 0; i < static_cast(mSpriteIDs.size()); i++) { if (mSpriteIDs.at(i) != 0) setSprite(i, mSpriteIDs.at(i), mSpriteColors.at(i)); } updateName(); } } void Being::showGmBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Gm]); if (show && mIsGM && mShowBadges) { const std::string gmBadge = paths.getStringValue("gmbadge"); if (!gmBadge.empty()) { mBadges[BadgeIndex::Gm] = AnimatedSprite::load( paths.getStringValue("badges") + gmBadge); } } updateBadgesCount(); } void Being::setGM(const bool gm) restrict2 { if (mIsGM != gm) { mIsGM = gm; showGmBadge(mIsGM); updateColors(); } } void Being::talkTo() const restrict2 { if (!npcHandler) return; if (!PacketLimiter::limitPackets(PacketType::PACKET_NPC_TALK)) { // using workaround... if (playerHandler && PacketLimiter::limitPackets(PacketType::PACKET_ATTACK)) { playerHandler->attack(mId, Keep_false); } return; } npcHandler->talk(mId); } void Being::draw(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { if (!mErased) { const int px = getActorX() + offsetX; const int py = getActorY() + offsetY; #ifdef EATHENA_SUPPORT if (mHorseInfo) { for_each_horses(mDownHorseSprites) { (*it)->draw(graphics, px + mHorseInfo->downOffsetX, py + mHorseInfo->downOffsetY); } ActorSprite::draw1(graphics, px, py); drawSpriteAt(graphics, px, py); for_each_horses(mUpHorseSprites) { (*it)->draw(graphics, px + mHorseInfo->upOffsetX, py + mHorseInfo->upOffsetY); } } else { ActorSprite::draw1(graphics, px, py); drawSpriteAt(graphics, px, py); } #else ActorSprite::draw1(graphics, px, py); drawSpriteAt(graphics, px, py); #endif } } void Being::drawSprites(Graphics *restrict const graphics, int posX, int posY) const restrict2 { const int sz = getNumberOfLayers(); for (int f = 0; f < sz; f ++) { const int rSprite = mSpriteHide[mSpriteRemap[f]]; if (rSprite == 1) continue; Sprite *restrict const sprite = getSprite(mSpriteRemap[f]); if (sprite) { sprite->setAlpha(mAlpha); sprite->draw(graphics, posX, posY); } } } void Being::drawSpritesSDL(Graphics *restrict const graphics, int posX, int posY) const restrict2 { const unsigned int sz = static_cast(size()); for (unsigned int f = 0; f < sz; f ++) { const int rSprite = mSpriteHide[mSpriteRemap[f]]; if (rSprite == 1) continue; const Sprite *restrict const sprite = getSprite(mSpriteRemap[f]); if (sprite) sprite->draw(graphics, posX, posY); } } void Being::drawSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (!userPalette) { CompoundSprite::draw(graphics, x, y); return; } if (mHighlightMapPortals && mMap && mSubType == fromInt(45, BeingTypeId) && !mMap->getHasWarps()) { graphics->setColor(userPalette-> getColorWithAlpha(UserColorId::PORTAL_HIGHLIGHT)); graphics->fillRectangle(Rect(x, y, mapTileSize, mapTileSize)); if (mDrawHotKeys && !mName.empty()) { const Color &color = userPalette->getColor(UserColorId::BEING); gui->getFont()->drawString(graphics, color, color, mName, x, y); } } if (mHighlightMonsterAttackRange && mType == ActorType::Monster && isAlive()) { int attackRange; if (mAttackRange) attackRange = mapTileSize * mAttackRange; else attackRange = mapTileSize; graphics->setColor(userPalette->getColorWithAlpha( UserColorId::MONSTER_ATTACK_RANGE)); graphics->fillRectangle(Rect( x - attackRange, y - attackRange, 2 * attackRange + mapTileSize, 2 * attackRange + mapTileSize)); } CompoundSprite::draw(graphics, x, y); if (mShowMobHP && mInfo && localPlayer && localPlayer->getTarget() == this && mType == ActorType::Monster) { // show hp bar here int maxHP = mMaxHP; if (!maxHP) maxHP = mInfo->getMaxHP(); drawHpBar(graphics, maxHP, mHP, mDamageTaken, UserColorId::MONSTER_HP, UserColorId::MONSTER_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } if (mShowOwnHP && mInfo && localPlayer == this && mAction != BeingAction::DEAD) { drawHpBar(graphics, PlayerInfo::getAttribute(Attributes::MAX_HP), PlayerInfo::getAttribute(Attributes::HP), 0, UserColorId::PLAYER_HP, UserColorId::PLAYER_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } void Being::drawHpBar(Graphics *restrict const graphics, const int maxHP, const int hp, const int damage, const UserColorIdT color1, const UserColorIdT color2, const int x, const int y, const int width, const int height) const restrict2 { 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 (!serverFeatures->haveServerHp()) { // old servers if ((!damage && (this != localPlayer || 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 { 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) restrict2 { mHP = hp; if (mMaxHP < mHP) mMaxHP = mHP; if (mType == ActorType::Monster) updatePercentHP(); } void Being::setMaxHP(const int hp) restrict2 { mMaxHP = hp; if (mMaxHP < mHP) mMaxHP = mHP; } void Being::resetCounters() restrict2 { mMoveTime = 0; mAttackTime = 0; mTalkTime = 0; mOtherTime = 0; mTestTime = cur_time; } void Being::recalcSpritesOrder() restrict2 { if (!mEnableReorderSprites) return; // logger->log("recalcSpritesOrder"); const size_t sz = size(); if (sz < 1) return; std::vector slotRemap; IntMap 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 == BeingAction::DEAD) dir = 9; const unsigned int hairSlot = charServerHandler->hairSprite(); for (size_t slot = 0; slot < sz; slot ++) { oldHide[slot] = mSpriteHide[slot]; mSpriteHide[slot] = 0; updatedSprite[slot] = false; } const size_t spriteIdSize = mSpriteIDs.size(); for (size_t slot = 0; slot < sz; slot ++) { slotRemap.push_back(static_cast(slot)); if (spriteIdSize <= slot) continue; const int id = mSpriteIDs[slot]; if (!id) continue; const ItemInfo &info = ItemDB::get(id); if (info.isRemoveSprites()) { const SpriteToItemMap *restrict const spriteToItems = info.getSpriteToItemReplaceMap(dir); if (spriteToItems) { FOR_EACHP (SpriteToItemMapCIter, itr, spriteToItems) { const int remSprite = itr->first; const IntMap &restrict itemReplacer = itr->second; if (remSprite >= 0) { // slot known if (itemReplacer.empty()) { mSpriteHide[remSprite] = 1; } else if (mSpriteHide[remSprite] != 1) { IntMapCIter 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], ItemColor_one, false, true); } else { setSprite(remSprite, repIt->second, ItemDB::get(repIt->second) .getDyeColorsString(mHairColor), ItemColor_one, false, true); } updatedSprite[remSprite] = true; } } } } else { // slot unknown. Search for real slot, this can be slow FOR_EACH (IntMapCIter, repIt, itemReplacer) { for (unsigned int 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], ItemColor_one, false, true); } else { setSprite(slot2, repIt->second, ItemDB::get(repIt->second) .getDyeColorsString( mHairColor), ItemColor_one, 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 int 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 IntMapCIter 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 int 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], ItemColor_one, 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], ItemColor_one, false, true); } } } } int Being::searchSlotValue(const std::vector &restrict slotRemap, const int val) const restrict2 { const size_t sz = size(); for (size_t slot = 0; slot < sz; slot ++) { if (slotRemap[slot] == val) return static_cast(slot); } return getNumberOfLayers() - 1; } void Being::searchSlotValueItr(std::vector::iterator &restrict it, int &restrict idx, std::vector &restrict 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) restrict2 { if (amount > 0) { if (!mMinHit || amount < mMinHit) mMinHit = amount; if (amount != mCriticalHit && (!mMaxHit || amount > mMaxHit)) mMaxHit = amount; } } Equipment *Being::getEquipment() restrict2 { Equipment *restrict const eq = new Equipment(); Equipment::Backend *restrict const bk = new BeingEquipBackend(this); eq->setBackend(bk); return eq; } void Being::undressItemById(const int id) restrict2 { 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() restrict2 { if (mGotComment || mName.empty()) return; mGotComment = true; mComment = loadComment(mName, mType); } std::string Being::loadComment(const std::string &restrict name, const ActorTypeT &restrict type) { std::string str; switch (type) { case ActorType::Player: str = settings.usersDir; break; case ActorType::Npc: str = settings.npcsDir; break; case ActorType::Unknown: case ActorType::Monster: case ActorType::FloorItem: case ActorType::Portal: case ActorType::LocalPet: case ActorType::Avatar: #ifdef EATHENA_SUPPORT case ActorType::Mercenary: case ActorType::Homunculus: case ActorType::Pet: case ActorType::SkillUnit: #endif default: return ""; } str.append(stringToHexPath(name)).append("/comment.txt"); if (Files::existsLocal(str)) { StringVect lines; Files::loadTextFileLocal(str, lines); if (lines.size() >= 2) return lines[1]; } return std::string(); } void Being::saveComment(const std::string &restrict name, const std::string &restrict comment, const ActorTypeT &restrict type) { std::string dir; switch (type) { case ActorType::Player: dir = settings.usersDir; break; case ActorType::Npc: dir = settings.npcsDir; break; case ActorType::Monster: case ActorType::FloorItem: case ActorType::Portal: case ActorType::LocalPet: case ActorType::Avatar: case ActorType::Unknown: #ifdef EATHENA_SUPPORT case ActorType::Pet: case ActorType::Mercenary: case ActorType::Homunculus: case ActorType::SkillUnit: #endif default: return; } dir.append(stringToHexPath(name)); Files::saveTextFile(dir, "comment.txt", (name + "\n").append(comment)); } void Being::setState(const uint8_t state) restrict2 { const bool shop = ((state & BeingFlag::SHOP) != 0); const bool away = ((state & BeingFlag::AWAY) != 0); const bool inactive = ((state & BeingFlag::INACTIVE) != 0); const bool needUpdate = (shop != mShop || away != mAway || inactive != mInactive); if (!serverFeatures->haveVending()) mShop = shop; mAway = away; mInactive = inactive; updateAwayEffect(); showShopBadge(mShop); showInactiveBadge(mInactive); showAwayBadge(mAway); if (needUpdate) { if (shop || away || inactive) mAdvanced = true; updateName(); addToCache(); } } void Being::setEmote(const uint8_t emotion, const int emote_time) restrict2 { if ((emotion & BeingFlag::SPECIAL) == BeingFlag::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 *restrict const sprite = info->sprites.front(); if (sprite) { mEmotionSprite = AnimatedSprite::clone(sprite->sprite); if (mEmotionSprite) mEmotionTime = info->time; else mEmotionTime = emote_time; } const int effectId = info->effectId; if (effectId >= 0) { effectManager->trigger(effectId, this); } } } if (mEmotionSprite) { mEmotionSprite->play(mSpriteAction); mEmotionSprite->setSpriteDirection(mSpriteDirection); } else { mEmotionTime = 0; } } } void Being::updatePercentHP() restrict2 { if (!mMaxHP) return; BLOCK_START("Being::updatePercentHP") if (mHP) { const unsigned num = mHP * 100 / mMaxHP; if (num != mNumber) { mNumber = num; if (updateNumber(mNumber)) setAction(mAction, 0); } } BLOCK_END("Being::updatePercentHP") } uint8_t Being::genderToInt(const GenderT sex) { switch (sex) { case Gender::FEMALE: case Gender::UNSPECIFIED: default: return 0; case Gender::MALE: return 1; case Gender::OTHER: return 3; } } GenderT 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 restrict2 { if (slot < 0 || static_cast(slot) >= mSpriteIDs.size()) return -1; return mSpriteIDs[slot]; } ItemColor Being::getSpriteColor(const int slot) const restrict2 { if (slot < 0 || static_cast(slot) >= mSpriteColorsIds.size()) return ItemColor_one; return mSpriteColorsIds[slot]; } void Being::addAfkEffect() restrict2 { addSpecialEffect(mAwayEffect); } void Being::removeAfkEffect() restrict2 { removeSpecialEffect(); } void Being::addSpecialEffect(const int effect) restrict2 { if (effectManager && Particle::enabled && !mSpecialParticle && effect != -1) { mSpecialParticle = effectManager->triggerReturn(effect, this); } } void Being::removeSpecialEffect() restrict2 { if (effectManager && mSpecialParticle) { mChildParticleEffects.removeLocally(mSpecialParticle); mSpecialParticle = nullptr; } delete2(mAnimationEffect); } void Being::updateAwayEffect() restrict2 { if (mAway) addAfkEffect(); else removeAfkEffect(); } void Being::addEffect(const std::string &restrict name) restrict2 { delete mAnimationEffect; mAnimationEffect = AnimatedSprite::load( paths.getStringValue("sprites") + name); } void Being::addPet(const BeingId id) restrict2 { if (!actorManager || !config.getBoolValue("usepets")) return; Being *restrict const pet = findChildPet(id); if (pet) { pet->incUsage(); return; } Being *const being = actorManager->createBeing( id, ActorType::LocalPet, BeingTypeId_zero); if (being) { being->setOwner(this); mPets.push_back(being); int dstX = mX; int dstY = mY; being->fixPetSpawnPos(dstX, dstY); being->setTileCoords(dstX, dstY); } } Being *Being::findChildPet(const BeingId id) restrict2 { FOR_EACH (std::vector::iterator, it, mPets) { Being *restrict const pet = *it; if (pet && pet->mId == id) return pet; } return nullptr; } void Being::removePet(const BeingId id) restrict2 { if (!actorManager) return; FOR_EACH (std::vector::iterator, it, mPets) { Being *restrict 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() restrict2 { FOR_EACH (std::vector::iterator, it, mPets) { Being *restrict const pet = *it; if (pet) { pet->setOwner(nullptr); actorManager->erase(pet); delete pet; } } mPets.clear(); } void Being::updatePets() restrict2 { removeAllPets(); FOR_EACH (std::vector::const_iterator, it, mSpriteIDs) { const int id = *it; if (!id) continue; const ItemInfo &restrict info = ItemDB::get(id); const BeingId pet = fromInt(info.getPet(), BeingId); if (pet != BeingId_zero) addPet(pet); } } void Being::unassignPet(const Being *restrict const pet1) restrict2 { FOR_EACH (std::vector::iterator, it, mPets) { if (*it == pet1) { mPets.erase(it); return; } } } void Being::fixPetSpawnPos(int &restrict dstX, int &restrict dstY) const { if (!mInfo || !mOwner) return; int offsetX1; int offsetY1; switch (mOwner->getCurrentAction()) { case BeingAction::SIT: offsetX1 = mInfo->getSitOffsetX(); offsetY1 = mInfo->getSitOffsetY(); break; case BeingAction::MOVE: offsetX1 = mInfo->getMoveOffsetX(); offsetY1 = mInfo->getMoveOffsetY(); break; case BeingAction::DEAD: offsetX1 = mInfo->getDeadOffsetX(); offsetY1 = mInfo->getDeadOffsetY(); break; case BeingAction::ATTACK: offsetX1 = mInfo->getAttackOffsetX(); offsetY1 = mInfo->getAttackOffsetY(); break; case BeingAction::SPAWN: case BeingAction::HURT: case BeingAction::STAND: case BeingAction::PRESTAND: case BeingAction::CAST: default: offsetX1 = mInfo->getTargetOffsetX(); offsetY1 = mInfo->getTargetOffsetY(); break; } int offsetX = offsetX1; int offsetY = offsetY1; switch (mOwner->getDirection()) { case BeingDirection::LEFT: offsetX = -offsetY1; offsetY = offsetX1; break; case BeingDirection::RIGHT: offsetX = offsetY1; offsetY = -offsetX1; break; case BeingDirection::UP: offsetY = -offsetY; offsetX = -offsetX; break; default: case BeingDirection::DOWN: break; } dstX += offsetX; dstY += offsetY; if (mMap) { if (!mMap->getWalk(dstX, dstY, getBlockWalkMask())) { dstX = mOwner->getTileX(); dstY = mOwner->getTileY(); } } } void Being::playSfx(const SoundInfo &sound, Being *restrict const being, const bool main, const int x, const int y) const restrict2 { BLOCK_START("Being::playSfx") 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); } BLOCK_END("Being::playSfx") } void Being::setLook(const uint16_t look) restrict2 { if (mType == ActorType::Player) setSubtype(mSubType, look); } void Being::setTileCoords(const int x, const int y) restrict2 { mX = x; mY = y; if (mMap) { mOffsetY = 0; mFixedOffsetY = mOffsetY; mOldHeight = mMap->getHeightOffset(mX, mY); mNeedPosUpdate = true; } } void Being::setMap(Map *restrict const map) restrict2 { ActorSprite::setMap(map); if (mMap) { mOffsetY = mMap->getHeightOffset(mX, mY); mFixedOffsetY = mOffsetY; mOldHeight = 0; mNeedPosUpdate = true; } } void Being::removeAllItemsParticles() restrict2 { FOR_EACH (SpriteParticleInfoIter, it, mSpriteParticles) delete (*it).second; mSpriteParticles.clear(); } void Being::addItemParticles(const int id, const SpriteDisplay &restrict display) restrict2 { SpriteParticleInfoIter it = mSpriteParticles.find(id); ParticleInfo *restrict pi = nullptr; if (it == mSpriteParticles.end()) { pi = new ParticleInfo(); mSpriteParticles[id] = pi; } else { pi = (*it).second; } if (!pi || !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) restrict2 { SpriteParticleInfoIter it = mSpriteParticles.find(id); if (it == mSpriteParticles.end()) return; ParticleInfo *restrict 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() restrict2 { FOR_EACH (SpriteParticleInfoIter, it, mSpriteParticles) { ParticleInfo *restrict 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); } } } } void Being::setTeamId(const uint16_t teamId) restrict2 { if (mTeamId != teamId) { mTeamId = teamId; showTeamBadge(mTeamId != 0); updateColors(); } } void Being::showTeamBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Team]); if (show && mTeamId && mShowBadges) { const std::string name = paths.getStringValue("badges") + paths.getStringValue(strprintf("team%dbadge", mTeamId)); if (!name.empty()) mBadges[BadgeIndex::Team] = AnimatedSprite::load(name); } updateBadgesCount(); } void Being::showBadges(const bool show) restrict2 { showTeamBadge(show); showGuildBadge(show); showGmBadge(show); showPartyBadge(show); showNameBadge(show); showShopBadge(show); showInactiveBadge(show); showAwayBadge(show); } void Being::showPartyBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Party]); if (show && !mPartyName.empty() && mShowBadges) { const std::string badge = BadgesDB::getPartyBadge(mPartyName); if (!badge.empty()) { mBadges[BadgeIndex::Party] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } updateBadgesCount(); } void Being::setPartyName(const std::string &restrict name) restrict2 { if (mPartyName != name) { mPartyName = name; showPartyBadge(!mPartyName.empty()); } } void Being::showShopBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Shop]); if (show && mShop && mShowBadges) { const std::string badge = paths.getStringValue("shopbadge"); if (!badge.empty()) { mBadges[BadgeIndex::Shop] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } updateBadgesCount(); } void Being::showInactiveBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Inactive]); if (show && mInactive && mShowBadges) { const std::string badge = paths.getStringValue("inactivebadge"); if (!badge.empty()) { mBadges[BadgeIndex::Inactive] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } updateBadgesCount(); } void Being::showAwayBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Away]); if (show && mAway && mShowBadges) { const std::string badge = paths.getStringValue("awaybadge"); if (!badge.empty()) { mBadges[BadgeIndex::Away] = AnimatedSprite::load( paths.getStringValue("badges") + badge); } } updateBadgesCount(); } void Being::updateBadgesCount() restrict2 { mBadgesCount = 0; for_each_badges() { if (mBadges[f]) mBadgesCount ++; } } #ifdef EATHENA_SUPPORT void Being::setChat(ChatObject *restrict const obj) restrict2 { delete mChat; mChat = obj; } void Being::setSellBoard(const std::string &restrict text) restrict2 { mShop = !text.empty() || !mBuyBoard.empty(); mSellBoard = text; updateName(); showShopBadge(mShop); } void Being::setBuyBoard(const std::string &restrict text) restrict2 { mShop = !text.empty() || !mSellBoard.empty(); mBuyBoard = text; updateName(); showShopBadge(mShop); } #endif void Being::enableShop(const bool b) restrict2 { mShop = b; updateName(); showShopBadge(mShop); } bool Being::isBuyShopEnabled() const restrict2 { #ifdef EATHENA_SUPPORT return mShop && (!serverFeatures->haveVending() || !mBuyBoard.empty()); #else return mShop; #endif } bool Being::isSellShopEnabled() const restrict2 { #ifdef EATHENA_SUPPORT return mShop && (!serverFeatures->haveVending() || !mSellBoard.empty()); #else return mShop; #endif } void Being::serverRemove() restrict2 { // remove some flags what can survive player remove and next visible mTrickDead = false; } #ifdef EATHENA_SUPPORT void Being::removeHorse() restrict2 { delete_all(mUpHorseSprites); mUpHorseSprites.clear(); delete_all(mDownHorseSprites); mDownHorseSprites.clear(); } void Being::setRiding(const bool b) restrict2 { if (serverFeatures->haveExtendedRiding()) return; if (b == (mHorseId != 0)) return; if (b) setHorse(1); else setHorse(0); } void Being::setHorse(const int horseId) restrict2 { if (mHorseId == horseId) return; mHorseId = horseId; setAction(mAction, 0); removeHorse(); if (mHorseId != 0) { mHorseInfo = HorseDB::get(horseId); if (mHorseInfo) { FOR_EACH (SpriteRefs, it, mHorseInfo->downSprites) { SpriteReference *restrict const ref = *it; AnimatedSprite *const sprite = AnimatedSprite::load( ref->sprite, ref->variant); mDownHorseSprites.push_back(sprite); sprite->play(mSpriteAction); sprite->setSpriteDirection(mSpriteDirection); } FOR_EACH (SpriteRefs, it, mHorseInfo->upSprites) { SpriteReference *restrict const ref = *it; AnimatedSprite *sprite = AnimatedSprite::load( ref->sprite, ref->variant); mUpHorseSprites.push_back(sprite); sprite->play(mSpriteAction); sprite->setSpriteDirection(mSpriteDirection); } } } else { mHorseInfo = nullptr; } } void Being::setTrickDead(const bool b) restrict2 { if (b != mTrickDead) { mTrickDead = b; setAction(mAction, 0); } } void Being::setSpiritBalls(const unsigned int balls) restrict2 { if (!Particle::enabled) { mSpiritBalls = balls; return; } if (balls > mSpiritBalls) { const int effectId = paths.getIntValue("spiritEffectId"); if (effectId != -1) addSpiritBalls(balls - mSpiritBalls, effectId); } else if (balls < mSpiritBalls) { removeSpiritBalls(mSpiritBalls - balls); } mSpiritBalls = balls; } void Being::addSpiritBalls(const unsigned int balls, const int effectId) restrict2 { if (!effectManager) return; for (unsigned int f = 0; f < balls; f ++) { Particle *particle = effectManager->triggerReturn( effectId, this); mSpiritParticles.push_back(particle); } } void Being::removeSpiritBalls(const unsigned int balls) restrict2 { if (!particleEngine) return; for (unsigned int f = 0; f < balls && !mSpiritParticles.empty(); f ++) { Particle *restrict const particle = mSpiritParticles.back(); mChildParticleEffects.removeLocally(particle); mSpiritParticles.pop_back(); } } #endif