[Scummvm-git-logs] scummvm master -> b5ad0b764324a679aefdeca1c32b5816f3db34df
bluegr
noreply at scummvm.org
Sun Mar 22 10:42:56 UTC 2026
This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
6eeae24d16 NANCY: Implement soundmatchpuzzle (whale sound matching) for Nancy 9
b5ad0b7643 DEVTOOLS: Add Nancy 9 patch for missing sound to create_nancy
Commit: 6eeae24d1654de984ffcc3b909565967b45040af
https://github.com/scummvm/scummvm/commit/6eeae24d1654de984ffcc3b909565967b45040af
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-22T12:42:06+02:00
Commit Message:
NANCY: Implement soundmatchpuzzle (whale sound matching) for Nancy 9
This is the whale sound-matching puzzle used in Nancy 9 (Danger on Deception
Island). The player hears a whale call by clicking one of 5 numbered buttons,
then clicks the matching whale image. Correct pairs stay lit. Player wins when
all the required pairs have been matched.
Changed paths:
engines/nancy/action/puzzle/soundmatchpuzzle.cpp
engines/nancy/action/puzzle/soundmatchpuzzle.h
diff --git a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
index b7250489386..46cdac167fb 100644
--- a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
+++ b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
@@ -32,34 +32,259 @@
namespace Nancy {
namespace Action {
+void SoundMatchPuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageNameLitButtons); // 0x000: overlay image (lit buttons)
+
+ // 0x021: feedback sounds (loaded at init, played on whale button clicks)
+ _feedbackSoundWrong.readNormal(stream); // 0x021: played on incorrect whale click
+ _feedbackSoundRight.readNormal(stream); // 0x052: played on correct whale click
+
+ stream.skip(1); // 0x083
+
+ _winScene.readData(stream); // 0x084
+ _winSound.readNormal(stream); // 0x09d
+ _exitScene.readData(stream); // 0x0ce
+
+ readRect(stream, _exitHotspot); // 0x0e7
+
+ _requiredPairs = stream.readUint16LE(); // 0x0f7
+
+ // 0x119: per-button entries (kNumButtons x 0x15c bytes each)
+ // NOTE: The original tangled the sound button and whale button
+ // data together, but we read them into separate structures for clarity.
+ for (int i = 0; i < kNumButtons; ++i) {
+ SoundButtonEntry &soundButton = _soundButtons[i];
+ WhaleButtonEntry &whaleButton = _whaleButtons[i];
+
+ readRect(stream, whaleButton.whaleSrcRect); // 0x13c
+ readRect(stream, whaleButton.whaleDestRect); // 0x14c
+
+ soundButton.sound.readNormal(stream); // 0x000
+
+ stream.skip(33); // 0x031: empty name field
+
+ soundButton.text = stream.readString('\0', 200); // 0x052: whale sound subtitle
+
+ whaleButton.correctSound = stream.readUint16LE(); // 0x11a
+
+ readRect(stream, soundButton.numSrcRect); // 0x11c
+ readRect(stream, soundButton.numDestRect); // 0x12c
+ }
+}
+
void SoundMatchPuzzle::init() {
- // TODO
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageNameLitButtons, _imageLitButtons);
+ _imageLitButtons.setTransparentColor(_drawSurface.getTransparentColor());
+
+ _selectedSoundButton = -1;
+
+ for (int i = 0; i < kNumButtons; ++i) {
+ _soundButtons[i].matched = false;
+ _whaleButtons[i].matched = false;
+ }
+
+ _matchedPairs = 0;
+ _isExiting = false;
+ _solveSubState = kIdle;
+
+ redraw();
}
void SoundMatchPuzzle::execute() {
- if (_state == kBegin) {
+ switch (_state) {
+ case kBegin:
init();
registerGraphics();
+
+ // Load feedback sounds so they are ready for playback in handleInput
+ if (_feedbackSoundWrong.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_feedbackSoundWrong);
+ if (_feedbackSoundRight.name != "NO SOUND")
+ g_nancy->_sound->loadSound(_feedbackSoundRight);
+
_state = kRun;
- }
+ // fall through
- // TODO
- // Stub - move to the winning screen
- warning("STUB - Nancy 9 Whale sounds puzzle");
- NancySceneState.setEventFlag(436, g_nancy->_true); // EV_Solved_Whale_Call
- SceneChangeDescription scene;
- scene.sceneID = 2936;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
-}
+ case kRun:
+ switch (_solveSubState) {
+ case kIdle:
+ break;
-void SoundMatchPuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+ case kSoundPlaying:
+ // Per-button (whale call) sound is playing. If it stops naturally
+ // before the player picks a whale, deselect and return to idle.
+ if (!g_nancy->_sound->isSoundPlaying(_soundButtons[_selectedSoundButton].sound)) {
+ g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+ NancySceneState.getTextbox().clear();
+ _selectedSoundButton = -1;
+ redraw();
+ _solveSubState = kIdle;
+ }
+ break;
+
+ case kCheckMatch:
+ // A whale button was clicked.
+ // Correct: wait for _feedbackSoundRight to stop, then check win.
+ // Wrong: wait for _feedbackSoundWrong to stop, then back to idle.
+ if (_soundButtons[_selectedSoundButton].matched) {
+ if (_feedbackSoundRight.name == "NO SOUND" ||
+ !g_nancy->_sound->isSoundPlaying(_feedbackSoundRight)) {
+ if (_matchedPairs >= _requiredPairs) {
+ if (_winSound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_winSound);
+ g_nancy->_sound->playSound(_winSound);
+ }
+ _solveSubState = kWinSound;
+ } else {
+ _selectedSoundButton = -1;
+ _solveSubState = kIdle;
+ }
+ }
+ } else {
+ if (_feedbackSoundWrong.name == "NO SOUND" ||
+ !g_nancy->_sound->isSoundPlaying(_feedbackSoundWrong)) {
+ _selectedSoundButton = -1;
+ _solveSubState = kIdle;
+ }
+ }
+ redraw();
+ break;
+
+ case kWinSound:
+ if (_winSound.name == "NO SOUND" ||
+ !g_nancy->_sound->isSoundPlaying(_winSound)) {
+ g_nancy->_sound->stopSound(_winSound);
+ _state = kActionTrigger;
+ }
+ break;
+ }
+ break;
+
+ case kActionTrigger:
+ g_nancy->_sound->stopSound(_feedbackSoundWrong);
+ g_nancy->_sound->stopSound(_feedbackSoundRight);
+ if (_selectedSoundButton >= 0)
+ g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+ g_nancy->_sound->stopSound(_winSound);
+ if (_isExiting)
+ _exitScene.execute();
+ else
+ _winScene.execute();
+ finishExecution();
+ break;
+ }
}
void SoundMatchPuzzle::handleInput(NancyInput &input) {
- // TODO
+ if (_state != kRun || _matchedPairs >= _requiredPairs)
+ return;
+
+ Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+ Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+ // Numbered button clicks â accepted in idle or while a sound is playing
+ if (_solveSubState == kIdle || _solveSubState == kSoundPlaying) {
+ for (int i = 0; i < kNumButtons; ++i) {
+ if (_soundButtons[i].matched)
+ continue; // already matched; numbered button is inactive
+ if (!_soundButtons[i].numDestRect.contains(mouseVP))
+ continue;
+
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ // Stop any currently playing per-button sound
+ if (_selectedSoundButton >= 0)
+ g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+
+ _selectedSoundButton = i;
+
+ if (_soundButtons[i].sound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_soundButtons[i].sound);
+ g_nancy->_sound->playSound(_soundButtons[i].sound);
+ }
+
+ NancySceneState.getTextbox().clear();
+ if (!_soundButtons[i].text.empty())
+ NancySceneState.getTextbox().addTextLine(_soundButtons[i].text);
+
+ redraw();
+ _solveSubState = kSoundPlaying;
+ }
+ return;
+ }
+ }
+
+ // Whale button clicks â only while a numbered button is selected and its sound plays
+ if (_solveSubState == kSoundPlaying) {
+ for (uint16 whaleButton = 0; whaleButton < kNumButtons; ++whaleButton) {
+ if (!_whaleButtons[whaleButton].whaleDestRect.contains(mouseVP))
+ continue;
+
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ // Stop the per-button sound now that the player has made a choice
+ g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+ NancySceneState.getTextbox().clear();
+
+ uint16 soundButton = _soundButtonIndex[_selectedSoundButton] + 1;
+
+ if (soundButton == _whaleButtons[whaleButton].correctSound && !_whaleButtons[whaleButton].matched) {
+ // Correct whale - match!
+ _soundButtons[_selectedSoundButton].matched = true;
+ _whaleButtons[whaleButton].matched = true;
+ ++_matchedPairs;
+ if (_feedbackSoundRight.name != "NO SOUND")
+ g_nancy->_sound->playSound(_feedbackSoundRight);
+ } else {
+ // Wrong whale
+ if (_feedbackSoundWrong.name != "NO SOUND")
+ g_nancy->_sound->playSound(_feedbackSoundWrong);
+ }
+
+ redraw();
+ _solveSubState = kCheckMatch;
+ }
+ return;
+ }
+ }
+
+ if (_exitHotspot.contains(mouseVP)) {
+ g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _isExiting = true;
+ _state = kActionTrigger;
+ }
+ }
+}
+
+void SoundMatchPuzzle::redraw() {
+ _drawSurface.clear(_drawSurface.getTransparentColor());
+
+ for (int i = 0; i < kNumButtons; ++i) {
+ if (_soundButtons[i].matched || i == _selectedSoundButton) {
+ const Common::Rect &dest = _soundButtons[i].numDestRect;
+ _drawSurface.blitFrom(_imageLitButtons, _soundButtons[i].numSrcRect,
+ Common::Point(dest.left, dest.top));
+ }
+
+ if (_whaleButtons[i].matched) {
+ const Common::Rect &dest = _whaleButtons[i].whaleDestRect;
+ _drawSurface.blitFrom(_imageLitButtons, _whaleButtons[i].whaleSrcRect,
+ Common::Point(dest.left, dest.top));
+ }
+ }
+
+ _needsRedraw = true;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/soundmatchpuzzle.h b/engines/nancy/action/puzzle/soundmatchpuzzle.h
index 435d38c6806..8a0cca6aa9b 100644
--- a/engines/nancy/action/puzzle/soundmatchpuzzle.h
+++ b/engines/nancy/action/puzzle/soundmatchpuzzle.h
@@ -23,12 +23,15 @@
#define NANCY_ACTION_SOUNDMATCHPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
namespace Nancy {
namespace Action {
-// Whale sound matching puzzle in Nancy 9
-
+// Whale sound-matching puzzle used in Nancy 9 (Danger on Deception Island).
+// The player hears a whale call by clicking one of 5 numbered buttons, then
+// clicks the matching whale image. Correct pairs stay lit. Player wins when
+// all the required pairs have been matched.
class SoundMatchPuzzle : public RenderActionRecord {
public:
SoundMatchPuzzle() : RenderActionRecord(7) {}
@@ -43,6 +46,72 @@ public:
protected:
Common::String getRecordTypeName() const override { return "SoundMatchPuzzle"; }
bool isViewportRelative() const override { return true; }
+
+ // File data
+
+ Common::Path _imageNameLitButtons;
+
+ static const int kNumButtons = 5;
+
+ SoundDescription _feedbackSoundWrong; // played on incorrect whale click
+ SoundDescription _feedbackSoundRight; // played on correct whale click
+ SceneChangeWithFlag _winScene;
+ SoundDescription _winSound; // played when all pairs are matched
+ SceneChangeWithFlag _exitScene;
+
+ Common::Rect _exitHotspot;
+
+ uint16 _requiredPairs = kNumButtons; // how many matches needed to win
+
+ struct SoundButtonEntry {
+ SoundDescription sound; // whale call sound
+ Common::String text; // onomatopoeia shown in the text box
+ Common::Rect numSrcRect; // overlay src for numbered button lit state
+ Common::Rect numDestRect; // screen dest for numbered button (hotspot)
+ bool matched = false; // whether the sound button has been correctly paired
+ };
+
+ SoundButtonEntry _soundButtons[kNumButtons];
+
+ struct WhaleButtonEntry {
+ Common::Rect whaleSrcRect; // overlay src for whale button lit state
+ Common::Rect whaleDestRect; // screen dest for whale button (hotspot)
+ uint16 correctSound = 0; // index (0..4) of the correct sound button
+ bool matched = false; // whether the whale button has been correctly paired
+ };
+
+ WhaleButtonEntry _whaleButtons[kNumButtons];
+
+ // Runtime state
+
+ Graphics::ManagedSurface _imageLitButtons;
+
+ int _selectedSoundButton = -1; // currently selected numbered button, -1 = none
+ int _matchedPairs = 0;
+ bool _isExiting = false;
+
+ enum SolveSubState {
+ kIdle = 0,
+ kCheckMatch = 1,
+ kSoundPlaying = 2,
+ kWinSound = 4
+ };
+ SolveSubState _solveSubState = kIdle;
+
+ // NOTE: In the original, the sound button and whale button
+ // data were intertwined in the puzzle data, and the sound
+ // button entries were not stored in order of their actual
+ // button index. This array maps the sound button entries to
+ // their actual button index (0..4) for easier handling at
+ // runtime.
+ // This means that this implementation is only valid for
+ // Nancy 9's specific puzzle data, but that's not a problem
+ // since this puzzle is only used in that game.
+ uint16 _soundButtonIndex[kNumButtons] = { 3, 2, 4, 0, 1 };
+
+ // Internal methods
+
+ void redraw();
};
} // End of namespace Action
Commit: b5ad0b764324a679aefdeca1c32b5816f3db34df
https://github.com/scummvm/scummvm/commit/b5ad0b764324a679aefdeca1c32b5816f3db34df
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-22T12:42:09+02:00
Commit Message:
DEVTOOLS: Add Nancy 9 patch for missing sound to create_nancy
Taken from https://www.herinteractive.com/2014/04/patch-danger-on-deception-island-clams-missing-and-csound-error/
Changed paths:
A devtools/create_nancy/files/nancy9/NCP06na.HIS
devtools/create_nancy/create_nancy.cpp
devtools/create_nancy/nancy9_data.h
dists/engine-data/nancy.dat
diff --git a/devtools/create_nancy/create_nancy.cpp b/devtools/create_nancy/create_nancy.cpp
index 2eedccd90e6..b6fad3e20b1 100644
--- a/devtools/create_nancy/create_nancy.cpp
+++ b/devtools/create_nancy/create_nancy.cpp
@@ -323,6 +323,8 @@ int main(int argc, char *argv[]) {
WRAPWITHOFFSET(writeGoodbyes(output, _nancy9Goodbyes))
WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging)) // same as 8
WRAPWITHOFFSET(writeEventFlagNames(output, _nancy9EventFlagNames))
+ WRAPWITHOFFSET(writePatchFile(output, 10, nancy9PatchSrcFiles, "files/nancy9"))
+ WRAPWITHOFFSET(writePatchAssociations(output, nancy9PatchAssociations))
// Nancy Drew: The Secret of Shadow Ranch
gameOffsets.push_back(output.pos());
diff --git a/devtools/create_nancy/files/nancy9/NCP06na.HIS b/devtools/create_nancy/files/nancy9/NCP06na.HIS
new file mode 100644
index 00000000000..8857481baaa
Binary files /dev/null and b/devtools/create_nancy/files/nancy9/NCP06na.HIS differ
diff --git a/devtools/create_nancy/nancy9_data.h b/devtools/create_nancy/nancy9_data.h
index 05123d8b7a4..37ca18d4071 100644
--- a/devtools/create_nancy/nancy9_data.h
+++ b/devtools/create_nancy/nancy9_data.h
@@ -975,4 +975,14 @@ const Common::Array<const char *> _nancy9EventFlagNames = {
"EV_Empty114",
};
+const Common::Array<const char *> nancy9PatchSrcFiles {
+ "NCP06na.HIS"
+};
+
+// Patch notes:
+// - The missing sound file is a patch from the original devs. Should only be enabled in the English version
+const Common::Array<PatchAssociation> nancy9PatchAssociations {
+ { { "language", "en" }, { "NCP06na.HIS" } }
+};
+
#endif // NANCY9DATA_H
diff --git a/dists/engine-data/nancy.dat b/dists/engine-data/nancy.dat
index 46c610fb54b..18fea5790e7 100644
Binary files a/dists/engine-data/nancy.dat and b/dists/engine-data/nancy.dat differ
More information about the Scummvm-git-logs
mailing list