/* 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/serializer.h" #include "engines/nancy/nancy.h" #include "engines/nancy/graphics.h" #include "engines/nancy/resource.h" #include "engines/nancy/input.h" #include "engines/nancy/sound.h" #include "engines/nancy/util.h" #include "engines/nancy/action/puzzle/orderingpuzzle.h" #include "engines/nancy/state/scene.h" namespace Nancy { namespace Action { void OrderingPuzzle::init() { for (uint i = 0; i < _destRects.size(); ++i) { if (i == 0) { _screenPosition = _destRects[i]; } else { _screenPosition.extend(_destRects[i]); } } for (uint i = 0; i < _overlayDests.size(); ++i) { _screenPosition.extend(_overlayDests[i]); } if (!_checkButtonDest.isEmpty()) { _screenPosition.extend(_checkButtonDest); } g_nancy->_resource->loadImage(_imageName, _image); _drawSurface.create(_screenPosition.width(), _screenPosition.height(), g_nancy->_graphics->getInputPixelFormat()); if (_image.hasPalette()) { uint8 palette[256 * 3]; _image.grabPalette(palette, 0, 256); _drawSurface.setPalette(palette, 0, 256); } setTransparent(true); _drawSurface.clear(_drawSurface.getTransparentColor()); setVisible(true); RenderObject::init(); } void OrderingPuzzle::readData(Common::SeekableReadStream &stream) { bool isPiano = _puzzleType == kPiano; bool isOrderItems = _puzzleType == kOrderItems; bool isKeypad = _puzzleType == kKeypad || _puzzleType == kKeypadTerse; readFilename(stream, _imageName); Common::Serializer ser(&stream, nullptr); ser.setVersion(g_nancy->getGameType()); uint16 numElements = 5; uint16 maxNumElements = 15; if (ser.getVersion() == kGameTypeVampire) { // Hardcoded in The Vampire Diaries numElements = maxNumElements = 5; } else { ser.syncAsUint16LE(numElements); } switch (_puzzleType) { case kOrderItems : ser.syncAsByte(_hasSecondState); ser.syncAsByte(_itemsStayDown); break; case kPiano : _itemsStayDown = false; break; case kKeypadTerse: // fall through case kKeypad : ser.syncAsByte(_itemsStayDown); ser.syncAsByte(_needButtonToCheckSuccess); readRect(ser, _checkButtonSrc); readRect(ser, _checkButtonDest); maxNumElements = 30; break; default: break; } // nancy7 moved the keypad rects at the end if (g_nancy->getGameType() <= kGameTypeNancy6 || !isKeypad) { readRectArray(ser, _down1Rects, numElements, maxNumElements); if (isOrderItems) { readRectArray(stream, _up2Rects, numElements, maxNumElements); readRectArray(stream, _down2Rects, numElements, maxNumElements); } readRectArray(ser, _destRects, numElements, maxNumElements); if (isPiano) { readRectArray(stream, _hotspots, numElements, maxNumElements); } else { _hotspots = _destRects; } } if (isPiano && g_nancy->getGameType() >= kGameTypeNancy8) { _specialCursor1Id = stream.readUint16LE(); readRect(stream, _specialCursor1Dest); _specialCursor2Id = stream.readUint16LE(); readRect(stream, _specialCursor2Dest); } uint sequenceLength = 5; ser.syncAsUint16LE(sequenceLength, kGameTypeNancy1); if (isKeypad) { ser.syncAsByte(_checkOrder, kGameTypeNancy7); } _correctSequence.resize(sequenceLength); uint sizeElem = 1; for (uint i = 0; i < sequenceLength; ++i) { switch (_puzzleType) { case kKeypadTerse: // fall through case kKeypad : // fall through case kOrdering: ser.syncAsByte(_correctSequence[i]); sizeElem = 1; break; case kPiano: ser.syncAsUint16LE(_correctSequence[i]); sizeElem = 2; break; case kOrderItems: // For some reason, OrderItems labels starting from 1 ser.syncAsUint16LE(_correctSequence[i]); --_correctSequence[i]; sizeElem = 2; break; default: error("OrderingPuzzle::readData(): Unsupported puzzle type %d", _puzzleType); } } ser.skip((maxNumElements - sequenceLength) * sizeElem, kGameTypeNancy1); if (isOrderItems) { uint numOverlays = 0; ser.syncAsUint16LE(_state2InvItem); ser.syncAsUint16LE(numOverlays); readRectArray(ser, _overlaySrcs, numOverlays); readRectArray(ser, _overlayDests, numOverlays); } else if (isPiano && g_nancy->getGameType() >= kGameTypeNancy8) { readFilenameArray(stream, _pianoSoundNames, numElements); stream.skip((maxNumElements - numElements) * 33); } if (ser.getVersion() > kGameTypeVampire) { _pushDownSound.readNormal(stream); if (isOrderItems) { _itemSound.readNormal(stream); _popUpSound.readNormal(stream); } } if (ser.getVersion() == kGameTypeVampire) { _solveExitScene._sceneChange.readData(stream, true); ser.skip(2); // shouldStopRendering ser.syncAsSint16LE(_solveExitScene._flag.label); ser.syncAsByte(_solveExitScene._flag.flag); } else { _solveExitScene.readData(stream); } ser.syncAsUint16LE(_solveSoundDelay); _solveSound.readNormal(stream); if (ser.getVersion() == kGameTypeVampire) { _exitScene._sceneChange.readData(stream, true); ser.skip(2); // shouldStopRendering ser.syncAsSint16LE(_exitScene._flag.label); ser.syncAsByte(_exitScene._flag.flag); } else { _exitScene.readData(stream); } readRect(stream, _exitHotspot); if (isKeypad && g_nancy->getGameType() >= kGameTypeNancy7) { if (_puzzleType == kKeypad) { readRectArray(ser, _down1Rects, numElements, maxNumElements); readRectArray(ser, _destRects, numElements, maxNumElements); } else if (_puzzleType == kKeypadTerse) { _down1Rects.resize(numElements); _destRects.resize(numElements); // Terse elements are the same size & placed on a grid (in the source image AND on screen) uint16 columns = stream.readUint16LE(); stream.skip(2); // rows uint16 width = stream.readUint16LE(); uint16 height = stream.readUint16LE(); Common::Point srcStartPos, srcDist, destStartPos, destDist; srcStartPos.x = stream.readUint16LE(); srcStartPos.y = stream.readUint16LE(); srcDist.x = stream.readUint16LE(); srcDist.y = stream.readUint16LE(); destStartPos.x = stream.readUint16LE(); destStartPos.y = stream.readUint16LE(); destDist.x = stream.readUint16LE(); destDist.y = stream.readUint16LE(); for (uint i = 0; i < numElements; ++i) { uint x = i % columns; uint y = i / columns; Common::Rect &src = _down1Rects[i]; src.left = srcStartPos.x + (x * srcDist.x) + (width * x); src.top = srcStartPos.y + (y * srcDist.y) + (height * y); src.setWidth(width + 1); src.setHeight(height + 1); Common::Rect &dest = _destRects[i]; dest.left = destStartPos.x + (x * destDist.x) + (width * x); dest.top = destStartPos.y + (y * destDist.y) + (height * y); dest.setWidth(width + 1); dest.setHeight(height + 1); } } _hotspots = _destRects; } _downItems.resize(numElements, false); _secondStateItems.resize(numElements, false); } void OrderingPuzzle::execute() { switch (_state) { case kBegin: init(); registerGraphics(); if (g_nancy->getGameType() > kGameTypeVampire) { g_nancy->_sound->loadSound(_pushDownSound); if (_puzzleType == kOrderItems) { g_nancy->_sound->loadSound(_itemSound); g_nancy->_sound->loadSound(_popUpSound); } } NancySceneState.setNoHeldItem(); _state = kRun; // fall through case kRun: switch (_solveState) { case kNotSolved: { if (!_itemsStayDown) { // Clear the pushed item if (g_nancy->_sound->isSoundPlaying(_pushDownSound)) { return; } for (uint i = 0; i < _downItems.size(); ++i) { if (_downItems[i]) { popUp(i); } } } bool solved = true; if (_puzzleType != kPiano) { if (_clickedSequence.size() >= _correctSequence.size()) { bool equal = true; if (_checkOrder) { equal = (_clickedSequence == _correctSequence); } else { for (uint i = 0; i < _correctSequence.size(); ++i) { bool found = false; for (uint j = 0; j < _clickedSequence.size(); ++j) { if (_correctSequence[i] == _clickedSequence[j]) { found = true; break; } } if (!found) { // Couldn't find one of the items in the correct sequence equal = false; break; } } } // Check the pressed sequence. If its length is above a certain number, // clear it and start anew if (!equal) { if (_puzzleType != kOrderItems) { uint maxNumPressed = 4; if (g_nancy->getGameType() > kGameTypeVampire) { if (_puzzleType == kKeypad || _puzzleType == kKeypadTerse) { maxNumPressed = _correctSequence.size(); } else { maxNumPressed = _correctSequence.size() + 1; } } if (_clickedSequence.size() > maxNumPressed) { clearAllElements(); return; } } else { // OrderItems has a slight delay, after which it actually clears if (_clickedSequence.size() == _correctSequence.size()) { if (_solveSoundPlayTime == 0) { _solveSoundPlayTime = g_nancy->getTotalPlayTime() + 500; } else { if (g_nancy->getTotalPlayTime() > _solveSoundPlayTime) { clearAllElements(); _solveSoundPlayTime = 0; return; } } } } solved = false; } } else { solved = false; } } else { // Piano puzzle checks only the last few elements if (_clickedSequence.size() < _correctSequence.size()) { return; } // Arbitrary number if (_clickedSequence.size() > 30) { _clickedSequence.erase(&_clickedSequence[0], &_clickedSequence[_clickedSequence.size() - _correctSequence.size()]); } for (uint i = 0; i < _correctSequence.size(); ++i) { if (_clickedSequence[_clickedSequence.size() - _correctSequence.size() + i] != (int16)_correctSequence[i]) { return; } } } if (_puzzleType == kKeypad && _needButtonToCheckSuccess) { // KeypadPuzzle moves to the "success" scene regardless whether the puzzle was solved or not, // provided the check button is pressed. if (_checkButtonPressed) { if (!g_nancy->_sound->isSoundPlaying(_pushDownSound)) { if (solved) { NancySceneState.setEventFlag(_solveExitScene._flag); } } else { return; } } else { return; } } else { if (solved) { if (_puzzleType == kOrderItems) { if (!g_nancy->_sound->isSoundPlaying(_pushDownSound)) { // Draw some overlays when solved correctly (OrderItems only) for (uint i = 0; i < _overlaySrcs.size(); ++i) { Common::Rect destRect = _overlayDests[i]; destRect.translate(-_screenPosition.left, -_screenPosition.top); _drawSurface.blitFrom(_image, _overlaySrcs[i], destRect); _needsRedraw = true; } } else { return; } } NancySceneState.setEventFlag(_solveExitScene._flag); } else { return; } } _solveSoundPlayTime = g_nancy->getTotalPlayTime() + _solveSoundDelay * 1000; _solveState = kPlaySound; } // fall through case kPlaySound: if (g_nancy->getTotalPlayTime() <= _solveSoundPlayTime) { break; } g_nancy->_sound->loadSound(_solveSound); g_nancy->_sound->playSound(_solveSound); _solveState = kWaitForSound; break; case kWaitForSound: if (!g_nancy->_sound->isSoundPlaying(_solveSound)) { _state = kActionTrigger; } break; } break; case kActionTrigger: if (g_nancy->getGameType() == kGameTypeVampire) { g_nancy->_sound->stopSound("BUOK"); } else { g_nancy->_sound->stopSound(_pushDownSound); } g_nancy->_sound->stopSound(_solveSound); if (_solveState == kNotSolved) { _exitScene.execute(); } else { NancySceneState.changeScene(_solveExitScene._sceneChange); } finishExecution(); break; } } void OrderingPuzzle::handleInput(NancyInput &input) { if (_solveState != kNotSolved) { return; } bool canClick = true; if ((_itemsStayDown || _puzzleType == kPiano) && g_nancy->_sound->isSoundPlaying(_pushDownSound)) { canClick = false; } if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) { g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor); if (canClick && input.input & NancyInput::kLeftMouseButtonUp) { _state = kActionTrigger; } return; } if (_needButtonToCheckSuccess && NancySceneState.getViewport().convertViewportToScreen(_checkButtonDest).contains(input.mousePos)) { g_nancy->_cursor->setCursorType(CursorManager::kHotspot); if (canClick && input.input & NancyInput::kLeftMouseButtonUp) { _checkButtonPressed = true; g_nancy->_sound->playSound(_pushDownSound); Common::Rect destRect = _checkButtonDest; destRect.translate(-_screenPosition.left, -_screenPosition.top); _drawSurface.blitFrom(_image, _checkButtonSrc, destRect); _needsRedraw = true; } } for (int i = 0; i < (int)_hotspots.size(); ++i) { if (NancySceneState.getViewport().convertViewportToScreen(_hotspots[i]).contains(input.mousePos)) { // Set the custom cursor for nancy8+ PianoPuzzle if (NancySceneState.getViewport().convertViewportToScreen(_specialCursor1Dest).contains(input.mousePos)) { g_nancy->_cursor->setCursorType((CursorManager::CursorType)_specialCursor1Id); } else if (NancySceneState.getViewport().convertViewportToScreen(_specialCursor2Dest).contains(input.mousePos)) { g_nancy->_cursor->setCursorType((CursorManager::CursorType)_specialCursor2Id); } else { g_nancy->_cursor->setCursorType(CursorManager::kHotspot); } if (canClick && input.input & NancyInput::kLeftMouseButtonUp) { if (_puzzleType == kOrderItems) { if (_itemsStayDown && _downItems[i]) { // Button is pressed, OrderItems does not allow for depressing return; } if (NancySceneState.getHeldItem() == _state2InvItem) { // We are holding the correct inventory, set the button to its alternate (dusted) state setToSecondState(i); return; } } if (_puzzleType == kPiano) { // Set the correct sound name for every piano key if (g_nancy->getGameType() <= kGameTypeNancy7) { // In earlier games, the sound name is the base sound + a number if (Common::isDigit(_pushDownSound.name.lastChar())) { _pushDownSound.name.deleteLastChar(); } _pushDownSound.name.insertChar('0' + i, _pushDownSound.name.size()); } else { // Later games added an array of sound names _pushDownSound.name = _pianoSoundNames[i]; } g_nancy->_sound->loadSound(_pushDownSound); } if (_puzzleType == kOrdering || _puzzleType == kKeypad || _puzzleType == kKeypadTerse) { // OrderingPuzzle and KeypadPuzzle allow for depressing buttons after they're pressed. // If the button is the last one the player pressed, it is removed from the order. // If not, the sequence is kept wrong and will be reset after enough buttons are pressed for (uint j = 0; j < _clickedSequence.size(); ++j) { if (_clickedSequence[j] == i && _downItems[i] == true) { popUp(i); if (_clickedSequence.back() == i) { _clickedSequence.pop_back(); } return; } } } _clickedSequence.push_back(i); pushDown(i); } return; } } } Common::String OrderingPuzzle::getRecordTypeName() const { switch (_puzzleType) { case kPiano: return "PianoPuzzle"; case kOrderItems: return "OrderItemsPuzzle"; case kKeypad: return "KeypadPuzzle"; case kKeypadTerse: return "KeypadTersePuzzle"; default: return "OrderingPuzzle"; } } void OrderingPuzzle::pushDown(uint id) { if (g_nancy->getGameType() == kGameTypeVampire) { g_nancy->_sound->playSound("BUOK"); } else { g_nancy->_sound->playSound(_pushDownSound); } _downItems[id] = true; Common::Rect destRect = _destRects[id]; destRect.translate(-_screenPosition.left, -_screenPosition.top); _drawSurface.blitFrom(_image, _secondStateItems[id] ? _down2Rects[id] : _down1Rects[id], destRect); _needsRedraw = true; } void OrderingPuzzle::setToSecondState(uint id) { g_nancy->_sound->playSound(_itemSound); _secondStateItems[id] = true; Common::Rect destRect = _destRects[id]; destRect.translate(-_screenPosition.left, -_screenPosition.top); _drawSurface.blitFrom(_image, _downItems[id] ? _down2Rects[id] : _up2Rects[id], destRect); _needsRedraw = true; } void OrderingPuzzle::popUp(uint id) { if (_itemsStayDown) { // Make sure we only play the sound when the buttons don't auto-depress if (g_nancy->getGameType() == kGameTypeVampire) { g_nancy->_sound->playSound("BUOK"); } else { if (_popUpSound.name.size()) { g_nancy->_sound->playSound(_popUpSound); } else { g_nancy->_sound->playSound(_pushDownSound); } } } _downItems[id] = false; Common::Rect destRect = _destRects[id]; destRect.translate(-_screenPosition.left, -_screenPosition.top); if (_secondStateItems[id] == false || _up2Rects.size() == 0) { _drawSurface.fillRect(destRect, _drawSurface.getTransparentColor()); } else { _drawSurface.blitFrom(_image, _up2Rects[id], destRect); } _needsRedraw = true; } void OrderingPuzzle::clearAllElements() { for (uint id = 0; id < _downItems.size(); ++id) { popUp(id); } _clickedSequence.clear(); return; } } // End of namespace Action } // End of namespace Nancy