[Scummvm-git-logs] scummvm master -> 5b3b4c985fdfa13f923a9714b9acc77ead6d2f6e
bluegr
noreply at scummvm.org
Wed Mar 18 07:16:37 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
b5a6f7b16b NANCY: Remove unused variable
5d9155fa3c NANCY: Fix include order
5b3b4c985f NANCY: Implement onebuildpuzzle for Nancy 9
Commit: b5a6f7b16bafcf9173b9a521aa72946bf2e70509
https://github.com/scummvm/scummvm/commit/b5a6f7b16bafcf9173b9a521aa72946bf2e70509
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-18T04:24:15+02:00
Commit Message:
NANCY: Remove unused variable
Changed paths:
engines/nancy/action/puzzle/arcadepuzzle.cpp
engines/nancy/action/puzzle/arcadepuzzle.h
diff --git a/engines/nancy/action/puzzle/arcadepuzzle.cpp b/engines/nancy/action/puzzle/arcadepuzzle.cpp
index 86d342de7d0..d64c61860fc 100644
--- a/engines/nancy/action/puzzle/arcadepuzzle.cpp
+++ b/engines/nancy/action/puzzle/arcadepuzzle.cpp
@@ -135,7 +135,7 @@ void ArcadePuzzle::readData(Common::SeekableReadStream &stream) {
_scoreStepSize = stream.readSint32LE(); // +0x305
_timeBonusMax = stream.readSint32LE(); // +0x309
_timeLimitSec = stream.readSint32LE(); // +0x30d
- _scoreParam4 = stream.readSint32LE(); // +0x311
+ stream.skip(4); // +0x311 (unused score param)
for (int i = 0; i < 6; ++i) // 6Ã49 = 294 bytes, +0x315..+0x43a
_sounds[i].readNormal(stream);
diff --git a/engines/nancy/action/puzzle/arcadepuzzle.h b/engines/nancy/action/puzzle/arcadepuzzle.h
index 79baa3303f7..bf63634151c 100644
--- a/engines/nancy/action/puzzle/arcadepuzzle.h
+++ b/engines/nancy/action/puzzle/arcadepuzzle.h
@@ -147,7 +147,6 @@ protected:
int32 _scoreStepSize = 1; // score per brick tier (0x305)
int32 _timeBonusMax = 100; // max time bonus (0x309)
int32 _timeLimitSec = 60; // time limit in seconds (0x30d)
- int32 _scoreParam4 = 0; // extra score param (0x311)
// Sounds: 6 permanent + 3 dynamic
SoundDescription _sounds[6]; // 0x315..0x43a (bounce, launch, brick hit A/B/C, wall)
Commit: 5d9155fa3c143e7f0dc478011ca8187dad7a9c98
https://github.com/scummvm/scummvm/commit/5d9155fa3c143e7f0dc478011ca8187dad7a9c98
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-18T09:04:16+02:00
Commit Message:
NANCY: Fix include order
Changed paths:
engines/nancy/action/puzzle/arcadepuzzle.cpp
diff --git a/engines/nancy/action/puzzle/arcadepuzzle.cpp b/engines/nancy/action/puzzle/arcadepuzzle.cpp
index d64c61860fc..dcb5f0e3c80 100644
--- a/engines/nancy/action/puzzle/arcadepuzzle.cpp
+++ b/engines/nancy/action/puzzle/arcadepuzzle.cpp
@@ -19,6 +19,9 @@
*
*/
+#include "common/system.h"
+#include "common/random.h"
+
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
@@ -29,9 +32,6 @@
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/arcadepuzzle.h"
-#include "common/system.h"
-#include "common/random.h"
-
namespace Nancy {
namespace Action {
Commit: 5b3b4c985fdfa13f923a9714b9acc77ead6d2f6e
https://github.com/scummvm/scummvm/commit/5b3b4c985fdfa13f923a9714b9acc77ead6d2f6e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-18T09:06:17+02:00
Commit Message:
NANCY: Implement onebuildpuzzle for Nancy 9
This is a drag-and-drop assembly puzzle used in Nancy Drew: Danger
on Deception Island.
Pieces can be rotated and must be placed in their correct slots.
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index c6878336146..daa6e09f128 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -19,6 +19,9 @@
*
*/
+#include "common/random.h"
+#include "common/system.h"
+
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
@@ -26,6 +29,7 @@
#include "engines/nancy/input.h"
#include "engines/nancy/util.h"
+#include "engines/nancy/enginedata.h"
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/onebuildpuzzle.h"
@@ -33,46 +37,499 @@ namespace Nancy {
namespace Action {
void OneBuildPuzzle::init() {
- // TODO
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &p = _pieces[i];
+ int w = p.srcRect.width();
+ int h = p.srcRect.height();
+
+ // Rotation 0: blit from source image
+ p.rotateSurfaces[0].create(w, h, _image.format);
+ p.rotateSurfaces[0].setTransparentColor(_drawSurface.getTransparentColor());
+ p.rotateSurfaces[0].blitFrom(_image, p.srcRect, Common::Point(0, 0));
+ p.hasSurface[0] = true;
+
+ // Rotations 1-3: only needed if pieces can rotate
+ if (_canRotateAll || p.isPreRotated) {
+ for (int r = 1; r < 4; ++r) {
+ rotateSurface90CW(p.rotateSurfaces[r - 1], p.rotateSurfaces[r]);
+ p.rotateSurfaces[r].setTransparentColor(_drawSurface.getTransparentColor());
+ p.hasSurface[r] = true;
+ }
+ }
+
+ // Initial position and rotation
+ if (p.isPreRotated) {
+ // Pre-rotated pieces start at their slot and are already placed
+ p.curRotation = 0;
+ p.gameRect = p.slotRect;
+ p.placed = true;
+ } else {
+ // Normal pieces start at home with defaultRotation
+ p.curRotation = p.defaultRotation;
+ p.gameRect = p.homeRect;
+ p.placed = false;
+ }
+
+ updatePieceRender(i);
+ p.setVisible(true);
+ p.setTransparent(true);
+ p.setZ(_z + (uint16)i + 1);
+ }
+}
+
+void OneBuildPuzzle::registerGraphics() {
+ for (uint i = 0; i < _pieces.size(); ++i)
+ _pieces[i].registerGraphics();
+}
+
+void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName);
+
+ _numPieces = stream.readUint16LE();
+ _freePlacement = stream.readByte();
+ _canRotateAll = stream.readByte();
+ stream.skip(6); // rotationMode, zoneHeight, zoneWidth, mouse-clamping flag
+ _slotTolerance = stream.readSint16LE();
+ _orderedPlacement = stream.readByte();
+
+ _placementOrder.resize(20);
+ for (uint i = 0; i < 20; ++i)
+ _placementOrder[i] = stream.readSint16LE();
+
+ _pieces.resize(_numPieces);
+ for (uint i = 0; i < 20; ++i) {
+ if (i < _numPieces) {
+ Piece &p = _pieces[i];
+ readRect(stream, p.srcRect);
+ readRect(stream, p.slotRect);
+ readRect(stream, p.homeRect);
+ p.defaultRotation = stream.readByte();
+ p.isPreRotated = stream.readByte();
+ } else {
+ stream.skip(50);
+ }
+ }
+
+ _pickupSound.readNormal(stream); // +0x43e: played when rotating a placed piece
+ _rotateSound.readNormal(stream); // +0x46f: played when picking up an unplaced piece
+ _dropSound.readNormal(stream); // +0x4a0: played when dropping a piece
+ readFilename(stream, _dropAlt1Filename);
+ readFilename(stream, _dropAlt2Filename);
+
+ _goodPlacementSound.readNormal(stream); // +0x513
+ readFilename(stream, _goodAlt1Filename);
+ readFilename(stream, _goodAlt2Filename);
+
+ _goodTexts.resize(3);
+ Common::String unusedKey;
+ for (uint i = 0; i < 3; ++i)
+ readFilename(stream, unusedKey);
+ char textBuf[200];
+ for (uint i = 0; i < 3; ++i) {
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _goodTexts[i], 200);
+ }
+
+ _badPlacementSound.readNormal(stream); // +0x841
+ readFilename(stream, _badAlt1Filename);
+ readFilename(stream, _badAlt2Filename);
+
+ _badTexts.resize(3);
+ for (uint i = 0; i < 3; ++i)
+ readFilename(stream, unusedKey);
+ for (uint i = 0; i < 3; ++i) {
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _badTexts[i], 200);
+ }
+
+ stream.skip(4); // unknown bytes at +0xb6f
+ _solveScene.readData(stream);
+ _completionSound.readNormal(stream);
+ readFilename(stream, unusedKey);
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _completionText, 200);
+
+ _cancelScene.readData(stream);
+ readRect(stream, _exitHotspot);
}
void OneBuildPuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+ g_nancy->_sound->loadSound(_pickupSound);
+ g_nancy->_sound->loadSound(_rotateSound);
+ g_nancy->_sound->loadSound(_dropSound);
+ g_nancy->_sound->loadSound(_goodPlacementSound);
+ g_nancy->_sound->loadSound(_badPlacementSound);
+ g_nancy->_sound->loadSound(_completionSound);
_state = kRun;
+ // fall through
+ case kRun:
+ switch (_solveState) {
+ case kIdle:
+ // Normal interaction; handleInput drives piece movement
+ break;
+ case kWaitTimer:
+ // Post-drop/pickup delay (300ms) before deciding outcome
+ if (g_system->getMillis() >= _timerEnd) {
+ g_nancy->_sound->stopSound(_currentSound);
+ if (!_isDropSound) {
+ // Pickup/rotate sound finished; return to idle (piece still dragging)
+ _solveState = kIdle;
+ } else if (_correctlyPlaced) {
+ playGoodPlacementSound();
+ checkAllPlaced();
+ } else {
+ // Wrong drop: play bad placement feedback
+ playBadPlacementSound();
+ }
+ }
+ break;
+ case kWaitPlaceSound:
+ // Waiting for good/bad placement sound to finish OR 1s minimum display time
+ if (!g_nancy->_sound->isSoundPlaying(_currentSound) || g_system->getMillis() >= _timerEnd) {
+ g_nancy->_sound->stopSound(_currentSound);
+ NancySceneState.getTextbox().clear();
+ _solveState = kIdle;
+ }
+ break;
+ case kWaitCompletion:
+ // Waiting for completion sound to finish before scene change
+ if (!g_nancy->_sound->isSoundPlaying(_completionSound)) {
+ _state = kActionTrigger;
+ }
+ break;
+ case kTriggerCompletion:
+ // Play completion sound/text, then wait for it to finish
+ g_nancy->_sound->playSound(_completionSound);
+ if (!_completionText.empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(_completionText);
+ }
+ _solveState = kWaitCompletion;
+ break;
+ }
+ break;
+ case kActionTrigger:
+ if (_isCancelled) {
+ _cancelScene.execute();
+ } else {
+ NancySceneState.setEventFlag(_solveScene._flag);
+ NancySceneState.changeScene(_solveScene._sceneChange);
+ }
+ break;
+ }
+}
+
+void OneBuildPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _isSolved || _isCancelled)
+ return;
+
+ const VIEW *viewData = GetEngineData(VIEW);
+ if (!viewData)
+ return;
+ Common::Rect vpScreen = viewData->screenPosition;
+ if (!vpScreen.contains(input.mousePos))
+ return;
+
+ Common::Point mouseVP(input.mousePos.x - vpScreen.left,
+ input.mousePos.y - vpScreen.top);
+
+ if (_isDragging) {
+ // Always update drag position while carrying a piece
+ updateDragPosition(mouseVP);
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+ if (_solveState != kIdle)
+ return;
+
+ // Right click while dragging: rotate the carried piece
+ if (input.input & NancyInput::kRightMouseButtonUp) {
+ rotatePiece(_pickedUpPiece);
+ Piece &pp = _pieces[_pickedUpPiece];
+ _pickedUpWidth = pp.rotateSurfaces[pp.curRotation].w;
+ _pickedUpHeight = pp.rotateSurfaces[pp.curRotation].h;
+ playPickupSound();
+ return;
+ }
+
+ // Left click while dragging: attempt to place
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ Piece &piece = _pieces[_pickedUpPiece];
+
+ Common::Rect slot = piece.slotRect;
+
+ // Correct placement: bounding-box must fit within slot +- tolerance
+ // (piece is rotation 0, same size as slot)
+ bool nearSlot = (piece.gameRect.left >= slot.left - _slotTolerance &&
+ piece.gameRect.top >= slot.top - _slotTolerance &&
+ piece.gameRect.right <= slot.right + _slotTolerance &&
+ piece.gameRect.bottom <= slot.bottom + _slotTolerance);
+
+ bool correctRotation = (piece.curRotation == 0);
+ bool orderOk = !_orderedPlacement ||
+ (_piecesPlaced < (uint16)_placementOrder.size() &&
+ _placementOrder[_piecesPlaced] == (int16)(_pickedUpPiece + 1));
+
+ if (nearSlot && correctRotation && orderOk) {
+ piece.gameRect = piece.slotRect;
+ piece.placed = true;
+ _correctlyPlaced = true;
+ ++_piecesPlaced;
+ } else {
+ _correctlyPlaced = false;
+ if (!_freePlacement) {
+ piece.gameRect = _prevDragGameRect;
+ } else {
+ piece.curRotation = piece.defaultRotation;
+ piece.gameRect = piece.homeRect;
+ }
+ }
+
+ updatePieceRender(_pickedUpPiece);
+ _isDragging = false;
+ _pickedUpPiece = -1;
+ playDropSound();
+ }
+ return;
}
- // TODO
- const uint16 sceneId = NancySceneState.getSceneInfo().sceneID;
- SceneChangeDescription scene;
+ // Not dragging: only process when idle
+ if (_solveState != kIdle)
+ return;
+
+ // Find topmost piece under cursor (separately tracking unplaced vs any)
+ int16 topmostUnplaced = -1;
+ int16 topmostAny = -1;
- if (sceneId == 6519) {
- // Stub - move to the winning screen
- warning("STUB - Nancy 9 Pipe joining puzzle under sink");
- NancySceneState.setEventFlag(425, g_nancy->_true); // EV_Solved_Pipes
- scene.sceneID = 6520;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
- } else if (sceneId == 2916) {
- // Stub - move to the winning screen
- warning("STUB - Nancy 9 Carborosaurus Puzzle");
- NancySceneState.setEventFlag(424, g_nancy->_true); // EV_Solved_Permit_Task
- scene.sceneID = 2915;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
- } else {
- warning("STUB - Nancy 9 One Build Puzzle");
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &p = _pieces[i];
+ if (!p.gameRect.contains(mouseVP))
+ continue;
+ if (topmostAny == -1 || p.getZOrder() > _pieces[topmostAny].getZOrder())
+ topmostAny = (int16)i;
+ if (!p.placed) {
+ if (topmostUnplaced == -1 || p.getZOrder() > _pieces[topmostUnplaced].getZOrder())
+ topmostUnplaced = (int16)i;
+ }
+ }
+
+ if (topmostAny != -1) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+ // Right click: rotate topmost piece under cursor
+ if (input.input & NancyInput::kRightMouseButtonUp) {
+ rotatePiece(topmostAny);
+ playPickupSound();
+ return;
+ }
+
+ // Left click on an unplaced piece: pick it up
+ if ((input.input & NancyInput::kLeftMouseButtonUp) && topmostUnplaced != -1) {
+ Piece &pp = _pieces[topmostUnplaced];
+ _pickedUpPiece = topmostUnplaced;
+ _isDragging = true;
+ _pickedUpWidth = pp.rotateSurfaces[pp.curRotation].w;
+ _pickedUpHeight = pp.rotateSurfaces[pp.curRotation].h;
+ pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.registerGraphics();
+ playRotateSoundAndStartTimer();
+ }
+ return;
+ }
+
+ // Check exit hotspot
+ Common::Rect exitScreen = NancySceneState.getViewport().convertViewportToScreen(_exitHotspot);
+ if (exitScreen.contains(input.mousePos)) {
+ g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _isCancelled = true;
+ _state = kActionTrigger;
+ }
}
}
-void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+// --- Internal helpers ---
+
+void OneBuildPuzzle::updatePieceRender(int pieceIdx) {
+ Piece &p = _pieces[pieceIdx];
+ int rot = p.curRotation;
+ if (!p.hasSurface[rot])
+ rot = 0;
+ if (!p.hasSurface[rot])
+ return;
+ p._drawSurface.create(p.rotateSurfaces[rot], p.rotateSurfaces[rot].getBounds());
+ p.setTransparent(true);
+ p.moveTo(p.gameRect);
}
-void OneBuildPuzzle::handleInput(NancyInput &input) {
- // TODO
+void OneBuildPuzzle::rotatePiece(int pieceIdx) {
+ Piece &p = _pieces[pieceIdx];
+ int oldRot = p.curRotation;
+ int oldW = p.rotateSurfaces[oldRot].w;
+ int oldH = p.rotateSurfaces[oldRot].h;
+
+ int newRot = (oldRot + 1) % 4;
+ p.curRotation = newRot;
+
+ int newW = p.hasSurface[newRot] ? p.rotateSurfaces[newRot].w : oldW;
+ int newH = p.hasSurface[newRot] ? p.rotateSurfaces[newRot].h : oldH;
+
+ // Preserve center point when changing dimensions
+ int cx = p.gameRect.left + oldW / 2;
+ int cy = p.gameRect.top + oldH / 2;
+ p.gameRect.left = cx - newW / 2;
+ p.gameRect.top = cy - newH / 2;
+ p.gameRect.right = p.gameRect.left + newW;
+ p.gameRect.bottom = p.gameRect.top + newH;
+
+ clampRectToViewport(p.gameRect);
+ updatePieceRender(pieceIdx);
+}
+
+void OneBuildPuzzle::updateDragPosition(Common::Point mouseVP) {
+ if (_pickedUpPiece == -1)
+ return;
+
+ Piece &p = _pieces[_pickedUpPiece];
+
+ // Save current position as "previous" before updating (for freePlacement restore)
+ _prevDragGameRect = p.gameRect;
+
+ int newLeft = mouseVP.x - _pickedUpWidth / 2;
+ int newTop = mouseVP.y - _pickedUpHeight / 2;
+
+ p.gameRect.left = newLeft;
+ p.gameRect.top = newTop;
+ p.gameRect.right = newLeft + _pickedUpWidth;
+ p.gameRect.bottom = newTop + _pickedUpHeight;
+
+ clampRectToViewport(p.gameRect);
+ updatePieceRender(_pickedUpPiece);
+}
+
+void OneBuildPuzzle::clampRectToViewport(Common::Rect &rect) {
+ const VIEW *viewData = GetEngineData(VIEW);
+ if (!viewData)
+ return;
+ int vpW = viewData->screenPosition.width();
+ int vpH = viewData->screenPosition.height();
+ int w = rect.width();
+ int h = rect.height();
+
+ if (rect.top < 0) {
+ rect.top = 0;
+ rect.bottom = h;
+ }
+ if (rect.bottom > vpH) {
+ rect.bottom = vpH;
+ rect.top = vpH - h;
+ }
+ if (rect.left < 0) {
+ rect.left = 0;
+ rect.right = w;
+ }
+ if (rect.right > vpW) {
+ rect.right = vpW;
+ rect.left = vpW - w;
+ }
+}
+
+void OneBuildPuzzle::checkAllPlaced() {
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if (!_pieces[i].placed)
+ return;
+ }
+ _isSolved = true;
+ _solveState = kTriggerCompletion;
+}
+
+void OneBuildPuzzle::playPickupSound() {
+ _currentSound = _pickupSound;
+ g_nancy->_sound->loadSound(_currentSound);
+ g_nancy->_sound->playSound(_currentSound);
+ _timerEnd = g_system->getMillis() + 300;
+ _isDropSound = false;
+}
+
+void OneBuildPuzzle::playRotateSoundAndStartTimer() {
+ _currentSound = _rotateSound;
+ g_nancy->_sound->loadSound(_currentSound);
+ g_nancy->_sound->playSound(_currentSound);
+ _solveState = kWaitTimer;
+ _timerEnd = g_system->getMillis() + 300;
+ _isDropSound = false;
+}
+
+void OneBuildPuzzle::playDropSound() {
+ _currentSound = _dropSound;
+ int roll = g_nancy->_randomSource->getRandomNumber(2);
+ if (roll == 1 && _dropAlt1Filename != "NO SOUND" && !_dropAlt1Filename.empty())
+ _currentSound.name = _dropAlt1Filename;
+ else if (roll == 2 && _dropAlt2Filename != "NO SOUND" && !_dropAlt2Filename.empty())
+ _currentSound.name = _dropAlt2Filename;
+ g_nancy->_sound->loadSound(_currentSound);
+ g_nancy->_sound->playSound(_currentSound);
+ _solveState = kWaitTimer;
+ _timerEnd = g_system->getMillis() + 300;
+ _isDropSound = true;
+}
+
+void OneBuildPuzzle::playGoodPlacementSound() {
+ int idx = g_nancy->_randomSource->getRandomNumber(2);
+ _currentSound = _goodPlacementSound;
+ if (idx == 1 && _goodAlt1Filename != "NO SOUND" && !_goodAlt1Filename.empty())
+ _currentSound.name = _goodAlt1Filename;
+ else if (idx == 2 && _goodAlt2Filename != "NO SOUND" && !_goodAlt2Filename.empty())
+ _currentSound.name = _goodAlt2Filename;
+ else
+ idx = 0;
+ g_nancy->_sound->loadSound(_currentSound);
+ g_nancy->_sound->playSound(_currentSound);
+ if (!_goodTexts[idx].empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(_goodTexts[idx]);
+ }
+ _solveState = kWaitPlaceSound;
+ _timerEnd = g_system->getMillis() + 1000;
+}
+
+void OneBuildPuzzle::playBadPlacementSound() {
+ int idx = g_nancy->_randomSource->getRandomNumber(2);
+ _currentSound = _badPlacementSound;
+ if (idx == 1 && _badAlt1Filename != "NO SOUND" && !_badAlt1Filename.empty())
+ _currentSound.name = _badAlt1Filename;
+ else if (idx == 2 && _badAlt2Filename != "NO SOUND" && !_badAlt2Filename.empty())
+ _currentSound.name = _badAlt2Filename;
+ else
+ idx = 0;
+ g_nancy->_sound->loadSound(_currentSound);
+ g_nancy->_sound->playSound(_currentSound);
+ if (!_badTexts[idx].empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(_badTexts[idx]);
+ }
+ _solveState = kWaitPlaceSound;
+ _timerEnd = g_system->getMillis() + 1000;
+}
+
+// static
+void OneBuildPuzzle::rotateSurface90CW(const Graphics::ManagedSurface &src, Graphics::ManagedSurface &dst) {
+ int srcW = src.w;
+ int srcH = src.h;
+ dst.create(srcH, srcW, src.format);
+
+ for (int y = 0; y < srcH; ++y) {
+ for (int x = 0; x < srcW; ++x) {
+ uint32 pixel = src.getPixel(x, y);
+ dst.setPixel(srcH - 1 - y, x, pixel);
+ }
+ }
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index 191180f15e6..4cfa69a7f42 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -23,18 +23,23 @@
#define NANCY_ACTION_ONEBUILDPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/renderobject.h"
namespace Nancy {
namespace Action {
-// Pipe joining puzzle under sink in Nancy 9
-
+// Drag-and-drop assembly puzzle used in Nancy Drew: Danger on Deception Island (Nancy 9).
+// Pieces can be rotated and must be placed in their correct slots.
+// Left-clicking an unplaced piece picks it up; right-clicking any piece rotates it.
+// If a piece is dropped (left-click) near its correct slot with correct rotation, it snaps in.
+// Otherwise it returns to its previous position (or home in free placement mode).
class OneBuildPuzzle : public RenderActionRecord {
public:
OneBuildPuzzle() : RenderActionRecord(7) {}
virtual ~OneBuildPuzzle() {}
void init() override;
+ void registerGraphics() override;
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
@@ -43,6 +48,114 @@ public:
protected:
Common::String getRecordTypeName() const override { return "OneBuildPuzzle"; }
bool isViewportRelative() const override { return true; }
+
+ struct Piece : RenderObject {
+ Piece() : RenderObject(0) {}
+
+ // File data
+ Common::Rect srcRect; // Source rect in source image
+ Common::Rect slotRect; // Correct placement rect (viewport coords)
+ Common::Rect homeRect; // Starting position (viewport coords)
+ uint8 defaultRotation = 0; // Rotation index that fits the slot
+ bool isPreRotated = false; // Piece starts already in place (slotRect position)
+
+ // Runtime
+ Common::Rect gameRect; // Current viewport-space rect
+ int curRotation = 0;
+ bool placed = false;
+
+ // Up to 4 rotation surfaces (rotation 1-3 only exist if canRotateAll or isPreRotated)
+ Graphics::ManagedSurface rotateSurfaces[4];
+ bool hasSurface[4] = {};
+
+ void setZ(uint16 z) { _z = z; _needsRedraw = true; }
+
+ protected:
+ bool isViewportRelative() const override { return true; }
+ };
+
+ // --- File data ---
+
+ Common::Path _imageName;
+ uint16 _numPieces = 0;
+ bool _freePlacement = false; // Wrong drop restores to previous position, not home
+ bool _canRotateAll = false; // All pieces can be rotated
+ int16 _slotTolerance = 0; // Proximity for snapping to slot
+ bool _orderedPlacement = false; // Pieces must be placed in a specific order
+ Common::Array<int16> _placementOrder; // 1-indexed piece IDs in required placement order
+
+ Common::Array<Piece> _pieces;
+
+ SoundDescription _pickupSound;
+ SoundDescription _rotateSound;
+ SoundDescription _dropSound;
+ Common::String _dropAlt1Filename;
+ Common::String _dropAlt2Filename;
+
+ SoundDescription _goodPlacementSound;
+ Common::String _goodAlt1Filename;
+ Common::String _goodAlt2Filename;
+ Common::Array<Common::String> _goodTexts; // 3 entries
+
+ SoundDescription _badPlacementSound;
+ Common::String _badAlt1Filename;
+ Common::String _badAlt2Filename;
+ Common::Array<Common::String> _badTexts; // 3 entries
+
+ SceneChangeWithFlag _solveScene;
+ SoundDescription _completionSound;
+ Common::String _completionText;
+
+ SceneChangeWithFlag _cancelScene;
+ Common::Rect _exitHotspot;
+
+ // --- Runtime state ---
+
+ Graphics::ManagedSurface _image;
+
+ int16 _pickedUpPiece = -1; // Index of currently dragged piece, -1 if none
+ bool _isDragging = false; // True while a piece is attached to the cursor
+ bool _isSolved = false;
+ bool _isCancelled = false;
+ enum SolveState {
+ kIdle = 0, // normal interaction; handleInput drives piece movement
+ kWaitTimer = 1, // 300ms delay after pickup/drop before evaluating outcome
+ kWaitPlaceSound = 2, // waiting for good/bad placement sound (or 1s timer) to finish
+ kWaitCompletion = 3, // waiting for completion sound to finish before scene change
+ kTriggerCompletion = 4 // play completion sound/text, then transition to kWaitCompletion
+ };
+ SolveState _solveState = kIdle;
+ bool _isDropSound = false; // True if last sound played was a drop sound
+ bool _correctlyPlaced = false; // True if the last drop was correctly placed
+ uint16 _piecesPlaced = 0; // Number of pieces correctly placed so far
+ uint32 _timerEnd = 0; // Millisecond timestamp when the current timer expires
+
+ // Previous drag position (for freePlacement restore on wrong drop)
+ Common::Rect _prevDragGameRect;
+
+ // Current rotation surface dimensions for the picked-up piece
+ int16 _pickedUpWidth = 0;
+ int16 _pickedUpHeight = 0;
+
+ // Currently playing sound (scratch copy updated each time a sound is played)
+ SoundDescription _currentSound;
+
+ // --- Internal methods ---
+
+ void playPickupSound(); // FUN_0047239c
+ void playRotateSoundAndStartTimer(); // FUN_0047212b
+ void playDropSound(); // FUN_004721dc
+ void playGoodPlacementSound(); // FUN_00472792
+ void playBadPlacementSound(); // FUN_00472440
+ void checkAllPlaced(); // FUN_00472ac6
+ void rotatePiece(int pieceIdx); // FUN_004719a5
+ void updateDragPosition(Common::Point mouseVP); // FUN_00471490
+ // Update the render object for a piece (set _drawSurface and moveTo gameRect)
+ void updatePieceRender(int pieceIdx);
+ // Rotate a surface 90 degrees clockwise into dst (dst is allocated here)
+ static void rotateSurface90CW(const Graphics::ManagedSurface &src, Graphics::ManagedSurface &dst);
+ // Clamp rect to viewport bounds while preserving dimensions - FUN_004713b8
+ void clampRectToViewport(Common::Rect &rect);
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list