/* * The ManaPlus Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * Copyright (C) 2011-2020 The ManaPlus Developers * Copyright (C) 2020-2023 The ManaVerse 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 "itemcolormanager.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/castingeffect.h" #include "being/localplayer.h" #include "being/playerinfo.h" #include "being/playerrelations.h" #include "being/homunculusinfo.h" #include "being/mercenaryinfo.h" #include "const/utils/timer.h" #include "const/resources/spriteaction.h" #include "enums/being/beingdirection.h" #include "enums/resources/map/blockmask.h" #include "fs/files.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/homunculushandler.h" #include "net/mercenaryhandler.h" #include "net/net.h" #include "net/npchandler.h" #include "net/packetlimiter.h" #include "net/playerhandler.h" #include "net/serverfeatures.h" #include "particle/particleinfo.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/groupdb.h" #include "resources/db/elementaldb.h" #include "resources/db/emotedb.h" #include "resources/db/homunculusdb.h" #include "resources/db/horsedb.h" #include "resources/db/languagedb.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/image/image.h" #include "resources/item/item.h" #include "resources/map/map.h" #include "resources/skill/skilldata.h" #include "resources/skill/skillinfo.h" #include "resources/sprite/animatedsprite.h" #include "gui/widgets/createwidget.h" #include "utils/checkutils.h" #include "utils/delete2.h" #include "utils/foreach.h" #include "utils/gettext.h" #include "utils/likely.h" #include "utils/stdmove.h" #include "utils/timer.h" #include "debug.h" const unsigned int CACHE_SIZE = 50; time_t 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; int Being::mPlayerSpeedAdjustment = 0; BadgeDrawType::Type Being::mShowBadges = BadgeDrawType::Top; int Being::mAwayEffect = -1; VisibleNamePos::Type Being::mVisibleNamePos = VisibleNamePos::Bottom; 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) : ActorSprite(id), mNextSound(), mInfo(BeingInfo::unknown), mEmotionSprite(nullptr), mAnimationEffect(nullptr), mCastingEffect(nullptr), mBadges(), mSpriteAction(SpriteAction::STAND), mName(), mExtName(), mRaceName(), mPartyName(), mGuildName(), mClanName(), mSpeech(), mDispName(nullptr), mNameColor(nullptr), mEquippedWeapon(nullptr), mPath(), mText(nullptr), mTextColor(nullptr), mDest(), mSlots(), mSpriteParticles(), mGuilds(), mParty(nullptr), mActionTime(0), mEmotionTime(0), mSpeechTime(0), mAttackSpeed(350), mLevel(0), mGroupId(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 != nullptr ? playerHandler->getDefaultWalkSpeed() : 1), mSpeed(playerHandler != nullptr ? playerHandler->getDefaultWalkSpeed() : 1), mIp(), mSpriteRemap(new int[20]), mSpriteHide(new int[20]), mSpriteDraw(new int[20]), mComment(), mBuyBoard(), mSellBoard(), mOwner(nullptr), mSpecialParticle(nullptr), mChat(nullptr), mHorseInfo(nullptr), mDownHorseSprites(), mUpHorseSprites(), mSpiritParticles(), mX(0), mY(0), mCachedX(0), mCachedY(0), mSortOffsetY(0), mPixelOffsetY(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), mCastEndTime(0), mLanguageId(-1), mBadgesX(0), mBadgesY(0), mCreatorId(BeingId_zero), 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), mBotAi(true), mAllowNpcEquipment(false) { for (int f = 0; f < 20; f ++) { mSpriteRemap[f] = f; mSpriteHide[f] = 0; mSpriteDraw[f] = 0; } for_each_badges() mBadges[f] = nullptr; } void Being::postInit(const BeingTypeId subtype, Map *const map) { setMap(map); setSubtype(subtype, 0); VisibleName::Type showName1 = VisibleName::Hide; switch (mType) { case ActorType::Player: case ActorType::Mercenary: case ActorType::Pet: case ActorType::Homunculus: case ActorType::Elemental: showName1 = static_cast( config.getIntValue("visiblenames")); break; case ActorType::Portal: case ActorType::SkillUnit: showName1 = VisibleName::Hide; break; default: case ActorType::Unknown: case ActorType::Npc: case ActorType::Monster: case ActorType::FloorItem: case ActorType::Avatar: break; } if (mType != ActorType::Npc) mGotComment = true; config.addListener("visiblenames", this); reReadConfig(); if (mType == ActorType::Npc || showName1 == VisibleName::Show) { setShowName(true); } 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) delete2(mCastingEffect) mBadgesCount = 0; delete2(mChat) removeHorse(); removeAllItemsParticles(); mSpiritParticles.clear(); } void Being::createSpeechBubble() restrict2 { CREATEWIDGETV0(mSpeechBubble, SpeechBubble); } void Being::setSubtype(const BeingTypeId subtype, const uint16_t look) restrict2 { if (mInfo == nullptr) return; if (subtype == mSubType && mLook == look) return; mSubType = subtype; mLook = look; switch (mType) { case ActorType::Monster: mInfo = MonsterDB::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::Pet: mInfo = PETDB::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::Mercenary: mInfo = MercenaryDB::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::Homunculus: mInfo = HomunculusDB::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_true, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::SkillUnit: mInfo = SkillUnitDb::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::Elemental: mInfo = ElementalDb::get(mSubType); if (mInfo != nullptr) { setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false, DisplayType::Item, mInfo->getColor(fromInt(mLook, ItemColor))); mYDiff = mInfo->getSortOffsetY(); } break; case ActorType::Npc: mInfo = NPCDB::get(mSubType); if (mInfo != nullptr) { setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false, DisplayType::Item, std::string()); mYDiff = mInfo->getSortOffsetY(); mAllowNpcEquipment = mInfo->getAllowEquipment(); } break; case ActorType::Avatar: mInfo = AvatarDB::get(mSubType); if (mInfo != nullptr) { setupSpriteDisplay(mInfo->getDisplay(), ForceDisplay_false, DisplayType::Item, std::string()); } break; case 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 != nullptr) { setSpriteId(charServerHandler->baseSprite(), id); } } else { const ItemInfo &restrict info = ItemDB::get(id); setRaceName(info.getName()); if (charServerHandler != nullptr) { setSpriteColor(charServerHandler->baseSprite(), id, info.getColor(fromInt(mLook, ItemColor))); } } break; } case ActorType::Portal: break; case ActorType::Unknown: case ActorType::FloorItem: default: reportAlways("Wrong being type %d in setSubType", CAST_S32(mType)) break; } } TargetCursorSizeT Being::getTargetCursorSize() const restrict2 { if (mInfo == nullptr) return TargetCursorSize::SMALL; return mInfo->getTargetCursorSize(); } void Being::setPixelPositionF(const Vector &restrict pos) restrict2 { Actor::setPixelPositionF(pos); updateCoords(); if (mText != nullptr) { mText->adviseXY(CAST_S32(pos.x), CAST_S32(pos.y) - getHeight() - mText->getHeight() - 9, mMoveNames); } } void Being::setDestination(const int dstX, const int dstY) restrict2 { if (mMap == nullptr) return; setPath(mMap->findPath(mX, mY, dstX, dstY, getBlockWalkMask(), 20)); } 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) restrict2 { if (userPalette == nullptr) 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; const size_t sz = mSpeech.size(); int time = 0; if (sz < 200) time = CAST_S32(SPEECH_TIME - 300 + (3 * sz)); if (time < CAST_S32(SPEECH_MIN_TIME)) time = CAST_S32(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 <= CAST_S32(SPEECH_MAX_TIME) ? time : CAST_S32(SPEECH_MAX_TIME); } const int speech = mSpeechType; if (speech == BeingSpeech::TEXT_OVERHEAD) { delete mText; mText = nullptr; mText = new Text(mSpeech, mPixelX, mPixelY - getHeight(), Graphics::CENTER, &userPalette->getColor(UserColorId::PARTICLE, 255U), Speech_true, nullptr); mText->adviseXY(mPixelX, (mY + 1) * mapTileSize - getHeight() - mText->getHeight() - 9, mMoveNames); } else { if (mSpeechBubble == nullptr) createSpeechBubble(); if (mSpeechBubble != nullptr) { const bool isShowName = (speech == BeingSpeech::NAME_IN_BUBBLE); mSpeechBubble->setCaption(isShowName ? mName : "", &theme->getColor(ThemeColorId::BUBBLE_NAME, 255), &theme->getColor(ThemeColorId::BUBBLE_NAME_OUTLINE, 255)); 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 == nullptr || attacker == nullptr) return; BLOCK_START("Being::takeDamage1") Font *font = nullptr; const Color *color; if (gui != nullptr) 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, 255U); } else { color = &userPalette->getColor(UserColorId::HIT_CRITICAL, 255U); } } else if (amount == 0) { 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, 255U); } else { color = &userPalette->getColor(UserColorId::MISS, 255U); } } else if (mType == ActorType::Monster || mType == ActorType::Mercenary || mType == ActorType::Pet || mType == ActorType::Homunculus || mType == ActorType::SkillUnit) { if (attacker == localPlayer) { color = &userPalette->getColor( UserColorId::HIT_LOCAL_PLAYER_MONSTER, 255U); } else { color = &userPalette->getColor( UserColorId::HIT_PLAYER_MONSTER, 255U); } } 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, 255U); attacker->setEnemy(true); attacker->updateColors(); } else { color = &userPalette->getColor(UserColorId::HIT_MONSTER_PLAYER, 255U); } if (chatWindow != nullptr && mShowBattleEvents) { if (this == localPlayer) { if (amount != 0) { ChatWindow::battleChatLog(strprintf("%s : Hit you -%d", attacker->getName().c_str(), amount), ChatMsgType::BY_OTHER, IgnoreRecord_false, TryRemoveColors_true); } else { ChatWindow::battleChatLog(strprintf("%s : Missed you", attacker->getName().c_str()), ChatMsgType::BY_OTHER, IgnoreRecord_false, TryRemoveColors_true); } } else if (attacker == localPlayer) { if (amount != 0) { ChatWindow::battleChatLog(strprintf("%s : You hit %s -%d", attacker->mName.c_str(), mName.c_str(), amount), ChatMsgType::BY_PLAYER, IgnoreRecord_false, TryRemoveColors_true); } else { ChatWindow::battleChatLog(strprintf("%s : You missed %s", attacker->mName.c_str(), mName.c_str()), ChatMsgType::BY_PLAYER, IgnoreRecord_false, TryRemoveColors_true); } } } if (font != nullptr && particleEngine != nullptr) { const std::string damage = amount != 0 ? toString(amount) : // TRANSLATORS: dodge or miss message in attacks type == AttackType::FLEE ? _("dodge") : _("miss"); // Show damage number particleEngine->addTextSplashEffect(damage, mPixelX, mPixelY - 16, color, font, true); } BLOCK_END("Being::takeDamage1") BLOCK_START("Being::takeDamage2") if (type != AttackType::SKILL) attacker->updateHit(amount); if (amount > 0) { if ((localPlayer != nullptr) && localPlayer == this) localPlayer->setLastHitFrom(attacker->mName); mDamageTaken += amount; if (mInfo != nullptr) { playSfx(mInfo->getSound(ItemSoundEvent::HURT), this, false, mX, mY); if (!mInfo->isStaticMaxHP()) { if ((mHP == 0) && mInfo->getMaxHP() < mDamageTaken) mInfo->setMaxHP(mDamageTaken); } } if ((mHP != 0) && isAlive()) { mHP -= amount; if (mHP < 0) mHP = 0; } if (mType == ActorType::Monster) { updatePercentHP(); updateName(); } else if (mType == ActorType::Player && (socialWindow != nullptr) && !mName.empty()) { socialWindow->updateAvatar(mName); } if (effectManager != nullptr) { const int hitEffectId = getHitEffect(attacker, type, attackId, level); if (hitEffectId >= 0) effectManager->trigger(hitEffectId, this, 0); } } else { if (effectManager != nullptr) { 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, 0); } } BLOCK_END("Being::takeDamage2") } int Being::getHitEffect(const Being *restrict const attacker, const AttackTypeT type, const int attackId, const int level) { if (effectManager == nullptr) 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) { const SkillData *restrict const data = skillDialog->getSkillDataByLevel(attackId, level); if (data == nullptr) 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 != nullptr) { const ItemInfo *restrict const attackerWeapon = attacker->getEquippedWeapon(); if (attackerWeapon != nullptr && 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 != nullptr) { const Attack *restrict const atk = info->getAttack(attackId); if (atk != nullptr) { 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 == nullptr) || (mInfo == nullptr)) return; BLOCK_START("Being::handleAttack") if (this != localPlayer) setAction(BeingAction::ATTACK, attackId); mLastAttackX = victim->mX; mLastAttackY = victim->mY; if (mType == ActorType::Player && (mEquippedWeapon != nullptr)) fireMissile(victim, mEquippedWeapon->getMissileConst()); else if (mInfo->getAttack(attackId) != nullptr) fireMissile(victim, mInfo->getAttack(attackId)->mMissile); reset(); mActionTime = tick_time; if (Net::getNetworkType() == ServerType::TMWATHENA && this != localPlayer) { const uint8_t dir = calcDirection(victim->mX, victim->mY); if (dir != 0U) setDirection(dir); } if ((damage != 0) && victim->mType == ActorType::Player && victim->mAction == BeingAction::SIT) { victim->setAction(BeingAction::STAND, 0); } if (mType == ActorType::Player) { if (mSlots.size() >= 10) { // here 10 is weapon slot int weaponId = mSlots[10].spriteId; if (weaponId == 0) 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::handleSkillCasting(Being *restrict const victim, const int skillId, const int skillLevel) restrict2 { if ((victim == nullptr) || (mInfo == nullptr) || (skillDialog == nullptr)) return; setAction(BeingAction::CAST, skillId); const SkillData *restrict const data = skillDialog->getSkillDataByLevel( skillId, skillLevel); if (data != nullptr) { effectManager->triggerDefault(data->castingSrcEffectId, this, paths.getIntValue("skillCastingSrcEffectId")); effectManager->triggerDefault(data->castingDstEffectId, victim, paths.getIntValue("skillCastingDstEffectId")); fireMissile(victim, data->castingMissile); } } void Being::handleSkill(Being *restrict const victim, const int damage, const int skillId, const int skillLevel) restrict2 { if ((victim == nullptr) || (mInfo == nullptr) || (skillDialog == nullptr)) return; const SkillInfo *restrict const skill = skillDialog->getSkill(skillId); const SkillData *restrict const data = skill != nullptr ? skill->getData1(skillLevel) : nullptr; if (data != nullptr) { effectManager->triggerDefault(data->srcEffectId, this, paths.getIntValue("skillSrcEffectId")); effectManager->triggerDefault(data->dstEffectId, victim, paths.getIntValue("skillDstEffectId")); fireMissile(victim, data->missile); } if (this != localPlayer && (skill != nullptr)) { const SkillType::SkillType type = skill->type; if ((type & SkillType::Attack) != 0 || (type & SkillType::Ground) != 0) { setAction(BeingAction::ATTACK, 1); } else { setAction(BeingAction::STAND, 1); } } reset(); mActionTime = tick_time; if (Net::getNetworkType() == ServerType::TMWATHENA && this != localPlayer) { const uint8_t dir = calcDirection(victim->mX, victim->mY); if (dir != 0U) setDirection(dir); } if ((damage != 0) && victim->mType == ActorType::Player && victim->mAction == BeingAction::SIT) { victim->setAction(BeingAction::STAND, 0); } if (data != nullptr) { 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 != BadgeDrawType::Hide) { const std::string badge = BadgesDB::getNameBadge(mName); if (!badge.empty()) { mBadges[BadgeIndex::Name] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } } void Being::setName(const std::string &restrict name) restrict2 { mExtName = name; 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 != BadgeDrawType::Hide) { const std::string badge = BadgesDB::getGuildBadge(mGuildName); if (!badge.empty()) { mBadges[BadgeIndex::Guild] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } } void Being::setGuildName(const std::string &restrict name) restrict2 { if (mGuildName != name) { mGuildName = name; showGuildBadge(!mGuildName.empty()); updateBadgesCount(); } } void Being::showClanBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Clan]) if (show && !mClanName.empty() && mShowBadges != BadgeDrawType::Hide) { const std::string badge = BadgesDB::getClanBadge(mClanName); if (!badge.empty()) { mBadges[BadgeIndex::Clan] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } } void Being::setClanName(const std::string &restrict name) restrict2 { if (mClanName != name) { mClanName = name; showClanBadge(!mClanName.empty()); updateBadgesCount(); } } void Being::setGuildPos(const std::string &restrict pos A_UNUSED) restrict2 { } void Being::addGuild(Guild *restrict const guild) restrict2 { if (guild == nullptr) return; mGuilds[guild->getId()] = guild; if (this == localPlayer && (socialWindow != nullptr)) socialWindow->addTab(guild); } void Being::removeGuild(const int id) restrict2 { if (this == localPlayer && (socialWindow != nullptr)) socialWindow->removeTab(mGuilds[id]); if (mGuilds[id] != nullptr) mGuilds[id]->removeMember(mName); 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 != nullptr) && 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 != nullptr) { if (this == localPlayer && (socialWindow != nullptr)) 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 != nullptr) old->removeMember(mId); if (party != nullptr) party->addMember(mId, mName); updateColors(); if (this == localPlayer && (socialWindow != nullptr)) { if (old != nullptr) socialWindow->removeTab(old); if (party != nullptr) socialWindow->addTab(party); } } void Being::updateGuild() restrict2 { if (localPlayer == nullptr) return; Guild *restrict const guild = localPlayer->getGuild(); if (guild == nullptr) { clearGuilds(); updateColors(); return; } if (guild->getMember(mName) != nullptr) { 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 != nullptr) old->removeMember(mName); updateColors(); if (this == localPlayer && (socialWindow != nullptr)) { if (old != nullptr) socialWindow->removeTab(old); if (guild != nullptr) socialWindow->addTab(guild); } } void Being::fireMissile(Being *restrict const victim, const MissileInfo &restrict missile) const restrict2 { BLOCK_START("Being::fireMissile") if (victim == nullptr || missile.particle.empty() || particleEngine == nullptr) { BLOCK_END("Being::fireMissile") return; } Particle *restrict const target = particleEngine->createChild(); if (target == nullptr) { BLOCK_END("Being::fireMissile") return; } // +++ add z particle position? Particle *restrict const missileParticle = target->addEffect( missile.particle, mPixelX, mPixelY, 0); target->moveBy(Vector(0.0F, 0.0F, missile.z)); target->setLifetime(missile.lifeTime); victim->controlAutoParticle(target); if (missileParticle != nullptr) { missileParticle->setDestination(target, missile.speed, 0.0F); missileParticle->setDieDistance(missile.dieDistance); missileParticle->setLifetime(missile.lifeTime); } BLOCK_END("Being::fireMissile") } std::string Being::getSitAction() const restrict2 { if (mHorseId != 0) return SpriteAction::SITRIDE; if (mMap != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if ((mask & BlockMask::GROUNDTOP) != 0) return SpriteAction::SITTOP; else if ((mask & BlockMask::AIR) != 0) return SpriteAction::SITSKY; else if ((mask & BlockMask::WATER) != 0) return SpriteAction::SITWATER; } return SpriteAction::SIT; } std::string Being::getMoveAction() const restrict2 { if (mHorseId != 0) return SpriteAction::RIDE; if (mMap != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if ((mask & BlockMask::AIR) != 0) return SpriteAction::FLY; else if ((mask & BlockMask::WATER) != 0) return SpriteAction::SWIM; } return SpriteAction::MOVE; } std::string Being::getWeaponAttackAction(const ItemInfo *restrict const weapon) const restrict2 { if (weapon == nullptr) return getAttackAction(); if (mHorseId != 0) return weapon->getRideAttackAction(); if (mMap != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if ((mask & BlockMask::AIR) != 0) return weapon->getSkyAttackAction(); else if ((mask & BlockMask::WATER) != 0) return weapon->getWaterAttackAction(); } return weapon->getAttackAction(); } std::string Being::getAttackAction(const Attack *restrict const attack1) const restrict2 { if (attack1 == nullptr) return getAttackAction(); if (mHorseId != 0) return attack1->mRideAction; if (mMap != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if ((mask & BlockMask::AIR) != 0) return attack1->mSkyAction; else if ((mask & BlockMask::WATER) != 0) return attack1->mWaterAction; } return attack1->mAction; } std::string Being::getCastAction(const SkillInfo *restrict const skill) const restrict2 { if (skill == nullptr) return getCastAction(); if (mHorseId != 0) return skill->castingRideAction; if (mMap != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if ((mask & BlockMask::AIR) != 0) return skill->castingSkyAction; else if ((mask & BlockMask::WATER) != 0) 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 != nullptr) { const unsigned char mask = mMap->getBlockMask(mX, mY); if (mTrickDead) { if ((mask & BlockMask::AIR) != 0) return SpriteAction::DEADSKY; else if ((mask & BlockMask::WATER) != 0) return SpriteAction::DEADWATER; else return SpriteAction::DEAD; } if ((mask & BlockMask::AIR) != 0) return SpriteAction::STANDSKY; else if ((mask & BlockMask::WATER) != 0) 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 != nullptr) { 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 != nullptr) { 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 != nullptr) { currentAction = getWeaponAttackAction(mEquippedWeapon); reset(); } else { if (mInfo == nullptr || mInfo->getAttack(attackId) == nullptr) break; currentAction = getAttackAction(mInfo->getAttack(attackId)); reset(); // attack particle effect if (ParticleEngine::enabled && (effectManager != nullptr)) { const int effectId = mInfo->getAttack(attackId)->mEffectId; if (effectId >= 0) { effectManager->triggerDirection(effectId, this, mSpriteDirection); } } } break; case BeingAction::CAST: if (skillDialog != nullptr) { const SkillInfo *restrict const info = skillDialog->getSkill(attackId); currentAction = getCastAction(info); } break; case BeingAction::HURT: if (mInfo != nullptr) { playSfx(mInfo->getSound(ItemSoundEvent::HURT), this, false, mX, mY); } break; case BeingAction::DEAD: currentAction = getDeadAction(); if (mInfo != nullptr) { playSfx(mInfo->getSound(ItemSoundEvent::DIE), this, false, mX, mY); if (mType == ActorType::Monster || mType == ActorType::Npc || mType == ActorType::SkillUnit) { mYDiff = mInfo->getDeadSortOffsetY(); } } break; case BeingAction::STAND: currentAction = getStandAction(); break; case BeingAction::SPAWN: if (mInfo != nullptr) { playSfx(mInfo->getSound(ItemSoundEvent::SPAWN), nullptr, true, mX, mY); } currentAction = getSpawnAction(); break; case BeingAction::PRESTAND: break; default: logger->log("Being::setAction unknown action: " + toString(CAST_U32(action))); break; } if (currentAction != SpriteAction::INVALID) { mSpriteAction = currentAction; play(currentAction); if (mEmotionSprite != nullptr) mEmotionSprite->play(currentAction); if (mAnimationEffect != nullptr) mAnimationEffect->play(currentAction); for_each_badges() { AnimatedSprite *const sprite = mBadges[f]; if (sprite != nullptr) sprite->play(currentAction); } for_each_horses(mDownHorseSprites) (*it)->play(currentAction); for_each_horses(mUpHorseSprites) (*it)->play(currentAction); 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 == 0) mFaceDirection = direction; SpriteDirection::Type dir; if ((mFaceDirection & BeingDirection::UP) != 0) { if ((mFaceDirection & BeingDirection::LEFT) != 0) dir = SpriteDirection::UPLEFT; else if ((mFaceDirection & BeingDirection::RIGHT) != 0) dir = SpriteDirection::UPRIGHT; else dir = SpriteDirection::UP; } else if ((mFaceDirection & BeingDirection::DOWN) != 0) { if ((mFaceDirection & BeingDirection::LEFT) != 0) dir = SpriteDirection::DOWNLEFT; else if ((mFaceDirection & BeingDirection::RIGHT) != 0) dir = SpriteDirection::DOWNRIGHT; else dir = SpriteDirection::DOWN; } else if ((mFaceDirection & BeingDirection::RIGHT) != 0) { dir = SpriteDirection::RIGHT; } else { dir = SpriteDirection::LEFT; } mSpriteDirection = dir; CompoundSprite::setSpriteDirection(dir); if (mEmotionSprite != nullptr) mEmotionSprite->setSpriteDirection(dir); if (mAnimationEffect != nullptr) mAnimationEffect->setSpriteDirection(dir); for_each_badges() { AnimatedSprite *const sprite = mBadges[f]; if (sprite != nullptr) sprite->setSpriteDirection(dir); } for_each_horses(mDownHorseSprites) (*it)->setSpriteDirection(dir); for_each_horses(mUpHorseSprites) (*it)->setSpriteDirection(dir); 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 != 0U) setDirection(dir); if (mMap == nullptr || !mMap->getWalk(pos.x, pos.y, getBlockWalkMask())) { setAction(BeingAction::STAND, 0); return; } mActionTime += mSpeed / 10; if ((mType != ActorType::Player || mUseDiagonal) && mX != pos.x && mY != pos.y) { mSpeed = mWalkSpeed * 14 / 10; } else { mSpeed = mWalkSpeed; } if (this == localPlayer) mSpeed = mSpeed + mPlayerSpeedAdjustment; if (mX != pos.x || mY != pos.y) { mOldHeight = mMap->getHeightOffset(mX, mY); if (mReachable == Reachable::REACH_NO && mMap->getBlockMask(mX, mY) != mMap->getBlockMask(pos.x, pos.y)) { mReachable = Reachable::REACH_UNKNOWN; } } mX = pos.x; mY = pos.y; const uint8_t height = mMap->getHeightOffset(mX, mY); mPixelOffsetY = height - mOldHeight; mFixedOffsetY = height; mNeedPosUpdate = true; setAction(BeingAction::MOVE, 0); } void Being::logic() restrict2 { BLOCK_START("Being::logic") if (A_UNLIKELY(mSpeechTime != 0)) { mSpeechTime--; if (mSpeechTime == 0 && mText != nullptr) delete2(mText) } if (A_UNLIKELY(mOwner != nullptr)) { if (mType == ActorType::Homunculus || mType == ActorType::Mercenary) { botLogic(); } } const int time = tick_time * MILLISECONDS_IN_A_TICK; if (mEmotionSprite != nullptr) mEmotionSprite->update(time); for_each_horses(mDownHorseSprites) (*it)->update(time); for_each_horses(mUpHorseSprites) (*it)->update(time); if (A_UNLIKELY(mCastEndTime != 0 && mCastEndTime < tick_time)) { mCastEndTime = 0; delete2(mCastingEffect) } if (A_UNLIKELY(mAnimationEffect)) { mAnimationEffect->update(time); if (mAnimationEffect->isTerminated()) delete2(mAnimationEffect) } if (A_UNLIKELY(mCastingEffect)) { mCastingEffect->update(time); if (mCastingEffect->isTerminated()) delete2(mCastingEffect) } for_each_badges() { AnimatedSprite *restrict const sprite = mBadges[f]; if (sprite != nullptr) sprite->update(time); } int frameCount = CAST_S32(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 (get_elapsed_time(mActionTime) >= mSpeed) nextTile(); break; } case BeingAction::ATTACK: { if (mActionTime == 0) break; int curFrame = 0; if (mAttackSpeed != 0) { 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(); const int yOffset = getOffset(); int offset = xOffset; if (offset == 0) offset = yOffset; if ((xOffset == 0) && (yOffset == 0)) mNeedPosUpdate = false; const int halfTile = mapTileSize / 2; const float offset2 = static_cast( mPixelOffsetY * abs(offset)) / 2; // mSortOffsetY = (mOldHeight - mFixedOffsetY + mPixelOffsetY) // * halfTile - offset2; mSortOffsetY = 0; const float yOffset3 = (mY + 1) * mapTileSize + yOffset - (mOldHeight + mPixelOffsetY) * halfTile + offset2; // Update pixel coordinates setPixelPositionF(static_cast(mX * mapTileSize + mapTileSize / 2 + xOffset), yOffset3, 0.0F); } if (A_UNLIKELY(mEmotionSprite)) { mEmotionTime--; if (mEmotionTime == 0) delete2(mEmotionSprite) } ActorSprite::logic(); if (frameCount < 10) frameCount = 10; if (A_UNLIKELY(!isAlive() && mSpeed != 0 && gameHandler->removeDeadBeings() && get_elapsed_time(mActionTime) / mSpeed >= frameCount)) { if (mType != ActorType::Player && (actorManager != nullptr)) actorManager->destroy(this); } const SoundInfo *restrict const sound = mNextSound.sound; if (A_UNLIKELY(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::botLogic() restrict2 { if ((mOwner == nullptr) || (mMap == nullptr) || (mInfo == nullptr)) return; const int time = tick_time; const int thinkTime = mInfo->getThinkTime(); if (abs(CAST_S32(mMoveTime) - time) < thinkTime) return; mMoveTime = time; int dstX = mOwner->mX; int dstY = mOwner->mY; const int warpDist = mInfo->getWarpDist(); const int divX = abs(dstX - mX); const int divY = abs(dstY - mY); if (divX >= warpDist || divY >= warpDist) { if (mType == ActorType::Homunculus) homunculusHandler->moveToMaster(); else mercenaryHandler->moveToMaster(); mBotAi = true; return; } if (!mBotAi) return; if (mAction == BeingAction::MOVE) { if (mOwner->mAction == BeingAction::MOVE) { updateBotFollow(dstX, dstY, divX, divY); } return; } switch (mOwner->mAction) { case BeingAction::MOVE: case BeingAction::PRESTAND: updateBotFollow(dstX, dstY, divX, divY); break; case BeingAction::STAND: case BeingAction::SPAWN: botFixOffset(dstX, dstY); moveBotTo(dstX, dstY); break; case BeingAction::ATTACK: { const Being *const target = localPlayer->getTarget(); if (target == nullptr) return; const BeingId targetId = target->getId(); if (mType == ActorType::Homunculus) { homunculusHandler->attack(targetId, Keep_true); } else { mercenaryHandler->attack(targetId, Keep_true); } break; } case BeingAction::SIT: case BeingAction::DEAD: botFixOffset(dstX, dstY); moveBotTo(dstX, dstY); break; case BeingAction::CAST: case BeingAction::HURT: default: break; } } void Being::botFixOffset(int &restrict dstX, int &restrict dstY) const { if ((mInfo == nullptr) || (mOwner == nullptr)) 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->mDirection) { 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 != nullptr) { if (!mMap->getWalk(dstX, dstY, getBlockWalkMask())) { dstX = mOwner->mX; dstY = mOwner->mY; } } } void Being::updateBotFollow(int dstX, int dstY, const int divX, const int divY) { const int followDist = mInfo->getStartFollowDist(); const int dist = mInfo->getFollowDist(); if (divX > followDist || divY > followDist) { if (dist > 0) { 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; } } botFixOffset(dstX, dstY); moveBotTo(dstX, dstY); } } void Being::moveBotTo(int dstX, int dstY) { const int dstX0 = mOwner->mX; const int dstY0 = mOwner->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) { if (mType == ActorType::Homunculus) homunculusHandler->move(dstX, dstY); else mercenaryHandler->move(dstX, dstY); return; } updateBotDirection(dstX, dstY); } void Being::updateBotDirection(const int dstX, const int dstY) { 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->mDirection; break; case 2: { const int dstX0 = mOwner->mX; const int dstY0 = mOwner->mY; 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: { const int dstX0 = mOwner->mX; const int dstY0 = mOwner->mY; 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 != 0U) && newDir != mDirection) { if (mType == ActorType::Homunculus) homunculusHandler->setDirection(newDir); else mercenaryHandler->setDirection(newDir); } } void Being::updateBadgesPosition() { const int px = mPixelX - mapTileSize / 2; const int py = mPixelY - mapTileSize * 2 - mapTileSize; if (mShowBadges != BadgeDrawType::Hide && mBadgesCount != 0U) { if (mDispName != nullptr && gui != nullptr) { if (mShowBadges == BadgeDrawType::Right) { const Font *restrict const font = gui->getFont(); mBadgesX = mDispName->getX() + mDispName->getWidth(); mBadgesY = mDispName->getY() - font->getHeight(); } else if (mShowBadges == BadgeDrawType::Bottom) { mBadgesX = px + 8 - mBadgesCount * 8; if (mVisibleNamePos == VisibleNamePos::Bottom) { mBadgesY = mDispName->getY(); } else { mBadgesY = py + settings.playerNameOffset + 16; } } else { mBadgesX = px + 8 - mBadgesCount * 8; if (mVisibleNamePos == VisibleNamePos::Top) mBadgesY = py - mDispName->getHeight(); else mBadgesY = py; } } else { if (mShowBadges == BadgeDrawType::Right) { mBadgesX = px + settings.playerBadgeAtRightOffset; mBadgesY = py; } else if (mShowBadges == BadgeDrawType::Bottom) { mBadgesX = px + 8 - mBadgesCount * 8; const int height = settings.playerNameOffset; if (mVisibleNamePos == VisibleNamePos::Bottom) mBadgesY = py + height; else mBadgesY = py + height + 16; } else { mBadgesX = px + 8 - mBadgesCount * 8; mBadgesY = py; } } } } void Being::drawEmotion(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { if (mErased) return; const int px = mPixelX - offsetX - mapTileSize / 2; const int py = mPixelY - offsetY - mapTileSize * 2 - mapTileSize; if (mAnimationEffect != nullptr) mAnimationEffect->draw(graphics, px, py); if (mShowBadges != BadgeDrawType::Hide && mBadgesCount != 0U) { int x = mBadgesX - offsetX; const int y = mBadgesY - offsetY; for_each_badges() { const AnimatedSprite *restrict const sprite = mBadges[f]; if (sprite != nullptr) { sprite->draw(graphics, x, y); x += 16; } } } if (mEmotionSprite != nullptr) mEmotionSprite->draw(graphics, px, py); } void Being::drawSpeech(const int offsetX, const int offsetY) restrict2 { if (mErased) return; if (mSpeech.empty()) return; const int px = mPixelX - offsetX; const int py = mPixelY - offsetY; const int speech = mSpeechType; // Draw speech above this being if (mSpeechTime == 0) { if (mSpeechBubble != nullptr && mSpeechBubble->mVisible == Visible_true) { 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 != nullptr) { mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), py - getHeight() - (mSpeechBubble->getHeight())); mSpeechBubble->setVisible(Visible_true); } } else if (mSpeechTime > 0 && speech == BeingSpeech::TEXT_OVERHEAD) { if (mSpeechBubble != nullptr) mSpeechBubble->setVisible(Visible_false); if ((mText == nullptr) && (userPalette != nullptr)) { mText = new Text(mSpeech, mPixelX, mPixelY - getHeight(), Graphics::CENTER, &theme->getColor(ThemeColorId::BUBBLE_TEXT, 255), Speech_true, nullptr); mText->adviseXY(mPixelX, (mY + 1) * mapTileSize - getHeight() - mText->getHeight() - 9, mMoveNames); } } else if (speech == BeingSpeech::NO_SPEECH) { if (mSpeechBubble != nullptr) mSpeechBubble->setVisible(Visible_false); delete2(mText) } } template int Being::getOffset() 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 && mSpeed) { const int time = get_elapsed_time(mActionTime); offset = (pos == BeingDirection::LEFT && neg == BeingDirection::RIGHT) ? (time * mMap->getTileWidth() / mSpeed) : (time * 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 != nullptr) { int offsetX = mPixelX; int offsetY = mPixelY; if (mInfo != nullptr) { offsetX += mInfo->getNameOffsetX(); offsetY += mInfo->getNameOffsetY(); } // Monster names show above the sprite instead of below it if (mType == ActorType::Monster || mVisibleNamePos == VisibleNamePos::Top) { offsetY += - settings.playerNameOffset - mDispName->getHeight(); } mDispName->adviseXY(offsetX, offsetY, mMoveNames); } updateBadgesPosition(); } void Being::optionChanged(const std::string &restrict value) restrict2 { if (mType == ActorType::Player && value == "visiblenames") { setShowName(config.getIntValue("visiblenames") == VisibleName::Show); updateBadgesPosition(); } } void Being::flashName(const int time) restrict2 { if (mDispName != nullptr) 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 == BadgeDrawType::Hide) { 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 && playerRelations.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 != nullptr) && localPlayer->getTarget() == this && mType != ActorType::Monster) { font = boldFont; } else if (mType == ActorType::Player && !playerRelations.isGoodName(this) && (gui != nullptr)) { font = gui->getSecureFont(); } if (mInfo != nullptr) { mDispName = new FlashText(displayName, mPixelX + mInfo->getNameOffsetX(), mPixelY + mInfo->getNameOffsetY(), Graphics::CENTER, mNameColor, font); } else { mDispName = new FlashText(displayName, mPixelX, mPixelY, Graphics::CENTER, mNameColor, font); } updateCoords(); } void Being::setDefaultNameColor(const UserColorIdT defaultColor) restrict2 { switch (mTeamId) { case 0: default: mNameColor = &userPalette->getColor(defaultColor, 255U); break; case 1: mNameColor = &userPalette->getColor(UserColorId::TEAM1, 255U); break; case 2: mNameColor = &userPalette->getColor(UserColorId::TEAM2, 255U); break; case 3: mNameColor = &userPalette->getColor(UserColorId::TEAM3, 255U); break; } } void Being::updateColors() { if (userPalette != nullptr) { if (mType == ActorType::Monster) { setDefaultNameColor(UserColorId::MONSTER); mTextColor = &userPalette->getColor(UserColorId::MONSTER, 255U); } else if (mType == ActorType::Npc) { setDefaultNameColor(UserColorId::NPC); mTextColor = &userPalette->getColor(UserColorId::NPC, 255U); } else if (mType == ActorType::Pet) { setDefaultNameColor(UserColorId::PET); mTextColor = &userPalette->getColor(UserColorId::PET, 255U); } else if (mType == ActorType::Homunculus) { setDefaultNameColor(UserColorId::HOMUNCULUS); mTextColor = &userPalette->getColor(UserColorId::HOMUNCULUS, 255U); } else if (mType == ActorType::SkillUnit) { setDefaultNameColor(UserColorId::SKILLUNIT); mTextColor = &userPalette->getColor(UserColorId::SKILLUNIT, 255U); } else if (this == localPlayer) { mNameColor = &userPalette->getColor(UserColorId::SELF, 255U); mTextColor = &theme->getColor(ThemeColorId::PLAYER, 255); } else { mTextColor = &theme->getColor(ThemeColorId::PLAYER, 255); if (playerRelations.getRelation(mName) != Relation::ERASED) mErased = false; else mErased = true; if (mIsGM) { mTextColor = &userPalette->getColor(UserColorId::GM, 255U); mNameColor = &userPalette->getColor(UserColorId::GM, 255U); } else if (mEnemy) { mNameColor = &userPalette->getColor(UserColorId::ENEMY, 255U); } else if ((mParty != nullptr) && (localPlayer != nullptr) && mParty == localPlayer->getParty()) { mNameColor = &userPalette->getColor(UserColorId::PARTY, 255U); } else if ((localPlayer != nullptr) && (getGuild() != nullptr) && getGuild() == localPlayer->getGuild()) { mNameColor = &userPalette->getColor(UserColorId::GUILD, 255U); } else if (playerRelations.getRelation(mName) == Relation::FRIEND) { mNameColor = &userPalette->getColor(UserColorId::FRIEND, 255U); } else if (playerRelations.getRelation(mName) == Relation::DISREGARDED || playerRelations.getRelation(mName) == Relation::BLACKLISTED) { mNameColor = &userPalette->getColor(UserColorId::DISREGARDED, 255U); } else if (playerRelations.getRelation(mName) == Relation::IGNORED || playerRelations.getRelation(mName) == Relation::ENEMY2) { mNameColor = &userPalette->getColor(UserColorId::IGNORED, 255U); } else if (playerRelations.getRelation(mName) == Relation::ERASED) { mNameColor = &userPalette->getColor(UserColorId::ERASED, 255U); } else { setDefaultNameColor(UserColorId::PC); } } if (mDispName != nullptr) mDispName->setColor(mNameColor); } } void Being::updateSprite(const unsigned int slot, const int id, const std::string &restrict color) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); if ((slot != 0U) && mSlots[slot].spriteId == id) return; setSpriteColor(slot, id, color); } // set sprite id, reset colors, reset cards void Being::setSpriteId(const unsigned int slot, const int id) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = mSlots[slot].spriteId; if (id1 != 0) 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 (!filename.empty()) { equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), filename), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } BeingSlot &beingSlot = mSlots[slot]; beingSlot.spriteId = id; beingSlot.color.clear(); beingSlot.colorId = ItemColor_one; beingSlot.cardsId = CardsList(nullptr); recalcSpritesOrder(); if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } // reset sprite id, reset colors, reset cards void Being::unSetSprite(const unsigned int slot) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); removeSprite(slot); mSpriteDraw[slot] = 0; BeingSlot &beingSlot = mSlots[slot]; const int id1 = beingSlot.spriteId; if (id1 != 0) removeItemParticles(id1); beingSlot.spriteId = 0; beingSlot.color.clear(); beingSlot.colorId = ItemColor_one; beingSlot.cardsId = CardsList(nullptr); recalcSpritesOrder(); if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } // set sprite id, use color string, reset cards void Being::setSpriteColor(const unsigned int slot, const int id, const std::string &color) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); // disabled for now, because it may broke replace/reorder sprites logic // if (slot && mSlots[slot].spriteId == id) // return; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = mSlots[slot].spriteId; if (id1 != 0) 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 (!filename.empty()) { equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } BeingSlot &beingSlot = mSlots[slot]; beingSlot.spriteId = id; beingSlot.color = color; beingSlot.colorId = ItemColor_one; beingSlot.cardsId = CardsList(nullptr); recalcSpritesOrder(); if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } // set sprite id, use color id, reset cards void Being::setSpriteColorId(const unsigned int slot, const int id, ItemColor colorId) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); // disabled for now, because it may broke replace/reorder sprites logic // if (slot && mSlots[slot].spriteId == id) // return; std::string color; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = mSlots[slot].spriteId; if (id1 != 0) 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 (!filename.empty()) { color = info.getDyeColorsString(colorId); equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } BeingSlot &beingSlot = mSlots[slot]; beingSlot.spriteId = id; beingSlot.color = STD_MOVE(color); beingSlot.colorId = colorId; beingSlot.cardsId = CardsList(nullptr); recalcSpritesOrder(); if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } // set sprite id, colors from cards, cards void Being::setSpriteCards(const unsigned int slot, const int id, const CardsList &cards) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); // disabled for now, because it may broke replace/reorder sprites logic // if (slot && mSlots[slot].spriteId == id) // return; ItemColor colorId = ItemColor_one; std::string color; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = mSlots[slot].spriteId; if (id1 != 0) 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 (!cards.isEmpty()) colorId = ItemColorManager::getColorFromCards(cards); if (!filename.empty()) { color = info.getDyeColorsString(colorId); equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticlesCards(id, info.getDisplay(), cards); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } BeingSlot &beingSlot = mSlots[slot]; beingSlot.spriteId = id; beingSlot.color = STD_MOVE(color); beingSlot.colorId = colorId; beingSlot.cardsId = CardsList(cards); recalcSpritesOrder(); if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } void Being::setWeaponId(const int id) restrict2 { if (id == 0) mEquippedWeapon = nullptr; else mEquippedWeapon = &ItemDB::get(id); } void Being::setTempSprite(const unsigned int slot, const int id) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); BeingSlot &beingSlot = mSlots[slot]; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = beingSlot.spriteId; if (id1 != 0) 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 (!filename.empty()) { ItemColor colorId = ItemColor_one; const CardsList &cards = beingSlot.cardsId; if (!cards.isEmpty()) colorId = ItemColorManager::getColorFromCards(cards); std::string color = beingSlot.color; if (color.empty()) color = info.getDyeColorsString(colorId); equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; // +++ here probably need use existing cards addItemParticles(id, info.getDisplay()); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } } void Being::setHairTempSprite(const unsigned int slot, const int id) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); const CardsList &cards = mSlots[slot].cardsId; // id = 0 means unequip if (id == 0) { removeSprite(slot); mSpriteDraw[slot] = 0; const int id1 = mSlots[slot].spriteId; if (id1 != 0) 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 (!filename.empty()) { ItemColor colorId = ItemColor_one; if (!cards.isEmpty()) colorId = ItemColorManager::getColorFromCards(cards); std::string color = info.getDyeColorsString(mHairColor); if (color.empty()) color = info.getDyeColorsString(colorId); equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); mSpriteDraw[slot] = id; addItemParticles(id, info.getDisplay()); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } } void Being::setHairColorSpriteID(const unsigned int slot, const int id) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); BeingSlot &beingSlot = mSlots[slot]; setSpriteColor(slot, id, beingSlot.color); } void Being::setSpriteColor(const unsigned int slot, const std::string &restrict color) restrict2 { if (charServerHandler == nullptr || slot >= charServerHandler->maxSprite()) return; if (slot >= CAST_U32(mSprites.size())) ensureSize(slot + 1); if (slot >= CAST_U32(mSlots.size())) mSlots.resize(slot + 1, BeingSlot()); // disabled for now, because it may broke replace/reorder sprites logic // if (slot && mSlots[slot].spriteId == id) // return; BeingSlot &beingSlot = mSlots[slot]; const int id = beingSlot.spriteId; // id = 0 means unequip if (id != 0) { 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 (!filename.empty()) { equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(slot, equipmentSprite); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } } beingSlot.color = color; beingSlot.colorId = ItemColor_one; if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } void Being::setHairStyle(const unsigned int slot, const int id) restrict2 { if (id != 0) { setSpriteColor(slot, id, ItemDB::get(id).getDyeColorsString(mHairColor)); } else { setSpriteColor(slot, 0, std::string()); } } void Being::setHairColor(const unsigned int slot, const ItemColor color) restrict2 { mHairColor = color; BeingSlot &beingSlot = mSlots[slot]; const int id = beingSlot.spriteId; if (id != 0) { setSpriteColor(slot, id, ItemDB::get(id).getDyeColorsString(color)); } } void Being::setSpriteSlot(const unsigned int slot, const BeingSlot &beingSlot) { mSlots[slot] = beingSlot; } void Being::dumpSprites() const restrict2 { STD_VECTOR::const_iterator it1 = mSlots.begin(); const STD_VECTOR::const_iterator it1_end = mSlots.end(); logger->log("sprites"); for (; it1 != it1_end; ++ it1) { logger->log("%d,%s,%d", (*it1).spriteId, (*it1).color.c_str(), toInt((*it1).colorId, int)); } } 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"); mPlayerSpeedAdjustment = serverConfig.getIntValue("playerSpeedAdjustment"); mShowBadges = static_cast( config.getIntValue("showBadges")); mVisibleNamePos = static_cast( config.getIntValue("visiblenamespos")); mUpdateConfigTime = cur_time; } BLOCK_END("Being::reReadConfig") } bool Being::updateFromCache() restrict2 { const BeingCacheEntry *restrict const entry = Being::getCacheEntry(getId()); if ((entry != nullptr) && 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 != nullptr) && Net::getNetworkType() == ServerType::TMWATHENA) { mShop = ((flags & BeingFlag::SHOP) != 0); } mAway = ((flags & BeingFlag::AWAY) != 0); mInactive = ((flags & BeingFlag::INACTIVE) != 0); if (mShop || mAway || mInactive) updateName(); } else { if (Net::getNetworkType() == ServerType::TMWATHENA) mShop = false; mAway = false; mInactive = false; } showShopBadge(mShop); showInactiveBadge(mInactive); showAwayBadge(mAway); updateAwayEffect(); if (mType == ActorType::Player || (mTeamId != 0U)) updateColors(); return true; } return false; } void Being::addToCache() const restrict2 { if (localPlayer == this) return; BeingCacheEntry *entry = Being::getCacheEntry(getId()); if (entry == nullptr) { entry = new BeingCacheEntry(getId()); beingInfoCache.push_front(entry); if (beingInfoCache.size() >= CACHE_SIZE) { delete beingInfoCache.back(); beingInfoCache.pop_back(); } } if (!mLowTraffic) return; entry->setName(mName); 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 (Net::getNetworkType() == ServerType::TMWATHENA && 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 == nullptr) 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 (charServerHandler == nullptr) return; if (gender != mGender) { mGender = gender; const unsigned int sz = CAST_U32(mSlots.size()); if (sz > CAST_U32(mSprites.size())) ensureSize(sz); // Reload all subsprites for (unsigned int i = 0; i < sz; i++) { BeingSlot &beingSlot = mSlots[i]; const int id = beingSlot.spriteId; if (id != 0) { 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 (!filename.empty()) { equipmentSprite = AnimatedSprite::delayedLoad( pathJoin(paths.getStringValue("sprites"), combineDye(filename, beingSlot.color)), 0); } if (equipmentSprite != nullptr) { equipmentSprite->setSpriteDirection(getSpriteDirection()); startTime = getStartTime(); lastTime = getLastTime(); } CompoundSprite::setSprite(i, equipmentSprite); setAction(mAction, 0); if (equipmentSprite != nullptr) { if (lastTime > 0) { equipmentSprite->setLastTime(startTime); equipmentSprite->update(lastTime); } } if (beingEquipmentWindow != nullptr) beingEquipmentWindow->updateBeing(this); } } updateName(); } } void Being::showGmBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Gm]) if (show && mIsGM && mShowBadges != BadgeDrawType::Hide && GroupDb::getShowBadge(mGroupId)) { const std::string &gmBadge = GroupDb::getBadge(mGroupId); if (!gmBadge.empty()) { mBadges[BadgeIndex::Gm] = AnimatedSprite::load( paths.getStringValue("badges") + gmBadge, 0); } } updateBadgesCount(); updateBadgesPosition(); } void Being::setGM(const bool gm) restrict2 { if (mIsGM != gm) { mIsGM = gm; updateColors(); } } void Being::talkTo() const restrict2 { if (npcHandler == nullptr) return; if (!PacketLimiter::limitPackets(PacketType::PACKET_NPC_TALK)) { // using workaround... if ((playerHandler != nullptr) && PacketLimiter::limitPackets(PacketType::PACKET_ATTACK)) { playerHandler->attack(mId, Keep_false); } return; } npcHandler->talk(this); } void Being::drawPlayer(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { if (!mErased) { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; if (mHorseInfo != nullptr) { HorseOffset &offset = mHorseInfo->offsets[mSpriteDirection]; for_each_horses(mDownHorseSprites) { (*it)->draw(graphics, px + offset.downOffsetX, py + offset.downOffsetY); } drawBeingCursor(graphics, px, py); drawPlayerSpriteAt(graphics, px + offset.riderOffsetX, py + offset.riderOffsetY); for_each_horses(mUpHorseSprites) { (*it)->draw(graphics, px + offset.upOffsetX, py + offset.upOffsetY); } } else { drawBeingCursor(graphics, px, py); drawPlayerSpriteAt(graphics, px, py); } } } void Being::drawBeingCursor(Graphics *const graphics, const int offsetX, const int offsetY) const { if (mUsedTargetCursor != nullptr) { mUsedTargetCursor->update(tick_time * MILLISECONDS_IN_A_TICK); if (mInfo == nullptr) { mUsedTargetCursor->draw(graphics, offsetX - mCursorPaddingX, offsetY - mCursorPaddingY); } else { mUsedTargetCursor->draw(graphics, offsetX + mInfo->getTargetOffsetX() - mCursorPaddingX, offsetY + mInfo->getTargetOffsetY() - mCursorPaddingY); } } } void Being::drawOther(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawOtherSpriteAt(graphics, px, py); } void Being::drawNpc(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawNpcSpriteAt(graphics, px, py); } void Being::drawMonster(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawMonsterSpriteAt(graphics, px, py); } void Being::drawHomunculus(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawHomunculusSpriteAt(graphics, px, py); } void Being::drawMercenary(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawMercenarySpriteAt(graphics, px, py); } void Being::drawElemental(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawBeingCursor(graphics, px, py); drawElementalSpriteAt(graphics, px, py); } void Being::drawPortal(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { // getActorX() + offsetX; const int px = mPixelX - mapTileSize / 2 + offsetX; // getActorY() + offsetY; const int py = mPixelY - mapTileSize + offsetY; drawPortalSpriteAt(graphics, px, py); } void Being::draw(Graphics *restrict const graphics, const int offsetX, const int offsetY) const restrict2 { switch (mType) { case ActorType::Player: drawPlayer(graphics, offsetX, offsetY); break; case ActorType::Portal: drawPortal(graphics, offsetX, offsetY); break; case ActorType::Homunculus: drawHomunculus(graphics, offsetX, offsetY); break; case ActorType::Mercenary: drawMercenary(graphics, offsetX, offsetY); break; case ActorType::Elemental: drawElemental(graphics, offsetX, offsetY); break; case ActorType::Monster: drawMonster(graphics, offsetX, offsetY); break; case ActorType::Npc: drawNpc(graphics, offsetX, offsetY); break; case ActorType::Pet: case ActorType::SkillUnit: case ActorType::Unknown: case ActorType::FloorItem: case ActorType::Avatar: default: drawOther(graphics, offsetX, offsetY); break; } } void Being::drawPlayerSprites(Graphics *restrict const graphics, const int posX, const int posY) const restrict2 { const int sz = CompoundSprite::getNumberOfLayers(); for (int f = 0; f < sz; f ++) { const int rSprite = mSpriteHide[mSpriteRemap[f]]; if (rSprite == 1) continue; Sprite *restrict const sprite = mSprites[mSpriteRemap[f]]; if (sprite != nullptr) { sprite->setAlpha(mAlpha); sprite->draw(graphics, posX, posY); } } } void Being::drawSpritesSDL(Graphics *restrict const graphics, const int posX, const int posY) const restrict2 { const size_t sz = mSprites.size(); for (size_t f = 0; f < sz; f ++) { const int rSprite = mSpriteHide[mSpriteRemap[f]]; if (rSprite == 1) continue; const Sprite *restrict const sprite = mSprites[mSpriteRemap[f]]; if (sprite != nullptr) sprite->draw(graphics, posX, posY); } } void Being::drawBasic(Graphics *restrict const graphics, const int x, const int y) const restrict2 { drawCompound(graphics, x, y); } void Being::drawCompound(Graphics *const graphics, const int posX, const int posY) const { FUNC_BLOCK("CompoundSprite::draw", 1) if (mNeedsRedraw) updateImages(); if (mSprites.empty()) // Nothing to draw return; if (mAlpha == 1.0F && (mImage != nullptr)) { graphics->drawImage(mImage, posX + mOffsetX, posY + mOffsetY); } else if ((mAlpha != 0.0F) && (mAlphaImage != nullptr)) { mAlphaImage->setAlpha(mAlpha); graphics->drawImage(mAlphaImage, posX + mOffsetX, posY + mOffsetY); } else { Being::drawPlayerSprites(graphics, posX, posY); } } void Being::drawPlayerSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { drawCompound(graphics, x, y); if (mShowOwnHP && (mInfo != nullptr) && localPlayer == this && mAction != BeingAction::DEAD) { drawHpBar(graphics, PlayerInfo::getAttribute(Attributes::PLAYER_MAX_HP), PlayerInfo::getAttribute(Attributes::PLAYER_HP), 0, UserColorId::PLAYER_HP, UserColorId::PLAYER_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } void Being::drawOtherSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { CompoundSprite::drawSimple(graphics, x, y); } void Being::drawNpcSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { drawCompound(graphics, x, y); } void Being::drawMonsterSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (mHighlightMonsterAttackRange && mType == ActorType::Monster && mAction != BeingAction::DEAD) { if (userPalette == nullptr) { CompoundSprite::drawSimple(graphics, x, y); return; } int attackRange; if (mAttackRange != 0) 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::drawSimple(graphics, x, y); if (mShowMobHP && (mInfo != nullptr) && (localPlayer != nullptr) && localPlayer->getTarget() == this && mType == ActorType::Monster) { // show hp bar here int maxHP = mMaxHP; if (maxHP == 0) 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); } } void Being::drawHomunculusSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (mHighlightMonsterAttackRange && mAction != BeingAction::DEAD) { if (userPalette == nullptr) { CompoundSprite::drawSimple(graphics, x, y); return; } int attackRange; if (mAttackRange != 0) 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::drawSimple(graphics, x, y); if (mShowMobHP && (mInfo != nullptr)) { const HomunculusInfo *const info = PlayerInfo::getHomunculus(); if ((info != nullptr) && mId == info->id) { // show hp bar here int maxHP = PlayerInfo::getStatBase(Attributes::HOMUN_MAX_HP); if (maxHP == 0) maxHP = mInfo->getMaxHP(); drawHpBar(graphics, maxHP, PlayerInfo::getStatBase(Attributes::HOMUN_HP), mDamageTaken, UserColorId::HOMUN_HP, UserColorId::HOMUN_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } } void Being::drawMercenarySpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (mHighlightMonsterAttackRange && mAction != BeingAction::DEAD) { if (userPalette == nullptr) { CompoundSprite::drawSimple(graphics, x, y); return; } int attackRange; if (mAttackRange != 0) 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::drawSimple(graphics, x, y); if (mShowMobHP && (mInfo != nullptr)) { const MercenaryInfo *const info = PlayerInfo::getMercenary(); if ((info != nullptr) && mId == info->id) { // show hp bar here int maxHP = PlayerInfo::getStatBase(Attributes::MERC_MAX_HP); if (maxHP == 0) maxHP = mInfo->getMaxHP(); drawHpBar(graphics, maxHP, PlayerInfo::getStatBase(Attributes::MERC_HP), mDamageTaken, UserColorId::MERC_HP, UserColorId::MERC_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } } void Being::drawElementalSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (mHighlightMonsterAttackRange && mAction != BeingAction::DEAD) { if (userPalette == nullptr) { CompoundSprite::drawSimple(graphics, x, y); return; } int attackRange; if (mAttackRange != 0) 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::drawSimple(graphics, x, y); if (mShowMobHP && (mInfo != nullptr)) { if (mId == PlayerInfo::getElementalId()) { // show hp bar here int maxHP = PlayerInfo::getStatBase(Attributes::ELEMENTAL_MAX_HP); if (maxHP == 0) maxHP = mInfo->getMaxHP(); drawHpBar(graphics, maxHP, PlayerInfo::getStatBase(Attributes::ELEMENTAL_HP), mDamageTaken, UserColorId::ELEMENTAL_HP, UserColorId::ELEMENTAL_HP2, x - 50 + mapTileSize / 2 + mInfo->getHpBarOffsetX(), y + mapTileSize - 6 + mInfo->getHpBarOffsetY(), 2 * 50, 4); } } } void Being::drawPortalSpriteAt(Graphics *restrict const graphics, const int x, const int y) const restrict2 { if (mHighlightMapPortals && (mMap != nullptr) && !mMap->getHasWarps()) { if (userPalette == nullptr) { CompoundSprite::drawSimple(graphics, x, y); return; } 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, 255U); gui->getFont()->drawString(graphics, color, color, mName, x, y); } } CompoundSprite::drawSimple(graphics, x, y); } 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 == nullptr)) return; float p; if (hp != 0) { 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); #ifdef TMWA_SUPPORT if (!serverFeatures->haveServerHp()) { // old servers if ((damage == 0 && (this != localPlayer || hp == maxHP)) || (hp == 0 && 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 #endif // TMWA_SUPPORT { 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 = mSprites.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 = sz; slot < 20; slot ++) { oldHide[slot] = 0; updatedSprite[slot] = false; } for (size_t slot = 0; slot < sz; slot ++) { oldHide[slot] = mSpriteHide[slot]; mSpriteHide[slot] = 0; updatedSprite[slot] = false; } size_t spriteIdSize = mSlots.size(); if (reportTrue(spriteIdSize > 20)) spriteIdSize = 20; for (size_t slot = 0; slot < sz; slot ++) { slotRemap.push_back(CAST_S32(slot)); if (spriteIdSize <= slot) continue; const int id = mSlots[slot].spriteId; if (id == 0) continue; const ItemInfo &info = ItemDB::get(id); if (info.isRemoveSprites()) { const SpriteToItemMap *restrict const spriteToItems = info.getSpriteToItemReplaceMap(dir); if (spriteToItems != nullptr) { FOR_EACHP (SpriteToItemMapCIter, itr, spriteToItems) { const int remSlot = itr->first; const IntMap &restrict itemReplacer = itr->second; if (remSlot >= 0) { // slot known if (CAST_U32(remSlot) >= spriteIdSize) continue; if (itemReplacer.empty()) { mSpriteHide[remSlot] = 1; } else if (mSpriteHide[remSlot] != 1) { IntMapCIter repIt = itemReplacer.find( mSlots[remSlot].spriteId); if (repIt == itemReplacer.end()) { repIt = itemReplacer.find(0); if (repIt == itemReplacer.end() || repIt->second == 0) { repIt = itemReplacer.end(); } } if (repIt != itemReplacer.end()) { mSpriteHide[remSlot] = repIt->second; if (repIt->second != 1) { if (CAST_U32(remSlot) != hairSlot) { setTempSprite(remSlot, repIt->second); } else { setHairTempSprite(remSlot, repIt->second); } updatedSprite[remSlot] = 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 (mSlots[slot2].spriteId == repIt->first) { mSpriteHide[slot2] = repIt->second; if (repIt->second != 1) { if (slot2 != hairSlot) { setTempSprite(slot2, repIt->second); } else { setHairTempSprite(slot2, repIt->second); } updatedSprite[slot2] = true; } } } } } } } } if (info.mDrawBefore[dir] > 0) { const int id2 = mSlots[info.mDrawBefore[dir]].spriteId; 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 = mSlots[info.mDrawAfter[dir]].spriteId; 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 (CAST_S32(spriteIdSize) > val) id = mSlots[val].spriteId; 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 BeingSlot &beingSlot = mSlots[slot]; const int id = beingSlot.spriteId; if (id == 0) continue; updatedSprite[slot] = true; setTempSprite(slot, id); } } } for (size_t slot = 0; slot < spriteIdSize; slot ++) { if (mSpriteHide[slot] == 0) { const BeingSlot &beingSlot = mSlots[slot]; const int id = beingSlot.spriteId; if (updatedSprite[slot] == false && mSpriteDraw[slot] != id) { setTempSprite(static_cast(slot), id); } } } } int Being::searchSlotValue(const STD_VECTOR &restrict slotRemap, const int val) const restrict2 { const size_t sz = mSprites.size(); for (size_t slot = 0; slot < sz; slot ++) { if (slotRemap[slot] == val) return CAST_S32(slot); } return CompoundSprite::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; } void Being::updateHit(const int amount) restrict2 { if (amount > 0) { if ((mMinHit == 0) || amount < mMinHit) mMinHit = amount; if (amount != mCriticalHit && ((mMaxHit == 0) || 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 = mSlots.size(); for (size_t f = 0; f < sz; f ++) { if (id == mSlots[f].spriteId) { unSetSprite(CAST_U32(f)); 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::Avatar: case ActorType::Mercenary: case ActorType::Homunculus: case ActorType::Pet: case ActorType::SkillUnit: case ActorType::Elemental: default: return ""; } str = pathJoin(str, stringToHexPath(name), "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::Avatar: case ActorType::Unknown: case ActorType::Pet: case ActorType::Mercenary: case ActorType::Homunculus: case ActorType::SkillUnit: case ActorType::Elemental: default: return; } dir = pathJoin(dir, 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 (Net::getNetworkType() == ServerType::TMWATHENA) 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 != nullptr) { const EmoteSprite *restrict const sprite = info->sprites.front(); if (sprite != nullptr) { mEmotionSprite = AnimatedSprite::clone(sprite->sprite); if (mEmotionSprite != nullptr) mEmotionTime = info->time; else mEmotionTime = emote_time; } const int effectId = info->effectId; if (effectId >= 0) { effectManager->trigger(effectId, this, 0); } } } if (mEmotionSprite != nullptr) { mEmotionSprite->play(mSpriteAction); mEmotionSprite->setSpriteDirection(mSpriteDirection); } else { mEmotionTime = 0; } } } void Being::updatePercentHP() restrict2 { if (mMaxHP == 0) return; BLOCK_START("Being::updatePercentHP") if (mHP != 0) { const unsigned num = mHP * 100 / mMaxHP; if (num != mNumber) { mNumber = num; if (updateNumber(mNumber)) setAction(mAction, 0); } } BLOCK_END("Being::updatePercentHP") } int Being::getSpriteID(const int slot) const restrict2 { if (slot < 0 || CAST_SIZE(slot) >= mSlots.size()) return -1; return mSlots[slot].spriteId; } const BeingSlot &Being::getSpriteSlot(const int slot) const restrict2 { if (slot < 0 || CAST_SIZE(slot) >= mSlots.size()) return *emptyBeingSlot; return mSlots[slot]; } ItemColor Being::getSpriteColor(const int slot) const restrict2 { if (slot < 0 || CAST_SIZE(slot) >= mSlots.size()) return ItemColor_one; return mSlots[slot].colorId; } void Being::addAfkEffect() restrict2 { addSpecialEffect(mAwayEffect); } void Being::removeAfkEffect() restrict2 { removeSpecialEffect(); } void Being::addSpecialEffect(const int effect) restrict2 { if ((effectManager != nullptr) && ParticleEngine::enabled && (mSpecialParticle == nullptr) && effect != -1) { mSpecialParticle = effectManager->triggerReturn(effect, this, 0); } } void Being::removeSpecialEffect() restrict2 { if ((effectManager != nullptr) && (mSpecialParticle != nullptr)) { 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, 0); } void Being::playSfx(const SoundInfo &sound, Being *const being, const bool main, const int x, const int y) const restrict2 { BLOCK_START("Being::playSfx") if (being != nullptr) { // 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 != nullptr) { mPixelOffsetY = 0; mFixedOffsetY = mPixelOffsetY; mOldHeight = mMap->getHeightOffset(mX, mY); mNeedPosUpdate = true; } } void Being::setMap(Map *restrict const map) restrict2 { mCastEndTime = 0; delete2(mCastingEffect) ActorSprite::setMap(map); if (mMap != nullptr) { mPixelOffsetY = mMap->getHeightOffset(mX, mY); mFixedOffsetY = mPixelOffsetY; 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 { const 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 == nullptr) || !pi->particles.empty()) return; // setup particle effects if (ParticleEngine::enabled && (particleEngine != nullptr)) { FOR_EACH (StringVectCIter, itr, display.particles) { Particle *const p = particleEngine->addEffect(*itr, 0, 0, 0); controlCustomParticle(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::addItemParticlesCards(const int id, const SpriteDisplay &restrict display, const CardsList &cards) restrict2 { const 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 == nullptr) || !pi->particles.empty()) return; // setup particle effects if (ParticleEngine::enabled && (particleEngine != nullptr)) { FOR_EACH (StringVectCIter, itr, display.particles) { Particle *const p = particleEngine->addEffect(*itr, 0, 0, 0); controlCustomParticle(p); pi->files.push_back(*itr); pi->particles.push_back(p); } for (int f = 0; f < maxCards; f ++) { const int cardId = cards.cards[f]; if (!Item::isItem(cardId)) continue; const ItemInfo &info = ItemDB::get(cardId); const SpriteDisplay &restrict display2 = info.getDisplay(); FOR_EACH (StringVectCIter, itr, display2.particles) { Particle *const p = particleEngine->addEffect(*itr, 0, 0, 0); controlCustomParticle(p); pi->files.push_back(*itr); pi->particles.push_back(p); } } } else { FOR_EACH (StringVectCIter, itr, display.particles) { pi->files.push_back(*itr); } for (int f = 0; f < maxCards; f ++) { const int cardId = cards.cards[f]; if (!Item::isItem(cardId)) continue; const ItemInfo &info = ItemDB::get(cardId); const SpriteDisplay &restrict display2 = info.getDisplay(); FOR_EACH (StringVectCIter, itr, display2.particles) { pi->files.push_back(*itr); } } } } void Being::removeItemParticles(const int id) restrict2 { const SpriteParticleInfoIter it = mSpriteParticles.find(id); if (it == mSpriteParticles.end()) return; ParticleInfo *restrict const pi = (*it).second; if (pi != nullptr) { 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 != nullptr) && !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, 0); controlCustomParticle(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 != 0U && mShowBadges != BadgeDrawType::Hide) { const std::string name = paths.getStringValue("badges") + paths.getStringValue(strprintf("team%dbadge", mTeamId)); if (!name.empty()) mBadges[BadgeIndex::Team] = AnimatedSprite::load(name, 0); } updateBadgesCount(); updateBadgesPosition(); } 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 != BadgeDrawType::Hide) { const std::string badge = BadgesDB::getPartyBadge(mPartyName); if (!badge.empty()) { mBadges[BadgeIndex::Party] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } updateBadgesCount(); updateBadgesPosition(); } 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 != BadgeDrawType::Hide) { const std::string badge = paths.getStringValue("shopbadge"); if (!badge.empty()) { mBadges[BadgeIndex::Shop] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } updateBadgesCount(); updateBadgesPosition(); } void Being::showInactiveBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Inactive]) if (show && mInactive && mShowBadges != BadgeDrawType::Hide) { const std::string badge = paths.getStringValue("inactivebadge"); if (!badge.empty()) { mBadges[BadgeIndex::Inactive] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } updateBadgesCount(); updateBadgesPosition(); } void Being::showAwayBadge(const bool show) restrict2 { delete2(mBadges[BadgeIndex::Away]) if (show && mAway && mShowBadges != BadgeDrawType::Hide) { const std::string badge = paths.getStringValue("awaybadge"); if (!badge.empty()) { mBadges[BadgeIndex::Away] = AnimatedSprite::load( paths.getStringValue("badges") + badge, 0); } } updateBadgesCount(); updateBadgesPosition(); } void Being::updateBadgesCount() restrict2 { mBadgesCount = 0; for_each_badges() { if (mBadges[f] != nullptr) mBadgesCount ++; } } 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); } void Being::enableShop(const bool b) restrict2 { mShop = b; updateName(); showShopBadge(mShop); } bool Being::isBuyShopEnabled() const restrict2 { return mShop && (Net::getNetworkType() == ServerType::TMWATHENA || !mBuyBoard.empty()); } bool Being::isSellShopEnabled() const restrict2 { return mShop && (Net::getNetworkType() == ServerType::TMWATHENA || !mSellBoard.empty()); } void Being::serverRemove() restrict2 noexcept2 { // remove some flags what can survive player remove and next visible mTrickDead = false; } void Being::addCast(const int dstX, const int dstY, const int skillId, const int skillLevel, const int range, const int waitTimeTicks) { if (waitTimeTicks <= 0) { mCastEndTime = 0; return; } mCastEndTime = tick_time + waitTimeTicks; SkillData *const data = skillDialog->getSkillDataByLevel( skillId, skillLevel); delete2(mCastingEffect) if (data != nullptr) { const std::string castingAnimation = data->castingAnimation; mCastingEffect = new CastingEffect(skillId, skillLevel, castingAnimation, dstX, dstY, range); mCastingEffect->setMap(mMap); } else { reportAlways("Want to draw casting for unknown skill %d", skillId) } } 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, false); if (mHorseInfo != nullptr) { FOR_EACH (SpriteRefs, it, mHorseInfo->downSprites) { const 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) { const SpriteReference *restrict const ref = *it; AnimatedSprite *const 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 (!ParticleEngine::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 == nullptr) return; for (unsigned int f = 0; f < balls; f ++) { Particle *const particle = effectManager->triggerReturn( effectId, this, 0); mSpiritParticles.push_back(particle); } } void Being::removeSpiritBalls(const unsigned int balls) restrict2 { if (particleEngine == nullptr) return; for (unsigned int f = 0; f < balls && !mSpiritParticles.empty(); f ++) { const Particle *restrict const particle = mSpiritParticles.back(); mChildParticleEffects.removeLocally(particle); mSpiritParticles.pop_back(); } } void Being::stopCast(const bool b) { if (b && mAction == BeingAction::CAST) setAction(BeingAction::STAND, 0); } void Being::fixDirectionOffsets(int &offsetX, int &offsetY) const { const uint8_t dir = mDirection; if ((dir & BeingDirection::DOWN) != 0) { // do nothing } else if ((dir & BeingDirection::UP) != 0) { offsetX = -offsetX; offsetY = -offsetY; } else if ((dir & BeingDirection::LEFT) != 0) { const int tmp = offsetY; offsetY = offsetX; offsetX = -tmp; } else if ((dir & BeingDirection::RIGHT) != 0) { const int tmp = offsetY; offsetY = -offsetX; offsetX = tmp; } } void Being::setLanguageId(const int lang) restrict2 noexcept2 { if (lang != mLanguageId) { delete2(mBadges[BadgeIndex::Lang]) const std::string &badge = LanguageDb::getIcon(lang); if (!badge.empty()) { mBadges[BadgeIndex::Lang] = AnimatedSprite::load(pathJoin( paths.getStringValue("languageIcons"), badge), 0); } mLanguageId = lang; } updateBadgesCount(); updateBadgesPosition(); } Being *Being::createBeing(const BeingId id, const ActorTypeT type, const BeingTypeId subType, Map *const map) { Being *const being = new Being(id, type); being->postInit(subType, map); return being; } void Being::setGroupId(const int id) { if (mGroupId != id) { mGroupId = id; const bool gm = GroupDb::getHighlightName(mGroupId); if (mIsGM != gm) { mIsGM = gm; updateColors(); } showGmBadge(id != 0); } }