[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 ¤t = _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