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

bluegr noreply at scummvm.org
Mon Sep 14 00:14:37 UTC 2026


This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
afe917228e NANCY: NANCY14: Implement new functionality for MatchPuzzle
891c820c98 NANCY: Refactor and simplify rotateBlit()
3ff5d3a2d1 NANCY: NANCY14: More work on HangmanPuzzle
f3e08d05d5 NANCY: Show NewSceneView item types correctly in the debugger
ca21db2694 NANCY: NANCY14: Implement new functionality for RippedLetterPuzzle
9fb7bfb678 NANCY: NANCY10: Add handling for the close on pickup inventory behavior
a88c2f18f3 NANCY: NANCY14-15: Use the correct number of items


Commit: afe917228ea1be8c7f80a601757cdbea14d45b0b
    https://github.com/scummvm/scummvm/commit/afe917228ea1be8c7f80a601757cdbea14d45b0b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:14+03:00

Commit Message:
NANCY: NANCY14: Implement new functionality for MatchPuzzle

Now, the model match game in Jane's game portal works correctly

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


diff --git a/engines/nancy/action/puzzle/matchpuzzle.cpp b/engines/nancy/action/puzzle/matchpuzzle.cpp
index 383ab386ee8..bfa2ab610c9 100644
--- a/engines/nancy/action/puzzle/matchpuzzle.cpp
+++ b/engines/nancy/action/puzzle/matchpuzzle.cpp
@@ -37,7 +37,104 @@
 namespace Nancy {
 namespace Action {
 
+static const uint kNumHighScores = 5;
+
+// How long a match stays highlighted, and how long a button stays pressed
+static const uint32 kMatchAnimTime = 800;
+static const uint32 kButtonDownTime = 250;
+// Pause on the WIN!!/TIME! message before the high score screen comes up
+static const uint32 kEndDelayTime = 2000;
+
+// The board has to sit above the viewport ornaments, which draw the panels the
+// score, target and timer are written into
+static const uint16 kNancy14ZOrder = 10;
+
+void MatchPuzzle::readDataNancy14(Common::SeekableReadStream &stream) {
+	readFilename(stream, _overlayName);
+	readFilename(stream, _buttonsImageName);
+
+	_rows = stream.readSint16LE();
+	_cols = stream.readSint16LE();
+
+	_startInactive = stream.readByte() != 0;
+	_inProgressFlag = stream.readSint16LE();
+	stream.skip(2);
+
+	readRect(stream, _doneButtonSrcRect);
+	readRect(stream, _doneButtonDestRect);
+	readRect(stream, _shuffleButtonSrcRect);
+	readRect(stream, _shuffleButtonDestRect);
+
+	_gridOffX = stream.readSint16LE();
+	_gridOffY = stream.readSint16LE();
+	_rowSpacing = stream.readSint16LE();
+	_colSpacing = stream.readSint16LE();
+
+	_fontID = stream.readUint16LE();
+	_fontColor = stream.readUint16LE();
+
+	readFilename(stream, _timerSuffix);
+	readFilename(stream, _winString);
+	readFilename(stream, _timeUpString);
+
+	readRect(stream, _scoreValueRect);
+	readRect(stream, _goalValueRect);
+	readRect(stream, _timerValueRect);
+	readRect(stream, _highScoreButtonRect);
+
+	_timeLimitSecs = stream.readSint16LE();
+	_scorePerTile = stream.readSint16LE();
+	_timeBonusFor3 = stream.readSint16LE();
+	_scoreBonusFor4 = stream.readSint16LE();
+	_timeBonusFor4 = stream.readSint16LE();
+	_scoreBonusFor5 = stream.readSint16LE();
+	_timeBonusFor5 = stream.readSint16LE();
+	_defaultScoreTarget = stream.readSint32LE();
+
+	readRect(stream, _matchedTileSrcRect);
+
+	_numTileTypes = stream.readSint16LE();
+	readRectArray(stream, _tileSrcRects, _numTileTypes);
+
+	readFilename(stream, _highScoreImageName);
+	readFilename(stream, _playerName);
+
+	readRectArray(stream, _highScoreRects, kNumHighScores);
+
+	_highScores.resize(kNumHighScores);
+	for (uint i = 0; i < kNumHighScores; ++i) {
+		readFilename(stream, _highScores[i].name);
+		_highScores[i].score = stream.readSint32LE();
+	}
+
+	_matchSound.readData(stream);
+	_selectSound.readData(stream);
+	_swapSound.readData(stream);
+	_winSound.readData(stream);
+	_timeUpSound.readData(stream);
+	_goButtonSound.readData(stream);
+
+	_solveSceneChange._sceneChange.sceneID = stream.readUint16LE();
+	_solveSceneChange._sceneChange.frameID = stream.readUint16LE();
+	_solveSceneChange._flag.label = stream.readSint16LE();
+	_solveSceneChange._flag.flag = stream.readByte();
+
+	_exitCursorType = stream.readUint16LE();
+	_exitSceneChange._sceneChange.sceneID = stream.readUint16LE();
+	_exitSceneChange._sceneChange.frameID = stream.readUint16LE();
+
+	readRect(stream, _exitHotspot);
+
+	_doneSceneChange._sceneChange.sceneID = stream.readUint16LE();
+	_doneSceneChange._sceneChange.frameID = stream.readUint16LE();
+}
+
 void MatchPuzzle::readData(Common::SeekableReadStream &stream) {
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		readDataNancy14(stream);
+		return;
+	}
+
 	// data+0x00..0x20  main sprite sheet name
 	readFilename(stream, _overlayName);
 	// data+0x21..0x41  score-panel background name
@@ -45,14 +142,14 @@ void MatchPuzzle::readData(Common::SeekableReadStream &stream) {
 
 	_rows         = stream.readSint16LE();  // data+0x42
 	_cols         = stream.readSint16LE();  // data+0x44
-	_numFlagTypes = stream.readSint16LE();  // data+0x46
+	_numTileTypes = stream.readSint16LE();  // data+0x46
 
 	readRect(stream, _shuffleButtonSrcRect);   // data+0x48..0x57 (source rect in sprite sheet)
 
-	_flagSrcRects.resize(26);
+	_tileSrcRects.resize(26);
 
 	for (int i = 0; i < 26; ++i)
-		readRect(stream, _flagSrcRects[i]); // data+0x58..0x1F7 (source rects in sprite sheet)
+		readRect(stream, _tileSrcRects[i]); // data+0x58..0x1F7 (source rects in sprite sheet)
 
 	// data+0x1F8..0x237 — 64 bytes unused (all zeros)
 	stream.skip(0x40);
@@ -78,9 +175,9 @@ void MatchPuzzle::readData(Common::SeekableReadStream &stream) {
 	_showScoreDisplay = stream.readByte() != 0;
 	_timeLimitSecs  = stream.readSint16LE();          // data+0x63E
 	_scoreTarget    = stream.readSint32LE();          // data+0x640
-	_scorePerFlag   = stream.readSint16LE();          // data+0x644
+	_scorePerTile   = stream.readSint16LE();          // data+0x644
 
-	readRect(stream, _matchedFlagSrcRect);            // data+0x646..0x655 matched/highlight src rect
+	readRect(stream, _matchedTileSrcRect);            // data+0x646..0x655 matched/highlight src rect
 
 	_timeBonusFor3  = stream.readSint16LE();          // data+0x656 (seconds)
 	_scoreBonusFor4 = stream.readSint16LE();          // data+0x658
@@ -136,6 +233,16 @@ void MatchPuzzle::init() {
 		_scorePanelImage.setTransparentColor(_drawSurface.getTransparentColor());
 	}
 
+	if (!_buttonsImageName.empty()) {
+		g_nancy->_resource->loadImage(_buttonsImageName, _buttonsImage);
+		_buttonsImage.setTransparentColor(_drawSurface.getTransparentColor());
+	}
+
+	if (!_highScoreImageName.empty()) {
+		g_nancy->_resource->loadImage(_highScoreImageName, _highScoreImage);
+		_highScoreImage.setTransparentColor(_drawSurface.getTransparentColor());
+	}
+
 	// Build grid — compute dest rects; cells will be filled by shuffleGrid()
 	_grid.resize(_cols);
 	for (int col = 0; col < _cols; ++col) {
@@ -144,6 +251,30 @@ void MatchPuzzle::init() {
 			computeDestRect(col, row);
 	}
 
+	if (_highScores.empty())
+		_highScores.resize(kNumHighScores);
+
+	sortHighScores();
+
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		setZOrder(kNancy14ZOrder);
+
+		// The target is whatever tops the high score list, and it has to be beaten outright
+		_scoreTarget = _highScores[0].score > 0 ? _highScores[0].score : _defaultScoreTarget;
+		_goalStr = Common::String::format("%d", _scoreTarget);
+		startRound();
+
+		if (_startInactive) {
+			_showHighScores = true;
+			_canResumeGame = true;
+			_gameSubState = kHighScores;
+		}
+
+		NancySceneState.setEventFlag(_inProgressFlag, _startInactive ? g_nancy->_true : g_nancy->_false);
+		redrawAllCells();
+		return;
+	}
+
 	// Initialise display strings
 	_goalStr  = Common::String::format("%d", _scoreTarget);
 	_scoreStr = Common::String::format("%d", (int32)0);
@@ -154,6 +285,78 @@ void MatchPuzzle::init() {
 	redrawAllCells();
 }
 
+void MatchPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+	if (block.names.empty())
+		return;
+
+	uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+	const Common::String &name = block.names[idx];
+	if (name.empty() || name == "NO SOUND")
+		return;
+
+	SoundDescription desc;
+	desc.name = name;
+	desc.channelID = block.channel;
+	desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+	desc.volume = block.volume;
+
+	g_nancy->_sound->loadSound(desc);
+	g_nancy->_sound->playSound(desc);
+}
+
+bool MatchPuzzle::isSoundBlockPlaying(const RandomSoundBlock &block) const {
+	return !block.names.empty() && g_nancy->_sound->isSoundPlaying((uint16)block.channel);
+}
+
+void MatchPuzzle::playMatchSound() {
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		playSoundBlock(_matchSound);
+	} else if (_slotWinSound.name != "NO SOUND") {
+		g_nancy->_sound->playSound(_slotWinSound);
+	}
+}
+
+bool MatchPuzzle::isMatchSoundPlaying() const {
+	if (g_nancy->getGameType() >= kGameTypeNancy14)
+		return isSoundBlockPlaying(_matchSound);
+
+	return g_nancy->_sound->isSoundPlaying(_slotWinSound);
+}
+
+// Reset score and timer and deal a fresh board
+void MatchPuzzle::startRound() {
+	_score = 0;
+	_scoreStr = Common::String::format("%d", _score);
+	_timerStr.clear();
+	_prevTimerSecs = -1;
+	_hasPiece1 = _hasPiece2 = false;
+	_hasSelection = false;
+	_timerDeadline = g_system->getMillis() + (uint32)_timeLimitSecs * 1000;
+	shuffleGrid(true);
+}
+
+void MatchPuzzle::sortHighScores() {
+	for (uint i = 0; i + 1 < _highScores.size(); ++i)
+		for (uint j = 0; j + 1 < _highScores.size() - i; ++j)
+			if (_highScores[j].score < _highScores[j + 1].score)
+				SWAP(_highScores[j], _highScores[j + 1]);
+}
+
+// Insert the score just achieved into the list, pushing the rest down
+void MatchPuzzle::insertHighScore() {
+	for (uint i = 0; i < _highScores.size(); ++i) {
+		if (_highScores[i].score >= _score)
+			continue;
+
+		for (uint j = _highScores.size() - 1; j > i; --j)
+			_highScores[j] = _highScores[j - 1];
+
+		_highScores[i].name = _playerName;
+		_highScores[i].score = _score;
+		return;
+	}
+}
+
 void MatchPuzzle::execute() {
 	switch (_state) {
 	case kBegin:
@@ -171,12 +374,16 @@ void MatchPuzzle::execute() {
 		_wonGame      = false;
 		_hasPiece1 = _hasPiece2 = false;
 		_hasSelection = false;
-		_gameSubState = kPlaying;
 		_showFlagName = false;
 		_prevTimerSecs = -1;
 
-		if (_timeLimitSecs > 0)
-			_timerDeadline = g_system->getMillis() + (uint32)_timeLimitSecs * 1000;
+		// init() already put Nancy14 into its starting substate
+		if (g_nancy->getGameType() < kGameTypeNancy14) {
+			_gameSubState = kPlaying;
+
+			if (_timeLimitSecs > 0)
+				_timerDeadline = g_system->getMillis() + (uint32)_timeLimitSecs * 1000;
+		}
 
 		_state = kRun;
 		// fall through
@@ -194,7 +401,10 @@ void MatchPuzzle::execute() {
 				int secs = remainMs / 1000;
 				if (secs != _prevTimerSecs) {
 					_prevTimerSecs = secs;
-					_timerStr = Common::String::format("%2dm %2ds", secs / 60, secs % 60);
+					if (g_nancy->getGameType() >= kGameTypeNancy14)
+						_timerStr = Common::String::format("%2d%s", secs, _timerSuffix.c_str());
+					else
+						_timerStr = Common::String::format("%2dm %2ds", secs / 60, secs % 60);
 					redrawAllCells();
 				}
 			}
@@ -210,9 +420,8 @@ void MatchPuzzle::execute() {
 						_showFlagName = true;
 					}
 					_scoreStr = Common::String::format("%d", _score);
-					if (_slotWinSound.name != "NO SOUND")
-						g_nancy->_sound->playSound(_slotWinSound);
-					_stateTimer   = now + 800;
+					playMatchSound();
+					_stateTimer   = now + kMatchAnimTime;
 					_gameSubState = kMatchAnim;
 					redrawAllCells();
 				}
@@ -230,9 +439,8 @@ void MatchPuzzle::execute() {
 						_showFlagName = true;
 					}
 					_scoreStr = Common::String::format("%d", _score);
-					if (_slotWinSound.name != "NO SOUND")
-						g_nancy->_sound->playSound(_slotWinSound);
-					_stateTimer   = now + 800;
+					playMatchSound();
+					_stateTimer   = now + kMatchAnimTime;
 					_gameSubState = kMatchAnim;
 					redrawAllCells();
 				}
@@ -241,6 +449,24 @@ void MatchPuzzle::execute() {
 
 			// Neither pending: check win/lose conditions
 			bool timerExpired  = (_timeLimitSecs > 0) && ((int32)(_timerDeadline - now) < 500);
+
+			if (g_nancy->getGameType() >= kGameTypeNancy14) {
+				// The score is only judged when the clock runs out, and the
+				// high score at the top of the list has to be beaten outright
+				if (timerExpired) {
+					_wonGame = _score > _scoreTarget;
+					_timerStr = _wonGame ? _winString : _timeUpString;
+					_hasSelection = false;
+
+					playSoundBlock(_wonGame ? _winSound : _timeUpSound);
+
+					_stateTimer = now + kEndDelayTime;
+					_gameSubState = kEndDelay;
+					redrawAllCells();
+				}
+				break;
+			}
+
 			bool reachedTarget = (_score >= _scoreTarget);
 
 			if (timerExpired || reachedTarget) {
@@ -270,7 +496,7 @@ void MatchPuzzle::execute() {
 			// inside the window — which is why a match makes three boops.
 			uint32 now = g_system->getMillis();
 			bool timerDone = (now >= _stateTimer);
-			bool soundDone = !g_nancy->_sound->isSoundPlaying(_slotWinSound);
+			bool soundDone = !isMatchSoundPlaying();
 
 			if (timerDone && soundDone) {
 				// Reshuffle only the cells that were part of the match
@@ -283,10 +509,10 @@ void MatchPuzzle::execute() {
 				_showFlagName = false;
 				redrawAllCells();
 				_gameSubState = kPlaying;
-			} else if (soundDone && _slotWinSound.name != "NO SOUND") {
+			} else if (soundDone) {
 				// Sound has finished but the match-anim window hasn't
 				// closed yet — replay it for the next "boop".
-				g_nancy->_sound->playSound(_slotWinSound);
+				playMatchSound();
 			}
 			break;
 		}
@@ -322,8 +548,8 @@ void MatchPuzzle::execute() {
 			// Insert current score into the top-5 high score list (descending)
 			int32 toInsert = _score;
 			for (int i = 0; i < 5; ++i) {
-				if (_highScores[i] < toInsert)
-					SWAP(_highScores[i], toInsert);
+				if (_highScores[i].score < toInsert)
+					SWAP(_highScores[i].score, toInsert);
 			}
 
 			if (_wonGame) {
@@ -345,6 +571,49 @@ void MatchPuzzle::execute() {
 			break;
 		}
 
+		case kButtonDown: { // Nancy14: a button is held down for a moment before it acts
+			if (g_system->getMillis() < _stateTimer)
+				break;
+
+			if (_shuffleButtonDown) {
+				_shuffleButtonDown = false;
+				_hasSelection = false;
+				shuffleGrid(true);
+			} else {
+				_doneButtonDown = false;
+
+				playSoundBlock(_goButtonSound);
+
+				_showHighScores = false;
+				NancySceneState.setEventFlag(_inProgressFlag, g_nancy->_false);
+
+				if (_canResumeGame) {
+					_canResumeGame = false;
+					startRound();
+				}
+			}
+
+			_gameSubState = kPlaying;
+			redrawAllCells();
+			break;
+		}
+
+		case kEndDelay: { // Nancy14: hold the win/time-up message, then show the high scores
+			if (g_system->getMillis() < _stateTimer)
+				break;
+
+			insertHighScore();
+			_showHighScores = true;
+			_canResumeGame = true;
+			_gameSubState = kHighScores;
+			NancySceneState.setEventFlag(_inProgressFlag, g_nancy->_true);
+			redrawAllCells();
+			break;
+		}
+
+		case kHighScores: // Nancy14: waiting for the GO button
+			break;
+
 		default:
 			break;
 		}
@@ -358,6 +627,8 @@ void MatchPuzzle::execute() {
 
 		if (_wonGame)
 			_solveSceneChange.execute();
+		else if (_leftThroughButton)
+			_doneSceneChange.execute();
 		else
 			_exitSceneChange.execute();
 
@@ -375,11 +646,20 @@ void MatchPuzzle::handleInput(NancyInput &input) {
 	localMouse -= Common::Point(vpPos.left, vpPos.top);
 
 	if (!_exitHotspot.isEmpty() && _exitHotspot.contains(localMouse)) {
-		g_nancy->_cursor->setCursorType(CursorManager::kMoveBackward);
+		if (g_nancy->getGameType() >= kGameTypeNancy14)
+			g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitCursorType, true);
+		else
+			g_nancy->_cursor->setCursorType(CursorManager::kMoveBackward);
+
 		if (input.input & NancyInput::kLeftMouseButtonUp)
 			_state = kActionTrigger;
 	}
 
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		handleInputNancy14(input, localMouse);
+		return;
+	}
+
 	if (_gameSubState != kPlaying)
 		return;
 
@@ -418,8 +698,8 @@ void MatchPuzzle::handleInput(NancyInput &input) {
 						redrawAllCells();
 					} else {
 						// Second click: swap the two flags and queue both for match-check
-						SWAP(_grid[_selCol][_selRow].flagType,
-						     _grid[col][row].flagType);
+						SWAP(_grid[_selCol][_selRow].tileType,
+						     _grid[col][row].tileType);
 
 						_piece1Col = _selCol;
 						_piece1Row = _selRow;
@@ -440,26 +720,131 @@ void MatchPuzzle::handleInput(NancyInput &input) {
 	}
 }
 
+void MatchPuzzle::handleInputNancy14(NancyInput &input, const Common::Point &localMouse) {
+	if (_state != kRun)
+		return;
+
+	if (_gameSubState != kPlaying && _gameSubState != kHighScores)
+		return;
+
+	// The button next to the board doubles as GO on the high score screen: it
+	// leaves the puzzle while a round is running or once the target was beaten,
+	// and dismisses the high scores otherwise
+	if (_doneButtonDestRect.contains(localMouse)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_leftThroughButton = true;
+
+			if (!_showHighScores || _wonGame) {
+				_state = kActionTrigger;
+			} else {
+				_doneButtonDown = true;
+				_stateTimer = g_system->getMillis() + kButtonDownTime;
+				_gameSubState = kButtonDown;
+				redrawAllCells();
+			}
+		}
+		input.eatMouseInput();
+		return;
+	}
+
+	// Brings up the high score list mid-game; GO puts the board back
+	if (_gameSubState != kHighScores && _highScoreButtonRect.contains(localMouse)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_showHighScores = true;
+			_gameSubState = kHighScores;
+			NancySceneState.setEventFlag(_inProgressFlag, g_nancy->_true);
+			redrawAllCells();
+		}
+		input.eatMouseInput();
+		return;
+	}
+
+	if (_gameSubState != kPlaying)
+		return;
+
+	if (_shuffleButtonDestRect.contains(localMouse)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			playSoundBlock(_swapSound);
+
+			_shuffleButtonDown = true;
+			_stateTimer = g_system->getMillis() + kButtonDownTime;
+			_gameSubState = kButtonDown;
+			redrawAllCells();
+		}
+		input.eatMouseInput();
+		return;
+	}
+
+	for (int col = 0; col < _cols; ++col) {
+		for (int row = 0; row < _rows; ++row) {
+			GridCell &cell = _grid[col][row];
+			if (!cell.visible)
+				continue;
+
+			// The clickable area is inset slightly from the tile
+			Common::Rect hotspot = cell.destRect;
+			hotspot.grow(-3);
+			if (!hotspot.contains(localMouse))
+				continue;
+
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				if (!_hasSelection) {
+					_selCol = col;
+					_selRow = row;
+					_hasSelection = true;
+
+					playSoundBlock(_selectSound);
+				} else {
+					playSoundBlock(_swapSound);
+
+					SWAP(_grid[_selCol][_selRow].tileType, cell.tileType);
+
+					_piece1Col = _selCol;
+					_piece1Row = _selRow;
+					_piece2Col = col;
+					_piece2Row = row;
+					_hasPiece1 = _hasPiece2 = true;
+					_hasSelection = false;
+					redrawAllCells();
+				}
+			}
+
+			input.eatMouseInput();
+			return;
+		}
+	}
+}
+
 void MatchPuzzle::shuffleGrid(bool allCells, int targetCol, int targetRow) {
-	// Valid flag indices are 0 .. (_numFlagTypes - 2) inclusive
-	int numTypes = (_numFlagTypes > 1) ? (_numFlagTypes - 1) : 1;
+	// Valid flag indices are 0 .. (_numTileTypes - 2) inclusive
+	int numTypes = (_numTileTypes > 1) ? (_numTileTypes - 1) : 1;
 
 	for (int row = 0; row < _rows; ++row) {
 		for (int col = 0; col < _cols; ++col) {
 			if (!allCells && (col != targetCol || row != targetRow))
 				continue;
 
-			// Pick a random type that doesn't match its above or left neighbour
+			// Pick a random type that doesn't match its above or left neighbour.
+			// A single tile dealt back into a finished match also has to avoid
+			// the tiles below and to the right of it, so it can't match again
+			// on the spot.
+			bool checkAllNeighbors = !allCells && g_nancy->getGameType() >= kGameTypeNancy14;
 			int16 chosen = 0;
 			for (int attempt = 0; attempt < 100; ++attempt) {
 				chosen = (int16)(g_nancy->_randomSource->getRandomNumber(numTypes - 1));
-				bool sameAbove = (row > 0) && (chosen == _grid[col][row - 1].flagType);
-				bool sameLeft  = (col > 0) && (chosen == _grid[col - 1][row].flagType);
-				if (!sameAbove && !sameLeft)
+				bool sameAbove = (row > 0) && (chosen == _grid[col][row - 1].tileType);
+				bool sameLeft  = (col > 0) && (chosen == _grid[col - 1][row].tileType);
+				bool sameBelow = checkAllNeighbors && (row < _rows - 1) && (chosen == _grid[col][row + 1].tileType);
+				bool sameRight = checkAllNeighbors && (col < _cols - 1) && (chosen == _grid[col + 1][row].tileType);
+				if (!sameAbove && !sameLeft && !sameBelow && !sameRight)
 					break;
 			}
 
-			_grid[col][row].flagType = chosen;
+			_grid[col][row].tileType = chosen;
 			_grid[col][row].visible  = true;
 			_grid[col][row].matched  = false;
 		}
@@ -474,20 +859,20 @@ void MatchPuzzle::checkForMatch(int col, int row) {
 	if (!_grid[col][row].visible)
 		return;
 
-	int16 type = _grid[col][row].flagType;
+	int16 type = _grid[col][row].tileType;
 	_matchedFlagType = type;
 
 	// --- Vertical run (fixed column, walk along rows) ---
 	int rStart = row, rEnd = row;
-	while (rStart > 0        && _grid[col][rStart - 1].flagType == type) --rStart;
-	while (rEnd   < _rows - 1 && _grid[col][rEnd   + 1].flagType == type) ++rEnd;
+	while (rStart > 0        && _grid[col][rStart - 1].tileType == type) --rStart;
+	while (rEnd   < _rows - 1 && _grid[col][rEnd   + 1].tileType == type) ++rEnd;
 	_matchRowStart = rStart;
 	_matchRowEnd   = rEnd;
 
 	// --- Horizontal run (fixed row, walk along cols) ---
 	int cStart = col, cEnd = col;
-	while (cStart > 0        && _grid[cStart - 1][row].flagType == type) --cStart;
-	while (cEnd   < _cols - 1 && _grid[cEnd   + 1][row].flagType == type) ++cEnd;
+	while (cStart > 0        && _grid[cStart - 1][row].tileType == type) --cStart;
+	while (cEnd   < _cols - 1 && _grid[cEnd   + 1][row].tileType == type) ++cEnd;
 	_matchColStart = cStart;
 	_matchColEnd   = cEnd;
 
@@ -499,7 +884,7 @@ void MatchPuzzle::checkForMatch(int col, int row) {
 		_hasVMatch = true;
 		for (int r = rStart; r <= rEnd; ++r) {
 			_grid[col][r].matched = true;
-			_score += _scorePerFlag;
+			_score += _scorePerTile;
 		}
 		if (vLen == 2)
 			_timerDeadline += (uint32)_timeBonusFor3 * 1000;
@@ -517,7 +902,7 @@ void MatchPuzzle::checkForMatch(int col, int row) {
 		_hasHMatch = true;
 		for (int c = cStart; c <= cEnd; ++c) {
 			_grid[c][row].matched = true;
-			_score += _scorePerFlag;
+			_score += _scorePerTile;
 		}
 		if (hLen == 2)
 			_timerDeadline += (uint32)_timeBonusFor3 * 1000;
@@ -530,17 +915,18 @@ void MatchPuzzle::checkForMatch(int col, int row) {
 		}
 	}
 
-	if (_score > _scoreTarget)
+	// Nancy14 has to be able to overshoot the target to beat it
+	if (g_nancy->getGameType() < kGameTypeNancy14 && _score > _scoreTarget)
 		_score = _scoreTarget;
 }
 
 void MatchPuzzle::computeDestRect(int col, int row) {
-	if (_flagSrcRects.empty())
+	if (_tileSrcRects.empty())
 		return;
 
 	// Cell size taken from the first flag rect (all flags are the same size)
-	int cellW = _flagSrcRects[0].width() - 1;
-	int cellH = _flagSrcRects[0].height() - 1;
+	int cellW = _tileSrcRects[0].width() - 1;
+	int cellH = _tileSrcRects[0].height() - 1;
 
 	// Column position: extra spacing per col + cell width
 	int left = col * (_colSpacing + cellW) + _gridOffX;
@@ -557,12 +943,12 @@ void MatchPuzzle::drawCell(int col, int row) {
 	if (!cell.visible)
 		return;
 
-	int type = cell.flagType;
-	if (type < 0 || type >= (int)_flagSrcRects.size())
+	int type = cell.tileType;
+	if (type < 0 || type >= (int)_tileSrcRects.size())
 		return;
 
 	// Draw matched cells with the highlight source rect ("50" graphic); others with their normal rect
-	const Common::Rect &srcRect = cell.matched ? _matchedFlagSrcRect : _flagSrcRects[type];
+	const Common::Rect &srcRect = cell.matched ? _matchedTileSrcRect : _tileSrcRects[type];
 	_drawSurface.blitFrom(_image, srcRect,
 	                      Common::Point(cell.destRect.left, cell.destRect.top));
 	_needsRedraw = true;
@@ -574,6 +960,62 @@ void MatchPuzzle::eraseCell(int col, int row) {
 	_needsRedraw = true;
 }
 
+// The original anchors text at the bottom row of the glyphs, ScummVM at the top of the line
+void MatchPuzzle::drawText(const Common::String &str, const Common::Point &pos) {
+	if (str.empty())
+		return;
+
+	const Graphics::Font *font = g_nancy->_graphics->getFont(_fontID);
+	if (!font)
+		font = g_nancy->_graphics->getFont(0);
+
+	if (!font)
+		return;
+
+	int y = pos.y - font->getFontHeight() + 1;
+	font->drawString(&_drawSurface, str, pos.x, y, _drawSurface.w - pos.x, _fontColor);
+	_needsRedraw = true;
+}
+
+void MatchPuzzle::drawHighScoreScreen() {
+	if (!_highScoreImage.empty())
+		_drawSurface.blitFrom(_highScoreImage, Common::Point(0, 0));
+
+	for (uint i = 0; i < _highScoreRects.size() && i < _highScores.size(); ++i) {
+		const Common::Rect &rect = _highScoreRects[i];
+		drawText(_highScores[i].name, Common::Point(rect.left, rect.top));
+		drawText(Common::String::format("%d", _highScores[i].score),
+		         Common::Point(rect.right, rect.bottom));
+	}
+
+	_needsRedraw = true;
+}
+
+void MatchPuzzle::drawBoardNancy14() {
+	for (int col = 0; col < _cols; ++col)
+		for (int row = 0; row < _rows; ++row)
+			drawCell(col, row);
+
+	// An empty score rect turns the whole score/target readout off
+	if (!_scoreValueRect.isEmpty()) {
+		drawText(_scoreStr, Common::Point(_scoreValueRect.left, _scoreValueRect.bottom));
+		drawText(_goalStr, Common::Point(_goalValueRect.left, _goalValueRect.bottom));
+	}
+
+	if (_timeLimitSecs > 0)
+		drawText(_timerStr, Common::Point(_timerValueRect.left, _timerValueRect.bottom));
+
+	// The buttons live in the scene background; only their pressed state is drawn
+	if (_shuffleButtonDown)
+		_drawSurface.blitFrom(_buttonsImage, _shuffleButtonSrcRect,
+		                      Common::Point(_shuffleButtonDestRect.left, _shuffleButtonDestRect.top));
+	else if (_doneButtonDown)
+		_drawSurface.blitFrom(_buttonsImage, _doneButtonSrcRect,
+		                      Common::Point(_doneButtonDestRect.left, _doneButtonDestRect.top));
+
+	_needsRedraw = true;
+}
+
 void MatchPuzzle::drawScorePanel() {
 	// ---- State 6: final score / high-score screen ----
 	if (_gameSubState == kScoreDisplay) {
@@ -601,7 +1043,7 @@ void MatchPuzzle::drawScorePanel() {
 				// High-score list: entries start one lineSpacing below the final score
 				int hsY = scoreY + lineSpacing;
 				for (int i = 0; i < 5; ++i) {
-					Common::String hs = Common::String::format("%d", _highScores[i]);
+					Common::String hs = Common::String::format("%d", _highScores[i].score);
 					font->drawString(&_drawSurface, hs, hsX, hsY, 80, 0);
 					hsY += lineSpacing;
 				}
@@ -663,14 +1105,24 @@ void MatchPuzzle::drawScorePanel() {
 			                 _flagNameRect.width(), 0);
 
 		int16 ft = _matchedFlagType;
-		if (ft >= 0 && ft < (int16)_flagSrcRects.size() && !_flagImageRect.isEmpty())
-			_drawSurface.blitFrom(_image, _flagSrcRects[ft],
+		if (ft >= 0 && ft < (int16)_tileSrcRects.size() && !_flagImageRect.isEmpty())
+			_drawSurface.blitFrom(_image, _tileSrcRects[ft],
 			                      Common::Point(_flagImageRect.left, _flagImageRect.top));
 	}
 }
 
 void MatchPuzzle::redrawAllCells() {
 	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		if (_showHighScores)
+			drawHighScoreScreen();
+		else
+			drawBoardNancy14();
+
+		return;
+	}
+
 	drawScorePanel();
 	// During state 6 the score-screen covers everything; skip cell drawing
 	if (_gameSubState != kScoreDisplay) {
diff --git a/engines/nancy/action/puzzle/matchpuzzle.h b/engines/nancy/action/puzzle/matchpuzzle.h
index 60f36a45d0b..1dfa8996558 100644
--- a/engines/nancy/action/puzzle/matchpuzzle.h
+++ b/engines/nancy/action/puzzle/matchpuzzle.h
@@ -34,10 +34,10 @@
 namespace Nancy {
 namespace Action {
 
-// Maritime Flag matching puzzle in Nancy 8.
-// The player spots 3/4/5 flags of the same type in a row or column and clicks
-// one to score points and extend the timer.  After every match the board is
-// reshuffled.  The game ends when the score target is reached.
+// Tile matching puzzle. Nancy 8 uses it for a maritime flag game, Nancy 14 for
+// "Model Match". The player swaps two neighbouring tiles to line up 3/4/5 of the
+// same type in a row or column, which scores points and extends the timer. The
+// matched tiles are then replaced with fresh random ones.
 class MatchPuzzle : public RenderActionRecord {
 public:
 	MatchPuzzle() : RenderActionRecord(7) {}
@@ -56,8 +56,13 @@ protected:
 
 	// ---------- Inner types ----------
 
+	struct HighScore {
+		Common::String name;
+		int32 score = 0;
+	};
+
 	struct GridCell {
-		int16 flagType = 0;       // index into _flagSrcRects / _flagSoundNames
+		int16 tileType = 0;       // index into _tileSrcRects
 		bool  visible  = false;   // true once the cell has been shuffled in
 		bool  matched  = false;   // true while cell is part of an active match
 		Common::Rect destRect;    // viewport-relative draw destination
@@ -77,6 +82,20 @@ protected:
 	void eraseCell(int col, int row);
 	void redrawAllCells();
 	void drawScorePanel();
+	void drawText(const Common::String &str, const Common::Point &pos);
+	void playMatchSound();
+	bool isMatchSoundPlaying() const;
+
+	// Nancy14 helpers
+	void readDataNancy14(Common::SeekableReadStream &stream);
+	void handleInputNancy14(NancyInput &input, const Common::Point &localMouse);
+	void drawHighScoreScreen();
+	void drawBoardNancy14();
+	void playSoundBlock(const RandomSoundBlock &block);
+	bool isSoundBlockPlaying(const RandomSoundBlock &block) const;
+	void startRound();
+	void sortHighScores();
+	void insertHighScore();
 
 	// ---------- Data (read from stream) ----------
 
@@ -85,11 +104,11 @@ protected:
 
 	int16 _rows         = 0;             // data+0x42
 	int16 _cols         = 0;             // data+0x44
-	int16 _numFlagTypes = 0;             // data+0x46  (rand % (_numFlagTypes-1))
+	int16 _numTileTypes = 0;             // data+0x46  (rand % (_numTileTypes-1))
 
 	// data+0x48: source rect of the shuffle button within the sprite sheet
 	Common::Rect _shuffleButtonSrcRect;
-	Common::Array<Common::Rect> _flagSrcRects;   // 26 source rects in sprite sheet
+	Common::Array<Common::Rect> _tileSrcRects;   // 26 source rects in sprite sheet
 
 	// Script execution (data+0x238..0x23A); _execScript also gates flag-name display
 	bool  _execScript = false;
@@ -108,10 +127,10 @@ protected:
 	// Timing / scoring (from data+0x63E region)
 	int16 _timeLimitSecs     = 0;   // data+0x63E (0 = no timer)
 	int32 _scoreTarget       = 0;   // data+0x640
-	int16 _scorePerFlag      = 0;   // data+0x644 points per matched flag
+	int16 _scorePerTile      = 0;   // data+0x644 points per matched flag
 
 	// Source rect for highlighted (matched) flag overlay (data+0x646)
-	Common::Rect _matchedFlagSrcRect;
+	Common::Rect _matchedTileSrcRect;
 
 	int16 _timeBonusFor3     = 0;   // data+0x656 extra seconds for 3-match
 	int16 _scoreBonusFor4    = 0;   // data+0x658 extra points  for 4-match
@@ -153,6 +172,48 @@ protected:
 
 	Common::Rect _exitHotspot;             // data+0x7E2  bottom-strip exit hotspot
 
+	// ---------- Nancy14-only data ----------
+
+	// Second sprite sheet, holding the pressed-down graphics of the two buttons
+	Common::Path _buttonsImageName;
+	// Full-screen backdrop of the high score list
+	Common::Path _highScoreImageName;
+
+	// Set while the puzzle waits on the high score screen instead of playing
+	int16 _inProgressFlag = kEvNoEvent;
+	// When set, the board starts frozen on the high score screen
+	bool _startInactive = false;
+
+	Common::Rect _doneButtonSrcRect;
+	Common::Rect _doneButtonDestRect;
+
+	uint16 _fontID = 0;
+	// Picks between the two color variants baked into the font image
+	uint16 _fontColor = 0;
+
+	Common::String _timerSuffix;    // appended to the seconds left, e.g. "s"
+	Common::String _winString;      // replaces the timer once the target is beaten
+	Common::String _timeUpString;   // replaces the timer when time runs out
+
+	// Opens the high score screen mid-game
+	Common::Rect _highScoreButtonRect;
+
+	// Used when the high score list is empty
+	int32 _defaultScoreTarget = 0;
+
+	Common::String _playerName;                     // name stored alongside a new high score
+	Common::Array<Common::Rect> _highScoreRects;    // left/top = name pos, right/bottom = score pos
+
+	RandomSoundBlock _matchSound;     // repeats while a match is highlighted
+	RandomSoundBlock _selectSound;    // first click on a tile
+	RandomSoundBlock _swapSound;      // second click, and the shuffle button
+	RandomSoundBlock _winSound;
+	RandomSoundBlock _timeUpSound;
+	RandomSoundBlock _goButtonSound;
+
+	uint16 _exitCursorType = 0;
+	SceneChangeWithFlag _doneSceneChange;   // leaving through the button next to the board
+
 	// ---------- Runtime state ----------
 
 	enum GameSubState {
@@ -162,7 +223,12 @@ protected:
 		kShuffleDelay  = 3, // wait for _shuffleTimer before applying full shuffle
 		kWaitSound     = 4, // wait for win/time-up sound to finish, then go to kScoreDisplay
 		kWaitDelay     = 5, // wait for display-delay timer, then go to kWaitSound
-		kScoreDisplay  = 6  // show scores, insert into high-score list, then exit or reset
+		kScoreDisplay  = 6, // show scores, insert into high-score list, then exit or reset
+
+		// Nancy14 only
+		kButtonDown    = 7, // a button is held down; apply its action once the timer runs out
+		kEndDelay      = 8, // pause on the win/time-up message before the high score screen
+		kHighScores    = 9  // high score screen, waiting for the GO button
 	};
 
 	GameSubState _gameSubState = kPlaying;
@@ -202,13 +268,22 @@ protected:
 	int  _prevTimerSecs = -1;   // last rendered timer value (seconds), for change detection
 
 	// High scores (top 5, descending; stored in memory, not persisted)
-	int32 _highScores[5] = {0, 0, 0, 0, 0};
+	Common::Array<HighScore> _highScores;
+
+	// Nancy14 runtime state
+	bool _showHighScores = false;   // high score screen is up, waiting for the GO button
+	bool _canResumeGame = false;    // the GO button restarts the round instead of resuming
+	bool _shuffleButtonDown = false;
+	bool _doneButtonDown = false;
+	bool _leftThroughButton = false;
 
 	// Rendering
 	Common::Array<Common::Array<GridCell>> _grid; // _grid[col][row]
 
 	Graphics::ManagedSurface _image;            // loaded sprite sheet
 	Graphics::ManagedSurface _scorePanelImage;  // score-panel background
+	Graphics::ManagedSurface _buttonsImage;     // Nancy14 pressed-button graphics
+	Graphics::ManagedSurface _highScoreImage;   // Nancy14 high score backdrop
 };
 
 } // End of namespace Action


Commit: 891c820c989d0a695f9060ce99531c5007a187b7
    https://github.com/scummvm/scummvm/commit/891c820c989d0a695f9060ce99531c5007a187b7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:17+03:00

Commit Message:
NANCY: Refactor and simplify rotateBlit()

It now works with surfaces of any bpp. Used in the RippedLetterPuzzle
in Nancy14, which features 32bpp surfaces

Changed paths:
    engines/nancy/graphics.cpp


diff --git a/engines/nancy/graphics.cpp b/engines/nancy/graphics.cpp
index d543212977e..b5aa9fa1cd0 100644
--- a/engines/nancy/graphics.cpp
+++ b/engines/nancy/graphics.cpp
@@ -339,11 +339,11 @@ void GraphicsManager::copyToManaged(void *src, Graphics::ManagedSurface &dst, ui
 void GraphicsManager::rotateBlit(const Graphics::ManagedSurface &src, Graphics::ManagedSurface &dest, byte rotation) {
 	assert(!src.empty() && !dest.empty());
 	assert(rotation <= 3);
-	assert(src.format.bytesPerPixel == 2 && dest.format.bytesPerPixel == 2);
+	assert(src.format.bytesPerPixel == dest.format.bytesPerPixel);
 
-	uint srcW = src.w;
-	uint srcH = src.h;
-	const uint16 *s, *e;
+	const uint srcW = src.w;
+	const uint srcH = src.h;
+	const uint bpp = src.format.bytesPerPixel;
 
 	if (rotation % 2) {
 		if (src.h != dest.w || src.w != dest.h) {
@@ -357,45 +357,33 @@ void GraphicsManager::rotateBlit(const Graphics::ManagedSurface &src, Graphics::
 		}
 	}
 
-	switch (rotation) {
-	case 0 :
+	if (rotation == 0) {
 		// No rotation, just blit
 		dest.rawBlitFrom(src, src.getBounds(), Common::Point());
 		return;
-	case 2 : {
-		// 180 degrees
-		uint16 *d;
-		for (uint y = 0; y < srcH; ++y) {
-			s = (const uint16 *)src.getBasePtr(0, y);
-			e = (const uint16 *)src.getBasePtr(srcW, y);
-			d = (uint16 *)dest.getBasePtr(srcW - 1, srcH - y - 1);
-			for (; s < e; ++s, --d) {
-				*d = *s;
-			}
-		}
-
-		break;
 	}
-	case 1 :
-		// 90 degrees
-		for (uint y = 0; y < srcH; ++y) {
-			s = (const uint16 *)src.getBasePtr(0, y);
-			for (uint x = 0; x < srcW; ++x, ++s) {
-				*((uint16 *)dest.getBasePtr(srcH - y - 1, x)) = *s;
-			}
-		}
 
-		break;
-	case 3 :
-		// 270 degrees
-		for (uint y = 0; y < srcH; ++y) {
-			s = (const uint16 *)src.getBasePtr(0, y);
-			for (uint x = 0; x < srcW; ++x, ++s) {
-				*((uint16 *)dest.getBasePtr(y, srcW - x - 1)) = *s;
+	for (uint y = 0; y < srcH; ++y) {
+		const byte *s = (const byte *)src.getBasePtr(0, y);
+		for (uint x = 0; x < srcW; ++x, s += bpp) {
+			byte *d;
+			switch (rotation) {
+			case 1:
+				// 90 degrees
+				d = (byte *)dest.getBasePtr(srcH - y - 1, x);
+				break;
+			case 2:
+				// 180 degrees
+				d = (byte *)dest.getBasePtr(srcW - x - 1, srcH - y - 1);
+				break;
+			default:
+				// 270 degrees
+				d = (byte *)dest.getBasePtr(y, srcW - x - 1);
+				break;
 			}
-		}
 
-		break;
+			memcpy(d, s, bpp);
+		}
 	}
 }
 


Commit: 3ff5d3a2d12cd3fbf13dcb0718f9d88c1a10829c
    https://github.com/scummvm/scummvm/commit/3ff5d3a2d12cd3fbf13dcb0718f9d88c1a10829c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:19+03:00

Commit Message:
NANCY: NANCY14: More work on HangmanPuzzle

- Identify sounds used
- Handle special case where Nancy loses on purpose and JJ gives her
  autograph in scene 3604
- Scene 3604 is the normal puzzle, with result scenes 3602 (win) and
  3601 (lose)

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


diff --git a/engines/nancy/action/puzzle/hangmanpuzzle.cpp b/engines/nancy/action/puzzle/hangmanpuzzle.cpp
index d9786410ec3..9a25371f519 100644
--- a/engines/nancy/action/puzzle/hangmanpuzzle.cpp
+++ b/engines/nancy/action/puzzle/hangmanpuzzle.cpp
@@ -20,12 +20,14 @@
  */
 
 #include "common/random.h"
+#include "common/system.h"
 
 #include "engines/nancy/nancy.h"
 #include "engines/nancy/cursor.h"
 #include "engines/nancy/graphics.h"
 #include "engines/nancy/input.h"
 #include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
 #include "engines/nancy/util.h"
 #include "engines/nancy/puzzledata.h"
 
@@ -67,17 +69,17 @@ void HangmanPuzzle::readData(Common::SeekableReadStream &stream) {
 		readFilename(stream, tile.name);
 	}
 
-	_fieldCE = stream.readSint16LE();	// 0xce
-	_fieldD0 = stream.readSint32LE();	// 0xd0
-	_fieldD4 = stream.readSint16LE();	// 0xd4
+	_letterSoundChannel = stream.readSint16LE();	// 0xce
+	_letterSoundLoops = stream.readSint32LE();		// 0xd0
+	_letterSoundVolume = stream.readSint16LE();		// 0xd4
 
-	for (uint i = 0; i < 3; ++i) {		// 0x12c/0x182/0x1d8
-		_sounds[i].readData(stream);
-	}
+	_correctSound.readData(stream);	// 0x12c
+	_wrongSound.readData(stream);	// 0x182
+	_revealSound.readData(stream);	// 0x1d8
 
-	readFilename(stream, _soundName);	// 0x236
+	readFilename(stream, _targetSequence);	// 0x236
 
-	SceneOutcome *outcomes[] = { &_winScene, &_winScene2, &_loseScene };
+	SceneOutcome *outcomes[] = { &_sequenceScene, &_winScene, &_loseScene };
 	for (SceneOutcome *outcome : outcomes) {
 		outcome->sceneID = stream.readSint16LE();
 		outcome->frameID = stream.readSint16LE();
@@ -156,9 +158,11 @@ void HangmanPuzzle::init() {
 	_revealed.resize(_word.size(), false);
 	_wrongCount = 0;
 	_hoverTile = -1;
-	_solved = false;
 	_lost = false;
-	_outcomeApplied = false;
+	_sequenceComplete = false;
+	_pendingFeedback = nullptr;
+	_outcome = nullptr;
+	_outcomeSoundStarted = false;
 
 	redraw();
 }
@@ -205,9 +209,9 @@ void HangmanPuzzle::redraw() {
 			Common::Point(_guessedRowRects[i].left, _guessedRowRects[i].top));
 	}
 
-	// Revealed word letters in their blanks.
+	// Revealed word letters in their blanks; after losing, the whole word.
 	for (uint i = 0; i < _revealed.size() && i < _letterSlotRects.size(); ++i) {
-		if (_revealed[i]) {
+		if (_revealed[i] || _lost) {
 			_drawSurface.blitFrom(_lettersImage, glyphForLetter(_word[i]),
 				Common::Point(_letterSlotRects[i].left, _letterSlotRects[i].top));
 		}
@@ -230,6 +234,13 @@ void HangmanPuzzle::commitGuess(uint tileIndex) {
 	}
 	tile.used = true;
 
+	RandomSoundBlock letterSound;
+	letterSound.names.push_back(tile.name);
+	letterSound.channel = _letterSoundChannel;
+	letterSound.numLoops = _letterSoundLoops;
+	letterSound.volume = _letterSoundVolume;
+	playSoundBlock(letterSound);
+
 	char c = tile.letter;
 	_guessed.push_back(c);
 
@@ -245,6 +256,30 @@ void HangmanPuzzle::commitGuess(uint tileIndex) {
 		++_wrongCount;
 	}
 
+	// The correct/wrong reaction follows the letter a second later
+	_pendingFeedback = correct ? &_correctSound : &_wrongSound;
+	_feedbackTime = g_system->getMillis() + 1000;
+
+	redraw();
+}
+
+void HangmanPuzzle::updateFeedback() {
+	if (_pendingFeedback && g_system->getMillis() >= _feedbackTime) {
+		playSoundBlock(*_pendingFeedback);
+		_pendingFeedback = nullptr;
+	} else if (!_targetSequence.empty() && _guessed.size() == _targetSequence.size()) {
+		_sequenceComplete = true;
+	}
+}
+
+void HangmanPuzzle::checkOutcome() {
+	bool sequenceMatched = _sequenceComplete;
+	for (uint i = 0; sequenceMatched && i < _guessed.size(); ++i) {
+		if (i >= _targetSequence.size() || _guessed[i] != _targetSequence[i]) {
+			sequenceMatched = false;
+		}
+	}
+
 	bool allRevealed = !_revealed.empty();
 	for (uint i = 0; i < _revealed.size(); ++i) {
 		if (!_revealed[i]) {
@@ -253,25 +288,58 @@ void HangmanPuzzle::commitGuess(uint tileIndex) {
 		}
 	}
 
-	if (allRevealed) {
-		_solved = true;
-	} else if (_wrongCount >= (int)_hangPieceRects.size()) {
+	if (_wrongCount >= (int)_hangPieceRects.size() && !_lost) {
 		_lost = true;
+		playSoundBlock(_revealSound);
+		_revealEndTime = g_system->getMillis() + 2000;
+		redraw();
 	}
 
-	redraw();
+	// A completed word beats the target sequence, which beats a completed hang figure
+	if (allRevealed) {
+		_outcome = &_winScene;
+	} else if (sequenceMatched) {
+		_outcome = &_sequenceScene;
+	} else if (_lost) {
+		_outcome = &_loseScene;
+	}
+
+	if (_outcome) {
+		_state = kActionTrigger;
+	}
 }
 
-void HangmanPuzzle::applyOutcome(const SceneOutcome &outcome) {
-	SceneChangeDescription desc;
-	desc.sceneID = outcome.sceneID;
-	desc.frameID = outcome.frameID;
-	NancySceneState.changeScene(desc);
-	NancySceneState.setEventFlag(outcome.flag);
+void HangmanPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+	if (block.names.empty()) {
+		return;
+	}
+
+	uint index = block.names.size() > 1 ?
+		g_nancy->_randomSource->getRandomNumber(block.names.size() - 1) : 0;
+	if (block.names[index].empty() || block.names[index] == "NO SOUND") {
+		return;
+	}
+
+	SoundDescription desc;
+	desc.name = block.names[index];
+	desc.channelID = block.channel;
+	desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+	desc.volume = block.volume;
+
+	g_nancy->_sound->loadSound(desc);
+	g_nancy->_sound->playSound(desc);
+
+	Common::String caption = resolveSubtitleText(desc.name, Common::String(), "AUTOTEXT");
+	if (caption.empty()) {
+		caption = resolveSubtitleText(desc.name, Common::String(), "CONVO");
+	}
+	if (!caption.empty()) {
+		showSubtitle(caption);
+	}
 }
 
 void HangmanPuzzle::handleInput(NancyInput &input) {
-	if (_state != kRun || _solved || _lost) {
+	if (_state != kRun) {
 		return;
 	}
 
@@ -309,12 +377,32 @@ void HangmanPuzzle::execute() {
 			NancySceneState.changeScene(_exitScene);
 			break;
 		}
-		if ((_solved || _lost) && !_outcomeApplied) {
-			_outcomeApplied = true;
-			applyOutcome(_solved ? _winScene : _loseScene);
-		}
+		updateFeedback();
+		checkOutcome();
 		break;
-	default:
+	case kActionTrigger:
+		if (_lost && g_system->getMillis() < _revealEndTime) {
+			break;
+		}
+
+		// The outcome's line plays out before the scene changes
+		if (!_outcomeSoundStarted) {
+			playSoundBlock(_outcome->sound);
+			_outcomeSoundStarted = true;
+			break;
+		}
+		if (!_outcome->sound.names.empty() && g_nancy->_sound->isSoundPlaying((uint16)_outcome->sound.channel)) {
+			break;
+		}
+
+		{
+			SceneChangeDescription desc;
+			desc.sceneID = _outcome->sceneID;
+			desc.frameID = _outcome->frameID;
+			NancySceneState.changeScene(desc);
+			NancySceneState.setEventFlag(_outcome->flag);
+		}
+		finishExecution();
 		break;
 	}
 }
diff --git a/engines/nancy/action/puzzle/hangmanpuzzle.h b/engines/nancy/action/puzzle/hangmanpuzzle.h
index a2d6bdd787f..a62b8c449db 100644
--- a/engines/nancy/action/puzzle/hangmanpuzzle.h
+++ b/engines/nancy/action/puzzle/hangmanpuzzle.h
@@ -35,8 +35,10 @@ namespace Action {
 // shown as a row of blanks; the player clicks the a-z letter tiles to guess.
 // A correct letter fills every matching blank; a wrong one draws the next
 // hang-stage piece. The word is fully revealed -> win; the hang figure is
-// completed (wrong guesses == number of hang pieces) -> lose. The chosen word
-// is remembered across visits so it is not immediately repeated.
+// completed (wrong guesses == number of hang pieces) -> lose. A record may also
+// carry a target letter sequence: playing exactly those letters, in order, takes
+// a third outcome regardless of the word. The chosen word is remembered across
+// visits so it is not immediately repeated.
 class HangmanPuzzle : public RenderActionRecord {
 public:
 	HangmanPuzzle() : RenderActionRecord(7) {}
@@ -80,8 +82,10 @@ protected:
 	Common::Rect glyphForLetter(char letter) const;	// glyph rect of the tile for this letter, or empty
 	int tileAtCursor(const Common::Point &mousePos) const;
 	void commitGuess(uint tileIndex);
+	void updateFeedback();
+	void checkOutcome();
 	void redraw();
-	void applyOutcome(const SceneOutcome &outcome);
+	void playSoundBlock(const RandomSoundBlock &block);
 
 	// -- File data --
 	Common::Path _puzzleImageName;		// 0x3d
@@ -95,16 +99,19 @@ protected:
 	Common::Array<Common::Rect> _guessedRowRects;	// 0xbe, guessed-letters row
 	Common::Array<LetterTile> _letters;				// 0x77, the a-z tiles
 
-	int16 _fieldCE = 0;			// 0xce
-	int32 _fieldD0 = 0;			// 0xd0
-	int16 _fieldD4 = 0;			// 0xd4
+	// Channel, loops and volume for the letter sound named by each tile
+	int16 _letterSoundChannel = 0;	// 0xce
+	int32 _letterSoundLoops = 0;	// 0xd0
+	int16 _letterSoundVolume = 0;	// 0xd4
 
-	RandomSoundBlock _sounds[3];	// 0x12c/0x182/0x1d8, feedback sound blocks
+	RandomSoundBlock _correctSound;	// 0x12c
+	RandomSoundBlock _wrongSound;	// 0x182
+	RandomSoundBlock _revealSound;	// 0x1d8, played when the word is revealed after losing
 
-	Common::Path _soundName;	// 0x236
-	SceneOutcome _winScene;		// 0x290
-	SceneOutcome _winScene2;	// 0x2f1
-	SceneOutcome _loseScene;	// 0x352
+	Common::String _targetSequence;	// 0x236, empty when unused
+	SceneOutcome _sequenceScene;	// 0x290
+	SceneOutcome _winScene;			// 0x2f1
+	SceneOutcome _loseScene;		// 0x352
 
 	// Give-up hotspot (count-prefixed 23-byte trailer): click to leave the puzzle.
 	Common::Rect _exitHotspot;
@@ -121,9 +128,15 @@ protected:
 	Common::Array<bool> _revealed;		// per word position
 	int _wrongCount = 0;
 	int _hoverTile = -1;
-	bool _solved = false;
 	bool _lost = false;
-	bool _outcomeApplied = false;
+	bool _sequenceComplete = false;	// as many letters played as the target sequence has
+
+	const RandomSoundBlock *_pendingFeedback = nullptr;
+	uint32 _feedbackTime = 0;	// when the pending correct/wrong sound starts
+	uint32 _revealEndTime = 0;	// when the lose reveal ends
+
+	SceneOutcome *_outcome = nullptr;
+	bool _outcomeSoundStarted = false;
 	bool _exitRequested = false;
 };
 


Commit: f3e08d05d52e3a9212c783987e6144decb154cdd
    https://github.com/scummvm/scummvm/commit/f3e08d05d52e3a9212c783987e6144decb154cdd
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:20+03:00

Commit Message:
NANCY: Show NewSceneView item types correctly in the debugger

Changed paths:
    engines/nancy/console.cpp


diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index 591ec5983df..e44192a1df1 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -936,22 +936,22 @@ bool NancyConsole::Cmd_getInventory(int argc, const char **argv) {
 			debugPrintf("\nItem %u, %s, %s, %s",
 				i,
 				inventoryData->itemDescriptions[i].name.c_str(),
-				keep == 0 ? "UseThenLose" : keep == 1 ? "KeepAlways" : "ReturnToInventory",
+				keep == 0 ? "UseThenLose" : keep == 1 ? "KeepAlways" : keep == 2 ? "ReturnToInventory" : "NewSceneView",
 				NancySceneState.hasItem(i) == g_nancy->_true ? "true" : "false");
 		}
 	} else {
 		for (int i = 1; i < argc; ++i) {
-			byte keep = inventoryData->itemDescriptions[i].keepItem;
 			int flagID = atoi(argv[i]);
 			if (flagID < 0 || flagID >= (int)numItems) {
 				debugPrintf("\nInvalid flag %s", argv[i]);
 				continue;
 			}
+			byte keep = inventoryData->itemDescriptions[flagID].keepItem;
 			debugPrintf("\nItem %u, %s, %s, %s",
 				flagID,
 				inventoryData->itemDescriptions[flagID].name.c_str(),
-				keep == 0 ? "UseThenLose" : keep == 1 ? "KeepAlways" : "ReturnToInventory",
-				NancySceneState.hasItem(i) == g_nancy->_true ? "true" : "false");
+				keep == 0 ? "UseThenLose" : keep == 1 ? "KeepAlways" : keep == 2 ? "ReturnToInventory" : "NewSceneView",
+				NancySceneState.hasItem(flagID) == g_nancy->_true ? "true" : "false");
 
 		}
 	}


Commit: ca21db2694b05471e5b26b96dc0ab188ca8d3edc
    https://github.com/scummvm/scummvm/commit/ca21db2694b05471e5b26b96dc0ab188ca8d3edc
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:23+03:00

Commit Message:
NANCY: NANCY14: Implement new functionality for RippedLetterPuzzle

Now, the paint bomb puzzle is handled correctly

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


diff --git a/engines/nancy/action/puzzle/rippedletterpuzzle.cpp b/engines/nancy/action/puzzle/rippedletterpuzzle.cpp
index ea1717a2935..fbe8b7896bb 100644
--- a/engines/nancy/action/puzzle/rippedletterpuzzle.cpp
+++ b/engines/nancy/action/puzzle/rippedletterpuzzle.cpp
@@ -48,8 +48,6 @@ void RippedLetterPuzzle::init() {
 
 	if (_useCustomPickUpTile) {
 		_pickedUpPiece._drawSurface.create(_image, _customPickUpTileSrc);
-	} else {
-		_pickedUpPiece._drawSurface.create(_destRects[0].width(), _destRects[0].height(), g_nancy->_graphics->getInputPixelFormat());
 	}
 
 	_pickedUpPiece.setVisible(false);
@@ -133,6 +131,19 @@ void RippedLetterPuzzle::readData(Common::SeekableReadStream &stream) {
 		}
 	}
 
+	if (g_nancy->getGameType() >= kGameTypeNancy14) {
+		// A piece can only be dropped into a slot belonging to the same group
+		// as the slot it originated from
+		_pieceGroups.resize(width * height);
+		for (uint i = 0; i < height; ++i) {
+			for (uint j = 0; j < width; ++j) {
+				_pieceGroups[i * width + j] = stream.readSint16LE();
+			}
+			stream.skip(maxWidth > width ? (maxWidth - width) * elemSize : 0);
+		}
+		stream.skip((maxWidth > width ? (maxHeight - height) * maxWidth : maxWidth * maxHeight - width * height) * elemSize);
+	}
+
 	_solveOrder.resize(width * height);
 	for (uint i = 0; i < height; ++i) {
 		for (uint j = 0; j < width; ++j) {
@@ -314,8 +325,7 @@ void RippedLetterPuzzle::handleInput(NancyInput &input) {
 				// No piece picked up
 
 				// Check if the mouse is inside the rotation hotspot
-				insideRect = _rotateHotspot;
-				insideRect.translate(screenHotspot.left, screenHotspot.top);
+				insideRect = getPieceHotspot(_rotateHotspot, screenHotspot);
 
 				if (_rotationType != kRotationNone && insideRect.contains(input.mousePos)) {
 					g_nancy->_cursor->setCursorType(rotateCursor);
@@ -335,8 +345,7 @@ void RippedLetterPuzzle::handleInput(NancyInput &input) {
 				}
 
 				// Check if the mouse is inside the pickup hotspot
-				insideRect = _takeHotspot;
-				insideRect.translate(screenHotspot.left, screenHotspot.top);
+				insideRect = getPieceHotspot(_takeHotspot, screenHotspot);
 
 				if (insideRect.contains(input.mousePos)) {
 					g_nancy->_cursor->setCursorType(takeCursor);
@@ -346,8 +355,7 @@ void RippedLetterPuzzle::handleInput(NancyInput &input) {
 
 						// First, copy the graphic from the full drawSurface...
 						if (!_useCustomPickUpTile) {
-							_pickedUpPiece._drawSurface.clear(g_nancy->_graphics->getTransColor());
-							_pickedUpPiece._drawSurface.blitFrom(_drawSurface, _destRects[i], Common::Point());
+							copyPieceToPickedUp(i);
 						}
 
 						_pickedUpPiece.setVisible(true);
@@ -371,11 +379,13 @@ void RippedLetterPuzzle::handleInput(NancyInput &input) {
 			} else {
 				// Currently carrying a piece
 
-				// Check if the mouse is inside the drop hotspot
-				insideRect = _dropHotspot;
-				insideRect.translate(screenHotspot.left, screenHotspot.top);
+				// Check if the mouse is inside the drop hotspot, and whether
+				// the held piece is allowed in this slot
+				insideRect = getPieceHotspot(_dropHotspot, screenHotspot);
+				bool sameGroup = _pieceGroups.empty() ||
+					_pieceGroups[i] == _pieceGroups[_puzzleState->pickedUpPieceID];
 
-				if (insideRect.contains(input.mousePos)) {
+				if (sameGroup && insideRect.contains(input.mousePos)) {
 					g_nancy->_cursor->setCursorType(dropCursor);
 
 					if (input.input & NancyInput::kLeftMouseButtonUp) {
@@ -389,8 +399,7 @@ void RippedLetterPuzzle::handleInput(NancyInput &input) {
 						} else {
 							// Yes, change the picked piece graphic
 							if (!_useCustomPickUpTile) {
-								_pickedUpPiece._drawSurface.clear(g_nancy->_graphics->getTransColor());
-								_pickedUpPiece._drawSurface.blitFrom(_drawSurface, _destRects[i], Common::Point());
+								copyPieceToPickedUp(i);
 							}
 
 							_pickedUpPiece.setVisible(true);
@@ -449,6 +458,20 @@ void RippedLetterPuzzle::drawPiece(const uint pos, const byte rotation, const in
 	GraphicsManager::rotateBlit(srcSurf, destSurf, rotation);
 }
 
+void RippedLetterPuzzle::copyPieceToPickedUp(const uint pos) {
+	// Pieces may have different shapes, so size the held tile to the one being picked up
+	const Common::Rect &rect = _destRects[pos];
+	_pickedUpPiece._drawSurface.create(rect.width(), rect.height(), _drawSurface.format);
+	_pickedUpPiece._drawSurface.blitFrom(_drawSurface, rect, Common::Point());
+}
+
+Common::Rect RippedLetterPuzzle::getPieceHotspot(const Common::Rect &hotspot, const Common::Rect &screenRect) const {
+	// An empty hotspot means the whole piece is interactive
+	Common::Rect ret = hotspot.height() ? hotspot : Common::Rect(screenRect.width(), screenRect.height());
+	ret.translate(screenRect.left, screenRect.top);
+	return ret;
+}
+
 bool RippedLetterPuzzle::checkOrder(bool useAlt) {
 	auto &current = _puzzleState->order;
 	auto &correct = useAlt ? _solveOrderAlt : _solveOrder;
diff --git a/engines/nancy/action/puzzle/rippedletterpuzzle.h b/engines/nancy/action/puzzle/rippedletterpuzzle.h
index 771bee0bb2d..20d32da0d11 100644
--- a/engines/nancy/action/puzzle/rippedletterpuzzle.h
+++ b/engines/nancy/action/puzzle/rippedletterpuzzle.h
@@ -62,6 +62,7 @@ public:
 	Common::Array<int8> _solveOrderAlt;
 	Common::Array<byte> _solveRotationsAlt;
 	Common::Array<Common::Array<byte>> _doubles;
+	Common::Array<int16> _pieceGroups;
 	bool _useAltSolution = false;
 
 	bool _useCustomPickUpTile = false;
@@ -91,6 +92,8 @@ protected:
 	Common::String getRecordTypeName() const override { return "RippedLetterPuzzle"; }
 
 	void drawPiece(const uint pos, const byte rotation, const int pieceID = -1);
+	void copyPieceToPickedUp(const uint pos);
+	Common::Rect getPieceHotspot(const Common::Rect &hotspot, const Common::Rect &screenRect) const;
 	bool checkOrder(bool useAlt);
 };
 


Commit: 9fb7bfb678bf7384d11bffaa6bb056f190ecbbb3
    https://github.com/scummvm/scummvm/commit/9fb7bfb678bf7384d11bffaa6bb056f190ecbbb3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:23+03:00

Commit Message:
NANCY: NANCY10: Add handling for the close on pickup inventory behavior

This is another behavior flag in the inventory UIIV chunk

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/ui/inventorypopup.cpp


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 2e00664e693..6f860b386fd 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -1280,11 +1280,11 @@ UIIV::UIIV(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	if (g_nancy->getGameType() >= kGameTypeNancy13)
 		readRect(*chunkStream, slotsHotspot);
 
-	// Two byte flags. The first controls where items added while the popup is
-	// open land in the inventory order (see appendItemsWhileOpen); the second
-	// is unused here.
+	// Two byte flags: where items added while the popup is open land in the
+	// inventory order (see appendItemsWhileOpen), and whether picking up an item
+	// closes the popup.
 	appendItemsWhileOpen = chunkStream->readByte();
-	chunkStream->skip(1);
+	closeOnPickup = chunkStream->readByte();
 
 	for (uint i = 0; i < kNumFilters; ++i) {
 		readUIButtonSlot(*chunkStream, filters[i]);
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index e4751f05f81..30606830b6c 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -805,6 +805,8 @@ struct UIIV : public EngineData {
 	// of the inventory order instead of being inserted at the front (so the most
 	// recently dropped item ends up last). See Scene::addItemToInventory.
 	byte appendItemsWhileOpen = 0;
+	// When nonzero, picking up an item closes the popup so it can be used on the scene.
+	byte closeOnPickup = 0;
 	UIButtonSlot filters[kNumFilters];              // 6 entries
 	Common::Array<Common::Rect> tabCaptionSrcRects; // 6 entries
 	Common::Rect tabCaptionDestRect;                // on-screen target
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 3052673dd19..0f0fd97199b 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -550,7 +550,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 
 			if (item.keepItem == kInvItemNewSceneView) {
 				// Close-up view: stash the item and warp to its scene, which
-				// dismisses the popup. A normal pickup keeps the popup open.
+				// dismisses the popup.
 				g_nancy->_sound->playSound("BUOK");
 				NancySceneState.pushScene(itemID);
 				SceneChangeDescription sceneChange;
@@ -558,6 +558,9 @@ void InventoryPopup::handleInput(NancyInput &input) {
 				sceneChange.continueSceneSound = item.sceneSoundFlag;
 				NancySceneState.changeScene(sceneChange);
 				close();
+			} else if (_uiivData->closeOnPickup) {
+				// Dismiss the popup so the held item can be used on the scene
+				close();
 			}
 
 			input.eatMouseInput();


Commit: a88c2f18f3ced53d6c48f024098e122eb8f531b6
    https://github.com/scummvm/scummvm/commit/a88c2f18f3ced53d6c48f024098e122eb8f531b6
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-14T03:14:25+03:00

Commit Message:
NANCY: NANCY14-15: Use the correct number of items

Fixes the mouse cursor when picking up items. This bumps the savegame
version

Changed paths:
    engines/nancy/nancy.cpp
    engines/nancy/nancy.h
    engines/nancy/state/scene.cpp


diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index 9bd2ebef9b4..994397e3700 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -925,8 +925,8 @@ void NancyEngine::populateStaticData() {
 		break;
 	case kGameTypeNancy14:
 	case kGameTypeNancy15:
-		_staticData.numItems = 50;
-		_staticData.numCursorTypes = 44;
+		_staticData.numItems = 49;
+		_staticData.numCursorTypes = 45;
 		break;
 	default:
 		_staticData.numItems = 50;
diff --git a/engines/nancy/nancy.h b/engines/nancy/nancy.h
index e422e32d99f..683ebaeb5ba 100644
--- a/engines/nancy/nancy.h
+++ b/engines/nancy/nancy.h
@@ -64,7 +64,8 @@ namespace Nancy {
 // - 7: Nancy10 unnamed notebook task event flags added
 // - 8: Nancy12 DrivingPuzzle fuel state persisted
 // - 9: RippedLetterPuzzle stores its scene ID and tried flag
-static const int kSavegameVersion = 9;
+// - 10: Nancy14/15 inventory arrays hold 49 items instead of 50
+static const int kSavegameVersion = 10;
 
 struct NancyGameDescription;
 
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 7945a138e8c..47dce9852f4 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1193,9 +1193,17 @@ void Scene::synchronize(Common::Serializer &ser) {
 		ser.syncAsUint32LE((uint32 &)_flags.logicConditions[i].timestamp);
 	}
 
+	const uint numItems = g_nancy->getStaticData().numItems;
+	uint numSavedItems = numItems;
+	if (ser.getVersion() < 10 && (g_nancy->getGameType() == kGameTypeNancy14 || g_nancy->getGameType() == kGameTypeNancy15)) {
+		// Nancy14/15 saves made before version 10 were written with an item
+		// count of 50, before the correct count of 49 was established.
+		numSavedItems = 50;
+	}
+
 	auto &order = getInventoryBox().getOrder();
 	uint prevSize = order.size();
-	order.resize(g_nancy->getStaticData().numItems);
+	order.resize(numSavedItems);
 
 	if (ser.isSaving()) {
 		for (uint i = prevSize; i < order.size(); ++i) {
@@ -1203,7 +1211,7 @@ void Scene::synchronize(Common::Serializer &ser) {
 		}
 	}
 
-	ser.syncArray(order.data(), g_nancy->getStaticData().numItems, Common::Serializer::Sint16LE);
+	ser.syncArray(order.data(), numSavedItems, Common::Serializer::Sint16LE);
 
 	while (order.size() && order.back() == -1) {
 		order.pop_back();
@@ -1214,12 +1222,16 @@ void Scene::synchronize(Common::Serializer &ser) {
 		getInventoryBox().onReorder();
 	}
 
-	ser.syncArray(_flags.items.data(), g_nancy->getStaticData().numItems, Common::Serializer::Byte);
+	_flags.items.resize(numSavedItems, g_nancy->_false);
+	ser.syncArray(_flags.items.data(), numSavedItems, Common::Serializer::Byte);
+	_flags.items.resize(numItems);
 	ser.syncAsSint16LE(_flags.heldItem);
 	g_nancy->_cursor->setCursorItemID(_flags.heldItem);
 
 	if (g_nancy->getGameType() >= kGameTypeNancy7) {
-		ser.syncArray(_flags.disabledItems.data(), g_nancy->getStaticData().numItems, Common::Serializer::Byte);
+		_flags.disabledItems.resize(numSavedItems, 0);
+		ser.syncArray(_flags.disabledItems.data(), numSavedItems, Common::Serializer::Byte);
+		_flags.disabledItems.resize(numItems);
 	}
 
 	ser.syncAsUint32LE((uint32 &)_timers.lastTotalTime);




More information about the Scummvm-git-logs mailing list