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

bluegr noreply at scummvm.org
Sat May 23 01:39:43 UTC 2026


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

Summary:
49c2dfbc51 NANCY: More work on the cell phone interface for Nancy10+
36257c744e NANCY: Implement CellPhonePopCellSceneFromStack for Nancy10+
d0aea692cc NANCY: Add information about cell phone contacts fields
4c107f1d83 NANCY: Don't try to load viewport videos in scenes that don't have them
02cd306964 NANCY: Return an empty string for captions that aren't found
834fe18348 NANCY: Refresh the cursor when a piece is selected in multibuildpuzzle
6fd36716c9 NANCY: More work on inventory item handling for Nancy10+


Commit: 49c2dfbc515d0f932954c764fe397f98833ddc45
    https://github.com/scummvm/scummvm/commit/49c2dfbc515d0f932954c764fe397f98833ddc45
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:25+03:00

Commit Message:
NANCY: More work on the cell phone interface for Nancy10+

It's now possible to place phone calls

Changed paths:
    engines/nancy/ui/cellphonepopup.cpp
    engines/nancy/ui/cellphonepopup.h


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 726a14815e1..e8798a4f7a5 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -74,6 +74,7 @@ void CellPhonePopup::init() {
 	_dialedNumber.clear();
 	_resolvedContact = -1;
 	_directoryScroll = 0;
+	_directorySelection = 0;
 
 	drawChrome();
 	drawScreenContent();
@@ -107,6 +108,7 @@ void CellPhonePopup::open() {
 	_dialedNumber.clear();
 	_resolvedContact = -1;
 	_directoryScroll = 0;
+	_directorySelection = 0;
 	_closeButtonHovered = false;
 
 	drawChrome();
@@ -141,9 +143,7 @@ void CellPhonePopup::updateGraphics() {
 		return;
 	}
 
-	// TODO: Process states 0xa..0x10 (web/email/search/help/browser)
-	// are not implemented — the corresponding screen states are
-	// missing from the enum too.
+	// TODO: web/email/search/help/browser modes not implemented.
 	switch (_screenState) {
 	case kWelcome:
 	case kDialing:
@@ -166,7 +166,11 @@ void CellPhonePopup::updateGraphics() {
 		break;
 
 	case kLookupContact: {
-		_resolvedContact = findContactByDialBuffer();
+		// Directory-mode calls pre-resolve the contact, so only fall back
+		// to dial-buffer lookup when the contact isn't already known.
+		if (_resolvedContact == -1) {
+			_resolvedContact = findContactByDialBuffer();
+		}
 		if (_resolvedContact == -1) {
 			enterScreenState(kInvalidNumber);
 		} else if (playSoundIfPresent(_uiclData->pickupSound)) {
@@ -185,13 +189,15 @@ void CellPhonePopup::updateGraphics() {
 		break;
 
 	case kConnected:
+		// Trigger the scene change once, then sit in kConnected so the
+		// connecting sprite stays on screen for the duration of the
+		// conversation. AR 128 closes the popup when the call ends.
 		if (_resolvedContact >= 0 &&
 				_resolvedContact < (int)_uiclData->contacts.size()) {
 			triggerContactCallSceneChange((uint)_resolvedContact);
+			_resolvedContact = -1;
+			resetDialPad();
 		}
-		_resolvedContact = -1;
-		resetDialPad();
-		enterScreenState(kWelcome);
 		break;
 
 	case kInvalidNumber:
@@ -218,18 +224,14 @@ void CellPhonePopup::updateGraphics() {
 // --------------------------------------------------------------------
 
 void CellPhonePopup::drawChrome() {
-	// Chrome reads from the primary overlay; all other blits use _spritesImage.
 	_drawSurface.blitFrom(_overlayImage, _uiclData->header.normalSrcRect,
 							Common::Point(0, 0));
 	drawCloseButton(_closeButtonHovered ? 1 : 0);
-	drawWebDirLabels();
 	drawCallButtonState(0);
 	_needsRedraw = true;
 }
 
 void CellPhonePopup::drawScreenContent() {
-	// Repaint chrome (clears the LCD strip), then layer state-specific
-	// content on top.
 	drawChrome();
 
 	if (_screenState != kConnected) {
@@ -238,10 +240,11 @@ void CellPhonePopup::drawScreenContent() {
 
 	switch (_screenState) {
 	case kWelcome:
-		// Welcome graphic is baked into the chrome; only the status-label
-		// overlay is layered on top when the carrier is unreachable.
+		drawWebDirLabels();
 		if (_noSignal) {
 			drawStatusLabels();
+		} else {
+			drawWelcomeScreen();
 		}
 		break;
 
@@ -249,6 +252,7 @@ void CellPhonePopup::drawScreenContent() {
 	case kPlaceCall:
 	case kWaitOutgoingRing:
 	case kLookupContact:
+		drawWebDirLabels();
 		drawDialLabel();
 		drawTypeMessage();
 		drawDialedNumber();
@@ -266,7 +270,6 @@ void CellPhonePopup::drawScreenContent() {
 		break;
 
 	case kDirectory:
-		drawDirHeading();
 		drawDirectoryList();
 		drawDirectoryArrows();
 		break;
@@ -422,8 +425,6 @@ void CellPhonePopup::drawCloseButton(uint state) {
 }
 
 void CellPhonePopup::drawStatusLabels() {
-	// Three stacked lines (No Signal / No Access / Old Email Only)
-	// at statusTextX with Y offsets -10 / +20 / +50.
 	const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId1);
 	if (!font) {
 		return;
@@ -457,11 +458,7 @@ void CellPhonePopup::drawDirHeading() {
 }
 
 void CellPhonePopup::drawDirectoryList() {
-	// The chunk stores one entry per dial-pattern variant, so several
-	// entries can share a name. Collapse by name to avoid showing the
-	// same contact multiple times.
-	// TODO: also sort by name and respect per-entry visibility flags,
-	// the way the original engine's contact-filter pass does.
+	// Contacts have one record per dial-pattern variant; collapse by name.
 	const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
 	if (!font) {
 		return;
@@ -476,7 +473,7 @@ void CellPhonePopup::drawDirectoryList() {
 			contactIdx < _uiclData->contacts.size() && visibleRow < maxRows;
 			++contactIdx) {
 		const UICL::Contact &c = _uiclData->contacts[contactIdx];
-		if (c.name.empty()) {
+		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
 
@@ -492,7 +489,6 @@ void CellPhonePopup::drawDirectoryList() {
 		}
 		seenNames.push_back(c.name);
 
-		// _directoryScroll counts deduplicated entries.
 		if (visited < _directoryScroll) {
 			++visited;
 			continue;
@@ -507,8 +503,31 @@ void CellPhonePopup::drawDirectoryList() {
 	}
 }
 
+void CellPhonePopup::drawWelcomeScreen() {
+	const UICL::SrcDestRectPair &ws = _uiclData->welcomeScreen;
+	if (ws.srcRect.isEmpty() || ws.destRect.isEmpty()) {
+		return;
+	}
+	const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+	_drawSurface.blitFrom(_spritesImage, ws.srcRect,
+							Common::Point(ws.destRect.left - chunkOrigin.x,
+											ws.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawBackLabel() {
+	const UICL::ThreeRectWidget &back = _uiclData->subButtons[2];
+	if (back.srcRectIdle.isEmpty() || back.destRect.isEmpty()) {
+		return;
+	}
+
+	const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+	_drawSurface.blitFrom(_spritesImage, back.srcRectIdle,
+							Common::Point(back.destRect.left - chunkOrigin.x,
+											back.destRect.top - chunkOrigin.y));
+}
+
 void CellPhonePopup::drawDirectoryArrows() {
-	// Sub-buttons 1 and 2 carry the up/down scroll arrows for list modes.
+	// Up/down scroll arrows are not in the chrome image; blit on every redraw.
 	const UICL::ThreeRectWidget &up = _uiclData->subButtons[1];
 	const UICL::ThreeRectWidget &down = _uiclData->subButtons[2];
 
@@ -524,6 +543,21 @@ void CellPhonePopup::drawDirectoryArrows() {
 								Common::Point(down.destRect.left - chunkOrigin.x,
 												down.destRect.top - chunkOrigin.y));
 	}
+
+	// Selection indicator next to the active row.
+	const Common::Rect &arrowSrc = _uiclData->dirArrowSrc;
+	if (arrowSrc.isEmpty()) {
+		return;
+	}
+	const uint maxRows = maxDirectoryRows();
+	if (_directorySelection >= maxRows) {
+		return;
+	}
+	const Common::Rect rowRect = directoryRowRect(_directorySelection);
+	const int arrowX = MAX(0, rowRect.left - arrowSrc.width() - 2);
+	const int arrowY = rowRect.top;
+	_drawSurface.blitFrom(_spritesImage, arrowSrc,
+							Common::Point(arrowX, arrowY));
 }
 
 // --------------------------------------------------------------------
@@ -535,14 +569,12 @@ void CellPhonePopup::resetDialPad() {
 }
 
 void CellPhonePopup::enterScreenState(ScreenState newState) {
-	// Always redraw, even when newState == _screenState, so successive
-	// digit entries in kDialing each refresh the readout.
+	// Always redraw, so successive digit entries refresh the readout.
 	_screenState = newState;
 	drawScreenContent();
 }
 
 void CellPhonePopup::appendDigit(byte slotIndex) {
-	// 11-char cap matches the LCD readout buffer width.
 	if (_dialedNumber.size() >= 11) {
 		return;
 	}
@@ -561,8 +593,7 @@ bool CellPhonePopup::playSoundIfPresent(const Common::Path &soundName) {
 
 	_callSound.name = nameStr;
 	if (_callSound.channelID == 0) {
-		// TODO: read the channel from the chunk's per-call channel slot
-		// instead of hardcoding it.
+		// TODO: should come from the per-call channel slot in the chunk.
 		_callSound.channelID = 28;
 	}
 	_callSound.volume = 100;
@@ -586,23 +617,25 @@ int CellPhonePopup::findContactByDialBuffer() const {
 		return -1;
 	}
 
-	// Contact prefix bytes are slot indices (0..9); _dialedNumber holds
-	// '0'..'9' chars, so convert each char back to its slot index.
+	// Dial pattern lives in prefix[2..], terminated by '\n'.
 	const uint dialLen = _dialedNumber.size();
+	const uint kDialOffset = 2;
 	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
 		const UICL::Contact &c = _uiclData->contacts[i];
+		if (!isContactVisible(c)) {
+			continue;
+		}
 		bool match = true;
 		for (uint b = 0; b < dialLen; ++b) {
 			const byte slotIdx = (byte)(_dialedNumber[b] - '0');
-			if (slotIdx != c.unknownPrefix[b]) {
+			if (kDialOffset + b >= sizeof(c.unknownPrefix) ||
+					slotIdx != c.unknownPrefix[kDialOffset + b]) {
 				match = false;
 				break;
 			}
 		}
-		// Require the prefix to terminate at the dialed length so a
-		// partial dial doesn't match a longer number.
-		if (match && dialLen < sizeof(c.unknownPrefix) &&
-				c.unknownPrefix[dialLen] == '\n') {
+		if (match && kDialOffset + dialLen < sizeof(c.unknownPrefix) &&
+				c.unknownPrefix[kDialOffset + dialLen] == '\n') {
 			return (int)i;
 		}
 	}
@@ -614,31 +647,35 @@ void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
 		return;
 	}
 
-	// Suffix layout: [0..1]=sceneID, [4..5]=eventFlag label, [6]=eventFlag value.
 	const UICL::Contact &c = _uiclData->contacts[contactIndex];
 
 	const uint16 sceneID = (uint16)c.unknownSuffix[0] | ((uint16)c.unknownSuffix[1] << 8);
-	if (sceneID == 9999) {
-		return; // "no scene" sentinel
+	if (sceneID == kNoScene) {
+		return;
 	}
+	const uint16 frameID = (uint16)c.unknownSuffix[2] | ((uint16)c.unknownSuffix[3] << 8);
 	const int16 eventFlagLabel = (int16)((uint16)c.unknownSuffix[4] |
 											((uint16)c.unknownSuffix[5] << 8));
 	const byte eventFlagValue = c.unknownSuffix[6];
 
 	SceneChangeDescription scene;
 	scene.sceneID = sceneID;
-	scene.frameID = 0;
+	scene.frameID = frameID;
 	scene.verticalOffset = 0;
-	scene.continueSceneSound = kContinueSceneSound;
+	// The destination scene's sound carries the conversation audio.
+	scene.continueSceneSound = kLoadSceneSound;
 
 	if (eventFlagLabel != -1) {
 		NancySceneState.setEventFlag(eventFlagLabel,
 										eventFlagValue ? g_nancy->_true : g_nancy->_false);
 	}
 
+	// Pushed so AR 128 in the conversation scene can return the player here.
+	NancySceneState.pushScene();
+
 	NancySceneState.changeScene(scene);
 
-	setVisible(false);
+	// Phone stays on screen through the conversation; AR 128 closes it.
 }
 
 // --------------------------------------------------------------------
@@ -646,7 +683,6 @@ void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
 // --------------------------------------------------------------------
 
 uint CellPhonePopup::maxDirectoryRows() const {
-	// fontId2 is the list font; fontId1 is the (larger) dial readout.
 	const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
 	if (!font) {
 		return 0;
@@ -657,15 +693,10 @@ uint CellPhonePopup::maxDirectoryRows() const {
 		return 0;
 	}
 
-	// TODO: derive the real visible-row count from the layout
-	// FUN_004d8476 sets up at directory entry (case 0xe). For now,
-	// cap at the LCD height divided by row pitch.
-	const int rowH = arrow.height() + 8;
-	if (rowH <= 0) {
-		return 0;
-	}
-	// Approximate LCD strip height as welcomeScreen height.
-	const int lcdH = _uiclData->welcomeScreen.destRect.height();
+	const int rowH = MAX(arrow.height() + 4, 14);
+	const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
+	const int firstRowOffset = 22;
+	const int lcdH = MAX(0, ws.height() - firstRowOffset);
 	return MAX<int>(1, lcdH / rowH);
 }
 
@@ -675,27 +706,66 @@ Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
 		return Common::Rect();
 	}
 
-	// Row layout: X start = dirArrowSrc.right + 5,
-	//             Y start = dirArrowSrc.top - 5,
-	//             pitch  = dirArrowSrc.height() + 8.
 	const Common::Rect &arrow = _uiclData->dirArrowSrc;
-	const int rowH = arrow.height() + 8;
-	const int xScreen = arrow.right + 5;
-	const int yScreen = arrow.top - 5 + (int)visibleIndex * rowH;
-	const int x = xScreen - _screenPosition.left;
-	const int y = yScreen - _screenPosition.top;
-	const int width = _screenPosition.width() - x - 4;
-	return Common::Rect(x, y, x + width, y + rowH);
+	const int rowH = MAX(arrow.height() + 4, 14);
+	const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
+	const int lcdLeft = ws.left - _screenPosition.left;
+	const int lcdTop  = ws.top  - _screenPosition.top;
+	const int lcdWidth = ws.width();
+	const int firstRowOffset = 22;
+
+	const int x = lcdLeft + arrow.width() + 4;
+	const int y = lcdTop + firstRowOffset + (int)visibleIndex * rowH;
+	const int width = MAX(0, lcdWidth - (arrow.width() + 4) - 2);
+	// Clamp to the LCD so a row rect can't leak onto the keypad below.
+	const int lcdBottom = lcdTop + ws.height();
+	const int rowBottom = MIN(y + rowH, lcdBottom);
+	if (rowBottom <= y) {
+		return Common::Rect();
+	}
+	return Common::Rect(x, y, x + width, rowBottom);
+}
+
+bool CellPhonePopup::isContactVisible(const UICL::Contact &c) const {
+	const uint16 flag = (uint16)c.unknownPrefix[0] | ((uint16)c.unknownPrefix[1] << 8);
+	if (flag == 10) {
+		return true;
+	}
+	if (flag == 11) {
+		return false;
+	}
+	return NancySceneState.getEventFlag((int16)flag, g_nancy->_true);
+}
+
+Common::Rect CellPhonePopup::backLabelHitRect() const {
+	// Back overlays the Web/Dir label area. Returns popup-local coordinates.
+	Common::Rect hit;
+	const Common::Rect &web = _uiclData->webLabel.destRect;
+	const Common::Rect &dir = _uiclData->dirLabel.destRect;
+	if (!web.isEmpty()) {
+		hit = web;
+	}
+	if (!dir.isEmpty()) {
+		if (hit.isEmpty()) {
+			hit = dir;
+		} else {
+			hit.extend(dir);
+		}
+	}
+	if (hit.isEmpty()) {
+		return hit;
+	}
+	hit.translate(-_screenPosition.left, -_screenPosition.top);
+	return hit;
 }
 
 int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
-	// Mirrors the dedup walk in drawDirectoryList.
 	Common::Array<Common::String> seenNames;
 	uint visited = 0;
 	uint visibleSoFar = 0;
 	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
 		const UICL::Contact &c = _uiclData->contacts[i];
-		if (c.name.empty()) {
+		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
 		bool duplicate = false;
@@ -722,8 +792,47 @@ int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
 	return -1;
 }
 
+void CellPhonePopup::moveDirectorySelection(int delta) {
+	if (_screenState != kDirectory || delta == 0) {
+		return;
+	}
+
+	const uint total = deduplicatedContactCount();
+	const uint maxRows = maxDirectoryRows();
+	if (total == 0 || maxRows == 0) {
+		return;
+	}
+
+	uint absolute = _directoryScroll + _directorySelection;
+
+	if (delta < 0) {
+		const uint dec = (uint)(-delta);
+		if (dec >= absolute) {
+			absolute = 0;
+		} else {
+			absolute -= dec;
+		}
+	} else {
+		absolute += (uint)delta;
+		if (absolute >= total) {
+			absolute = total - 1;
+		}
+	}
+
+	if (absolute < _directoryScroll) {
+		_directoryScroll = absolute;
+		_directorySelection = 0;
+	} else if (absolute >= _directoryScroll + maxRows) {
+		_directorySelection = maxRows - 1;
+		_directoryScroll = absolute - _directorySelection;
+	} else {
+		_directorySelection = absolute - _directoryScroll;
+	}
+
+	drawScreenContent();
+}
+
 uint CellPhonePopup::directoryRowAt(const Common::Point &chunkMouse) const {
-	// directoryRowRect returns popup-local rects; convert the mouse too.
 	const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
 									chunkMouse.y - _screenPosition.top);
 	const uint maxRows = maxDirectoryRows();
@@ -741,16 +850,14 @@ void CellPhonePopup::startCallToContact(uint contactIndex) {
 	}
 	const UICL::Contact &c = _uiclData->contacts[contactIndex];
 
-	// Rebuild _dialedNumber from the contact prefix (slot indices,
-	// terminated by '\n') so the call flow's lookup matches.
+	// Rebuild _dialedNumber so the call flow's lookup matches.
 	_dialedNumber.clear();
-	for (uint b = 0; b < sizeof(c.unknownPrefix); ++b) {
+	for (uint b = 2; b < sizeof(c.unknownPrefix); ++b) {
 		const byte v = c.unknownPrefix[b];
 		if (v == '\n') {
 			break;
 		}
 		if (v > 9) {
-			// Non-digit slot index: abort so the call hits kInvalidNumber.
 			_dialedNumber.clear();
 			break;
 		}
@@ -766,7 +873,7 @@ uint CellPhonePopup::deduplicatedContactCount() const {
 	Common::Array<Common::String> seen;
 	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
 		const UICL::Contact &c = _uiclData->contacts[i];
-		if (c.name.empty()) {
+		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
 		bool dup = false;
@@ -788,7 +895,6 @@ uint CellPhonePopup::deduplicatedContactCount() const {
 // --------------------------------------------------------------------
 
 Common::Point CellPhonePopup::mouseToChunkCoords(const Common::Point &mouse) const {
-	// Chunk destRects are screen coords, so chunk-mouse == screen-mouse.
 	return mouse;
 }
 
@@ -797,14 +903,14 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		return;
 	}
 
-	// Transient call-flow states ignore everything except the close X.
+	// Mid-call states accept only the close X.
 	const bool transientCallState =
 		_screenState == kPlaceCall || _screenState == kWaitOutgoingRing ||
 		_screenState == kLookupContact || _screenState == kWaitPickup ||
 		_screenState == kConnected || _screenState == kInvalidNumber ||
 		_screenState == kWaitInvalid;
 
-	// Close (X) — checked first so it wins overlap.
+	// Close (X) wins on overlap.
 	if (_uiclData->header.secondaryButtonEnabled) {
 		const UIButtonRecord &closeBtn = _uiclData->header.secondaryButton;
 		Common::Rect closeScreen = closeBtn.destRect;
@@ -831,51 +937,61 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 	}
 
 	if (transientCallState) {
-		if (_screenPosition.contains(input.mousePos)) {
-			input.eatMouseInput();
-		}
+		// Block the viewport from seeing the cursor (edge-pan, etc.).
+		input.eatMouseInput();
 		return;
 	}
 
 	const Common::Point chunkMouse = mouseToChunkCoords(input.mousePos);
 
-	// Directory mode: scroll arrows + click-to-call on a contact row.
 	if (_screenState == kDirectory) {
 		const Common::Rect &upDst = _uiclData->subButtons[1].destRect;
 		const Common::Rect &downDst = _uiclData->subButtons[2].destRect;
 
+		// Up/down move the selection; scrolling kicks in at page edges.
 		if (upDst.contains(chunkMouse)) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
-				if (_directoryScroll > 0) {
-					--_directoryScroll;
-					drawScreenContent();
-				}
+				moveDirectorySelection(-1);
 				input.eatMouseInput();
 				return;
 			}
 		} else if (downDst.contains(chunkMouse)) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
-				const uint total = deduplicatedContactCount();
-				const uint rows = maxDirectoryRows();
-				const uint maxScroll = total > rows ? total - rows : 0;
-				if (_directoryScroll < maxScroll) {
-					++_directoryScroll;
-					drawScreenContent();
-				}
+				moveDirectorySelection(+1);
+				input.eatMouseInput();
+				return;
+			}
+		}
+
+		// Invisible Back hotspot. Gated so it can't intercept up/down clicks.
+		const Common::Rect backHit = backLabelHitRect();
+		const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
+										chunkMouse.y - _screenPosition.top);
+		const bool overUpDown =
+			upDst.contains(chunkMouse) || downDst.contains(chunkMouse);
+		if (!overUpDown && !backHit.isEmpty() && backHit.contains(popupMouse)) {
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_directoryScroll = 0;
+				_directorySelection = 0;
+				_dialedNumber.clear();
+				enterScreenState(kWelcome);
 				input.eatMouseInput();
 				return;
 			}
 		}
 
+		// Row click selects (the call button does the actual call).
 		const uint row = directoryRowAt(chunkMouse);
 		if (row != (uint)-1) {
 			const int contactIdx = contactIndexForVisibleRow(row);
 			if (contactIdx >= 0) {
 				g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 				if (input.input & NancyInput::kLeftMouseButtonUp) {
-					startCallToContact((uint)contactIdx);
+					_directorySelection = row;
+					drawScreenContent();
 					input.eatMouseInput();
 					return;
 				}
@@ -885,11 +1001,38 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		// can override the directory by starting a fresh dial.
 	}
 
+	// Call/talk button. Checked before the dial-pad loop so an overlapping
+	// slot can't eat it. The keypad Talk key is slot 12; the callButton
+	// widget covers a different region.
+	if (_uiclData->callButton.destRect.contains(chunkMouse) ||
+			_uiclData->dialPadSlots[12].destRect.contains(chunkMouse)) {
+		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			if (!_noSignal) {
+				if (_screenState == kDirectory) {
+					const int contactIdx =
+						contactIndexForVisibleRow(_directorySelection);
+					if (contactIdx >= 0) {
+						// Pre-resolve so kLookupContact skips the dial-buffer
+						// match and the ring/pickup animation still plays.
+						_resolvedContact = contactIdx;
+						enterScreenState(kPlaceCall);
+					}
+				} else if (!_dialedNumber.empty()) {
+					enterScreenState(kPlaceCall);
+				}
+			}
+			input.eatMouseInput();
+			return;
+		}
+	}
+
 	// Dial-pad slot behaviour:
 	//   0..9   - digit input
-	//   10, 11 - *, # (no-op for now)
-	//   12     - welcome / dial toggle (TODO: hook up)
-	//   13     - web mode (TODO: not implemented)
+	//   10, 11 - *, # (no-op)
+	//   12     - call/talk key (handled above)
+	//   13     - web mode (TODO)
 	//   14     - directory toggle
 	int newHovered = -1;
 	for (uint i = 0; i < UICL::kNumDialPadSlots; ++i) {
@@ -919,42 +1062,24 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 			} else if (newHovered == 14) {
 				if (_screenState == kDirectory) {
 					_directoryScroll = 0;
+					_directorySelection = 0;
 					enterScreenState(kWelcome);
 				} else {
 					_dialedNumber.clear();
 					_directoryScroll = 0;
+					_directorySelection = 0;
 					enterScreenState(kDirectory);
 				}
 			}
-			// TODO: slots 10/11 (*, #), slot 12 (mode toggle), slot 13
-			// (web mode) are unimplemented; they fall through as no-ops.
-
-			input.eatMouseInput();
-			return;
-		}
-	}
-
-	// Call button — disabled while in no-signal mode.
-	if (_uiclData->callButton.destRect.contains(chunkMouse)) {
-		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
-
-		if (input.input & NancyInput::kLeftMouseButtonUp) {
-			if (!_dialedNumber.empty() && !_noSignal) {
-				enterScreenState(kPlaceCall);
-			}
 			input.eatMouseInput();
 			return;
 		}
 	}
 
-	// TODO: sub-buttons (10 entries in _uiclData->subButtons) are not
-	// hooked up — they drive email/search/help/browser modes and the
-	// in-call menu in the original.
+	// TODO: sub-buttons (email/search/help/browser/in-call menu) not hooked up.
 
-	// Swallow any remaining click so it doesn't fall through to the scene.
-	if (_screenPosition.contains(input.mousePos)) {
-		input.eatMouseInput();
-	}
+	// Block the viewport from acting on the cursor while the phone is up.
+	input.eatMouseInput();
 }
 
 } // End of namespace UI
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 2d3ca4ffe1c..80a09e83066 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -23,25 +23,17 @@
 #define NANCY_UI_CELLPHONEPOPUP_H
 
 #include "engines/nancy/commontypes.h"
+#include "engines/nancy/enginedata.h"
 #include "engines/nancy/renderobject.h"
-#include "engines/nancy/sound.h"
 
 namespace Nancy {
 
 struct NancyInput;
-struct UICL;
 
 namespace UI {
 
-// Nancy 10+ cell phone popup driven by the UICL chunk.
-//
-// Implemented modes: welcome screen, call-placement (dial → ring →
-// lookup → pickup → connect → invalid), directory list, no-signal
-// overlay.
-//
-// TODO: email, contact search, help, and browser modes are not
-// implemented; web/email/redial sub-buttons and the in-call menu
-// are unhooked.
+// Nancy 10+ cell phone popup, driven by the UICL chunk.
+// TODO: email, search, help, browser modes; in-call menu; redial.
 class CellPhonePopup : public RenderObject {
 public:
 	CellPhonePopup();
@@ -56,29 +48,26 @@ public:
 	void close();
 	void toggle() { if (_isVisible) close(); else open(); }
 
-	// Replaces the welcome graphic with the No Signal / No Access /
-	// Old Email Only labels and disables outgoing calls.
-	// TODO: hook this up to the scene event flag that drives it in
-	// the original engine.
+	// Swaps the welcome graphic for the No Signal / No Access / Old Email
+	// Only labels and blocks outgoing calls. TODO: hook to scene flag.
 	void setNoSignal(bool noSignal);
 
 private:
 	enum ScreenState : int {
-		kWelcome          = 0,  // idle / dial-pad accepting input
-		kDialing          = 1,  // digit accumulating
-		kPlaceCall        = 2,  // start outgoing-ring sound
+		kWelcome          = 0,
+		kDialing          = 1,
+		kPlaceCall        = 2,
 		kWaitOutgoingRing = 3,
-		kLookupContact    = 4,  // match dial buffer, start pickup
+		kLookupContact    = 4,
 		kWaitPickup       = 5,
-		kConnected        = 6,  // trigger contact scene change
-		kInvalidNumber    = 7,  // start invalid-number sound
+		kConnected        = 6,
+		kInvalidNumber    = 7,
 		kWaitInvalid      = 8,
-		kDirectory        = 9   // contact list, scrollable
+		kDirectory        = 9
 	};
 
-	// Drawing helpers
-	void drawChrome();              // popup overlay + persistent labels + buttons
-	void drawScreenContent();       // LCD area: dispatches on _screenState
+	void drawChrome();
+	void drawScreenContent();
 	void drawStatusIcons();
 	void drawWebDirLabels();
 	void drawDialLabel();
@@ -92,8 +81,9 @@ private:
 	void drawDirectoryList();
 	void drawDirHeading();
 	void drawDirectoryArrows();
+	void drawWelcomeScreen();
+	void drawBackLabel();
 
-	// State machine helpers
 	void resetDialPad();
 	void enterScreenState(ScreenState newState);
 	void appendDigit(byte slotIndex);
@@ -102,52 +92,47 @@ private:
 	void triggerContactCallSceneChange(uint contactIndex);
 	int findContactByDialBuffer() const;
 
-	// Directory helpers
 	uint maxDirectoryRows() const;
 	uint directoryRowAt(const Common::Point &chunkMouse) const;
 	Common::Rect directoryRowRect(uint visibleIndex) const;
 	void startCallToContact(uint contactIndex);
 	// Visible (deduplicated) row -> raw contact index, or -1.
 	int contactIndexForVisibleRow(uint visibleRow) const;
-	// Total deduplicated contacts in the directory list.
 	uint deduplicatedContactCount() const;
+	// True when the contact's visibility flag is currently unlocked.
+	bool isContactVisible(const UICL::Contact &c) const;
+	// Popup-local rect of the Back hotspot in directory mode.
+	Common::Rect backLabelHitRect() const;
+	// Move the directory selection by delta, scrolling as needed.
+	void moveDirectorySelection(int delta);
 
-	// Mouse hit-test helpers
 	Common::Point mouseToChunkCoords(const Common::Point &mouse) const;
 
 	const UICL *_uiclData;
 
-	// Chrome image (header.imageName).
+	// Chrome (header.imageName) and sprite atlas (overlayImageName).
 	Graphics::ManagedSurface _overlayImage;
-	// Sprite atlas (overlayImageName). Source for every non-chrome
-	// blit: status icons, labels, headings, call/close buttons.
 	Graphics::ManagedSurface _spritesImage;
 
 	bool _closeButtonHovered = false;
 
 	ScreenState _screenState = kWelcome;
 
-	// '0'..'9' chars of the digits the player has dialed. Convert with
-	// `c - '0'` to recover the slot index that the contact prefix stores.
+	// Dialed digits as '0'..'9' chars; convert with `c - '0'` to get
+	// the slot index that matches a contact's dial prefix.
 	Common::String _dialedNumber;
 
-	// Active dial-tone / ring / pickup / invalid-number cue. The
-	// original engine reuses one channel for all of them.
 	SoundDescription _callSound;
 
-	// Contact index resolved during kLookupContact, valid through
-	// kWaitPickup; -1 means the number didn't match.
+	// Resolved during kLookupContact, valid through kWaitPickup; -1 = miss.
 	int _resolvedContact = -1;
 
-	// Dial-pad slot under the cursor, or -1.
 	int _hoveredSlot = -1;
 
-	// Index of the first deduplicated contact rendered in directory mode.
-	// Updated by the up/down arrow sub-buttons.
+	// First visible deduplicated contact, and the active row within the page.
 	uint _directoryScroll = 0;
+	uint _directorySelection = 0;
 
-	// When true, the LCD shows the status labels and outgoing calls
-	// are blocked. See setNoSignal().
 	bool _noSignal = false;
 };
 


Commit: 36257c744ea4583a0a202819d2e56a7cb4c58417
    https://github.com/scummvm/scummvm/commit/36257c744ea4583a0a202819d2e56a7cb4c58417
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:27+03:00

Commit Message:
NANCY: Implement CellPhonePopCellSceneFromStack for Nancy10+

Used to return to the current scene after placing a phone call

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


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 0c741ba7905..0fb762b9254 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -332,6 +332,9 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new PopInvViewPriorScene();
 	case 126:
 		return new GoInvViewScene();
+	case 128:
+		// Nancy 10+
+		return new CellPhonePopCellSceneFromStack();
 	case 130:
 		// Nancy 10+
 		warning("ChangeCellPhoneInfo - not implemented yet");
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index d3fad02f548..b0663a851cf 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -238,6 +238,23 @@ void AddSearchLink::execute() {
 	finishExecution();
 }
 
+void CellPhonePopCellSceneFromStack::readData(Common::SeekableReadStream &stream) {
+	_sceneChange.readData(stream);
+}
+
+void CellPhonePopCellSceneFromStack::execute() {
+	if (_sceneChange.sceneID == kNoScene) {
+		NancySceneState.popScene(false);
+	} else {
+		NancySceneState.changeScene(_sceneChange);
+	}
+
+	// Conversation is over; take the phone down.
+	NancySceneState.getCellPhonePopup().close();
+
+	finishExecution();
+}
+
 void BumpPlayerClock::readData(Common::SeekableReadStream &stream) {
 	_relative = stream.readByte();
 	_hours = stream.readUint16LE();
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 38f6ce69234..1bf6885b2b1 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -187,6 +187,19 @@ protected:
 	Common::String getRecordTypeName() const override { return "AddSearchLink"; }
 };
 
+// Returns from a cellphone-driven conversation scene to the pre-call scene.
+// sceneID == kNoScene pops the saved scene; any other sceneID overrides it.
+class CellPhonePopCellSceneFromStack : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	SceneChangeDescription _sceneChange;
+
+protected:
+	Common::String getRecordTypeName() const override { return "CellPhonePopCellSceneFromStack"; }
+};
+
 // Changes the in-game time. Used prior to the introduction of SetPlayerClock.
 class BumpPlayerClock : public ActionRecord {
 public:


Commit: d0aea692cccb553232e9fef8d83fc889bad36e08
    https://github.com/scummvm/scummvm/commit/d0aea692cccb553232e9fef8d83fc889bad36e08
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:28+03:00

Commit Message:
NANCY: Add information about cell phone contacts fields

Changed paths:
    engines/nancy/enginedata.h


diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 5c4dc7146e1..cbac93d172a 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -583,9 +583,17 @@ struct UICL : public EngineData {
 	};
 
 	struct Contact {
-		byte unknownPrefix[13];   // 13 bytes preceding the name (purpose not yet determined)
-		Common::String name;      // 20-byte null-terminated contact name
-		byte unknownSuffix[8];    // 8 bytes following the name (purpose not yet determined)
+		// Prefix layout:
+		//   [0..1]   visibility flag (10 = always, 11 = never, else =
+		//            scene event-flag index; contact hidden until set).
+		//   [2..8]   7-digit dial pattern (slot indices 0..9).
+		//   [9]      '\n' terminator.
+		//   [10..12] unused.
+		byte unknownPrefix[13];
+		Common::String name;      // 20-byte null-terminated
+		// Suffix layout: [0..1] sceneID, [2..3] frameID,
+		// [4..5] event-flag label, [6] event-flag value, [7] unused.
+		byte unknownSuffix[8];
 	};
 
 	struct SrcDestRectPair {


Commit: 4c107f1d83027ca53125ac4df75d28fb96b4afcd
    https://github.com/scummvm/scummvm/commit/4c107f1d83027ca53125ac4df75d28fb96b4afcd
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:28+03:00

Commit Message:
NANCY: Don't try to load viewport videos in scenes that don't have them

Happens with cell phone scenes in Nancy10+

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


diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index a1506be3ee4..8af99d22288 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1011,12 +1011,18 @@ void Scene::load(bool fromSaveFile) {
 		_sceneState.currentScene.paletteID = 0;
 	}
 
-	_viewport.loadVideo(_sceneState.summary.videoFile,
-						_sceneState.currentScene.frameID,
-						_sceneState.currentScene.verticalOffset,
-						_sceneState.summary.panningType,
-						_sceneState.summary.videoFormat,
-						_sceneState.summary.palettes.size() ? _sceneState.summary.palettes[(byte)_sceneState.currentScene.paletteID] : Common::Path());
+	if (_sceneState.summary.videoFile != "NO_ART_SCENE") {
+		const Common::Path palettePath = !_sceneState.summary.palettes.empty() ?
+			_sceneState.summary.palettes[(byte)_sceneState.currentScene.paletteID] :
+			Common::Path();
+
+		_viewport.loadVideo(_sceneState.summary.videoFile,
+							_sceneState.currentScene.frameID,
+							_sceneState.currentScene.verticalOffset,
+							_sceneState.summary.panningType,
+							_sceneState.summary.videoFormat,
+							palettePath);
+	}
 
 	if (_viewport.getFrameCount() <= 1) {
 		_viewport.disableEdges(kLeft | kRight);


Commit: 02cd306964d52076e70849a6003aeefa664ded55
    https://github.com/scummvm/scummvm/commit/02cd306964d52076e70849a6003aeefa664ded55
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:30+03:00

Commit Message:
NANCY: Return an empty string for captions that aren't found

This is a workaround, for now, and happens with some conversations in
Nancy10 (e.g. when calling the Rawleys)

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


diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 7f9c4e4efd5..d2622ddc3e0 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -185,7 +185,9 @@ void ConversationSound::readTerseCaptionText(Common::SeekableReadStream &stream)
 	const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
 	assert(convo);
 
-	_text = convo->texts[key];
+	// WORKAROUND: Return an empty string for captions that aren't found.
+	// Happens with some conversations in Nancy10 (e.g. when calling the Rawleys).
+	_text = convo->texts.getValOrDefault(key, "");
 }
 
 void ConversationSound::readTerseResponseText(Common::SeekableReadStream &stream, ResponseStruct &response) {


Commit: 834fe18348c273b7d4f702e4f21092183a97eb1c
    https://github.com/scummvm/scummvm/commit/834fe18348c273b7d4f702e4f21092183a97eb1c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:31+03:00

Commit Message:
NANCY: Refresh the cursor when a piece is selected in multibuildpuzzle

Addresses the remaining issue of not showing the correct mouse cursor
during closeups

Fix #16743

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


diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index ab3b651f2cb..88ec7581676 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -436,6 +436,8 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
 	}
 
 	if (_selectedPiece != -1) {
+		g_nancy->_cursor->setCursorType(dragCursor);
+
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
 			int sel = _selectedPiece;
 			_selectedPiece = -1;


Commit: 6fd36716c955a45108f4edaa709f4edf92497373
    https://github.com/scummvm/scummvm/commit/6fd36716c955a45108f4edaa709f4edf92497373
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:32+03:00

Commit Message:
NANCY: More work on inventory item handling for Nancy10+

Changed paths:
    engines/nancy/ui/inventorypopup.cpp


diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index a352af534b0..eb24e8693b9 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -127,10 +127,17 @@ void InventoryPopup::rebuildVisibleList() {
 	const uint16 numItems = MIN<uint16>(g_nancy->getStaticData().numItems,
 										_invData->itemDescriptions.size());
 
+	const int16 heldItem = NancySceneState.getHeldItem();
+
 	for (uint16 id = 0; id < numItems; ++id) {
 		if (NancySceneState.hasItem(id) != g_nancy->_true)
 			continue;
 
+		// `hasItem` reports the held item as owned; the player is already
+		// carrying it, so don't list it in the grid too.
+		if ((int16)id == heldItem)
+			continue;
+
 		const INV::ItemDescription &desc = _invData->itemDescriptions[id];
 
 		switch (_activeFilterIndex) {
@@ -304,6 +311,14 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		input.mousePos.x - _screenPosition.left + _uiivData->header.normalDestRect.left,
 		input.mousePos.y - _screenPosition.top  + _uiivData->header.normalDestRect.top);
 
+	// Nancy 10+ only blends the held-item sprite onto kNormal / kHotspot
+	// cursors. Switch to kHotspot when holding so the item stays visible
+	// while hovering popup widgets.
+	const CursorManager::CursorType hoverCursor =
+		NancySceneState.getHeldItem() == -1
+			? CursorManager::kHotspotArrow
+			: CursorManager::kHotspot;
+
 	// Scrollbar interaction takes priority while dragging.
 	const UISliderRecord &slider = _uiivData->header.slider;
 	if (_uiivData->header.sliderEnabled) {
@@ -322,7 +337,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		const bool overScrollbar = thumbInChunk.contains(chunkMouse);
 
 		if (_scrollbarDragging) {
-			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			g_nancy->_cursor->setCursorType(hoverCursor);
 
 			const int newThumbTop = chunkMouse.y - _scrollbarGrabOffset;
 			const int clamped = CLIP<int>(newThumbTop, track.top, track.top + travel);
@@ -345,7 +360,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 			_needsRedraw = true;
 		}
 		if (overScrollbar) {
-			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			g_nancy->_cursor->setCursorType(hoverCursor);
 			if (slider.isDraggable && (input.input & NancyInput::kLeftMouseButtonDown)) {
 				_scrollbarDragging = true;
 				_scrollbarGrabOffset = chunkMouse.y - thumbY;
@@ -373,7 +388,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 			_needsRedraw = true;
 		}
 		if (overClose) {
-			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+			g_nancy->_cursor->setCursorType(hoverCursor);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				input.eatMouseInput();
 				close();
@@ -382,28 +397,68 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		}
 	}
 
-	int newHovered = -1;
+	// If the player is already holding an item, any click on the slot grid
+	// puts it back into the inventory. Otherwise a click on an occupied
+	// slot picks that item up (or navigates to its close-up scene).
+	const int16 heldItem = NancySceneState.getHeldItem();
+
+	int hoveredSlot = -1;
 	for (uint i = 0; i < kSlotsPerPage; ++i) {
 		if (i >= _uiivData->slotDestRects.size())
 			break;
-		if (_slotItemIDs[i] < 0)
+		if (!_uiivData->slotDestRects[i].contains(chunkMouse))
 			continue;
-		if (_uiivData->slotDestRects[i].contains(chunkMouse)) {
-			newHovered = (int)i;
-			break;
-		}
+		if (heldItem == -1 && _slotItemIDs[i] < 0)
+			continue;
+		hoveredSlot = (int)i;
+		break;
 	}
 
-	if (newHovered != -1) {
-		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+	if (hoveredSlot != -1) {
+		g_nancy->_cursor->setCursorType(hoverCursor);
 
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
-			const int16 itemID = _slotItemIDs[newHovered];
-			if (itemID >= 0) {
-				// Pick the item up: it leaves the inventory and becomes
-				// the held item (cursor sprite)
-				NancySceneState.removeItemFromInventory(itemID, true);
+			if (heldItem != -1) {
+				const int16 slotItem = _slotItemIDs[hoveredSlot];
+
+				// Empty slot: drop the held item back into the inventory
+				// and keep the popup open. Occupied slot: swap — held
+				// item goes into the inventory, the clicked item becomes
+				// the new held item.
+				NancySceneState.addItemToInventory(heldItem);
+				if (slotItem >= 0 && slotItem != heldItem) {
+					NancySceneState.removeItemFromInventory(slotItem, true);
+				}
 				refreshGrid();
+				input.eatMouseInput();
+				return;
+			}
+
+			const int16 itemID = _slotItemIDs[hoveredSlot];
+			if (itemID >= 0) {
+				const INV::ItemDescription &item = _invData->itemDescriptions[itemID];
+				const byte disabled = NancySceneState.getItemDisabledState(itemID);
+
+				if (disabled) {
+					if (disabled == 2) {
+						NancySceneState.playItemCantSound(itemID);
+					}
+					input.eatMouseInput();
+					return;
+				}
+
+				const bool pickUp = item.keepItem != kInvItemNewSceneView;
+				NancySceneState.removeItemFromInventory(itemID, pickUp);
+
+				if (item.keepItem == kInvItemNewSceneView) {
+					// Close-up view: stash the item and warp to its scene.
+					NancySceneState.pushScene(itemID);
+					SceneChangeDescription sceneChange;
+					sceneChange.sceneID = item.sceneID;
+					sceneChange.continueSceneSound = item.sceneSoundFlag;
+					NancySceneState.changeScene(sceneChange);
+				}
+
 				close();
 				input.eatMouseInput();
 				return;
@@ -433,7 +488,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 			continue;
 		}
 
-		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+		g_nancy->_cursor->setCursorType(hoverCursor);
 
 		drawFilterTab(i, true);
 




More information about the Scummvm-git-logs mailing list