scummvm/engines/saga/puzzle.cpp
2022-12-21 14:58:09 +01:00

584 lines
17 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 "saga/saga.h"
#include "saga/actor.h"
#include "saga/interface.h"
#include "saga/scene.h"
#include "saga/sprite.h"
#include "saga/puzzle.h"
#include "saga/render.h"
#include "common/system.h"
#include "common/timer.h"
namespace Saga {
#define ITE_ACTOR_PUZZLE 176
#define PUZZLE_X_OFFSET 72
#define PUZZLE_Y_OFFSET 46
#define PUZZLE_FIT 0x01 // 1 when in correct position
#define PUZZLE_MOVED 0x04 // 1 when somewhere in the box
#define PUZZLE_ALL_SET PUZZLE_FIT | PUZZLE_MOVED
// Puzzle portraits
#define RID_ITE_SAKKA_APPRAISING 6
#define RID_ITE_SAKKA_DENIAL 7
#define RID_ITE_SAKKA_EXCITED 8
#define RID_ITE_JFERRET_SERIOUS 9
#define RID_ITE_JFERRET_GOOFY 10
#define RID_ITE_JFERRET_ALOOF 11
const char portraitList[] = {
RID_ITE_JFERRET_SERIOUS,
RID_ITE_JFERRET_GOOFY,
RID_ITE_JFERRET_SERIOUS,
RID_ITE_JFERRET_GOOFY,
RID_ITE_JFERRET_ALOOF
};
enum rifOptions {
kROLater = 0,
kROAccept = 1,
kRODecline = 2,
kROHint = 3
};
Puzzle::Puzzle(SagaEngine *vm) : _vm(vm), _solved(false), _active(false) {
_lang = _vm->getLanguageIndex();
_hintRqState = kRQNoHint;
_hintOffer = 0;
_hintCount = 0;
_helpCount = 0;
_puzzlePiece = -1;
_newPuzzle = true;
_sliding = false;
_hintBox.left = 70;
_hintBox.top = 105;
_hintBox.setWidth(240);
_hintBox.setHeight(30);
_hintNextRqState = kRQNoHint;
_hintGiver = 0;
_hintSpeaker = 0;
_slidePointX = _slidePointY = 0;
initPieceInfo(0, 268, 18, 0, 0, 0 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 3,
Point(0, 1), Point(0, 62), Point(15, 31), Point(0, 0), Point(0, 0), Point(0, 0));
initPieceInfo(1, 270, 52, 0, 0, 0 + PUZZLE_X_OFFSET, 32 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 31), Point(0, 47), Point(39, 47), Point(15, 1), Point(0, 0), Point(0, 0));
initPieceInfo(2, 19, 51, 0, 0, 0 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 0), Point(23, 46), Point(39, 15), Point(31, 0), Point(0, 0), Point(0, 0));
initPieceInfo(3, 73, 0, 0, 0, 32 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 6,
Point(0, 0), Point(8, 16), Point(0, 31), Point(31, 31), Point(39, 15), Point(31, 0));
initPieceInfo(4, 0, 35, 0, 0, 64 + PUZZLE_X_OFFSET, 16 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 15), Point(15, 46), Point(23, 32), Point(7, 1), Point(0, 0), Point(0, 0));
initPieceInfo(5, 215, 0, 0, 0, 24 + PUZZLE_X_OFFSET, 32 + PUZZLE_Y_OFFSET, 0, 6,
Point(0, 15), Point(8, 31), Point(39, 31), Point(47, 16), Point(39, 0), Point(8, 0));
initPieceInfo(6, 159, 0, 0, 0, 32 + PUZZLE_X_OFFSET, 48 + PUZZLE_Y_OFFSET, 0, 5,
Point(0, 16), Point(8, 31), Point(55, 31), Point(39, 1), Point(32, 15), Point(0, 0));
initPieceInfo(7, 9, 70, 0, 0, 80 + PUZZLE_X_OFFSET, 32 + PUZZLE_Y_OFFSET, 0, 5,
Point(0, 31), Point(8, 47), Point(23, 47), Point(31, 31), Point(15, 1), Point(0, 0));
initPieceInfo(8, 288, 18, 0, 0, 96 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 31), Point(15, 62), Point(31, 32), Point(15, 1), Point(0, 0), Point(0, 0));
initPieceInfo(9, 112, 0, 0, 0, 112 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 0), Point(16, 31), Point(47, 31), Point(31, 0), Point(0, 0), Point(0, 0));
initPieceInfo(10, 27, 89, 0, 0, 104 + PUZZLE_X_OFFSET, 32 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 47), Point(31, 47), Point(31, 0), Point(24, 0), Point(0, 0), Point(0, 0));
initPieceInfo(11, 43, 0, 0, 0, 136 + PUZZLE_X_OFFSET, 32 + PUZZLE_Y_OFFSET, 0, 6,
Point(0, 0), Point(0, 47), Point(15, 47), Point(15, 15), Point(31, 15), Point(23, 0));
initPieceInfo(12, 0, 0, 0, 0, 144 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 0), Point(24, 47), Point(39, 47), Point(39, 0), Point(0, 0), Point(0, 0));
initPieceInfo(13, 262, 0, 0, 0, 64 + PUZZLE_X_OFFSET, 0 + PUZZLE_Y_OFFSET, 0, 3,
Point(0, 0), Point(23, 46), Point(47, 0), Point(0, 0), Point(0, 0), Point(0, 0));
initPieceInfo(14, 271, 103, 0, 0, 152 + PUZZLE_X_OFFSET, 48 + PUZZLE_Y_OFFSET, 0, 4,
Point(0, 0), Point(0, 31), Point(31, 31), Point(31, 0), Point(0, 0), Point(0, 0));
}
void Puzzle::initPieceInfo(int i, int16 curX, int16 curY, byte offX, byte offY, int16 trgX,
int16 trgY, uint8 flag, uint8 count, Point point0, Point point1,
Point point2, Point point3, Point point4, Point point5) {
_pieceInfo[i].curX = curX;
_pieceInfo[i].curY = curY;
_pieceInfo[i].offX = offX;
_pieceInfo[i].offY = offY;
_pieceInfo[i].trgX = trgX;
_pieceInfo[i].trgY = trgY;
_pieceInfo[i].flag = flag;
_pieceInfo[i].count = count;
_pieceInfo[i].point[0] = point0;
_pieceInfo[i].point[1] = point1;
_pieceInfo[i].point[2] = point2;
_pieceInfo[i].point[3] = point3;
_pieceInfo[i].point[4] = point4;
_pieceInfo[i].point[5] = point5;
}
void Puzzle::execute() {
_active = true;
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, kPuzzleHintTime, this, "sagaPuzzleHint");
initPieces();
showPieces();
_vm->_interface->setMode(kPanelConverse);
clearHint();
//_solved = true; // Cheat
//exitPuzzle();
}
void Puzzle::exitPuzzle() {
_active = false;
_vm->getTimerManager()->removeTimerProc(&hintTimerCallback);
_vm->_scene->changeScene(ITE_SCENE_LODGE, 0, kTransitionNoFade);
_vm->_interface->setMode(kPanelMain);
}
void Puzzle::initPieces() {
SpriteInfo *spI;
ActorData *puzzle = _vm->_actor->getActor(_vm->_actor->actorIndexToId(ITE_ACTOR_PUZZLE));
int frameNumber;
SpriteList *spriteList;
_vm->_actor->getSpriteParams(puzzle, frameNumber, spriteList);
for (int i = 0; i < PUZZLE_PIECES; i++) {
spI = &((*spriteList)[i]);
_pieceInfo[i].offX = (byte)(spI->width >> 1);
_pieceInfo[i].offY = (byte)(spI->height >> 1);
if (_newPuzzle) {
_pieceInfo[i].curX = pieceOrigins[i].x;
_pieceInfo[i].curY = pieceOrigins[i].y;
}
_piecePriority[i] = i;
}
_newPuzzle = false;
}
void Puzzle::showPieces() {
ActorData *puzzle = _vm->_actor->getActor(_vm->_actor->actorIndexToId(ITE_ACTOR_PUZZLE));
int frameNumber;
SpriteList *spriteList;
_vm->_actor->getSpriteParams(puzzle, frameNumber, spriteList);
for (int j = PUZZLE_PIECES - 1; j >= 0; j--) {
int num = _piecePriority[j];
if (_puzzlePiece != num) {
_vm->_sprite->draw(*spriteList, num, Point(_pieceInfo[num].curX, _pieceInfo[num].curY), 256);
}
}
}
void Puzzle::drawCurrentPiece() {
ActorData *puzzle = _vm->_actor->getActor(_vm->_actor->actorIndexToId(ITE_ACTOR_PUZZLE));
int frameNumber;
SpriteList *spriteList;
_vm->_actor->getSpriteParams(puzzle, frameNumber, spriteList);
_vm->_sprite->draw(*spriteList, _puzzlePiece,
Point(_pieceInfo[_puzzlePiece].curX, _pieceInfo[_puzzlePiece].curY), 256, true);
}
void Puzzle::movePiece(Point mousePt) {
int newx, newy;
showPieces();
if (_puzzlePiece == -1)
return;
if (_sliding) {
newx = _slidePointX;
newy = _slidePointY;
} else {
if (mousePt.y >= 137)
return;
newx = mousePt.x;
newy = mousePt.y;
}
newx -= _pieceInfo[_puzzlePiece].offX;
newy -= _pieceInfo[_puzzlePiece].offY;
_pieceInfo[_puzzlePiece].curX = newx;
_pieceInfo[_puzzlePiece].curY = newy;
drawCurrentPiece();
}
void Puzzle::handleClick(Point mousePt) {
if (_puzzlePiece != -1) {
dropPiece(mousePt);
if (!_active)
return; // we won
drawCurrentPiece();
_puzzlePiece = -1;
return;
}
for (int j = 0; j < PUZZLE_PIECES; j++) {
int i = _piecePriority[j];
int adjX = mousePt.x - _pieceInfo[i].curX;
int adjY = mousePt.y - _pieceInfo[i].curY;
if (hitTestPoly(&_pieceInfo[i].point[0], _pieceInfo[i].count, Point(adjX, adjY))) {
_puzzlePiece = i;
break;
}
}
if (_puzzlePiece == -1)
return;
alterPiecePriority();
// Display scene background
_vm->_scene->draw();
showPieces();
int newx = mousePt.x - _pieceInfo[_puzzlePiece].offX;
int newy = mousePt.y - _pieceInfo[_puzzlePiece].offY;
if (newx != _pieceInfo[_puzzlePiece].curX
|| newy != _pieceInfo[_puzzlePiece].curY) {
_pieceInfo[_puzzlePiece].curX = newx;
_pieceInfo[_puzzlePiece].curY = newy;
}
_vm->_interface->setStatusText(pieceNames[_lang][_puzzlePiece]);
}
void Puzzle::alterPiecePriority() {
for (int i = 1; i < PUZZLE_PIECES; i++) {
if (_puzzlePiece == _piecePriority[i]) {
for (int j = i - 1; j >= 0; j--)
_piecePriority[j+1] = _piecePriority[j];
_piecePriority[0] = _puzzlePiece;
break;
}
}
}
void Puzzle::slidePiece(int x1, int y1, int x2, int y2) {
int count;
PointList slidePoints;
slidePoints.resize(320);
x1 += _pieceInfo[_puzzlePiece].offX;
y1 += _pieceInfo[_puzzlePiece].offY;
count = pathLine(slidePoints, 0, Point(x1, y1),
Point(x2 + _pieceInfo[_puzzlePiece].offX, y2 + _pieceInfo[_puzzlePiece].offY));
if (count > 1) {
int factor = count / 4;
_sliding = true;
if (!factor)
factor++;
for (int i = 1; i < count; i += factor) {
_slidePointX = slidePoints[i].x;
_slidePointY = slidePoints[i].y;
_vm->_render->drawScene();
_vm->_system->delayMillis(10);
}
_sliding = false;
}
_pieceInfo[_puzzlePiece].curX = x2;
_pieceInfo[_puzzlePiece].curY = y2;
}
void Puzzle::dropPiece(Point mousePt) {
int boxx = PUZZLE_X_OFFSET;
int boxy = PUZZLE_Y_OFFSET;
int boxw = boxx + 184;
int boxh = boxy + 80;
// if the center is within the box quantize within
// else move it back to its original start point
if (mousePt.x >= boxx && mousePt.x < boxw && mousePt.y >= boxy && mousePt.y <= boxh) {
ActorData *puzzle = _vm->_actor->getActor(_vm->_actor->actorIndexToId(ITE_ACTOR_PUZZLE));
SpriteInfo *spI;
int frameNumber;
SpriteList *spriteList;
_vm->_actor->getSpriteParams(puzzle, frameNumber, spriteList);
int newx = mousePt.x - _pieceInfo[_puzzlePiece].offX;
int newy = mousePt.y - _pieceInfo[_puzzlePiece].offY;
if (newx < boxx)
newx = PUZZLE_X_OFFSET;
if (newy < boxy)
newy = PUZZLE_Y_OFFSET;
spI = &((*spriteList)[_puzzlePiece]);
if (newx + spI->width > boxw)
newx = boxw - spI->width;
if (newy + spI->height > boxh)
newy = boxh - spI->height;
int x1 = ((newx - PUZZLE_X_OFFSET) & ~7) + PUZZLE_X_OFFSET;
int y1 = ((newy - PUZZLE_Y_OFFSET) & ~7) + PUZZLE_Y_OFFSET;
int x2 = x1 + 8;
int y2 = y1 + 8;
newx = (x2 - newx < newx - x1) ? x2 : x1;
newy = (y2 - newy < newy - y1) ? y2 : y1;
// if any part of the puzzle piece falls outside the box
// force it back in
// is the piece at the target location
if (newx == _pieceInfo[_puzzlePiece].trgX
&& newy == _pieceInfo[_puzzlePiece].trgY) {
_pieceInfo[_puzzlePiece].flag |= (PUZZLE_MOVED | PUZZLE_FIT);
} else {
_pieceInfo[_puzzlePiece].flag &= ~PUZZLE_FIT;
_pieceInfo[_puzzlePiece].flag |= PUZZLE_MOVED;
}
_pieceInfo[_puzzlePiece].curX = newx;
_pieceInfo[_puzzlePiece].curY = newy;
} else {
int newx = pieceOrigins[_puzzlePiece].x;
int newy = pieceOrigins[_puzzlePiece].y;
_pieceInfo[_puzzlePiece].flag &= ~(PUZZLE_FIT | PUZZLE_MOVED);
// slide piece from current position to new position
slidePiece(_pieceInfo[_puzzlePiece].curX, _pieceInfo[_puzzlePiece].curY,
newx, newy);
}
// is the puzzle completed?
_solved = true;
for (int i = 0; i < PUZZLE_PIECES; i++)
if ((_pieceInfo[i].flag & PUZZLE_FIT) == 0) {
_solved = false;
break;
}
if (_solved)
exitPuzzle();
}
void Puzzle::hintTimerCallback(void *refCon) {
((Puzzle *)refCon)->solicitHint();
}
void Puzzle::solicitHint() {
// CHECKME: This is potentially called from a different thread because it is
// called from a timer callback. However, it does not seem to take any
// precautions to avoid race conditions.
int i;
_vm->_actor->setSpeechColor(1, _vm->iteColorBlack());
_vm->getTimerManager()->removeTimerProc(&hintTimerCallback);
switch (_hintRqState) {
case kRQSpeaking:
if (_vm->_actor->isSpeaking()) {
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, 50 * 1000000, this, "sagaPuzzleHint");
break;
}
_hintRqState = _hintNextRqState;
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, 100*1000000/3, this, "sagaPuzzleHint");
break;
case kRQNoHint:
// Pick a random hint request.
i = _hintOffer++;
if (_hintOffer >= NUM_SOLICIT_REPLIES)
_hintOffer = 0;
// Determine which of the journeymen will offer then
// hint, and then show that character's portrait.
_hintGiver = portraitList[i];
_hintSpeaker = _hintGiver - RID_ITE_JFERRET_SERIOUS;
_vm->_interface->setRightPortrait(_hintGiver);
_vm->_actor->nonActorSpeech(_hintBox, &solicitStr[_lang][i], 1, PUZZLE_SOLICIT_SOUNDS + i * 3 + _hintSpeaker, 0);
// Add Rif's reply to the list.
clearHint();
// Roll to see if Sakka scolds
if (_vm->_rnd.getRandomNumber(1)) {
_hintRqState = kRQSakkaDenies;
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, 200*1000000, this, "sagaPuzzleHint");
} else {
_hintRqState = kRQSpeaking;
_hintNextRqState = kRQHintRequested;
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, 50*1000000, this, "sagaPuzzleHint");
}
break;
case kRQHintRequested:
i = _vm->_rnd.getRandomNumber(NUM_SAKKA - 1);
_vm->_actor->nonActorSpeech(_hintBox, &sakkaStr[_lang][i], 1, PUZZLE_SAKKA_SOUNDS + i, 0);
_vm->_interface->setRightPortrait(RID_ITE_SAKKA_APPRAISING);
_hintRqState = kRQSpeaking;
_hintNextRqState = kRQHintRequestedStage2;
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, 50*1000000, this, "sagaPuzzleHint");
_vm->_interface->converseClear();
_vm->_interface->converseAddText(optionsStr[_lang][kROAccept], 0, 1, 0, 0);
_vm->_interface->converseAddText(optionsStr[_lang][kRODecline], 0, 2, 0, 0);
_vm->_interface->converseAddText(optionsStr[_lang][kROLater], 0, 0, 0, 0);
_vm->_interface->converseDisplayText();
break;
case kRQHintRequestedStage2:
if (_vm->_rnd.getRandomNumber(1)) { // Skip Reply part
i = _vm->_rnd.getRandomNumber(NUM_WHINES - 1);
_vm->_actor->nonActorSpeech(_hintBox, &whineStr[_lang][i], 1, PUZZLE_WHINE_SOUNDS + i * 3 + _hintSpeaker, 0);
}
_vm->_interface->setRightPortrait(_hintGiver);
_hintRqState = kRQSakkaDenies;
break;
case kRQSakkaDenies:
_vm->_interface->converseClear();
_vm->_interface->converseAddText(optionsStr[_lang][kROAccept], 0, 1, 0, 0);
_vm->_interface->converseAddText(optionsStr[_lang][kRODecline], 0, 2, 0, 0);
_vm->_interface->converseAddText(optionsStr[_lang][kROLater], 0, 0, 0, 0);
_vm->_interface->converseDisplayText();
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, kPuzzleHintTime, this, "sagaPuzzleHint");
_hintRqState = kRQSkipEverything;
break;
default:
break;
}
}
void Puzzle::handleReply(int reply) {
switch (reply) {
case 0: // Quit the puzzle
exitPuzzle();
break;
case 1: // Accept the hint
giveHint();
break;
case 2: // Decline the hint
_vm->_actor->abortSpeech();
_hintRqState = kRQNoHint;
_vm->getTimerManager()->removeTimerProc(&hintTimerCallback);
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, kPuzzleHintTime * 2, this, "sagaPuzzleHint");
clearHint();
break;
default:
break;
}
}
void Puzzle::giveHint() {
int i, total = 0;
_vm->_interface->converseClear();
_vm->_actor->abortSpeech();
_vm->_interface->setRightPortrait(_hintGiver);
for (i = 0; i < PUZZLE_PIECES; i++)
total += _pieceInfo[i].flag & PUZZLE_FIT;
if (_hintCount == 0 && (_pieceInfo[1].flag & PUZZLE_FIT
|| _pieceInfo[12].flag & PUZZLE_FIT))
_hintCount++;
if (_hintCount == 1 && _pieceInfo[14].flag & PUZZLE_FIT)
_hintCount++;
if (_hintCount == 2 && total > 3)
_hintCount++;
_vm->_actor->setSpeechColor(1, _vm->iteColorBlack());
if (_hintCount < 3) {
_vm->_actor->nonActorSpeech(_hintBox, &hintStr[_lang][_hintCount], 1, PUZZLE_HINT_SOUNDS + _hintCount * 3 + _hintSpeaker, 0);
} else {
int piece = 0;
for (i = PUZZLE_PIECES - 1; i >= 0; i--) {
piece = _piecePriority[i];
if (_pieceInfo[piece].flag & PUZZLE_MOVED
&& !(_pieceInfo[piece].flag & PUZZLE_FIT)) {
if (_helpCount < 12)
_helpCount++;
break;
}
}
if (i >= 0) {
static char hintBuf[64];
static const char *hintPtr = hintBuf;
Common::sprintf_s(hintBuf, optionsStr[_lang][kROHint], pieceNames[_lang][piece]);
_vm->_actor->nonActorSpeech(_hintBox, &hintPtr, 1, PUZZLE_TOOL_SOUNDS + _hintSpeaker + piece * 3, 0);
} else {
// If no pieces are in the wrong place
_vm->_actor->nonActorSpeech(_hintBox, &hintStr[_lang][3], 1, PUZZLE_HINT_SOUNDS + 3 * 3 + _hintSpeaker, 0);
}
}
_hintCount++;
_hintRqState = kRQNoHint;
_vm->_interface->converseAddText(optionsStr[_lang][kROLater], 0, 0, 0, 0);
_vm->_interface->converseDisplayText();
_vm->getTimerManager()->removeTimerProc(&hintTimerCallback);
_vm->getTimerManager()->installTimerProc(&hintTimerCallback, kPuzzleHintTime, this, "sagaPuzzleHint");
}
void Puzzle::clearHint() {
_vm->_interface->converseClear();
_vm->_interface->converseAddText(optionsStr[_lang][kROLater], 0, 0, 0, 0);
_vm->_interface->converseDisplayText();
_vm->_interface->setStatusText(" ");
}
} // End of namespace Saga