/* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2010 The Mana Developers * * This file is part of The Mana 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.h" #include "actorspritemanager.h" #include "animatedsprite.h" #include "client.h" #include "configuration.h" #include "effectmanager.h" #include "graphics.h" #include "guild.h" #include "localplayer.h" #include "log.h" #include "map.h" #include "particle.h" #include "party.h" #include "simpleanimation.h" #include "sound.h" #include "sprite.h" #include "text.h" #include "statuseffect.h" #include "gui/buy.h" #include "gui/buysell.h" #include "gui/gui.h" #include "gui/npcdialog.h" #include "gui/npcpostdialog.h" #include "gui/sell.h" #include "gui/socialwindow.h" #include "gui/speechbubble.h" #include "gui/theme.h" #include "gui/userpalette.h" #include "net/charhandler.h" #include "net/gamehandler.h" #include "net/net.h" #include "net/npchandler.h" #include "net/playerhandler.h" #include "resources/beinginfo.h" #include "resources/colordb.h" #include "resources/emotedb.h" #include "resources/image.h" #include "resources/itemdb.h" #include "resources/iteminfo.h" #include "resources/monsterdb.h" #include "resources/npcdb.h" #include "resources/resourcemanager.h" #include "utils/dtor.h" #include "utils/stringutils.h" #include "utils/xml.h" #include #include #define HAIR_FILE "hair.xml" #define PARTICLE_LOCATION "graphics/particles/" static const int DEFAULT_BEING_WIDTH = 32; static const int DEFAULT_BEING_HEIGHT = 32; int Being::mNumberOfHairstyles = 1; // TODO: mWalkTime used by eAthena only Being::Being(int id, Type type, int subtype, Map *map): ActorSprite(id), mInfo(BeingInfo::Unknown), mActionTime(0), mEmotion(0), mEmotionTime(0), mSpeechTime(0), mAttackType(1), mAttackSpeed(350), mAction(STAND), mSubType(0xFFFF), mDirection(DOWN), mSpriteDirection(DIRECTION_DOWN), mDispName(0), mShowName(false), mEquippedWeapon(NULL), mText(0), mGender(GENDER_UNSPECIFIED), mParty(NULL), mIsGM(false), mType(type), mX(0), mY(0), mDamageTaken(0) { setMap(map); setSubtype(subtype); mSpeechBubble = new SpeechBubble; mWalkSpeed = Net::getPlayerHandler()->getDefaultWalkSpeed(); if (getType() == PLAYER) mShowName = config.getValue("visiblenames", 1); config.addListener("visiblenames", this); if (getType() == PLAYER || getType() == NPC) setShowName(true); updateColors(); } Being::~Being() { config.removeListener("visiblenames", this); delete mSpeechBubble; delete mDispName; delete mText; mSpeechBubble = 0; mDispName = 0; mText = 0; } void Being::setSubtype(Uint16 subtype) { if (subtype == mSubType) return; mSubType = subtype; if (getType() == MONSTER) { mInfo = MonsterDB::get(mSubType); setName(mInfo->getName()); setupSpriteDisplay(mInfo->getDisplay()); } else if (getType() == NPC) { mInfo = NPCDB::get(mSubType); setupSpriteDisplay(mInfo->getDisplay(), false); } else if (getType() == PLAYER) { int id = -100 - subtype; // Prevent showing errors when sprite doesn't exist if (!ItemDB::exists(id)) id = -100; setSprite(Net::getCharHandler()->baseSprite(), id); } } ActorSprite::TargetCursorSize Being::getTargetCursorSize() const { return mInfo->getTargetCursorSize(); } unsigned char Being::getWalkMask() const { return mInfo->getWalkMask(); } Map::BlockType Being::getBlockType() const { return mInfo->getBlockType(); } void Being::setPosition(const Vector &pos) { Actor::setPosition(pos); updateCoords(); if (mText) mText->adviseXY((int)pos.x, (int)pos.y - getHeight() - mText->getHeight() - 6); } void Being::setDestination(int dstX, int dstY) { if (Net::getNetworkType() == ServerInfo::TMWATHENA) { if (mMap) setPath(mMap->findPath(mX, mY, dstX, dstY, getWalkMask())); return; } // Manaserv's part: // We can't calculate anything without a map anyway. if (!mMap) return; // Don't handle flawed destinations from server... if (dstX == 0 || dstY == 0) return; // If the destination is unwalkable, don't bother trying to get there if (!mMap->getWalk(dstX / 32, dstY / 32)) return; Position dest = mMap->checkNodeOffsets(getCollisionRadius(), getWalkMask(), dstX, dstY); Path thisPath = mMap->findPixelPath(mPos.x, mPos.y, dest.x, dest.y, getCollisionRadius(), getWalkMask()); if (thisPath.empty()) { // If there is no path but the destination is on the same walkable tile, // we accept it. if ((int)mPos.x / 32 == dest.x / 32 && (int)mPos.y / 32 == dest.y / 32) { mDest.x = dest.x; mDest.y = dest.y; } setPath(Path()); return; } // The destination is valid, so we set it. mDest.x = dest.x; mDest.y = dest.y; setPath(thisPath); } void Being::clearPath() { mPath.clear(); } void Being::setPath(const Path &path) { mPath = path; if ((Net::getNetworkType() == ServerInfo::TMWATHENA) && mAction != WALK && mAction != DEAD) { nextTile(); mActionTime = tick_time; } } void Being::setSpeech(const std::string &text, int time) { // Remove colors mSpeech = removeColors(text); // Trim whitespace trim(mSpeech); // Check for links std::string::size_type start = mSpeech.find('['); std::string::size_type end = mSpeech.find(']', start); while (start != std::string::npos && end != 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) < end)) { start = mSpeech.find('[', start + 1); } std::string::size_type position = mSpeech.find('|'); if (mSpeech[start + 1] == '@' && mSpeech[start + 2] == '@') { mSpeech.erase(end, 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); end = mSpeech.find(']', start); } if (!mSpeech.empty()) mSpeechTime = time <= SPEECH_MAX_TIME ? time : SPEECH_MAX_TIME; const int speech = (int) config.getValue("speech", TEXT_OVERHEAD); if (speech == TEXT_OVERHEAD) { if (mText) delete mText; mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), gcn::Graphics::CENTER, &userPalette->getColor(UserPalette::PARTICLE), true); } } void Being::takeDamage(Being *attacker, int amount, AttackType type) { gcn::Font *font; std::string damage = amount ? toString(amount) : type == FLEE ? "dodge" : "miss"; const gcn::Color *color; font = gui->getInfoParticleFont(); // Selecting the right color if (type == CRITICAL || type == FLEE) { color = &userPalette->getColor(UserPalette::HIT_CRITICAL); } else if (!amount) { if (attacker == player_node) { // This is intended to be the wrong direction to visually // differentiate between hits and misses color = &userPalette->getColor(UserPalette::HIT_MONSTER_PLAYER); } else { color = &userPalette->getColor(UserPalette::MISS); } } else if (getType() == MONSTER) { color = &userPalette->getColor(UserPalette::HIT_PLAYER_MONSTER); } else { color = &userPalette->getColor(UserPalette::HIT_MONSTER_PLAYER); } // Show damage number particleEngine->addTextSplashEffect(damage, getPixelX(), getPixelY() - 16, color, font, true); if (amount > 0) { sound.playSfx(mInfo->getSound(SOUND_EVENT_HURT)); if (getType() == MONSTER) { mDamageTaken += amount; updateName(); } if (type != CRITICAL) effectManager->trigger(26, this); else effectManager->trigger(28, this); } } void Being::handleAttack(Being *victim, int damage, AttackType type) { if (this != player_node) setAction(Being::ATTACK, 1); if (getType() == PLAYER && victim && mEquippedWeapon) fireMissile(victim, mEquippedWeapon->getMissileParticle()); else fireMissile(victim, mInfo->getAttack(mAttackType)->missileParticle); if (Net::getNetworkType() == ServerInfo::TMWATHENA) { reset(); mActionTime = tick_time; } sound.playSfx(mInfo->getSound((damage > 0) ? SOUND_EVENT_HIT : SOUND_EVENT_MISS)); } void Being::setName(const std::string &name) { if (getType() == NPC) { mName = name.substr(0, name.find('#', 0)); showName(); } else { mName = name; if (getType() == PLAYER && getShowName()) showName(); } } void Being::setShowName(bool doShowName) { if (mShowName == doShowName) return; mShowName = doShowName; if (doShowName) showName(); else { delete mDispName; mDispName = 0; } } void Being::setGuildName(const std::string &name) { logger->log("Got guild name \"%s\" for being %s(%i)", name.c_str(), mName.c_str(), mId); } void Being::setGuildPos(const std::string &pos) { logger->log("Got guild position \"%s\" for being %s(%i)", pos.c_str(), mName.c_str(), mId); } void Being::addGuild(Guild *guild) { mGuilds[guild->getId()] = guild; guild->addMember(mId, mName); if (this == player_node && socialWindow) { socialWindow->addTab(guild); } } void Being::removeGuild(int id) { if (this == player_node && socialWindow) { socialWindow->removeTab(mGuilds[id]); } mGuilds[id]->removeMember(mId); mGuilds.erase(id); } Guild *Being::getGuild(const std::string &guildName) const { std::map::const_iterator itr, itr_end = mGuilds.end(); for (itr = mGuilds.begin(); itr != itr_end; ++itr) { Guild *guild = itr->second; if (guild->getName() == guildName) { return guild; } } return NULL; } Guild *Being::getGuild(int id) const { std::map::const_iterator itr; itr = mGuilds.find(id); if (itr != mGuilds.end()) { return itr->second; } return NULL; } void Being::clearGuilds() { std::map::const_iterator itr, itr_end = mGuilds.end(); for (itr = mGuilds.begin(); itr != itr_end; ++itr) { Guild *guild = itr->second; if (this == player_node && socialWindow) socialWindow->removeTab(guild); guild->removeMember(mId); } mGuilds.clear(); } void Being::setParty(Party *party) { if (party == mParty) return; Party *old = mParty; mParty = party; if (old) { old->removeMember(mId); } if (party) { party->addMember(mId, mName); } updateColors(); if (this == player_node && socialWindow) { if (old) socialWindow->removeTab(old); if (party) socialWindow->addTab(party); } } void Being::fireMissile(Being *victim, const std::string &particle) { if (!victim || particle.empty()) return; Particle *target = particleEngine->createChild(); Particle *missile = target->addEffect(particle, getPixelX(), getPixelY()); if (missile) { target->setLifetime(2000); target->moveBy(Vector(0.0f, 0.0f, 32.0f)); victim->controlParticle(target); missile->setDestination(target, 7, 0); missile->setDieDistance(8); missile->setLifetime(900); } } void Being::setAction(Action action, int attackType) { SpriteAction currentAction = ACTION_INVALID; switch (action) { case WALK: currentAction = ACTION_WALK; break; case SIT: currentAction = ACTION_SIT; break; case ATTACK: if (mEquippedWeapon) { currentAction = mEquippedWeapon->getAttackType(); reset(); } else { mAttackType = attackType; currentAction = mInfo->getAttack(attackType)->action; reset(); if (Net::getNetworkType() == ServerInfo::MANASERV) { int rotation = 0; //attack particle effect std::string particleEffect = mInfo->getAttack(attackType) ->particleEffect; if (!particleEffect.empty() && Particle::enabled) { switch (mSpriteDirection) { case DIRECTION_DOWN: rotation = 0; break; case DIRECTION_LEFT: rotation = 90; break; case DIRECTION_UP: rotation = 180; break; case DIRECTION_RIGHT: rotation = 270; break; default: break; } Particle *p; p = particleEngine->addEffect(particleEffect, 0, 0, rotation); controlParticle(p); } } } break; case HURT: //currentAction = ACTION_HURT; // Buggy: makes the player stop // attacking and unable to attack // again until he moves break; case DEAD: currentAction = ACTION_DEAD; sound.playSfx(mInfo->getSound(SOUND_EVENT_DIE)); break; case STAND: currentAction = ACTION_STAND; break; } if (currentAction != ACTION_INVALID) { play(currentAction); mAction = action; } if (currentAction != ACTION_WALK) mActionTime = tick_time; } void Being::setDirection(Uint8 direction) { if (mDirection == direction) return; mDirection = direction; // if the direction does not change much, keep the common component int mFaceDirection = mDirection & direction; if (!mFaceDirection) mFaceDirection = direction; SpriteDirection dir; if (mFaceDirection & UP) dir = DIRECTION_UP; else if (mFaceDirection & DOWN) dir = DIRECTION_DOWN; else if (mFaceDirection & RIGHT) dir = DIRECTION_RIGHT; else dir = DIRECTION_LEFT; mSpriteDirection = dir; CompoundSprite::setDirection(dir); } /** TODO: Used by eAthena only */ void Being::nextTile() { if (mPath.empty()) { setAction(STAND); return; } Position pos = mPath.front(); mPath.pop_front(); int dir = 0; if (pos.x > mX) dir |= RIGHT; else if (pos.x < mX) dir |= LEFT; if (pos.y > mY) dir |= DOWN; else if (pos.y < mY) dir |= UP; setDirection(dir); if (!mMap->getWalk(pos.x, pos.y, getWalkMask())) { setAction(STAND); return; } mX = pos.x; mY = pos.y; setAction(WALK); mActionTime += (int)(mWalkSpeed.x / 10); } int Being::getCollisionRadius() const { // FIXME: Get this from XML file return 16; } void Being::logic() { // Reduce the time that speech is still displayed if (mSpeechTime > 0) mSpeechTime--; // Remove text and speechbubbles if speech boxes aren't being used if (mSpeechTime == 0 && mText) { delete mText; mText = 0; } if ((Net::getNetworkType() == ServerInfo::MANASERV) && (mAction != DEAD)) { const Vector dest = (mPath.empty()) ? mDest : Vector(mPath.front().x, mPath.front().y); // This is a hack that stops NPCs from running off the map... if (mDest.x <= 0 && mDest.y <= 0) return; // The Vector representing the difference between current position // and the next destination path node. Vector dir = dest - mPos; const float nominalLength = dir.length(); // When we've not reached our destination, move to it. if (nominalLength > 0.0f && !mWalkSpeed.isNull()) { // The deplacement of a point along a vector is calculated // using the Unit Vector (â) multiplied by the point speed. // â = a / ||a|| (||a|| is the a length.) // Then, diff = (dir/||dir||) * speed. const Vector normalizedDir = dir.normalized(); Vector diff(normalizedDir.x * mWalkSpeed.x, normalizedDir.y * mWalkSpeed.y); // Test if we don't miss the destination by a move too far: if (diff.length() > nominalLength) { setPosition(mPos + dir); // Also, if the destination is reached, try to get the next // path point, if existing. if (!mPath.empty()) mPath.pop_front(); } // Otherwise, go to it using the nominal speed. else setPosition(mPos + diff); if (mAction != WALK) setAction(WALK); // Update the player sprite direction. // N.B.: We only change this if the distance is more than one pixel. if (nominalLength > 1.0f) { int direction = 0; const float dx = std::abs(dir.x); float dy = std::abs(dir.y); // When not using mouse for the player, we slightly prefer // UP and DOWN position, especially when walking diagonally. if (this == player_node && !player_node->isPathSetByMouse()) dy = dy + 2; if (dx > dy) direction |= (dir.x > 0) ? RIGHT : LEFT; else direction |= (dir.y > 0) ? DOWN : UP; setDirection(direction); } } else if (!mPath.empty()) { // If the current path node has been reached, // remove it and go to the next one. mPath.pop_front(); } else if (mAction == WALK) { setAction(STAND); } } else if (Net::getNetworkType() == ServerInfo::TMWATHENA) { int frameCount = getFrameCount(); switch (mAction) { case STAND: case SIT: case DEAD: case HURT: break; case WALK: if ((int) ((get_elapsed_time(mActionTime) * frameCount) / getWalkSpeed().x) >= frameCount) nextTile(); break; case ATTACK: int rotation = 0; std::string particleEffect = ""; int curFrame = (get_elapsed_time(mActionTime) * frameCount) / mAttackSpeed; //attack particle effect if (mEquippedWeapon) { particleEffect = mEquippedWeapon->getParticleEffect(); if (!particleEffect.empty() && findSameSubstring(particleEffect, PARTICLE_LOCATION).empty()) particleEffect = PARTICLE_LOCATION + particleEffect; } else { particleEffect = mInfo->getAttack(mAttackType) ->particleEffect; } if (!particleEffect.empty() && Particle::enabled && curFrame == 1) { switch (mDirection) { case DOWN: rotation = 0; break; case LEFT: rotation = 90; break; case UP: rotation = 180; break; case RIGHT: rotation = 270; break; default: break; } Particle *p; p = particleEngine->addEffect(particleEffect, 0, 0, rotation); controlParticle(p); } if (curFrame >= frameCount) nextTile(); break; } // Update pixel coordinates setPosition(mX * 32 + 16 + getXOffset(), mY * 32 + 32 + getYOffset()); } if (mEmotion != 0) { mEmotionTime--; if (mEmotionTime == 0) mEmotion = 0; } ActorSprite::logic(); int frameCount = getFrameCount(); if (frameCount < 10) frameCount = 10; if (!isAlive() && Net::getGameHandler()->removeDeadBeings() && (int) ((get_elapsed_time(mActionTime) / getWalkSpeed().x) >= frameCount)) actorSpriteManager->destroy(this); } void Being::drawEmotion(Graphics *graphics, int offsetX, int offsetY) { if (!mEmotion) return; const int px = getPixelX() - offsetX - 16; const int py = getPixelY() - offsetY - 64 - 32; const int emotionIndex = mEmotion - 1; if (emotionIndex >= 0 && emotionIndex <= EmoteDB::getLast()) EmoteDB::getAnimation(emotionIndex)->draw(graphics, px, py); } void Being::drawSpeech(int offsetX, int offsetY) { const int px = getPixelX() - offsetX; const int py = getPixelY() - offsetY; const int speech = (int) config.getValue("speech", TEXT_OVERHEAD); // Draw speech above this being if (mSpeechTime == 0) { if (mSpeechBubble->isVisible()) mSpeechBubble->setVisible(false); } else if (mSpeechTime > 0 && (speech == NAME_IN_BUBBLE || speech == NO_NAME_IN_BUBBLE)) { const bool showName = (speech == NAME_IN_BUBBLE); if (mText) { delete mText; mText = NULL; } mSpeechBubble->setCaption(showName ? mName : "", mTextColor); mSpeechBubble->setText(mSpeech, showName); mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), py - getHeight() - (mSpeechBubble->getHeight())); mSpeechBubble->setVisible(true); } else if (mSpeechTime > 0 && speech == TEXT_OVERHEAD) { mSpeechBubble->setVisible(false); if (! mText) { mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), gcn::Graphics::CENTER, &userPalette->getColor(UserPalette::PARTICLE), true); } } else if (speech == NO_SPEECH) { mSpeechBubble->setVisible(false); if (mText) delete mText; mText = NULL; } } /** TODO: eAthena only */ int Being::getOffset(char pos, char neg) const { // Check whether we're walking in the requested direction if (mAction != WALK || !(mDirection & (pos | neg))) return 0; int offset = 0; if (mMap) { offset = (pos == LEFT && neg == RIGHT) ? (int)((get_elapsed_time(mActionTime) * mMap->getTileWidth()) / mWalkSpeed.x) : (int)((get_elapsed_time(mActionTime) * mMap->getTileHeight()) / mWalkSpeed.y); } // We calculate the offset _from_ the _target_ location offset -= 32; if (offset > 0) offset = 0; // Going into negative direction? Invert the offset. if (mDirection & pos) offset = -offset; return offset; } int Being::getWidth() const { return std::max(CompoundSprite::getWidth(), DEFAULT_BEING_WIDTH); } int Being::getHeight() const { return std::max(CompoundSprite::getHeight(), DEFAULT_BEING_HEIGHT); } void Being::updateCoords() { if (!mDispName) return; // Monster names show above the sprite instead of below it if (getType() == MONSTER) mDispName->adviseXY(getPixelX(), getPixelY() - getHeight() - mDispName->getHeight()); else mDispName->adviseXY(getPixelX(), getPixelY()); } void Being::optionChanged(const std::string &value) { if (getType() == PLAYER && value == "visiblenames") { setShowName(config.getValue("visiblenames", 1)); } } void Being::flashName(int time) { if (mDispName) mDispName->flash(time); } void Being::showName() { delete mDispName; mDispName = 0; std::string mDisplayName(mName); if (config.getValue("showgender", false)) { if (getGender() == GENDER_FEMALE) mDisplayName += " \u2640"; else if (getGender() == GENDER_MALE) mDisplayName += " \u2642"; } if (getType() == MONSTER) { if (config.getValue("showMonstersTakedDamage", false)) { mDisplayName += ", " + toString(getDamageTaken()); } } gcn::Font *font = 0; if (player_node && player_node->getTarget() == this && getType() != MONSTER) { font = boldFont; } mDispName = new FlashText(mDisplayName, getPixelX(), getPixelY(), gcn::Graphics::CENTER, mNameColor, font); updateCoords(); } void Being::updateColors() { if (getType() == MONSTER) { mNameColor = &userPalette->getColor(UserPalette::MONSTER); mTextColor = &userPalette->getColor(UserPalette::MONSTER); } else if (getType() == NPC) { mNameColor = &userPalette->getColor(UserPalette::NPC); mTextColor = &userPalette->getColor(UserPalette::NPC); } else if (this == player_node) { mNameColor = &userPalette->getColor(UserPalette::SELF); mTextColor = &Theme::getThemeColor(Theme::PLAYER); } else { mTextColor = &userPalette->getColor(Theme::PLAYER); if (mIsGM) { mTextColor = &userPalette->getColor(UserPalette::GM); mNameColor = &userPalette->getColor(UserPalette::GM); } else if (mParty && mParty == player_node->getParty()) { mNameColor = &userPalette->getColor(UserPalette::PARTY); } else { mNameColor = &userPalette->getColor(UserPalette::PC); } } if (mDispName) { mDispName->setColor(mNameColor); } } void Being::setSprite(unsigned int slot, int id, const std::string &color, bool isWeapon) { assert(slot < Net::getCharHandler()->maxSprite()); if (slot >= size()) ensureSize(slot + 1); if (slot >= mSpriteIDs.size()) mSpriteIDs.resize(slot + 1, 0); if (slot >= mSpriteColors.size()) mSpriteColors.resize(slot + 1, ""); // id = 0 means unequip if (id == 0) { removeSprite(slot); if (isWeapon) mEquippedWeapon = NULL; } else { std::string filename = ItemDB::get(id).getSprite(mGender); AnimatedSprite *equipmentSprite = NULL; if (!filename.empty()) { if (!color.empty()) filename += "|" + color; equipmentSprite = AnimatedSprite::load("graphics/sprites/" + filename); } if (equipmentSprite) equipmentSprite->setDirection(getSpriteDirection()); CompoundSprite::setSprite(slot, equipmentSprite); if (isWeapon) mEquippedWeapon = &ItemDB::get(id); setAction(mAction); } mSpriteIDs[slot] = id; mSpriteColors[slot] = color; } void Being::setSpriteID(unsigned int slot, int id) { setSprite(slot, id, mSpriteColors[slot]); } void Being::setSpriteColor(unsigned int slot, const std::string &color) { setSprite(slot, mSpriteIDs[slot], color); } int Being::getNumberOfLayers() const { return CompoundSprite::getNumberOfLayers(); } void Being::load() { // Hairstyles are encoded as negative numbers. Count how far negative // we can go. int hairstyles = 1; while (ItemDB::get(-hairstyles).getSprite(GENDER_MALE) != "error.xml") hairstyles++; mNumberOfHairstyles = hairstyles; } void Being::updateName() { if (mShowName) showName(); } void Being::setGender(Gender gender) { if (gender != mGender) { mGender = gender; // Reload all subsprites for (unsigned int i = 0; i < mSpriteIDs.size(); i++) { if (mSpriteIDs.at(i) != 0) setSprite(i, mSpriteIDs.at(i), mSpriteColors.at(i)); } updateName(); } } void Being::setGM(bool gm) { mIsGM = gm; updateColors(); } bool Being::canTalk() { return mType == NPC; } void Being::talkTo() { Net::getNpcHandler()->talk(mId); } bool Being::isTalking() { return NpcDialog::isActive() || BuyDialog::isActive() || SellDialog::isActive() || BuySellDialog::isActive() || NpcPostDialog::isActive(); }