[Scummvm-git-logs] scummvm master -> 6cfb9c87554f5b8f38b0e19f1e27d24de4c1c72f

bluegr noreply at scummvm.org
Wed Jul 8 02:36:35 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:
88f96d750d NANCY: NANCY11: Cleanup comments
3181ebddc1 NANCY: Add scene change info to action record printing
a7089d53c1 NANCY: Use the kNoScene enum instead of the literal value
031b043a46 NANCY: NANCY11: More work on the keypad terse variant of OrderingPuzzle
db9f3edc3f NANCY: Cleanup comments
6cfb9c8755 NANCY: NANCY12: Implement SewingMachinePuzzle


Commit: 88f96d750d8ae021fa6980a58705dd4ec4b24483
    https://github.com/scummvm/scummvm/commit/88f96d750d8ae021fa6980a58705dd4ec4b24483
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:18+03:00

Commit Message:
NANCY: NANCY11: Cleanup comments

Changed paths:
    engines/nancy/action/puzzle/cardgamepuzzle.cpp


diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.cpp b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
index 62d34aa2a91..6dfe6888446 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.cpp
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
@@ -501,8 +501,7 @@ void CardGamePuzzle::playVoice(const Common::String &name) {
 	showSubtitle(name);
 }
 
-// The card-game lines carry no inline caption; the original looks the subtitle up by sound name in
-// the Autotext table. Mirror that and push it to the textbox (as QuizPuzzle does).
+// The card-game lines carry no inline caption; look the subtitle up by sound name in the Autotext table.
 void CardGamePuzzle::showSubtitle(const Common::String &soundName) {
 	const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
 	if (!autotext) {


Commit: 3181ebddc1a0e6e3d6101d7e56f5cba6512b41f2
    https://github.com/scummvm/scummvm/commit/3181ebddc1a0e6e3d6101d7e56f5cba6512b41f2
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:20+03:00

Commit Message:
NANCY: Add scene change info to action record printing

Helps debug scenes with complex logic, that may jump to several
different scenes

Changed paths:
    engines/nancy/action/actionrecord.h
    engines/nancy/action/navigationrecords.h
    engines/nancy/action/secondarymovie.h
    engines/nancy/action/secondaryvideo.h
    engines/nancy/action/soundrecords.h
    engines/nancy/console.cpp


diff --git a/engines/nancy/action/actionrecord.h b/engines/nancy/action/actionrecord.h
index a4d6fcf503a..ca9bdaf0390 100644
--- a/engines/nancy/action/actionrecord.h
+++ b/engines/nancy/action/actionrecord.h
@@ -120,6 +120,7 @@ public:
 
 	// Used for debugging
 	virtual Common::String getRecordTypeName() const = 0;
+	virtual Common::String getRecordExtraInfo() const { return ""; }
 
 	// Used for handling kCursorType dependency
 	virtual bool canHaveHotspot() const { return false; }
diff --git a/engines/nancy/action/navigationrecords.h b/engines/nancy/action/navigationrecords.h
index 4d224fe2a55..4c3999c1743 100644
--- a/engines/nancy/action/navigationrecords.h
+++ b/engines/nancy/action/navigationrecords.h
@@ -35,6 +35,8 @@ public:
 
 	SceneChangeDescription _sceneChange;
 
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+
 protected:
 	Common::String getRecordTypeName() const override { return "SceneChange"; }
 };
@@ -187,6 +189,8 @@ public:
 	SceneChangeDescription _defaultScene;
 	Common::Array<HotspotDescription> _hotspots;
 
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Default scene %d", _defaultScene.sceneID); }
+
 protected:
 	Common::String getRecordTypeName() const override { return "HotMultiframeMultisceneCursorTypeSceneChange"; }
 };
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index e272dee2722..f70dfcb1a88 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -140,6 +140,8 @@ public:
 		return _isRandom && !_isDone && !_randomStopRequested;
 	}
 
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+
 protected:
 	Common::String getRecordTypeName() const override {
 		return _isRandom ? "PlayRandomMovie" : "PlaySecondaryMovie";
diff --git a/engines/nancy/action/secondaryvideo.h b/engines/nancy/action/secondaryvideo.h
index 3167c2677ee..8137060baaf 100644
--- a/engines/nancy/action/secondaryvideo.h
+++ b/engines/nancy/action/secondaryvideo.h
@@ -87,6 +87,8 @@ public:
 		return g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotTalk : CursorManager::kHotspot;
 	}
 
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+
 protected:
 	Common::String getRecordTypeName() const override { return "PlaySecondaryVideo"; }
 
diff --git a/engines/nancy/action/soundrecords.h b/engines/nancy/action/soundrecords.h
index 6679648a775..26e7e79500d 100644
--- a/engines/nancy/action/soundrecords.h
+++ b/engines/nancy/action/soundrecords.h
@@ -110,6 +110,8 @@ public:
 	SceneChangeDescription _sceneChange;
 	FlagDescription _flag;
 
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+
 protected:
 	Common::String getRecordTypeName() const override;
 };
@@ -173,7 +175,8 @@ public:
 	Common::Array<HotspotDescription> _hotspots; // 0x31
 
 	bool canHaveHotspot() const override { return true; }
-
+	
+	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
 protected:
 	Common::String getRecordTypeName() const override { return "PlaySoundMultiHS"; }
 };
diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index b7d5068ae2e..b70cad20c37 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -476,6 +476,10 @@ void NancyConsole::printActionRecord(const Action::ActionRecord *record, bool no
 		record->getRecordTypeName().c_str(),
 		record->_execType == Action::ActionRecord::kRepeating ? "kRepeating" : "kOneShot");
 
+	Common::String extraInfo = record->getRecordExtraInfo();
+	if (!extraInfo.empty())
+		debugPrintf("\n\textraInfo: %s", extraInfo.c_str());
+
 	if (!noDependencies && record->_dependencies.children.size()) {
 		debugPrintf("\n\tDependencies:");
 


Commit: a7089d53c16a287cb7127ec27c9002b54efe1c3f
    https://github.com/scummvm/scummvm/commit/a7089d53c16a287cb7127ec27c9002b54efe1c3f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:22+03:00

Commit Message:
NANCY: Use the kNoScene enum instead of the literal value

Changed paths:
    engines/nancy/action/puzzle/cardgamepuzzle.cpp
    engines/nancy/action/puzzle/twodialpuzzle.cpp
    engines/nancy/action/puzzle/typingquizpuzzle.cpp


diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.cpp b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
index 6dfe6888446..f634c710cab 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.cpp
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
@@ -590,11 +590,11 @@ void CardGamePuzzle::execute() {
 			const bool tie = (playerScore == aiScore);
 
 			// The result normally uses the first outcome scene; the second is the tie/alternate
-			// path the 0xff deal mode takes. 9999 marks an absent scene.
-			if (tie && _dealMode == 0xff && _winSceneStartEnemy != 9999) {
+			// path the 0xff deal mode takes.
+			if (tie && _dealMode == 0xff && _winSceneStartEnemy != kNoScene) {
 				sceneChange.sceneID = _winSceneStartEnemy;
 			} else {
-				sceneChange.sceneID = (_winSceneStartPlayer != 9999) ? _winSceneStartPlayer : _winSceneStartEnemy;
+				sceneChange.sceneID = (_winSceneStartPlayer != kNoScene) ? _winSceneStartPlayer : _winSceneStartEnemy;
 			}
 
 			// Record the outcome: the player-win flag when ahead, else the alternate flag on a tie.
diff --git a/engines/nancy/action/puzzle/twodialpuzzle.cpp b/engines/nancy/action/puzzle/twodialpuzzle.cpp
index e1021b188f2..79b8668ee0b 100644
--- a/engines/nancy/action/puzzle/twodialpuzzle.cpp
+++ b/engines/nancy/action/puzzle/twodialpuzzle.cpp
@@ -143,7 +143,7 @@ void TwoDialPuzzle::execute() {
 			// The matched solution supplies the scene to change to.
 			for (uint i = 0; i < _solutions.size(); ++i) {
 				const DialSolution &sol = _solutions[i];
-				if (sol.sceneID != 9999 &&
+				if (sol.sceneID != kNoScene &&
 						_currentPositions[0] == sol.positions[0] &&
 						_currentPositions[1] == sol.positions[1] &&
 						NancySceneState.getEventFlag(sol.condition.label, sol.condition.flag)) {
diff --git a/engines/nancy/action/puzzle/typingquizpuzzle.cpp b/engines/nancy/action/puzzle/typingquizpuzzle.cpp
index 24a6c5f2d4c..a825d1722ab 100644
--- a/engines/nancy/action/puzzle/typingquizpuzzle.cpp
+++ b/engines/nancy/action/puzzle/typingquizpuzzle.cpp
@@ -380,7 +380,7 @@ void TypingQuizPuzzle::redraw() {
 
 void TypingQuizPuzzle::triggerSceneChange() {
 	if (_reachedTarget) {
-		if (_winScene.sceneID != 9999)
+		if (_winScene.sceneID != kNoScene)
 			NancySceneState.changeScene(_winScene);
 		if (_winFlag != -1)
 			NancySceneState.setEventFlag(_winFlag, g_nancy->_true);


Commit: 031b043a465972330c94dead433aaf908f1a2737
    https://github.com/scummvm/scummvm/commit/031b043a465972330c94dead433aaf908f1a2737
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:23+03:00

Commit Message:
NANCY: NANCY11: More work on the keypad terse variant of OrderingPuzzle

Fixes the name check puzzle before the actual card game puzzle against
the Betty automaton.

A small hack has been added for this puzzle, which modifies the solve
scene override and allows the player to get the needed token given when
winning this puzzle.

Changed paths:
    engines/nancy/action/puzzle/orderingpuzzle.cpp


diff --git a/engines/nancy/action/puzzle/orderingpuzzle.cpp b/engines/nancy/action/puzzle/orderingpuzzle.cpp
index 8e6fca0a82c..21eea76a44f 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/orderingpuzzle.cpp
@@ -305,18 +305,27 @@ void OrderingPuzzle::readData(Common::SeekableReadStream &stream) {
 			}
 		} else if (_puzzleType == kKeypadTerse) {
 			// Terse elements are the same size & placed on a grid (in the source image AND on screen)
-			uint16 columns = stream.readUint16LE();
 
-			// The Nancy 11 multi-stage terse keypad chains scenes: each scene holds one stage and
-			// the grid is preceded by the scene to advance to once this stage's code is entered.
-			// Standalone/final keypads omit it. A grid column count can never exceed maxNumElements,
-			// so a larger leading value is that scene id - it overrides the solve scene's target
-			// (the rest of the solve scene change is reused), then the real column count follows.
-			if (g_nancy->getGameType() >= kGameTypeNancy11 && columns > maxNumElements) {
-				_solveExitScene._sceneChange.sceneID = columns;
-				columns = stream.readUint16LE();
+			// In Nancy 11 the grid block is preceded by the scene to advance to on solving, which
+			// overrides the solve scene's target (the rest of the solve scene change is reused).
+			// 0 and 9999 mean "none", falling back to the solve scene read earlier.
+			if (g_nancy->getGameType() >= kGameTypeNancy11) {
+				uint16 advanceSceneID = stream.readUint16LE();
+
+				// HACK: In Nancy11, in the Betty automaton scene, this is set to scene 2721, but
+				// scene 2720 (the one set initially) is the one that eventually allows the player
+				// to gain the needed token to proceed.
+				// TODO: What is the correct way to handle this?
+				if (g_nancy->getGameType() == kGameTypeNancy11 && advanceSceneID == 2721 &&
+					_solveExitScene._sceneChange.sceneID == 2720)
+					advanceSceneID = 2720;
+
+				if (advanceSceneID != 0 && advanceSceneID != kNoScene) {
+					_solveExitScene._sceneChange.sceneID = advanceSceneID;
+				}
 			}
 
+			uint16 columns = stream.readUint16LE();
 			uint16 rows = stream.readUint16LE();
 
 			uint16 width = stream.readUint16LE();
@@ -486,7 +495,10 @@ void OrderingPuzzle::execute() {
 								}
 							}
 
-							if (_clickedSequence.size() > maxNumPressed) {
+							// Puzzles with a manual check button (e.g. the Nancy 11 Betty automaton keypad)
+							// keep the entered buttons down until the check button is pressed, so they
+							// must not auto-clear the entry here based on its length.
+							if (!_needButtonToCheckSuccess && _clickedSequence.size() > maxNumPressed) {
 								clearAllElements();
 								return;
 							}
@@ -542,19 +554,41 @@ void OrderingPuzzle::execute() {
 			}
 
 			if ((_puzzleType == kKeypad || _puzzleType == kKeypadTerse) && _needButtonToCheckSuccess) {
-				// KeypadPuzzle and KeypadTersePuzzle move to the "success" scene regardless whether the
-				// puzzle was solved or not, provided the check button is pressed. (e.g. the Nancy 11 Betty
-				// automaton keypad, where a red button confirms the entry.)
-				if (_checkButtonPressed) {
-					if (!g_nancy->_sound->isSoundPlaying(_pushDownSound)) {
-						if (solved) {
-							NancySceneState.setEventFlag(_solveExitScene._flag);
+				// The entry is only acted on when the confirm ("red") button is pressed.
+				if (!_checkButtonPressed) {
+					return;
+				}
+
+				if (g_nancy->_sound->isSoundPlaying(_pushDownSound)) {
+					// Wait for the button-press sound to finish first.
+					return;
+				}
+
+				if (g_nancy->getGameType() >= kGameTypeNancy11) {
+					// The confirm button does nothing on an empty entry. A pad that carries a code also
+					// does nothing on a non-matching entry (wrong names or a wrong order), clearing it.
+					// Otherwise it advances.
+					bool codedMismatch = !_correctSequence.empty() && !solved && _itemsStayDown;
+
+					if (_clickedSequence.empty() || codedMismatch) {
+						if (codedMismatch) {
+							clearAllElements();
 						}
-					} else {
+
+						_checkButtonPressed = false;
+						Common::Rect destRect = _checkButtonDest;
+						destRect.translate(-_screenPosition.left, -_screenPosition.top);
+						_drawSurface.fillRect(destRect, _drawSurface.getTransparentColor());
+						_needsRedraw = true;
 						return;
 					}
+
+					NancySceneState.setEventFlag(_solveExitScene._flag);
 				} else {
-					return;
+					// Earlier games advance to the success scene regardless; the flag is set only on a solve.
+					if (solved) {
+						NancySceneState.setEventFlag(_solveExitScene._flag);
+					}
 				}
 			} else {
 				if (solved) {


Commit: db9f3edc3ff280436961d941fe1b35b2328af56d
    https://github.com/scummvm/scummvm/commit/db9f3edc3ff280436961d941fe1b35b2328af56d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:24+03:00

Commit Message:
NANCY: Cleanup comments

Changed paths:
    engines/nancy/action/arfactory.cpp


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 31f0e60d385..41046c6bcc1 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -181,15 +181,13 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new HotMultiframeMultiSceneCursorTypeSceneChange(); // Moved from 24 to 27 in Nancy10
 	case 28:
 		return new InteractiveVideo();	// Moved from 26 to 28 in Nancy10
-	case 29:
-		// Nancy 10+
+	case 29:	// Nancy10
 		return new ControlUIItems();
 	case 30:	// Nancy11
 		return new StopPlayerScrolling();
 	case 31:	// Nancy11
 		return new StartPlayerScrolling();
-	case 32:
-		// Nancy 10+
+	case 32:	// Nancy10
 		return new UIPopupPrepScene();
 	case 45:	// Nancy11 - random-movie variant of PlaySecondaryMovie
 		return new PlaySecondaryMovie(true);
@@ -272,7 +270,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new ModifyListEntry(ModifyListEntry::kDelete);
 	case 73:
 		return new ModifyListEntry(ModifyListEntry::kMark);
-	case 74:	// Nancy 10
+	case 74:	// Nancy10
 		return new FrameTextBox(true);
 	case 75:
 		if (g_nancy->getGameType() <= kGameTypeNancy9)
@@ -367,20 +365,15 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new PopInvViewPriorScene();
 	case 126:
 		return new GoInvViewScene();
-	case 128:
-		// Nancy10+
+	case 128:	// Nancy10
 		return new CellPhonePopCellSceneFromStack();
-	case 129:
-		// Nancy10+
+	case 129:	// Nancy10
 		return new SetCellPhoneBatteryAndSignal();
-	case 130:
-		// Nancy10+
+	case 130:	// Nancy10
 		return new ChangeCellPhoneInfo();
-	case 131:
-		// Nancy10+
+	case 131:	// Nancy10
 		return new AddSearchLink();
-	case 132:
-		// Nancy12+
+	case 132:	// Nancy12
 		return new ResourceUse();
 	case 140:
 		if (g_nancy->getGameType() >= kGameTypeNancy12)


Commit: 6cfb9c87554f5b8f38b0e19f1e27d24de4c1c72f
    https://github.com/scummvm/scummvm/commit/6cfb9c87554f5b8f38b0e19f1e27d24de4c1c72f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-08T05:36:25+03:00

Commit Message:
NANCY: NANCY12: Implement SewingMachinePuzzle

In this puzzle, the player feeds a piece of cloth through a sewing
machine needle

Changed paths:
  A engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
  A engines/nancy/action/puzzle/sewingmachinepuzzle.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 41046c6bcc1..785b0b2b59b 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -69,6 +69,7 @@
 #include "engines/nancy/action/puzzle/rotatinglockpuzzle.h"
 #include "engines/nancy/action/puzzle/safedialpuzzle.h"
 #include "engines/nancy/action/puzzle/setplayerclock.h"
+#include "engines/nancy/action/puzzle/sewingmachinepuzzle.h"
 #include "engines/nancy/action/puzzle/sliderpuzzle.h"
 #include "engines/nancy/action/puzzle/sortpuzzle.h"
 #include "engines/nancy/action/puzzle/soundequalizerpuzzle.h"
@@ -456,8 +457,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new PlaySoundEventFlagTerse();
 	// -- Nancy 12 new puzzles/action records --
 	case 162:
-		// TODO: Nancy12 - sewing machine puzzle.
-		return nullptr;
+		return new SewingMachinePuzzle();
 	case 163:
 		return new MirrorLightPuzzle();
 	case 164:
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
new file mode 100644
index 00000000000..659fd93badd
--- /dev/null
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
@@ -0,0 +1,214 @@
+/* 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/action/puzzle/sewingmachinepuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+void SewingMachinePuzzle::readData(Common::SeekableReadStream &stream) {
+	// 87-byte PuzzleBase header blob.
+	readFilename(stream, _imageName);	// blob 0x00
+	readRect(stream, _rects[0]);		// blob 0x21
+	readRect(stream, _rects[1]);		// blob 0x31
+	_directionVector.x = stream.readSint32LE();	// blob 0x41
+	_directionVector.y = stream.readSint32LE();	// blob 0x45
+	_extentVector.x = stream.readSint32LE();	// blob 0x49
+	_extentVector.y = stream.readSint32LE();	// blob 0x4d
+	for (int i = 0; i < 3; ++i) {
+		_params[i] = stream.readSint16LE();		// blob 0x51 / 0x53 / 0x55
+	}
+
+	_soundBlock.readData(stream);
+
+	readActionZoneArray(stream, _zones);
+}
+
+void SewingMachinePuzzle::classifyZones() {
+	for (uint i = 0; i < _zones.size(); ++i) {
+		const ActionZone &z = _zones[i];
+		switch (z.type) {
+		case 0x0b:	// the needle line: touching it plays a stitch cue and sets a flag
+			_collisionZone = i;
+			break;
+		case 0x0c:	// bottom trigger: its SpecialEffect carries the win scene + fade
+			_triggerZones.push_back(i);
+			if (z.specialEffectId >= 1000 && _winScene.sceneID == kNoScene) {
+				_winScene.sceneID = z.specialEffectId;
+				_winEventFlag = z.val49;
+				if (z.hasSpecialEffect) {
+					_winHasFade = true;
+					_winFadeType = z.seType;
+					_winFadeTotalTime = z.seTotalTime;
+					_winFadeToBlackTime = z.seFadeToBlackTime;
+					_winFadeRect = z.seRect;
+				}
+			}
+			break;
+		case 0x14:	// play-area boundary
+			_boundaryZone = i;
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+void SewingMachinePuzzle::playSoundBlock(const RandomSoundBlock &block) {
+	if (block.names.empty()) {
+		return;
+	}
+
+	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;
+	}
+
+	SoundDescription 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);
+}
+
+void SewingMachinePuzzle::drawCloth() {
+	// The cloth is a tall strip; the fixed needle (drawn by the scene) sits over it.
+	// The scene shows through wherever the strip does not cover.
+	_drawSurface.clear(g_nancy->_graphics->getTransColor());
+	_drawSurface.blitFrom(_image, _clothPos);
+	_needsRedraw = true;
+}
+
+void SewingMachinePuzzle::feedCloth(const Common::Point &delta) {
+	Common::Point newPos(CLIP<int>(_clothPos.x + delta.x, -_maxSteer, _maxSteer),
+		CLIP<int>(_clothPos.y + delta.y, -_maxFeed, 0));
+
+	int moved = ABS(newPos.x - _clothPos.x) + ABS(newPos.y - _clothPos.y);
+	_clothPos = newPos;
+
+	if (moved == 0) {
+		// A pause resets the stitch stroke, matching the original's hysteresis.
+		_strokeDistance = 0.0;
+		return;
+	}
+
+	_strokeDistance += moved;
+	drawCloth();
+
+	// A stitch cue (needle sound) fires each time the cloth advances past the threshold.
+	if (_strokeDistance >= _params[1] && _collisionZone >= 0) {
+		_strokeDistance = 0.0;
+		playSoundBlock(_zones[_collisionZone]._sound);
+	}
+
+	// The seam is finished once the cloth has been fed all the way through.
+	if (_maxFeed > 0 && _clothPos.y <= -_maxFeed && !_solved) {
+		_solved = true;
+		_state = kActionTrigger;
+	}
+}
+
+void SewingMachinePuzzle::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());
+
+	// Feed scrolls the whole strip past the viewport; steer lets the cloth wiggle
+	// horizontally to follow the seam under the needle (range is a tunable guess).
+	_maxFeed = MAX(0, _image.h - _drawSurface.h);
+	_maxSteer = 100;
+}
+
+void SewingMachinePuzzle::execute() {
+	switch (_state) {
+	case kBegin:
+		init();
+		registerGraphics();
+		classifyZones();
+		playSoundBlock(_soundBlock);	// start the sewing-machine ambience
+		drawCloth();
+		_state = kRun;
+		break;
+	case kRun:
+		break;
+	case kActionTrigger:
+		// Seam finished: raise the win flag, then cross-dissolve to the scene the
+		// trigger zone's SpecialEffect points at.
+		if (_winEventFlag != -1) {
+			NancySceneState.setEventFlag(_winEventFlag, g_nancy->_true);
+		}
+		if (_winScene.sceneID >= 1000 && _winScene.sceneID != kNoScene) {
+			if (_winHasFade) {
+				NancySceneState.specialEffect(_winFadeType, _winFadeTotalTime, _winFadeToBlackTime, _winFadeRect);
+			}
+			NancySceneState.changeScene(_winScene);
+		}
+		finishExecution();
+		break;
+	}
+}
+
+void SewingMachinePuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun) {
+		return;
+	}
+
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+	if (input.input & NancyInput::kLeftMouseButtonDown) {
+		_dragging = true;
+		_lastDragPos = input.mousePos;
+	}
+
+	if (_dragging && (input.input & NancyInput::kLeftMouseButtonHeld)) {
+		feedCloth(input.mousePos - _lastDragPos);
+		_lastDragPos = input.mousePos;
+	}
+
+	if (input.input & NancyInput::kLeftMouseButtonUp) {
+		_dragging = false;
+		_strokeDistance = 0.0;
+	}
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.h b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
new file mode 100644
index 00000000000..3e82d498087
--- /dev/null
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
@@ -0,0 +1,111 @@
+/* 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_SEWINGMACHINEPUZZLE_H
+#define NANCY_ACTION_SEWINGMACHINEPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/action/navigationrecords.h"
+#include "engines/nancy/action/actionzone.h"
+
+namespace Nancy {
+namespace Action {
+
+// Nancy12 sewing-machine puzzle (action record 162): the player feeds a piece of
+// cloth through a sewing machine needle.
+//
+// The cloth is a tall strip that the player drags to feed under the fixed needle;
+// stitch/needle sounds play as it slides, and feeding it all the way through
+// cross-dissolves to the trigger zone's win scene.
+// TODO: the horizontal steer range and the drag-to-feed scale are tunable guesses.
+class SewingMachinePuzzle : public RenderActionRecord {
+public:
+	SewingMachinePuzzle() : RenderActionRecord(7) {}
+	virtual ~SewingMachinePuzzle() {}
+
+	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 "SewingMachinePuzzle"; }
+
+	// Sorts the ActionZone array into its gameplay roles.
+	void classifyZones();
+	// Plays one entry of a random-sound block (needle/stitch cues).
+	void playSoundBlock(const RandomSoundBlock &block);
+	// Draws the sewing view (the "BED_Sewing_OVL" overlay).
+	void drawCloth();
+	// Advances the sewing by a drag delta, firing stitch/needle cues and marking
+	// the puzzle solved once enough cloth has been fed through.
+	void feedCloth(const Common::Point &delta);
+
+	// Background image ("BED_Sewing_OVL").
+	Common::Path _imageName;
+
+	// 87-byte PuzzleBase header blob: image name + two cloth regions + a direction
+	// vector + an extent vector (its magnitude = the seam length) + three params
+	// (_params[1] = stitch distance threshold, _params[2] = its hysteresis).
+	Common::Rect _rects[2];
+	Common::Point _directionVector;
+	Common::Point _extentVector;
+	int16 _params[3] = {};
+
+	RandomSoundBlock _soundBlock;
+	Common::Array<ActionZone> _zones;
+
+	// ActionZone roles (indices into _zones; -1 == none).
+	int _collisionZone = -1;			// type 0x0b: the needle line + its sounds/flag
+	int _boundaryZone = -1;				// type 0x14
+	Common::Array<uint> _triggerZones;	// type 0x0c: completion triggers
+
+	// Win transition from a trigger zone's SpecialEffect: its leading id is the win
+	// scene, the effect is the fade played over the change.
+	SceneChangeDescription _winScene;
+	int16 _winEventFlag = -1;
+	bool _winHasFade = false;
+	byte _winFadeType = 0;
+	uint16 _winFadeTotalTime = 0;
+	uint16 _winFadeToBlackTime = 0;
+	Common::Rect _winFadeRect;
+
+	// Runtime state. The cloth is a tall strip drawn at (_clothPos); dragging scrolls
+	// it under the fixed needle (feed = vertical, steer = horizontal).
+	Common::Point _clothPos;			// top-left of the cloth on screen
+	int _maxFeed = 0;					// vertical scroll range (image height - viewport)
+	int _maxSteer = 0;					// horizontal steer range each way
+	double _strokeDistance = 0.0;		// movement accumulated toward the next stitch
+	bool _dragging = false;
+	Common::Point _lastDragPos;
+	bool _solved = false;
+
+	Graphics::ManagedSurface _image;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_SEWINGMACHINEPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index fc4410e58af..451fc52218d 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -53,6 +53,7 @@ MODULE_OBJS = \
   action/puzzle/rotatinglockpuzzle.o \
   action/puzzle/safedialpuzzle.o \
   action/puzzle/setplayerclock.o \
+  action/puzzle/sewingmachinepuzzle.o \
   action/puzzle/sliderpuzzle.o \
   action/puzzle/sortpuzzle.o \
   action/puzzle/soundequalizerpuzzle.o \




More information about the Scummvm-git-logs mailing list