[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