[Scummvm-git-logs] scummvm master -> de8bc30818731610740996069539aa9a7dc679ec
bluegr
noreply at scummvm.org
Sat Mar 21 14:03:36 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:
de8bc30818 NANCY: Implement memorypuzzle for Nancy 9
Commit: de8bc30818731610740996069539aa9a7dc679ec
https://github.com/scummvm/scummvm/commit/de8bc30818731610740996069539aa9a7dc679ec
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-21T16:02:23+02:00
Commit Message:
NANCY: Implement memorypuzzle for Nancy 9
This is a memory matching puzzle, used in the Nancy 9 toy box.
It has three tabs, each containing 24 cards (6x4 grid).
The player flips cards to find matching pairs; matching pairs stay face-up.
Player wins when matchedPairs >= requiredPairs.
Changed paths:
engines/nancy/action/puzzle/memorypuzzle.cpp
engines/nancy/action/puzzle/memorypuzzle.h
diff --git a/engines/nancy/action/puzzle/memorypuzzle.cpp b/engines/nancy/action/puzzle/memorypuzzle.cpp
index 854c8983e0b..b22bcae8a6e 100644
--- a/engines/nancy/action/puzzle/memorypuzzle.cpp
+++ b/engines/nancy/action/puzzle/memorypuzzle.cpp
@@ -19,6 +19,8 @@
*
*/
+#include "common/random.h"
+
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
@@ -32,34 +34,344 @@
namespace Nancy {
namespace Action {
+// Binary layout (offsets from stream start, all LE):
+// 0x000 33 image filename
+// 0x021 576 36 x face src rect (int32 l/t/r/b x 4) [types 0..35]
+// 0x261 48 3 x tab indicator src rect (one per tab)
+// 0x291 384 24 x card screen rect (viewport-relative, int32 x 4)
+// 0x411 16 tab rect (screen rect where the active-tab graphic is drawn)
+// 0x421 144 3 x 3 x 16 tab hotspot rects [currentTab][targetTab]
+// 0x4b1 4 flipDelay (uint32, milliseconds)
+// 0x4b5 4 numPairs (uint32; clamped [4..36] in init)
+// 0x4b9 4 requiredPairs (uint32; clamped [2..36] in init)
+// 0x4bd 1 cursor flag (byte; skip in ScummVM)
+// 0x4be 1 shuffle flag (0 = pairs stay within tab, nonzero = global)
+// 0x4bf 49 match sound (played when a matching pair is found)
+// 0x4f0 49 card flip sound (played when flipping a card face-up)
+// 0x521 25 win SceneChangeWithFlag
+// 0x53a 1 unknown (skip)
+// 0x53b 49 win sound
+// 0x56c 16 exit hotspot rect
+// [total: 0x57c = 1404 bytes]
+void MemoryPuzzle::readData(Common::SeekableReadStream &stream) {
+ // 0x000: image filename (33 bytes)
+ readFilename(stream, _imageName);
+
+ // 0x021: 36 face src rects (for types 0..35)
+ for (int i = 0; i < kMaxTypes; ++i)
+ readRect(stream, _faceSrcRects[i]);
+
+ // 0x261: tab indicator src rects (one per tab; drawn over the active tab button)
+ for (int tab = 0; tab < kNumTabs; ++tab)
+ readRect(stream, _tabSrcRects[tab]);
+
+ // 0x291: 24 card screen-position rects (viewport-relative)
+ for (int i = 0; i < kCardsPerTab; ++i)
+ readRect(stream, _cardRects[i]);
+
+ // 0x411: tab rect (screen destination for the active tab indicator)
+ readRect(stream, _tabRect);
+
+ // 0x421: tab hotspot rects - 3 tabs x 3 slots x 16 bytes
+ for (int tab = 0; tab < kNumTabs; ++tab)
+ for (int slot = 0; slot < 3; ++slot)
+ readRect(stream, _tabHotspots[tab][slot]);
+
+ // 0x4b1: flipDelay (uint32), numPairs (uint32), requiredPairs (uint32)
+ _flipDelay = stream.readUint32LE();
+ _numPairs = stream.readUint32LE();
+ _requiredPairs = stream.readUint32LE();
+
+ // 0x4bd: cursor flag (ignored in ScummVM)
+ stream.skip(1);
+
+ // 0x4be: shuffle flag
+ _shuffleGlobal = (stream.readByte() != 0);
+
+ // 0x4bf: match sound; 0x4f0: card flip sound
+ _matchSound.readNormal(stream);
+ _cardFlipSound.readNormal(stream);
+
+ // 0x521: win scene + flag
+ _winScene.readData(stream);
+
+ stream.skip(1); // 0x53a: unknown
+
+ // 0x53b: win sound
+ _winSound.readNormal(stream);
+
+ // 0x56c: exit hotspot
+ readRect(stream, _exitHotspot);
+}
+
+// Shuffles type IDs (0..numPairs-1) into the 72-card array so that every type
+// appears exactly twice. numPairs is clamped to [4, 36]; cards beyond numPairs
+// remain typeId -1 (unassigned, unselectable). requiredPairs is clamped to [2, totalCards/2].
+void MemoryPuzzle::initCards() {
+ _numPairs = CLIP<uint32>(_numPairs, 4, (uint32)kMaxTypes);
+
+ const int totalCards = kNumTabs * kCardsPerTab;
+ const uint32 maxRequire = (uint32)(totalCards / 2);
+ _requiredPairs = CLIP<uint32>(_requiredPairs, 2, maxRequire);
+
+ // Init all cards
+ for (int i = 0; i < totalCards; ++i) {
+ _cards[i].typeId = -1;
+ _cards[i].flipState = 0;
+ _cards[i].matchState = 0;
+ }
+ _matchedPairs = 0;
+ _firstFlip = -1;
+ _secondFlip = -1;
+ _flipTimerActive = false;
+
+ int nextType = 0;
+
+ if (!_shuffleGlobal) {
+ // By-tab: pairs are always within the same tab.
+ for (int tab = 0; tab < kNumTabs; ++tab) {
+ int base = tab * kCardsPerTab;
+ for (int i = 0; i < kCardsPerTab; ++i) {
+ if (_cards[base + i].typeId != -1)
+ continue;
+ if (nextType >= _numPairs)
+ break; // all types used up for this tab
+
+ _cards[base + i].typeId = nextType;
+
+ // Find a random unassigned slot in the same tab for the pair
+ int partner;
+ do {
+ partner = g_nancy->_randomSource->getRandomNumber(kCardsPerTab - 1);
+ } while (_cards[base + partner].typeId != -1);
+ _cards[base + partner].typeId = nextType;
+
+ ++nextType;
+ }
+ }
+ } else {
+ // Global: pairs may be in different tabs.
+ for (int i = 0; i < totalCards; ++i) {
+ if (_cards[i].typeId != -1)
+ continue;
+ if (nextType >= _numPairs)
+ break;
+
+ _cards[i].typeId = nextType;
+
+ // Find a random unassigned slot anywhere
+ int partner;
+ do {
+ partner = g_nancy->_randomSource->getRandomNumber(totalCards - 1);
+ } while (_cards[partner].typeId != -1);
+ _cards[partner].typeId = nextType;
+
+ ++nextType;
+ }
+ }
+}
+
void MemoryPuzzle::init() {
- // TODO
+ 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(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ _currentTab = 0;
+ initCards();
+ redrawCards();
}
void MemoryPuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+ if (_cardFlipSound.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_cardFlipSound);
+ if (_matchSound.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_matchSound);
_state = kRun;
+ // fall through
+
+ case kRun:
+ switch (_solveSubState) {
+ case kPlaying:
+ // Flip-back timer: hide non-matching cards when timer expires
+ if (_flipTimerActive && g_system->getMillis() >= _flipTimerEnd)
+ flipBackCards();
+ checkIfSolved();
+ if (_isSolved)
+ _solveSubState = kPlayWinSound;
+ break;
+
+ case kPlayWinSound:
+ if (_winSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_winSound);
+ g_nancy->_sound->playSound(_winSound);
+ _solveSubState = kWaitWinSound;
+ } else {
+ _state = kActionTrigger;
+ }
+ break;
+
+ case kWaitWinSound:
+ if (!g_nancy->_sound->isSoundPlaying(_winSound)) {
+ g_nancy->_sound->stopSound(_winSound);
+ _state = kActionTrigger;
+ }
+ break;
+ }
+ break;
+
+ case kActionTrigger:
+ g_nancy->_sound->stopSound(_cardFlipSound);
+ g_nancy->_sound->stopSound(_matchSound);
+ g_nancy->_sound->stopSound(_winSound);
+ _winScene.execute();
+ finishExecution();
+ break;
+ }
+}
+
+void MemoryPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _solveSubState != kPlaying || _isSolved)
+ return;
+
+ Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+ Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+ // Exit hotspot
+ if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
+ g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+ if (input.input & NancyInput::kLeftMouseButtonUp)
+ _state = kActionTrigger;
+ return;
}
- // TODO
- // Stub - move to the winning screen
- warning("STUB - Memory puzzle");
- NancySceneState.setEventFlag(423, g_nancy->_true); // EV_Solved_Necklace_Box
- SceneChangeDescription scene;
- scene.sceneID = 3846;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
+ // Tab switching: _tabHotspots[currentTab][slot] where slot is the target tab
+ for (int slot = 0; slot < kNumTabs; ++slot) {
+ if (_tabHotspots[_currentTab][slot].contains(mouseVP)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ if ((input.input & NancyInput::kLeftMouseButtonUp) && slot != _currentTab) {
+ // Cancel any pending flip-back timer and flip both cards back
+ if (_flipTimerActive)
+ flipBackCards();
+ // Also flip back any lone first-flip card
+ if (_firstFlip != -1) {
+ _cards[_firstFlip].flipState = 0;
+ _firstFlip = -1;
+ }
+ _currentTab = slot;
+ redrawCards();
+ }
+ return;
+ }
+ }
+
+ // Card clicks are blocked while the flip-back timer is running
+ if (_flipTimerActive)
+ return;
+
+ if (!(input.input & NancyInput::kLeftMouseButtonUp))
+ return;
+
+ int base = _currentTab * kCardsPerTab;
+ for (int i = 0; i < kCardsPerTab; ++i) {
+ if (!_cardRects[i].contains(mouseVP))
+ continue;
+
+ int idx = base + i;
+ CardState &card = _cards[idx];
+
+ // Unassigned or already matched or face-up: ignore
+ if (card.typeId == -1 || card.matchState != 0 || card.flipState != 0)
+ return;
+
+ // Flip this card face-up
+ card.flipState = 1;
+ if (_cardFlipSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_cardFlipSound);
+ redrawCards();
+
+ if (_firstFlip == -1) {
+ // First card of a potential pair
+ _firstFlip = idx;
+ } else {
+ // Second card flipped: check for match
+ CardState &first = _cards[_firstFlip];
+ if (first.typeId == card.typeId && first.typeId != -1) {
+ // Match! Mark both as matched
+ first.matchState = 1;
+ card.matchState = 1;
+ first.flipState = 0;
+ card.flipState = 0;
+ ++_matchedPairs;
+ _firstFlip = -1;
+ if (_matchSound.name != "NO SOUND")
+ g_nancy->_sound->playSound(_matchSound);
+ redrawCards();
+ } else {
+ // No match: start flip-back timer
+ _secondFlip = idx;
+ _flipTimerActive = true;
+ _flipTimerEnd = g_system->getMillis() + _flipDelay;
+ }
+ }
+ return;
+ }
}
-void MemoryPuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+void MemoryPuzzle::checkIfSolved() {
+ if (_matchedPairs >= _requiredPairs)
+ _isSolved = true;
}
-void MemoryPuzzle::handleInput(NancyInput &input) {
- // TODO
+void MemoryPuzzle::flipBackCards() {
+ if (_firstFlip != -1) {
+ _cards[_firstFlip].flipState = 0;
+ _firstFlip = -1;
+ }
+ if (_secondFlip != -1) {
+ _cards[_secondFlip].flipState = 0;
+ _secondFlip = -1;
+ }
+ _flipTimerActive = false;
+ _flipTimerEnd = 0;
+ redrawCards();
+}
+
+void MemoryPuzzle::redrawCards() {
+ _drawSurface.clear(_drawSurface.getTransparentColor());
+
+ // Draw the active tab indicator over the corresponding tab button.
+ // The scene background shows inactive tab visuals; the overlay only marks the active one.
+ if (_currentTab < kNumTabs && !_tabSrcRects[_currentTab].isEmpty())
+ _drawSurface.blitFrom(_image, _tabSrcRects[_currentTab],
+ Common::Point(_tabRect.left, _tabRect.top));
+
+ // Draw face-up and matched cards. Face-down cards are left transparent so
+ // the scene background (which carries the card-back visual) shows through.
+ int base = _currentTab * kCardsPerTab;
+ for (int i = 0; i < kCardsPerTab; ++i) {
+ int idx = base + i;
+ const CardState &card = _cards[idx];
+ const Common::Rect &dest = _cardRects[i];
+
+ if (card.matchState != 0 || card.flipState != 0) {
+ int t = card.typeId;
+ if (t >= 0 && t < kMaxTypes && !_faceSrcRects[t].isEmpty())
+ _drawSurface.blitFrom(_image, _faceSrcRects[t],
+ Common::Point(dest.left, dest.top));
+ }
+ }
+ _needsRedraw = true;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/memorypuzzle.h b/engines/nancy/action/puzzle/memorypuzzle.h
index fe5d42c7881..0c17f967754 100644
--- a/engines/nancy/action/puzzle/memorypuzzle.h
+++ b/engines/nancy/action/puzzle/memorypuzzle.h
@@ -23,12 +23,15 @@
#define NANCY_ACTION_MEMORYPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
namespace Nancy {
namespace Action {
-// Memory puzzle (toy box) in Nancy 9
-
+// Memory matching puzzle, used in the Nancy 9 toy box.
+// It has three tabs, each containing 24 cards(6x4 grid).
+// The player flips cards to find matching pairs; matching pairs stay face-up.
+// Player wins when matchedPairs >= requiredPairs.
class MemoryPuzzle : public RenderActionRecord {
public:
MemoryPuzzle() : RenderActionRecord(7) {}
@@ -43,6 +46,68 @@ public:
protected:
Common::String getRecordTypeName() const override { return "MemoryPuzzle"; }
bool isViewportRelative() const override { return true; }
+
+ // File data
+
+ Common::Path _imageName;
+
+ static const int kMaxTypes = 36; // 3 tabs x 12 pairs each
+ static const int kCardsPerTab = 24; // hardcoded in original
+ static const int kNumTabs = 3; // hardcoded in original
+
+ Common::Rect _faceSrcRects[kMaxTypes]; // [type] -> face src rect on image
+ Common::Rect _tabSrcRects[kNumTabs]; // [tab] -> tab indicator src rect (drawn for active tab)
+
+ Common::Rect _cardRects[kCardsPerTab]; // viewport-relative screen positions (shared across tabs)
+ Common::Rect _tabRect; // screen rect where the active tab indicator is drawn
+ Common::Rect _tabHotspots[kNumTabs][3]; // [currentTab][targetTab]: hit-test rects for tab switching
+ Common::Rect _exitHotspot;
+
+ uint32 _numPairs = 12; // pairs in the shuffled layout (clamped [4..36])
+ uint32 _requiredPairs = 12; // pairs needed to win (clamped [2..36])
+ uint32 _flipDelay = 1500; // ms before non-matching cards flip back
+
+ bool _shuffleGlobal = false; // false = pairs stay within same tab; true = can cross tabs
+
+ SoundDescription _cardFlipSound; // played when a card is flipped face-up
+ SoundDescription _matchSound; // played when a matching pair is found
+ SceneChangeWithFlag _winScene;
+ SoundDescription _winSound;
+
+ // Runtime state
+
+ struct CardState {
+ int typeId = -1; // 0..35; -1 = unassigned
+ int flipState = 0; // 0 = face-down, 1 = face-up (player-flipped, unmatched)
+ int matchState = 0; // 0 = unmatched, 1 = matched (stays face-up)
+ };
+
+ // _cards[tab * kCardsPerTab + i] = state of card i on tab `tab`
+ CardState _cards[kNumTabs * kCardsPerTab];
+
+ Graphics::ManagedSurface _image;
+
+ int _currentTab = 0;
+ int _firstFlip = -1; // absolute card index of first face-up unmatched card
+ int _secondFlip = -1; // absolute card index of second (timer pending)
+ bool _flipTimerActive = false;
+ uint32 _flipTimerEnd = 0;
+ int _matchedPairs = 0;
+ bool _isSolved = false;
+
+ enum SolveSubState {
+ kPlaying = 0,
+ kPlayWinSound = 1,
+ kWaitWinSound = 2
+ };
+ SolveSubState _solveSubState = kPlaying;
+
+ // Internal methods
+
+ void initCards(); // shuffle types into all 72 card slots
+ void checkIfSolved();
+ void flipBackCards(); // unflip non-matching pair after timer expires
+ void redrawCards();
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list