Files
scummvm-cursorfix/engines/nancy/action/puzzle/orderingpuzzle.cpp
2026-02-02 04:50:13 +01:00

635 lines
18 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 "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