[Scummvm-git-logs] scummvm master -> 9f31d256415fb6a667dc4bad037da4c59384ef5d

bluegr noreply at scummvm.org
Sun Jun 7 05:55:36 UTC 2026


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

Summary:
c1653febe9 NANCY: More work on the gridmappuzzle in Nancy10
c6df72b129 NANCY: Fix incoming call sequence in Nancy10+
9f31d25641 NANCY: Use the per-frame blit array when checking for overlay hotspots


Commit: c1653febe9650952aa4738bae345151b3b77b2c5
    https://github.com/scummvm/scummvm/commit/c1653febe9650952aa4738bae345151b3b77b2c5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-07T08:55:22+03:00

Commit Message:
NANCY: More work on the gridmappuzzle in Nancy10

- Glyph sprites are now drawn correctly
- Glyphs now appear correctly when discovering them
- Glyphs now render correctly when placing them on the map
- Glyphs can only be dropped at specific locations on the map grid
- Letters are rendered correctly when dropping glyphs on the map

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


diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index 0fee0e4d43a..21d1ccbc0f4 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -78,9 +78,9 @@ void GridMapPuzzle::readData(Common::SeekableReadStream &stream) {
 	stream.skip(1);
 
 	for (int i = 0; i < kMaxItems; ++i)
-		_leftHalfIdx[i] = stream.readSint16LE();
+		_letterByMapRow[i] = stream.readSint16LE();
 	for (int i = 0; i < kMaxItems; ++i)
-		_rightHalfIdx[i] = stream.readSint16LE();
+		_letterByMapCol[i] = stream.readSint16LE();
 	for (int i = 0; i < kMaxItems; ++i)
 		_resultSlot[i] = stream.readSint16LE();
 
@@ -131,6 +131,7 @@ void GridMapPuzzle::readData(Common::SeekableReadStream &stream) {
 
 void GridMapPuzzle::initState() {
 	GridMapPuzzleData *gmd = (GridMapPuzzleData *)NancySceneState.getPuzzleData(GridMapPuzzleData::getTag());
+
 	if (_retainState && gmd && gmd->itemState.size() >= (uint)_numItems * 6) {
 		const Common::Array<int16> &state = gmd->itemState;
 		for (int i = 0; i < (int)_numItems; ++i) {
@@ -141,17 +142,18 @@ void GridMapPuzzle::initState() {
 			_items[i].itemsRow = state[i * 6 + 4];
 			_items[i].itemsCol = state[i * 6 + 5];
 		}
-		return;
+	} else {
+		for (int i = 0; i < kMaxItems; ++i)
+			_items[i] = ItemSlot();
 	}
 
-	for (int i = 0; i < kMaxItems; ++i)
-		_items[i] = ItemSlot();
-
-	// Items with their autoplace flag set start placed in the items grid
-	// (acting as the source/inventory). Other items stay hidden until released
-	// by gameplay events outside this puzzle.
+	// Always re-check autoplace flags: items released after the last save
+	// (e.g. discovered by examining a wall between visits) should appear in
+	// the items grid even when restoring a previous state.
 	const int itemsCols = MAX<int>(1, (int)_itemsCols);
 	for (int i = 0; i < (int)_numItems; ++i) {
+		if (_items[i].inMap || _items[i].inItems)
+			continue;
 		if (_autoPlaceFlag[i] != -1 && NancySceneState.getEventFlag(_autoPlaceFlag[i], g_nancy->_true)) {
 			_items[i].inItems = true;
 			_items[i].itemsRow = (int16)(i / itemsCols);
@@ -189,6 +191,9 @@ void GridMapPuzzle::init() {
 	g_nancy->_resource->loadImage(_boardImageName, _boardImage);
 	_boardImage.setTransparentColor(_drawSurface.getTransparentColor());
 
+	g_nancy->_resource->loadImage(_cursorImageName, _cursorImage);
+	_cursorImage.setTransparentColor(_drawSurface.getTransparentColor());
+
 	initState();
 
 	_heldItem = -1;
@@ -256,14 +261,16 @@ void GridMapPuzzle::execute() {
 }
 
 Common::Rect GridMapPuzzle::mapCellRect(int row, int col) const {
-	int x = (int)_mapOriginX + col * ((int)_mapSpacingX + _mapCellW);
-	int y = (int)_mapOriginY + row * ((int)_mapSpacingY + _mapCellH);
+	// Stride uses the raw src-rect dimensions (right - left, before readRect's
+	// inclusive→exclusive +1), to match the original's cell layout.
+	int x = (int)_mapOriginX + col * ((int)_mapSpacingX + _mapCellW - 1);
+	int y = (int)_mapOriginY + row * ((int)_mapSpacingY + _mapCellH - 1);
 	return Common::Rect(x, y, x + _mapCellW, y + _mapCellH);
 }
 
 Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
-	int x = (int)_itemsOriginX + col * ((int)_itemsSpacingX + _itemsCellW);
-	int y = (int)_itemsOriginY + row * ((int)_itemsSpacingY + _itemsCellH);
+	int x = (int)_itemsOriginX + col * ((int)_itemsSpacingX + _itemsCellW - 1);
+	int y = (int)_itemsOriginY + row * ((int)_itemsSpacingY + _itemsCellH - 1);
 	return Common::Rect(x, y, x + _itemsCellW, y + _itemsCellH);
 }
 
@@ -309,20 +316,24 @@ int GridMapPuzzle::findItemInItems(int row, int col) const {
 	return -1;
 }
 
-Common::Rect GridMapPuzzle::resultsCellRect(int row, int col) const {
-	int x = (int)_resultsOriginX + col * ((int)_resultsSpacingX + _resultsCellW);
-	int y = (int)_resultsOriginY + row * ((int)_resultsSpacingY + _resultsCellH);
-	return Common::Rect(x, y, x + _resultsCellW, y + _resultsCellH);
-}
-
-bool GridMapPuzzle::isCorrectMapPlacement(int item, int row, int col) const {
+bool GridMapPuzzle::isValidMapSlot(int row, int col) const {
+	// A map cell is a real placement slot iff at least one solution places
+	// some item there. Empty map cells outside any solution reject drops.
 	for (int s = 0; s < (int)_numSolutions; ++s) {
-		if (_solutionRows[s][item] == (int16)row && _solutionCols[s][item] == (int16)col)
-			return true;
+		for (int i = 0; i < (int)_numItems; ++i) {
+			if (_solutionRows[s][i] == (int16)row && _solutionCols[s][i] == (int16)col)
+				return true;
+		}
 	}
 	return false;
 }
 
+Common::Rect GridMapPuzzle::resultsCellRect(int row, int col) const {
+	int x = (int)_resultsOriginX + col * ((int)_resultsSpacingX + _resultsCellW - 1);
+	int y = (int)_resultsOriginY + row * ((int)_resultsSpacingY + _resultsCellH - 1);
+	return Common::Rect(x, y, x + _resultsCellW, y + _resultsCellH);
+}
+
 void GridMapPuzzle::handleInput(NancyInput &input) {
 	if (_state != kRun || _subState != kPlaying)
 		return;
@@ -332,6 +343,7 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 
 	if (_heldItem != -1 && _heldDrawPos != mouseVP) {
 		_heldDrawPos = mouseVP;
+		_skipHeldDraw = false;
 		redraw();
 	}
 
@@ -359,6 +371,16 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 	if (!(input.input & NancyInput::kLeftMouseButtonUp))
 		return;
 
+	// Reset the held-draw suppression flag on every click; the swap branch
+	// below sets it back to true when appropriate.
+	_skipHeldDraw = false;
+
+	// Sync the held draw position to the click point. Mouse-move tracking
+	// only runs while something is held, so without this a pickup right
+	// after a plain drop would render the new held glyph at the previous
+	// drop's coordinates for one frame.
+	_heldDrawPos = mouseVP;
+
 	int existingItem = hitMap ? findItemInMap(row, col) : findItemInItems(iRow, iCol);
 
 	if (_heldItem == -1) {
@@ -374,23 +396,27 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 			g_nancy->_sound->playSound(_pickupSound);
 		}
 	} else {
-		// Drops onto the map grid only succeed when the cell is the correct
-		// placement for the held item per any solution; otherwise the cell
-		// rejects the drop and the item stays held.
-		if (hitMap && !isCorrectMapPlacement(_heldItem, row, col))
+		// Drop. Map cells only accept the held item if they are real
+		// placement slots (i.e. used by some solution); items cells always
+		// accept. An occupied cell sends its current occupant back to the
+		// cursor so the player can swap.
+		if (hitMap && !isValidMapSlot(row, col))
 			return;
 
 		if (existingItem != -1) {
-			// Items-grid cells allow swap: the existing item becomes the new
-			// held one. Map cells are gated by the correctness check above, so
-			// they can't be occupied by anything except the held item itself.
-			if (hitMap)
-				return;
-			_items[existingItem].inItems = false;
-			_items[_heldItem].inItems = true;
-			_items[_heldItem].itemsRow = (int16)iRow;
-			_items[_heldItem].itemsCol = (int16)iCol;
+			if (hitMap) {
+				_items[existingItem].inMap = false;
+				_items[_heldItem].inMap = true;
+				_items[_heldItem].mapRow = (int16)row;
+				_items[_heldItem].mapCol = (int16)col;
+			} else {
+				_items[existingItem].inItems = false;
+				_items[_heldItem].inItems = true;
+				_items[_heldItem].itemsRow = (int16)iRow;
+				_items[_heldItem].itemsCol = (int16)iCol;
+			}
 			_heldItem = existingItem;
+			_skipHeldDraw = true;
 			if (_pickupSound.name != "NO SOUND") {
 				g_nancy->_sound->loadSound(_pickupSound);
 				g_nancy->_sound->playSound(_pickupSound);
@@ -446,6 +472,8 @@ void GridMapPuzzle::redraw() {
 
 	for (int i = 0; i < (int)_numItems; ++i) {
 		const ItemSlot &slot = _items[i];
+		// Small map-glyph atlas is on the board image; large items-grid
+		// sprites and the drag cursor are on the cursor image.
 		if (slot.inMap && slot.mapRow >= 0 && slot.mapCol >= 0) {
 			const Common::Rect &src = _mapItemSrcRects[i];
 			if (!src.isEmpty()) {
@@ -457,14 +485,16 @@ void GridMapPuzzle::redraw() {
 			const Common::Rect &src = _itemsItemSrcRects[i];
 			if (!src.isEmpty()) {
 				Common::Rect dst = itemsCellRect(slot.itemsRow, slot.itemsCol);
-				_drawSurface.blitFrom(_boardImage, src, Common::Point(dst.left, dst.top));
+				_drawSurface.blitFrom(_cursorImage, src, Common::Point(dst.left, dst.top));
 			}
 		}
 	}
 
 	// Each item placed in the map contributes two letter halves to the results
-	// bar (its assigned slot indexes a pair of adjacent cells). When all items
-	// are correctly placed the letters spell out the solution sentence.
+	// bar at the item's fixed slot. The letters themselves are looked up from
+	// the placement coordinates: column picks the left half, row the right.
+	// When the right items end up at the right cells the strip spells out
+	// the solution sentence.
 	const int resultsCols = MAX<int>(1, (int)_resultsCols);
 	for (int i = 0; i < (int)_numItems; ++i) {
 		if (!_items[i].inMap)
@@ -475,8 +505,10 @@ void GridMapPuzzle::redraw() {
 		const int base = slot * 2;
 		const int row  = base / resultsCols;
 		const int col  = base % resultsCols;
-		const int li = _leftHalfIdx[i];
-		const int ri = _rightHalfIdx[i];
+		const int mapRow = (int)_items[i].mapRow;
+		const int mapCol = (int)_items[i].mapCol;
+		const int li = (mapCol >= 0 && mapCol < kMaxItems) ? _letterByMapCol[mapCol] : -1;
+		const int ri = (mapRow >= 0 && mapRow < kMaxItems) ? _letterByMapRow[mapRow] : -1;
 		if (li >= 0 && li < kMaxResultRects && !_resultSrcRects[li].isEmpty()) {
 			Common::Rect dst = resultsCellRect(row, col);
 			_drawSurface.blitFrom(_boardImage, _resultSrcRects[li], Common::Point(dst.left, dst.top));
@@ -487,14 +519,14 @@ void GridMapPuzzle::redraw() {
 		}
 	}
 
-	if (_heldItem >= 0 && _heldItem < (int)_numItems) {
+	if (_heldItem >= 0 && _heldItem < (int)_numItems && !_skipHeldDraw) {
 		const Common::Rect &src = _itemsItemSrcRects[_heldItem].isEmpty()
 		                          ? _mapItemSrcRects[_heldItem]
 		                          : _itemsItemSrcRects[_heldItem];
 		if (!src.isEmpty()) {
 			int x = _heldDrawPos.x - src.width()  / 2;
 			int y = _heldDrawPos.y - src.height() / 2;
-			_drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
+			_drawSurface.blitFrom(_cursorImage, src, Common::Point(x, y));
 		}
 	}
 
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.h b/engines/nancy/action/puzzle/gridmappuzzle.h
index 4630e56513b..0a0b6c53ba1 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.h
+++ b/engines/nancy/action/puzzle/gridmappuzzle.h
@@ -93,15 +93,18 @@ protected:
 	Common::Rect _itemsItemSrcRects[kMaxItems];
 	Common::Rect _resultSrcRects[kMaxResultRects];
 
-	int16 _leftHalfIdx[kMaxItems];
-	int16 _rightHalfIdx[kMaxItems];
-	int16 _resultSlot[kMaxItems];
+	// Result-letter lookups are indexed by the placed item's MAP coordinates,
+	// not by its item index. _letterByMapCol[mapCol] is rendered as the left
+	// half of the item's letter pair; _letterByMapRow[mapRow] as the right.
+	int16 _letterByMapRow[kMaxItems] = {};
+	int16 _letterByMapCol[kMaxItems] = {};
+	int16 _resultSlot[kMaxItems] = {};
 
-	int16 _autoPlaceFlag[kMaxItems];
+	int16 _autoPlaceFlag[kMaxItems] = {};
 
 	uint16 _numSolutions = 0;
-	int16 _solutionRows[kMaxSolutions][kMaxItems];
-	int16 _solutionCols[kMaxSolutions][kMaxItems];
+	int16 _solutionRows[kMaxSolutions][kMaxItems] = {};
+	int16 _solutionCols[kMaxSolutions][kMaxItems] = {};
 
 	SoundDescription _pickupSound;
 	SoundDescription _placeSound;
@@ -129,9 +132,14 @@ protected:
 	ItemSlot _items[kMaxItems];
 	int      _heldItem = -1;
 	Common::Point _heldDrawPos;
+	// Set on a swap (drop onto an occupied cell) so the freshly-placed
+	// glyph isn't covered by the picked-up one drawn at the cursor.
+	// Cleared on the next mouse move.
+	bool     _skipHeldDraw = false;
 	bool     _isSolved = false;
 
 	Graphics::ManagedSurface _boardImage;
+	Graphics::ManagedSurface _cursorImage; // item sprite atlas (right-side panel)
 
 	void initState();
 	void persistState();
@@ -143,7 +151,7 @@ protected:
 	bool hitTestItems(const Common::Point &p, int &outRow, int &outCol) const;
 	int findItemInMap(int row, int col) const;
 	int findItemInItems(int row, int col) const;
-	bool isCorrectMapPlacement(int item, int row, int col) const;
+	bool isValidMapSlot(int row, int col) const;
 	void checkSolved();
 };
 


Commit: c6df72b129d8daab1c09d348ce98597e36c37792
    https://github.com/scummvm/scummvm/commit/c6df72b129d8daab1c09d348ce98597e36c37792
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-07T08:55:24+03:00

Commit Message:
NANCY: Fix incoming call sequence in Nancy10+

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


diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index f3054b3afbe..7ac1e104dcd 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -221,7 +221,6 @@ void ControlUIItems::execute() {
 			break;
 		case kUITypeCellphone: {
 			UI::CellPhonePopup &phone = NancySceneState.getCellPhonePopup();
-			phone.open();
 
 			if (_startScene != (int16)kNoScene) {
 				SceneChangeDescription scene;
@@ -230,11 +229,10 @@ void ControlUIItems::execute() {
 				scene.verticalOffset = 0;
 				// The destination scene's sound carries the conversation audio.
 				scene.continueSceneSound = kLoadSceneSound;
-
-				// Save the pre-call scene on the popup so AR 128 returns
-				// there without touching the global push slot.
-				phone.setReturnScene(NancySceneState.getSceneInfo());
-				NancySceneState.changeScene(scene);
+				// Phone rings, picks up, and changeScenes into `scene`.
+				phone.startIncomingCall(scene);
+			} else {
+				phone.open();
 			}
 			break;
 		}
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index d9aab409861..dcec5b05f61 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -265,6 +265,21 @@ void CellPhonePopup::open() {
 	}
 }
 
+void CellPhonePopup::startIncomingCall(const SceneChangeDescription &scene) {
+	// open() resets state, so save the pending scene afterwards. Joining
+	// kPlaceCall hands off to the existing ring / pickup / connect chain;
+	// kLookupContact skips the contact lookup when _hasPendingCallScene
+	// is set, and kConnected uses the stored scene for changeScene.
+	if (!_isVisible) {
+		open();
+	}
+	_pendingCallScene = scene;
+	_hasPendingCallScene = true;
+	_resolvedContact = -1;
+	resetDialPad();
+	enterScreenState(kPlaceCall);
+}
+
 void CellPhonePopup::close() {
 	if (!_isVisible) {
 		return;
@@ -274,6 +289,9 @@ void CellPhonePopup::close() {
 		g_nancy->_sound->stopSound(_callSound);
 	}
 
+	// Closing the phone while ringing declines the call.
+	_hasPendingCallScene = false;
+
 	setVisible(false);
 
 	if (!_uiclData->header.sounds[1].name.empty()) {
@@ -314,14 +332,19 @@ void CellPhonePopup::updateGraphics() {
 		break;
 
 	case kLookupContact: {
-		// 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();
+		// Incoming calls already know the destination scene, so the
+		// contact lookup is skipped. Directory-mode outgoing calls
+		// pre-resolve the contact, leaving only dial-buffer lookup.
+		if (!_hasPendingCallScene) {
+			if (_resolvedContact == -1) {
+				_resolvedContact = findContactByDialBuffer();
+			}
+			if (_resolvedContact == -1) {
+				enterScreenState(kInvalidNumber);
+				break;
+			}
 		}
-		if (_resolvedContact == -1) {
-			enterScreenState(kInvalidNumber);
-		} else if (playSoundIfPresent(_uiclData->pickupSound)) {
+		if (playSoundIfPresent(_uiclData->pickupSound)) {
 			enterScreenState(kWaitPickup);
 		} else {
 			enterScreenState(kConnected);
@@ -340,7 +363,15 @@ void CellPhonePopup::updateGraphics() {
 		// 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 &&
+		// Incoming calls carry their destination in _pendingCallScene;
+		// outgoing calls resolve it from the active contact.
+		if (_hasPendingCallScene) {
+			SceneChangeDescription scene = _pendingCallScene;
+			_hasPendingCallScene = false;
+			setReturnScene(NancySceneState.getSceneInfo());
+			NancySceneState.changeScene(scene);
+			resetDialPad();
+		} else if (_resolvedContact >= 0 &&
 				_resolvedContact < (int)_contacts.size()) {
 			triggerContactCallSceneChange((uint)_resolvedContact);
 			_resolvedContact = -1;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 0536c135a7f..9a2759b502b 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -71,6 +71,12 @@ public:
 	void setReturnScene(const SceneChangeDescription &scene);
 	bool consumeReturnScene(SceneChangeDescription &out);
 
+	// Start an incoming-call sequence: opens the popup, stores the
+	// destination scene, and joins the kPlaceCall state chain so it
+	// rings, picks up, shows the connecting sprite, and changeScenes
+	// into `scene` (AR 128 returns via the setReturnScene slot).
+	void startIncomingCall(const SceneChangeDescription &scene);
+
 private:
 	enum ScreenState : int {
 		kWelcome          = 0,
@@ -219,6 +225,11 @@ private:
 	bool _noSignal = false;
 	bool _batteryLow = false;
 
+	// Incoming-call destination (set by startIncomingCall, consumed by the
+	// kConnected handler once the player has answered).
+	SceneChangeDescription _pendingCallScene;
+	bool _hasPendingCallScene = false;
+
 	SceneChangeDescription _returnScene;
 	bool _hasReturnScene = false;
 };


Commit: 9f31d256415fb6a667dc4bad037da4c59384ef5d
    https://github.com/scummvm/scummvm/commit/9f31d256415fb6a667dc4bad037da4c59384ef5d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-07T08:55:25+03:00

Commit Message:
NANCY: Use the per-frame blit array when checking for overlay hotspots

This check is now done like the ones before it, to ensure uniformity

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


diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index f4da96d9787..dcf30656ceb 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -347,7 +347,7 @@ void Overlay::execute() {
 						} else {
 							// nancy3 added a per-frame flag for hotspots. This allows the overlay to be clickable
 							// even without a scene change (useful for setting flags).
-							if (_blitDescriptions[i].hasHotspot == kPlayOverlayWithHotspot) {
+							if (_blitDescriptions[blitsForThisFrame[i]].hasHotspot == kPlayOverlayWithHotspot) {
 								_hotspot = _screenPosition;
 								_hasHotspot = true;
 							}




More information about the Scummvm-git-logs mailing list