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