/* 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/nancy.h" #include "engines/nancy/graphics.h" #include "engines/nancy/resource.h" #include "engines/nancy/sound.h" #include "engines/nancy/input.h" #include "engines/nancy/util.h" #include "engines/nancy/state/scene.h" #include "engines/nancy/action/puzzle/collisionpuzzle.h" namespace Nancy { namespace Action { void CollisionPuzzle::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); _image.setTransparentColor(_drawSurface.getTransparentColor()); if (_puzzleType == kCollision) { _pieces.resize(_pieceSrcs.size(), Piece()); for (uint i = 0; i < _pieceSrcs.size(); ++i) { _pieces[i]._drawSurface.create(_image, _pieceSrcs[i]); Common::Rect pos = getScreenPosition(_startLocations[i]); if (_lineWidth == 6) { pos.translate(-1, 0); // Improvement } _pieces[i].moveTo(pos); _pieces[i]._gridPos = _startLocations[i]; _pieces[i].setVisible(true); _pieces[i].setTransparent(true); } } else { for (uint y = 0; y < _grid.size(); ++y) { for (uint x = 0; x < _grid[0].size(); ++x) { if (_grid[y][x] == 0) { continue; } Piece newPiece; uint id = _grid[y][x]; switch (id) { case 1 : newPiece._w = 2; break; case 2 : newPiece._h = 2; break; case 3 : newPiece._w = 3; break; case 4 : newPiece._h = 3; break; case 5 : newPiece._w = 2; newPiece._h = 2; break; case 6 : newPiece._w = 2; break; default : continue; } newPiece._drawSurface.create(_image, _pieceSrcs[id - 1]); Common::Rect pos = getScreenPosition(Common::Point(x, y)); if (_lineWidth == 6) { pos.translate(-1, 0); // Improvement } pos.setWidth(newPiece._drawSurface.w); pos.setHeight(newPiece._drawSurface.h); newPiece.moveTo(pos); newPiece._gridPos = Common::Point(x, y); newPiece.setVisible(true); newPiece.setTransparent(true); if (id == 6) { // The solve piece is pushed to the front _pieces.insert_at(0, newPiece); } else { _pieces.push_back(newPiece); } } } } if (_puzzleType == kCollision) { drawGrid(); } registerGraphics(); } void CollisionPuzzle::registerGraphics() { for (uint i = 0; i < _pieces.size(); ++i) { _pieces[i].registerGraphics(); } RenderActionRecord::registerGraphics(); } void CollisionPuzzle::updateGraphics() { if (_state == kRun) { if (_timerSrcs.size()) { uint32 currentTime = g_nancy->getTotalPlayTime() - _puzzleStartTime; int graphicForTime = currentTime / ((_timerTime * 1000) / _timerSrcs.size()); if (graphicForTime != _currentTimerGraphic) { _drawSurface.fillRect(_timerDest, _drawSurface.getTransparentColor()); _drawSurface.blitFrom(_image, _timerSrcs[graphicForTime], _timerDest); _needsRedraw = true; _currentTimerGraphic = graphicForTime; NancySceneState.setEventFlag(_timerFlagIds[graphicForTime], g_nancy->_true); } } if (_currentlyAnimating != -1) { // Framerate-dependent animation. Should be fine since we limit the engine to ~60fps ++_currentAnimFrame; bool horizontal = _lastPosition.x != _pieces[_currentlyAnimating]._gridPos.x; int diff = horizontal ? _lastPosition.x - _pieces[_currentlyAnimating]._gridPos.x : _lastPosition.y - _pieces[_currentlyAnimating]._gridPos.y; int maxFrames = _framesPerMove * abs(diff); if (_currentAnimFrame > maxFrames) { if (_puzzleType == kCollision && _grid[_pieces[_currentlyAnimating]._gridPos.y][_pieces[_currentlyAnimating]._gridPos.x] == _currentlyAnimating + 1) { g_nancy->_sound->playSound(_homeSound); } else { g_nancy->_sound->playSound(_wallHitSound); } _currentlyAnimating = -1; _currentAnimFrame = -1; return; } Common::Rect destRect = getScreenPosition(_lastPosition); Common::Rect endPos = getScreenPosition(_pieces[_currentlyAnimating]._gridPos); if (_lineWidth == 6) { destRect.translate(-1, 0); // Improvement endPos.translate(-1, 0); // Improvement } Common::Point dest(destRect.left, destRect.top); if (horizontal) { dest.x = destRect.left + (endPos.left - dest.x) * _currentAnimFrame / maxFrames; } else { dest.y = destRect.top + (endPos.top - dest.y) * _currentAnimFrame / maxFrames; } _pieces[_currentlyAnimating].moveTo(dest); } } } void CollisionPuzzle::readData(Common::SeekableReadStream &stream) { readFilename(stream, _imageName); uint16 numPieces = 0; uint16 width = stream.readUint16LE(); uint16 height = stream.readUint16LE(); if (_puzzleType == kCollision) { numPieces = stream.readUint16LE(); } else { _tileMoveExitPos.y = stream.readUint16LE(); _tileMoveExitPos.x = stream.readUint16LE(); _tileMoveExitSize = stream.readUint16LE(); numPieces = 6; } _grid.resize(height, Common::Array(width)); for (uint y = 0; y < height; ++y) { for (uint x = 0; x < width; ++x) { _grid[y][x] = stream.readUint16LE(); } stream.skip((8 - width) * 2); } stream.skip((8 - height) * 8 * 2); if (_puzzleType == kCollision) { _startLocations.resize(numPieces); for (uint i = 0; i < numPieces; ++i) { _startLocations[i].x = stream.readUint16LE(); _startLocations[i].y = stream.readUint16LE(); } stream.skip((5 - numPieces) * 4); readRectArray(stream, _pieceSrcs, numPieces, 5); readRectArray(stream, _homeSrcs, numPieces, 5); readRect(stream, _verticalWallSrc); readRect(stream, _horizontalWallSrc); readRect(stream, _blockSrc); } else { readRectArray(stream, _pieceSrcs, 6); if (g_nancy->getGameType() >= kGameTypeNancy8) { _usesExitButton = stream.readByte(); readRect(stream, _exitButtonSrc); readRect(stream, _exitButtonDest); } } _gridPos.x = stream.readUint32LE(); _gridPos.y = stream.readUint32LE(); _lineWidth = stream.readUint16LE(); _framesPerMove = stream.readUint16LE(); if (g_nancy->getGameType() <= kGameTypeNancy7) { stream.skip(3); } else if (_puzzleType == kTileMove) { uint16 numTimerGraphics = stream.readUint16LE(); _timerTime = stream.readUint32LE(); readRectArray(stream, _timerSrcs, numTimerGraphics, 10); _timerFlagIds.resize(numTimerGraphics); for (uint i = 0; i < numTimerGraphics; ++i) { _timerFlagIds[i] = stream.readSint16LE(); } stream.skip((10 - numTimerGraphics) * 2); readRect(stream, _timerDest); } _moveSound.readNormal(stream); if (_puzzleType == kCollision) { _homeSound.readNormal(stream); } _wallHitSound.readNormal(stream); if (_puzzleType == kTileMove && g_nancy->getGameType() >= kGameTypeNancy8) { _exitButtonSound.readNormal(stream); } _solveScene.readData(stream); _solveSoundDelay = stream.readUint16LE(); _solveSound.readNormal(stream); _exitScene.readData(stream); readRect(stream, _exitHotspot); } void CollisionPuzzle::execute() { switch (_state) { case kBegin : init(); g_nancy->_sound->loadSound(_moveSound); g_nancy->_sound->loadSound(_wallHitSound); g_nancy->_sound->loadSound(_homeSound); NancySceneState.setNoHeldItem(); _puzzleStartTime = g_nancy->getTotalPlayTime(); _state = kRun; // fall through case kRun : if (_currentlyAnimating != -1) { return; } // Check timer if (_timerSrcs.size()) { if ((g_nancy->getTotalPlayTime() - _puzzleStartTime) > _timerTime * 1000) { _state = kActionTrigger; return; } } if (_puzzleType == kCollision) { // Check if every tile is in its "home" for (uint i = 0; i < _pieces.size(); ++i) { if (_grid[_pieces[i]._gridPos.y][_pieces[i]._gridPos.x] != i + 1) { return; } } } else { // Check if either: // - the solve tile is over the exit or; // - the solve tile is outside the bounds of the grid (and is thus inside the exit) Common::Point pos = _pieces[0]._gridPos; Common::Rect posRect(pos.x, pos.y, pos.x + _pieces[0]._w, pos.y + _pieces[0]._h); Common::Rect gridRect(_grid.size(), _grid[0].size()); if (!posRect.contains(_tileMoveExitPos) && gridRect.contains(pos)) { return; } } _solveSoundPlayTime = g_nancy->getTotalPlayTime() + _solveSoundDelay * 1000; _state = kActionTrigger; _solved = true; return; case kActionTrigger : if (_solved) { if (_solveSoundPlayTime != 0) { if (g_nancy->getTotalPlayTime() < _solveSoundPlayTime) { return; } g_nancy->_sound->loadSound(_solveSound); g_nancy->_sound->playSound(_solveSound); NancySceneState.setEventFlag(_solveScene._flag); _solveSoundPlayTime = 0; return; } else { if (g_nancy->_sound->isSoundPlaying(_solveSound)) { return; } NancySceneState.changeScene(_solveScene._sceneChange); } } else { if (g_nancy->_sound->isSoundPlaying(_exitButtonSound)) { return; } _exitScene.execute(); } g_nancy->_sound->stopSound(_solveSound); g_nancy->_sound->stopSound(_moveSound); g_nancy->_sound->stopSound(_wallHitSound); g_nancy->_sound->stopSound(_homeSound); finishExecution(); } } Common::Point CollisionPuzzle::movePiece(uint pieceID, WallType direction) { Common::Point newPos = _pieces[pieceID]._gridPos; bool done = false; uint preStopWallType = 0; uint postStopWallType = 0; int inc = 0; bool horizontal = false; switch (direction) { case kWallLeft : preStopWallType = kWallRight; postStopWallType = kWallLeft; inc = -1; horizontal = true; break; case kWallRight : preStopWallType = kWallLeft; postStopWallType = kWallRight; inc = 1; horizontal = true; break; case kWallUp : preStopWallType = kWallDown; postStopWallType = kWallUp; inc = -1; horizontal = false; break; case kWallDown : preStopWallType = kWallUp; postStopWallType = kWallDown; inc = 1; horizontal = false; break; default: return { -1, -1 }; } // Set the last possible position to check before the piece would be out of bounds int lastPos = inc > 0 ? (horizontal ? (int)_grid[0].size() : (int)_grid.size()) : -1; if (lastPos != -1) { // For TileMove, ensure wider pieces won't clip out lastPos -= inc * ((horizontal ? _pieces[pieceID]._w : _pieces[pieceID]._h) - 1); } for (int i = (horizontal ? newPos.x : newPos.y) + inc; (inc > 0 ? i < lastPos : i > lastPos); i += inc) { // First, check if other pieces would block Common::Point comparePos = newPos; if (horizontal) { comparePos.x = i; } else { comparePos.y = i; } Common::Rect compareRect(comparePos.x, comparePos.y, comparePos.x + _pieces[pieceID]._w, comparePos.y + _pieces[pieceID]._h); for (uint j = 0; j < _pieces.size(); ++j) { if (pieceID == j) { continue; } Common::Rect pieceBounds( _pieces[j]._gridPos.x, _pieces[j]._gridPos.y, _pieces[j]._gridPos.x + _pieces[j]._w, _pieces[j]._gridPos.y + _pieces[j]._h); if (pieceBounds.intersects(compareRect)) { done = true; break; } } if (done) { break; } if (_puzzleType == kCollision) { // Next, check the grid for blocking walls uint16 evalVal = horizontal ? _grid[newPos.y][i] : _grid[i][newPos.x]; if (evalVal == postStopWallType) { if (horizontal) { newPos.x = i; } else { newPos.y = i; } break; } else if (evalVal == preStopWallType || evalVal == kBlock) { break; } } if (horizontal) { newPos.x = i; } else { newPos.y = i; } } // Move result outside of grid when the exit is at an edge, and the moved piece is on top of the exit if (_puzzleType == kTileMove && pieceID == 0) { Common::Rect compareRect(newPos.x, newPos.y, newPos.x + _pieces[pieceID]._w, newPos.y + _pieces[pieceID]._h); if (compareRect.contains(_tileMoveExitPos)) { if (horizontal && (_tileMoveExitPos.x == 0 || _tileMoveExitPos.x == (int)_grid[0].size() - 1)) { newPos.x += inc * _tileMoveExitSize; } else if (!horizontal && (_tileMoveExitPos.y == 0 || _tileMoveExitPos.y == (int)_grid.size() - 1)) { newPos.y += inc * _tileMoveExitSize; } } } return newPos; } Common::Rect CollisionPuzzle::getScreenPosition(Common::Point gridPos) { Common::Rect dest = _pieceSrcs[0]; dest.moveTo(0, 0); dest.right -= 1; dest.bottom -= 1; if (_puzzleType == kTileMove) { dest.setWidth(dest.width() / 2); } dest.moveTo(_gridPos); dest.translate(gridPos.x * dest.width(), gridPos.y *dest.height()); dest.translate(gridPos.x * _lineWidth, gridPos.y * _lineWidth); dest.right += 1; dest.bottom += 1; return dest; } void CollisionPuzzle::drawGrid() { // Improvement: original rendering does not line up with the grid on either difficulty, but ours does // The differences are marked below for (uint y = 0; y < _grid.size(); ++y) { for (uint x = 0; x < _grid[y].size(); ++x) { uint16 cell = _grid[y][x]; Common::Rect cellRect = getScreenPosition(Common::Point(x, y)); Common::Point dest(cellRect.left, cellRect.top); switch (cell) { case kBlock : if (_lineWidth != 6) { // Improvement dest.x += 1; dest.y += 1; } _drawSurface.blitFrom(_image, _blockSrc, dest); break; case kWallLeft : dest.x -= _lineWidth - _lineWidth / 6; dest.y = cellRect.top + (cellRect.height() - _verticalWallSrc.height()) / 2; _drawSurface.blitFrom(_image, _verticalWallSrc, dest); break; case kWallRight : dest.x = cellRect.right - 1 + _lineWidth / 6; dest.y = cellRect.top + (cellRect.height() - _verticalWallSrc.height()) / 2; _drawSurface.blitFrom(_image, _verticalWallSrc, dest); break; case kWallUp : dest.x += (cellRect.width() - _horizontalWallSrc.width()) / 2; dest.y -= _lineWidth - _lineWidth / 6; _drawSurface.blitFrom(_image, _horizontalWallSrc, dest); break; case kWallDown : dest.x += (cellRect.width() - _horizontalWallSrc.width()) / 2; dest.y = cellRect.bottom - 1 + _lineWidth / 6; if (_lineWidth != 6) { // Improvement ++dest.y; } _drawSurface.blitFrom(_image, _horizontalWallSrc, dest); break; default : if (cell == 0) { continue; } if (_lineWidth == 6) { // Improvement dest.x -= 1; } else { dest.x += 1; dest.y += 1; } _drawSurface.blitFrom(_image, _homeSrcs[cell - 1], dest); } } } _needsRedraw = true; } void CollisionPuzzle::handleInput(NancyInput &input) { if (_state != kRun) { return; } if (_usesExitButton) { if (NancySceneState.getViewport().convertViewportToScreen(_exitButtonDest).contains(input.mousePos)) { g_nancy->_cursor->setCursorType(CursorManager::kHotspot); if (input.input & NancyInput::kLeftMouseButtonUp) { _drawSurface.blitFrom(_image, _exitButtonSrc, _exitButtonDest); _needsRedraw = true; g_nancy->_sound->loadSound(_exitButtonSound); g_nancy->_sound->playSound(_exitButtonSound); _state = kActionTrigger; } return; } } else { if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) { g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor); if (input.input & NancyInput::kLeftMouseButtonUp) { _state = kActionTrigger; } return; } } if (_currentlyAnimating != -1) { return; } for (uint i = 0; i < _pieces.size(); ++i) { Common::Point checkPos; Common::Rect left, right, up, down; Common::Rect screenPos = _pieces[i].getScreenPosition(); if (_pieces[i]._w == _pieces[i]._h) { // Width == height, all movement is permitted, hotspots are 10 pixels wide left.setWidth(10); left.setHeight(screenPos.height() - 20); left.moveTo(screenPos.left, screenPos.top + 10); right = left; right.translate(screenPos.width() - 10, 0); up.setHeight(10); up.setWidth(screenPos.width() - 20); up.moveTo(screenPos.left + 10, screenPos.top); down = up; down.translate(0, screenPos.height() - 10); } else if (_pieces[i]._w > _pieces[i]._h) { // Width > height, only left/right movement is permitted, hotspots are the size of 1 cell left.setWidth(screenPos.width() / _pieces[i]._w); left.setHeight(screenPos.height() / _pieces[i]._h); left.moveTo(screenPos.left, screenPos.top); right = left; right.translate(right.width() * (_pieces[i]._w - 1), 0); } else { // Width < height, only up/down movement is permitted, hotspots are the size of 1 cell up.setWidth(screenPos.width() / _pieces[i]._w); up.setHeight(screenPos.height() / _pieces[i]._h); up.moveTo(screenPos.left, screenPos.top); down = up; down.translate(0, down.height() * (_pieces[i]._h - 1)); } if (!left.isEmpty()) { if (left.contains(input.mousePos)) { checkPos = movePiece(i, kWallLeft); if (checkPos != _pieces[i]._gridPos) { g_nancy->_cursor->setCursorType(CursorManager::kMoveLeft); if (input.input & NancyInput::kLeftMouseButtonUp) { _lastPosition = _pieces[i]._gridPos; _pieces[i]._gridPos = checkPos; _currentlyAnimating = i; g_nancy->_sound->playSound(_moveSound); } return; } } } if (!right.isEmpty()) { if (right.contains(input.mousePos)) { checkPos = movePiece(i, kWallRight); if (checkPos != _pieces[i]._gridPos) { g_nancy->_cursor->setCursorType(CursorManager::kMoveRight); if (input.input & NancyInput::kLeftMouseButtonUp) { _lastPosition = _pieces[i]._gridPos; _pieces[i]._gridPos = checkPos; _currentlyAnimating = i; g_nancy->_sound->playSound(_moveSound); } return; } } } if (!up.isEmpty()) { if (up.contains(input.mousePos)) { checkPos = movePiece(i, kWallUp); if (checkPos != _pieces[i]._gridPos) { g_nancy->_cursor->setCursorType(CursorManager::kMoveUp); if (input.input & NancyInput::kLeftMouseButtonUp) { _lastPosition = _pieces[i]._gridPos; _pieces[i]._gridPos = checkPos; _currentlyAnimating = i; g_nancy->_sound->playSound(_moveSound); } return; } } } if (!down.isEmpty()) { if (down.contains(input.mousePos)) { checkPos = movePiece(i, kWallDown); if (checkPos != _pieces[i]._gridPos) { g_nancy->_cursor->setCursorType(CursorManager::kMoveDown); if (input.input & NancyInput::kLeftMouseButtonUp) { _lastPosition = _pieces[i]._gridPos; _pieces[i]._gridPos = checkPos; _currentlyAnimating = i; g_nancy->_sound->playSound(_moveSound); } return; } } } } } } // End of namespace Action } // End of namespace Nancy