[Scummvm-git-logs] scummvm master -> a29e4f455d449e572cac58a50560038e32b062ac

bluegr noreply at scummvm.org
Tue Mar 17 20:39: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:
a29e4f455d Implement cuttingpuzzle - Art Studio Lathe Puzzle in Nancy 8


Commit: a29e4f455d449e572cac58a50560038e32b062ac
    https://github.com/scummvm/scummvm/commit/a29e4f455d449e572cac58a50560038e32b062ac
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-17T22:39:20+02:00

Commit Message:
Implement cuttingpuzzle - Art Studio Lathe Puzzle in Nancy 8

Changed paths:
    engines/nancy/action/puzzle/cuttingpuzzle.cpp
    engines/nancy/action/puzzle/cuttingpuzzle.h


diff --git a/engines/nancy/action/puzzle/cuttingpuzzle.cpp b/engines/nancy/action/puzzle/cuttingpuzzle.cpp
index b3d7c35cd3c..ddf0162a3c3 100644
--- a/engines/nancy/action/puzzle/cuttingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/cuttingpuzzle.cpp
@@ -32,34 +32,388 @@
 namespace Nancy {
 namespace Action {
 
+void CuttingPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageName1);      // +0x000 (33 bytes)
+	readFilename(stream, _imageName2);      // +0x021 (33 bytes)
+
+	_numGrooves = stream.readUint16LE();    // +0x042
+	stream.skip(2);						  // +0x044 (unknown)
+	stream.skip(2);                         // +0x046 (unknown)
+
+	// Destination rects (viewport-local screen positions)
+	readRect(stream, _noAnimDest);          // +0x048
+	readRect(stream, _leverDest);           // +0x058
+	readRect(stream, _switchDest);          // +0x068
+	readRectArray(stream, _grooveDest, 8);  // +0x078..0x0f7
+	readRectArray(stream, _markerDest, 8);  // +0x0f8..0x177
+	readRectArray(stream, _bladeDest, 8);   // +0x178..0x1f7
+	readRect(stream, _animFrameDest);       // +0x1f8
+
+	// Source rects (from sprite sheet)
+	readRect(stream, _noAnimSrc);                    // +0x208
+	readRectArray(stream, _leverSrc, 3);             // +0x218..0x247  depths 1..3
+	readRect(stream, _switchOnSrc);                  // +0x248
+	readRectArray(stream, _grooveTypeSrc, 6);        // +0x258..0x2b7  types 0..5
+	readRectArray(stream, _bladeSrc, 4);             // +0x2b8..0x2f7  depths 0..3
+	readRect(stream, _baseSrc);                      // +0x2f8
+
+	_numAnimFrames = stream.readUint16LE();          // +0x308
+	readRectArray(stream, _animSrc, 12);             // +0x30a..0x3c9  (always 12 slots)
+	_frameDelayMs = stream.readUint16LE();           // +0x3ca
+
+	// Correct groove depths (target answer).  Always 8 slots in the data file.
+	_correctGrooves.resize(8, 0);
+	for (int i = 0; i < 8; ++i)
+		_correctGrooves[i] = stream.readSint16LE();  // +0x3cc..0x3db
+
+	_latheSound.readNormal(stream);                  // +0x3dc (49 bytes)
+	_clickSound.readNormal(stream);                  // +0x40d (49 bytes)
+	_clickSound2.readNormal(stream);                 // +0x43e (49 bytes)
+	_clickSound3.readNormal(stream);                 // +0x46f (49 bytes)
+	_cutSound.readNormal(stream);                    // +0x4a0 (49 bytes)
+
+	_puzzleSolvedScene.readData(stream);                 // +0x4d1 (25 bytes)
+	_doneSoundDelaySecs = stream.readUint16LE();     // +0x4ea
+
+	_doneSound.readNormal(stream);                   // +0x4ec (49 bytes)
+
+	_itemCheckByte = stream.readByte();              // +0x51d
+	_itemID        = stream.readSint16LE();          // +0x51e
+
+	// Solve scene: plain SceneChangeDescription (20 bytes) + 2-byte shouldStopRendering skip
+	_missingGogglesScene.readData(stream);                    // +0x520 (20 bytes)
+	stream.skip(2);                                  // +0x534 skip
+
+	_cancelScene.readData(stream);                   // +0x536 (25 bytes)
+}
+
 void CuttingPuzzle::init() {
-	// TODO
+	Common::Rect screenBounds = NancySceneState.getViewport().getBounds();
+	_drawSurface.create(screenBounds.width(), screenBounds.height(),
+	                    g_nancy->_graphics->getInputPixelFormat());
+	_drawSurface.clear(g_nancy->_graphics->getTransColor());
+	setTransparent(true);
+	setVisible(true);
+	moveTo(screenBounds);
+
+	g_nancy->_resource->loadImage(_imageName1, _image1);
+	g_nancy->_resource->loadImage(_imageName2, _image2);
+	_image1.setTransparentColor(_drawSurface.getTransparentColor());
+	_image2.setTransparentColor(_drawSurface.getTransparentColor());
+
+	_grooveDepths.resize(_numGrooves, 0);
+
+	redrawSurface();
+}
+
+// Returns the groove-type sprite index (0..5) for groove slot i, based on depth and neighbors.
+// Type 0: shallow (depth 1)
+// Type 1: medium  (depth 2)
+// Type 2: deep single   (depth 3, no deep neighbors)
+// Type 3: deep right-edge (depth 3, deep neighbor on left)
+// Type 4: deep left-edge  (depth 3, deep neighbor on right)
+// Type 5: deep middle     (depth 3, deep neighbors on both sides)
+int CuttingPuzzle::grooveTypeForIndex(int i) const {
+	int depth = _grooveDepths[i];
+	if (depth == 1)
+		return 0;
+	if (depth == 2)
+		return 1;
+	// depth == 3
+	bool leftDeep  = (i > 0)                    && (_grooveDepths[i - 1] == 3);
+	bool rightDeep = (i < (int)_numGrooves - 1) && (_grooveDepths[i + 1] == 3);
+	if (i == 0)
+		return rightDeep ? 4 : 2;
+	if (i == (int)_numGrooves - 1)
+		return leftDeep ? 3 : 2;
+	if (leftDeep && rightDeep)
+		return 5;
+	if (leftDeep)
+		return 3;
+	if (rightDeep)
+		return 4;
+	return 2;
+}
+
+void CuttingPuzzle::redrawSurface() {
+	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	// image1 = main sprite sheet (grooves, lever, blade, base, switch, no-anim overlay)
+	// image2 = animation frames only
+
+	if (!_latheRunning || _subState != kIdle) {
+		// Static overlay (lathe off)
+		_drawSurface.blitFrom(_image1, _noAnimSrc, _noAnimDest);
+		_animRestore = false;
+	} else {
+		// Lathe running: show the on-switch sprite and the current animation frame.
+		_drawSurface.blitFrom(_image1, _switchOnSrc, _switchDest);
+		if (_numAnimFrames > 0 && _animFrame < _animSrc.size())
+			_drawSurface.blitFrom(_image2, _animSrc[_animFrame], _animFrameDest);
+	}
+
+	// Draw the blade/marker base at the current position (opaque).
+	if (_currentMarkerPos < _markerDest.size())
+		_drawSurface.blitFrom(_image1, _baseSrc, _markerDest[_currentMarkerPos]);
+
+	// Draw the lever sprite (if depth > 0).
+	if (_currentLeverDepth > 0 && _currentLeverDepth <= (uint16)_leverSrc.size())
+		_drawSurface.blitFrom(_image1, _leverSrc[_currentLeverDepth - 1], _leverDest);
+
+	// Draw each groove that has been cut.
+	for (uint i = 0; i < _numGrooves; ++i) {
+		if (_grooveDepths[i] > 0) {
+			int type = grooveTypeForIndex(i);
+			if (type < (int)_grooveTypeSrc.size() && i < _grooveDest.size())
+				_drawSurface.blitFrom(_image1, _grooveTypeSrc[type], _grooveDest[i]);
+		}
+	}
+
+	// Draw the blade assembly at the current position (opaque).
+	if (_currentMarkerPos < _bladeDest.size() && _currentLeverDepth < _bladeSrc.size())
+		_drawSurface.blitFrom(_image1, _bladeSrc[_currentLeverDepth], _bladeDest[_currentMarkerPos]);
+
+	_needsRedraw = true;
 }
 
 void CuttingPuzzle::execute() {
-	if (_state == kBegin) {
+	switch (_state) {
+	case kBegin:
 		init();
 		registerGraphics();
+
+		g_nancy->_sound->loadSound(_latheSound);
+		g_nancy->_sound->loadSound(_clickSound);
+		g_nancy->_sound->loadSound(_clickSound2);
+		g_nancy->_sound->loadSound(_clickSound3);
+		g_nancy->_sound->loadSound(_cutSound);
+
 		_state = kRun;
+		break;
+
+	case kRun: {
+		switch (_subState) {
+		case kIdle:
+			if (!_latheRunning) {
+				// Check if the current groove configuration is the correct answer.
+				bool allMatch = true;
+				for (uint i = 0; i < _numGrooves; ++i) {
+					if (_grooveDepths[i] != _correctGrooves[i]) {
+						allMatch = false;
+						break;
+					}
+				}
+				if (allMatch) {
+					_solved = true;
+					_timerDeadline = g_system->getMillis() + (uint32)_doneSoundDelaySecs * 1000;
+					_subState = kWaitTimer;
+					break;
+				}
+			} else {
+				// Advance animation frame.
+				if (_animFrame + 1 < _numAnimFrames) {
+					++_animFrame;
+					redrawSurface();
+				} else {
+					// Completed one full animation loop → one macro-cycle.
+					_animFrame = 0;
+
+					// Play the lathe loop sound if it's not already playing.
+					if (!g_nancy->_sound->isSoundPlaying(_latheSound))
+						g_nancy->_sound->playSound(_latheSound);
+
+					if (_macroCycleCount == 14) {
+						// The lathe run is complete: record the groove depth.
+						_latheRunning = false;
+						_animRestore  = true;
+
+						// The groove gets the maximum of the previous depth and the
+						// current lever setting.
+						if (_currentMarkerPos < _numGrooves) {
+							if (_grooveDepths[_currentMarkerPos] < (int16)_currentLeverDepth)
+								_grooveDepths[_currentMarkerPos] = (int16)_currentLeverDepth;
+						}
+
+						// Check whether the item requirement is satisfied.
+						if (_currentMarkerPos < _numGrooves && _grooveDepths[_currentMarkerPos] >= 1) {
+							if (_itemCheckByte != 0) {
+								_gogglesMissing = NancySceneState.hasItem(_itemID) == g_nancy->_false;
+							}
+						}
+
+						if (!_gogglesMissing) {
+							// Lever snaps back to 0 after the run.
+							_currentLeverDepth = 0;
+							_leverReset        = true;
+							_macroCycleCount   = 0;
+						}
+					} else {
+						// During cycles 2..11, play the cutting sound (if lever is down).
+						// During cycles 12..13, stop the cutting sound.
+						if (_macroCycleCount < 12) {
+							if (_macroCycleCount > 1 && _currentLeverDepth > 0) {
+								if (!g_nancy->_sound->isSoundPlaying(_cutSound))
+									g_nancy->_sound->playSound(_cutSound);
+							}
+						} else {
+							if (_currentLeverDepth > 0)
+								g_nancy->_sound->stopSound(_cutSound);
+						}
+						++_macroCycleCount;
+					}
+
+					redrawSurface();
+				}
+			}
+
+			// Set the frame-advance timer and move to kWaitTimer.
+			_timerDeadline = g_system->getMillis() + _frameDelayMs;
+			_subState = kWaitTimer;
+			break;
+
+		case kLatheFinished:
+			if (_solved) {
+				// Load and play the completion sound, then wait for it to finish.
+				g_nancy->_sound->loadSound(_doneSound);
+				g_nancy->_sound->playSound(_doneSound);
+				_subState = kWaitDoneSound;
+			} else {
+				// Not solved: finish this AR (will process outcome in kActionTrigger).
+				_state = kActionTrigger;
+			}
+			break;
+
+		case kWaitTimer:
+			if (g_system->getMillis() >= _timerDeadline) {
+				if (_solved || _gogglesMissing) {
+					_subState = kLatheFinished;
+				} else {
+					_subState = kIdle;
+				}
+			}
+			break;
+
+		case kWaitDoneSound:
+			if (!g_nancy->_sound->isSoundPlaying(_doneSound)) {
+				g_nancy->_sound->stopSound(_doneSound);
+				_state = kActionTrigger;
+			}
+			break;
+		}
+		break;
 	}
 
-	// TODO
-	// Stub - move to the winning screen
-	warning("STUB - Nancy 8 Art Studio Lathe Puzzle");
-	NancySceneState.setEventFlag(341, g_nancy->_true); // Set dowel puzzle flag to solved
-	SceneChangeDescription scene;
-	scene.sceneID = 3854;
-	NancySceneState.resetStateToInit();
-	NancySceneState.changeScene(scene);
-}
+	case kActionTrigger:
+		// Stop all sounds.
+		g_nancy->_sound->stopSound(_latheSound);
+		g_nancy->_sound->stopSound(_cutSound);
+		g_nancy->_sound->stopSound(_clickSound);
+		g_nancy->_sound->stopSound(_clickSound2);
+		g_nancy->_sound->stopSound(_clickSound3);
 
-void CuttingPuzzle::readData(Common::SeekableReadStream &stream) {
-	// TODO
-	stream.skip(stream.size() - stream.pos());
+		if (_cancelled) {
+			// Player explicitly cancelled: go to the cancel scene and possibly set
+			// a flag if any grooves were cut.
+			bool anyGroove = false;
+			for (uint i = 0; i < _numGrooves; ++i) {
+				if (_grooveDepths[i] != 0) {
+					anyGroove = true;
+					break;
+				}
+			}
+			if (anyGroove)
+				NancySceneState.setEventFlag(_cancelScene._flag);
+			if (_cancelScene._sceneChange.sceneID != kNoScene)
+				NancySceneState.changeScene(_cancelScene._sceneChange);
+		} else if (_solved) {
+			_puzzleSolvedScene.execute();
+		} else if (_gogglesMissing) {
+			NancySceneState.changeScene(_missingGogglesScene);
+		}
+		// If none of the above conditions apply, the AR finishes without a
+		// scene change (lathe ran without producing the correct answer and no
+		// item was needed).
+
+		finishExecution();
+		break;
+	}
 }
 
 void CuttingPuzzle::handleInput(NancyInput &input) {
-	// TODO
+	if (_state != kRun || _latheRunning)
+		return;
+
+	// Right-click cancels the puzzle (no dedicated exit hotspot in the data).
+	if (input.input & NancyInput::kRightMouseButtonUp) {
+		_cancelled = true;
+		_state = kActionTrigger;
+		return;
+	}
+
+	// Convert mouse position to viewport-local coordinates.
+	Common::Point localMouse = input.mousePos;
+	Common::Rect vpPos = NancySceneState.getViewport().getScreenPosition();
+	localMouse -= Common::Point(vpPos.left, vpPos.top);
+
+	// Lever: left half of the rect rotates the knob left (decrement depth),
+	// right half rotates right (increment depth).
+	if (_leverDest.contains(localMouse)) {
+		int midX = (_leverDest.left + _leverDest.right) / 2;
+		bool rotateLeft = localMouse.x < midX;
+
+		g_nancy->_cursor->setCursorType(rotateLeft ? CursorManager::kRotateCCW
+		                                            : CursorManager::kRotateCW);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			if (rotateLeft)
+				_currentLeverDepth = (_currentLeverDepth == 0) ? 3 : _currentLeverDepth - 1;
+			else
+				_currentLeverDepth = (_currentLeverDepth + 1) % 4;
+			g_nancy->_sound->playSound(_clickSound);
+			redrawSurface();
+		}
+		return;
+	}
+
+	// Start/stop switch: start the lathe (re-clicking while stopped is a no-op
+	// since the lathe auto-stops after 14 cycles).
+	if (_switchDest.contains(localMouse)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_latheRunning    = true;
+			_macroCycleCount = 0;
+			_animFrame       = 0;
+			g_nancy->_sound->playSound(_clickSound2);
+			redrawSurface();
+		}
+		return;
+	}
+
+	// Blade-position needle: clicking the left half moves the blade left,
+	// clicking the right half moves it right.
+	if (_currentMarkerPos < _markerDest.size() &&
+	        _markerDest[_currentMarkerPos].contains(localMouse)) {
+		int midX = (_markerDest[_currentMarkerPos].left + _markerDest[_currentMarkerPos].right) / 2;
+		bool goLeft = localMouse.x < midX;
+
+		if (goLeft && _currentMarkerPos > 0) {
+			g_nancy->_cursor->setCursorType(CursorManager::kMoveLeft);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				--_currentMarkerPos;
+				g_nancy->_sound->playSound(_clickSound3);
+				redrawSurface();
+			}
+		} else if (!goLeft && _currentMarkerPos + 1 < _numGrooves) {
+			g_nancy->_cursor->setCursorType(CursorManager::kMoveRight);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				++_currentMarkerPos;
+				g_nancy->_sound->playSound(_clickSound3);
+				redrawSurface();
+			}
+		}
+		return;
+	}
 }
 
 } // End of namespace Action
diff --git a/engines/nancy/action/puzzle/cuttingpuzzle.h b/engines/nancy/action/puzzle/cuttingpuzzle.h
index 530f25171a8..116e565eb00 100644
--- a/engines/nancy/action/puzzle/cuttingpuzzle.h
+++ b/engines/nancy/action/puzzle/cuttingpuzzle.h
@@ -23,12 +23,24 @@
 #define NANCY_ACTION_CUTTINGPUZZLE_H
 
 #include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
 
 namespace Nancy {
 namespace Action {
 
-// Art Studio Lathe Puzzle in Nancy 8
-
+// Art Studio Lathe Puzzle in Nancy 8.
+//
+// The player uses a lathe to cut grooves into a wooden dowel. The UI has:
+//   - A blade that can be moved to different horizontal positions (marker rects)
+//   - A lever that sets the depth of the cut (0=none, 1=shallow, 2=medium, 3=deep)
+//   - A start/stop switch that runs the lathe
+//
+// Running the lathe plays an animation over 14 macro-cycles. When the animation
+// completes, the groove depth at the current blade position is recorded as the
+// maximum of the previous depth and the current lever setting.
+//
+// The puzzle is solved when grooveDepths[i] == correctGrooves[i] for all i.
+//
 class CuttingPuzzle : public RenderActionRecord {
 public:
 	CuttingPuzzle() : RenderActionRecord(7) {}
@@ -41,8 +53,87 @@ public:
 	void handleInput(NancyInput &input) override;
 
 protected:
+	enum SubState {
+		kIdle           = 0, // waiting for player input, or checking solution
+		kLatheFinished  = 1, // one lathe run ended; determine outcome
+		kWaitTimer      = 2, // waiting for frame-advance delay
+		kWaitDoneSound  = 3  // waiting for the completion sound to finish
+	};
+
 	Common::String getRecordTypeName() const override { return "CuttingPuzzle"; }
 	bool isViewportRelative() const override { return true; }
+
+	void redrawSurface();
+	int grooveTypeForIndex(int i) const;
+
+	// ---- data-file fields ----
+
+	Common::Path _imageName1;   // data+0x000 sprite sheet (background/restore image)
+	Common::Path _imageName2; // data+0x021 sprite sheet (interactive overlays)
+
+	uint16 _numGrooves  = 0;      // data+0x042  how many grooves to cut
+	// data+0x044 skipped (2 bytes)
+	// data+0x046 skipped (2 bytes)
+
+	// Destination rects (viewport-local screen positions to draw to)
+	Common::Rect _noAnimDest;                    // data+0x048
+	Common::Rect _leverDest;                     // data+0x058
+	Common::Rect _switchDest;                    // data+0x068
+	Common::Array<Common::Rect> _grooveDest;     // data+0x078 (8 rects, one per groove slot)
+	Common::Array<Common::Rect> _markerDest;     // data+0x0f8 (8 rects, blade position markers)
+	Common::Array<Common::Rect> _bladeDest;      // data+0x178 (8 rects, blade assembly positions)
+	Common::Rect _animFrameDest;                 // data+0x1f8
+
+	// Source rects (within the sprite sheet)
+	Common::Rect _noAnimSrc;                     // data+0x208  static overlay when lathe is off
+	Common::Array<Common::Rect> _leverSrc;       // data+0x218  3 rects for lever depths 1..3
+	Common::Rect _switchOnSrc;                   // data+0x248  switch sprite when lathe is on
+	Common::Array<Common::Rect> _grooveTypeSrc;  // data+0x258  6 rects for groove visual types 0..5
+	Common::Array<Common::Rect> _bladeSrc;       // data+0x2b8  4 rects for blade at lever depths 0..3
+	Common::Rect _baseSrc;                       // data+0x2f8  base/knob sprite (opaque)
+
+	uint16 _numAnimFrames = 0;                   // data+0x308
+	Common::Array<Common::Rect> _animSrc;        // data+0x30a  12 animation frame rects
+	uint16 _frameDelayMs  = 0;                   // data+0x3ca  delay between animation frames (ms)
+
+	Common::Array<int16> _correctGrooves;        // data+0x3cc  8 target depths (one per groove slot)
+
+	SoundDescription _latheSound;  // data+0x3dc  looping lathe running sound
+	SoundDescription _clickSound;  // data+0x40d  click sound (lever / UI)
+	SoundDescription _clickSound2; // data+0x43e  click sound 2
+	SoundDescription _clickSound3; // data+0x46f  click sound 3
+	SoundDescription _cutSound;    // data+0x4a0  groove-cutting sound
+
+	SceneChangeWithFlag _puzzleSolvedScene;    // data+0x4d1 (25 bytes)
+	uint16 _doneSoundDelaySecs = 0;        // data+0x4ea  wait before playing done sound (seconds)
+	SoundDescription _doneSound;           // data+0x4ec
+
+	byte  _itemCheckByte = 0;              // data+0x51d  0=no check, nonzero=require inventory item
+	int16 _itemID        = -1;             // data+0x51e
+
+	SceneChangeDescription _missingGogglesScene;    // data+0x520  (20 bytes + 2-byte skip = 22 bytes total)
+	SceneChangeWithFlag    _cancelScene;   // data+0x536  (25 bytes)
+
+	// ---- runtime state ----
+
+	Graphics::ManagedSurface _image1;
+	Graphics::ManagedSurface _image2;
+
+	uint16 _currentMarkerPos   = 0; // blade position index (0..numGrooves-1)
+	uint16 _currentLeverDepth  = 0; // lever depth (0=none, 1=shallow, 2=med, 3=deep)
+	Common::Array<int16> _grooveDepths;  // recorded depth at each groove position
+
+	bool _latheRunning  = false;
+	bool _animRestore   = false; // true when lathe just stopped: restore animation area on next draw
+	bool _leverReset    = false; // true when lever just snapped back to 0: show reset animation once
+	uint16 _animFrame   = 0;     // current animation frame index (0..numAnimFrames-1)
+
+	SubState _subState  = kIdle;
+	bool _cancelled     = false;
+	bool _solved        = false;
+	bool _gogglesMissing      = false;
+	uint32 _timerDeadline = 0;
+	uint16 _macroCycleCount = 0; // counts lathe macro-cycles (0..14); at 14 the run ends
 };
 
 } // End of namespace Action




More information about the Scummvm-git-logs mailing list