Files
2026-02-02 04:50:13 +01:00

477 lines
15 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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