[Scummvm-git-logs] scummvm master -> 168fd513ebd9a09f302a348eb3940f1cf6541244
bluegr
noreply at scummvm.org
Mon Mar 16 22:12:27 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
168fd513eb NANCY: Implement matchpuzzle - Maritime Flag matching puzzle in Nancy 8
Commit: 168fd513ebd9a09f302a348eb3940f1cf6541244
https://github.com/scummvm/scummvm/commit/168fd513ebd9a09f302a348eb3940f1cf6541244
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-17T00:11:17+02:00
Commit Message:
NANCY: Implement matchpuzzle - Maritime Flag matching puzzle in Nancy 8
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 42cd59742a6..18947345cf1 100644
--- a/engines/nancy/action/puzzle/matchpuzzle.cpp
+++ b/engines/nancy/action/puzzle/matchpuzzle.cpp
@@ -29,67 +29,649 @@
#include "engines/nancy/state/scene.h"
#include "engines/nancy/action/puzzle/matchpuzzle.h"
+#include "graphics/font.h"
+
+#include "common/system.h"
+#include "common/random.h"
+
namespace Nancy {
namespace Action {
-void MatchPuzzle::init() {
- // TODO
- //_screenPosition = _displayBounds;
+void MatchPuzzle::readData(Common::SeekableReadStream &stream) {
+ // data+0x00..0x20 main sprite sheet name
+ readFilename(stream, _overlayName);
+ // data+0x21..0x41 score-panel background name
+ readFilename(stream, _flagPointBgName);
+
+ _rows = stream.readSint16LE(); // data+0x42
+ _cols = stream.readSint16LE(); // data+0x44
+ _numFlagTypes = stream.readSint16LE(); // data+0x46
+
+ readRect(stream, _shuffleButtonSrcRect); // data+0x48..0x57 (source rect in sprite sheet)
+
+ _flagSrcRects.resize(26);
+
+ for (int i = 0; i < 26; ++i)
+ readRect(stream, _flagSrcRects[i]); // data+0x58..0x1F7 (source rects in sprite sheet)
+
+ // data+0x1F8..0x237 â 64 bytes unused (all zeros)
+ stream.skip(0x40);
+
+ // data+0x238 script execution / flag-info display enable (byte)
+ _execScript = stream.readByte() != 0;
+ stream.skip(1); // data+0x239 padding
+ _scriptID = stream.readSint16LE(); // data+0x23A script ID
+
+ // data+0x23C score-panel font ID (uint16)
+ /*_scorePanelFontID = */stream.readUint16LE();
+
+ // data+0x23E..0x25E 33-byte label string drawn in score panel
+ readFilename(stream, _displayLabelString);
+
+ // data+0x25F..0x5B8 â 26 per-flag-type names (33 bytes each)
+ readFilenameArray(stream, _flagSoundNames, 26);
- //_drawSurface.create(_screenPosition.width(), _screenPosition.height(), g_nancy->_graphics->getInputPixelFormat());
- //_drawSurface.clear(g_nancy->_graphics->getTransColor());
+ // data+0x5B9..0x63C â 132 bytes (4 Ã 33-byte strings, e.g. score-panel labels)
+ stream.skip(132);
+ // data+0x63D score-panel display enable (byte)
+ _showScoreDisplay = stream.readByte() != 0;
+ _timeLimitSecs = stream.readSint16LE(); // data+0x63E
+ _scoreTarget = stream.readSint32LE(); // data+0x640
+ _scorePerFlag = stream.readSint16LE(); // data+0x644
+
+ readRect(stream, _matchedFlagSrcRect); // data+0x646..0x655 matched/highlight src rect
+
+ _timeBonusFor3 = stream.readSint16LE(); // data+0x656 (seconds)
+ _scoreBonusFor4 = stream.readSint16LE(); // data+0x658
+ _timeBonusFor4 = stream.readSint16LE(); // data+0x65A (seconds)
+ _scoreBonusFor5 = stream.readSint16LE(); // data+0x65C
+ _timeBonusFor5 = stream.readSint16LE(); // data+0x65E (seconds)
+ _gridOffX = stream.readSint16LE(); // data+0x660
+ _gridOffY = stream.readSint16LE(); // data+0x662
+ _rowSpacing = stream.readSint16LE(); // data+0x664
+ _colSpacing = stream.readSint16LE(); // data+0x666
+
+ // data+0x668..0x6E7 â 8 Ã 16-byte viewport-local destination rects
+ readRect(stream, _labelStringRect); // data+0x668 â label string display rect
+ readRect(stream, _shuffleButtonDestRect); // data+0x678 â shuffle button on-screen rect (hotspot)
+ readRect(stream, _goalValueRect); // data+0x688 â goal value text rect
+ readRect(stream, _scoreValueRect); // data+0x698 â score value text rect
+ readRect(stream, _timerValueRect); // data+0x6A8 â timer text rect
+ readRect(stream, _flagNameRect); // data+0x6B8 â matched flag name text rect
+ readRect(stream, _flagImageRect); // data+0x6C8 â matched flag image rect
+ readRect(stream, _hsDisplayRect); // data+0x6D8 â high-score display positions
+
+ _scoreDisplayDelay = stream.readSint16LE(); // data+0x6E8
+
+ _slotWinSound.readNormal(stream); // data+0x6EA..0x71A match animation sound
+ _shuffleSound.readNormal(stream); // data+0x71B..0x74B
+ _cardPlaceSound.readNormal(stream); // data+0x74C..0x77C
+
+ _solveSceneChange.readData(stream); // data+0x77D..0x795 win scene
+ stream.skip(2); // data+0x796..0x797 pre-result delay (unused)
+
+ _matchSuccessSound.readNormal(stream); // data+0x798..0x7C8 win/time-up sound
+
+ _exitSceneChange.readData(stream); // data+0x7C9..0x7E1 quit/exit scene
+
+ readRect(stream, _exitHotspot); // data+0x7E2..0x7F1 exit hotspot
+}
+
+void MatchPuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
g_nancy->_resource->loadImage(_overlayName, _image);
- RenderActionRecord::init();
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ if (!_flagPointBgName.empty()) {
+ g_nancy->_resource->loadImage(_flagPointBgName, _scorePanelImage);
+ _scorePanelImage.setTransparentColor(_drawSurface.getTransparentColor());
+ }
+
+ // Build grid â compute dest rects; cells will be filled by shuffleGrid()
+ _grid.resize(_cols);
+ for (int col = 0; col < _cols; ++col) {
+ _grid[col].resize(_rows);
+ for (int row = 0; row < _rows; ++row)
+ computeDestRect(col, row);
+ }
+
+ // Initialise display strings
+ _goalStr = Common::String::format("%d", _scoreTarget);
+ _scoreStr = Common::String::format("%d", (int32)0);
+ if (_timeLimitSecs > 0)
+ _timerStr = Common::String::format("%2dm %2ds", _timeLimitSecs / 60, _timeLimitSecs % 60);
+
+ shuffleGrid(true);
+ redrawAllCells();
}
void MatchPuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+
+ if (_slotWinSound.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_slotWinSound);
+ if (_shuffleSound.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_shuffleSound);
+ if (_cardPlaceSound.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_cardPlaceSound);
+
+ _score = 0;
+ _wonGame = false;
+ _hasPiece1 = _hasPiece2 = false;
+ _hasSelection = false;
+ _gameSubState = kPlaying;
+ _showFlagName = false;
+ _prevTimerSecs = -1;
+
+ if (_timeLimitSecs > 0)
+ _timerDeadline = g_system->getMillis() + (uint32)_timeLimitSecs * 1000;
+
_state = kRun;
+ // fall through
+
+ case kRun:
+ switch (_gameSubState) {
+
+ case kPlaying: { // Playing â update timer display, process pending clicks, check win/lose
+ uint32 now = g_system->getMillis();
+
+ // Update countdown timer display once per second
+ if (_timeLimitSecs > 0) {
+ int32 remainMs = (int32)(_timerDeadline - now);
+ if (remainMs < 0) remainMs = 0;
+ int secs = remainMs / 1000;
+ if (secs != _prevTimerSecs) {
+ _prevTimerSecs = secs;
+ _timerStr = Common::String::format("%2dm %2ds", secs / 60, secs % 60);
+ redrawAllCells();
+ }
+ }
+
+ // Process piece 1 first
+ if (_hasPiece1) {
+ _hasPiece1 = false;
+ checkForMatch(_piece1Col, _piece1Row);
+ if (_hasVMatch || _hasHMatch) {
+ _hasSelection = false;
+ if (_execScript && (uint16)_matchedFlagType < _flagSoundNames.size()) {
+ _flagNameStr = _flagSoundNames[_matchedFlagType];
+ _showFlagName = true;
+ }
+ _scoreStr = Common::String::format("%d", _score);
+ if (_slotWinSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_slotWinSound);
+ _stateTimer = now + 800;
+ _gameSubState = kMatchAnim;
+ redrawAllCells();
+ }
+ break;
+ }
+
+ // Process piece 2 on the next frame after piece 1 is cleared
+ if (_hasPiece2) {
+ _hasPiece2 = false;
+ checkForMatch(_piece2Col, _piece2Row);
+ if (_hasVMatch || _hasHMatch) {
+ _hasSelection = false;
+ if (_execScript && (uint16)_matchedFlagType < _flagSoundNames.size()) {
+ _flagNameStr = _flagSoundNames[_matchedFlagType];
+ _showFlagName = true;
+ }
+ _scoreStr = Common::String::format("%d", _score);
+ if (_slotWinSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_slotWinSound);
+ _stateTimer = now + 800;
+ _gameSubState = kMatchAnim;
+ redrawAllCells();
+ }
+ break;
+ }
+
+ // Neither pending: check win/lose conditions
+ bool timerExpired = (_timeLimitSecs > 0) && ((int32)(_timerDeadline - now) < 500);
+ bool reachedTarget = (_score >= _scoreTarget);
+
+ if (timerExpired || reachedTarget) {
+ _wonGame = reachedTarget;
+ _timerStr = _wonGame ? "WIN!!!" : "TIME!";
+ _hasSelection = false;
+ _gameSubState = kStartEndSeq;
+ redrawAllCells();
+ }
+ break;
+ }
+
+ case kStartEndSeq: {
+ if (_matchSuccessSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_matchSuccessSound);
+ g_nancy->_sound->playSound(_matchSuccessSound);
+ }
+ _stateTimer = g_system->getMillis() + (uint32)_scoreDisplayDelay * 1000;
+ _gameSubState = kWaitDelay;
+ break;
+ }
+
+ case kMatchAnim: { // Match animation: wait for 800ms timer and sound to finish
+ uint32 now = g_system->getMillis();
+ bool timerDone = (now >= _stateTimer);
+ bool soundDone = !g_nancy->_sound->isSoundPlaying(_slotWinSound);
+
+ if (timerDone && soundDone) {
+ // Reshuffle only the cells that were part of the match
+ for (int c = 0; c < _cols; ++c)
+ for (int r = 0; r < _rows; ++r)
+ if (_grid[c][r].matched)
+ shuffleGrid(false, c, r);
+
+ _hasVMatch = _hasHMatch = false;
+ _showFlagName = false;
+ redrawAllCells();
+ _gameSubState = kPlaying;
+ }
+ break;
+ }
+
+ case kShuffleDelay: { // Shuffle-button delay
+ if (g_system->getMillis() >= _shuffleTimer) {
+ shuffleGrid(true);
+ redrawAllCells();
+ _gameSubState = kPlaying;
+ }
+ break;
+ }
+
+ case kWaitSound: { // Wait for win/time-up sound to finish, then enter score display
+ if (!g_nancy->_sound->isSoundPlaying(_matchSuccessSound)) {
+ _stateTimer = g_system->getMillis() + (uint32)_scoreDisplayDelay * 1000;
+ _gameSubState = kScoreDisplay;
+ redrawAllCells(); // show the final-score / high-score screen
+ }
+ break;
+ }
+
+ case kWaitDelay: { // Wait for delay timer, then go to sound-wait
+ if (g_system->getMillis() >= _stateTimer)
+ _gameSubState = kWaitSound;
+ break;
+ }
+
+ case kScoreDisplay: { // Score display â show until timer expires, then insert score and exit/reset
+ if (g_system->getMillis() < _stateTimer)
+ break;
+
+ // 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 (_wonGame) {
+ _state = kActionTrigger;
+ } else {
+ // Time ran out â fresh round
+ _score = 0;
+ _scoreStr = Common::String::format("%d", (int32)0);
+ _hasPiece1 = _hasPiece2 = false;
+ _hasSelection = false;
+ _showFlagName = false;
+ _prevTimerSecs = -1;
+ if (_timeLimitSecs > 0)
+ _timerDeadline = g_system->getMillis() + (uint32)_timeLimitSecs * 1000;
+ shuffleGrid(true);
+ _gameSubState = kPlaying;
+ redrawAllCells();
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ break;
+
+ case kActionTrigger:
+ g_nancy->_sound->stopSound(_slotWinSound);
+ g_nancy->_sound->stopSound(_shuffleSound);
+ g_nancy->_sound->stopSound(_cardPlaceSound);
+ g_nancy->_sound->stopSound(_matchSuccessSound);
+
+ if (_wonGame)
+ _solveSceneChange.execute();
+ else
+ _exitSceneChange.execute();
+
+ finishExecution();
+ break;
+ }
+}
+
+void MatchPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun)
+ return;
+ // Convert mouse to viewport-local coordinates.
+ Common::Rect vpPos = NancySceneState.getViewport().getScreenPosition();
+ Common::Point localMouse = input.mousePos;
+ localMouse -= Common::Point(vpPos.left, vpPos.top);
+
+ if (!_exitHotspot.isEmpty() && _exitHotspot.contains(localMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kMoveBackward);
+ if (input.input & NancyInput::kLeftMouseButtonUp)
+ _state = kActionTrigger;
}
- // TODO
- // Stub - return to the main menu
- warning("STUB - Nancy 8 flag game");
- _exitSceneChange.execute();
+ if (_gameSubState != kPlaying)
+ return;
+
+ // Shuffle button â use the on-screen destination rect for hit-testing
+ if (_shuffleButtonDestRect.contains(localMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _hasSelection = false;
+ if (_shuffleSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_shuffleSound);
+ _shuffleTimer = g_system->getMillis() + 500;
+ _gameSubState = kShuffleDelay;
+ }
+ return;
+ }
+
+ // Grid cell click
+ for (int col = 0; col < _cols; ++col) {
+ for (int row = 0; row < _rows; ++row) {
+ if (!_grid[col][row].visible)
+ continue;
+ if (_grid[col][row].destRect.contains(localMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ if (!_hasSelection) {
+ // First click: remember this cell as the selection
+ _selCol = col;
+ _selRow = row;
+ _hasSelection = true;
+ if (_cardPlaceSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_cardPlaceSound);
+ redrawAllCells();
+ } else if (col == _selCol && row == _selRow) {
+ // Clicked same cell again: deselect
+ _hasSelection = false;
+ redrawAllCells();
+ } else {
+ // Second click: swap the two flags and queue both for match-check
+ SWAP(_grid[_selCol][_selRow].flagType,
+ _grid[col][row].flagType);
+
+ _piece1Col = _selCol;
+ _piece1Row = _selRow;
+ _piece2Col = col;
+ _piece2Row = row;
+ _hasPiece1 = true;
+ _hasPiece2 = true;
+ _hasSelection = false;
+
+ if (_cardPlaceSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_cardPlaceSound);
+ redrawAllCells();
+ }
+ }
+ return;
+ }
+ }
+ }
}
-void MatchPuzzle::readData(Common::SeekableReadStream &stream) {
- readFilename(stream, _overlayName);
- readFilename(stream, _flagPointBackgroundName);
+void MatchPuzzle::shuffleGrid(bool allCells, int targetCol, int targetRow) {
+ // Valid flag indices are 0 .. (_numFlagTypes - 2) inclusive
+ int numTypes = (_numFlagTypes > 1) ? (_numFlagTypes - 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
+ 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)
+ break;
+ }
+
+ _grid[col][row].flagType = chosen;
+ _grid[col][row].visible = true;
+ _grid[col][row].matched = false;
+ }
+ }
+}
+
+void MatchPuzzle::checkForMatch(int col, int row) {
+ _hasVMatch = _hasHMatch = false;
- stream.skip(2); // TODO (value: 5)
- stream.skip(2); // TODO (value: 7)
- stream.skip(2); // 26 flags
+ if (col < 0 || col >= _cols || row < 0 || row >= _rows)
+ return;
+ if (!_grid[col][row].visible)
+ return;
- readRect(stream,_shuffleButtonRect);
- readRectArray(stream, _flagRects, 26);
+ int16 type = _grid[col][row].flagType;
+ _matchedFlagType = type;
- stream.skip(103); // TODO (mostly zeroes)
+ // --- 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;
+ _matchRowStart = rStart;
+ _matchRowEnd = rEnd;
- readFilenameArray(stream, _flagNames, 26);
+ // --- 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;
+ _matchColStart = cStart;
+ _matchColEnd = cEnd;
- stream.skip(132); // TODO (zeroes)
- stream.skip(173); // TODO
+ int vLen = rEnd - rStart; // 2 â 3-match, 3 â 4-match, 4 â 5-match
+ int hLen = cEnd - cStart;
- _slotWinSound.readNormal(stream);
- _shuffleSound.readNormal(stream);
- _cardPlaceSound.readNormal(stream);
+ // --- Score and mark: vertical match (>=3 flags) ---
+ if (vLen >= 2) {
+ _hasVMatch = true;
+ for (int r = rStart; r <= rEnd; ++r) {
+ _grid[col][r].matched = true;
+ _score += _scorePerFlag;
+ }
+ // 3-bonus checks hLen (apparent original bug), kept faithful
+ if (hLen == 2)
+ _timerDeadline += (uint32)_timeBonusFor3 * 1000;
+ else if (vLen == 3) {
+ _score += _scoreBonusFor4;
+ _timerDeadline += (uint32)_timeBonusFor4 * 1000;
+ } else if (vLen >= 4) {
+ _score += _scoreBonusFor5;
+ _timerDeadline += (uint32)_timeBonusFor5 * 1000;
+ }
+ }
- _solveSceneChange.readData(stream);
- stream.skip(2);
- _matchSuccessSound.readNormal(stream);
- _exitSceneChange.readData(stream);
+ // --- Score and mark: horizontal match (>=3 flags) ---
+ if (hLen >= 2) {
+ _hasHMatch = true;
+ for (int c = cStart; c <= cEnd; ++c) {
+ _grid[c][row].matched = true;
+ _score += _scorePerFlag;
+ }
+ if (hLen == 2)
+ _timerDeadline += (uint32)_timeBonusFor3 * 1000;
+ else if (hLen == 3) {
+ _score += _scoreBonusFor4;
+ _timerDeadline += (uint32)_timeBonusFor4 * 1000;
+ } else if (hLen >= 4) {
+ _score += _scoreBonusFor5;
+ _timerDeadline += (uint32)_timeBonusFor5 * 1000;
+ }
+ }
- stream.skip(16); // TODO
+ if (_score > _scoreTarget)
+ _score = _scoreTarget;
}
-void MatchPuzzle::handleInput(NancyInput &input) {
- // TODO
+void MatchPuzzle::computeDestRect(int col, int row) {
+ if (_flagSrcRects.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;
+
+ // Column position: extra spacing per col + cell width
+ int left = col * (_colSpacing + cellW) + _gridOffX;
+ // Row position: extra spacing per row + cell height
+ int top = row * (_rowSpacing + cellH) + _gridOffY;
+
+ _grid[col][row].destRect = Common::Rect(left, top, left + cellW, top + cellH);
+}
+
+// ---- rendering helpers ------------------------------------------------------
+
+void MatchPuzzle::drawCell(int col, int row) {
+ const GridCell &cell = _grid[col][row];
+ if (!cell.visible)
+ return;
+
+ int type = cell.flagType;
+ if (type < 0 || type >= (int)_flagSrcRects.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];
+ _drawSurface.blitFrom(_image, srcRect,
+ Common::Point(cell.destRect.left, cell.destRect.top));
+ _needsRedraw = true;
+}
+
+void MatchPuzzle::eraseCell(int col, int row) {
+ _drawSurface.fillRect(_grid[col][row].destRect,
+ _drawSurface.getTransparentColor());
+ _needsRedraw = true;
+}
+
+void MatchPuzzle::drawScorePanel() {
+ // ---- State 6: final score / high-score screen ----
+ if (_gameSubState == kScoreDisplay) {
+ if (!_scorePanelImage.empty())
+ _drawSurface.blitFrom(_scorePanelImage, Common::Point(0, 0));
+
+ if (_showScoreDisplay) {
+ const Graphics::Font *font = g_nancy->_graphics->getFont(_scriptID);
+ if (!font) font = g_nancy->_graphics->getFont(0);
+ if (font) {
+ const int fh = font->getFontHeight();
+ const int lineSpacing = fh + 12;
+
+ // Final score
+ Common::Rect rect = _hsDisplayRect;
+ rect.moveTo(196, 37);
+ rect = NancySceneState.getViewport().convertViewportToScreen(rect);
+ rect = convertToLocal(rect);
+
+ int scoreX = rect.left + 60;
+ int hsX = rect.right - 1;
+ int scoreY = (rect.bottom - 1) - 16 + lineSpacing;
+ font->drawString(&_drawSurface, _scoreStr, scoreX, scoreY, 100, 0);
+
+ // 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]);
+ font->drawString(&_drawSurface, hs, hsX, hsY, 80, 0);
+ hsY += lineSpacing;
+ }
+ }
+ }
+ return; // don't draw the normal gameplay overlay
+ }
+
+ // ---- Normal gameplay ----
+
+ // Draw the shuffle button sprite
+ if (!_shuffleButtonSrcRect.isEmpty() && !_shuffleButtonDestRect.isEmpty())
+ _drawSurface.blitFrom(_image, _shuffleButtonSrcRect,
+ Common::Point(_shuffleButtonDestRect.left, _shuffleButtonDestRect.top));
+
+ if (!_showScoreDisplay)
+ return;
+
+ // The score-panel font is determined by _scriptID, instead of _scorePanelFontID.
+ const Graphics::Font *font = g_nancy->_graphics->getFont(_scriptID);
+ if (!font)
+ font = g_nancy->_graphics->getFont(0);
+
+ // Helper: vertically centre font within rect (font may be taller than rect)
+ const int fh = font->getFontHeight();
+ auto textY = [&](const Common::Rect &r) {
+ return r.top + (r.height() - fh) / 2 - 1;
+ };
+
+ // Static label string (empty in practice â labels are baked into the background image)
+ if (!_displayLabelString.empty() && !_labelStringRect.isEmpty())
+ font->drawString(&_drawSurface, _displayLabelString,
+ _labelStringRect.left, textY(_labelStringRect),
+ _labelStringRect.width(), 0);
+
+ // Goal value (fixed for the lifetime of the puzzle)
+ if (!_goalValueRect.isEmpty())
+ font->drawString(&_drawSurface, _goalStr,
+ _goalValueRect.left, textY(_goalValueRect),
+ _goalValueRect.width(), 0);
+
+ // Current score value
+ if (!_scoreValueRect.isEmpty())
+ font->drawString(&_drawSurface, _scoreStr,
+ _scoreValueRect.left, textY(_scoreValueRect),
+ _scoreValueRect.width(), 0);
+
+ // Countdown timer
+ if (_timeLimitSecs > 0 && !_timerValueRect.isEmpty())
+ font->drawString(&_drawSurface, _timerStr,
+ _timerValueRect.left, textY(_timerValueRect),
+ _timerValueRect.width(), 0);
+
+ // Matched flag info â shown briefly after a match (gated by _execScript)
+ if (_execScript && _showFlagName) {
+ if (!_flagNameStr.empty() && !_flagNameRect.isEmpty())
+ font->drawString(&_drawSurface, _flagNameStr,
+ _flagNameRect.left, textY(_flagNameRect),
+ _flagNameRect.width(), 0);
+
+ int16 ft = _matchedFlagType;
+ if (ft >= 0 && ft < (int16)_flagSrcRects.size() && !_flagImageRect.isEmpty())
+ _drawSurface.blitFrom(_image, _flagSrcRects[ft],
+ Common::Point(_flagImageRect.left, _flagImageRect.top));
+ }
+}
+
+void MatchPuzzle::redrawAllCells() {
+ _drawSurface.clear(_drawSurface.getTransparentColor());
+ drawScorePanel();
+ // During state 6 the score-screen covers everything; skip cell drawing
+ if (_gameSubState != kScoreDisplay) {
+ for (int col = 0; col < _cols; ++col)
+ for (int row = 0; row < _rows; ++row)
+ drawCell(col, row);
+ }
+ _needsRedraw = true;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/matchpuzzle.h b/engines/nancy/action/puzzle/matchpuzzle.h
index ef91a3e95e7..c3cdd886b35 100644
--- a/engines/nancy/action/puzzle/matchpuzzle.h
+++ b/engines/nancy/action/puzzle/matchpuzzle.h
@@ -23,12 +23,21 @@
#define NANCY_ACTION_MATCHPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+
+#include "graphics/managed_surface.h"
+#include "common/array.h"
+#include "common/path.h"
+#include "common/rect.h"
+#include "common/str.h"
namespace Nancy {
namespace Action {
-// Flag puzzle in Nancy 8
-
+// 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.
class MatchPuzzle : public RenderActionRecord {
public:
MatchPuzzle() : RenderActionRecord(7) {}
@@ -44,24 +53,161 @@ protected:
Common::String getRecordTypeName() const override { return "MatchPuzzle"; }
bool isViewportRelative() const override { return true; }
- Graphics::ManagedSurface _image;
- //Common::Rect _displayBounds;
-
- Common::Rect _shuffleButtonRect;
- Common::Array<Common::Rect> _flagRects;
-
- Common::Path _overlayName;
- Common::Path _flagPointBackgroundName;
-
- Common::StringArray _flagNames;
-
- SoundDescription _slotWinSound;
- SoundDescription _shuffleSound;
- SoundDescription _cardPlaceSound;
- SoundDescription _matchSuccessSound;
-
- SceneChangeWithFlag _solveSceneChange;
- SceneChangeWithFlag _exitSceneChange;
+ // ---------- Inner types ----------
+
+ struct GridCell {
+ int16 flagType = 0; // index into _flagSrcRects / _flagSoundNames
+ 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
+ };
+
+ // ---------- Helpers ----------
+
+ // Shuffle: if allCells=true all cells, otherwise only (col,row). FUN_0046421e
+ void shuffleGrid(bool allCells, int col = 0, int row = 0);
+ // Check cell (col,row) for a 3+ run; fills _match* fields. FUN_00464ba6
+ void checkForMatch(int col, int row);
+ // Compute the viewport-relative dest rect for cell (col,row). FUN_004643ef
+ void computeDestRect(int col, int row);
+
+ // Rendering helpers
+ void drawCell(int col, int row);
+ void eraseCell(int col, int row);
+ void redrawAllCells();
+ void drawScorePanel(); // FUN_004660ff
+
+ // ---------- Data (read from stream) ----------
+
+ Common::Path _overlayName; // main flag sprite sheet (CIFTREE)
+ Common::Path _flagPointBgName; // score-panel background image
+
+ int16 _rows = 0; // data+0x42
+ int16 _cols = 0; // data+0x44
+ int16 _numFlagTypes = 0; // data+0x46 (rand % (_numFlagTypes-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
+
+ // Script execution (data+0x238..0x23A); _execScript also gates flag-name display
+ bool _execScript = false;
+ int16 _scriptID = 0;
+
+ // Score-panel display font and label (data+0x23C..0x25E)
+ //uint16 _scorePanelFontID = 0; // data+0x23C
+ Common::String _displayLabelString; // data+0x23E (33 bytes)
+
+ // 26 per-flag-type names drawn in score panel on match (data+0x25F..0x5B8)
+ Common::StringArray _flagSoundNames;
+
+ // Score-panel display enable (data+0x63D)
+ bool _showScoreDisplay = false;
+
+ // 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
+
+ // Source rect for highlighted (matched) flag overlay (data+0x646)
+ Common::Rect _matchedFlagSrcRect;
+
+ int16 _timeBonusFor3 = 0; // data+0x656 extra seconds for 3-match
+ int16 _scoreBonusFor4 = 0; // data+0x658 extra points for 4-match
+ int16 _timeBonusFor4 = 0; // data+0x65A extra seconds for 4-match
+ int16 _scoreBonusFor5 = 0; // data+0x65C extra points for 5-match
+ int16 _timeBonusFor5 = 0; // data+0x65E extra seconds for 5-match
+ int16 _gridOffX = 0; // data+0x660 grid x offset within viewport
+ int16 _gridOffY = 0; // data+0x662 grid y offset within viewport
+ int16 _rowSpacing = 0; // data+0x664 extra pixels between rows
+ int16 _colSpacing = 0; // data+0x666 extra pixels between cols
+
+ // Score-panel destination rects (data+0x668..0x6E7, 8 Ã 16 bytes)
+ Common::Rect _labelStringRect; // data+0x668 â where to draw _displayLabelString
+ Common::Rect _shuffleButtonDestRect;// data+0x678 â on-screen position of shuffle button (hotspot)
+ Common::Rect _goalValueRect; // data+0x688 â where to draw goal value
+ Common::Rect _scoreValueRect; // data+0x698 â where to draw score value
+ Common::Rect _timerValueRect; // data+0x6A8 â where to draw timer
+ Common::Rect _flagNameRect; // data+0x6B8 â where to draw matched flag name
+ Common::Rect _flagImageRect; // data+0x6C8 â where to draw matched flag image
+ // data+0x6D8 (16 bytes): high-score display positions â skipped
+
+ // High-score display positions (data+0x6D8, 4 Ã int32 packed as a rect):
+ // left = x-coord for the "final score" value
+ // top = y-coord for the "final score" value
+ // right = x-coord for the high-score value list
+ // bottom= y-coord of the first high-score entry
+ Common::Rect _hsDisplayRect;
+
+ int16 _scoreDisplayDelay = 0; // data+0x6E8 score display pause (seconds)
+
+ // Sounds
+ SoundDescription _slotWinSound; // data+0x6EA â played during match anim
+ SoundDescription _shuffleSound; // data+0x71B
+ SoundDescription _cardPlaceSound; // data+0x74C
+ SoundDescription _matchSuccessSound; // data+0x798 â played on win/time-up
+
+ SceneChangeWithFlag _solveSceneChange; // data+0x77D win scene
+ SceneChangeWithFlag _exitSceneChange; // data+0x7C9 quit scene
+
+ Common::Rect _exitHotspot; // data+0x7E2 bottom-strip exit hotspot
+
+ // ---------- Runtime state ----------
+
+ enum GameSubState {
+ kPlaying = 0, // normal gameplay; process clicks, update timer, check win/lose
+ kStartEndSeq = 1, // play win/time-up sound, set display-delay timer, go to kWaitDelay
+ kMatchAnim = 2, // wait 800 ms + sound to finish, then reshuffle matched cells
+ 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
+ };
+
+ GameSubState _gameSubState = kPlaying;
+ bool _wonGame = false;
+
+ // Timer tracking
+ uint32 _timerDeadline = 0; // abs ms when timer expires
+ uint32 _stateTimer = 0; // abs ms for state timeouts
+ uint32 _shuffleTimer = 0; // abs ms for shuffle-button delay
+
+ // Score
+ int32 _score = 0;
+
+ // First-click selection (before the swap) â no visual, just remembered internally
+ bool _hasSelection = false;
+ int _selCol = 0, _selRow = 0;
+
+ // Post-swap match-check queue (piece1 = first cell, piece2 = second cell)
+ bool _hasPiece1 = false;
+ int _piece1Col = 0, _piece1Row = 0;
+ bool _hasPiece2 = false;
+ int _piece2Col = 0, _piece2Row = 0;
+
+ // Match results from checkForMatch
+ int _matchRowStart = 0, _matchRowEnd = 0; // vertical run bounds (row indices)
+ int _matchColStart = 0, _matchColEnd = 0; // horizontal run bounds (col indices)
+ bool _hasVMatch = false; // vertical run >= 3
+ bool _hasHMatch = false; // horizontal run >= 3
+ int16 _matchedFlagType = 0; // flag type of the matched run (for sound)
+
+ // Score panel display strings (updated whenever values change)
+ Common::String _goalStr; // formatted goal target, set once at init
+ Common::String _scoreStr; // formatted current score
+ Common::String _timerStr; // formatted time remaining
+ Common::String _flagNameStr;// name of the last matched flag type
+ bool _showFlagName = false; // true while matched-flag info is visible (this+0x800)
+ 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};
+
+ // Rendering
+ Common::Array<Common::Array<GridCell>> _grid; // _grid[col][row]
+
+ Graphics::ManagedSurface _image; // loaded sprite sheet
+ Graphics::ManagedSurface _scorePanelImage; // score-panel background
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list