[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