/* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team * Copyright (C) 2009-2012 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 "client.h" #include "configuration.h" #include "effectmanager.h" #include "event.h" #include "game.h" #include "guild.h" #include "localplayer.h" #include "log.h" #include "map.h" #include "particle.h" #include "party.h" #include "playerrelations.h" #include "sound.h" #include "sprite.h" #include "statuseffect.h" #include "text.h" #include "gui/gui.h" #include "gui/socialwindow.h" #include "gui/speechbubble.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/itemdb.h" #include "resources/iteminfo.h" #include "resources/monsterdb.h" #include "resources/npcdb.h" #include "resources/statuseffectdb.h" #include "resources/theme.h" #include "resources/userpalette.h" #include "utils/stringutils.h" #include Being::Being(int id, Type type, int subtype, Map *map) : ActorSprite(id) , mInfo(BeingInfo::Unknown) { setMap(map); setType(type, subtype); mSpeechBubble = new SpeechBubble; mSpeechBubble->addDeathListener(this); mMoveSpeed = Net::getPlayerHandler()->getDefaultMoveSpeed(); listen(Event::ConfigChannel); listen(Event::ChatChannel); } Being::~Being() { delete mSpeechBubble; delete mDispName; delete mText; } /** * Can be used to change the type of the being. * * Practical use: players (usually GMs) can change into monsters and back. */ void Being::setType(Type type, int subtype) { if (mType == type && mSubType == subtype) return; mType = type; mSubType = subtype; for (auto &spriteState : mSpriteStates) { spriteState.visibleId = 0; spriteState.particles.clear(); } switch (getType()) { case MONSTER: mInfo = MonsterDB::get(mSubType); setName(mInfo->name); setupSpriteDisplay(mInfo->display); break; case NPC: mInfo = NPCDB::get(mSubType); setupSpriteDisplay(mInfo->display, false); mShowName = true; break; case PLAYER: { mSprites.clear(); mChildParticleEffects.clear(); int id = -100 - subtype; // Prevent showing errors when sprite doesn't exist if (!itemDb->exists(id)) id = -100; setSprite(Net::getCharHandler()->baseSprite(), id); restoreAllSpriteParticles(); mShowName = this == local_player ? config.showOwnName : config.visibleNames; break; } case FLOOR_ITEM: case PORTAL: case UNKNOWN: break; } mSprites.doRedraw(); updateName(); updateNamePosition(); updateColors(); } bool Being::isTargetSelection() const { return mInfo->targetSelection; } ActorSprite::TargetCursorSize Being::getTargetCursorSize() const { return mInfo->targetCursorSize; } Cursor Being::getHoverCursor() const { return mInfo->hoverCursor; } unsigned char Being::getWalkMask() const { return mInfo->walkMask; } Map::BlockType Being::getBlockType() const { return mInfo->blockType; } void Being::setMoveSpeed(const Vector &speed) { mMoveSpeed = speed; // If we already can, recalculate the system speed right away. if (mMap) mSpeedPixelsPerSecond = Net::getPlayerHandler()->getPixelsPerSecondMoveSpeed(speed); } int Being::getSpeechTextYPosition() const { return getPixelY() - std::min(getHeight(), 64) - 6; } void Being::setPosition(const Vector &pos) { Actor::setPosition(pos); updateNamePosition(); if (mText) mText->adviseXY(getPixelX(), getSpeechTextYPosition()); } void Being::setDestination(int dstX, int dstY) { // 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 const int tileWidth = mMap->getTileWidth(); const int tileHeight = mMap->getTileHeight(); if (!mMap->getWalk(dstX / tileWidth, dstY / tileHeight)) return; 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 / tileWidth == dest.x / tileWidth && (int)mPos.y / tileHeight == dest.y / tileHeight) { 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; } void Being::setSpeech(const std::string &text, int time) { mSpeech = text; removeColors(mSpeech); 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()) mSpeechTimer.set(std::min(time, SPEECH_MAX_TIME)); const int speech = config.speech; if (speech == TEXT_OVERHEAD) { delete mText; mText = new Text(mSpeech, getPixelX(), getSpeechTextYPosition(), gcn::Graphics::CENTER, &Theme::getThemeColor(Theme::BUBBLE_TEXT), true); } } void Being::takeDamage(Being *attacker, int amount, AttackType type, int attackId) { 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) { if (attacker == local_player) { color = &userPalette->getColor( UserPalette::HIT_LOCAL_PLAYER_CRITICAL); } else { color = &userPalette->getColor(UserPalette::HIT_CRITICAL); } } else if (!amount) { if (attacker == local_player) { // This is intended to be the wrong direction to visually // differentiate between hits and misses color = &userPalette->getColor(UserPalette::HIT_LOCAL_PLAYER_MISS); } else { color = &userPalette->getColor(UserPalette::MISS); } } else if (getType() == MONSTER) { if (attacker == local_player) { color = &userPalette->getColor( UserPalette::HIT_LOCAL_PLAYER_MONSTER); } else { color = &userPalette->getColor( UserPalette::HIT_PLAYER_MONSTER); } } else { color = &userPalette->getColor(UserPalette::HIT_MONSTER_PLAYER); } // Show damage number particleEngine->addTextSplashEffect(damage, getPixelX(), getPixelY() - getHeight(), color, font, true); if (amount > 0) { auto &hurtSfx = mInfo->getSound(SoundEvent::Hurt); if (attacker) sound.playSfx(hurtSfx, attacker->getPixelX(), attacker->getPixelY()); else sound.playSfx(hurtSfx); if (getType() == MONSTER) { mDamageTaken += amount; updateName(); } // Init the particle effect path based on current weapon or default. int hitEffectId = 0; const ItemInfo *attackerWeapon = attacker ? attacker->getEquippedWeapon() : nullptr; if (attackerWeapon && attacker->getType() == PLAYER) { if (type != CRITICAL) hitEffectId = attackerWeapon->hitEffectId; else hitEffectId = attackerWeapon->criticalHitEffectId; } else if (attacker && attacker->getType() == MONSTER) { const Attack &attack = attacker->getInfo().getAttack(attackId); if (type != CRITICAL) hitEffectId = attack.hitEffectId; else hitEffectId = attack.criticalHitEffectId; } else { if (type != CRITICAL) hitEffectId = paths.getIntValue("hitEffectId"); else hitEffectId = paths.getIntValue("criticalHitEffectId"); } effectManager->trigger(hitEffectId, this); } } 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 != local_player) setAction(Being::ATTACK, attackId); if (victim) { lookAt(victim->getPosition()); if (getType() == PLAYER && mEquippedWeapon) fireMissile(victim, mEquippedWeapon->missileParticleFile); else fireMissile(victim, mInfo->getAttack(attackId).missileParticleFilename); } if (getType() == PLAYER) { auto itemInfo = mEquippedWeapon; // Fall back to racesprite item if (!itemInfo) itemInfo = &itemDb->get(-100 - mSubType); const auto event = damage > 0 ? EquipmentSoundEvent::Hit : EquipmentSoundEvent::Strike; const auto &soundFile = itemInfo->getSound(event); sound.playSfx(soundFile, getPixelX(), getPixelY()); } else { const auto event = damage > 0 ? SoundEvent::Hit : SoundEvent::Miss; const auto &soundFile = mInfo->getSound(event); sound.playSfx(soundFile, getPixelX(), getPixelY()); } } void Being::setName(const std::string &name) { if (getType() == NPC) mName = name.substr(0, name.find('#', 0)); else mName = name; updateName(); } void Being::setShowName(bool doShowName) { if (mShowName == doShowName) return; mShowName = doShowName; updateName(); } void Being::setGuildName(const std::string &name) { Log::info("Got guild name \"%s\" for being %s(%i)", name.c_str(), mName.c_str(), mId); } void Being::setGuildPos(const std::string &pos) { Log::info("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 == local_player && socialWindow) socialWindow->addTab(guild); } void Being::removeGuild(int id) { const auto it = mGuilds.find(id); assert(it != mGuilds.end()); auto [_, guild] = *it; if (this == local_player && socialWindow) socialWindow->removeTab(guild); guild->removeMember(mId); mGuilds.erase(it); } Guild *Being::getGuild(const std::string &guildName) const { for (auto &[_, guild] : mGuilds) if (guild->getName() == guildName) return guild; return nullptr; } Guild *Being::getGuild(int id) const { auto itr = mGuilds.find(id); if (itr != mGuilds.end()) return itr->second; return nullptr; } void Being::clearGuilds() { for (auto &[_, guild] : mGuilds) { if (this == local_player && 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); } updateColors(); if (this == local_player && 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 *missile = particleEngine->addEffect(particle, getPixelX(), getPixelY()); if (missile) { 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::setStatusEffect(int id, bool active) { const auto it = mStatusEffects.find(id); const bool wasActive = it != mStatusEffects.end(); if (active != wasActive) { if (active) mStatusEffects.insert(id); else mStatusEffects.erase(it); updateStatusEffect(id, active); } } void Being::updateStatusEffect(int id, bool newStatus) { auto effect = StatusEffectDB::getStatusEffect(id); if (!effect) return; if (Particle *particle = effect->getParticle(newStatus)) mStatusParticleEffects[id] = ParticleHandle(particle); else mStatusParticleEffects.erase(id); } void Being::setAction(Action action, int attackId) { std::string currentAction = SpriteAction::INVALID; switch (action) { 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 = SpriteAction::SIT; break; case ATTACK: if (mEquippedWeapon) { currentAction = mEquippedWeapon->attackAction; mSprites.reset(); } else { currentAction = mInfo->getAttack(attackId).action; mSprites.reset(); // Attack particle effect if (Particle::enabled) { int effectId = mInfo->getAttack(attackId).effectId; 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); } } break; case HURT: //currentAction = SpriteAction::HURT;// Buggy: makes the player stop // attacking and unable to attack // again until he moves. // TODO: fix this! break; case DEAD: currentAction = SpriteAction::DEAD; sound.playSfx(mInfo->getSound(SoundEvent::Die), getPixelX(), getPixelY()); break; case STAND: currentAction = SpriteAction::STAND; break; } if (currentAction != SpriteAction::INVALID) { mSprites.play(currentAction); mAction = action; } if (currentAction != SpriteAction::MOVE) mActionTimer.set(); } void Being::setAction(const std::string &action) { // Actions are triggered by strings from abilities when using manaserv, // it's not necessarily an attack, but it seems the most appropriate value. mAction = ATTACK; mSprites.play(action); } void Being::lookAt(const Vector &destPos) { // 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) { // 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_t direction) { if (!direction || mDirection == direction) return; mDirection = direction; SpriteDirection dir = DIRECTION_DEFAULT; if (mDirection & UP) dir = DIRECTION_UP; else if (mDirection & DOWN) dir = DIRECTION_DOWN; else if (mDirection & RIGHT) dir = DIRECTION_RIGHT; else dir = DIRECTION_LEFT; mSpriteDirection = dir; updatePlayerSprites(); mSprites.setDirection(dir); } int Being::getCollisionRadius() const { // FIXME: Get this from XML file once a better pathfinding algorithm is up. return 16; } void Being::logic() { // Remove text and speechbubbles if speech boxes aren't being used if (mText && mSpeechTimer.passed()) { delete mText; mText = nullptr; } if (mRestoreParticlesOnLogic) { mRestoreParticlesOnLogic = false; restoreAllSpriteParticles(); // Restart status/particle effects, if needed for (int id : mStatusEffects) { const StatusEffect *effect = StatusEffectDB::getStatusEffect(id); if (effect && effect->persistentParticleEffect) updateStatusEffect(id, true); } } if (mAction != DEAD && !mSpeedPixelsPerSecond.isNull()) { updateMovement(); // Update particle effects const float py = mPos.y + paths.getIntValue("spriteOffsetY"); for (auto &spriteState : mSpriteStates) for (auto &particle : spriteState.particles) particle->moveTo(mPos.x, py); for (auto &[_, p] : mStatusParticleEffects) p->moveTo(mPos.x, py); } ActorSprite::logic(); // 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() && getType() != PLAYER) if (mActionTimer.elapsed() > std::max(mSprites.getMaxDuration(), 1500)) actorSpriteManager->scheduleDelete(this); } void Being::updateMovement() { float dt = Time::deltaTime(); while (dt > 0.f) { const Vector dest = mPath.empty() ? mDest : Vector(mPath.front().x, mPath.front().y); // Avoid going to flawed destinations // We make the being stop move in that case. if (dest.x <= 0 || dest.y <= 0) { mDest = mPos; mPath.clear(); break; } // The Vector representing the difference between current position // and the next destination path node. const Vector dir = dest - mPos; // When we've not reached our destination, move to it. if (!dir.isNull()) { const float distanceToDest = dir.length(); // 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 * mSpeedPixelsPerSecond.x * dt, normalizedDir.y * mSpeedPixelsPerSecond.y * dt); const float distanceToMove = diff.length(); // Test if we don't miss the destination by a move too far: if (distanceToMove > distanceToDest) { setPosition(dest); // Also, if the destination is reached, try to get the next // path point, if existing. if (!mPath.empty()) { mPath.pop_front(); if (mPath.empty()) pathFinished(); } // Set dt to the time left after performing this move. dt -= dt * (distanceToDest / distanceToMove); } else { // Otherwise, go to it using the nominal speed. setPosition(mPos + diff); // And set the remaining time to 0. dt = 0.f; } if (mAction != MOVE) setAction(MOVE); // 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 local_player // 2. When it is the local_player but only by mouse // (because in that case, the path can have more than one tile.) if (local_player != this || local_player->isPathSetByMouse()) { int direction = 0; const float dx = std::abs(dir.x); const 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()) { // If the current path node has been reached, // remove it and go to the next one. mPath.pop_front(); if (mPath.empty()) pathFinished(); } else { if (mAction == MOVE) setAction(STAND); break; } } } void Being::drawSpeech(int offsetX, int offsetY) { const int px = getPixelX() - offsetX; const int speech = config.speech; // Draw speech above this being if (mSpeechTimer.passed()) { if (mSpeechBubble->isVisible()) mSpeechBubble->setVisible(false); } else if (speech == NAME_IN_BUBBLE || speech == NO_NAME_IN_BUBBLE) { const bool showName = (speech == NAME_IN_BUBBLE); delete mText; mText = nullptr; mSpeechBubble->setCaption(showName ? mName : std::string(), mNameColor); mSpeechBubble->setText(mSpeech, showName); mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2), getSpeechTextYPosition() - mSpeechBubble->getHeight() - offsetY); mSpeechBubble->setVisible(true); } else if (speech == TEXT_OVERHEAD) { mSpeechBubble->setVisible(false); if (!mText) { mText = new Text(mSpeech, getPixelX(), getPixelY() - getHeight(), gcn::Graphics::CENTER, &Theme::getThemeColor(Theme::BUBBLE_TEXT), true); } } else if (speech == NO_SPEECH) { mSpeechBubble->setVisible(false); delete mText; mText = nullptr; } } void Being::updateNamePosition() { if (!mDispName) return; // 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::flashName(int time) { if (mDispName) mDispName->flash(time); } void Being::updateName() { delete mDispName; mDispName = nullptr; if (!mShowName) return; std::string mDisplayName(mName); if (getType() == PLAYER) { if (config.showGender) { if (getGender() == Gender::Female) mDisplayName += " \u2640"; else if (getGender() == Gender::Male) mDisplayName += " \u2642"; } // Display the IP when under tmw-Athena (GM only). if (Net::getNetworkType() == ServerType::TmwAthena && local_player && local_player->getShowIp() && getIp()) { mDisplayName += strprintf(" %s", ipToString(getIp())); } } if (getType() == MONSTER) { if (config.showMonstersTakedDamage) { mDisplayName += ", " + toString(getDamageTaken()); } } gcn::Font *font = nullptr; if (local_player && local_player->getTarget() == this && getType() != MONSTER) { font = boldFont; } mDispName = new FlashText(mDisplayName, getPixelX(), getPixelY(), gcn::Graphics::CENTER, mNameColor, font); updateNamePosition(); } void Being::addSpriteParticles(SpriteState &spriteState, const SpriteDisplay &display) { if (!particleEngine) // happens in CharSelectDialog, for example return; if (!spriteState.particles.empty()) return; for (const auto &particle : display.particles) { Particle *p = particleEngine->addEffect(particle, 0, 0, 0); spriteState.particles.emplace_back(p); } } void Being::restoreAllSpriteParticles() { if (mType != PLAYER) return; for (auto &spriteState : mSpriteStates) { if (spriteState.id) { auto &itemInfo = itemDb->get(spriteState.id); addSpriteParticles(spriteState, itemInfo.display); } } } void Being::updateColors() { switch (getType()) { case ActorSprite::UNKNOWN: return; case ActorSprite::PLAYER: if (this == local_player) { mNameColor = &userPalette->getColor(UserPalette::SELF); } else if (mIsGM) { mNameColor = &userPalette->getColor(UserPalette::GM); } else if (mParty && mParty == local_player->getParty()) { mNameColor = &userPalette->getColor(UserPalette::PARTY); } else { mNameColor = &userPalette->getColor(UserPalette::PC); } break; case ActorSprite::NPC: mNameColor = &userPalette->getColor(UserPalette::NPC); break; case ActorSprite::MONSTER: mNameColor = &userPalette->getColor(UserPalette::MONSTER); break; case ActorSprite::FLOOR_ITEM: case ActorSprite::PORTAL: return; } if (mDispName) mDispName->setColor(mNameColor); } /** * Updates the visible sprite IDs of the player, taking into account the item * replacements. */ void Being::updatePlayerSprites() { if (mType != PLAYER) return; // hack for allow different logic in dead player const int direction = mAction == DEAD ? DIRECTION_DEAD : mSpriteDirection; // Get the current item IDs std::vector itemIDs(mSpriteStates.size()); for (size_t i = 0; i < mSpriteStates.size(); i++) itemIDs[i] = mSpriteStates[i].id; // Apply the replacements for (auto &spriteState : mSpriteStates) { if (!spriteState.id) continue; auto &itemInfo = itemDb->get(spriteState.id); for (const auto &replacement : itemInfo.replacements) { if (replacement.direction != DIRECTION_ALL && replacement.direction != direction) continue; if (replacement.sprite == SPRITE_ALL) { if (replacement.items.empty()) { itemIDs.assign(itemIDs.size(), 0); } else { for (int &id : itemIDs) { for (auto &item : replacement.items) if (!item.from || id == item.from) id = item.to; } } } else if (replacement.sprite < itemIDs.size()) { int &id = itemIDs[replacement.sprite]; if (replacement.items.empty()) { id = 0; } else { for (auto &item : replacement.items) if (!item.from || id == item.from) id = item.to; } } } } // Set the new sprites bool newSpriteSet = false; mSprites.ensureSize(mSpriteStates.size()); for (size_t i = 0; i < mSpriteStates.size(); i++) { auto &spriteState = mSpriteStates[i]; if (spriteState.visibleId == itemIDs[i]) continue; spriteState.visibleId = itemIDs[i]; if (spriteState.visibleId == 0) { mSprites.set(i, nullptr); } else { newSpriteSet = true; auto &itemInfo = itemDb->get(spriteState.visibleId); std::string filename = itemInfo.getSprite(mGender, mSubType); Sprite *equipmentSprite = nullptr; if (!filename.empty()) { if (!spriteState.color.empty()) filename += "|" + spriteState.color; equipmentSprite = Sprite::load( paths.getStringValue("sprites") + filename); if (equipmentSprite) equipmentSprite->setDirection(getSpriteDirection()); } mSprites.set(i, equipmentSprite); } } // Make sure any new sprites are set to the correct action if (newSpriteSet) setAction(mAction); } void Being::setSprite(unsigned slot, int id, const std::string &color, bool isWeapon) { if (slot >= mSpriteStates.size()) mSpriteStates.resize(slot + 1); auto &spriteState = mSpriteStates[slot]; // Clear current particles when the ID changes if (spriteState.id != id) spriteState.particles.clear(); // Clear the current sprite when the color changes if (spriteState.color != color && spriteState.visibleId) { spriteState.visibleId = 0; mSprites.set(slot, nullptr); } spriteState.id = id; spriteState.color = color; if (id == 0) // id = 0 means unequip { if (isWeapon) mEquippedWeapon = nullptr; } else { auto &itemInfo = itemDb->get(id); if (mType == PLAYER) addSpriteParticles(spriteState, itemInfo.display); if (isWeapon) mEquippedWeapon = &itemInfo; } updatePlayerSprites(); } void Being::setSpriteID(unsigned slot, int id) { assert(slot < mSpriteStates.size()); setSprite(slot, id, mSpriteStates[slot].color); } void Being::setSpriteColor(unsigned slot, const std::string &color) { assert(slot < mSpriteStates.size()); setSprite(slot, mSpriteStates[slot].id, color); } bool Being::drawnWhenBehind() const { // For now, just draw actors with only one layer when obscured return mSprites.getNumberOfLayers() == 1; } void Being::setGender(Gender gender) { if (gender != mGender) { mGender = gender; // Reset all sprites to force reload with the correct gender for (size_t i = 0; i < mSpriteStates.size(); i++) { auto &spriteState = mSpriteStates[i]; if (spriteState.visibleId) { mSprites.set(i, nullptr); spriteState.visibleId = 0; } } updatePlayerSprites(); if (config.showGender) updateName(); } } void Being::setGM(bool gm) { mIsGM = gm; updateColors(); } void Being::setIp(int ip) { if (mIp == ip) return; mIp = ip; if (local_player && local_player->getShowIp()) updateName(); } bool Being::canTalk() { return mType == NPC; } void Being::talkTo() { Net::getNpcHandler()->talk(mId); } void Being::event(Event::Channel channel, const Event &event) { if (channel == Event::ChatChannel && (event.getType() == Event::Being || event.getType() == Event::Player) && event.getInt("permissions") & PlayerPermissions::SPEECH_FLOAT) { try { if (mId == event.getInt("beingId")) { setSpeech(event.getString("text")); } } catch (BadEvent badEvent) {} } else if (channel == Event::ConfigChannel && event.getType() == Event::ConfigOptionChanged) { if (getType() == PLAYER && event.hasValue(&Config::visibleNames)) { setShowName(config.visibleNames); } } } void Being::death(const gcn::Event &event) { if (event.getSource() == mSpeechBubble) mSpeechBubble = nullptr; } void Being::setMap(Map *map) { for (auto &spriteState : mSpriteStates) spriteState.particles.clear(); mStatusParticleEffects.clear(); mRestoreParticlesOnLogic = true; ActorSprite::setMap(map); // Recalculate pixel/tick speed if (map && !mMoveSpeed.isNull()) { mSpeedPixelsPerSecond = Net::getPlayerHandler()->getPixelsPerSecondMoveSpeed(mMoveSpeed, map); } }