[Scummvm-git-logs] scummvm master -> 8f7c638085cdcc57d6c72207f332fc4d82bf26f0
bluegr
noreply at scummvm.org
Fri Sep 25 01:39:17 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
1171e20a18 NANCY: NANCY15: Clean up comments in MagicBoxPuzzle
b5f8be6cd5 NANCY: NANCY15: Implement sorting / weighing puzzle (WeightSortPuzzle)
6032e4d3ec NANCY: NANCY15: Fix solving QuizPuzzle when there is no scene change
2b8ba9b161 NANCY: NANCY15: Don't show empty in-between frames in SafeDialPuzzle
ec4f1cd641 NANCY: NANCY15: Correctly set flag when clicking on a RolloverOverlay
8f7c638085 NANCY14-15: Implement PaintPuzzle functionality
Commit: 1171e20a189a736d719c4d38fc45f664f9f24444
https://github.com/scummvm/scummvm/commit/1171e20a189a736d719c4d38fc45f664f9f24444
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:01+03:00
Commit Message:
NANCY: NANCY15: Clean up comments in MagicBoxPuzzle
Changed paths:
engines/nancy/action/puzzle/magicboxpuzzle.cpp
engines/nancy/action/puzzle/magicboxpuzzle.h
diff --git a/engines/nancy/action/puzzle/magicboxpuzzle.cpp b/engines/nancy/action/puzzle/magicboxpuzzle.cpp
index cef4f0fcd3e..70a1e7fc252 100644
--- a/engines/nancy/action/puzzle/magicboxpuzzle.cpp
+++ b/engines/nancy/action/puzzle/magicboxpuzzle.cpp
@@ -44,28 +44,25 @@ static void readRectList(Common::SeekableReadStream &stream, Common::Array<Commo
}
void MagicBoxPuzzle::readData(Common::SeekableReadStream &stream) {
- _gridFlowType = stream.readUint16LE(); // 0x00
+ _gridFlowType = stream.readUint16LE();
- _numCols = stream.readSint32LE(); // 0x02
- _numRows = stream.readSint32LE(); // 0x06
- _subgridCols = stream.readSint32LE(); // 0x0a
- _subgridRows = stream.readSint32LE(); // 0x0e
+ _numCols = stream.readSint32LE();
+ _numRows = stream.readSint32LE();
+ _subgridCols = stream.readSint32LE();
+ _subgridRows = stream.readSint32LE();
- _allCellsBlank = stream.readByte(); // 0x12
- _unknown13 = stream.readByte(); // 0x13
- _allowTakeBack = stream.readByte(); // 0x14
+ _allCellsBlank = stream.readByte();
+ _unknown13 = stream.readByte();
+ _allowTakeBack = stream.readByte();
- // The board, row by row. Each cell is a single int; 0 leaves the cell blank.
uint numCells = (uint)MAX<int32>(0, _numRows) * (uint)MAX<int32>(0, _numCols);
_cellValues.resize(numCells);
for (uint i = 0; i < numCells; ++i) {
_cellValues[i] = stream.readSint32LE();
}
- // One destination rect per blank cell, in the order the cells were read.
readRectList(stream, _slotDests);
- // The loose tiles waiting in the tray.
uint16 numPieces = stream.readUint16LE();
_pieceValues.resize(numPieces);
for (uint16 i = 0; i < numPieces; ++i) {
@@ -75,7 +72,6 @@ void MagicBoxPuzzle::readData(Common::SeekableReadStream &stream) {
readFilename(stream, _tileImageName);
readRectList(stream, _tileSrcs);
- // The tray spots. There is at least one per loose tile.
readRectList(stream, _trayDests);
readFilename(stream, _indicatorImageName);
@@ -93,7 +89,7 @@ void MagicBoxPuzzle::readData(Common::SeekableReadStream &stream) {
_hoverCursorType = stream.readUint16LE();
_dragCursorType = stream.readUint16LE();
- stream.skip(8); // speed and step of the original's tile-slide animation
+ stream.skip(8); // tile-slide animation speed and step
for (uint i = 0; i < kNumSounds; ++i) {
_sounds[i].readData(stream);
@@ -133,7 +129,7 @@ void MagicBoxPuzzle::init() {
_indicatorImage.setTransparentColor(_drawSurface.getTransparentColor());
}
- // The slots are the blank cells, taken in the same order as their dest rects.
+ // Slots are the blank cells, in the same order as their dest rects.
_slotCells.clear();
for (uint i = 0; i < _cellValues.size(); ++i) {
if (_allCellsBlank || _cellValues[i] == 0) {
@@ -271,7 +267,7 @@ SoundDescription MagicBoxPuzzle::playSoundBlock(const RandomSoundBlock &block) {
void MagicBoxPuzzle::redraw() {
_drawSurface.clear(g_nancy->_graphics->getTransColor());
- // The fixed cells are part of the scene background; only the loose tiles are drawn.
+ // The fixed cells are part of the scene background.
for (uint i = 0; i < _piecePlacement.size(); ++i) {
if (i == (uint)_carriedPiece) {
continue;
@@ -290,7 +286,7 @@ void MagicBoxPuzzle::redraw() {
_drawSurface.blitFrom(_tileImage, src, Common::Point(dest.left, dest.top));
}
- // A marker lights up next to every row and column that already adds up.
+ // A marker lights up beside every row and column that already adds up.
if (!_indicatorSrcs.empty() && !_indicatorSrcs[0].isEmpty()) {
const Graphics::ManagedSurface &image = _indicatorImage.empty() ? _tileImage : _indicatorImage;
for (uint i = 0; i < _indicatorDests.size(); ++i) {
@@ -346,7 +342,7 @@ void MagicBoxPuzzle::handleInput(NancyInput &input) {
const bool click = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
- // -- Carrying a tile: drop it into a free slot, or put it back in its tray spot. --
+ // Carrying a tile: drop it into a free slot, or put it back in the tray.
if (_carriedPiece >= 0) {
setDataCursor(_dragCursorType);
_carriedObject.handleInput(input);
@@ -374,7 +370,7 @@ void MagicBoxPuzzle::handleInput(NancyInput &input) {
return;
}
- // -- Not carrying: pick a tile up from the tray or off the board. --
+ // Not carrying: pick a tile up from the tray or off the board.
int piece = trayPieceAtCursor(input.mousePos);
if (piece == -1 && _allowTakeBack) {
int slot = slotAtCursor(input.mousePos);
diff --git a/engines/nancy/action/puzzle/magicboxpuzzle.h b/engines/nancy/action/puzzle/magicboxpuzzle.h
index fd0dce7ab45..a1c775234b3 100644
--- a/engines/nancy/action/puzzle/magicboxpuzzle.h
+++ b/engines/nancy/action/puzzle/magicboxpuzzle.h
@@ -29,15 +29,8 @@
namespace Nancy {
namespace Action {
-// Number-square puzzle, new in Nancy15 (AR 183). A grid of numbered cells has a few of
-// its cells left blank; the matching number of loose tiles waits in a tray beside the
-// board. A tile is picked up by clicking it, carried on the cursor and dropped into a
-// blank cell (or back into the tray). The board is solved once every row and every
-// column adds up to the target printed beside it.
-//
-// The record is loaded through the shared GridFlow factory, whose leading type word must
-// be zero for this puzzle. The grid is also divided into equal subgrids, which is what
-// spaces the cell rects out into blocks on screen.
+// Number-square puzzle, new in Nancy15 (AR 183). Loose numbered tiles are carried from a
+// tray into the blank cells of a grid, until every row and column adds up to its target.
class MagicBoxPuzzle : public RenderActionRecord {
public:
MagicBoxPuzzle() : RenderActionRecord(7) {}
@@ -54,7 +47,6 @@ public:
protected:
Common::String getRecordTypeName() const override { return "MagicBoxPuzzle"; }
- // The board slots are the blank cells, in row-major order; one dest rect per slot.
int slotAtCursor(const Common::Point &mousePos) const;
int trayPieceAtCursor(const Common::Point &mousePos) const;
@@ -62,7 +54,7 @@ protected:
int32 colSum(int col) const;
bool isSolved() const;
- // Lifts the given piece onto the cursor, or sets the carried one down for an index of -1.
+ // An index of -1 sets the carried piece back down.
void carryPiece(int piece, NancyInput &input);
Common::Rect tileSrc(int32 value) const;
@@ -73,21 +65,21 @@ protected:
static const uint kNumSounds = 10;
// -- File data --
- uint16 _gridFlowType = 0; // 0x00, validated as 0 by the factory
- int32 _numCols = 0; // 0x02
- int32 _numRows = 0; // 0x06
- int32 _subgridCols = 0; // 0x0a
- int32 _subgridRows = 0; // 0x0e
- byte _allCellsBlank = 0; // 0x12, forces every cell to be a droppable slot
- byte _unknown13 = 0; // 0x13
- byte _allowTakeBack = 0; // 0x14, lets a placed tile be picked up again
+ uint16 _gridFlowType = 0;
+ int32 _numCols = 0;
+ int32 _numRows = 0;
+ int32 _subgridCols = 0; // the grid is drawn in blocks of this size
+ int32 _subgridRows = 0;
+ byte _allCellsBlank = 0; // every cell becomes a droppable slot
+ byte _unknown13 = 0;
+ byte _allowTakeBack = 0; // a placed tile can be picked up again
Common::Array<int32> _cellValues; // row-major, 0 marks a blank cell
Common::Array<Common::Rect> _slotDests; // one per blank cell, in row-major order
- Common::Array<int32> _pieceValues; // the loose tiles
+ Common::Array<int32> _pieceValues;
Common::Path _tileImageName;
Common::Array<Common::Rect> _tileSrcs; // indexed by tile value - 1
- Common::Array<Common::Rect> _trayDests; // resting place of each loose tile
+ Common::Array<Common::Rect> _trayDests;
Common::Path _indicatorImageName;
Common::Array<Common::Rect> _indicatorSrcs;
Common::Array<Common::Rect> _indicatorDests; // _numRows row markers, then _numCols column ones
@@ -101,8 +93,6 @@ protected:
SceneChangeDescription _solveScene;
FlagDescription _solveFlag;
RandomSoundBlock _solveSound;
- // The second outcome of the record. Neither of the shipped Nancy15 boards can be
- // failed, so nothing reaches it.
SceneChangeDescription _failScene;
FlagDescription _failFlag;
RandomSoundBlock _failSound;
@@ -115,7 +105,7 @@ protected:
// -- Runtime state --
Common::Array<int> _slotCells; // board cell index of each slot
Common::Array<int> _slotContents; // piece in each slot, or -1
- Common::Array<int> _piecePlacement; // slot each piece sits in, or -1 while in its tray spot
+ Common::Array<int> _piecePlacement; // slot each piece sits in, or -1 while in the tray
int _carriedPiece = -1;
Misc::MouseFollowObject _carriedObject;
bool _solved = false;
Commit: b5f8be6cd5d129b509da67a551b3b1cde1f2d70d
https://github.com/scummvm/scummvm/commit/b5f8be6cd5d129b509da67a551b3b1cde1f2d70d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:02+03:00
Commit Message:
NANCY: NANCY15: Implement sorting / weighing puzzle (WeightSortPuzzle)
The microscope fras sorting and weighing puzzle is now working
Changed paths:
A engines/nancy/action/puzzle/weightsortpuzzle.cpp
A engines/nancy/action/puzzle/weightsortpuzzle.h
engines/nancy/action/arfactory.cpp
engines/nancy/module.mk
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 06afed6e8d9..d0d5f2249cf 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -100,7 +100,7 @@
#include "engines/nancy/action/puzzle/turningpuzzle.h"
#include "engines/nancy/action/puzzle/twodialpuzzle.h"
#include "engines/nancy/action/puzzle/typingquizpuzzle.h"
-//#include "engines/nancy/action/puzzle/weightsortpuzzle.h"
+#include "engines/nancy/action/puzzle/weightsortpuzzle.h"
#include "engines/nancy/action/puzzle/whalesurvivorpuzzle.h"
#include "engines/nancy/action/puzzle/wordfindpuzzle.h"
@@ -542,8 +542,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
//return new EscapeGridPuzzle();
return nullptr; // TODO
case 185:
- // return new WeightSortPuzzle();
- return nullptr; // TODO
+ return new WeightSortPuzzle();
case 200:
return new SoundEqualizerPuzzle();
case 201:
diff --git a/engines/nancy/action/puzzle/weightsortpuzzle.cpp b/engines/nancy/action/puzzle/weightsortpuzzle.cpp
new file mode 100644
index 00000000000..635793e42ff
--- /dev/null
+++ b/engines/nancy/action/puzzle/weightsortpuzzle.cpp
@@ -0,0 +1,546 @@
+/* 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/cursor.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/weightsortpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+static void readRectList16(Common::SeekableReadStream &stream, Common::Array<Common::Rect> &dst) {
+ uint16 num = stream.readUint16LE();
+ dst.resize(num);
+ for (uint16 i = 0; i < num; ++i) {
+ readRect(stream, dst[i]);
+ }
+}
+
+static void readIntList16(Common::SeekableReadStream &stream, Common::Array<int32> &dst) {
+ uint16 num = stream.readUint16LE();
+ dst.resize(num);
+ for (uint16 i = 0; i < num; ++i) {
+ dst[i] = stream.readSint32LE();
+ }
+}
+
+void WeightSortPuzzle::readContainer(Common::SeekableReadStream &stream, Container &dst) {
+ dst.showTotalWeight = stream.readByte();
+ dst.countsTowardOutcome = (stream.readSint16LE() != 0);
+ dst.pickUpMode = (byte)stream.readSint16LE();
+ dst.dropInPlace = (stream.readSint16LE() != 0);
+
+ for (uint i = 0; i < kNumOutcomes; ++i) {
+ dst.ranges[i].maxExclusive = stream.readSint32LE();
+ dst.ranges[i].minExclusive = stream.readSint32LE();
+ }
+
+ readRect(stream, dst.area);
+ readRect(stream, dst.shakeHotspot);
+
+ readFilename(stream, dst.readoutImageName);
+ readRectList16(stream, dst.digitSrcs);
+ readRectList16(stream, dst.digitDests);
+
+ // The accepted kinds arrive as a list of bit sets, folded into one mask.
+ dst.acceptedKinds = 0;
+ int16 numKinds = stream.readSint16LE();
+ for (int16 i = 0; i < numKinds; ++i) {
+ dst.acceptedKinds |= stream.readSint32LE();
+ }
+
+ dst.pickUpSound.readData(stream);
+ dst.putDownSound.readData(stream);
+
+ readIntList16(stream, dst.initialContents);
+ readIntList16(stream, dst.requiredContents);
+}
+
+void WeightSortPuzzle::readData(Common::SeekableReadStream &stream) {
+ _firstNeedsAllContainers = stream.readByte();
+ _secondNeedsAllContainers = stream.readByte();
+ _firstNeedsEmptyCursor = stream.readByte();
+ _returnOnMiss = stream.readByte();
+
+ _hoverCursorType = stream.readUint16LE();
+ _dragCursorType = stream.readUint16LE();
+ _rejectCursorType = stream.readUint16LE();
+
+ stream.skip(8); // object-slide animation speed and step
+
+ int16 numTypes = stream.readSint16LE();
+ _objectTypes.resize(numTypes > 0 ? numTypes : 0);
+ for (uint i = 0; i < _objectTypes.size(); ++i) {
+ ObjectType &type = _objectTypes[i];
+ type.kind = stream.readSint32LE();
+ type.weight = stream.readSint32LE();
+ readFilename(stream, type.imageName);
+
+ int16 numSrcs = stream.readSint16LE();
+ type.srcs.resize(numSrcs > 0 ? numSrcs : 0);
+ for (uint j = 0; j < type.srcs.size(); ++j) {
+ readRect(stream, type.srcs[j]);
+ }
+ }
+
+ int16 numContainers = stream.readSint16LE();
+ _containers.resize(numContainers > 0 ? numContainers : 0);
+ for (uint i = 0; i < _containers.size(); ++i) {
+ readContainer(stream, _containers[i]);
+ }
+
+ _rejectSound.readData(stream);
+
+ for (uint i = 0; i < kNumOutcomes; ++i) {
+ Outcome &outcome = _outcomes[i];
+ outcome.scene.sceneID = stream.readUint16LE();
+ outcome.scene.frameID = stream.readUint16LE();
+ outcome.scene.continueSceneSound = kContinueSceneSound;
+ outcome.flag.label = stream.readSint16LE();
+ outcome.flag.flag = stream.readByte();
+ outcome.sound.readData(stream);
+ }
+
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
+ _exitScene.continueSceneSound = kContinueSceneSound;
+}
+
+// Drops the object at a random spot fully inside its container.
+void WeightSortPuzzle::scatter(Object &object) {
+ const Common::Rect &area = _containers[object.container].area;
+ int w = object.src.width();
+ int h = object.src.height();
+
+ int spanX = MAX(0, area.width() - w);
+ int spanY = MAX(0, area.height() - h);
+ int x = area.left + (spanX ? (int)g_nancy->_randomSource->getRandomNumber(spanX) : 0);
+ int y = area.top + (spanY ? (int)g_nancy->_randomSource->getRandomNumber(spanY) : 0);
+
+ object.dest = Common::Rect((int16)x, (int16)y, (int16)(x + w), (int16)(y + h));
+}
+
+void WeightSortPuzzle::scatterContainer(uint container) {
+ for (uint i = 0; i < _objects.size(); ++i) {
+ if (_objects[i].container == (int)container) {
+ scatter(_objects[i]);
+ }
+ }
+}
+
+void WeightSortPuzzle::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);
+
+ _typeImages.resize(_objectTypes.size());
+ for (uint i = 0; i < _objectTypes.size(); ++i) {
+ if (!_objectTypes[i].imageName.empty()) {
+ g_nancy->_resource->loadImage(_objectTypes[i].imageName, _typeImages[i]);
+ _typeImages[i].setTransparentColor(_drawSurface.getTransparentColor());
+ }
+ }
+
+ _readoutImages.resize(_containers.size());
+ for (uint i = 0; i < _containers.size(); ++i) {
+ if (!_containers[i].readoutImageName.empty()) {
+ g_nancy->_resource->loadImage(_containers[i].readoutImageName, _readoutImages[i]);
+ _readoutImages[i].setTransparentColor(_drawSurface.getTransparentColor());
+ }
+ }
+
+ _objects.clear();
+ for (uint i = 0; i < _containers.size(); ++i) {
+ const Common::Array<int32> &contents = _containers[i].initialContents;
+ for (uint j = 0; j < contents.size(); ++j) {
+ int32 typeID = contents[j];
+ if (typeID < 0 || (uint)typeID >= _objectTypes.size()) {
+ continue;
+ }
+
+ const ObjectType &type = _objectTypes[typeID];
+ if (type.srcs.empty()) {
+ continue;
+ }
+
+ Object object;
+ object.type = typeID;
+ object.container = i;
+ object.src = type.srcs[g_nancy->_randomSource->getRandomNumber(type.srcs.size() - 1)];
+ scatter(object);
+ _objects.push_back(object);
+ }
+ }
+
+ _carriedObjectID = -1;
+ _carriedFrom = -1;
+ _outcome = -1;
+ _exitRequested = false;
+
+ NancySceneState.setNoHeldItem();
+
+ redraw();
+ registerGraphics();
+ _carriedObject.registerGraphics();
+}
+
+int WeightSortPuzzle::objectAtCursor(const Common::Point &mousePos) const {
+ // Later objects are drawn on top of earlier ones, so they are picked up first.
+ for (int i = (int)_objects.size() - 1; i >= 0; --i) {
+ if (_objects[i].container == -1) {
+ continue;
+ }
+ if (NancySceneState.getViewport().convertViewportToScreen(_objects[i].dest).contains(mousePos)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+int WeightSortPuzzle::containerAtCursor(const Common::Point &mousePos) const {
+ for (uint i = 0; i < _containers.size(); ++i) {
+ if (NancySceneState.getViewport().convertViewportToScreen(_containers[i].area).contains(mousePos)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+int WeightSortPuzzle::shakeHotspotAtCursor(const Common::Point &mousePos) const {
+ for (uint i = 0; i < _containers.size(); ++i) {
+ if (_containers[i].shakeHotspot.isEmpty()) {
+ continue;
+ }
+ if (NancySceneState.getViewport().convertViewportToScreen(_containers[i].shakeHotspot).contains(mousePos)) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+int WeightSortPuzzle::countListed(const Common::Array<int32> &list, int32 type) {
+ int count = 0;
+ for (uint i = 0; i < list.size(); ++i) {
+ if (list[i] == type) {
+ ++count;
+ }
+ }
+ return count;
+}
+
+int WeightSortPuzzle::countIn(uint container, int32 type) const {
+ int count = 0;
+ for (uint i = 0; i < _objects.size(); ++i) {
+ if (_objects[i].container == (int)container && (int32)_objects[i].type == type) {
+ ++count;
+ }
+ }
+ return count;
+}
+
+bool WeightSortPuzzle::canPickUp(const Object &object) const {
+ const Container &container = _containers[object.container];
+
+ switch (container.pickUpMode) {
+ case kPickUpOriginal:
+ return !object.placedByHand;
+ case kPickUpSpare:
+ return countIn(object.container, object.type) >
+ countListed(container.requiredContents, object.type);
+ default:
+ return true;
+ }
+}
+
+int32 WeightSortPuzzle::readoutValue(uint container) const {
+ int32 value = 0;
+ for (uint i = 0; i < _objects.size(); ++i) {
+ if (_objects[i].container != (int)container) {
+ continue;
+ }
+ value += _containers[container].showTotalWeight ? _objectTypes[_objects[i].type].weight : 1;
+ }
+ return value;
+}
+
+bool WeightSortPuzzle::containerPasses(uint container, uint outcome) const {
+ const Container &data = _containers[container];
+ int32 total = readoutValue(container);
+
+ if (total >= data.ranges[outcome].maxExclusive || total <= data.ranges[outcome].minExclusive) {
+ return false;
+ }
+
+ // Only the first outcome asks for the container's required contents.
+ if (outcome == 0) {
+ for (uint i = 0; i < data.requiredContents.size(); ++i) {
+ int32 type = data.requiredContents[i];
+ if (countIn(container, type) < countListed(data.requiredContents, type)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+int WeightSortPuzzle::reachedOutcome() const {
+ uint numCounted = 0;
+ uint numPassing[kNumOutcomes] = {};
+
+ for (uint i = 0; i < _containers.size(); ++i) {
+ if (!_containers[i].countsTowardOutcome) {
+ continue;
+ }
+
+ ++numCounted;
+ for (uint j = 0; j < kNumOutcomes; ++j) {
+ if (containerPasses(i, j)) {
+ ++numPassing[j];
+ }
+ }
+ }
+
+ if (numCounted == 0) {
+ return -1;
+ }
+
+ // The second outcome is tested first and takes the board if it passes.
+ if (_secondNeedsAllContainers ? (numPassing[1] == numCounted) : (numPassing[1] > 0)) {
+ return 1;
+ }
+
+ bool cursorOK = !_firstNeedsEmptyCursor || _carriedObjectID == -1;
+ if (cursorOK && (_firstNeedsAllContainers ? (numPassing[0] == numCounted) : (numPassing[0] > 0))) {
+ return 0;
+ }
+
+ return -1;
+}
+
+void WeightSortPuzzle::carryObject(int object, NancyInput &input) {
+ _carriedObjectID = object;
+
+ if (object >= 0 && !_typeImages[_objects[object].type].empty()) {
+ _carriedObject._drawSurface.create(_typeImages[_objects[object].type], _objects[object].src);
+ _carriedObject.setTransparent(true);
+ _carriedObject.setVisible(true);
+ _carriedObject.pickUp();
+ _carriedObject.handleInput(input);
+ } else {
+ _carriedObject.setVisible(false);
+ _carriedObject.putDown();
+ }
+}
+
+void WeightSortPuzzle::setDataCursor(uint16 cursorType, bool hotspotVariant) const {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)cursorType, true, hotspotVariant);
+}
+
+SoundDescription WeightSortPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+ SoundDescription desc;
+ if (block.names.empty()) {
+ return desc;
+ }
+
+ uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+ const Common::String &name = block.names[idx];
+ if (name.empty() || name == "NO SOUND") {
+ return desc;
+ }
+
+ desc.name = name;
+ desc.channelID = block.channel;
+ desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+ desc.volume = block.volume;
+
+ g_nancy->_sound->loadSound(desc);
+ g_nancy->_sound->playSound(desc);
+ return desc;
+}
+
+void WeightSortPuzzle::redraw() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ for (uint i = 0; i < _objects.size(); ++i) {
+ const Object &object = _objects[i];
+ if (object.container == -1 || _typeImages[object.type].empty()) {
+ continue;
+ }
+ _drawSurface.blitFrom(_typeImages[object.type], object.src,
+ Common::Point(object.dest.left, object.dest.top));
+ }
+
+ // Every readout slot holds a digit, so shorter numbers come out zero-padded.
+ for (uint i = 0; i < _containers.size(); ++i) {
+ const Container &container = _containers[i];
+ if (container.digitDests.empty() || container.digitSrcs.size() < 10 || _readoutImages[i].empty()) {
+ continue;
+ }
+
+ int32 value = readoutValue(i);
+ for (int slot = (int)container.digitDests.size() - 1; slot >= 0; --slot) {
+ _drawSurface.blitFrom(_readoutImages[i], container.digitSrcs[value % 10],
+ Common::Point(container.digitDests[slot].left, container.digitDests[slot].top));
+ value /= 10;
+ }
+ }
+
+ _needsRedraw = true;
+}
+
+void WeightSortPuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ _state = kRun;
+ // fall through
+ case kRun:
+ if (_exitRequested) {
+ _state = kActionTrigger;
+ break;
+ }
+
+ if (_outcome != -1) {
+ if (_endSound.name.empty() || !g_nancy->_sound->isSoundPlaying(_endSound)) {
+ _state = kActionTrigger;
+ }
+ }
+
+ break;
+ case kActionTrigger:
+ if (_exitRequested) {
+ NancySceneState.setEventFlag(_exitFlag);
+ NancySceneState.changeScene(_exitScene);
+ } else {
+ NancySceneState.setEventFlag(_outcomes[_outcome].flag);
+ NancySceneState.changeScene(_outcomes[_outcome].scene);
+ }
+
+ finishExecution();
+ break;
+ }
+}
+
+void WeightSortPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _outcome != -1 || _exitRequested) {
+ return;
+ }
+
+ const bool click = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
+
+ // Carrying an object: drop it into a container that takes its kind.
+ if (_carriedObjectID >= 0) {
+ Object &carried = _objects[_carriedObjectID];
+ int target = containerAtCursor(input.mousePos);
+ bool accepted = (target >= 0) &&
+ (_containers[target].acceptedKinds & _objectTypes[carried.type].kind) != 0;
+
+ setDataCursor((target >= 0 && !accepted) ? _rejectCursorType : _dragCursorType);
+ _carriedObject.handleInput(input);
+
+ if (click) {
+ if (accepted) {
+ carried.container = target;
+ carried.placedByHand = true;
+ carried.dest.moveTo(input.mousePos.x - carried.dest.width() / 2,
+ input.mousePos.y - carried.dest.height() / 2);
+ carried.dest.clip(_containers[target].area);
+ carried.dest.setWidth(carried.src.width());
+ carried.dest.setHeight(carried.src.height());
+
+ playSoundBlock(_containers[target].putDownSound);
+ if (!_containers[target].dropInPlace) {
+ scatterContainer(target);
+ }
+ } else {
+ if (target >= 0) {
+ playSoundBlock(_rejectSound);
+ }
+
+ // Nothing took it, so it goes back where it came from.
+ carried.container = _carriedFrom;
+ if (target >= 0 || _returnOnMiss) {
+ scatter(carried);
+ }
+ }
+
+ carryObject(-1, input);
+ redraw();
+
+ _outcome = reachedOutcome();
+ if (_outcome != -1) {
+ _endSound = playSoundBlock(_outcomes[_outcome].sound);
+ }
+ }
+
+ input.eatMouseInput();
+ return;
+ }
+
+ // Not carrying: pick an object up, or shake a container to re-scatter it.
+ int object = objectAtCursor(input.mousePos);
+ if (object >= 0 && canPickUp(_objects[object])) {
+ setDataCursor(_hoverCursorType);
+ if (click) {
+ _carriedFrom = _objects[object].container;
+ playSoundBlock(_containers[_carriedFrom].pickUpSound);
+ _objects[object].container = -1;
+ carryObject(object, input);
+ redraw();
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ int shaken = shakeHotspotAtCursor(input.mousePos);
+ if (shaken >= 0) {
+ setDataCursor(_hoverCursorType);
+ if (click) {
+ scatterContainer(shaken);
+ redraw();
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ if (!_exitHotspot.isEmpty() &&
+ NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
+ setDataCursor(_exitCursorType, false);
+ if (click) {
+ _exitRequested = true;
+ }
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/weightsortpuzzle.h b/engines/nancy/action/puzzle/weightsortpuzzle.h
new file mode 100644
index 00000000000..bf3da58e078
--- /dev/null
+++ b/engines/nancy/action/puzzle/weightsortpuzzle.h
@@ -0,0 +1,166 @@
+/* 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_WEIGHTSORTPUZZLE_H
+#define NANCY_ACTION_WEIGHTSORTPUZZLE_H
+
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
+
+namespace Nancy {
+namespace Action {
+
+// Sorting puzzle, new in Nancy15 (AR 185, the frass sort). Loose objects are carried from
+// one container to another; each has a kind and a weight, and a container only takes the
+// kinds it declares and can show a running readout of what it holds.
+//
+// Both an object's sprite and its resting place are picked at random, so the same pile
+// never comes out looking the same twice.
+//
+// A board whose containers do not count towards either outcome never ends on its own and
+// is left through the give-up hotspot, which is what the Nancy15 sort screen does.
+class WeightSortPuzzle : public RenderActionRecord {
+public:
+ WeightSortPuzzle() : RenderActionRecord(7) {}
+ virtual ~WeightSortPuzzle() {}
+
+ 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 uint kNumOutcomes = 2;
+
+ enum PickUpMode : byte {
+ kPickUpAny = 0,
+ kPickUpOriginal = 1, // only what the container started with, not what was put in
+ kPickUpSpare = 2 // only down to what the container is required to hold
+ };
+
+ struct ObjectType {
+ int32 kind = 0;
+ int32 weight = 0;
+ Common::Path imageName;
+ Common::Array<Common::Rect> srcs;
+ };
+
+ // The open range of totals a container has to sit in for one of the two outcomes.
+ struct OutcomeRange {
+ int32 maxExclusive = 0;
+ int32 minExclusive = 0;
+ };
+
+ struct Container {
+ byte showTotalWeight = 0; // readout totals weight instead of counting objects
+ bool countsTowardOutcome = false;
+ byte pickUpMode = kPickUpAny;
+ bool dropInPlace = false; // false re-scatters the container after a drop
+ OutcomeRange ranges[kNumOutcomes];
+ Common::Rect area; // where its contents are scattered
+ Common::Rect shakeHotspot; // clicking it re-scatters the container
+ Common::Path readoutImageName;
+ Common::Array<Common::Rect> digitSrcs; // digits 0-9
+ Common::Array<Common::Rect> digitDests; // one slot per readout digit
+ int32 acceptedKinds = 0; // OR of the kind bits it takes
+ RandomSoundBlock pickUpSound;
+ RandomSoundBlock putDownSound;
+ Common::Array<int32> initialContents; // object type indices, duplicates allowed
+ Common::Array<int32> requiredContents; // what it has to hold for the first outcome
+ };
+
+ struct Outcome {
+ SceneChangeDescription scene;
+ FlagDescription flag;
+ RandomSoundBlock sound;
+ };
+
+ struct Object {
+ uint type = 0;
+ int container = -1; // -1 while it rides the cursor
+ bool placedByHand = false;
+ Common::Rect src;
+ Common::Rect dest;
+ };
+
+protected:
+ Common::String getRecordTypeName() const override { return "WeightSortPuzzle"; }
+
+ static void readContainer(Common::SeekableReadStream &stream, Container &dst);
+
+ void scatter(Object &object);
+ void scatterContainer(uint container);
+ int objectAtCursor(const Common::Point &mousePos) const;
+ int containerAtCursor(const Common::Point &mousePos) const;
+ int shakeHotspotAtCursor(const Common::Point &mousePos) const;
+ bool canPickUp(const Object &object) const;
+ int countIn(uint container, int32 type) const;
+ static int countListed(const Common::Array<int32> &list, int32 type);
+ int32 readoutValue(uint container) const;
+ bool containerPasses(uint container, uint outcome) const;
+ // Which outcome the board has reached, or -1 while it is still being sorted.
+ int reachedOutcome() const;
+
+ void carryObject(int object, NancyInput &input);
+ void setDataCursor(uint16 cursorType, bool hotspotVariant = true) const;
+ SoundDescription playSoundBlock(const RandomSoundBlock &block);
+ void redraw();
+
+ // -- File data --
+ byte _firstNeedsAllContainers = 0; // else a single passing container is enough
+ byte _secondNeedsAllContainers = 0;
+ byte _firstNeedsEmptyCursor = 0;
+ byte _returnOnMiss = 0; // dropping outside every container puts it back
+ uint16 _hoverCursorType = 0;
+ uint16 _dragCursorType = 0;
+ uint16 _rejectCursorType = 0;
+
+ Common::Array<ObjectType> _objectTypes;
+ Common::Array<Container> _containers;
+
+ RandomSoundBlock _rejectSound; // a container refusing an object
+ Outcome _outcomes[kNumOutcomes];
+
+ Common::Rect _exitHotspot;
+ uint16 _exitCursorType = 0;
+ SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag;
+
+ // -- Runtime state --
+ Common::Array<Object> _objects;
+ int _carriedObjectID = -1;
+ int _carriedFrom = -1;
+ Misc::MouseFollowObject _carriedObject;
+ int _outcome = -1;
+ bool _exitRequested = false;
+ SoundDescription _endSound;
+
+ Common::Array<Graphics::ManagedSurface> _typeImages;
+ Common::Array<Graphics::ManagedSurface> _readoutImages;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_WEIGHTSORTPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 26302ea8d74..e0712f5b4b8 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -83,6 +83,7 @@ MODULE_OBJS = \
action/puzzle/turningpuzzle.o \
action/puzzle/twodialpuzzle.o \
action/puzzle/typingquizpuzzle.o \
+ action/puzzle/weightsortpuzzle.o \
action/puzzle/whalesurvivorpuzzle.o \
action/puzzle/wordfindpuzzle.o \
ui/fullscreenimage.o \
Commit: 6032e4d3ecfaea1e7710a187146cf76c1055d3c1
https://github.com/scummvm/scummvm/commit/6032e4d3ecfaea1e7710a187146cf76c1055d3c1
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:03+03:00
Commit Message:
NANCY: NANCY15: Fix solving QuizPuzzle when there is no scene change
In this case, only the solve flag should be set
Fixes filling in the environmental analysis table in the clipboard
Changed paths:
engines/nancy/action/puzzle/quizpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/quizpuzzle.cpp b/engines/nancy/action/puzzle/quizpuzzle.cpp
index 39c2e53e104..04a95347b69 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.cpp
+++ b/engines/nancy/action/puzzle/quizpuzzle.cpp
@@ -520,12 +520,15 @@ void QuizPuzzle::executeNancy9() {
bool allSolved = checkAllSolved();
if (allSolved != _solved) {
_solved = allSolved;
- if (!allSolved && _solveScene._flag.label != -1) {
- NancySceneState.setEventFlag(_solveScene._flag.label, g_nancy->_false);
+ if (_solveScene._flag.label != -1) {
+ NancySceneState.setEventFlag(_solveScene._flag.label,
+ allSolved ? _solveScene._flag.flag : g_nancy->_false);
}
}
- if (_solved) {
+ // A record whose solve scene is the one it already runs in never
+ // finishes: it only sets its flag, so its hotspots stay clickable.
+ if (_solved && _solveScene._sceneChange.sceneID != NancySceneState.getSceneInfo().sceneID) {
_internalState = kStartDone;
}
}
Commit: 2b8ba9b1619f805fd3e399188235acd85fdf0296
https://github.com/scummvm/scummvm/commit/2b8ba9b1619f805fd3e399188235acd85fdf0296
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:04+03:00
Commit Message:
NANCY: NANCY15: Don't show empty in-between frames in SafeDialPuzzle
Fixes the animation when opening the locker lock in the Hilihili
Research Center
Changed paths:
engines/nancy/action/puzzle/safedialpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/safedialpuzzle.cpp b/engines/nancy/action/puzzle/safedialpuzzle.cpp
index 47ed7f41835..c3fe172a7c4 100644
--- a/engines/nancy/action/puzzle/safedialpuzzle.cpp
+++ b/engines/nancy/action/puzzle/safedialpuzzle.cpp
@@ -327,6 +327,11 @@ void SafeDialPuzzle::turnDial(bool forwards) {
}
void SafeDialPuzzle::drawDialFrame(uint frame) {
+ // Some dials have no in-between frames, and leave their slots empty
+ if (_dialSrcs[frame].isEmpty()) {
+ return;
+ }
+
if (frame >= _dialSrcs.size() / 2 && !_imageName2.empty()) {
_drawSurface.blitFrom(_image2, _dialSrcs[frame], _dialDest);
} else {
Commit: ec4f1cd641f8de2c446c407e29f8c201bfc18471
https://github.com/scummvm/scummvm/commit/ec4f1cd641f8de2c446c407e29f8c201bfc18471
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:04+03:00
Commit Message:
NANCY: NANCY15: Correctly set flag when clicking on a RolloverOverlay
Fixes navigating through the interface of the greenhouse computer
Changed paths:
engines/nancy/action/overlay.cpp
engines/nancy/action/overlay.h
diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index b8f4f8862c6..15fbbcb31e6 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -693,10 +693,10 @@ void RolloverOverlay::readData(Common::SeekableReadStream &stream) {
_sceneChange.sceneID = stream.readUint16LE();
_sceneChange.frameID = stream.readUint16LE();
- int16 verticalOffset = stream.readSint16LE();
- _sceneChange.verticalOffset = verticalOffset >= 0 ? verticalOffset : 0;
+ _sceneChange.continueSceneSound = kContinueSceneSound;
- _sceneChange.continueSceneSound = stream.readByte();
+ _flagOnClick.label = stream.readSint16LE();
+ _flagOnClick.flag = stream.readByte();
_clickSound.readData(stream);
}
@@ -779,6 +779,8 @@ void RolloverOverlay::execute() {
setVisible(false);
_hasHotspot = false;
+ NancySceneState.setEventFlag(_flagOnClick);
+
if (_sceneChange.sceneID != kNoScene) {
NancySceneState.changeScene(_sceneChange);
}
diff --git a/engines/nancy/action/overlay.h b/engines/nancy/action/overlay.h
index 04f7a926402..90499d96598 100644
--- a/engines/nancy/action/overlay.h
+++ b/engines/nancy/action/overlay.h
@@ -194,7 +194,8 @@ protected:
// Nancy14 AR 53. A rollover label: an image that is only drawn while the mouse
// is inside its hotspot. Entering the hotspot plays a sound and sets an event
-// flag, and clicking it plays a second sound before changing the scene.
+// flag, and clicking it plays a second sound before setting another event flag
+// and changing the scene.
class RolloverOverlay : public RenderActionRecord {
public:
RolloverOverlay() : RenderActionRecord(7) {}
@@ -231,6 +232,8 @@ protected:
uint16 _hoverSoundOnce = 0;
RandomSoundBlock _hoverSound;
SceneChangeDescription _sceneChange;
+ // Set when the click sound finishes, even when there's no scene change
+ FlagDescription _flagOnClick;
RandomSoundBlock _clickSound;
bool _isHovered = false;
Commit: 8f7c638085cdcc57d6c72207f332fc4d82bf26f0
https://github.com/scummvm/scummvm/commit/8f7c638085cdcc57d6c72207f332fc4d82bf26f0
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-25T04:39:05+03:00
Commit Message:
NANCY14-15: Implement PaintPuzzle functionality
- Both games: Keep final painted puzzle on screen after it's solved
- Implement Nancy15 syrup jars
- Implement new Nancy15 fields
- Colors are painted with anti-aliased edges
Nancy14: the finished painting stays on screen correctly
Nancy15: the shaved ice puzzle is working correctly
Changed paths:
engines/nancy/action/puzzle/paintpuzzle.cpp
engines/nancy/action/puzzle/paintpuzzle.h
diff --git a/engines/nancy/action/puzzle/paintpuzzle.cpp b/engines/nancy/action/puzzle/paintpuzzle.cpp
index 326362ab368..487f77708db 100644
--- a/engines/nancy/action/puzzle/paintpuzzle.cpp
+++ b/engines/nancy/action/puzzle/paintpuzzle.cpp
@@ -37,7 +37,12 @@ namespace Nancy {
namespace Action {
void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
+ const bool isNancy15 = g_nancy->getGameType() >= kGameTypeNancy15;
+
readFilename(stream, _imageName); // 0x3d
+ if (isNancy15) {
+ readFilename(stream, _objectsImageName);
+ }
_hoverCursorType = stream.readUint16LE(); // 0x5e
_offset.x = stream.readSint32LE(); // 0x60
_offset.y = stream.readSint32LE(); // 0x64
@@ -52,6 +57,10 @@ void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
color.b = stream.readByte();
readRect(stream, color.swatchRect);
readRect(stream, color.fillRect);
+ if (isNancy15) {
+ readRect(stream, color.objectRect);
+ color.flagLabel = stream.readSint16LE();
+ }
}
int16 numRegions = stream.readSint16LE();
@@ -64,6 +73,14 @@ void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
region.targetColor = stream.readSint16LE();
}
+ if (isNancy15) {
+ _unorderedSolution = stream.readByte();
+ _paintOnce = stream.readByte();
+ // Input outside the canvas rect / hover cursor over regions. All shipped
+ // puzzles accept input everywhere and use no region hover cursor.
+ stream.skip(2);
+ }
+
_sounds[0].readData(stream); // 0xa4
_sounds[1].readData(stream); // 0xfa
@@ -77,10 +94,7 @@ void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
_solveSound.readData(stream); // 0x150
- // Trailing count-prefixed array of 23-byte give-up hotspots
- // {Rect, uint16 cursorType, uint16 sceneID, int16 flagLabel, byte flagValue}.
- // The exit always jumps to the scene's first frame.
- readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
+ readExitHotspots(stream, _exitHotspots);
}
void PaintPuzzle::init() {
@@ -95,6 +109,9 @@ void PaintPuzzle::init() {
moveTo(vpBounds);
g_nancy->_resource->loadImage(_imageName, _image);
+ if (!_objectsImageName.empty()) {
+ g_nancy->_resource->loadImage(_objectsImageName, _objectsImage);
+ }
_regionImages.resize(_regions.size());
for (uint i = 0; i < _regions.size(); ++i) {
@@ -107,6 +124,8 @@ void PaintPuzzle::init() {
_hoverRegion = -1;
_hoverColor = -1;
_solved = false;
+ _solveHandled = false;
+ _takenExit = -1;
redraw();
}
@@ -138,7 +157,8 @@ byte PaintPuzzle::shapeAlpha(const Graphics::ManagedSurface &img, int x, int y)
int PaintPuzzle::regionAtCursor(const Common::Point &mousePos) const {
for (uint i = 0; i < _regions.size(); ++i) {
- if (_regions[i].rect.isEmpty() || i >= _regionImages.size()) {
+ if (_regions[i].rect.isEmpty() || i >= _regionImages.size() ||
+ (_paintOnce && _regions[i].currentColor != -1)) {
continue;
}
Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(_regions[i].rect);
@@ -170,7 +190,8 @@ void PaintPuzzle::drawRegion(uint regionIndex) {
// The region image is a shape mask (alpha channel, or color key for
// alpha-less images). Recolor its shape pixels to the chosen flat color while
// keeping each pixel's original alpha, so the anti-aliased edges blend with
- // the black outline behind them rather than covering it.
+ // the black outline behind them rather than covering it. Overlapping regions
+ // blend over the ones drawn before them.
const PaintColor &color = _colors[c];
const Common::Rect &dst = region.rect;
@@ -178,69 +199,89 @@ void PaintPuzzle::drawRegion(uint regionIndex) {
for (int x = 0; x < img.w && dst.left + x < _drawSurface.w; ++x) {
byte a = shapeAlpha(img, x, y);
if (a != 0) {
- _drawSurface.setPixel(dst.left + x, dst.top + y,
- _drawSurface.format.ARGBToColor(a, color.r, color.g, color.b));
+ blendPixel(dst.left + x, dst.top + y, a, color.r, color.g, color.b);
}
}
}
}
-// Draws the held color's brush sprite at the cursor. The cursor is blanked while a
-// color is held, so the brush is what the player sees moving.
-void PaintPuzzle::drawBrush() {
- if (_heldColor < 0 || _heldColor >= (int)_colors.size() || _image.w == 0) {
+// Composites a pixel over what is already drawn at (x,y).
+void PaintPuzzle::blendPixel(int x, int y, byte a, byte r, byte g, byte b) {
+ if (x < 0 || y < 0 || x >= _drawSurface.w || y >= _drawSurface.h) {
return;
}
- const Common::Rect &src = _colors[_heldColor].fillRect;
- if (src.isEmpty()) {
+ if (a != 255) {
+ byte da, dr, dg, db;
+ _drawSurface.format.colorToARGB(_drawSurface.getPixel(x, y), da, dr, dg, db);
+
+ uint destAlpha = da * (255 - a) / 255;
+ uint outAlpha = a + destAlpha;
+ r = (r * a + dr * destAlpha) / outAlpha;
+ g = (g * a + dg * destAlpha) / outAlpha;
+ b = (b * a + db * destAlpha) / outAlpha;
+ a = outAlpha;
+ }
+
+ _drawSurface.setPixel(x, y, _drawSurface.format.ARGBToColor(a, r, g, b));
+}
+
+// Draws a sprite from img, skipping transparent pixels and compositing
+// anti-aliased edges over what is already drawn.
+void PaintPuzzle::drawSprite(const Graphics::ManagedSurface &img, const Common::Rect &src, const Common::Point &dest) {
+ if (img.w == 0 || src.isEmpty()) {
return;
}
byte tr, tg, tb;
g_nancy->_graphics->getInputPixelFormat().colorToRGB(g_nancy->_graphics->getTransColor(), tr, tg, tb);
- // The sprite is centered on the cursor, then shifted by the offset.
- const Common::Point dest(_brushPos.x - src.width() / 2 - _offset.x,
- _brushPos.y - src.height() / 2 - _offset.y);
-
- for (int y = 0; y < src.height() && src.top + y < _image.h; ++y) {
+ for (int y = 0; y < src.height() && src.top + y < img.h; ++y) {
const int dy = dest.y + y;
if (dy < 0 || dy >= _drawSurface.h) {
continue;
}
- for (int x = 0; x < src.width() && src.left + x < _image.w; ++x) {
+ for (int x = 0; x < src.width() && src.left + x < img.w; ++x) {
const int dx = dest.x + x;
if (dx < 0 || dx >= _drawSurface.w) {
continue;
}
byte a, r, g, b;
- _image.format.colorToARGB(_image.getPixel(src.left + x, src.top + y), a, r, g, b);
+ img.format.colorToARGB(img.getPixel(src.left + x, src.top + y), a, r, g, b);
if (a == 0 || (r == tr && g == tg && b == tb)) {
continue;
}
- if (a != 255) {
- // Composite the anti-aliased edges over the painted regions below,
- // instead of replacing their paint with semi-transparent pixels.
- byte da, dr, dg, db;
- _drawSurface.format.colorToARGB(_drawSurface.getPixel(dx, dy), da, dr, dg, db);
-
- uint destAlpha = da * (255 - a) / 255;
- uint outAlpha = a + destAlpha;
- r = (r * a + dr * destAlpha) / outAlpha;
- g = (g * a + dg * destAlpha) / outAlpha;
- b = (b * a + db * destAlpha) / outAlpha;
- a = outAlpha;
- }
+ blendPixel(dx, dy, a, r, g, b);
+ }
+ }
+}
- _drawSurface.setPixel(dx, dy, _drawSurface.format.ARGBToColor(a, r, g, b));
+// Draws each color's object at its swatch, except for the held color's.
+void PaintPuzzle::drawObjects() {
+ for (uint i = 0; i < _colors.size(); ++i) {
+ if ((int)i != _heldColor) {
+ drawSprite(_objectsImage, _colors[i].objectRect,
+ Common::Point(_colors[i].swatchRect.left, _colors[i].swatchRect.top));
}
}
}
+// Draws the held color's brush sprite at the cursor. The cursor is blanked while a
+// color is held, so the brush is what the player sees moving.
+void PaintPuzzle::drawBrush() {
+ if (_heldColor < 0 || _heldColor >= (int)_colors.size()) {
+ return;
+ }
+
+ // The sprite is centered on the cursor, then shifted by the offset.
+ const Common::Rect &src = _colors[_heldColor].fillRect;
+ drawSprite(_image, src, Common::Point(_brushPos.x - src.width() / 2 - _offset.x,
+ _brushPos.y - src.height() / 2 - _offset.y));
+}
+
void PaintPuzzle::redraw() {
_drawSurface.clear(0);
@@ -252,6 +293,7 @@ void PaintPuzzle::redraw() {
}
}
+ drawObjects();
drawBrush();
_needsRedraw = true;
@@ -261,17 +303,57 @@ bool PaintPuzzle::isSolved() const {
if (_regions.empty()) {
return false;
}
+
+ if (!_unorderedSolution) {
+ for (uint i = 0; i < _regions.size(); ++i) {
+ if (_regions[i].currentColor != _regions[i].targetColor) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Every target must be matched by the color of a different region
+ Common::Array<bool> used(_regions.size(), false);
for (uint i = 0; i < _regions.size(); ++i) {
- if (_regions[i].currentColor != _regions[i].targetColor) {
+ bool found = false;
+ for (uint j = 0; j < _regions.size(); ++j) {
+ if (!used[j] && _regions[j].currentColor == _regions[i].targetColor) {
+ used[j] = true;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
return false;
}
}
return true;
}
+// Each color's flag is set while that color is on at least one region.
+void PaintPuzzle::updateColorFlags() {
+ for (uint i = 0; i < _colors.size(); ++i) {
+ if (_colors[i].flagLabel == -1) {
+ continue;
+ }
+
+ bool used = false;
+ for (uint j = 0; j < _regions.size(); ++j) {
+ if (_regions[j].currentColor == (int16)(i + 1)) {
+ used = true;
+ break;
+ }
+ }
+ NancySceneState.setEventFlag(_colors[i].flagLabel, used ? g_nancy->_true : g_nancy->_false);
+ }
+}
+
void PaintPuzzle::paintRegion(uint regionIndex, int colorIndex) {
// Region colors are 1-based palette indices; 0 means unpainted
_regions[regionIndex].currentColor = (int16)(colorIndex + 1);
+ playSoundBlock(_sounds[1]);
+ updateColorFlags();
if (isSolved()) {
_solved = true;
}
@@ -304,10 +386,13 @@ bool PaintPuzzle::isSoundBlockPlaying(const RandomSoundBlock &block) const {
}
void PaintPuzzle::handleInput(NancyInput &input) {
- if (_state != kRun || _solved) {
+ // A solve without a scene change leaves the puzzle playable
+ if (_state != kRun || (_solved && !_solveHandled)) {
return;
}
+ const bool isNancy15 = g_nancy->getGameType() >= kGameTypeNancy15;
+
// A held color turns the cursor into its brush sprite, which the puzzle draws itself.
if (_heldColor >= 0) {
Common::Rect screenPt(input.mousePos.x, input.mousePos.y, input.mousePos.x + 1, input.mousePos.y + 1);
@@ -322,12 +407,25 @@ void PaintPuzzle::handleInput(NancyInput &input) {
g_nancy->_cursor->setCursorType(CursorManager::kNancy13Blank, true, false);
}
- // Give-up hotspot: leave the puzzle.
- if (!_exitHotspot.isEmpty() &&
- NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
- g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitCursorType, true);
+ // Exit hotspots: leave the puzzle. In Nancy15 they only work with empty hands,
+ // as one covers the canvas.
+ for (uint i = 0; i < _exitHotspots.size() && !(isNancy15 && _heldColor >= 0); ++i) {
+ if (!_exitHotspots[i].hotspot.isEmpty() &&
+ NancySceneState.getViewport().convertViewportToScreen(_exitHotspots[i].hotspot).contains(input.mousePos)) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitHotspots[i].cursorType, true);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _takenExit = (int)i;
+ }
+ input.eatMouseInput();
+ return;
+ }
+ }
+
+ // Over a paintable region with a color picked: paint it on click.
+ int region = _heldColor >= 0 ? regionAtCursor(input.mousePos) : -1;
+ if (region >= 0) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _exitRequested = true;
+ paintRegion((uint)region, _heldColor);
}
input.eatMouseInput();
return;
@@ -337,23 +435,32 @@ void PaintPuzzle::handleInput(NancyInput &input) {
if (color >= 0) {
g_nancy->_cursor->setCursorType((CursorManager::CursorType)_hoverCursorType, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _heldColor = color;
- redraw();
+ if (isNancy15 && color == _heldColor) {
+ dropColor();
+ } else {
+ _heldColor = color;
+ playSoundBlock(_sounds[0]);
+ redraw();
+ }
}
input.eatMouseInput();
return;
}
- // Over a paintable region with a color picked: paint it on click.
- int region = regionAtCursor(input.mousePos);
- if (region >= 0 && _heldColor >= 0) {
- if (input.input & NancyInput::kLeftMouseButtonUp) {
- paintRegion((uint)region, _heldColor);
- }
+ // Clicking outside the canvas puts the held color back.
+ if (isNancy15 && _heldColor >= 0 && (input.input & NancyInput::kLeftMouseButtonUp) &&
+ !NancySceneState.getViewport().convertViewportToScreen(_canvasRect).contains(input.mousePos)) {
+ dropColor();
input.eatMouseInput();
}
}
+void PaintPuzzle::dropColor() {
+ _heldColor = -1;
+ playSoundBlock(_sounds[0]);
+ redraw();
+}
+
void PaintPuzzle::execute() {
switch (_state) {
case kBegin:
@@ -362,7 +469,13 @@ void PaintPuzzle::execute() {
_state = kRun;
break;
case kRun:
- if (_exitRequested || _solved) {
+ if (_solved && !_solveHandled && _solveScene._sceneChange.sceneID == kNoScene) {
+ // No scene change: set the flag and keep the puzzle on screen, so
+ // the scene's own records can react to it.
+ _solveHandled = true;
+ playSoundBlock(_solveSound);
+ NancySceneState.setEventFlag(_solveScene._flag);
+ } else if (_takenExit >= 0 || (_solved && !_solveHandled)) {
if (_solved) {
playSoundBlock(_solveSound);
}
@@ -371,13 +484,19 @@ void PaintPuzzle::execute() {
break;
case kActionTrigger:
// The solve sound gets to finish first
- if (!_exitRequested && isSoundBlockPlaying(_solveSound)) {
+ if (_takenExit < 0 && isSoundBlockPlaying(_solveSound)) {
break;
}
- if (_exitRequested) {
- NancySceneState.setEventFlag(_exitFlag);
- NancySceneState.changeScene(_exitScene);
+ // The overlay may outlive the puzzle, so don't leave the brush in it
+ if (_heldColor >= 0) {
+ _heldColor = -1;
+ redraw();
+ }
+
+ if (_takenExit >= 0) {
+ NancySceneState.setEventFlag(_exitHotspots[_takenExit].flag);
+ NancySceneState.changeScene(_exitHotspots[_takenExit].scene);
} else {
_solveScene.execute();
}
diff --git a/engines/nancy/action/puzzle/paintpuzzle.h b/engines/nancy/action/puzzle/paintpuzzle.h
index fbe7ad9c093..0a6b287b05c 100644
--- a/engines/nancy/action/puzzle/paintpuzzle.h
+++ b/engines/nancy/action/puzzle/paintpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_PAINTPUZZLE_H
#include "engines/nancy/commontypes.h"
+#include "engines/nancy/util.h"
#include "engines/nancy/action/actionrecord.h"
namespace Nancy {
@@ -32,6 +33,8 @@ namespace Action {
// palette (Vincent's brush) and clicks the regions of a picture to fill them.
// Each region has a target color; the puzzle is solved once every region holds
// its target color. A filled region is its shape recolored to the chosen color.
+// Nancy15 adds per-color objects (paint jars) drawn at the swatches, per-color
+// event flags, order-independent solutions and single-paint regions.
class PaintPuzzle : public RenderActionRecord {
public:
PaintPuzzle() : RenderActionRecord(7) {}
@@ -45,16 +48,24 @@ public:
bool isViewportRelative() const override { return true; }
+ // The painted picture stays on screen in the NO_ART_SCENE conversation
+ // that follows the puzzle
+ bool survivesSceneChange(bool nextSceneIsNoArt) const override { return nextSceneIsNoArt; }
+
protected:
Common::String getRecordTypeName() const override { return "PaintPuzzle"; }
- // A palette color: its RGB, the clickable swatch, and its fill sprite.
+ // A palette color: its RGB, the clickable swatch, its brush sprite, and
+ // (Nancy15) the object sprite drawn at the swatch plus a flag set while the
+ // color is used on any region.
struct PaintColor {
byte r = 0;
byte g = 0;
byte b = 0;
Common::Rect swatchRect;
Common::Rect fillRect;
+ Common::Rect objectRect;
+ int16 flagLabel = -1;
};
// A fillable region of the picture: an overlay shape drawn at a position,
@@ -73,8 +84,13 @@ protected:
// 255/0 from the color key for alpha-less images. 0 means outside the shape.
byte shapeAlpha(const Graphics::ManagedSurface &img, int x, int y) const;
void paintRegion(uint regionIndex, int colorIndex);
+ void dropColor();
void drawRegion(uint regionIndex);
+ void blendPixel(int x, int y, byte a, byte r, byte g, byte b);
+ void drawSprite(const Graphics::ManagedSurface &img, const Common::Rect &src, const Common::Point &dest);
+ void drawObjects();
void drawBrush();
+ void updateColorFlags();
void redraw();
bool isSolved() const;
void playSoundBlock(const RandomSoundBlock &block);
@@ -82,6 +98,7 @@ protected:
// -- File data --
Common::Path _imageName; // 0x3d
+ Common::Path _objectsImageName;
uint16 _hoverCursorType = 0; // 0x5e - raw Nancy14 cursor type, shown over a color swatch
Common::Point _offset; // 0x60 (two int32) - brush sprite offset from the cursor
Common::Rect _canvasRect; // 0x68
@@ -89,26 +106,27 @@ protected:
Common::Array<PaintColor> _colors; // 0x78
Common::Array<PaintRegion> _regions; // 0x94
+ bool _unorderedSolution = false; // Region targets may be matched in any order
+ bool _paintOnce = false; // A painted region can't be repainted
+
RandomSoundBlock _sounds[2]; // 0xa4/0xfa
SceneChangeWithFlag _solveScene; // 0x1a6
RandomSoundBlock _solveSound; // 0x150, plays before the solve scene change
- // Give-up hotspot (count-prefixed 23-byte trailer): click to leave the puzzle.
- Common::Rect _exitHotspot;
- uint16 _exitCursorType = 0;
- SceneChangeDescription _exitScene;
- FlagDescription _exitFlag;
+ Common::Array<ExitHotspot> _exitHotspots;
// -- Runtime state --
Graphics::ManagedSurface _image;
+ Graphics::ManagedSurface _objectsImage;
Common::Array<Graphics::ManagedSurface> _regionImages;
int _heldColor = -1;
Common::Point _brushPos;
int _hoverRegion = -1;
int _hoverColor = -1;
bool _solved = false;
- bool _exitRequested = false;
+ bool _solveHandled = false;
+ int _takenExit = -1;
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list