[Scummvm-git-logs] scummvm master -> 4ece07193d0c910c8112c366ce81748c40530843

bluegr noreply at scummvm.org
Sun May 24 23:04:17 UTC 2026


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

Summary:
f380dcfe22 NANCY: Implement SortPuzzle for Nancy10
b32bc5ffe0 NANCY: Implement BeadPuzzle for Nancy10
4ece07193d NANCY: Implement DotConnectPuzzle for Nancy10


Commit: f380dcfe22ff47d3cf738a245019fd13087caece
    https://github.com/scummvm/scummvm/commit/f380dcfe22ff47d3cf738a245019fd13087caece
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-25T02:04:03+03:00

Commit Message:
NANCY: Implement SortPuzzle for Nancy10

This is a sorting puzzle, where several gems are sorted per type, color
and size, which is visible in the back of each gem, when picked up

Changed paths:
  A engines/nancy/action/puzzle/sortpuzzle.cpp
  A engines/nancy/action/puzzle/sortpuzzle.h
    engines/nancy/action/arfactory.cpp
    engines/nancy/puzzledata.cpp
    engines/nancy/puzzledata.h


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 9f3fe55fbf2..22a8b9915e1 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -61,6 +61,7 @@
 #include "engines/nancy/action/puzzle/safedialpuzzle.h"
 #include "engines/nancy/action/puzzle/setplayerclock.h"
 #include "engines/nancy/action/puzzle/sliderpuzzle.h"
+#include "engines/nancy/action/puzzle/sortpuzzle.h"
 #include "engines/nancy/action/puzzle/soundequalizerpuzzle.h"
 #include "engines/nancy/action/puzzle/soundmatchpuzzle.h"
 #include "engines/nancy/action/puzzle/spigotpuzzle.h"
@@ -461,7 +462,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new MemoryPuzzle();
 	// -- Nancy 10 and up --
 	case 239:
-		//return new SortPuzzle();
+		return new SortPuzzle();
 	case 241:
 		// return new DotConnectPuzzle();
 	case 242:
diff --git a/engines/nancy/action/puzzle/sortpuzzle.cpp b/engines/nancy/action/puzzle/sortpuzzle.cpp
new file mode 100644
index 00000000000..fdb103184ee
--- /dev/null
+++ b/engines/nancy/action/puzzle/sortpuzzle.cpp
@@ -0,0 +1,397 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/random.h"
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/puzzledata.h"
+#include "engines/nancy/action/puzzle/sortpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+// Tiles within a color group share their board sprite (srcRow, srcCol); the
+// hidden `value` field disambiguates them, which is visible in the back of
+// each gem, when picking it up.
+// TODO: Use correct cursor type (hand instead of eyeglass) when picking up gems.
+static void packGrid(const SortPuzzle::Cell grid[][SortPuzzle::kMaxCols], uint16 rows, uint16 cols, Common::Array<int16> &out) {
+	out.clear();
+	out.push_back((int16)rows);
+	out.push_back((int16)cols);
+	for (uint16 r = 0; r < rows; ++r) {
+		for (uint16 c = 0; c < cols; ++c) {
+			const SortPuzzle::Cell &cell = grid[r][c];
+			out.push_back(cell.srcRow);
+			out.push_back(cell.srcCol);
+			out.push_back(cell.value);
+			out.push_back(cell.isEmpty ? 1 : 0);
+		}
+	}
+}
+
+static bool unpackGrid(const Common::Array<int16> &in, SortPuzzle::Cell grid[][SortPuzzle::kMaxCols], uint16 expectedRows, uint16 expectedCols) {
+	if (in.size() < 2 || in[0] != (int16)expectedRows || in[1] != (int16)expectedCols)
+		return false;
+	uint32 need = 2 + (uint32)expectedRows * expectedCols * 4;
+	if (in.size() < need)
+		return false;
+	uint32 idx = 2;
+	for (uint16 r = 0; r < expectedRows; ++r) {
+		for (uint16 c = 0; c < expectedCols; ++c) {
+			SortPuzzle::Cell &cell = grid[r][c];
+			cell.srcRow  = in[idx++];
+			cell.srcCol  = in[idx++];
+			cell.value   = in[idx++];
+			cell.isEmpty = (in[idx++] != 0);
+		}
+	}
+	return true;
+}
+
+void SortPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _boardImageName);
+	readFilename(stream, _cursorImageName);
+
+	_retainState  = (stream.readByte() != 0);
+	_rows         = stream.readUint16LE();
+	_cols         = stream.readUint16LE();
+	stream.skip(2);
+	_groupDivisor = stream.readUint16LE();
+	_valueRange   = stream.readUint16LE();
+
+	for (int r = 0; r < kMaxSourceRows; ++r)
+		for (int c = 0; c < kMaxSourceCols; ++c)
+			readRect(stream, _cellSrcRects[r][c]);
+
+	for (int i = 0; i < kNumCursors; ++i)
+		readRect(stream, _cursorSrcRects[i]);
+
+	_originX  = stream.readUint16LE();
+	_originY  = stream.readUint16LE();
+	_spacingY = stream.readUint16LE();
+	_spacingX = stream.readUint16LE();
+
+	_pickupSound.readNormal(stream);
+	_dropSound.readNormal(stream);
+
+	_winScene.readData(stream);
+	stream.skip(2);
+	_winFlag.label = stream.readSint16LE();
+	_winFlag.flag  = stream.readByte();
+	_winSound.readNormal(stream);
+
+	_cancelScene.readData(stream);
+	stream.skip(2);
+	_cancelFlag.label = stream.readSint16LE();
+	_cancelFlag.flag  = stream.readByte();
+
+	readRect(stream, _exitHotspot);
+	stream.skip(2); // exit cursor type id
+
+	if (_rows > kMaxRows) _rows = kMaxRows;
+	if (_cols > kMaxCols) _cols = kMaxCols;
+	if (_groupDivisor == 0) _groupDivisor = 1;
+	if (_valueRange == 0) _valueRange = 1;
+
+	_cellWidth  = _cellSrcRects[0][0].width();
+	_cellHeight = _cellSrcRects[0][0].height();
+}
+
+void SortPuzzle::initState() {
+	SortPuzzleData *spd = (SortPuzzleData *)NancySceneState.getPuzzleData(SortPuzzleData::getTag());
+	if (_retainState && spd && !spd->currentState.empty() && !spd->solvedState.empty()) {
+		if (unpackGrid(spd->currentState, _current, _rows, _cols) &&
+		    unpackGrid(spd->solvedState, _solved, _rows, _cols))
+			return;
+	}
+
+	const int groupSize = MAX<int>(1, (int)_cols / (int)_groupDivisor);
+
+	for (int r = 0; r < (int)_rows; ++r) {
+		for (int c = 0; c < (int)_cols; ++c) {
+			Cell &cell = _solved[r][c];
+			cell.srcRow  = (int16)r;
+			cell.srcCol  = (int16)(c / groupSize);
+			cell.value   = (int16)(g_nancy->_randomSource->getRandomNumber(_valueRange - 1));
+			cell.isEmpty = false;
+
+			int groupStart = (c / groupSize) * groupSize;
+			int k = c;
+			while (k > groupStart && _solved[r][k].value < _solved[r][k - 1].value) {
+				SWAP(_solved[r][k], _solved[r][k - 1]);
+				--k;
+			}
+		}
+	}
+
+	const int total = (int)_rows * (int)_cols;
+	bool taken[kMaxRows * kMaxCols] = {};
+	for (int r = 0; r < (int)_rows; ++r) {
+		for (int c = 0; c < (int)_cols; ++c) {
+			int idx;
+			do {
+				idx = (int)g_nancy->_randomSource->getRandomNumber(total - 1);
+			} while (taken[idx]);
+			taken[idx] = true;
+			int sr = idx / (int)_cols;
+			int sc = idx % (int)_cols;
+			_current[r][c] = _solved[sr][sc];
+		}
+	}
+
+	persistState();
+}
+
+void SortPuzzle::persistState() {
+	SortPuzzleData *spd = (SortPuzzleData *)NancySceneState.getPuzzleData(SortPuzzleData::getTag());
+	if (!spd)
+		return;
+	packGrid(_current, _rows, _cols, spd->currentState);
+	packGrid(_solved,  _rows, _cols, spd->solvedState);
+}
+
+void SortPuzzle::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(_boardImageName, _boardImage);
+	_boardImage.setTransparentColor(_drawSurface.getTransparentColor());
+	g_nancy->_resource->loadImage(_cursorImageName, _cursorImage);
+	_cursorImage.setTransparentColor(_drawSurface.getTransparentColor());
+
+	initState();
+
+	_hasHeld  = false;
+	_isSolved = false;
+	_subState = kPlaying;
+
+	redraw();
+}
+
+void SortPuzzle::execute() {
+	switch (_state) {
+	case kBegin:
+		init();
+		registerGraphics();
+		_state = kRun;
+		// fall through
+
+	case kRun:
+		switch (_subState) {
+		case kPlaying:
+			break;
+		case kPlayWinSound:
+			if (_winSound.name != "NO SOUND") {
+				g_nancy->_sound->loadSound(_winSound);
+				g_nancy->_sound->playSound(_winSound);
+				_subState = kWaitWinSound;
+			} else {
+				_subState = kExitToWin;
+			}
+			break;
+		case kWaitWinSound:
+			if (!g_nancy->_sound->isSoundPlaying(_winSound)) {
+				g_nancy->_sound->stopSound(_winSound);
+				_subState = kExitToWin;
+			}
+			break;
+		case kExitToWin:
+		case kExitToCancel:
+			_state = kActionTrigger;
+			break;
+		}
+		break;
+
+	case kActionTrigger:
+		g_nancy->_sound->stopSound(_pickupSound);
+		g_nancy->_sound->stopSound(_dropSound);
+		g_nancy->_sound->stopSound(_winSound);
+		if (_subState == kExitToWin) {
+			SortPuzzleData *spd = (SortPuzzleData *)NancySceneState.getPuzzleData(SortPuzzleData::getTag());
+			if (spd) {
+				spd->currentState.clear();
+				spd->solvedState.clear();
+			}
+			if (_winFlag.label != -1)
+				NancySceneState.setEventFlag(_winFlag);
+			if (_winScene.sceneID != kNoScene)
+				NancySceneState.changeScene(_winScene);
+		} else {
+			if (_cancelFlag.label != -1)
+				NancySceneState.setEventFlag(_cancelFlag);
+			if (_cancelScene.sceneID != kNoScene)
+				NancySceneState.changeScene(_cancelScene);
+		}
+		finishExecution();
+		break;
+	}
+}
+
+Common::Rect SortPuzzle::cellRect(int row, int col) const {
+	int x = (int)_originX + col * ((int)_spacingX + _cellWidth);
+	int y = (int)_originY + row * ((int)_spacingY + _cellHeight);
+	return Common::Rect(x, y, x + _cellWidth, y + _cellHeight);
+}
+
+bool SortPuzzle::hitTestCell(const Common::Point &p, int &outRow, int &outCol) const {
+	for (int r = 0; r < (int)_rows; ++r) {
+		for (int c = 0; c < (int)_cols; ++c) {
+			if (cellRect(r, c).contains(p)) {
+				outRow = r;
+				outCol = c;
+				return true;
+			}
+		}
+	}
+	return false;
+}
+
+void SortPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun || _subState != kPlaying)
+		return;
+
+	Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+	Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+	if (_hasHeld && _heldDrawPos != mouseVP) {
+		_heldDrawPos = mouseVP;
+		redraw();
+	}
+
+	int row, col;
+	bool hitCell = hitTestCell(mouseVP, row, col);
+
+	if (!hitCell) {
+		if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
+			g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+			if (input.input & NancyInput::kLeftMouseButtonUp)
+				_subState = kExitToCancel;
+		}
+		return;
+	}
+
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+	if (!(input.input & NancyInput::kLeftMouseButtonUp))
+		return;
+
+	if (!_hasHeld) {
+		if (_current[row][col].isEmpty)
+			return;
+		_held = _current[row][col];
+		_hasHeld = true;
+		_current[row][col].isEmpty = true;
+		if (_pickupSound.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_pickupSound);
+			g_nancy->_sound->playSound(_pickupSound);
+		}
+	} else {
+		Cell target = _current[row][col];
+		_current[row][col] = _held;
+		_current[row][col].isEmpty = false;
+		if (_dropSound.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_dropSound);
+			g_nancy->_sound->playSound(_dropSound);
+		}
+		if (target.isEmpty) {
+			_hasHeld = false;
+			checkSolved();
+		} else {
+			_held = target;
+		}
+	}
+
+	persistState();
+
+	redraw();
+}
+
+void SortPuzzle::checkSolved() {
+	for (int r = 0; r < (int)_rows; ++r) {
+		for (int c = 0; c < (int)_cols; ++c) {
+			const Cell &cur = _current[r][c];
+			const Cell &sol = _solved[r][c];
+			if (cur.isEmpty || cur.srcRow != sol.srcRow ||
+			    cur.srcCol != sol.srcCol || cur.value != sol.value)
+				return;
+		}
+	}
+	_isSolved = true;
+	_subState = kPlayWinSound;
+}
+
+void SortPuzzle::redraw() {
+	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	for (int r = 0; r < (int)_rows; ++r) {
+		for (int c = 0; c < (int)_cols; ++c) {
+			const Cell &cell = _current[r][c];
+			if (cell.isEmpty)
+				continue;
+			if (cell.srcRow < 0 || cell.srcRow >= kMaxSourceRows ||
+			    cell.srcCol < 0 || cell.srcCol >= kMaxSourceCols)
+				continue;
+			const Common::Rect &src = _cellSrcRects[cell.srcRow][cell.srcCol];
+			if (src.isEmpty())
+				continue;
+			Common::Rect dst = cellRect(r, c);
+			_drawSurface.blitFrom(_boardImage, src, Common::Point(dst.left, dst.top));
+		}
+	}
+
+	if (_hasHeld) {
+		bool drawn = false;
+		if (_held.value >= 0 && _held.value < kNumCursors) {
+			const Common::Rect &src = _cursorSrcRects[_held.value];
+			if (!src.isEmpty()) {
+				int x = _heldDrawPos.x - src.width()  / 2;
+				int y = _heldDrawPos.y - src.height() / 2;
+				_drawSurface.blitFrom(_cursorImage, src, Common::Point(x, y));
+				drawn = true;
+			}
+		}
+		if (!drawn && _held.srcRow >= 0 && _held.srcRow < kMaxSourceRows &&
+		    _held.srcCol >= 0 && _held.srcCol < kMaxSourceCols) {
+			const Common::Rect &src = _cellSrcRects[_held.srcRow][_held.srcCol];
+			if (!src.isEmpty()) {
+				int x = _heldDrawPos.x - _cellWidth  / 2;
+				int y = _heldDrawPos.y - _cellHeight / 2;
+				_drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
+			}
+		}
+	}
+
+	_needsRedraw = true;
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/sortpuzzle.h b/engines/nancy/action/puzzle/sortpuzzle.h
new file mode 100644
index 00000000000..5002db89dd6
--- /dev/null
+++ b/engines/nancy/action/puzzle/sortpuzzle.h
@@ -0,0 +1,133 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_SORTPUZZLE_H
+#define NANCY_ACTION_SORTPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+
+namespace Nancy {
+namespace Action {
+
+// Tile-sorting puzzle: cells of a shuffled grid are picked up and swapped one
+// at a time until every cell is back in the position it held in the original
+// (pre-shuffle) layout. Tiles are sorted per type, color and size, which is
+// visible in the back of each gem when picked up.
+// Called from scene 2036 in Nancy10.
+class SortPuzzle : public RenderActionRecord {
+public:
+	SortPuzzle() : RenderActionRecord(7) {}
+	virtual ~SortPuzzle() {}
+
+	void init() override;
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+	void handleInput(NancyInput &input) override;
+
+	bool isViewportRelative() const override { return true; }
+
+	static const int kMaxRows = 9;
+	static const int kMaxCols = 9;
+
+	struct Cell {
+		int16 srcRow = 0;
+		int16 srcCol = 0;
+		int16 value = 0;
+		bool isEmpty = false;
+	};
+
+protected:
+	Common::String getRecordTypeName() const override { return "SortPuzzle"; }
+
+	static const int kMaxSourceRows = 8;
+	static const int kMaxSourceCols = 5;
+	static const int kNumCursors    = 10;
+
+	// File data
+
+	Common::Path _boardImageName;
+	Common::Path _cursorImageName;
+
+	bool   _retainState  = false;
+	uint16 _rows         = 0;
+	uint16 _cols         = 0;
+	uint16 _groupDivisor = 1;
+	uint16 _valueRange   = 1;
+
+	Common::Rect _cellSrcRects[kMaxSourceRows][kMaxSourceCols];
+	Common::Rect _cursorSrcRects[kNumCursors];
+
+	uint16 _originX  = 0;
+	uint16 _originY  = 0;
+	uint16 _spacingX = 0;
+	uint16 _spacingY = 0;
+	int16  _cellWidth  = 0;
+	int16  _cellHeight = 0;
+
+	SoundDescription _pickupSound;
+	SoundDescription _dropSound;
+
+	SceneChangeDescription _winScene;
+	FlagDescription        _winFlag;
+	SoundDescription       _winSound;
+	SceneChangeDescription _cancelScene;
+	FlagDescription        _cancelFlag;
+
+	Common::Rect _exitHotspot;
+
+	// Runtime state
+
+	enum SubState {
+		kPlaying = 0,
+		kPlayWinSound,
+		kWaitWinSound,
+		kExitToWin,
+		kExitToCancel
+	};
+
+	SubState _subState = kPlaying;
+
+	Cell _current[kMaxRows][kMaxCols];
+	Cell _solved[kMaxRows][kMaxCols];
+
+	Cell _held;
+	bool _hasHeld   = false;
+	bool _isSolved  = false;
+
+	Common::Point _heldDrawPos;
+
+	Graphics::ManagedSurface _boardImage;
+	Graphics::ManagedSurface _cursorImage;
+
+	void initState();
+	void persistState();
+	void redraw();
+	void checkSolved();
+	Common::Rect cellRect(int row, int col) const;
+	bool hitTestCell(const Common::Point &p, int &outRow, int &outCol) const;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_SORTPUZZLE_H
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 5d1781dee78..6c7c7c3a306 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -213,6 +213,19 @@ void TableData::synchronize(Common::Serializer &ser) {
 	ser.syncArray(comboValues.data(), num, Common::Serializer::FloatLE);
 }
 
+static void syncInt16Array(Common::Serializer &ser, Common::Array<int16> &arr) {
+	uint16 num = (uint16)arr.size();
+	ser.syncAsUint16LE(num);
+	if (ser.isLoading())
+		arr.resize(num);
+	ser.syncArray(arr.data(), num, Common::Serializer::Sint16LE);
+}
+
+void SortPuzzleData::synchronize(Common::Serializer &ser) {
+	syncInt16Array(ser, currentState);
+	syncInt16Array(ser, solvedState);
+}
+
 void QuizPuzzleData::synchronize(Common::Serializer &ser) {
 	// Serialize as: numScenes, then for each scene: sceneID, numBoxes, box data
 	uint16 numScenes = (uint16)boxCorrect.size();
@@ -324,6 +337,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
 		return new AssemblyPuzzleData();
 	case QuizPuzzleData::getTag():
 		return new QuizPuzzleData();
+	case SortPuzzleData::getTag():
+		return new SortPuzzleData();
 	case JournalData::getTag():
 		return new JournalData();
 	case TableData::getTag():
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index d5df60db10c..29ad5d982e2 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -115,6 +115,20 @@ struct AssemblyPuzzleData : public SimplePuzzleData {
 	static constexpr uint32 getTag() { return MKTAG('A', 'S', 'M', 'B'); }
 };
 
+// Cached current/solved tile layouts for a SortPuzzle. Each cell is encoded as
+// 4 consecutive int16s: srcRow, srcCol, value, isEmpty. The first two int16s
+// of each array are the grid rows and cols.
+struct SortPuzzleData : public PuzzleData {
+	SortPuzzleData() {}
+	virtual ~SortPuzzleData() {}
+
+	static constexpr uint32 getTag() { return MKTAG('S', 'O', 'R', 'T'); }
+	virtual void synchronize(Common::Serializer &ser);
+
+	Common::Array<int16> currentState;
+	Common::Array<int16> solvedState;
+};
+
 struct QuizPuzzleData : public PuzzleData {
 	QuizPuzzleData() {}
 	virtual ~QuizPuzzleData() {}


Commit: b32bc5ffe04bbbe51e264b95594fa381a9dd9d57
    https://github.com/scummvm/scummvm/commit/b32bc5ffe04bbbe51e264b95594fa381a9dd9d57
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-25T02:04:05+03:00

Commit Message:
NANCY: Implement BeadPuzzle for Nancy10

String-of-beads puzzle. The player picks beads from a source area and places
them onto a thread; when all slots are filled the sequence is scored against
the solution, yielding perfect / partial / wrong results.

Changed paths:
  A engines/nancy/action/puzzle/beadpuzzle.cpp
  A engines/nancy/action/puzzle/beadpuzzle.h
    engines/nancy/action/arfactory.cpp
    engines/nancy/puzzledata.cpp
    engines/nancy/puzzledata.h


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 22a8b9915e1..415c104c327 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -36,6 +36,7 @@
 #include "engines/nancy/action/puzzle/arcadepuzzle.h"
 #include "engines/nancy/action/puzzle/assemblypuzzle.h"
 #include "engines/nancy/action/puzzle/bballpuzzle.h"
+#include "engines/nancy/action/puzzle/beadpuzzle.h"
 #include "engines/nancy/action/puzzle/bulpuzzle.h"
 #include "engines/nancy/action/puzzle/bombpuzzle.h"
 #include "engines/nancy/action/puzzle/collisionpuzzle.h"
@@ -468,7 +469,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 	case 242:
 		// return new MagnetMazePuzzle();
 	case 243:
-		// return new BeadPuzzle();
+		return new BeadPuzzle();
 	case 244:
 		// return new GridMapPuzzle();
 	// -- Nancy 11 and up --
diff --git a/engines/nancy/action/puzzle/beadpuzzle.cpp b/engines/nancy/action/puzzle/beadpuzzle.cpp
new file mode 100644
index 00000000000..291403e224d
--- /dev/null
+++ b/engines/nancy/action/puzzle/beadpuzzle.cpp
@@ -0,0 +1,391 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/puzzledata.h"
+#include "engines/nancy/action/puzzle/beadpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+static BeadPuzzleData *getBeadData() {
+	return (BeadPuzzleData *)NancySceneState.getPuzzleData(BeadPuzzleData::getTag());
+}
+
+static const uint32 kDropTickMs = 40;
+
+void BeadPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageName);
+
+	_numSlots     = stream.readUint16LE();
+	_numBeadTypes = stream.readUint16LE();
+
+	for (int i = 0; i < kMaxBeadTypes; ++i)
+		readRect(stream, _beadSrcRects[i]);
+
+	readRect(stream, _threadSrc);
+	readRect(stream, _threadDest);
+	readRect(stream, _removeHotspot);
+	readRect(stream, _resultHotspot);
+
+	for (int i = 0; i < kMaxBeadTypes; ++i)
+		readRect(stream, _pickupHotspots[i]);
+
+	for (int i = 0; i < kMaxSlots; ++i)
+		readRect(stream, _slotDestRects[i]);
+
+	for (int i = 0; i < kMaxSlots; ++i)
+		readRect(stream, _resultDestRects[i]);
+
+	// Solution stores beadType+1 per slot; 0 marks unused tail entries.
+	for (int i = 0; i < kMaxSlots; ++i)
+		_solution[i] = stream.readUint16LE();
+
+	stream.skip(2);
+
+	_pickupSound.readNormal(stream);
+	_placeSound.readNormal(stream);
+	_removeSound.readNormal(stream);
+
+	_mistakeThreshold = stream.readSint16LE();
+
+	_partialSound.readNormal(stream);
+	_wrongSound.readNormal(stream);
+
+	_partialFlag.label = stream.readSint16LE();
+	_partialFlag.flag  = stream.readByte();
+
+	_perfectSound.readNormal(stream);
+
+	_perfectFlag.label = stream.readSint16LE();
+	_perfectFlag.flag  = stream.readByte();
+
+	_defaultScene.readData(stream);
+	stream.skip(2);
+	_solvedScene.readData(stream);
+	stream.skip(2);
+
+	readRect(stream, _exitHotspot);
+
+	if (_numSlots > kMaxSlots)
+		_numSlots = kMaxSlots;
+	if (_numBeadTypes > kMaxBeadTypes)
+		_numBeadTypes = kMaxBeadTypes;
+}
+
+void BeadPuzzle::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(_imageName, _image);
+	_image.setTransparentColor(_drawSurface.getTransparentColor());
+
+	_placed.clear();
+	BeadPuzzleData *bpd = getBeadData();
+	if (bpd) {
+		for (uint i = 0; i < bpd->placedBeads.size() && i < (uint)_numSlots; ++i)
+			_placed.push_back(bpd->placedBeads[i]);
+	}
+
+	_heldBead = -1;
+	_subState = kPlaying;
+	_resultKind = kNoResult;
+	_resultSoundPlayed = false;
+
+	redraw();
+}
+
+void BeadPuzzle::persistState() {
+	BeadPuzzleData *bpd = getBeadData();
+	if (!bpd)
+		return;
+	bpd->placedBeads.clear();
+	for (uint i = 0; i < _placed.size(); ++i)
+		bpd->placedBeads.push_back(_placed[i]);
+}
+
+void BeadPuzzle::execute() {
+	switch (_state) {
+	case kBegin:
+		init();
+		registerGraphics();
+		_state = kRun;
+		// fall through
+
+	case kRun:
+		switch (_subState) {
+		case kPlaying:
+			if ((int)_placed.size() >= (int)_numSlots && _heldBead == -1)
+				evaluate();
+			break;
+
+		case kDroppingBead:
+			if (g_system->getMillis() >= _dropNextTick) {
+				if (_dropCurrentSlot <= (int)_placed.size()) {
+					_placed.push_back(_heldBead);
+					_heldBead = -1;
+					_subState = kPlaying;
+					persistState();
+				} else {
+					--_dropCurrentSlot;
+					_dropNextTick = g_system->getMillis() + kDropTickMs;
+				}
+				redraw();
+			}
+			break;
+
+		case kShowingResult:
+			if (!_resultSoundPlayed) {
+				const SoundDescription *snd = nullptr;
+				if (_resultKind == kPartial)      snd = &_partialSound;
+				else if (_resultKind == kWrong)   snd = &_wrongSound;
+				if (snd && snd->name != "NO SOUND") {
+					g_nancy->_sound->loadSound(*snd);
+					g_nancy->_sound->playSound(*snd);
+				}
+				_resultSoundPlayed = true;
+			}
+			break;
+
+		case kPerfectWaitSound:
+			if (g_system->getMillis() >= _perfectExitTime &&
+			    !g_nancy->_sound->isSoundPlaying(_perfectSound)) {
+				g_nancy->_sound->stopSound(_perfectSound);
+				_subState = kExitToSolved;
+			}
+			break;
+
+		case kExitToSolved:
+		case kExitToDefault:
+			_state = kActionTrigger;
+			break;
+		}
+		break;
+
+	case kActionTrigger:
+		g_nancy->_sound->stopSound(_pickupSound);
+		g_nancy->_sound->stopSound(_placeSound);
+		g_nancy->_sound->stopSound(_removeSound);
+		g_nancy->_sound->stopSound(_partialSound);
+		g_nancy->_sound->stopSound(_wrongSound);
+		g_nancy->_sound->stopSound(_perfectSound);
+		{
+			const SceneChangeDescription &dest = (_subState == kExitToSolved) ? _solvedScene : _defaultScene;
+			if (dest.sceneID != kNoScene)
+				NancySceneState.changeScene(dest);
+		}
+		finishExecution();
+		break;
+	}
+}
+
+void BeadPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun)
+		return;
+	if (_subState == kDroppingBead ||
+	    _subState == kPerfectWaitSound ||
+	    _subState == kExitToSolved ||
+	    _subState == kExitToDefault)
+		return;
+
+	Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+	Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+	if (_heldBead != -1 && _heldDrawPos != mouseVP) {
+		_heldDrawPos = mouseVP;
+		redraw();
+	}
+
+	if (_subState == kShowingResult) {
+		if (!_resultHotspot.isEmpty() && _resultHotspot.contains(mouseVP)) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				g_nancy->_sound->stopSound(_partialSound);
+				g_nancy->_sound->stopSound(_wrongSound);
+				if (!_placed.empty())
+					_placed.pop_back();
+				_heldBead = -1;
+				_subState = kPlaying;
+				_resultKind = kNoResult;
+				_resultSoundPlayed = false;
+				persistState();
+				redraw();
+			}
+			return;
+		}
+	} else if (_heldBead == -1) {
+		for (int i = 0; i < (int)_numBeadTypes; ++i) {
+			if (_pickupHotspots[i].isEmpty() || !_pickupHotspots[i].contains(mouseVP))
+				continue;
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				if ((int)_placed.size() < (int)_numSlots) {
+					_heldBead = (int16)i;
+					_heldDrawPos = mouseVP;
+					if (_pickupSound.name != "NO SOUND") {
+						g_nancy->_sound->loadSound(_pickupSound);
+						g_nancy->_sound->playSound(_pickupSound);
+					}
+					redraw();
+				}
+			}
+			return;
+		}
+
+		if (!_removeHotspot.isEmpty() && _removeHotspot.contains(mouseVP) && !_placed.empty()) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_placed.pop_back();
+				if (_removeSound.name != "NO SOUND") {
+					g_nancy->_sound->loadSound(_removeSound);
+					g_nancy->_sound->playSound(_removeSound);
+				}
+				persistState();
+				redraw();
+			}
+			return;
+		}
+	} else {
+		if ((int)_placed.size() < (int)_numSlots &&
+		    !_removeHotspot.isEmpty() &&
+		    _removeHotspot.contains(mouseVP)) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				g_nancy->_sound->stopSound(_pickupSound);
+				if (_placeSound.name != "NO SOUND") {
+					g_nancy->_sound->loadSound(_placeSound);
+					g_nancy->_sound->playSound(_placeSound);
+				}
+				// Animate the bead sliding from the bottom of the thread up
+				// to the next free slot before committing it.
+				_subState = kDroppingBead;
+				_dropCurrentSlot = (int16)((int)_numSlots - 1);
+				_dropNextTick = g_system->getMillis() + kDropTickMs;
+				redraw();
+			}
+			return;
+		}
+
+		if (_heldBead >= 0 && _heldBead < (int)_numBeadTypes &&
+		    !_pickupHotspots[_heldBead].isEmpty() &&
+		    _pickupHotspots[_heldBead].contains(mouseVP)) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_heldBead = -1;
+				g_nancy->_sound->stopSound(_pickupSound);
+				redraw();
+			}
+			return;
+		}
+	}
+
+	if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
+		g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+		if (input.input & NancyInput::kLeftMouseButtonUp)
+			_subState = kExitToDefault;
+	}
+}
+
+void BeadPuzzle::evaluate() {
+	int mismatches = 0;
+	for (int i = 0; i < (int)_numSlots; ++i) {
+		int expected = (int)_solution[i] - 1;
+		int actual   = _placed[i];
+		if (actual != expected)
+			++mismatches;
+	}
+
+	if (mismatches == 0) {
+		_resultKind = kPerfect;
+		if (_perfectFlag.label != -1)
+			NancySceneState.setEventFlag(_perfectFlag);
+		if (_perfectSound.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_perfectSound);
+			g_nancy->_sound->playSound(_perfectSound);
+		}
+		_perfectExitTime = g_system->getMillis() + 4000;
+		_subState = kPerfectWaitSound;
+	} else if (mismatches > _mistakeThreshold) {
+		_resultKind = kWrong;
+		_subState = kShowingResult;
+	} else {
+		_resultKind = kPartial;
+		if (_partialFlag.label != -1)
+			NancySceneState.setEventFlag(_partialFlag);
+		_subState = kShowingResult;
+	}
+
+	redraw();
+}
+
+void BeadPuzzle::redraw() {
+	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	if (!_threadSrc.isEmpty() && !_threadDest.isEmpty())
+		_drawSurface.blitFrom(_image, _threadSrc, Common::Point(_threadDest.left, _threadDest.top));
+
+	const bool resultMode = (_subState == kShowingResult ||
+	                         _subState == kPerfectWaitSound);
+	const Common::Rect *dests = resultMode ? _resultDestRects : _slotDestRects;
+
+	for (int i = 0; i < (int)_placed.size(); ++i) {
+		int t = _placed[i];
+		if (t < 0 || t >= (int)_numBeadTypes)
+			continue;
+		const Common::Rect &src = _beadSrcRects[t];
+		const Common::Rect &dst = dests[i];
+		if (src.isEmpty() || dst.isEmpty())
+			continue;
+		_drawSurface.blitFrom(_image, src, Common::Point(dst.left, dst.top));
+	}
+
+	if (_subState == kDroppingBead && _heldBead >= 0 && _heldBead < (int)_numBeadTypes &&
+	    _dropCurrentSlot >= 0 && _dropCurrentSlot < (int)_numSlots) {
+		const Common::Rect &src = _beadSrcRects[_heldBead];
+		const Common::Rect &dst = _slotDestRects[_dropCurrentSlot];
+		if (!src.isEmpty() && !dst.isEmpty())
+			_drawSurface.blitFrom(_image, src, Common::Point(dst.left, dst.top));
+	} else if (_subState == kPlaying && _heldBead >= 0 && _heldBead < (int)_numBeadTypes) {
+		const Common::Rect &src = _beadSrcRects[_heldBead];
+		if (!src.isEmpty()) {
+			int x = _heldDrawPos.x - src.width()  / 2;
+			int y = _heldDrawPos.y - src.height() / 2;
+			_drawSurface.blitFrom(_image, src, Common::Point(x, y));
+		}
+	}
+
+	_needsRedraw = true;
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/beadpuzzle.h b/engines/nancy/action/puzzle/beadpuzzle.h
new file mode 100644
index 00000000000..2d3bfdd7c57
--- /dev/null
+++ b/engines/nancy/action/puzzle/beadpuzzle.h
@@ -0,0 +1,130 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_BEADPUZZLE_H
+#define NANCY_ACTION_BEADPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+
+namespace Nancy {
+namespace Action {
+
+// String-of-beads puzzle. The player picks beads from a source area and places
+// them onto a thread; when all slots are filled the sequence is scored against
+// the solution, yielding perfect / partial / wrong results.
+// Called from scene 6251 in Nancy10.
+class BeadPuzzle : public RenderActionRecord {
+public:
+	BeadPuzzle() : RenderActionRecord(7) {}
+	virtual ~BeadPuzzle() {}
+
+	void init() override;
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+	void handleInput(NancyInput &input) override;
+
+	bool isViewportRelative() const override { return true; }
+
+protected:
+	Common::String getRecordTypeName() const override { return "BeadPuzzle"; }
+
+	static const int kMaxBeadTypes = 10;
+	static const int kMaxSlots     = 25;
+
+	// File data
+
+	Common::Path _imageName;
+
+	uint16 _numSlots     = 0;
+	uint16 _numBeadTypes = 0;
+
+	Common::Rect _beadSrcRects[kMaxBeadTypes];
+
+	Common::Rect _threadSrc;
+	Common::Rect _threadDest;
+
+	Common::Rect _removeHotspot;
+	Common::Rect _resultHotspot;
+
+	Common::Rect _pickupHotspots[kMaxBeadTypes];
+
+	Common::Rect _slotDestRects[kMaxSlots];
+	Common::Rect _resultDestRects[kMaxSlots];
+
+	uint16 _solution[kMaxSlots] = {}; // expected bead type per slot (file stores type+1; we store the raw value)
+
+	SoundDescription _pickupSound;   // played while a bead is held next to the cursor
+	SoundDescription _placeSound;    // played when a bead is placed onto the thread
+	SoundDescription _removeSound;   // played when a bead is taken off the thread
+
+	int16 _mistakeThreshold = 0;
+
+	SoundDescription _partialSound;
+	SoundDescription _wrongSound;
+
+	FlagDescription _partialFlag;
+	SoundDescription _perfectSound;
+	FlagDescription _perfectFlag;
+
+	SceneChangeDescription _defaultScene;
+	SceneChangeDescription _solvedScene;
+
+	Common::Rect _exitHotspot;
+
+	// Runtime state
+
+	enum SubState {
+		kPlaying = 0,
+		kDroppingBead,
+		kShowingResult,
+		kPerfectWaitSound,
+		kExitToSolved,
+		kExitToDefault
+	};
+
+	SubState _subState = kPlaying;
+
+	Common::Array<int16> _placed;
+	int16 _heldBead = -1;
+	Common::Point _heldDrawPos;
+
+	int16  _dropCurrentSlot = 0;
+	uint32 _dropNextTick    = 0;
+
+	enum ResultKind { kNoResult, kPerfect, kPartial, kWrong };
+	ResultKind _resultKind = kNoResult;
+
+	uint32 _perfectExitTime = 0;
+	bool   _resultSoundPlayed = false;
+
+	Graphics::ManagedSurface _image;
+
+	void redraw();
+	void evaluate();
+	void persistState();
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_BEADPUZZLE_H
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 6c7c7c3a306..f53f465a6e3 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -221,6 +221,10 @@ static void syncInt16Array(Common::Serializer &ser, Common::Array<int16> &arr) {
 	ser.syncArray(arr.data(), num, Common::Serializer::Sint16LE);
 }
 
+void BeadPuzzleData::synchronize(Common::Serializer &ser) {
+	syncInt16Array(ser, placedBeads);
+}
+
 void SortPuzzleData::synchronize(Common::Serializer &ser) {
 	syncInt16Array(ser, currentState);
 	syncInt16Array(ser, solvedState);
@@ -337,6 +341,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
 		return new AssemblyPuzzleData();
 	case QuizPuzzleData::getTag():
 		return new QuizPuzzleData();
+	case BeadPuzzleData::getTag():
+		return new BeadPuzzleData();
 	case SortPuzzleData::getTag():
 		return new SortPuzzleData();
 	case JournalData::getTag():
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index 29ad5d982e2..886edddeb99 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -115,6 +115,17 @@ struct AssemblyPuzzleData : public SimplePuzzleData {
 	static constexpr uint32 getTag() { return MKTAG('A', 'S', 'M', 'B'); }
 };
 
+// Placed bead-type ids on the thread.
+struct BeadPuzzleData : public PuzzleData {
+	BeadPuzzleData() {}
+	virtual ~BeadPuzzleData() {}
+
+	static constexpr uint32 getTag() { return MKTAG('B', 'E', 'A', 'D'); }
+	virtual void synchronize(Common::Serializer &ser);
+
+	Common::Array<int16> placedBeads;
+};
+
 // Cached current/solved tile layouts for a SortPuzzle. Each cell is encoded as
 // 4 consecutive int16s: srcRow, srcCol, value, isEmpty. The first two int16s
 // of each array are the grid rows and cols.


Commit: 4ece07193d0c910c8112c366ce81748c40530843
    https://github.com/scummvm/scummvm/commit/4ece07193d0c910c8112c366ce81748c40530843
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-25T02:04:05+03:00

Commit Message:
NANCY: Implement DotConnectPuzzle for Nancy10

Connect-the-dots puzzle. Player clicks dots to chain lines into a path;
re-clicking the current tip pops the last line. Wins once kNumEdges lines
have been drawn that match the solution either forward or fully reversed.

Changed paths:
  A engines/nancy/action/puzzle/dotconnectpuzzle.cpp
  A engines/nancy/action/puzzle/dotconnectpuzzle.h
    engines/nancy/action/arfactory.cpp


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 415c104c327..06a5bc7b633 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -42,6 +42,7 @@
 #include "engines/nancy/action/puzzle/collisionpuzzle.h"
 #include "engines/nancy/action/puzzle/cubepuzzle.h"
 #include "engines/nancy/action/puzzle/cuttingpuzzle.h"
+#include "engines/nancy/action/puzzle/dotconnectpuzzle.h"
 #include "engines/nancy/action/puzzle/matchpuzzle.h"
 #include "engines/nancy/action/puzzle/hamradiopuzzle.h"
 #include "engines/nancy/action/puzzle/leverpuzzle.h"
@@ -465,7 +466,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 	case 239:
 		return new SortPuzzle();
 	case 241:
-		// return new DotConnectPuzzle();
+		return new DotConnectPuzzle();
 	case 242:
 		// return new MagnetMazePuzzle();
 	case 243:
diff --git a/engines/nancy/action/puzzle/dotconnectpuzzle.cpp b/engines/nancy/action/puzzle/dotconnectpuzzle.cpp
new file mode 100644
index 00000000000..a3194f6db63
--- /dev/null
+++ b/engines/nancy/action/puzzle/dotconnectpuzzle.cpp
@@ -0,0 +1,356 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/dotconnectpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+void DotConnectPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageName);
+
+	for (int i = 0; i < kNumDots; ++i)
+		readRect(stream, _dotSrcRects[i]);
+	for (int i = 0; i < kNumDots; ++i)
+		readRect(stream, _dotHighlightSrcRects[i]);
+
+	_lineColorR = stream.readByte();
+	_lineColorG = stream.readByte();
+	_lineColorB = stream.readByte();
+
+	_lineThickness = stream.readSint16LE();
+
+	for (int i = 0; i < kNumEdges; ++i) {
+		_solution[i].a = stream.readSint32LE();
+		_solution[i].b = stream.readSint32LE();
+	}
+
+	_clickSound.readNormal(stream);
+	_firstLineHint.readNormal(stream);
+	_startHint.readNormal(stream);
+	_tooManyLinesSound.readNormal(stream);
+	_allCoveredSound.readNormal(stream);
+
+	_winScene.readData(stream);
+	stream.skip(2);
+	_winFlag.label = stream.readSint16LE();
+	_winFlag.flag  = stream.readByte();
+	_winDelaySec = stream.readUint16LE();
+	_winSound.readNormal(stream);
+
+	_exitScene.readData(stream);
+	stream.skip(2);
+	_exitFlag.label = stream.readSint16LE();
+	_exitFlag.flag  = stream.readByte();
+
+	readRect(stream, _exitHotspot);
+}
+
+void DotConnectPuzzle::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(_imageName, _image);
+	_image.setTransparentColor(_drawSurface.getTransparentColor());
+
+	for (int i = 0; i < kNumDots; ++i)
+		_isActiveDot[i] = false;
+	for (int i = 0; i < kNumEdges; ++i) {
+		if (_solution[i].a >= 0 && _solution[i].a < kNumDots)
+			_isActiveDot[_solution[i].a] = true;
+		if (_solution[i].b >= 0 && _solution[i].b < kNumDots)
+			_isActiveDot[_solution[i].b] = true;
+	}
+
+	_drawn.clear();
+	_currentTip = -1;
+	_subState = kPlaying;
+	_firstLineHintPlayed = false;
+	_startHintPlayed = false;
+	_allCoveredPlayed = false;
+	_tooManyPlayed = false;
+
+	redraw();
+}
+
+void DotConnectPuzzle::execute() {
+	switch (_state) {
+	case kBegin:
+		init();
+		registerGraphics();
+		_state = kRun;
+		// fall through
+
+	case kRun:
+		switch (_subState) {
+		case kPlaying:
+			break;
+		case kWaitWinDelay:
+			if (g_system->getMillis() >= _winDelayEndTime) {
+				if (_winSound.name != "NO SOUND") {
+					g_nancy->_sound->loadSound(_winSound);
+					g_nancy->_sound->playSound(_winSound);
+					_subState = kWaitWinSound;
+				} else {
+					_subState = kExitToWin;
+				}
+			}
+			break;
+		case kPlayWinSound:
+			_subState = kWaitWinSound;
+			break;
+		case kWaitWinSound:
+			if (!g_nancy->_sound->isSoundPlaying(_winSound)) {
+				g_nancy->_sound->stopSound(_winSound);
+				_subState = kExitToWin;
+			}
+			break;
+		case kExitToWin:
+		case kExitToCancel:
+			_state = kActionTrigger;
+			break;
+		}
+		break;
+
+	case kActionTrigger:
+		g_nancy->_sound->stopSound(_clickSound);
+		g_nancy->_sound->stopSound(_firstLineHint);
+		g_nancy->_sound->stopSound(_startHint);
+		g_nancy->_sound->stopSound(_tooManyLinesSound);
+		g_nancy->_sound->stopSound(_allCoveredSound);
+		g_nancy->_sound->stopSound(_winSound);
+		if (_subState == kExitToWin) {
+			if (_winFlag.label != -1)
+				NancySceneState.setEventFlag(_winFlag);
+			if (_winScene.sceneID != kNoScene)
+				NancySceneState.changeScene(_winScene);
+		} else {
+			if (_exitFlag.label != -1)
+				NancySceneState.setEventFlag(_exitFlag);
+			if (_exitScene.sceneID != kNoScene)
+				NancySceneState.changeScene(_exitScene);
+		}
+		finishExecution();
+		break;
+	}
+}
+
+void DotConnectPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun || _subState != kPlaying)
+		return;
+
+	Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+	Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+	if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
+		g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_subState = kExitToCancel;
+		}
+		return;
+	}
+
+	for (int i = 0; i < kNumDots; ++i) {
+		if (_dotSrcRects[i].isEmpty() || !_dotSrcRects[i].contains(mouseVP))
+			continue;
+		if (dotAlreadyUsed(i))
+			continue;
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		if (input.input & NancyInput::kLeftMouseButtonUp)
+			onDotClicked(i);
+		return;
+	}
+}
+
+// The current tip stays clickable so it can act as an undo handle.
+bool DotConnectPuzzle::dotAlreadyUsed(int dot) const {
+	if (dot == _currentTip)
+		return false;
+	for (uint i = 0; i < _drawn.size(); ++i) {
+		if (_drawn[i].a == dot || _drawn[i].b == dot)
+			return true;
+	}
+	return false;
+}
+
+void DotConnectPuzzle::onDotClicked(int dot) {
+	if (_clickSound.name != "NO SOUND") {
+		g_nancy->_sound->loadSound(_clickSound);
+		g_nancy->_sound->playSound(_clickSound);
+	}
+
+	if (!_startHintPlayed) {
+		_startHintPlayed = true;
+		if (_startHint.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_startHint);
+			g_nancy->_sound->playSound(_startHint);
+		}
+	}
+
+	if (_currentTip == -1) {
+		_currentTip = (int16)dot;
+		redraw();
+		return;
+	}
+
+	if (dot == _currentTip) {
+		if (_drawn.empty()) {
+			_currentTip = -1;
+		} else {
+			const Edge &last = _drawn.back();
+			int16 other = (last.a == _currentTip) ? (int16)last.b : (int16)last.a;
+			_drawn.pop_back();
+			_currentTip = other;
+		}
+		redraw();
+		return;
+	}
+
+	Edge e;
+	e.a = _currentTip;
+	e.b = dot;
+	_drawn.push_back(e);
+	_currentTip = (int16)dot;
+
+	if (_drawn.size() == 1 && !_firstLineHintPlayed) {
+		const Edge &d = _drawn[0];
+		bool matchesFirst = (d.a == _solution[0].a && d.b == _solution[0].b);
+		bool matchesLastReversed = (d.a == _solution[kNumEdges - 1].b &&
+		                            d.b == _solution[kNumEdges - 1].a);
+		if (matchesFirst || matchesLastReversed) {
+			_firstLineHintPlayed = true;
+			if (_firstLineHint.name != "NO SOUND") {
+				g_nancy->_sound->loadSound(_firstLineHint);
+				g_nancy->_sound->playSound(_firstLineHint);
+			}
+		}
+	}
+
+	if (_drawn.size() == 16 && !_tooManyPlayed) {
+		_tooManyPlayed = true;
+		if (_tooManyLinesSound.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_tooManyLinesSound);
+			g_nancy->_sound->playSound(_tooManyLinesSound);
+		}
+	}
+
+	checkWin();
+	redraw();
+}
+
+void DotConnectPuzzle::checkWin() {
+	if ((int)_drawn.size() != kNumEdges)
+		return;
+
+	bool allMatch = true;
+	for (int i = 0; i < kNumEdges; ++i) {
+		const Edge &d = _drawn[i];
+		const Edge &fwd = _solution[i];
+		const Edge &rev = _solution[kNumEdges - 1 - i];
+		bool matchFwd = (d.a == fwd.a && d.b == fwd.b);
+		bool matchRev = (d.a == rev.b && d.b == rev.a);
+		if (!matchFwd && !matchRev) {
+			allMatch = false;
+			break;
+		}
+	}
+
+	if (allMatch) {
+		_subState = kWaitWinDelay;
+		_winDelayEndTime = g_system->getMillis() + (uint32)_winDelaySec * 1000;
+		return;
+	}
+
+	if (!_allCoveredPlayed) {
+		bool covered[kNumDots] = {};
+		for (uint i = 0; i < _drawn.size(); ++i) {
+			if (_drawn[i].a >= 0 && _drawn[i].a < kNumDots) covered[_drawn[i].a] = true;
+			if (_drawn[i].b >= 0 && _drawn[i].b < kNumDots) covered[_drawn[i].b] = true;
+		}
+		for (int i = 0; i < kNumDots; ++i) {
+			if (_isActiveDot[i] && !covered[i])
+				return;
+		}
+		_allCoveredPlayed = true;
+		if (_allCoveredSound.name != "NO SOUND") {
+			g_nancy->_sound->loadSound(_allCoveredSound);
+			g_nancy->_sound->playSound(_allCoveredSound);
+		}
+	}
+}
+
+void DotConnectPuzzle::redraw() {
+	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	// The symbols themselves live on the underlying scene background; the
+	// _dotSrcRects values are screen positions (not coordinates inside the
+	// overlay image), so the puzzle never blits them from the overlay.
+
+	uint32 color = _drawSurface.format.RGBToColor(_lineColorR, _lineColorG, _lineColorB);
+	int thick = MAX<int>(1, _lineThickness);
+	for (uint i = 0; i < _drawn.size(); ++i) {
+		const Edge &e = _drawn[i];
+		if (e.a < 0 || e.a >= kNumDots || e.b < 0 || e.b >= kNumDots)
+			continue;
+		Common::Point ca((_dotSrcRects[e.a].left + _dotSrcRects[e.a].right) / 2,
+		                 (_dotSrcRects[e.a].top  + _dotSrcRects[e.a].bottom) / 2);
+		Common::Point cb((_dotSrcRects[e.b].left + _dotSrcRects[e.b].right) / 2,
+		                 (_dotSrcRects[e.b].top  + _dotSrcRects[e.b].bottom) / 2);
+		for (int dy = -thick; dy <= thick; ++dy) {
+			for (int dx = -thick; dx <= thick; ++dx) {
+				_drawSurface.drawLine(ca.x + dx, ca.y + dy, cb.x + dx, cb.y + dy, color);
+			}
+		}
+	}
+
+	// Punch line pixels back out at every dot's screen rect so the scene's
+	// pre-rendered symbols remain visible through any line passing over them.
+	for (int i = 0; i < kNumDots; ++i) {
+		if (!_dotSrcRects[i].isEmpty())
+			_drawSurface.fillRect(_dotSrcRects[i], _drawSurface.getTransparentColor());
+	}
+
+	// Overlay the active tip with its highlight sprite from the overlay image.
+	if (_currentTip >= 0 && _currentTip < kNumDots &&
+	    !_dotHighlightSrcRects[_currentTip].isEmpty()) {
+		const Common::Rect &dst = _dotSrcRects[_currentTip];
+		_drawSurface.blitFrom(_image, _dotHighlightSrcRects[_currentTip],
+			Common::Point(dst.left, dst.top));
+	}
+
+	_needsRedraw = true;
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/dotconnectpuzzle.h b/engines/nancy/action/puzzle/dotconnectpuzzle.h
new file mode 100644
index 00000000000..e1d538e4aff
--- /dev/null
+++ b/engines/nancy/action/puzzle/dotconnectpuzzle.h
@@ -0,0 +1,124 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_DOTCONNECTPUZZLE_H
+#define NANCY_ACTION_DOTCONNECTPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+
+namespace Nancy {
+namespace Action {
+
+// Connect-the-dots puzzle. Player clicks dots to chain lines into a path;
+// re-clicking the current tip pops the last line. Wins once kNumEdges lines
+// have been drawn that match the solution either forward or fully reversed.
+// Called from scene 6243 in Nancy10.
+class DotConnectPuzzle : public RenderActionRecord {
+public:
+	DotConnectPuzzle() : RenderActionRecord(7) {}
+	virtual ~DotConnectPuzzle() {}
+
+	void init() override;
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+	void handleInput(NancyInput &input) override;
+
+	bool isViewportRelative() const override { return true; }
+
+protected:
+	Common::String getRecordTypeName() const override { return "DotConnectPuzzle"; }
+
+	static const int kNumDots  = 17;
+	static const int kNumEdges = 11;
+
+	struct Edge {
+		int32 a = 0;
+		int32 b = 0;
+	};
+
+	// File data
+
+	Common::Path _imageName;
+
+	Common::Rect _dotSrcRects[kNumDots];
+	Common::Rect _dotHighlightSrcRects[kNumDots];
+
+	byte _lineColorR  = 0;
+	byte _lineColorG  = 0;
+	byte _lineColorB  = 0;
+	int16 _lineThickness = 1;
+
+	Edge _solution[kNumEdges];
+
+	SoundDescription _clickSound;
+	SoundDescription _firstLineHint;
+	SoundDescription _startHint;
+	SoundDescription _tooManyLinesSound;
+	SoundDescription _allCoveredSound;
+
+	SceneChangeDescription _winScene;
+	FlagDescription _winFlag;
+	uint16 _winDelaySec = 0;
+	SoundDescription _winSound;
+
+	SceneChangeDescription _exitScene;
+	FlagDescription _exitFlag;
+
+	Common::Rect _exitHotspot;
+
+	// Runtime state
+
+	enum SubState {
+		kPlaying = 0,
+		kWaitWinDelay,
+		kPlayWinSound,
+		kWaitWinSound,
+		kExitToWin,
+		kExitToCancel
+	};
+
+	SubState _subState = kPlaying;
+
+	Common::Array<Edge> _drawn;
+	int16 _currentTip = -1;
+
+	bool _isActiveDot[kNumDots] = {};
+	bool _firstLineHintPlayed = false;
+	bool _startHintPlayed     = false;
+	bool _allCoveredPlayed    = false;
+	bool _tooManyPlayed       = false;
+
+	uint32 _winDelayEndTime = 0;
+
+	Graphics::ManagedSurface _image;
+
+	void redraw();
+	bool dotAlreadyUsed(int dot) const;
+	void onDotClicked(int dot);
+	void checkWin();
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_DOTCONNECTPUZZLE_H




More information about the Scummvm-git-logs mailing list