[Scummvm-git-logs] scummvm master -> c89a8cf7a9c9c3009cc2fcf48eacc908cd000573

bluegr noreply at scummvm.org
Sun Jul 5 23:27:23 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:
c89a8cf7a9 NANCY: NANCY11: Implement changes for alchemy puzzle to OrderingPuzzle


Commit: c89a8cf7a9c9c3009cc2fcf48eacc908cd000573
    https://github.com/scummvm/scummvm/commit/c89a8cf7a9c9c3009cc2fcf48eacc908cd000573
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T02:27:06+03:00

Commit Message:
NANCY: NANCY11: Implement changes for alchemy puzzle to OrderingPuzzle

Now, the alchemy puzzle fully works

Changed paths:
    engines/nancy/action/puzzle/orderingpuzzle.cpp
    engines/nancy/action/puzzle/orderingpuzzle.h


diff --git a/engines/nancy/action/puzzle/orderingpuzzle.cpp b/engines/nancy/action/puzzle/orderingpuzzle.cpp
index d09fecf70b3..086f13b429d 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/orderingpuzzle.cpp
@@ -52,6 +52,17 @@ void OrderingPuzzle::init() {
 		_screenPosition.extend(_checkButtonDest);
 	}
 
+	// The recipe display only exists for the Nancy 11 multi-stage alchemy keypad
+	if (_puzzleType == kKeypad && _numStages > 1) {
+		for (uint i = 0; i < _mixedListDests.size(); ++i) {
+			_screenPosition.extend(_mixedListDests[i]);
+		}
+
+		for (uint i = 0; i < _currentRecipeDests.size(); ++i) {
+			_screenPosition.extend(_currentRecipeDests[i]);
+		}
+	}
+
 	g_nancy->_resource->loadImage(_imageName, _image);
 	_drawSurface.create(_screenPosition.width(), _screenPosition.height(), g_nancy->_graphics->getInputPixelFormat());
 
@@ -65,6 +76,8 @@ void OrderingPuzzle::init() {
 	_drawSurface.clear(_drawSurface.getTransparentColor());
 	setVisible(true);
 
+	drawStageDisplay();
+
 	RenderObject::init();
 }
 
@@ -240,10 +253,18 @@ void OrderingPuzzle::readData(Common::SeekableReadStream &stream) {
 				stream.skip(30 * 16);       // tighter per-button hotspots (we use _destRects)
 			} else {
 				if (g_nancy->getGameType() >= kGameTypeNancy11) {
-					// Nancy 11 multi-stage keypad: a stage count, per-stage display rects, the codes
-					// for stages 1+, a final code matrix and an alternate scene, then the button rects.
+					// Nancy 11 multi-stage keypad (the alchemy keypad): a stage count, the display rects,
+					// the codes for stages 1+, a code matrix and an alternate scene (both unused by the
+					// sequential model), then the button rects.
 					_numStages = stream.readUint16LE();
-					stream.skip(20 * 16 + 1); // per-stage display rects + blink flag
+
+					// The mixed-recipe list: as each stage is solved its small symbol is added to a vertical
+					// column on the right; the current stage's symbol is shown large at the bottom.
+					readRectArray(stream, _mixedListSrcs, 5);
+					readRectArray(stream, _mixedListDests, 5);
+					readRectArray(stream, _currentRecipeSrcs, 5);
+					readRectArray(stream, _currentRecipeDests, 5);
+					_stageDisplayBlink = (stream.readByte() != 0);
 
 					_stageSequences.resize(4);
 					_stageCheckOrder.resize(4);
@@ -257,7 +278,27 @@ void OrderingPuzzle::readData(Common::SeekableReadStream &stream) {
 						stream.skip(30 - len);
 					}
 
-					stream.skip(25 + 25); // final code matrix + alternate scene (unused by the sequential model)
+					// The 5x5 matrix holds the lethal recipes (1-based key ids, a 0 terminates a row);
+					// entering one and pressing the cauldron jumps to the death scene that follows.
+					_dangerRecipes.clear();
+					for (uint r = 0; r < 5; ++r) {
+						Common::Array<uint16> recipe;
+						bool ended = false;
+						for (uint c = 0; c < 5; ++c) {
+							byte key = stream.readByte();
+							if (!ended && key != 0) {
+								recipe.push_back(key - 1);
+							} else {
+								ended = true;
+							}
+						}
+
+						if (!recipe.empty()) {
+							_dangerRecipes.push_back(recipe);
+						}
+					}
+
+					_deathScene.readData(stream);
 				}
 				readRectArray(ser, _down1Rects, numElements, maxNumElements);
 				readRectArray(ser, _destRects, numElements, maxNumElements);
@@ -369,6 +410,44 @@ void OrderingPuzzle::execute() {
 				}
 			}
 
+			if (_puzzleType == kKeypad && _numStages > 1) {
+				// Nancy 11 alchemy keypad: mix the recipes one stage at a time. The cauldron confirms the
+				// current entry - the right ingredients blink the recipe symbol + play a chime and advance
+				// (or win on the last stage); a lethal combination jumps to the death scene; anything else
+				// just clears.
+				if (!_checkButtonPressed || g_nancy->_sound->isSoundPlaying(_pushDownSound)) {
+					return;
+				}
+
+				_checkButtonPressed = false;
+
+				// Release the cauldron button: clear the pressed sprite so the next press animates again
+				Common::Rect checkButtonRect = _checkButtonDest;
+				checkButtonRect.translate(-_screenPosition.left, -_screenPosition.top);
+				_drawSurface.fillRect(checkButtonRect, _drawSurface.getTransparentColor());
+				_needsRedraw = true;
+
+				if (enteredKeysMatchStage()) {
+					g_nancy->_sound->loadSound(_solveSound);
+					g_nancy->_sound->playSound(_solveSound);
+					_stageBlinkEndTime = g_nancy->getTotalPlayTime() + 400 + _solveSoundDelay * 1000;
+					_stageBlinkNextToggle = g_nancy->getTotalPlayTime() + 100;
+					_stageSymbolVisible = !_stageDisplayBlink;
+					drawStageDisplay();
+					_solveState = kStageBlink;
+					break;
+				}
+
+				if (enteredKeysMatchDangerRecipe()) {
+					_stageDeath = true;
+					_state = kActionTrigger;
+					break;
+				}
+
+				clearAllElements();
+				return;
+			}
+
 			bool solved = true;
 
 			if (_puzzleType != kPiano) {
@@ -462,36 +541,7 @@ void OrderingPuzzle::execute() {
 				}
 			}
 
-			if (_puzzleType == kKeypad && _numStages > 1) {
-				// Nancy 11 multi-stage keypad: every stage's code must be entered in turn.
-				// The check button confirms the current code; a wrong code resets the stage.
-				if (!_checkButtonPressed) {
-					return;
-				}
-
-				if (g_nancy->_sound->isSoundPlaying(_pushDownSound)) {
-					return;
-				}
-
-				_checkButtonPressed = false;
-
-				if (!solved) {
-					clearAllElements();
-					return;
-				}
-
-				if (_currentStage + 1 < (int)_numStages) {
-					++_currentStage;
-					_correctSequence = _stageSequences[_currentStage - 1];
-					_checkOrder = _stageCheckOrder[_currentStage - 1];
-					clearAllElements();
-					return;
-				}
-
-				// Final stage solved
-				NancySceneState.setEventFlag(_solveExitScene._flag);
-				_currentStage = 0;
-			} else if (_puzzleType == kKeypad && _needButtonToCheckSuccess) {
+			if (_puzzleType == kKeypad && _needButtonToCheckSuccess) {
 				// KeypadPuzzle moves to the "success" scene regardless whether the puzzle was solved or not,
 				// provided the check button is pressed.
 				if (_checkButtonPressed) {
@@ -546,6 +596,35 @@ void OrderingPuzzle::execute() {
 				_state = kActionTrigger;
 			}
 
+			break;
+		case kStageBlink:
+			// Blink the just-solved recipe symbol while the chime plays, then reveal the next stage
+			// (or move to the win scene after the last one).
+			if (_stageDisplayBlink && g_nancy->getTotalPlayTime() >= _stageBlinkNextToggle) {
+				_stageSymbolVisible = !_stageSymbolVisible;
+				_stageBlinkNextToggle = g_nancy->getTotalPlayTime() + 100;
+				drawStageDisplay();
+			}
+
+			if (g_nancy->getTotalPlayTime() < _stageBlinkEndTime || g_nancy->_sound->isSoundPlaying(_solveSound)) {
+				break;
+			}
+
+			_stageSymbolVisible = true;
+
+			if (_currentStage + 1 < (int)_numStages) {
+				++_currentStage;
+				_correctSequence = _stageSequences[_currentStage - 1];
+				_checkOrder = _stageCheckOrder[_currentStage - 1];
+				clearAllElements();
+				drawStageDisplay();
+				_solveState = kNotSolved;
+				break;
+			}
+
+			NancySceneState.setEventFlag(_solveExitScene._flag);
+			_currentStage = 0;
+			_state = kActionTrigger;
 			break;
 		}
 
@@ -559,7 +638,10 @@ void OrderingPuzzle::execute() {
 
 		g_nancy->_sound->stopSound(_solveSound);
 
-		if (_solveState == kNotSolved) {
+		if (_stageDeath) {
+			NancySceneState.changeScene(_deathScene._sceneChange);
+			NancySceneState.setEventFlag(_deathScene._flag);
+		} else if (_solveState == kNotSolved) {
 			_exitScene.execute();
 		} else {
 			NancySceneState.changeScene(_solveExitScene._sceneChange);
@@ -757,5 +839,91 @@ void OrderingPuzzle::clearAllElements() {
 	return;
 }
 
+void OrderingPuzzle::drawStageDisplay() {
+	if (!(_puzzleType == kKeypad && _numStages > 1) || _currentRecipeDests.empty()) {
+		return;
+	}
+
+	// The current stage's recipe symbol, shown large at the bottom (hidden on blink-off frames)
+	if (_currentStage < (int)_currentRecipeSrcs.size()) {
+		Common::Rect destRect = _currentRecipeDests[_currentStage];
+		destRect.translate(-_screenPosition.left, -_screenPosition.top);
+		if (_stageSymbolVisible) {
+			_drawSurface.blitFrom(_image, _currentRecipeSrcs[_currentStage], destRect);
+		} else {
+			_drawSurface.fillRect(destRect, _drawSurface.getTransparentColor());
+		}
+	}
+
+	// The list of already-mixed recipe symbols, stacked in the vertical column
+	for (int i = 0; i <= _currentStage && i < (int)_mixedListSrcs.size(); ++i) {
+		Common::Rect destRect = _mixedListDests[i];
+		destRect.translate(-_screenPosition.left, -_screenPosition.top);
+		_drawSurface.blitFrom(_image, _mixedListSrcs[i], destRect);
+	}
+
+	_needsRedraw = true;
+}
+
+bool OrderingPuzzle::enteredKeysMatchStage() const {
+	// The entry must be exactly the current stage's code (positional if _checkOrder, otherwise the
+	// same multiset of keys).
+	if (_clickedSequence.size() != _correctSequence.size()) {
+		return false;
+	}
+
+	if (_checkOrder) {
+		return _clickedSequence == _correctSequence;
+	}
+
+	Common::Array<uint16> pool = _clickedSequence;
+	for (uint i = 0; i < _correctSequence.size(); ++i) {
+		bool found = false;
+		for (uint j = 0; j < pool.size(); ++j) {
+			if (pool[j] == _correctSequence[i]) {
+				pool.remove_at(j);
+				found = true;
+				break;
+			}
+		}
+
+		if (!found) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool OrderingPuzzle::enteredKeysMatchDangerRecipe() const {
+	// A lethal combination is matched when the entered keys contain every key of one of the danger
+	// recipes (order and extra presses don't matter).
+	for (uint i = 0; i < _dangerRecipes.size(); ++i) {
+		const Common::Array<uint16> &recipe = _dangerRecipes[i];
+		bool matched = true;
+
+		for (uint j = 0; j < recipe.size(); ++j) {
+			bool found = false;
+			for (uint k = 0; k < _clickedSequence.size(); ++k) {
+				if (_clickedSequence[k] == recipe[j]) {
+					found = true;
+					break;
+				}
+			}
+
+			if (!found) {
+				matched = false;
+				break;
+			}
+		}
+
+		if (matched) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
 } // End of namespace Action
 } // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/orderingpuzzle.h b/engines/nancy/action/puzzle/orderingpuzzle.h
index 9d15e954b0d..c4308327a07 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.h
+++ b/engines/nancy/action/puzzle/orderingpuzzle.h
@@ -38,7 +38,7 @@ namespace Action {
 //		- KeypadPuzzleTerse: Same as above, but data format is shorter, and supports up to 100 buttons
 class OrderingPuzzle : public RenderActionRecord {
 public:
-	enum SolveState { kNotSolved, kPlaySound, kWaitForSound };
+	enum SolveState { kNotSolved, kPlaySound, kWaitForSound, kStageBlink };
 	enum PuzzleType { kOrdering, kPiano, kOrderItems, kKeypad, kKeypadTerse };
 	OrderingPuzzle(PuzzleType type) : RenderActionRecord(7), _puzzleType(type) {}
 	virtual ~OrderingPuzzle() {}
@@ -58,6 +58,9 @@ protected:
 	void setToSecondState(uint id);
 	void popUp(uint id);
 	void clearAllElements();
+	void drawStageDisplay();
+	bool enteredKeysMatchStage() const;
+	bool enteredKeysMatchDangerRecipe() const;
 
 	Common::Path _imageName;
 	bool _hasSecondState = false;
@@ -81,6 +84,24 @@ protected:
 	Common::Array<bool> _stageCheckOrder;
 	int _currentStage = 0;
 
+	// Nancy 11 recipe keypad (the alchemy keypad) display: each solved stage adds its small symbol
+	// to a vertical list, and the current stage's symbol is shown large. Both are sprites in the
+	// puzzle image, indexed by stage.
+	Common::Array<Common::Rect> _mixedListSrcs;
+	Common::Array<Common::Rect> _mixedListDests;
+	Common::Array<Common::Rect> _currentRecipeSrcs;
+	Common::Array<Common::Rect> _currentRecipeDests;
+	bool _stageDisplayBlink = false;
+	bool _stageSymbolVisible = true;
+	Time _stageBlinkEndTime;
+	Time _stageBlinkNextToggle;
+
+	// Lethal ingredient combinations: entering one and pressing the cauldron mixes a deadly potion
+	// and jumps to _deathScene (the explosion).
+	Common::Array<Common::Array<uint16> > _dangerRecipes;
+	SceneChangeWithFlag _deathScene;
+	bool _stageDeath = false;
+
 	uint16 _specialCursor1Id = CursorManager::kHotspot;
 	Common::Rect _specialCursor1Dest;
 	uint16 _specialCursor2Id = CursorManager::kHotspot;




More information about the Scummvm-git-logs mailing list