scummvm/engines/twp/dialog.cpp

567 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/config-manager.h"
#include "twp/twp.h"
#include "twp/detection.h"
#include "twp/dialog.h"
#include "twp/motor.h"
#include "twp/resmanager.h"
#include "twp/squtil.h"
#include "twp/tsv.h"
namespace Twp {
class SerialMotors : public Motor {
public:
SerialMotors(const std::initializer_list<Common::SharedPtr<Motor> > &motors) : _motors(motors) {}
explicit SerialMotors(const Common::Array<Common::SharedPtr<Motor> > &motors) : _motors(motors) {}
void onUpdate(float elapsed) override {
if (!_motors.empty()) {
_motors[0]->update(elapsed);
if (!_motors[0]->isEnabled()) {
debugC(kDebugDialog, "SerialMotors next");
_motors.remove_at(0);
}
} else {
debugC(kDebugDialog, "SerialMotors is over");
disable();
}
}
private:
Common::Array<Common::SharedPtr<Motor> > _motors;
};
class SelectLabelMotor : public Motor {
public:
SelectLabelMotor(Dialog *dlg, int line, const Common::String &name)
: _dlg(dlg), _line(line), _name(name) {
}
void onUpdate(float elapsed) override {
_dlg->selectLabel(_line, _name);
disable();
}
private:
Dialog *_dlg;
int _line;
Common::String _name;
};
CondStateVisitor::CondStateVisitor(Dialog *dlg, DialogSelMode mode) : _dlg(dlg), _mode(mode) {
}
DialogConditionState CondStateVisitor::createState(int line, DialogConditionMode mode) {
return DialogConditionState(mode, _dlg->_context.actor, _dlg->_context.dialogName, line);
}
DialogConditionState::DialogConditionState() = default;
DialogConditionState::DialogConditionState(DialogConditionMode m, const Common::String &k, const Common::String &dlg, int ln)
: mode(m), actorKey(k), dialog(dlg), line(ln) {
}
void CondStateVisitor::visit(const YOnce &node) {
if (_mode == DialogSelMode::Choose)
_dlg->_states.push_back(createState(node._line, DialogConditionMode::Once));
}
void CondStateVisitor::visit(const YShowOnce &node) {
if (_mode == DialogSelMode::Show)
_dlg->_states.push_back(createState(node._line, DialogConditionMode::ShowOnce));
}
void CondStateVisitor::visit(const YOnceEver &node) {
if (_mode == DialogSelMode::Choose)
_dlg->_states.push_back(createState(node._line, DialogConditionMode::OnceEver));
}
void CondStateVisitor::visit(const YTempOnce &node) {
if (_mode == DialogSelMode::Show)
_dlg->_states.push_back(createState(node._line, DialogConditionMode::TempOnce));
}
ExpVisitor::ExpVisitor(Dialog *dialog) : _dialog(dialog) {}
ExpVisitor::~ExpVisitor() = default;
void ExpVisitor::visit(const YCodeExp &node) {
debugC(kDebugDialog, "execute code %s", node._code.c_str());
sqexec(g_twp->getVm(), node._code.c_str(), "dialog");
}
void ExpVisitor::visit(const YGoto &node) {
debugC(kDebugDialog, "execute goto %s", node._name.c_str());
_dialog->selectLabel(node._line, node._name);
}
void ExpVisitor::visit(const YShutup &node) {
debugC(kDebugDialog, "shutup");
_dialog->_tgt->shutup();
}
void ExpVisitor::visit(const YPause &node) {
debugC(kDebugDialog, "pause %d", node._time);
_dialog->_action = _dialog->_tgt->pause(node._time);
}
void ExpVisitor::visit(const YWaitFor &node) {
debugC(kDebugDialog, "TODO: waitFor {%s}", node._actor.c_str());
}
void ExpVisitor::visit(const YParrot &node) {
_dialog->_context.parrot = node._active;
}
void ExpVisitor::visit(const YDialog &node) {
_dialog->_context.actor = node._actor;
}
void ExpVisitor::visit(const YOverride &node) {
warning("TODO: override %s", node._node.c_str());
}
void ExpVisitor::visit(const YAllowObjects &node) {
warning("TODO: allowObjects");
}
void ExpVisitor::visit(const YWaitWhile &node) {
debugC(kDebugDialog, "wait while");
_dialog->_action = _dialog->_tgt->waitWhile(node._cond);
}
void ExpVisitor::visit(const YLimit &node) {
debugC(kDebugDialog, "limit");
_dialog->_context.limit = node._max;
}
void ExpVisitor::visit(const YSay &node) {
_dialog->_action = _dialog->_tgt->say(node._actor, node._text);
}
CondVisitor::CondVisitor(Dialog *dialog) : _dialog(dialog) {}
CondVisitor::~CondVisitor() = default;
void CondVisitor::visit(const YCodeCond &node) {
_accepted = _dialog->isCond(node._code);
}
void CondVisitor::visit(const YOnce &node) {
_accepted = _dialog->isOnce(node._line);
}
void CondVisitor::visit(const YShowOnce &node) {
_accepted = _dialog->isShowOnce(node._line);
}
void CondVisitor::visit(const YOnceEver &node) {
_accepted = _dialog->isOnceEver(node._line);
}
void CondVisitor::visit(const YTempOnce &node) {
_accepted = _dialog->isTempOnce(node._line);
}
DialogSlot::DialogSlot() : Node("DialogSlot") {}
Dialog::Dialog() : Node("Dialog") {}
Dialog::~Dialog() = default;
static YChoice *getChoice(DialogSlot *slot) {
return (YChoice *)(slot->_stmt->_exp.get());
}
void Dialog::choose(int choice) {
if (_state == WaitingForChoice) {
choose(&_slots[choice]);
}
}
void Dialog::choose(DialogSlot *slot) {
if (slot && slot->_isValid) {
sqcall("onChoiceClick");
for (size_t i = 0; i < slot->_stmt->_conds.size(); i++) {
Common::SharedPtr<YCond> cond = slot->_stmt->_conds[i];
CondStateVisitor v(slot->_dlg, DialogSelMode::Choose);
cond->accept(v);
}
YChoice *choice = getChoice(slot);
if (slot->_dlg->_context.parrot) {
slot->_dlg->_state = DialogState::Active;
slot->_dlg->_tgt->say(slot->_dlg->_context.actor, choice->_text);
slot->_dlg->_action = Common::SharedPtr<SelectLabelMotor>(new SelectLabelMotor(slot->_dlg, choice->_goto->_line, choice->_goto->_name));
} else {
slot->_dlg->selectLabel(choice->_goto->_line, choice->_goto->_name);
}
}
}
void Dialog::start(const Common::String &actor, const Common::String &name, const Common::String &node) {
_context.actor = actor;
_context.dialogName = name;
_context.parrot = true;
_context.limit = MAXCHOICES;
// keepIf(self.states, proc(x: DialogConditionState): bool = x.mode != TempOnce);
Common::String path = name + ".byack";
debugC(kDebugDialog, "start dialog %s", path.c_str());
GGPackEntryReader reader;
reader.open(*g_twp->_pack, path);
YackParser parser;
_cu.reset(parser.parse(&reader));
selectLabel(0, node);
update(0);
}
void Dialog::update(float dt) {
_fadeTime += dt;
switch (_state) {
case DialogState::None:
break;
case DialogState::Active:
running(dt);
break;
case DialogState::WaitingForChoice: {
Color color = _tgt->actorColor(_context.actor);
Color colorHover = _tgt->actorColorHover(_context.actor);
for (size_t i = 0; i < MAXDIALOGSLOTS; i++) {
DialogSlot *slot = &_slots[i];
if (slot->_isValid) {
Rectf rect = Rectf::fromPosAndSize(slot->getPos() - Math::Vector2d(0.f, -slot->getSize().getY() / 2.f), Math::Vector2d(SCREEN_WIDTH - SLOTMARGIN, slot->getSize().getY()));
bool over = rect.contains(_mousePos);
// shake choice when cursor is over
if ((slot->_shakeTime > 0.0f) && slot->_shake) {
slot->_shake->update(dt);
slot->_shakeTime -= dt;
if (slot->_shakeTime < 0.f) {
slot->_shakeTime = 0.f;
}
}
if (over && !slot->_over && slot->_shakeTime < 0.1f) {
slot->_shakeTime = 0.25f;
slot->_shake = Common::ScopedPtr<Motor>(new Shake(slot, 0.6f));
slot->_over = over;
}
if (!over) {
slot->_over = false;
}
// slide choice text wen text is too large
const float width = slot->getSize().getX();
if (width > (SCREEN_WIDTH - SLOTMARGIN)) {
if (over) {
if ((width + slot->getPos().getX()) > (SCREEN_WIDTH - SLOTMARGIN)) {
slot->setPos(Math::Vector2d(slot->getPos().getX() - SLIDINGSPEED * dt, slot->getPos().getY()));
if ((width + slot->getPos().getX()) < (SCREEN_WIDTH - SLOTMARGIN)) {
slot->setPos(Math::Vector2d((SCREEN_WIDTH - SLOTMARGIN) - width, slot->getPos().getY()));
}
}
} else if (slot->getPos().getX() < SLOTMARGIN) {
slot->setPos(Math::Vector2d(slot->getPos().getX() + SLIDINGSPEED * dt, slot->getPos().getY()));
if (slot->getPos().getX() > SLOTMARGIN) {
slot->setPos(Math::Vector2d(SLOTMARGIN, slot->getPos().getY()));
}
}
}
slot->_text.setColor(over ? colorHover : color);
if (over && g_twp->_cursor.isLeftDown())
choose(i);
}
}
} break;
}
}
bool Dialog::isOnce(int line) const {
for (const auto &state : _states) {
if (state.mode == Once && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
debugC(kDebugDialog, "isOnce %d: false", line);
return false;
}
}
debugC(kDebugDialog, "isOnce %d: true", line);
return true;
}
bool Dialog::isShowOnce(int line) const {
for (const auto &state : _states) {
if (state.mode == ShowOnce && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
debugC(kDebugDialog, "isShowOnce %d: false", line);
return false;
}
}
debugC(kDebugDialog, "isShowOnce %d: true", line);
return true;
}
bool Dialog::isOnceEver(int line) const {
for (const auto &state : _states) {
if (state.mode == OnceEver && state.dialog == _context.dialogName && state.line == line) {
debugC(kDebugDialog, "isOnceEver %d: false", line);
return false;
}
}
debugC(kDebugDialog, "isOnceEver %d: true", line);
return true;
}
bool Dialog::isTempOnce(int line) const {
for (const auto &state : _states) {
if (state.mode == TempOnce && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
debugC(kDebugDialog, "isTempOnce %d: false", line);
return false;
}
}
debugC(kDebugDialog, "isTempOnce %d: true", line);
return true;
}
bool Dialog::isCond(const Common::String &cond) const {
bool result = _tgt->execCond(cond);
debugC(kDebugDialog, "isCond '%s': %s", cond.c_str(), result ? "TRUE" : "FALSE");
return result;
}
Common::SharedPtr<YLabel> Dialog::label(int line, const Common::String &name) const {
for (auto label : _cu->_labels) {
if ((label->_name == name) && (label->_line >= line)) {
return label;
}
}
line = 0;
for (auto label : _cu->_labels) {
if ((label->_name == name) && (label->_line >= line)) {
return label;
}
}
return nullptr;
}
void Dialog::selectLabel(int line, const Common::String &name) {
debugC(kDebugDialog, "select label %s", name.c_str());
_lbl = label(line, name);
_currentStatement = 0;
clearSlots();
_state = _lbl ? Active : None;
}
void Dialog::gotoNextLabel() {
if (_lbl) {
size_t i = Twp::find(_cu->_labels, _lbl);
if ((i != (size_t)-1) && (i != _cu->_labels.size() - 1)) {
Common::SharedPtr<YLabel> label = _cu->_labels[i + 1];
selectLabel(label->_line, label->_name);
} else {
_state = None;
}
}
}
void Dialog::updateChoiceStates() {
_state = WaitingForChoice;
_fadeTime = 0.f;
for (auto &_slot : _slots) {
DialogSlot *slot = &_slot;
if (slot->_isValid) {
for (auto cond : slot->_stmt->_conds) {
CondStateVisitor v(this, DialogSelMode::Show);
cond->accept(v);
}
}
}
}
void Dialog::run(Common::SharedPtr<YStatement> stmt) {
if (acceptConditions(stmt)) {
ExpVisitor visitor(this);
stmt->_exp->accept(visitor);
IsGoto isGoto;
stmt->_exp->accept(isGoto);
if (isGoto._isGoto)
return;
}
_currentStatement++;
}
bool Dialog::acceptConditions(Common::SharedPtr<YStatement> stmt) {
CondVisitor vis(this);
for (auto cond : stmt->_conds) {
cond->accept(vis);
if (!vis._accepted) {
return false;
}
}
return true;
}
void Dialog::running(float dt) {
if (_action && _action->isEnabled())
_action->update(dt);
else if (!_lbl)
_state = None;
else if (_currentStatement == _lbl->_stmts.size())
gotoNextLabel();
else {
_state = Active;
while (_lbl && (_currentStatement < _lbl->_stmts.size()) && (_state == Active)) {
Common::SharedPtr<YStatement> statmt = _lbl->_stmts[_currentStatement];
IsChoice isChoice;
statmt->_exp->accept(isChoice);
if (!acceptConditions(statmt))
_currentStatement++;
else if (isChoice._isChoice) {
addSlot(statmt);
_currentStatement++;
} else if (choicesReady())
updateChoiceStates();
else if (_action && _action->isEnabled()) {
_action->update(dt);
return;
} else {
IsShutup isShutup;
statmt->_exp->accept(isShutup);
if (g_twp->isSomeoneTalking() && !isShutup._isShutup) {
return;
}
run(statmt);
if (_lbl && (_currentStatement == _lbl->_stmts.size()))
gotoNextLabel();
}
}
if (choicesReady())
updateChoiceStates();
else if (!_action || !_action->isEnabled())
_state = None;
}
}
static Common::String text(const Common::String &txt) {
Common::String result(g_twp->getTextDb().getText(txt));
result = remove(result, '(', ')');
result = remove(result, '{', '}');
return result;
}
void Dialog::addSlot(Common::SharedPtr<YStatement> stmt) {
YChoice *choice = (YChoice *)stmt->_exp.get();
if ((!_slots[choice->_number - 1]._isValid) && (numSlots() < _context.limit)) {
DialogSlot *slot = &_slots[choice->_number - 1];
slot->_text.setFont("sayline");
slot->_text.setText(Common::String::format("● %s", text(choice->_text).c_str()));
slot->_stmt = stmt;
slot->_dlg = this;
Math::Vector2d slotSize(slot->_text.getBounds());
float slotHeight = slotSize.getY() - 3.f;
slot->setSize({slotSize.getX(), slotHeight});
const float y = slotHeight * (MAXCHOICES - numSlots() - 2);
slot->setPos(Math::Vector2d(SLOTMARGIN, y));
slot->_isValid = true;
}
}
int Dialog::numSlots() const {
int num = 0;
for (const auto &_slot : _slots) {
if (_slot._isValid)
num++;
}
return num;
}
void Dialog::clearSlots() {
for (auto &_slot : _slots) {
_slot._isValid = false;
}
}
void Dialog::drawCore(const Math::Matrix4 &trsf) {
if (_state == WaitingForChoice) {
// draw HUD background
SpriteSheet *gameSheet = g_twp->_resManager->spriteSheet("GameSheet");
const SpriteSheetFrame &backingFrame = gameSheet->getFrame("ui_backing_tall");
Texture *gameTexture = g_twp->_resManager->texture(gameSheet->meta.image);
float alpha = 0.33f; // prefs(UiBackingAlpha);
g_twp->getGfx().drawSprite(backingFrame.frame, *gameTexture, Color(0, 0, 0, alpha * getAlpha()), trsf);
}
const float slotDelay = 0.1f;
float fadeTime = MIN(_fadeTime, 1.0f + slotDelay * MAXCHOICES);
int slotNum = 0;
for (auto &_slot : _slots) {
DialogSlot *slot = &_slot;
if (slot->_isValid) {
Color c = slot->_text.getColor();
Math::Matrix4 t(slot->getTrsf(trsf));
float alpha = CLIP(6.f * (fadeTime - slotNum * slotDelay), 0.f, 1.f);
t.translate(Math::Vector3d(0.f, (6.f * alpha), 0.f));
slot->_text.setColor(Color::withAlpha(c, alpha));
slot->_text.draw(g_twp->getGfx(), t);
slotNum++;
}
}
}
int Dialog::getActiveSlot(const Math::Vector2d &pos) const {
int index = -1;
int num = 0;
for (int i = 0; i < MAXDIALOGSLOTS; i++) {
const DialogSlot *slot = &_slots[i];
if (!slot->_isValid)
continue;
const Math::Vector2d p = slot->getPos();
const Math::Vector2d s = slot->getSize();
const Rectf r(p.getX(), p.getY() + s.getY() / 2.f, s.getX(), s.getY());
if (r.contains(pos)) {
index = num;
}
num++;
}
return index;
}
Math::Vector2d Dialog::getChoicePos(int index) const {
int n = 0;
for (int i = 0; i < MAXDIALOGSLOTS; i++) {
const DialogSlot *slot = &_slots[i];
if (!slot->_isValid)
continue;
if (n == index) {
Math::Vector2d p(slot->getPos());
Math::Vector2d s(slot->getSize());
return Math::Vector2d(p.getX() + s.getX() / 2.f, p.getY() + s.getY() + 8.f);
}
n++;
}
return Math::Vector2d();
}
Math::Vector2d Dialog::getNextChoicePos(const Math::Vector2d &pos) {
int index = getActiveSlot(pos);
index = MIN(index + 1, numSlots() - 1);
return getChoicePos(index);
}
Math::Vector2d Dialog::getPreviousChoicePos(const Math::Vector2d &pos) {
int index = getActiveSlot(pos);
index = MAX(index - 1, 0);
return getChoicePos(index);
}
} // namespace Twp