[Scummvm-git-logs] scummvm master -> 8df74debc84dd2011cb0a14fe89d0d7e0dec9eee

bluegr noreply at scummvm.org
Mon Jul 6 19:23:15 UTC 2026


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

Summary:
4b78ad6919 NANCY: NANCY10: Remove duplicate click sound when closing popups
60b8026152 nANCY: NANCY10: Cellphone popup fixes
5fc0abe4ed NANCY: Change getTextFromCaseInsensitiveKey to accept a const reference
5bcaa69ee6 NANCY: NANCY11: Implement voice playing for CardGamePuzzle
7c8c909f25 NANCY: NANCY10: Fix tasklist checkbox cursor
8df74debc8 NANCY: NANCY11: Allow pressing button to proceed for KeypadTerse puzzle


Commit: 4b78ad691994286ac5568eb5eadccb36d4a147c4
    https://github.com/scummvm/scummvm/commit/4b78ad691994286ac5568eb5eadccb36d4a147c4
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:22:58+03:00

Commit Message:
NANCY: NANCY10: Remove duplicate click sound when closing popups

Fix #16916

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


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index d03bdd5e232..8c0d583d389 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -277,9 +277,8 @@ void CellPhonePopup::startIncomingCall(const SceneChangeDescription &scene) {
 }
 
 void CellPhonePopup::close() {
-	if (!_isVisible) {
+	if (!_isVisible)
 		return;
-	}
 
 	if (!_callSound.name.empty()) {
 		g_nancy->_sound->stopSound(_callSound);
@@ -289,11 +288,6 @@ void CellPhonePopup::close() {
 	_hasPendingCallScene = false;
 
 	setVisible(false);
-
-	if (!_uiclData->header.sounds[1].name.empty()) {
-		g_nancy->_sound->loadSound(_uiclData->header.sounds[1]);
-		g_nancy->_sound->playSound(_uiclData->header.sounds[1]);
-	}
 }
 
 void CellPhonePopup::updateGraphics() {
@@ -1149,7 +1143,6 @@ void CellPhonePopup::playButtonClickSound(const UIButtonRecord &button) {
 	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND"))
 		return;
 
-	sound.numLoops = 1;
 	g_nancy->_sound->loadSound(sound);
 	g_nancy->_sound->playSound(sound);
 }
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 167b54530fb..97363f952ec 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -141,11 +141,6 @@ void NotebookPopup::close() {
 		return;
 
 	setVisible(false);
-
-	if (!_uinbData->header.sounds[1].name.empty()) {
-		g_nancy->_sound->loadSound(_uinbData->header.sounds[1]);
-		g_nancy->_sound->playSound(_uinbData->header.sounds[1]);
-	}
 }
 
 void NotebookPopup::drawBackground() {
@@ -614,13 +609,9 @@ void NotebookPopup::toggleCheckbox(uint entryIndex) {
 
 void NotebookPopup::playButtonClickSound(const UIButtonRecord &button) {
 	SoundDescription sound = button.clickSound;
-	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND")) {
-		// Fall back to the header's shared button-click slot (2; 0/1 = open/close).
-		sound = _uinbData->header.sounds[2];
-	}
-	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND")) {
+	if (sound.name.empty() || sound.name.equalsIgnoreCase("NO SOUND"))
 		return;
-	}
+
 	g_nancy->_sound->loadSound(sound);
 	g_nancy->_sound->playSound(sound);
 }


Commit: 60b80261522895ee9dc303306b5f69722fa1a834
    https://github.com/scummvm/scummvm/commit/60b80261522895ee9dc303306b5f69722fa1a834
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:23:01+03:00

Commit Message:
nANCY: NANCY10: Cellphone popup fixes

- Fix sound of last keypress before call - fix #16905
- Buttons are now depressed when clicked - fix #16915
- Don't autodial numbers with 7 digits - fix #16917
- Add back button to web and directory screens - fix #16918

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 8c0d583d389..7bdb0691239 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -248,6 +248,8 @@ void CellPhonePopup::open() {
 	_closeButtonHovered = false;
 	_scrollUpHovered = false;
 	_scrollDownHovered = false;
+	_autoDialPending = false;
+	_pressedSlot = -1;
 
 	drawChrome();
 	drawScreenContent();
@@ -286,6 +288,8 @@ void CellPhonePopup::close() {
 
 	// Closing the phone while ringing declines the call.
 	_hasPendingCallScene = false;
+	_autoDialPending = false;
+	_pressedSlot = -1;
 
 	setVisible(false);
 }
@@ -295,6 +299,17 @@ void CellPhonePopup::updateGraphics() {
 		return;
 	}
 
+	// A queued auto-dial / Talk waits for the key's DTMF tone to finish so the
+	// last digit stays audible before the outgoing-ring sound takes over the
+	// shared call-sound channel.
+	if (_autoDialPending) {
+		if (!callSoundIsStillPlaying()) {
+			_autoDialPending = false;
+			enterScreenState(kPlaceCall);
+		}
+		return;
+	}
+
 	// TODO: web/email/search/help/browser modes not implemented.
 	switch (_screenState) {
 	case kWelcome:
@@ -469,10 +484,12 @@ void CellPhonePopup::drawScreenContent() {
 		drawDirectoryList();
 		drawDirectoryArrows();
 		drawHeading(_uiclData->dialHilite);
+		drawBackButton(0);
 		break;
 
 	case kOnlineHub: {
 		drawHeading(_uiclData->onlineHeading);
+		drawBackButton(0);
 		// Email / Web option buttons (subButtons 3 and 4) sit inside the LCD.
 		const UICL::ThreeRectWidget &emailBtn = _uiclData->subButtons[3];
 		const UICL::ThreeRectWidget &webBtn   = _uiclData->subButtons[4];
@@ -516,6 +533,9 @@ void CellPhonePopup::drawScreenContent() {
 		break;
 	}
 
+	// Keypad depress feedback sits on top of everything else.
+	drawPressedDialKey();
+
 	_needsRedraw = true;
 }
 
@@ -1011,6 +1031,23 @@ void CellPhonePopup::drawBackButton(uint subButtonIndex) {
 											back.destRect.top - chunkOrigin.y));
 }
 
+void CellPhonePopup::drawPressedDialKey() {
+	if (_pressedSlot < 0 || _pressedSlot >= (int)UICL::kNumDialPadSlots) {
+		return;
+	}
+	// A dial-pad slot's single srcRect is the lit / depressed key sprite; the
+	// idle keypad is baked into the chrome image. Blit it over the key's dest
+	// rect so the key visibly depresses while held.
+	const UICL::DialPadSlot &slot = _uiclData->dialPadSlots[_pressedSlot];
+	if (slot.srcRect.isEmpty() || slot.destRect.isEmpty()) {
+		return;
+	}
+	const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+	_drawSurface.blitFrom(_spritesImage, slot.srcRect,
+							Common::Point(slot.destRect.left - chunkOrigin.x,
+											slot.destRect.top - chunkOrigin.y));
+}
+
 Common::Rect CellPhonePopup::backButtonHitRect(uint subButtonIndex) const {
 	// Popup-local hit rect for a Back sub-button.
 	Common::Rect r = _uiclData->subButtons[subButtonIndex].destRect;
@@ -1108,19 +1145,15 @@ void CellPhonePopup::appendDigit(byte slotIndex) {
 	_dialedNumber += (char)('0' + slotIndex);
 	enterScreenState(kDialing);
 
-	// Auto-dial without a Talk press: a leading '1' is a full 11-digit number;
-	// anything else is a 7-digit local number that dials as soon as it matches
-	// a contact (or reaches 7 digits, ringing through to "We're sorry").
+	// Auto-dial without a Talk press only once the full 11-digit number has
+	// been entered. The call is queued rather than placed immediately so
+	// updateGraphics can wait for the last key's DTMF tone to finish (which
+	// shares the call-sound channel with the outgoing ring).
 	if (_noSignal) {
 		return;
 	}
-	const bool longDistance = (_dialedNumber[0] == '1');
-	if (longDistance) {
-		if (_dialedNumber.size() >= 11) {
-			enterScreenState(kPlaceCall);
-		}
-	} else if (findContactByDialBuffer() != -1 || _dialedNumber.size() >= 7) {
-		enterScreenState(kPlaceCall);
+	if (_dialedNumber.size() >= 11) {
+		_autoDialPending = true;
 	}
 }
 
@@ -1136,6 +1169,9 @@ void CellPhonePopup::playDialPadSound(const Common::String &name) {
 	sound.numLoops = 1;
 	g_nancy->_sound->loadSound(sound);
 	g_nancy->_sound->playSound(sound);
+	// Track the tone on the call-sound channel so a queued auto-dial / Talk can
+	// wait for it to finish before ringing (see updateGraphics).
+	_callSound = sound;
 }
 
 void CellPhonePopup::playButtonClickSound(const UIButtonRecord &button) {
@@ -1593,6 +1629,24 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		drawScreenContent();
 	}
 
+	// Depress the dial-pad key under the cursor while the mouse button is held;
+	// clear it on release (this runs before the click handlers so the depressed
+	// sprite isn't left behind once the key's action redraws the screen).
+	int newPressed = -1;
+	if ((input.input & (NancyInput::kLeftMouseButtonDown | NancyInput::kLeftMouseButtonHeld)) &&
+			!(input.input & NancyInput::kLeftMouseButtonUp)) {
+		for (uint i = 0; i < UICL::kNumDialPadSlots; ++i) {
+			if (_uiclData->dialPadSlots[i].destRect.contains(chunkMouse)) {
+				newPressed = (int)i;
+				break;
+			}
+		}
+	}
+	if (newPressed != _pressedSlot) {
+		_pressedSlot = newPressed;
+		drawScreenContent();
+	}
+
 	// Help "?" button: opens the help page in the content view. Hidden
 	// (and unclickable) on sub-screens that already show their own heading.
 	if (!isSubScreenState() &&
@@ -1628,8 +1682,9 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 			}
 		}
 
-		// Invisible Back hotspot. Gated so it can't intercept up/down clicks.
-		const Common::Rect backHit = backLabelHitRect();
+		// Visible Back button at the bottom of the display. Gated so it can't
+		// intercept up/down clicks.
+		const Common::Rect backHit = backButtonHitRect(0);
 		const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
 										chunkMouse.y - _screenPosition.top);
 		const bool overUpDown =
@@ -1670,7 +1725,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 										chunkMouse.y - _screenPosition.top);
 		const Common::Rect emailR = hubEmailRect();
 		const Common::Rect webR   = hubWebRect();
-		const Common::Rect backHit = backLabelHitRect();
+		const Common::Rect backHit = backButtonHitRect(0);
 
 		if (emailR.contains(popupMouse)) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
@@ -1882,10 +1937,12 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 						// Pre-resolve so kLookupContact skips the dial-buffer
 						// match and the ring/pickup animation still plays.
 						_resolvedContact = contactIdx;
-						enterScreenState(kPlaceCall);
+						// Defer until the Talk key's tone finishes (see
+						// updateGraphics).
+						_autoDialPending = true;
 					}
 				} else if (!_dialedNumber.empty()) {
-					enterScreenState(kPlaceCall);
+					_autoDialPending = true;
 				}
 			}
 			input.eatMouseInput();
@@ -1923,28 +1980,17 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 				}
 				appendDigit((byte)newHovered);
 			} else if (newHovered == 13) {
-				// Online toggle: opens the Email/Web hub.
-				if (isOnlineMode()) {
-					_directoryScroll = 0;
-					_directorySelection = 0;
-					enterScreenState(kWelcome);
-				} else {
-					_dialedNumber.clear();
-					_directoryScroll = 0;
-					_directorySelection = 0;
-					enterScreenState(kOnlineHub);
-				}
+				// Opens the Email/Web hub. Re-pressing does not toggle back to
+				// the welcome screen — the on-screen Back button does that.
+				_dialedNumber.clear();
+				_directoryScroll = 0;
+				_directorySelection = 0;
+				enterScreenState(kOnlineHub);
 			} else if (newHovered == 14) {
-				if (_screenState == kDirectory) {
-					_directoryScroll = 0;
-					_directorySelection = 0;
-					enterScreenState(kWelcome);
-				} else {
-					_dialedNumber.clear();
-					_directoryScroll = 0;
-					_directorySelection = 0;
-					enterScreenState(kDirectory);
-				}
+				_dialedNumber.clear();
+				_directoryScroll = 0;
+				_directorySelection = 0;
+				enterScreenState(kDirectory);
 			}
 			input.eatMouseInput();
 			return;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 92a7963ed9f..4570424424d 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -111,9 +111,12 @@ private:
 	void drawDirectoryArrows();
 	void drawWelcomeScreen();
 	// Blit a sub-button's idle sprite at its chunk dest (used for the visible
-	// Back buttons: subButtons[0] on the help page, subButtons[7] in the
-	// zoomed email / browser content view).
+	// Back buttons: subButtons[0] on the help / directory / online screens,
+	// subButtons[7] in the zoomed email / browser content view).
 	void drawBackButton(uint subButtonIndex);
+	// Blit the lit key sprite of the currently held dial-pad slot over its
+	// dest rect, so keypad keys visually depress while pressed.
+	void drawPressedDialKey();
 
 	// Generic list renderer used by web / email modes.
 	void drawLinkList();
@@ -237,6 +240,13 @@ private:
 
 	int _hoveredSlot = -1;
 
+	// Dial-pad slot currently held down (shows the lit / depressed key), or -1.
+	int _pressedSlot = -1;
+
+	// A call queued by auto-dial / Talk, waiting for the key's DTMF tone to
+	// finish before entering kPlaceCall (see updateGraphics).
+	bool _autoDialPending = false;
+
 	// First visible deduplicated contact, and the active row within the page.
 	uint _directoryScroll = 0;
 	uint _directorySelection = 0;


Commit: 5fc0abe4eda6bae887c4ff2ed7606bceea460e69
    https://github.com/scummvm/scummvm/commit/5fc0abe4eda6bae887c4ff2ed7606bceea460e69
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:23:02+03:00

Commit Message:
NANCY: Change getTextFromCaseInsensitiveKey to accept a const reference

Changed paths:
    engines/nancy/util.cpp
    engines/nancy/util.h


diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index c8b13d726a1..57e3946334a 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -344,32 +344,33 @@ void assembleTextLine(char *rawCaption, Common::String &output, uint size) {
 	}
 }
 
-Common::String getTextFromCaseInsensitiveKey(Common::HashMap<Common::String, Common::String> texts, Common::String &key) {
+Common::String getTextFromCaseInsensitiveKey(Common::HashMap<Common::String, Common::String> texts, const Common::String &key) {
 	if (texts.contains(key)) {
 		return texts[key];
 	} else {
 		// Nancy10+ searched keyed texts in a key insensitive way, but
 		// the possible permutations involve mainly the last character
 		// being upper or lower case, so just try that before giving up.
-		if (key[key.size() - 1] == toupper(key[key.size() - 1]))
-			key[key.size() - 1] = tolower(key[key.size() - 1]);
+		Common::String keyCopy = key;
+		if (keyCopy[keyCopy.size() - 1] == toupper(keyCopy[keyCopy.size() - 1]))
+			keyCopy[keyCopy.size() - 1] = tolower(keyCopy[keyCopy.size() - 1]);
 		else
-			key[key.size() - 1] = toupper(key[key.size() - 1]);
+			keyCopy[keyCopy.size() - 1] = toupper(keyCopy[keyCopy.size() - 1]);
 
-		if (texts.contains(key))
-			return texts[key];
+		if (texts.contains(keyCopy))
+			return texts[keyCopy];
 
 		// Try all uppercase
-		key.toUppercase();
+		keyCopy.toUppercase();
 
-		if (texts.contains(key))
-			return texts[key];
+		if (texts.contains(keyCopy))
+			return texts[keyCopy];
 
 		// Check for lowercase cases for the second to last character, for Nancy11+
-		key[key.size() - 2] = tolower(key[key.size() - 2]);
+		keyCopy[keyCopy.size() - 2] = tolower(keyCopy[keyCopy.size() - 2]);
 
-		if (texts.contains(key))
-			return texts[key];
+		if (texts.contains(keyCopy))
+			return texts[keyCopy];
 	}
 
 	warning("Key not found: %s", key.c_str());
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 0977792e16f..c60752e1a84 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -65,7 +65,7 @@ void readUISlider(Common::SeekableReadStream &stream, UISliderRecord &dst);
 void readUIPopupHeader(Common::SeekableReadStream &stream, UIPopupHeader &dst);
 void readUIButtonSlot(Common::SeekableReadStream &stream, UIButtonSlot &dst);
 
-Common::String getTextFromCaseInsensitiveKey(Common::HashMap<Common::String, Common::String> texts, Common::String &key);
+Common::String getTextFromCaseInsensitiveKey(Common::HashMap<Common::String, Common::String> texts, const Common::String &key);
 
 // Abstract base class used for loading data that would take too much time in a single frame
 class DeferredLoader {


Commit: 5bcaa69ee617718ed2ec56f7ffd7a5bfd8d26cc9
    https://github.com/scummvm/scummvm/commit/5bcaa69ee617718ed2ec56f7ffd7a5bfd8d26cc9
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:23:03+03:00

Commit Message:
NANCY: NANCY11: Implement voice playing for CardGamePuzzle

Fixes playing the "Go Fish!" puzzle with Jane

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


diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.cpp b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
index 53620380544..df99349ddeb 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.cpp
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
@@ -85,16 +85,22 @@ void CardGamePuzzle::readData(Common::SeekableReadStream &stream) {
 	readFilename(stream, _moveVoiceName);          // 0xbc4
 	readFilename(stream, _dealVoiceName);          // 0xbe5
 	stream.skip(0xc2b - 0xc06);                    // 0xc06 deal SFX (alt)
-	for (int i = 0; i < kMaxCols; ++i)             // 0xc2b AI per-column match table (-> 0xdd8)
+	for (int i = 0; i < kMaxCols; ++i)             // 0xc2b AI per-column ask table (-> 0xdd8)
 		readFilename(stream, _matchVoice[0][i]);
-	stream.skip(0xee0 - 0xdd8);                    // side-0 no-move / made-move lines
+	readFilename(stream, _noMoveVoice[1]);         // 0xdd8 player "go fish"
+	stream.skip(0xe7d - 0xdf9);
+	readFilename(stream, _madeMoveVoice[1]);       // 0xe7d player "here you go"
+	stream.skip(0xee0 - 0xe9e);
 	readFilename(stream, _enemyScoredVoiceName);   // 0xee0
 	stream.skip(0xf22 - 0xf01);                    // alt variant
 	readFilename(stream, _endVoiceName[0]);        // 0xf22 (AI wins)
 	stream.skip(0xf68 - 0xf43);                    // remaining side-0 lines
-	for (int i = 0; i < kMaxCols; ++i)             // 0xf68 player per-column match table (-> 0x1115)
+	for (int i = 0; i < kMaxCols; ++i)             // 0xf68 player per-column ask table (-> 0x1115)
 		readFilename(stream, _matchVoice[1][i]);
-	stream.skip(0x121d - 0x1115);                  // side-1 no-move / made-move lines
+	readFilename(stream, _noMoveVoice[0]);         // 0x1115 AI "go fish"
+	stream.skip(0x11ba - 0x1136);
+	readFilename(stream, _madeMoveVoice[0]);       // 0x11ba AI "here you go"
+	stream.skip(0x121d - 0x11db);
 	readFilename(stream, _playerScoredVoiceName);  // 0x121d (= 0xee0 + 0x33d)
 	stream.skip(0x125f - 0x123e);                  // alt variant
 	readFilename(stream, _endVoiceName[1]);        // 0x125f (player wins, = 0xf22 + 0x33d)
@@ -207,45 +213,112 @@ bool CardGamePuzzle::hasPlayableColumn(int side) const {
 	return false;
 }
 
-// One Go Fish "ask": the side asks the opponent for the given rank. If the opponent holds any cards
-// of it, they all transfer (owning three scores a set) and the side asks again. Otherwise the side
-// draws a card from the deck; the turn then passes, unless the draw-again rule applies and it drew
-// the rank it asked for. Ends the game if the deck runs out on a draw.
-bool CardGamePuzzle::askForColumn(int side, int askedCol) {
+// Transfer every card the opponent holds of the asked rank to the asking side.
+bool CardGamePuzzle::takeCards(int side, int col) {
 	int opponent = side ^ 1;
-	bool took = false;
+	bool moved = false;
 
 	for (int row = 0; row < _numRows; ++row) {
-		if (_board[opponent].grid[row][askedCol] == 1) {
-			_board[opponent].grid[row][askedCol] = 0;
-			_board[opponent].colCount[askedCol]--;
-			_board[side].grid[row][askedCol] = 1;
-			_board[side].colCount[askedCol]++;
-			took = true;
+		if (_board[opponent].grid[row][col] == 1) {
+			_board[opponent].grid[row][col] = 0;
+			_board[opponent].colCount[col]--;
+			_board[side].grid[row][col] = 1;
+			_board[side].colCount[col]++;
+			moved = true;
 		}
 	}
 
-	if (took) {
-		if (_board[side].colCount[askedCol] >= 3 && !_board[side].colComplete[askedCol]) {
-			_board[side].colComplete[askedCol] = 1;
-			_board[side].score++;
-			playVoice(side == 1 ? _playerScoredVoiceName : _enemyScoredVoiceName);
-		} else if (askedCol < kMaxCols) {
-			// A rank-specific line for the taken cards (silent in scenes that don't use them)
-			playVoice(_matchVoice[side][askedCol]);
+	return moved;
+}
+
+// Start an ask: the mover asks the opponent for a rank. Play its "do you have any X?" line; the
+// resolution waits until that line finishes (see updateGraphics/resolveAsk).
+void CardGamePuzzle::beginAsk(int side, int col) {
+	_mover = side;
+	_currentTurn = side;
+	_askedCol = col;
+	playVoice(_matchVoice[side][col]);
+	_phase = kAskSound;
+	drawBoard();
+}
+
+// The ask voice has finished: hand the cards over (or go fish), play the answer, and wait for it and
+// the card-slide to finish before the turn advances.
+void CardGamePuzzle::resolveAsk() {
+	bool before[kMaxRows][kMaxCols];
+	for (int r = 0; r < kMaxRows; ++r)
+		for (int c = 0; c < kMaxCols; ++c)
+			before[r][c] = (_board[1].grid[r][c] == 1);
+
+	if (takeCards(_mover, _askedCol)) {
+		if (_board[_mover].colCount[_askedCol] >= 3 && !_board[_mover].colComplete[_askedCol]) {
+			_board[_mover].colComplete[_askedCol] = 1;
+			_board[_mover].score++;
+			playVoice(_mover == 1 ? _playerScoredVoiceName : _enemyScoredVoiceName);
+		} else {
+			playVoice(_madeMoveVoice[_mover]); // "here you go"
 		}
 
-		return true; // the opponent had the rank: ask again
+		_goAgain = true; // the opponent had the rank: ask again
+	} else {
+		playVoice(_noMoveVoice[_mover]); // "go fish"
+		int drawnCol = dealOne(_mover);
+		_goAgain = (drawnCol != -1 && _switchTurnRule != 0 && drawnCol == _askedCol);
+		if (drawnCol == -1) {
+			endGame();
+		}
 	}
 
-	// Go fish
-	int drawnCol = dealOne(side);
-	if (drawnCol == -1) {
-		endGame();
-		return false;
+	startMoveAnimation(before);
+	_phase = kAnswerSound;
+}
+
+// The answer has finished: the mover asks again (a take, or a draw-again), or the turn passes.
+void CardGamePuzzle::advanceTurn() {
+	int next = _goAgain ? _mover : (_mover ^ 1);
+	if (next == 1) {
+		startPlayerTurn();
+	} else {
+		_aiDelayUntil = g_nancy->getTotalPlayTime() + 700;
+		_phase = kAiDelay;
 	}
+}
+
+// Begin the player's turn. If the player holds no rank they can ask for, they are forced to go fish
+// and the turn passes to the AI.
+void CardGamePuzzle::startPlayerTurn() {
+	_mover = 1;
+	_currentTurn = 1;
 
-	return (_switchTurnRule != 0 && drawnCol == askedCol);
+	if (!hasPlayableColumn(1)) {
+		if (dealOne(1) == -1) {
+			endGame();
+			return;
+		}
+
+		_aiDelayUntil = g_nancy->getTotalPlayTime() + 700;
+		_phase = kAiDelay;
+	} else {
+		_phase = kWaitInput;
+	}
+
+	drawBoard();
+}
+
+// The AI picks a rank it is building and asks for it; with nothing to ask, it goes fish and passes.
+void CardGamePuzzle::startAiAsk() {
+	int col = aiPickColumn();
+	if (col == -1) {
+		if (dealOne(0) == -1) {
+			endGame();
+		} else {
+			startPlayerTurn();
+		}
+		return;
+	}
+
+	_lastAiColumn = col;
+	beginAsk(0, col);
 }
 
 // The AI asks for a rank it already holds one or two of, preferring ranks where it holds two (so a
@@ -280,29 +353,6 @@ int CardGamePuzzle::aiPickColumn() {
 	return -1;
 }
 
-// The AI (side 0) takes its whole turn at once: it keeps asking as long as each ask grants another
-// turn, then control returns to the player. The game ends once a draw finds the deck exhausted.
-void CardGamePuzzle::runAiTurn() {
-	_lastAiColumn = -1;
-
-	for (int guard = 0; !_gameOver && guard < 1000; ++guard) {
-		int col = aiPickColumn();
-		if (col == -1) {
-			// Nothing to ask for: go fish and end the turn.
-			if (dealOne(0) == -1) {
-				endGame();
-			}
-			break;
-		}
-
-		bool goAgain = askForColumn(0, col);
-		_lastAiColumn = col;
-		if (!goAgain) {
-			break;
-		}
-	}
-}
-
 void CardGamePuzzle::endGame() {
 	_gameOver = true;
 
@@ -339,6 +389,7 @@ void CardGamePuzzle::startMoveAnimation(const bool beforeGrid[kMaxRows][kMaxCols
 }
 
 void CardGamePuzzle::updateGraphics() {
+	// Step any in-progress card slide first; the turn stays parked until it finishes.
 	if (_animating) {
 		if (g_nancy->getTotalPlayTime() < _animNextStep) {
 			return;
@@ -355,23 +406,36 @@ void CardGamePuzzle::updateGraphics() {
 		return;
 	}
 
-	// Once the final move has settled and the winner's line has had its moment, transition out.
-	if (_gameOver && _awaitingEnd && g_nancy->getTotalPlayTime() >= _endWaitUntil) {
-		_awaitingEnd = false;
-		_state = kActionTrigger;
+	// Once the final move has settled and the winner's line has finished, transition out.
+	if (_gameOver) {
+		if (_awaitingEnd && g_nancy->getTotalPlayTime() >= _endWaitUntil &&
+				!g_nancy->_sound->isSoundPlaying(_voiceSound)) {
+			_awaitingEnd = false;
+			_state = kActionTrigger;
+		}
 		return;
 	}
 
-	// If it becomes the player's turn but they hold no rank they can ask for, they must go fish and
-	// pass, so the AI takes over. This resolves a turn the player could otherwise never act on.
-	if (_state == kRun && !_gameOver && !_awaitingEnd && _currentTurn == 1 && !hasPlayableColumn(1)) {
-		if (dealOne(1) == -1) {
-			endGame();
-		} else if (!_gameOver) {
-			runAiTurn();
+	switch (_phase) {
+	case kAskSound:
+		// The "do you have any X?" line has played; hand the cards over (or go fish).
+		if (!g_nancy->_sound->isSoundPlaying(_voiceSound)) {
+			resolveAsk();
 		}
-
-		drawBoard();
+		break;
+	case kAnswerSound:
+		// The answer line has played (and the cards have slid); take the next turn.
+		if (!g_nancy->_sound->isSoundPlaying(_voiceSound)) {
+			advanceTurn();
+		}
+		break;
+	case kAiDelay:
+		if (g_nancy->getTotalPlayTime() >= _aiDelayUntil) {
+			startAiAsk();
+		}
+		break;
+	default:
+		break;
 	}
 }
 
@@ -387,6 +451,23 @@ void CardGamePuzzle::playVoice(const Common::String &name) {
 	_voiceSound.volume = 85;
 	g_nancy->_sound->loadSound(_voiceSound);
 	g_nancy->_sound->playSound(_voiceSound);
+
+	showSubtitle(name);
+}
+
+// The card-game lines carry no inline caption; the original looks the subtitle up by sound name in
+// the Autotext table. Mirror that and push it to the textbox (as QuizPuzzle does).
+void CardGamePuzzle::showSubtitle(const Common::String &soundName) {
+	const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+	if (!autotext) {
+		return;
+	}
+
+	Common::String text = getTextFromCaseInsensitiveKey(autotext->texts, soundName);
+	if (!text.empty()) {
+		NancySceneState.getTextbox().clear();
+		NancySceneState.getTextbox().addTextLine(text);
+	}
 }
 
 void CardGamePuzzle::init() {
@@ -409,7 +490,11 @@ void CardGamePuzzle::init() {
 			_availMap[row][col] = (row < _numRows && col < _numCols) ? 1 : 0;
 
 	_deckRemaining = _numCols * _numRows;
+	_mover = _startPlayer;
 	_currentTurn = _startPlayer;
+	_askedCol = -1;
+	_goAgain = false;
+	_phase = kWaitInput;
 	_lastAiColumn = -1;
 	_gameOver = false;
 	_gaveUp = false;
@@ -431,10 +516,14 @@ void CardGamePuzzle::execute() {
 		init();
 		registerGraphics();
 
-		// When the AI is dealt the opening move, it plays out its turn before the player's first ask.
-		if (_startPlayer == 0 && !_gameOver) {
-			runAiTurn();
-			_currentTurn = 1;
+		// Kick off the first turn. When the AI is dealt the opening move, it asks first.
+		if (_startPlayer == 0) {
+			_mover = 0;
+			_currentTurn = 0;
+			_aiDelayUntil = g_nancy->getTotalPlayTime() + 700;
+			_phase = kAiDelay;
+		} else {
+			startPlayerTurn();
 		}
 
 		drawBoard();
@@ -531,7 +620,9 @@ void CardGamePuzzle::handleInput(NancyInput &input) {
 		return;
 	}
 
-	if (_gameOver || _animating || _currentTurn != 1) {
+	// Clicks are only accepted while waiting for the player to ask (not mid-ask, mid-answer, or during
+	// the AI's turn).
+	if (_gameOver || _phase != kWaitInput) {
 		return;
 	}
 
@@ -543,25 +634,7 @@ void CardGamePuzzle::handleInput(NancyInput &input) {
 	g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
 
 	if (input.input & NancyInput::kLeftMouseButtonUp) {
-		bool before[kMaxRows][kMaxCols];
-		for (int r = 0; r < kMaxRows; ++r)
-			for (int c = 0; c < kMaxCols; ++c)
-				before[r][c] = (_board[1].grid[r][c] == 1);
-
-		playVoice(_moveVoiceName);
-		bool goAgain = askForColumn(1, col);
-
-		// A failed ask (go fish) ends the player's turn, so the AI plays out its whole turn before
-		// control returns. A successful ask keeps the turn: the player just asks again.
-		if (_state == kRun && !_gameOver && !goAgain) {
-			runAiTurn();
-		}
-
-		if (_state == kRun && !_gameOver) {
-			startMoveAnimation(before);
-		} else {
-			drawBoard();
-		}
+		beginAsk(1, col);
 	}
 }
 
diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.h b/engines/nancy/action/puzzle/cardgamepuzzle.h
index 50904061582..352335a27ff 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.h
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.h
@@ -27,10 +27,9 @@
 namespace Nancy {
 namespace Action {
 
-// Two-player card game versus an AI opponent, new in Nancy 11 (Curse of Blackmoor Manor, AR 246).
-// Cards are dealt into a shared grid; each player stacks up to three cards per column, and a full
-// column scores a "set". Implementation is staged: this currently parses the data chunk and sets up
-// the board; the deal/match/AI gameplay is being filled in incrementally.
+// A "Go fish!" card game, new in Nancy 11 (Curse of Blackmoor Manor, AR 246).
+// There are two variants, one with the cards placed in a grid vs a human NPC, and
+// one with the cards placed in columns vs an automaton.
 class CardGamePuzzle : public RenderActionRecord {
 public:
 	CardGamePuzzle() : RenderActionRecord(7) {}
@@ -69,16 +68,20 @@ protected:
 	// The column the player is pointing at (owning 1-2 cards in it), or -1 if none is under the mouse.
 	int columnUnderMouse(const Common::Point &mousePos) const;
 	bool hasPlayableColumn(int side) const; // whether a side holds any incomplete rank it can ask for
-	// One "ask" (Go Fish): the side asks the opponent for a rank. If the opponent holds it, take every
-	// card of that rank (scoring a set at three) and ask again; otherwise draw a card ("go fish").
-	// Returns true if the same side should ask again.
-	bool askForColumn(int side, int askedCol);
 	int aiPickColumn();       // the AI's chosen rank to ask for, or -1 if it holds no incomplete rank
-	void runAiTurn();         // the AI asks repeatedly until its turn ends, then detects game over
+
+	// Go Fish turn flow, driven as a small state machine so asks and answers are voiced and sequenced.
+	bool takeCards(int side, int col); // transfer every opponent card of a rank to the side; true if any moved
+	void beginAsk(int side, int col);  // start an ask: play the "do you have any X?" line, enter kAskSound
+	void resolveAsk();                 // ask voice done: take or go fish, play the answer, start its wait
+	void advanceTurn();                // answer done: ask again, or pass the turn to the other side
+	void startPlayerTurn();            // enter kWaitInput (or auto-go-fish + pass if no rank to ask)
+	void startAiAsk();                 // the AI picks a rank and asks, or goes fish and passes
 	void endGame();
 	// Compare side 1's grid against a pre-move snapshot and start sliding the changed cards.
 	void startMoveAnimation(const bool beforeGrid[kMaxRows][kMaxCols]);
 	void playVoice(const Common::String &name); // play a voiced line / SFX on the card-game channel
+	void showSubtitle(const Common::String &soundName); // push the line's AUTOTEXT caption to the textbox
 
 	Common::Path _imageName;
 
@@ -121,9 +124,11 @@ protected:
 	// Voiced lines / SFX (all on the card-game channel). Read selectively from the 0xba3..0x1304 block.
 	Common::String _moveVoiceName;         // data+0xbc4 (card-move SFX)
 	Common::String _dealVoiceName;         // data+0xbe5 (card-deal SFX)
-	Common::String _matchVoice[2][kMaxCols]; // data+0xc2b (AI) / 0xf68 (player), keyed by column
-	Common::String _enemyScoredVoiceName;  // data+0xee0 (a column completed for the AI)
-	Common::String _playerScoredVoiceName; // data+0x121d (a column completed for the player)
+	Common::String _matchVoice[2][kMaxCols]; // the "do you have any X?" ask, by side; 0xc2b (AI) / 0xf68 (player)
+	Common::String _madeMoveVoice[2];      // the "here you go" answer, by side; 0xe7d (player) / 0x11ba (AI)
+	Common::String _noMoveVoice[2];        // the "go fish" answer, by side; 0xdd8 (player) / 0x1115 (AI)
+	Common::String _enemyScoredVoiceName;  // data+0xee0 (a set completed for the AI)
+	Common::String _playerScoredVoiceName; // data+0x121d (a set completed for the player)
 	Common::String _endVoiceName[2];       // data+0xf22 (AI wins) / 0x125f (player wins)
 	SoundDescription _voiceSound;
 
@@ -131,12 +136,27 @@ protected:
 	bool _awaitingEnd = false;
 	uint32 _endWaitUntil = 0;
 
+	// Turn state machine. The mover asks, the ask voice plays, the cards move and the answer voice
+	// plays, then the turn either repeats (a take) or passes. The AI's asks run through the same
+	// phases so its turn is visible (Nancy's cards being taken) and voiced.
+	enum Phase {
+		kWaitInput,   // the player's turn: waiting for a card click
+		kAskSound,    // a "do you have any X?" voice is playing
+		kAnswerSound, // an answer voice is playing and/or the taken cards are sliding
+		kAiDelay      // a short pause before the AI's next ask
+	};
+	Phase _phase = kWaitInput;
+	int _mover = 1;        // which side is currently asking (0 = AI/Jane, 1 = player/Nancy)
+	int _askedCol = -1;    // the rank being asked for
+	bool _goAgain = false; // whether the mover asks again once the answer finishes
+	uint32 _aiDelayUntil = 0;
+
 	// Runtime board state
 	Graphics::ManagedSurface _image;
 	PlayerBoard _board[2];
 	byte _availMap[kMaxRows][kMaxCols]; // shared deck: 1 = card still on the table
 	int _deckRemaining = 0;
-	int _currentTurn = 0;               // which side is to play (the player's side draws the grid)
+	int _currentTurn = 0;               // side owning the turn highlight (mirrors _mover)
 	int _lastAiColumn = -1;             // the AI avoids immediately repeating its previous column
 	bool _gameOver = false;
 


Commit: 7c8c909f25371fec55bb5e8863e19a58213bac2a
    https://github.com/scummvm/scummvm/commit/7c8c909f25371fec55bb5e8863e19a58213bac2a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:23:04+03:00

Commit Message:
NANCY: NANCY10: Fix tasklist checkbox cursor

Fix #16914

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


diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 97363f952ec..cb1bb60a907 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -350,8 +350,11 @@ void NotebookPopup::handleInput(NancyInput &input) {
 		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
 			toggleCheckbox(_checkboxEntryIndices[k]);
-			input.eatMouseInput();
 		}
+		// Swallow the input (as the popup does for its other widgets) so the
+		// viewport / action manager behind the popup don't override the hotspot
+		// cursor we just set.
+		input.eatMouseInput();
 		return;
 	}
 


Commit: 8df74debc84dd2011cb0a14fe89d0d7e0dec9eee
    https://github.com/scummvm/scummvm/commit/8df74debc84dd2011cb0a14fe89d0d7e0dec9eee
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-06T22:23:05+03:00

Commit Message:
NANCY: NANCY11: Allow pressing button to proceed for KeypadTerse puzzle

Fixes adding a sequence and then pressing the red button when playing
against Betty the automaton

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


diff --git a/engines/nancy/action/puzzle/orderingpuzzle.cpp b/engines/nancy/action/puzzle/orderingpuzzle.cpp
index 086f13b429d..532c14520ec 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/orderingpuzzle.cpp
@@ -541,9 +541,10 @@ void OrderingPuzzle::execute() {
 				}
 			}
 
-			if (_puzzleType == kKeypad && _needButtonToCheckSuccess) {
-				// KeypadPuzzle moves to the "success" scene regardless whether the puzzle was solved or not,
-				// provided the check button is pressed.
+			if ((_puzzleType == kKeypad || _puzzleType == kKeypadTerse) && _needButtonToCheckSuccess) {
+				// KeypadPuzzle and KeypadTersePuzzle move to the "success" scene regardless whether the
+				// puzzle was solved or not, provided the check button is pressed. (e.g. the Nancy 11 Betty
+				// automaton keypad, where a red button confirms the entry.)
 				if (_checkButtonPressed) {
 					if (!g_nancy->_sound->isSoundPlaying(_pushDownSound)) {
 						if (solved) {




More information about the Scummvm-git-logs mailing list