summaryrefslogtreecommitdiff
path: root/src/being.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/being.cpp')
-rw-r--r--src/being.cpp1162
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);
+ }
}