477 lines
15 KiB
C++
477 lines
15 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/debug.h"
|
||
#include "common/memstream.h"
|
||
|
||
#include "qdengine/qdengine.h"
|
||
#include "qdengine/minigames/adv/common.h"
|
||
#include "qdengine/minigames/adv/m_puzzle.h"
|
||
#include "qdengine/minigames/adv/RunTime.h"
|
||
#include "qdengine/minigames/adv/Rect.h"
|
||
#include "qdengine/minigames/adv/qdMath.h"
|
||
#include "qdengine/system/input/keyboard_input.h"
|
||
|
||
namespace QDEngine {
|
||
|
||
typedef Rect<float, mgVect2f> Rectf;
|
||
|
||
MinigameInterface *createMinigamePuzzle(MinigameManager *runtime) {
|
||
return new Puzzle(runtime);
|
||
}
|
||
|
||
enum {
|
||
EVENT_GET,
|
||
EVENT_PUT,
|
||
EVENT_SWAP,
|
||
EVENT_ROTATE_IN_FIELD,
|
||
EVENT_RETURN,
|
||
EVENT_PUT_RIGHT,
|
||
EVENT_CLICK_RIGHT,
|
||
EVENT_CLICK,
|
||
EVENT_ROTATE_IN_STACK,
|
||
EVENT_FIELD_ROTATE
|
||
};
|
||
|
||
const char *Puzzle::getStateName(int angle, bool selected, bool small) const {
|
||
static const char *small_pref = "inv_";
|
||
static const char *selected_suf = "_sel";
|
||
|
||
static char buf[32];
|
||
buf[31] = 0;
|
||
|
||
assert(angle >= 0 && angle < _angles);
|
||
angle = (angle + _globalAngle) % _angles;
|
||
|
||
snprintf(buf, 31, "%s%02d%s", !_singleSize && small ? small_pref : "", angle + 1, selected ? selected_suf : "");
|
||
return buf;
|
||
}
|
||
|
||
Puzzle::Puzzle(MinigameManager *runtime) {
|
||
_runtime = runtime;
|
||
|
||
if (!_runtime->getParameter("game_size", _gameSize, true))
|
||
return;
|
||
assert(_gameSize > 0 && _gameSize < 100);
|
||
|
||
_field.resize(_gameSize, -1);
|
||
_globalAngle = 0;
|
||
|
||
_singleSize = _runtime->getParameter("small_objects", false);
|
||
|
||
_angles = _runtime->getParameter("angles", 4);
|
||
assert(_angles > 0 && _angles < 10);
|
||
|
||
if (!(_stackBottom = _runtime->getObject(_runtime->parameter("inventory_bottom"))))
|
||
return;
|
||
if (!_runtime->getParameter("inventory_size", _stackSize, true))
|
||
return;
|
||
|
||
if (_runtime->getParameter("rotate_period", _rotateTimePeriod, false)) {
|
||
assert(sqr(sqrt((float)_gameSize)) == _gameSize);
|
||
if (sqr(sqrt((float)_gameSize)) != _gameSize)
|
||
return;
|
||
} else
|
||
_rotateTimePeriod = 86400; // сутки
|
||
_nextRotateTime = _runtime->getTime() + _rotateTimePeriod;
|
||
|
||
_flySpeed = _runtime->getParameter("inventory_drop_speed", 240.f);
|
||
assert(_flySpeed > 0.f);
|
||
_returnSpeed = _runtime->getParameter("inventory_return_speed", -1.f);
|
||
|
||
const char *name_begin = _runtime->parameter("obj_name_begin", "obj_");
|
||
|
||
char buf[128];
|
||
buf[127] = 0;
|
||
|
||
Common::MemoryReadWriteStream gameData(DisposeAfterUse::YES);
|
||
for (int idx = 0; idx < _gameSize; ++idx) {
|
||
snprintf(buf, 127, "%s%02d", name_begin, idx + 1);
|
||
|
||
Node node;
|
||
node.obj = _runtime->getObject(buf);
|
||
|
||
if (_runtime->debugMode()) {
|
||
node.pos = _nodes.size();
|
||
node.angle = 0;
|
||
_field[node.pos] = node.pos;
|
||
} else
|
||
node.angle = _runtime->rnd(0, _angles - 1);
|
||
node.obj.setState(getStateName(node.angle, false, true));
|
||
|
||
node.obj->R().write(gameData);
|
||
|
||
_nodes.push_back(node);
|
||
}
|
||
|
||
if (!_runtime->processGameData(gameData))
|
||
return;
|
||
|
||
GameInfo *gameInfo = _runtime->getCurrentGameInfo();
|
||
if (gameInfo) {
|
||
Common::MemoryReadStream data((byte *)gameInfo->_gameData, gameInfo->_dataSize);
|
||
for (int idx = 0; idx < _gameSize; ++idx) {
|
||
mgVect3f crd;
|
||
crd.read(data);
|
||
_nodes[idx].obj->set_R(crd);
|
||
_positions.push_back(crd);
|
||
}
|
||
} else {
|
||
for (int idx = 0; idx < _gameSize; ++idx) {
|
||
mgVect3f crd;
|
||
crd.read(gameData);
|
||
_nodes[idx].obj->set_R(crd);
|
||
_positions.push_back(crd);
|
||
}
|
||
}
|
||
|
||
if (_runtime->debugMode())
|
||
_nodes[0].angle = _angles - 1;
|
||
|
||
_size = _runtime->getSize(_nodes[0].obj);
|
||
debugC(2, kDebugMinigames, "size = (%6.2f,%6.2f)", _size.x, _size.y);
|
||
|
||
_depth = _nodes[0].obj.depth(runtime);
|
||
|
||
_stackPlaceSize = _runtime->getParameter("inventory_place_size", _size * 1.2f);
|
||
assert(_stackPlaceSize.x > 0.f && _stackPlaceSize.x < 500.f && _stackPlaceSize.y > 0.f && _stackPlaceSize.y < 500.f);
|
||
debugC(2, kDebugMinigames, "stackPlaceSize = (%5.1f, %5.1f)", _stackPlaceSize.x, _stackPlaceSize.y);
|
||
|
||
_prevPlace = -1;
|
||
_pickedItem = -1;
|
||
_mouseObjPose = stidx(_stackSize + 1);
|
||
|
||
_inField = _runtime->debugMode() ? _nodes.size() : 0;
|
||
_nextObjTime = _runtime->getTime();
|
||
|
||
setState(MinigameInterface::RUNNING);
|
||
}
|
||
|
||
Puzzle::~Puzzle() {
|
||
for (auto &it : _nodes)
|
||
_runtime->release(it.obj);
|
||
|
||
_runtime->release(_stackBottom);
|
||
}
|
||
|
||
void Puzzle::rotate(int item) {
|
||
assert(item >= 0 && item < (int)_nodes.size());
|
||
_nodes[item].angle = (_nodes[item].angle + 1) % _angles;
|
||
}
|
||
|
||
int Puzzle::stidx(int idx) const {
|
||
return -idx - 2;
|
||
}
|
||
|
||
bool Puzzle::testPlace(int item) const {
|
||
assert(item >= 0 && item < (int)_nodes.size());
|
||
return _nodes[item].pos == item && _nodes[item].angle == 0;
|
||
}
|
||
|
||
bool Puzzle::isFlying(int idx) const {
|
||
for (auto &it : _flyObjs)
|
||
if (it.data == idx)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
bool Puzzle::isOnMouse(const Node& node) const {
|
||
if (node.pos == _mouseObjPose) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void Puzzle::put(int where, int what, float flowSpeed) {
|
||
assert(where < (int)_field.size());
|
||
assert(what >= 0 && what < (int)_nodes.size());
|
||
|
||
Node& node = _nodes[what];
|
||
int start = node.pos;
|
||
|
||
if (flowSpeed > 0.f || isFlying(what)) {
|
||
FlyQDObject* flyObj = 0;
|
||
|
||
FlyQDObjects::iterator fit;
|
||
for (fit = _flyObjs.begin(); fit != _flyObjs.end(); fit++) {
|
||
if (fit->data == what)
|
||
break;
|
||
}
|
||
if (fit != _flyObjs.end()) // Этот фрагмент уже летит, просто поменять точку назначения
|
||
flyObj = fit;
|
||
else { // Добавляем новый летящий фрагмент
|
||
_flyObjs.push_back(FlyQDObject());
|
||
flyObj = &_flyObjs.back();
|
||
|
||
flyObj->data = what;
|
||
|
||
mgVect3f from = isOnMouse(node) ? node.obj->R() : start < -1 ? stackPosition(stidx(start)) : position(start);
|
||
flyObj->current = _runtime->world2game(from);
|
||
node.obj->set_R(from);
|
||
|
||
flyObj->speed = flowSpeed;
|
||
}
|
||
|
||
mgVect3f to = where < -1 ? stackPosition(stidx(where)) : position(where);
|
||
flyObj->target = _runtime->world2game(to);
|
||
flyObj->depth = _runtime->getDepth(to);
|
||
}
|
||
|
||
if (where >= 0)
|
||
_field[where] = what;
|
||
|
||
node.pos = where;
|
||
}
|
||
|
||
void Puzzle::putOnStack(int what, float speed) {
|
||
put(stidx((int)_stack.size()), what, speed);
|
||
_stack.push_back(what);
|
||
}
|
||
|
||
void Puzzle::returnToStack() {
|
||
assert(_pickedItem != -1);
|
||
_runtime->event(EVENT_RETURN, _runtime->mousePosition());
|
||
if (_prevPlace >= 0)
|
||
put(_prevPlace, _pickedItem);
|
||
else
|
||
putOnStack(_pickedItem, _returnSpeed);
|
||
_prevPlace = -1;
|
||
_pickedItem = -1;
|
||
_runtime->event(EVENT_CLICK, _runtime->mousePosition());
|
||
}
|
||
|
||
void Puzzle::quant(float dt) {
|
||
if (_pickedItem == -1)
|
||
_runtime->setGameHelpVariant(0);
|
||
else
|
||
_runtime->setGameHelpVariant(1);
|
||
|
||
if (_runtime->getTime() > _nextRotateTime) {
|
||
_runtime->event(EVENT_FIELD_ROTATE, mgVect2f(400, 300));
|
||
_nextRotateTime = _runtime->getTime() + _rotateTimePeriod;
|
||
_globalAngle = (_globalAngle + 1) % _angles;
|
||
_runtime->setCompleteHelpVariant(_globalAngle);
|
||
}
|
||
|
||
FlyQDObjects::iterator fit = _flyObjs.begin();
|
||
while (fit != _flyObjs.end())
|
||
if (!isOnMouse(_nodes[fit->data]) && fit->quant(dt, _nodes[fit->data].obj, _runtime))
|
||
++fit;
|
||
else
|
||
fit = _flyObjs.erase(fit);
|
||
|
||
if (_inField < (int)_nodes.size() && _runtime->getTime() > _nextObjTime &&
|
||
((int)_stack.size() < _stackSize - 1 || ((int)_stack.size() < _stackSize && _pickedItem == -1))) { // нужно добавить в инвентори фишку
|
||
// ищем случайный не выставленный фрагмент
|
||
int freeIdx = round(_runtime->rnd(0.f, _nodes.size() - 1));
|
||
Nodes::iterator it = _nodes.begin();
|
||
for (;;) {
|
||
if (++it == _nodes.end())
|
||
it = _nodes.begin();
|
||
if (it->isFree())
|
||
if (!freeIdx--)
|
||
break;
|
||
}
|
||
int idx = Common::distance(_nodes.begin(), it);
|
||
|
||
++_inField;
|
||
_nextObjTime = _runtime->getTime() + _stackPlaceSize.y / _flySpeed;
|
||
|
||
it->pos = stidx(_stackSize);
|
||
it->obj.setState(getStateName(it->angle, false, true));
|
||
|
||
putOnStack(idx, _flySpeed);
|
||
}
|
||
|
||
mgVect2f mouse = _runtime->mousePosition();
|
||
|
||
int hovPlace = -1; // Номер места которое сейчас под мышкой
|
||
for (int idx = 0; idx < (int)_stack.size(); ++idx)
|
||
if (_nodes[_stack[idx]].obj.hit(mouse)) {
|
||
hovPlace = stidx(idx);
|
||
break;
|
||
}
|
||
if (hovPlace == -1) {
|
||
float radius = 0.5f * _size.x;
|
||
for (int idx = 0; idx < _gameSize; ++idx)
|
||
if (dist(_runtime->world2game(position(idx)), mouse) < radius) {
|
||
hovPlace = idx;
|
||
break;
|
||
}
|
||
}
|
||
if (hovPlace == -1) {
|
||
mgVect2i st = _stackBottom->screen_R();
|
||
st.y -= _stackPlaceSize.y * _stackSize - 0.5f * _stackPlaceSize.x;
|
||
Rectf stackPos(st.x - 0.5f * _stackPlaceSize.x, st.y, _stackPlaceSize.x, _stackPlaceSize.y * _stackSize);
|
||
if (stackPos.point_inside(mouse))
|
||
hovPlace = stidx(_stackSize);
|
||
}
|
||
|
||
if (_runtime->mouseLeftPressed()) {
|
||
if (hovPlace >= 0) { // клик по полю
|
||
Indexes::value_type& hovItem = _field[hovPlace];
|
||
if (hovItem == -1) // клик по пустой ячейке
|
||
if (_pickedItem == -1) // на мыши ничего нет
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
else { // кладем фрагмент с мыши
|
||
put(hovPlace, _pickedItem);
|
||
if (testPlace(_pickedItem)) // положили на свое свое место
|
||
_runtime->event(EVENT_PUT_RIGHT, mouse);
|
||
else // просто положили
|
||
_runtime->event(EVENT_PUT, mouse);
|
||
_pickedItem = -1;
|
||
_prevPlace = -1;
|
||
} else { // клик по непустой ячейке
|
||
if (testPlace(hovPlace)) // клик по правильно уложенной фишке
|
||
_runtime->event(EVENT_CLICK_RIGHT, mouse);
|
||
else if (_pickedItem != -1) { // поменять с тем что на мыше
|
||
bool swap = true;
|
||
if (_prevPlace >= 0)
|
||
put(_prevPlace, hovItem);
|
||
else
|
||
putOnStack(hovItem, _returnSpeed);
|
||
if (testPlace(hovItem)) { // оказалась при обмене на своем месте
|
||
_runtime->event(EVENT_PUT_RIGHT, _runtime->world2game(position(_prevPlace)));
|
||
swap = false;
|
||
}
|
||
put(hovPlace, _pickedItem);
|
||
if (testPlace(_pickedItem)) { // положили на свое свое место
|
||
_runtime->event(EVENT_PUT_RIGHT, mouse);
|
||
swap = false;
|
||
}
|
||
if (swap) // просто обменяли
|
||
_runtime->event(EVENT_SWAP, mouse);
|
||
_pickedItem = -1;
|
||
_prevPlace = -1;
|
||
} else { // взять фрагмент на мышь
|
||
_runtime->event(EVENT_GET, mouse);
|
||
_prevPlace = hovPlace;
|
||
_pickedItem = hovItem;
|
||
_nodes[_pickedItem].pos = _mouseObjPose;
|
||
hovItem = -1;
|
||
}
|
||
}
|
||
} else if (hovPlace < -1) { // клик по стеку
|
||
int hovStack = stidx(hovPlace);
|
||
if (_pickedItem == -1) // на мыши ничего нет
|
||
if (hovStack < (int)_stack.size()) { // взять фрагмент из стека на мышь
|
||
_runtime->event(EVENT_GET, mouse);
|
||
Indexes::iterator it = _stack.begin() + hovStack;
|
||
assert(*it >= 0);
|
||
_prevPlace = -1;
|
||
_pickedItem = *it;
|
||
_nodes[_pickedItem].pos = _mouseObjPose;
|
||
_stack.erase(it);
|
||
for (int idx = hovStack; idx < (int)_stack.size(); ++idx)
|
||
put(stidx(idx), _stack[idx], _flySpeed);
|
||
} else // пустой клик в области стека
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
else // вернуть фишку на место
|
||
returnToStack();
|
||
} else // пустой клик мимо игрового поля
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
} else if (_runtime->mouseRightPressed()) {
|
||
if (_pickedItem == -1) {
|
||
if (hovPlace >= 0) { // клик по полю
|
||
if (testPlace(hovPlace)) // клик по правильно уложенной фишке
|
||
_runtime->event(EVENT_CLICK_RIGHT, mouse);
|
||
else {
|
||
Indexes::value_type& hovItem = _field[hovPlace];
|
||
if (hovItem >= 0) {
|
||
rotate(hovItem);
|
||
if (testPlace(hovItem)) // повернули на правильный угол
|
||
_runtime->event(EVENT_PUT_RIGHT, mouse);
|
||
else // просто положили
|
||
_runtime->event(EVENT_ROTATE_IN_FIELD, mouse);
|
||
} else // попытка прокрутить пустое место
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
}
|
||
} else if (hovPlace < -1) { // клик по стеку
|
||
int hovStack = stidx(hovPlace);
|
||
if (hovStack < (int)_stack.size()) { // покрутить внутри стека
|
||
_runtime->event(EVENT_ROTATE_IN_STACK, mouse);
|
||
rotate(_stack[hovStack]);
|
||
} else // попытка прокрутить пустое место
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
} else // пустой клик мимо игрового поля
|
||
_runtime->event(EVENT_CLICK, mouse);
|
||
} else // вернуть фишку на место
|
||
returnToStack();
|
||
}
|
||
|
||
bool iWin = true;
|
||
for (int idx = 0; idx < (int)_nodes.size(); ++idx) {
|
||
Node& node = _nodes[idx];
|
||
if (node.pos != -1) {
|
||
if (node.pos >= 0) {
|
||
if (isFlying(idx))
|
||
node.obj.setState(getStateName(node.angle, false, false));
|
||
else {
|
||
node.obj.setState(getStateName(node.angle, node.pos == hovPlace && !testPlace(idx), false));
|
||
node.obj->set_R(position(node.pos));
|
||
}
|
||
} else if (idx == _pickedItem) {
|
||
node.obj.setState(getStateName(node.angle, hovPlace >= 0 && !testPlace(hovPlace), false));
|
||
node.obj->set_R(_runtime->game2world(mouse, _stackBottom.depth(_runtime) - 200));
|
||
} else {
|
||
node.obj.setState(getStateName(node.angle, node.pos == hovPlace && _pickedItem == -1, true));
|
||
if (!isFlying(idx))
|
||
node.obj->set_R(stackPosition(stidx(node.pos)));
|
||
}
|
||
iWin = iWin && testPlace(idx);
|
||
} else {
|
||
_runtime->hide(node.obj);
|
||
iWin = false;
|
||
}
|
||
}
|
||
|
||
if (iWin)
|
||
setState(GAME_WIN);
|
||
|
||
}
|
||
|
||
const mgVect3f &Puzzle::position(int num) const {
|
||
assert(num >= 0 && num < (int)_positions.size());
|
||
// Если глобальный поворот ненулевой, пересчитываем индекс
|
||
if (_globalAngle > 0) {
|
||
int size = sqrt((float)_gameSize);
|
||
int y = num / size;
|
||
int x = num - y * size;
|
||
--size;
|
||
for (int angle = 0; angle < _globalAngle; ++angle) {
|
||
int tmp = x;
|
||
x = size - y;
|
||
y = tmp;
|
||
}
|
||
num = y * (size + 1) + x;
|
||
}
|
||
assert(num >= 0 && num < (int)_positions.size());
|
||
return _positions[num];
|
||
}
|
||
|
||
mgVect3f Puzzle::stackPosition(int num) const {
|
||
mgVect3f bottom = _runtime->world2game(_stackBottom);
|
||
bottom.y -= _stackPlaceSize.y * num;
|
||
return _runtime->game2world(bottom);
|
||
}
|
||
|
||
} // namespace QDEngine
|