[Scummvm-git-logs] scummvm master -> f4cfed09bcfb1dcde3e16126e95c19be24cb7cb8
bluegr
noreply at scummvm.org
Sun Jul 26 02:36:03 UTC 2026
This automated email contains information about 8 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
934494d370 NANCY: NANCY10: Fix grid gaps in GridMapPuzzle, and update comments
8b924e8dbf NANCY: NANCY10: Clip overflowed text in ScrollTextBox
b39d3b3b27 NANCY: NANCY10: More work on the full/mini textbox
28bfc71ac2 NANCY: NANCY10: Fix issues with GridMapPuzzle (Petroglyph puzzle)
957fd44b78 NANCY: NANCY10: Fix cursor when placing pieces in OneBuildPuzzle
85ea8f5e81 NANCY: Handle common sounds from the console play_sound command
6808a7a71a NANCY: NANCY10: Cleanup comments
f4cfed09bc NANCY: Activate all AR records when loading a game
Commit: 934494d370f045374a5ac425e5097765ae05fd9f
https://github.com/scummvm/scummvm/commit/934494d370f045374a5ac425e5097765ae05fd9f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:42+03:00
Commit Message:
NANCY: NANCY10: Fix grid gaps in GridMapPuzzle, and update comments
Fix #16980
Changed paths:
engines/nancy/action/puzzle/gridmappuzzle.cpp
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index b229acd2e21..6aea72e5bef 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -255,16 +255,22 @@ void GridMapPuzzle::execute() {
Common::Rect GridMapPuzzle::mapCellRect(int row, int col) const {
// 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);
+ // inclusiveâexclusive +1). The rect spans the full stride so cells tile the
+ // grid with no gaps, keeping the grab-hand cursor steady across the whole
+ // grid. Only blits read the rect's top-left, so sprites still land correctly.
+ int strideX = (int)_mapSpacingX + _mapCellW - 1;
+ int strideY = (int)_mapSpacingY + _mapCellH - 1;
+ int x = (int)_mapOriginX + col * strideX;
+ int y = (int)_mapOriginY + row * strideY;
+ return Common::Rect(x, y, x + strideX, y + strideY);
}
Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
- 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);
+ int strideX = (int)_itemsSpacingX + _itemsCellW - 1;
+ int strideY = (int)_itemsSpacingY + _itemsCellH - 1;
+ int x = (int)_itemsOriginX + col * strideX;
+ int y = (int)_itemsOriginY + row * strideY;
+ return Common::Rect(x, y, x + strideX, y + strideY);
}
bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol) const {
@@ -347,9 +353,10 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
if (!hitMap)
hitItems = hitTestItems(mouseVP, iRow, iCol);
- // Grid cells (map and items) use the grab-hand cursor; picking something up
- // doesn't change it. The exit hotspot uses the puzzle-exit cursor, and every
- // other area (letter headers, gaps) keeps the idle eyeglass.
+ // Both grids (map and items) show the grab-hand cursor across their whole
+ // area; picking something up doesn't change it. The exit hotspot uses the
+ // puzzle-exit cursor, and everything outside the grids (the letter strips,
+ // the gap between the grids) keeps the idle eyeglass.
if (!hitMap && !hitItems) {
if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
Commit: 8b924e8dbf92e0bc36d1aa1e1a0479b992b10d0c
https://github.com/scummvm/scummvm/commit/8b924e8dbf92e0bc36d1aa1e1a0479b992b10d0c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:43+03:00
Commit Message:
NANCY: NANCY10: Clip overflowed text in ScrollTextBox
Fix #17006
Changed paths:
engines/nancy/ui/scrolltextbox.cpp
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index 9a858580149..ddcf5b4b72b 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -160,15 +160,16 @@ void ScrollTextBox::drawContent() {
scrollY = (int)(_scrollPos * (contentHeight - visibleTextHeight));
}
} else {
- // A strip at the top of the overlay, sized to the text with a two-line
- // minimum. Drop the chrome's bottom margin so it hugs the content.
+ // Fixed two-line strip at the top of the overlay. Overflow is clipped
+ // (hidden beneath the taskbar), matching the original rather than growing
+ // the box down over the UI.
const Font *font = g_nancy->_graphics->getFont(fontID);
const int lineStep = font->getLineHeight() + font->getLineHeight() / 9;
- const int twoLineContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
- const int miniHeight = MIN(viewport.top + MAX(contentHeight, twoLineContent), fullHeight);
+ const int stripContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
+ const int miniHeight = MIN(viewport.top + stripContent, fullHeight);
boxRect = Common::Rect(_fullPopupRect.left, _fullPopupRect.top,
_fullPopupRect.right, _fullPopupRect.top + miniHeight);
- visibleTextHeight = contentHeight;
+ visibleTextHeight = MIN(contentHeight, stripContent);
}
moveTo(boxRect);
Commit: b39d3b3b27a8938ae2dfa2ff3c5c92a29d404b26
https://github.com/scummvm/scummvm/commit/b39d3b3b27a8938ae2dfa2ff3c5c92a29d404b26
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:45+03:00
Commit Message:
NANCY: NANCY10: More work on the full/mini textbox
- Use the correct screen rect for the mini text strip
- Use the correct mini strip width
Fix #17006, #16890
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/enginedata.cpp
engines/nancy/enginedata.h
engines/nancy/state/scene.cpp
engines/nancy/ui/scrolltextbox.cpp
engines/nancy/ui/scrolltextbox.h
engines/nancy/ui/textbox.cpp
engines/nancy/ui/textbox.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 437943d42aa..9255f99ab1e 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -288,7 +288,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ModifyListEntry(ModifyListEntry::kDelete);
case 73:
return new ModifyListEntry(ModifyListEntry::kMark);
- case 74: // Nancy10
+ case 74: // Nancy10 only: writes the full, taskbar-covering box
return new FrameTextBox(true);
case 75:
if (g_nancy->getGameType() <= kGameTypeNancy9)
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index b41ff97a5f8..4254c70a819 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -282,7 +282,7 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
if (g_nancy->getGameType() >= kGameTypeNancy10) {
lineStartXCursor = chunkStream->readUint16LE(); // text left inset
- chunkStream->skip(2); // bottom margin
+ stripRightMargin = chunkStream->readUint16LE(); // strip text right margin (see FrameTextBox)
chunkStream->skip(2); // unused
chunkStream->skip(2); // initial color (we use the AR for this)
}
@@ -894,7 +894,7 @@ TASK::TASK(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
readRect(*chunkStream, srcRect);
readRect(*chunkStream, dstRect);
readRect(*chunkStream, unkRect1);
- readRect(*chunkStream, unkRect2);
+ readRect(*chunkStream, ccTextboxScreenRect);
// The button count varies by game (Nancy12 adds a 6th slot for the coin
// purse), so derive it from the chunk size. A slot marked "NO_UI_ITEM" (e.g.
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index c7e9fd94237..94a60407fe5 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -182,6 +182,7 @@ struct TBOX : public EngineData {
int32 maxScrollWidth = 0;
int32 firstLineY = 0; // added to the y-cursor when starting a new line
uint16 lineStartXCursor = 0; // left inset of the text within the text area
+ uint16 stripRightMargin = 0; // subtracted from contentWidth for the strip's text width
int32 unknown1 = 0;
int32 unknown2 = 0;
int32 contentWidth = 0;
@@ -553,7 +554,9 @@ struct TASK : public EngineData {
Common::Rect srcRect;
Common::Rect dstRect;
Common::Rect unkRect1;
- Common::Rect unkRect2;
+ // Screen rect of the closed-caption text strip the taskbar draws (the
+ // ScrollTextBox's mini strip is positioned to it).
+ Common::Rect ccTextboxScreenRect;
ButtonRecord buttons[kNumButtons];
};
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 3ed6fcee6d3..f43fdd7d813 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1545,7 +1545,7 @@ void Scene::handleInput() {
// original); restored automatically once the popup closes.
_taskbar->setPopupLockout(popupOpen);
}
- if (_taskbar && !_textbox.isFullMode() && !popupOpen) {
+ if (_taskbar && !_textbox.coversTaskbar() && !popupOpen) {
// MENU and HELP leave gameplay entirely, which would cut off the
// taskbar click sound. The original defers the transition until that
// sound finishes, so we hold the click here and only switch state
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index ddcf5b4b72b..8a1749ca56a 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -42,6 +42,7 @@ ScrollTextBox::ScrollTextBox() :
_scrollbarDragging(false),
_scrollbarHovered(false),
_scrollbarGrabOffset(0),
+ _fullMode(false),
_expanded(false),
_fontIDOverride(-1),
_autoClearTime(0) {}
@@ -75,14 +76,22 @@ void ScrollTextBox::init() {
}
moveTo(_fullPopupRect);
+ // The taskbar owns the caption strip, so take the strip's text origin from the
+ // TASK chunk rather than the conversation overlay.
+ auto *task = GetEngineData(TASK);
+ if (task) {
+ _stripScreenRect = task->ccTextboxScreenRect;
+ }
+
Common::Rect bounds = _screenPosition;
bounds.moveTo(0, 0);
_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
- // Key the text surface on the transparent color so only glyphs composite,
- // over the chrome when expanded or onto the taskbar strip when mini.
+ // Key on the transparent color so only glyphs composite. Size for the wider
+ // (strip) layout plus the line-start inset so either fits.
const uint32 transColor = g_nancy->_graphics->getTransColor();
- initSurfaces(textViewportLocal().width(), kHypertextSurfaceHeight,
+ const int surfaceWidth = _tboxData->lineStartXCursor + MAX<int>(stripTextWidth(), textViewportLocal().width());
+ initSurfaces(surfaceWidth, kHypertextSurfaceHeight,
g_nancy->_graphics->getInputPixelFormat(), transColor, transColor);
_fullSurface.setTransparentColor(transColor);
@@ -115,14 +124,19 @@ Common::Rect ScrollTextBox::textViewportLocal() const {
return r;
}
+int ScrollTextBox::stripTextWidth() const {
+ // Strip text spans lineStartXCursor..(contentWidth - stripRightMargin), narrower
+ // than the conversation viewport (matches FrameTextBox's normal path).
+ return (_tboxData->contentWidth - _tboxData->stripRightMargin - _tboxData->lineStartXCursor) + 1;
+}
+
void ScrollTextBox::drawBackground() {
_drawSurface.blitFrom(_overlayImage, _header->normalSrcRect,
Common::Point(0, 0));
_needsRedraw = true;
}
-void ScrollTextBox::drawContent() {
- // Render all text into the scratch surface to measure its height.
+void ScrollTextBox::layoutText(int wrapWidth, uint16 fontID) {
const uint32 transColor = g_nancy->_graphics->getTransColor();
_drawnTextHeight = 0;
_numDrawnLines = 0;
@@ -130,24 +144,34 @@ void ScrollTextBox::drawContent() {
_fullSurface.fillRect(Common::Rect(0, 0, _fullSurface.w, _fullSurface.h), transColor);
_textHighlightSurface.fillRect(Common::Rect(0, 0, _textHighlightSurface.w, _textHighlightSurface.h), transColor);
- Common::Rect textBounds(0, 0, _fullSurface.w, _fullSurface.h);
- // The text's inset within the text area is the line-start X cursor. There is
- // no vertical inset â the first line starts at the top of the text area, so
- // don't add scrollbarDefaultPos (which is the scrollbar's gutter position).
- textBounds.left += _tboxData->lineStartXCursor;
+ // The line-start X cursor is the text's horizontal inset; there is no vertical
+ // inset, so the first line starts at the top of the text area.
+ Common::Rect textBounds(_tboxData->lineStartXCursor, 0,
+ _tboxData->lineStartXCursor + wrapWidth, _fullSurface.h);
+ drawAllText(textBounds, 0, fontID, _tboxData->highlightConversationFontID);
+}
+
+void ScrollTextBox::drawContent() {
+ const uint32 transColor = g_nancy->_graphics->getTransColor();
+ const Common::Rect viewport = textViewportLocal();
+ const int fullHeight = _fullPopupRect.height();
// Narration/caption text uses the (white) highlight font, not the (cyan)
// conversation body font that the ConversationPopup uses.
const uint16 fontID = _fontIDOverride != -1 ? (uint16)_fontIDOverride : _tboxData->highlightConversationFontID;
- drawAllText(textBounds, 0, fontID, _tboxData->highlightConversationFontID);
- // Mini strip vs full overlay with scrollbar, decided by whether the text
- // overflows the viewport (not by line count): a caption that fits stays mini.
- const Common::Rect viewport = textViewportLocal();
- const int fullHeight = _fullPopupRect.height();
- const int contentHeight = getInnerHeight();
+ // Lay out at the strip's text width first, then pick the box: Nancy 11+ by line
+ // count, Nancy 10 by which FrameTextBox AR wrote the text (74 full / 75 strip).
+ layoutText(stripTextWidth(), fontID);
+ _expanded = g_nancy->getGameType() >= kGameTypeNancy11 ? (_numDrawnLines > 2) : _fullMode;
- _expanded = contentHeight > viewport.height();
+ if (_expanded) {
+ // The expanded box reserves the gutter for its slider, so re-lay-out
+ // narrower to clear it.
+ layoutText(viewport.width(), fontID);
+ }
+
+ const int contentHeight = getInnerHeight();
Common::Rect boxRect;
int visibleTextHeight;
@@ -160,13 +184,12 @@ void ScrollTextBox::drawContent() {
scrollY = (int)(_scrollPos * (contentHeight - visibleTextHeight));
}
} else {
- // Fixed two-line strip at the top of the overlay. Overflow is clipped
- // (hidden beneath the taskbar), matching the original rather than growing
- // the box down over the UI.
+ // Fixed two-line strip; overflow is clipped (hidden beneath the taskbar).
+ // Line 3 starts at exactly 2 * lineStep, so the clip must add no padding.
const Font *font = g_nancy->_graphics->getFont(fontID);
const int lineStep = font->getLineHeight() + font->getLineHeight() / 9;
- const int stripContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
- const int miniHeight = MIN(viewport.top + stripContent, fullHeight);
+ const int stripContent = 2 * lineStep;
+ const int miniHeight = MIN(viewport.top + 1 + stripContent, fullHeight);
boxRect = Common::Rect(_fullPopupRect.left, _fullPopupRect.top,
_fullPopupRect.right, _fullPopupRect.top + miniHeight);
visibleTextHeight = MIN(contentHeight, stripContent);
@@ -175,25 +198,26 @@ void ScrollTextBox::drawContent() {
moveTo(boxRect);
_drawSurface.create(boxRect.width(), boxRect.height(), g_nancy->_graphics->getInputPixelFormat());
- int textLeft = viewport.left;
+ // Expanded text clears the slider gutter; the strip aligns to the taskbar's
+ // caption rect (on top of the baked-in lineStartXCursor), or falls back to 0.
+ const bool haveStripRect = !_expanded && !_stripScreenRect.isEmpty();
+ int textLeft = _expanded ? viewport.left
+ : (haveStripRect ? _stripScreenRect.left - _fullPopupRect.left : 0);
int textTop = viewport.top;
if (_expanded) {
+ // The textured overlay panel, with its slider.
setTransparent(false);
drawBackground();
} else {
- // The taskbar draws the strip, so keep mini transparent with no chrome,
- // and pull the text left into the unused scrollbar gap.
+ // The taskbar already draws the strip, so keep it transparent with no chrome.
setTransparent(true);
_drawSurface.clear(transColor);
- // Nudge the first line down one pixel so its top padding matches the
- // original's mini strip.
- textTop += 1;
- const UISliderRecord &sl = _header->slider;
- if (_header->sliderEnabled && !sl.destRect.isEmpty()) {
- // Align the glyphs to the scrollbar's left edge, dropping the
- // line-start inset that is baked into the text surface (the mini
- // strip reclaims the whole gutter).
- textLeft = toPopupLocal(sl.destRect, sl.destUsesGameFrameOffset != 0).left - _tboxData->lineStartXCursor;
+ if (haveStripRect) {
+ textTop = _stripScreenRect.top - _fullPopupRect.top;
+ } else {
+ // Nudge the first line down one pixel so its top padding matches the
+ // original's caption strip.
+ textTop += 1;
}
}
@@ -209,10 +233,8 @@ void ScrollTextBox::drawContent() {
}
void ScrollTextBox::redrawScroll() {
- // Only reachable while dragging the scrollbar, which only exists in the
- // expanded state, so the layout (box rect, surface size, text in _fullSurface)
- // is already settled. Just re-composite the visible slice at the new scroll
- // offset instead of re-running drawContent()'s full text re-layout.
+ // Only runs while dragging the scrollbar, so the layout is already settled:
+ // re-composite the visible slice at the new scroll offset, no re-layout.
const Common::Rect viewport = textViewportLocal();
const int visibleTextHeight = viewport.height();
const int contentHeight = getInnerHeight();
@@ -306,12 +328,27 @@ void ScrollTextBox::setOverrideFont(uint fontID) {
_fontIDOverride = (int)fontID;
}
+void ScrollTextBox::setFullMode(bool fullMode) {
+ // Nancy 11+ has no second box; the mode only matters for Nancy 10.
+ if (g_nancy->getGameType() >= kGameTypeNancy11 || _fullMode == fullMode) {
+ return;
+ }
+
+ _fullMode = fullMode;
+
+ // The AR writes its text before selecting the mode, so re-render.
+ if (_isVisible) {
+ drawContent();
+ }
+}
+
void ScrollTextBox::clear() {
HypertextParser::clear();
_scrollPos = 0.0f;
_scrollbarDragging = false;
_scrollbarHovered = false;
+ _fullMode = false;
_expanded = false;
_fontIDOverride = -1;
_autoClearTime = 0;
@@ -332,7 +369,7 @@ void ScrollTextBox::handleInput(NancyInput &input) {
const Common::Point localMouse = popupLocalMouse(input.mousePos);
const UISliderRecord &slider = _header->slider;
- // The scrollbar only exists in the expanded state.
+ // The scrollbar only exists on the expanded box.
if (_header->sliderEnabled && _expanded) {
const Common::Rect trackLocal = toPopupLocal(slider.destRect, slider.destUsesGameFrameOffset != 0);
const int trackHeight = trackLocal.height();
@@ -357,11 +394,8 @@ void ScrollTextBox::handleInput(NancyInput &input) {
released = true;
}
- // Re-composite only when the thumb moves (or the drag just ended, to
- // repaint the thumb out of its pressed state), and via redrawScroll() --
- // a cheap slice re-blit -- rather than drawContent()'s full text
- // re-layout. This keeps dragging smooth instead of re-rendering the
- // whole text surface each frame.
+ // Re-composite (via the cheap redrawScroll()) only when the thumb moves
+ // or the drag ends, to keep dragging smooth.
if (newScrollPos != _scrollPos || released) {
_scrollPos = newScrollPos;
redrawScroll();
diff --git a/engines/nancy/ui/scrolltextbox.h b/engines/nancy/ui/scrolltextbox.h
index 9b798014a1b..2cd0b9758bc 100644
--- a/engines/nancy/ui/scrolltextbox.h
+++ b/engines/nancy/ui/scrolltextbox.h
@@ -55,14 +55,20 @@ public:
// that many milliseconds (used by inventory closed captions).
void addTextLine(const Common::String &text, uint32 autoClearTime = 0);
void setOverrideFont(uint fontID);
+ // Nancy 10 only: pick the full box (AR 74) or the strip (AR 75). Ignored from
+ // Nancy 11 on, which picks by line count.
+ void setFullMode(bool fullMode);
+ // True while the full box is showing and covering the taskbar buttons.
+ bool coversTaskbar() const { return _isVisible && _expanded; }
void clear() override;
private:
void drawBackground();
void drawContent();
+ // Render the text into the scratch surface, wrapping at wrapWidth.
+ void layoutText(int wrapWidth, uint16 fontID);
// Re-composite the expanded overlay at the current scroll position without
- // re-laying out the text (which drawContent() does). Used while dragging the
- // scrollbar, so a drag is a cheap slice re-blit rather than a full re-render.
+ // re-laying out the text; used for cheap scrollbar-drag updates.
void redrawScroll();
void drawScrollbar(UIButtonState state);
uint16 getInnerHeight() const;
@@ -71,6 +77,9 @@ private:
// viewport brought into the popup surface's space).
Common::Rect textViewportLocal() const;
+ // Width the strip wraps at: its own text area from TBOX, not the viewport.
+ int stripTextWidth() const;
+
// Popup-local bounding rect of the scrollbar thumb at the current scroll
// position, or an empty rect when the slider is disabled.
Common::Rect computeThumbRect() const;
@@ -86,6 +95,9 @@ private:
// The fully-expanded popup rect on screen, with the game-frame offset
// applied. The mini strip shares its top edge.
Common::Rect _fullPopupRect;
+ // Screen rect the taskbar reserves for the caption strip (TASK chunk). The
+ // strip's text is aligned to it; empty when there is no taskbar data.
+ Common::Rect _stripScreenRect;
const TBOX *_tboxData;
Graphics::ManagedSurface _overlayImage;
@@ -95,8 +107,10 @@ private:
bool _scrollbarHovered;
int _scrollbarGrabOffset;
- // True when text overflows the viewport: full overlay with scrollbar.
- // False when it fits: mini strip above the taskbar, sized to content.
+ // Nancy 10 only: true selects the AR 74 full box, false the AR 75 strip.
+ // Defaults false since every writer but AR 74 uses the strip.
+ bool _fullMode;
+ // True for the full, taskbar-covering box (with scrollbar); false for the strip.
bool _expanded;
int _fontIDOverride;
diff --git a/engines/nancy/ui/textbox.cpp b/engines/nancy/ui/textbox.cpp
index e6a2adcc206..d86f359fa13 100644
--- a/engines/nancy/ui/textbox.cpp
+++ b/engines/nancy/ui/textbox.cpp
@@ -119,9 +119,13 @@ void Textbox::updateGraphics() {
RenderObject::updateGraphics();
}
-void Textbox::setFullMode(bool open, uint32 timeoutMs) {
- // No-op: the Nancy 10+ overlay shows itself when it holds text, and the
- // Nancy 1-9 box has no full mode. Kept for the FrameTextBox AR's API.
+bool Textbox::coversTaskbar() const {
+ return _scrollTextBox ? _scrollTextBox->coversTaskbar() : false;
+}
+
+void Textbox::setFullMode(bool open) {
+ if (_scrollTextBox)
+ _scrollTextBox->setFullMode(open);
}
void Textbox::handleInput(NancyInput &input) {
diff --git a/engines/nancy/ui/textbox.h b/engines/nancy/ui/textbox.h
index 9fbc7266139..23080039b12 100644
--- a/engines/nancy/ui/textbox.h
+++ b/engines/nancy/ui/textbox.h
@@ -52,9 +52,13 @@ public:
void addTextLine(const Common::String &text, uint32 autoClearTime = 0);
void setOverrideFont(const uint fontID);
- // Vestigial "full mode" toggle, now a no-op. Kept for the FrameTextBox AR.
- void setFullMode(bool open, uint32 timeoutMs = 15000);
- bool isFullMode() const { return false; }
+ // Nancy 10 only: picks the full, taskbar-covering box (AR 74) over the strip
+ // (AR 75). No effect on Nancy 1-9 (no such mode) or Nancy 11+ (a single box).
+ // The AR owns the full box's auto-close timer.
+ void setFullMode(bool open);
+ // True while the text box visually covers the taskbar buttons, so Scene can
+ // skip taskbar input. Always false before Nancy 10, whose box sits clear of them.
+ bool coversTaskbar() const;
private:
uint16 getInnerHeight() const;
Commit: 28bfc71ac27feeed72f4b0cc53148ba900fef567
https://github.com/scummvm/scummvm/commit/28bfc71ac27feeed72f4b0cc53148ba900fef567
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:47+03:00
Commit Message:
NANCY: NANCY10: Fix issues with GridMapPuzzle (Petroglyph puzzle)
- Allow placing glyphs on any square on the grid, not just right ones
- Use correct graphic when dragging a glyph
- Remove item from inventory when accessing it via GoInvViewScene
- Play the BUDE sound when dropping a held item back to the inventory
Fix #17014
Changed paths:
engines/nancy/action/inventoryrecords.cpp
engines/nancy/action/puzzle/gridmappuzzle.cpp
engines/nancy/action/puzzle/gridmappuzzle.h
engines/nancy/ui/inventorypopup.cpp
diff --git a/engines/nancy/action/inventoryrecords.cpp b/engines/nancy/action/inventoryrecords.cpp
index 64ed1f693c5..7f6948b1dd9 100644
--- a/engines/nancy/action/inventoryrecords.cpp
+++ b/engines/nancy/action/inventoryrecords.cpp
@@ -214,6 +214,10 @@ void GoInvViewScene::execute() {
if (!disabled && item.keepItem == kInvItemNewSceneView) {
if (_addToInventory || NancySceneState.hasItem(_itemID)) {
NancySceneState.pushScene(_itemID);
+ // Viewing an item's close-up takes it out of the inventory; the
+ // re-add on return (popScene, or a cancel scene's AddInventory
+ // record) is what plays the "returned to inventory" click.
+ NancySceneState.removeItemFromInventory(_itemID, false);
} else {
// Do not add the item to the inventory, only go to its scene
NancySceneState.pushScene();
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index 6aea72e5bef..de5eb93673e 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -254,15 +254,13 @@ void GridMapPuzzle::execute() {
}
Common::Rect GridMapPuzzle::mapCellRect(int row, int col) const {
- // Stride uses the raw src-rect dimensions (right - left, before readRect's
- // inclusiveâexclusive +1). The rect spans the full stride so cells tile the
- // grid with no gaps, keeping the grab-hand cursor steady across the whole
- // grid. Only blits read the rect's top-left, so sprites still land correctly.
+ // Sprite destination. Stride uses the raw src-rect dimensions (right - left,
+ // before readRect's inclusiveâexclusive +1). Blits read the rect's top-left.
int strideX = (int)_mapSpacingX + _mapCellW - 1;
int strideY = (int)_mapSpacingY + _mapCellH - 1;
int x = (int)_mapOriginX + col * strideX;
int y = (int)_mapOriginY + row * strideY;
- return Common::Rect(x, y, x + strideX, y + strideY);
+ return Common::Rect(x, y, x + _mapCellW, y + _mapCellH);
}
Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
@@ -270,13 +268,34 @@ Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
int strideY = (int)_itemsSpacingY + _itemsCellH - 1;
int x = (int)_itemsOriginX + col * strideX;
int y = (int)_itemsOriginY + row * strideY;
- return Common::Rect(x, y, x + strideX, y + strideY);
+ return Common::Rect(x, y, x + _itemsCellW, y + _itemsCellH);
+}
+
+Common::Rect GridMapPuzzle::mapCellHitRect(int row, int col) const {
+ // Hit area = sprite rect grown by half the spacing on each side. Neighboring
+ // cells then meet with no gaps, so the whole grid reads as one hand-cursor
+ // region and every point maps to its nearest cell.
+ Common::Rect r = mapCellRect(row, col);
+ r.left -= (int)_mapSpacingX / 2;
+ r.right += (int)_mapSpacingX / 2;
+ r.top -= (int)_mapSpacingY / 2;
+ r.bottom += (int)_mapSpacingY / 2;
+ return r;
+}
+
+Common::Rect GridMapPuzzle::itemsCellHitRect(int row, int col) const {
+ Common::Rect r = itemsCellRect(row, col);
+ r.left -= (int)_itemsSpacingX / 2;
+ r.right += (int)_itemsSpacingX / 2;
+ r.top -= (int)_itemsSpacingY / 2;
+ r.bottom += (int)_itemsSpacingY / 2;
+ return r;
}
bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol) const {
for (int r = 0; r < (int)_mapRows; ++r) {
for (int c = 0; c < (int)_mapCols; ++c) {
- if (mapCellRect(r, c).contains(p)) {
+ if (mapCellHitRect(r, c).contains(p)) {
outRow = r;
outCol = c;
return true;
@@ -289,7 +308,7 @@ bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol)
bool GridMapPuzzle::hitTestItems(const Common::Point &p, int &outRow, int &outCol) const {
for (int r = 0; r < (int)_itemsRows; ++r) {
for (int c = 0; c < (int)_itemsCols; ++c) {
- if (itemsCellRect(r, c).contains(p)) {
+ if (itemsCellHitRect(r, c).contains(p)) {
outRow = r;
outCol = c;
return true;
@@ -315,18 +334,6 @@ int GridMapPuzzle::findItemInItems(int row, int col) const {
return -1;
}
-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) {
- 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);
@@ -397,13 +404,10 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
g_nancy->_sound->playSound(_pickupSound);
}
} else {
- // 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;
-
+ // Drop. Any map or items cell accepts the held item; the wrong
+ // placement just decodes to the wrong letters in the results strip.
+ // An occupied cell sends its current occupant back to the cursor so
+ // the player can swap.
if (existingItem != -1) {
if (hitMap) {
_items[existingItem].inMap = false;
@@ -520,14 +524,14 @@ void GridMapPuzzle::redraw() {
}
}
+ // The glyph following the cursor uses the small map sprite (on the board
+ // image), not the large items-grid sprite.
if (_heldItem >= 0 && _heldItem < (int)_numItems && !_skipHeldDraw) {
- const Common::Rect &src = _itemsItemSrcRects[_heldItem].isEmpty()
- ? _mapItemSrcRects[_heldItem]
- : _itemsItemSrcRects[_heldItem];
+ const Common::Rect &src = _mapItemSrcRects[_heldItem];
if (!src.isEmpty()) {
int x = _heldDrawPos.x - src.width() / 2;
int y = _heldDrawPos.y - src.height() / 2;
- _drawSurface.blitFrom(_cursorImage, src, Common::Point(x, y));
+ _drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
}
}
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.h b/engines/nancy/action/puzzle/gridmappuzzle.h
index 0a0b6c53ba1..c0c964cab45 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.h
+++ b/engines/nancy/action/puzzle/gridmappuzzle.h
@@ -146,12 +146,13 @@ protected:
void redraw();
Common::Rect mapCellRect(int row, int col) const;
Common::Rect itemsCellRect(int row, int col) const;
+ Common::Rect mapCellHitRect(int row, int col) const;
+ Common::Rect itemsCellHitRect(int row, int col) const;
Common::Rect resultsCellRect(int row, int col) const;
bool hitTestMap(const Common::Point &p, int &outRow, int &outCol) const;
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 isValidMapSlot(int row, int col) const;
void checkSolved();
};
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 7009e5108c9..3b9b8bf482c 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -485,6 +485,9 @@ void InventoryPopup::handleInput(NancyInput &input) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
NancySceneState.addItemToInventory(heldItem);
+ // Dropping a held item back plays the button-down sound
+ // layered over the item-added sound.
+ g_nancy->_sound->playSound("BUDE");
}
input.eatMouseInput();
}
Commit: 957fd44b78b6b86d6b810e20a19f5d703738c19b
https://github.com/scummvm/scummvm/commit/957fd44b78b6b86d6b810e20a19f5d703738c19b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:49+03:00
Commit Message:
NANCY: NANCY10: Fix cursor when placing pieces in OneBuildPuzzle
The cursor would incorrectly flash as a magnifying glass when putting
down pieces in places that are not the solution positions.
Fix #16991
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 114f7646558..23dc6d00f28 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -529,11 +529,9 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
return;
}
- // Not dragging: only process when idle
- if (_solveState != kIdle)
- return;
-
- // Find topmost piece under cursor (separately tracking unplaced vs any)
+ // Not dragging: find the topmost piece under the cursor. The hover cursor is
+ // refreshed even while a drop/placement sound plays (non-idle) so a piece put
+ // down off-target keeps the piece cursor; only clicks are gated on kIdle.
int16 topmostUnplaced = -1;
int16 topmostAny = -1;
@@ -553,6 +551,10 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
if (topmostUnplaced != -1)
setPieceCursor();
+ // Clicks are only processed when idle
+ if (_solveState != kIdle)
+ return;
+
// Left click on an unplaced piece: pick it up
// Right click: pick it up and rotate it
bool leftClick = (input.input & NancyInput::kLeftMouseButtonUp);
@@ -576,6 +578,10 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
return;
}
+ // Nothing else is interactive while a drop/placement sound plays
+ if (_solveState != kIdle)
+ return;
+
// Check exit hotspot
Common::Rect exitScreen = NancySceneState.getViewport().convertViewportToScreen(_exitHotspot);
if (exitScreen.contains(input.mousePos)) {
Commit: 85ea8f5e81bf526c53314ccd397af1231713c21a
https://github.com/scummvm/scummvm/commit/85ea8f5e81bf526c53314ccd397af1231713c21a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:51+03:00
Commit Message:
NANCY: Handle common sounds from the console play_sound command
Changed paths:
engines/nancy/console.cpp
engines/nancy/sound.h
diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index 9f076c414e2..a082a464be2 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -414,6 +414,11 @@ bool NancyConsole::Cmd_playSound(int argc, const char **argv) {
return true;
}
+ if (g_nancy->_sound->isCommonSound(argv[1])) {
+ g_nancy->_sound->playSound(argv[1]);
+ return true;
+ }
+
Common::File *f = new Common::File;
if (!f->open(Common::Path(argv[1]).appendInPlace(".his"))) {
debugPrintf("Failed to open '%s.his'\n", argv[1]);
diff --git a/engines/nancy/sound.h b/engines/nancy/sound.h
index 01594f92e11..fc047e7c46b 100644
--- a/engines/nancy/sound.h
+++ b/engines/nancy/sound.h
@@ -122,6 +122,8 @@ public:
Audio::Timestamp getLength(const SoundDescription &description);
Audio::Timestamp getLength(const Common::String &chunkName);
+ bool isCommonSound(const Common::String &soundName) const { return _commonSounds.contains(soundName); }
+
void soundEffectMaintenance();
void recalculateSoundEffects();
Commit: 6808a7a71a144c5086007493d88988d851c198b3
https://github.com/scummvm/scummvm/commit/6808a7a71a144c5086007493d88988d851c198b3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:52+03:00
Commit Message:
NANCY: NANCY10: Cleanup comments
Changed paths:
engines/nancy/ui/textbox.h
diff --git a/engines/nancy/ui/textbox.h b/engines/nancy/ui/textbox.h
index 23080039b12..83894ba5b02 100644
--- a/engines/nancy/ui/textbox.h
+++ b/engines/nancy/ui/textbox.h
@@ -54,7 +54,6 @@ public:
// Nancy 10 only: picks the full, taskbar-covering box (AR 74) over the strip
// (AR 75). No effect on Nancy 1-9 (no such mode) or Nancy 11+ (a single box).
- // The AR owns the full box's auto-close timer.
void setFullMode(bool open);
// True while the text box visually covers the taskbar buttons, so Scene can
// skip taskbar input. Always false before Nancy 10, whose box sits clear of them.
Commit: f4cfed09bcfb1dcde3e16126e95c19be24cb7cb8
https://github.com/scummvm/scummvm/commit/f4cfed09bcfb1dcde3e16126e95c19be24cb7cb8
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:53+03:00
Commit Message:
NANCY: Activate all AR records when loading a game
The original re-runs every scene action record on load, without
persisting per-record state, which is exactly what happens when
entering a scene.
This probably won't introduce regressions, but it should be tested
just in case. Some cursory tests showed no issues.
Fix #16892
Changed paths:
engines/nancy/action/actionmanager.cpp
diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index f062244c793..1c56956ae17 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -631,8 +631,15 @@ void ActionManager::synchronize(Common::Serializer &ser) {
ser.syncAsByte(rec->_isActive);
ser.syncAsByte(rec->_isDone);
- // Forcefully re-activate Autotext records, since we need to regenerate the surface
- if (ser.isLoading() && g_nancy->getGameType() >= kGameTypeNancy6 && rec->_type == 61) {
+ if (ser.isLoading()) {
+ // Records restart fresh on load, just like a normal scene entry: clearing
+ // _isDone lets one-shot records run again -- chained tutorial narration
+ // sounds, conversations, overlays, etc. Anything that must stay "done" is
+ // gated by its own dependencies (event flags, scene counts, inventory),
+ // which are restored separately. Without this, a sound or conversation
+ // that had finished before the save stays silent on load (e.g. the
+ // Nancy 10 movement tutorial). _isActive is recomputed from the record's
+ // dependencies on the next frame.
rec->_isDone = false;
}
}
More information about the Scummvm-git-logs
mailing list