[Scummvm-git-logs] scummvm master -> 7b25da5b1526c754494c317aee6f5642f7c20293
bluegr
noreply at scummvm.org
Tue Aug 4 23:50:55 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
7b25da5b15 NANCY: NANCY14: Implement PaintPuzzle
Commit: 7b25da5b1526c754494c317aee6f5642f7c20293
https://github.com/scummvm/scummvm/commit/7b25da5b1526c754494c317aee6f5642f7c20293
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-05T02:50:44+03:00
Commit Message:
NANCY: NANCY14: Implement PaintPuzzle
Paint-fill puzzle, new in Nancy14 (AR 181). The player picks a color
from a 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.
Changed paths:
A engines/nancy/action/puzzle/paintpuzzle.cpp
A engines/nancy/action/puzzle/paintpuzzle.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 9c4beaa3188..0c414aeeb60 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -65,6 +65,7 @@
#include "engines/nancy/action/puzzle/orderingpuzzle.h"
#include "engines/nancy/action/puzzle/overridelockpuzzle.h"
#include "engines/nancy/action/puzzle/pachinkopuzzle.h"
+#include "engines/nancy/action/puzzle/paintpuzzle.h"
#include "engines/nancy/action/puzzle/passwordpuzzle.h"
#include "engines/nancy/action/puzzle/peepholepuzzle.h"
#include "engines/nancy/action/puzzle/pegspuzzle.h"
@@ -517,9 +518,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 180: // BlockingPuzzle
// TODO: not yet implemented
return nullptr;
- case 181: // PaintPuzzle
- // TODO: not yet implemented
- return nullptr;
+ case 181:
+ return new PaintPuzzle();
case 182: // DecoderPuzzle
// TODO: not yet implemented
return nullptr;
diff --git a/engines/nancy/action/puzzle/paintpuzzle.cpp b/engines/nancy/action/puzzle/paintpuzzle.cpp
new file mode 100644
index 00000000000..341df841eb5
--- /dev/null
+++ b/engines/nancy/action/puzzle/paintpuzzle.cpp
@@ -0,0 +1,277 @@
+/* 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/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/action/puzzle/paintpuzzle.h"
+
+#include "engines/nancy/state/scene.h"
+
+namespace Nancy {
+namespace Action {
+
+void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName); // 0x3d
+ _field5e = stream.readSint16LE(); // 0x5e
+ _offset.x = stream.readSint32LE(); // 0x60
+ _offset.y = stream.readSint32LE(); // 0x64
+ readRect(stream, _canvasRect); // 0x68
+
+ int16 numColors = stream.readSint16LE();
+ _colors.resize(numColors); // 0x78
+ for (int16 i = 0; i < numColors; ++i) {
+ PaintColor &color = _colors[i];
+ color.r = stream.readByte();
+ color.g = stream.readByte();
+ color.b = stream.readByte();
+ readRect(stream, color.swatchRect);
+ readRect(stream, color.fillRect);
+ }
+
+ int16 numRegions = stream.readSint16LE();
+ _regions.resize(numRegions); // 0x94
+ for (int16 i = 0; i < numRegions; ++i) {
+ PaintRegion ®ion = _regions[i];
+ readFilename(stream, region.name);
+ readRect(stream, region.rect);
+ region.currentColor = stream.readSint16LE();
+ region.targetColor = stream.readSint16LE();
+ }
+
+ _sounds[0].readData(stream); // 0xa4
+ _sounds[1].readData(stream); // 0xfa
+
+ _field1a6 = stream.readSint16LE(); // 0x1a6
+ _outcome.field0 = stream.readSint16LE(); // 0x1a8
+ _outcome.sceneID = stream.readSint16LE();
+ _outcome.flag = stream.readByte();
+
+ _sounds[2].readData(stream);
+
+ // Trailing block read by the base record reader (25 bytes).
+ _tailField0 = stream.readSint32LE();
+ _tailField1 = stream.readSint16LE();
+ _tailVector[0] = stream.readSint32LE();
+ _tailVector[1] = stream.readSint32LE();
+ _tailVector[2] = stream.readSint32LE();
+ _tailField2 = stream.readSint16LE();
+ _tailSceneID = stream.readSint16LE();
+ _tailFlag = stream.readSint16LE();
+ _tailByte = stream.readByte();
+}
+
+void PaintPuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ // Use the BGRA32 transparent format so the recolored region shapes keep their
+ // source alpha and their anti-aliased edges blend with the black line-art
+ // behind them instead of covering it.
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getTransparentPixelFormat());
+ _drawSurface.clear(0);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+
+ _regionImages.resize(_regions.size());
+ for (uint i = 0; i < _regions.size(); ++i) {
+ if (!_regions[i].name.empty()) {
+ g_nancy->_resource->loadImage(_regions[i].name, _regionImages[i]);
+ }
+ }
+
+ _heldColor = -1;
+ _hoverRegion = -1;
+ _hoverColor = -1;
+ _solved = false;
+ _outcomeApplied = false;
+
+ redraw();
+}
+
+int PaintPuzzle::colorSwatchAtCursor(const Common::Point &mousePos) const {
+ for (uint i = 0; i < _colors.size(); ++i) {
+ if (_colors[i].swatchRect.isEmpty()) {
+ continue;
+ }
+ if (NancySceneState.getViewport().convertViewportToScreen(_colors[i].swatchRect).contains(mousePos)) {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+byte PaintPuzzle::shapeAlpha(const Graphics::ManagedSurface &img, int x, int y) const {
+ if (img.w == 0 || x < 0 || y < 0 || x >= img.w || y >= img.h) {
+ return 0;
+ }
+ uint32 p = img.getPixel(x, y);
+ if (img.format.aBits() != 0) {
+ byte a, r, g, b;
+ img.format.colorToARGB(p, a, r, g, b);
+ return a;
+ }
+ return p != img.getTransparentColor() ? 255 : 0;
+}
+
+int PaintPuzzle::regionAtCursor(const Common::Point &mousePos) const {
+ for (uint i = 0; i < _regions.size(); ++i) {
+ if (_regions[i].rect.isEmpty() || i >= _regionImages.size()) {
+ continue;
+ }
+ Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(_regions[i].rect);
+ if (!screenRect.contains(mousePos)) {
+ continue;
+ }
+ // Hit-test the region's actual shape, not just its bounding rect, so
+ // overlapping bounding boxes don't register clicks for the wrong region.
+ if (shapeAlpha(_regionImages[i], mousePos.x - screenRect.left, mousePos.y - screenRect.top) != 0) {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+// Draws a painted region: its overlay shape recolored to the flat fill color.
+void PaintPuzzle::drawRegion(uint regionIndex) {
+ const PaintRegion ®ion = _regions[regionIndex];
+ int c = region.currentColor;
+ if (c < 0 || c >= (int)_colors.size() || regionIndex >= _regionImages.size()) {
+ return;
+ }
+
+ const Graphics::ManagedSurface &img = _regionImages[regionIndex];
+ if (img.w == 0 || img.h == 0) {
+ return;
+ }
+
+ // 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.
+ const PaintColor &color = _colors[c];
+ const Common::Rect &dst = region.rect;
+
+ for (int y = 0; y < img.h && dst.top + y < _drawSurface.h; ++y) {
+ 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));
+ }
+ }
+ }
+}
+
+void PaintPuzzle::redraw() {
+ _drawSurface.clear(0);
+
+ // Only painted regions are drawn on the overlay; the picture outline and
+ // palette come from the scene background.
+ for (uint i = 0; i < _regions.size(); ++i) {
+ if (_regions[i].currentColor >= 0) {
+ drawRegion(i);
+ }
+ }
+
+ _needsRedraw = true;
+}
+
+bool PaintPuzzle::isSolved() const {
+ if (_regions.empty()) {
+ return false;
+ }
+ for (uint i = 0; i < _regions.size(); ++i) {
+ if (_regions[i].currentColor != _regions[i].targetColor) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void PaintPuzzle::paintRegion(uint regionIndex, int colorIndex) {
+ _regions[regionIndex].currentColor = (int16)colorIndex;
+ if (isSolved()) {
+ _solved = true;
+ }
+ redraw();
+}
+
+void PaintPuzzle::applyOutcome(const SceneOutcome &outcome) {
+ SceneChangeDescription desc;
+ desc.sceneID = outcome.sceneID;
+ NancySceneState.changeScene(desc);
+ NancySceneState.setEventFlag(outcome.field0, outcome.flag);
+}
+
+void PaintPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _solved) {
+ return;
+ }
+
+ int color = colorSwatchAtCursor(input.mousePos);
+ if (color >= 0) {
+ // Over a color swatch: show the blue puzzle-hotspot hand and pick the
+ // color on click.
+ g_nancy->_cursor->setCursorType(CursorManager::kPuzzleArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _heldColor = color;
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ // Over a paintable region with a color picked: paint it on click. The
+ // cursor is left as the held paintbrush item.
+ int region = regionAtCursor(input.mousePos);
+ if (region >= 0 && _heldColor >= 0) {
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ paintRegion((uint)region, _heldColor);
+ }
+ input.eatMouseInput();
+ }
+}
+
+void PaintPuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ registerGraphics();
+ _state = kRun;
+ break;
+ case kRun:
+ if (_solved && !_outcomeApplied) {
+ _outcomeApplied = true;
+ applyOutcome(_outcome);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/paintpuzzle.h b/engines/nancy/action/puzzle/paintpuzzle.h
new file mode 100644
index 00000000000..713f13dc1c8
--- /dev/null
+++ b/engines/nancy/action/puzzle/paintpuzzle.h
@@ -0,0 +1,122 @@
+/* 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_PAINTPUZZLE_H
+#define NANCY_ACTION_PAINTPUZZLE_H
+
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/action/actionrecord.h"
+
+namespace Nancy {
+namespace Action {
+
+// Paint-fill puzzle, new in Nancy14 (AR 181). The player picks a color from a
+// 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.
+class PaintPuzzle : public RenderActionRecord {
+public:
+ PaintPuzzle() : RenderActionRecord(7) {}
+ virtual ~PaintPuzzle() {}
+
+ 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 "PaintPuzzle"; }
+
+ // A palette color: its RGB, the clickable swatch, and its fill sprite.
+ struct PaintColor {
+ byte r = 0;
+ byte g = 0;
+ byte b = 0;
+ Common::Rect swatchRect;
+ Common::Rect fillRect;
+ };
+
+ // A fillable region of the picture: an overlay shape drawn at a position,
+ // its current color index, and the target it must hold to be solved.
+ struct PaintRegion {
+ Common::Path name;
+ Common::Rect rect;
+ int16 currentColor = -1;
+ int16 targetColor = -1;
+ };
+
+ struct SceneOutcome {
+ int16 field0 = 0;
+ int16 sceneID = 0;
+ byte flag = 0;
+ };
+
+ int colorSwatchAtCursor(const Common::Point &mousePos) const;
+ int regionAtCursor(const Common::Point &mousePos) const;
+ // Alpha of the region overlay's shape at (x,y): the pixel's alpha channel, or
+ // 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 drawRegion(uint regionIndex);
+ void redraw();
+ bool isSolved() const;
+ void applyOutcome(const SceneOutcome &outcome);
+
+ // -- File data --
+ Common::Path _imageName; // 0x3d
+ int16 _field5e = 0; // 0x5e
+ Common::Point _offset; // 0x60 (two int32)
+ Common::Rect _canvasRect; // 0x68
+
+ Common::Array<PaintColor> _colors; // 0x78
+ Common::Array<PaintRegion> _regions; // 0x94
+
+ RandomSoundBlock _sounds[3]; // 0xa4/0xfa (before) + one after the outcome
+
+ int16 _field1a6 = 0; // 0x1a6
+ SceneOutcome _outcome; // 0x1a8
+
+ // Trailing block read by the base record reader (vtable+0x24), 25 bytes.
+ int32 _tailField0 = 0;
+ int16 _tailField1 = 0;
+ int32 _tailVector[3] = { 0, 0, 0 };
+ int16 _tailField2 = 0;
+ int16 _tailSceneID = 0;
+ int16 _tailFlag = 0;
+ byte _tailByte = 0;
+
+ // -- Runtime state --
+ Graphics::ManagedSurface _image;
+ Common::Array<Graphics::ManagedSurface> _regionImages;
+ int _heldColor = -1;
+ int _hoverRegion = -1;
+ int _hoverColor = -1;
+ bool _solved = false;
+ bool _outcomeApplied = false;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_PAINTPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index c82b8813691..fb5bee710f8 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -49,6 +49,7 @@ MODULE_OBJS = \
action/puzzle/orderingpuzzle.o \
action/puzzle/overridelockpuzzle.o \
action/puzzle/pachinkopuzzle.o \
+ action/puzzle/paintpuzzle.o \
action/puzzle/passwordpuzzle.o \
action/puzzle/peepholepuzzle.o \
action/puzzle/pegspuzzle.o \
More information about the Scummvm-git-logs
mailing list