diff --git a/src/Battlescape/AlienInventory.cpp b/src/Battlescape/AlienInventory.cpp index f9ee038548..67db64833d 100644 --- a/src/Battlescape/AlienInventory.cpp +++ b/src/Battlescape/AlienInventory.cpp @@ -31,7 +31,9 @@ #include "../Mod/RuleInterface.h" #include "../Savegame/BattleUnit.h" #include "../Savegame/SavedGame.h" +#include "../Savegame/SavedBattleGame.h" #include "../Ufopaedia/Ufopaedia.h" +#include "InventoryItemSprite.h" namespace OpenXcom { @@ -151,40 +153,39 @@ void AlienInventory::drawGrid() /** * Draws the items contained in the alien's hands. */ -void AlienInventory::drawItems() +void AlienInventory::drawItems() const { const SavedBattleGame* save = _game->getSavedGame()->getSavedBattle(); ScriptWorkerBlit work; _items->clear(); - if (_selUnit != 0) + if (_selUnit != nullptr) { - SurfaceSet *texture = _game->getMod()->getSurfaceSet("BIGOBS.PCK"); + const SurfaceSet* surfaceSet = _game->getMod()->getSurfaceSet("BIGOBS.PCK"); for (const auto* item : *_selUnit->getInventory()) { - if (item->getSlot()->getType() == INV_HAND) - { - const Surface* frame = item->getBigSprite(texture, save, _animFrame); - - if (!frame) - continue; + if (item->getSlot()->getType() != INV_HAND) { continue; } - int x = item->getSlot()->getX() + item->getRules()->getHandSpriteOffX(); - x += _game->getMod()->getAlienInventoryOffsetX(); + const auto handSlot = item->getSlot(); + SDL_Rect spriteBounds = InventoryItemSprite::getHandCenteredSpriteBounds(*item); + spriteBounds.x += handSlot->getX() + _game->getMod()->getAlienInventoryOffsetX(); + spriteBounds.y += handSlot->getY(); - if (item->getSlot()->isRightHand()) - x -= _dynamicOffset; - else if (item->getSlot()->isLeftHand()) - x += _dynamicOffset; + // offset bounds by dynamic offset to account for large aliens. + spriteBounds.x -= (handSlot->isRightHand() ? -_dynamicOffset : + handSlot->isLeftHand() ? _dynamicOffset : + throw std::logic_error("Item in hand slot with bad hand value.")); - int y = item->getSlot()->getY() + item->getRules()->getHandSpriteOffY(); + InventoryItemSprite(*item, *save, *_items, spriteBounds).draw(*surfaceSet, InventorySpriteContext::ALIEN_INV_HAND, _animFrame); - BattleItem::ScriptFill(&work, item, save, BODYPART_ITEM_INVENTORY, _animFrame, 0); - work.executeBlit(frame, _items, x, y, 0); - } - else - { - continue; - } + /// offset for hand overlay + auto handSlotBounds = SDL_Rect{ + static_cast(handSlot->getX() + 1 + _game->getMod()->getAlienInventoryOffsetX()), + static_cast(handSlot->getY() + 1), + RuleInventory::HAND_SLOT_W-1, + RuleInventory::HAND_SLOT_H-2, + }; + // this should render no default effects, but allows for scripting. + InventoryItemSprite(*item, *save, *_items, handSlotBounds).drawHandOverlay(InventorySpriteContext::ALIEN_INV_HAND, _animFrame); } } } diff --git a/src/Battlescape/AlienInventory.h b/src/Battlescape/AlienInventory.h index 9e41ac155e..cd71c19766 100644 --- a/src/Battlescape/AlienInventory.h +++ b/src/Battlescape/AlienInventory.h @@ -58,7 +58,7 @@ class AlienInventory : public InteractiveSurface /// Draws the inventory grid. void drawGrid(); /// Draws the inventory items. - void drawItems(); + void drawItems() const; /// Blits the inventory onto another surface. void blit(SDL_Surface *surface) override; /// Special handling for mouse clicks. diff --git a/src/Battlescape/BattlescapeState.cpp b/src/Battlescape/BattlescapeState.cpp index b6b3d11612..d0a1929ea8 100644 --- a/src/Battlescape/BattlescapeState.cpp +++ b/src/Battlescape/BattlescapeState.cpp @@ -31,6 +31,8 @@ #include "UnitInfoState.h" #include "InventoryState.h" #include "AlienInventoryState.h" +#include "InventoryItemSprite.h" +#include "SpriteOverlay.h" #include "Pathfinding.h" #include "BattlescapeGame.h" #include "WarningMessage.h" @@ -163,22 +165,6 @@ BattlescapeState::BattlescapeState() : _btnZeroTUs = new BattlescapeButton(10, 23, x + 49, y + 33); _btnLeftHandItem = new InteractiveSurface(32, 48, x + 8, y + 4); _btnRightHandItem = new InteractiveSurface(32, 48, x + 280, y + 4); - _numAmmoLeft.reserve(RuleItem::AmmoSlotMax); - _numAmmoRight.reserve(RuleItem::AmmoSlotMax); - for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) - { - _numAmmoLeft.push_back(new NumberText(30, 5, x + 8, y + 4 + 6 * slot)); - _numAmmoRight.push_back(new NumberText(30, 5, x + 280, y + 4 + 6 * slot)); - } - _numMedikitLeft.reserve(RuleItem::MedikitSlots); - _numMedikitRight.reserve(RuleItem::MedikitSlots); - for (int slot = 0; slot < RuleItem::MedikitSlots; ++slot) - { - _numMedikitLeft.push_back(new NumberText(30, 5, x + 9, y + 32 + 7 * slot)); - _numMedikitRight.push_back(new NumberText(30, 5, x + 281, y + 32 + 7 * slot)); - } - _numTwoHandedIndicatorLeft = new NumberText(10, 5, x + 36, y + 46); - _numTwoHandedIndicatorRight = new NumberText(10, 5, x + 308, y + 46); const int visibleUnitX = _game->getMod()->getInterface("battlescape")->getElement("visibleUnits")->x; const int visibleUnitY = _game->getMod()->getInterface("battlescape")->getElement("visibleUnits")->y; for (int i = 0; i < VISIBLE_MAX; ++i) @@ -357,18 +343,6 @@ BattlescapeState::BattlescapeState() : add(_btnZeroTUs, "buttonZeroTUs", "battlescape", _icons); add(_btnLeftHandItem, "buttonLeftHand", "battlescape", _icons); add(_btnRightHandItem, "buttonRightHand", "battlescape", _icons); - for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) - { - add(_numAmmoLeft[slot], "numAmmoLeft", "battlescape", _icons); - add(_numAmmoRight[slot], "numAmmoRight", "battlescape", _icons); - } - for (int slot = 0; slot < RuleItem::MedikitSlots; ++slot) - { - add(_numMedikitLeft[slot], "numMedikitLeft", "battlescape", _icons); - add(_numMedikitRight[slot], "numMedikitRight", "battlescape", _icons); - } - add(_numTwoHandedIndicatorLeft, "numTwoHandedIndicatorLeft", "battlescape", _icons); - add(_numTwoHandedIndicatorRight, "numTwoHandedIndicatorRight", "battlescape", _icons); for (int i = 0; i < VISIBLE_MAX; ++i) { add(_btnVisibleUnit[i]); @@ -415,19 +389,6 @@ BattlescapeState::BattlescapeState() : _numLayers->setColor(Palette::blockOffset(1)-2); _numLayers->setValue(1); - for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) - { - _numAmmoLeft[slot]->setValue(999); - _numAmmoRight[slot]->setValue(999); - } - for (int slot = 0; slot < RuleItem::MedikitSlots; ++slot) - { - _numMedikitLeft[slot]->setValue(999); - _numMedikitRight[slot]->setValue(999); - } - _numTwoHandedIndicatorLeft->setValue(2); - _numTwoHandedIndicatorRight->setValue(2); - _icons->onMouseIn((ActionHandler)&BattlescapeState::mouseInIcons); _icons->onMouseOut((ActionHandler)&BattlescapeState::mouseOutIcons); @@ -1875,72 +1836,19 @@ bool BattlescapeState::playableUnitSelected() } /** - * Draw hand item with ammo number. + * Draw hand item inventory sprite and reaction indicator. */ -void BattlescapeState::drawItem(BattleItem* item, Surface* hand, std::vector &ammoText, std::vector &medikitText, NumberText *twoHandedText, bool drawReactionIndicator) +void BattlescapeState::drawItem(const BattleItem* item, Surface* hand, bool drawReactionIndicator) const { hand->clear(); - for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) - { - ammoText[slot]->setVisible(false); - } - for (int slot = 0; slot < RuleItem::MedikitSlots; ++slot) - { - medikitText[slot]->setVisible(false); - } - twoHandedText->setVisible(false); + if (item) { - const RuleItem *rule = item->getRules(); - rule->drawHandSprite(_game->getMod()->getSurfaceSet("BIGOBS.PCK"), hand, item, _save, _save->getAnimFrame()); - for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) - { - if (item->isAmmoVisibleForSlot(slot)) - { - BattleItem* ammo = item->getAmmoForSlot(slot); - if (!ammo) - { - ammoText[slot]->setVisible(true); - ammoText[slot]->setValue(0); - } - else - { - ammoText[slot]->setVisible(true); - ammoText[slot]->setValue(ammo->getAmmoQuantity()); - } - } - } - twoHandedText->setVisible(rule->isTwoHanded()); - twoHandedText->setColor(rule->isBlockingBothHands() ? _twoHandedRed : _twoHandedGreen); - if (rule->getBattleType() == BT_MEDIKIT) - { - medikitText[0]->setVisible(true); - medikitText[0]->setValue(item->getPainKillerQuantity()); - medikitText[1]->setVisible(true); - medikitText[1]->setValue(item->getStimulantQuantity()); - medikitText[2]->setVisible(true); - medikitText[2]->setValue(item->getHealQuantity()); - } + const SurfaceSet* surfaceSet = _game->getMod()->getSurfaceSet("BIGOBS.PCK"); + int animFrame = _save->getAnimFrame(); - // primed grenade indicator (static) - /* - if (item->getFuseTimer() >= 0) - { - Surface *tempSurface = _game->getMod()->getSurfaceSet("SCANG.DAT")->getFrame(6); - tempSurface->setX((RuleInventory::HAND_W - rule->getInventoryWidth()) * RuleInventory::SLOT_W / 2); - tempSurface->setY((RuleInventory::HAND_H - rule->getInventoryHeight()) * RuleInventory::SLOT_H / 2); - tempSurface->blit(hand); - } - */ - // primed grenade indicator (animated) - if (item->getFuseTimer() >= 0) - { - const int Pulsate[8] = { 0, 1, 2, 3, 4, 3, 2, 1 }; - Surface *tempSurface = _game->getMod()->getSurfaceSet("SCANG.DAT")->getFrame(6); - int x = (RuleInventory::HAND_W - rule->getInventoryWidth()) * RuleInventory::SLOT_W / 2; - int y = (RuleInventory::HAND_H - rule->getInventoryHeight()) * RuleInventory::SLOT_H / 2; - tempSurface->blitNShade(hand, x, y, Pulsate[_save->getAnimFrame() % 8], false, item->isFuseEnabled() ? 0 : 32); - } + InventoryItemSprite(*item, *_save, *hand, InventoryItemSprite::getHandCenteredSpriteBounds(*item)).draw(*surfaceSet, InventorySpriteContext::BATTSCAPE_HAND, animFrame); + InventoryItemSprite(*item, *_save, *hand, SpriteOverlay::getSurfaceBounds(*hand)).drawHandOverlay(InventorySpriteContext::BATTSCAPE_HAND, animFrame); } if (drawReactionIndicator) { @@ -1988,8 +1896,8 @@ void BattlescapeState::drawHandsItems() } } } - drawItem(leftHandItem, _btnLeftHandItem, _numAmmoLeft, _numMedikitLeft, _numTwoHandedIndicatorLeft, left); - drawItem(rightHandItem, _btnRightHandItem, _numAmmoRight, _numMedikitRight, _numTwoHandedIndicatorRight, right); + drawItem(leftHandItem, _btnLeftHandItem, left); + drawItem(rightHandItem, _btnRightHandItem, right); } /** @@ -2138,6 +2046,8 @@ void BattlescapeState::updateSoldierInfo(bool checkFOV) crop.blit(_rank); } } + auto overlay = SpriteOverlay(*_rank, SpriteOverlay::getSurfaceBounds(*_rank), _save); + overlay.draw(*soldier->getArmor(), battleUnit, _save->getAnimFrame()); } else { @@ -2418,6 +2328,13 @@ void BattlescapeState::animate() blinkVisibleUnitButtons(); blinkHealthBar(); + if(auto unit = _save->getSelectedUnit()) + { + auto overlay = SpriteOverlay(*_rank, SpriteOverlay::getSurfaceBounds(*_rank), _save); + overlay.draw(*unit->getArmor(), unit, _save->getAnimFrame()); + } + + if (!_map->getProjectile()) { drawHandsItems(); diff --git a/src/Battlescape/BattlescapeState.h b/src/Battlescape/BattlescapeState.h index ae3096c21c..00073bfe97 100644 --- a/src/Battlescape/BattlescapeState.h +++ b/src/Battlescape/BattlescapeState.h @@ -71,9 +71,6 @@ class BattlescapeState : public State WarningMessage *_warning; Text *_txtName; NumberText *_numTimeUnits, *_numEnergy, *_numHealth, *_numMorale, *_numLayers; - std::vector _numAmmoLeft, _numAmmoRight; - std::vector _numMedikitLeft, _numMedikitRight; - NumberText *_numTwoHandedIndicatorLeft, *_numTwoHandedIndicatorRight; Uint8 _twoHandedRed, _twoHandedGreen; Bar *_barTimeUnits, *_barEnergy, *_barHealth, *_barMorale, *_barMana; bool _manaBarVisible; @@ -103,7 +100,7 @@ class BattlescapeState : public State /// Shifts the red colors of the visible unit buttons backgrounds. void blinkVisibleUnitButtons(); /// Draw hand item with ammo number. - void drawItem(BattleItem *item, Surface *hand, std::vector &ammoText, std::vector &medikitText, NumberText *twoHandedText, bool drawReactionIndicator); + void drawItem(const BattleItem *item, Surface *hand, bool drawReactionIndicator) const; /// Draw both hands sprites. void drawHandsItems(); /// Shifts the colors of the health bar when unit has fatal wounds. diff --git a/src/Battlescape/Inventory.cpp b/src/Battlescape/Inventory.cpp index 270be559a6..086a66f8eb 100644 --- a/src/Battlescape/Inventory.cpp +++ b/src/Battlescape/Inventory.cpp @@ -47,6 +47,7 @@ #include "../Engine/Screen.h" #include "../Engine/CrossPlatform.h" #include "TileEngine.h" +#include "InventoryItemSprite.h" namespace OpenXcom { @@ -62,18 +63,19 @@ namespace OpenXcom */ Inventory::Inventory(Game *game, int width, int height, int x, int y, bool base) : InteractiveSurface(width, height, x, y), _game(game), _selUnit(0), _selItem(0), _tu(true), _base(base), _mouseOverItem(0), _groundOffset(0), _animFrame(0) { - _twoHandedRed = _game->getMod()->getInterface("battlescape")->getElement("twoHandedRed")->color; - _twoHandedGreen = _game->getMod()->getInterface("battlescape")->getElement("twoHandedGreen")->color; - _depth = _game->getSavedGame()->getSavedBattle()->getDepth(); _grid = new Surface(width, height, 0, 0); _items = new Surface(width, height, 0, 0); _gridLabels = new Surface(width, height, 0, 0); _selection = new Surface(RuleInventory::HAND_W * RuleInventory::SLOT_W, RuleInventory::HAND_H * RuleInventory::SLOT_H, x, y); + _bigObs = _game->getMod()->getSurfaceSet("BIGOBS.PCK"); _warning = new WarningMessage(224, 24, 48, 176); _stackNumber = new NumberText(15, 15, 0, 0); _stackNumber->setBordered(true); + Uint8 stackNumberColor = _game->getMod()->getInterface("inventory")->getElement("numStack")->color; + _stackNumber->setColor(stackNumberColor); + _warning->initText(_game->getMod()->getFont("FONT_BIG"), _game->getMod()->getFont("FONT_SMALL"), _game->getLanguage()); _warning->setColor(_game->getMod()->getInterface("battlescape")->getElement("warning")->color2); _warning->setTextColor(_game->getMod()->getInterface("battlescape")->getElement("warning")->color); @@ -82,11 +84,6 @@ Inventory::Inventory(Game *game, int width, int height, int x, int y, bool base) _animTimer->onTimer((SurfaceHandler)&Inventory::animate); _animTimer->start(); - _stunIndicator = _game->getMod()->getSurface("BigStunIndicator", false); - _woundIndicator = _game->getMod()->getSurface("BigWoundIndicator", false); - _burnIndicator = _game->getMod()->getSurface("BigBurnIndicator", false); - _shockIndicator = _game->getMod()->getSurface("BigShockIndicator", false); - const SavedBattleGame *battleSave = _game->getSavedGame()->getSavedBattle(); if (battleSave) { @@ -95,7 +92,7 @@ Inventory::Inventory(Game *game, int width, int height, int x, int y, bool base) { if (!enviro->getInventoryShockIndicator().empty()) { - _shockIndicator = _game->getMod()->getSurface(enviro->getInventoryShockIndicator(), false); + auto _shockIndicator = _game->getMod()->getSurface(enviro->getInventoryShockIndicator(), false); } } } @@ -298,71 +295,50 @@ void Inventory::drawGridLabels(bool showTuCost) */ void Inventory::drawItems() { - const int Pulsate[8] = { 0, 1, 2, 3, 4, 3, 2, 1 }; const SavedBattleGame* save = _game->getSavedGame()->getSavedBattle(); - Surface *tempSurface = _game->getMod()->getSurfaceSet("SCANG.DAT")->getFrame(6); - auto primers = [&](int x, int y, bool a) - { - tempSurface->blitNShade(_items, x, y, Pulsate[_animFrame % 8], false, a ? 0 : 32); - }; - auto indicators = [&](Surface *surf, int x, int y) - { - surf->blitNShade(_items, x, y, Pulsate[_animFrame % 8]); - }; ScriptWorkerBlit work; _items->clear(); - Uint8 color = _game->getMod()->getInterface("inventory")->getElement("numStack")->color; - Uint8 color2 = _game->getMod()->getInterface("inventory")->getElement("numStack")->color2; - if (_selUnit != 0) + + if (_selUnit != nullptr) { - SurfaceSet *texture = _game->getMod()->getSurfaceSet("BIGOBS.PCK"); // Soldier items - for (auto* invItem : *_selUnit->getInventory()) + for (const auto* invItem : *_selUnit->getInventory()) { - const Surface *frame = invItem->getBigSprite(texture, save, _animFrame); + // if the item is selected (grabbed by the cursor) continue. It isn't handled here. + if (invItem == _selItem) { continue; } - if (invItem == _selItem || !frame) - continue; - - int x, y; - if (invItem->getSlot()->getType() == INV_SLOT) - { - x = (invItem->getSlot()->getX() + invItem->getSlotX() * RuleInventory::SLOT_W); - y = (invItem->getSlot()->getY() + invItem->getSlotY() * RuleInventory::SLOT_H); - } - else if (invItem->getSlot()->getType() == INV_HAND) - { - x = (invItem->getSlot()->getX() + invItem->getRules()->getHandSpriteOffX()); - y = (invItem->getSlot()->getY() + invItem->getRules()->getHandSpriteOffY()); - } - else - { - continue; - } - BattleItem::ScriptFill(&work, invItem, save, BODYPART_ITEM_INVENTORY, _animFrame, 0); - work.executeBlit(frame, _items, x, y, 0); + const auto itemSlot = invItem->getSlot(); - // two-handed indicator - if (invItem->getSlot()->getType() == INV_HAND) + if (itemSlot->getType() != INV_HAND) // not hand slot { - if (invItem->getRules()->isTwoHanded() || invItem->getRules()->isBlockingBothHands()) - { - NumberText text = NumberText(10, 5, 0, 0); - text.setPalette(getPalette()); - text.setColor(invItem->getRules()->isBlockingBothHands() ? _twoHandedRed : _twoHandedGreen); - text.setBordered(false); - text.setX(invItem->getSlot()->getX() + RuleInventory::HAND_W * RuleInventory::SLOT_W - 5); - text.setY(invItem->getSlot()->getY() + RuleInventory::HAND_H * RuleInventory::SLOT_H - 7); - text.setValue(2); - text.blit(_items->getSurface()); - } + SDL_Rect spriteBounds = InventoryItemSprite::getInvSpriteBounds(*invItem); + spriteBounds.x += itemSlot->getX(); + spriteBounds.y += itemSlot->getY(); + + // if the cursor is hovering over an item append the hover context. + auto context = _mouseOverItem == invItem ? InventorySpriteContext::SOLDIER_INV_SLOT.with(InventorySpriteContext::CURSOR_HOVER) + : InventorySpriteContext::SOLDIER_INV_SLOT; + InventoryItemSprite(*invItem, *save, *_items, spriteBounds).draw(*_bigObs, context, _animFrame); } - - // grenade primer indicators - if (invItem->getFuseTimer() >= 0 && invItem->getRules()->getInventoryWidth() > 0) + else // hand slot { - primers(x, y, invItem->isFuseEnabled()); + SDL_Rect spriteBounds = InventoryItemSprite::getHandCenteredSpriteBounds(*invItem); + spriteBounds.x += itemSlot->getX(); + spriteBounds.y += itemSlot->getY(); + + // the bounds for the hand overlay are inset by one pixel compared to the standard hand box, + // because the outline is drawn inside the hand box. + const auto handOverlayBounds = SDL_Rect{ + static_cast(itemSlot->getX() + 1), + static_cast(itemSlot->getY() + 1), + (RuleInventory::HAND_SLOT_W) - 1, + (RuleInventory::HAND_SLOT_H) - 2, + }; + auto context = _mouseOverItem == invItem ? InventorySpriteContext::SOLDIER_INV_HAND.with(InventorySpriteContext::CURSOR_HOVER) + : InventorySpriteContext::SOLDIER_INV_HAND; + InventoryItemSprite(*invItem, *save, *_items, spriteBounds).draw(*_bigObs, context, _animFrame); + InventoryItemSprite(*invItem, *save, *_items, handOverlayBounds).drawHandOverlay(context, _animFrame); } } @@ -370,18 +346,19 @@ void Inventory::drawItems() stackLayer.setPalette(getPalette()); // Ground items - int fatalWounds = 0; auto& occupiedSlots = *clearOccupiedSlotsCache(); - for (auto* groundItem : *_selUnit->getTile()->getInventory()) + for (const auto* groundItem : *_selUnit->getTile()->getInventory()) { - const Surface *frame = groundItem->getBigSprite(texture, save, _animFrame); - // note that you can make items invisible by setting their width or height to 0 (for example used with tank corpse items) - if (groundItem == _selItem || groundItem->getRules()->getInventoryHeight() == 0 || groundItem->getRules()->getInventoryWidth() == 0 || !frame) - continue; - - // check if item is in visible range - if (groundItem->getSlotX() < _groundOffset || groundItem->getSlotX() >= _groundOffset + _groundSlotsX) + const auto rules = groundItem->getRules(); + // ground items can have more potentially valid states that need to be filtered. + if (groundItem == _selItem // selected item not handled here. + || rules->getInventoryHeight() == 0 || rules->getInventoryWidth() == 0 // items with no width or height also filtered (tank corpses) + || rules->getBigSprite() == -1 // items with no sprite filtered + || groundItem->getSlotX() < _groundOffset // item out of sight (left side) + || groundItem->getSlotX() >= _groundOffset + _groundSlotsX) // item out of sight (right side) + { continue; + } // check if something was draw here before auto& pos = occupiedSlots[groundItem->getSlotY()][groundItem->getSlotX() - _groundOffset]; @@ -394,57 +371,14 @@ void Inventory::drawItems() pos = true; } - int x, y; - x = (groundItem->getSlot()->getX() + (groundItem->getSlotX() - _groundOffset) * RuleInventory::SLOT_W); - y = (groundItem->getSlot()->getY() + groundItem->getSlotY() * RuleInventory::SLOT_H); - BattleItem::ScriptFill(&work, groundItem, save, BODYPART_ITEM_INVENTORY, _animFrame, 0); - work.executeBlit(frame, _items, x, y, 0); - - // grenade primer indicators - if (groundItem->getFuseTimer() >= 0 && groundItem->getRules()->getInventoryWidth() > 0) - { - primers(x, y, groundItem->isFuseEnabled()); - } + const auto itemSlot = groundItem->getSlot(); + SDL_Rect spriteBounds = InventoryItemSprite::getGroundSlotSpriteBounds(*groundItem, _groundOffset); + spriteBounds.x += itemSlot->getX(); + spriteBounds.y += itemSlot->getY(); - // fatal wounds - fatalWounds = 0; - if (groundItem->getUnit()) - { - // don't show on dead units - if (groundItem->getUnit()->getStatus() == STATUS_UNCONSCIOUS && groundItem->getUnit()->indicatorsAreEnabled()) - { - fatalWounds = groundItem->getUnit()->getFatalWounds(); - if (_burnIndicator && groundItem->getUnit()->getFire() > 0) - { - indicators(_burnIndicator, x, y); - } - else if (_woundIndicator && fatalWounds > 0) - { - indicators(_woundIndicator, x, y); - } - else if (_shockIndicator && groundItem->getUnit()->hasNegativeHealthRegen()) - { - indicators(_shockIndicator, x, y); - } - else if (_stunIndicator) - { - indicators(_stunIndicator, x, y); - } - } - } - if (fatalWounds > 0) - { - _stackNumber->setX((groundItem->getSlot()->getX() + ((groundItem->getSlotX() + groundItem->getRules()->getInventoryWidth()) - _groundOffset) * RuleInventory::SLOT_W)-4); - if (fatalWounds > 9) - { - _stackNumber->setX(_stackNumber->getX()-4); - } - _stackNumber->setY((groundItem->getSlot()->getY() + (groundItem->getSlotY() + groundItem->getRules()->getInventoryHeight()) * RuleInventory::SLOT_H)-6); - _stackNumber->setValue(fatalWounds); - _stackNumber->draw(); - _stackNumber->setColor(color2); - _stackNumber->blit(stackLayer.getSurface()); - } + auto context = _mouseOverItem == groundItem ? InventorySpriteContext::SOLDIER_INV_SLOT.with(InventorySpriteContext::CURSOR_HOVER) + : InventorySpriteContext::SOLDIER_INV_SLOT; + InventoryItemSprite(*groundItem, *save, *_items, spriteBounds).draw(*_bigObs, InventorySpriteContext::SOLDIER_INV_GROUND, _animFrame); // item stacking if (_stackLevel[groundItem->getSlotX()][groundItem->getSlotY()] > 1) @@ -457,7 +391,6 @@ void Inventory::drawItems() _stackNumber->setY((groundItem->getSlot()->getY() + (groundItem->getSlotY() + groundItem->getRules()->getInventoryHeight()) * RuleInventory::SLOT_H)-6); _stackNumber->setValue(_stackLevel[groundItem->getSlotX()][groundItem->getSlotY()]); _stackNumber->draw(); - _stackNumber->setColor(color); _stackNumber->blit(stackLayer.getSurface()); } } @@ -474,7 +407,18 @@ void Inventory::drawSelectedItem() if (_selItem) { _selection->clear(); - _selItem->getRules()->drawHandSprite(_game->getMod()->getSurfaceSet("BIGOBS.PCK"), _selection, _selItem, _game->getSavedGame()->getSavedBattle(), _animFrame); + SDL_Rect bounds = InventoryItemSprite::getHandCenteredSpriteBounds(*_selItem); + const auto save = _game->getSavedGame()->getSavedBattle(); + InventoryItemSprite(*_selItem, *save, *_selection, bounds).draw(*_bigObs, InventorySpriteContext::SOLDIER_INV_CURSOR, _animFrame); + + auto handSlotBounds = SDL_Rect{ + static_cast(_selection->getX()), + static_cast(_selection->getY()), + RuleInventory::HAND_SLOT_W, + RuleInventory::HAND_SLOT_H, + }; + // this renders no effects by default, but allows for scripting. + InventoryItemSprite(*_selItem, *save, *_selection, bounds).drawHandOverlay(InventorySpriteContext::SOLDIER_INV_CURSOR, _animFrame); } } diff --git a/src/Battlescape/Inventory.h b/src/Battlescape/Inventory.h index 945a20e259..db42ae2d72 100644 --- a/src/Battlescape/Inventory.h +++ b/src/Battlescape/Inventory.h @@ -31,6 +31,7 @@ class WarningMessage; class BattleItem; class BattleUnit; class NumberText; +class SurfaceSet; class Timer; /** @@ -41,8 +42,12 @@ class Inventory : public InteractiveSurface { private: Game *_game; - Surface *_grid, *_items, *_gridLabels, *_selection; - Uint8 _twoHandedRed, _twoHandedGreen; + Surface *_grid, *_items, *_gridLabels; + /// surface for the currently select item, if any. Equal in size to a hand-slot. + Surface *_selection; + /// Surface set containing big objects (inventory sprite pictures). + SurfaceSet* _bigObs = nullptr; + WarningMessage *_warning; BattleUnit *_selUnit; BattleItem *_selItem; @@ -51,7 +56,6 @@ class Inventory : public InteractiveSurface int _groundOffset, _animFrame; std::map > _stackLevel; std::vector> _occupiedSlotsCache; - Surface *_stunIndicator, *_woundIndicator, *_burnIndicator, *_shockIndicator; NumberText *_stackNumber; std::string _searchString; Timer *_animTimer; diff --git a/src/Battlescape/InventoryItemSprite.cpp b/src/Battlescape/InventoryItemSprite.cpp new file mode 100644 index 0000000000..6026134b91 --- /dev/null +++ b/src/Battlescape/InventoryItemSprite.cpp @@ -0,0 +1,367 @@ +/* + * Copyright 2010-2023 OpenXcom Developers. + * + * This file is part of OpenXcom. + * + * OpenXcom 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 3 of the License, or + * (at your option) any later version. + * + * OpenXcom 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 OpenXcom. If not, see . + */ +#include +#include "InventoryItemSprite.h" +#include "SpriteOverlay.h" +#include "../Engine/Surface.h" +#include "../Engine/SurfaceSet.h" +#include "../Engine/Script.h" +#include "../Engine/ScriptBind.h" +#include "../Mod/Mod.h" +#include "../Mod/ModScript.h" +#include "../Mod/RuleItem.h" +#include "../Mod/RuleInventory.h" +#include "../Mod/RuleInterface.h" +#include "../Mod/RuleEnviroEffects.h" +#include "../Savegame/BattleItem.h" +#include "../Savegame/BattleUnit.h" +#include "../Savegame/SavedBattleGame.h" +#include "../Interface/Text.h" +#include "../Interface/NumberText.h" + +namespace OpenXcom +{ + +/** + * @brief Draws the inventory item sprite in the current context. + * @param surfaceSet The set that contains this sprite (should be BIGOBS.PCK). +*/ +void InventoryItemSprite::draw(const SurfaceSet& surfaceSet, InventorySpriteContext context, int animationFrame) +{ + const Surface* sprite = getBigSprite(&surfaceSet, _save, animationFrame); + + if (!sprite) + { + Log(LOG_WARNING) << "Attempted to draw item without sprite. Item: " << _ruleItem.getType(); + return; + } + + BattleItem::ScriptFill(&_scriptWorker, _battleItem, _save, BODYPART_ITEM_INVENTORY, animationFrame, 0); + _scriptWorker.executeBlit(sprite, &_target, _bounds.x, _bounds.y, 0); + + SpriteOverlay(_target, _bounds, _save).draw(_ruleItem, _battleItem, &context, animationFrame); + + if (context.options & InventorySpriteContext::DRAW_GRENADE) { drawGrenadePrimedIndicator(animationFrame); } + if (context.options & InventorySpriteContext::DRAW_FATAL_WOUNDS) { drawFatalWoundIndicator(); } + if (context.options & InventorySpriteContext::DRAW_CORPSE_STATE) { drawCorpseIndicator(animationFrame); } +} + +/** + * @brief Draws the hand-slot overlays, including related scripting. +*/ +void InventoryItemSprite::drawHandOverlay(InventorySpriteContext context, int animationFrame) +{ + context.options = static_cast(context.options | InventorySpriteContext::DRAW_HAND_OVERLAY); + SpriteOverlay(_target, _bounds, _save).draw(_ruleItem, _battleItem, &context, animationFrame); + + if (context.options & InventorySpriteContext::DRAW_AMMO) { drawAmmoIndicator(); } + if (context.options & InventorySpriteContext::DRAW_MEDIKIT) { drawMedkitIndicator(); } + if (context.options & InventorySpriteContext::DRAW_TWOHAND) { drawTwoHandIndicator(); } +} + +std::tuple getDimensions(const RuleItem& ruleItem) +{ + // item dimensions in inventory units. + int invSlotW = ruleItem.getInventoryWidth(); + int invSlotH = ruleItem.getInventoryHeight(); + + // sprite bounding dimensions in pixels. + Uint16 itemW = invSlotW * RuleInventory::SLOT_W; + Uint16 itemH = invSlotH * RuleInventory::SLOT_H; + + return std::tuple{ invSlotW ,invSlotH, itemW, itemH }; +} + +/** + * @brief Gets the bounds for proper display of an inventory sprite relative to it's inventory slot. + * @param groundOffset the number of inventory units the ground inventory slot is offset. + * @return A SDL_Rect describing the sprite's bounds, relative to it's inventory slot. +*/ +SDL_Rect InventoryItemSprite::getInvSpriteBounds(const BattleItem& battleItem) +{ + assert(battleItem.getSlot()->getType() == INV_SLOT && "getInvSpriteBounds called on non-inventory item."); + const auto itemRules = battleItem.getRules(); + + // sprite bounding dimensions in pixels. + Uint16 itemW = itemRules->getInventoryWidth() * RuleInventory::SLOT_W; + Uint16 itemH = itemRules->getInventoryHeight() * RuleInventory::SLOT_H; + + // offset the box by its place in inventory container * slot size. + Sint16 itemX = battleItem.getSlotX() * RuleInventory::SLOT_W; + Sint16 itemY = battleItem.getSlotY() * RuleInventory::SLOT_H; + + return SDL_Rect{ itemX, itemY, itemW, itemH }; +} + +SDL_Rect InventoryItemSprite::getGroundSlotSpriteBounds(const BattleItem& battleItem, int groundOffset) +{ + assert(battleItem.getSlot()->getType() == INV_GROUND && "getGroundSlotSpriteBounds called on non-ground item."); + const auto itemRules = battleItem.getRules(); + + // sprite bounding dimensions in pixels. + Uint16 itemW = itemRules->getInventoryWidth() * RuleInventory::SLOT_W; + Uint16 itemH = itemRules->getInventoryHeight() * RuleInventory::SLOT_H; + + // offset the box by place in the ground container, after taking into account the ground offset (in inventory units) + Sint16 itemX = (battleItem.getSlotX() - groundOffset) * RuleInventory::SLOT_W; + Sint16 itemY = battleItem.getSlotY() * RuleInventory::SLOT_H; + + return SDL_Rect{ itemX, itemY, itemW, itemH }; +} + +SDL_Rect InventoryItemSprite::getHandCenteredSpriteBounds(const RuleItem& ruleItem) +{ + // no assert, calling this on items not in hand is potentially valid. + + // item dimensions in inventory units. + int invSlotW = ruleItem.getInventoryWidth(); + int invSlotH = ruleItem.getInventoryHeight(); + + // sprite bounding dimensions in pixels. + Uint16 itemW = invSlotW * RuleInventory::SLOT_W; + Uint16 itemH = invSlotH * RuleInventory::SLOT_H; + + // position item by half the difference in item size and hand slot size in order to center. + Sint16 itemX = (RuleInventory::HAND_W - invSlotW) * RuleInventory::SLOT_W / 2; + Sint16 itemY = (RuleInventory::HAND_H - invSlotH) * RuleInventory::SLOT_H / 2; + + return SDL_Rect{ itemX, itemY, itemW, itemH }; +} + +/** + * Gets the item's inventory sprite. + * @return Return current inventory sprite. + */ +const Surface* InventoryItemSprite::getBigSprite(const SurfaceSet* set, const SavedBattleGame* save, int animFrame) const +{ + int spriteIndex = _ruleItem.getBigSprite(); + if (spriteIndex == -1) { return nullptr; } + + const Surface* ruleSurf = set->getFrame(spriteIndex); + //enforce compatibility with basic version + if (ruleSurf == nullptr) + { + throw Exception("Image missing in 'BIGOBS.PCK' for item '" + _ruleItem.getType() + "'"); + } + + spriteIndex = ModScript::scriptFunc2( + &_ruleItem, + spriteIndex, 0, + _battleItem, save, BODYPART_ITEM_INVENTORY, animFrame, 0 + ); + + auto* scriptSurf = set->getFrame(spriteIndex); + return scriptSurf != nullptr ? scriptSurf : ruleSurf; +} + +namespace // some short helper functions. +{ +/** + * @brief Transforms a number into its position in a triangle wave. + * For example, triangleWave(x, 8, 4) => (0, 1, 2, 3, 4, 3, 2, 1) +*/ +int triangleWave(int number, int period, int amplitude){ return abs((number % period) - amplitude); } + +/// Gets the number of digits in a positive number less than 1000. +int getDigits(int number) { return number < 10 ? 1 : number < 100 ? 2 : 3; } + +std::pair topLeft(const SDL_Rect& bounds, int numW, int numH, int spacing) +{ + return std::pair{bounds.x + spacing, bounds.y + spacing}; +} + +std::pair topRight(const SDL_Rect& bounds, int numW, int numH, int spacing) +{ + return std::pair{bounds.x + bounds.w - numW - spacing, bounds.y + spacing}; +} + +std::pair bottomLeft(const SDL_Rect& bounds, int numW, int numH, int spacing) +{ + return std::pair{bounds.x + spacing, bounds.y + bounds.h - numH - spacing}; +} + +std::pair bottomRight(const SDL_Rect& bounds, int numW, int numH, int spacing) +{ + return std::pair{bounds.x + bounds.w - numW - spacing, bounds.y + bounds.h - numH - spacing}; +} + +} // namespace + +/** + * @brief Draws a numberText relative to a corner of this overlays bounding box. + * @param getChildCoord Function to get the appropriate corner offset. + * @param spacing Amount of additional space to move from the corner. + * @param number The number to draw (must be positive and less than 1000). + * @param yRowOffset an optional additional y-offset. The number will be rendered at (numHeight + 1) * yRowOffset. + * @param bordered if the number should be bordered or not. +*/ +template +void InventoryItemSprite::drawNumberCorner(const CornerFunc getChildCoord, int spacing, int number, int color, int yRowOffset, bool bordered) +{ + int numW = getDigits(number) * (bordered ? 5 : 4); /// width of number. + int numH = bordered ? 6 : 5; /// height of number. + + auto [x, y] = getChildCoord(_bounds, numW, numH, spacing); + _numberRender.setX(x); + _numberRender.setY(y + yRowOffset * (numH + 1)); + + // avoid resizing if possible. + if (numW > _numberRender.getWidth()) { _numberRender.setWidth(numW); } + if (numH != _numberRender.getHeight()) { _numberRender.setHeight(numH); } + + _numberRender.setColor(color); + _numberRender.setBordered(bordered); + _numberRender.setValue(number); + _numberRender.setPalette(_target.getPalette()); + + _numberRender.blit(_target.getSurface()); +} + +void InventoryItemSprite::drawGrenadePrimedIndicator(int animationFrame) const +{ + if (_battleItem->getFuseTimer() < 0) { return; } + + /// TODO: This should be const, but the get methods are not. + const Surface* primedIndicator = const_cast(_mod).getSurfaceSet("SCANG.DAT")->getFrame(6); + // if the grenade is primed, but without the fuse enabled, it gets a grey indicator. This is used for flares in XCF. + int newColor = _battleItem->isFuseEnabled() ? 0 : 32; /// TODO: these colors should be moved to the interface. + + primedIndicator->blitNShade(&_target, _bounds.x, _bounds.y, triangleWave(animationFrame, 8, 4), false, newColor); +} + +namespace +{ +Surface* getCorpseStateIndicator(const SavedBattleGame& save, const Mod& mod, const BattleUnit& unit) { + /// TODO: This should be const, but the get methods are not. + Mod& mutableMod = const_cast(*save.getMod()); + const auto enviro = save.getEnviroEffects(); + + if (unit.getFire() > 0) { return mutableMod.getSurface("BigBurnIndicator", false); } + if (unit.getFatalWounds() > 0) { return mutableMod.getSurface("BigWoundIndicator", false); } + if (unit.hasNegativeHealthRegen()) + { + return enviro && !enviro->getInventoryShockIndicator().empty() ? mutableMod.getSurface(enviro->getInventoryShockIndicator(), false) + : mutableMod.getSurface("BigShockIndicator", false); + } + return mutableMod.getSurface("BigStunIndicator", false); +} +} + +void InventoryItemSprite::drawCorpseIndicator(int animationFrame) const +{ + /// TODO: Implemented to prevent a regression in functionality, but this might be better as a script. + const auto unit = _battleItem->getUnit(); + if (!unit || unit->getStatus() != STATUS_UNCONSCIOUS) { return; } + + if (const Surface* corpseStateIndicator = getCorpseStateIndicator(*_save, _mod, *unit)) + { + corpseStateIndicator->blitNShade(&_target, _bounds.x, _bounds.y, triangleWave(animationFrame, 8, 4)); + } +} + +void InventoryItemSprite::drawFatalWoundIndicator() +{ + const auto unit = _battleItem->getUnit(); + if (!unit || unit->getStatus() != STATUS_UNCONSCIOUS) { return; } + + int woundCount = unit->getFatalWounds(); + + /// TODO: this element should be replaced with a more descriptively named element. + int woundColor = getInterfaceElementMember(_mod, "inventory", "numStack", &Element::color2).value_or(0); + + drawNumberCorner(bottomRight, 0, woundCount, woundColor, 0, true); +} + +void InventoryItemSprite::drawAmmoIndicator() +{ + int ammoColor = _battleItem->getSlot()->isRightHand() ? getInterfaceElementMember(_mod, "battlescape", "numAmmoRight", &Element::color).value_or(0) : + _battleItem->getSlot()->isLeftHand() ? getInterfaceElementMember(_mod, "battlescape", "numAmmoLeft", &Element::color).value_or(0) + : throw std::logic_error("item in hand with bad hand value."); + + for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) + { + if (_battleItem->isAmmoVisibleForSlot(slot)) + { + const BattleItem* ammo = _battleItem->getAmmoForSlot(slot); + int ammoQuant = ammo ? ammo->getAmmoQuantity() : 0; + + drawNumberCorner(topLeft, 0, ammoQuant, ammoColor, slot); + } + } +} + +void InventoryItemSprite::drawMedkitIndicator() +{ + if (_ruleItem.getBattleType() != BT_MEDIKIT) { return; } + + int medkitColor = _battleItem->getSlot()->isRightHand() ? getInterfaceElementMember(_mod, "battlescape", "numMedikitRight", &Element::color).value_or(0) : + _battleItem->getSlot()->isLeftHand() ? getInterfaceElementMember(_mod, "battlescape", "numMedikitLeft", &Element::color).value_or(0) + : throw std::logic_error("item in hand with bad hand value."); + + drawNumberCorner(bottomLeft, 1, _battleItem->getPainKillerQuantity(), medkitColor, -2); + drawNumberCorner(bottomLeft, 1, _battleItem->getStimulantQuantity(), medkitColor, -1); + drawNumberCorner(bottomLeft, 1, _battleItem->getHealQuantity(), medkitColor, 0); +} + +void InventoryItemSprite::drawTwoHandIndicator() +{ + if (!_ruleItem.isTwoHanded()) { return; } + + int color = _ruleItem.isBlockingBothHands() ? getInterfaceElementMember(_mod, "battlescape", "twoHandedRed", &Element::color).value_or(0) + : getInterfaceElementMember(_mod, "battlescape", "twoHandedGreen", &Element::color).value_or(0); + + drawNumberCorner(bottomRight, 1, 2, color); +} + +/// Register the constants and options appropriate for InventorySpriteContext in scripting. Be aware this has some limited r/w capabilities. +void InventorySpriteContext::ScriptRegister(ScriptParserBase* parser) +{ + Bind invSpriteContextBinder = { parser }; + + invSpriteContextBinder.addCustomConst("INV_SLOT_W", RuleInventory::SLOT_W); + invSpriteContextBinder.addCustomConst("INV_SLOT_H", RuleInventory::SLOT_H); + invSpriteContextBinder.addCustomConst("INV_HAND_SLOT_COUNT_W", RuleInventory::HAND_W); + invSpriteContextBinder.addCustomConst("INV_HAND_SLOT_COUNT_H", RuleInventory::HAND_H); + invSpriteContextBinder.addCustomConst("INV_HAND_OVERLAY_W", RuleInventory::HAND_SLOT_W); + invSpriteContextBinder.addCustomConst("INV_HAND_OVERLAY_H", RuleInventory::HAND_SLOT_H); + + invSpriteContextBinder.addCustomConst("DRAW_GRENADE_INDICATOR", InventorySpriteContext::DRAW_GRENADE); + invSpriteContextBinder.addCustomConst("DRAW_CORPSE_STATE", InventorySpriteContext::DRAW_CORPSE_STATE); + invSpriteContextBinder.addCustomConst("DRAW_FATAL_WOUNDS", InventorySpriteContext::DRAW_FATAL_WOUNDS); + invSpriteContextBinder.addCustomConst("DRAW_AMMO", InventorySpriteContext::DRAW_AMMO); + invSpriteContextBinder.addCustomConst("DRAW_MEDIKIT", InventorySpriteContext::DRAW_MEDIKIT); + invSpriteContextBinder.addCustomConst("DRAW_TWOHAND_INDICATOR", InventorySpriteContext::DRAW_TWOHAND); + + invSpriteContextBinder.add<&InventorySpriteContextScript::isRenderOptionSet>("isRenderOptionSet", "Gets if a render option is set or not (via bitwise &). (result, option)"); + invSpriteContextBinder.add<&InventorySpriteContextScript::getRenderOptions>("getRenderOptions", "Gets the current render options."); + invSpriteContextBinder.add<&InventorySpriteContextScript::setRenderOptions>("setRenderOptions", "Sets (turns on) a given render option or options."); + invSpriteContextBinder.add<&InventorySpriteContextScript::unsetRenderOptions>("unsetRenderOptions", "Unsets (turns off) a given render option or options."); + + invSpriteContextBinder.addCustomConst("CURSOR_HOVER", InventorySpriteContext::CURSOR_HOVER); + invSpriteContextBinder.addCustomConst("CURSOR_SELECTED", InventorySpriteContext::CURSOR_SELECTED); + invSpriteContextBinder.addCustomConst("INVENTORY_AMMO", InventorySpriteContext::INVENTORY_AMMO); + invSpriteContextBinder.addCustomConst("SCREEN_INVENTORY", InventorySpriteContext::SCREEN_INVENTORY); + invSpriteContextBinder.addCustomConst("SCREEN_ALIEN_INV", InventorySpriteContext::SCREEN_ALIEN_INV); + invSpriteContextBinder.addCustomConst("SCREEN_BATTSCAPE", InventorySpriteContext::SCREEN_BATTSCAPE); + + invSpriteContextBinder.add<&InventorySpriteContextScript::isInContext>("isInContext", "Gets if the overlay is in a given context or not (via bitwise &). (result option)"); + invSpriteContextBinder.add<&InventorySpriteContextScript::getContext>("getContext", "Gets the current render context."); +} + +} diff --git a/src/Battlescape/InventoryItemSprite.h b/src/Battlescape/InventoryItemSprite.h new file mode 100644 index 0000000000..85fa46ce24 --- /dev/null +++ b/src/Battlescape/InventoryItemSprite.h @@ -0,0 +1,182 @@ +#pragma once +/* + * Copyright 2010-2023 OpenXcom Developers. + * + * This file is part of OpenXcom. + * + * OpenXcom 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 3 of the License, or + * (at your option) any later version. + * + * OpenXcom 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 OpenXcom. If not, see . + */ +#include "../Engine/Script.h" +#include "../Interface/NumberText.h" +#include "../Savegame/BattleItem.h" +#include "../Savegame/SavedBattleGame.h" + +namespace OpenXcom +{ + +class Surface; +class SurfaceSet; +class SavedBattleGame; +class BattleItem; +class Mod; +class RuleItem; +class ScriptParserBase; + +/// Struct associating the sprites render context with its render options. +struct InventorySpriteContext +{ + /// The context in which this class is being rendered. Immutable. + const enum RenderContext : int + { + SCREEN_INVENTORY = 1 << 0, + SCREEN_BATTSCAPE = 1 << 1, + SCREEN_ALIEN_INV = 1 << 2, + SCREEN_UFOPEDIA = 1 << 3, + CURSOR_HOVER = 1 << 4, /// The cursor is hovering over this item in the inventory window. + CURSOR_SELECTED = 1 << 5, /// Item is grabed/selected by the cursor in the inventory window. + INVENTORY_AMMO = 1 << 6, /// Item is being render in the inventory ammo window. + } renderContext; + + /// The options to use when displaying this sprite. Can be changed via scripting. + enum OverlayOptions : int + { + DRAW_NONE = 0, + DRAW_GRENADE = 1 << 0, /// draw the grenade primed indicator + DRAW_CORPSE_STATE = 1 << 1, /// draw the corpse state indicator. Note by default there are no sprites associated with this. + DRAW_FATAL_WOUNDS = 1 << 2, /// draw the number of fatal wounds. + DRAW_AMMO = 1 << 3, /// draw the ammo indicator. + DRAW_MEDIKIT = 1 << 4, /// draw the medikit quantity indicator. + DRAW_TWOHAND = 1 << 5, /// draw the two-hand indicator. + DRAW_HAND_OVERLAY = 1 << 6, /// Item is being rendered with a hand-overlay. (This value should only get set by Sprite Overlay) + DRAW_ALL = INT_MAX, + } options; + + // standard contexts + + static const InventorySpriteContext SOLDIER_INV_HAND; + static const InventorySpriteContext SOLDIER_INV_SLOT; + static const InventorySpriteContext SOLDIER_INV_GROUND; + static const InventorySpriteContext SOLDIER_INV_AMMO; + static const InventorySpriteContext SOLDIER_INV_CURSOR; + static const InventorySpriteContext ALIEN_INV_HAND; + static const InventorySpriteContext BATTSCAPE_HAND; + static const InventorySpriteContext UFOPEDIA_ARTICLE; + + /// returns a copy of the object with the new value or-ed in. + InventorySpriteContext with(RenderContext otherContext) const + { + return InventorySpriteContext{ static_cast(renderContext | otherContext), options }; + } + + static constexpr const char* ScriptName = "InvSpriteContext"; + static void ScriptRegister(ScriptParserBase* parser); +}; + +inline constexpr InventorySpriteContext InventorySpriteContext::SOLDIER_INV_HAND{ RenderContext::SCREEN_INVENTORY, static_cast(DRAW_GRENADE | DRAW_TWOHAND)}; +inline constexpr InventorySpriteContext InventorySpriteContext::SOLDIER_INV_SLOT{ RenderContext::SCREEN_INVENTORY, OverlayOptions::DRAW_GRENADE}; +inline constexpr InventorySpriteContext InventorySpriteContext::SOLDIER_INV_GROUND{ RenderContext::SCREEN_INVENTORY, static_cast(DRAW_GRENADE | DRAW_FATAL_WOUNDS | DRAW_CORPSE_STATE )}; +inline constexpr InventorySpriteContext InventorySpriteContext::SOLDIER_INV_CURSOR{ static_cast(CURSOR_SELECTED | SCREEN_INVENTORY), OverlayOptions::DRAW_NONE}; +inline constexpr InventorySpriteContext InventorySpriteContext::SOLDIER_INV_AMMO{ RenderContext::SCREEN_INVENTORY, OverlayOptions::DRAW_NONE}; +inline constexpr InventorySpriteContext InventorySpriteContext::ALIEN_INV_HAND{ RenderContext::SCREEN_ALIEN_INV, OverlayOptions::DRAW_NONE}; +inline constexpr InventorySpriteContext InventorySpriteContext::BATTSCAPE_HAND{ RenderContext::SCREEN_BATTSCAPE, static_cast(DRAW_GRENADE | DRAW_AMMO | DRAW_MEDIKIT | DRAW_TWOHAND)}; +inline constexpr InventorySpriteContext InventorySpriteContext::UFOPEDIA_ARTICLE{ RenderContext::SCREEN_UFOPEDIA, OverlayOptions::DRAW_NONE }; + +/// Namespace for segregating InventorySpriteContext scripting functions. +namespace InventorySpriteContextScript +{ + inline void getContext(InventorySpriteContext* context, int& result) { result = context->renderContext; } + /// Checks if a given render context is valid for the current context. + inline void isInContext(InventorySpriteContext* context, int& result, int contextOption) { result = contextOption & context->renderContext; } + + typedef InventorySpriteContext::OverlayOptions OverlayOptions; + inline void getRenderOptions(InventorySpriteContext* context, int& result) { result = context->options; } + inline void isRenderOptionSet(InventorySpriteContext* context, int& result, int options) { result = options & context->options; } + inline void setRenderOptions(InventorySpriteContext* context, int options) { context->options = static_cast(context->options | options); } + inline void unsetRenderOptions(InventorySpriteContext* context, int options) { context->options = static_cast(context->options & ~options); } +} + +/** + * @brief Handles rendering of an inventory item sprite, including all scripting. Non-owening and should not leave the local scope. +*/ +class InventoryItemSprite +{ +private: + /// Worker for pixel level script blitting. + ScriptWorkerBlit _scriptWorker{}; + /// BattleItem coresponding with this sprite. Null if initialized in a ufopedia context. + const BattleItem* _battleItem = nullptr; + /// RuleItem coresponding with this sprite. Always initialized. + const RuleItem& _ruleItem; + /// SavedBattleGame context the sprite is operating in. Null if initialized in a ufopedia context. + const SavedBattleGame* _save = nullptr; + /// Mod context the sprite is operating in. Null if initalized in a ufopedia context. + const Mod& _mod; + /// Surface this item should be draw to. + Surface& _target; + /// Bounding box for the sprite. + const SDL_Rect _bounds; + /// Number Text context provided for rendering/scripting. + NumberText _numberRender = NumberText(4, 5); + +public: + /** + * @brief Creates a new inventory item sprite. + * @param target The surface the item is going to be rendered to. + * @param spriteBounds The bounds of the sprite relative to target. + */ + InventoryItemSprite(const BattleItem& battleItem, const SavedBattleGame& save, Surface& target, const SDL_Rect& spriteBounds) + : _battleItem(&battleItem), _ruleItem(*_battleItem->getRules()), _save(&save), _mod(*_save->getMod()), _target(target), _bounds(spriteBounds) {} + + /** + * @brief Create a new invetory item sprite, without requiring a battle context (for the ufopedia). + * @param target The surface the item is going to be rendered to. + * @param spriteBounds The bounds of the sprite, relative to target. + */ + InventoryItemSprite(const RuleItem& ruleItem, const Mod& mod, Surface& target, const SDL_Rect& spriteBounds) + : _ruleItem(ruleItem), _mod(mod), _target(target), _bounds(spriteBounds) {} + + /// Draw the sprite, including scripted recolor and blitting. + void draw(const SurfaceSet& surfaceSet, InventorySpriteContext context, int animationFrame = 0); + /// Draw the hand overlay, which does not include the sprite. + void drawHandOverlay(InventorySpriteContext context, int animationFrame = 0); + + /// Gets the bounds for proper display of an inventory sprite relative to it's inventory slot. + static SDL_Rect getInvSpriteBounds(const BattleItem& battleItem); + /// Gets the bounds for display of an inventory sprite relative to a hand slot (fixed size). + static SDL_Rect getHandCenteredSpriteBounds(const BattleItem& battleItem) + { + return InventoryItemSprite::getHandCenteredSpriteBounds(*battleItem.getRules()); + } + /// Gets the bounds for display of an inventory sprite relative to a hand slot (fixed size). + static SDL_Rect getHandCenteredSpriteBounds(const RuleItem& ruleItem); + /// Gets the bounds for display of an inventory sprite relative to a ground inventory slot. + static SDL_Rect getGroundSlotSpriteBounds(const BattleItem& battleItem, int groundOffset); + +private: + /// gets the appropriate big item sprite, including scripted replacement. + const Surface* getBigSprite(const SurfaceSet* set, const SavedBattleGame* save, int animFrame) const; + + void drawGrenadePrimedIndicator(int animationFrame) const; + void drawAmmoIndicator(); + void drawMedkitIndicator(); + void drawTwoHandIndicator(); + void drawCorpseIndicator(int animationFrame) const; + void drawFatalWoundIndicator(); + + /// Draws a number in a given corner. + template + void drawNumberCorner(CornerFunc getChildCoord, int spacing, int number, int color, int yRowOffset = 0, bool bordered = false); +}; + +} diff --git a/src/Battlescape/InventoryState.cpp b/src/Battlescape/InventoryState.cpp index 1cf16772d1..921fa60ae6 100644 --- a/src/Battlescape/InventoryState.cpp +++ b/src/Battlescape/InventoryState.cpp @@ -22,6 +22,8 @@ #include "InventoryPersonalState.h" #include #include "Inventory.h" +#include "InventoryItemSprite.h" +#include "SpriteOverlay.h" #include "../Basescape/SoldierArmorState.h" #include "../Basescape/SoldierAvatarState.h" #include "../Engine/Game.h" @@ -528,6 +530,9 @@ void InventoryState::init() armorSurface->blitNShade(_soldier, 0, 0); } } + auto bounds = SpriteOverlay::getSurfaceBounds(*_soldier); + auto save = _game->getSavedGame()->getSavedBattle(); + SpriteOverlay(*_soldier, bounds, save).draw(*unit->getArmor(), unit, _inv->getAnimFrame()); // coming from InventoryLoad window... if (_globalLayoutIndex > -1) @@ -2038,9 +2043,9 @@ void InventoryState::handle(Action *action) */ void InventoryState::think() { + int anim = _inv->getAnimFrame(); if (_mouseHoverItem) { - int anim = _inv->getAnimFrame(); int seq = std::max(((anim - _mouseHoverItemFrame) / 10) - 1, 0); // `-1` cause that first item will be show bit more longer int modulo = 0; for (int slot = 0; slot < RuleItem::AmmoSlotMax; ++slot) @@ -2088,13 +2093,32 @@ void InventoryState::think() r.w -= 2; r.h -= 2; _selAmmo->drawRect(&r, Palette::blockOffset(0)+15); - firstAmmo->getRules()->drawHandSprite(_game->getMod()->getSurfaceSet("BIGOBS.PCK"), _selAmmo, firstAmmo, _game->getSavedGame()->getSavedBattle(), anim); + + const SDL_Rect spriteBounds = InventoryItemSprite::getHandCenteredSpriteBounds(*firstAmmo); + const auto save = _game->getSavedGame()->getSavedBattle(); + const auto& surfaceSet = *_game->getMod()->getSurfaceSet("BIGOBS.PCK", anim); + InventoryItemSprite(*firstAmmo, *save, *_selAmmo, spriteBounds).draw(surfaceSet, InventorySpriteContext::SOLDIER_INV_AMMO, anim); + + constexpr auto handSlotBounds = SDL_Rect{ + static_cast(1), + static_cast(1), + RuleInventory::HAND_SLOT_W-1, + RuleInventory::HAND_SLOT_H-2, + }; + InventoryItemSprite(*firstAmmo, *save, *_selAmmo, handSlotBounds).drawHandOverlay(InventorySpriteContext::SOLDIER_INV_AMMO, anim); } else { _selAmmo->clear(); } } + /// animate the paperdoll scripts. + if (BattleUnit* unit = _battleGame->getSelectedUnit()) + { + auto bounds = SpriteOverlay::getSurfaceBounds(*_soldier); + auto save = _game->getSavedGame()->getSavedBattle(); + SpriteOverlay(*_soldier, bounds, save).draw(*unit->getArmor(), unit, anim); + } State::think(); } diff --git a/src/Battlescape/SpriteOverlay.cpp b/src/Battlescape/SpriteOverlay.cpp new file mode 100644 index 0000000000..1d5663976c --- /dev/null +++ b/src/Battlescape/SpriteOverlay.cpp @@ -0,0 +1,219 @@ +#include "SpriteOverlay.h" +#include "InventoryItemSprite.h" +#include "../Engine/Language.h" +#include "../Engine/Script.h" +#include "../Engine/ScriptBind.h" +#include "../Interface/Text.h" +#include "../Interface/NumberText.h" +#include "../Mod/Armor.h" +#include "../Mod/Mod.h" +#include "../Mod/ModScript.h" +#include "../Mod/RuleInventory.h" +#include "../Savegame/SavedBattleGame.h" +#include "../Savegame/BattleUnit.h" +#include "../Savegame/Soldier.h" + +namespace OpenXcom +{ +/** + * @brief Draws a scripted sprite overlay. + * @tparam Battle The battlescape instance of the object associated with this script. + * @tparam Context Context information to pass in to the script. Mutable. + * @tparam Callback The ModScript struct to use when calling this script + * @tparam Rule The rule object associated with the object this script targets. + * @param animationFrame The current animation frame. Defaults to 0. +*/ +template +void SpriteOverlay::draw(const Rule& rule, const Battle* battle, Context* context, int animationFrame) +{ + ModScript::scriptCallback(&rule, battle, _save, this, context, animationFrame); +} + +/** + * @brief Draws a scripted sprite overlay for hooks lacking a context. + * @tparam Battle The battlescape instance of the object associated with this script. + * @tparam Callback The ModScript struct to use when calling this script + * @tparam Rule The rule object associated with the object this script targets. + * @param animationFrame The current animation frame. Defaults to 0. +*/ +template +void SpriteOverlay::draw(const Rule& rule, const Battle* battle, int animationFrame) +{ + ModScript::scriptCallback(&rule, battle, _save, this, animationFrame); +} + +/// Draw the paperdoll overlay. +template void SpriteOverlay::draw(const Armor& armor, const BattleUnit* unit, int animationFrame); +/// Draw the unitRank overlay. +template void SpriteOverlay::draw(const Armor& armor, const BattleUnit* unit, int animationFrame); + +/** + * @brief Draws an overlay for an inventory sprite/bigobj. + * @param context Struct containing information about the current rendering context and it's options. R/W by the script. +*/ +void SpriteOverlay::draw(const RuleItem& ruleItem, const BattleItem* battleItem, InventorySpriteContext* context, int animationFrame) +{ + if (context->options & InventorySpriteContext::DRAW_HAND_OVERLAY) { + ModScript::scriptCallback(&ruleItem, battleItem, _save, this, context, animationFrame); + } + else { + ModScript::scriptCallback(&ruleItem, battleItem, _save, this, context, animationFrame); + } +}; + +/// Draws a number on the surface the sprite targets. +void SpriteOverlay::drawNumber(int value, int x, int y, int width, int height, int color) +{ + _numberRender.clear(); + _numberRender.setX(_bounds.x + x); + _numberRender.setY(_bounds.y + y); + + // avoid resizing if possible. + if (width > _numberRender.getWidth()) { _numberRender.setWidth(width); } + if (height != _numberRender.getHeight()) { _numberRender.setWidth(height); } + + _numberRender.setPalette(_target.getPalette()); + _numberRender.setColor(static_cast(color)); + _numberRender.setBordered(false); + _numberRender.setValue(value); + _numberRender.blit(_target.getSurface()); +} + +/// Draws text on on the surface the sprite targets. +void SpriteOverlay::drawText(const std::string& text, int x, int y, int width, int height, int color) +{ + auto surfaceText = Text(width, height, _bounds.x + x, _bounds.y + y); + surfaceText.setPalette(_target.getPalette()); + auto temp = Language(); // note getting the selected language is tough here, and might not even be what is wanted. + const auto mod = _save->getMod(); + surfaceText.initText(mod->getFont("FONT_BIG"), mod->getFont("FONT_SMALL"), &temp); + surfaceText.setSmall(); + surfaceText.setColor(color); + surfaceText.setText(text); + surfaceText.blit(_target.getSurface()); +} + + +void SpriteOverlay::blit(const Surface* source, int x, int y) +{ + source->blitNShade(&_target, _bounds.x + x, _bounds.y + y, 0, false, 0); +} + +void SpriteOverlay::blitCrop(const Surface* source, int x1, int y1, int x2, int y2) +{ + const auto crop = GraphSubset({_bounds.x + x1, _bounds.x + x2}, {_bounds.y + y1, _bounds.y + y2}); + source->blitNShade(&_target, _bounds.x, _bounds.y, 0, crop); +} + +void SpriteOverlay::blitShadeCrop(const Surface* source, int shade, int x, int y, int x1, int y1, int x2, int y2) +{ + const auto crop = GraphSubset({x1, x2}, {y1, y2}); + source->blitNShade(&_target, _bounds.x + x, _bounds.y + y, shade, crop); +} + +void SpriteOverlay::blitShade(const Surface* source, int x, int y, int shade) +{ + source->blitNShade(&_target, _bounds.x + x, _bounds.y + y, shade, false, 0); +} + +void SpriteOverlay::blitShadeRecolor(const Surface* source, int x, int y, int shade, int newColor) +{ + source->blitNShade(&_target, _bounds.x, _bounds.y, shade, false, newColor); +} + +void SpriteOverlay::drawLine(int x1, int y1, int x2, int y2, int color) +{ + _target.drawLine(_bounds.x + x1, _bounds.y + y1, _bounds.x + x2, _bounds.y + y2, color); +} + +void SpriteOverlay::drawRect(int x1, int y1, int x2, int y2, int color) +{ + _target.drawRect(_bounds.x + x1, _bounds.y + y1, _bounds.x + x2, _bounds.y + y2, color); +} + +void SpriteOverlay::drawCirc(int x, int y, int radius, int color) +{ + _target.drawCircle(_bounds.x + x, _bounds.y + y, radius, color); +} + +//// Script binding +namespace SpriteOverlayScript { + +std::string debugDisplayScript(const SpriteOverlay* overlay) +{ + if (overlay == nullptr) { return "null"; } + + std::ostringstream output; + output << " (x:" << overlay->_bounds.x << " y:" << overlay->_bounds.y << " w:" << overlay->_bounds.w << " h:" << overlay->_bounds.h << ")"; + return output.str(); +} + +} // SpriteOverlayScript + +void SpriteOverlay::ScriptRegister(ScriptParserBase* parser) +{ + Bind spriteOverlayBinder = { parser }; + + spriteOverlayBinder.add<&SpriteOverlay::blit>("blit", "Blits a sprite onto the overlay. (sprite x y)"); + spriteOverlayBinder.add<&SpriteOverlay::blitCrop>("blitCrop", "Blits a sprite onto the overlay with a crop. (sprite x y cropX1 cropY1 cropX2 cropY2)"); + spriteOverlayBinder.add<&SpriteOverlay::blitShade>("blitShade", "Blits and shades a sprite onto the overlay. (sprite x y shade)"); + spriteOverlayBinder.add<&SpriteOverlay::blitShadeRecolor>("blitShadeRecolor", "Blits, shades, and recolors a sprite onto the overlay. (sprite x y shade color)"); + + spriteOverlayBinder.add<&SpriteOverlay::drawNumber>("drawNumber", "Draws number on the overlay. (number x y width height color)"); + spriteOverlayBinder.add<&SpriteOverlay::drawText>("drawText", "Draws text on the overlay. (text x y width height color"); + + spriteOverlayBinder.add<&SpriteOverlay::drawLine>("drawLine", "Draws a line on the overlay. (x1 y1 x2 y2 color)"); + spriteOverlayBinder.add<&SpriteOverlay::drawRect>("drawRect", "Draws a rectangle on the overlay. (x1 y1 x2 y2 color)"); + spriteOverlayBinder.add<&SpriteOverlay::drawCirc>("drawCirc", "Draws a circle on the overlay. (x y radius color)"); + + spriteOverlayBinder.add<&SpriteOverlay::getWidth>("getWidth", "Gets the width of this overlay."); + spriteOverlayBinder.add<&SpriteOverlay::getHeight>("getHeight", "Gets the height of this overlay."); + + spriteOverlayBinder.addDebugDisplay<&SpriteOverlayScript::debugDisplayScript>(); +} + +/** + * Constructor of inventory sprite overlay script parser. + */ +ModScript::InventorySpriteOverlayParser::InventorySpriteOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod) + : ScriptParserEvents{ shared, name, "item", "battle_game", "overlay", "render_context", "anim_frame" } +{ + BindBase bindBase{ this }; + bindBase.addCustomPtr("rules", mod); + + bindBase.parser->registerPointerType(); +} + +/** + * Constructor of hand overlay script parser. + */ +ModScript::HandOverlayParser::HandOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod) + : ScriptParserEvents{ shared, name, "item", "battle_game", "overlay", "render_context", "anim_frame" } +{ + BindBase bindBase{ this }; + bindBase.addCustomPtr("rules", mod); + + bindBase.parser->registerPointerType(); +} + +/** + * Constructor of hand overlay script parser. + */ +ModScript::UnitPaperdollOverlayParser::UnitPaperdollOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod) + : ScriptParserEvents{ shared, name, "unit", "battle_game", "overlay", "anim_frame" } +{ + BindBase bindBase{ this }; + bindBase.addCustomPtr("rules", mod); +} + +/** + * Constructor of hand overlay script parser. + */ +ModScript::UnitRankOverlayParser::UnitRankOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod) + : ScriptParserEvents{ shared, name, "unit", "battle_game", "overlay", "anim_frame" } +{ + BindBase bindBase{ this }; + bindBase.addCustomPtr("rules", mod); +} + +} diff --git a/src/Battlescape/SpriteOverlay.h b/src/Battlescape/SpriteOverlay.h new file mode 100644 index 0000000000..8a37e26499 --- /dev/null +++ b/src/Battlescape/SpriteOverlay.h @@ -0,0 +1,109 @@ +#pragma once +/* + * Copyright 2010-2023 OpenXcom Developers. + * + * This file is part of OpenXcom. + * + * OpenXcom 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 3 of the License, or + * (at your option) any later version. + * + * OpenXcom 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 OpenXcom. If not, see . + */ +#include "InventoryItemSprite.h" +#include "../Interface/NumberText.h" + +namespace OpenXcom +{ + +class SpriteOverlay; +class Surface; +class ScriptParserBase; +class SavedBattleGame; + +/// Namespace for segregating SpriteOverlay scripting functions. +namespace SpriteOverlayScript +{ +std::string debugDisplayScript(const SpriteOverlay* overlay); +} + +/** + * @brief Class for rendering an overlay on top of an existing sprite via scripting. +*/ +class SpriteOverlay +{ +private: + /// Surface this item should be draw to. + Surface& _target; + /// Bounding box for the sprite. + const SDL_Rect _bounds; + /// Battlescape Instance we are working with. + const SavedBattleGame* _save; + /// Number Text context provided for rendering/scripting. + NumberText _numberRender = NumberText(4, 5); + +public: + /** + * @brief Creates a new InventoryItemSprite should not be stored or persist beyond the local scope. + * @param target The surface the sprite is render to. + * @param spriteBounds The bounds the sprite and scripting should work with, relative to target. The sprite is rendered at x, y of this rect, and that coordinate is treated as 0,0 for scripting. + */ + SpriteOverlay(Surface& target, const SDL_Rect& spriteBounds, const SavedBattleGame *save) + : _target(target), _bounds(spriteBounds), _save(save) {} + + // no copy or move. + SpriteOverlay(SpriteOverlay&) = delete; + SpriteOverlay& operator=(SpriteOverlay&) = delete; + + /// Draw the scripted overlay. + template + void draw(const Rule& rule, const Battle* battle, Context* context, int animationFrame = 0); + + /// Draw the scripted overlay for hooks lacking a context object. + template + void draw(const Rule& rule, const Battle* battle, int animationFrame = 0); + + void draw(const RuleItem& ruleItem, const BattleItem* battleItem, InventorySpriteContext* context, int animationFrame); + + // these methods are exposed for scripting. + + /// Gets the width of this overlay. + [[nodiscard]] int getWidth() const { return _bounds.w; } + /// Gets the height of this overlay. + [[nodiscard]] int getHeight() const { return _bounds.h; } + + friend std::string SpriteOverlayScript::debugDisplayScript(const SpriteOverlay* overlay); + + /// Gets a bounding box based on a surface's size, with the x and y set to 0. + static SDL_Rect getSurfaceBounds(const Surface& surface) + { + return SDL_Rect{ 0, 0, static_cast(surface.getWidth()), static_cast(surface.getHeight()) }; + } + + /// Name of class used in script. + static constexpr const char* ScriptName = "SpriteOverlay"; + /// Register all useful function used by script. + static void ScriptRegister(ScriptParserBase* parser); + +private: + void drawNumber(int value, int x, int y, int width, int height, int color); + void drawText(const std::string& text, int x, int y, int width, int height, int color); + + void blit(const Surface* source, int x, int y); + void blitCrop(const Surface* source, int x1, int y1, int x2, int y2); + void blitShadeCrop(const Surface* source, int shade, int x, int y, int x1, int y1, int x2, int y2); + void blitShade(const Surface* source, int x, int y, int shade); + void blitShadeRecolor(const Surface* source, int x, int y, int shade, int newColor); + void drawLine(int x1, int y1, int x2, int y2, int color); + void drawRect(int x1, int y1, int x2, int y2, int color); + void drawCirc(int x, int y, int radius, int color); +}; + +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 663249f2db..ee0e495815 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -91,6 +91,7 @@ set ( battlescape_src Battlescape/InfoboxOKState.cpp Battlescape/InfoboxState.cpp Battlescape/Inventory.cpp + Battlescape/InventoryItemSprite.cpp Battlescape/InventoryLoadState.cpp Battlescape/InventoryPersonalState.cpp Battlescape/InventorySaveState.cpp @@ -115,6 +116,7 @@ set ( battlescape_src Battlescape/ScannerState.cpp Battlescape/ScannerView.cpp Battlescape/SkillMenuState.cpp + Battlescape/SpriteOverlay.cpp Battlescape/TileEngine.cpp Battlescape/TurnDiaryState.cpp Battlescape/UnitDieBState.cpp diff --git a/src/Engine/Surface.h b/src/Engine/Surface.h index eb4a2bbafc..dfe66c4393 100644 --- a/src/Engine/Surface.h +++ b/src/Engine/Surface.h @@ -30,7 +30,6 @@ namespace OpenXcom class Font; class Language; -class ScriptWorkerBase; class SurfaceCrop; template class SurfaceRaw; diff --git a/src/Mod/Mod.cpp b/src/Mod/Mod.cpp index c3a5a6be4e..1ed89de227 100644 --- a/src/Mod/Mod.cpp +++ b/src/Mod/Mod.cpp @@ -6214,6 +6214,26 @@ bool Mod::isDemigod() const return _difficultyDemigod; } +/** + * @brief Gets an element member allowing for the fact that the element or the interface might not exist. + * @tparam MemberType The type of the element member desired. + * @param member pointer to the member of the element we want. + * @param fallback value to return if the element is not found. + * @return An optional containing the member of the element if it exists. +*/ +template +std::optional getInterfaceElementMember(const Mod& mod, const std::string& interfaceName, const std::string& elementName, MemberType Element::* member) +{ + static_assert(std::is_same::value || std::is_same::value, "Element member type must be bool or int"); + + const auto interface = mod.getInterface(interfaceName, false); + if (interface == nullptr) { return std::nullopt; } + + const auto element = interface->getElement(elementName); + if (element == nullptr) { return std::nullopt; } + + return element->*member; +} //////////////////////////////////////////////////////////// // Script binding @@ -6323,6 +6343,51 @@ void getInventoryScript(const Mod* mod, const RuleInventory* &inv, const std::st } } +/** + * @brief Custom method for retrieving a sprite by set for scripting. + * @param surface The surface the sprite is loaded into. nullptr on error. + */ +void getSpiteScript(const Mod* mod, const Surface*& surface, const std::string& setName, int index) +{ + surface = mod ? const_cast(mod)->getSurfaceSet(setName, false)->getFrame(index) : nullptr; +} + +/** + * @brief Custom method for retrieving a sprite by name for scripting. + * @param surface The surface the sprite is loaded into. nullptr on error. + */ +void getNamedSpriteScript(const Mod* mod, const Surface*& surface, const std::string& spriteName) +{ + surface = mod ? const_cast(mod)->getSurface(spriteName, false) : nullptr; +} + +/** + * @brief Custom method for retrieving an interface element color. + * @param color Out reference for the color. -1 on error. +*/ +void getInterfaceElementColorScript(const Mod* mod, int &color, const std::string& interfaceName, const std::string& elementName) +{ + color = mod ? getInterfaceElementMember(*mod, interfaceName, elementName, &Element::color).value_or(-1) : -1; +} +/** + * @brief Custom method for retrieving an interface element color2. + * @param color Out reference for the color. -1 on error. + */ +void getInterfaceElementColor2Script(const Mod* mod, int& color, const std::string& interfaceName, const std::string& elementName) +{ + color = mod ? getInterfaceElementMember(*mod, interfaceName, elementName, &Element::color2).value_or(-1) : -1; +} + +std::string surfaceDebugDisplayScript(const Surface* surface) +{ + if (surface == nullptr) { return "null"; } + + std::ostringstream output; + output << " (x:" << surface->getX() << " y:" << surface->getY() + << " w:" << surface->getWidth() << " h:" << surface->getHeight() << ")"; + return output.str(); +} + } // namespace /** @@ -6338,6 +6403,18 @@ void Mod::ScriptRegister(ScriptParserBase *parser) parser->registerPointerType(); parser->registerPointerType(); + { + // since all user generated references to surfaces should be constant, we will call them sprite. + const std::string name = "Sprite"; + parser->registerRawPointerType(name); + Bind surfaceBinder = {parser, name}; + + surfaceBinder.add<&Surface::getWidth>("getWidth", "Get's the width of the sprite. (width)"); + surfaceBinder.add<&Surface::getHeight>("getHeight", "Get's the width of the sprite. (height)"); + +// surfaceBinder.addDebugDisplay<&debugDisplayScript>(); + } + Bind mod = { parser }; mod.add<&offset<&Mod::_soundOffsetBattle>>("getSoundOffsetBattle", "convert mod sound index in first argument to runtime index in given set, second argument is mod id"); @@ -6365,6 +6442,11 @@ void Mod::ScriptRegister(ScriptParserBase *parser) mod.add<&Mod::getInventoryBelt>("getRuleInventoryBelt"); mod.add<&Mod::getInventoryGround>("getRuleInventoryGround"); + mod.add<&getSpiteScript>("getSpriteFromSet", "Gets a sprite identified by set name and index from the appropriate store. (sprite set index"); + mod.add<&getNamedSpriteScript>("getNamedSprite", "Get a sprite identified by a string. (sprite spriteName)"); + mod.add<&getInterfaceElementColorScript>("getInterfaceElementColor", "Gets the color of a specific interface element. -1 on error. (color interface element)"); + mod.add<&getInterfaceElementColor2Script>("getInterfaceElementColor2", "Gets the color of a specific interface element. -1 on error. (color interface element)"); + mod.addScriptValue<&Mod::_scriptGlobal, &ModScriptGlobal::getScriptValues>(); } diff --git a/src/Mod/Mod.h b/src/Mod/Mod.h index 87497bbf86..76e7dfd56a 100644 --- a/src/Mod/Mod.h +++ b/src/Mod/Mod.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "../Engine/Options.h" #include "../Engine/FileMap.h" @@ -88,6 +89,7 @@ class ExtraStrings; class RuleCommendations; class StatString; class RuleInterface; +struct Element; class RuleGlobe; class RuleConverter; class SoundDefinition; @@ -1107,4 +1109,8 @@ class Mod }; +/// Gets an element member allowing for the fact that the element or the interface might not exist. +template +std::optional getInterfaceElementMember(const Mod& mod, const std::string& interfaceName, const std::string& elementName, MemberType Element::* member); + } diff --git a/src/Mod/ModScript.h b/src/Mod/ModScript.h index a35517ea47..89849c4fb4 100644 --- a/src/Mod/ModScript.h +++ b/src/Mod/ModScript.h @@ -54,6 +54,8 @@ class Mod; class BattleUnit; class BattleUnitVisibility; class BattleItem; +class SpriteOverlay; +struct InventorySpriteContext; struct StatAdjustment; class Ufo; @@ -92,6 +94,14 @@ class ModScript { RecolorUnitParser(ScriptGlobal* shared, const std::string& name, Mod* mod); }; + struct UnitPaperdollOverlayParser : ScriptParserEvents, const BattleUnit*, const SavedBattleGame*, SpriteOverlay*, int> + { + UnitPaperdollOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod); + }; + struct UnitRankOverlayParser : ScriptParserEvents, const BattleUnit*, const SavedBattleGame*, SpriteOverlay*, int> + { + UnitRankOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod); + }; struct SelectUnitParser : ScriptParserEvents { SelectUnitParser(ScriptGlobal* shared, const std::string& name, Mod* mod); @@ -181,6 +191,14 @@ class ModScript { SelectItemParser(ScriptGlobal* shared, const std::string& name, Mod* mod); }; + struct InventorySpriteOverlayParser : ScriptParserEvents, const BattleItem*, const SavedBattleGame*, SpriteOverlay*, InventorySpriteContext*, int> + { + InventorySpriteOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod); + }; + struct HandOverlayParser : ScriptParserEvents, const BattleItem*, const SavedBattleGame*, SpriteOverlay*, InventorySpriteContext*, int> + { + HandOverlayParser(ScriptGlobal* shared, const std::string& name, Mod* mod); + }; struct TryPsiAttackItemParser : ScriptParserEvents, const BattleItem*, const BattleUnit*, const BattleUnit*, const RuleSkill*, int, int, int, RNG::RandomState*, int, int, const SavedBattleGame*> { @@ -291,6 +309,8 @@ class ModScript using RecolorUnitSprite = MACRO_NAMED_SCRIPT("recolorUnitSprite", RecolorUnitParser); using SelectUnitSprite = MACRO_NAMED_SCRIPT("selectUnitSprite", SelectUnitParser); + using UnitPaperdollOverlay = MACRO_NAMED_SCRIPT("unitPaperdollOverlay", UnitPaperdollOverlayParser); + using UnitRankOverlay = MACRO_NAMED_SCRIPT("unitRankOverlay", UnitRankOverlayParser); using SelectMoveSoundUnit = MACRO_NAMED_SCRIPT("selectMoveSoundUnit", SelectMoveSoundUnitParser); using ReactionUnitAction = MACRO_NAMED_SCRIPT("reactionUnitAction", ReactionUnitParser); @@ -319,6 +339,8 @@ class ModScript using RecolorItemSprite = MACRO_NAMED_SCRIPT("recolorItemSprite", RecolorItemParser); using SelectItemSprite = MACRO_NAMED_SCRIPT("selectItemSprite", SelectItemParser); + using InventorySpriteOverlay = MACRO_NAMED_SCRIPT("inventorySpriteOverlay", InventorySpriteOverlayParser); + using HandOverlay = MACRO_NAMED_SCRIPT("handOverlay", HandOverlayParser); using ReactionWeaponAction = MACRO_NAMED_SCRIPT("reactionWeaponAction", ReactionUnitParser); @@ -394,6 +416,8 @@ class ModScript using BattleUnitScripts = ScriptGroupgetBigSprite(texture, save, animFrame); - if (frame) - { - ScriptWorkerBlit scr; - BattleItem::ScriptFill(&scr, item, save, BODYPART_ITEM_INVENTORY, animFrame, 0); - scr.executeBlit(frame, surface, this->getHandSpriteOffX(), this->getHandSpriteOffY(), 0); - } - } - else - { - frame = texture->getFrame(this->getBigSprite()); - frame->blitNShade(surface, this->getHandSpriteOffX(), this->getHandSpriteOffY()); - } -} - -/** - * item's hand spite x offset - * @return x offset - */ -int RuleItem::getHandSpriteOffX() const -{ - return (RuleInventory::HAND_W - getInventoryWidth()) * RuleInventory::SLOT_W/2; -} - -/** - * item's hand spite y offset - * @return y offset - */ -int RuleItem::getHandSpriteOffY() const -{ - return (RuleInventory::HAND_H - getInventoryHeight()) * RuleInventory::SLOT_H/2; -} /** * Gets the heal quantity of the item. @@ -2681,6 +2637,16 @@ void getBattleTypeScript(const RuleItem *ri, int &ret) ret = (int)BT_NONE; } +void getMedikitTypeScript(const RuleItem* ri, int& ret) +{ + if (ri) + { + ret = static_cast(ri->getMediKitType()); + return; + } + ret = -1; +} + void isSingleTargetScript(const RuleItem* r, int &ret) { if (r) @@ -2839,6 +2805,11 @@ void RuleItem::ScriptRegister(ScriptParserBase* parser) ri.addCustomConst("BT_FLARE", BT_FLARE); ri.addCustomConst("BT_CORPSE", BT_CORPSE); + ri.addCustomConst("BMT_NORMAL", BMT_NORMAL); + ri.addCustomConst("BMT_HEAL", BMT_HEAL); + ri.addCustomConst("BMT_PAINKILER", BMT_PAINKILLER); + ri.addCustomConst("BMT_STIMULANT", BMT_STIMULANT); + ri.add<&getTypeScript>("getType"); ri.add<&RuleItem::getAccuracyAimed>("getAccuracyAimed"); @@ -2857,6 +2828,14 @@ void RuleItem::ScriptRegister(ScriptParserBase* parser) ri.add<&RuleItem::getArmor>("getArmorValue"); ri.add<&RuleItem::getWeight>("getWeight"); + ri.add<&RuleItem::getClipSize>("getClipSize"); + ri.add<&getMedikitTypeScript>("getMediKitType", "Gets the the medikit type. WARNING: BMT_NORMAL if not a medikit."); + ri.add<&RuleItem::getHealQuantity>("getMaxHealQuantity", "Gets the default number of heal charges."); + ri.add<&RuleItem::getPainKillerQuantity>("getMaxPainKillerQuantity", "Gets the default number of painkiller charges."); + ri.add<&RuleItem::getStimulantQuantity>("getMaxStimulantQuantity", "Gets the default number of stim charges."); + ri.add<&RuleItem::getInventoryWidth>("getInvWidth", "Gets an items inventory width."); + ri.add<&RuleItem::getInventoryHeight>("getInvHeight", "Gets an items inventory height."); + ri.add<&RuleItem::getBigSprite>("getBigSpriteIndex", "Gets the index of the BIGOBS sprite used to display this item (without scripting)."); ri.add<&getBattleTypeScript>("getBattleType"); ri.add<&RuleItem::getWaypoints>("getWaypoints"); ri.add<&RuleItem::isWaterOnly>("isWaterOnly"); diff --git a/src/Mod/RuleItem.h b/src/Mod/RuleItem.h index 9cea9db212..629c17cd4b 100644 --- a/src/Mod/RuleItem.h +++ b/src/Mod/RuleItem.h @@ -752,12 +752,6 @@ class RuleItem int getClipSize() const; /// Gets the chance of special effect like zombify or corpse explosion or mine triggering. int getSpecialChance() const; - /// Draws the item's hand sprite onto a surface. - void drawHandSprite(const SurfaceSet *texture, Surface *surface, const BattleItem *item = 0, const SavedBattleGame* save = 0, int animFrame = 0) const; - /// item's hand spite x offset - int getHandSpriteOffX() const; - /// item's hand spite y offset - int getHandSpriteOffY() const; /// Gets the medikit heal quantity. int getHealQuantity() const; /// Gets the medikit pain killer quantity. diff --git a/src/OpenXcom.2010.vcxproj b/src/OpenXcom.2010.vcxproj index 377bb45827..5898a27649 100644 --- a/src/OpenXcom.2010.vcxproj +++ b/src/OpenXcom.2010.vcxproj @@ -449,6 +449,7 @@ + @@ -582,6 +583,7 @@ + @@ -846,6 +848,7 @@ + @@ -983,6 +986,7 @@ + @@ -1019,4 +1023,4 @@ - + \ No newline at end of file diff --git a/src/OpenXcom.2010.vcxproj.filters b/src/OpenXcom.2010.vcxproj.filters index 7ec1035208..bdcda7c8e4 100644 --- a/src/OpenXcom.2010.vcxproj.filters +++ b/src/OpenXcom.2010.vcxproj.filters @@ -1133,6 +1133,12 @@ Savegame + + Battlescape + + + Battlescape + @@ -2327,6 +2333,12 @@ Savegame + + Battlescape + + + Battlescape + diff --git a/src/Savegame/BattleItem.cpp b/src/Savegame/BattleItem.cpp index 3bc198945e..c610edba13 100644 --- a/src/Savegame/BattleItem.cpp +++ b/src/Savegame/BattleItem.cpp @@ -706,41 +706,6 @@ const Surface *BattleItem::getFloorSprite(const SurfaceSet *set, const SavedBatt } } -/** - * Gets the item's inventory sprite. - * @return Return current inventory sprite. - */ -const Surface *BattleItem::getBigSprite(const SurfaceSet *set, const SavedBattleGame *save, int animFrame) const -{ - int i = _rules->getBigSprite(); - if (i != -1) - { - const Surface *surf = set->getFrame(i); - //enforce compatibility with basic version - if (surf == nullptr) - { - throw Exception("Image missing in 'BIGOBS.PCK' for item '" + _rules->getType() + "'"); - } - - i = ModScript::scriptFunc2( - _rules, - i, 0, - this, save, BODYPART_ITEM_INVENTORY, animFrame, 0 - ); - - auto* newSurf = set->getFrame(i); - if (newSurf == nullptr) - { - newSurf = surf; - } - return newSurf; - } - else - { - return nullptr; - } -} - /** * Check if item use any ammo. * @return True if item accept ammo. @@ -1509,6 +1474,7 @@ void BattleItem::ScriptRegister(ScriptParserBase* parser) bi.addRules("getRuleItem"); bi.addPair("getBattleUnit"); + bi.add<&BattleItem::isAmmoVisibleForSlot>("isAmmoVisibleForSlot", "Shows if ammo for a given slot is visible or not. (result slot)"); bi.addFunc("getAmmoItem"); bi.addFunc("getAmmoItem"); bi.addFunc("getAmmoForSlot"); diff --git a/src/Savegame/BattleItem.h b/src/Savegame/BattleItem.h index 25555af0ad..3df7508d65 100644 --- a/src/Savegame/BattleItem.h +++ b/src/Savegame/BattleItem.h @@ -147,8 +147,6 @@ class BattleItem bool occupiesSlot(int x, int y, BattleItem *item = 0) const; /// Gets the item's floor sprite. const Surface *getFloorSprite(const SurfaceSet *set, const SavedBattleGame *save, int animFrame, int shade) const; - /// Gets the item's inventory sprite. - const Surface *getBigSprite(const SurfaceSet *set, const SavedBattleGame *save, int animFrame) const; /// Check if item can use any ammo. bool isWeaponWithAmmo() const; diff --git a/src/Savegame/BattleUnit.cpp b/src/Savegame/BattleUnit.cpp index 72b02dea94..5593aa068d 100644 --- a/src/Savegame/BattleUnit.cpp +++ b/src/Savegame/BattleUnit.cpp @@ -5846,6 +5846,10 @@ void setFireScript(BattleUnit *bu, int val) } } +void getStatusScript(const BattleUnit* bu, int& status) +{ + status = bu ? bu->getStatus() : 0; +} void getVisibleUnitsCountScript(BattleUnit *bu, int &ret) { @@ -6142,6 +6146,7 @@ void BattleUnit::ScriptRegister(ScriptParserBase* parser) bu.add<&BattleUnit::getAggression>("getAggression"); bu.add<&BattleUnit::getTurretDirection>("getTurretDirection"); bu.add<&BattleUnit::getWalkingPhase>("getWalkingPhase"); + bu.add<&BattleUnit::indicatorsAreEnabled>("indicatorsAreEnabled", "Checks if indicators are enabled."); bu.add<&BattleUnit::disableIndicators>("disableIndicators"); bu.add<&BattleUnit::getVisible>("isVisible"); @@ -6188,10 +6193,11 @@ void BattleUnit::ScriptRegister(ScriptParserBase* parser) bu.add<&setBaseStatRangeScript<&BattleUnit::_morale, 0, 100>>("setMorale"); bu.add<&addBaseStatRangeScript<&BattleUnit::_morale, 0, 100>>("addMorale"); + bu.add<&getStatusScript>("getStatus", "Gets the units current status (STATUS_UNCONSCIOUS, STATUS_DEAD...)"); bu.add<&BattleUnit::getFire>("getFire"); bu.add<&setFireScript>("setFire"); - + bu.add<&BattleUnit::hasNegativeHealthRegen>("hasNegativeHealthRegen", "Is the unit's health regeneration negative (in shock)?"); bu.add<&setArmorValueScript>("setArmor", "first arg is side, second one is new value of armor"); bu.add<&addArmorValueScript>("addArmor", "first arg is side, second one is value to add to armor"); @@ -6206,10 +6212,8 @@ void BattleUnit::ScriptRegister(ScriptParserBase* parser) UnitStats::addGetStatsScript<&BattleUnit::_stats>(bu, "Stats."); UnitStats::addSetStatsWithCurrScript<&BattleUnit::_stats, &BattleUnit::_tu, &BattleUnit::_energy, &BattleUnit::_health, &BattleUnit::_mana>(bu, "Stats."); - UnitStats::addGetStatsScript<&BattleUnit::_exp>(bu, "Exp.", true); - bu.add<&getMovmentTypeScript>("getMovmentType", BindBase::functionInvisible); //old bugged name bu.add<&getMovmentTypeScript>("getMovementType", "get move type of unit"); bu.add<&getOriginalMovmentTypeScript>("getOriginalMovementType", "get original move type of unit"); @@ -6296,6 +6300,17 @@ void BattleUnit::ScriptRegister(ScriptParserBase* parser) bu.addCustomConst("COLOR_X1_SILVER", 14); bu.addCustomConst("COLOR_X1_SPECIAL", 15); + bu.addCustomConst("STATUS_STANDING", STATUS_STANDING); + bu.addCustomConst("STATUS_WALKING", STATUS_WALKING); + bu.addCustomConst("STATUS_FLYING", STATUS_FLYING); + bu.addCustomConst("STATUS_TURNING", STATUS_TURNING); + bu.addCustomConst("STATUS_AIMING", STATUS_AIMING); + bu.addCustomConst("STATUS_COLLAPSING", STATUS_COLLAPSING); + bu.addCustomConst("STATUS_DEAD", STATUS_DEAD); + bu.addCustomConst("STATUS_UNCONSCIOUS", STATUS_UNCONSCIOUS); + bu.addCustomConst("STATUS_PANICKING", STATUS_PANICKING); + bu.addCustomConst("STATUS_BERSERK", STATUS_BERSERK); + bu.addCustomConst("STATUS_IGNORE_ME", STATUS_IGNORE_ME); bu.addCustomConst("LOOK_BLONDE", LOOK_BLONDE); bu.addCustomConst("LOOK_BROWNHAIR", LOOK_BROWNHAIR); diff --git a/src/Savegame/SavedBattleGame.cpp b/src/Savegame/SavedBattleGame.cpp index c910de6937..70608a3327 100644 --- a/src/Savegame/SavedBattleGame.cpp +++ b/src/Savegame/SavedBattleGame.cpp @@ -1944,7 +1944,7 @@ BattleItem *SavedBattleGame::createItemForUnit(const RuleItem *rule, BattleUnit { return nullptr; } - + BattleItem *item = new BattleItem(rule, getCurrentItemId()); if (!unit->addItem(item, _rule, false, fixedWeapon, fixedWeapon)) { @@ -3541,7 +3541,7 @@ std::string debugDisplayScript(const SavedBattleGame* p) } // namespace /** - * Register Armor in script parser. + * Register Save Battle Game in script parser. * @param parser Script parser. */ void SavedBattleGame::ScriptRegister(ScriptParserBase* parser) diff --git a/src/Ufopaedia/ArticleStateItem.cpp b/src/Ufopaedia/ArticleStateItem.cpp index 4d76ad68fd..742326ec1c 100644 --- a/src/Ufopaedia/ArticleStateItem.cpp +++ b/src/Ufopaedia/ArticleStateItem.cpp @@ -21,11 +21,13 @@ #include #include "Ufopaedia.h" #include "ArticleStateItem.h" +#include "../Battlescape/InventoryItemSprite.h" #include "../Mod/Mod.h" #include "../Mod/ArticleDefinition.h" #include "../Mod/RuleItem.h" #include "../Engine/Game.h" #include "../Engine/Surface.h" +#include "../Engine/SurfaceSet.h" #include "../Engine/LocalizedText.h" #include "../Engine/Unicode.h" #include "../Interface/Text.h" @@ -177,8 +179,9 @@ namespace OpenXcom _image = new Surface(32, 48, 157, 5); add(_image); - item->drawHandSprite(_game->getMod()->getSurfaceSet("BIGOBS.PCK"), _image); - + const auto& mod = *_game->getMod(); + auto itemSprite = InventoryItemSprite(*item, mod, *_image, InventoryItemSprite::getHandCenteredSpriteBounds(*item)); + itemSprite.draw(*const_cast(mod).getSurfaceSet("BIGOBS.PCK"), InventorySpriteContext::UFOPEDIA_ARTICLE); int ammoSlot = defs->getAmmoSlotForPage(_state->current_page); int ammoSlotPrevUsage = defs->getAmmoSlotPrevUsageForPage(_state->current_page); @@ -381,7 +384,9 @@ namespace OpenXcom addAmmoDamagePower(currShow, type); - type->drawHandSprite(_game->getMod()->getSurfaceSet("BIGOBS.PCK"), _imageAmmo[currShow]); + const auto& mod = *_game->getMod(); + auto itemSprite = InventoryItemSprite(*type, mod, *_imageAmmo[currShow], InventoryItemSprite::getHandCenteredSpriteBounds(*type)); + itemSprite.draw(*const_cast(mod).getSurfaceSet("BIGOBS.PCK"), InventorySpriteContext::UFOPEDIA_ARTICLE); ++currShow; if (currShow == maxShow)