[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