diff options
Diffstat (limited to 'src/being.cpp')
-rw-r--r-- | src/being.cpp | 1162 |
1 files changed, 665 insertions, 497 deletions
diff --git a/src/being.cpp b/src/being.cpp index be8afa79..f65afbd3 100644 --- a/src/being.cpp +++ b/src/being.cpp @@ -21,104 +21,162 @@ #include "being.h" +#include "actorspritemanager.h" #include "animatedsprite.h" #include "client.h" #include "configuration.h" #include "effectmanager.h" +#include "event.h" +#include "game.h" #include "graphics.h" +#include "guild.h" #include "localplayer.h" #include "log.h" #include "map.h" #include "particle.h" +#include "party.h" +#include "playerrelations.h" #include "simpleanimation.h" #include "sound.h" #include "text.h" -#include "statuseffect.h" #include "gui/gui.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/playerhandler.h" +#include "net/npchandler.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/resourcemanager.h" +#include "resources/monsterdb.h" +#include "resources/npcdb.h" +#include "resources/theme.h" +#include "resources/userpalette.h" - -#include "utils/dtor.h" #include "utils/stringutils.h" -#include "utils/xml.h" -#include "net/net.h" -#include "net/playerhandler.h" #include <cassert> #include <cmath> -#define BEING_EFFECTS_FILE "effects.xml" #define HAIR_FILE "hair.xml" -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, int subtype, Map *map): - mFrame(0), - mWalkTime(0), - mEmotion(0), mEmotionTime(0), +Being::Being(int id, Type type, int subtype, Map *map): + ActorSprite(id), + mInfo(BeingInfo::Unknown), + mActionTime(0), mSpeechTime(0), mAttackSpeed(350), mAction(STAND), - mSubType(subtype), - mId(id), + mSubType(0xFFFF), mDirection(DOWN), mSpriteDirection(DIRECTION_DOWN), - mMap(NULL), mDispName(0), mShowName(false), mEquippedWeapon(NULL), mText(0), - mStunMode(0), - mAlpha(1.0f), - mStatusParticleEffects(&mStunParticleEffects, false), - mChildParticleEffects(&mStatusParticleEffects, false), - mMustResetParticles(false), - mX(0), mY(0), + mGender(GENDER_UNSPECIFIED), + mParty(NULL), + mIsGM(false), + mType(type), + mSpeedPixelsPerTick(Vector(0.0f, 0.0f, 0.0f)), mDamageTaken(0), - mUsedTargetCursor(NULL) + mIp(0) { setMap(map); + setSubtype(subtype); mSpeechBubble = new SpeechBubble; - mNameColor = &userPalette->getColor(UserPalette::NPC); - mTextColor = &Theme::getThemeColor(Theme::CHAT); - mWalkSpeed = Net::getPlayerHandler()->getDefaultWalkSpeed(); -} + mMoveSpeed = Net::getPlayerHandler()->getDefaultMoveSpeed(); -Being::~Being() -{ - mUsedTargetCursor = NULL; - delete_all(mSprites); + if (getType() == PLAYER) + mShowName = config.getBoolValue("visiblenames"); - if (player_node && player_node->getTarget() == this) - player_node->setTarget(NULL); + if (getType() == PLAYER || getType() == NPC) + setShowName(true); - setMap(NULL); + updateColors(); + listen(Event::ConfigChannel); + listen(Event::ChatChannel); +} +Being::~Being() +{ 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::setMoveSpeed(const Vector &speed) +{ + mMoveSpeed = speed; + // If we already can, recalculate the system speed right away. + if (mMap) + mSpeedPixelsPerTick = + Net::getPlayerHandler()->getPixelsPerTickMoveSpeed(speed); } void Being::setPosition(const Vector &pos) { - mPos = pos; + Actor::setPosition(pos); updateCoords(); @@ -129,38 +187,46 @@ void Being::setPosition(const Vector &pos) 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) + 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)) + const int tileWidth = mMap->getTileWidth(); + const int tileHeight = mMap->getTileHeight(); + if (!mMap->getWalk(dstX / tileWidth, dstY / tileHeight)) return; - Position dest = mMap->checkNodeOffsets(getCollisionRadius(), getWalkMask(), - dstX, dstY); - Path thisPath = mMap->findPixelPath(mPos.x, mPos.y, dest.x, dest.y, - getCollisionRadius(), getWalkMask()); + Position dest(0, 0); + Path thisPath; + if (Net::getPlayerHandler()->usePixelPrecision()) + { + dest = mMap->checkNodeOffsets(getCollisionRadius(), getWalkMask(), + dstX, dstY); + thisPath = mMap->findPixelPath((int) mPos.x, (int) mPos.y, + dest.x, dest.y, + getCollisionRadius(), getWalkMask()); + } + else + { + // We center the destination. + dest.x = (dstX / tileWidth) * tileWidth + tileWidth / 2; + dest.y = (dstY / tileHeight) * tileHeight + tileHeight / 2; + // and find a tile centered pixel path + thisPath = mMap->findTilePath((int) mPos.x, (int) mPos.y, + dest.x, dest.y, 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) + if ((int)mPos.x / tileWidth == dest.x / tileWidth + && (int)mPos.y / tileHeight == dest.y / tileHeight) { mDest.x = dest.x; mDest.y = dest.y; @@ -184,13 +250,6 @@ void Being::clearPath() void Being::setPath(const Path &path) { mPath = path; - - if ((Net::getNetworkType() == ServerInfo::TMWATHENA) && - mAction != WALK && mAction != DEAD) - { - nextTile(); - mWalkTime = tick_time; - } } void Being::setSpeech(const std::string &text, int time) @@ -235,7 +294,7 @@ void Being::setSpeech(const std::string &text, int time) if (!mSpeech.empty()) mSpeechTime = time <= SPEECH_MAX_TIME ? time : SPEECH_MAX_TIME; - const int speech = (int) config.getValue("speech", TEXT_OVERHEAD); + const int speech = config.getIntValue("speech"); if (speech == TEXT_OVERHEAD) { if (mText) @@ -249,7 +308,8 @@ void Being::setSpeech(const std::string &text, int time) } } -void Being::takeDamage(Being *attacker, int amount, AttackType type) +void Being::takeDamage(Being *attacker, int amount, + AttackType type, int attackId) { gcn::Font *font; std::string damage = amount ? toString(amount) : type == FLEE ? @@ -304,99 +364,220 @@ void Being::takeDamage(Being *attacker, int amount, AttackType type) // Show damage number particleEngine->addTextSplashEffect(damage, - getPixelX(), getPixelY() - 16, + getPixelX(), getPixelY() - getHeight(), color, font, true); if (amount > 0) { + if (mInfo) + { + if (attacker) + { + sound.playSfx(mInfo->getSound(SOUND_EVENT_HURT), + attacker->getTileX(), attacker->getTileY()); + } + else + { + sound.playSfx(mInfo->getSound(SOUND_EVENT_HURT)); + } + } + if (getType() == MONSTER) { mDamageTaken += amount; updateName(); } - if (type != CRITICAL) - effectManager->trigger(26, this); + // Init the particle effect path based on current weapon or default. + int hitEffectId = 0; + const ItemInfo *attackerWeapon = attacker ? + attacker->getEquippedWeapon() : 0; + + if (attackerWeapon && attacker->getType() == PLAYER) + { + if (type != CRITICAL) + hitEffectId = attackerWeapon->getHitEffectId(); + else + hitEffectId = attackerWeapon->getCriticalHitEffectId(); + } + else if (attacker && attacker->getType() == MONSTER) + { + const Attack *attack = attacker->getInfo()->getAttack(attackId); + + if (type != CRITICAL) + hitEffectId = attack->mHitEffectId; + else + hitEffectId = attack->mCriticalHitEffectId; + } else - effectManager->trigger(28, this); + { + if (type != CRITICAL) + hitEffectId = paths.getIntValue("hitEffectId"); + else + hitEffectId = paths.getIntValue("criticalHitEffectId"); + } + effectManager->trigger(hitEffectId, this); } } -void Being::handleAttack(Being *victim, int damage, AttackType type) +void Being::handleAttack(Being *victim, int damage, int attackId) { + // Monsters, NPCs and remote players handle the first attack (id="1") + // per default. + // TODO: Fix this for Manaserv by sending the attack id. + // TODO: Add attack type handling, see Attack struct and AttackType + // and make use of it by grouping attacks per attack type and add random + // attack use on tA, based on normal and critical attack types. if (this != player_node) - setAction(Being::ATTACK, 1); + setAction(Being::ATTACK, attackId); - if (getType() == PLAYER && victim) - { - if (mEquippedWeapon) - { - fireMissile(victim, mEquippedWeapon->getMissileParticle()); - } - } - if (Net::getNetworkType() == ServerInfo::TMWATHENA) - { - mFrame = 0; - mWalkTime = tick_time; - } + if (victim) + lookAt(victim->getPosition()); + + if (getType() == PLAYER && victim && mEquippedWeapon) + fireMissile(victim, mEquippedWeapon->getMissileParticleFile()); + else + fireMissile(victim, + mInfo->getAttack(attackId)->mMissileParticleFilename); + + sound.playSfx(mInfo->getSound((damage > 0) ? + SOUND_EVENT_HIT : SOUND_EVENT_MISS), + getPixelX(), getPixelY()); } void Being::setName(const std::string &name) { - mName = name; - - if (getShowName()) + if (getType() == NPC) + { + mName = name.substr(0, name.find('#', 0)); showName(); + } + else + { + mName = name; + + if (getType() == PLAYER && getShowName()) + showName(); + } } void Being::setShowName(bool doShowName) { - bool oldShow = mShowName; + if (mShowName == doShowName) + return; + mShowName = doShowName; - if (doShowName != oldShow) + if (doShowName) + showName(); + else { - if (doShowName) - showName(); - else - { - delete mDispName; - mDispName = 0; - } + 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); + 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); + logger->log("Got guild position \"%s\" for being %s(%i)", pos.c_str(), + mName.c_str(), mId); } -void Being::setMap(Map *map) +void Being::addGuild(Guild *guild) { - // Remove sprite from potential previous map - if (mMap) - mMap->removeSprite(mMapSprite); + mGuilds[guild->getId()] = guild; + guild->addMember(mId, mName); - mMap = map; + if (this == player_node && socialWindow) + { + socialWindow->addTab(guild); + } +} - // Add sprite to potential new map - if (mMap) - mMapSprite = mMap->addSprite(this); +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<int, Guild*>::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; +} - // Clear particle effect list because child particles became invalid - mChildParticleEffects.clear(); - mMustResetParticles = true; // Reset status particles on next redraw +Guild *Being::getGuild(int id) const +{ + std::map<int, Guild*>::const_iterator itr; + itr = mGuilds.find(id); + if (itr != mGuilds.end()) + { + return itr->second; + } + + return NULL; +} + +void Being::clearGuilds() +{ + std::map<int, Guild*>::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::controlParticle(Particle *particle) +void Being::setParty(Party *party) { - mChildParticleEffects.addLocally(particle); + if (party == mParty) + return; + + Party *old = mParty; + mParty = party; + + if (old) + { + old->removeMember(mId); + } + + 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) @@ -404,72 +585,179 @@ 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()); + Particle *missile = particleEngine->addEffect(particle, + getPixelX(), getPixelY()); if (missile) { - target->setLifetime(2000); - target->moveBy(Vector(0.0f, 0.0f, 32.0f)); + Particle *target = particleEngine->createChild(); + target->moveBy(Vector(0.0f, 0.0f, + Game::instance()->getCurrentTileWidth())); + target->setLifetime(1000); victim->controlParticle(target); missile->setDestination(target, 7, 0); missile->setDieDistance(8); missile->setLifetime(900); } + } -void Being::setAction(Action action, int attackType) +void Being::setAction(Action action, int attackId) { - SpriteAction currentAction = ACTION_INVALID; + std::string currentAction = SpriteAction::INVALID; switch (action) { - case WALK: - currentAction = ACTION_WALK; + case MOVE: + currentAction = SpriteAction::MOVE; + // Note: When adding a run action, + // Differentiate walk and run with action name, + // while using only the ACTION_MOVE. break; case SIT: - currentAction = ACTION_SIT; + currentAction = SpriteAction::SIT; break; case ATTACK: if (mEquippedWeapon) - currentAction = mEquippedWeapon->getAttackType(); + { + currentAction = mEquippedWeapon->getAttackAction(); + reset(); + } else - currentAction = ACTION_ATTACK; + { + currentAction = mInfo->getAttack(attackId)->mAction; + reset(); + + // Attack particle effect + if (Particle::enabled) + { + int effectId = mInfo->getAttack(attackId)->mEffectId; + int rotation = 0; + switch (mSpriteDirection) + { + case DIRECTION_DOWN: rotation = 0; break; + case DIRECTION_LEFT: rotation = 90; break; + case DIRECTION_UP: rotation = 180; break; + case DIRECTION_RIGHT: rotation = 270; break; + default: break; + } + effectManager->trigger(effectId, this, rotation); + } + + } - for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++) - if (*it) - (*it)->reset(); break; case HURT: - //currentAction = ACTION_HURT; // Buggy: makes the player stop + //currentAction = SpriteAction::HURT;// Buggy: makes the player stop // attacking and unable to attack - // again until he moves + // again until he moves. + // TODO: fix this! break; case DEAD: - currentAction = ACTION_DEAD; + currentAction = SpriteAction::DEAD; + sound.playSfx(mInfo->getSound(SOUND_EVENT_DIE), + getPixelX(), getPixelY()); break; case STAND: - currentAction = ACTION_STAND; + currentAction = SpriteAction::STAND; break; } - if (currentAction != ACTION_INVALID) + if (currentAction != SpriteAction::INVALID) { - for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++) - if (*it) - (*it)->play(currentAction); + play(currentAction); mAction = action; } + + if (currentAction != SpriteAction::MOVE) + mActionTime = tick_time; } -void Being::setDirection(Uint8 direction) +void Being::lookAt(const Vector &destPos) { - if (Net::getNetworkType() == ServerInfo::MANASERV) + // We first handle simple cases + + // If the two positions are the same, + // don't update the direction since it's only a matter of keeping + // the previous one. + if (mPos.x == destPos.x && mPos.y == destPos.y) + return; + + if (mPos.x == destPos.x) + { + if (mPos.y > destPos.y) + setDirection(UP); + else + setDirection(DOWN); + return; + } + + if (mPos.y == destPos.y) + { + if (mPos.x > destPos.x) + setDirection(LEFT); + else + setDirection(RIGHT); + return; + } + + // Now let's handle diagonal cases + // First, find the lower angle: + if (mPos.x < destPos.x) { - if (mDirection == direction) + // Up-right direction + if (mPos.y > destPos.y) + { + // Compute tan of the angle + if ((mPos.y - destPos.y) / (destPos.x - mPos.x) < 1) + // The angle is less than 45°, we look to the right + setDirection(RIGHT); + else + setDirection(UP); return; + } + else // Down-right + { + // Compute tan of the angle + if ((destPos.y - mPos.y) / (destPos.x - mPos.x) < 1) + // The angle is less than 45°, we look to the right + setDirection(RIGHT); + else + setDirection(DOWN); + return; + } } + else + { + // Up-left direction + if (mPos.y > destPos.y) + { + // Compute tan of the angle + if ((mPos.y - destPos.y) / (mPos.x - destPos.x) < 1) + // The angle is less than 45°, we look to the left + setDirection(LEFT); + else + setDirection(UP); + return; + } + else // Down-left + { + // Compute tan of the angle + if ((destPos.y - mPos.y) / (mPos.x - destPos.x) < 1) + // The angle is less than 45°, we look to the left + setDirection(LEFT); + else + setDirection(DOWN); + return; + } + } +} + +void Being::setDirection(Uint8 direction) +{ + if (!direction || mDirection == direction) + return; mDirection = direction; @@ -484,50 +772,12 @@ void Being::setDirection(Uint8 direction) dir = DIRECTION_LEFT; mSpriteDirection = dir; - for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++) - if (*it) - (*it)->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); - mWalkTime += (int)(mWalkSpeed.x / 10); + CompoundSprite::setDirection(dir); } int Being::getCollisionRadius() const { - // FIXME: Get this from XML file + // FIXME: Get this from XML file once a better pathfinding algorithm is up. return 16; } @@ -544,35 +794,42 @@ void Being::logic() mText = 0; } - if ((Net::getNetworkType() == ServerInfo::MANASERV) && (mAction != DEAD)) + if ((mAction != DEAD) && !mSpeedPixelsPerTick.isNull()) { 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) + // Avoid going to flawed destinations + if (dest.x <= 0 || dest.y <= 0) + { + // We make the being stop move in that case. + mDest = mPos; + mPath.clear(); + // By returning now, we're losing one tick for the rest of the logic + // but as we have reset the destination, the next tick will be fine. return; + } // The Vector representing the difference between current position // and the next destination path node. Vector dir = dest - mPos; - const float nominalLength = dir.length(); + float distance = dir.length(); // When we've not reached our destination, move to it. - if (nominalLength > 0.0f && !mWalkSpeed.isNull()) + if (distance > 0.0f) { // 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); + Vector diff(normalizedDir.x * mSpeedPixelsPerTick.x, + normalizedDir.y * mSpeedPixelsPerTick.y); // Test if we don't miss the destination by a move too far: - if (diff.length() > nominalLength) + if (diff.length() > distance) { setPosition(mPos + dir); @@ -581,32 +838,45 @@ void Being::logic() if (!mPath.empty()) mPath.pop_front(); } - // Otherwise, go to it using the nominal speed. else + { + // Otherwise, go to it using the nominal speed. setPosition(mPos + diff); + // And reset the nominalLength to the actual move length + distance = diff.length(); + } - if (mAction != WALK) - setAction(WALK); + if (mAction != MOVE) + setAction(MOVE); // Update the player sprite direction. - // N.B.: We only change this if the distance is more than one pixel. - if (nominalLength > 1.0f) + // N.B.: We only change this if the distance is more than one pixel + // to avoid flawing the ending direction for players, + // but always for very slow beings. + float maxDistance = mSpeedPixelsPerTick.length(); + if (distance > ((maxDistance > 1.0f) ? 1.0f : 0.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); + // The player direction is handled for keyboard + // by LocalPlayer::startWalking(), we shouldn't get + // in the way here for other cases. + // Hence, we set the direction in Being::logic() only when: + // 1. It is not the localPlayer + // 2. When it is the localPlayer but only by mouse + // (because in that case, the path can have more than one tile.) + if ((player_node == this && player_node->isPathSetByMouse()) + || player_node != this) + { + int direction = 0; + const float dx = std::abs(dir.x); + float dy = std::abs(dir.y); + + if (dx > dy) + direction |= (dir.x > 0) ? RIGHT : LEFT; + else + direction |= (dir.y > 0) ? DOWN : UP; + + setDirection(direction); + } } } else if (!mPath.empty()) @@ -615,108 +885,30 @@ void Being::logic() // remove it and go to the next one. mPath.pop_front(); } - else if (mAction == WALK) + else if (mAction == MOVE) { setAction(STAND); } } - else if (Net::getNetworkType() == ServerInfo::TMWATHENA) - { - // Update pixel coordinates - setPosition(mX * 32 + 16 + getXOffset(), - mY * 32 + 32 + getYOffset()); - } - - if (mEmotion != 0) - { - mEmotionTime--; - if (mEmotionTime == 0) - mEmotion = 0; - } - - // Update sprite animations - if (mUsedTargetCursor) - mUsedTargetCursor->update(tick_time * MILLISECONDS_IN_A_TICK); - - for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++) - if (*it) - (*it)->update(tick_time * MILLISECONDS_IN_A_TICK); - - // Restart status/particle effects, if needed - if (mMustResetParticles) - { - mMustResetParticles = false; - for (std::set<int>::iterator it = mStatusEffects.begin(); - it != mStatusEffects.end(); it++) - { - const StatusEffect *effect = StatusEffect::getStatusEffect(*it, true); - if (effect && effect->particleEffectIsPersistent()) - updateStatusEffect(*it, true); - } - } - // Update particle effects - mChildParticleEffects.moveTo(mPos.x, mPos.y); -} + ActorSprite::logic(); -void Being::draw(Graphics *graphics, int offsetX, int offsetY) const -{ - // TODO: Eventually, we probably should fix all sprite offsets so that - // these translations aren't necessary anymore. The sprites know - // best where their base point should be. - const int px = getPixelX() + offsetX - 16; - // Temporary fix to the Y offset. - const int py = getPixelY() + offsetY - - ((Net::getNetworkType() == ServerInfo::MANASERV) ? 15 : 32); - - if (mUsedTargetCursor) - mUsedTargetCursor->draw(graphics, px, py); - - for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++) + // Remove it after 1.5 secs if the dead animation isn't long enough, + // or simply play it until it's finished. + if (!isAlive() && Net::getGameHandler()->removeDeadBeings() && + get_elapsed_time(mActionTime) > std::max(getDuration(), 1500)) { - if (*it) - { - if ((*it)->getAlpha() != mAlpha) - (*it)->setAlpha(mAlpha); - (*it)->draw(graphics, px, py); - } - } -} - -void Being::drawSpriteAt(Graphics *graphics, int x, int y) const -{ - const int px = x - 16; - const int py = y - 32; - for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++) - { - if (*it) - { - if ((*it)->getAlpha() != mAlpha) - (*it)->setAlpha(mAlpha); - (*it)->draw(graphics, px, py); - } + if (getType() != PLAYER) + 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); + const int speech = config.getIntValue("speech"); // Draw speech above this being if (mSpeechTime == 0) @@ -766,291 +958,267 @@ void Being::drawSpeech(int offsetX, int offsetY) } } -void Being::setStatusEffectBlock(int offset, Uint16 newEffects) +void Being::updateCoords() { - for (int i = 0; i < STATUS_EFFECTS; i++) - { - int index = StatusEffect::blockEffectIndexToEffectIndex(offset + i); + if (!mDispName) + return; - if (index != -1) - setStatusEffect(index, (newEffects & (1 << i)) > 0); - } + // Monster names show above the sprite instead of below it + if (getType() == MONSTER) + mDispName->adviseXY(getPixelX(), getPixelY() - getHeight()); + else + mDispName->adviseXY(getPixelX(), getPixelY() + mDispName->getHeight()); } -void Being::handleStatusEffect(StatusEffect *effect, int effectId) +void Being::flashName(int time) { - if (!effect) - return; + if (mDispName) + mDispName->flash(time); +} + +void Being::showName() +{ + delete mDispName; + mDispName = 0; + std::string mDisplayName(mName); - // TODO: Find out how this is meant to be used - // (SpriteAction != Being::Action) - //SpriteAction action = effect->getAction(); - //if (action != ACTION_INVALID) - // setAction(action); + Being* player = static_cast<Being*>(this); + if (player) + { + if (config.getBoolValue("showgender")) + { + if (getGender() == GENDER_FEMALE) + mDisplayName += " \u2640"; + else if (getGender() == GENDER_MALE) + mDisplayName += " \u2642"; + } - Particle *particle = effect->getParticle(); + // Display the IP when under tmw-Athena (GM only). + if (Net::getNetworkType() == ServerInfo::TMWATHENA && player_node + && player_node->getShowIp() && player->getIp()) + { + mDisplayName += strprintf(" %s", ipToString(player->getIp())); + } + } - if (effectId >= 0) + if (getType() == MONSTER) { - mStatusParticleEffects.setLocally(effectId, particle); + if (config.getBoolValue("showMonstersTakedDamage")) + { + mDisplayName += ", " + toString(getDamageTaken()); + } } - else + + gcn::Font *font = 0; + if (player_node && player_node->getTarget() == this + && getType() != MONSTER) { - mStunParticleEffects.clearLocally(); - if (particle) - mStunParticleEffects.addLocally(particle); + font = boldFont; } -} -void Being::updateStunMode(int oldMode, int newMode) -{ - handleStatusEffect(StatusEffect::getStatusEffect(oldMode, false), -1); - handleStatusEffect(StatusEffect::getStatusEffect(newMode, true), -1); -} + mDispName = new FlashText(mDisplayName, getPixelX(), getPixelY(), + gcn::Graphics::CENTER, mNameColor, font); -void Being::updateStatusEffect(int index, bool newStatus) -{ - handleStatusEffect(StatusEffect::getStatusEffect(index, newStatus), index); + updateCoords(); } -void Being::setStatusEffect(int index, bool active) +void Being::updateColors() { - const bool wasActive = mStatusEffects.find(index) != mStatusEffects.end(); - - if (active != wasActive) + if (getType() == MONSTER) + { + mNameColor = &userPalette->getColor(UserPalette::MONSTER); + mTextColor = &userPalette->getColor(UserPalette::MONSTER); + } + else if (getType() == NPC) { - updateStatusEffect(index, active); - if (active) - mStatusEffects.insert(index); + 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 - mStatusEffects.erase(index); + { + mNameColor = &userPalette->getColor(UserPalette::PC); + } + } + + if (mDispName) + { + mDispName->setColor(mNameColor); } } -/** TODO: eAthena only */ -int Being::getOffset(char pos, char neg) const +void Being::setSprite(unsigned int slot, int id, const std::string &color, + bool isWeapon) { - // Check whether we're walking in the requested direction - if (mAction != WALK || !(mDirection & (pos | neg))) - return 0; + assert(slot < Net::getCharHandler()->maxSprite()); - int offset = 0; + if (slot >= size()) + ensureSize(slot + 1); - if (mMap) + if (slot >= mSpriteIDs.size()) + mSpriteIDs.resize(slot + 1); + + if (slot >= mSpriteColors.size()) + mSpriteColors.resize(slot + 1); + + // id = 0 means unequip + if (id == 0) { - offset = (pos == LEFT && neg == RIGHT) ? - (int)((get_elapsed_time(mWalkTime) - * mMap->getTileWidth()) / mWalkSpeed.x) : - (int)((get_elapsed_time(mWalkTime) - * mMap->getTileHeight()) / mWalkSpeed.y); + removeSprite(slot); + + if (isWeapon) + mEquippedWeapon = 0; } + else + { + std::string filename = itemDb->get(id).getSprite(mGender); + AnimatedSprite *equipmentSprite = 0; - // We calculate the offset _from_ the _target_ location - offset -= 32; - if (offset > 0) - offset = 0; + if (!filename.empty()) + { + if (!color.empty()) + filename += "|" + color; - // Going into negative direction? Invert the offset. - if (mDirection & pos) - offset = -offset; + equipmentSprite = AnimatedSprite::load( + paths.getStringValue("sprites") + filename); + } - return offset; -} + if (equipmentSprite) + equipmentSprite->setDirection(getSpriteDirection()); -int Being::getWidth() const -{ - AnimatedSprite *base = NULL; + CompoundSprite::setSprite(slot, equipmentSprite); - for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++) - if ((base = (*it))) - break; + if (isWeapon) + mEquippedWeapon = &itemDb->get(id); - if (base) - return std::max(base->getWidth(), DEFAULT_BEING_WIDTH); + setAction(mAction); + } - return DEFAULT_BEING_WIDTH; + mSpriteIDs[slot] = id; + mSpriteColors[slot] = color; } -int Being::getHeight() const +void Being::setSpriteID(unsigned int slot, int id) { - AnimatedSprite *base = NULL; - - for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++) - if ((base = (*it))) - break; - - if (base) - return std::max(base->getHeight(), DEFAULT_BEING_HEIGHT); - - return DEFAULT_BEING_HEIGHT; + setSprite(slot, id, mSpriteColors[slot]); } -void Being::setTargetAnimation(SimpleAnimation *animation) +void Being::setSpriteColor(unsigned int slot, const std::string &color) { - mUsedTargetCursor = animation; - mUsedTargetCursor->reset(); + setSprite(slot, mSpriteIDs[slot], color); } -struct EffectDescription { - std::string mGFXEffect; - std::string mSFXEffect; -}; - -static EffectDescription *default_effect = NULL; -static std::map<int, EffectDescription *> effects; -static bool effects_initialized = false; +int Being::getNumberOfLayers() const +{ + return CompoundSprite::getNumberOfLayers(); +} -static EffectDescription *getEffectDescription(xmlNodePtr node, int *id) +void Being::load() { - EffectDescription *ed = new EffectDescription; + // Hairstyles are encoded as negative numbers. Count how far negative + // we can go. + int hairstyles = 1; - *id = atoi(XML::getProperty(node, "id", "-1").c_str()); - ed->mSFXEffect = XML::getProperty(node, "audio", ""); - ed->mGFXEffect = XML::getProperty(node, "particle", ""); + while (itemDb->get(-hairstyles).getSprite(GENDER_MALE) != + paths.getStringValue("spriteErrorFile")) + hairstyles++; - return ed; + mNumberOfHairstyles = hairstyles; } -static EffectDescription *getEffectDescription(int effectId) +void Being::updateName() { - if (!effects_initialized) - { - XML::Document doc(BEING_EFFECTS_FILE); - xmlNodePtr root = doc.rootNode(); + if (mShowName) + showName(); +} - if (!root || !xmlStrEqual(root->name, BAD_CAST "being-effects")) - { - logger->log("Error loading being effects file: " - BEING_EFFECTS_FILE); - return NULL; - } +void Being::setGender(Gender gender) +{ + if (gender != mGender) + { + mGender = gender; - for_each_xml_child_node(node, root) + // Reload all subsprites + for (unsigned int i = 0; i < mSpriteIDs.size(); i++) { - int id; - - if (xmlStrEqual(node->name, BAD_CAST "effect")) - { - EffectDescription *EffectDescription = - getEffectDescription(node, &id); - effects[id] = EffectDescription; - } - else if (xmlStrEqual(node->name, BAD_CAST "default")) - { - EffectDescription *effectDescription = - getEffectDescription(node, &id); - - if (default_effect) - delete default_effect; - - default_effect = effectDescription; - } + if (mSpriteIDs.at(i) != 0) + setSprite(i, mSpriteIDs.at(i), mSpriteColors.at(i)); } - effects_initialized = true; - } // done initializing - - EffectDescription *ed = effects[effectId]; - - return ed ? ed : default_effect; + updateName(); + } } -void Being::internalTriggerEffect(int effectId, bool sfx, bool gfx) +void Being::setGM(bool gm) { - logger->log("Special effect #%d on %s", effectId, - getId() == player_node->getId() ? "self" : "other"); + mIsGM = gm; - EffectDescription *ed = getEffectDescription(effectId); - - if (!ed) - { - logger->log("Unknown special effect and no default recorded"); - return; - } - - if (gfx && !ed->mGFXEffect.empty()) - { - Particle *selfFX; - - selfFX = particleEngine->addEffect(ed->mGFXEffect, 0, 0); - controlParticle(selfFX); - } - - if (sfx && !ed->mSFXEffect.empty()) - sound.playSfx(ed->mSFXEffect); + updateColors(); } -void Being::updateCoords() +bool Being::canTalk() { - if (mDispName) - { - mDispName->adviseXY(getPixelX(), getPixelY()); - } + return mType == NPC; } -void Being::flashName(int time) +void Being::talkTo() { - if (mDispName) - mDispName->flash(time); + Net::getNpcHandler()->talk(mId); } -void Being::showName() +void Being::event(Event::Channel channel, const Event &event) { - delete mDispName; - mDispName = 0; - std::string mDisplayName(mName); - - if (getType() == PLAYER) + if (channel == Event::ChatChannel && + (event.getType() == Event::Being + || event.getType() == Event::Player) && + event.getInt("permissions") & PlayerRelation::SPEECH_FLOAT) { - Player* player = static_cast<Player*>(this); - if (player) + try { - if (config.getValue("showgender", false)) - { - if (player->getGender() == GENDER_FEMALE) - mDisplayName += " \u2640"; - else - mDisplayName += " \u2642"; - } - if (Net::getNetworkType() == ServerInfo::TMWATHENA && player_node - && player_node->getShowIp() && player->getIp()) + if (mId == event.getInt("beingId")) { - mDisplayName += strprintf(" %s", ipToString(player->getIp())); + setSpeech(event.getString("text")); } } + catch (BadEvent badEvent) + {} } - else if (getType() == MONSTER) + else if (channel == Event::ConfigChannel && + event.getType() == Event::ConfigOptionChanged) { - if (config.getValue("showMonstersTakedDamage", false)) + if (getType() == PLAYER && event.getString("option") == "visiblenames") { - mDisplayName += ", " + toString(getDamageTaken()); + setShowName(config.getBoolValue("visiblenames")); } } - mDispName = new FlashText(mDisplayName, getPixelX(), getPixelY(), - gcn::Graphics::CENTER, mNameColor); -} - -int Being::getNumberOfLayers() const -{ - return mSprites.size(); } -void Being::load() +void Being::setMap(Map *map) { - // Hairstyles are encoded as negative numbers. Count how far negative - // we can go. - int hairstyles = 1; + ActorSprite::setMap(map); - while (ItemDB::get(-hairstyles).getSprite(GENDER_MALE) != - paths.getValue("spriteErrorFile", "error.xml")) - hairstyles++; - - mNumberOfHairstyles = hairstyles; -} - -void Being::updateName() -{ - if (mShowName) - showName(); + // Recalculate pixel/tick speed + if (map && !mMoveSpeed.isNull()) + { + mSpeedPixelsPerTick = + Net::getPlayerHandler()->getPixelsPerTickMoveSpeed(mMoveSpeed, map); + } } |