[Scummvm-git-logs] scummvm master -> 82ba094179be728991416bde5ab21475e8483d76

bluegr noreply at scummvm.org
Fri Mar 13 07:11:00 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:
82ba094179 NANCY: Implement the Nancy 8 Squid Toss (angletoss) puzzle


Commit: 82ba094179be728991416bde5ab21475e8483d76
    https://github.com/scummvm/scummvm/commit/82ba094179be728991416bde5ab21475e8483d76
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-13T09:10:04+02:00

Commit Message:
NANCY: Implement the Nancy 8 Squid Toss (angletoss) puzzle

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


diff --git a/engines/nancy/action/puzzle/angletosspuzzle.cpp b/engines/nancy/action/puzzle/angletosspuzzle.cpp
index a71e75d5f4b..c22d17188d2 100644
--- a/engines/nancy/action/puzzle/angletosspuzzle.cpp
+++ b/engines/nancy/action/puzzle/angletosspuzzle.cpp
@@ -40,59 +40,213 @@ void AngleTossPuzzle::init() {
 	setVisible(true);
 	moveTo(screenBounds);
 
-	// TODO
+	g_nancy->_resource->loadImage(_imageName, _image);
+	_image.setTransparentColor(_drawSurface.getTransparentColor());
+
+	// Draw the initial angle and power indicators.
+	// The throw button sprite is NOT drawn here — the static background already shows the
+	// idle button state. The sprite (data+0x4d) is the launched/pressed overlay, drawn
+	// only when LAUNCH is clicked (mirroring the DAT_0059bfbd conditional in FUN_0044b1fa).
+	_drawSurface.blitFrom(_image, _angleSprites[_curAngle], _angleDisplay);
+	_drawSurface.blitFrom(_image, _powerSprites[_curPower], _powerDisplay);
+}
+
+void AngleTossPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageName);
+
+	// data+0x21..0x2c: 6 × uint16.
+	// _initialPower/_initialAngle: starting player position (copied to object+0x24/0x26 in original).
+	// _numPowers/_numAngles: UI control bounds.
+	// _targetPower/_targetAngle: the correct answer for this round (compared in FUN_0044a6be).
+	_initialPower = stream.readUint16LE();
+	_initialAngle = stream.readUint16LE();
+	_numPowers    = stream.readUint16LE();
+	_numAngles    = stream.readUint16LE();
+	_targetPower  = stream.readUint16LE();
+	_targetAngle  = stream.readUint16LE();
+
+	// 22 rects — see header for full mapping.
+	readRect(stream, _throwHotspot);					// Rect  0 — data+0x2d
+	readRect(stream, _throwDisplay);					// Rect  1 — data+0x3d
+	readRect(stream, _throwSprite);					// Rect  2 — data+0x4d
+	readRect(stream, _aimLeftHotspot);				// Rect  3 — data+0x5d
+	readRect(stream, _aimRightHotspot);				// Rect  4 — data+0x6d
+	readRect(stream, _angleDisplay);					// Rect  5 — data+0x7d
+	readRectArray(stream, _angleSprites, 5);		// Rects 6-10 — data+0x8d
+	readRect(stream, _powerDisplay);					// Rect 11 — data+0xdd
+	readRectArray(stream, _powerHotspots, 5);		// Rects 12-16 — data+0xed
+	readRectArray(stream, _powerSprites, 5);		// Rects 17-21 — data+0x13d
+
+	_powerSound.readNormal(stream);		// data+0x18d
+	_squeakSound.readNormal(stream);	// data+0x1be
+	_chainSound.readNormal(stream);		// data+0x1ef
+
+	_throwSquidScene.readData(stream);	// data+0x220, ends at data+0x238
+
+	// HACK: We've skipped some of the flag data at this point,
+	// so go back to read them correctly
+	_throwSquidScene._flag.label = kEvNoEvent;
+	_throwSquidScene._flag.flag = 0;
+	stream.seek(-3, SEEK_CUR);
+
+	_powerTooStrongFlag = stream.readSint16LE();
+	_powerTooWeakFlag = stream.readSint16LE();
+	_angleTooLeftFlag = stream.readSint16LE();
+	_angleTooRightFlag = stream.readSint16LE();
+	_winFlag = stream.readSint16LE();
+
+	_exitScene.readData(stream);		// data+0x240
+	readRect(stream, _exitHotspot);		// data+0x259
 }
 
 void AngleTossPuzzle::execute() {
-	if (_state == kBegin) {
+	switch (_state) {
+	case kBegin:
+		// Restore the player selection to the starting position for this round.
+		_curPower = _initialPower;
+		_curAngle = _initialAngle;
+
 		init();
 		registerGraphics();
+
+		g_nancy->_sound->loadSound(_powerSound);
+		g_nancy->_sound->loadSound(_squeakSound);
+		g_nancy->_sound->loadSound(_chainSound);
+
+		// FUN_0044a526: clear all result/hint flags before the player starts.
+		NancySceneState.setEventFlag(_powerTooStrongFlag, g_nancy->_false);
+		NancySceneState.setEventFlag(_powerTooWeakFlag, g_nancy->_false);
+		NancySceneState.setEventFlag(_angleTooLeftFlag, g_nancy->_false);
+		NancySceneState.setEventFlag(_angleTooRightFlag, g_nancy->_false);
+		NancySceneState.setEventFlag(_winFlag, g_nancy->_false);
+
 		_state = kRun;
+		break;
+
+	case kRun:
+		// Wait for the chain sound to finish, then evaluate the throw (FUN_0044a6be)
+		// and always transition to _throwSquidScene so the animation plays.
+		if (_isThrown && !g_nancy->_sound->isSoundPlaying(_chainSound)) {
+			_isThrown = false;
+
+			// FUN_0044a6be: set exactly one result flag based on how accurate the throw was.
+			// The animation scene reads these flags to decide what to show.
+			if (_curPower == _targetPower && _curAngle == _targetAngle) {
+				// Exact match of power and angle — player wins round!
+				NancySceneState.setEventFlag(_winFlag, g_nancy->_true);
+			} else if (_curPower > _targetPower) {
+				// Power too strong
+				NancySceneState.setEventFlag(_powerTooStrongFlag, g_nancy->_true);
+			} else if (_curPower < _targetPower) {
+				// Power too weak
+				NancySceneState.setEventFlag(_powerTooWeakFlag, g_nancy->_true);
+			} else if (_curAngle > _targetAngle) {
+				// Angle too far right
+				NancySceneState.setEventFlag(_angleTooRightFlag, g_nancy->_true);
+			} else if (_curAngle < _targetAngle) {
+				// Angle too far left
+				NancySceneState.setEventFlag(_angleTooLeftFlag, g_nancy->_true);
+			}
+
+			_state = kActionTrigger;
+		}
+		break;
+
+	case kActionTrigger:
+		g_nancy->_sound->stopSound(_powerSound);
+		g_nancy->_sound->stopSound(_squeakSound);
+		g_nancy->_sound->stopSound(_chainSound);
+
+		if (_exitPressed) {
+			_exitScene.execute();
+		} else {
+			_throwSquidScene.execute();
+		}
+
+		finishExecution();
+		break;
 	}
-
-	// TODO
-	// Stub - return to the winning screen
-	warning("STUB - Nancy 8 Squid Toss game");
-	SceneChangeDescription scene;
-	scene.sceneID = 4465;
-	NancySceneState.resetStateToInit();
-	NancySceneState.changeScene(scene);
 }
 
-void AngleTossPuzzle::readData(Common::SeekableReadStream &stream) {
-	Common::Path tmp;
-	readFilename(stream, tmp);
-	stream.skip(12);	// TODO
-
-	for (uint i = 0; i < 22; ++i) {
-		Common::Rect r;
-		readRect(stream, r);
-
-		/*
-		Common::String desc = Common::String::format("AngleTossPuzzle rect %d", i);
-		debug("%s %d, %d, %d, %d", desc.c_str(), r.left, r.top, r.right, r.bottom);
-
-		Graphics::Surface *s = g_system->lockScreen();
-		s->fillRect(r, 255);
-		g_system->unlockScreen();
-		g_system->updateScreen();
-		g_system->delayMillis(1000);
-		*/
+void AngleTossPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun || _isThrown) {
+		return;
 	}
 
-	_powerSound.readNormal(stream);
-	_squeakSound.readNormal(stream);
-	_chainSound.readNormal(stream);
+	// All rects are in viewport-local coordinates.
+	Common::Point localMousePos = input.mousePos;
+	Common::Rect vpPos = NancySceneState.getViewport().getScreenPosition();
+	localMousePos -= Common::Point(vpPos.left, vpPos.top);
 
-	_throwSquidScene.readData(stream);
-	stream.skip(7); // TODO
-	_exitScene.readData(stream);
+	// Exit button
+	if (_exitHotspot.contains(localMousePos)) {
+		g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
 
-	stream.skip(16); // TODO
-}
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_exitPressed = true;
+			_state = kActionTrigger;
+		}
+		return;
+	}
 
-void AngleTossPuzzle::handleInput(NancyInput &input) {
-	// TODO
+	// Aim left arrow — always show hotspot cursor; only act when not already at min
+	if (_aimLeftHotspot.contains(localMousePos)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+		if (_curAngle > 0 && (input.input & NancyInput::kLeftMouseButtonUp)) {
+			--_curAngle;
+			_drawSurface.fillRect(_angleDisplay, _drawSurface.getTransparentColor());
+			_drawSurface.blitFrom(_image, _angleSprites[_curAngle], _angleDisplay);
+			g_nancy->_sound->playSound(_squeakSound);
+			_needsRedraw = true;
+		}
+		return;
+	}
+
+	// Aim right arrow — always show hotspot cursor; only act when not already at max
+	if (_aimRightHotspot.contains(localMousePos)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+		if (_curAngle + 1 < _numAngles && (input.input & NancyInput::kLeftMouseButtonUp)) {
+			++_curAngle;
+			_drawSurface.fillRect(_angleDisplay, _drawSurface.getTransparentColor());
+			_drawSurface.blitFrom(_image, _angleSprites[_curAngle], _angleDisplay);
+			g_nancy->_sound->playSound(_squeakSound);
+			_needsRedraw = true;
+		}
+		return;
+	}
+
+	// Power-level buttons — direct selection (Whale = 0, Dolphin = 1, Trout = 2, Shrimp = 3, Fish Fry = 4)
+	for (uint i = 0; i < _powerHotspots.size(); ++i) {
+		if (_powerHotspots[i].contains(localMousePos)) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+			if ((input.input & NancyInput::kLeftMouseButtonUp) && _curPower != i) {
+				_curPower = i;
+				_drawSurface.fillRect(_powerDisplay, _drawSurface.getTransparentColor());
+				_drawSurface.blitFrom(_image, _powerSprites[_curPower], _powerDisplay);
+				g_nancy->_sound->playSound(_powerSound);
+				_needsRedraw = true;
+			}
+			return;
+		}
+	}
+
+	// LAUNCH button — hotspot is rect 0 (_throwHotspot), sprite is drawn at rect 1 (_throwDisplay)
+	if (_throwHotspot.contains(localMousePos)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			g_nancy->_sound->playSound(_chainSound);
+			_isThrown = true;
+
+			// Show the launched/pressed overlay sprite (DAT_0059bfbd != 0 path in FUN_0044b1fa).
+			_drawSurface.blitFrom(_image, _throwSprite, _throwDisplay);
+			_needsRedraw = true;
+		}
+		return;
+	}
 }
 
 } // End of namespace Action
diff --git a/engines/nancy/action/puzzle/angletosspuzzle.h b/engines/nancy/action/puzzle/angletosspuzzle.h
index b669da5a697..43b550f8bdc 100644
--- a/engines/nancy/action/puzzle/angletosspuzzle.h
+++ b/engines/nancy/action/puzzle/angletosspuzzle.h
@@ -27,7 +27,21 @@
 namespace Nancy {
 namespace Action {
 
-// Squid Toss mini-game in Nancy 8
+// Squid toss mini-game in Nancy 8.
+//
+// UI layout:
+//   Left panel  - 5 direct-select power buttons (Whale / Dolphin / Trout / Shrimp / Fish Fry).
+//   Right panel - Ship's wheel with two arrow buttons (aim left / aim right), 5 angle positions.
+//   Top-right   - "LAUNCH" button that fires the fish.
+//   Top-left    - "MENU" button (handled as exit hotspot).
+//
+// Each AR instance represents one round of the puzzle. The player adjusts angle and power
+// then clicks LAUNCH. If the selection matches the AR's _targetPower/_targetAngle, the win
+// flag is set and the game transitions to _throwSquidScene to play the throw animation.
+// Wrong throws transition to _throwSquidScene, showing the appropriate failure animation,
+// based on which of the four fail flags is set (too strong/weak, too left/right). After the
+// separate AR instances (each with their own target) implement the 3-round mechanic.
+// FUN_0044a526 and FUN_0044a6be handle flag clearing and result evaluation in the original.
 
 class AngleTossPuzzle : public RenderActionRecord {
 public:
@@ -44,12 +58,72 @@ protected:
 	Common::String getRecordTypeName() const override { return "AngleTossPuzzle"; }
 	bool isViewportRelative() const override { return true; }
 
-	SoundDescription _powerSound;
-	SoundDescription _squeakSound;
-	SoundDescription _chainSound;
+	Common::Path _imageName;
+
+	// data+0x21..0x2c: 6 × uint16.
+	// _initialPower/_initialAngle: starting player selection (copied to object+0x24/0x26 in original).
+	// _numPowers/_numAngles: UI control bounds (always 5 in practice).
+	// _targetPower/_targetAngle: the correct answer for this AR instance (compared in FUN_0044a6be).
+	uint16 _initialPower = 0;
+	uint16 _initialAngle = 0;
+	uint16 _numPowers = 0;
+	uint16 _numAngles = 0;
+	uint16 _targetPower = 0;
+	uint16 _targetAngle = 0;
+
+	// The 22 rects read from the data file, in stream order.
+	// Rect-to-data-offset mapping confirmed from the render callback (FUN_0044b1fa).
+	//
+	// Sprite rects (_fooSprite) are source areas within the loaded image.
+	// Display rects (_fooDisplay) are destination areas on the viewport overlay.
+	// Hotspot rects (_fooHotspot) are clickable areas in viewport-local coordinates.
+	//
+	//   Rect  0 — data+0x2d  _throwHotspot         (clickable area for the throw button)
+	//   Rect  1 — data+0x3d  _throwDisplay         (where throw button sprite is drawn)
+	//   Rect  2 — data+0x4d  _throwSprite          (throw button source in image)
+	//   Rect  3 — data+0x5d  _aimDecHotspot        (aim-left arrow)
+	//   Rect  4 — data+0x6d  _aimIncHotspot        (aim-right arrow)
+	//   Rect  5 — data+0x7d  _angleDisplay         (single screen position for angle indicator)
+	//   Rects 6-10 — data+0x8d..0xcd  _angleSprites[5]  (5 angle images in sprite sheet)
+	//   Rect 11 — data+0xdd  _powerDisplay         (single screen position for power indicator)
+	//   Rects 12-16 — data+0xed..0x12d  _powerHotspots[5]  (power-select click areas)
+	//   Rects 17-21 — data+0x13d..0x17d  _powerSprites[5]  (5 power images in sprite sheet)
+
+	Common::Rect _throwHotspot;						// Rect  0 — clickable area for LAUNCH
+	Common::Rect _throwDisplay;						// Rect  1 — sprite dest on screen
+	Common::Rect _throwSprite;						// Rect  2 — source in image
+	Common::Rect _aimLeftHotspot;					// Rect  3
+	Common::Rect _aimRightHotspot;					// Rect  4
+	Common::Rect _angleDisplay;						// Rect  5 — single dest on screen
+	Common::Array<Common::Rect> _angleSprites;		// Rects 6-10 — 5 source frames in image
+	Common::Rect _powerDisplay;						// Rect 11 — single dest on screen
+	Common::Array<Common::Rect> _powerHotspots;		// Rects 12-16 — click areas
+	Common::Array<Common::Rect> _powerSprites;		// Rects 17-21 — 5 source frames in image
+
+	SoundDescription _powerSound;		// Played when changing power level
+	SoundDescription _squeakSound;		// Played when changing aim angle
+	SoundDescription _chainSound;		// Played when LAUNCH is pressed
+
+	SceneChangeWithFlag _throwSquidScene;	// Triggered on throw (FUN_0044a6be result)
+
+	int16 _powerTooStrongFlag = -1;   // 0x236
+	int16 _powerTooWeakFlag = -1; // 0x238
+
+	int16 _angleTooLeftFlag = -1;	// 0x23a
+	int16 _angleTooRightFlag = -1;	// 0x23c
+
+	int16 _winFlag = -1;	// 0x23e
 
-	SceneChangeWithFlag _throwSquidScene;
 	SceneChangeWithFlag _exitScene;
+	Common::Rect _exitHotspot;
+
+	Graphics::ManagedSurface _image;
+
+	uint16 _curPower = 0;
+	uint16 _curAngle = 0;
+	
+	bool _isThrown = false;		// True while the chain sound plays after LAUNCH is pressed
+	bool _exitPressed = false;
 };
 
 } // End of namespace Action




More information about the Scummvm-git-logs mailing list