[Scummvm-git-logs] scummvm master -> 1874b80f3d0c8e3adfe1483b24584c7f7507b757
bluegr
noreply at scummvm.org
Tue May 19 18:17:27 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
1874b80f3d NANCY: Rewrite multibuildpuzzle to be closed to the original sources
Commit: 1874b80f3d0c8e3adfe1483b24584c7f7507b757
https://github.com/scummvm/scummvm/commit/1874b80f3d0c8e3adfe1483b24584c7f7507b757
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-19T21:17:18+03:00
Commit Message:
NANCY: Rewrite multibuildpuzzle to be closed to the original sources
Fixes issues with puzzles using multibuildpuzzle in Nancy 9: the
sandwich puzzle, the book stacking puzzle and the sand castle puzzle
Fix #16752, #16743, #16744, #16774
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 ead89f99a40..ab3b651f2cb 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -50,7 +50,6 @@ void MultiBuildPuzzle::init() {
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));
@@ -65,7 +64,6 @@ void MultiBuildPuzzle::init() {
}
}
- // All pieces start at their homeRect (slot) in unplaced visual state
p.curRotation = 0;
p.gameRect = p.homeRect;
p.isPlaced = false;
@@ -76,22 +74,6 @@ void MultiBuildPuzzle::init() {
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)
- }
- }
-
_isInitialized = true;
}
@@ -99,83 +81,73 @@ void MultiBuildPuzzle::registerGraphics() {
if (!_isInitialized)
return;
- 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);
+ _autoSolveOnDrop = stream.readByte() != 0;
_canRotateAll = stream.readByte() != 0;
- // 0x48-0x5e: 23 unknown bytes
- stream.skip(23);
+ _useRotationHotspot = stream.readByte() != 0;
+ _rotHotspotHeight = stream.readSint16LE();
+ _rotHotspotWidth = stream.readSint16LE();
+
+ readRect(stream, _targetZone);
- // 0x5f: pieces (always 20 Ã 67 bytes in data, only numPieces are valid)
+ _allowAltZoneSnap = stream.readByte() != 0;
+ _checkOverlapOnDrop = stream.readByte() != 0;
+
+ // Pieces: data file always has 20 Ã 67-byte slots; only _numPieces are used.
+ // Reserve up-front so counter-spawn push_back doesn't reallocate (pieces are
+ // RenderObjects already registered with the graphics manager).
+ _pieces.reserve(80);
_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
+ readRect(stream, p.srcRect);
+ readRect(stream, p.homeRect);
+ readRect(stream, p.altSrcRect);
+ readRect(stream, p.cuSrcRect);
+ p.counterByte = stream.readByte();
+ p.mustPlace = stream.readByte();
+ p.mustNotPlace = stream.readByte();
} else {
stream.skip(67);
}
}
- // 0x59b: three SoundDescriptions (0x31 bytes each)
_rotationSound.readNormal(stream);
_pickupSound.readNormal(stream);
_dropSound.readNormal(stream);
- // 0x62e: 6 unknown bytes
- stream.skip(6);
+ _dragCursorID = stream.readSint16LE();
+ _exitCursorID1 = stream.readSint16LE();
+ _exitCursorID2 = stream.readSint16LE();
- // 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)
+ readFilename(stream, _solveTextKey);
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);
+ readRect(stream, _exitHotspot2);
}
void MultiBuildPuzzle::execute() {
@@ -192,28 +164,21 @@ void MultiBuildPuzzle::execute() {
case kRun:
switch (_solveState) {
case kIdle:
- // Normal interaction; handleInput drives piece movement.
break;
case kWaitTimer:
- // Short animation after placing a piece.
- if (g_system->getMillis() >= _timerEnd) {
+ // Short debounce after placing a piece.
+ if (g_system->getMillis() >= _timerEnd)
_solveState = kIdle;
- }
break;
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);
- }
+ // Solve sound + caption are now played synchronously inside
+ // checkIfSolved(); this state shouldn't normally be reached.
_solveState = kWaitSolveSound;
break;
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;
@@ -228,15 +193,17 @@ void MultiBuildPuzzle::execute() {
g_nancy->_sound->stopSound(_dropSound);
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);
+ // Cancel flag is only set if at least one piece was placed (or
+ // spawned). For sandwich (all counter pieces) the spawn delta is
+ // what trips the gate when a bad ingredient was placed.
if (_cancelScene._flag.label != kFlagNoLabel) {
uint16 count = 0;
- for (uint i = 0; i < _pieces.size(); ++i) {
+ for (uint i = 0; i < _numPieces; ++i) {
if (_pieces[i].isPlaced && _pieces[i].counterByte == 0)
++count;
}
+ count += (uint16)(_pieces.size() - _numPieces);
if (count > 0)
NancySceneState.setEventFlag(_cancelScene._flag);
}
@@ -249,25 +216,145 @@ void MultiBuildPuzzle::execute() {
}
}
+CursorManager::CursorType MultiBuildPuzzle::cursorFromDataID(int16 id, CursorManager::CursorType fallback) const {
+ if (id < 0 || id > 21)
+ return fallback;
+ return (CursorManager::CursorType)id;
+}
+
+bool MultiBuildPuzzle::altZoneSnapValid() const {
+ // Valid when the dragged piece sits atop a moved piece, within an X
+ // tolerance of 20 px and a Y tolerance of 4 px. Used for sand-castle
+ // stacking when _allowAltZoneSnap is set.
+ if (_pickedUpPiece < 0)
+ return false;
+
+ const Piece &pp = _pieces[_pickedUpPiece];
+ int halfW = (pp.gameRect.width() - 1) / 2;
+ int halfH = (pp.gameRect.height() - 1) / 2;
+ int cx = pp.gameRect.left + halfW;
+ int cy = pp.gameRect.top + halfH;
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ if ((int)i == _pickedUpPiece)
+ continue;
+ const Piece &other = _pieces[i];
+ if (other.gameRect == other.homeRect)
+ continue;
+ if (other.gameRect.top - 4 < cy + halfH &&
+ cy < other.gameRect.top &&
+ other.gameRect.left - 20 < cx - halfW &&
+ cx + halfW < (other.gameRect.right - 1) + 20)
+ return true;
+ }
+ return false;
+}
+
+void MultiBuildPuzzle::spawnCounterPiece(int srcIdx) {
+ if (srcIdx < 0 || srcIdx >= (int)_pieces.size())
+ return;
+ // Defensive cap to avoid runaway memory use (sand castle can spawn endlessly).
+ if (_pieces.size() >= 80)
+ return;
+
+ const Piece &src = _pieces[srcIdx];
+ // All clones share surfaces with the original piece at typeIdx.
+ int sharedType = (src.typeIdx >= 0) ? src.typeIdx : srcIdx;
+ const Piece &surf = _pieces[sharedType];
+
+ _pieces.push_back(Piece());
+ Piece &np = _pieces.back();
+ np.srcRect = src.srcRect;
+ np.homeRect = src.homeRect;
+ np.altSrcRect = src.altSrcRect;
+ np.cuSrcRect = src.cuSrcRect;
+ np.counterByte = src.counterByte;
+ np.mustPlace = src.mustPlace;
+ np.mustNotPlace = src.mustNotPlace;
+ np.typeIdx = sharedType;
+ np.curRotation = 0;
+ np.gameRect = np.homeRect;
+ np.isPlaced = false;
+
+ // Each clone needs its own ManagedSurface since it's an independent RenderObject.
+ for (int r = 0; r < 4; ++r) {
+ if (!surf.hasSurface[r])
+ continue;
+ int w = surf.rotateSurfaces[r].w;
+ int h = surf.rotateSurfaces[r].h;
+ np.rotateSurfaces[r].create(w, h, surf.rotateSurfaces[r].format);
+ np.rotateSurfaces[r].setTransparentColor(surf.rotateSurfaces[r].getTransparentColor());
+ np.rotateSurfaces[r].blitFrom(surf.rotateSurfaces[r], Common::Point(0, 0));
+ np.hasSurface[r] = true;
+ }
+
+ int newIdx = (int)_pieces.size() - 1;
+ updatePieceRender(newIdx);
+ _pieces[newIdx].setVisible(true);
+ _pieces[newIdx].setTransparent(true);
+ _pieces[newIdx].setZ((uint16)(_z + newIdx + 1));
+ _pieces[newIdx].registerGraphics();
+}
+
bool MultiBuildPuzzle::isValidDrop() const {
+ if (_pickedUpPiece < 0)
+ return false;
+
const Piece &pp = _pieces[_pickedUpPiece];
- // 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))
+ // Bounding-box-inside test. Left/top are strict; right/bottom allow up to
+ // kEdgeTolerance px of overflow (matches the design tolerance the engine
+ // uses for the overlap check below).
+ const int kEdgeTolerance = 3;
+ bool inTargetZone =
+ !_targetZone.isEmpty() &&
+ _targetZone.left < pp.gameRect.left &&
+ pp.gameRect.right <= _targetZone.right + kEdgeTolerance &&
+ _targetZone.top < pp.gameRect.top &&
+ pp.gameRect.bottom <= _targetZone.bottom + kEdgeTolerance;
+
+ if (!inTargetZone) {
+ if (!_allowAltZoneSnap || !altZoneSnapValid())
return false;
+ }
+
+ // Overlap check with 3-px tolerance: pieces are considered overlapping
+ // only if their bounding boxes overlap by more than 3 pixels (so adjacent
+ // or barely-touching placements are accepted).
+ if (_checkOverlapOnDrop) {
+ int ppLeft = pp.gameRect.left;
+ int ppTop = pp.gameRect.top;
+ int ppRight = pp.gameRect.right - 1;
+ int ppBottom = pp.gameRect.bottom - 1;
- // Overlap check: piece must not overlap any already-placed piece
for (uint i = 0; i < _pieces.size(); ++i) {
- if ((int)i != _pickedUpPiece && _pieces[i].isPlaced &&
- pp.gameRect.intersects(_pieces[i].gameRect)) {
+ if ((int)i == _pickedUpPiece)
+ continue;
+ if (!_pieces[i].isPlaced)
+ continue;
+ const Piece &other = _pieces[i];
+ int otherLeft = other.gameRect.left;
+ int otherTop = other.gameRect.top;
+ int otherRight = other.gameRect.right - 1;
+ int otherBottom = other.gameRect.bottom - 1;
+
+ bool xOverlap = false;
+ if (ppLeft < otherLeft)
+ xOverlap = (otherLeft + 3 <= ppRight);
+ else if (ppLeft <= otherRight - 3)
+ xOverlap = true;
+
+ if (!xOverlap)
+ continue;
+
+ bool yOverlap = false;
+ if (ppTop < otherTop)
+ yOverlap = (otherTop + 3 <= ppBottom);
+ else if (ppTop <= otherBottom - 3)
+ yOverlap = true;
+
+ if (yOverlap)
return false;
- }
}
}
@@ -288,11 +375,14 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
Common::Point mouseVP(input.mousePos.x - vpScreen.left,
input.mousePos.y - vpScreen.top);
+ CursorManager::CursorType dragCursor = cursorFromDataID(_dragCursorID, CursorManager::kCustom1);
+
if (_isDragging) {
- // Update dragged piece to center on cursor
+ // Centre dragged piece on cursor. Offset uses (width-1)/2 to match
+ // the original's inclusive-coordinate delta arithmetic.
Piece &pp = _pieces[_pickedUpPiece];
- int newLeft = mouseVP.x - _pickedUpWidth / 2;
- int newTop = mouseVP.y - _pickedUpHeight / 2;
+ int newLeft = mouseVP.x - (_pickedUpWidth - 1) / 2;
+ int newTop = mouseVP.y - (_pickedUpHeight - 1) / 2;
pp.gameRect.left = newLeft;
pp.gameRect.top = newTop;
pp.gameRect.right = newLeft + _pickedUpWidth;
@@ -300,7 +390,7 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
updatePieceRender(_pickedUpPiece);
bool validDrop = isValidDrop();
- g_nancy->_cursor->setCursorType(validDrop ? CursorManager::kCustom1 : CursorManager::kNormal);
+ g_nancy->_cursor->setCursorType(validDrop ? dragCursor : CursorManager::kNormal);
// Right click: rotate the carried piece
if ((input.input & NancyInput::kRightMouseButtonUp) && pp.hasSurface[1]) {
@@ -312,14 +402,9 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
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.
+ // Left click: drop. Invalid drop returns the piece to its home rect.
if (input.input & NancyInput::kLeftMouseButtonUp) {
- // 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
+ // Clear drag state before updatePieceRender so the correct visual is chosen.
_isDragging = false;
int placedIdx = _pickedUpPiece;
_pickedUpPiece = -1;
@@ -327,17 +412,21 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
if (validDrop) {
pp.isPlaced = true;
g_nancy->_sound->playSound(_dropSound);
+
+ // Counter pieces respawn at home for unlimited supply.
+ if (pp.counterByte != 0)
+ spawnCounterPiece(placedIdx);
} 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();
+ // Solve check runs on drop for puzzles with _autoSolveOnDrop, and
+ // also once piece count grows past the original's auto-solve trigger.
+ if (_autoSolveOnDrop || _pieces.size() > 79)
+ checkIfSolved();
- // Brief debounce before next interaction (only if not transitioning to solve)
if (_solveState == kIdle) {
_solveState = kWaitTimer;
_timerEnd = g_system->getMillis() + 300;
@@ -356,9 +445,8 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
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;
+ int newLeft = mouseVP.x - (_pickedUpWidth - 1) / 2;
+ int newTop = mouseVP.y - (_pickedUpHeight - 1) / 2;
pp.gameRect = Common::Rect(newLeft, newTop,
newLeft + _pickedUpWidth, newTop + _pickedUpHeight);
updatePieceRender(sel);
@@ -367,18 +455,56 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
return;
}
- // Not dragging and nothing selected: look for a piece to pick up / select
+ // Find a piece to pick up. First pass detects the rotation hotspot
+ // (a small rect at each piece's top-left) when rotation is enabled.
+ int16 topmostRot = -1;
int16 topmost = -1;
+ bool rotationsEnabled = _canRotateAll && _useRotationHotspot &&
+ _rotHotspotWidth > 0 && _rotHotspotHeight > 0;
for (int i = (int)_pieces.size() - 1; i >= 0; --i) {
Piece &p = _pieces[i];
if (!p.gameRect.contains(mouseVP))
continue;
+ // Placed counter pieces are locked (can't re-grab the placed ingredient).
+ if (p.isPlaced && p.counterByte != 0)
+ continue;
+ if (rotationsEnabled) {
+ Common::Rect rotRect(p.gameRect.left, p.gameRect.top,
+ p.gameRect.left + _rotHotspotWidth,
+ p.gameRect.top + _rotHotspotHeight);
+ if (rotRect.contains(mouseVP)) {
+ if (topmostRot == -1 || p.getZOrder() > _pieces[topmostRot].getZOrder())
+ topmostRot = (int16)i;
+ continue;
+ }
+ }
if (topmost == -1 || p.getZOrder() > _pieces[topmost].getZOrder())
topmost = (int16)i;
}
+ if (topmostRot != -1) {
+ // Hovering rotation hotspot: click rotates 90° and picks up.
+ g_nancy->_cursor->setCursorType(CursorManager::kRotateCW);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ Piece &pp = _pieces[topmostRot];
+ pp.isPlaced = false;
+ pp.curRotation = (pp.curRotation + 1) % 4;
+ if (!pp.hasSurface[pp.curRotation])
+ pp.curRotation = 0;
+ pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.registerGraphics();
+ _isDragging = true;
+ _pickedUpPiece = topmostRot;
+ _pickedUpWidth = pp.rotateSurfaces[pp.curRotation].w;
+ _pickedUpHeight = pp.rotateSurfaces[pp.curRotation].h;
+ g_nancy->_sound->playSound(_rotationSound);
+ updatePieceRender(topmostRot);
+ }
+ return;
+ }
+
if (topmost != -1) {
- g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+ g_nancy->_cursor->setCursorType(dragCursor);
if (input.input & NancyInput::kLeftMouseButtonUp) {
Piece &pp = _pieces[topmost];
@@ -388,8 +514,7 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
pp.registerGraphics();
if (_hasCloseupImage && !pp.cuSrcRect.isEmpty()) {
- // First click shows the close-up view centered on the
- // piece's current center.
+ // First click shows the closeup view centred on the piece.
_selectedPiece = topmost;
const int cuW = pp.cuSrcRect.width();
const int cuH = pp.cuSrcRect.height();
@@ -399,12 +524,11 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
const int centerY = pp.gameRect.top + pieceH / 2;
int cuLeft = centerX - cuW / 2;
int cuTop = centerY - cuH / 2;
- // Clamp so the close-up stays fully inside the viewport.
cuLeft = CLIP<int>(cuLeft, 0, MAX(0, vpScreen.width() - cuW));
cuTop = CLIP<int>(cuTop, 0, MAX(0, vpScreen.height() - cuH));
pp.gameRect = Common::Rect(cuLeft, cuTop, cuLeft + cuW, cuTop + cuH);
} else {
- // Direct drag: first click immediately starts dragging
+ // Direct drag on first click.
_isDragging = true;
_pickedUpPiece = topmost;
_pickedUpWidth = pp.rotateSurfaces[0].w;
@@ -416,80 +540,79 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
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();
- }
- }
- }
+ // Exit hotspots: a click in either fires the exit path. Each uses its own data cursor id.
+ if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
+ checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
}
-void MultiBuildPuzzle::checkIfSolvedOnExit() {
- if (_requiredPieces == 1) {
- // Check constraints again
- bool placedBadPiece = false;
- bool placedPiece = false;
-
- 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;
- }
- }
+bool MultiBuildPuzzle::checkExitHotspot(const Common::Rect &hot, int16 cursorID, const NancyInput &input) {
+ if (hot.isEmpty())
+ return false;
+ Common::Rect exitScreen = NancySceneState.getViewport().convertViewportToScreen(hot);
+ if (!exitScreen.contains(input.mousePos))
+ return false;
+ g_nancy->_cursor->setCursorType(cursorFromDataID(cursorID, g_nancy->_cursor->_puzzleExitCursor));
+ if (input.input & NancyInput::kLeftMouseButtonUp)
+ checkIfSolvedOnExit();
+ return 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 {
+void MultiBuildPuzzle::checkIfSolvedOnExit() {
+ // Run the solve check; if it doesn't mark the puzzle solved, cancel out.
+ checkIfSolved();
+ if (!_isSolved) {
_isCancelled = true;
_state = kActionTrigger;
}
}
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)
+ // Count = placed pieces with counterByte == 0, plus the spawn delta
+ // (so counter-piece puzzles like sandwich count each placement).
uint16 count = 0;
- for (uint i = 0; i < _pieces.size(); ++i) {
+ for (uint i = 0; i < _numPieces; ++i) {
if (_pieces[i].isPlaced && _pieces[i].counterByte == 0)
++count;
}
+ count += (uint16)(_pieces.size() - _numPieces);
if (count < _requiredPieces)
return;
- // Check constraints: no placed mustNotPlace piece, no unplaced mustPlace piece
- for (uint i = 0; i < _pieces.size(); ++i) {
+ // Bail without solving on any constraint failure.
+ for (uint i = 0; i < _numPieces; ++i) {
if (_pieces[i].isPlaced && _pieces[i].mustNotPlace)
return;
if (!_pieces[i].isPlaced && _pieces[i].mustPlace)
return;
}
- // Solved!
+ // Play sound + caption inline (rather than parking in kPlaySolveSound for
+ // execute() to pick up) so the transition is deterministic regardless of
+ // action-record processing order.
_isSolved = true;
- _solveState = kPlaySolveSound;
+ g_nancy->_sound->playSound(_solveSound);
+
+ // Caption: prefer the CONVO lookup of the text key. An empty lookup
+ // result means audio-only â keep the textbox empty. Only fall back to
+ // the raw _solveText when the key isn't in CONVO.
+ Common::String textToShow;
+ bool useLookup = false;
+ if (!_solveTextKey.empty()) {
+ const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
+ if (convo && convo->texts.contains(_solveTextKey)) {
+ textToShow = convo->texts[_solveTextKey];
+ useLookup = true;
+ }
+ }
+ if (!useLookup)
+ textToShow = _solveText;
+ if (!textToShow.empty()) {
+ NancySceneState.getTextbox().clear();
+ NancySceneState.getTextbox().addTextLine(textToShow);
+ }
+
+ _solveState = kWaitSolveSound;
}
void MultiBuildPuzzle::updatePieceRender(int pieceIdx) {
@@ -510,15 +633,14 @@ void MultiBuildPuzzle::updatePieceRender(int pieceIdx) {
p._drawSurface.blitFrom(p.rotateSurfaces[rot], Common::Point(0, 0));
}
} else if (isSelected && _hasCloseupImage && !p.cuSrcRect.isEmpty()) {
- // Show zoomed close-up
+ // Zoomed closeup.
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.
+ // At rest on the shelf: show srcRect from primary image.
int w = p.srcRect.width();
int h = p.srcRect.height();
p._drawSurface.create(w, h, _primaryImage.format);
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index 93b1f3534de..0a3b755982d 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_MULTIBUILDPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/cursor.h"
#include "engines/nancy/renderobject.h"
namespace Nancy {
@@ -59,21 +60,19 @@ protected:
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)
+ Common::Rect srcRect; // Source in primary image (unplaced visual)
+ Common::Rect homeRect; // Slot position in viewport coords
+ Common::Rect altSrcRect; // If non-empty: used as source for sprite creation
+ Common::Rect cuSrcRect; // Source in closeup image
+ uint8 counterByte = 0; // Non-zero: respawns on placement; doesn't count toward solve
+ uint8 mustPlace = 0; // Must be placed for solution
+ uint8 mustNotPlace = 0; // Placing this fails the solution check
+
+ Common::Rect gameRect; // Current viewport-space rect
int curRotation = 0;
bool isPlaced = false;
+ int typeIdx = -1; // -1 for original pieces; source piece index for counter clones
- // Up to 4 rotation surfaces (rotation 1-3 only if canRotateAll or altSrcRect non-zero)
Graphics::ManagedSurface rotateSurfaces[4];
bool hasSurface[4] = {};
@@ -82,49 +81,54 @@ 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
+ uint16 _requiredPieces = 0; // Minimum placed pieces (counterByte==0) for solve check
+ bool _autoSolveOnDrop = false; // If true, solve check fires after each drop
bool _canRotateAll = false;
+ bool _useRotationHotspot = false; // Rotation triggered only by clicking a hotspot rect
+ int16 _rotHotspotHeight = 0;
+ int16 _rotHotspotWidth = 0;
+ bool _allowAltZoneSnap = false; // Allow drop outside target zone if stacking on a moved piece
+ bool _checkOverlapOnDrop = false; // Reject drop if it overlaps an already-placed piece
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 _rotationSound;
SoundDescription _pickupSound;
SoundDescription _dropSound;
+ int16 _dragCursorID = 16; // Cursor for hover/drag/drop (kCustom1 by default)
+ int16 _exitCursorID1 = -1; // -1: use _puzzleExitCursor
+ int16 _exitCursorID2 = -1;
+
SceneChangeWithFlag _solveScene;
SoundDescription _solveSound;
- Common::String _solveText;
+ Common::String _solveTextKey; // Looked up in CONVO chunk first
+ Common::String _solveText; // Raw fallback used if key missing
SceneChangeWithFlag _cancelScene;
Common::Rect _exitHotspot;
- Common::Rect _targetZone; // Valid drop area (drawer/plate/etc.); pieces must be within this to solve
-
- // --- Runtime state ---
+ Common::Rect _exitHotspot2;
+ Common::Rect _targetZone; // Valid drop area (drawer/plate/...)
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
+ int16 _selectedPiece = -1; // Piece shown in closeup view, -1 if none
+ int16 _pickedUpPiece = -1; // 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
+ kIdle = 0,
+ kWaitTimer = 1,
+ kWaitSolveSound = 4,
+ kPlaySolveSound = 5
};
SolveState _solveState = kIdle;
uint32 _timerEnd = 0;
@@ -132,18 +136,22 @@ protected:
int16 _pickedUpWidth = 0;
int16 _pickedUpHeight = 0;
- // Initialization flag, used to ensure that the puzzle pieces have been initialized
- // before drawing them on screen
bool _isInitialized = false;
- // --- Internal methods ---
-
- void checkIfSolved(); // FUN_0046da47
+ void checkIfSolved();
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);
+ // Clone an existing piece at the end of _pieces (counter-piece respawn).
+ void spawnCounterPiece(int srcIdx);
+ // Drop is valid if it overhangs the top of a moved piece (sand-castle stacking).
+ bool altZoneSnapValid() const;
+ // Map data cursor id (0..21) to CursorManager::CursorType; out-of-range falls back.
+ CursorManager::CursorType cursorFromDataID(int16 id, CursorManager::CursorType fallback) const;
+ // Tests one exit hotspot and (if hovered) sets its cursor / handles the click.
+ // Returns true when the cursor is inside `hot`, so the caller can skip the
+ // other hotspot.
+ bool checkExitHotspot(const Common::Rect &hot, int16 cursorID, const NancyInput &input);
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list