diff options
author | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-02-10 18:54:06 +0100 |
---|---|---|
committer | Thorbjørn Lindeijer <bjorn@lindeijer.nl> | 2025-02-13 10:17:52 +0100 |
commit | f215d88305b914b6f8afdf4b360b116054589211 (patch) | |
tree | bf8d292b3bace89244471ae6ba50f369f728e270 | |
parent | 2b9ea79e776f943f95d4cf948ca2266d419ccd2f (diff) | |
download | mana-f215d88305b914b6f8afdf4b360b116054589211.tar.gz mana-f215d88305b914b6f8afdf4b360b116054589211.tar.bz2 mana-f215d88305b914b6f8afdf4b360b116054589211.tar.xz mana-f215d88305b914b6f8afdf4b360b116054589211.zip |
Implemented support for item replacements
Specifying an unknown sprite or direction is reported as error and will
not cause any replacements. Specific item replacements are supported
also when not specifying the sprite.
Replacements do not affect particle effects.
Part of https://git.themanaworld.org/mana/mana/-/issues/92
-rw-r--r-- | NEWS | 1 | ||||
-rw-r--r-- | src/being.cpp | 146 | ||||
-rw-r--r-- | src/being.h | 2 | ||||
-rw-r--r-- | src/compoundsprite.cpp | 11 | ||||
-rw-r--r-- | src/compoundsprite.h | 2 | ||||
-rw-r--r-- | src/resources/itemdb.cpp | 104 | ||||
-rw-r--r-- | src/resources/itemdb.h | 7 | ||||
-rw-r--r-- | src/resources/iteminfo.h | 28 | ||||
-rw-r--r-- | src/utils/stringutils.h | 5 |
9 files changed, 267 insertions, 39 deletions
@@ -7,6 +7,7 @@ - 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 particle effects on equipment - Added support for hit/miss sounds on equipment for all players - Added online player list to Social window diff --git a/src/being.cpp b/src/being.cpp index 56ed65f7..aec9deb0 100644 --- a/src/being.cpp +++ b/src/being.cpp @@ -762,6 +762,7 @@ void Being::setDirection(Uint8 direction) dir = DIRECTION_LEFT; mSpriteDirection = dir; + updateSprites(); CompoundSprite::setDirection(dir); } @@ -1099,6 +1100,110 @@ void Being::updateColors() } } +/** + * Updates the visible sprite IDs of the being, taking into account the item + * replacements. + */ +void Being::updateSprites() +{ + // hack for allow different logic in dead player + const int direction = mAction == DEAD ? DIRECTION_DEAD : mSpriteDirection; + + // Get the current item IDs + std::vector<int> 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; + + 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) + { + CompoundSprite::setSprite(i, nullptr); + } + else + { + newSpriteSet = true; + + auto &itemInfo = itemDb->get(spriteState.visibleId); + std::string filename = itemInfo.getSprite(mGender, mSubType); + AnimatedSprite *equipmentSprite = nullptr; + + if (!filename.empty()) + { + if (!spriteState.color.empty()) + filename += "|" + spriteState.color; + + equipmentSprite = AnimatedSprite::load( + paths.getStringValue("sprites") + filename); + + if (equipmentSprite) + equipmentSprite->setDirection(getSpriteDirection()); + } + + CompoundSprite::setSprite(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) { @@ -1114,43 +1219,32 @@ void Being::setSprite(unsigned slot, int id, const std::string &color, if (spriteState.id != id) removeSpriteParticles(spriteState); + // Clear the current sprite when the color changes + if (spriteState.color != color) + { + spriteState.visibleId = 0; + CompoundSprite::setSprite(slot, nullptr); + } + spriteState.id = id; spriteState.color = color; if (id == 0) // id = 0 means unequip { - removeSprite(slot); - if (isWeapon) mEquippedWeapon = nullptr; } else { auto &itemInfo = itemDb->get(id); - std::string filename = itemInfo.getSprite(mGender, mSubType); - AnimatedSprite *equipmentSprite = nullptr; - - if (!filename.empty()) - { - if (!color.empty()) - filename += "|" + color; - - equipmentSprite = AnimatedSprite::load( - paths.getStringValue("sprites") + filename); - } - - if (equipmentSprite) - equipmentSprite->setDirection(getSpriteDirection()); - - CompoundSprite::setSprite(slot, equipmentSprite); addSpriteParticles(spriteState, itemInfo.display); if (isWeapon) mEquippedWeapon = &itemInfo; - - setAction(mAction); } + + updateSprites(); } void Being::setSpriteID(unsigned slot, int id) @@ -1183,14 +1277,18 @@ void Being::setGender(Gender gender) { mGender = gender; - // Reload all subsprites + // Reset all sprites to force reload with the correct gender for (size_t i = 0; i < mSpriteStates.size(); i++) { - auto &sprite = mSpriteStates[i]; - if (sprite.id != 0) - setSprite(i, sprite.id, sprite.color); + auto &spriteState = mSpriteStates[i]; + if (spriteState.visibleId) + { + CompoundSprite::setSprite(i, nullptr); + spriteState.visibleId = 0; + } } + updateSprites(); updateName(); } } diff --git a/src/being.h b/src/being.h index abe056c0..0f32e60f 100644 --- a/src/being.h +++ b/src/being.h @@ -442,6 +442,7 @@ class Being : public ActorSprite, public EventListener protected: struct SpriteState { int id = 0; + int visibleId = 0; std::string color; std::vector<Particle*> particles; }; @@ -464,6 +465,7 @@ class Being : public ActorSprite, public EventListener void restoreAllSpriteParticles(); void updateColors(); + void updateSprites(); /** * Gets the advised Y chat text position. diff --git a/src/compoundsprite.cpp b/src/compoundsprite.cpp index 2e39fb47..24a166a8 100644 --- a/src/compoundsprite.cpp +++ b/src/compoundsprite.cpp @@ -158,17 +158,6 @@ void CompoundSprite::setSprite(int layer, Sprite *sprite) mNeedsRedraw = true; } -void CompoundSprite::removeSprite(int layer) -{ - // Skip if it won't change anything - if (!mSprites.at(layer)) - return; - - delete mSprites.at(layer); - mSprites.at(layer) = nullptr; - mNeedsRedraw = true; -} - void CompoundSprite::clear() { // Skip if it won't change anything diff --git a/src/compoundsprite.h b/src/compoundsprite.h index bb5dddc8..17e2dd6c 100644 --- a/src/compoundsprite.h +++ b/src/compoundsprite.h @@ -77,8 +77,6 @@ public: Sprite *getSprite(int layer) const { return mSprites.at(layer); } - void removeSprite(int layer); - void clear(); void ensureSize(size_t layerCount); diff --git a/src/resources/itemdb.cpp b/src/resources/itemdb.cpp index 68ddcd75..a6e65c71 100644 --- a/src/resources/itemdb.cpp +++ b/src/resources/itemdb.cpp @@ -21,6 +21,7 @@ #include "resources/itemdb.h" +#include "configuration.h" #include "log.h" #include "resources/hairdb.h" @@ -29,10 +30,11 @@ #include "utils/dtor.h" #include "utils/gettext.h" #include "utils/stringutils.h" -#include "configuration.h" +#include "net/tmwa/protocol.h" #include <cassert> +#include <string_view> void setStatsList(std::list<ItemStat> stats) { @@ -60,6 +62,62 @@ static ItemType itemTypeFromString(const std::string &name, int id = 0) return ITEM_UNUSABLE; } +static uint8_t spriteFromString(std::string_view name) +{ + if (name.empty()) + return SPRITE_ALL; + if (name == "race" || name == "type") + return TmwAthena::SPRITE_BASE; + if (name == "shoes" || name == "boot" || name == "boots") + return TmwAthena::SPRITE_SHOE; + if (name == "bottomclothes" || name == "bottom" || name == "pants") + return TmwAthena::SPRITE_BOTTOMCLOTHES; + if (name == "topclothes" || name == "top" || name == "torso" || name == "body") + return TmwAthena::SPRITE_TOPCLOTHES; + if (name == "misc1") + return TmwAthena::SPRITE_MISC1; + if (name == "misc2" || name == "scarf" || name == "scarfs") + return TmwAthena::SPRITE_MISC2; + if (name == "hair") + return TmwAthena::SPRITE_HAIR; + if (name == "hat" || name == "hats") + return TmwAthena::SPRITE_HAT; + if (name == "wings") + return TmwAthena::SPRITE_CAPE; + if (name == "glove" || name == "gloves") + return TmwAthena::SPRITE_GLOVES; + if (name == "weapon" || name == "weapons") + return TmwAthena::SPRITE_WEAPON; + if (name == "shield" || name == "shields") + return TmwAthena::SPRITE_SHIELD; + if (name == "amulet" || name == "amulets") + return 12; + if (name == "ring" || name == "rings") + return 13; + + return SPRITE_UNKNOWN; +} + +static uint8_t directionFromString(std::string_view name) +{ + if (name.empty()) + return DIRECTION_ALL; + if (name == "down" || name == "downall") + return DIRECTION_DOWN; + if (name == "left") + return DIRECTION_LEFT; + if (name == "up" || name == "upall") + return DIRECTION_UP; + if (name == "right") + return DIRECTION_RIGHT; + + // hack for died action. + if (name == "died") + return DIRECTION_DEAD; + + return DIRECTION_UNKNOWN; +} + void ItemDB::loadEmptyItemDefinition() { mUnknown->name = _("Unknown item"); @@ -166,6 +224,46 @@ void ItemDB::loadFloorSprite(SpriteDisplay &display, XML::Node floorNode) } } +void ItemDB::loadReplacement(ItemInfo &info, XML::Node replaceNode) +{ + std::string_view spriteString; + std::string_view directionString; + + replaceNode.attribute("sprite", spriteString); + replaceNode.attribute("direction", directionString); + + const uint8_t sprite = spriteFromString(spriteString); + const uint8_t direction = directionFromString(directionString); + + if (sprite == SPRITE_UNKNOWN) + { + logger->log("ItemDB: Invalid sprite name '%s' in replace tag", + spriteString.data()); + return; + } + + if (direction == DIRECTION_UNKNOWN) + { + logger->log("ItemDB: Invalid direction name '%s' in replace tag", + directionString.data()); + return; + } + + Replacement &replace = info.replacements.emplace_back(); + replace.sprite = sprite; + replace.direction = direction; + + for (auto child : replaceNode.children()) + { + if (child.name() == "item") + { + Replacement::Item &item = replace.items.emplace_back(); + child.attribute("from", item.from); + child.attribute("to", item.to); + } + } +} + void ItemDB::unload() { logger->log("Unloading item database..."); @@ -228,6 +326,10 @@ void ItemDB::loadCommonRef(ItemInfo &itemInfo, XML::Node node, const std::string { loadFloorSprite(itemInfo.display, itemChild); } + else if (itemChild.name() == "replace") + { + loadReplacement(itemInfo, itemChild); + } } } diff --git a/src/resources/itemdb.h b/src/resources/itemdb.h index 8359452c..69620122 100644 --- a/src/resources/itemdb.h +++ b/src/resources/itemdb.h @@ -133,13 +133,18 @@ class ItemDB /** * Loads the sound references contained in a <sound> tag. */ - void loadSoundRef(ItemInfo &itemInfo, XML::Node node); + void loadSoundRef(ItemInfo &itemInfo, XML::Node node); /** * Loads the floor item references contained in a <floor> tag. */ void loadFloorSprite(SpriteDisplay &display, XML::Node node); + /** + * Loads the <replace> tag. + */ + void loadReplacement(ItemInfo &info, XML::Node replaceNode); + // Items database std::map<int, ItemInfo *> mItemInfos; std::map<std::string, ItemInfo *> mNamedItemInfos; diff --git a/src/resources/iteminfo.h b/src/resources/iteminfo.h index 42c65c80..4f9ab10a 100644 --- a/src/resources/iteminfo.h +++ b/src/resources/iteminfo.h @@ -67,6 +67,32 @@ namespace ManaServ { class ManaServItemDB; } +enum ReplacementDirection : uint8_t +{ + DIRECTION_ALL = DIRECTION_DEFAULT, + DIRECTION_DEAD = DIRECTION_INVALID, + DIRECTION_UNKNOWN, +}; + +enum ReplacementSprite : uint8_t +{ + SPRITE_UNKNOWN = 254, + SPRITE_ALL = 255, +}; + +struct Replacement +{ + struct Item + { + int from = 0; // ID to replace (0: any) + int to = 0; // Replace with this ID (0: remove) + }; + + uint8_t sprite = SPRITE_ALL; // sprite slot to replace + uint8_t direction = DIRECTION_ALL; // direction in which to replace + std::vector<Item> items; // specific items to replace (empty: remove) +}; + /** * Defines a class for storing generic item infos. */ @@ -109,6 +135,8 @@ public: ItemType type = ITEM_UNUSABLE; /**< Item type. */ + std::vector<Replacement> replacements; + const std::string &getSprite(Gender gender, int race) const; const std::string &getSound(EquipmentSoundEvent event) const; diff --git a/src/utils/stringutils.h b/src/utils/stringutils.h index 8781a9c4..ca54803d 100644 --- a/src/utils/stringutils.h +++ b/src/utils/stringutils.h @@ -149,6 +149,11 @@ inline void fromString(const char *str, std::string &value) value = str; } +inline void fromString(const char *str, std::string_view &value) +{ + value = str; +} + inline void fromString(const char *str, int &value) { value = atoi(str); |