[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