[Scummvm-git-logs] scummvm master -> 8c55bf925f5e3c6e1ff6c5a695d00361bffc4a93
bluegr
noreply at scummvm.org
Fri Mar 20 20:55:10 UTC 2026
This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
8d3ac2ef83 NANCY: Use the correct cursor while dragging items in onebuildpuzzle
8c55bf925f NANCY: Implement multibuildpuzzle
Commit: 8d3ac2ef83813cc724f062d4125e2f7b24749097
https://github.com/scummvm/scummvm/commit/8d3ac2ef83813cc724f062d4125e2f7b24749097
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-20T22:53:17+02:00
Commit Message:
NANCY: Use the correct cursor while dragging items in onebuildpuzzle
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index daa6e09f128..e3f0cd5ab6a 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -243,7 +243,7 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
if (_isDragging) {
// Always update drag position while carrying a piece
updateDragPosition(mouseVP);
- g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
if (_solveState != kIdle)
return;
Commit: 8c55bf925f5e3c6e1ff6c5a695d00361bffc4a93
https://github.com/scummvm/scummvm/commit/8c55bf925f5e3c6e1ff6c5a695d00361bffc4a93
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-20T22:53:20+02:00
Commit Message:
NANCY: Implement multibuildpuzzle
Click-and-drag assembly puzzle used in Nancy 9
Used for three puzzles:
- book sorting: click and drag book pieces in a drawer, leaving no gaps (win
condition is checked on placement)
- sandwich making: click and drag ingredients onto a plate. Some ingredients
are bad and lead to food poisoning (win condition is checked on exit)
- sand castle building: free placement of sand pieces (no win condition)
Changed paths:
engines/nancy/action/puzzle/multibuildpuzzle.cpp
engines/nancy/action/puzzle/multibuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index e36014c8fd1..af1bd0d8c12 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -26,6 +26,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/multibuildpuzzle.h"
@@ -33,54 +34,497 @@ namespace Nancy {
namespace Action {
void MultiBuildPuzzle::init() {
- // TODO
+ g_nancy->_resource->loadImage(_primaryImageName, _primaryImage);
+ _primaryImage.setTransparentColor(_drawSurface.getTransparentColor());
+
+ if (_hasCloseupImage) {
+ g_nancy->_resource->loadImage(_closeupImageName, _closeupImage);
+ _closeupImage.setTransparentColor(_drawSurface.getTransparentColor());
+ }
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &p = _pieces[i];
+
+ const Common::Rect &spriteSrc = !p.altSrcRect.isEmpty() ? p.altSrcRect : p.srcRect;
+
+ int w = spriteSrc.width();
+ int h = spriteSrc.height();
+
+ // Rotation 0: blit from primary image using sprite source rect
+ p.rotateSurfaces[0].create(w, h, _primaryImage.format);
+ p.rotateSurfaces[0].setTransparentColor(_primaryImage.getTransparentColor());
+ p.rotateSurfaces[0].blitFrom(_primaryImage, spriteSrc, Common::Point(0, 0));
+ p.hasSurface[0] = true;
+
+ // Rotations 1-3: created if canRotateAll or piece has a valid altSrcRect
+ if (_canRotateAll || !p.altSrcRect.isEmpty()) {
+ for (int r = 1; r < 4; ++r) {
+ rotateSurface90CW(p.rotateSurfaces[r - 1], p.rotateSurfaces[r]);
+ p.rotateSurfaces[r].setTransparentColor(_primaryImage.getTransparentColor());
+ p.hasSurface[r] = true;
+ }
+ }
+
+ // All pieces start at their homeRect (slot) in unplaced visual state
+ p.curRotation = 0;
+ p.gameRect = p.homeRect;
+ p.isPlaced = false;
+
+ updatePieceRender(i);
+ p.setVisible(true);
+ p.setTransparent(true);
+ p.setZ((uint16)(_z + i + 1));
+ }
+
+ if (_hasCloseupImage) {
+ _shelfSlots.resize(_pieces.size());
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &slot = _shelfSlots[i];
+ int w = _pieces[i].srcRect.width();
+ int h = _pieces[i].srcRect.height();
+ slot._drawSurface.create(w, h, _primaryImage.format);
+ slot._drawSurface.setTransparentColor(_primaryImage.getTransparentColor());
+ slot._drawSurface.blitFrom(_primaryImage, _pieces[i].srcRect, Common::Point(0, 0));
+ slot.moveTo(_pieces[i].homeRect);
+ slot.setTransparent(true);
+ slot.setVisible(true);
+ slot.setZ(_z); // Below all active pieces (_z+1 and up)
+ }
+ }
+}
+
+void MultiBuildPuzzle::registerGraphics() {
+ if (_hasCloseupImage) {
+ for (uint i = 0; i < _shelfSlots.size(); ++i)
+ _shelfSlots[i].registerGraphics();
+ }
+ for (uint i = 0; i < _pieces.size(); ++i)
+ _pieces[i].registerGraphics();
+}
+
+void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
+ // 0x00: primary image name (33 bytes)
+ readFilename(stream, _primaryImageName);
+
+ // 0x21: closeup image name (33 bytes)
+ Common::String secName;
+ readFilename(stream, secName);
+ _closeupImageName = Common::Path(secName);
+ _hasCloseupImage = (secName != "NO_FILE" && !secName.empty());
+
+ // 0x42: numPieces, requiredPieces
+ _numPieces = stream.readUint16LE();
+ _requiredPieces = stream.readUint16LE();
+
+ // 0x46: 1 unknown byte, 0x47: canRotateAll
+ stream.skip(1);
+ _canRotateAll = stream.readByte() != 0;
+
+ // 0x48-0x5e: 23 unknown bytes
+ stream.skip(23);
+
+ // 0x5f: pieces (always 20 Ã 67 bytes in data, only numPieces are valid)
+ _pieces.resize(_numPieces);
+ for (uint i = 0; i < 20; ++i) {
+ if (i < _numPieces) {
+ Piece &p = _pieces[i];
+ readRect(stream, p.srcRect); // 0x00, 16 bytes
+ readRect(stream, p.homeRect); // 0x10, 16 bytes
+ readRect(stream, p.altSrcRect); // 0x20, 16 bytes
+ readRect(stream, p.cuSrcRect); // 0x30, 16 bytes
+ p.counterByte = stream.readByte(); // 0x40
+ p.mustPlace = stream.readByte(); // 0x41
+ p.mustNotPlace = stream.readByte(); // 0x42
+ } else {
+ stream.skip(67);
+ }
+ }
+
+ // 0x59b: three SoundDescriptions (0x31 bytes each)
+ _sounds[0].readNormal(stream);
+ _sounds[1].readNormal(stream);
+ _sounds[2].readNormal(stream);
+
+ // 0x62e: 6 unknown bytes
+ stream.skip(6);
+
+ // 0x634: solve scene, 25 bytes
+ _solveScene.readData(stream);
+
+ // 0x64d: solve sound (49 bytes)
+ _solveSound.readNormal(stream);
+
+ // 0x67e: solve text key (33 bytes, looked up in string table)
+ Common::String solveTextKey;
+ readFilename(stream, solveTextKey);
+
+ // 0x69f: solve raw text (200 bytes)
+ char textBuf[200];
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _solveText, 200);
+
+ // 0x767: cancel scene, 25 bytes
+ _cancelScene.readData(stream);
+
+ // 0x780: exit hotspot rect (16 bytes)
+ readRect(stream, _exitHotspot);
+
+ // 0x790: target drop-zone rect â pieces must be within this area to count as validly placed
+ readRect(stream, _targetZone);
}
void MultiBuildPuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+ g_nancy->_sound->loadSound(_sounds[0]);
+ g_nancy->_sound->loadSound(_sounds[1]);
+ g_nancy->_sound->loadSound(_sounds[2]);
+ g_nancy->_sound->loadSound(_solveSound);
_state = kRun;
- }
+ // fall through
+ case kRun:
+ switch (_solveState) {
+ case kIdle:
+ // Normal interaction; handleInput drives piece movement.
+ break;
- // TODO
- // Stub - move to the winning screen
+ case kWaitTimer:
+ // Short animation after placing a piece.
+ if (g_system->getMillis() >= _timerEnd) {
+ _solveState = kIdle;
+ }
+ break;
- SceneChangeDescription scene;
- uint16 sceneID = NancySceneState.getSceneInfo().sceneID;
+ case kPlaySolveSound:
+ // Play solve sound and show solve text, then wait for it to finish.
+ g_nancy->_sound->playSound(_solveSound);
+ if (!_solveText.empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(_solveText);
+ }
+ _solveState = kWaitSolveSound;
+ break;
- switch (sceneID) {
- case 2025:
- warning("STUB - Nancy 9 Sand castle puzzle");
- scene.sceneID = 2024;
+ case kWaitSolveSound:
+ // Wait until solve sound has finished, then trigger scene change.
+ if (!g_nancy->_sound->isSoundPlaying(_solveSound)) {
+ g_nancy->_sound->stopSound(_solveSound);
+ _state = kActionTrigger;
+ }
+ break;
+ }
break;
- case 2575:
- warning("STUB - Nancy 9 Sandwich making puzzle");
- NancySceneState.setEventFlag(428, g_nancy->_true); // EV_Solved_Sandwich_Bad
- NancySceneState.setEventFlag(429, g_nancy->_true); // EV_Solved_Sandwich_Good
- scene.sceneID = 2572;
- break;
- case 2585:
- warning("STUB - Nancy 9 Book sorting puzzle");
- NancySceneState.setEventFlag(397, g_nancy->_true); // Set puzzle flag to solved
- scene.sceneID = 2583;
+
+ case kActionTrigger:
+ g_nancy->_sound->stopSound(_sounds[0]);
+ g_nancy->_sound->stopSound(_sounds[1]);
+ g_nancy->_sound->stopSound(_sounds[2]);
+ g_nancy->_sound->stopSound(_solveSound);
+ if (_isCancelled) {
+ // Change to cancel scene unconditionally, but only set the cancel flag if
+ // at least one counterByte==0 piece was placed.
+ NancySceneState.changeScene(_cancelScene._sceneChange);
+ if (_cancelScene._flag.label != kFlagNoLabel) {
+ uint16 count = 0;
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if (_pieces[i].isPlaced && _pieces[i].counterByte == 0)
+ ++count;
+ }
+ if (count > 0)
+ NancySceneState.setEventFlag(_cancelScene._flag);
+ }
+ } else {
+ NancySceneState.setEventFlag(_solveScene._flag);
+ NancySceneState.changeScene(_solveScene._sceneChange);
+ }
+ finishExecution();
break;
- default:
- warning("MultiBuildPuzzle: Unknown scene %d", sceneID);
+ }
+}
+
+void MultiBuildPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _solveState != kIdle || _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) {
+ // Update dragged piece to center on cursor
+ Piece &pp = _pieces[_pickedUpPiece];
+ int newLeft = mouseVP.x - _pickedUpWidth / 2;
+ int newTop = mouseVP.y - _pickedUpHeight / 2;
+ pp.gameRect.left = newLeft;
+ pp.gameRect.top = newTop;
+ pp.gameRect.right = newLeft + _pickedUpWidth;
+ pp.gameRect.bottom = newTop + _pickedUpHeight;
+ updatePieceRender(_pickedUpPiece);
+
+ g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+
+ // Right click: rotate the carried piece
+ if ((input.input & NancyInput::kRightMouseButtonUp) && pp.hasSurface[1]) {
+ pp.curRotation = (pp.curRotation + 1) % 4;
+ _pickedUpWidth = pp.rotateSurfaces[pp.curRotation].w;
+ _pickedUpHeight = pp.rotateSurfaces[pp.curRotation].h;
+ g_nancy->_sound->playSound(_sounds[0]);
+ updatePieceRender(_pickedUpPiece);
+ return;
+ }
+
+ // Left click: drop the piece wherever the cursor is.
+ // For non-closeup puzzles: reject if the drop center is outside the target zone or
+ // the piece overlaps an already-placed piece; piece returns to shelf on rejection.
+ // For closeup puzzles: no geometric checks â free-form placement.
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ bool validDrop = true;
+
+ // Geometric checks apply only to non-closeup puzzles with a win condition (e.g. books).
+ // Sand castle (no closeup image, _requiredPieces=0) allows free stacking.
+ // Sandwich puzzle (has closeup image) allows free-form placement.
+ if (!_hasCloseupImage && _requiredPieces > 0) {
+ // Boundary check: drop center must be inside the target zone
+ Common::Point dropCenter((pp.gameRect.left + pp.gameRect.right) / 2,
+ (pp.gameRect.top + pp.gameRect.bottom) / 2);
+ if (!_targetZone.isEmpty() && !_targetZone.contains(dropCenter))
+ validDrop = false;
+
+ // Overlap check: piece must not overlap any already-placed piece
+ if (validDrop) {
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if ((int)i != _pickedUpPiece && _pieces[i].isPlaced &&
+ pp.gameRect.intersects(_pieces[i].gameRect)) {
+ validDrop = false;
+ break;
+ }
+ }
+ }
+ }
+
+ // Clear drag state BEFORE updatePieceRender so the correct visual is chosen:
+ // - valid drop: isPlaced=true, isDragging=false -> shows rotation surface at drop pos
+ // - invalid drop: isPlaced=false, isDragging=false -> shows shelf srcRect at homeRect
+ _isDragging = false;
+ int placedIdx = _pickedUpPiece;
+ _pickedUpPiece = -1;
+
+ if (validDrop) {
+ pp.isPlaced = true;
+ g_nancy->_sound->playSound(_sounds[1]);
+ } else {
+ // Return piece to its shelf position
+ pp.gameRect = pp.homeRect;
+ }
+
+ updatePieceRender(placedIdx);
+
+ // After placing, check if the puzzle is now solved (FUN_0046da47)
+ checkIfSolved();
+
+ // Brief debounce before next interaction (only if not transitioning to solve)
+ if (_solveState == kIdle) {
+ _solveState = kWaitTimer;
+ _timerEnd = g_system->getMillis() + 300;
+ }
+ }
+ return;
+ }
+
+ if (_selectedPiece != -1) {
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ int sel = _selectedPiece;
+ _selectedPiece = -1;
+ Piece &pp = _pieces[sel];
+ _pickedUpPiece = sel;
+ _isDragging = true;
+ pp.curRotation = 0;
+ _pickedUpWidth = pp.rotateSurfaces[0].w;
+ _pickedUpHeight = pp.rotateSurfaces[0].h;
+ // Centre the drag rect on the cursor now that we leave the CU display position
+ int newLeft = mouseVP.x - _pickedUpWidth / 2;
+ int newTop = mouseVP.y - _pickedUpHeight / 2;
+ pp.gameRect = Common::Rect(newLeft, newTop,
+ newLeft + _pickedUpWidth, newTop + _pickedUpHeight);
+ updatePieceRender(sel);
+ }
+ return;
+ }
+
+ // Not dragging and nothing selected: look for a piece to pick up / select
+ int16 topmost = -1;
+ for (int i = (int)_pieces.size() - 1; i >= 0; --i) {
+ Piece &p = _pieces[i];
+ if (!p.gameRect.contains(mouseVP))
+ continue;
+ if (topmost == -1 || p.getZOrder() > _pieces[topmost].getZOrder())
+ topmost = (int16)i;
+ }
+
+ if (topmost != -1) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ Piece &pp = _pieces[topmost];
+ pp.isPlaced = false;
+ pp.curRotation = 0;
+ pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.registerGraphics();
+ g_nancy->_sound->playSound(_sounds[0]);
+
+ if (_hasCloseupImage && !pp.cuSrcRect.isEmpty()) {
+ // First click shows close-up view, centered in the viewport.
+ _selectedPiece = topmost;
+ int cuW = pp.cuSrcRect.width();
+ int cuH = pp.cuSrcRect.height();
+ int cuLeft = (vpScreen.width() - cuW) / 2;
+ int cuTop = (vpScreen.height() - cuH) / 2;
+ pp.gameRect = Common::Rect(cuLeft, cuTop, cuLeft + cuW, cuTop + cuH);
+ } else {
+ // Direct drag: first click immediately starts dragging
+ _isDragging = true;
+ _pickedUpPiece = topmost;
+ _pickedUpWidth = pp.rotateSurfaces[0].w;
+ _pickedUpHeight = pp.rotateSurfaces[0].h;
+ }
+ updatePieceRender(topmost);
+ }
+ return;
+ }
+
+ // Check exit hotspot
+ if (!_exitHotspot.isEmpty()) {
+ 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) {
+ checkIfSolvedOnExit();
+ }
+ }
}
+}
+
+void MultiBuildPuzzle::checkIfSolvedOnExit() {
+ if (_requiredPieces == 1) {
+ // Check constraints again
+ bool placedBadPiece = false;
+ bool placedPiece = false;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if (_pieces[i].isPlaced) {
+ if (_pieces[i].mustNotPlace) {
+ NancySceneState.setEventFlag(_cancelScene._flag);
+ placedBadPiece = true;
+ break;
+ }
+ placedPiece = true;
+ }
+ }
+
+ if (!placedPiece || placedBadPiece) {
+ // Player hasn't placed any pieces, or has placed
+ // at least one bad piece -> retry scene or lose,
+ // depending on the cancelScene flag.
+ _isCancelled = true;
+ _state = kActionTrigger;
+ } else {
+ // Player has placed only good pieces -> player won.
+ _isSolved = true;
+ _solveState = kPlaySolveSound;
+ }
+ } else {
+ _isCancelled = true;
+ _state = kActionTrigger;
+ }
}
-void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+void MultiBuildPuzzle::checkIfSolved() {
+ // Non-CU puzzles (no secondary image) with _requiredPieces == 0 have no win condition;
+ // the player exits manually (e.g. sand castle). CU puzzles (sandwich) with
+ // _requiredPieces == 0 still use mustPlace/mustNotPlace constraints to determine solve.
+ if (_requiredPieces == 0 && !_hasCloseupImage)
+ return;
+
+ // Count correctly placed pieces (those with counterByte == 0)
+ uint16 count = 0;
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if (_pieces[i].isPlaced && _pieces[i].counterByte == 0)
+ ++count;
+ }
+
+ if (count < _requiredPieces)
+ return;
+
+ // Check constraints: no placed mustNotPlace piece, no unplaced mustPlace piece
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if (_pieces[i].isPlaced && _pieces[i].mustNotPlace)
+ return;
+ if (!_pieces[i].isPlaced && _pieces[i].mustPlace)
+ return;
+ }
+
+ // Solved!
+ _isSolved = true;
+ _solveState = kPlaySolveSound;
}
-void MultiBuildPuzzle::handleInput(NancyInput &input) {
- // TODO
+void MultiBuildPuzzle::updatePieceRender(int pieceIdx) {
+ Piece &p = _pieces[pieceIdx];
+ bool isSelected = (!_isDragging && pieceIdx == _selectedPiece);
+ bool isDragging = (_isDragging && pieceIdx == _pickedUpPiece);
+
+ if (p.isPlaced || isDragging) {
+ // Placed or being dragged: show rotation sprite.
+ int rot = p.curRotation;
+ if (!p.hasSurface[rot])
+ rot = 0;
+ if (p.hasSurface[rot]) {
+ int w = p.rotateSurfaces[rot].w;
+ int h = p.rotateSurfaces[rot].h;
+ p._drawSurface.create(w, h, p.rotateSurfaces[rot].format);
+ p._drawSurface.setTransparentColor(p.rotateSurfaces[rot].getTransparentColor());
+ p._drawSurface.blitFrom(p.rotateSurfaces[rot], Common::Point(0, 0));
+ }
+ } else if (isSelected && _hasCloseupImage && !p.cuSrcRect.isEmpty()) {
+ // Show zoomed close-up
+ int w = p.cuSrcRect.width();
+ int h = p.cuSrcRect.height();
+ p._drawSurface.create(w, h, _closeupImage.format);
+ p._drawSurface.setTransparentColor(_closeupImage.getTransparentColor());
+ p._drawSurface.blitFrom(_closeupImage, p.cuSrcRect, Common::Point(0, 0));
+ } else {
+ // Unplaced and at rest on the shelf (or selected with no close-up available):
+ // show srcRect from primary image.
+ int w = p.srcRect.width();
+ int h = p.srcRect.height();
+ p._drawSurface.create(w, h, _primaryImage.format);
+ p._drawSurface.setTransparentColor(_primaryImage.getTransparentColor());
+ p._drawSurface.blitFrom(_primaryImage, p.srcRect, Common::Point(0, 0));
+ }
+
+ p.setTransparent(true);
+ p.moveTo(p.gameRect);
+}
+
+void MultiBuildPuzzle::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/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index 2a54ffa88f7..6bf2be774ed 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -23,18 +23,25 @@
#define NANCY_ACTION_MULTIBUILDPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/renderobject.h"
namespace Nancy {
namespace Action {
-// Puzzle in Nancy 9, where an item is built from smaller pieces
-
+// Click-and-drag assembly puzzle used in Nancy 9.
+// Used for three puzzles:
+// - book sorting: click and drag book pieces in a drawer, leaving no gaps (win
+// condition is checked on placement)
+// - sandwich making: click and drag ingredients onto a plate. Some ingredients
+// are bad and lead to food poisoning (win condition is checked on exit)
+// - sand castle building: free placement of sand pieces (no win condition)
class MultiBuildPuzzle : public RenderActionRecord {
public:
MultiBuildPuzzle() : RenderActionRecord(7) {}
virtual ~MultiBuildPuzzle() {}
void init() override;
+ void registerGraphics() override;
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
@@ -43,9 +50,96 @@ public:
protected:
Common::String getRecordTypeName() const override { return "MultiBuildPuzzle"; }
bool isViewportRelative() const override { return true; }
+
+ // A single puzzle piece. Each piece is its own RenderObject.
+ // Unplaced: _drawSurface shows srcRect from primary image.
+ // Placed: _drawSurface shows the rotation sprite (from altSrcRect or srcRect).
+ struct Piece : RenderObject {
+ Piece() : RenderObject(0) {}
+
+ // --- File data ---
+ Common::Rect srcRect; // Source rect in primary image (unplaced visual)
+ Common::Rect homeRect; // Screen position in viewport coords (= the slot)
+ Common::Rect altSrcRect; // If non-zero area: used as source for sprite creation
+ Common::Rect cuSrcRect; // Source rect in secondary image (overlay when placed)
+ uint8 counterByte = 0; // If 0, piece counts toward requiredPieces tally
+ uint8 mustPlace = 0; // If 1, piece MUST be placed for solution
+ uint8 mustNotPlace = 0; // If 1, placing this piece fails the solution check
+
+ // --- Runtime ---
+ Common::Rect gameRect; // Current viewport-space rect (homeRect or cursor-following)
+ int curRotation = 0;
+ bool isPlaced = false;
+
+ // Up to 4 rotation surfaces (rotation 1-3 only if canRotateAll or altSrcRect non-zero)
+ 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 _primaryImageName;
+ Common::Path _closeupImageName;
+ bool _hasCloseupImage = false;
+
+ uint16 _numPieces = 0;
+ uint16 _requiredPieces = 0; // Minimum placed pieces (with counterByte==0) needed to trigger solve check
+ bool _canRotateAll = false;
+
+ Common::Array<Piece> _pieces;
+ // For closeup puzzles (e.g. sandwich): permanent shelf visuals that stay at homeRect
+ // so the shelf always shows the source ingredient regardless of where the active piece is.
+ Common::Array<Piece> _shelfSlots;
+
+ SoundDescription _sounds[3]; // [0]=pickup/move, [1]=placement, [2]=extra
+
+ SceneChangeWithFlag _solveScene;
+ SoundDescription _solveSound;
+ Common::String _solveText;
+
+ SceneChangeWithFlag _cancelScene;
+ Common::Rect _exitHotspot;
+ Common::Rect _targetZone; // Valid drop area (drawer/plate/etc.); pieces must be within this to solve
+
+ // --- Runtime state ---
+
+ Graphics::ManagedSurface _primaryImage;
+ Graphics::ManagedSurface _closeupImage;
+
+ int16 _selectedPiece = -1; // Index of selected piece (CU view shown, not yet dragging), -1 if none
+ int16 _pickedUpPiece = -1; // Index of piece being dragged, -1 if none
+ bool _isDragging = false;
+ bool _isSolved = false;
+ bool _isCancelled = false;
+
+ enum SolveState {
+ kIdle = 0,
+ kWaitTimer = 1,
+ kWaitSolveSound = 4, // Waiting for solve sound to stop playing
+ kPlaySolveSound = 5 // Trigger solve sound + text, then wait
+ };
+ SolveState _solveState = kIdle;
+ uint32 _timerEnd = 0;
+
+ int16 _pickedUpWidth = 0;
+ int16 _pickedUpHeight = 0;
+
+ // --- Internal methods ---
+
+ void checkIfSolved(); // FUN_0046da47
+ void checkIfSolvedOnExit();
+ // Update a piece's _drawSurface and position to match its current state
+ 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);
};
} // End of namespace Action
} // End of namespace Nancy
-#endif // NANCY_ACTION_ONEBUILDPUZZLE_H
+#endif // NANCY_ACTION_MULTIBUILDPUZZLE_H
More information about the Scummvm-git-logs
mailing list