[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 &region = _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 &region = _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