[Scummvm-git-logs] scummvm master -> 19e50b7fa2d466ae2e376772db676d8bb658db91
bluegr
noreply at scummvm.org
Wed Sep 9 06:22:52 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:
19e50b7fa2 NANCY: NANCY14: Fix the tea puzzle in BuildPuzzle
Commit: 19e50b7fa2d466ae2e376772db676d8bb658db91
https://github.com/scummvm/scummvm/commit/19e50b7fa2d466ae2e376772db676d8bb658db91
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-09T09:22:43+03:00
Commit Message:
NANCY: NANCY14: Fix the tea puzzle in BuildPuzzle
- The tea puzzle has four exit hotspots (exit, teapot, recipe book and
sink), instead of just the exit hotspot
- Placing all the correct ingredients will no longer end this puzzle
immediately
Changed paths:
engines/nancy/action/puzzle/buildpuzzle.cpp
engines/nancy/action/puzzle/buildpuzzle.h
engines/nancy/util.cpp
engines/nancy/util.h
diff --git a/engines/nancy/action/puzzle/buildpuzzle.cpp b/engines/nancy/action/puzzle/buildpuzzle.cpp
index 4600897d373..74a4ec13d18 100644
--- a/engines/nancy/action/puzzle/buildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/buildpuzzle.cpp
@@ -56,11 +56,12 @@ void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
_requiredPlaced = stream.readUint16LE();
_usePlacedGate = stream.readByte();
_stateItemID = stream.readUint16LE();
- stream.skip(32); // 0x4e: two overlay rects
- SoundDescription unused;
- readSoundBlock(stream, unused);
- stream.skip(32); // 0x6e: two more overlay rects
- readSoundBlock(stream, unused);
+ readRect(stream, _submitSrcRect);
+ readRect(stream, _submitHotspot);
+ readSoundBlock(stream, _submitSound);
+ readRect(stream, _startOverSrcRect);
+ readRect(stream, _startOverHotspot);
+ readSoundBlock(stream, _startOverSound);
stream.skip(32); // 0x8e: the "done" overlay gate and its rect
readFilename(stream, _anim1Name);
@@ -152,7 +153,7 @@ void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
readSoundBlock(stream, _pickupSound);
readSoundBlock(stream, _dropSound);
readSoundBlock(stream, _notebookSound);
- readSoundBlock(stream, _resetSound);
+ readSoundBlock(stream, _putDownSound);
_wrongIngredientFlag = stream.readSint16LE();
_solvedFlag = stream.readSint16LE();
@@ -162,6 +163,7 @@ void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
_solveFlag.label = stream.readSint16LE();
_solveFlag.flag = stream.readByte();
+ SoundDescription unused;
readSoundBlock(stream, unused);
_failScene.sceneID = stream.readUint16LE();
@@ -173,8 +175,10 @@ void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
readSoundBlock(stream, unused);
// The count-prefixed 23-byte hotspot records shared by the later puzzles.
- readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
- _exitScene.continueSceneSound = kContinueSceneSound;
+ readExitHotspots(stream, _exitHotspots);
+ for (uint i = 0; i < _exitHotspots.size(); ++i) {
+ _exitHotspots[i].scene.continueSceneSound = kContinueSceneSound;
+ }
}
void BuildPuzzle::setFlagOnChange(int16 label, bool value, int8 &last) {
@@ -259,6 +263,9 @@ void BuildPuzzle::init() {
_cursorItem.setTransparent(true);
_cursorItem.setVisible(false);
+ _buttonPress.setTransparent(true);
+ _buttonPress.setVisible(false);
+
_isInitialized = true;
}
@@ -276,6 +283,7 @@ void BuildPuzzle::registerGraphics() {
}
_cursorItem.registerGraphics();
+ _buttonPress.registerGraphics();
}
byte BuildPuzzle::carriedAmount() const {
@@ -495,6 +503,7 @@ void BuildPuzzle::pickUpPiece(int16 pieceIdx) {
// exists while it is in a zone, so it goes away rather than onto the cursor.
if (piece.assignedZone != -1) {
adjustZone(piece.assignedZone, piece.sourceID, -1);
+ setPlacedCount(_placedCount - 1);
piece.assignedZone = -1;
if (pieceIdx >= (int16)_numDefined) {
@@ -578,13 +587,106 @@ void BuildPuzzle::placePiece(int16 pieceIdx, int16 zoneIdx, const Common::Point
_pieces[placedIdx].setZOrder((uint16)(_z + placedIdx + 1));
updatePieceRender(placedIdx);
+ setPlacedCount(_placedCount + 1);
+
bool solved = checkSolved();
setFlagOnChange(_solvedFlag, solved, _lastSolvedFlag);
- if (solved) {
+ // Without the gate the puzzle waits to be handed in, so a wrong mix can be
+ // thrown away first.
+ if (!_usePlacedGate) {
+ return;
+ }
+
+ if (solved && _placedCount >= (int16)_requiredPlaced) {
_isSolved = true;
_state = kActionTrigger;
+ } else if (!solved && _placedCount > (int16)_requiredPlaced) {
+ _isFailed = true;
+ _state = kActionTrigger;
+ }
+}
+
+void BuildPuzzle::setPlacedCount(int16 count) {
+ _placedCount = MAX<int16>(0, count);
+
+ if (_stateItemID != 255) {
+ TableData *table = (TableData *)NancySceneState.getPuzzleData(TableData::getTag());
+ if (table) {
+ table->setSingleValue(_stateItemID, _placedCount);
+ }
+ }
+}
+
+void BuildPuzzle::pressButton(HeldButton button) {
+ const Common::Rect &src = button == kSubmitButton ? _submitSrcRect : _startOverSrcRect;
+ const Common::Rect &dest = button == kSubmitButton ? _submitHotspot : _startOverHotspot;
+ SoundDescription &sound = button == kSubmitButton ? _submitSound : _startOverSound;
+
+ // Both buttons draw their pressed art out of the main image.
+ if (!src.isEmpty()) {
+ _buttonPress._drawSurface.create(_image, src);
+ _buttonPress.setTransparent(true);
+ _buttonPress.moveTo(dest);
+ _buttonPress.setVisible(true);
+ }
+
+ g_nancy->_sound->loadSound(sound);
+ g_nancy->_sound->playSound(sound);
+
+ _heldButton = button;
+ // Submit holds a little longer than the button that clears the board.
+ _buttonTimerEnd = g_system->getMillis() + (button == kSubmitButton ? 500 : 300);
+}
+
+void BuildPuzzle::takeOutcome() {
+ if (checkSolved()) {
+ if (_solveScene.sceneID != kNoScene) {
+ _isSolved = true;
+ _state = kActionTrigger;
+ }
+ } else if (_failScene.sceneID != kNoScene) {
+ _isFailed = true;
+ _state = kActionTrigger;
+ }
+}
+
+void BuildPuzzle::resetPuzzle() {
+ for (uint i = 0; i < _zones.size(); ++i) {
+ Zone &zone = _zones[i];
+ zone.numWrong = 0;
+ zone.numHeld = 0;
+ for (uint j = 0; j < zone.counts.size(); ++j) {
+ zone.counts[j] = 0;
+ }
+ }
+
+ for (uint i = 0; i < _holds.size(); ++i) {
+ _holds[i].setVisible(!_holds[i].srcRect.isEmpty());
+ }
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &piece = _pieces[i];
+ piece.assignedZone = -1;
+
+ // The copies made while filling the zones go away again.
+ if (i >= _numDefined) {
+ piece.inUse = false;
+ piece.setVisible(false);
+ continue;
+ }
+
+ piece.liveRect = piece.destRect;
+ updatePieceRender((int16)i);
}
+
+ _heldPiece = -1;
+ _closeupPiece = -1;
+ _activeHold = -1;
+ setPlacedCount(0);
+
+ setFlagOnChange(_solvedFlag, false, _lastSolvedFlag);
+ setFlagOnChange(_wrongIngredientFlag, false, _lastWrongFlag);
}
void BuildPuzzle::execute() {
@@ -595,14 +697,28 @@ void BuildPuzzle::execute() {
_state = kRun;
break;
case kRun:
+ if (_heldButton != kNoButton && g_system->getMillis() >= _buttonTimerEnd) {
+ HeldButton button = _heldButton;
+ _heldButton = kNoButton;
+ _buttonPress.setVisible(false);
+
+ if (button == kSubmitButton) {
+ takeOutcome();
+ } else {
+ resetPuzzle();
+ }
+ }
break;
case kActionTrigger:
if (_isSolved) {
NancySceneState.setEventFlag(_solveFlag);
NancySceneState.changeScene(_solveScene);
- } else {
- NancySceneState.setEventFlag(_exitFlag);
- NancySceneState.changeScene(_exitScene);
+ } else if (_isFailed) {
+ NancySceneState.setEventFlag(_failFlag);
+ NancySceneState.changeScene(_failScene);
+ } else if (_takenExit >= 0) {
+ NancySceneState.setEventFlag(_exitHotspots[_takenExit].flag);
+ NancySceneState.changeScene(_exitHotspots[_takenExit].scene);
}
finishExecution();
@@ -620,12 +736,38 @@ void BuildPuzzle::handleInput(NancyInput &input) {
return;
}
+ if (_heldButton != kNoButton) {
+ return;
+ }
+
Common::Point mouseVP(input.mousePos.x - viewData->screenPosition.left,
input.mousePos.y - viewData->screenPosition.top);
bool clicked = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
updateCursorItem(mouseVP);
+ // The buttons sit above the board, and submitting needs enough pieces placed.
+ if (!_startOverHotspot.isEmpty() && _startOverHotspot.contains(mouseVP)) {
+ setPieceCursor(false);
+
+ if (clicked) {
+ pressButton(kStartOverButton);
+ }
+
+ return;
+ }
+
+ if (!_submitHotspot.isEmpty() && _submitHotspot.contains(mouseVP) &&
+ _placedCount >= (int16)_requiredPlaced) {
+ setPieceCursor(false);
+
+ if (clicked) {
+ pressButton(kSubmitButton);
+ }
+
+ return;
+ }
+
// A close-up covers the board; clicking it takes the piece, except for a
// piece that is only ever there to be looked at.
if (_closeupPiece != -1) {
@@ -764,20 +906,25 @@ void BuildPuzzle::handleInput(NancyInput &input) {
return;
}
- if (_exitHotspot.isEmpty()) {
- return;
- }
+ for (uint i = 0; i < _exitHotspots.size(); ++i) {
+ const ExitHotspot &exit = _exitHotspots[i];
+ if (exit.hotspot.isEmpty() ||
+ !NancySceneState.getViewport().convertViewportToScreen(exit.hotspot).contains(input.mousePos)) {
+ continue;
+ }
- if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
- if (_exitCursorType != 0) {
- g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitCursorType, true, true);
+ if (exit.cursorType != 0) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)exit.cursorType, true, true);
} else {
g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
}
if (clicked) {
+ _takenExit = (int16)i;
_state = kActionTrigger;
}
+
+ return;
}
}
diff --git a/engines/nancy/action/puzzle/buildpuzzle.h b/engines/nancy/action/puzzle/buildpuzzle.h
index df1eb1a6577..45ffa8686ea 100644
--- a/engines/nancy/action/puzzle/buildpuzzle.h
+++ b/engines/nancy/action/puzzle/buildpuzzle.h
@@ -34,7 +34,7 @@ namespace Action {
// being assigned a zone index rather than by matching a rect.
class BuildPuzzle : public RenderActionRecord {
public:
- BuildPuzzle() : RenderActionRecord(7), _cursorItem(99) {}
+ BuildPuzzle() : RenderActionRecord(7), _buttonPress(98), _cursorItem(99) {}
virtual ~BuildPuzzle() {}
void init() override;
@@ -154,15 +154,24 @@ protected:
SoundDescription _pickupSound;
SoundDescription _dropSound;
SoundDescription _notebookSound;
- SoundDescription _resetSound;
+ SoundDescription _putDownSound;
SoundDescription _holdSound;
+ // Two on-screen buttons: one hands the puzzle in, one clears it. Only the
+ // parfait puzzle has them; the rest are handed in through an exit hotspot.
+ Common::Rect _submitSrcRect;
+ Common::Rect _submitHotspot;
+ SoundDescription _submitSound;
+ Common::Rect _startOverSrcRect;
+ Common::Rect _startOverHotspot;
+ SoundDescription _startOverSound;
+
// Both cleared when the puzzle starts from scratch.
int16 _wrongIngredientFlag = -1; // set once something not in a recipe is dropped in
int16 _solvedFlag = -1;
- // When _usePlacedGate is set, the scene only changes once this many pieces
- // have been placed.
+ // The submit button lights up at _requiredPlaced pieces. With _usePlacedGate
+ // set, dropping the last one ends the puzzle by itself instead.
uint16 _requiredPlaced = 0;
byte _usePlacedGate = 0;
uint16 _stateItemID = 255; // shared item state tracking the placed count
@@ -181,6 +190,18 @@ protected:
Graphics::ManagedSurface _pieceImage; // a kind 3 piece's own close-up art
Common::Path _pieceImageName;
+ // The button showing its pressed art, and until when.
+ enum HeldButton {
+ kNoButton = 0,
+ kSubmitButton,
+ kStartOverButton
+ };
+ HeldButton _heldButton = kNoButton;
+ uint32 _buttonTimerEnd = 0;
+ RenderObject _buttonPress;
+
+ int16 _placedCount = 0;
+
// Whatever is currently on the cursor: a scoop, or an ingredient.
RenderObject _cursorItem;
int16 _activeHold = -1;
@@ -190,7 +211,7 @@ protected:
int16 _closeupPiece = -1;
uint16 _numDefined = 0; // pieces read from the record; the rest are spare slots
bool _isSolved = false;
- bool _leaveRequested = false;
+ bool _isFailed = false;
bool _isInitialized = false;
void setPieceCursor(bool isHeld);
@@ -214,11 +235,19 @@ protected:
int16 clonePiece(int16 pieceIdx);
void updatePieceRender(int16 pieceIdx);
bool checkSolved() const;
-
- SceneChangeDescription _exitScene;
- FlagDescription _exitFlag;
- Common::Rect _exitHotspot;
- uint16 _exitCursorType = 0;
+ // Also updates the shared item state that mirrors the count
+ void setPlacedCount(int16 count);
+ // Show a button pressed; it acts once its art has been up for a moment
+ void pressButton(HeldButton button);
+ // Hand the puzzle in: the solve scene, or the fail scene when a zone is short
+ void takeOutcome();
+ // Empty every zone and put all the pieces back
+ void resetPuzzle();
+
+ // The tea puzzle has four: backing away, plus the teapot, the recipe book
+ // and the sink.
+ Common::Array<ExitHotspot> _exitHotspots;
+ int16 _takenExit = -1;
};
} // End of namespace Action
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index 2ced2153745..cd2bc0eb71d 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -406,26 +406,31 @@ Common::String resolveSubtitleText(const Common::String &keyOrText, const Common
return fallback;
}
-void readExitHotspot(Common::SeekableReadStream &stream, Common::Rect &hotspot, uint16 &cursorType,
- SceneChangeDescription &scene, FlagDescription &flag) {
+void readExitHotspots(Common::SeekableReadStream &stream, Common::Array<ExitHotspot> &hotspots) {
int16 numZones = stream.readSint16LE();
+ hotspots.resize(numZones);
for (int16 i = 0; i < numZones; ++i) {
- Common::Rect zone;
- readRect(stream, zone);
- uint16 zoneCursor = stream.readUint16LE();
- uint16 sceneID = stream.readUint16LE();
- int16 flagLabel = stream.readSint16LE();
- byte flagValue = stream.readByte();
-
- if (i == 0) {
- hotspot = zone;
- cursorType = zoneCursor;
- scene.sceneID = sceneID;
- scene.frameID = 0;
- flag.label = flagLabel;
- flag.flag = flagValue;
- }
+ ExitHotspot &zone = hotspots[i];
+ readRect(stream, zone.hotspot);
+ zone.cursorType = stream.readUint16LE();
+ zone.scene.sceneID = stream.readUint16LE();
+ zone.scene.frameID = 0;
+ zone.flag.label = stream.readSint16LE();
+ zone.flag.flag = stream.readByte();
+ }
+}
+
+void readExitHotspot(Common::SeekableReadStream &stream, Common::Rect &hotspot, uint16 &cursorType,
+ SceneChangeDescription &scene, FlagDescription &flag) {
+ Common::Array<ExitHotspot> hotspots;
+ readExitHotspots(stream, hotspots);
+
+ if (!hotspots.empty()) {
+ hotspot = hotspots[0].hotspot;
+ cursorType = hotspots[0].cursorType;
+ scene = hotspots[0].scene;
+ flag = hotspots[0].flag;
}
}
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 51ba0a6fa32..0e046498e9e 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -71,11 +71,21 @@ Common::String resolveSubtitleText(const Common::String &keyOrText, const Common
// AUTOTEXT key, falling back to the literal text when the key is not present in the table.
Common::String readSubtitleText(Common::SeekableReadStream &stream);
-// Reads the count-prefixed array of 23-byte hotspot records that the Nancy 13
-// and 14 puzzle records end with, keeping the first: the give-up hotspot. The
-// field after the scene id is an event-flag label, NOT a frame id, so the exit
-// always lands on the scene's first frame. Leaves the scene's continueSceneSound
-// alone, as the puzzles differ on it.
+// One of the 23-byte hotspot records the Nancy 13 and 14 puzzle records end with.
+struct ExitHotspot {
+ Common::Rect hotspot;
+ uint16 cursorType = 0;
+ SceneChangeDescription scene;
+ FlagDescription flag;
+};
+
+// Reads the count-prefixed array of those records. Most puzzles describe a single
+// give-up hotspot, but a record may carry several. The field after the scene id is
+// an event-flag label, NOT a frame id, so an exit always lands on the first frame.
+// Leaves continueSceneSound alone, as the puzzles differ on it.
+void readExitHotspots(Common::SeekableReadStream &stream, Common::Array<ExitHotspot> &hotspots);
+
+// Reads the same array but keeps only the first record.
void readExitHotspot(Common::SeekableReadStream &stream, Common::Rect &hotspot, uint16 &cursorType,
SceneChangeDescription &scene, FlagDescription &flag);
More information about the Scummvm-git-logs
mailing list