/* 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 . * */ #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 > &motors) : _motors(motors) {} explicit SerialMotors(const Common::Array > &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 > _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 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(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(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 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 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 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 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 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 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