mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
773 lines
25 KiB
C++
773 lines
25 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 "dgds/dialog.h"
|
|
|
|
#include "common/debug.h"
|
|
#include "common/rect.h"
|
|
#include "common/system.h"
|
|
|
|
#include "dgds/dgds.h"
|
|
#include "dgds/request.h"
|
|
#include "dgds/scene.h"
|
|
#include "dgds/font.h"
|
|
#include "dgds/drawing.h"
|
|
#include "dgds/debug_util.h"
|
|
|
|
namespace Dgds {
|
|
|
|
|
|
int Dialog::_lastSelectedDialogItemNum = 0;
|
|
Dialog *Dialog::_lastDialogSelectionChangedFor = nullptr;
|
|
|
|
|
|
Dialog::Dialog() : _num(0), _bgColor(0), _fontColor(0), _selectionBgCol(0), _selectonFontCol(0),
|
|
_fontSize(0), _flags(kDlgFlagNone), _frameType(kDlgFramePlain), _time(0), _nextDialogDlgNum(0),
|
|
_nextDialogFileNum(0), _fileNum(0), _talkDataNum(0), _talkDataHeadNum(0)
|
|
{}
|
|
|
|
|
|
void Dialog::draw(Graphics::ManagedSurface *dst, DialogDrawStage stage) {
|
|
if (!_state)
|
|
_state.reset(new DialogState());
|
|
|
|
switch (_frameType) {
|
|
case kDlgFramePlain: return drawType1(dst, stage);
|
|
case kDlgFrameBorder: return drawType2(dst, stage);
|
|
case kDlgFrameThought: return drawType3(dst, stage);
|
|
case kDlgFrameRounded: return drawType4(dst, stage);
|
|
default: error("unexpected frame type %d for dialog %d", _frameType, _num);
|
|
}
|
|
}
|
|
|
|
|
|
const DgdsFont *Dialog::getDlgTextFont() const {
|
|
const FontManager *fontman = DgdsEngine::getInstance()->getFontMan();
|
|
FontManager::FontType fontType = FontManager::kGameDlgFont;
|
|
if (_fontSize == 1)
|
|
fontType = FontManager::k8x8Font;
|
|
else if (_fontSize == 3)
|
|
fontType = FontManager::k4x5Font;
|
|
else if (_fontSize == 4 && DgdsEngine::getInstance()->getGameId() == GID_WILLY)
|
|
fontType = FontManager::kGameDlgFont;
|
|
else if (_fontSize == 5 && DgdsEngine::getInstance()->getGameId() == GID_HOC)
|
|
fontType = FontManager::kChinaFont;
|
|
return fontman->getFont(fontType);
|
|
}
|
|
|
|
// box with simple frame
|
|
void Dialog::drawType1(Graphics::ManagedSurface *dst, DialogDrawStage stage) {
|
|
if (!_state)
|
|
return;
|
|
int x = _rect.x;
|
|
int y = _rect.y;
|
|
int w = _rect.width;
|
|
int h = _rect.height;
|
|
|
|
DgdsEngine *engine = DgdsEngine::getInstance();
|
|
if (stage == kDlgDrawStageBackground) {
|
|
dst->frameRect(Common::Rect(x, y, x + w, y + h), _bgColor);
|
|
if (engine->getGameId() != GID_WILLY) {
|
|
dst->fillRect(Common::Rect(x + 1, y + 1, x + w - 1, y + h - 1), _fontColor);
|
|
} else {
|
|
dst->frameRect(Common::Rect(x + 1, y + 1, x + w - 1, y + h - 1), _fontColor);
|
|
dst->fillRect(Common::Rect(x + 2, y + 2, x + w - 2, y + h - 2), _bgColor);
|
|
}
|
|
} else if (stage == kDlgDrawFindSelectionPointXY) {
|
|
drawFindSelectionXY();
|
|
} else if (stage == kDlgDrawFindSelectionTxtOffset) {
|
|
drawFindSelectionTxtOffset();
|
|
} else {
|
|
if (engine->getGameId() != GID_WILLY)
|
|
_state->_loc = DgdsRect(x + 3, y + 3, w - 6, h - 6);
|
|
else
|
|
_state->_loc = DgdsRect(x + 5, y + 5, w - 10, h - 10);
|
|
byte txtCol = (engine->getGameId() == GID_WILLY) ? _fontColor : _bgColor;
|
|
drawForeground(dst, txtCol, _str);
|
|
}
|
|
}
|
|
|
|
void Dialog::drawType2BackgroundDragon(Graphics::ManagedSurface *dst, const Common::String &title) {
|
|
_state->_loc = DgdsRect(_rect.x + 6, _rect.y + 6, _rect.width - 12, _rect.height - 12);
|
|
RequestData::fillBackground(dst, _rect.x, _rect.y, _rect.width, _rect.height, 0);
|
|
RequestData::drawCorners(dst, 11, _rect.x, _rect.y, _rect.width, _rect.height);
|
|
if (!title.empty()) {
|
|
// TODO: Maybe should measure the font?
|
|
_state->_loc.y += 11;
|
|
_state->_loc.height -= 11;
|
|
RequestData::drawHeader(dst, _rect.x, _rect.y, _rect.width, 4, title, 0, true, 0, 15);
|
|
}
|
|
|
|
if (hasFlag(kDlgFlagFlatBg)) {
|
|
dst->fillRect(_state->_loc.toCommonRect(), 0);
|
|
} else {
|
|
RequestData::fillBackground(dst, _state->_loc.x, _state->_loc.y, _state->_loc.width, _state->_loc.height, 6);
|
|
}
|
|
|
|
RequestData::drawCorners(dst, 19, _state->_loc.x - 2, _state->_loc.y - 2,
|
|
_state->_loc.width + 4, _state->_loc.height + 4);
|
|
|
|
_state->_loc.y++;
|
|
_state->_loc.height--;
|
|
_state->_loc.x += 8;
|
|
_state->_loc.width -= 16;
|
|
}
|
|
|
|
void Dialog::drawType2BackgroundChina(Graphics::ManagedSurface *dst, const Common::String &title) {
|
|
if (title.empty()) {
|
|
_state->_loc = DgdsRect(_rect.x + 10, _rect.y + 10, _rect.width - 20, _rect.height - 20);
|
|
RequestData::fillBackground(dst, _rect.x, _rect.y, _rect.width, _rect.height, 0);
|
|
RequestData::drawCorners(dst, 1, _rect.x, _rect.y, _rect.width, _rect.height);
|
|
} else {
|
|
// This is 1 more pixel down than the original, but seems to be needed to get the right spot?
|
|
_state->_loc = DgdsRect(_rect.x + 6, _rect.y + 17, _rect.width - 12, _rect.height - 24);
|
|
dst->fillRect(Common::Rect(Common::Point(_rect.x, _rect.y), _rect.width, _rect.height), 0);
|
|
RequestData::drawCorners(dst, 11, _rect.x, _rect.y, _rect.width, _rect.height);
|
|
RequestData::drawHeader(dst, _rect.x, _rect.y, _rect.width, 2, title, _fontColor, false, 0, 0);
|
|
}
|
|
}
|
|
|
|
void Dialog::drawType2BackgroundBeamish(Graphics::ManagedSurface *dst, const Common::String &title) {
|
|
byte fillCol = 0;
|
|
if (DgdsEngine::getInstance()->isAltDlgColors())
|
|
fillCol = 20;
|
|
|
|
_state->_loc = DgdsRect(_rect.x + 11, _rect.y + 10, _rect.width - 22, _rect.height - 20);
|
|
if (title.empty()) {
|
|
dst->fillRect(Common::Rect(Common::Point(_rect.x + 2, _rect.y + 2), _rect.width - 4, _rect.height - 4), fillCol);
|
|
uint16 cornerOffset = DgdsEngine::getInstance()->isAltDlgColors() ? 46 : 54;
|
|
RequestData::drawCorners(dst, cornerOffset, _rect.x, _rect.y, _rect.width, _rect.height);
|
|
} else {
|
|
dst->fillRect(Common::Rect(Common::Point(_rect.x + 2, _rect.y + 2), _rect.width - 4, _rect.height - 4), fillCol);
|
|
RequestData::drawCorners(dst, 46, _rect.x, _rect.y, _rect.width, _rect.height);
|
|
_state->_loc.y += 15;
|
|
_state->_loc.height -= 15;
|
|
RequestData::drawHeader(dst, _rect.x, _rect.y + 5, _rect.width, 2, title, _fontColor, false, 0, 0);
|
|
}
|
|
}
|
|
|
|
// box with fancy frame and optional title (everything before ":")
|
|
void Dialog::drawType2(Graphics::ManagedSurface *dst, DialogDrawStage stage) {
|
|
if (!_state)
|
|
return;
|
|
|
|
Common::String title;
|
|
Common::String txt;
|
|
|
|
//
|
|
// Colon has to be followed by the first CR to be used as a heading
|
|
//
|
|
uint32 colonpos = _str.find(':');
|
|
uint32 crpos = _str.find('\r');
|
|
bool haveColon = colonpos != Common::String::npos;
|
|
bool haveCR = crpos != Common::String::npos;
|
|
if (haveColon && haveCR && crpos == colonpos + 1) {
|
|
title = _str.substr(0, colonpos);
|
|
txt = _str.substr(colonpos + 1);
|
|
// Most have a CR after the colon? trim it to remove a blank line.
|
|
if (txt.size() && txt[0] == '\r')
|
|
txt = txt.substr(1);
|
|
} else {
|
|
txt = _str;
|
|
}
|
|
|
|
//
|
|
// Special case for HoC to update the Shekel count in their description
|
|
// and Willy Beamish update dollars
|
|
//
|
|
// This is how the original games do it too.
|
|
//
|
|
DgdsEngine *engine = DgdsEngine::getInstance();
|
|
if (_fileNum == 0x5d && _num == 0x32 && engine->getGameId() == GID_HOC) {
|
|
int16 shekels = engine->getGDSScene()->getGlobal(44);
|
|
const Common::String numstr = Common::String::format("%3d", shekels);
|
|
uint32 offset = txt.find("###");
|
|
if (offset != Common::String::npos)
|
|
txt.replace(offset, 3, numstr);
|
|
} else if (_fileNum == 67 && _num == 9 && engine->getGameId() == GID_WILLY) {
|
|
int16 cents = engine->getGDSScene()->getGlobal(3);
|
|
const Common::String numstr = Common::String::format("%3d.%02d", cents / 100, cents % 100);
|
|
uint32 offset = txt.find("###.##");
|
|
if (offset != Common::String::npos)
|
|
txt.replace(offset, 6, numstr);
|
|
}
|
|
|
|
if (stage == kDlgDrawStageBackground) {
|
|
if (engine->getGameId() == GID_DRAGON)
|
|
drawType2BackgroundDragon(dst, title);
|
|
else if (engine->getGameId() == GID_HOC)
|
|
drawType2BackgroundChina(dst, title);
|
|
else
|
|
drawType2BackgroundBeamish(dst, title);
|
|
|
|
} else if (stage == kDlgDrawFindSelectionPointXY) {
|
|
drawFindSelectionXY();
|
|
} else if (stage == kDlgDrawFindSelectionTxtOffset) {
|
|
drawFindSelectionTxtOffset();
|
|
} else {
|
|
drawForeground(dst, _fontColor, txt);
|
|
}
|
|
}
|
|
|
|
// Find the last line that will be printed - we don't use empty lines
|
|
static uint _countPrintedLines(const Common::Array<Common::String> &lines) {
|
|
uint nprinted = 0;
|
|
for (uint i = 0; i < lines.size(); i++) {
|
|
if (!lines[i].empty())
|
|
nprinted = i;
|
|
}
|
|
return nprinted + 1;
|
|
}
|
|
|
|
// Comic thought box made up of circles with 2 circles going up to it.
|
|
// Draw circles with 5/4 more pixels in x because the pixels are not square.
|
|
void Dialog::drawType3(Graphics::ManagedSurface *dst, DialogDrawStage stage) {
|
|
if (!_state)
|
|
return;
|
|
|
|
if (stage == kDlgDrawStageBackground) {
|
|
uint16 xradius = 9999;
|
|
uint16 yradius = 40;
|
|
const int16 usabley = _rect.height - 31;
|
|
const int16 usablex = _rect.width - 30;
|
|
for (uint16 testyradius = 40; testyradius != 0; testyradius--) {
|
|
int16 testxradius = (testyradius * 5) / 4;
|
|
if ((usablex / testxradius > 2) && (usabley / testyradius > 2)) {
|
|
testxradius = usablex % testxradius + usabley % testyradius;
|
|
if (testxradius < xradius) {
|
|
yradius = testyradius;
|
|
xradius = testxradius;
|
|
}
|
|
}
|
|
if (testyradius < 20 && xradius != 9999)
|
|
break;
|
|
}
|
|
|
|
xradius = (yradius * 5) / 4;
|
|
const int16 circlesAcross = MAX(1, usablex / xradius - 1);
|
|
const int16 circlesDown = MAX(1, usabley / yradius - 1);
|
|
|
|
uint16 x = _rect.x + xradius;
|
|
uint16 y = _rect.y + yradius;
|
|
|
|
bool isbig = _rect.x + _rect.width / 2 > 160;
|
|
if (isbig)
|
|
x = x + 30;
|
|
|
|
byte fgcol = 0;
|
|
byte bgcol = 15;
|
|
if (hasFlag(kDlgFlagFlatBg)) {
|
|
bgcol = _bgColor;
|
|
fgcol = _fontColor;
|
|
}
|
|
|
|
for (int i = 1; i < circlesDown; i++) {
|
|
Drawing::filledCircle(x, y, xradius, yradius, dst, fgcol, bgcol);
|
|
y += yradius;
|
|
}
|
|
for (int i = 1; i < circlesAcross; i++) {
|
|
Drawing::filledCircle(x, y, xradius, yradius, dst, fgcol, bgcol);
|
|
x += xradius;
|
|
}
|
|
for (int i = 1; i < circlesDown; i++) {
|
|
Drawing::filledCircle(x, y, xradius, yradius, dst, fgcol, bgcol);
|
|
y -= yradius;
|
|
}
|
|
for (int i = 1; i < circlesAcross; i++) {
|
|
Drawing::filledCircle(x, y, xradius, yradius, dst, fgcol, bgcol);
|
|
x -= xradius;
|
|
}
|
|
|
|
uint16 smallCircleX;
|
|
if (isbig) {
|
|
Drawing::filledCircle((x - xradius) - 5, y + circlesDown * yradius + 5, 10, 8, dst, fgcol, bgcol);
|
|
smallCircleX = (x - xradius) - 20;
|
|
} else {
|
|
Drawing::filledCircle(x + circlesAcross * xradius + 5, y + circlesDown * yradius + 5, 10, 8, dst, fgcol, bgcol);
|
|
smallCircleX = x + circlesAcross * xradius + 20;
|
|
}
|
|
|
|
Drawing::filledCircle(smallCircleX, y + circlesDown * yradius + 25, 5, 4, dst, fgcol, bgcol);
|
|
|
|
int16 yoff = (yradius * 27) / 32;
|
|
dst->fillRect(Common::Rect(x, y - yoff,
|
|
x + (circlesAcross - 1) * xradius + 1,
|
|
y + (circlesDown - 1) * yradius + yoff + 1), bgcol);
|
|
int16 xoff = (xradius * 27) / 32;
|
|
dst->fillRect(Common::Rect(x - xoff, y,
|
|
x + (circlesAcross - 1) * xradius + xoff + 1,
|
|
y + (circlesDown - 1) * yradius + 1), bgcol);
|
|
|
|
int16 textRectX = x - xradius / 2;
|
|
int16 textRectY = y - yradius / 2;
|
|
assert(_state);
|
|
_state->_loc = DgdsRect(textRectX, textRectY, circlesAcross * xradius , circlesDown * yradius);
|
|
} else if (stage == kDlgDrawFindSelectionPointXY) {
|
|
drawFindSelectionXY();
|
|
} else if (stage == kDlgDrawFindSelectionTxtOffset) {
|
|
drawFindSelectionTxtOffset();
|
|
} else {
|
|
drawForeground(dst, _fontColor, _str);
|
|
}
|
|
}
|
|
|
|
// ellipse in Dragon, text with no background in HoC
|
|
void Dialog::drawType4(Graphics::ManagedSurface *dst, DialogDrawStage stage) {
|
|
if (!_state)
|
|
return;
|
|
|
|
int x = _rect.x;
|
|
int y = _rect.y;
|
|
int w = _rect.width;
|
|
int h = _rect.height;
|
|
|
|
int midy = (h - 1) / 2;
|
|
byte fillcolor;
|
|
byte fillbgcolor;
|
|
if (!hasFlag(kDlgFlagFlatBg)) {
|
|
fillcolor = 0;
|
|
fillbgcolor = 15;
|
|
} else {
|
|
fillcolor = _fontColor;
|
|
fillbgcolor = _bgColor;
|
|
}
|
|
|
|
const DgdsGameId gameId = DgdsEngine::getInstance()->getGameId();
|
|
if (stage == kDlgDrawStageBackground) {
|
|
//int radius = (midy * 5) / 4;
|
|
|
|
// This is not exactly the same as the original - might need some work to get pixel-perfect
|
|
// Beamish uses 20x20 dialogs of type 4 to have effects without actually drawing anything.
|
|
if (gameId != GID_HOC && (w > 22 || h > 22)) {
|
|
Common::Rect drawRect(x, y, x + w, y + h);
|
|
dst->drawRoundRect(drawRect, midy, fillbgcolor, true);
|
|
dst->drawRoundRect(drawRect, midy, fillcolor, false);
|
|
}
|
|
} else if (stage == kDlgDrawFindSelectionPointXY) {
|
|
drawFindSelectionXY();
|
|
} else if (stage == kDlgDrawFindSelectionTxtOffset) {
|
|
drawFindSelectionTxtOffset();
|
|
} else {
|
|
assert(_state);
|
|
if (gameId != GID_HOC) {
|
|
_state->_loc = DgdsRect(x + midy, y + 1, w - midy, h - 1);
|
|
} else {
|
|
_state->_loc = DgdsRect(x, y, w, h);
|
|
fillcolor = 25; // ignore the color??
|
|
}
|
|
|
|
// WORKAROUND for the Willy Beamish dialogs which are 20x20 - these should not be drawn
|
|
if (gameId == GID_WILLY && (w <= 22 && h <= 22))
|
|
return;
|
|
|
|
drawForeground(dst, fillcolor, _str);
|
|
}
|
|
}
|
|
|
|
int _stringWidthIgnoringTrainingSpace(const DgdsFont *font, const Common::String &line) {
|
|
if (Common::isSpace(line.lastChar())) {
|
|
// Find end without trailing spaces
|
|
int i = line.size() - 2;
|
|
while (i > 0 && Common::isSpace(line[i]))
|
|
i--;
|
|
return font->getStringWidth(line.substr(0, i + 1));
|
|
} else {
|
|
return font->getStringWidth(line);
|
|
}
|
|
}
|
|
|
|
int _maxWidthIgnoringTrailingSpace(const DgdsFont *font, const Common::Array<Common::String> &lines) {
|
|
//
|
|
// The line wrapper returns width including trailing spaces, but for accurate
|
|
// layout we need to ignore spaces in the string width.
|
|
//
|
|
int maxWidth = 0;
|
|
for (const auto &line : lines) {
|
|
maxWidth = MAX(_stringWidthIgnoringTrainingSpace(font, line), maxWidth);
|
|
}
|
|
return maxWidth;
|
|
}
|
|
|
|
|
|
void Dialog::drawFindSelectionXY() {
|
|
if (!_state)
|
|
return;
|
|
|
|
const DgdsFont *font = getDlgTextFont();
|
|
|
|
// Find the appropriate _lastMouseX/lastMouseY value given the last _strMouseLoc.
|
|
|
|
int x = _state->_loc.x;
|
|
_state->_lastMouseX = x;
|
|
int y = _state->_loc.y + 1;
|
|
_state->_lastMouseY = y;
|
|
_state->_charWidth = font->getMaxCharWidth();
|
|
_state->_charHeight = font->getFontHeight();
|
|
if (_state->_strMouseLoc) {
|
|
Common::Array<Common::String> lines;
|
|
font->wordWrapText(_str, _state->_loc.width, lines, 0, Graphics::kWordWrapOnExplicitNewLines | Graphics::kWordWrapAllowTrailingWhitespace);
|
|
uint nlines = _countPrintedLines(lines);
|
|
int maxWidth = _maxWidthIgnoringTrailingSpace(font, lines);
|
|
|
|
if (hasFlag(kDlgFlagLeftJust)) {
|
|
x = x + (_state->_loc.width - maxWidth - 1) / 2;
|
|
_state->_lastMouseX = x;
|
|
y = y + (_state->_loc.height - ((int)nlines * _state->_charHeight) - 1) / 2;
|
|
_state->_lastMouseY = y;
|
|
}
|
|
|
|
if (_state->_strMouseLoc >= (int)_str.size())
|
|
_state->_strMouseLoc = _str.size() - 1;
|
|
|
|
// Find the location of the mouse loc in the wrapped string.
|
|
int totalchars = 0;
|
|
for (uint lineno = 0; lineno < lines.size(); lineno++) {
|
|
// +1 char for the space or CR that caused the wrap.
|
|
int nexttotalchars = totalchars + lines[lineno].size() + 1;
|
|
if (nexttotalchars >= _state->_strMouseLoc)
|
|
break;
|
|
totalchars = nexttotalchars;
|
|
y += _state->_charHeight;
|
|
}
|
|
|
|
// now get width of the remaining string to the mouse str offset
|
|
x += _stringWidthIgnoringTrainingSpace(font, _str.substr(totalchars, _state->_strMouseLoc - totalchars));
|
|
|
|
// TODO: does this make sense?
|
|
if (_state->_loc.x + _state->_loc.width < (x + font->getCharWidth(_str[_state->_strMouseLoc]))) {
|
|
if (_str[_state->_strMouseLoc] < '!') {
|
|
_state->_charHeight = 0;
|
|
_state->_charWidth = 0;
|
|
_state->_lastMouseY = 0;
|
|
_state->_lastMouseX = 0;
|
|
return;
|
|
}
|
|
x = _state->_loc.x;
|
|
y += _state->_charHeight;
|
|
}
|
|
|
|
_state->_lastMouseX = x;
|
|
_state->_lastMouseY = y;
|
|
_state->_charWidth = font->getCharWidth(_str[_state->_strMouseLoc]);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get offsets into a string for a given set of wrapped lines.
|
|
*
|
|
* Font::wordWrapText will wrap the lines on a space or a CR, so each
|
|
* line's offset is the total chars from the previous line plus 1.
|
|
* each
|
|
*
|
|
* Returns one more value than the number of lines - the last one is
|
|
* s.size() for convenience.
|
|
*/
|
|
static Common::Array<int> _wrappedLineOffsets(const Common::String &s, const Common::Array<Common::String> &lines) {
|
|
Common::Array<int> ret;
|
|
int off = 0;
|
|
for (const Common::String &l : lines) {
|
|
ret.push_back(off);
|
|
off += l.size() + 1;
|
|
}
|
|
ret.push_back(s.size());
|
|
return ret;
|
|
}
|
|
|
|
void Dialog::drawFindSelectionTxtOffset() {
|
|
if (!_state)
|
|
return;
|
|
|
|
// Find the appropriate _strMouseLoc value given the last x/y position.
|
|
|
|
const DgdsFont *font = getDlgTextFont();
|
|
int lastMouseX = _state->_lastMouseX;
|
|
int lastMouseY = _state->_lastMouseY;
|
|
int lineHeight = font->getFontHeight();
|
|
int dlgx = _state->_loc.x;
|
|
int dlgy = _state->_loc.y;
|
|
|
|
Common::Array<Common::String> lines;
|
|
font->wordWrapText(_str, _state->_loc.width, lines, 0, Graphics::kWordWrapOnExplicitNewLines | Graphics::kWordWrapAllowTrailingWhitespace);
|
|
uint numPrintedLines = _countPrintedLines(lines);
|
|
int maxWidth = _maxWidthIgnoringTrailingSpace(font, lines);
|
|
|
|
if (hasFlag(kDlgFlagLeftJust)) {
|
|
int textHeight = numPrintedLines * lineHeight;
|
|
dlgx += (_state->_loc.width - maxWidth - 1) / 2;
|
|
dlgy += (_state->_loc.height - textHeight - 1) / 2;
|
|
}
|
|
|
|
const Common::Array<int> lineOffs = _wrappedLineOffsets(_str, lines);
|
|
|
|
uint lineno;
|
|
uint totalchars = 0;
|
|
for (lineno = 0; lineno < lines.size() && dlgy + lineHeight < lastMouseY; lineno++) {
|
|
totalchars = lineOffs[lineno + 1];
|
|
dlgy += lineHeight;
|
|
}
|
|
|
|
if (lineno < lines.size()) {
|
|
const Common::String &line = lines[lineno];
|
|
for (uint charno = 0; charno < line.size(); charno++) {
|
|
int charwidth = font->getCharWidth(line[charno]);
|
|
if (lastMouseX <= dlgx + charwidth) {
|
|
_state->_strMouseLoc = totalchars + charno;
|
|
return;
|
|
}
|
|
dlgx += charwidth;
|
|
}
|
|
// Mouse is off the end of the line
|
|
totalchars += line.size() + 1;
|
|
_state->_strMouseLoc = totalchars;
|
|
return;
|
|
}
|
|
|
|
_state->_strMouseLoc = _str.size();
|
|
return;
|
|
}
|
|
|
|
void Dialog::drawForeground(Graphics::ManagedSurface *dst, uint16 fontcol, const Common::String &txt) {
|
|
// This is where we actually draw the text.
|
|
// For now do the simplest wrapping, no highlighting.
|
|
assert(_state);
|
|
|
|
Common::StringArray lines;
|
|
const DgdsFont *font = getDlgTextFont();
|
|
const int h = font->getFontHeight();
|
|
font->wordWrapText(txt, _state->_loc.width, lines, 0, Graphics::kWordWrapOnExplicitNewLines | Graphics::kWordWrapAllowTrailingWhitespace);
|
|
uint numPrintedLines = _countPrintedLines(lines);
|
|
|
|
int ystart = _state->_loc.y + (_state->_loc.height - (int)numPrintedLines * h) / 2;
|
|
|
|
int x = _state->_loc.x;
|
|
|
|
int highlightStart = INT_MAX;
|
|
int highlightEnd = INT_MAX;
|
|
if (_state->_selectedAction) {
|
|
// find the txt in the full dlg string, as action offsets include the heading
|
|
int txtoffset = _str.find(txt);
|
|
highlightStart = (int)_state->_selectedAction->strStart - txtoffset;
|
|
highlightEnd = (int)_state->_selectedAction->strEnd - txtoffset;
|
|
}
|
|
|
|
const Common::Array<int> lineOffs = _wrappedLineOffsets(txt, lines);
|
|
|
|
Graphics::TextAlign align;
|
|
int xwidth;
|
|
if (hasFlag(kDlgFlagLeftJust)) {
|
|
int maxlen = 0;
|
|
// each line left-aligned, but overall block is still centered
|
|
for (const auto &line : lines)
|
|
maxlen = MAX(maxlen, _stringWidthIgnoringTrainingSpace(font, line));
|
|
x += (_state->_loc.width - maxlen) / 2;
|
|
align = Graphics::kTextAlignLeft;
|
|
xwidth = maxlen;
|
|
} else {
|
|
align = Graphics::kTextAlignCenter;
|
|
xwidth = _state->_loc.width;
|
|
}
|
|
|
|
for (uint i = 0; i < lines.size(); i++) {
|
|
// Draw every line unhighlighted then highlight bits as needed
|
|
font->drawString(dst, lines[i], x, ystart + i * h, xwidth, fontcol, align);
|
|
if (highlightStart < lineOffs[i + 1] && highlightEnd > lineOffs[i]) {
|
|
// Highlight on this line. Redraw whatever part is highlighted.
|
|
int lineLen = (int)lines[i].size();
|
|
int lineHighlightStart = MAX(highlightStart - lineOffs[i], 0);
|
|
int lineHighlightEnd = MIN(highlightEnd - lineOffs[i], lineLen);
|
|
int highlightXOff = lineHighlightStart ? font->getStringWidth(lines[i].substr(0, lineHighlightStart)) : 0;
|
|
Common::String highlightString = lines[i].substr(lineHighlightStart, lineHighlightEnd - lineHighlightStart);
|
|
font->drawString(dst, highlightString, x + highlightXOff, ystart + i * h, xwidth, _selectonFontCol, align);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Dialog::setFlag(DialogFlags flg) {
|
|
_flags = static_cast<DialogFlags>(_flags | flg);
|
|
}
|
|
|
|
void Dialog::clearFlag(DialogFlags flg) {
|
|
_flags = static_cast<DialogFlags>(_flags & ~flg);
|
|
}
|
|
|
|
void Dialog::flipFlag(DialogFlags flg) {
|
|
_flags = static_cast<DialogFlags>(_flags ^ flg);
|
|
}
|
|
|
|
bool Dialog::hasFlag(DialogFlags flg) const {
|
|
return _flags & flg;
|
|
}
|
|
|
|
void Dialog::clear() {
|
|
clearFlag(kDlgFlagHiFinished);
|
|
clearFlag(kDlgFlagRedrawSelectedActionChanged);
|
|
clearFlag(kDlgFlagHi10);
|
|
clearFlag(kDlgFlagHi20);
|
|
clearFlag(kDlgFlagHi40);
|
|
clearFlag(kDlgFlagVisible);
|
|
_state.reset();
|
|
}
|
|
|
|
void Dialog::updateSelectedAction(int delta) {
|
|
if (!_state)
|
|
return;
|
|
|
|
if (_lastDialogSelectionChangedFor != this) {
|
|
_lastDialogSelectionChangedFor = this;
|
|
_lastSelectedDialogItemNum = 0;
|
|
}
|
|
|
|
if (_state->_selectedAction) {
|
|
for (uint i = 0; i < _action.size(); i++) {
|
|
if (_state->_selectedAction == &_action[i]) {
|
|
_lastSelectedDialogItemNum = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_lastSelectedDialogItemNum += delta;
|
|
if (!_action.empty()) {
|
|
while (_lastSelectedDialogItemNum < 0)
|
|
_lastSelectedDialogItemNum += _action.size();
|
|
_lastSelectedDialogItemNum = _lastSelectedDialogItemNum % _action.size();
|
|
}
|
|
|
|
int mouseX = _state->_loc.x + _state->_loc.width;
|
|
int mouseY = _state->_loc.y + _state->_loc.height - 2;
|
|
if (_action.size() > 1) {
|
|
_state->_strMouseLoc = _action[_lastSelectedDialogItemNum].strStart;
|
|
draw(nullptr, kDlgDrawFindSelectionPointXY);
|
|
// Move the mouse over the selected item
|
|
mouseY = _state->_lastMouseY + _state->_charHeight / 2;
|
|
}
|
|
|
|
if (_action.size() > 1 || !delta) {
|
|
debug(1, "Dialog %d: update mouse to %d, %d (mouseloc %d, selnum %d)", _num, mouseX, mouseY, _state->_strMouseLoc, _lastSelectedDialogItemNum);
|
|
g_system->warpMouse(mouseX, mouseY);
|
|
}
|
|
}
|
|
|
|
struct DialogAction *Dialog::pickAction(bool isClosing, bool isForceClose) {
|
|
DgdsEngine *engine = DgdsEngine::getInstance();
|
|
if (!isForceClose && isClosing) {
|
|
if (_action.empty())
|
|
return nullptr;
|
|
else
|
|
return &_action[engine->getRandom().getRandomNumber(_action.size() - 1)];
|
|
}
|
|
assert(_state);
|
|
const Common::Point lastMouse = engine->getLastMouse();
|
|
if (_state->_loc.x <= lastMouse.x &&
|
|
_state->_loc.x + _state->_loc.width >= lastMouse.x &&
|
|
_state->_loc.y <= lastMouse.y &&
|
|
_state->_loc.y + _state->_loc.height >= lastMouse.y) {
|
|
_state->_lastMouseX = lastMouse.x;
|
|
_state->_lastMouseY = lastMouse.y;
|
|
draw(nullptr, kDlgDrawFindSelectionTxtOffset);
|
|
|
|
char underMouse;
|
|
if (_state->_strMouseLoc >= 0 && _state->_strMouseLoc < (int)_str.size())
|
|
underMouse = _str[_state->_strMouseLoc];
|
|
else
|
|
underMouse = '\0';
|
|
|
|
for (auto &action : _action) {
|
|
if ((action.strStart <= _state->_strMouseLoc && _state->_strMouseLoc <= action.strEnd) ||
|
|
(_state->_strMouseLoc == action.strEnd + 1 && underMouse == '\r' && _str[action.strEnd] != '\r')) {
|
|
return &action;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: maybe not in original, but if we are closing and
|
|
// there is only one action, always do that action.
|
|
if (isClosing && _action.size() == 1)
|
|
return &_action[0];
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Common::String Dialog::dump(const Common::String &indent) const {
|
|
Common::String str = Common::String::format(
|
|
"%sDialog<num %d %s bgcol %d fcol %d selbgcol %d selfontcol %d fntsz %d flags 0x%02x frame %d delay %d next %d:%d talkdata %d:%d",
|
|
indent.c_str(), _num, _rect.dump("").c_str(), _bgColor, _fontColor, _selectionBgCol, _selectonFontCol, _fontSize,
|
|
_flags, _frameType, _time, _nextDialogFileNum, _nextDialogDlgNum, _talkDataNum, _talkDataHeadNum);
|
|
str += indent + " state=" + (_state ? _state->dump("") : "null");
|
|
if (_action.size()) {
|
|
str += "\n";
|
|
str += DebugUtil::dumpStructList(indent, "actions", _action);
|
|
}
|
|
str += "\n";
|
|
str += indent + " str='" + _str + "'>";
|
|
return str;
|
|
}
|
|
|
|
Common::Error Dialog::syncState(Common::Serializer &s) {
|
|
s.syncAsUint32LE(_flags);
|
|
bool hasState = _state.get() != nullptr;
|
|
s.syncAsByte(hasState);
|
|
if (hasState) {
|
|
if (!_state)
|
|
_state.reset(new DialogState());
|
|
_state->syncState(s);
|
|
} else {
|
|
_state.reset();
|
|
}
|
|
return Common::kNoError;
|
|
}
|
|
|
|
Common::String DialogState::dump(const Common::String &indent) const {
|
|
return Common::String::format("%sDialogState<hide %d loc %s lastmouse %d %d charsz %d %d mousestr %d selaction %p>",
|
|
indent.c_str(), _hideTime, _loc.dump("").c_str(), _lastMouseX, _lastMouseY, _charWidth,
|
|
_charHeight, _strMouseLoc, (void *)_selectedAction);
|
|
}
|
|
|
|
Common::Error DialogState::syncState(Common::Serializer &s) {
|
|
s.syncAsUint32LE(_hideTime);
|
|
s.syncAsSint16LE(_lastMouseX);
|
|
s.syncAsSint16LE(_lastMouseY);
|
|
s.syncAsUint16LE(_charWidth);
|
|
s.syncAsUint16LE(_charHeight);
|
|
s.syncAsUint32LE(_strMouseLoc);
|
|
|
|
s.syncAsUint16LE(_loc.x);
|
|
s.syncAsUint16LE(_loc.y);
|
|
s.syncAsUint16LE(_loc.width);
|
|
s.syncAsUint16LE(_loc.height);
|
|
|
|
return Common::kNoError;
|
|
}
|
|
|
|
|
|
Common::String DialogAction::dump(const Common::String &indent) const {
|
|
Common::String str = Common::String::format("%sDialogueAction<span: %d-%d", indent.c_str(), strStart, strEnd);
|
|
str += DebugUtil::dumpStructList(indent, "opList", sceneOpList);
|
|
if (!sceneOpList.empty()) {
|
|
str += "\n";
|
|
str += indent;
|
|
}
|
|
str += ">";
|
|
return str;
|
|
}
|
|
|
|
} // End of namespace Dgds
|