/* 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 "engines/nancy/util.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/input.h"
#include "engines/nancy/cursor.h"
#include "engines/nancy/puzzledata.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/rippedletterpuzzle.h"
#include "graphics/transform_struct.h"
namespace Nancy {
namespace Action {
void RippedLetterPuzzle::init() {
Common::Rect screenBounds = NancySceneState.getViewport().getBounds();
_drawSurface.create(screenBounds.width(), screenBounds.height(), g_nancy->_graphics->getInputPixelFormat());
_drawSurface.clear(g_nancy->_graphics->getTransColor());
setTransparent(true);
setVisible(true);
moveTo(screenBounds);
g_nancy->_resource->loadImage(_imageName, _image);
if (_useCustomPickUpTile) {
_pickedUpPiece._drawSurface.create(_image, _customPickUpTileSrc);
} else {
_pickedUpPiece._drawSurface.create(_destRects[0].width(), _destRects[0].height(), g_nancy->_graphics->getInputPixelFormat());
}
_pickedUpPiece.setVisible(false);
}
void RippedLetterPuzzle::registerGraphics() {
_pickedUpPiece.registerGraphics();
RenderObject::registerGraphics();
}
void RippedLetterPuzzle::readData(Common::SeekableReadStream &stream) {
_puzzleState = (RippedLetterPuzzleData *)NancySceneState.getPuzzleData(RippedLetterPuzzleData::getTag());
assert(_puzzleState);
readFilename(stream, _imageName);
byte maxWidth = 6;
byte maxHeight = g_nancy->getGameType() <= kGameTypeNancy6 ? 4 : 5;
byte width = maxWidth;
byte height = maxHeight;
if (g_nancy->getGameType() >= kGameTypeNancy5) {
width = stream.readByte();
height = stream.readByte();
}
// All the checks for whether width is greater than maxWidth are
// to account for nancy9 scene 2428, where the dimensions are 15x1
for (uint i = 0; i < height; ++i) {
readRectArray(stream, _srcRects, width, maxWidth);
}
stream.skip((maxWidth >= width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * 16);
for (uint i = 0; i < height; ++i) {
readRectArray(stream, _destRects, width, maxWidth);
}
stream.skip((maxWidth >= width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * 16);
readRect(stream, _rotateHotspot);
readRect(stream, _takeHotspot);
readRect(stream, _dropHotspot);
if (g_nancy->getGameType() >= kGameTypeNancy7) {
_rotationType = (RotationType)stream.readUint16LE();
}
uint elemSize = g_nancy->getGameType() <= kGameTypeNancy8 ? 1 : 2;
_initOrder.resize(width * height);
assert(width * height <= 24); // If this gets hit we need to increase the sizes in RippedLetterPuzzleData
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_initOrder[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
_initRotations.resize(width * height);
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_initRotations[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
if (g_nancy->getGameType() >= kGameTypeNancy9) {
uint16 numDoubledElements = stream.readUint16LE();
_doubles.resize(numDoubledElements);
uint i = 0;
for (uint j = 0; j < 20; ++j) {
int16 id = stream.readSint16LE();
if (id == -1) {
++i;
} else {
_doubles[i].push_back(id);
}
}
}
_solveOrder.resize(width * height);
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_solveOrder[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
_solveRotations.resize(width * height);
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_solveRotations[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
if (g_nancy->getGameType() >= kGameTypeNancy9) {
_useAltSolution = stream.readByte();
_solveOrderAlt.resize(width * height);
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_solveOrderAlt[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
_solveRotationsAlt.resize(width * height);
for (uint i = 0; i < height; ++i) {
for (uint j = 0; j < width; ++j) {
_solveRotationsAlt[i * width + j] = (elemSize == 1 ? stream.readByte() : stream.readSint16LE());
}
stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
}
stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
}
if (g_nancy->getGameType() >= kGameTypeNancy7) {
_useCustomPickUpTile = stream.readByte();
readRect(stream, _customPickUpTileSrc);
}
_takeSound.readNormal(stream);
_dropSound.readNormal(stream);
_rotateSound.readNormal(stream);
_solveExitScene.readData(stream);
_solveSound.readNormal(stream);
_exitScene.readData(stream);
readRect(stream, _exitHotspot);
if (g_nancy->getGameType() >= kGameTypeNancy9) {
_customCursorID = stream.readSint16LE();
}
}
void RippedLetterPuzzle::execute() {
switch (_state) {
case kBegin:
_puzzleState = (RippedLetterPuzzleData *)NancySceneState.getPuzzleData(RippedLetterPuzzleData::getTag());
assert(_puzzleState);
init();
registerGraphics();
NancySceneState.setNoHeldItem();
if (!_puzzleState->playerHasTriedPuzzle) {
_puzzleState->order = _initOrder;
_puzzleState->rotations = _initRotations;
_puzzleState->playerHasTriedPuzzle = true;
} else if (_puzzleState->_pickedUpPieceID != -1) {
// Puzzle was left while still holding a piece (e.g. by clicking a scene item).
// Make sure we put the held piece back in its place
_puzzleState->order[_puzzleState->_pickedUpPieceLastPos] = _puzzleState->_pickedUpPieceID;
_puzzleState->rotations[_puzzleState->_pickedUpPieceLastPos] = _puzzleState->_pickedUpPieceRot;
_puzzleState->_pickedUpPieceID = -1;
_puzzleState->_pickedUpPieceLastPos = -1;
_puzzleState->_pickedUpPieceRot = 0;
}
for (uint i = 0; i < _puzzleState->order.size(); ++i) {
drawPiece(i, _puzzleState->rotations[i], _puzzleState->order[i]);
}
g_nancy->_sound->loadSound(_takeSound);
g_nancy->_sound->loadSound(_dropSound);
g_nancy->_sound->loadSound(_rotateSound);
_state = kRun;
// fall through
case kRun:
switch (_solveState) {
case kNotSolved :
for (uint i = 0; i < _puzzleState->order.size(); ++i) {
if (_puzzleState->rotations[i] != _solveRotations[i] || !checkOrder(false)) {
if (_useAltSolution) {
if (!checkOrder(true)) {
return;
}
} else {
return;
}
}
}
g_nancy->_sound->loadSound(_solveSound);
g_nancy->_sound->playSound(_solveSound);
_solveState = kWaitForSound;
break;
case kWaitForSound :
if (!g_nancy->_sound->isSoundPlaying(_solveSound)) {
g_nancy->_sound->stopSound(_solveSound);
_state = kActionTrigger;
}
break;
}
break;
case kActionTrigger :
switch (_solveState) {
case kNotSolved:
_exitScene.execute();
break;
case kWaitForSound:
if (_solveExitScene._sceneChange.sceneID == NancySceneState.getSceneInfo().sceneID) {
// nancy9 scene 2484 is auto-solved for you, but has a valid scene change back to itself
return;
}
_solveExitScene.execute();
_puzzleState->playerHasTriedPuzzle = false;
break;
}
g_nancy->_sound->stopSound(_takeSound);
g_nancy->_sound->stopSound(_dropSound);
g_nancy->_sound->stopSound(_rotateSound);
finishExecution();
}
}
void RippedLetterPuzzle::handleInput(NancyInput &input) {
if (_state == kBegin) {
return;
}
for (uint i = 0; i < _puzzleState->order.size(); ++i) {
Common::Rect screenHotspot = NancySceneState.getViewport().convertViewportToScreen(_destRects[i]);
if (screenHotspot.contains(input.mousePos)) {
Common::Rect insideRect;
if (_puzzleState->_pickedUpPieceID == -1) {
// No piece picked up
// Check if the mouse is inside the rotation hotspot
insideRect = _rotateHotspot;
insideRect.translate(screenHotspot.left, screenHotspot.top);
if (_rotationType != kRotationNone && insideRect.contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(CursorManager::kRotateCW);
if (input.input & NancyInput::kLeftMouseButtonUp) {
// Player has clicked, rotate the piece
int inc = (_rotationType == kRotation90 ? 1 : 2);
if ((_puzzleState->rotations[i] += inc) > 3) {
_puzzleState->rotations[i] -= 4;
}
drawPiece(i, _puzzleState->rotations[i], _puzzleState->order[i]);
g_nancy->_sound->playSound(_rotateSound);
}
break;
}
// Check if the mouse is inside the pickup hotspot
insideRect = _takeHotspot;
insideRect.translate(screenHotspot.left, screenHotspot.top);
if (insideRect.contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
if (input.input & NancyInput::kLeftMouseButtonUp) {
// Player has clicked, take the piece
// First, copy the graphic from the full drawSurface...
if (!_useCustomPickUpTile) {
_pickedUpPiece._drawSurface.clear(g_nancy->_graphics->getTransColor());
_pickedUpPiece._drawSurface.blitFrom(_drawSurface, _destRects[i], Common::Point());
}
_pickedUpPiece.setVisible(true);
_pickedUpPiece.setTransparent(true);
_pickedUpPiece.pickUp();
// ...then change the data...
_puzzleState->_pickedUpPieceID = _puzzleState->order[i];
_puzzleState->_pickedUpPieceRot = _puzzleState->rotations[i];
_puzzleState->order[i] = -1;
_puzzleState->_pickedUpPieceLastPos = i;
// ...then clear the piece from the drawSurface
drawPiece(i, 0);
g_nancy->_sound->playSound(_takeSound);
}
break;
}
} else {
// Currently carrying a piece
// Check if the mouse is inside the drop hotspot
insideRect = _dropHotspot;
insideRect.translate(screenHotspot.left, screenHotspot.top);
if (insideRect.contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
if (input.input & NancyInput::kLeftMouseButtonUp) {
// Player has clicked, drop the piece and pick up a new one
// Check if we should pick up a new piece
if (_puzzleState->order[i] == -1) {
// No, hide the picked up piece graphic
_pickedUpPiece.setVisible(false);
_puzzleState->_pickedUpPieceLastPos = -1;
} else {
// Yes, change the picked piece graphic
if (!_useCustomPickUpTile) {
_pickedUpPiece._drawSurface.clear(g_nancy->_graphics->getTransColor());
_pickedUpPiece._drawSurface.blitFrom(_drawSurface, _destRects[i], Common::Point());
}
_pickedUpPiece.setVisible(true);
_pickedUpPiece.setTransparent(true);
}
SWAP(_puzzleState->order[i], _puzzleState->_pickedUpPieceID);
SWAP(_puzzleState->rotations[i], _puzzleState->_pickedUpPieceRot);
// Draw the newly placed piece
drawPiece(i, _puzzleState->rotations[i], _puzzleState->order[i]);
g_nancy->_sound->playSound(_dropSound);
}
break;
}
}
}
}
_pickedUpPiece.handleInput(input);
if (_puzzleState->_pickedUpPieceID == -1) {
// No piece picked up, check the exit hotspot
if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(_customCursorID != -1 ? (CursorManager::CursorType)_customCursorID : g_nancy->_cursor->_puzzleExitCursor);
if (input.input & NancyInput::kLeftMouseButtonUp) {
// Player has clicked, exit
_state = kActionTrigger;
}
}
}
}
void RippedLetterPuzzle::drawPiece(const uint pos, const byte rotation, const int pieceID) {
// Clear the selected position
_drawSurface.fillRect(_destRects[pos], _drawSurface.getTransparentColor());
_needsRedraw = true;
// No piece, just clear
if (pieceID == -1) {
return;
}
// Create temporary ManagedSurfaces and call the custom rotation function
Graphics::ManagedSurface srcSurf(_image, _srcRects[pieceID]);
Graphics::ManagedSurface destSurf(_drawSurface, _destRects[pos]);
GraphicsManager::rotateBlit(srcSurf, destSurf, rotation);
}
bool RippedLetterPuzzle::checkOrder(bool useAlt) {
auto ¤t = _puzzleState->order;
auto &correct = useAlt ? _solveOrderAlt : _solveOrder;
for (uint i = 0; i < correct.size(); ++i) {
bool foundCorrect = false;
bool isDoubled = false;
for (auto &d : _doubles) {
for (byte e : d) {
if (current[i] == e) {
isDoubled = true;
break;
}
}
if (isDoubled) {
for (byte e : d) {
if (correct[i] == e) {
foundCorrect = true;
break;
}
}
if (!foundCorrect) {
return false;
}
break;
}
}
if (!isDoubled) {
if (current[i] != correct[i]) {
return false;
}
}
}
return true;
}
} // End of namespace Action
} // End of namespace Nancy