[Scummvm-git-logs] scummvm master -> 1d4874acffb537a6ad01857afb1751f095ebab69

bluegr noreply at scummvm.org
Wed Aug 19 21:53:47 UTC 2026


This automated email contains information about 5 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
880280ed42 NANCY: NANCY13: Disable AVF video playing and update stale comments
4c1c74c749 NANCY: Fix immediate scene changes that point to the same scene
18fea43456 NANCY: Enhancement: Allow skipping cutscenes and dialog lines with esc
ca25044ca9 NANCY: Enhancement: Add mouse wheel functionality to several UI widgets
1d4874acff NEWS: Add entries for new functionality and fixes for Nancy games


Commit: 880280ed42d2028689701c7c1719158fb40c6cec
    https://github.com/scummvm/scummvm/commit/880280ed42d2028689701c7c1719158fb40c6cec
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-20T00:53:26+03:00

Commit Message:
NANCY: NANCY13: Disable AVF video playing and update stale comments

Nancy13 dropped AVF videos altogether, and only used Bink videos

Changed paths:
    engines/nancy/action/secondarymovie.cpp
    engines/nancy/action/secondarymovie.h


diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 81b15f545f5..d300a17d4a5 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -214,8 +214,8 @@ void PlaySecondaryMovie::readRandomMovieDataNancy14(Common::Serializer &ser, Com
 	}
 
 	// Recognition ("secondary") movie: its name followed by its own blt
-	// descriptors. Stored for future playback; the descriptors are consumed to
-	// keep the stream aligned (no home in the struct yet).
+	// descriptors. The descriptors are consumed to keep the stream aligned
+	// (no home in the struct yet).
 	readFilename(ser, _secondaryMovie.name);
 	uint16 numSecondaryDescs = 0;
 	ser.syncAsUint16LE(numSecondaryDescs);
@@ -229,7 +229,6 @@ void PlaySecondaryMovie::readRandomMovieDataNancy14(Common::Serializer &ser, Com
 
 void PlaySecondaryMovie::applyStartingRandomSequence() {
 	// "RandomMovie" picks any sequence; otherwise look up by name.
-	// Only the first sequence is played; chained playback is TODO.
 	if (!_sequences.empty()) {
 		int startIdx = -1;
 		if (_startingSequenceName == "RandomMovie") {
@@ -561,7 +560,11 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
 	if (g_nancy->getGameType() >= kGameTypeNancy7) {
 		uint16 videoType = 0;
 		ser.syncAsUint16LE(videoType);
-		_videoPlaytype = videoType == kVideoPlaytypeBink ? kVideoPlaytypeBink : kVideoPlaytypeAVF;
+
+		// Nancy13 dropped AVF altogether, so the slot no longer selects a container.
+		if (g_nancy->getGameType() <= kGameTypeNancy12) {
+			_videoPlaytype = videoType == kVideoPlaytypeBink ? kVideoPlaytypeBink : kVideoPlaytypeAVF;
+		}
 	}
 
 	ser.skip(2, kGameTypeVampire, kGameTypeNancy9); // videoPlaySource
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index fd366c78cd1..4cc4792806e 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -95,7 +95,7 @@ public:
 	Common::Path _paletteName;
 	Common::Path _bitmapOverlayName;
 
-	// Container the record asks for; only present from Nancy7 to Nancy13.
+	// Container the record asks for; only meaningful from Nancy7 to Nancy12.
 	byte _videoPlaytype = kVideoPlaytypeAuto;
 
 	uint16 _videoFormat = kLargeVideoFormat;
@@ -139,9 +139,8 @@ public:
 	uint16 _randomPlayerCursorAllowed = kPlayerCursorAllowed;
 	Common::Array<RandomSequence> _sequences;
 
-	// Nancy13+ carries one extra "secondary" movie (a recognition animation)
-	// after the sequence list. Stored for future playback; reading it is
-	// required so the trailing hotspot list stays aligned.
+	// Nancy13+ carries one extra "secondary" movie after the sequence list: the
+	// character's recognition animation, played while the mouse hovers it.
 	RandomSequence _secondaryMovie;
 
 	// Nancy13 talkable characters: the scene to open when the character is


Commit: 4c1c74c74909ee89a674791f472376b8296e6a63
    https://github.com/scummvm/scummvm/commit/4c1c74c74909ee89a674791f472376b8296e6a63
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-20T00:53:27+03:00

Commit Message:
NANCY: Fix immediate scene changes that point to the same scene

This allows us to finally get rid of a hack for Nancy9, scene 5651
(bottle stuck between rocks).

Fix #16782 and #17025

Changed paths:
    engines/nancy/action/soundrecords.cpp
    engines/nancy/state/scene.cpp


diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index 1814f86a04d..b4143548b55 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -211,7 +211,9 @@ void PlaySound::readDataNancy13(Common::SeekableReadStream &stream) {
 void PlaySound::execute() {
 	switch (_state) {
 	case kBegin:
-		g_nancy->_sound->loadSound(_sound, &_soundEffect);
+		// The channel is always unloaded and reloaded, so a sound that is still
+		// playing restarts from the beginning instead of being left alone
+		g_nancy->_sound->loadSound(_sound, &_soundEffect, true);
 		g_nancy->_sound->playSound(_sound);
 
 		// Nancy13+ shows the sound's subtitle (resolved from its name) in the
@@ -239,17 +241,13 @@ void PlaySound::execute() {
 			break;
 		}
 
-		if (_changeSceneImmediately) {
-			applyAfterSoundAction();
-			NancySceneState.changeScene(_sceneChange);
-			finishExecution();
-			break;
-		}
-
 		_state = kRun;
 		break;
 	case kRun:
-		if (!g_nancy->_sound->isSoundPlaying(_sound)) {
+		// changeSceneImmediately means the record doesn't wait for the sound to
+		// end, not that the scene changes within this same pass; the records
+		// between this one and the end of the list still get to run first
+		if (_changeSceneImmediately || !g_nancy->_sound->isSoundPlaying(_sound)) {
 			_state = kActionTrigger;
 		}
 
@@ -262,7 +260,9 @@ void PlaySound::execute() {
 			NancySceneState.setEventFlag(_flag);
 		}
 
-		g_nancy->_sound->stopSound(_sound);
+		if (!_changeSceneImmediately) {
+			g_nancy->_sound->stopSound(_sound);
+		}
 
 		finishExecution();
 		break;
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index e36efabcc9a..739895202fc 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -240,19 +240,6 @@ void Scene::changeScene(const SceneChangeDescription &sceneDescription) {
 		return;
 	}
 
-	// HACK: Nancy 9 tries to reload the same scene when changing
-	// angle/power in scene 5651 (stuck bottle in rocks). This ends up
-	// resetting the scene flags, which makes the angle/power buttons
-	// unresponsive. We avoid reloading the scene in this case, if the
-	// new scene is the same as the current one. This has the negative
-	// side-effect that the button arrows are not updated, but at least
-	// it makes them usable.
-	// TODO: find a better solution for this.
-	if (sceneDescription.sceneID == _sceneState.currentScene.sceneID &&
-		g_nancy->getGameType() == kGameTypeNancy9 && sceneDescription.sceneID == 5651) {
-		return;
-	}
-
 	_sceneState.nextScene = sceneDescription;
 	_state = kLoad;
 }


Commit: 18fea4345639fce73a4bb4c2567319d6109a94c9
    https://github.com/scummvm/scummvm/commit/18fea4345639fce73a4bb4c2567319d6109a94c9
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-20T00:53:29+03:00

Commit Message:
NANCY: Enhancement: Allow skipping cutscenes and dialog lines with esc

Changed paths:
    engines/nancy/action/conversation.cpp
    engines/nancy/action/conversation.h
    engines/nancy/action/secondarymovie.cpp
    engines/nancy/action/secondarymovie.h
    engines/nancy/state/scene.cpp
    engines/nancy/state/scene.h


diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 8c1f4889866..b975a2b484f 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -392,7 +392,7 @@ void ConversationSound::execute() {
 			}
 		}
 
-		if (!g_nancy->_sound->isSoundPlaying(_sound) && isVideoDonePlaying()) {
+		if (!g_nancy->_sound->isSoundPlaying(_sound) && (_isSkipped || isVideoDonePlaying())) {
 			g_nancy->_sound->stopSound(_sound);
 
 			bool hasResponses = false;
@@ -449,6 +449,8 @@ void ConversationSound::execute() {
 		}
 		break;
 	case kActionTrigger:
+		_isSkipped = false;
+
 		if (!g_nancy->_sound->isSoundPlaying(_responseGenericSound)) {
 			// process flags structs
 			for (auto &flags : _flagsStructs) {
@@ -493,6 +495,23 @@ void ConversationSound::execute() {
 	}
 }
 
+void ConversationSound::skipLine() {
+	switch (_state) {
+	case kRun:
+		// Cut the NPC's line short. Stopping the sound and marking the line as
+		// skipped makes the next execute() take the "line has finished" path.
+		g_nancy->_sound->stopSound(_sound);
+		_isSkipped = true;
+		break;
+	case kActionTrigger:
+		// The player's chosen response is playing; cut that short instead
+		g_nancy->_sound->stopSound(_responseGenericSound);
+		break;
+	default:
+		break;
+	}
+}
+
 void ConversationSound::addConditionalDialogue() {
 	if (g_nancy->getGameType() >= kGameTypeNancy12) {
 		addConditionalDialogueNancy12();
diff --git a/engines/nancy/action/conversation.h b/engines/nancy/action/conversation.h
index 8d15bda741c..4dc3c4835b3 100644
--- a/engines/nancy/action/conversation.h
+++ b/engines/nancy/action/conversation.h
@@ -50,6 +50,10 @@ public:
 	virtual bool isVideoDonePlaying() { return true; }
 	bool isViewportRelative() const override { return true; }
 
+	// Enhancement: cut the currently playing line short, as if its sound and
+	// video had just finished. Any available responses still get shown.
+	void skipLine();
+
 protected:
 	struct ConversationFlag {
 		byte type;
@@ -137,6 +141,7 @@ protected:
 
 	bool _hasDrawnTextbox;
 	int16 _pickedResponse;
+	bool _isSkipped = false;
 
 	const byte _noResponse;
 };
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index d300a17d4a5..7016df86ed0 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -917,6 +917,21 @@ void PlaySecondaryMovie::execute() {
 	}
 }
 
+void PlaySecondaryMovie::skip() {
+	if (_isRandom || _state != kRun || _playerCursorAllowed != kNoPlayerCursorAllowed) {
+		return;
+	}
+
+	g_nancy->_sound->stopSound(_sound);
+
+	if (!_isFinished) {
+		_decoder.pauseVideo(true);
+		_isFinished = true;
+	}
+
+	_state = kActionTrigger;
+}
+
 // --- PlayRandomMovieControl --------------------------------------------
 
 void PlayRandomMovieControl::readData(Common::SeekableReadStream &stream) {
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index 4cc4792806e..49fd839635a 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -91,6 +91,11 @@ public:
 
 	bool getIsFinished() const { return _isFinished; }
 
+	// Enhancement: jump a cinematic straight to its end, as if it had finished
+	// playing on its own. Only movies that hide the player cursor are skipped;
+	// the rest are background animations the player isn't waiting on.
+	void skip();
+
 	Common::Path _videoName;
 	Common::Path _paletteName;
 	Common::Path _bitmapOverlayName;
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 739895202fc..396bc28787b 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -36,6 +36,7 @@
 #include "engines/nancy/state/scene.h"
 #include "engines/nancy/state/map.h"
 
+#include "engines/nancy/action/conversation.h"
 #include "engines/nancy/action/secondarymovie.h"
 
 #include "engines/nancy/ui/button.h"
@@ -1566,9 +1567,21 @@ void Scene::handleInput() {
 				g_nancy->_cursor->warpCursor(input.mousePos);
 			}
 		}
-	} else if (!_activeMovie) {
-		// Check if player has pressed esc
-		if (input.input & NancyInput::kOpenMainMenu) {
+	}
+
+	// Check if player has pressed esc. While a dialogue line or a cinematic is
+	// playing, esc skips it instead of opening the main menu. Only the initial
+	// press counts, so holding the key down doesn't skip line after line.
+	const bool escPressed = (input.input & NancyInput::kOpenMainMenu) != 0;
+	const bool escJustPressed = escPressed && !_escHeld;
+	_escHeld = escPressed;
+
+	if (escJustPressed) {
+		if (_activeConversation) {
+			_activeConversation->skipLine();
+		} else if (_activeMovie) {
+			_activeMovie->skip();
+		} else {
 			g_nancy->setState(NancyState::kMainMenu);
 			return;
 		}
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 29cd250b325..1c742ff67f7 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -372,6 +372,9 @@ private:
 	// down a persistent random-movie whose scene chain is over.
 	bool _hadRandomMovieARThisScene = false;
 
+	// Whether esc was already down last frame, so a held key only skips once
+	bool _escHeld = false;
+
 	// Contains a screenshot of the Scene state from the last time it was exited
 	Graphics::ManagedSurface _lastScreenshot;
 


Commit: ca25044ca9667b5e74bec1fd822be5038cd1f264
    https://github.com/scummvm/scummvm/commit/ca25044ca9667b5e74bec1fd822be5038cd1f264
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-20T00:53:34+03:00

Commit Message:
NANCY: Enhancement: Add mouse wheel functionality to several UI widgets

Mouse wheel functionality can now be used to scroll the content of the
following widgets:
- Notebook
- Task list
- Inventory
- Cellphone help
- Cellphone web browser
- Text box
- Conversation text box

Changed paths:
    engines/nancy/action/puzzle/peepholepuzzle.cpp
    engines/nancy/input.cpp
    engines/nancy/input.h
    engines/nancy/ui/cellphonepopup.cpp
    engines/nancy/ui/cellphonepopup.h
    engines/nancy/ui/conversationpopup.cpp
    engines/nancy/ui/inventorybox.cpp
    engines/nancy/ui/inventorypopup.cpp
    engines/nancy/ui/notebookpopup.cpp
    engines/nancy/ui/scrollbar.cpp
    engines/nancy/ui/scrollbar.h
    engines/nancy/ui/scrolltextbox.cpp
    engines/nancy/ui/textbox.cpp


diff --git a/engines/nancy/action/puzzle/peepholepuzzle.cpp b/engines/nancy/action/puzzle/peepholepuzzle.cpp
index 67273a35a35..aa3c614405e 100644
--- a/engines/nancy/action/puzzle/peepholepuzzle.cpp
+++ b/engines/nancy/action/puzzle/peepholepuzzle.cpp
@@ -30,6 +30,8 @@
 
 #include "engines/nancy/state/scene.h"
 
+#include "engines/nancy/ui/scrollbar.h"
+
 namespace Nancy {
 namespace Action {
 
@@ -113,6 +115,24 @@ void PeepholePuzzle::handleInput(NancyInput &input) {
 		}
 	}
 
+	// The mouse wheel scrolls the contents vertically while the cursor is over them,
+	// as an alternative to holding down the up/down buttons
+	if ((input.input & NancyInput::kMouseWheel) && _innerBounds.height() > _dest.height() &&
+			NancySceneState.getViewport().convertViewportToScreen(_dest).contains(input.mousePos)) {
+		const int scrollPixels = UI::wheelScrollPixels(_dest.height());
+		_currentSrc.translate(0, (input.input & NancyInput::kMouseWheelUp) ? -scrollPixels : scrollPixels);
+
+		if (_currentSrc.top < _innerBounds.top) {
+			_currentSrc.translate(0, _innerBounds.top - _currentSrc.top);
+		} else if (_currentSrc.bottom > _innerBounds.bottom) {
+			_currentSrc.translate(0, _innerBounds.bottom - _currentSrc.bottom);
+		}
+
+		input.eatMouseWheelInput();
+		checkButtons();
+		drawInner();
+	}
+
 	if (_pressedButton != -1) {
 		if (input.input & NancyInput::kLeftMouseButtonHeld) {
 			// Player is still holding the left button, check if mouse has moved outside bounds
diff --git a/engines/nancy/input.cpp b/engines/nancy/input.cpp
index dbae5afb1d3..c63dcbe14ac 100644
--- a/engines/nancy/input.cpp
+++ b/engines/nancy/input.cpp
@@ -38,7 +38,7 @@ void InputManager::processEvents() {
 	using namespace Common;
 	Common::Event event;
 
-	_inputs &= ~(NancyInput::kLeftMouseButtonDown | NancyInput::kLeftMouseButtonUp | NancyInput::kRightMouseButtonDown | NancyInput::kRightMouseButtonUp | NancyInput::kRaycastMap);
+	_inputs &= ~(NancyInput::kLeftMouseButtonDown | NancyInput::kLeftMouseButtonUp | NancyInput::kRightMouseButtonDown | NancyInput::kRightMouseButtonUp | NancyInput::kRaycastMap | NancyInput::kMouseWheel);
 	_otherKbdInput.clear();
 
 	while (g_nancy->getEventManager()->pollEvent(event)) {
@@ -48,6 +48,14 @@ void InputManager::processEvents() {
 			_otherKbdInput.push_back(event.kbd);
 			_inputBeginState = g_nancy->getState();
 			break;
+		case EVENT_WHEELUP:
+			_inputs |= NancyInput::kMouseWheelUp;
+			_inputBeginState = g_nancy->getState();
+			break;
+		case EVENT_WHEELDOWN:
+			_inputs |= NancyInput::kMouseWheelDown;
+			_inputBeginState = g_nancy->getState();
+			break;
 		case EVENT_CUSTOM_ENGINE_ACTION_START:
 			_inputBeginState = g_nancy->getState();
 
diff --git a/engines/nancy/input.h b/engines/nancy/input.h
index 5f232f3e149..3f2c16777c4 100644
--- a/engines/nancy/input.h
+++ b/engines/nancy/input.h
@@ -53,16 +53,23 @@ struct NancyInput {
 		kMoveFastModifier		= 1 << 10,
 		kOpenMainMenu			= 1 << 11,
 		kRaycastMap				= 1 << 12,
+		kMouseWheelUp			= 1 << 13,
+		kMouseWheelDown			= 1 << 14,
 
 		kLeftMouseButton		= kLeftMouseButtonDown | kLeftMouseButtonHeld | kLeftMouseButtonUp,
-		kRightMouseButton		= kRightMouseButtonDown | kRightMouseButtonHeld | kRightMouseButtonUp
+		kRightMouseButton		= kRightMouseButtonDown | kRightMouseButtonHeld | kRightMouseButtonUp,
+		kMouseWheel				= kMouseWheelUp | kMouseWheelDown
 	};
 
 	Common::Point mousePos;
 	uint16 input;
 	Common::Array<Common::KeyState> otherKbdInput;
 
-	void eatMouseInput() { mousePos.x = -1; input &= ~(kLeftMouseButton | kRightMouseButton); }
+	void eatMouseInput() { mousePos.x = -1; input &= ~(kLeftMouseButton | kRightMouseButton | kMouseWheel); }
+
+	// Consumes just the wheel input, leaving the mouse position and buttons alone,
+	// so a widget that scrolls can still keep updating its hover state
+	void eatMouseWheelInput() { input &= ~kMouseWheel; }
 };
 
 // This class handles collecting events and translating them to a NancyInput object,
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 2436a588ce0..57bfe8101f6 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -1173,6 +1173,12 @@ void CellPhonePopup::renderContentPage(int surfaceWidth) {
 	_contentCacheHotspots = ht.hotspots();
 }
 
+const Common::Rect &CellPhonePopup::contentViewScreenRect() const {
+	return (isHelpContentView() || _uiclData->emailListContainer.isEmpty())
+			? _uiclData->welcomeScreen.destRect
+			: _uiclData->emailListContainer;
+}
+
 uint CellPhonePopup::contentScrollStep() const {
 	const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
 	if (!font) {
@@ -1181,11 +1187,7 @@ uint CellPhonePopup::contentScrollStep() const {
 
 	// Original: one click scrolls ~1/10th of the article (capped near a full
 	// page), plus 1.25 line heights.
-	const Common::Rect &ws =
-		(isHelpContentView() || _uiclData->emailListContainer.isEmpty())
-			? _uiclData->welcomeScreen.destRect
-			: _uiclData->emailListContainer;
-	const int viewH = MAX(0, ws.height() - 2);
+	const int viewH = MAX(0, contentViewScreenRect().height() - 2);
 	int page = MIN((int)_contentCacheTextHeight / 10, MAX(0, viewH - 30));
 	return (font->getFontHeight() * 5) / 4 + page;
 }
@@ -1204,10 +1206,7 @@ void CellPhonePopup::drawContentView() {
 	// blits fullEmptyScreenSrc), so the keypad is no longer visible underneath
 	// and we render into the larger LCD area that emailListContainer defines.
 	// The help page keeps the regular chrome, so it renders into the small LCD.
-	const Common::Rect &ws =
-		(isHelpContentView() || _uiclData->emailListContainer.isEmpty())
-			? _uiclData->welcomeScreen.destRect
-			: _uiclData->emailListContainer;
+	const Common::Rect &ws = contentViewScreenRect();
 	const int lcdLeft = ws.left - _screenPosition.left;
 	const int lcdTop  = ws.top  - _screenPosition.top;
 	const int lcdW    = ws.width();
@@ -2687,6 +2686,25 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		const Common::Point popupMouseLink(chunkMouse.x - _screenPosition.left,
 											chunkMouse.y - _screenPosition.top);
 
+		// The mouse wheel scrolls the page over the LCD itself, by the same
+		// amount as a click on the up/down arrows. drawContentView() clamps.
+		if ((input.input & NancyInput::kMouseWheel) &&
+				contentViewScreenRect().contains(chunkMouse)) {
+			const uint wheelStep = contentScrollStep();
+			const uint oldScroll = _contentScroll;
+			if (input.input & NancyInput::kMouseWheelUp) {
+				_contentScroll = _contentScroll > wheelStep ? _contentScroll - wheelStep : 0;
+			} else {
+				_contentScroll += wheelStep;
+			}
+
+			input.eatMouseWheelInput();
+
+			if (_contentScroll != oldScroll) {
+				drawScreenContent();
+			}
+		}
+
 		// The main browser page carries the top-row SEARCH button (subButtons[8])
 		// which opens the search list; it highlights green while hovered.
 		if (_contentHeading == &_uiclData->browserHeading && !isBrowserArticle() &&
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 3c9003533d1..d71a955c265 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -181,6 +181,9 @@ private:
 	// surface (+ text height, image/link hotspots). Called by drawContentView
 	// only when the page key changes.
 	void renderContentPage(int surfaceWidth);
+	// The LCD area the content view renders into: the small screen for the help
+	// page, the taller zoomed-in one for browser / email articles.
+	const Common::Rect &contentViewScreenRect() const;
 	// Per-click scroll amount (pixels) for the article/help content view.
 	uint contentScrollStep() const;
 	// Enter the content view for a list entry whose AUTOTEXT key is `key`.
diff --git a/engines/nancy/ui/conversationpopup.cpp b/engines/nancy/ui/conversationpopup.cpp
index b91c3a4254e..32977ada207 100644
--- a/engines/nancy/ui/conversationpopup.cpp
+++ b/engines/nancy/ui/conversationpopup.cpp
@@ -27,6 +27,8 @@
 
 #include "engines/nancy/state/scene.h"
 
+#include "engines/nancy/ui/scrollbar.h"
+
 #include "engines/nancy/ui/conversationpopup.h"
 
 namespace Nancy {
@@ -273,6 +275,11 @@ void ConversationPopup::handleInput(NancyInput &input) {
 			return;
 		}
 
+		if (scrollWithMouseWheel(input, _screenPosition, _scrollPos,
+				wheelScrollStep(getLocalTextRect().height(), getInnerHeight()))) {
+			redrawScroll();
+		}
+
 		if (overThumb != _scrollbarHovered) {
 			_scrollbarHovered = overThumb;
 			drawScrollbar(overThumb ? kUIButtonHover : kUIButtonIdle);
diff --git a/engines/nancy/ui/inventorybox.cpp b/engines/nancy/ui/inventorybox.cpp
index 28b8661679b..da709be0d45 100644
--- a/engines/nancy/ui/inventorybox.cpp
+++ b/engines/nancy/ui/inventorybox.cpp
@@ -98,6 +98,17 @@ void InventoryBox::handleInput(NancyInput &input) {
 	}
 
 	if (_order.size()) {
+		// The mouse wheel scrolls a whole page of items at a time, both over the
+		// item grid and over the scrollbar itself
+		const uint numPages = (_order.size() - 1) / 4 + 1;
+		Common::Rect wheelArea = _screenPosition;
+		wheelArea.extend(_scrollbar->getTrackRect());
+
+		float scrollPos = _scrollbar->getPos();
+		if (scrollWithMouseWheel(input, wheelArea, scrollPos, 1.0f / (float)numPages)) {
+			_scrollbar->setPosition(scrollPos);
+		}
+
 		_scrollbar->handleInput(input);
 	}
 
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 3b9b8bf482c..3052673dd19 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -29,6 +29,7 @@
 #include "engines/nancy/state/scene.h"
 
 #include "engines/nancy/ui/inventorypopup.h"
+#include "engines/nancy/ui/scrollbar.h"
 #include "engines/nancy/ui/taskbar.h"
 
 namespace Nancy {
@@ -427,6 +428,13 @@ void InventoryPopup::handleInput(NancyInput &input) {
 			return;
 		}
 
+		// The mouse wheel moves a whole page of items at a time
+		const uint numPages = (_visibleItems.size() + kSlotsPerPage - 1) / kSlotsPerPage;
+		if (numPages > 1 && scrollWithMouseWheel(input, _screenPosition, _scrollPos, 1.0f / (float)(numPages - 1))) {
+			updatePageFromScroll();
+			refreshGrid();
+		}
+
 		if (overScrollbar != _scrollbarHovered) {
 			_scrollbarHovered = overScrollbar;
 			drawScrollbar(overScrollbar ? kUIButtonHover : kUIButtonIdle);
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index d329d03d4f2..c78eccb5d3d 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -33,6 +33,7 @@
 
 #include "engines/nancy/state/scene.h"
 
+#include "engines/nancy/ui/scrollbar.h"
 #include "engines/nancy/ui/taskbar.h"
 
 #include "engines/nancy/ui/notebookpopup.h"
@@ -315,6 +316,11 @@ void NotebookPopup::handleInput(NancyInput &input) {
 			return;
 		}
 
+		if (scrollWithMouseWheel(input, _screenPosition, _scrollPos,
+				wheelScrollStep(toPopupLocal(_uinbData->textRect, false).height(), _drawnTextHeight))) {
+			redrawScroll();
+		}
+
 		if (overThumb != _scrollbarHovered) {
 			_scrollbarHovered = overThumb;
 			drawScrollbar(overThumb ? kUIButtonHover : kUIButtonIdle);
diff --git a/engines/nancy/ui/scrollbar.cpp b/engines/nancy/ui/scrollbar.cpp
index c59b45a7d6c..b11f2c7d009 100644
--- a/engines/nancy/ui/scrollbar.cpp
+++ b/engines/nancy/ui/scrollbar.cpp
@@ -29,6 +29,38 @@
 namespace Nancy {
 namespace UI {
 
+// A single mouse wheel notch scrolls this much of the visible height
+static const float kWheelScrollPageFraction = 0.25f;
+
+int wheelScrollPixels(int visibleHeight) {
+	return (int)(visibleHeight * kWheelScrollPageFraction);
+}
+
+float wheelScrollStep(int visibleHeight, int contentHeight) {
+	const int scrollableHeight = contentHeight - visibleHeight;
+	if (visibleHeight <= 0 || scrollableHeight <= 0) {
+		return 0.0f;
+	}
+
+	return (float)wheelScrollPixels(visibleHeight) / (float)scrollableHeight;
+}
+
+bool scrollWithMouseWheel(NancyInput &input, const Common::Rect &hotspot, float &scrollPos, float step) {
+	if (!(input.input & NancyInput::kMouseWheel) || step <= 0.0f || !hotspot.contains(input.mousePos)) {
+		return false;
+	}
+
+	const float newPos = CLIP<float>(scrollPos + (input.input & NancyInput::kMouseWheelUp ? -step : step), 0.0f, 1.0f);
+	input.eatMouseWheelInput();
+
+	if (newPos == scrollPos) {
+		return false;
+	}
+
+	scrollPos = newPos;
+	return true;
+}
+
 Scrollbar::Scrollbar(uint16 zOrder, const Common::Rect &srcBounds, const Common::Point &topPosition, uint16 scrollDistance, bool isVertical) :
 	Scrollbar(zOrder, srcBounds, g_nancy->_graphics->_object0, topPosition, scrollDistance, isVertical) {}
 
@@ -111,6 +143,19 @@ void Scrollbar::handleInput(NancyInput &input) {
 	}
 }
 
+Common::Rect Scrollbar::getTrackRect() const {
+	Common::Rect track = _screenPosition;
+	track.moveTo(_startPosition);
+
+	if (_isVertical) {
+		track.bottom += _maxDist;
+	} else {
+		track.right += _maxDist;
+	}
+
+	return track;
+}
+
 void Scrollbar::setPosition(float pos) {
 	_currentPosition = pos;
 	if (_isVertical) {
diff --git a/engines/nancy/ui/scrollbar.h b/engines/nancy/ui/scrollbar.h
index 07a0ee72760..a774b3798ef 100644
--- a/engines/nancy/ui/scrollbar.h
+++ b/engines/nancy/ui/scrollbar.h
@@ -30,6 +30,18 @@ struct NancyInput;
 
 namespace UI {
 
+// Returns the number of pixels a single mouse wheel notch should scroll a view of
+// the given height, so that scrolling advances by a fixed part of a page.
+int wheelScrollPixels(int visibleHeight);
+
+// Same, expressed as a fraction of the total scroll range. Returns 0 when the
+// content fits inside the viewport and thus cannot be scrolled.
+float wheelScrollStep(int visibleHeight, int contentHeight);
+
+// Applies mouse wheel input to a [0, 1] scroll position, as long as the mouse is
+// inside the hotspot. Consumes the wheel input and returns true if the position changed.
+bool scrollWithMouseWheel(NancyInput &input, const Common::Rect &hotspot, float &scrollPos, float step);
+
 class Scrollbar : public RenderObject {
 public:
 	Scrollbar(uint16 zOrder, const Common::Rect &srcBounds, const Common::Point &topPosition, uint16 scrollDistance, bool isVertical = true);
@@ -43,6 +55,10 @@ public:
 	void resetPosition();
 	float getPos() const { return _currentPosition; }
 
+	// The screen rect the thumb travels along. Unlike _screenPosition, which only
+	// covers the thumb itself, this covers the whole scrollbar.
+	Common::Rect getTrackRect() const;
+
 	void setPosition(float pos);
 	void calculatePosition();
 
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index ad2f24153f9..480fd46a1d1 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -27,6 +27,7 @@
 
 #include "engines/nancy/state/scene.h"
 
+#include "engines/nancy/ui/scrollbar.h"
 #include "engines/nancy/ui/scrolltextbox.h"
 
 namespace Nancy {
@@ -407,6 +408,11 @@ void ScrollTextBox::handleInput(NancyInput &input) {
 			return;
 		}
 
+		if (scrollWithMouseWheel(input, _screenPosition, _scrollPos,
+				wheelScrollStep(textViewportLocal().height(), getInnerHeight()))) {
+			redrawScroll();
+		}
+
 		if (overThumb != _scrollbarHovered) {
 			_scrollbarHovered = overThumb;
 			drawScrollbar(overThumb ? kUIButtonHover : kUIButtonIdle);
diff --git a/engines/nancy/ui/textbox.cpp b/engines/nancy/ui/textbox.cpp
index ca2eda74480..0fc6a8a1bb0 100644
--- a/engines/nancy/ui/textbox.cpp
+++ b/engines/nancy/ui/textbox.cpp
@@ -137,8 +137,18 @@ void Textbox::handleInput(NancyInput &input) {
 	if (!isVisible())
 		return;
 
-	if (_scrollbar)
+	if (_scrollbar) {
+		Common::Rect wheelArea = _screenPosition;
+		wheelArea.extend(_scrollbar->getTrackRect());
+
+		float scrollPos = _scrollbar->getPos();
+		if (scrollWithMouseWheel(input, wheelArea, scrollPos,
+				wheelScrollStep(_screenPosition.height(), getInnerHeight()))) {
+			_scrollbar->setPosition(scrollPos);
+		}
+
 		_scrollbar->handleInput(input);
+	}
 
 	bool hasHighlight = false;
 	for (uint i = 0; i < _hotspots.size(); ++i) {


Commit: 1d4874acffb537a6ad01857afb1751f095ebab69
    https://github.com/scummvm/scummvm/commit/1d4874acffb537a6ad01857afb1751f095ebab69
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-20T00:53:35+03:00

Commit Message:
NEWS: Add entries for new functionality and fixes for Nancy games

Changed paths:
    NEWS.md


diff --git a/NEWS.md b/NEWS.md
index 7592f650650..e9a1752e34e 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -76,6 +76,11 @@ For a more comprehensive changelog of the latest experimental code, see:
    - Generic flags are no longer cleared when saving or auto-saving. Clearing
      them introduced issues when auto-saving was done in some scenes.
    - Loading from the GMM is now possible before starting a new scene.
+   - It's now possible to skip cutscenes and dialog lines with escape.
+   - Mouse wheel functionality has been added to several UI widgets.
+   - Fixed issues with the memory puzzle in Nancy9.
+   - Fixed issues with the "I can't" item sound.
+   - Answers entered in password kind of puzzles are now more forgiving (e.g. "Xoc" and "Lady Xoc" are now both acceptable answers).
 
  SCUMM:
    - Improved cursor scale in early Windows HE games.




More information about the Scummvm-git-logs mailing list