[Scummvm-git-logs] scummvm master -> 4f1da2fa23c0aa884d0697375af04a541d4e6a24
bluegr
noreply at scummvm.org
Thu Sep 3 22:54:47 UTC 2026
This automated email contains information about 9 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
88d13168df NANCY: NANCY13/14: Use a common method to read puzzle exit hotspots
14100ada2e NANCY: Implement a new common method to set RenderObject z-ordering
17e88d7a7c NANCY: Use MouseFollowObject in puzzles where objects are dragged
99e5ae3e20 NANCY: NANCY13: Implement new functionality in MultiBuildPuzzle
f3e5ccb02c NANCY: NANCY13: Implemented the replacement for AR 27
8f2fc7283c NANCY: NANCY13: Implement changes in interactive videos
6d93fd8c74 NANCY: NANCY15: Implement the new AR 52 - OverlayMultiframeTerse
8b2c9a7bba NANCY: NANCY15: Implement differences in the LOAD chunk
4f1da2fa23 NANCY: NANCY15: Added handling for new character glyphs in fonts
Commit: 88d13168df190d56b665a275342cde50755bc0b2
https://github.com/scummvm/scummvm/commit/88d13168df190d56b665a275342cde50755bc0b2
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:17+03:00
Commit Message:
NANCY: NANCY13/14: Use a common method to read puzzle exit hotspots
Changed paths:
engines/nancy/action/cameraaction.cpp
engines/nancy/action/puzzle/adjustpuzzle.cpp
engines/nancy/action/puzzle/blockspuzzle.cpp
engines/nancy/action/puzzle/buildpuzzle.cpp
engines/nancy/action/puzzle/decoderpuzzle.cpp
engines/nancy/action/puzzle/dropsortpuzzle.cpp
engines/nancy/action/puzzle/hangmanpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/pachinkopuzzle.cpp
engines/nancy/action/puzzle/paintpuzzle.cpp
engines/nancy/action/puzzle/pegspuzzle.cpp
engines/nancy/action/puzzle/scalepuzzle.cpp
engines/nancy/action/puzzle/stepobjectspuzzle.cpp
engines/nancy/action/puzzle/turningpuzzle.cpp
engines/nancy/action/puzzle/wordfindpuzzle.cpp
engines/nancy/util.cpp
engines/nancy/util.h
diff --git a/engines/nancy/action/cameraaction.cpp b/engines/nancy/action/cameraaction.cpp
index fea41e963cd..5a5dc65b534 100644
--- a/engines/nancy/action/cameraaction.cpp
+++ b/engines/nancy/action/cameraaction.cpp
@@ -59,24 +59,7 @@ void CameraAction::readData(Common::SeekableReadStream &stream) {
// Trailing count-prefixed array of 23-byte give-up hotspots. The exit always
// jumps to the scene's first frame.
- int16 numExitZones = stream.readSint16LE();
- for (int16 i = 0; i < numExitZones; ++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;
- _exitScene.frameID = 0;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void CameraAction::init() {
diff --git a/engines/nancy/action/puzzle/adjustpuzzle.cpp b/engines/nancy/action/puzzle/adjustpuzzle.cpp
index 23752fcc137..17e03539184 100644
--- a/engines/nancy/action/puzzle/adjustpuzzle.cpp
+++ b/engines/nancy/action/puzzle/adjustpuzzle.cpp
@@ -100,24 +100,7 @@ void AdjustPuzzle::readData(Common::SeekableReadStream &stream) {
// Trailing count-prefixed array of 23-byte give-up hotspots
// {Rect, uint16 cursorType, uint16 sceneID, int16 flagLabel, byte flagValue}.
// The exit always jumps to the scene's first frame.
- int16 numExitZones = stream.readSint16LE();
- for (int16 i = 0; i < numExitZones; ++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;
- _exitScene.frameID = 0;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void AdjustPuzzle::init() {
diff --git a/engines/nancy/action/puzzle/blockspuzzle.cpp b/engines/nancy/action/puzzle/blockspuzzle.cpp
index 6f0f32a71dd..0d98ce5731d 100644
--- a/engines/nancy/action/puzzle/blockspuzzle.cpp
+++ b/engines/nancy/action/puzzle/blockspuzzle.cpp
@@ -60,25 +60,7 @@ void BlocksPuzzle::readData(Common::SeekableReadStream &stream) {
// 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 (leave the puzzle unsolved), 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 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = 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.
- _exitScene.frameID = 0;
- _exitFlag.label = exitFlagLabel;
- _exitFlag.flag = exitFlagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
// The block shapes, each a 13-byte descriptor of its row in the atlas image.
int16 numBlocks = stream.readSint16LE();
diff --git a/engines/nancy/action/puzzle/buildpuzzle.cpp b/engines/nancy/action/puzzle/buildpuzzle.cpp
index cfa8add0054..069ffa34e87 100644
--- a/engines/nancy/action/puzzle/buildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/buildpuzzle.cpp
@@ -173,25 +173,8 @@ void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
readSoundBlock(stream, unused);
// The count-prefixed 23-byte hotspot records shared by the later puzzles.
- int16 numExitZones = stream.readSint16LE();
- for (int16 i = 0; i < numExitZones; ++i) {
- Common::Rect zone;
- readRect(stream, zone);
- uint16 cursorType = stream.readUint16LE();
- uint16 sceneID = stream.readUint16LE();
- int16 flagLabel = stream.readSint16LE();
- byte flagValue = stream.readByte();
-
- if (i == 0) {
- _exitHotspot = zone;
- _exitCursorType = cursorType;
- _exitScene.sceneID = sceneID;
- _exitScene.frameID = 0;
- _exitScene.continueSceneSound = kContinueSceneSound;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
+ _exitScene.continueSceneSound = kContinueSceneSound;
}
void BuildPuzzle::setFlagOnChange(int16 label, bool value, int8 &last) {
diff --git a/engines/nancy/action/puzzle/decoderpuzzle.cpp b/engines/nancy/action/puzzle/decoderpuzzle.cpp
index 7825416dd1a..a69ce722bfa 100644
--- a/engines/nancy/action/puzzle/decoderpuzzle.cpp
+++ b/engines/nancy/action/puzzle/decoderpuzzle.cpp
@@ -130,26 +130,8 @@ void DecoderPuzzle::readData(Common::SeekableReadStream &stream) {
_solveSound.readData(stream); // 0x185
- // Count-prefixed 23-byte hotspot records; the first is the "give up" hotspot
- 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;
- _exitScene.frameID = 0;
- _exitScene.continueSceneSound = kContinueSceneSound;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
+ _exitScene.continueSceneSound = kContinueSceneSound;
}
void DecoderPuzzle::init() {
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.cpp b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
index f79e6e78d5a..fda706a7ad0 100644
--- a/engines/nancy/action/puzzle/dropsortpuzzle.cpp
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
@@ -113,25 +113,7 @@ void DropSortPuzzle::readData(Common::SeekableReadStream &stream) {
_loseSound.readData(stream);
// Count-prefixed 23-byte hotspot records; the first is the "give up / exit" hotspot.
- 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 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = 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.
- _exitScene.frameID = 0;
- _exitFlag.label = exitFlagLabel;
- _exitFlag.flag = exitFlagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void DropSortPuzzle::init() {
diff --git a/engines/nancy/action/puzzle/hangmanpuzzle.cpp b/engines/nancy/action/puzzle/hangmanpuzzle.cpp
index 580f53e7a0c..d9786410ec3 100644
--- a/engines/nancy/action/puzzle/hangmanpuzzle.cpp
+++ b/engines/nancy/action/puzzle/hangmanpuzzle.cpp
@@ -89,24 +89,7 @@ void HangmanPuzzle::readData(Common::SeekableReadStream &stream) {
// Trailing count-prefixed array of 23-byte give-up hotspots
// {Rect, uint16 cursorType, uint16 sceneID, int16 flagLabel, byte flagValue}.
// The exit always jumps to the scene's first frame.
- int16 numExitZones = stream.readSint16LE();
- for (int16 i = 0; i < numExitZones; ++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;
- _exitScene.frameID = 0;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
HangmanData *HangmanPuzzle::getPuzzleData() const {
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 86f5408a334..e8bdf6b24a8 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -242,28 +242,10 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
_solveScene.readData(stream); // 0x1cf (0x1bd in Nancy13, where it ends the blob)
if (isNancy13) {
- // Shared hotspot records; the first is the give-up hotspot, which replaces
- // the header's cancel scene.
- int16 numZones = stream.readSint16LE();
- for (int16 i = 0; i < numZones; ++i) {
- Common::Rect zone;
- readRect(stream, zone);
- uint16 cursorType = stream.readUint16LE();
- uint16 sceneID = stream.readUint16LE();
- int16 flagLabel = stream.readSint16LE();
- byte flagValue = stream.readByte();
-
- if (i == 0) {
- _exitHotspot = zone;
- _exitCursorType = cursorType;
- _cancelScene._sceneChange.sceneID = sceneID;
- // The field after the scene id is an event-flag label, not a frame.
- _cancelScene._sceneChange.frameID = 0;
- _cancelScene._sceneChange.continueSceneSound = kContinueSceneSound;
- _cancelScene._flag.label = flagLabel;
- _cancelScene._flag.flag = flagValue;
- }
- }
+ // The give-up hotspot replaces the header's cancel scene.
+ readExitHotspot(stream, _exitHotspot, _exitCursorType,
+ _cancelScene._sceneChange, _cancelScene._flag);
+ _cancelScene._sceneChange.continueSceneSound = kContinueSceneSound;
} else {
_cancelScene.readData(stream); // 0x1e8 (ends the 513-byte blob)
}
diff --git a/engines/nancy/action/puzzle/pachinkopuzzle.cpp b/engines/nancy/action/puzzle/pachinkopuzzle.cpp
index 974bcde66a4..139d56e1458 100644
--- a/engines/nancy/action/puzzle/pachinkopuzzle.cpp
+++ b/engines/nancy/action/puzzle/pachinkopuzzle.cpp
@@ -108,24 +108,7 @@ void PachinkoPuzzle::readData(Common::SeekableReadStream &stream) {
readActionZoneArray(stream, _zones, true);
// The base trailer's hotspot records; the first is the give-up exit.
- int16 numExit = stream.readSint16LE();
- for (int16 i = 0; i < numExit; ++i) {
- Common::Rect r;
- readRect(stream, r);
- uint16 cursorType = stream.readUint16LE();
- uint16 sceneID = stream.readUint16LE();
- int16 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = stream.readByte();
-
- if (i == 0) {
- _exitHotspot = r;
- _exitCursorType = cursorType;
- _exitScene.sceneID = sceneID;
- _exitScene.frameID = 0;
- _exitFlag.label = exitFlagLabel;
- _exitFlag.flag = exitFlagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void PachinkoPuzzle::readMachine(Common::SeekableReadStream &stream, Machine &m) {
diff --git a/engines/nancy/action/puzzle/paintpuzzle.cpp b/engines/nancy/action/puzzle/paintpuzzle.cpp
index f961f4e65f4..f39d5632900 100644
--- a/engines/nancy/action/puzzle/paintpuzzle.cpp
+++ b/engines/nancy/action/puzzle/paintpuzzle.cpp
@@ -74,24 +74,7 @@ void PaintPuzzle::readData(Common::SeekableReadStream &stream) {
// Trailing count-prefixed array of 23-byte give-up hotspots
// {Rect, uint16 cursorType, uint16 sceneID, int16 flagLabel, byte flagValue}.
// The exit always jumps to the scene's first frame.
- int16 numExitZones = stream.readSint16LE();
- for (int16 i = 0; i < numExitZones; ++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;
- _exitScene.frameID = 0;
- _exitFlag.label = flagLabel;
- _exitFlag.flag = flagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void PaintPuzzle::init() {
diff --git a/engines/nancy/action/puzzle/pegspuzzle.cpp b/engines/nancy/action/puzzle/pegspuzzle.cpp
index 089044d6b95..0ba6da76c51 100644
--- a/engines/nancy/action/puzzle/pegspuzzle.cpp
+++ b/engines/nancy/action/puzzle/pegspuzzle.cpp
@@ -76,25 +76,7 @@ void PegsPuzzle::readData(Common::SeekableReadStream &stream) {
// 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 (leave the puzzle unsolved), 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 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = 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.
- _exitScene.frameID = 0;
- _exitFlag.label = exitFlagLabel;
- _exitFlag.flag = exitFlagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
// Five random-sound blocks: [0] peg select, [1] jump, [2] selection pulse, [3] win, [4] lose.
_sounds.resize(5);
diff --git a/engines/nancy/action/puzzle/scalepuzzle.cpp b/engines/nancy/action/puzzle/scalepuzzle.cpp
index 0827237ff05..31221d4057e 100644
--- a/engines/nancy/action/puzzle/scalepuzzle.cpp
+++ b/engines/nancy/action/puzzle/scalepuzzle.cpp
@@ -104,26 +104,7 @@ void ScalePuzzle::readData(Common::SeekableReadStream &stream) {
// 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;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
}
void ScalePuzzle::init() {
diff --git a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
index c3d72f76959..a8adcfacb4e 100644
--- a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
+++ b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
@@ -77,25 +77,7 @@ void StepObjectsPuzzle::readData(Common::SeekableReadStream &stream) {
object.startCol = stream.readUint16LE();
}
- int16 numZones = stream.readSint16LE();
- for (int16 i = 0; i < numZones; ++i) {
- Common::Rect zone;
- readRect(stream, zone);
- uint16 cursorType = stream.readUint16LE();
- uint16 sceneID = stream.readUint16LE();
- int16 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = stream.readByte();
-
- if (i == 0) {
- _exitHotspot = zone;
- _exitCursorType = cursorType;
- _exitScene.sceneID = sceneID;
- // 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;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
_sounds.resize(kNumSounds);
for (uint i = 0; i < kNumSounds; ++i) {
diff --git a/engines/nancy/action/puzzle/turningpuzzle.cpp b/engines/nancy/action/puzzle/turningpuzzle.cpp
index 243b22d0b2c..7409131bb82 100644
--- a/engines/nancy/action/puzzle/turningpuzzle.cpp
+++ b/engines/nancy/action/puzzle/turningpuzzle.cpp
@@ -176,25 +176,7 @@ void TurningPuzzle::readDataNancy13(Common::SeekableReadStream &stream) {
// A count-prefixed array of 23-byte hotspot records (as in PegsPuzzle);
// the first one is the "give up" hotspot.
- 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 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = stream.readByte();
-
- if (i == 0) {
- _exitHotspot = r;
- _exitCursorType = cursorType;
- _exitScene._sceneChange.sceneID = sceneID;
- // 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;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene._sceneChange, _exitScene._flag);
uint16 numTypes = stream.readUint16LE();
_pieceTypes.resize(numTypes);
diff --git a/engines/nancy/action/puzzle/wordfindpuzzle.cpp b/engines/nancy/action/puzzle/wordfindpuzzle.cpp
index d701bbad3b0..a5b334caa78 100644
--- a/engines/nancy/action/puzzle/wordfindpuzzle.cpp
+++ b/engines/nancy/action/puzzle/wordfindpuzzle.cpp
@@ -70,25 +70,7 @@ void WordFindPuzzle::readData(Common::SeekableReadStream &stream) {
}
// The shared 23-byte exit-hotspot record.
- 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 exitFlagLabel = stream.readSint16LE();
- byte exitFlagValue = 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.
- _exitScene.frameID = 0;
- _exitFlag.label = exitFlagLabel;
- _exitFlag.flag = exitFlagValue;
- }
- }
+ readExitHotspot(stream, _exitHotspot, _exitCursorType, _exitScene, _exitFlag);
_sounds.resize(4);
for (uint i = 0; i < 4; ++i) {
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index 40ee54a9e0a..2ced2153745 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -406,6 +406,29 @@ Common::String resolveSubtitleText(const Common::String &keyOrText, const Common
return fallback;
}
+void readExitHotspot(Common::SeekableReadStream &stream, Common::Rect &hotspot, uint16 &cursorType,
+ SceneChangeDescription &scene, FlagDescription &flag) {
+ int16 numZones = stream.readSint16LE();
+
+ for (int16 i = 0; i < numZones; ++i) {
+ Common::Rect zone;
+ readRect(stream, zone);
+ uint16 zoneCursor = stream.readUint16LE();
+ uint16 sceneID = stream.readUint16LE();
+ int16 flagLabel = stream.readSint16LE();
+ byte flagValue = stream.readByte();
+
+ if (i == 0) {
+ hotspot = zone;
+ cursorType = zoneCursor;
+ scene.sceneID = sceneID;
+ scene.frameID = 0;
+ flag.label = flagLabel;
+ flag.flag = flagValue;
+ }
+ }
+}
+
Common::String readSubtitleText(Common::SeekableReadStream &stream) {
char buf[30];
stream.read(buf, sizeof(buf));
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 5cc58864d5e..51ba0a6fa32 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -71,6 +71,14 @@ Common::String resolveSubtitleText(const Common::String &keyOrText, const Common
// AUTOTEXT key, falling back to the literal text when the key is not present in the table.
Common::String readSubtitleText(Common::SeekableReadStream &stream);
+// Reads the count-prefixed array of 23-byte hotspot records that the Nancy 13
+// and 14 puzzle records end with, keeping the first: the give-up hotspot. The
+// field after the scene id is an event-flag label, NOT a frame id, so the exit
+// always lands on the scene's first frame. Leaves the scene's continueSceneSound
+// alone, as the puzzles differ on it.
+void readExitHotspot(Common::SeekableReadStream &stream, Common::Rect &hotspot, uint16 &cursorType,
+ SceneChangeDescription &scene, FlagDescription &flag);
+
// Shows `text` as a single line in the game textbox, replacing its current contents.
// Does nothing when `text` is empty or when the player has subtitles disabled. A
// non-negative `overrideFontID` selects a font other than the textbox default. When
Commit: 14100ada2efca2c84dc6572eff8e6f2121c694ae
https://github.com/scummvm/scummvm/commit/14100ada2efca2c84dc6572eff8e6f2121c694ae
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:20+03:00
Commit Message:
NANCY: Implement a new common method to set RenderObject z-ordering
This method will automatically call registerGraphics() to get the
object erased and re-inserted, to simplify the z-ordering code.
Changed paths:
engines/nancy/action/puzzle/assemblypuzzle.cpp
engines/nancy/action/puzzle/buildpuzzle.cpp
engines/nancy/action/puzzle/buildpuzzle.h
engines/nancy/action/puzzle/multibuildpuzzle.cpp
engines/nancy/action/puzzle/multibuildpuzzle.h
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.h
engines/nancy/action/puzzle/tangrampuzzle.cpp
engines/nancy/graphics.cpp
engines/nancy/graphics.h
engines/nancy/misc/mousefollow.h
engines/nancy/renderobject.cpp
engines/nancy/renderobject.h
diff --git a/engines/nancy/action/puzzle/assemblypuzzle.cpp b/engines/nancy/action/puzzle/assemblypuzzle.cpp
index bc143d3f089..d3e3d4662a1 100644
--- a/engines/nancy/action/puzzle/assemblypuzzle.cpp
+++ b/engines/nancy/action/puzzle/assemblypuzzle.cpp
@@ -44,7 +44,7 @@ void AssemblyPuzzle::init() {
piece.setVisible(true);
piece.setTransparent(true);
piece.moveTo(piece.placed ? piece.destRects[piece.curRotation] : piece.startRect);
- piece.setZ(_z + i + _pieces.size());
+ piece.setZOrder(_z + i + _pieces.size());
}
rotateBase(true);
@@ -222,13 +222,11 @@ void AssemblyPuzzle::handleInput(NancyInput &input) {
for (uint j = 1; j < _pieces.size(); ++j) {
Piece &piece = _pieces[j];
if (!piece.placed && piece.getZOrder() > _pieces[i].getZOrder()) {
- piece.setZ(piece.getZOrder() - 1);
- piece.registerGraphics();
+ piece.setZOrder(piece.getZOrder() - 1);
}
}
- _pieces[i].setZ(_z + _pieces.size() * 2);
- _pieces[i].registerGraphics();
+ _pieces[i].setZOrder(_z + _pieces.size() * 2);
} else {
// Clicked the dest of the picked up piece, or an already placed one; simply put it down
_pickedUpPiece = -1;
@@ -319,8 +317,7 @@ void AssemblyPuzzle::rotateBase(bool ccw) {
base = 1;
}
- piece.setZ(_z + base + 4 * (piece.layer - 1));
- piece.registerGraphics();
+ piece.setZOrder(_z + base + 4 * (piece.layer - 1));
piece.moveTo(piece.destRects[piece.curRotation]);
piece._drawSurface.create(_image, piece.srcRects[piece.curRotation]);
diff --git a/engines/nancy/action/puzzle/buildpuzzle.cpp b/engines/nancy/action/puzzle/buildpuzzle.cpp
index 069ffa34e87..4600897d373 100644
--- a/engines/nancy/action/puzzle/buildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/buildpuzzle.cpp
@@ -239,7 +239,7 @@ void BuildPuzzle::init() {
piece.inUse = (i < _numDefined);
piece.sourceID = (int16)i;
piece.liveRect = piece.destRect;
- piece.setZ(_z + (uint16)i + 1);
+ piece.setZOrder(_z + (uint16)i + 1);
updatePieceRender((int16)i);
}
@@ -252,7 +252,7 @@ void BuildPuzzle::init() {
hold._drawSurface.create(_altImage, hold.srcRect);
hold.setTransparent(true);
hold.moveTo(hold.destRect);
- hold.setZ(_z + (uint16)i + 1);
+ hold.setZOrder(_z + (uint16)i + 1);
hold.setVisible(true);
}
@@ -278,8 +278,6 @@ void BuildPuzzle::registerGraphics() {
_cursorItem.registerGraphics();
}
-// The graphics manager keeps its object list sorted as objects are inserted, so
-// a new z only takes effect once the piece is registered again.
byte BuildPuzzle::carriedAmount() const {
return _activeHold != -1 ? MAX<byte>(_holds[_activeHold].amount, 1) : 1;
}
@@ -318,12 +316,6 @@ void BuildPuzzle::updateCursorItem(const Common::Point &mouseVP) {
_cursorItem.setTransparent(true);
_cursorItem.moveTo(dest);
_cursorItem.setVisible(true);
- _cursorItem.registerGraphics();
-}
-
-void BuildPuzzle::setPieceZ(int16 pieceIdx, uint16 z) {
- _pieces[pieceIdx].setZ(z);
- _pieces[pieceIdx].registerGraphics();
}
void BuildPuzzle::updatePieceRender(int16 pieceIdx) {
@@ -482,7 +474,7 @@ void BuildPuzzle::openCloseup(int16 pieceIdx) {
piece.setTransparent(true);
piece.moveTo(dest);
piece.setVisible(true);
- setPieceZ(pieceIdx, (uint16)(_z + _pieces.size() + 2));
+ _pieces[pieceIdx].setZOrder((uint16)(_z + _pieces.size() + 2));
}
void BuildPuzzle::closeCloseup() {
@@ -492,7 +484,7 @@ void BuildPuzzle::closeCloseup() {
int16 pieceIdx = _closeupPiece;
_closeupPiece = -1;
- setPieceZ(pieceIdx, (uint16)(_z + pieceIdx + 1));
+ _pieces[pieceIdx].setZOrder((uint16)(_z + pieceIdx + 1));
updatePieceRender(pieceIdx);
}
@@ -513,7 +505,7 @@ void BuildPuzzle::pickUpPiece(int16 pieceIdx) {
}
_heldPiece = pieceIdx;
- setPieceZ(pieceIdx, (uint16)(_z + _pieces.size() + 1));
+ _pieces[pieceIdx].setZOrder((uint16)(_z + _pieces.size() + 1));
g_nancy->_sound->loadSound(_pickupSound);
g_nancy->_sound->playSound(_pickupSound);
@@ -525,7 +517,7 @@ void BuildPuzzle::returnPiece(int16 pieceIdx) {
piece.liveRect = piece.destRect;
piece.assignedZone = -1;
_heldPiece = -1;
- setPieceZ(pieceIdx, (uint16)(_z + pieceIdx + 1));
+ _pieces[pieceIdx].setZOrder((uint16)(_z + pieceIdx + 1));
updatePieceRender(pieceIdx);
}
@@ -583,7 +575,7 @@ void BuildPuzzle::placePiece(int16 pieceIdx, int16 zoneIdx, const Common::Point
g_nancy->_sound->playSound(_dropSound);
_heldPiece = -1;
- setPieceZ(placedIdx, (uint16)(_z + placedIdx + 1));
+ _pieces[placedIdx].setZOrder((uint16)(_z + placedIdx + 1));
updatePieceRender(placedIdx);
bool solved = checkSolved();
@@ -692,7 +684,6 @@ void BuildPuzzle::handleInput(NancyInput &input) {
// The scoop is emptied by the drop and goes back to its place.
if (_activeHold != -1) {
_holds[_activeHold].setVisible(true);
- _holds[_activeHold].registerGraphics();
_activeHold = -1;
}
} else {
@@ -755,12 +746,10 @@ void BuildPuzzle::handleInput(NancyInput &input) {
if (clicked) {
if (_activeHold == (int16)i) {
_holds[i].setVisible(true);
- _holds[i].registerGraphics();
_activeHold = -1;
} else {
if (_activeHold != -1) {
_holds[_activeHold].setVisible(true);
- _holds[_activeHold].registerGraphics();
}
_activeHold = (int16)i;
diff --git a/engines/nancy/action/puzzle/buildpuzzle.h b/engines/nancy/action/puzzle/buildpuzzle.h
index fafabdf034d..df1eb1a6577 100644
--- a/engines/nancy/action/puzzle/buildpuzzle.h
+++ b/engines/nancy/action/puzzle/buildpuzzle.h
@@ -87,7 +87,6 @@ protected:
struct HoldSlot : RenderObject {
HoldSlot() : RenderObject(0) {}
- void setZ(uint16 z) { _z = z; _needsRedraw = true; }
bool isViewportRelative() const override { return true; }
Common::Rect srcRect; // empty
@@ -100,7 +99,6 @@ protected:
struct Piece : RenderObject {
Piece() : RenderObject(0) {}
- void setZ(uint16 z) { _z = z; _needsRedraw = true; }
bool isViewportRelative() const override { return true; }
Common::Rect srcRect; // art while the piece sits at home
@@ -196,7 +194,6 @@ protected:
bool _isInitialized = false;
void setPieceCursor(bool isHeld);
- void setPieceZ(int16 pieceIdx, uint16 z);
// Draw the carried art at the cursor, or hide it when nothing is carried
void updateCursorItem(const Common::Point &mouseVP);
// The scoop a piece is dropped with, 1 when it is carried by hand
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index 17031a3378c..f91fd902c7a 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -86,7 +86,7 @@ void MultiBuildPuzzle::init() {
updatePieceRender(i);
p.setVisible(true);
p.setTransparent(true);
- p.setZ((uint16)(_z + i + 1));
+ p.setZOrder((uint16)(_z + i + 1));
}
_isInitialized = true;
@@ -407,7 +407,7 @@ void MultiBuildPuzzle::spawnCounterPiece(int srcIdx) {
updatePieceRender(newIdx);
_pieces[newIdx].setVisible(true);
_pieces[newIdx].setTransparent(true);
- _pieces[newIdx].setZ((uint16)(_z + newIdx + 1));
+ _pieces[newIdx].setZOrder((uint16)(_z + newIdx + 1));
_pieces[newIdx].registerGraphics();
}
@@ -663,7 +663,7 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
pp.curRotation = (pp.curRotation + 1) % 4;
if (!pp.hasSurface[pp.curRotation])
pp.curRotation = 0;
- pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.setZOrder((uint16)(_z + (int)_pieces.size() * 2));
pp.registerGraphics();
_isDragging = true;
_pickedUpPiece = topmostRot;
@@ -682,7 +682,7 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
Piece &pp = _pieces[topmost];
pp.isPlaced = false;
pp.curRotation = 0;
- pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.setZOrder((uint16)(_z + (int)_pieces.size() * 2));
pp.registerGraphics();
if (_hasCloseupImage && !pp.cuSrcRect.isEmpty()) {
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index 0b368b7a0ec..d6bffaafd4e 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -78,8 +78,6 @@ protected:
Graphics::ManagedSurface rotateSurfaces[4];
bool hasSurface[4] = {};
- void setZ(uint16 z) { _z = z; _needsRedraw = true; }
-
bool isViewportRelative() const override { return true; }
};
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index e8bdf6b24a8..ce1b3246fd5 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -146,7 +146,7 @@ void OneBuildPuzzle::init() {
p.setVisible(true);
p.setTransparent(true);
- p.setZ(_z + (uint16)i + 1);
+ p.setZOrder(_z + (uint16)i + 1);
updatePieceRender(i);
}
@@ -832,7 +832,7 @@ void OneBuildPuzzle::pickUpPiece(int16 pieceIdx, bool rotate) {
_isDragging = true;
_pickedUpWidth = pp.rotateSurfaces[pp.curRotation].w;
_pickedUpHeight = pp.rotateSurfaces[pp.curRotation].h;
- pp.setZ((uint16)(_z + (int)_pieces.size() * 2));
+ pp.setZOrder((uint16)(_z + (int)_pieces.size() * 2));
pp.registerGraphics();
}
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index 170b6c125d5..911f07c95d0 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -113,8 +113,6 @@ protected:
Graphics::ManagedSurface altSurface;
bool useAltSurface = false;
- void setZ(uint16 z) { _z = z; _needsRedraw = true; }
-
bool isViewportRelative() const override { return true; }
};
diff --git a/engines/nancy/action/puzzle/tangrampuzzle.cpp b/engines/nancy/action/puzzle/tangrampuzzle.cpp
index 7085279e557..79a205eb5c3 100644
--- a/engines/nancy/action/puzzle/tangrampuzzle.cpp
+++ b/engines/nancy/action/puzzle/tangrampuzzle.cpp
@@ -63,7 +63,7 @@ void TangramPuzzle::init() {
curTile->setTransparent(true);
curTile->setVisible(true);
drawToBuffer(*curTile);
- curTile->setZ(_z + 1);
+ curTile->setZOrder(_z + 1);
// Then, add the actual tiles
for (uint i = 0; i < _tileSrcs.size(); ++i) {
@@ -74,7 +74,7 @@ void TangramPuzzle::init() {
curTile->moveTo(_tileDests[i]);
curTile->setTransparent(true);
curTile->setVisible(true);
- curTile->setZ(_z + curTile->_id + 1);
+ curTile->setZOrder(_z + curTile->_id + 1);
curTile->drawMask();
drawToBuffer(*curTile);
@@ -366,13 +366,11 @@ void TangramPuzzle::moveToTop(uint id) {
for (uint i = 1; i < _tiles.size(); ++i) {
Tile &tile = _tiles[i];
if (tile.getZOrder() > _tiles[id].getZOrder()) {
- tile.setZ(tile.getZOrder() - 1);
- tile.registerGraphics();
+ tile.setZOrder(tile.getZOrder() - 1);
}
}
- _tiles[id].setZ(_z + _tiles.size());
- _tiles[id].registerGraphics();
+ _tiles[id].setZOrder(_z + _tiles.size());
}
void TangramPuzzle::redrawBuffer(const Common::Rect &rect) {
diff --git a/engines/nancy/graphics.cpp b/engines/nancy/graphics.cpp
index ee931391d9d..b1aaf6465fc 100644
--- a/engines/nancy/graphics.cpp
+++ b/engines/nancy/graphics.cpp
@@ -192,6 +192,20 @@ void GraphicsManager::addObject(RenderObject *object) {
_objects.insert(object);
}
+void GraphicsManager::reorderObject(RenderObject *object) {
+ // The array is kept sorted as objects are inserted, so an object that has
+ // changed its z depth has to be taken out and put back to move. One that is
+ // not in the list yet has nothing to reorder; it will be sorted when it is
+ // added.
+ for (auto &r : _objects) {
+ if (r == object) {
+ _objects.erase(&r);
+ _objects.insert(object);
+ return;
+ }
+ }
+}
+
void GraphicsManager::removeObject(RenderObject *object) {
for (auto &r : _objects) {
if (r == object) {
diff --git a/engines/nancy/graphics.h b/engines/nancy/graphics.h
index f2e9d7bb0f7..c23fe308d40 100644
--- a/engines/nancy/graphics.h
+++ b/engines/nancy/graphics.h
@@ -44,6 +44,8 @@ public:
void addObject(RenderObject *object);
void removeObject(RenderObject *object);
+ // Move an already-added object to its new place in the draw order
+ void reorderObject(RenderObject *object);
void clearObjects();
void redrawAll();
diff --git a/engines/nancy/misc/mousefollow.h b/engines/nancy/misc/mousefollow.h
index 1e0eef08ff0..f5df29ce61d 100644
--- a/engines/nancy/misc/mousefollow.h
+++ b/engines/nancy/misc/mousefollow.h
@@ -49,7 +49,6 @@ public:
virtual void pickUp() { _isPickedUp = true; }
virtual void putDown() { _isPickedUp = false; }
- void setZ(uint16 z) { _z = z; _needsRedraw = true; }
void handleInput(NancyInput &input);
bool isViewportRelative() const override { return true; }
diff --git a/engines/nancy/renderobject.cpp b/engines/nancy/renderobject.cpp
index bde35d17477..5281c06bb3c 100644
--- a/engines/nancy/renderobject.cpp
+++ b/engines/nancy/renderobject.cpp
@@ -39,6 +39,16 @@ RenderObject::RenderObject(uint16 zOrder, Graphics::ManagedSurface &surface, con
_screenPosition = destBounds;
}
+void RenderObject::setZOrder(uint16 z) {
+ if (_z == z) {
+ return;
+ }
+
+ _z = z;
+ _needsRedraw = true;
+ g_nancy->_graphics->reorderObject(this);
+}
+
void RenderObject::init() {
_previousScreenPosition = _screenPosition;
}
diff --git a/engines/nancy/renderobject.h b/engines/nancy/renderobject.h
index 5f437d86738..5befdb74601 100644
--- a/engines/nancy/renderobject.h
+++ b/engines/nancy/renderobject.h
@@ -76,7 +76,9 @@ public:
Common::Rect getBounds() const { return Common::Rect(_screenPosition.width(), _screenPosition.height()); }
uint16 getZOrder() const { return _z; }
- void setZOrder(uint16 z) { _z = z; }
+ // Also moves the object in the draw order, so changing the z of a registered
+ // object takes effect without re-registering it
+ void setZOrder(uint16 z);
Graphics::ManagedSurface _drawSurface;
Commit: 17e88d7a7c27749b8c59acf7cd7578b8679a7ae9
https://github.com/scummvm/scummvm/commit/17e88d7a7c27749b8c59acf7cd7578b8679a7ae9
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:22+03:00
Commit Message:
NANCY: Use MouseFollowObject in puzzles where objects are dragged
Changed paths:
engines/nancy/action/puzzle/beadpuzzle.cpp
engines/nancy/action/puzzle/beadpuzzle.h
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/gridmappuzzle.cpp
engines/nancy/action/puzzle/gridmappuzzle.h
engines/nancy/action/puzzle/mindpuzzle.cpp
engines/nancy/action/puzzle/mindpuzzle.h
engines/nancy/action/puzzle/pegspuzzle.cpp
engines/nancy/action/puzzle/pegspuzzle.h
engines/nancy/action/puzzle/sortpuzzle.cpp
engines/nancy/action/puzzle/sortpuzzle.h
engines/nancy/action/puzzle/stepobjectspuzzle.cpp
engines/nancy/action/puzzle/stepobjectspuzzle.h
engines/nancy/misc/mousefollow.h
diff --git a/engines/nancy/action/puzzle/beadpuzzle.cpp b/engines/nancy/action/puzzle/beadpuzzle.cpp
index e5b8663c814..80a97b5dd31 100644
--- a/engines/nancy/action/puzzle/beadpuzzle.cpp
+++ b/engines/nancy/action/puzzle/beadpuzzle.cpp
@@ -139,6 +139,7 @@ void BeadPuzzle::execute() {
case kBegin:
init();
registerGraphics();
+ _heldBeadObject.registerGraphics();
_state = kRun;
// fall through
@@ -153,7 +154,7 @@ void BeadPuzzle::execute() {
if (g_system->getMillis() >= _dropNextTick) {
if (_dropCurrentSlot <= (int)_placed.size()) {
_placed.push_back(_heldBead);
- _heldBead = -1;
+ holdBead(-1, nullptr);
_subState = kPlaying;
persistState();
} else {
@@ -217,10 +218,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
- if (_heldBead != -1 && _heldDrawPos != mouseVP) {
- _heldDrawPos = mouseVP;
- redraw();
- }
+ _heldBeadObject.handleInput(input);
if (_subState == kShowingResult) {
if (!_resultHotspot.isEmpty() && _resultHotspot.contains(mouseVP)) {
@@ -230,7 +228,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
g_nancy->_sound->stopSound(_wrongSound);
if (!_placed.empty())
_placed.pop_back();
- _heldBead = -1;
+ holdBead(-1, nullptr);
_subState = kPlaying;
_resultKind = kNoResult;
_resultSoundPlayed = false;
@@ -246,8 +244,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if ((int)_placed.size() < (int)_numSlots) {
- _heldBead = (int16)i;
- _heldDrawPos = mouseVP;
+ holdBead((int16)i, &input);
if (_pickupSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_pickupSound);
g_nancy->_sound->playSound(_pickupSound);
@@ -282,9 +279,11 @@ void BeadPuzzle::handleInput(NancyInput &input) {
g_nancy->_sound->loadSound(_placeSound);
g_nancy->_sound->playSound(_placeSound);
}
- // Animate the bead sliding from the bottom of the thread up
- // to the next free slot before committing it.
+ // The bead leaves the cursor and slides from the bottom of the
+ // thread up to the next free slot before it is committed.
_subState = kDroppingBead;
+ _heldBeadObject.setVisible(false);
+ _heldBeadObject.putDown();
_dropCurrentSlot = (int16)((int)_numSlots - 1);
_dropNextTick = g_system->getMillis() + kDropTickMs;
redraw();
@@ -297,7 +296,7 @@ void BeadPuzzle::handleInput(NancyInput &input) {
_pickupHotspots[_heldBead].contains(mouseVP)) {
g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _heldBead = -1;
+ holdBead(-1, nullptr);
g_nancy->_sound->stopSound(_pickupSound);
redraw();
}
@@ -344,6 +343,28 @@ void BeadPuzzle::evaluate() {
redraw();
}
+// Puts a bead on the cursor, or takes the held one off it for a bead of -1. The
+// input is only needed when picking one up, to place it under the cursor at once.
+void BeadPuzzle::holdBead(int16 bead, NancyInput *input) {
+ _heldBead = bead;
+
+ if (_heldBead >= 0 && _heldBead < (int16)_numBeadTypes &&
+ _image.getBounds().contains(_beadSrcRects[_heldBead])) {
+ _heldBeadObject._drawSurface.create(_image, _beadSrcRects[_heldBead]);
+ _heldBeadObject.setTransparent(true);
+ _heldBeadObject.setVisible(true);
+ _heldBeadObject.pickUp();
+ if (input) {
+ _heldBeadObject.handleInput(*input);
+ }
+ } else {
+ _heldBeadObject.setVisible(false);
+ _heldBeadObject.putDown();
+ }
+
+ redraw();
+}
+
void BeadPuzzle::redraw() {
_drawSurface.clear(_drawSurface.getTransparentColor());
@@ -371,13 +392,6 @@ void BeadPuzzle::redraw() {
const Common::Rect &dst = _slotDestRects[_dropCurrentSlot];
if (!src.isEmpty() && !dst.isEmpty())
_drawSurface.blitFrom(_image, src, Common::Point(dst.left, dst.top));
- } else if (_subState == kPlaying && _heldBead >= 0 && _heldBead < (int)_numBeadTypes) {
- const Common::Rect &src = _beadSrcRects[_heldBead];
- if (!src.isEmpty()) {
- int x = _heldDrawPos.x - src.width() / 2;
- int y = _heldDrawPos.y - src.height() / 2;
- _drawSurface.blitFrom(_image, src, Common::Point(x, y));
- }
}
_needsRedraw = true;
diff --git a/engines/nancy/action/puzzle/beadpuzzle.h b/engines/nancy/action/puzzle/beadpuzzle.h
index 2d3bfdd7c57..c31713c6431 100644
--- a/engines/nancy/action/puzzle/beadpuzzle.h
+++ b/engines/nancy/action/puzzle/beadpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_BEADPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
#include "engines/nancy/commontypes.h"
namespace Nancy {
@@ -106,7 +107,7 @@ protected:
Common::Array<int16> _placed;
int16 _heldBead = -1;
- Common::Point _heldDrawPos;
+ Misc::MouseFollowObject _heldBeadObject;
int16 _dropCurrentSlot = 0;
uint32 _dropNextTick = 0;
@@ -120,6 +121,7 @@ protected:
Graphics::ManagedSurface _image;
void redraw();
+ void holdBead(int16 bead, NancyInput *input);
void evaluate();
void persistState();
};
diff --git a/engines/nancy/action/puzzle/blockspuzzle.cpp b/engines/nancy/action/puzzle/blockspuzzle.cpp
index 0d98ce5731d..7db658c3c85 100644
--- a/engines/nancy/action/puzzle/blockspuzzle.cpp
+++ b/engines/nancy/action/puzzle/blockspuzzle.cpp
@@ -114,6 +114,7 @@ void BlocksPuzzle::init() {
redraw();
registerGraphics();
+ _carriedObject.registerGraphics();
}
Common::Rect BlocksPuzzle::blockSrc(int16 block, byte rotation, int16 frame) const {
@@ -148,7 +149,24 @@ bool BlocksPuzzle::isSolved() const {
return true;
}
-void BlocksPuzzle::pickUp(int16 cell) {
+void BlocksPuzzle::updateCarried(NancyInput *input) {
+ if (_carriedBlock == kNoBlock || !_image.getBounds().contains(_carriedSrc)) {
+ _carriedObject.setVisible(false);
+ _carriedObject.putDown();
+ return;
+ }
+
+ _carriedObject._drawSurface.create(_image, _carriedSrc);
+ _carriedObject.setTransparent(true);
+ _carriedObject.setVisible(true);
+ _carriedObject.pickUp();
+
+ if (input) {
+ _carriedObject.handleInput(*input);
+ }
+}
+
+void BlocksPuzzle::pickUp(int16 cell, NancyInput &input) {
playSoundBlock(_sounds[kHandleSound]);
if (cell == kTurntableCell) {
@@ -164,10 +182,11 @@ void BlocksPuzzle::pickUp(int16 cell) {
c.block = kNoBlock;
}
+ updateCarried(&input);
redraw();
}
-void BlocksPuzzle::drop(int16 cell) {
+void BlocksPuzzle::drop(int16 cell, NancyInput &input) {
playSoundBlock(_sounds[kHandleSound]);
// Putting a block down where one already sits swaps them, so the displaced block ends
@@ -196,6 +215,7 @@ void BlocksPuzzle::drop(int16 cell) {
}
}
+ updateCarried(&input);
redraw();
}
@@ -233,13 +253,6 @@ void BlocksPuzzle::redraw() {
_drawSurface.blitFrom(_image, _turnSrc, Common::Point(_turntableDest.left, _turntableDest.top));
}
- // The carried block rides the cursor. While it is being turned in hand its sprite is
- // the animation's current frame.
- if (_carriedBlock != kNoBlock) {
- _drawSurface.blitFrom(_image, _carriedSrc,
- Common::Point(_dragPos.x - _carriedSrc.width() / 2, _dragPos.y - _carriedSrc.height() / 2));
- }
-
_needsRedraw = true;
}
@@ -332,6 +345,7 @@ void BlocksPuzzle::execute() {
}
_carriedSrc = _turnSrc;
+ updateCarried(nullptr);
redraw();
break;
}
@@ -370,12 +384,7 @@ void BlocksPuzzle::handleInput(NancyInput &input) {
}
// The carried block keeps tracking the cursor even while it is being turned.
- if (_carriedBlock != kNoBlock) {
- 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();
- }
+ _carriedObject.handleInput(input);
if (_puzzleState != kPlaying) {
return;
@@ -401,7 +410,7 @@ void BlocksPuzzle::handleInput(NancyInput &input) {
if (cell != kNoBlock) {
setDataCursor(_carryCursorType);
if (click) {
- drop(cell);
+ drop(cell, input);
}
input.eatMouseInput();
@@ -415,7 +424,7 @@ void BlocksPuzzle::handleInput(NancyInput &input) {
if (cell != kNoBlock) {
setDataCursor(_carryCursorType);
if (click) {
- pickUp(cell);
+ pickUp(cell, input);
}
input.eatMouseInput();
@@ -436,7 +445,7 @@ void BlocksPuzzle::handleInput(NancyInput &input) {
if (NancySceneState.getViewport().convertViewportToScreen(_turntableDest).contains(input.mousePos)) {
setDataCursor(_carryCursorType);
if (click) {
- pickUp(kTurntableCell);
+ pickUp(kTurntableCell, input);
}
input.eatMouseInput();
diff --git a/engines/nancy/action/puzzle/blockspuzzle.h b/engines/nancy/action/puzzle/blockspuzzle.h
index 432f60c067a..668136f5a00 100644
--- a/engines/nancy/action/puzzle/blockspuzzle.h
+++ b/engines/nancy/action/puzzle/blockspuzzle.h
@@ -24,6 +24,7 @@
#include "engines/nancy/commontypes.h"
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
namespace Nancy {
namespace Action {
@@ -94,8 +95,10 @@ protected:
// occupied cells; putting down considers every cell.
int16 cellAtCursor(const Common::Point &mousePos, bool occupiedOnly) const;
bool isSolved() const;
- void pickUp(int16 cell);
- void drop(int16 cell);
+ void pickUp(int16 cell, NancyInput &input);
+ void drop(int16 cell, NancyInput &input);
+ // Syncs the block riding the cursor with _carriedBlock / _carriedSrc
+ void updateCarried(NancyInput *input);
void startTurn();
void redraw();
// Zone cursors take the idle sprite of their type, hover/drag cursors the hotspot one.
@@ -133,7 +136,7 @@ protected:
int16 _carriedBlock = kNoBlock; // the block on the cursor, or kNoBlock
byte _carriedRotation = 0;
Common::Rect _carriedSrc;
- Common::Point _dragPos; // cursor position (viewport space) while carrying
+ Misc::MouseFollowObject _carriedObject;
int16 _turnBlock = kNoBlock; // the block being turned, or kNoBlock
byte _turnRotation = 0;
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.cpp b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
index fda706a7ad0..f610e9800f6 100644
--- a/engines/nancy/action/puzzle/dropsortpuzzle.cpp
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
@@ -158,6 +158,7 @@ void DropSortPuzzle::init() {
redraw();
registerGraphics();
+ _carriedObject.registerGraphics();
}
Common::Point DropSortPuzzle::beltPosition(float progress) const {
@@ -195,10 +196,21 @@ int DropSortPuzzle::binAtCursor(const Common::Point &mousePos) const {
return -1;
}
-Common::Point DropSortPuzzle::cursorToViewport(const Common::Point &mousePos) const {
- Common::Rect screenPt(mousePos.x, mousePos.y, mousePos.x + 1, mousePos.y + 1);
- Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
- return Common::Point(vpPt.left, vpPt.top);
+// Puts a candy on the cursor, or takes the carried one off it for a type of kNoItem.
+void DropSortPuzzle::carryItem(int16 type, NancyInput &input) {
+ _carriedType = type;
+
+ if (_carriedType != kNoItem && _carriedType < (int16)_itemSrcRects.size() &&
+ _image.getBounds().contains(_itemSrcRects[_carriedType])) {
+ _carriedObject._drawSurface.create(_image, _itemSrcRects[_carriedType]);
+ _carriedObject.setTransparent(true);
+ _carriedObject.setVisible(true);
+ _carriedObject.pickUp();
+ _carriedObject.handleInput(input);
+ } else {
+ _carriedObject.setVisible(false);
+ _carriedObject.putDown();
+ }
}
void DropSortPuzzle::applyDrop(int binIndex, int16 type) {
@@ -252,14 +264,6 @@ void DropSortPuzzle::redraw() {
Common::Point(_strikeDestRects[i].left, _strikeDestRects[i].top));
}
- // The candy currently being carried, following the cursor.
- if (_carriedType != kNoItem) {
- const Common::Rect &src = _itemSrcRects[_carriedType];
- int w = src.width();
- int h = src.height();
- _drawSurface.blitFrom(_image, src, Common::Point(_dragPos.x - w / 2, _dragPos.y - h / 2));
- }
-
drawCounter();
_needsRedraw = true;
@@ -370,7 +374,7 @@ void DropSortPuzzle::execute() {
}
// The belt and candies move every frame, so keep the overlay in sync.
- if (moviesUpdated || !_items.empty() || _carriedType != kNoItem) {
+ if (moviesUpdated || !_items.empty()) {
redraw();
}
@@ -425,14 +429,13 @@ void DropSortPuzzle::handleInput(NancyInput &input) {
if (_carriedType != kNoItem) {
// Raw Nancy13 cursor type ids from the AR data, applied via the "set from script" path.
g_nancy->_cursor->setCursorType((CursorManager::CursorType)_dragCursorType, true);
- _dragPos = cursorToViewport(input.mousePos);
- redraw();
+ _carriedObject.handleInput(input);
if (click) {
int bin = binAtCursor(input.mousePos);
if (bin >= 0) {
applyDrop(bin, _carriedType);
- _carriedType = kNoItem;
+ carryItem(kNoItem, input);
redraw();
}
}
@@ -446,9 +449,8 @@ void DropSortPuzzle::handleInput(NancyInput &input) {
if (item >= 0) {
g_nancy->_cursor->setCursorType((CursorManager::CursorType)_hoverCursorType, true);
if (click) {
- _carriedType = _items[item].type;
+ carryItem(_items[item].type, input);
_items.remove_at(item);
- _dragPos = cursorToViewport(input.mousePos);
playSoundBlock(_pickupSound);
redraw();
}
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.h b/engines/nancy/action/puzzle/dropsortpuzzle.h
index b5619b13384..904c9842cb0 100644
--- a/engines/nancy/action/puzzle/dropsortpuzzle.h
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.h
@@ -25,6 +25,7 @@
#include "engines/nancy/commontypes.h"
#include "engines/nancy/movieplayer.h"
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
namespace Nancy {
namespace Action {
@@ -73,11 +74,11 @@ protected:
Common::Rect itemDestAt(const Common::Point &pos, int16 type) const;
int itemAtCursor(const Common::Point &mousePos) const; // belt item under the cursor, or -1
int binAtCursor(const Common::Point &mousePos) const; // bin under the cursor, or -1
- Common::Point cursorToViewport(const Common::Point &mousePos) const;
// Drops a candy into a bin: correct-bin sound if accepted, else a mistake (for an
// enabled bin). Also handles candies falling off the belt into the reject bin.
void applyDrop(int binIndex, int16 type);
+ void carryItem(int16 type, NancyInput &input);
void redraw();
void drawCounter();
@@ -137,7 +138,7 @@ protected:
int _rejectBin = -1; // bin at the end of the belt (unsorted candies land here)
int16 _carriedType = kNoItem;
- Common::Point _dragPos;
+ Misc::MouseFollowObject _carriedObject;
int _strikes = 0;
bool _solved = false;
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index bfdf8a71780..0d5061dd573 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -201,6 +201,7 @@ void GridMapPuzzle::execute() {
case kBegin:
init();
registerGraphics();
+ _heldObject.registerGraphics();
_state = kRun;
// fall through
@@ -345,11 +346,7 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
- if (_heldItem != -1 && _heldDrawPos != mouseVP) {
- _heldDrawPos = mouseVP;
- _skipHeldDraw = false;
- redraw();
- }
+ _heldObject.handleInput(input);
int row = 0, col = 0;
bool hitMap = hitTestMap(mouseVP, row, col);
@@ -381,22 +378,12 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
if (!(input.input & NancyInput::kLeftMouseButtonUp))
return;
- // Reset the held-draw suppression flag on every click; the swap branch
- // below sets it back to true when appropriate.
- _skipHeldDraw = false;
-
- // Sync the held draw position to the click point. Mouse-move tracking
- // only runs while something is held, so without this a pickup right
- // after a plain drop would render the new held glyph at the previous
- // drop's coordinates for one frame.
- _heldDrawPos = mouseVP;
-
int existingItem = hitMap ? findItemInMap(row, col) : findItemInItems(iRow, iCol);
if (_heldItem == -1) {
if (existingItem == -1)
return;
- _heldItem = existingItem;
+ holdItem(existingItem, input);
if (hitMap)
_items[_heldItem].inMap = false;
else
@@ -422,8 +409,7 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
_items[_heldItem].itemsRow = (int16)iRow;
_items[_heldItem].itemsCol = (int16)iCol;
}
- _heldItem = existingItem;
- _skipHeldDraw = true;
+ holdItem(existingItem, input);
if (_pickupSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_pickupSound);
g_nancy->_sound->playSound(_pickupSound);
@@ -438,7 +424,7 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
_items[_heldItem].itemsRow = (int16)iRow;
_items[_heldItem].itemsCol = (int16)iCol;
}
- _heldItem = -1;
+ holdItem(-1, input);
if (_placeSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_placeSound);
g_nancy->_sound->playSound(_placeSound);
@@ -474,6 +460,24 @@ void GridMapPuzzle::checkSolved() {
}
}
+// Puts an item on the cursor, or takes the held one off it for an item of -1. The
+// glyph that rides the cursor is the small map sprite, not the large items-grid one.
+void GridMapPuzzle::holdItem(int item, NancyInput &input) {
+ _heldItem = item;
+
+ if (_heldItem >= 0 && _heldItem < (int)_numItems &&
+ _boardImage.getBounds().contains(_mapItemSrcRects[_heldItem])) {
+ _heldObject._drawSurface.create(_boardImage, _mapItemSrcRects[_heldItem]);
+ _heldObject.setTransparent(true);
+ _heldObject.setVisible(true);
+ _heldObject.pickUp();
+ _heldObject.handleInput(input);
+ } else {
+ _heldObject.setVisible(false);
+ _heldObject.putDown();
+ }
+}
+
void GridMapPuzzle::redraw() {
_drawSurface.clear(_drawSurface.getTransparentColor());
@@ -526,17 +530,6 @@ 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 = _mapItemSrcRects[_heldItem];
- if (!src.isEmpty()) {
- int x = _heldDrawPos.x - src.width() / 2;
- int y = _heldDrawPos.y - src.height() / 2;
- _drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
- }
- }
-
_needsRedraw = true;
}
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.h b/engines/nancy/action/puzzle/gridmappuzzle.h
index c0c964cab45..7b493a7fe67 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.h
+++ b/engines/nancy/action/puzzle/gridmappuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_GRIDMAPPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
#include "engines/nancy/commontypes.h"
namespace Nancy {
@@ -131,11 +132,7 @@ protected:
ItemSlot _items[kMaxItems];
int _heldItem = -1;
- Common::Point _heldDrawPos;
- // Set on a swap (drop onto an occupied cell) so the freshly-placed
- // glyph isn't covered by the picked-up one drawn at the cursor.
- // Cleared on the next mouse move.
- bool _skipHeldDraw = false;
+ Misc::MouseFollowObject _heldObject;
bool _isSolved = false;
Graphics::ManagedSurface _boardImage;
@@ -144,6 +141,7 @@ protected:
void initState();
void persistState();
void redraw();
+ void holdItem(int item, NancyInput &input);
Common::Rect mapCellRect(int row, int col) const;
Common::Rect itemsCellRect(int row, int col) const;
Common::Rect mapCellHitRect(int row, int col) const;
diff --git a/engines/nancy/action/puzzle/mindpuzzle.cpp b/engines/nancy/action/puzzle/mindpuzzle.cpp
index f66e0f38515..6f359d81ed2 100644
--- a/engines/nancy/action/puzzle/mindpuzzle.cpp
+++ b/engines/nancy/action/puzzle/mindpuzzle.cpp
@@ -172,6 +172,22 @@ void MindPuzzle::drawPeg(int color, const Common::Rect &dest) {
_drawSurface.blitFrom(_image, _ballSrcRects[color], dest);
}
+// Puts the given color on the cursor, or takes the held ball off it for a color of -1.
+void MindPuzzle::holdBall(int color, NancyInput &input) {
+ _heldColor = color;
+
+ if (_heldColor >= 0 && _heldColor < (int)_ballSrcRects.size()) {
+ _heldBall._drawSurface.create(_image, _ballSrcRects[_heldColor]);
+ _heldBall.setTransparent(true);
+ _heldBall.setVisible(true);
+ _heldBall.pickUp();
+ _heldBall.handleInput(input);
+ } else {
+ _heldBall.setVisible(false);
+ _heldBall.putDown();
+ }
+}
+
void MindPuzzle::redraw() {
_drawSurface.clear(g_nancy->_graphics->getTransColor());
@@ -191,16 +207,6 @@ void MindPuzzle::redraw() {
}
}
- // The ball being dragged follows the cursor, centered on it.
- if (_heldColor >= 0 && _heldColor < (int)_ballSrcRects.size()) {
- const Common::Rect &src = _ballSrcRects[_heldColor];
- int w = src.width();
- int h = src.height();
- Common::Rect dst(_heldDrawPos.x - w / 2, _heldDrawPos.y - h / 2,
- _heldDrawPos.x - w / 2 + w, _heldDrawPos.y - h / 2 + h);
- _drawSurface.blitFrom(_image, src, dst);
- }
-
_needsRedraw = true;
}
@@ -259,6 +265,7 @@ void MindPuzzle::execute() {
case kBegin:
init();
registerGraphics();
+ _heldBall.registerGraphics();
_state = kRun;
// fall through
case kRun:
@@ -312,20 +319,16 @@ void MindPuzzle::handleInput(NancyInput &input) {
// ball already there) or anywhere else to send it back to the palette.
if (_heldColor != -1) {
g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
-
- if (_heldDrawPos != mouseVP) {
- _heldDrawPos = mouseVP;
- redraw();
- }
+ _heldBall.handleInput(input);
if (input.input & NancyInput::kLeftMouseButtonUp) {
int slot = -1;
if (slotHit(mouseVP, slot)) {
int16 previous = _guess[_currentRow][slot];
_guess[_currentRow][slot] = _heldColor;
- _heldColor = previous; // keep dragging the displaced ball, if any
+ holdBall(previous, input); // keep dragging the displaced ball, if any
} else {
- _heldColor = -1;
+ holdBall(-1, input);
}
redraw();
}
@@ -337,9 +340,7 @@ void MindPuzzle::handleInput(NancyInput &input) {
if (color != -1) {
g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _heldColor = color;
- _heldDrawPos = mouseVP;
- redraw();
+ holdBall(color, input);
}
return;
}
@@ -349,9 +350,8 @@ void MindPuzzle::handleInput(NancyInput &input) {
if (slotHit(mouseVP, slot) && _guess[_currentRow][slot] != -1) {
g_nancy->_cursor->setCursorType(CursorManager::kDragHand);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _heldColor = _guess[_currentRow][slot];
+ holdBall(_guess[_currentRow][slot], input);
_guess[_currentRow][slot] = -1;
- _heldDrawPos = mouseVP;
redraw();
}
return;
diff --git a/engines/nancy/action/puzzle/mindpuzzle.h b/engines/nancy/action/puzzle/mindpuzzle.h
index c06e1c9c198..0e2ed73ff13 100644
--- a/engines/nancy/action/puzzle/mindpuzzle.h
+++ b/engines/nancy/action/puzzle/mindpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_MINDPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
#include "engines/nancy/commontypes.h"
namespace Nancy {
@@ -91,7 +92,7 @@ protected:
int16 _currentRow = 0;
int16 _heldColor = -1;
- Common::Point _heldDrawPos; // viewport-local cursor while a ball is held
+ Misc::MouseFollowObject _heldBall; // the held ball's sprite, riding the cursor
int16 _remainingGuesses = 0;
bool _solved = false;
@@ -103,6 +104,7 @@ protected:
void generateSecret();
void scoreRow(int row);
void drawPeg(int color, const Common::Rect &dest);
+ void holdBall(int color, NancyInput &input);
void redraw();
int paletteHit(const Common::Point &mouseVP) const;
bool slotHit(const Common::Point &mouseVP, int &slot) const;
diff --git a/engines/nancy/action/puzzle/pegspuzzle.cpp b/engines/nancy/action/puzzle/pegspuzzle.cpp
index 0ba6da76c51..189dbe6181c 100644
--- a/engines/nancy/action/puzzle/pegspuzzle.cpp
+++ b/engines/nancy/action/puzzle/pegspuzzle.cpp
@@ -139,6 +139,7 @@ void PegsPuzzle::init() {
redraw();
registerGraphics();
+ _carriedObject.registerGraphics();
}
bool PegsPuzzle::validCell(int col, int row) const {
@@ -231,10 +232,22 @@ void PegsPuzzle::doJump(int fromCol, int fromRow, int destCol, int destRow) {
_board[cellIndex(destCol, destRow)] = kPeg;
}
-Common::Point PegsPuzzle::cursorToViewport(const Common::Point &mousePos) const {
- Common::Rect screenPt(mousePos.x, mousePos.y, mousePos.x + 1, mousePos.y + 1);
- Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
- return Common::Point(vpPt.left, vpPt.top);
+// Lifts the peg at the given cell onto the cursor, or sets the carried one down
+// for a column of -1.
+void PegsPuzzle::carryPeg(int col, int row, NancyInput &input) {
+ _carriedCol = col;
+ _carriedRow = row;
+
+ if (_carriedCol >= 0 && _image.getBounds().contains(_pegSrc)) {
+ _carriedObject._drawSurface.create(_image, _pegSrc);
+ _carriedObject.setTransparent(true);
+ _carriedObject.setVisible(true);
+ _carriedObject.pickUp();
+ _carriedObject.handleInput(input);
+ } else {
+ _carriedObject.setVisible(false);
+ _carriedObject.putDown();
+ }
}
void PegsPuzzle::setDataCursor(uint16 cursorType, bool hotspotVariant) const {
@@ -265,13 +278,6 @@ void PegsPuzzle::redraw() {
}
}
- // The dragged piece, following the cursor.
- if (carrying) {
- int w = _pegSrc.right - _pegSrc.left;
- int h = _pegSrc.bottom - _pegSrc.top;
- _drawSurface.blitFrom(_image, _pegSrc, Common::Point(_dragPos.x - w / 2, _dragPos.y - h / 2));
- }
-
_needsRedraw = true;
}
@@ -353,10 +359,7 @@ void PegsPuzzle::handleInput(NancyInput &input) {
// -- Carrying a peg: it follows the cursor; drop it on a reachable hole to jump. --
if (_carriedCol >= 0) {
setDataCursor(_dragCursorType);
-
- // The dragged piece tracks the cursor (viewport space).
- _dragPos = cursorToViewport(input.mousePos);
- redraw();
+ _carriedObject.handleInput(input);
if (click) {
int col, row;
@@ -367,7 +370,7 @@ void PegsPuzzle::handleInput(NancyInput &input) {
// The peg is always set down, whether it jumped or not. Chaining another jump
// is simply picking the landed peg up again.
- _carriedCol = _carriedRow = -1;
+ carryPeg(-1, -1, input);
redraw();
}
@@ -381,11 +384,7 @@ void PegsPuzzle::handleInput(NancyInput &input) {
if (_board[cellIndex(col, row)] == kPeg && cellHasAnyMove(col, row)) {
setDataCursor(_hoverCursorType);
if (click) {
- _carriedCol = col;
- _carriedRow = row;
- // Seed the drag position before the first draw, otherwise the piece would
- // briefly appear at the previous drag's position.
- _dragPos = cursorToViewport(input.mousePos);
+ carryPeg(col, row, input);
playSoundBlock(_sounds[0]);
redraw();
}
diff --git a/engines/nancy/action/puzzle/pegspuzzle.h b/engines/nancy/action/puzzle/pegspuzzle.h
index 243e465c0e1..5d36a981567 100644
--- a/engines/nancy/action/puzzle/pegspuzzle.h
+++ b/engines/nancy/action/puzzle/pegspuzzle.h
@@ -24,6 +24,7 @@
#include "engines/nancy/commontypes.h"
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
namespace Nancy {
namespace Action {
@@ -70,7 +71,7 @@ protected:
int pegCount() const;
void doJump(int fromCol, int fromRow, int destCol, int destRow);
- Common::Point cursorToViewport(const Common::Point &mousePos) const;
+ void carryPeg(int col, int row, NancyInput &input);
// The puzzle's cursors are raw Nancy13 cursor type ids stored in the AR data.
// Zone cursors take the idle sprite of their type, hover/drag cursors the hotspot one.
void setDataCursor(uint16 cursorType, bool hotspotVariant = true) const;
@@ -112,7 +113,7 @@ protected:
Common::Array<Common::Rect> _destRects;
int _carriedCol = -1; // the peg currently picked up (dragged), or -1
int _carriedRow = -1;
- Common::Point _dragPos; // cursor position (viewport space) while dragging
+ Misc::MouseFollowObject _carriedObject; // the carried peg's sprite, riding the cursor
bool _ended = false;
bool _solved = false;
bool _exitRequested = false;
diff --git a/engines/nancy/action/puzzle/sortpuzzle.cpp b/engines/nancy/action/puzzle/sortpuzzle.cpp
index fee922edc11..191fd4d9969 100644
--- a/engines/nancy/action/puzzle/sortpuzzle.cpp
+++ b/engines/nancy/action/puzzle/sortpuzzle.cpp
@@ -322,6 +322,7 @@ void SortPuzzle::execute() {
case kBegin:
init();
registerGraphics();
+ _heldObject.registerGraphics();
_state = kRun;
// fall through
@@ -423,10 +424,7 @@ void SortPuzzle::handleInput(NancyInput &input) {
debug("-----");
}
- if (_hasHeld && _heldDrawPos != mouseVP) {
- _heldDrawPos = mouseVP;
- redraw();
- }
+ _heldObject.handleInput(input);
// Nancy 12 uses the dedicated puzzle hands: a closed one over a piece that can
// be picked up, an open one over a slot the carried piece can go into. Off the
@@ -464,12 +462,7 @@ void SortPuzzle::handleInput(NancyInput &input) {
return;
if (!_hasHeld) {
- _held = _current[row][col];
- _hasHeld = true;
- // Anchor the piece to the mouse right away; the move check above only runs
- // once something is already held, so it would otherwise be drawn for one
- // frame at the spot where the previous piece was dropped
- _heldDrawPos = mouseVP;
+ holdCell(_current[row][col], true, input);
_current[row][col].isEmpty = true;
if (_pickupSound.name != "NO SOUND") {
g_nancy->_sound->loadSound(_pickupSound);
@@ -484,10 +477,10 @@ void SortPuzzle::handleInput(NancyInput &input) {
g_nancy->_sound->playSound(_dropSound);
}
if (target.isEmpty) {
- _hasHeld = false;
+ holdCell(_held, false, input);
checkSolved();
} else {
- _held = target;
+ holdCell(target, true, input);
}
}
@@ -557,6 +550,38 @@ void SortPuzzle::checkSolved() {
_subState = kPlayWinSound;
}
+// Puts a piece on the cursor, or takes the held one off it when hasHeld is false.
+void SortPuzzle::holdCell(const Cell &cell, bool hasHeld, NancyInput &input) {
+ _held = cell;
+ _hasHeld = hasHeld;
+
+ if (!_hasHeld) {
+ _heldObject.setVisible(false);
+ _heldObject.putDown();
+ return;
+ }
+
+ // Older games carry a separate cursor image with one sprite per value;
+ // Nancy 12 has no cursor image and draws the held gem from the board image.
+ if (g_nancy->getGameType() < kGameTypeNancy12 && _held.value >= 0 && _held.value < kNumCursors &&
+ _cursorImage.getBounds().contains(_cursorSrcRects[_held.value])) {
+ _heldObject._drawSurface.create(_cursorImage, _cursorSrcRects[_held.value]);
+ } else {
+ Common::Rect src = g_nancy->getGameType() >= kGameTypeNancy12 ? heldSprite(_held) : cellSprite(_held);
+ if (!_boardImage.getBounds().contains(src)) {
+ _heldObject.setVisible(false);
+ _heldObject.putDown();
+ return;
+ }
+ _heldObject._drawSurface.create(_boardImage, src);
+ }
+
+ _heldObject.setTransparent(true);
+ _heldObject.setVisible(true);
+ _heldObject.pickUp();
+ _heldObject.handleInput(input);
+}
+
void SortPuzzle::redraw() {
_drawSurface.clear(_drawSurface.getTransparentColor());
@@ -573,29 +598,6 @@ void SortPuzzle::redraw() {
}
}
- if (_hasHeld) {
- bool drawn = false;
- // Older games carry a separate cursor image with one sprite per value;
- // Nancy 12 has no cursor image and draws the held gem from the board image.
- if (g_nancy->getGameType() < kGameTypeNancy12 && _held.value >= 0 && _held.value < kNumCursors) {
- const Common::Rect &src = _cursorSrcRects[_held.value];
- 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));
- drawn = true;
- }
- }
- if (!drawn) {
- Common::Rect src = g_nancy->getGameType() >= kGameTypeNancy12 ? heldSprite(_held) : cellSprite(_held);
- if (!src.isEmpty()) {
- int x = _heldDrawPos.x - src.width() / 2;
- int y = _heldDrawPos.y - src.height() / 2;
- _drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
- }
- }
- }
-
_needsRedraw = true;
}
diff --git a/engines/nancy/action/puzzle/sortpuzzle.h b/engines/nancy/action/puzzle/sortpuzzle.h
index db26f5536fa..e5de3100e9f 100644
--- a/engines/nancy/action/puzzle/sortpuzzle.h
+++ b/engines/nancy/action/puzzle/sortpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_SORTPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
#include "engines/nancy/commontypes.h"
namespace Nancy {
@@ -140,7 +141,7 @@ protected:
bool _hasHeld = false;
bool _isSolved = false;
- Common::Point _heldDrawPos;
+ Misc::MouseFollowObject _heldObject;
Graphics::ManagedSurface _boardImage;
Graphics::ManagedSurface _cursorImage;
@@ -148,6 +149,7 @@ protected:
void initState();
void persistState();
void redraw();
+ void holdCell(const Cell &cell, bool hasHeld, NancyInput &input);
void checkSolved();
bool cellsMatch(const Cell &cur, const Cell &sol) const;
Common::Rect cellRect(int row, int col) const;
diff --git a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
index a8adcfacb4e..759fba0ac0d 100644
--- a/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
+++ b/engines/nancy/action/puzzle/stepobjectspuzzle.cpp
@@ -102,6 +102,7 @@ void StepObjectsPuzzle::init() {
redraw();
registerGraphics();
+ _carriedObject.registerGraphics();
}
Common::Rect StepObjectsPuzzle::getCellRect(const StepObject &object, int row, int col) const {
@@ -165,7 +166,7 @@ bool StepObjectsPuzzle::isSolutionMatched() const {
void StepObjectsPuzzle::resetBoard() {
_trail.clear();
_playerSteps.clear();
- _carriedID = -1;
+ putDownCarried();
for (uint i = 0; i < _objects.size(); ++i) {
_objects[i].row = _objects[i].startRow;
@@ -191,7 +192,15 @@ void StepObjectsPuzzle::pickUp(uint objectID) {
_trail.push_back(step);
_carriedID = objectID;
- _carriedRect = getCellRect(object, object.row, object.col);
+
+ // The sprite starts out on the cell it was picked up from, until the cursor moves it
+ _carriedObject._drawSurface.create(object.srcRect.width(), object.srcRect.height(),
+ g_nancy->_graphics->getTransparentPixelFormat());
+ _carriedObject._drawSurface.clear(0);
+ drawSprite(_carriedObject._drawSurface, object.srcRect, Common::Point(), 255);
+ _carriedObject.moveTo(getCellRect(object, object.row, object.col));
+ _carriedObject.setVisible(true);
+ _carriedObject.pickUp();
beginStepSound(kSoundPickUp, false);
}
@@ -230,13 +239,19 @@ void StepObjectsPuzzle::drop(int row, int col) {
}
}
- _carriedID = -1;
+ putDownCarried();
_lastStepCorrect = correct;
beginStepSound(kSoundStep, true);
redraw();
}
-void StepObjectsPuzzle::drawSprite(const Common::Rect &srcRect, const Common::Point &destPos, byte alpha) {
+void StepObjectsPuzzle::putDownCarried() {
+ _carriedID = -1;
+ _carriedObject.setVisible(false);
+ _carriedObject.putDown();
+}
+
+void StepObjectsPuzzle::drawSprite(Graphics::ManagedSurface &dest, const Common::Rect &srcRect, const Common::Point &destPos, byte alpha) {
if (srcRect.isEmpty() || !_image.getBounds().contains(srcRect)) {
return;
}
@@ -247,13 +262,13 @@ void StepObjectsPuzzle::drawSprite(const Common::Rect &srcRect, const Common::Po
for (int y = 0; y < srcRect.height(); ++y) {
int destY = destPos.y + y;
- if (destY < 0 || destY >= _drawSurface.h) {
+ if (destY < 0 || destY >= dest.h) {
continue;
}
for (int x = 0; x < srcRect.width(); ++x) {
int destX = destPos.x + x;
- if (destX < 0 || destX >= _drawSurface.w) {
+ if (destX < 0 || destX >= dest.w) {
continue;
}
@@ -265,7 +280,7 @@ void StepObjectsPuzzle::drawSprite(const Common::Rect &srcRect, const Common::Po
continue;
}
- _drawSurface.setPixel(destX, destY, _drawSurface.format.ARGBToColor(a * alpha / 255, r, g, b));
+ dest.setPixel(destX, destY, dest.format.ARGBToColor(a * alpha / 255, r, g, b));
}
}
}
@@ -283,9 +298,9 @@ void StepObjectsPuzzle::redraw() {
Common::Rect cell = getCellRect(object, step.row, step.col);
if (object.footprintSrcRect.isEmpty()) {
- drawSprite(object.srcRect, Common::Point(cell.left, cell.top), kFootprintAlpha);
+ drawSprite(_drawSurface, object.srcRect, Common::Point(cell.left, cell.top), kFootprintAlpha);
} else {
- drawSprite(object.footprintSrcRect, Common::Point(cell.left, cell.top), 255);
+ drawSprite(_drawSurface, object.footprintSrcRect, Common::Point(cell.left, cell.top), 255);
}
}
@@ -296,11 +311,7 @@ void StepObjectsPuzzle::redraw() {
const StepObject &object = _objects[i];
Common::Rect cell = getCellRect(object, object.row, object.col);
- drawSprite(object.srcRect, Common::Point(cell.left, cell.top), 255);
- }
-
- if (_carriedID >= 0) {
- drawSprite(_objects[_carriedID].srcRect, Common::Point(_carriedRect.left, _carriedRect.top), 255);
+ drawSprite(_drawSurface, object.srcRect, Common::Point(cell.left, cell.top), 255);
}
_needsRedraw = true;
@@ -400,17 +411,7 @@ void StepObjectsPuzzle::handleInput(NancyInput &input) {
if (_carriedID >= 0) {
const StepObject &object = _objects[_carriedID];
setDataCursor(_cursorType);
-
- // The carried sprite is centered on the cursor and kept inside the viewport
- Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
- Common::Rect screenPt(input.mousePos.x, input.mousePos.y, input.mousePos.x + 1, input.mousePos.y + 1);
- Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
- int16 w = object.srcRect.width();
- int16 h = object.srcRect.height();
- int16 left = CLIP<int16>(vpPt.left - w / 2, vpBounds.left, vpBounds.right - w);
- int16 top = CLIP<int16>(vpPt.top - h / 2, vpBounds.top, vpBounds.bottom - h);
- _carriedRect = Common::Rect(left, top, left + w, top + h);
- redraw();
+ _carriedObject.handleInput(input);
if (click) {
int row, col;
diff --git a/engines/nancy/action/puzzle/stepobjectspuzzle.h b/engines/nancy/action/puzzle/stepobjectspuzzle.h
index c1b817f951c..5be6bbf78f9 100644
--- a/engines/nancy/action/puzzle/stepobjectspuzzle.h
+++ b/engines/nancy/action/puzzle/stepobjectspuzzle.h
@@ -24,6 +24,7 @@
#include "engines/nancy/commontypes.h"
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/misc/mousefollow.h"
namespace Nancy {
namespace Action {
@@ -99,7 +100,8 @@ protected:
void setDataCursor(uint16 cursorType, bool hotspotVariant = true) const;
void redraw();
- void drawSprite(const Common::Rect &srcRect, const Common::Point &destPos, byte alpha);
+ void putDownCarried();
+ void drawSprite(Graphics::ManagedSurface &dest, const Common::Rect &srcRect, const Common::Point &destPos, byte alpha);
SoundDescription playSoundBlock(const RandomSoundBlock &block);
// File data
@@ -131,7 +133,7 @@ protected:
Common::Array<Step> _playerSteps;
PuzzleState _puzzleState = kIdle;
int _carriedID = -1;
- Common::Rect _carriedRect;
+ Misc::MouseFollowObject _carriedObject;
uint32 _stepSoundEnd = 0;
bool _lastStepWasDrop = false;
bool _lastStepCorrect = false;
diff --git a/engines/nancy/misc/mousefollow.h b/engines/nancy/misc/mousefollow.h
index f5df29ce61d..0ad0dd9ee13 100644
--- a/engines/nancy/misc/mousefollow.h
+++ b/engines/nancy/misc/mousefollow.h
@@ -35,10 +35,18 @@ namespace Misc {
// sure that its center stays above the mouse hotspot. The position
// of such an object is clipped to the Viewport. Used in puzzles:
// - AssemblyPuzzle
+// - BeadPuzzle
+// - BlocksPuzzle
// - CubePuzzle
+// - DropSortPuzzle
+// - GridMapPuzzle
+// - MindPuzzle
+// - PegsPuzzle
// - RippedLetterPuzzle
-// - TowerPuzzle
+// - SortPuzzle
+// - StepObjectsPuzzle
// - TangramPuzzle
+// - TowerPuzzle
class MouseFollowObject : public RenderObject {
public:
MouseFollowObject();
Commit: 99e5ae3e20c94dfb09f3ebf8db5418832726366e
https://github.com/scummvm/scummvm/commit/99e5ae3e20c94dfb09f3ebf8db5418832726366e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:24+03:00
Commit Message:
NANCY: NANCY13: Implement new functionality in MultiBuildPuzzle
Now, it's possible to play the hamburger puzzle with Frank Hardy
Changed paths:
engines/nancy/action/puzzle/multibuildpuzzle.cpp
engines/nancy/action/puzzle/multibuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index f91fd902c7a..6f4694e00b8 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -33,6 +33,12 @@
namespace Nancy {
namespace Action {
+// Nancy 13 stores several flags outside a SceneChangeWithFlag.
+static void readFlag(Common::SeekableReadStream &stream, FlagDescription &flag) {
+ flag.label = stream.readSint16LE();
+ flag.flag = stream.readByte();
+}
+
void MultiBuildPuzzle::init() {
g_nancy->_resource->loadImage(_primaryImageName, _primaryImage);
_primaryImage.setTransparentColor(_drawSurface.getTransparentColor());
@@ -60,22 +66,31 @@ void MultiBuildPuzzle::init() {
for (uint i = 0; i < _pieces.size(); ++i) {
Piece &p = _pieces[i];
- const Common::Rect &spriteSrc = !p.altSrcRect.isEmpty() ? p.altSrcRect : p.srcRect;
+ if (!p.imageName.empty()) {
+ // Nancy 13: the piece brings its own closeup image.
+ g_nancy->_resource->loadImage(p.imageName, p.image);
+ p.image.setTransparentColor(_drawSurface.getTransparentColor());
+ }
- int w = spriteSrc.width();
- int h = spriteSrc.height();
+ // Fixtures are never dragged, so they get no drag sprites.
+ if (p.kind == kPieceIngredient) {
+ const Common::Rect &spriteSrc = !p.altSrcRect.isEmpty() ? p.altSrcRect : p.srcRect;
- p.rotateSurfaces[0].create(w, h, _primaryImage.format);
- p.rotateSurfaces[0].setTransparentColor(_primaryImage.getTransparentColor());
- p.rotateSurfaces[0].blitFrom(_primaryImage, spriteSrc, Common::Point(0, 0));
- p.hasSurface[0] = true;
+ int w = spriteSrc.width();
+ int h = spriteSrc.height();
- // Rotations 1-3: created if canRotateAll or piece has a valid altSrcRect
- if (_canRotateAll || !p.altSrcRect.isEmpty()) {
- for (int r = 1; r < 4; ++r) {
- rotateSurface90CW(p.rotateSurfaces[r - 1], p.rotateSurfaces[r]);
- p.rotateSurfaces[r].setTransparentColor(_primaryImage.getTransparentColor());
- p.hasSurface[r] = true;
+ p.rotateSurfaces[0].create(w, h, _primaryImage.format);
+ p.rotateSurfaces[0].setTransparentColor(_primaryImage.getTransparentColor());
+ p.rotateSurfaces[0].blitFrom(_primaryImage, spriteSrc, Common::Point(0, 0));
+ p.hasSurface[0] = true;
+
+ // Rotations 1-3: created if canRotateAll or piece has a valid altSrcRect
+ if (_canRotateAll || !p.altSrcRect.isEmpty()) {
+ for (int r = 1; r < 4; ++r) {
+ rotateSurface90CW(p.rotateSurfaces[r - 1], p.rotateSurfaces[r]);
+ p.rotateSurfaces[r].setTransparentColor(_primaryImage.getTransparentColor());
+ p.hasSurface[r] = true;
+ }
}
}
@@ -105,6 +120,7 @@ void MultiBuildPuzzle::registerGraphics() {
void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
const bool isNancy10 = g_nancy->getGameType() >= kGameTypeNancy10;
+ const bool isNancy13 = g_nancy->getGameType() >= kGameTypeNancy13;
readFilename(stream, _primaryImageName);
@@ -144,39 +160,53 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_animLayout[i] = stream.readSint16LE();
}
- // Pieces: data file always has 20 Ã 67-byte slots; only _numPieces are used.
+ // Pieces: the data file always has 20 slots; only _numPieces are used.
// Reserve up-front so counter-spawn push_back doesn't reallocate (pieces are
// RenderObjects already registered with the graphics manager).
- // In Nancy 10, the pieces have been bumped up to 83.
+ // Slot size: 67 bytes in Nancy 9, 83 in Nancy 10, 116 in Nancy 13.
+ const uint pieceSize = isNancy13 ? 116 : (isNancy10 ? 83 : 67);
_pieces.reserve(80);
_pieces.resize(_numPieces);
for (uint i = 0; i < 20; ++i) {
- if (i < _numPieces) {
- Piece &p = _pieces[i];
- if (isNancy10) {
- // Same rect order as Nancy 9, with placedDstRect appended.
- // srcRect is empty when the unplaced piece is baked into the
- // scene overlay (cake mixing) and non-empty when it must be
- // rendered live at rest (plant potting).
- readRect(stream, p.srcRect);
- readRect(stream, p.homeRect);
- readRect(stream, p.altSrcRect);
- readRect(stream, p.cuSrcRect);
- readRect(stream, p.placedDstRect);
- p.counterByte = stream.readByte();
- p.mustPlace = stream.readByte();
- p.mustNotPlace = stream.readByte();
- } else {
- readRect(stream, p.srcRect);
- readRect(stream, p.homeRect);
- readRect(stream, p.altSrcRect);
- readRect(stream, p.cuSrcRect);
- p.counterByte = stream.readByte();
- p.mustPlace = stream.readByte();
- p.mustNotPlace = stream.readByte();
+ if (i >= _numPieces) {
+ stream.skip(pieceSize);
+ continue;
+ }
+
+ Piece &p = _pieces[i];
+ // srcRect is empty when the unplaced piece is baked into the scene
+ // overlay (cake mixing) and non-empty when it must be rendered live at
+ // rest (plant potting).
+ readRect(stream, p.srcRect);
+ readRect(stream, p.homeRect);
+ readRect(stream, p.altSrcRect);
+ readRect(stream, p.cuSrcRect);
+
+ if (isNancy10) {
+ readRect(stream, p.placedDstRect);
+ }
+
+ if (isNancy13) {
+ Common::String pieceImage;
+ readFilename(stream, pieceImage);
+ if (pieceImage != "NO_FILE" && !pieceImage.empty())
+ p.imageName = Common::Path(pieceImage);
+ }
+
+ p.counterByte = stream.readByte();
+ p.mustPlace = stream.readByte();
+
+ if (isNancy13) {
+ // The count byte doubles as a piece kind; fixtures take no
+ // part in the recipe check.
+ if (p.mustPlace >= kPieceInspect) {
+ p.kind = p.mustPlace;
+ p.mustPlace = 0;
}
+ p.compare = stream.readByte();
} else {
- stream.skip(isNancy10 ? 83 : 67);
+ // Older games only distinguish "must be placed" from "must not be".
+ p.compare = stream.readByte() != 0 ? kDoNotPlace : kPlaceExactly;
}
}
@@ -195,6 +225,44 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
assembleTextLine(missedBuf, _missedText, 200);
}
+ char textBuf[200];
+
+ if (isNancy13) {
+ // Flags now precede their scene, the exit cursors sit next to their
+ // hotspots, and the submit scene/sound/caption are new.
+ _dragCursorID = stream.readSint16LE();
+
+ readFlag(stream, _minCountFlag);
+ _solveScene._sceneChange.readData(stream);
+ stream.skip(2); // shouldStopRendering
+
+ _solveSound.readNormal(stream);
+ readFilename(stream, _solveTextKey);
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _solveText, 200);
+
+ readFlag(stream, _solveScene._flag);
+ _submitScene._sceneChange.readData(stream);
+ stream.skip(2);
+
+ _submitSound.readNormal(stream);
+ readFilename(stream, _submitTextKey);
+ stream.read(textBuf, 200);
+ assembleTextLine(textBuf, _submitText, 200);
+
+ readRect(stream, _exitHotspot);
+ _exitCursorID1 = stream.readSint16LE();
+ readFlag(stream, _submitScene._flag);
+
+ _cancelScene._sceneChange.readData(stream);
+ stream.skip(2);
+
+ readRect(stream, _exitHotspot2);
+ _exitCursorID2 = stream.readSint16LE();
+ readFlag(stream, _cancelScene._flag);
+ return;
+ }
+
_dragCursorID = stream.readSint16LE();
_exitCursorID1 = stream.readSint16LE();
_exitCursorID2 = stream.readSint16LE();
@@ -203,7 +271,6 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_solveSound.readNormal(stream);
readFilename(stream, _solveTextKey);
- char textBuf[200];
stream.read(textBuf, 200);
assembleTextLine(textBuf, _solveText, 200);
@@ -211,6 +278,8 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, _exitHotspot);
readRect(stream, _exitHotspot2);
+
+ _minCountFlag = _cancelScene._flag;
}
void MultiBuildPuzzle::execute() {
@@ -223,6 +292,7 @@ void MultiBuildPuzzle::execute() {
g_nancy->_sound->loadSound(_dropSound);
g_nancy->_sound->loadSound(_solveSound);
g_nancy->_sound->loadSound(_missedSound);
+ g_nancy->_sound->loadSound(_submitSound);
_state = kRun;
// fall through
case kRun:
@@ -249,6 +319,21 @@ void MultiBuildPuzzle::execute() {
}
break;
+ case kWaitSubmitSound:
+ if (!g_nancy->_sound->isSoundPlaying(_submitSound)) {
+ g_nancy->_sound->stopSound(_submitSound);
+ _state = kActionTrigger;
+ }
+ break;
+
+ case kResetButtonHeld:
+ // The button shows its pressed sprite briefly, then clears the board.
+ if (g_system->getMillis() >= _timerEnd) {
+ _solveState = kIdle;
+ resetPuzzle();
+ }
+ break;
+
case kAnimStep: {
if (!_hasAnimImage) {
if (g_system->getMillis() >= _timerEnd)
@@ -307,7 +392,11 @@ void MultiBuildPuzzle::execute() {
g_nancy->_sound->stopSound(_dropSound);
g_nancy->_sound->stopSound(_solveSound);
g_nancy->_sound->stopSound(_missedSound);
- if (_isCancelled) {
+ g_nancy->_sound->stopSound(_submitSound);
+ if (_isSubmitted) {
+ NancySceneState.setEventFlag(_submitScene._flag);
+ NancySceneState.changeScene(_submitScene._sceneChange);
+ } else if (_isCancelled) {
NancySceneState.changeScene(_cancelScene._sceneChange);
// Cancel flag is only set if at least one piece was placed (or
// spawned). For sandwich (all counter pieces) the spawn delta is
@@ -332,7 +421,10 @@ void MultiBuildPuzzle::execute() {
}
CursorManager::CursorType MultiBuildPuzzle::cursorFromDataID(int16 id, CursorManager::CursorType fallback) const {
- if (id < 0 || id > 21)
+ // Nancy 13 ids reach past the older games' cursor range.
+ const int16 maxID = g_nancy->getGameType() >= kGameTypeNancy13
+ ? (int16)g_nancy->getStaticData().numCursorTypes - 1 : 21;
+ if (id < 0 || id > maxID)
return fallback;
return (CursorManager::CursorType)id;
}
@@ -383,9 +475,11 @@ void MultiBuildPuzzle::spawnCounterPiece(int srcIdx) {
np.homeRect = src.homeRect;
np.altSrcRect = src.altSrcRect;
np.cuSrcRect = src.cuSrcRect;
+ np.placedDstRect = src.placedDstRect;
np.counterByte = src.counterByte;
np.mustPlace = src.mustPlace;
- np.mustNotPlace = src.mustNotPlace;
+ np.compare = src.compare;
+ np.kind = src.kind;
np.typeIdx = sharedType;
np.curRotation = 0;
np.gameRect = np.homeRect;
@@ -477,7 +571,7 @@ bool MultiBuildPuzzle::isValidDrop() const {
}
void MultiBuildPuzzle::handleInput(NancyInput &input) {
- if (_state != kRun || _solveState != kIdle || _isSolved || _isCancelled)
+ if (_state != kRun || _solveState != kIdle || _isSolved || _isCancelled || _isSubmitted)
return;
const VIEW *viewData = GetEngineData(VIEW);
@@ -489,8 +583,8 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
// reachable while an ingredient closeup is showing, so the player can back
// away at the bottom of the screen to cancel adding it.
if (!_isDragging && !vpScreen.contains(input.mousePos)) {
- if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
- checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
+ if (!checkExitHotspot(_exitHotspot, _exitCursorID1, true, input))
+ checkExitHotspot(_exitHotspot2, _exitCursorID2, false, input);
return;
}
@@ -577,8 +671,12 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
updatePieceRender(placedIdx);
- if (_autoSolveOnDrop || _pieces.size() > 79)
+ if (g_nancy->getGameType() >= kGameTypeNancy13 && _pieces.size() > 79) {
+ // Safety valve: the piece array is full, so hand the order over.
+ submitPuzzle();
+ } else if (_autoSolveOnDrop || _pieces.size() > 79) {
checkIfSolved();
+ }
if (validDrop && _hasAnimImage && _solveState != kAnimStep &&
_solveState != kAnimWaitFrame) {
@@ -602,13 +700,22 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
// drag. Anywhere else the exit hotspots stay live, so the player can back
// away at the bottom of the screen to cancel adding the ingredient.
if (!pp.gameRect.contains(mouseVP)) {
- if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
- checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
+ if (!checkExitHotspot(_exitHotspot, _exitCursorID1, true, input))
+ checkExitHotspot(_exitHotspot2, _exitCursorID2, false, input);
return;
}
g_nancy->_cursor->setCursorType(dragCursor, true);
+ if ((input.input & NancyInput::kLeftMouseButtonUp) && pp.kind != kPieceIngredient) {
+ // Fixtures are only there to be looked at; clicking puts them away.
+ int sel = _selectedPiece;
+ _selectedPiece = -1;
+ pp.gameRect = pp.homeRect;
+ updatePieceRender(sel);
+ return;
+ }
+
if (input.input & NancyInput::kLeftMouseButtonUp) {
int sel = _selectedPiece;
_selectedPiece = -1;
@@ -685,28 +792,14 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
pp.setZOrder((uint16)(_z + (int)_pieces.size() * 2));
pp.registerGraphics();
- if (_hasCloseupImage && !pp.cuSrcRect.isEmpty()) {
- // First click shows the closeup view. When the piece carries a
- // fixed closeup destination (cake mixing), the closeup appears at
- // that absolute, screen-centred position; otherwise it is centred
- // on the piece (plant potting).
- _selectedPiece = topmost;
- const int cuW = pp.cuSrcRect.width();
- const int cuH = pp.cuSrcRect.height();
- int cuLeft;
- int cuTop;
- if (!pp.placedDstRect.isEmpty()) {
- cuLeft = pp.placedDstRect.left;
- cuTop = pp.placedDstRect.top;
- } else {
- const int pieceW = pp.rotateSurfaces[pp.curRotation].w;
- const int pieceH = pp.rotateSurfaces[pp.curRotation].h;
- cuLeft = pp.gameRect.left + pieceW / 2 - cuW / 2;
- cuTop = pp.gameRect.top + pieceH / 2 - cuH / 2;
- cuLeft = CLIP<int>(cuLeft, 0, MAX(0, vpScreen.width() - cuW));
- cuTop = CLIP<int>(cuTop, 0, MAX(0, vpScreen.height() - cuH));
+ if ((_hasCloseupImage || !pp.image.empty()) && !pp.cuSrcRect.isEmpty()) {
+ // First click shows the closeup view.
+ openCloseup(topmost, vpScreen);
+ if (pp.kind == kPieceReset) {
+ // The reset button clears the board on its own.
+ _solveState = kResetButtonHeld;
+ _timerEnd = g_system->getMillis() + 200;
}
- pp.gameRect = Common::Rect(cuLeft, cuTop, cuLeft + cuW, cuTop + cuH);
} else {
// Direct drag on first click.
_isDragging = true;
@@ -721,19 +814,48 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
}
// Exit hotspots: a click in either fires the exit path. Each uses its own data cursor id.
- if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
- checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
+ if (!checkExitHotspot(_exitHotspot, _exitCursorID1, true, input))
+ checkExitHotspot(_exitHotspot2, _exitCursorID2, false, input);
}
-bool MultiBuildPuzzle::checkExitHotspot(const Common::Rect &hot, int16 cursorID, const NancyInput &input) {
+void MultiBuildPuzzle::openCloseup(int pieceIdx, const Common::Rect &viewportScreen) {
+ // A piece with a fixed closeup destination uses it (cake mixing, all of
+ // Nancy 13); otherwise the closeup is centred on the piece (plant potting).
+ Piece &p = _pieces[pieceIdx];
+ _selectedPiece = (int16)pieceIdx;
+
+ const int cuW = p.cuSrcRect.width();
+ const int cuH = p.cuSrcRect.height();
+ int cuLeft;
+ int cuTop;
+ if (!p.placedDstRect.isEmpty()) {
+ cuLeft = p.placedDstRect.left;
+ cuTop = p.placedDstRect.top;
+ } else {
+ const int pieceW = p.rotateSurfaces[p.curRotation].w;
+ const int pieceH = p.rotateSurfaces[p.curRotation].h;
+ cuLeft = p.gameRect.left + pieceW / 2 - cuW / 2;
+ cuTop = p.gameRect.top + pieceH / 2 - cuH / 2;
+ cuLeft = CLIP<int>(cuLeft, 0, MAX(0, viewportScreen.width() - cuW));
+ cuTop = CLIP<int>(cuTop, 0, MAX(0, viewportScreen.height() - cuH));
+ }
+ p.gameRect = Common::Rect(cuLeft, cuTop, cuLeft + cuW, cuTop + cuH);
+}
+
+bool MultiBuildPuzzle::checkExitHotspot(const Common::Rect &hot, int16 cursorID, bool isSubmit, const NancyInput &input) {
if (hot.isEmpty())
return false;
Common::Rect exitScreen = NancySceneState.getViewport().convertViewportToScreen(hot);
if (!exitScreen.contains(input.mousePos))
return false;
- g_nancy->_cursor->setCursorType(cursorFromDataID(cursorID, g_nancy->_cursor->_puzzleExitCursor));
- if (input.input & NancyInput::kLeftMouseButtonUp)
- checkIfSolvedOnExit();
+ g_nancy->_cursor->setCursorType(cursorFromDataID(cursorID, g_nancy->_cursor->_puzzleExitCursor),
+ g_nancy->getGameType() >= kGameTypeNancy13);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ if (isSubmit && g_nancy->getGameType() >= kGameTypeNancy13)
+ submitPuzzle();
+ else
+ checkIfSolvedOnExit();
+ }
return true;
}
@@ -746,6 +868,39 @@ void MultiBuildPuzzle::checkIfSolvedOnExit() {
}
}
+void MultiBuildPuzzle::submitPuzzle() {
+ // The order is handed over as-is; the two flags say what was in it.
+ updateSolveFlags();
+ _isSubmitted = true;
+ g_nancy->_sound->playSound(_submitSound);
+ showSubtitle(resolveSubtitleText(_submitTextKey, _submitText, "CONVO"));
+ _solveState = kWaitSubmitSound;
+}
+
+void MultiBuildPuzzle::resetPuzzle() {
+ // Counter clones live past _numPieces; dropping them deregisters their
+ // render objects.
+ if (_pieces.size() > _numPieces)
+ _pieces.resize(_numPieces);
+
+ _selectedPiece = -1;
+ _pickedUpPiece = -1;
+ _isDragging = false;
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &p = _pieces[i];
+ p.placeCount = 0;
+ p.isPlaced = false;
+ p.curRotation = 0;
+ p.gameRect = p.homeRect;
+ updatePieceRender(i);
+ }
+
+ // The flags are rewritten on the next board change.
+ _minCountFlagLastValue = -1;
+ _solveFlagLastValue = -1;
+}
+
bool MultiBuildPuzzle::updateSolveFlags() {
uint16 total = 0;
for (uint i = 0; i < _numPieces; ++i) {
@@ -759,13 +914,13 @@ bool MultiBuildPuzzle::updateSolveFlags() {
// Below the threshold, Nancy 10 leaves it latched (so backing away from the
// counter and stepping back up keeps BAKE available), while Nancy 11+ clears
// it. Written only on a value change (see member comment).
- if (_cancelScene._flag.label != kFlagNoLabel) {
+ if (_minCountFlag.label != kFlagNoLabel) {
const bool enough = total >= _requiredPieces;
if (enough || g_nancy->getGameType() >= kGameTypeNancy11) {
- byte want = enough ? _cancelScene._flag.flag
- : (_cancelScene._flag.flag == g_nancy->_false ? g_nancy->_true : g_nancy->_false);
+ byte want = enough ? _minCountFlag.flag
+ : (_minCountFlag.flag == g_nancy->_false ? g_nancy->_true : g_nancy->_false);
if ((int)want != _minCountFlagLastValue) {
- NancySceneState.setEventFlag(_cancelScene._flag.label, want);
+ NancySceneState.setEventFlag(_minCountFlag.label, want);
_minCountFlagLastValue = want;
}
}
@@ -778,17 +933,21 @@ bool MultiBuildPuzzle::updateSolveFlags() {
// every ingredient count matches; only then is it raised to its true value.
bool exact = true;
for (uint i = 0; i < _numPieces; ++i) {
- if (_pieces[i].placeCount > 0 && _pieces[i].mustNotPlace > 0) {
+ const Piece &p = _pieces[i];
+ if (p.placeCount > 0 && p.compare == kDoNotPlace) {
exact = false;
break;
}
- // mustPlace is an exact required count only when non-zero. A zero
- // mustPlace means the piece has no count requirement (e.g. cake
- // cooking, where the win rule is just "place enough good ingredients
- // and no bad ones"); placing it must not fail the check.
- if (_pieces[i].mustPlace > 0 && _pieces[i].placeCount != _pieces[i].mustPlace) {
- exact = false;
- break;
+ // A zero mustPlace means no count requirement (cake cooking).
+ if (p.mustPlace > 0) {
+ if (p.compare == kPlaceExactly && p.placeCount != p.mustPlace) {
+ exact = false;
+ break;
+ }
+ if (p.compare == kPlaceAtLeast && p.placeCount < p.mustPlace) {
+ exact = false;
+ break;
+ }
}
}
@@ -826,7 +985,7 @@ void MultiBuildPuzzle::checkIfSolved() {
return;
for (uint i = 0; i < _numPieces; ++i) {
- if (_pieces[i].isPlaced && _pieces[i].mustNotPlace)
+ if (_pieces[i].isPlaced && _pieces[i].compare == kDoNotPlace)
return;
if (!_pieces[i].isPlaced && _pieces[i].mustPlace)
return;
@@ -889,13 +1048,14 @@ void MultiBuildPuzzle::updatePieceRender(int pieceIdx) {
p._drawSurface.setTransparentColor(p.rotateSurfaces[rot].getTransparentColor());
p._drawSurface.blitFrom(p.rotateSurfaces[rot], Common::Point(0, 0));
}
- } else if (isSelected && _hasCloseupImage && !p.cuSrcRect.isEmpty()) {
- // Zoomed closeup.
+ } else if (isSelected && (_hasCloseupImage || !p.image.empty()) && !p.cuSrcRect.isEmpty()) {
+ // Zoomed closeup, drawn from the piece's own image when it has one.
+ const Graphics::ManagedSurface &source = p.image.empty() ? _closeupImage : p.image;
int w = p.cuSrcRect.width();
int h = p.cuSrcRect.height();
- p._drawSurface.create(w, h, _closeupImage.format);
- p._drawSurface.setTransparentColor(_closeupImage.getTransparentColor());
- p._drawSurface.blitFrom(_closeupImage, p.cuSrcRect, Common::Point(0, 0));
+ p._drawSurface.create(w, h, source.format);
+ p._drawSurface.setTransparentColor(source.getTransparentColor());
+ p._drawSurface.blitFrom(source, p.cuSrcRect, Common::Point(0, 0));
} else {
// At rest on the shelf: show srcRect from primary image.
int w = p.srcRect.width();
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index d6bffaafd4e..cf3db0cc82f 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -36,6 +36,7 @@ namespace Action {
// - sandwich making: click and drag ingredients onto a plate. Some ingredients
// are bad and lead to food poisoning (win condition is checked on exit)
// - sand castle building: free placement of sand pieces (no win condition)
+// - Nancy 13 burger counter: assemble an order, then ring it up at a hotspot
class MultiBuildPuzzle : public RenderActionRecord {
public:
MultiBuildPuzzle() : RenderActionRecord(7) {}
@@ -54,6 +55,20 @@ protected:
Common::String getRecordTypeName() const override { return "MultiBuildPuzzle"; }
bool isValidDrop() const;
+ // Nancy 13 stores this in the mustPlace byte; values below kPieceInspect are counts.
+ enum PieceKind {
+ kPieceIngredient = 0, // Normal draggable piece
+ kPieceInspect = 101, // Opens a closeup, dismissed by the next click
+ kPieceReset = 102 // Button that clears the whole board
+ };
+
+ // How a piece's placement count is matched against mustPlace.
+ enum PieceCompare {
+ kPlaceExactly = 0,
+ kDoNotPlace = 1,
+ kPlaceAtLeast = 2 // Nancy 13
+ };
+
// A single puzzle piece. Each piece is its own RenderObject.
// Unplaced: _drawSurface shows srcRect from primary image.
// Placed: _drawSurface shows the rotation sprite (from altSrcRect or srcRect).
@@ -65,9 +80,11 @@ protected:
Common::Rect altSrcRect; // If non-empty: used as source for sprite creation
Common::Rect cuSrcRect; // Source in closeup image
Common::Rect placedDstRect; // Nancy 10: placement destination on screen
+ Common::Path imageName; // Nancy 13: the piece's own closeup image, if any
uint8 counterByte = 0; // Non-zero: respawns on placement; doesn't count toward solve
- uint8 mustPlace = 0; // Exact required placement count, checked only when non-zero (0 = no count requirement). 0/1 in Nancy 9; recipe quantities in cake mixing; 0 for all pieces in cake cooking
- uint8 mustNotPlace = 0; // Non-zero: placing this fails the solution check
+ uint8 mustPlace = 0; // Required placement count; 0 = no count requirement
+ uint8 compare = kPlaceExactly; // How placeCount is matched against mustPlace
+ uint8 kind = kPieceIngredient;
uint8 placeCount = 0; // Runtime: number of times this piece (or any clone of it) has been placed
Common::Rect gameRect; // Current viewport-space rect
@@ -77,6 +94,7 @@ protected:
Graphics::ManagedSurface rotateSurfaces[4];
bool hasSurface[4] = {};
+ Graphics::ManagedSurface image;
bool isViewportRelative() const override { return true; }
};
@@ -129,6 +147,17 @@ protected:
Common::String _solveText; // Raw fallback used if key missing
SceneChangeWithFlag _cancelScene;
+
+ // "Enough pieces placed" flag; its own field in Nancy 13, the cancel scene's before.
+ FlagDescription _minCountFlag;
+
+ // Nancy 13: outcome of ringing the order up. The order is always accepted;
+ // its contents are carried by the two flags updateSolveFlags() writes.
+ SceneChangeWithFlag _submitScene;
+ SoundDescription _submitSound;
+ Common::String _submitTextKey;
+ Common::String _submitText;
+
Common::Rect _exitHotspot;
Common::Rect _exitHotspot2;
Common::Rect _targetZone; // Valid drop area (drawer/plate/...)
@@ -141,6 +170,7 @@ protected:
bool _isDragging = false;
bool _isSolved = false;
bool _isCancelled = false;
+ bool _isSubmitted = false;
// Event-flag write tracking: the original re-writes the solve flags on every
// drop, but re-writing an unchanged flag can re-trigger scene voice lines, so
@@ -149,12 +179,14 @@ protected:
int _solveFlagLastValue = -1; // last value written to the solve-scene flag (-1 = never)
enum SolveState {
- kIdle = 0,
- kWaitTimer = 1,
- kWaitSolveSound = 4,
- kPlaySolveSound = 5,
- kAnimStep = 6, // Blit current frame, advance counters
- kAnimWaitFrame = 7 // ~100 ms hold between frames
+ kIdle = 0,
+ kWaitTimer = 1,
+ kWaitSolveSound = 4,
+ kPlaySolveSound = 5,
+ kAnimStep = 6, // Blit current frame, advance counters
+ kAnimWaitFrame = 7, // ~100 ms hold between frames
+ kWaitSubmitSound = 8, // Nancy 13: order rung up, waiting on the bell
+ kResetButtonHeld = 9 // Nancy 13: reset button shown pressed for 200 ms
};
SolveState _solveState = kIdle;
uint32 _timerEnd = 0;
@@ -184,22 +216,27 @@ protected:
void checkIfSolved();
void checkIfSolvedOnExit();
- // Updates the global cancel/solve event flags from the current placement
- // state. Returns true on exact match (all placeCounts == mustPlace, no
- // mustNotPlace pieces placed).
+ // Nancy 13: ring the order up through the first exit hotspot.
+ void submitPuzzle();
+ // Nancy 13: return every piece to its slot and drop the counter clones.
+ void resetPuzzle();
+ // Updates the min-count/solve event flags. Returns true when every piece
+ // satisfies its own placement rule.
bool updateSolveFlags();
void updatePieceRender(int pieceIdx);
static void rotateSurface90CW(const Graphics::ManagedSurface &src, Graphics::ManagedSurface &dst);
// Clone an existing piece at the end of _pieces (counter-piece respawn).
void spawnCounterPiece(int srcIdx);
+ // Move a piece into its closeup position and select it.
+ void openCloseup(int pieceIdx, const Common::Rect &viewportScreen);
// Drop is valid if it overhangs the top of a moved piece (sand-castle stacking).
bool altZoneSnapValid() const;
- // Map data cursor id (0..21) to CursorManager::CursorType; out-of-range falls back.
+ // Map a data cursor id to CursorManager::CursorType; out-of-range falls back.
CursorManager::CursorType cursorFromDataID(int16 id, CursorManager::CursorType fallback) const;
// Tests one exit hotspot and (if hovered) sets its cursor / handles the click.
// Returns true when the cursor is inside `hot`, so the caller can skip the
- // other hotspot.
- bool checkExitHotspot(const Common::Rect &hot, int16 cursorID, const NancyInput &input);
+ // other hotspot. In Nancy 13 the first hotspot rings the order up.
+ bool checkExitHotspot(const Common::Rect &hot, int16 cursorID, bool isSubmit, const NancyInput &input);
};
} // End of namespace Action
Commit: f3e5ccb02c0f7542b48a93ff39a194f07adcbb48
https://github.com/scummvm/scummvm/commit/f3e5ccb02c0f7542b48a93ff39a194f07adcbb48
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:26+03:00
Commit Message:
NANCY: NANCY13: Implemented the replacement for AR 27
This AR picks what happens from the item the player is holding, but
every accepted item now comes with its own sound and event flags, and
says whether it is returned to the inventory or used up. The scene only
changes once the sound has finished playing.
It's now possible to enter the gems in the machine in the Caboose wagon
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/navigationrecords.cpp
engines/nancy/action/navigationrecords.h
engines/nancy/action/soundrecords.cpp
engines/nancy/action/soundrecords.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 191117924d2..d424f9def08 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -197,7 +197,10 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
else
return new HotMultiframeMultiSceneChange(); // Moved from 13
case 27: // Nancy10
- return new HotMultiframeMultiSceneCursorTypeSceneChange(); // Moved from 24
+ if (g_nancy->getGameType() <= kGameTypeNancy12)
+ return new HotMultiframeMultiSceneCursorTypeSceneChange(); // Moved from 24
+ else
+ return new HotMultiframeInvTypeSceneChange();
case 28: // Nancy10
return new InteractiveVideo(); // Moved from 26
case 29: // Nancy10
diff --git a/engines/nancy/action/navigationrecords.cpp b/engines/nancy/action/navigationrecords.cpp
index 6761c82da4e..ebaa72abe12 100644
--- a/engines/nancy/action/navigationrecords.cpp
+++ b/engines/nancy/action/navigationrecords.cpp
@@ -20,9 +20,11 @@
*/
#include "engines/nancy/nancy.h"
+#include "engines/nancy/sound.h"
#include "engines/nancy/util.h"
#include "engines/nancy/action/navigationrecords.h"
+#include "engines/nancy/action/soundrecords.h"
#include "engines/nancy/state/scene.h"
@@ -290,6 +292,101 @@ void HotMultiframeMultiSceneCursorTypeSceneChange::execute() {
}
}
+void HotMultiframeInvTypeSceneChange::ItemUse::readData(Common::SeekableReadStream &stream) {
+ itemID = stream.readSint16LE();
+ loseItem = stream.readByte();
+ readMultiNameSound(stream, sound, ccText);
+ sceneID = stream.readUint16LE();
+
+ uint16 numFlags = stream.readUint16LE();
+ flags.resize(numFlags);
+ for (FlagDescription &flag : flags) {
+ flag.label = stream.readSint16LE();
+ flag.flag = (byte)stream.readSint16LE();
+ }
+}
+
+void HotMultiframeInvTypeSceneChange::readData(Common::SeekableReadStream &stream) {
+ uint16 numItems = stream.readUint16LE();
+ _itemUses.resize(numItems);
+ for (ItemUse &use : _itemUses) {
+ use.readData(stream);
+ }
+
+ // The fallback block has the same layout; its item id and lose/return byte
+ // are present in the data, but the engine never looks at them
+ _defaultItemUse.readData(stream);
+
+ uint16 numHotspots = stream.readUint16LE();
+ _hotspots.resize(numHotspots);
+ for (HotspotDescription &hotspot : _hotspots) {
+ hotspot.readData(stream);
+ }
+}
+
+void HotMultiframeInvTypeSceneChange::execute() {
+ switch (_state) {
+ case kBegin:
+ _activeUse = nullptr;
+ _state = kRun;
+ // fall through
+ case kRun:
+ _hasHotspot = false;
+ for (const HotspotDescription &hotspot : _hotspots) {
+ if (hotspot.frameID == NancySceneState.getSceneInfo().frameID) {
+ _hasHotspot = true;
+ _hotspot = hotspot.coords;
+ }
+ }
+
+ break;
+ case kActionTrigger:
+ if (!_activeUse) {
+ for (const ItemUse &use : _itemUses) {
+ if (use.itemID == NancySceneState.getHeldItem()) {
+ _activeUse = &use;
+ break;
+ }
+ }
+
+ if (_activeUse) {
+ if (_activeUse->loseItem) {
+ NancySceneState.setHeldItem(-1);
+ } else {
+ // Puts the item back in the inventory, which also empties the cursor
+ NancySceneState.addItemToInventory(_activeUse->itemID);
+ }
+ } else {
+ _activeUse = &_defaultItemUse;
+ }
+
+ g_nancy->_sound->loadSound(_activeUse->sound);
+ g_nancy->_sound->playSound(_activeUse->sound);
+ showSubtitle(_activeUse->ccText);
+
+ for (const FlagDescription &flag : _activeUse->flags) {
+ NancySceneState.setEventFlag(flag);
+ }
+
+ break;
+ }
+
+ // The scene change waits for the sound to finish playing
+ if (!g_nancy->_sound->isSoundPlaying(_activeUse->sound)) {
+ if (_activeUse->sceneID != kNoScene) {
+ SceneChangeDescription sceneChange;
+ sceneChange.sceneID = _activeUse->sceneID;
+ sceneChange.continueSceneSound = kContinueSceneSound;
+ NancySceneState.changeScene(sceneChange);
+ }
+
+ _isDone = true;
+ }
+
+ break;
+ }
+}
+
void MapCall::readData(Common::SeekableReadStream &stream) {
stream.skip(1);
}
diff --git a/engines/nancy/action/navigationrecords.h b/engines/nancy/action/navigationrecords.h
index c6e9cb512d4..c188bad1b7e 100644
--- a/engines/nancy/action/navigationrecords.h
+++ b/engines/nancy/action/navigationrecords.h
@@ -196,6 +196,43 @@ protected:
Common::String getRecordTypeName() const override { return "HotMultiframeMultisceneCursorTypeSceneChange"; }
};
+// Nancy13 replacement for the record above (AR 27 in both). Also picks what happens
+// from the item the player is holding, but every accepted item now comes with its own
+// sound and event flags, and says whether it is returned to the inventory or used up.
+// The scene only changes once the sound has finished playing.
+class HotMultiframeInvTypeSceneChange : public ActionRecord {
+public:
+ // One branch of the record. The same block, minus the item fields, describes
+ // what happens when the held item matches nothing.
+ struct ItemUse {
+ void readData(Common::SeekableReadStream &stream);
+
+ int16 itemID = -1;
+ bool loseItem = false;
+ SoundDescription sound;
+ Common::String ccText;
+ uint16 sceneID = kNoScene;
+ Common::Array<FlagDescription> flags;
+ };
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+ Common::Array<ItemUse> _itemUses;
+ ItemUse _defaultItemUse;
+ Common::Array<HotspotDescription> _hotspots;
+
+ bool canHaveHotspot() const override { return true; }
+
+ Common::String getRecordExtraInfo() const override { return Common::String::format("Default scene %d", _defaultItemUse.sceneID); }
+
+protected:
+ Common::String getRecordTypeName() const override { return "HotMultiframeInvTypeSceneChange"; }
+
+ // The branch the record is currently waiting on; also marks that it has triggered
+ const ItemUse *_activeUse = nullptr;
+};
+
// Simply switches to the Map state. TVD/nancy1 only.
class MapCall : public ActionRecord {
public:
diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index 290f06b22e7..7e28108a140 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -188,25 +188,30 @@ void PlaySound::readData(Common::SeekableReadStream &stream) {
stream.skip(2); // VIDEO_STOP_RENDERING, VIDEO_CONTINUE_RENDERING
}
-void PlaySound::readDataNancy13(Common::SeekableReadStream &stream) {
- // The sound is a set of candidate names, one picked at random (0 = no sound).
+void readMultiNameSound(Common::SeekableReadStream &stream, SoundDescription &sound, Common::String &ccText) {
const uint16 numNames = stream.readUint16LE();
- if (numNames > 0) {
- Common::Array<Common::String> names;
- names.resize(numNames);
- for (uint16 i = 0; i < numNames; ++i) {
- readFilename(stream, names[i]);
- }
+ if (numNames == 0) {
+ return;
+ }
+
+ Common::Array<Common::String> names;
+ names.resize(numNames);
+ for (uint16 i = 0; i < numNames; ++i) {
+ readFilename(stream, names[i]);
+ }
- _sound.channelID = stream.readUint16LE();
- _sound.numLoops = stream.readUint32LE();
- _sound.volume = stream.readUint16LE();
+ sound.channelID = stream.readUint16LE();
+ sound.numLoops = stream.readUint32LE();
+ sound.volume = stream.readUint16LE();
- _sound.name = names[selectRandomSound(names)];
+ sound.name = names[selectRandomSound(names)];
- // Subtitles are keyed by the played sound's name in the CVTX chunks.
- _ccText = resolveSoundSubtitle(_sound.name);
- }
+ // Subtitles are keyed by the played sound's name in the CVTX chunks.
+ ccText = resolveSoundSubtitle(sound.name);
+}
+
+void PlaySound::readDataNancy13(Common::SeekableReadStream &stream) {
+ readMultiNameSound(stream, _sound, _ccText);
// No inline SoundEffectDescription anymore, and the scene change is just a
// scene ID (frame/vertical offset stay 0).
diff --git a/engines/nancy/action/soundrecords.h b/engines/nancy/action/soundrecords.h
index 7b571714efe..51ee7332152 100644
--- a/engines/nancy/action/soundrecords.h
+++ b/engines/nancy/action/soundrecords.h
@@ -27,6 +27,12 @@
namespace Nancy {
namespace Action {
+// Reads a Nancy13+ sound block: a list of candidate names, one of which is picked
+// at random, followed by the shared channel/loop/volume fields. A block with no
+// names carries no sound at all and stops after the count. Also resolves the
+// picked sound's subtitle, which Nancy13+ keys off the sound name.
+void readMultiNameSound(Common::SeekableReadStream &stream, SoundDescription &sound, Common::String &ccText);
+
// Sets the volume for a particular channel.
class SetVolume : public ActionRecord {
public:
Commit: 8f2fc7283c132760cba626ff55576386e3c1e33e
https://github.com/scummvm/scummvm/commit/8f2fc7283c132760cba626ff55576386e3c1e33e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:29+03:00
Commit Message:
NANCY: NANCY13: Implement changes in interactive videos
Handles the interactive video towards the game's finale
Changed paths:
engines/nancy/action/interactivevideo.cpp
engines/nancy/action/interactivevideo.h
engines/nancy/action/secondarymovie.cpp
engines/nancy/action/secondarymovie.h
diff --git a/engines/nancy/action/interactivevideo.cpp b/engines/nancy/action/interactivevideo.cpp
index e9a4c882a7b..a18746d51b6 100644
--- a/engines/nancy/action/interactivevideo.cpp
+++ b/engines/nancy/action/interactivevideo.cpp
@@ -32,6 +32,34 @@
namespace Nancy {
namespace Action {
+void readInteractiveVideoFile(const Common::Path &filename, InteractiveVideoData &data) {
+ Common::SeekableReadStream *ivFile = SearchMan.createReadStreamForMember(filename.append(".iv"));
+ assert(ivFile);
+
+ readFilename(*ivFile, data.videoName);
+
+ uint32 numFrames = ivFile->readUint32LE();
+ data.frames.resize(numFrames);
+ for (uint i = 0; i < numFrames; ++i) {
+ InteractiveFrame &frame = data.frames[i];
+ frame.frameID = ivFile->readUint16LE();
+ uint16 numHotspots = ivFile->readUint16LE();
+ frame.triggerOnNoHotspot = ivFile->readByte();
+ frame.noHSFlagID = ivFile->readSint16LE();
+ frame.noHSCursorID = ivFile->readSint16LE();
+
+ frame.hotspots.resize(numHotspots);
+ for (uint j = 0; j < numHotspots; ++j) {
+ frame.hotspots[j].setID = ivFile->readSint32LE();
+ readRect(*ivFile, frame.hotspots[j].hotspot);
+ frame.hotspots[j].flagID = ivFile->readSint16LE();
+ frame.hotspots[j].cursorID = ivFile->readSint16LE();
+ }
+ }
+
+ delete ivFile;
+}
+
void InteractiveVideo::readData(Common::SeekableReadStream &stream) {
Common::Path ivFilename;
readFilename(stream, ivFilename);
@@ -50,10 +78,7 @@ void InteractiveVideo::readData(Common::SeekableReadStream &stream) {
_cursors[i] = stream.readSint16LE();
}
- Common::SeekableReadStream *ivFile = SearchMan.createReadStreamForMember(ivFilename.append(".iv"));
- assert(ivFile);
-
- readFilename(*ivFile, _videoName);
+ readInteractiveVideoFile(ivFilename, _ivData);
// WORKAROUND: In Nancy 9, the Feeding Frenzy mini-game plays 6 videos (the whales that pop up)
// with a normal arrow cursor. In such cases, the cursor manager reverts to the default arrow
@@ -63,31 +88,10 @@ void InteractiveVideo::readData(Common::SeekableReadStream &stream) {
// all supported games. Fixes bug #16792.
const uint16 sceneId = NancySceneState.getSceneInfo().sceneID;
if (g_nancy->getGameType() == kGameTypeNancy9 && (sceneId == 2992 || sceneId == 2995 || sceneId == 2996)) {
- if (_videoName.toString().contains("WhaleFeed") && _cursors[0] == CursorManager::kNormalArrow) {
+ if (_ivData.videoName.toString().contains("WhaleFeed") && _cursors[0] == CursorManager::kNormalArrow) {
_cursors[0] = CursorManager::kNormal;
}
}
-
- uint32 numFrames = ivFile->readUint32LE();
- _frames.resize(numFrames);
- for (uint i = 0; i < numFrames; ++i) {
- InteractiveFrame &frame = _frames[i];
- frame.frameID = ivFile->readUint16LE();
- uint16 numHotspots = ivFile->readUint16LE();
- frame.triggerOnNoHotspot = ivFile->readByte();
- frame.noHSFlagID = ivFile->readSint16LE();
- frame.noHSCursorID = ivFile->readSint16LE();
-
- frame.hotspots.resize(numHotspots);
- for (uint j = 0; j < numHotspots; ++j) {
- ivFile->skip(4);
- readRect(*ivFile, frame.hotspots[j].hotspot);
- frame.hotspots[j].flagID = ivFile->readSint16LE();
- frame.hotspots[j].cursorID = ivFile->readSint16LE();
- }
- }
-
- delete ivFile;
}
void InteractiveVideo::execute() {
@@ -121,7 +125,7 @@ void InteractiveVideo::handleInput(NancyInput &input) {
return;
}
- for (auto &frame : _frames) {
+ for (auto &frame : _ivData.frames) {
if (frame.frameID == curFrame) {
// Found data for the current video frame
diff --git a/engines/nancy/action/interactivevideo.h b/engines/nancy/action/interactivevideo.h
index 3f55b1c0189..57c4f70cc22 100644
--- a/engines/nancy/action/interactivevideo.h
+++ b/engines/nancy/action/interactivevideo.h
@@ -30,6 +30,30 @@ namespace Action {
class ActionManager;
class PlaySecondaryMovie;
+struct InteractiveHotspot {
+ Common::Rect hotspot;
+ int32 setID = 0;
+ int16 flagID = -1;
+ int16 cursorID = -1;
+};
+
+struct InteractiveFrame {
+ uint16 frameID = 0;
+ bool triggerOnNoHotspot = false;
+ int16 noHSFlagID = -1;
+ int16 noHSCursorID = -1;
+ Common::Array<InteractiveHotspot> hotspots;
+};
+
+// Contents of an .iv file: the movie the hotspots belong to, and the list of
+// movie frames that have hotspots on them.
+struct InteractiveVideoData {
+ Common::Path videoName;
+ Common::Array<InteractiveFrame> frames;
+};
+
+void readInteractiveVideoFile(const Common::Path &filename, InteractiveVideoData &data);
+
class InteractiveVideo : public ActionRecord {
public:
InteractiveVideo() {}
@@ -43,26 +67,10 @@ public:
protected:
Common::String getRecordTypeName() const override { return "InteractiveVideo"; }
- struct InteractiveHotspot {
- Common::Rect hotspot;
- int16 flagID = -1;
- int16 cursorID = -1;
- };
-
- struct InteractiveFrame {
- uint16 frameID = 0;
- bool triggerOnNoHotspot = false;
- int16 noHSFlagID = -1;
- int16 noHSCursorID = -1;
- Common::Array<InteractiveHotspot> hotspots;
- };
-
Common::Array<FlagDescription> _flags;
Common::Array<int16> _cursors;
- // IV file data
- Common::Path _videoName;
- Common::Array<InteractiveFrame> _frames;
+ InteractiveVideoData _ivData;
// Pointer to a movie AR
PlaySecondaryMovie *_movieAR = nullptr;
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 5c1f90ac3a2..3ff6f8e64a3 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -67,7 +67,62 @@ bool PlaySecondaryMovie::survivesSceneChange(bool nextSceneIsNoArt) const {
return isRandom() && g_nancy->getGameType() < kGameTypeNancy13 && !_isDone && !_randomStopRequested;
}
+const PlaySecondaryMovie::InteractiveSet *PlaySecondaryMovie::getInteractiveSet(int32 setID) const {
+ for (const InteractiveSet &set : _interactiveSets) {
+ if (set.setID == setID) {
+ return &set;
+ }
+ }
+
+ return nullptr;
+}
+
+void PlaySecondaryMovie::handleInteractiveInput(NancyInput &input) {
+ if (_state != kRun) {
+ return;
+ }
+
+ int curFrame = _decoder.getCurFrame();
+ if (curFrame < 0) {
+ return;
+ }
+
+ for (const InteractiveFrame &frame : _interactiveVideo.frames) {
+ if (frame.frameID != curFrame) {
+ continue;
+ }
+
+ for (const InteractiveHotspot &hotspot : frame.hotspots) {
+ if (!NancySceneState.getViewport().convertViewportToScreen(hotspot.hotspot).contains(input.mousePos)) {
+ continue;
+ }
+
+ // The set the hotspot belongs to describes what it does; hotspots
+ // carry their own flag and cursor in Nancy10 and earlier instead.
+ const InteractiveSet *set = getInteractiveSet(hotspot.setID);
+ if (!set) {
+ return;
+ }
+
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)set->cursorID, true);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ NancySceneState.setEventFlag(set->flagDesc);
+ }
+
+ return;
+ }
+
+ return;
+ }
+}
+
void PlaySecondaryMovie::handleInput(NancyInput &input) {
+ if (_movieType == kInteractiveMovie) {
+ handleInteractiveInput(input);
+ return;
+ }
+
// The character's box (set as the hotspot while it is on screen) is
// clickable; clicking opens its conversation scene, and hovering drives the
// recognition movie. The talk hover cursor is applied by ActionManager via
@@ -601,22 +656,27 @@ void PlaySecondaryMovie::readDataNancy14(Common::Serializer &ser, Common::Seekab
_sound.name = "NO SOUND";
- // AR 47 ("InteractiveVideo") appends a name, a flag byte, and a list of
- // named {value, flag} entries on top of the AR-44 movie data.
+ // AR 47 ("InteractiveVideo") appends its interactive-video data on top of
+ // the AR-44 movie data.
if (_movieType == kInteractiveMovie) {
- readFilename(ser, _interactiveName);
- byte flag = 0;
- ser.syncAsByte(flag);
- _interactiveFlag = flag != 0;
-
- uint16 numEntries = 0;
- ser.syncAsUint16LE(numEntries);
- _interactiveEntries.resize(numEntries);
- for (uint i = 0; i < numEntries; ++i) {
- readFilename(ser, _interactiveEntries[i].name);
- ser.syncAsUint32LE(_interactiveEntries[i].value);
- ser.syncAsByte(_interactiveEntries[i].flag);
- }
+ readInteractiveData(ser);
+ }
+}
+
+void PlaySecondaryMovie::readInteractiveData(Common::Serializer &ser) {
+ readFilename(ser, _interactiveName);
+
+ ser.skip(1); // Draws the hotspot rects on top of the movie when set
+
+ uint16 numSets = 0;
+ ser.syncAsUint16LE(numSets);
+ _interactiveSets.resize(numSets);
+ for (uint i = 0; i < numSets; ++i) {
+ InteractiveSet &set = _interactiveSets[i];
+ ser.syncAsSint16LE(set.setID);
+ ser.syncAsSint16LE(set.flagDesc.label);
+ ser.syncAsByte(set.flagDesc.flag);
+ ser.syncAsSint16LE(set.cursorID);
}
}
@@ -640,9 +700,17 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
return;
}
- // Nancy13's AR 41 shares this class but carries a more compact chunk.
- if (g_nancy->getGameType() == kGameTypeNancy13 && _movieType == kSecondaryMovieTerse) {
+ // Nancy13's AR 41 shares this class but carries a more compact chunk, and
+ // AR 47 stacks its interactive-video data on top of that same layout.
+ if (g_nancy->getGameType() == kGameTypeNancy13 &&
+ (_movieType == kSecondaryMovieTerse || _movieType == kInteractiveMovie)) {
readDataNancy13(ser, stream);
+
+ if (_movieType == kInteractiveMovie) {
+ readInteractiveData(ser);
+ readInteractiveVideoFile(_interactiveName, _interactiveVideo);
+ }
+
return;
}
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index 1500824c2a8..a2add21fe09 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -25,6 +25,7 @@
#include "common/ptr.h"
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/action/interactivevideo.h"
#include "engines/nancy/movieplayer.h"
namespace Nancy {
@@ -134,17 +135,19 @@ public:
// (AR 150) can change it later.
byte _movieVolume = 100;
- // AR 47 "InteractiveVideo" (a PlaySecondaryMovie subclass): after the
- // normal AR-44-style movie data it carries a name, a flag byte, and a
- // list of named {value, flag} entries. Read but not yet acted on.
- struct InteractiveEntry {
- Common::Path name;
- uint32 value = 0;
- byte flag = 0;
+ // AR 47 "InteractiveVideo" (a PlaySecondaryMovie subclass): the movie's
+ // clickable areas live in an external .iv file, which lists them per movie
+ // frame and tags each one with a set ID. The record itself carries the name
+ // of that file and the table below, which turns a set ID into the event flag
+ // a click sets and the cursor shown while the mouse is over the area.
+ struct InteractiveSet {
+ int16 setID = 0;
+ FlagDescription flagDesc;
+ int16 cursorID = -1;
};
Common::Path _interactiveName;
- bool _interactiveFlag = false;
- Common::Array<InteractiveEntry> _interactiveEntries;
+ Common::Array<InteractiveSet> _interactiveSets;
+ InteractiveVideoData _interactiveVideo;
Common::Array<FlagAtFrame> _frameFlags;
MultiEventFlagDescription _triggerFlags;
FlagDescription _videoStartFlag;
@@ -255,6 +258,16 @@ protected:
void readDataNancy14(Common::Serializer &ser, Common::SeekableReadStream &stream);
+ // AR 47 appends the name of its .iv file and the set table to the movie data.
+ void readInteractiveData(Common::Serializer &ser);
+
+ // The set a hotspot belongs to, or nullptr if the record doesn't describe it.
+ const InteractiveSet *getInteractiveSet(int32 setID) const;
+
+ // Picks the hover cursor from the hotspots the .iv file lists for the frame
+ // currently on screen, and sets their event flag when one is clicked.
+ void handleInteractiveInput(NancyInput &input);
+
// Apply a RandomSequence's playback config to the PSM flat fields
// and reload the decoder. Returns true on success.
bool activateRandomSequence(int index);
Commit: 6d93fd8c74cadcc81540a99f185929cc71ee8c18
https://github.com/scummvm/scummvm/commit/6d93fd8c74cadcc81540a99f185929cc71ee8c18
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:30+03:00
Commit Message:
NANCY: NANCY15: Implement the new AR 52 - OverlayMultiframeTerse
The new overlay types in the initial sequence are now handled
correctly, and the game can now start
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/overlay.cpp
engines/nancy/action/overlay.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index d424f9def08..9cbd3085116 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -235,8 +235,12 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 50:
return new ConversationVideo();
case 51:
- case 52:
return new PlaySecondaryVideo();
+ case 52:
+ if (g_nancy->getGameType() <= kGameTypeNancy14)
+ return new PlaySecondaryVideo();
+ else
+ return new OverlayMultiframeTerse();
case 53:
if (g_nancy->getGameType() <= kGameTypeNancy13)
return new PlaySecondaryMovie(PlaySecondaryMovie::kSecondaryMovie);
diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 5a6ba185153..ce9d836cbbc 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -446,6 +446,24 @@ void OverlayStaticTerse::readData(Common::SeekableReadStream &stream) {
_overlayType = kPlayOverlayStatic;
}
+void OverlayMultiframeTerse::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName);
+ _z = stream.readUint16LE();
+
+ uint16 numBlitDescriptions = stream.readUint16LE();
+ _blitDescriptions.resize(numBlitDescriptions);
+ for (auto &bm : _blitDescriptions) {
+ bm.readData(stream);
+ }
+
+ // Every blit description carries its own source rect, so the single general
+ // source rect they all point to is left empty; execute() then takes both the
+ // position and the size from the description itself.
+ _srcRects.push_back(Common::Rect());
+
+ _overlayType = kPlayOverlayStatic;
+}
+
void OverlayAnimTerse::readData(Common::SeekableReadStream &stream) {
readFilename(stream, _imageName);
stream.skip(2); // VIDEO_STOP_RENDERING, VIDEO_CONTINUE_RENDERING
diff --git a/engines/nancy/action/overlay.h b/engines/nancy/action/overlay.h
index 449c63fc6b9..13590e9ac85 100644
--- a/engines/nancy/action/overlay.h
+++ b/engines/nancy/action/overlay.h
@@ -88,7 +88,9 @@ public:
bool canHaveHotspot() const override { return true; }
bool isViewportRelative() const override { return true; }
bool survivesSceneChange(bool nextSceneIsNoArt) const override { return nextSceneIsNoArt; }
- Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Scene %d, file %s", _sceneChange.sceneID, _imageName.baseName().c_str());
+ }
protected:
Common::String getRecordTypeName() const override;
@@ -108,6 +110,20 @@ protected:
Common::String getRecordTypeName() const override { return "OverlayStaticTerse"; }
};
+// Short version of a static overlay for a moving scene background. Unlike
+// OverlayStaticTerse, which carries a single source/destination pair, this one
+// carries a blit description for every background frame the overlay appears on.
+class OverlayMultiframeTerse : public Overlay {
+public:
+ OverlayMultiframeTerse() : Overlay(kInterruptibleAnimation) {}
+ virtual ~OverlayMultiframeTerse() {}
+
+ void readData(Common::SeekableReadStream &stream) override;
+
+protected:
+ Common::String getRecordTypeName() const override { return "OverlayMultiframeTerse"; }
+};
+
// Short version of an animated overlay; assumes scene background doesn't move
class OverlayAnimTerse : public Overlay {
public:
@@ -175,7 +191,9 @@ public:
bool canHaveHotspot() const override { return true; }
CursorManager::CursorType getHoverCursor() const override { return (CursorManager::CursorType)_hoverCursor; }
bool cursorSetFromScript() const override { return true; }
- Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Scene %d, file %s", _sceneChange.sceneID, _imageName.baseName().c_str());
+ }
protected:
Common::String getRecordTypeName() const override { return "RolloverOverlay"; }
Commit: 8b2c9a7bba26de7b0e83f20afd742bd1b278969e
https://github.com/scummvm/scummvm/commit/8b2c9a7bba26de7b0e83f20afd742bd1b278969e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:33+03:00
Commit Message:
NANCY: NANCY15: Implement differences in the LOAD chunk
Fixes showing text in the original save/load menus
Changed paths:
engines/nancy/enginedata.cpp
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index d8175c40c1c..427739acade 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -586,7 +586,8 @@ LOAD::LOAD(Common::SeekableReadStream *chunkStream) :
readRectArray(*chunkStream, _textboxBounds, 9);
readRect(*chunkStream, _inputTextboxBounds);
- chunkStream->skip(25); // prefixes and suffixes for filenames
+ // Prefixes and suffixes for filenames. Nancy15 widened the last one from 5 to 32 bytes
+ chunkStream->skip(s.getVersion() <= kGameTypeNancy14 ? 25 : 52);
_mainFontID = chunkStream->readSint16LE();
_highlightFontID = chunkStream->readSint16LE();
Commit: 4f1da2fa23c0aa884d0697375af04a541d4e6a24
https://github.com/scummvm/scummvm/commit/4f1da2fa23c0aa884d0697375af04a541d4e6a24
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-04T01:54:34+03:00
Commit Message:
NANCY: NANCY15: Added handling for new character glyphs in fonts
Changed paths:
engines/nancy/font.cpp
diff --git a/engines/nancy/font.cpp b/engines/nancy/font.cpp
index 193cadeb343..6617f3c946f 100644
--- a/engines/nancy/font.cpp
+++ b/engines/nancy/font.cpp
@@ -141,6 +141,14 @@ void Font::read(Common::SeekableReadStream &stream) {
numCharacters = 115;
}
+
+ if (g_nancy->getGameType() >= kGameTypeNancy15) {
+ // Nancy15 added two more characters. Their glyphs are only present in a
+ // few of the fonts, and it is not yet known which characters they are
+ stream.skip(4);
+
+ numCharacters = 117;
+ }
}
_characterRects.resize(numCharacters);
More information about the Scummvm-git-logs
mailing list