scummvm/engines/dgds/inventory.cpp
2025-03-25 20:28:22 +11:00

499 lines
16 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "common/util.h"
#include "graphics/managed_surface.h"
#include "dgds/inventory.h"
#include "dgds/dgds.h"
#include "dgds/scene.h"
#include "dgds/image.h"
#include "dgds/font.h"
#include "dgds/request.h"
#include "dgds/includes.h"
#include "dgds/globals.h"
namespace Dgds {
/*static*/ const byte Inventory::HOC_CHARACTER_QUALS[] = { 0, 9, 7, 8 };
Inventory::Inventory() : _isOpen(false), _prevPageBtn(nullptr), _nextPageBtn(nullptr),
_invClock(nullptr), _itemZoomBox(nullptr), _exitButton(nullptr), _clockSkipMinBtn(nullptr),
_itemArea(nullptr), _clockSkipHrBtn(nullptr), _dropBtn(nullptr), _itemBox(nullptr),
_giveToBtn(nullptr), _changeCharBtn(nullptr), _highlightItemNo(-1), _itemOffset(0),
_openedFromSceneNum(0), _showZoomBox(false), _fullWidth(-1)
{
}
void Inventory::open() {
// Allow double-open because that's how the inventory shows item
// descriptions.
_isOpen = true;
DgdsEngine *engine = DgdsEngine::getInstance();
int curScene = engine->getScene()->getNum();
if (engine->getGameId() == GID_WILLY) {
_openedFromSceneNum = curScene;
return;
}
if (curScene != 2) {
_openedFromSceneNum = curScene;
engine->changeScene(2);
} else {
engine->getScene()->runEnterSceneOps();
}
}
void Inventory::close() {
if (!_isOpen)
return;
_isOpen = false;
DgdsEngine *engine = DgdsEngine::getInstance();
if (engine->getGameId() != GID_WILLY) {
assert(_openedFromSceneNum != 0);
engine->changeScene(_openedFromSceneNum);
}
_showZoomBox = false;
_openedFromSceneNum = 0;
_highlightItemNo = -1;
}
void Inventory::setRequestData(const REQFileData &data) {
_reqData = data;
if (_reqData._requests.empty()) {
warning("No inventory request data to load");
return;
}
RequestData &req = _reqData._requests[0];
_prevPageBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(14));
_nextPageBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(15));
_invClock = dynamic_cast<TextAreaGadget *>(req.findGadgetByNumWithFlags3Not0x40(23));
_itemBox = req.findGadgetByNumWithFlags3Not0x40(8);
_itemZoomBox = req.findGadgetByNumWithFlags3Not0x40(9);
_exitButton = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(17));
_clockSkipMinBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(24));
_clockSkipHrBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(25));
_giveToBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(29));
_changeCharBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(27));
_dropBtn = dynamic_cast<ButtonGadget *>(req.findGadgetByNumWithFlags3Not0x40(16));
_itemArea = dynamic_cast<ImageGadget *>(req.findGadgetByNumWithFlags3Not0x40(8));
_fullWidth = req._rect.width;
// TODO! Beamish doesn't have a zoom box, or it's a different ID?
if (DgdsEngine::getInstance()->getGameId() == GID_WILLY)
_itemZoomBox = _itemBox;
if (!_prevPageBtn || !_nextPageBtn || !_itemZoomBox || !_exitButton || !_itemArea)
error("Didn't get all expected inventory gadgets");
}
void Inventory::drawHeader(Graphics::ManagedSurface &surf) {
// This really should be a text area, but it's hard-coded in the game.
const DgdsFont *font = RequestData::getMenuFont();
const RequestData &r = _reqData._requests[0];
static const char *title;
if (DgdsEngine::getInstance()->getGameLang() == Common::EN_ANY)
title = "INVENTORY";
else if (DgdsEngine::getInstance()->getGameLang() == Common::DE_DEU)
title = "INVENTAR";
else if (DgdsEngine::getInstance()->getGameLang() == Common::FR_FRA)
title = "INVENTAIRE";
else
error("Unsupported language %d", DgdsEngine::getInstance()->getGameLang());
DgdsGameId gameId = DgdsEngine::getInstance()->getGameId();
byte txtColor = (gameId == GID_HOC ? 25 : 0);
int titleWidth = font->getStringWidth(title);
// Dragon always draws the header in the same spot; HoC centers it.
int x1, y1;
if (gameId == GID_DRAGON) {
// Constant offset
x1 = r._rect.x + 112;
y1 = r._rect.y + 7;
} else if (gameId == GID_HOC) {
// Centered on window
x1 = r._rect.x + (r._rect.width - font->getStringWidth(title)) / 2 - 3;
y1 = r._rect.y + 11;
} else { // GID_WILLY
// Constant offset
x1 = r._rect.x + 154;
y1 = r._rect.y + 8;
}
// Draw the border around the text
byte topColor = (gameId == GID_DRAGON ? 0xdf : (gameId == GID_HOC ? 16 : 15));
byte botColor = (gameId == GID_DRAGON ? 0xff : (gameId == GID_HOC ? 20 : 19));
int x2 = x1 + titleWidth + 6;
int y2 = y1 + font->getFontHeight();
surf.drawLine(x1, y1, x2, y1, topColor);
surf.drawLine(x2, y1 + 1, x2, y2, topColor);
surf.drawLine(x1, y1 + 1, x1, y2, botColor);
surf.drawLine(x1 + 1, y2, x1 + titleWidth + 5, y2, botColor);
// In willy also fill the area in the middle
if (gameId == GID_WILLY)
surf.fillRect(Common::Rect(Common::Point(x1 + 1, y1 + 1), titleWidth + 5, font->getFontHeight() - 1), 17);
font->drawString(&surf, title, x1 + 4, y1 + 2, titleWidth, txtColor);
}
void Inventory::draw(Graphics::ManagedSurface &surf, int itemCount) {
RequestData &boxreq = _reqData._requests[0];
DgdsEngine *engine = DgdsEngine::getInstance();
DgdsGameId gameId = engine->getGameId();
if (_showZoomBox) {
if (gameId != GID_WILLY)
_itemZoomBox->setVisible(true);
boxreq._rect.width = _fullWidth;
} else {
boxreq._rect.width = _itemBox->_width + _itemBox->_x * 2;
if (gameId != GID_WILLY) {
_itemZoomBox->setVisible(false);
boxreq._rect.width--;
}
}
//
// Decide whether the nextpage/prevpage buttons should be visible
//
bool needPageButtons =
(_itemArea->_width / _itemArea->_xStep) *
(_itemArea->_height / _itemArea->_yStep) < itemCount;
_prevPageBtn->setVisible(needPageButtons);
_nextPageBtn->setVisible(needPageButtons);
//
// Decide whether the time buttons should be visible (only in Dragon and Willy Beamish)
// In Willy Beamish the buttons are controlled by a global.
//
bool showTimeButtons;
switch (gameId) {
case GID_DRAGON:
showTimeButtons = true;
break;
case GID_WILLY:
showTimeButtons = static_cast<WillyGlobals *>(engine->getGameGlobals())->isDrawTimeSkipButtons();
break;
default:
showTimeButtons = false;
break;
}
if (_clockSkipMinBtn)
_clockSkipMinBtn->setVisible(showTimeButtons);
if (_clockSkipHrBtn)
_clockSkipHrBtn->setVisible(showTimeButtons);
//
// Decide whether the give-to and swap char buttons should be visible (only in China)
//
int16 otherChar = 0;
if (gameId == GID_HOC) {
otherChar = engine->getGDSScene()->getGlobal(0x34);
_giveToBtn->setVisible(otherChar != 0);
// This is only used to give the location so it's always false.
_changeCharBtn->setVisible(false);
}
boxreq.drawInvType(&surf);
if (gameId == GID_HOC && otherChar != 0) {
int16 swapCharIcon = DgdsEngine::HOC_CHAR_SWAP_ICONS[otherChar];
Common::Point pt = _changeCharBtn->topLeft();
engine->getIcons()->drawBitmap(swapCharIcon, pt.x, pt.y, boxreq._rect.toCommonRect(), surf);
}
drawHeader(surf);
drawTime(surf);
drawItems(surf);
}
void Inventory::drawTime(Graphics::ManagedSurface &surf) {
DgdsEngine *engine = DgdsEngine::getInstance();
if (engine->getGameId() == GID_HOC || !_invClock)
return;
const DgdsFont *font = RequestData::getMenuFont();
const Common::String timeStr = engine->getClock().getTimeStr();
const Common::Point clockpos = _invClock->topLeft();
if (engine->getGameId() == GID_DRAGON) {
surf.fillRect(Common::Rect(clockpos, _invClock->_width, _invClock->_height), 0);
RequestData::drawCorners(&surf, 19, clockpos.x - 2, clockpos.y - 2,
_invClock->_width + 4, _invClock->_height + 4);
} else { // GID_WILLY
surf.fillRect(Common::Rect(Common::Point(clockpos.x, clockpos.y - 1),
_invClock->_width, _invClock->_height + 2), 0);
RequestData::drawCorners(&surf, 25, clockpos.x - 2, clockpos.y - 5,
_invClock->_width + 8, _invClock->_height + 7);
}
font->drawString(&surf, timeStr, clockpos.x + 4, clockpos.y, font->getStringWidth(timeStr), _invClock->_col3);
}
void Inventory::drawItems(Graphics::ManagedSurface &surf) {
DgdsEngine *engine = DgdsEngine::getInstance();
const Common::SharedPtr<Image> &icons = engine->getIcons();
int x = (engine->getGameId() == GID_WILLY ? -2 : 0);
int y = 0;
const int xstep = _itemArea->_xStep;
const int ystep = _itemArea->_yStep;
const int imgAreaX = _itemArea->_parentX + _itemArea->_x;
const int imgAreaY = _itemArea->_parentY + _itemArea->_y;
Common::Rect itemRect(Common::Point(imgAreaX, imgAreaY), _itemArea->_width, _itemArea->_height);
if (!icons)
return;
// TODO: does this need to be adjusted ever?
const Common::Rect drawMask(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
int offset = _itemOffset;
Common::Array<GameItem> &items = engine->getGDSScene()->getGameItems();
for (auto &item: items) {
if (!isItemInInventory(item))
continue;
if (offset) {
offset--;
continue;
}
if (item._num == _highlightItemNo) {
// draw highlighted
Common::Rect highlightRect(Common::Point(imgAreaX + x, imgAreaY + y), xstep, ystep);
surf.fillRect(highlightRect, 4);
}
// Update the icon bounds - Note: original doesn't do this here, but if we don't then
// the Napent in Dragon is weirdly offset in y because its rect has a large height?
Common::SharedPtr<Graphics::ManagedSurface> icon = icons->getSurface(item._iconNum);
if (icon) {
item._rect.width = MIN((int)icon->w, item._rect.width);
item._rect.height = MIN((int)icon->h, item._rect.height);
}
// calculate draw offset for the image
int drawX = imgAreaX + x + (xstep - item._rect.width) / 2;
int drawY = imgAreaY + y + (ystep - item._rect.height) / 2;
icons->drawBitmap(item._iconNum, drawX, drawY, drawMask, surf);
item._rect.x = drawX;
item._rect.y = drawY;
x += xstep;
// Willy Beamish area is 270 and step is 54, so hack the width slightly.
if (x >= _itemArea->_width - 2) {
x = 0;
y += ystep;
}
if (y >= _itemArea->_height) {
break;
}
}
}
void Inventory::mouseUpdate(const Common::Point &pt) {
DgdsEngine *engine = DgdsEngine::getInstance();
GameItem *dragItem = engine->getScene()->getDragItem();
if (dragItem) {
engine->setMouseCursor(dragItem->_iconNum);
const RequestData &req = _reqData._requests[0];
if (!req._rect.contains(pt)) {
// dragged an item outside the inventory
dragItem->_inSceneNum = _openedFromSceneNum;
close();
}
} else {
engine->setMouseCursor(kDgdsMouseGameDefault);
}
}
GameItem *Inventory::itemUnderMouse(const Common::Point &pt) {
if (!_itemArea)
return nullptr;
DgdsEngine *engine = DgdsEngine::getInstance();
Common::Array<GameItem> &items = engine->getGDSScene()->getGameItems();
if (_itemArea->containsPoint(pt)) {
const int imgAreaX = _itemArea->_parentX + _itemArea->_x;
const int imgAreaY = _itemArea->_parentY + _itemArea->_y;
const int numAcross = _itemArea->_width / _itemArea->_xStep;
const int itemrow = (pt.y - imgAreaY) / _itemArea->_yStep;
const int itemcol = (pt.x - imgAreaX) / _itemArea->_xStep;
int itemnum = numAcross * itemrow + itemcol + _itemOffset;
for (auto &item: items) {
if (!isItemInInventory(item))
continue;
if (itemnum) {
itemnum--;
continue;
}
return &item;
}
}
return nullptr;
}
bool Inventory::isItemInInventory(const GameItem &item) {
DgdsEngine *engine = DgdsEngine::getInstance();
DgdsGameId gameId = engine->getGameId();
bool result = item._inSceneNum == 2; // && (item._flags & 4)
if (gameId == GID_HOC) {
int16 currentCharacter = engine->getGDSScene()->getGlobal(0x33);
assert(currentCharacter < 4);
result = result && item._quality == HOC_CHARACTER_QUALS[currentCharacter];
}
return result;
}
void Inventory::mouseLDown(const Common::Point &pt) {
DgdsEngine *engine = DgdsEngine::getInstance();
// In willy beamish, might be clicking to drop a drag item - ignore.
GameItem *dragItem = engine->getScene()->getDragItem();
if (dragItem)
return;
// If clicking outside area, ignore mousedown - will close on mouseup.
RequestData &boxreq = _reqData._requests[0];
if (!boxreq._rect.contains(pt))
return;
if (engine->getScene()->hasVisibleDialog() || !_itemBox->containsPoint(pt)) {
return engine->getScene()->mouseLDown(pt);
} else {
GameItem *underMouse = itemUnderMouse(pt);
if (underMouse) {
_highlightItemNo = underMouse->_num;
engine->getScene()->runOps(underMouse->onPickUpOps);
engine->getScene()->setDragItem(underMouse);
underMouse->_flags |= kItemStateWasInInv;
if (underMouse->_iconNum)
engine->setMouseCursor(underMouse->_iconNum);
}
}
}
void Inventory::mouseLUp(const Common::Point &pt) {
DgdsEngine *engine = DgdsEngine::getInstance();
GameItem *dragItem = engine->getScene()->getDragItem();
if (dragItem) {
if (engine->getGameId() == GID_WILLY)
dragItem->_inSceneNum = 2;
engine->getScene()->onDragFinish(pt);
return;
}
GDSScene *gds = engine->getGDSScene();
engine->setMouseCursor(kDgdsMouseGameDefault);
int itemsPerPage = (_itemArea->_width / _itemArea->_xStep) * (_itemArea->_height / _itemArea->_yStep);
if (_exitButton->containsPoint(pt)) {
close();
} else if (_nextPageBtn->containsPoint(pt) && _nextPageBtn->isVisible()) {
int numInvItems = 0;
const Common::Array<GameItem> &items = engine->getGDSScene()->getGameItems();
for (auto &item: items) {
if (isItemInInventory(item))
numInvItems++;
}
if (_itemOffset < numInvItems && _itemOffset + itemsPerPage <= numInvItems)
_itemOffset += itemsPerPage;
} else if (_prevPageBtn->containsPoint(pt) && _prevPageBtn->isVisible()) {
if (_itemOffset > 0)
_itemOffset -= itemsPerPage;
} else if (_clockSkipMinBtn && _clockSkipMinBtn->isVisible() && _clockSkipMinBtn->containsPoint(pt)) {
engine->getClock().addGameTime(1);
} else if (_clockSkipHrBtn && _clockSkipHrBtn->isVisible() && _clockSkipHrBtn->containsPoint(pt)) {
engine->getClock().addGameTime(60);
} else if (_giveToBtn && _giveToBtn->isVisible() && _giveToBtn->containsPoint(pt)) {
Common::Array<GameItem> &items = engine->getGDSScene()->getGameItems();
for (auto &item: items) {
if (item._num == _highlightItemNo) {
item._quality = HOC_CHARACTER_QUALS[gds->getGlobal(0x34)];
break;
}
}
} else if (_changeCharBtn && _changeCharBtn->containsPoint(pt)) {
int16 prevChar = gds->getGlobal(0x33);
gds->setGlobal(0x33, gds->getGlobal(0x34));
gds->setGlobal(0x34, prevChar);
} else if (_dropBtn && _dropBtn->containsPoint(pt) && _highlightItemNo >= 0) {
Common::Array<GameItem> &items = engine->getGDSScene()->getGameItems();
for (auto &item: items) {
if (item._num == _highlightItemNo) {
item._inSceneNum = _openedFromSceneNum;
break;
}
}
}
}
void Inventory::mouseRUp(const Common::Point &pt) {
DgdsEngine *engine = DgdsEngine::getInstance();
if (_itemBox->containsPoint(pt)) {
GameItem *underMouse = itemUnderMouse(pt);
if (underMouse) {
setShowZoomBox(true);
if (engine->getGameId() == GID_HOC) {
// Slight hack - blank the background if zooming in HOC because it uses
// different palettes for zoomed items (original does this too)
// We also do this on scene transition, but need to do it again
// here for zooming within the box.
engine->getBackgroundBuffer().fillRect(Common::Rect(SCREEN_WIDTH, SCREEN_HEIGHT), 0);
}
engine->getScene()->runOps(underMouse->onLookOps);
}
} else {
engine->getScene()->mouseRUp(pt);
}
}
Common::Error Inventory::syncState(Common::Serializer &s) {
s.syncAsUint16LE(_openedFromSceneNum);
s.syncAsByte(_isOpen);
s.syncAsSint16LE(_highlightItemNo);
s.syncAsSint16LE(_itemOffset);
return Common::kNoError;
}
} // end namespace Dgds