/* 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/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 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