[Scummvm-git-logs] scummvm master -> 3b875efc0e7b3d06a3846f8a73fd07cd895262f5
bluegr
noreply at scummvm.org
Thu Jul 2 06:23:23 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:
296f7391e7 NANCY: NANCY12: Add enums for OneBuildPuzzle
1d80f75fe7 NANCY: NANCY13: Add handling of new boot chunk data
a047513400 NANCY: NANCY10: Simplify the FrameTextBox AR
827cdcee96 NANCY: NANCY13: Map Action Records and add relevant TODOs
eb11becdbf NANCY: NANCY6: Fix subtitle not shown in HamRadioPuzzle
5d9736aacb NANCY: NANCY6: Fix answer checking in PasswordPuzzle
8909a5ddd9 NANCY: Refactor movie playing code to be inside a common class
c3fd298f75 NANCY: NANCY12: Implement BoardGamePuzzle
3b875efc0e NANCY: NANCY12: Initial work on MirrorLightPuzzle and ActionZone
Commit: 296f7391e7f4abb73bec6a451091d8415e289b5c
https://github.com/scummvm/scummvm/commit/296f7391e7f4abb73bec6a451091d8415e289b5c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:02+03:00
Commit Message:
NANCY: NANCY12: Add enums for OneBuildPuzzle
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 17a795ab9f4..618c3fe777a 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -153,14 +153,14 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
_solveScene.readData(stream); // 0x1cf
_cancelScene.readData(stream); // 0x1e8 (ends the 513-byte blob)
- // --- Six random-sound blocks: pickup, rotate, drop, good, bad, completion ---
- RandomSoundBlock blocks[6];
- for (uint i = 0; i < 6; ++i)
+ // --- Random-sound blocks: pickup, rotate, drop, good, bad, completion ---
+ RandomSoundBlock blocks[kNumSounds];
+ for (uint i = 0; i < kNumSounds; ++i)
blocks[i].readData(stream);
- SoundDescription *sounds[6] = { &_pickupSound, &_rotateSound, &_dropSound,
- &_goodPlacementSound, &_badPlacementSound, &_completionSound };
- for (uint i = 0; i < 6; ++i) {
+ SoundDescription *sounds[kNumSounds] = { &_pickupSound, &_rotateSound, &_dropSound,
+ &_goodPlacementSound, &_badPlacementSound, &_completionSound };
+ for (uint i = 0; i < kNumSounds; ++i) {
SoundDescription &s = *sounds[i];
s.name = blocks[i].names.empty() ? "NO SOUND" : blocks[i].names[0];
s.channelID = blocks[i].channel;
@@ -174,7 +174,7 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
Common::String *goodAlts[2] = { &_goodAlt1Filename, &_goodAlt2Filename };
Common::String *badAlts[2] = { &_badAlt1Filename, &_badAlt2Filename };
Common::String **altSets[3] = { dropAlts, goodAlts, badAlts };
- const uint altBlocks[3] = { 2, 3, 4 };
+ const uint altBlocks[3] = { kDropSound, kGoodSound, kBadSound };
for (uint i = 0; i < 3; ++i) {
for (uint a = 0; a < 2; ++a) {
const RandomSoundBlock &b = blocks[altBlocks[i]];
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index 753299539ac..6eb05f42df3 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -111,6 +111,16 @@ protected:
Common::Array<Piece> _pieces;
+ // Nancy12 stores the puzzle's sounds as this many random-sound blocks, in
+ // this fixed on-disk order.
+ static const uint kNumSounds = 6;
+ static const uint kPickupSound = 0;
+ static const uint kRotateSound = 1;
+ static const uint kDropSound = 2;
+ static const uint kGoodSound = 3;
+ static const uint kBadSound = 4;
+ static const uint kCompletionSound = 5;
+
SoundDescription _pickupSound;
SoundDescription _rotateSound;
SoundDescription _dropSound;
Commit: 1d80f75fe71d34db61f22188be284f930f907712
https://github.com/scummvm/scummvm/commit/1d80f75fe71d34db61f22188be284f930f907712
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:04+03:00
Commit Message:
NANCY: NANCY13: Add handling of new boot chunk data
- Implemented differences in the UIIV and INV chunks
- Implemented a parser for the MMIX (Music Mix Table) chunk
- Corrected the number of item and regular cursors
Changed paths:
engines/nancy/enginedata.cpp
engines/nancy/enginedata.h
engines/nancy/nancy.cpp
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 88e9128045f..732982644c8 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -159,7 +159,10 @@ INV::INV(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
s.syncAsUint16LE(captionAutoClearTime, kGameTypeNancy3);
readFilename(s, inventoryBoxIconsImageName);
- readFilename(s, inventoryCursorsImageName);
+ // Nancy13 split the cursor sheet into two images and moved both cursor
+ // image names into the CURS chunk, so the inventory cursors name is no
+ // longer stored here.
+ readFilename(s, inventoryCursorsImageName, kGameTypeVampire, kGameTypeNancy12);
s.skip(0x4, kGameTypeVampire, kGameTypeNancy1); // inventory box icons surface w/h
s.skip(0x4, kGameTypeVampire, kGameTypeNancy1); // inventory cursors surface w/h
@@ -1081,6 +1084,12 @@ UIIV::UIIV(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
readRectArray(*chunkStream, slotSrcRects, 16);
readRectArray(*chunkStream, slotDestRects, 16);
+ // Nancy13 inserted a 16-byte Rect here (before the original 2-byte field):
+ // the clickable bounding region of the inventory item slots, used as a
+ // PtInRect gate before hit-testing the individual slots.
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ readRect(*chunkStream, slotsHotspot);
+
chunkStream->skip(2);
for (uint i = 0; i < kNumFilters; ++i) {
@@ -1150,4 +1159,19 @@ UIRC::UIRC(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
}
}
+MMIX::MMIX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
+ const uint16 count = chunkStream->readUint16LE();
+ records.resize(count);
+
+ for (uint16 i = 0; i < count; ++i) {
+ Record &rec = records[i];
+ readFilename(*chunkStream, rec.name);
+ const uint16 numTracks = chunkStream->readUint16LE();
+ rec.musicNames.resize(numTracks);
+ for (uint16 j = 0; j < numTracks; ++j) {
+ readFilename(*chunkStream, rec.musicNames[j]);
+ }
+ }
+}
+
} // End of namespace Nancy
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 80311273362..8658e75a767 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -715,6 +715,7 @@ struct UIIV : public EngineData {
UIPopupHeader header;
Common::Array<Common::Rect> slotSrcRects; // 16 entries (image coords)
Common::Array<Common::Rect> slotDestRects; // 16 entries (screen coords)
+ Common::Rect slotsHotspot; // Nancy13+: clickable region of the item slots
UIButtonSlot filters[kNumFilters]; // 6 entries
Common::Array<Common::Rect> tabCaptionSrcRects; // 6 entries
Common::Rect tabCaptionDestRect; // on-screen target
@@ -774,6 +775,20 @@ struct UIRC : public EngineData {
Common::Array<ItemRecord> items;
};
+
+// Music mix table. Introduced in Nancy 13. Each record maps a short location
+// code (e.g. "BRI", "CAM", "TUT") to the set of music / ambience tracks that
+// may play there ("TacitA"/"TacitB" are the silence variants).
+struct MMIX : public EngineData {
+ struct Record {
+ Common::String name;
+ Common::Array<Common::String> musicNames;
+ };
+
+ MMIX(Common::SeekableReadStream *chunkStream);
+
+ Common::Array<Record> records;
+};
} // End of namespace Nancy
#endif // NANCY_ENGINEDATA_H
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index 68778d92ce1..b4ee347a02c 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -567,8 +567,8 @@ void NancyEngine::bootGameEngine() {
LOAD_BOOT(UIRC) // UI overlay element table
// Nancy 13+
- // RCPR and RCLB chunks have been removed
- // LOAD_BOOT(MMIX)
+ // RCPR and RCLB chunks have been removed (used in the RayCast puzzle, which has been dropped)
+ LOAD_BOOT(MMIX) // Music mix table (location code -> music tracks)
// Nancy 14
// LOAD_BOOT(UICM)
@@ -768,8 +768,8 @@ void NancyEngine::populateStaticData() {
_staticData.numCursorTypes = 37;
break;
case kGameTypeNancy13:
- _staticData.numItems = 50;
- _staticData.numCursorTypes = 37;
+ _staticData.numItems = 42;
+ _staticData.numCursorTypes = 45;
break;
case kGameTypeNancy14:
case kGameTypeNancy15:
Commit: a0475134000b6b94e428afc6419bb2911e2eb3eb
https://github.com/scummvm/scummvm/commit/a0475134000b6b94e428afc6419bb2911e2eb3eb
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:05+03:00
Commit Message:
NANCY: NANCY10: Simplify the FrameTextBox AR
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/miscrecords.cpp
engines/nancy/action/miscrecords.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 8d508efe0bc..e6ece524fbf 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -269,13 +269,12 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ModifyListEntry(ModifyListEntry::kDelete);
case 73:
return new ModifyListEntry(ModifyListEntry::kMark);
- case 74: // Added in Nancy 10
- case 75: // Changed in Nancy 10
- if (g_nancy->getGameType() <= kGameTypeNancy9 && type == 75) {
+ case 74: // Nancy 10
+ return new FrameTextBox(true);
+ case 75:
+ if (g_nancy->getGameType() <= kGameTypeNancy9)
return new TextBoxWrite();
- } else {
- return new FrameTextBox(static_cast<FrameTextBox::Variant>(type));
- }
+ return new FrameTextBox(false);
case 76:
return new TextboxClear();
case 77:
@@ -366,20 +365,19 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 126:
return new GoInvViewScene();
case 128:
- // Nancy 10+
+ // Nancy10+
return new CellPhonePopCellSceneFromStack();
case 129:
- // Nancy 10+
+ // Nancy10+
return new SetCellPhoneBatteryAndSignal();
case 130:
- // Nancy 10+
+ // Nancy10+
return new ChangeCellPhoneInfo();
case 131:
- // Nancy 10+
+ // Nancy10+
return new AddSearchLink();
case 132:
- // ResourceUse ("UIResource Adjust") - adjusts a UI overlay resource (see the UIRC
- // boot chunk) at runtime. Added in Nancy12.
+ // Nancy12+
return new ResourceUse();
case 140:
if (g_nancy->getGameType() >= kGameTypeNancy12)
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 544e4a56517..eceec99fa7f 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -221,19 +221,10 @@ void FrameTextBox::readData(Common::SeekableReadStream &stream) {
void FrameTextBox::execute() {
auto &tb = NancySceneState.getTextbox();
tb.clear();
- if (!_text.empty()) {
+ if (!_text.empty())
tb.addTextLine(_text);
- }
-
- // Variant 74 opens the full-width textbox overlay that covers the taskbar
- // buttons; the original arms a 15-second timer that drops it back to the
- // closed strip. Variant 75 is the legacy closed/strip path.
- if (_variant == kVariant74) {
- tb.setFullMode(true);
- } else {
- tb.setFullMode(false);
- }
+ tb.setFullMode(_fullMode);
finishExecution();
}
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 13ba5ce3443..740e8eecb8e 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -130,17 +130,12 @@ protected:
// text into the new (UICO-driven) textbox
class FrameTextBox : public ActionRecord {
public:
- enum Variant {
- kVariant74 = 74,
- kVariant75 = 75
- };
-
- FrameTextBox(Variant variant) : _variant(variant), _flags(0), _slot(0) {}
+ FrameTextBox(bool fullMode) : _fullMode(fullMode), _flags(0), _slot(0) {}
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
- Variant _variant;
+ bool _fullMode;
Common::String _text;
int16 _flags;
int16 _slot;
Commit: 827cdcee968b3337c9f1014f2120da51048c8a1b
https://github.com/scummvm/scummvm/commit/827cdcee968b3337c9f1014f2120da51048c8a1b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:06+03:00
Commit Message:
NANCY: NANCY13: Map Action Records and add relevant TODOs
Changed paths:
engines/nancy/action/arfactory.cpp
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index e6ece524fbf..834bd09e225 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -389,15 +389,33 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
// Saves a cropped image of the screen to a bitmap/TGA file.
// TODO: debug-only feature, not implemented
return nullptr;
- case 147: // Nancy11
- return new FadeSoundToSilence();
+ case 145:
+ // Nancy13 renumbered the sound-playing AR block downwards; PlaySound
+ // moved here (it was 150 up to Nancy12).
+ // TODO: verify the Nancy13 PlaySound on-disk layout matches PlaySound::readData.
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new PlaySound();
+ return nullptr;
+ case 146:
+ // Nancy13: FadeSoundToSilence moved here (was 147).
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new FadeSoundToSilence();
+ return nullptr;
+ case 147:
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new SetVolume(); // Nancy13: SetVolume moved here (was 148)
+ return new FadeSoundToSilence(); // Nancy11
case 148:
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new StopSound(); // Nancy13: StopSound moved here (was 154)
if (g_nancy->getGameType() >= kGameTypeNancy12)
return new SetVolume(); // Moved from 149 in Nancy12
// MakeScreenFile - seems to save a cropped image of the screen in a bitmap file?
// TODO: Used in Nancy 9, sand castle puzzle. Moved to 141 in Nancy12.
return nullptr;
case 149:
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new StopSound(); // Nancy13: StopAndUnloadSound moved here (was 155)
if (g_nancy->getGameType() >= kGameTypeNancy12)
return new PlaySoundEventFlagTerse(); // Moved from 161 in Nancy12
else if (g_nancy->getGameType() >= kGameTypeNancy9)
@@ -454,17 +472,52 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 165:
return new MindPuzzle();
case 166:
- // OneBuildPuzzle, moved here from 234 in Nancy12.
- // TODO: verify the Nancy12 data layout against OneBuildPuzzle::readData
+ // OneBuildPuzzle, moved here from 234 in Nancy12
return new OneBuildPuzzle();
case 167:
// TODO: Nancy12 ChasePuzzle (new), not implemented
return nullptr;
case 168:
return new Set3DSoundListenerPosition();
+ // -- Nancy 13 new/relocated puzzles (types 169-176) --
+ case 169:
+ // StepObjectsPuzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
case 170:
- // Moved to 140 in Nancy12
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
+ // WordFindPuzzle, new in Nancy13. This reuses the slot that used
+ // to hold SetPlayerClock (which itself moved to 140 in Nancy12).
+ // TODO: not yet implemented
+ return nullptr;
+ }
+ // SetPlayerClock lived here up to Nancy11; moved to 140 in Nancy12
return new SetPlayerClock();
+ case 171:
+ // TurningPuzzle, moved here from 209 in Nancy13.
+ if (g_nancy->getGameType() >= kGameTypeNancy13)
+ return new TurningPuzzle();
+ return nullptr;
+ case 172:
+ // BlocksPuzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
+ case 173:
+ // PegsPuzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
+ case 174:
+ // Unknown Puzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
+ case 175:
+ // Unknown Puzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
+ case 176:
+ // Unknown Puzzle, new in Nancy13
+ // TODO: not yet implemented
+ return nullptr;
case 200:
return new SoundEqualizerPuzzle();
case 201:
Commit: eb11becdbf796bf741cd2b0a0a3660fa15341d4d
https://github.com/scummvm/scummvm/commit/eb11becdbf796bf741cd2b0a0a3660fa15341d4d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:07+03:00
Commit Message:
NANCY: NANCY6: Fix subtitle not shown in HamRadioPuzzle
Fix #16876
Changed paths:
engines/nancy/action/puzzle/hamradiopuzzle.cpp
diff --git a/engines/nancy/action/puzzle/hamradiopuzzle.cpp b/engines/nancy/action/puzzle/hamradiopuzzle.cpp
index 5ea096f246a..42343fed118 100644
--- a/engines/nancy/action/puzzle/hamradiopuzzle.cpp
+++ b/engines/nancy/action/puzzle/hamradiopuzzle.cpp
@@ -311,9 +311,14 @@ void HamRadioPuzzle::execute() {
break;
}
- // Morse code is incorrect
+ // Morse code is invalid (unrecognized, or Send pressed without any
+ // dots/dashes). Play the "bad letter" sound, whose caption reads
+ // "Invalid Character", and stop here: falling through to the textbox
+ // refresh below would immediately overwrite that caption.
if (!foundCorrect) {
+ _curMorseString.clear();
_badLetterSound.loadAndPlay();
+ break;
}
}
Commit: 5d9736aacbe507531d46ab2fe6e8ac015be63994
https://github.com/scummvm/scummvm/commit/5d9736aacbe507531d46ab2fe6e8ac015be63994
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:08+03:00
Commit Message:
NANCY: NANCY6: Fix answer checking in PasswordPuzzle
Fix #16886
Changed paths:
engines/nancy/action/puzzle/passwordpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/passwordpuzzle.cpp b/engines/nancy/action/puzzle/passwordpuzzle.cpp
index fe0f5aa5e50..66fc2170237 100644
--- a/engines/nancy/action/puzzle/passwordpuzzle.cpp
+++ b/engines/nancy/action/puzzle/passwordpuzzle.cpp
@@ -118,8 +118,15 @@ void PasswordPuzzle::execute() {
bool solvedCurrentInput = false;
if (correctAnswers.size()) {
+ // The original accepts the input if a correct answer appears
+ // as a case-insensitive substring of what the player typed
+ // (e.g. answer "Xoc" is matched by typing "Lady Xoc").
+ Common::String inputLower = activeField;
+ inputLower.toLowercase();
for (uint i = 0; i < correctAnswers.size(); ++i) {
- if (activeField.equalsIgnoreCase(correctAnswers[i])) {
+ Common::String answerLower = correctAnswers[i];
+ answerLower.toLowercase();
+ if (inputLower.contains(answerLower)) {
solvedCurrentInput = true;
break;
}
Commit: 8909a5ddd9924c0304155058e925bfb9e056e720
https://github.com/scummvm/scummvm/commit/8909a5ddd9924c0304155058e925bfb9e056e720
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:09+03:00
Commit Message:
NANCY: Refactor movie playing code to be inside a common class
This helps with the following:
- avoid code duplication
- helps create a common video interface
- helps with separation of concerns
- allow us to reuse that code in some puzzles in the future
- add caching at a lower layer, to help with display of Bink video
panoramas
- hides all the code that checks for the video type, which is now done
by checking for file existence
Changed paths:
A engines/nancy/movieplayer.cpp
A engines/nancy/movieplayer.h
engines/nancy/action/actionmanager.cpp
engines/nancy/action/conversation.cpp
engines/nancy/action/conversation.h
engines/nancy/action/interactivevideo.cpp
engines/nancy/action/secondarymovie.cpp
engines/nancy/action/secondarymovie.h
engines/nancy/action/secondaryvideo.cpp
engines/nancy/action/secondaryvideo.h
engines/nancy/console.cpp
engines/nancy/module.mk
engines/nancy/state/map.cpp
engines/nancy/state/map.h
engines/nancy/ui/viewport.cpp
engines/nancy/ui/viewport.h
diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index 625aff0beaf..324571c365b 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -656,7 +656,7 @@ void ActionManager::synchronizeMovieWithSound() {
if (length.msecs() != 0) {
// ..and set the movie's playback speed to match
- movie->_decoder->setRate(Common::Rational(movie->_decoder->getDuration().msecs(), length.msecs()));
+ movie->_decoder.setRate(Common::Rational(movie->_decoder.getDuration().msecs(), length.msecs()));
}
}
}
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index f4370ebc9c5..d49e175d45d 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -673,7 +673,7 @@ bool ConversationSound::ConversationFlags::isSatisfied() const {
}
void ConversationVideo::init() {
- if (!_decoder.loadFile(Common::Path(_videoName + ".avf"))) {
+ if (!_decoder.loadFile(Common::Path(_videoName))) {
error("Couldn't load video file %s", _videoName.c_str());
}
diff --git a/engines/nancy/action/conversation.h b/engines/nancy/action/conversation.h
index 558b69ad2e9..5c6e7d180f9 100644
--- a/engines/nancy/action/conversation.h
+++ b/engines/nancy/action/conversation.h
@@ -23,7 +23,7 @@
#define NANCY_ACTION_CONVERSATION_H
#include "engines/nancy/action/actionrecord.h"
-#include "engines/nancy/video.h"
+#include "engines/nancy/movieplayer.h"
namespace Nancy {
namespace Action {
@@ -153,7 +153,7 @@ protected:
uint _videoFormat = kLargeVideoFormat;
uint16 _firstFrame = 0;
int16 _lastFrame = 0;
- AVFDecoder _decoder;
+ MoviePlayer _decoder;
};
class ConversationCelLoader;
diff --git a/engines/nancy/action/interactivevideo.cpp b/engines/nancy/action/interactivevideo.cpp
index 21ea17c58b6..e9a4c882a7b 100644
--- a/engines/nancy/action/interactivevideo.cpp
+++ b/engines/nancy/action/interactivevideo.cpp
@@ -116,7 +116,7 @@ void InteractiveVideo::handleInput(NancyInput &input) {
return;
}
- int curFrame = _movieAR->_decoder->getCurFrame();
+ int curFrame = _movieAR->_decoder.getCurFrame();
if (curFrame < 0) {
return;
}
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 1b1e5d2152d..c504e75271e 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -44,13 +44,6 @@ PlaySecondaryMovie::PlaySecondaryMovie(bool isRandom)
}
}
-void PlaySecondaryMovie::resetDecoder() {
- if (_videoType == kVideoPlaytypeAVF) {
- _decoder.reset(new AVFDecoder());
- } else {
- _decoder.reset(new Video::BinkDecoder());
- }
-}
PlaySecondaryMovie::~PlaySecondaryMovie() {
if (NancySceneState.getActiveMovie() == this) {
@@ -123,7 +116,6 @@ void PlaySecondaryMovie::readRandomMovieData(Common::Serializer &ser, Common::Se
_videoName = src.name;
_firstFrame = src.startFrame;
_lastFrame = src.lastFrame;
- _videoType = kVideoPlaytypeBink;
_videoFormat = kLargeVideoFormat;
_videoSceneChange = kMovieNoSceneChange;
_playerCursorAllowed = (byte)_randomPlayerCursorAllowed;
@@ -145,20 +137,9 @@ bool PlaySecondaryMovie::activateRandomSequence(int index) {
_firstFrame = src.startFrame;
_lastFrame = src.lastFrame;
- // Reload the decoder with the new movie. The original engine
- // auto-detects AVF vs Bink from disk; we honour the existing
- // _videoType but fall back to the other if needed.
- resetDecoder();
-
- Common::Path withExt = _videoName.append(_videoType == kVideoPlaytypeAVF ? ".avf" : ".bik");
- if (!_decoder->loadFile(withExt)) {
- _videoType = _videoType == kVideoPlaytypeAVF ? kVideoPlaytypeBink : kVideoPlaytypeAVF;
- resetDecoder();
- withExt = _videoName.append(_videoType == kVideoPlaytypeAVF ? ".avf" : ".bik");
- if (!_decoder->loadFile(withExt)) {
- warning("PlayRandomMovie: couldn't load %s", _videoName.toString().c_str());
- return false;
- }
+ if (!_decoder.loadFile(_videoName)) {
+ warning("PlayRandomMovie: couldn't load %s", _videoName.toString().c_str());
+ return false;
}
_isFinished = false;
@@ -205,9 +186,7 @@ int PlaySecondaryMovie::rollNextSequence() {
_randomPauseEndTime = g_system->getMillis() + (uint32)MAX<int32>(0, pauseMs);
_randomChainState = kRandomPaused;
setVisible(false);
- if (_decoder) {
- _decoder->pauseVideo(true);
- }
+ _decoder.pauseVideo(true);
return -1;
}
@@ -243,7 +222,7 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
readFilename(ser, _paletteName, kGameTypeVampire, kGameTypeVampire);
readFilename(ser, _bitmapOverlayName, kGameTypeVampire, kGameTypeNancy9);
- ser.syncAsUint16LE(_videoType, kGameTypeNancy7);
+ ser.skip(2, kGameTypeNancy7); // videoType
ser.skip(2, kGameTypeVampire, kGameTypeNancy9); // videoPlaySource
ser.syncAsUint16LE(_videoFormat);
if (g_nancy->getGameType() >= kGameTypeNancy10)
@@ -300,12 +279,8 @@ void PlaySecondaryMovie::readData(Common::SeekableReadStream &stream) {
}
void PlaySecondaryMovie::init() {
- if (!_decoder) {
- resetDecoder();
- }
-
- if (!_decoder->isVideoLoaded()) {
- if (!_decoder->loadFile(_videoName.append(_videoType == kVideoPlaytypeAVF ? ".avf" : ".bik"))) {
+ if (!_decoder.isVideoLoaded()) {
+ if (!_decoder.loadFile(_videoName)) {
error("Couldn't load video file %s", _videoName.toString().c_str());
}
@@ -330,11 +305,7 @@ void PlaySecondaryMovie::init() {
}
void PlaySecondaryMovie::onPause(bool pause) {
- if (!_decoder) {
- resetDecoder();
- }
-
- _decoder->pauseVideo(pause);
+ _decoder.pauseVideo(pause);
RenderActionRecord::onPause(pause);
}
@@ -361,7 +332,7 @@ void PlaySecondaryMovie::execute() {
// Sync audio and video. This is mostly relevant for some nancy2 scenes, as the
// devs stopped using the built-in movie sound around nancy4. The 12 ms
// difference is roughly how long it takes for a single execution of the main game loop
- ((AVFDecoder *)_decoder.get())->addFrameTime(12);
+ _decoder.addFrameTime(12);
}
if (_playerCursorAllowed == kNoPlayerCursorAllowed) {
@@ -375,7 +346,7 @@ void PlaySecondaryMovie::execute() {
_state = kRun;
- if (Common::Rect(_decoder->getWidth(), _decoder->getHeight()) == NancySceneState.getViewport().getBounds()) {
+ if (Common::Rect(_decoder.getWidth(), _decoder.getHeight()) == NancySceneState.getViewport().getBounds()) {
g_nancy->_graphics->suppressNextDraw();
break;
}
@@ -432,18 +403,18 @@ void PlaySecondaryMovie::execute() {
// another action record, but doesn't do so, because updateGraphics() gets called after all
// action record execution. Instead, the movie's own scene change (which is inexplicably enabled)
// gets triggered, and teleports the player to the wrong place instead of making them lose the game
- if (!_decoder->isPlaying() && _isVisible && !_isFinished) {
- _decoder->start();
+ if (!_decoder.isPlaying() && _isVisible && !_isFinished) {
+ _decoder.start();
if (_playDirection == kPlayMovieReverse) {
- _decoder->setRate(-_decoder->getRate());
- _decoder->seekToFrame(_lastFrame);
+ _decoder.setRate(-_decoder.getRate());
+ _decoder.seekToFrame(_lastFrame);
} else {
- _decoder->seekToFrame(_firstFrame);
+ _decoder.seekToFrame(_firstFrame);
}
}
- if (_decoder->needsUpdate()) {
+ if (_decoder.needsUpdate()) {
uint descID = 0;
for (uint i = 0; i < _videoDescs.size(); ++i) {
@@ -452,24 +423,24 @@ void PlaySecondaryMovie::execute() {
}
}
- GraphicsManager::copyToManaged(*_decoder->decodeNextFrame(), _fullFrame, g_nancy->getGameType() == kGameTypeVampire, _videoFormat == kSmallVideoFormat);
+ GraphicsManager::copyToManaged(*_decoder.decodeNextFrame(), _fullFrame, g_nancy->getGameType() == kGameTypeVampire, _videoFormat == kSmallVideoFormat);
_drawSurface.create(_fullFrame, _videoDescs[descID].srcRect);
moveTo(_videoDescs[descID].destRect);
_needsRedraw = true;
for (auto &f : _frameFlags) {
- if (_decoder->getCurFrame() == f.frameID) {
+ if (_decoder.getCurFrame() == f.frameID) {
NancySceneState.setEventFlag(f.flagDesc);
}
}
}
- if ((_decoder->getCurFrame() == _lastFrame && _playDirection == kPlayMovieForward) ||
- (_decoder->getCurFrame() == _firstFrame && _playDirection == kPlayMovieReverse) ||
- _decoder->endOfVideo()) {
+ if ((_decoder.getCurFrame() == _lastFrame && _playDirection == kPlayMovieForward) ||
+ (_decoder.getCurFrame() == _firstFrame && _playDirection == kPlayMovieReverse) ||
+ _decoder.endOfVideo()) {
- _decoder->pauseVideo(true);
+ _decoder.pauseVideo(true);
_isFinished = true;
if (_isRandom) {
@@ -507,8 +478,8 @@ void PlaySecondaryMovie::execute() {
// Allow looping
if (!_isDone) {
_isFinished = false;
- _decoder->seek(0);
- _decoder->pauseVideo(false);
+ _decoder.seek(0);
+ _decoder.pauseVideo(false);
} else if (_playerCursorAllowed == kNoPlayerCursorAllowed) {
// The movie finished and isn't looping, so restore the cursor now.
// WORKAROUND: Don't restore the cursor for Nancy 8, scenes 5420 - 5422
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index a251bde1060..e272dee2722 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -25,10 +25,7 @@
#include "common/ptr.h"
#include "engines/nancy/action/actionrecord.h"
-
-namespace Video {
-class VideoDecoder;
-}
+#include "engines/nancy/movieplayer.h"
namespace Nancy {
namespace Action {
@@ -98,7 +95,6 @@ public:
Common::Path _paletteName;
Common::Path _bitmapOverlayName;
- uint16 _videoType = kVideoPlaytypeAVF;
uint16 _videoFormat = kLargeVideoFormat;
uint16 _videoSceneChange = kMovieNoSceneChange;
byte _playerCursorAllowed = kPlayerCursorAllowed;
@@ -114,7 +110,7 @@ public:
SceneChangeDescription _sceneChange;
Common::Array<SecondaryVideoDescription> _videoDescs;
- Common::ScopedPtr<Video::VideoDecoder> _decoder;
+ MoviePlayer _decoder;
// Random-movie state (only populated when _isRandom).
bool _isRandom = false;
@@ -154,9 +150,6 @@ protected:
void readRandomMovieData(Common::Serializer &ser, Common::SeekableReadStream &stream);
void readRandomSequence(Common::Serializer &ser, RandomSequence &seq);
- // (Re)create _decoder as an AVFDecoder or BinkDecoder matching _videoType.
- void resetDecoder();
-
// Apply a RandomSequence's playback config to the PSM flat fields
// and reload the decoder. Returns true on success.
bool activateRandomSequence(int index);
diff --git a/engines/nancy/action/secondaryvideo.cpp b/engines/nancy/action/secondaryvideo.cpp
index c1547957c57..25ad754dfd2 100644
--- a/engines/nancy/action/secondaryvideo.cpp
+++ b/engines/nancy/action/secondaryvideo.cpp
@@ -38,7 +38,7 @@ void PlaySecondaryVideo::init() {
_decoder.close();
}
- if (!_decoder.loadFile(_filename.append(".avf"))) {
+ if (!_decoder.loadFile(_filename)) {
error("Couldn't load video file %s", _filename.toString().c_str());
}
@@ -63,7 +63,7 @@ void PlaySecondaryVideo::updateGraphics() {
return;
}
- if (_isInFrame && _decoder.isPlaying() ? _decoder.needsUpdate() || _decoder.atEnd() : true) {
+ if (_isInFrame && _decoder.isPlaying() ? _decoder.needsUpdate() || _decoder.endOfVideo() : true) {
int lastAnimationFrame = -1;
switch (_hoverState) {
case kNoHover:
@@ -115,7 +115,7 @@ void PlaySecondaryVideo::updateGraphics() {
}
if (lastAnimationFrame > -1 &&
- (_decoder.atEnd() ||
+ (_decoder.endOfVideo() ||
_decoder.getCurFrame() == lastAnimationFrame)) {
if (_hoverState == kNoHover) {
_decoder.seekToFrame(_loopFirstFrame);
diff --git a/engines/nancy/action/secondaryvideo.h b/engines/nancy/action/secondaryvideo.h
index d6cde625e98..3167c2677ee 100644
--- a/engines/nancy/action/secondaryvideo.h
+++ b/engines/nancy/action/secondaryvideo.h
@@ -24,7 +24,7 @@
#include "engines/nancy/cursor.h"
#include "engines/nancy/nancy.h"
-#include "engines/nancy/video.h"
+#include "engines/nancy/movieplayer.h"
#include "engines/nancy/action/actionrecord.h"
namespace Nancy {
@@ -92,7 +92,7 @@ protected:
Graphics::ManagedSurface _fullFrame;
HoverState _hoverState = kNoHover;
- AVFDecoder _decoder;
+ MoviePlayer _decoder;
int _currentViewportFrame = -1;
int _currentViewportScroll = -1;
bool _isInFrame = false;
diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index a8fc38dbac5..b7d5068ae2e 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -25,8 +25,8 @@
#include "audio/audiostream.h"
#include "image/bmp.h"
-#include "video/bink_decoder.h"
+#include "engines/nancy/movieplayer.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/console.h"
#include "engines/nancy/resource.h"
@@ -76,31 +76,20 @@ NancyConsole::~NancyConsole() {}
void NancyConsole::postEnter() {
GUI::Debugger::postEnter();
if (!_videoFile.empty()) {
- Common::Path withExt = _videoFile;
- Video::VideoDecoder *dec = new AVFDecoder();
-
- if (!dec->loadFile(withExt.append(".avf"))) {
- // No AVF found, try Bink
- delete dec;
- dec = new Video::BinkDecoder();
-
- if (!dec->loadFile(withExt.append(".bik"))) {
- debugPrintf("Failed to load video '%s'\n", _videoFile.toString(Common::Path::kNativeSeparator).c_str());
- delete dec;
- dec = nullptr;
- }
- }
+ MoviePlayer player;
- if (dec) {
+ if (!player.loadFile(_videoFile)) {
+ debugPrintf("Failed to load video '%s'\n", _videoFile.toString(Common::Path::kNativeSeparator).c_str());
+ } else {
Graphics::ManagedSurface surf;
if (!_paletteFile.empty()) {
GraphicsManager::loadSurfacePalette(surf, _paletteFile);
}
- dec->start();
+ player.start();
Common::EventManager *ev = g_system->getEventManager();
- while (!g_nancy->shouldQuit() && !dec->endOfVideo()) {
+ while (!g_nancy->shouldQuit() && !player.endOfVideo()) {
Common::Event event;
if (ev->pollEvent(event)) {
if (event.type == Common::EVENT_CUSTOM_ENGINE_ACTION_END && event.customType == InputManager::kNancyActionLeftClick) {
@@ -108,8 +97,8 @@ void NancyConsole::postEnter() {
}
}
- if (dec->needsUpdate()) {
- const Graphics::Surface *frame = dec->decodeNextFrame();
+ if (player.needsUpdate()) {
+ const Graphics::Surface *frame = player.decodeNextFrame();
if (frame) {
GraphicsManager::copyToManaged(*frame, surf, !_paletteFile.empty());
g_nancy->_graphics->debugDrawToScreen(surf);
@@ -124,7 +113,6 @@ void NancyConsole::postEnter() {
_videoFile.clear();
_paletteFile.clear();
- delete dec;
}
if (!_imageFile.empty()) {
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 1fb0010dfd5..79b5aeeaf7a 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -98,6 +98,7 @@ MODULE_OBJS = \
font.o \
graphics.o \
iff.o \
+ movieplayer.o \
input.o \
metaengine.o \
nancy.o \
diff --git a/engines/nancy/movieplayer.cpp b/engines/nancy/movieplayer.cpp
new file mode 100644
index 00000000000..bb9727d5e65
--- /dev/null
+++ b/engines/nancy/movieplayer.cpp
@@ -0,0 +1,198 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "common/system.h"
+
+#include "video/bink_decoder.h"
+
+#include "engines/nancy/video.h"
+#include "engines/nancy/commontypes.h"
+
+#include "engines/nancy/movieplayer.h"
+
+namespace Nancy {
+
+MoviePlayer::MoviePlayer() {}
+
+MoviePlayer::~MoviePlayer() {}
+
+bool MoviePlayer::loadFile(const Common::Path &name, bool bidirectionalCache) {
+ const Common::Path avfPath = name.append(".avf");
+ const Common::Path bikPath = name.append(".bik");
+
+ // Detect the format from which file exists (AVF wins if both do).
+ if (Common::File::exists(avfPath)) {
+ _videoType = kVideoPlaytypeAVF;
+ _decoder.reset(new AVFDecoder(bidirectionalCache ? AVFDecoder::kLoadBidirectional : AVFDecoder::kLoadForward));
+ } else if (Common::File::exists(bikPath)) {
+ _videoType = kVideoPlaytypeBink;
+ _decoder.reset(new Video::BinkDecoder());
+ } else {
+ _decoder.reset();
+ return false;
+ }
+
+ _currentSurface = nullptr;
+
+ if (!_decoder->loadFile(_videoType == kVideoPlaytypeAVF ? avfPath : bikPath)) {
+ _decoder.reset();
+ return false;
+ }
+ return true;
+}
+
+bool MoviePlayer::isVideoLoaded() const {
+ return _decoder && _decoder->isVideoLoaded();
+}
+
+void MoviePlayer::close() {
+ _currentSurface = nullptr;
+ if (_decoder) {
+ _decoder->close();
+ }
+}
+
+void MoviePlayer::start() { if (_decoder) _decoder->start(); }
+void MoviePlayer::stop() { if (_decoder) _decoder->stop(); }
+void MoviePlayer::pauseVideo(bool pause) { if (_decoder) _decoder->pauseVideo(pause); }
+bool MoviePlayer::isPlaying() const { return _decoder && _decoder->isPlaying(); }
+bool MoviePlayer::needsUpdate() const { return _decoder && _decoder->needsUpdate(); }
+
+bool MoviePlayer::endOfVideo() const {
+ if (!_decoder) {
+ return true;
+ }
+ // AVF has its own stricter end check (accounts for playback time / reversal).
+ if (_videoType == kVideoPlaytypeAVF) {
+ return ((AVFDecoder *)_decoder.get())->atEnd();
+ }
+ return _decoder->endOfVideo();
+}
+
+void MoviePlayer::seekToFrame(uint frame) { if (_decoder) _decoder->seekToFrame(frame); }
+void MoviePlayer::seek(const Audio::Timestamp &time) { if (_decoder) _decoder->seek(time); }
+void MoviePlayer::rewind() { if (_decoder) _decoder->rewind(); }
+void MoviePlayer::setRate(const Common::Rational &rate) { _decoder->setRate(rate); }
+Common::Rational MoviePlayer::getRate() const { return _decoder->getRate(); }
+void MoviePlayer::setReverse(bool reverse) { _decoder->setReverse(reverse); }
+
+int MoviePlayer::getCurFrame() const { return _decoder ? _decoder->getCurFrame() : -1; }
+int MoviePlayer::getFrameCount() const { return _decoder ? _decoder->getFrameCount() : 0; }
+Audio::Timestamp MoviePlayer::getDuration() const { return _decoder->getDuration(); }
+uint16 MoviePlayer::getWidth() const { return _decoder->getWidth(); }
+uint16 MoviePlayer::getHeight() const { return _decoder->getHeight(); }
+
+void MoviePlayer::addFrameTime(uint16 timeToAdd) {
+ if (_decoder && _videoType == kVideoPlaytypeAVF) {
+ ((AVFDecoder *)_decoder.get())->addFrameTime(timeToAdd);
+ }
+}
+
+const Graphics::Surface *MoviePlayer::decodeNextFrame(int frameNr) {
+ if (frameNr < 0) {
+ return _decoder->decodeNextFrame();
+ }
+
+ if (_videoType == kVideoPlaytypeAVF) {
+ AVFDecoder *decoder = (AVFDecoder *)_decoder.get();
+ const Graphics::Surface *frame = decoder->decodeFrame(frameNr);
+ decoder->seek(frameNr); // seek to take advantage of caching
+ return frame;
+ }
+
+ _decoder->seekToFrame(frameNr);
+ return _decoder->decodeNextFrame();
+}
+
+// --- Simple frame-range player ------------------------------------------
+
+void MoviePlayer::storeCurrentFrame() {
+ _currentSurface = _decoder->decodeNextFrame();
+}
+
+void MoviePlayer::goToFrame(int frameNr) {
+ if (!_decoder) {
+ return;
+ }
+
+ if (!_decoder->isPlaying()) {
+ _decoder->start();
+ }
+ _rangePlaying = false;
+ _decoder->setReverse(false);
+ _decoder->seekToFrame(frameNr);
+ storeCurrentFrame();
+
+ int frameCount = _decoder->getFrameCount();
+ uint32 durationMs = _decoder->getDuration().msecs();
+ _frameDelayMs = (frameCount > 0 && durationMs > 0) ? (durationMs / frameCount) : 66;
+}
+
+void MoviePlayer::playRange(int first, int last) {
+ if (!_decoder) {
+ return;
+ }
+
+ if (!_decoder->isPlaying()) {
+ _decoder->start();
+ }
+ _step = (last >= first) ? 1 : -1;
+ _rangeLast = last;
+ _decoder->setReverse(_step < 0);
+ _decoder->seekToFrame(first);
+ storeCurrentFrame();
+ _rangePlaying = (first != last);
+ _lastFrameTime = g_system->getMillis();
+}
+
+bool MoviePlayer::update() {
+ if (!_rangePlaying || !_decoder) {
+ return false;
+ }
+
+ // Own the frame timing (one frame per delay, never skipping), so playback
+ // starts immediately and stays smooth even when the decoder's own timing
+ // would stall after a seek.
+ uint32 now = g_system->getMillis();
+ if (now - _lastFrameTime < _frameDelayMs) {
+ return false;
+ }
+ _lastFrameTime = now;
+
+ if (_decoder->endOfVideo() ||
+ (_step > 0 && _decoder->getCurFrame() >= _rangeLast) ||
+ (_step < 0 && _decoder->getCurFrame() <= _rangeLast)) {
+ _rangePlaying = false;
+ return false;
+ }
+
+ storeCurrentFrame();
+ return true;
+}
+
+void MoviePlayer::drawFrame(Graphics::ManagedSurface &dst, const Common::Point &pos) const {
+ if (_currentSurface) {
+ dst.blitFrom(*_currentSurface, pos);
+ }
+}
+
+} // End of namespace Nancy
diff --git a/engines/nancy/movieplayer.h b/engines/nancy/movieplayer.h
new file mode 100644
index 00000000000..6b32b5b923d
--- /dev/null
+++ b/engines/nancy/movieplayer.h
@@ -0,0 +1,122 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_MOVIEPLAYER_H
+#define NANCY_MOVIEPLAYER_H
+
+#include "common/ptr.h"
+#include "common/path.h"
+#include "common/rational.h"
+#include "common/rect.h"
+
+#include "audio/timestamp.h"
+
+#include "graphics/managed_surface.h"
+
+#include "engines/nancy/commontypes.h"
+
+namespace Graphics {
+struct Surface;
+}
+
+namespace Video {
+class VideoDecoder;
+}
+
+namespace Nancy {
+
+// The single low-level interface to the AVF/Bink video decoders. Every consumer
+// (PlaySecondaryMovie, PlaySecondaryVideo, Conversation, Viewport, Map,
+// BoardGamePuzzle, ...) owns one of these instead of a raw Video::VideoDecoder,
+// so the AVF-vs-Bink specifics (decoder creation, .avf/.bik file selection, and
+// the AVF-only behaviour of decodeFrame/addFrameTime/endOfVideo) live in one
+// place. All other methods forward 1:1 to the underlying decoder, so playback
+// behaviour is unchanged; higher-level playback/timing logic stays per consumer.
+class MoviePlayer {
+public:
+ MoviePlayer();
+ ~MoviePlayer();
+
+ // Load <name> + ".avf"/".bik", auto-detecting the format from which file
+ // exists (AVF preferred if both do) and creating the matching decoder.
+ // bidirectionalCache selects the AVF frame-cache hint (for scrubbing).
+ bool loadFile(const Common::Path &name, bool bidirectionalCache = false);
+ bool isVideoLoaded() const;
+ void close();
+ Video::VideoDecoder *getDecoder() { return _decoder.get(); }
+
+ // Playback control - forwarded to the decoder.
+ void start();
+ void stop();
+ void pauseVideo(bool pause);
+ bool isPlaying() const;
+ bool needsUpdate() const;
+ bool endOfVideo() const; // AVF uses AVFDecoder::atEnd()
+
+ void seekToFrame(uint frame);
+ void seek(const Audio::Timestamp &time);
+ void rewind();
+ void setRate(const Common::Rational &rate);
+ Common::Rational getRate() const;
+ void setReverse(bool reverse);
+
+ int getCurFrame() const;
+ int getFrameCount() const;
+ Audio::Timestamp getDuration() const;
+ uint16 getWidth() const;
+ uint16 getHeight() const;
+ void addFrameTime(uint16 timeToAdd); // AVF only, no-op otherwise
+
+ // Decode a frame: frameNr < 0 returns the next frame; otherwise that
+ // specific frame via the format-appropriate cached path (AVF decodeFrame +
+ // seek; Bink seekToFrame + decodeNextFrame).
+ const Graphics::Surface *decodeNextFrame(int frameNr = -1);
+
+ // --- Optional simple frame-range player, built on the above. Handy for
+ // consumers (e.g. BoardGamePuzzle) that just want to show a still frame or
+ // play [first, last] over time. Uses its own, no-skip timing so it starts
+ // immediately and stays smooth, and decodes into an internal surface.
+ void goToFrame(int frameNr); // show a single still frame
+ void playRange(int first, int last); // begin timed playback (reverse if last < first)
+ bool update(); // advance; true if the shown frame changed
+ bool isRangePlaying() const { return _rangePlaying; }
+ int getCurrentFrame() const { return getCurFrame(); }
+ void drawFrame(Graphics::ManagedSurface &dst, const Common::Point &pos) const;
+
+private:
+ void storeCurrentFrame();
+
+ Common::ScopedPtr<Video::VideoDecoder> _decoder;
+ byte _videoType = kVideoPlaytypeAVF;
+
+ // Simple frame-range player state. _currentSurface points at the decoder's
+ // last-decoded frame (owned by it, valid until the next decode).
+ const Graphics::Surface *_currentSurface = nullptr;
+ int _rangeLast = 0;
+ int _step = 1;
+ bool _rangePlaying = false;
+ uint32 _lastFrameTime = 0;
+ uint32 _frameDelayMs = 66;
+};
+
+} // End of namespace Nancy
+
+#endif // NANCY_MOVIEPLAYER_H
diff --git a/engines/nancy/state/map.cpp b/engines/nancy/state/map.cpp
index 230cd5f219e..73ea2c47ba5 100644
--- a/engines/nancy/state/map.cpp
+++ b/engines/nancy/state/map.cpp
@@ -173,7 +173,7 @@ void Map::MapViewport::loadVideo(const Common::Path &filename, const Common::Pat
_decoder.close();
}
- if (!_decoder.loadFile(filename.append(".avf"))) {
+ if (!_decoder.loadFile(filename)) {
error("Couldn't load video file %s", filename.toString().c_str());
}
diff --git a/engines/nancy/state/map.h b/engines/nancy/state/map.h
index 319e60a5876..0f51eb2a7fa 100644
--- a/engines/nancy/state/map.h
+++ b/engines/nancy/state/map.h
@@ -26,7 +26,7 @@
#include "common/singleton.h"
#include "engines/nancy/sound.h"
-#include "engines/nancy/video.h"
+#include "engines/nancy/movieplayer.h"
#include "engines/nancy/state/state.h"
@@ -68,7 +68,7 @@ protected:
void playVideo() { _decoder.start(); }
void unloadVideo() { _decoder.close(); }
- AVFDecoder _decoder;
+ MoviePlayer _decoder;
private:
};
diff --git a/engines/nancy/ui/viewport.cpp b/engines/nancy/ui/viewport.cpp
index fc7e1b116d1..1a4be6fc8c0 100644
--- a/engines/nancy/ui/viewport.cpp
+++ b/engines/nancy/ui/viewport.cpp
@@ -32,7 +32,6 @@
#include "engines/nancy/ui/viewport.h"
#include "common/config-manager.h"
-#include "video/bink_decoder.h"
namespace Nancy {
namespace UI {
@@ -219,39 +218,14 @@ void Viewport::handleInput(NancyInput &input) {
}
void Viewport::loadVideo(const Common::Path &filename, uint frameNr, uint verticalScroll, byte panningType, uint16 format, const Common::Path &palette) {
- if (_decoder->isVideoLoaded()) {
- _decoder->close();
+ if (_decoder.isVideoLoaded()) {
+ _decoder.close();
}
- Common::String suffix;
-
- if (_videoType == kVideoPlaytypeAVF) {
- suffix = ".avf";
-
- if (!Common::File::exists(filename.append(".avf"))) {
- if (Common::File::exists(filename.append(".bik"))) {
- suffix = ".bik";
- _videoType = kVideoPlaytypeBink;
- _decoder.reset(new Video::BinkDecoder());
- } else {
- error("Couldn't load video file %s.avf or %s.bik", filename.toString().c_str(), filename.toString().c_str());
- }
- }
- } else {
- suffix = ".bik";
- if (!Common::File::exists(filename.append(".bik"))) {
- if (Common::File::exists(filename.append(".avf"))) {
- suffix = ".avf";
- _videoType = kVideoPlaytypeAVF;
- _decoder.reset(new AVFDecoder());
- } else {
- error("Couldn't load video file %s.avf or %s.bik", filename.toString().c_str(), filename.toString().c_str());
- }
- }
- }
-
- if (!_decoder->loadFile(filename.append(suffix))) {
- error("Couldn't load video file %s", filename.toString().c_str());
+ // The scene video uses the bidirectional AVF frame cache so scrubbing in
+ // both directions is fast; the player selects AVF/Bink by which file exists.
+ if (!_decoder.loadFile(filename, true)) {
+ error("Couldn't load video file %s.avf or %s.bik", filename.toString().c_str(), filename.toString().c_str());
}
_videoFormat = format;
@@ -273,23 +247,10 @@ void Viewport::loadVideo(const Common::Path &filename, uint frameNr, uint vertic
}
void Viewport::setFrame(uint frameNr) {
- assert(frameNr < _decoder->getFrameCount());
+ assert(frameNr < (uint)_decoder.getFrameCount());
- const Graphics::Surface *newFrame;
-
- if (_videoType == kVideoPlaytypeAVF) {
- AVFDecoder *decoder = dynamic_cast<AVFDecoder *>(_decoder.get());
- if (!decoder)
- error("Viewport::setFrame(): Decoder is not an AVFDecoder");
- newFrame = decoder->decodeFrame(frameNr);
- decoder->seek(frameNr); // Seek to take advantage of caching
- } else {
- Video::BinkDecoder *decoder = dynamic_cast<Video::BinkDecoder *>(_decoder.get());
- if (!decoder)
- error("Viewport::setFrame(): Decoder is not a BinkDecoder");
- decoder->seekToFrame(frameNr); // Seek to take advantage of caching
- newFrame = decoder->decodeNextFrame();
- }
+ // The player returns the frame using the format-appropriate cached path.
+ const Graphics::Surface *newFrame = _decoder.decodeNextFrame(frameNr);
// Format 1 uses quarter-size images, while format 2 uses full-size ones
// Videos in TVD are always upside-down
diff --git a/engines/nancy/ui/viewport.h b/engines/nancy/ui/viewport.h
index 6b2c5cb9367..7d3a8adb0e8 100644
--- a/engines/nancy/ui/viewport.h
+++ b/engines/nancy/ui/viewport.h
@@ -23,7 +23,8 @@
#define NANCY_UI_VIEWPORT_H
#include "engines/nancy/time.h"
-#include "engines/nancy/video.h"
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/movieplayer.h"
#include "engines/nancy/renderobject.h"
@@ -48,10 +49,9 @@ public:
_videoFormat(kLargeVideoFormat),
_stickyCursorPos(-1, -1),
_panningType(kPanNone),
- _decoder(new AVFDecoder(AVFDecoder::kLoadBidirectional)),
_autoMove(false) {}
- virtual ~Viewport() { _decoder->close(); _fullFrame.free(); }
+ virtual ~Viewport() { _decoder.close(); _fullFrame.free(); }
void init() override;
void handleInput(NancyInput &input);
@@ -66,7 +66,7 @@ public:
void scrollUp(uint delta);
void scrollDown(uint delta);
- uint16 getFrameCount() const { return _decoder->isVideoLoaded() ? _decoder->getFrameCount() : 0; }
+ uint16 getFrameCount() const { return _decoder.getFrameCount(); }
uint16 getCurFrame() const { return _currentFrame; }
uint16 getCurVerticalScroll() const { return _drawSurface.getOffsetFromOwner().y; }
uint16 getMaxScroll() const;
@@ -88,8 +88,7 @@ protected:
byte _panningType;
- Common::ScopedPtr<Video::VideoDecoder> _decoder;
- uint16 _videoType = kVideoPlaytypeAVF;
+ MoviePlayer _decoder;
uint16 _currentFrame;
uint16 _videoFormat;
Graphics::ManagedSurface _fullFrame;
Commit: c3fd298f7557785a2ca5f0257f01926bae8842ac
https://github.com/scummvm/scummvm/commit/c3fd298f7557785a2ca5f0257f01926bae8842ac
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:10+03:00
Commit Message:
NANCY: NANCY12: Implement BoardGamePuzzle
Movie-driven board game introduced in Nancy12 (AR 164) - the "Mother Clock"
puzzle. The board states are pre-rendered frames of a Bink movie the game
navigates between as moves are made.
Changed paths:
A engines/nancy/action/puzzle/boardgamepuzzle.cpp
A engines/nancy/action/puzzle/boardgamepuzzle.h
engines/nancy/action/arfactory.cpp
engines/nancy/module.mk
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 834bd09e225..67dceb62758 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -37,6 +37,7 @@
#include "engines/nancy/action/puzzle/assemblypuzzle.h"
#include "engines/nancy/action/puzzle/bballpuzzle.h"
#include "engines/nancy/action/puzzle/beadpuzzle.h"
+#include "engines/nancy/action/puzzle/boardgamepuzzle.h"
#include "engines/nancy/action/puzzle/bulpuzzle.h"
#include "engines/nancy/action/puzzle/bombpuzzle.h"
#include "engines/nancy/action/puzzle/cardgamepuzzle.h"
@@ -467,8 +468,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
// TODO: Nancy12 MirrorLightPuzzle (new), not implemented
return nullptr;
case 164:
- // TODO: Nancy12 BoardGamePuzzle (new), not implemented
- return nullptr;
+ return new BoardGamePuzzle();
case 165:
return new MindPuzzle();
case 166:
diff --git a/engines/nancy/action/puzzle/boardgamepuzzle.cpp b/engines/nancy/action/puzzle/boardgamepuzzle.cpp
new file mode 100644
index 00000000000..485b317ea91
--- /dev/null
+++ b/engines/nancy/action/puzzle/boardgamepuzzle.cpp
@@ -0,0 +1,293 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/boardgamepuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+void BoardGamePuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName); // 0x00
+ stream.skip(2); // int16 card count (10)
+ readRectArray(stream, _cardBlueSrcs, kNumButtons); // 0x23, available-card sprites
+ readRectArray(stream, _cardWhiteSrcs, kNumButtons); // 0xc3, active-card sprites
+ readRectArray(stream, _buttonRects, kNumButtons); // 0x163, on-screen card positions
+ readRect(stream, _resetPressedSrc); // 0x203, pressed reset sprite
+ readRect(stream, _resetButtonRect); // 0x213, on-screen reset position
+ readFilename(stream, _movieName); // 0x223, the board-game movie
+ readRect(stream, _movieRect); // 0x244
+ readRect(stream, _boardRect); // 0x254
+
+ _framesPerPosition = stream.readSint16LE(); // 0x264
+ _winTarget = stream.readSint16LE(); // 0x266
+ stream.skip(2); // 0x268
+
+ _moves.resize(kNumMoves);
+ for (uint i = 0; i < kNumMoves; ++i) { // 0x26a, 12 x 10-byte records
+ _moves[i].amount = stream.readSint16LE();
+ _moves[i].jumpFrom = stream.readSint16LE();
+ _moves[i].jumpTo = stream.readSint16LE();
+ _moves[i].jumpFrameStart = stream.readSint16LE();
+ _moves[i].jumpFrameEnd = stream.readSint16LE();
+ }
+
+ _winScene.readData(stream); // 0x2e2, SceneChangeWithFlag
+ _loseScene.readData(stream); // 0x2fb, SceneChangeWithFlag
+
+ // Six random-sound blocks (button/click/clank/slide/key/beep).
+ for (uint i = 0; i < kNumSounds; ++i) {
+ _sounds[i].readData(stream);
+ }
+}
+
+void BoardGamePuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ _moviePlayer.loadFile(_movieName);
+ _buttonUsed.clear();
+ _buttonUsed.resize(kNumButtons, false);
+ _position = 0;
+ _solved = false;
+ _lost = false;
+ _boardState = kBoardWaiting;
+ _moviePlayer.goToFrame(framePosition(_position));
+
+ redraw();
+}
+
+void BoardGamePuzzle::drawCard(uint index) {
+ if (index >= _buttonRects.size() || _buttonRects[index].isEmpty()) {
+ return;
+ }
+
+ const Common::Rect *src;
+ if ((int)index == _activeCard) {
+ src = &_cardWhiteSrcs[index]; // currently being played
+ } else if (!_buttonUsed[index]) {
+ src = &_cardBlueSrcs[index]; // available
+ } else {
+ return; // used -> dark card from the background shows
+ }
+
+ const Common::Rect &dst = _buttonRects[index];
+ _drawSurface.blitFrom(_image, *src, Common::Point(dst.left, dst.top));
+}
+
+void BoardGamePuzzle::redraw() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ // The board movie over the board region. Everything else (board frame, the
+ // reset button, and the dark "used" cards) lives on the scene background and
+ // shows through the transparent surface. _image is a sprite sheet holding
+ // only the blue/white card variants + the pressed reset button.
+ _moviePlayer.drawFrame(_drawSurface, Common::Point(_boardRect.left, _boardRect.top));
+
+ // The move cards: blue when available, white while being played.
+ for (uint i = 0; i < _buttonRects.size(); ++i) {
+ drawCard(i);
+ }
+
+ // The reset button only appears (its pressed sprite) while it is held.
+ if (_resetPressed && !_resetButtonRect.isEmpty()) {
+ _drawSurface.blitFrom(_image, _resetPressedSrc,
+ Common::Point(_resetButtonRect.left, _resetButtonRect.top));
+ }
+
+ _needsRedraw = true;
+}
+
+void BoardGamePuzzle::playSoundBlock(uint index) {
+ if (index >= kNumSounds) {
+ return;
+ }
+
+ const RandomSoundBlock &block = _sounds[index];
+ if (block.names.empty() || block.names[0].empty() || block.names[0] == "NO SOUND") {
+ return;
+ }
+
+ SoundDescription desc;
+ desc.name = block.names[0];
+ desc.channelID = block.channel;
+ desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+ desc.volume = block.volume;
+
+ g_nancy->_sound->loadSound(desc);
+ g_nancy->_sound->playSound(desc);
+}
+
+void BoardGamePuzzle::resolveMove(int button) {
+ // Mirrors the original move-resolution rules: advance the token along the
+ // track, warp on a jump square, win by landing on the target exactly, lose
+ // by overshooting it. Positions map to movie frames via framePosition().
+ const MoveRecord &m = _moves[button];
+ int oldPos = _position;
+ int startFrame, endFrame;
+
+ if (oldPos == 0) {
+ startFrame = 0;
+ _position = m.amount;
+ endFrame = framePosition(_position);
+ } else if (oldPos == m.jumpFrom) {
+ startFrame = m.jumpFrameStart;
+ endFrame = m.jumpFrameEnd;
+ _position = m.jumpTo;
+ } else if (m.amount + oldPos > _winTarget) {
+ _lost = true;
+ startFrame = framePosition(oldPos);
+ endFrame = startFrame;
+ } else {
+ startFrame = framePosition(oldPos);
+ _position += m.amount;
+ endFrame = framePosition(_position);
+ if (_position == _winTarget) {
+ _solved = true;
+ }
+ }
+
+ _buttonUsed[button] = true;
+ _activeCard = button; // shows the white sprite while the move plays
+ _boardState = kBoardAnimating;
+ playSoundBlock(kSlideSound);
+ _moviePlayer.playRange(startFrame, endFrame);
+
+ // Full redraw once at the start so the played card turns white and the first
+ // frame is shown; the animation then only re-blits the movie region.
+ redraw();
+}
+
+void BoardGamePuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ registerGraphics();
+ _state = kRun;
+ // fall through
+ case kRun:
+ if (_resetPressed && g_nancy->getTotalPlayTime() - _resetPressedTime > 200) {
+ _resetPressed = false;
+ redraw();
+ }
+
+ if (_boardState == kBoardAnimating) {
+ if (_moviePlayer.update()) {
+ // Only the board (movie) region changes each frame; re-blitting
+ // the whole overlay + re-darkening buttons here would be far too
+ // slow and make playback drop frames. The static chrome stays.
+ _moviePlayer.drawFrame(_drawSurface, Common::Point(_boardRect.left, _boardRect.top));
+ _needsRedraw = true;
+ }
+ if (!_moviePlayer.isRangePlaying()) {
+ // The played card is now used: a full redraw turns it dark.
+ _activeCard = -1;
+ redraw();
+
+ if (_solved) {
+ playSoundBlock(kWinSound);
+ _resultTime = g_nancy->getTotalPlayTime();
+ _boardState = kBoardResult;
+ } else if (_lost) {
+ playSoundBlock(kLoseSound);
+ _resultTime = g_nancy->getTotalPlayTime();
+ _boardState = kBoardResult;
+ } else {
+ playSoundBlock(kLandSound);
+ _boardState = kBoardWaiting;
+ }
+ }
+ } else if (_boardState == kBoardResult) {
+ // Hold on the finished board briefly before the scene change.
+ if (g_nancy->getTotalPlayTime() - _resultTime > 1500) {
+ _state = kActionTrigger;
+ }
+ }
+ break;
+ case kActionTrigger:
+ if (_solved) {
+ _winScene.execute();
+ } else if (_lost) {
+ _loseScene.execute();
+ }
+ finishExecution();
+ break;
+ }
+}
+
+void BoardGamePuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _boardState != kBoardWaiting) {
+ return;
+ }
+
+ // Reset returns the token to the start and re-enables every move button.
+ if (!_resetButtonRect.isEmpty() &&
+ NancySceneState.getViewport().convertViewportToScreen(_resetButtonRect).contains(input.mousePos)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _position = 0;
+ for (uint i = 0; i < _buttonUsed.size(); ++i) {
+ _buttonUsed[i] = false;
+ }
+ _resetPressed = true;
+ _resetPressedTime = g_nancy->getTotalPlayTime();
+ playSoundBlock(kResetSound);
+ _moviePlayer.goToFrame(framePosition(_position));
+ redraw();
+ }
+ return;
+ }
+
+ // A move button advances the token; each may be used once.
+ for (uint i = 0; i < _buttonRects.size(); ++i) {
+ if (_buttonUsed[i] || _buttonRects[i].isEmpty()) {
+ continue;
+ }
+ if (!NancySceneState.getViewport().convertViewportToScreen(_buttonRects[i]).contains(input.mousePos)) {
+ continue;
+ }
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ playSoundBlock(kButtonSound);
+ resolveMove(i);
+ }
+ return;
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/boardgamepuzzle.h b/engines/nancy/action/puzzle/boardgamepuzzle.h
new file mode 100644
index 00000000000..c2a586829c8
--- /dev/null
+++ b/engines/nancy/action/puzzle/boardgamepuzzle.h
@@ -0,0 +1,122 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_BOARDGAMEPUZZLE_H
+#define NANCY_ACTION_BOARDGAMEPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/movieplayer.h"
+
+namespace Nancy {
+namespace Action {
+
+// Movie-driven board game introduced in Nancy12 (AR 164) - the "Mother Clock"
+// puzzle. The board states are pre-rendered frames of a Bink movie the game
+// navigates between as moves are made.
+class BoardGamePuzzle : public RenderActionRecord {
+public:
+ BoardGamePuzzle() : RenderActionRecord(7) {}
+ virtual ~BoardGamePuzzle() {}
+
+ void init() override;
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+ void handleInput(NancyInput &input) override;
+
+ bool isViewportRelative() const override { return true; }
+
+protected:
+ Common::String getRecordTypeName() const override { return "BoardGamePuzzle"; }
+
+ static const uint kNumSounds = 6;
+ static const uint kNumButtons = 10;
+ static const uint kNumMoves = 12;
+
+ // One turn's scripted move: advance by _amount; landing on _jumpFrom warps
+ // to _jumpTo playing movie frames [_jumpFrameStart, _jumpFrameEnd].
+ struct MoveRecord {
+ int16 amount = 0;
+ int16 jumpFrom = 0;
+ int16 jumpTo = 0;
+ int16 jumpFrameStart = 0;
+ int16 jumpFrameEnd = 0;
+ };
+
+ // File data
+ Common::Path _imageName;
+ Common::Path _movieName; // the board-game Bink movie ("..._ANIM")
+ Common::Rect _movieRect; // movie source/params rect (buf+0x244)
+ Common::Rect _boardRect; // board area within the viewport (buf+0x254)
+
+ Common::Array<Common::Rect> _cardBlueSrcs; // available-card sprites in _image (buf+0x23)
+ Common::Array<Common::Rect> _cardWhiteSrcs; // active-card sprites in _image (buf+0xc3)
+ Common::Array<Common::Rect> _buttonRects; // 10 on-screen card positions (buf+0x163)
+ Common::Rect _resetPressedSrc; // pressed reset sprite in _image (buf+0x203)
+ Common::Rect _resetButtonRect; // on-screen reset position (buf+0x213)
+
+ int16 _framesPerPosition = 0; // track position N shows at movie frame N*this+1
+ int16 _winTarget = 0; // land here exactly to win; overshoot loses
+ Common::Array<MoveRecord> _moves; // 12 scripted turn records (buf+0x26a)
+
+ SceneChangeWithFlag _winScene; // buf+0x2e2
+ SceneChangeWithFlag _loseScene; // buf+0x2fb (also the exit/quit target)
+
+ RandomSoundBlock _sounds[kNumSounds]; // button / click / clank / slide / key / beep
+
+ // Named indices into _sounds (best-guess mapping from the sound names).
+ enum SoundIndex {
+ kButtonSound = 0, // ButtonDownUp02 - a move button is pressed
+ kResetSound = 1, // Click_MetalButton02 - the reset button
+ kLandSound = 2, // Clank_Metal01 - token settles on a square
+ kSlideSound = 3, // MetalSlide09 - token slides during a move
+ kWinSound = 4, // Key_Turn_Double - reached the target
+ kLoseSound = 5 // Beep01 - overshot the target
+ };
+
+ // Runtime state
+ enum BoardState { kBoardWaiting, kBoardAnimating, kBoardResult };
+
+ MoviePlayer _moviePlayer;
+ int16 _position = 0; // current track position (0.._winTarget)
+ Common::Array<bool> _buttonUsed; // a move button may be pressed once
+ int16 _activeCard = -1; // card currently being played (white sprite)
+ bool _resetPressed = false; // reset button showing its pressed sprite
+ uint32 _resetPressedTime = 0;
+ BoardState _boardState = kBoardWaiting;
+ bool _solved = false; // reached the target exactly
+ bool _lost = false; // overshot the target
+ uint32 _resultTime = 0; // ms timestamp when the game ended, for the result hold
+
+ Graphics::ManagedSurface _image;
+
+ void redraw();
+ void drawCard(uint index);
+ void resolveMove(int button);
+ void playSoundBlock(uint index);
+ int framePosition(int position) const { return _framesPerPosition * position + 1; }
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_BOARDGAMEPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 79b5aeeaf7a..09fa6b98a4f 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -20,6 +20,7 @@ MODULE_OBJS = \
action/puzzle/assemblypuzzle.o \
action/puzzle/bballpuzzle.o \
action/puzzle/beadpuzzle.o \
+ action/puzzle/boardgamepuzzle.o \
action/puzzle/bulpuzzle.o \
action/puzzle/bombpuzzle.o \
action/puzzle/cardgamepuzzle.o \
Commit: 3b875efc0e7b3d06a3846f8a73fd07cd895262f5
https://github.com/scummvm/scummvm/commit/3b875efc0e7b3d06a3846f8a73fd07cd895262f5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-02T09:23:11+03:00
Commit Message:
NANCY: NANCY12: Initial work on MirrorLightPuzzle and ActionZone
In this puzzle, the player rotates a set of mirrors within their angle
limits to route a beam of light from a source to a target.
Still missing functionality, described in relevant TODOs.
Also, introduce the reusable ActionZone code, used in some puzzles
in Nancy12.
Changed paths:
A engines/nancy/action/actionzone.cpp
A engines/nancy/action/actionzone.h
A engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
A engines/nancy/action/puzzle/mirrorlightpuzzle.h
engines/nancy/action/arfactory.cpp
engines/nancy/module.mk
diff --git a/engines/nancy/action/actionzone.cpp b/engines/nancy/action/actionzone.cpp
new file mode 100644
index 00000000000..dc8f730137e
--- /dev/null
+++ b/engines/nancy/action/actionzone.cpp
@@ -0,0 +1,152 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/stream.h"
+
+#include "engines/nancy/util.h"
+#include "engines/nancy/action/actionzone.h"
+
+namespace Nancy {
+namespace Action {
+
+void ActionZone::readData(Common::SeekableReadStream &stream) {
+ // Base ActionZone (matches the original "Action Zone Boundary OVL" reader).
+ typeField = stream.readSint32LE();
+ type = typeField & 0xFF;
+
+ readRect(stream, rect);
+ readFilename(stream, ovlName);
+
+ pointA = stream.readSint32LE();
+ pointB = stream.readSint32LE();
+ valC = stream.readSint32LE();
+ valD = stream.readSint32LE();
+ val49 = stream.readSint16LE();
+ val4b = stream.readByte();
+
+ // Random-sound block: count, then that many 33-byte names + 8 bytes of params.
+ _sound.readData(stream);
+
+ // Subtype-specific trailing data. Fields not yet needed are skipped to keep
+ // the stream aligned.
+ switch (type) {
+ case 1: // base only
+ case 5:
+ case 0x10:
+ case 0x14: // Boundary
+ break;
+ case 0: // Special Effect (and movement variants) - peeked terminator
+ case 0x11:
+ case 0x12:
+ case 0x13:
+ readSpecialEffect(stream);
+ break;
+ case 0x0b:
+ stream.skip(3); // int16 + byte (plain, no peek)
+ break;
+ case 0x0e:
+ stream.skip(2); // int16
+ break;
+ case 0x0f:
+ stream.skip(4); // int16 + int16
+ break;
+ case 3:
+ stream.skip(8); // double
+ break;
+ case 0x17: // Flat Tire (min/max)
+ stream.skip(8); // int32 + int32
+ break;
+ case 4:
+ stream.skip(10); // double + int16
+ break;
+ case 2:
+ stream.skip(24); // Rect + int32 + int16 + int16
+ break;
+ case 0x0c:
+ readSpecialEffect(stream);
+ stream.skip(3); // int16 + byte
+ break;
+ case 0x15:
+ readSpecialEffect(stream);
+ stream.skip(4); // int32
+ break;
+ case 0x0d: // OverlayZone
+ readOverlayZone(stream);
+ break;
+ case 0x16: // OverlayZone + int32
+ readOverlayZone(stream);
+ stream.skip(4);
+ break;
+ default:
+ warning("Nancy12 ActionZone: unknown type %d - chunk may desync", type);
+ break;
+ }
+}
+
+// Special Effect block: an int16 id, then a byte. If the byte is the 0xff
+// terminator the effect is absent; otherwise it is the first byte of an embedded
+// 21-byte SpecialEffect record (5 int32 + 1 byte).
+void ActionZone::readSpecialEffect(Common::SeekableReadStream &stream) {
+ specialEffectId = stream.readUint16LE();
+ byte b = stream.readByte();
+ if (b == 0xff) {
+ return;
+ }
+
+ stream.seek(-1, SEEK_CUR);
+ hasSpecialEffect = true;
+ for (int i = 0; i < 5; ++i) {
+ specialEffect[i] = stream.readSint32LE();
+ }
+ specialEffectFlag = stream.readByte();
+}
+
+void ActionZone::readOverlayZone(Common::SeekableReadStream &stream) {
+ readFilename(stream, overlayName);
+
+ int16 numSrcRects = stream.readSint16LE();
+ if (numSrcRects > 0) {
+ overlaySrcRects.resize(numSrcRects);
+ for (int i = 0; i < numSrcRects; ++i) {
+ readRect(stream, overlaySrcRects[i]);
+ }
+ }
+
+ readRect(stream, overlayDestRect);
+ stream.skip(4); // int32
+ stream.skip(1); // byte (loop/play mode)
+ stream.skip(4); // int32
+}
+
+void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out) {
+ int16 count = stream.readSint16LE();
+ if (count <= 0) {
+ return;
+ }
+
+ out.resize(count);
+ for (int i = 0; i < count; ++i) {
+ out[i].readData(stream);
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/actionzone.h b/engines/nancy/action/actionzone.h
new file mode 100644
index 00000000000..9d359e15647
--- /dev/null
+++ b/engines/nancy/action/actionzone.h
@@ -0,0 +1,84 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_ACTIONZONE_H
+#define NANCY_ACTION_ACTIONZONE_H
+
+#include "common/array.h"
+#include "common/rect.h"
+#include "common/str.h"
+
+#include "engines/nancy/commontypes.h"
+
+namespace Common {
+class SeekableReadStream;
+}
+
+namespace Nancy {
+namespace Action {
+
+// A polymorphic "ActionZone" record, embedded as a count-prefixed array inside
+// every Nancy12 PuzzleBase puzzle (Driving/Minigolf/MirrorLight/BoardGame/Mind/
+// Chase). The zone's type is the low byte of its leading int32; each subtype
+// reads a different amount of trailing data. readData() consumes exactly the
+// right number of bytes for the type so the surrounding chunk stays in sync.
+struct ActionZone {
+ byte type = 0;
+ int32 typeField = 0; // full leading int32; low byte == type
+
+ Common::Rect rect;
+ Common::String ovlName; // OVL sprite name ("" / sentinel == none)
+
+ int32 pointA = 0; // 8 bytes (a point) at base+0x39
+ int32 pointB = 0;
+ int32 valC = 0; // 8 bytes at base+0x41
+ int32 valD = 0;
+ int16 val49 = 0;
+ byte val4b = 0;
+
+ RandomSoundBlock _sound;
+
+ // Special Effect (an embedded 21-byte SpecialEffect record, present on the
+ // special-effect subtypes when the effect byte is not the 0xff terminator).
+ uint16 specialEffectId = 0;
+ bool hasSpecialEffect = false;
+ int32 specialEffect[5] = {};
+ byte specialEffectFlag = 0;
+
+ // OverlayZone subtypes (0x0d / 0x16)
+ Common::String overlayName;
+ Common::Array<Common::Rect> overlaySrcRects;
+ Common::Rect overlayDestRect;
+
+ void readData(Common::SeekableReadStream &stream);
+
+private:
+ void readSpecialEffect(Common::SeekableReadStream &stream);
+ void readOverlayZone(Common::SeekableReadStream &stream);
+};
+
+// Reads an int16 count, then that many ActionZones.
+void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out);
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_ACTIONZONE_H
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 67dceb62758..26bdc2e47d2 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -53,6 +53,7 @@
#include "engines/nancy/action/puzzle/mazechasepuzzle.h"
#include "engines/nancy/action/puzzle/memorypuzzle.h"
#include "engines/nancy/action/puzzle/mindpuzzle.h"
+#include "engines/nancy/action/puzzle/mirrorlightpuzzle.h"
#include "engines/nancy/action/puzzle/mouselightpuzzle.h"
#include "engines/nancy/action/puzzle/multibuildpuzzle.h"
#include "engines/nancy/action/puzzle/onebuildpuzzle.h"
@@ -465,8 +466,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
// TODO: Nancy12 - new puzzle (reuses the AT_DRIVING_PUZZLE debug string), not implemented
return nullptr;
case 163:
- // TODO: Nancy12 MirrorLightPuzzle (new), not implemented
- return nullptr;
+ return new MirrorLightPuzzle();
case 164:
return new BoardGamePuzzle();
case 165:
diff --git a/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp b/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
new file mode 100644
index 00000000000..25b563b095b
--- /dev/null
+++ b/engines/nancy/action/puzzle/mirrorlightpuzzle.cpp
@@ -0,0 +1,331 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/mirrorlightpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+// TODO - open items for this puzzle:
+// - Win/exit scene progression: the "solved" event flag is set outside the
+// puzzle data (not embedded here) and is still unidentified, so the scene
+// may not advance after a win.
+// - No exit/cancel path: how the player quits an unsolved puzzle is unknown.
+// - Win presentation: only a static lit-bulb overlay is drawn; the original
+// plays a bulb-lights-up animation with a sound cue - neither is wired up.
+// - Detector zone is chosen heuristically (any non-boundary zone away from the
+// source); the true detector zone is unconfirmed.
+// - Mirror rotation step (2 deg/click) and the glowing light-ray rendering are
+// approximations, not the original's exact behaviour.
+
+void MirrorLightPuzzle::readData(Common::SeekableReadStream &stream) {
+ // 65-byte base header.
+ readFilename(stream, _imageName);
+ _beamAngle = stream.readSint16LE();
+ _beamOriginX = stream.readSint32LE();
+ _beamOriginY = stream.readSint32LE();
+ stream.skip(4);
+ _glowRadius = stream.readSint16LE();
+ stream.skip(8); // step size (double)
+ stream.skip(8);
+
+ // Mirror sprite frames - one per evenly-spaced angle around a full turn.
+ uint16 numFrames = stream.readUint16LE();
+ _frameSrcRects.resize(numFrames);
+ for (uint i = 0; i < numFrames; ++i) {
+ readRect(stream, _frameSrcRects[i]);
+ }
+
+ // Mirrors. The original reads every record then ignores any past kMaxMirrors.
+ uint16 numMirrors = stream.readUint16LE();
+ _mirrors.resize(numMirrors);
+ for (uint i = 0; i < numMirrors; ++i) {
+ Mirror &m = _mirrors[i];
+ readRect(stream, m.destRect);
+ m.angle = (double)stream.readSint16LE() * (M_PI / 180.0);
+ stream.skip(16); // secondary rect (unused; empty in the data)
+ m.minAngle = (double)stream.readSint16LE() * (M_PI / 180.0);
+ m.maxAngle = (double)stream.readSint16LE() * (M_PI / 180.0);
+ }
+ if (_mirrors.size() > kMaxMirrors) {
+ _mirrors.resize(kMaxMirrors);
+ }
+
+ readActionZoneArray(stream, _zones);
+}
+
+uint MirrorLightPuzzle::frameForAngle(double angle) const {
+ if (_frameSrcRects.empty()) {
+ return 0;
+ }
+
+ const double twoPi = 2.0 * M_PI;
+ double a = fmod(angle, twoPi);
+ if (a < 0.0) {
+ a += twoPi;
+ }
+
+ uint frame = (uint)((a / twoPi) * _frameSrcRects.size() + 0.5);
+ return frame % _frameSrcRects.size();
+}
+
+void MirrorLightPuzzle::drawMirror(uint index) {
+ const Mirror &m = _mirrors[index];
+ if (m.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Rect &src = _frameSrcRects[frameForAngle(m.angle)];
+ _drawSurface.blitFrom(_image, src, m.destRect);
+}
+
+void MirrorLightPuzzle::traceBeam() {
+ _beamPath.clear();
+ _solved = false;
+
+ // The original marches the beam a fixed step at a time, reflecting off each
+ // mirror it enters. It works in a y-up frame (it negates dy when measuring
+ // angles), so in screen space (y-down) a beam at angle a advances along
+ // (cos a, -sin a): the level's 270deg source points straight down. Each
+ // mirror's stored angle is its surface NORMAL, so reflection is the standard
+ // r = d - 2(d.n)n (verified against the original: the 270deg beam off the
+ // fixed 75deg mirror heads to the top-right mirror).
+ const double kStep = 1.0;
+ const int kMaxSteps = 20000;
+ const byte kZoneBoundary = 0x14;
+
+ double a = (double)_beamAngle * (M_PI / 180.0);
+ double dx = cos(a);
+ double dy = -sin(a);
+
+ double px = _beamOriginX;
+ double py = _beamOriginY;
+ Common::Point origin((int16)px, (int16)py);
+ _beamPath.push_back(origin);
+
+ Common::Rect vp = NancySceneState.getViewport().getBounds();
+ int lastMirror = -1;
+
+ for (int steps = 0; steps < kMaxSteps; ++steps) {
+ px += dx * kStep;
+ py += dy * kStep;
+ Common::Point p((int16)(px + 0.5), (int16)(py + 0.5));
+
+ if (p.x < 0 || p.y < 0 || p.x >= vp.width() || p.y >= vp.height()) {
+ break; // beam left the play area without solving
+ }
+
+ // Stepping clear of the last mirror re-arms it for another bounce.
+ if (lastMirror != -1 && !_mirrors[lastMirror].destRect.contains(p)) {
+ lastMirror = -1;
+ }
+
+ int hit = -1;
+ for (uint i = 0; i < _mirrors.size(); ++i) {
+ if ((int)i != lastMirror && !_mirrors[i].destRect.isEmpty() &&
+ _mirrors[i].destRect.contains(p)) {
+ hit = (int)i;
+ break;
+ }
+ }
+
+ if (hit != -1) {
+ _beamPath.push_back(p);
+ // Reflect about the mirror's surface normal (its stored angle).
+ double nx = cos(_mirrors[hit].angle);
+ double ny = -sin(_mirrors[hit].angle);
+ double dot = dx * nx + dy * ny;
+ dx -= 2.0 * dot * nx;
+ dy -= 2.0 * dot * ny;
+ lastMirror = hit;
+ continue;
+ }
+
+ // Reaching a detector zone (any non-boundary zone away from the source)
+ // solves the puzzle. TODO: confirm which zone is the true detector.
+ for (uint i = 0; i < _zones.size(); ++i) {
+ const ActionZone &z = _zones[i];
+ if (z.type != kZoneBoundary && !z.rect.isEmpty() &&
+ !z.rect.contains(origin) && z.rect.contains(p)) {
+ _solved = true;
+ break;
+ }
+ }
+ if (_solved) {
+ _beamPath.push_back(p);
+ return;
+ }
+ }
+
+ _beamPath.push_back(Common::Point((int16)(px + 0.5), (int16)(py + 0.5)));
+}
+
+void MirrorLightPuzzle::redraw() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ // Mirror sprites first, then the beam on top of them.
+ for (uint i = 0; i < _mirrors.size(); ++i) {
+ drawMirror(i);
+ }
+
+ // The beam as a round radial falloff (dim green-white outer -> bright white
+ // core) to read as glowing light. TODO: the original composites actual
+ // glowing light-ray sprites; a soft/additive glow is a follow-up.
+ if (_beamPath.size() >= 2) {
+ int t = MAX<int>(2, _glowRadius);
+ uint32 colors[3] = {
+ _drawSurface.format.RGBToColor(90, 120, 90), // outer haze
+ _drawSurface.format.RGBToColor(180, 210, 180), // mid glow
+ _drawSurface.format.RGBToColor(255, 255, 255) // core
+ };
+ int bands[3] = { t, t / 2, MAX<int>(1, t / 4) };
+ // Outer band first so brighter bands land on top.
+ for (int band = 0; band < 3; ++band) {
+ int r = bands[band];
+ for (uint i = 1; i < _beamPath.size(); ++i) {
+ const Common::Point &p0 = _beamPath[i - 1];
+ const Common::Point &p1 = _beamPath[i];
+ for (int oy = -r; oy <= r; ++oy) {
+ for (int ox = -r; ox <= r; ++ox) {
+ if (ox * ox + oy * oy > r * r) {
+ continue;
+ }
+ _drawSurface.drawLine(p0.x + ox, p0.y + oy, p1.x + ox, p1.y + oy, colors[band]);
+ }
+ }
+ }
+ }
+ }
+
+ // Once solved, light up the detector overlay (the bulb graphic).
+ if (_solved) {
+ for (uint i = 0; i < _zones.size(); ++i) {
+ const ActionZone &z = _zones[i];
+ if (!z.overlaySrcRects.empty() && !z.overlayDestRect.isEmpty()) {
+ _drawSurface.blitFrom(_image, z.overlaySrcRects[0],
+ Common::Point(z.overlayDestRect.left, z.overlayDestRect.top));
+ }
+ }
+ }
+
+ _needsRedraw = true;
+}
+
+void MirrorLightPuzzle::rotateMirror(uint index, bool clockwise) {
+ Mirror &m = _mirrors[index];
+ if (m.minAngle == m.maxAngle) {
+ return; // fixed mirror
+ }
+
+ // Fine step so the beam can be aimed precisely; clamped to the mirror's
+ // rotation range. The exact per-click amount in the original is unconfirmed.
+ const double step = 2.0 * (M_PI / 180.0);
+ m.angle += clockwise ? step : -step;
+ if (m.angle < m.minAngle) {
+ m.angle = m.minAngle;
+ } else if (m.angle > m.maxAngle) {
+ m.angle = m.maxAngle;
+ }
+
+ traceBeam();
+ redraw();
+}
+
+void MirrorLightPuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(),
+ g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ traceBeam();
+ redraw();
+}
+
+void MirrorLightPuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ registerGraphics();
+ _state = kRun;
+ // fall through
+ case kRun:
+ if (_solved) {
+ // Hold on the lit-bulb win state briefly before releasing.
+ if (_solvedTime == 0) {
+ _solvedTime = g_nancy->getTotalPlayTime();
+ } else if (g_nancy->getTotalPlayTime() - _solvedTime > 1500) {
+ _state = kActionTrigger;
+ }
+ }
+ break;
+ case kActionTrigger:
+ // TODO: the win/exit scene transitions are driven externally (an event
+ // flag the puzzle sets on solve + separate scene ARs), not embedded here.
+ // The solved event flag is still unidentified, so the scene may not
+ // advance yet; for now just release the record.
+ finishExecution();
+ break;
+ }
+}
+
+void MirrorLightPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _solved) {
+ return;
+ }
+
+ for (uint i = 0; i < _mirrors.size(); ++i) {
+ Mirror &m = _mirrors[i];
+ if (m.minAngle == m.maxAngle) {
+ continue; // fixed mirror
+ }
+
+ Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(m.destRect);
+ if (!screenRect.contains(input.mousePos)) {
+ continue;
+ }
+
+ // Clicking the right half rotates one way, the left half the other.
+ bool clockwise = input.mousePos.x >= (screenRect.left + screenRect.right) / 2;
+ g_nancy->_cursor->setCursorType(clockwise ? CursorManager::kRotateRight : CursorManager::kRotateLeft);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ rotateMirror(i, clockwise);
+ }
+ return;
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/mirrorlightpuzzle.h b/engines/nancy/action/puzzle/mirrorlightpuzzle.h
new file mode 100644
index 00000000000..9c935e09dbe
--- /dev/null
+++ b/engines/nancy/action/puzzle/mirrorlightpuzzle.h
@@ -0,0 +1,94 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_MIRRORLIGHTPUZZLE_H
+#define NANCY_ACTION_MIRRORLIGHTPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/action/actionzone.h"
+
+namespace Nancy {
+namespace Action {
+
+// Light-reflection puzzle introduced in Nancy12 (AR 163). The player rotates a
+// set of mirrors within their angle limits to route a beam of light from a
+// source to a target.
+class MirrorLightPuzzle : public RenderActionRecord {
+public:
+ MirrorLightPuzzle() : RenderActionRecord(7) {}
+ virtual ~MirrorLightPuzzle() {}
+
+ void init() override;
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+ void handleInput(NancyInput &input) override;
+
+ bool isViewportRelative() const override { return true; }
+
+ static const uint16 kMaxMirrors = 20;
+
+protected:
+ Common::String getRecordTypeName() const override { return "MirrorLightPuzzle"; }
+
+ struct Mirror {
+ Common::Rect destRect; // where the mirror is drawn / clicked
+ double angle = 0.0; // current angle (radians)
+ double minAngle = 0.0; // rotation limits (min == max == fixed mirror)
+ double maxAngle = 0.0;
+ };
+
+ // File data
+ Common::Path _imageName;
+
+ int16 _beamAngle = 0; // initial beam angle (degrees)
+ int32 _beamOriginX = 0;
+ int32 _beamOriginY = 0;
+ int16 _glowRadius = 0; // beam-glow half-width in pixels
+
+ // Mirror sprite frames - the mirror appearance at each of kNumFrames angles
+ // (full turn split evenly), indexed by angle.
+ Common::Array<Common::Rect> _frameSrcRects;
+
+ Common::Array<Mirror> _mirrors;
+
+ Common::Array<ActionZone> _zones;
+
+ // Runtime state
+ int16 _pickedUpMirror = -1;
+ bool _solved = false;
+ uint32 _solvedTime = 0; // ms timestamp when solved, for the win hold
+ Common::Array<Common::Point> _beamPath; // traced beam polyline, in viewport coords
+
+ Graphics::ManagedSurface _image;
+
+ uint frameForAngle(double angle) const;
+ void drawMirror(uint index);
+ void rotateMirror(uint index, bool clockwise);
+ void traceBeam();
+ void redraw();
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_MIRRORLIGHTPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 09fa6b98a4f..f2cdce3da5c 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -3,6 +3,7 @@ MODULE := engines/nancy
MODULE_OBJS = \
action/actionmanager.o \
action/actionrecord.o \
+ action/actionzone.o \
action/arfactory.o \
action/autotext.o \
action/datarecords.o \
@@ -36,6 +37,7 @@ MODULE_OBJS = \
action/puzzle/matchpuzzle.o \
action/puzzle/memorypuzzle.o \
action/puzzle/mindpuzzle.o \
+ action/puzzle/mirrorlightpuzzle.o \
action/puzzle/mouselightpuzzle.o \
action/puzzle/multibuildpuzzle.o \
action/puzzle/onebuildpuzzle.o \
More information about the Scummvm-git-logs
mailing list