diff options
author | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-02-12 17:47:01 +0100 |
---|---|---|
committer | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-02-13 10:18:06 +0100 |
commit | 787a8f1e72a7640eb4c356e692fe763ac185f159 (patch) | |
tree | d034dfd90d92b920f96157c75b8d7f078843e434 | |
parent | f215d88305b914b6f8afdf4b360b116054589211 (diff) | |
download | mana-787a8f1e72a7640eb4c356e692fe763ac185f159.tar.gz mana-787a8f1e72a7640eb4c356e692fe763ac185f159.tar.bz2 mana-787a8f1e72a7640eb4c356e692fe763ac185f159.tar.xz mana-787a8f1e72a7640eb4c356e692fe763ac185f159.zip |
Support changing being base type
The GM command @class (alias @charclass) can be used by GMs to change
their character class (also referred to as job, race, base or species).
Changes of the class are now supported, even supporting switching
between appearing as player, monster or NPC.
Part of https://git.themanaworld.org/mana/mana/-/issues/92
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | src/actorsprite.h | 3 | ||||
-rw-r--r-- | src/being.cpp | 134 | ||||
-rw-r--r-- | src/being.h | 22 | ||||
-rw-r--r-- | src/compoundsprite.cpp | 15 | ||||
-rw-r--r-- | src/compoundsprite.h | 3 | ||||
-rw-r--r-- | src/localplayer.h | 3 | ||||
-rw-r--r-- | src/net/tmwa/adminhandler.cpp | 3 | ||||
-rw-r--r-- | src/net/tmwa/beinghandler.cpp | 58 | ||||
-rw-r--r-- | src/net/tmwa/charserverhandler.cpp | 25 | ||||
-rw-r--r-- | src/net/tmwa/charserverhandler.h | 2 |
11 files changed, 160 insertions, 111 deletions
@@ -7,9 +7,10 @@ - Added support for reading most client-data settings from settings.xml - Added support for XML includes, both absolute and relative - Added support for map/layer mask -- Added support for sprite replacements +- Added support for item sprite replacements - Added support for particle effects on equipment - Added support for hit/miss sounds on equipment for all players +- Added support for players changing into monsters or NPCs - Added online player list to Social window - Added notification sound on receiving whisper - Added default ports when connecting to a custom server diff --git a/src/actorsprite.h b/src/actorsprite.h index 58780ffb..a25481ec 100644 --- a/src/actorsprite.h +++ b/src/actorsprite.h @@ -40,7 +40,8 @@ public: PLAYER, NPC, MONSTER, - FLOOR_ITEM + FLOOR_ITEM, + PORTAL }; enum TargetCursorSize diff --git a/src/being.cpp b/src/being.cpp index aec9deb0..e62ac0e8 100644 --- a/src/being.cpp +++ b/src/being.cpp @@ -62,23 +62,15 @@ Being::Being(int id, Type type, int subtype, Map *map): ActorSprite(id), - mInfo(BeingInfo::Unknown), - mType(type) + mInfo(BeingInfo::Unknown) { setMap(map); - setSubtype(subtype); + setType(type, subtype); mSpeechBubble = new SpeechBubble; mMoveSpeed = Net::getPlayerHandler()->getDefaultMoveSpeed(); - if (getType() == PLAYER) - mShowName = config.visibleNames; - - if (getType() == PLAYER || getType() == NPC) - setShowName(true); - - updateColors(); listen(Event::ConfigChannel); listen(Event::ChatChannel); } @@ -91,17 +83,27 @@ Being::~Being() mSpeechBubble = nullptr; mDispName = nullptr; mText = nullptr; - - removeAllSpriteParticles(); } -void Being::setSubtype(Uint16 subtype) +/** + * 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 (subtype == mSubType) + if (mType == type && mSubType == subtype) return; + mType = type; mSubType = subtype; + for (auto &spriteState : mSpriteStates) + { + spriteState.visibleId = 0; + spriteState.particles.clear(); + } + switch (getType()) { case MONSTER: @@ -112,8 +114,12 @@ void Being::setSubtype(Uint16 subtype) case NPC: mInfo = NPCDB::get(mSubType); setupSpriteDisplay(mInfo->display, false); + mShowName = true; break; case PLAYER: { + clear(); + mChildParticleEffects.clear(); + int id = -100 - subtype; // Prevent showing errors when sprite doesn't exist @@ -121,12 +127,22 @@ void Being::setSubtype(Uint16 subtype) 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; } + + doRedraw(); + + updateName(); + updateNamePosition(); + updateColors(); } bool Being::isTargetSelection() const @@ -172,7 +188,7 @@ void Being::setPosition(const Vector &pos) { Actor::setPosition(pos); - updateCoords(); + updateNamePosition(); if (mText) mText->adviseXY(getPixelX(), getSpeechTextYPosition()); @@ -303,8 +319,8 @@ void Being::takeDamage(Being *attacker, int amount, AttackType type, int attackId) { gcn::Font *font; - std::string damage = amount ? toString(amount) : type == FLEE ? - "dodge" : "miss"; + std::string damage = amount ? toString(amount) + : (type == FLEE ? "dodge" : "miss"); const gcn::Color *color; font = gui->getInfoParticleFont(); @@ -449,17 +465,11 @@ void Being::handleAttack(Being *victim, int damage, int attackId) void Being::setName(const std::string &name) { if (getType() == NPC) - { mName = name.substr(0, name.find('#', 0)); - showName(); - } else - { mName = name; - if (getType() == PLAYER && getShowName()) - showName(); - } + updateName(); } void Being::setShowName(bool doShowName) @@ -468,14 +478,7 @@ void Being::setShowName(bool doShowName) return; mShowName = doShowName; - - if (doShowName) - showName(); - else - { - delete mDispName; - mDispName = nullptr; - } + updateName(); } void Being::setGuildName(const std::string &name) @@ -744,14 +747,14 @@ void Being::lookAt(const Vector &destPos) } } -void Being::setDirection(Uint8 direction) +void Being::setDirection(uint8_t direction) { if (!direction || mDirection == direction) return; mDirection = direction; - SpriteDirection dir; + SpriteDirection dir = DIRECTION_DEFAULT; if (mDirection & UP) dir = DIRECTION_UP; else if (mDirection & DOWN) @@ -762,7 +765,7 @@ void Being::setDirection(Uint8 direction) dir = DIRECTION_LEFT; mSpriteDirection = dir; - updateSprites(); + updatePlayerSprites(); CompoundSprite::setDirection(dir); } @@ -954,7 +957,7 @@ void Being::drawSpeech(int offsetX, int offsetY) } } -void Being::updateCoords() +void Being::updateNamePosition() { if (!mDispName) return; @@ -972,10 +975,14 @@ void Being::flashName(int time) mDispName->flash(time); } -void Being::showName() +void Being::updateName() { delete mDispName; mDispName = nullptr; + + if (!mShowName) + return; + std::string mDisplayName(mName); if (getType() == PLAYER) @@ -1014,7 +1021,7 @@ void Being::showName() mDispName = new FlashText(mDisplayName, getPixelX(), getPixelY(), gcn::Graphics::CENTER, mNameColor, font); - updateCoords(); + updateNamePosition(); } void Being::addSpriteParticles(SpriteState &spriteState, const SpriteDisplay &display) @@ -1048,6 +1055,9 @@ void Being::removeAllSpriteParticles() void Being::restoreAllSpriteParticles() { + if (mType != PLAYER) + return; + for (auto &spriteState : mSpriteStates) { if (spriteState.id) @@ -1095,17 +1105,18 @@ void Being::updateColors() } if (mDispName) - { mDispName->setColor(mNameColor); - } } /** - * Updates the visible sprite IDs of the being, taking into account the item + * Updates the visible sprite IDs of the player, taking into account the item * replacements. */ -void Being::updateSprites() +void Being::updatePlayerSprites() { + if (mType != PLAYER) + return; + // hack for allow different logic in dead player const int direction = mAction == DEAD ? DIRECTION_DEAD : mSpriteDirection; @@ -1163,6 +1174,8 @@ void Being::updateSprites() // Set the new sprites bool newSpriteSet = false; + ensureSize(mSpriteStates.size()); + for (size_t i = 0; i < mSpriteStates.size(); i++) { auto &spriteState = mSpriteStates[i]; @@ -1207,9 +1220,6 @@ void Being::updateSprites() void Being::setSprite(unsigned slot, int id, const std::string &color, bool isWeapon) { - if (slot >= size()) - ensureSize(slot + 1); - if (slot >= mSpriteStates.size()) mSpriteStates.resize(slot + 1); @@ -1220,7 +1230,7 @@ void Being::setSprite(unsigned slot, int id, const std::string &color, removeSpriteParticles(spriteState); // Clear the current sprite when the color changes - if (spriteState.color != color) + if (spriteState.color != color && spriteState.visibleId) { spriteState.visibleId = 0; CompoundSprite::setSprite(slot, nullptr); @@ -1238,13 +1248,14 @@ void Being::setSprite(unsigned slot, int id, const std::string &color, { auto &itemInfo = itemDb->get(id); - addSpriteParticles(spriteState, itemInfo.display); + if (mType == PLAYER) + addSpriteParticles(spriteState, itemInfo.display); if (isWeapon) mEquippedWeapon = &itemInfo; } - updateSprites(); + updatePlayerSprites(); } void Being::setSpriteID(unsigned slot, int id) @@ -1265,12 +1276,6 @@ bool Being::drawnWhenBehind() const return CompoundSprite::getNumberOfLayers() == 1; } -void Being::updateName() -{ - if (mShowName) - showName(); -} - void Being::setGender(Gender gender) { if (gender != mGender) @@ -1288,8 +1293,10 @@ void Being::setGender(Gender gender) } } - updateSprites(); - updateName(); + updatePlayerSprites(); + + if (config.showGender) + updateName(); } } @@ -1300,6 +1307,17 @@ void Being::setGM(bool 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; @@ -1340,7 +1358,9 @@ void Being::event(Event::Channel channel, const Event &event) void Being::setMap(Map *map) { // Remove sprite particles because ActorSprite is going to kill them all - removeAllSpriteParticles(); + for (auto &spriteState : mSpriteStates) + spriteState.particles.clear(); + mRestoreSpriteParticlesOnLogic = true; ActorSprite::setMap(map); diff --git a/src/being.h b/src/being.h index 0f32e60f..4a29c739 100644 --- a/src/being.h +++ b/src/being.h @@ -109,7 +109,8 @@ class Being : public ActorSprite, public EventListener * Constructor. * * @param id a unique being id - * @param subtype partly determines the type of the being + * @param type type of being + * @param subtype refers to a specific npc, monster, species, etc. * @param map the map the being is on */ Being(int id, Type type, int subtype, Map *map); @@ -119,6 +120,8 @@ class Being : public ActorSprite, public EventListener Type getType() const final { return mType; } + void setType(Type type, int subtype); + /** * Removes all path nodes from this being. */ @@ -283,11 +286,6 @@ class Being : public ActorSprite, public EventListener uint16_t getSubType() const { return mSubType; } - /** - * Set Being's subtype (mostly for view for monsters and NPCs) - */ - void setSubtype(uint16_t subtype); - const BeingInfo &getInfo() const { return *mInfo; } @@ -416,7 +414,7 @@ class Being : public ActorSprite, public EventListener * Sets the IP or an IP hash. * The TMW-Athena server sends this information only to GMs. */ - void setIp(int ip) { mIp = ip; } + void setIp(int ip); /** * Returns the player's IP or an IP hash. @@ -455,9 +453,7 @@ class Being : public ActorSprite, public EventListener /** * Updates name's location. */ - void updateCoords(); - - void showName(); + void updateNamePosition(); void addSpriteParticles(SpriteState &spriteState, const SpriteDisplay &display); void removeSpriteParticles(SpriteState &spriteState); @@ -465,7 +461,7 @@ class Being : public ActorSprite, public EventListener void restoreAllSpriteParticles(); void updateColors(); - void updateSprites(); + void updatePlayerSprites(); /** * Gets the advised Y chat text position. @@ -487,7 +483,7 @@ class Being : public ActorSprite, public EventListener int mAttackSpeed = 350; /**< Attack speed */ Action mAction = STAND; /**< Action the being is performing */ - uint16_t mSubType = 0xFFFF; /**< Subtype (graphical view, basically) */ + int mSubType = 0xFFFF; /**< Subtype (graphical view, basically) */ uint8_t mDirection = DOWN; /**< Facing direction */ uint8_t mSpriteDirection = DIRECTION_DOWN; /**< Facing direction */ @@ -525,7 +521,7 @@ class Being : public ActorSprite, public EventListener private: void updateMovement(); - const Type mType; + Type mType = UNKNOWN; /** Speech Bubble components */ SpeechBubble *mSpeechBubble; diff --git a/src/compoundsprite.cpp b/src/compoundsprite.cpp index 24a166a8..76039bbd 100644 --- a/src/compoundsprite.cpp +++ b/src/compoundsprite.cpp @@ -29,9 +29,7 @@ #include <SDL.h> -CompoundSprite::CompoundSprite() -{ -} +CompoundSprite::CompoundSprite() = default; CompoundSprite::~CompoundSprite() { @@ -178,6 +176,12 @@ void CompoundSprite::ensureSize(size_t layerCount) mSprites.resize(layerCount); } +void CompoundSprite::doRedraw() +{ + if (mNeedsRedraw) + redraw(); +} + int CompoundSprite::getDuration() const { int duration = 0; @@ -209,8 +213,9 @@ static void updateValues(int &dimension, int &pos, int imgDimUL, int imgDimRD, i void CompoundSprite::redraw() const { #if 1 // TODO_SDL2: Does it make sense to implement CompoundSprite? - mWidth = mSprites.at(0)->getWidth(); - mHeight = mSprites.at(0)->getHeight(); + auto baseSprite = mSprites.empty() ? nullptr : mSprites.at(0); + mWidth = baseSprite ? baseSprite->getWidth() : 0; + mHeight = baseSprite ? baseSprite->getHeight() : 0; mOffsetX = 0; mOffsetY = 0; mNeedsRedraw = false; diff --git a/src/compoundsprite.h b/src/compoundsprite.h index 17e2dd6c..f32e4e6c 100644 --- a/src/compoundsprite.h +++ b/src/compoundsprite.h @@ -81,8 +81,7 @@ public: void ensureSize(size_t layerCount); - void doRedraw() - { mNeedsRedraw = true; } + void doRedraw(); private: void redraw() const; diff --git a/src/localplayer.h b/src/localplayer.h index 0b53964c..c88faaea 100644 --- a/src/localplayer.h +++ b/src/localplayer.h @@ -61,8 +61,7 @@ enum class LocalPlayer final : public Being { public: - LocalPlayer(int id= 65535, int subtype = 0); - + LocalPlayer(int id = 65535, int subtype = 0); ~LocalPlayer() override; void logic() override; diff --git a/src/net/tmwa/adminhandler.cpp b/src/net/tmwa/adminhandler.cpp index c60050bd..4c4ecdad 100644 --- a/src/net/tmwa/adminhandler.cpp +++ b/src/net/tmwa/adminhandler.cpp @@ -68,10 +68,7 @@ void AdminHandler::handleMessage(MessageIn &msg) id = msg.readInt32(); int ip = msg.readInt32(); if (Being *player = actorSpriteManager->findBeing(id)) - { player->setIp(ip); - player->updateName(); - } break; } } diff --git a/src/net/tmwa/beinghandler.cpp b/src/net/tmwa/beinghandler.cpp index e2c4f158..ba983542 100644 --- a/src/net/tmwa/beinghandler.cpp +++ b/src/net/tmwa/beinghandler.cpp @@ -78,29 +78,51 @@ BeingHandler::BeingHandler(bool enableSync): handledMessages = _messages; } -static Being *createBeing(int id, short job) +static ActorSprite::Type typeFromJob(short job) { - ActorSprite::Type type = ActorSprite::UNKNOWN; if (job <= 25 || (job >= 4001 && job <= 4049)) - type = ActorSprite::PLAYER; - else if (job >= 46 && job <= 1000) - type = ActorSprite::NPC; - else if (job > 1000 && job <= 2000) - type = ActorSprite::MONSTER; - else if (job == 45) + return ActorSprite::PLAYER; + if (job >= 46 && job <= 1000) + return ActorSprite::NPC; + if (job > 1000 && job <= 2000) + return ActorSprite::MONSTER; + if (job == 45) + return ActorSprite::PORTAL; + + return ActorSprite::UNKNOWN; +} + +static Being *createBeing(int id, short job) +{ + const auto type = typeFromJob(job); + if (type == ActorSprite::PORTAL) return nullptr; // Skip portals Being *being = actorSpriteManager->createBeing(id, type, job); if (type == ActorSprite::PLAYER || type == ActorSprite::NPC) { - MessageOut outMsg(0x0094); - outMsg.writeInt32(id);//readLong(2)); + MessageOut outMsg(CMSG_NAME_REQUEST); + outMsg.writeInt32(id); } return being; } +static void updateBeingType(Being *being, short job) +{ + const auto type = typeFromJob(job); + const bool typeChanged = being->getType() != type; + + being->setType(type, job); + + if (typeChanged && type == ActorSprite::PLAYER) + { + MessageOut outMsg(CMSG_NAME_REQUEST); + outMsg.writeInt32(being->getId()); + } +} + static void handleMoveMessage(Map *map, Being *dstBeing, Uint16 srcX, Uint16 srcY, Uint16 dstX, Uint16 dstY) @@ -209,7 +231,7 @@ void BeingHandler::handleMessage(MessageIn &msg) speed = 150.0f; // In ticks per tile * 10 dstBeing->setMoveSpeed(Vector(speed / 10, speed / 10)); - dstBeing->setSubtype(job); + updateBeingType(dstBeing, job); hairStyle = msg.readInt16(); weapon = msg.readInt16(); headBottom = msg.readInt16(); @@ -417,6 +439,9 @@ void BeingHandler::handleMessage(MessageIn &msg) switch (type) { + case LOOK::BASE: + updateBeingType(dstBeing, id); + break; case LOOK::HAIR: { // const int look = id / 256; @@ -531,7 +556,7 @@ void BeingHandler::handleMessage(MessageIn &msg) else dstBeing->setMoveSpeed(Net::getPlayerHandler()->getDefaultMoveSpeed()); - dstBeing->setSubtype(job); + updateBeingType(dstBeing, job); hairStyle = msg.readInt16(); weapon = msg.readInt16(); shield = msg.readInt16(); @@ -545,8 +570,9 @@ void BeingHandler::handleMessage(MessageIn &msg) headTop = msg.readInt16(); headMid = msg.readInt16(); hairColor = msg.readInt16(); - shoes = msg.readInt16(); - gloves = msg.readInt16(); + msg.readInt16(); // clothes_color + msg.readInt8(); // head_dir + msg.readInt8(); // unused2 msg.readInt32(); // guild msg.readInt16(); // emblem msg.readInt16(); // manner @@ -601,11 +627,11 @@ void BeingHandler::handleMessage(MessageIn &msg) } else if (msg.getId() == SMSG_PLAYER_MOVE) { - msg.readInt8(); // unknown + msg.readInt8(); // five } msg.readInt8(); // Lv - msg.readInt8(); // unknown + msg.readInt8(); // unused dstBeing->setStunMode(stunMode); dstBeing->setStatusEffectBlock(0, (statusEffects >> 16) & 0xffff); diff --git a/src/net/tmwa/charserverhandler.cpp b/src/net/tmwa/charserverhandler.cpp index 636b58ce..39155201 100644 --- a/src/net/tmwa/charserverhandler.cpp +++ b/src/net/tmwa/charserverhandler.cpp @@ -28,7 +28,6 @@ #include "gui/charcreatedialog.h" #include "gui/okdialog.h" -#include "net/logindata.h" #include "net/net.h" #include "net/tmwa/gamehandler.h" @@ -210,21 +209,20 @@ void CharServerHandler::readPlayerData(MessageIn &msg, Net::Character *character const Token &token = static_cast<LoginHandler*>(Net::getLoginHandler())->getToken(); - auto *tempPlayer = new LocalPlayer(msg.readInt32(), 0); - tempPlayer->setGender(token.sex); + const int id = msg.readInt32(); character->data.mAttributes[EXP] = msg.readInt32(); character->data.mAttributes[MONEY] = msg.readInt32(); character->data.mStats[JOB].exp = msg.readInt32(); - int temp = msg.readInt32(); + const int temp = msg.readInt32(); character->data.mStats[JOB].base = temp; character->data.mStats[JOB].mod = temp; - tempPlayer->setSprite(SPRITE_SHOE, msg.readInt16()); - tempPlayer->setSprite(SPRITE_GLOVES, msg.readInt16()); - tempPlayer->setSprite(SPRITE_CAPE, msg.readInt16()); - tempPlayer->setSprite(SPRITE_MISC1, msg.readInt16()); + const int shoe = msg.readInt16(); + const int gloves = msg.readInt16(); + const int cape = msg.readInt16(); + const int misc1 = msg.readInt16(); msg.readInt32(); // option msg.readInt32(); // karma @@ -240,8 +238,15 @@ void CharServerHandler::readPlayerData(MessageIn &msg, Net::Character *character const uint16_t race = msg.readInt16(); // class (used for race) int hairStyle = msg.readInt8(); msg.readInt8(); // look - tempPlayer->setSubtype(race); - Uint16 weapon = msg.readInt16(); + const uint16_t weapon = msg.readInt16(); + + auto *tempPlayer = new LocalPlayer(id, race); + tempPlayer->setGender(token.sex); + + tempPlayer->setSprite(SPRITE_SHOE, shoe); + tempPlayer->setSprite(SPRITE_GLOVES, gloves); + tempPlayer->setSprite(SPRITE_CAPE, cape); + tempPlayer->setSprite(SPRITE_MISC1, misc1); tempPlayer->setSprite(SPRITE_WEAPON, weapon, "", true); character->data.mAttributes[LEVEL] = msg.readInt16(); diff --git a/src/net/tmwa/charserverhandler.h b/src/net/tmwa/charserverhandler.h index 646a545e..b0d3e970 100644 --- a/src/net/tmwa/charserverhandler.h +++ b/src/net/tmwa/charserverhandler.h @@ -74,7 +74,7 @@ class CharServerHandler final : public MessageHandler, public Net::CharHandler void connect(); private: - void readPlayerData(MessageIn &msg, Net::Character *character); + static void readPlayerData(MessageIn &msg, Net::Character *character); }; } // namespace TmwAthena |