/* 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 . * */ /* * This code is based on the CRAB engine * * Copyright (c) Arvind Raja Yadav * * Licensed under MIT * */ #include "crab/crab.h" #include "crab/XMLDoc.h" #include "crab/input/cursor.h" #include "crab/ui/map.h" namespace Crab { using namespace pyrodactyl::ui; using namespace pyrodactyl::image; using namespace pyrodactyl::input; //------------------------------------------------------------------------ // Purpose: Load stuff that can't be modified by the user //------------------------------------------------------------------------ void Map::load(const Common::Path &filename, pyrodactyl::event::Info &info) { XMLDoc conf(filename); if (conf.ready()) { rapidxml::xml_node *node = conf.doc()->first_node("map"); if (nodeValid(node)) { if (nodeValid("img", node)) { rapidxml::xml_node *imgnode = node->first_node("img"); loadNum(_speed, "speed", imgnode); for (auto n = imgnode->first_node("map"); n != nullptr; n = n->next_sibling("map")) _map.push_back(n); } if (nodeValid("fg", node)) _fg.load(node->first_node("fg")); if (nodeValid("dim", node)) { loadNum(_camera.w, "x", node->first_node("dim")); loadNum(_camera.h, "y", node->first_node("dim")); } if (nodeValid("pos", node)) _pos.load(node->first_node("pos")); if (nodeValid("scroll", node)) _scroll.load(node->first_node("scroll")); if (nodeValid("marker", node)) _marker.load(node->first_node("marker")); if (nodeValid("title", node)) _title.load(node->first_node("title")); if (nodeValid("locations", node)) _travel.load(node->first_node("locations")); if (nodeValid("overlay", node)) _buOverlay.load(node->first_node("overlay")); } } setImage(_cur, true); update(info); calcBounds(); } //------------------------------------------------------------------------ // Purpose: Draw //------------------------------------------------------------------------ void Map::draw(pyrodactyl::event::Info &info) { // The map graphic is clipped to fit inside the UI _imgBg.draw(_pos.x, _pos.y, &_camera); if (_overlay) { // The overlay needs to be clipped as well, so we must find the intersection of the camera and the clip itself for (auto &i : _map[_cur]._reveal) { Rect r = i; int X = _pos.x + i.x - _camera.x, Y = _pos.y + i.y - _camera.y; // Do not draw any area of the clip that is outside the camera bounds // If we're outside the left edges, we need to cull the left point if (X < _pos.x) { X += _camera.x - i.x; r.x += _camera.x - i.x; r.w -= _camera.x - i.x; if (r.w < 0) r.w = 0; } if (Y < _pos.y) { Y += _camera.y - i.y; r.y += _camera.y - i.y; r.h -= _camera.y - i.y; if (r.h < 0) r.h = 0; } // If we're outside the right edge, we need to cull the width and height if (X + r.w > _pos.x + _camera.w) r.w = ABS(_pos.x + _camera.w - X); // abs to fix crash incase _pos.x + _camera.w < X if (Y + r.h > _pos.y + _camera.h) r.h = ABS(_pos.y + _camera.h - Y); // abs to fix crash incase _pos.y + _camera.h < Y _imgOverlay.draw(X, Y, &r); } } _travel.draw(_camera.x - _pos.x, _camera.y - _pos.y); _fg.draw(); _buOverlay.draw(); _title._text = info.curLocName(); _title.draw(); _marker.draw(_pos, _playerPos, _camera); _scroll.draw(); } //------------------------------------------------------------------------ // Purpose: Center the world map on a spot //------------------------------------------------------------------------ void Map::center(const Vector2i &vec) { _camera.x = vec.x - _camera.w / 2; _camera.y = vec.y - _camera.h / 2; validate(); } //------------------------------------------------------------------------ // Purpose: Keep the camera in bounds and decide marker visibility //------------------------------------------------------------------------ void Map::validate() { // Make all scroll buttons visible first for (auto &i : _scroll._element) i._visible = true; // Keep camera in bounds if (_camera.x + _camera.w > _size.x) _camera.x = _size.x - _camera.w; if (_camera.y + _camera.h > _size.y) _camera.y = _size.y - _camera.h; if (_camera.x < 0) _camera.x = 0; if (_camera.y < 0) _camera.y = 0; // decide visibility of scroll buttons _scroll._element[DIRECTION_RIGHT]._visible = !(_camera.x == _size.x - _camera.w); _scroll._element[DIRECTION_DOWN]._visible = !(_camera.y == _size.y - _camera.h); _scroll._element[DIRECTION_LEFT]._visible = !(_camera.x == 0); _scroll._element[DIRECTION_UP]._visible = !(_camera.y == 0); } //------------------------------------------------------------------------ // Purpose: Move //------------------------------------------------------------------------ void Map::move(const Common::Event &event) { // Reset the velocity to avoid weirdness _vel.x = 0; _vel.y = 0; // We don't use the result, but this keeps the button states up to date _scroll.handleEvents(event); switch (event.type) { case Common::EVENT_LBUTTONDOWN: case Common::EVENT_RBUTTONDOWN: { bool click = false; int count = 0; for (auto &i : _scroll._element) { if (i.contains(g_engine->_mouse->_button)) { if (count == DIRECTION_UP) _vel.y = -1 * _speed; else if (count == DIRECTION_DOWN) _vel.y = _speed; else if (count == DIRECTION_RIGHT) _vel.x = _speed; else if (count == DIRECTION_LEFT) _vel.x = -1 * _speed; click = true; } count++; } if (!click) { _pan = true; _vel.x = 0; _vel.y = 0; } else _pan = false; } break; case Common::EVENT_LBUTTONUP: case Common::EVENT_RBUTTONUP: _pan = false; break; case Common::EVENT_MOUSEMOVE: if (_pan) { _camera.x -= g_engine->_mouse->_rel.x; _camera.y -= g_engine->_mouse->_rel.y; validate(); } break; default: // Move the map camera if player presses the direction keys if (g_engine->_inputManager->state(IU_UP)) _vel.y = -1 * _speed; else if (g_engine->_inputManager->state(IU_DOWN)) _vel.y = _speed; else if (g_engine->_inputManager->state(IU_RIGHT)) _vel.x = _speed; else if (g_engine->_inputManager->state(IU_LEFT)) _vel.x = -1 * _speed; // Stop moving when we release a key (but only in that direction) else if (!g_engine->_inputManager->state(IU_UP) && !g_engine->_inputManager->state(IU_DOWN)) _vel.y = 0; else if (!g_engine->_inputManager->state(IU_LEFT) && !g_engine->_inputManager->state(IU_RIGHT)) _vel.x = 0; break; } } //------------------------------------------------------------------------ // Purpose: Internal Events //------------------------------------------------------------------------ void Map::internalEvents(pyrodactyl::event::Info &info) { // The map overlay and button state should be in sync _buOverlay._state = _overlay; _camera.x += _vel.x; _camera.y += _vel.y; validate(); for (auto &i : _travel._element) i._visible = i.x >= _camera.x && i.y >= _camera.y; _marker.internalEvents(_pos, _playerPos, _camera, _bounds); } //------------------------------------------------------------------------ // Purpose: Handle Events //------------------------------------------------------------------------ bool Map::handleEvents(pyrodactyl::event::Info &info, const Common::Event &event) { int choice = _travel.handleEvents(event, -1 * _camera.x, -1 * _camera.y); if (choice >= 0) { _curLoc = _travel._element[choice]._loc; _pan = false; return true; } _marker.handleEvents(_pos, _playerPos, _camera, event); move(event); if (_buOverlay.handleEvents(event) == BUAC_LCLICK) _overlay = _buOverlay._state; return false; } void Map::setImage(const uint &val, const bool &force) { if (force || (_cur != val && val < _map.size())) { _cur = val; _imgBg.deleteImage(); _imgOverlay.deleteImage(); _imgBg.load(_map[_cur]._pathBg); _imgOverlay.load(_map[_cur]._pathOverlay); _size.x = _imgBg.w(); _size.y = _imgBg.h(); _marker.clear(); for (auto &i : _map[_cur]._dest) _marker.addButton(i._name, i._pos.x, i._pos.y); _marker.assignPaths(); } } //------------------------------------------------------------------------ // Purpose: Select the marker corresponding to a quest title //------------------------------------------------------------------------ void Map::selectDest(const Common::String &name) { _marker.selectDest(name); } //------------------------------------------------------------------------ // Purpose: Update the status of the fast travel buttons //------------------------------------------------------------------------ void Map::update(pyrodactyl::event::Info &info) { for (auto &i : _travel._element) { i._unlock.evaluate(info); i._visible = i._unlock.result(); } } //------------------------------------------------------------------------ // Purpose: Add a rectangle to the revealed world map data //------------------------------------------------------------------------ void Map::revealAdd(const int &id, const Rect &area) { if ((uint)id < _map.size()) { for (auto &i : _map[id]._reveal) if (i == area) return; _map[id]._reveal.push_back(area); } } //------------------------------------------------------------------------ // Purpose: Add or remove a destination marker from the world map //------------------------------------------------------------------------ void Map::destAdd(const Common::String &name, const int &x, const int &y) { if (_cur < _map.size()) { for (auto &i : _map[_cur]._dest) { if (i._name == name) { i._pos.x = x; i._pos.y = y; return; } } _map[_cur].destAdd(name, x, y); _marker.addButton(name, x, y); _marker.assignPaths(); } } void Map::destDel(const Common::String &name) { if (_cur < _map.size()) { for (auto i = _map[_cur]._dest.begin(); i != _map[_cur]._dest.end(); ++i) { if (i->_name == name) { _map[_cur]._dest.erase(i); break; } } _marker.erase(name); } } //------------------------------------------------------------------------ // Purpose: Save and load object state //------------------------------------------------------------------------ void Map::saveState(rapidxml::xml_document<> &doc, rapidxml::xml_node *root) { rapidxml::xml_node *child = doc.allocate_node(rapidxml::node_element, "map"); child->append_attribute(doc.allocate_attribute("cur", g_engine->_stringPool->get(_cur))); saveBool(_overlay, "overlay", doc, child); for (auto &r : _map) { rapidxml::xml_node *child_data = doc.allocate_node(rapidxml::node_element, "data"); r.saveState(doc, child_data); child->append_node(child_data); } root->append_node(child); } void Map::loadState(rapidxml::xml_node *node) { if (nodeValid("map", node)) { rapidxml::xml_node *mapnode = node->first_node("map"); loadBool(_overlay, "overlay", mapnode); int val = _cur; loadNum(val, "cur", mapnode); auto r = _map.begin(); for (rapidxml::xml_node *n = mapnode->first_node("data"); n != nullptr && r != _map.end(); n = n->next_sibling("data"), ++r) r->loadState(n); setImage(val, true); } } //------------------------------------------------------------------------ // Purpose: Reset the UI positions in response to change in resolution //------------------------------------------------------------------------ void Map::setUI() { _pos.setUI(); _fg.setUI(); _travel.setUI(); _marker.setUI(); _buOverlay.setUI(); _scroll.setUI(); _title.setUI(); calcBounds(); } } // End of namespace Crab