mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
567 lines
16 KiB
C++
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
|