[Scummvm-git-logs] scummvm master -> 99995e7640661156bb214f5f75336b384fd01c60
bluegr
noreply at scummvm.org
Mon Jul 20 01:09:06 UTC 2026
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
b955826797 NANCY: NANCY10: More optimizations when scrolling content
e3eeec2f36 NANCY: NANCY13: Set the exit scene flag in the Nancy13 puzzle ARs
9addd14721 NANCY: NANCY10: Add new functionality to MazeChasePuzzle
99995e7640 NANCY: NANCY13: Implement ScalePuzzle
Commit: b95582679736e83ef5d86da76288564fa3082d63
https://github.com/scummvm/scummvm/commit/b95582679736e83ef5d86da76288564fa3082d63
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T04:08:56+03:00
Commit Message:
NANCY: NANCY10: More optimizations when scrolling content
This should reduce CPU usage considerably when scrolling. An extra
optimization for #16973
Changed paths:
engines/nancy/ui/conversationpopup.cpp
engines/nancy/ui/conversationpopup.h
engines/nancy/ui/inventorypopup.cpp
engines/nancy/ui/notebookpopup.cpp
engines/nancy/ui/notebookpopup.h
engines/nancy/ui/scrolltextbox.cpp
engines/nancy/ui/scrolltextbox.h
diff --git a/engines/nancy/ui/conversationpopup.cpp b/engines/nancy/ui/conversationpopup.cpp
index 069dbef981b..b91c3a4254e 100644
--- a/engines/nancy/ui/conversationpopup.cpp
+++ b/engines/nancy/ui/conversationpopup.cpp
@@ -118,6 +118,11 @@ void ConversationPopup::drawBackground() {
}
void ConversationPopup::drawContent() {
+ layoutText();
+ paintVisibleText();
+}
+
+void ConversationPopup::layoutText() {
_drawnTextHeight = 0;
_numDrawnLines = 0;
_hotspots.clear();
@@ -129,7 +134,9 @@ void ConversationPopup::drawContent() {
textBounds.left += _tboxData->scrollbarDefaultPos.x;
drawAllText(textBounds, 0, _tboxData->conversationFontID, _tboxData->highlightConversationFontID);
+}
+void ConversationPopup::paintVisibleText() {
Common::Rect localTextRect = getLocalTextRect();
const uint16 inner = getInnerHeight();
@@ -140,6 +147,8 @@ void ConversationPopup::drawContent() {
MAX<int>(0, _fullSurface.h - outer));
}
+ // The text is already laid out in _fullSurface; scrolling just re-blits a
+ // different vertical slice of it.
_drawSurface.blitFrom(_fullSurface,
Common::Rect(0, scrollY, _fullSurface.w, scrollY + outer),
Common::Point(localTextRect.left, localTextRect.top));
@@ -147,6 +156,15 @@ void ConversationPopup::drawContent() {
_needsRedraw = true;
}
+void ConversationPopup::redrawScroll() {
+ // The text layout in _fullSurface is unchanged; re-composite the popup at the
+ // new scroll offset. drawBackground() wipes the previous slice, then the text
+ // slice and scrollbar are repainted, skipping the expensive text re-layout.
+ drawBackground();
+ paintVisibleText();
+ drawScrollbar(_scrollbarDragging ? kUIButtonPressed : (_scrollbarHovered ? kUIButtonHover : kUIButtonIdle));
+}
+
uint16 ConversationPopup::getInnerHeight() const {
return _drawnTextHeight + _tboxData->scrollbarDefaultPos.y;
}
@@ -238,14 +256,12 @@ void ConversationPopup::handleInput(NancyInput &input) {
const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
const float newScrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
- // Only re-render when the thumb actually moves. drawContent() re-lays out
- // the whole text surface, so calling it every frame while the button is
- // merely held down pins the CPU and makes dragging choppy.
+ // Re-composite only when the thumb actually moves, and via redrawScroll()
+ // (a cheap slice re-blit) rather than a full text re-layout. This keeps
+ // dragging smooth instead of re-rendering the whole text surface each frame.
if (newScrollPos != _scrollPos) {
_scrollPos = newScrollPos;
- drawBackground();
- drawContent();
- drawScrollbar(kUIButtonPressed);
+ redrawScroll();
}
if (input.input & NancyInput::kLeftMouseButtonUp) {
diff --git a/engines/nancy/ui/conversationpopup.h b/engines/nancy/ui/conversationpopup.h
index ba0b77b5a5b..2842f8079ed 100644
--- a/engines/nancy/ui/conversationpopup.h
+++ b/engines/nancy/ui/conversationpopup.h
@@ -64,7 +64,17 @@ public:
private:
void drawBackground();
+ // Full content rebuild: lay the text out into _fullSurface (expensive), then
+ // paint the visible slice. Only needed when the text itself changes.
void drawContent();
+ // Lay the response text out into _fullSurface (the expensive step).
+ void layoutText();
+ // Blit the currently-visible vertical slice of _fullSurface into the popup.
+ // Cheap; safe to call every scroll step.
+ void paintVisibleText();
+ // Re-composite the popup at the current scroll position without re-laying out
+ // the text. Used while dragging the scrollbar.
+ void redrawScroll();
void drawScrollbar(UIButtonState state);
uint16 getInnerHeight() const;
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 16fc372ab15..7009e5108c9 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -409,9 +409,9 @@ void InventoryPopup::handleInput(NancyInput &input) {
const int clamped = CLIP<int>(newThumbTop, track.top, track.top + travel);
const float newScrollPos = travel > 0 ? (float)(clamped - track.top) / (float)travel : 0.0f;
- // Only re-render when the thumb actually moves. refreshGrid() redraws the
- // whole popup, so calling it every frame while the button is merely held
- // down pins the CPU and makes dragging choppy.
+ // Only re-render when the thumb actually moves to a new page.
+ // refreshGrid() redraws the whole item grid, so re-running it on every
+ // drag frame is wasted work.
if (newScrollPos != _scrollPos) {
_scrollPos = newScrollPos;
updatePageFromScroll();
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index ea5d64dea52..d329d03d4f2 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -298,13 +298,12 @@ void NotebookPopup::handleInput(NancyInput &input) {
const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
const float newScrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
- // Only re-render when the thumb actually moves. refreshContent() re-lays
- // out the whole popup (background, tabs, caption and full text surface),
- // so calling it every frame while the button is merely held down pins the
- // CPU and makes dragging choppy.
+ // Re-composite only when the thumb actually moves, and via redrawScroll()
+ // (a cheap slice re-blit) rather than a full text re-layout. This keeps
+ // dragging smooth instead of re-rendering the whole journal each frame.
if (newScrollPos != _scrollPos) {
_scrollPos = newScrollPos;
- refreshContent();
+ redrawScroll();
}
if (input.input & NancyInput::kLeftMouseButtonUp) {
@@ -530,15 +529,15 @@ void NotebookPopup::buildTextLines() {
}
void NotebookPopup::drawContent() {
+ layoutText();
+ paintVisibleText();
+}
+
+void NotebookPopup::layoutText() {
if (!_uinbData) {
return;
}
- // Convert the text rect to popup-local with the same game-frame-aware
- // conversion the tabs / close button use, so text and caption stay aligned
- // with them when the popup overlays the game frame.
- const Common::Rect localTextRect = toPopupLocal(_uinbData->textRect, false);
-
HypertextParser::clear();
_checkboxRects.clear();
_checkboxEntryIndices.clear();
@@ -561,6 +560,17 @@ void NotebookPopup::drawContent() {
Common::Rect hypertextBounds(leftInset, 0, _fullSurface.w, _fullSurface.h);
drawAllText(hypertextBounds, 0, fontID, fontID);
+}
+
+void NotebookPopup::paintVisibleText() {
+ if (!_uinbData) {
+ return;
+ }
+
+ // Convert the text rect to popup-local with the same game-frame-aware
+ // conversion the tabs / close button use, so text and caption stay aligned
+ // with them when the popup overlays the game frame.
+ const Common::Rect localTextRect = toPopupLocal(_uinbData->textRect, false);
const int visibleH = localTextRect.height();
const int maxScroll = MAX<int>(0, (int)_drawnTextHeight - visibleH);
@@ -570,11 +580,15 @@ void NotebookPopup::drawContent() {
scrollY = safeMax;
}
+ // The text is already laid out in _fullSurface; scrolling just re-blits a
+ // different vertical slice of it.
Common::Rect srcSlice(0, scrollY,
_fullSurface.w, scrollY + visibleH);
_drawSurface.blitFrom(_fullSurface, srcSlice,
Common::Point(localTextRect.left, localTextRect.top));
+ const UIButtonSlot &activeTab = _uinbData->tabs[_activeTab];
+ const bool tasksTab = activeTab.enabled && activeTab.id != notebookJournalTabId();
if (tasksTab) {
buildCheckboxRects(localTextRect, scrollY, visibleH);
}
@@ -582,6 +596,18 @@ void NotebookPopup::drawContent() {
_needsRedraw = true;
}
+void NotebookPopup::redrawScroll() {
+ // The text layout in _fullSurface is unchanged; re-composite the popup at the
+ // new scroll offset. drawBackground() wipes the previous (transparent-keyed)
+ // text, so the chrome must be repainted, but the expensive text re-layout in
+ // layoutText() is skipped.
+ drawBackground();
+ drawTabs();
+ drawCaption();
+ paintVisibleText();
+ drawForeground();
+}
+
void NotebookPopup::buildCheckboxRects(const Common::Rect &localTextRect, int scrollY, int visibleH) {
JournalData *journalData = (JournalData *)NancySceneState.getPuzzleData(JournalData::getTag());
if (!journalData || !journalData->journalEntries.contains(kNotebookTabTasks)) {
diff --git a/engines/nancy/ui/notebookpopup.h b/engines/nancy/ui/notebookpopup.h
index 16d383ad03c..072247e0f27 100644
--- a/engines/nancy/ui/notebookpopup.h
+++ b/engines/nancy/ui/notebookpopup.h
@@ -75,7 +75,18 @@ private:
// Blit the active tab's title image ("CASE JOURNAL" / "TASKS") into the
// header strip above the text area.
void drawCaption();
+ // Full content rebuild: lay the active tab's text out into the scratch
+ // surface, then paint the visible slice. Expensive; only call when the text
+ // itself changes (open, tab switch, checkbox toggle, ModifyListEntry).
void drawContent();
+ // Lay the active tab's text out into _fullSurface (the expensive step).
+ void layoutText();
+ // Blit the currently-visible vertical slice of _fullSurface into the popup
+ // and rebuild the checkbox hit rects. Cheap; safe to call every scroll step.
+ void paintVisibleText();
+ // Re-composite the whole popup at the current scroll position without
+ // re-laying out the text. Used while dragging the scrollbar.
+ void redrawScroll();
// Paint foreground widgets (close button, scrollbar) on top of the
// already-drawn background + content layers.
void drawForeground();
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index c3d415ea3c9..1a46e4cd0dd 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -208,6 +208,29 @@ void ScrollTextBox::drawContent() {
_needsRedraw = true;
}
+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.
+ const Common::Rect viewport = textViewportLocal();
+ const int visibleTextHeight = viewport.height();
+ const int contentHeight = getInnerHeight();
+ int scrollY = 0;
+ if (contentHeight > visibleTextHeight) {
+ scrollY = (int)(_scrollPos * (contentHeight - visibleTextHeight));
+ }
+
+ // drawBackground() repaints the chrome, wiping the previous (transparent-keyed)
+ // text so the new slice doesn't smear over the old one.
+ drawBackground();
+ _drawSurface.blitFrom(_fullSurface,
+ Common::Rect(0, scrollY, _fullSurface.w, scrollY + visibleTextHeight),
+ Common::Point(viewport.left, viewport.top));
+ drawScrollbar(_scrollbarDragging ? kUIButtonPressed : (_scrollbarHovered ? kUIButtonHover : kUIButtonIdle));
+ _needsRedraw = true;
+}
+
uint16 ScrollTextBox::getInnerHeight() const {
return _drawnTextHeight + _tboxData->scrollbarDefaultPos.y;
}
@@ -334,13 +357,14 @@ void ScrollTextBox::handleInput(NancyInput &input) {
released = true;
}
- // Only re-render when the thumb actually moves (or the drag just ended, to
- // repaint the thumb out of its pressed state). drawContent() re-lays out
- // the whole text surface, so calling it every frame while the button is
- // merely held down pins the CPU and makes dragging choppy.
+ // 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.
if (newScrollPos != _scrollPos || released) {
_scrollPos = newScrollPos;
- drawContent();
+ redrawScroll();
}
input.eatMouseInput();
diff --git a/engines/nancy/ui/scrolltextbox.h b/engines/nancy/ui/scrolltextbox.h
index 3c94ee47ca0..ffffb4d8e5d 100644
--- a/engines/nancy/ui/scrolltextbox.h
+++ b/engines/nancy/ui/scrolltextbox.h
@@ -60,6 +60,10 @@ public:
private:
void drawBackground();
void drawContent();
+ // 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.
+ void redrawScroll();
void drawScrollbar(UIButtonState state);
uint16 getInnerHeight() const;
Commit: e3eeec2f365f5d1de6b0b882d2497a4c31d9adb5
https://github.com/scummvm/scummvm/commit/e3eeec2f365f5d1de6b0b882d2497a4c31d9adb5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T04:08:57+03:00
Commit Message:
NANCY: NANCY13: Set the exit scene flag in the Nancy13 puzzle ARs
Changed paths:
engines/nancy/action/puzzle/blockspuzzle.cpp
engines/nancy/action/puzzle/blockspuzzle.h
engines/nancy/action/puzzle/dropsortpuzzle.cpp
engines/nancy/action/puzzle/dropsortpuzzle.h
engines/nancy/action/puzzle/pegspuzzle.cpp
engines/nancy/action/puzzle/pegspuzzle.h
engines/nancy/action/puzzle/stepobjectspuzzle.cpp
engines/nancy/action/puzzle/stepobjectspuzzle.h
engines/nancy/action/puzzle/turningpuzzle.cpp
engines/nancy/action/puzzle/wordfindpuzzle.cpp
engines/nancy/action/puzzle/wordfindpuzzle.h
diff --git a/engines/nancy/action/puzzle/blockspuzzle.cpp b/engines/nancy/action/puzzle/blockspuzzle.cpp
index d3c1fe9a8dd..ea1888afd85 100644
--- a/engines/nancy/action/puzzle/blockspuzzle.cpp
+++ b/engines/nancy/action/puzzle/blockspuzzle.cpp
@@ -66,16 +66,17 @@ void BlocksPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, r);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1); // trailing flag
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = r;
_exitCursorType = cursorType;
_exitScene.sceneID = sceneID;
- // A frameID of 0xffff means "no specific frame" (the target scene may be a
- // video, so seeking to 65535 must be avoided) - keep the default frame 0.
- _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = exitFlagLabel;
+ _exitFlag.flag = exitFlagValue;
}
}
@@ -369,6 +370,7 @@ void BlocksPuzzle::execute() {
break;
case kActionTrigger:
if (_exitRequested) {
+ NancySceneState.setEventFlag(_exitFlag);
if (_exitScene.sceneID != kNoScene) {
NancySceneState.changeScene(_exitScene);
}
diff --git a/engines/nancy/action/puzzle/blockspuzzle.h b/engines/nancy/action/puzzle/blockspuzzle.h
index c2df25de629..4d0c5f38e78 100644
--- a/engines/nancy/action/puzzle/blockspuzzle.h
+++ b/engines/nancy/action/puzzle/blockspuzzle.h
@@ -121,6 +121,7 @@ protected:
Common::Rect _exitHotspot;
uint16 _exitCursorType = 0;
SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag; // set on give-up
RandomSoundBlock _sounds[kNumSounds]; // turn / handle / success
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.cpp b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
index 259039f320b..27fe80d370b 100644
--- a/engines/nancy/action/puzzle/dropsortpuzzle.cpp
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
@@ -119,14 +119,17 @@ void DropSortPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, r);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1); // trailing flag
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = r;
_exitCursorType = cursorType;
_exitScene.sceneID = sceneID;
- _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = exitFlagLabel;
+ _exitFlag.flag = exitFlagValue;
}
}
}
@@ -414,6 +417,7 @@ void DropSortPuzzle::execute() {
}
case kActionTrigger:
if (_exitRequested) {
+ NancySceneState.setEventFlag(_exitFlag);
if (_exitScene.sceneID != kNoScene) {
NancySceneState.changeScene(_exitScene);
}
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.h b/engines/nancy/action/puzzle/dropsortpuzzle.h
index 64603d2c668..b5619b13384 100644
--- a/engines/nancy/action/puzzle/dropsortpuzzle.h
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.h
@@ -127,6 +127,7 @@ protected:
Common::Rect _exitHotspot;
uint16 _exitCursorType = 0;
SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag; // set on give-up
// -- Runtime state --
Common::Array<BeltItem> _items; // candies currently on the belt, oldest first
diff --git a/engines/nancy/action/puzzle/pegspuzzle.cpp b/engines/nancy/action/puzzle/pegspuzzle.cpp
index 5a1cbcf4367..6c765590df9 100644
--- a/engines/nancy/action/puzzle/pegspuzzle.cpp
+++ b/engines/nancy/action/puzzle/pegspuzzle.cpp
@@ -82,14 +82,17 @@ void PegsPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, r);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1); // trailing flag
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = r;
_exitCursorType = cursorType;
_exitScene.sceneID = sceneID;
- _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = exitFlagLabel;
+ _exitFlag.flag = exitFlagValue;
}
}
@@ -346,6 +349,7 @@ void PegsPuzzle::execute() {
}
case kActionTrigger: {
if (_exitRequested) {
+ NancySceneState.setEventFlag(_exitFlag);
if (_exitScene.sceneID != kNoScene) {
NancySceneState.changeScene(_exitScene);
}
diff --git a/engines/nancy/action/puzzle/pegspuzzle.h b/engines/nancy/action/puzzle/pegspuzzle.h
index fd9e33f7610..a045ac7e544 100644
--- a/engines/nancy/action/puzzle/pegspuzzle.h
+++ b/engines/nancy/action/puzzle/pegspuzzle.h
@@ -102,6 +102,7 @@ protected:
Common::Rect _exitHotspot;
uint16 _exitCursorType = 0; // the record's leading field is its cursor type
SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag; // set on give-up
Common::Array<RandomSoundBlock> _sounds; // 5 blocks: select / jump / pulse / win / lose
diff --git a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
index 383e3d70603..3baa8dc1344 100644
--- a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
+++ b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
@@ -83,14 +83,17 @@ void StepObjectsPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, zone);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1);
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = zone;
_exitCursorType = cursorType;
_exitScene.sceneID = sceneID;
- _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = exitFlagLabel;
+ _exitFlag.flag = exitFlagValue;
}
}
@@ -396,8 +399,11 @@ void StepObjectsPuzzle::execute() {
if (_solveScene.sceneID != kNoScene) {
NancySceneState.changeScene(_solveScene);
}
- } else if (_exitScene.sceneID != kNoScene) {
- NancySceneState.changeScene(_exitScene);
+ } else {
+ NancySceneState.setEventFlag(_exitFlag);
+ if (_exitScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_exitScene);
+ }
}
finishExecution();
diff --git a/engines/nancy/action/puzzle/stepobjectspuzzle.h b/engines/nancy/action/puzzle/stepobjectspuzzle.h
index 7cf3c7ac79a..b9933a0e32f 100644
--- a/engines/nancy/action/puzzle/stepobjectspuzzle.h
+++ b/engines/nancy/action/puzzle/stepobjectspuzzle.h
@@ -121,6 +121,7 @@ protected:
Common::Rect _exitHotspot;
uint16 _exitCursorType = 0;
SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag; // set on give-up
Common::Array<RandomSoundBlock> _sounds;
diff --git a/engines/nancy/action/puzzle/turningpuzzle.cpp b/engines/nancy/action/puzzle/turningpuzzle.cpp
index fc5825da93b..a0013c1acdd 100644
--- a/engines/nancy/action/puzzle/turningpuzzle.cpp
+++ b/engines/nancy/action/puzzle/turningpuzzle.cpp
@@ -180,15 +180,17 @@ void TurningPuzzle::readDataNancy13(Common::SeekableReadStream &stream) {
readRect(stream, r);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1);
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = r;
_exitCursorType = cursorType;
_exitScene._sceneChange.sceneID = sceneID;
- // 0xffff means "no frame" - the target scene may be a video.
- _exitScene._sceneChange.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene._sceneChange.frameID = 0;
+ _exitScene._flag.label = exitFlagLabel;
+ _exitScene._flag.flag = exitFlagValue;
_solveScene._sceneChange = _exitScene._sceneChange;
}
}
diff --git a/engines/nancy/action/puzzle/wordfindpuzzle.cpp b/engines/nancy/action/puzzle/wordfindpuzzle.cpp
index fd9c0eebfa7..fa765576b4b 100644
--- a/engines/nancy/action/puzzle/wordfindpuzzle.cpp
+++ b/engines/nancy/action/puzzle/wordfindpuzzle.cpp
@@ -76,14 +76,17 @@ void WordFindPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, r);
uint16 cursorType = stream.readUint16LE();
uint16 sceneID = stream.readUint16LE();
- uint16 frameID = stream.readUint16LE();
- stream.skip(1);
+ int16 exitFlagLabel = stream.readSint16LE();
+ byte exitFlagValue = stream.readByte();
if (i == 0) {
_exitHotspot = r;
_exitCursorType = cursorType;
_exitScene.sceneID = sceneID;
- _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ // The field after the scene id is a flag label (set on give-up), not a frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = exitFlagLabel;
+ _exitFlag.flag = exitFlagValue;
}
}
@@ -320,6 +323,9 @@ void WordFindPuzzle::execute() {
break;
}
case kActionTrigger: {
+ if (!_allFound) {
+ NancySceneState.setEventFlag(_exitFlag);
+ }
const SceneChangeDescription &sc = _allFound ? _solveScene : _exitScene;
if (sc.sceneID != kNoScene) {
NancySceneState.changeScene(sc);
diff --git a/engines/nancy/action/puzzle/wordfindpuzzle.h b/engines/nancy/action/puzzle/wordfindpuzzle.h
index 2d8fb9ecd6c..2a9a8eaa086 100644
--- a/engines/nancy/action/puzzle/wordfindpuzzle.h
+++ b/engines/nancy/action/puzzle/wordfindpuzzle.h
@@ -85,6 +85,7 @@ protected:
Common::Rect _exitHotspot;
uint16 _exitCursorType = 0;
SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag; // set on give-up
Common::Array<RandomSoundBlock> _sounds; // 4 blocks
Commit: 9addd14721c8783c701dd11f6710417cc7a6a6d0
https://github.com/scummvm/scummvm/commit/9addd14721c8783c701dd11f6710417cc7a6a6d0
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T04:08:57+03:00
Commit Message:
NANCY: NANCY10: Add new functionality to MazeChasePuzzle
Nancy10 added functionality to handle how the player peace leaves the
board once it reaches the exit.
Fix #16996
Changed paths:
engines/nancy/action/puzzle/mazechasepuzzle.cpp
engines/nancy/action/puzzle/mazechasepuzzle.h
diff --git a/engines/nancy/action/puzzle/mazechasepuzzle.cpp b/engines/nancy/action/puzzle/mazechasepuzzle.cpp
index ae9cef80e36..ef4837891c9 100644
--- a/engines/nancy/action/puzzle/mazechasepuzzle.cpp
+++ b/engines/nancy/action/puzzle/mazechasepuzzle.cpp
@@ -156,7 +156,9 @@ void MazeChasePuzzle::readData(Common::SeekableReadStream &stream) {
_exitPos.y = stream.readUint16LE();
if (isNancy10) {
- stream.skip(1); // TODO: unknown byte before the grid.
+ // Selects how the player piece leaves the board when it reaches the
+ // exit: zero makes it disappear at the hole, non-zero slides it off.
+ _pieceDisappearsAtExit = stream.readByte() == 0;
}
_grid.resize(height, Common::Array<uint16>(width));
@@ -233,8 +235,14 @@ void MazeChasePuzzle::execute() {
}
if (_pieces[0]._gridPos == _exitPos) {
- _pieces[0]._gridPos = _exitPos + Common::Point(_exitPos.x == 0 ? -1 : 1, 0);
- ++_currentAnimFrame;
+ if (_pieceDisappearsAtExit) {
+ // The piece vanishes at the hole instead of sliding past the edge
+ _pieces[0].setVisible(false);
+ } else {
+ _pieces[0]._gridPos = _exitPos + Common::Point(_exitPos.x == 0 ? -1 : 1, 0);
+ ++_currentAnimFrame;
+ }
+
g_nancy->_sound->loadSound(_solveSound);
g_nancy->_sound->playSound(_solveSound);
_solved = true;
diff --git a/engines/nancy/action/puzzle/mazechasepuzzle.h b/engines/nancy/action/puzzle/mazechasepuzzle.h
index da50ddf5f9d..e05c8123b28 100644
--- a/engines/nancy/action/puzzle/mazechasepuzzle.h
+++ b/engines/nancy/action/puzzle/mazechasepuzzle.h
@@ -117,6 +117,12 @@ protected:
int _currentAnimFrame = -1;
+ // nancy10 added a byte before the grid selecting how the player piece
+ // leaves the board once it reaches the exit. When zero (the roadrunner
+ // minigame), the piece simply vanishes at the hole; otherwise it slides
+ // off past the edge of the board and stays visible (the nancy5 dancers).
+ bool _pieceDisappearsAtExit = false;
+
uint32 _solveSoundPlayTime = 0;
bool _solved = false;
bool _reset = false;
Commit: 99995e7640661156bb214f5f75336b384fd01c60
https://github.com/scummvm/scummvm/commit/99995e7640661156bb214f5f75336b384fd01c60
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T04:08:57+03:00
Commit Message:
NANCY: NANCY13: Implement ScalePuzzle
Balance-scale puzzle, new in Nancy13 (AR 174). A row of numbered
"sampler" figures sits above two scale pans (a "minus" pan on the left
and a "plus" pan on the right) with a numeric indicator between them.
Coins of known values start in a tray; each is picked up by clicking it
and dropping onto an empty slot on either pan or back into the tray.
The indicator shows the running total sum(right values) - sum(left
values).
Each figure has to be matched in turn: a figure lights (and its latch
bar opens) when the number of coins resting on the two pans equals its
required coin count and the indicator magnitude equals the figure's
number. Figures must be matched in order, the lights stay on once lit,
and clearing the pans resets them. The puzzle is solved once every
figure of the current scene is lit.
Changed paths:
A engines/nancy/action/puzzle/scalepuzzle.cpp
A engines/nancy/action/puzzle/scalepuzzle.h
engines/nancy/action/arfactory.cpp
engines/nancy/module.mk
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 7bdbc855876..3f9cb7e0b23 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -72,6 +72,7 @@
#include "engines/nancy/action/puzzle/rippedletterpuzzle.h"
#include "engines/nancy/action/puzzle/rotatinglockpuzzle.h"
#include "engines/nancy/action/puzzle/safedialpuzzle.h"
+#include "engines/nancy/action/puzzle/scalepuzzle.h"
#include "engines/nancy/action/puzzle/setplayerclock.h"
#include "engines/nancy/action/puzzle/sewingmachinepuzzle.h"
#include "engines/nancy/action/puzzle/sliderpuzzle.h"
@@ -486,11 +487,9 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 173:
return new PegsPuzzle();
case 174:
- // ScalePuzzle (balance scale), new in Nancy13
- // TODO: not yet implemented
- return nullptr;
+ return new ScalePuzzle(); // balance scale
case 175:
- // PachinkoPuzzle (ball drop), new in Nancy13
+ // PachinkoPuzzle (ball drop)
// TODO: not yet implemented
return nullptr;
case 176:
diff --git a/engines/nancy/action/puzzle/scalepuzzle.cpp b/engines/nancy/action/puzzle/scalepuzzle.cpp
new file mode 100644
index 00000000000..d7c08e7a723
--- /dev/null
+++ b/engines/nancy/action/puzzle/scalepuzzle.cpp
@@ -0,0 +1,463 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/random.h"
+#include "common/util.h"
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/scalepuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+// Reads a count-prefixed array of int32 rects (one of the original's sprite-container
+// sub-objects).
+static void readRects(Common::SeekableReadStream &stream, Common::Array<Common::Rect> &rects) {
+ uint16 count = stream.readUint16LE();
+ rects.resize(count);
+ for (uint i = 0; i < count; ++i) {
+ readRect(stream, rects[i]);
+ }
+}
+
+void ScalePuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName); // 0x00
+ // The puzzle carries its own cursors as raw Nancy13 cursor type ids.
+ _hoverCursorType = stream.readUint16LE(); // 0x21
+ _dragCursorType = stream.readUint16LE(); // 0x23
+
+ // Applied when the puzzle comes out solved. Several scenes use 9999 (no scene) and
+ // leave the transition to whatever watches the solve flag.
+ _solveScene.sceneID = stream.readUint16LE(); // 0x25
+ _solveScene.continueSceneSound = kContinueSceneSound;
+ _solveFlag.label = stream.readSint16LE(); // 0x27
+ _solveFlag.flag = stream.readByte(); // 0x29
+
+ _latchSound.readData(stream); // played on give-up
+
+ // The figures to match this scene: a required coin count, the figure's number, its
+ // open-latch sprite and destination rects, and the sound played when it lights.
+ int16 numTargets = stream.readSint16LE();
+ _targets.resize(numTargets);
+ for (int16 i = 0; i < numTargets; ++i) {
+ Target &t = _targets[i];
+ t.coinCount = stream.readByte();
+ t.number = stream.readSint32LE();
+ readRect(stream, t.latchSrc);
+ readRect(stream, t.latchDst);
+ t.sound.readData(stream);
+ }
+
+ // The coin definitions: the inventory item id gating each coin, its value, and its sprite
+ // in the overlay image.
+ int16 numCoins = stream.readSint16LE();
+ _coins.resize(numCoins);
+ for (int16 i = 0; i < numCoins; ++i) {
+ Coin &c = _coins[i];
+ c.itemID = stream.readSint16LE();
+ c.value = stream.readSint32LE();
+ readRect(stream, c.src);
+ }
+
+ // The three slot groups, then a couple of loose sprite rects, the red-light atlas, the
+ // indicator atlas and the indicator's on-screen rect.
+ readRects(stream, _left.dests); // container 0xb3 - left pan slots
+ readRects(stream, _right.dests); // container 0xbd - right pan slots
+ readRects(stream, _tray.dests); // container 0xc7 - source-tray slots
+ readRect(stream, _coinBackSrc); // 0xd1
+ readRects(stream, _pileFrames); // 0xe1
+ readRect(stream, _altSrc); // 0xef
+ readRects(stream, _lightFrames); // 0xff - red lights
+ readRects(stream, _indicatorFrames); // 0x85 - indicator digits (-N..+N)
+ readRect(stream, _indicatorDest); // 0x8f
+
+ _pickupSound.readData(stream); // 0x170
+ _dropTraySound.readData(stream); // 0x218
+ _dropPanSound.readData(stream); // 0x1c4
+
+ // A count-prefixed array of fixed 23-byte hotspot records:
+ // {rect, u16 cursorType, u16 sceneID, u16 frameID, byte}. The sample carries one - the
+ // "give up / exit" hotspot, with the exit cursor type.
+ int16 numZones = stream.readSint16LE();
+ for (int16 i = 0; i < numZones; ++i) {
+ Common::Rect r;
+ readRect(stream, r);
+ uint16 cursorType = stream.readUint16LE();
+ uint16 sceneID = stream.readUint16LE();
+ int16 flagLabel = stream.readSint16LE();
+ byte flagValue = stream.readByte();
+
+ if (i == 0) {
+ _exitHotspot = r;
+ _exitCursorType = cursorType;
+ _exitScene.sceneID = sceneID;
+ // The field after the scene id is a flag label (set on give-up), not a frame; the
+ // exit always goes to the scene's first frame.
+ _exitScene.frameID = 0;
+ _exitFlag.label = flagLabel;
+ _exitFlag.flag = flagValue;
+ }
+ }
+}
+
+void ScalePuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(), g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ // The indicator reads zero at the middle frame of its strip; the running total shifts it.
+ _indicatorZeroFrame = _indicatorFrames.size() / 2;
+
+ // Every slot starts empty.
+ _left.coins.resize(_left.dests.size());
+ _right.coins.resize(_right.dests.size());
+ _tray.coins.resize(_tray.dests.size());
+ for (uint i = 0; i < _left.coins.size(); ++i) {
+ _left.coins[i] = kNoCoin;
+ }
+ for (uint i = 0; i < _right.coins.size(); ++i) {
+ _right.coins[i] = kNoCoin;
+ }
+ for (uint i = 0; i < _tray.coins.size(); ++i) {
+ _tray.coins[i] = kNoCoin;
+ }
+
+ // Place only the coins the player is carrying: the original filters the coin list by the
+ // inventory "has item" state (coins are collected over the course of the game), so coins
+ // the player does not own are never placed.
+ Common::Array<int16> available;
+ for (uint i = 0; i < _coins.size(); ++i) {
+ if (NancySceneState.hasItem(_coins[i].itemID) == g_nancy->_true) {
+ available.push_back((int16)i);
+ }
+ }
+
+ Common::Array<uint> freeSlots;
+ for (uint i = 0; i < _tray.dests.size(); ++i) {
+ freeSlots.push_back(i);
+ }
+ for (uint i = 0; i < available.size() && !freeSlots.empty(); ++i) {
+ uint k = g_nancy->_randomSource->getRandomNumber(freeSlots.size() - 1);
+ _tray.coins[freeSlots[k]] = available[i];
+ freeSlots.remove_at(k);
+ }
+
+ recomputeBalance();
+
+ NancySceneState.setNoHeldItem();
+
+ redraw();
+ registerGraphics();
+}
+
+ScalePuzzle::SlotGroup &ScalePuzzle::group(SlotRegion region) {
+ switch (region) {
+ case kLeftPan:
+ return _left;
+ case kRightPan:
+ return _right;
+ default:
+ return _tray;
+ }
+}
+
+bool ScalePuzzle::slotAtCursor(const Common::Point &mousePos, bool wantEmpty, SlotRegion &outRegion, uint &outIndex) const {
+ static const SlotRegion order[3] = { kSourceTray, kLeftPan, kRightPan };
+ for (int g = 0; g < 3; ++g) {
+ const SlotGroup &grp = (order[g] == kLeftPan) ? _left : (order[g] == kRightPan) ? _right : _tray;
+ for (uint i = 0; i < grp.dests.size(); ++i) {
+ bool empty = (grp.coins[i] == kNoCoin);
+ if (empty != wantEmpty) {
+ continue;
+ }
+ if (NancySceneState.getViewport().convertViewportToScreen(grp.dests[i]).contains(mousePos)) {
+ outRegion = order[g];
+ outIndex = i;
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+void ScalePuzzle::recomputeBalance() {
+ _tilt = 0;
+ _placedCount = 0;
+
+ // The indicator reads sum(right pan values) - sum(left pan values).
+ for (uint i = 0; i < _left.coins.size(); ++i) {
+ if (_left.coins[i] != kNoCoin) {
+ _tilt -= _coins[_left.coins[i]].value;
+ ++_placedCount;
+ }
+ }
+ for (uint i = 0; i < _right.coins.size(); ++i) {
+ if (_right.coins[i] != kNoCoin) {
+ _tilt += _coins[_right.coins[i]].value;
+ ++_placedCount;
+ }
+ }
+
+ // Match the figures in order (mirrors FUN_0046b8b0). Walk the targets skipping the ones
+ // already lit; act on the first target whose required coin count equals the number of
+ // coins now on the pans. If the indicator magnitude matches its number it lights (and
+ // plays its latch sound the first time); otherwise it, and every later target, is
+ // cleared. A target that is not yet lit stops the walk, so they can only be lit in turn.
+ for (uint i = 0; i < _targets.size(); ++i) {
+ Target &t = _targets[i];
+ if (_placedCount == t.coinCount) {
+ if ((int)ABS(_tilt) == t.number) {
+ if (!t.lit) {
+ t.lit = true;
+ _endSound = playSoundBlock(t.sound);
+ }
+ } else {
+ for (uint j = i; j < _targets.size(); ++j) {
+ _targets[j].lit = false;
+ }
+ }
+ break;
+ }
+ if (!t.lit) {
+ break;
+ }
+ }
+
+ // Clearing the pans resets every light.
+ if (_placedCount == 0) {
+ for (uint i = 0; i < _targets.size(); ++i) {
+ _targets[i].lit = false;
+ }
+ }
+
+ // Solved once every figure of the scene is lit.
+ _solved = !_targets.empty();
+ for (uint i = 0; i < _targets.size(); ++i) {
+ if (!_targets[i].lit) {
+ _solved = false;
+ }
+ }
+}
+
+void ScalePuzzle::blitCentered(const Common::Rect &src, const Common::Rect &slot) {
+ int x = slot.left + (slot.width() - src.width()) / 2;
+ int y = slot.top + (slot.height() - src.height()) / 2;
+ _drawSurface.blitFrom(_image, src, Common::Point(x, y));
+}
+
+void ScalePuzzle::redraw() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ // The coins resting in the tray and on the two pans, each centred in its slot.
+ const SlotGroup *groups[3] = { &_tray, &_left, &_right };
+ for (int g = 0; g < 3; ++g) {
+ const SlotGroup &grp = *groups[g];
+ for (uint i = 0; i < grp.dests.size(); ++i) {
+ int16 coin = grp.coins[i];
+ if (coin == kNoCoin) {
+ continue;
+ }
+ blitCentered(_coins[coin].src, grp.dests[i]);
+ }
+ }
+
+ // The numeric indicator, showing the running total.
+ if (!_indicatorFrames.empty()) {
+ int idx = CLIP<int>(_indicatorZeroFrame + _tilt, 0, (int)_indicatorFrames.size() - 1);
+ _drawSurface.blitFrom(_image, _indicatorFrames[idx], Common::Point(_indicatorDest.left, _indicatorDest.top));
+ }
+
+ // The coin-count token, drawn at the position for the current number of coins on the pans.
+ if (_placedCount > 0 && (uint)(_placedCount - 1) < _pileFrames.size() && !_coinBackSrc.isEmpty()) {
+ const Common::Rect &pos = _pileFrames[_placedCount - 1];
+ _drawSurface.blitFrom(_image, _coinBackSrc, Common::Point(pos.left, pos.top));
+ }
+
+ // Each matched figure: its red light (the shared light sprite drawn at the light slot for
+ // its coin count) and its open-latch bar.
+ for (uint i = 0; i < _targets.size(); ++i) {
+ const Target &t = _targets[i];
+ if (!t.lit) {
+ continue;
+ }
+ int lightIdx = t.coinCount - 1;
+ if (lightIdx >= 0 && (uint)lightIdx < _lightFrames.size() && !_altSrc.isEmpty()) {
+ const Common::Rect &pos = _lightFrames[lightIdx];
+ _drawSurface.blitFrom(_image, _altSrc, Common::Point(pos.left, pos.top));
+ }
+ _drawSurface.blitFrom(_image, t.latchSrc, Common::Point(t.latchDst.left, t.latchDst.top));
+ }
+
+ // The coin currently being carried, following the cursor.
+ if (_carriedCoin != kNoCoin) {
+ const Common::Rect &src = _coins[_carriedCoin].src;
+ _drawSurface.blitFrom(_image, src, Common::Point(_dragPos.x - src.width() / 2, _dragPos.y - src.height() / 2));
+ }
+
+ _needsRedraw = true;
+}
+
+void ScalePuzzle::setDataCursor(uint16 cursorType) const {
+ // The ids in the AR data are raw Nancy13 cursor types, which is exactly what the
+ // "set from script" path expects.
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)cursorType, true);
+}
+
+SoundDescription ScalePuzzle::playSoundBlock(const RandomSoundBlock &block) {
+ SoundDescription desc;
+ if (block.names.empty()) {
+ return desc;
+ }
+
+ uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+ const Common::String &name = block.names[idx];
+ if (name.empty() || name == "NO SOUND") {
+ return desc;
+ }
+
+ desc.name = name;
+ desc.channelID = block.channel;
+ desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+ desc.volume = block.volume;
+
+ g_nancy->_sound->loadSound(desc);
+ g_nancy->_sound->playSound(desc);
+ return desc;
+}
+
+void ScalePuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ _state = kRun;
+ // fall through
+ case kRun:
+ if (_exitRequested) {
+ _state = kActionTrigger;
+ break;
+ }
+
+ if (_solved) {
+ bool soundDone = _endSound.name.empty() || !g_nancy->_sound->isSoundPlaying(_endSound);
+ if (soundDone) {
+ _state = kActionTrigger;
+ }
+ }
+ break;
+ case kActionTrigger:
+ if (_exitRequested) {
+ NancySceneState.setEventFlag(_exitFlag);
+ if (_exitScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_exitScene);
+ }
+ } else {
+ // Solved: play the latch sound, set the solve flag, change scene (9999 = stay).
+ playSoundBlock(_latchSound);
+ NancySceneState.setEventFlag(_solveFlag);
+ if (_solveScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_solveScene);
+ }
+ }
+
+ finishExecution();
+ break;
+ }
+}
+
+void ScalePuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _solved) {
+ return;
+ }
+
+ const bool click = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
+
+ // -- Carrying a coin: it follows the cursor; drop it on an empty slot. --
+ if (_carriedCoin != kNoCoin) {
+ setDataCursor(_dragCursorType);
+
+ Common::Rect screenPt(input.mousePos.x, input.mousePos.y, input.mousePos.x + 1, input.mousePos.y + 1);
+ Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
+ _dragPos = Common::Point(vpPt.left, vpPt.top);
+ redraw();
+
+ if (click) {
+ SlotRegion region;
+ uint idx;
+ if (slotAtCursor(input.mousePos, true, region, idx)) {
+ group(region).coins[idx] = _carriedCoin;
+ _carriedCoin = kNoCoin;
+ playSoundBlock(region == kSourceTray ? _dropTraySound : _dropPanSound);
+ recomputeBalance();
+ redraw();
+ }
+ }
+
+ input.eatMouseInput();
+ return;
+ }
+
+ // -- Not carrying: pick up a coin, or leave via the exit hotspot. --
+ SlotRegion region;
+ uint idx;
+ if (slotAtCursor(input.mousePos, false, region, idx)) {
+ setDataCursor(_hoverCursorType);
+ if (click) {
+ SlotGroup &grp = group(region);
+ _carriedCoin = grp.coins[idx];
+ grp.coins[idx] = kNoCoin;
+
+ Common::Rect screenPt(input.mousePos.x, input.mousePos.y, input.mousePos.x + 1, input.mousePos.y + 1);
+ Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
+ _dragPos = Common::Point(vpPt.left, vpPt.top);
+
+ playSoundBlock(_pickupSound);
+ recomputeBalance();
+ redraw();
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ if (!_exitHotspot.isEmpty() &&
+ NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
+ setDataCursor(_exitCursorType);
+ if (click) {
+ _exitRequested = true;
+ }
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/scalepuzzle.h b/engines/nancy/action/puzzle/scalepuzzle.h
new file mode 100644
index 00000000000..da853757fc4
--- /dev/null
+++ b/engines/nancy/action/puzzle/scalepuzzle.h
@@ -0,0 +1,155 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_SCALEPUZZLE_H
+#define NANCY_ACTION_SCALEPUZZLE_H
+
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/action/actionrecord.h"
+
+namespace Nancy {
+namespace Action {
+
+// Balance-scale puzzle, new in Nancy13 (AR 174). A row of numbered
+// "sampler" figures sits above two scale pans (a "minus" pan on the left
+// and a "plus" pan on the right) with a numeric indicator between them.
+// Coins of known values start in a tray; each is picked up by clicking it
+// and dropping onto an empty slot on either pan or back into the tray.
+// The indicator shows the running total sum(right values) - sum(left
+// values).
+//
+// Each figure has to be matched in turn: a figure lights (and its latch
+// bar opens) when the number of coins resting on the two pans equals its
+// required coin count and the indicator magnitude equals the figure's
+// number. Figures must be matched in order, the lights stay on once lit,
+// and clearing the pans resets them. The puzzle is solved once every
+// figure of the current scene is lit.
+class ScalePuzzle : public RenderActionRecord {
+public:
+ ScalePuzzle() : RenderActionRecord(7) {}
+ virtual ~ScalePuzzle() {}
+
+ void init() override;
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+ void handleInput(NancyInput &input) override;
+
+ bool isViewportRelative() const override { return true; }
+
+protected:
+ Common::String getRecordTypeName() const override { return "ScalePuzzle"; }
+
+ static const int16 kNoCoin = -1;
+
+ // Which of the three slot groups a slot belongs to.
+ enum SlotRegion { kSourceTray = 0, kLeftPan = 1, kRightPan = 2 };
+
+ // A draggable coin. Its value determines how far it moves the indicator; a coin is only
+ // placed if the player is carrying its inventory item (coins are collected in the game).
+ struct Coin {
+ int16 itemID = -1; // 0x00 - inventory item id gating this coin
+ int32 value = 0; // 0x02
+ Common::Rect src; // 0x06 - coin sprite in the overlay image
+ };
+
+ // A numbered figure to be matched. It lights when exactly "coinCount" coins rest on the
+ // pans and the indicator magnitude equals "number"; its latch sprite is then drawn.
+ struct Target {
+ byte coinCount = 0; // 0x00 - required number of coins on the pans
+ int32 number = 0; // 0x01 - the figure's number (required |indicator|)
+ Common::Rect latchSrc; // 0x05 - the open-latch sprite in the overlay image
+ Common::Rect latchDst; // 0x15 - where it is drawn (a bar along the top)
+ RandomSoundBlock sound; // the latch-open sound, played once when it lights
+ bool lit = false; // runtime: has it been matched?
+ };
+
+ // One group of slots (tray / left pan / right pan): the fixed on-screen positions and,
+ // per slot, the index of the coin resting there (or kNoCoin).
+ struct SlotGroup {
+ Common::Array<Common::Rect> dests;
+ Common::Array<int16> coins;
+ };
+
+ SlotGroup &group(SlotRegion region);
+ // The slot whose rect contains the cursor. When wantEmpty is set only empty slots match
+ // (used while carrying), otherwise only occupied slots match (used while picking up).
+ bool slotAtCursor(const Common::Point &mousePos, bool wantEmpty, SlotRegion &outRegion, uint &outIndex) const;
+ void recomputeBalance();
+ // Draws the image region src centred inside slot (the original centres coins in their
+ // slots, FUN_004b6660 case 0).
+ void blitCentered(const Common::Rect &src, const Common::Rect &slot);
+ void redraw();
+ void setDataCursor(uint16 cursorType) const;
+ SoundDescription playSoundBlock(const RandomSoundBlock &block);
+
+ // -- File data --
+ Common::Path _imageName; // 0x00
+ uint16 _hoverCursorType = 0; // 0x21 - raw Nancy13 cursor type over a coin
+ uint16 _dragCursorType = 0; // 0x23 - raw Nancy13 cursor type while carrying
+ SceneChangeDescription _solveScene; // 0x25 - applied when solved (9999 => none)
+ FlagDescription _solveFlag; // 0x27 - set when solved
+ RandomSoundBlock _latchSound; // the first sound block; played on give-up
+
+ Common::Array<Target> _targets; // the figures to match in this scene
+ Common::Array<Coin> _coins; // the coin definitions
+
+ SlotGroup _tray; // container 0xc7 - source tray (35 slots)
+ SlotGroup _left; // container 0xb3 - left ("minus") pan (7 slots)
+ SlotGroup _right; // container 0xbd - right ("plus") pan (7 slots)
+
+ Common::Rect _coinBackSrc; // 0xd1 - the coin-count token sprite
+ Common::Array<Common::Rect> _pileFrames; // 0xe1 - token positions (drawn at placedCount-1)
+ Common::Rect _altSrc; // 0xef - the lit red-light sprite
+ Common::Array<Common::Rect> _lightFrames; // 0xff - the 10 red-light positions (by coinCount-1)
+ Common::Array<Common::Rect> _indicatorFrames; // 0x85 - the numeric indicator (-N..+N)
+ Common::Rect _indicatorDest; // 0x8f - where the indicator is drawn
+
+ RandomSoundBlock _pickupSound; // 0x170 - coin picked up
+ RandomSoundBlock _dropTraySound; // 0x218 - coin dropped back into the tray
+ RandomSoundBlock _dropPanSound; // 0x1c4 - coin dropped onto a pan
+
+ // The clickable "give up / exit" hotspot (the base-class hotspot record). Clicking it
+ // always jumps to the scene's first frame and sets an event flag.
+ Common::Rect _exitHotspot;
+ uint16 _exitCursorType = 0;
+ SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag;
+
+ // -- Runtime state --
+ int _indicatorZeroFrame = 0; // the "0" frame (frames.size() / 2)
+ int _tilt = 0; // the indicator value: sum(right) - sum(left)
+ int _placedCount = 0; // coins resting on the two pans
+
+ int16 _carriedCoin = kNoCoin; // the coin on the cursor, or kNoCoin
+ Common::Point _dragPos; // cursor position (viewport space) while carrying
+
+ bool _solved = false;
+ bool _exitRequested = false;
+ SoundDescription _endSound; // the cue we wait on before changing scene
+
+ Graphics::ManagedSurface _image;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_SCALEPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 19aeacaeb11..424915e92c4 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -56,6 +56,7 @@ MODULE_OBJS = \
action/puzzle/rippedletterpuzzle.o \
action/puzzle/rotatinglockpuzzle.o \
action/puzzle/safedialpuzzle.o \
+ action/puzzle/scalepuzzle.o \
action/puzzle/setplayerclock.o \
action/puzzle/sewingmachinepuzzle.o \
action/puzzle/sliderpuzzle.o \
More information about the Scummvm-git-logs
mailing list