[Scummvm-git-logs] scummvm master -> 36cd91f7a03ded17a5c1a28e28d7029b8dd9fdaf
bluegr
noreply at scummvm.org
Wed Sep 2 22:27:04 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
e7ad614b99 NANCY: Fix an issue in puzzles, when setting their solved flag
0fedf95a84 NANCY: More cleanup of the AR factory code
36cd91f7a0 NANCY: NANCY14: Implement BuildPuzzle
Commit: e7ad614b997826617e34bf543f5e03c636f28615
https://github.com/scummvm/scummvm/commit/e7ad614b997826617e34bf543f5e03c636f28615
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-03T01:26:52+03:00
Commit Message:
NANCY: Fix an issue in puzzles, when setting their solved flag
Puzzles affected are LeverPuzzle, OrderingPuzzle and TurningPuzzle.
This issue was found in Nancy13's piano puzzle, and prevented the
puzzle from being solved. In that scene, setting the solved flag
before changing the scene would invalidate the puzzle's record, so
it never moved to the correct scene upon solving.
This case would only happen if the puzzle record invalidates itself
upon solving, which is why it wasn't caught before.
Changed paths:
engines/nancy/action/puzzle/leverpuzzle.cpp
engines/nancy/action/puzzle/orderingpuzzle.cpp
engines/nancy/action/puzzle/orderingpuzzle.h
engines/nancy/action/puzzle/turningpuzzle.cpp
engines/nancy/action/puzzle/turningpuzzle.h
diff --git a/engines/nancy/action/puzzle/leverpuzzle.cpp b/engines/nancy/action/puzzle/leverpuzzle.cpp
index a8d055c046e..950c95e2d6c 100644
--- a/engines/nancy/action/puzzle/leverpuzzle.cpp
+++ b/engines/nancy/action/puzzle/leverpuzzle.cpp
@@ -113,7 +113,6 @@ void LeverPuzzle::execute() {
}
}
- NancySceneState.setEventFlag(_solveExitScene._flag);
_solveSoundPlayTime = g_nancy->getTotalPlayTime() + _solveSoundDelay * 1000;
_solveState = kPlaySound;
break;
@@ -143,7 +142,10 @@ void LeverPuzzle::execute() {
if (_solveState == kNotSolved) {
_exitScene.execute();
} else {
- NancySceneState.changeScene(_solveExitScene._sceneChange);
+ // The flag is only set here: setting it as soon as the puzzle is solved can
+ // invalidate this record's own dependencies, which stops it from being executed
+ // again before it ever reaches this point.
+ _solveExitScene.execute();
}
finishExecution();
diff --git a/engines/nancy/action/puzzle/orderingpuzzle.cpp b/engines/nancy/action/puzzle/orderingpuzzle.cpp
index 96e094c7425..2531afd394e 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/orderingpuzzle.cpp
@@ -594,11 +594,11 @@ void OrderingPuzzle::execute() {
return;
}
- NancySceneState.setEventFlag(_solveExitScene._flag);
+ _shouldSetSolveFlag = true;
} else {
// Earlier games advance to the success scene regardless; the flag is set only on a solve.
if (solved) {
- NancySceneState.setEventFlag(_solveExitScene._flag);
+ _shouldSetSolveFlag = true;
}
}
} else {
@@ -618,7 +618,7 @@ void OrderingPuzzle::execute() {
}
}
- NancySceneState.setEventFlag(_solveExitScene._flag);
+ _shouldSetSolveFlag = true;
} else {
return;
}
@@ -668,7 +668,7 @@ void OrderingPuzzle::execute() {
break;
}
- NancySceneState.setEventFlag(_solveExitScene._flag);
+ _shouldSetSolveFlag = true;
_currentStage = 0;
_state = kActionTrigger;
break;
@@ -688,6 +688,11 @@ void OrderingPuzzle::execute() {
_deathScene.execute();
} else if (_solveState == kNotSolved) {
_exitScene.execute();
+ } else if (_shouldSetSolveFlag) {
+ // The flag is only set here: setting it as soon as the solution is entered can
+ // invalidate this record's own dependencies, which stops it from being executed
+ // again before it ever reaches this point.
+ _solveExitScene.execute();
} else {
NancySceneState.changeScene(_solveExitScene._sceneChange);
}
diff --git a/engines/nancy/action/puzzle/orderingpuzzle.h b/engines/nancy/action/puzzle/orderingpuzzle.h
index 1c1e957f303..4ea568af399 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.h
+++ b/engines/nancy/action/puzzle/orderingpuzzle.h
@@ -138,6 +138,7 @@ protected:
Common::Array<bool> _secondStateItems;
Time _solveSoundPlayTime;
bool _checkButtonPressed = false;
+ bool _shouldSetSolveFlag = false;
PuzzleType _puzzleType;
};
diff --git a/engines/nancy/action/puzzle/turningpuzzle.cpp b/engines/nancy/action/puzzle/turningpuzzle.cpp
index 178f7b075bd..243b22d0b2c 100644
--- a/engines/nancy/action/puzzle/turningpuzzle.cpp
+++ b/engines/nancy/action/puzzle/turningpuzzle.cpp
@@ -420,7 +420,7 @@ void TurningPuzzle::execute() {
_solveState = kWaitForAnimation;
} else {
_solveState = kWaitForSound;
- NancySceneState.setEventFlag(_solveScene._flag);
+ _shouldSetSolveFlag = true;
}
_objectCurrentlyTurning = -1;
_turnFrameID = 0;
@@ -440,7 +440,7 @@ void TurningPuzzle::execute() {
} else if (g_nancy->getTotalPlayTime() > _solveSoundDelayTime) {
g_nancy->_sound->loadSound(_solveSound);
g_nancy->_sound->playSound(_solveSound);
- NancySceneState.setEventFlag(_solveScene._flag);
+ _shouldSetSolveFlag = true;
_solveState = kWaitForSound;
}
@@ -454,8 +454,11 @@ void TurningPuzzle::execute() {
return;
}
- if (g_nancy->getGameType() >= kGameTypeNancy13) {
- // The solve scene and its event flag both come from the header.
+ // Nancy13 takes the solve scene and its event flag from the header. In every case
+ // the flag is only set here: setting it as soon as the puzzle is solved can
+ // invalidate this record's own dependencies, which stops it from being executed
+ // again before it ever reaches this point.
+ if (g_nancy->getGameType() >= kGameTypeNancy13 || _shouldSetSolveFlag) {
_solveScene.execute();
} else {
NancySceneState.changeScene(_solveScene._sceneChange);
diff --git a/engines/nancy/action/puzzle/turningpuzzle.h b/engines/nancy/action/puzzle/turningpuzzle.h
index 455d0933279..de129897f0b 100644
--- a/engines/nancy/action/puzzle/turningpuzzle.h
+++ b/engines/nancy/action/puzzle/turningpuzzle.h
@@ -132,6 +132,7 @@ protected:
uint32 _solveAnimFace = 0;
SolveState _solveState = kNotSolved;
+ bool _shouldSetSolveFlag = false;
};
} // End of namespace Action
Commit: 0fedf95a84fa029b6d72bc14535c870e6050ca1f
https://github.com/scummvm/scummvm/commit/0fedf95a84fa029b6d72bc14535c870e6050ca1f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-03T01:26:53+03:00
Commit Message:
NANCY: More cleanup of the AR factory code
This helps to streamline it, making it easier to read, and prepare it
for all the upcoming puzzle types in Nancy14 and Nancy15
Changed paths:
engines/nancy/action/arfactory.cpp
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 0be2d009e72..830df3649f4 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -39,8 +39,10 @@
#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/blockingpuzzle.h"
#include "engines/nancy/action/puzzle/blockspuzzle.h"
#include "engines/nancy/action/puzzle/boardgamepuzzle.h"
+//#include "engines/nancy/action/puzzle/buildpuzzle.h"
#include "engines/nancy/action/puzzle/bulpuzzle.h"
#include "engines/nancy/action/puzzle/bombpuzzle.h"
#include "engines/nancy/action/puzzle/cardgamepuzzle.h"
@@ -51,7 +53,9 @@
#include "engines/nancy/action/puzzle/dotconnectpuzzle.h"
#include "engines/nancy/action/puzzle/drivingpuzzle.h"
#include "engines/nancy/action/puzzle/dropsortpuzzle.h"
+//#include "engines/nancy/action/puzzle/escapegridpuzzle.h"
#include "engines/nancy/action/puzzle/gridmappuzzle.h"
+//#include "engines/nancy/action/puzzle/magicboxpuzzle.h"
#include "engines/nancy/action/puzzle/matchpuzzle.h"
#include "engines/nancy/action/puzzle/hamradiopuzzle.h"
#include "engines/nancy/action/puzzle/hangmanpuzzle.h"
@@ -94,6 +98,7 @@
#include "engines/nancy/action/puzzle/turningpuzzle.h"
#include "engines/nancy/action/puzzle/twodialpuzzle.h"
#include "engines/nancy/action/puzzle/typingquizpuzzle.h"
+//#include "engines/nancy/action/puzzle/weightsortpuzzle.h"
#include "engines/nancy/action/puzzle/whalesurvivorpuzzle.h"
#include "engines/nancy/action/puzzle/wordfindpuzzle.h"
@@ -110,18 +115,17 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new Hot1FrSceneChange(CursorManager::kHotspot);
else
- return new SceneChange(); // Moved from 12 in Nancy10
+ return new SceneChange(); // Moved from 12
case 11:
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new HotMultiframeSceneChange(CursorManager::kHotspot);
else
return new Hot1FrSceneChange(CursorManager::kNormal, true, true);
case 12:
- if (g_nancy->getGameType() <= kGameTypeNancy9) {
+ if (g_nancy->getGameType() <= kGameTypeNancy9)
return new SceneChange();
- } else {
+ else
return new HotMultiframeSceneChange(CursorManager::kNormal, true);
- }
case 13:
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new HotMultiframeMultiSceneChange();
@@ -141,14 +145,14 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new HotMultiframeSceneChange(CursorManager::kMoveForward);
else
- return new Hot1FrSceneChange(CursorManager::kMoveLeft); // Moved from 22 in Nancy10
+ return new Hot1FrSceneChange(CursorManager::kMoveLeft); // Moved from 22
case 20:
if (g_nancy->getGameType() == kGameTypeVampire)
return new PaletteThisScene();
else if (g_nancy->getGameType() <= kGameTypeNancy9)
return new HotMultiframeSceneChange(CursorManager::kMoveUp);
else
- return new Hot1FrSceneChange(CursorManager::kMoveRight); // Moved from 23 in Nancy10
+ return new Hot1FrSceneChange(CursorManager::kMoveRight); // Moved from 23
case 21:
if (g_nancy->getGameType() == kGameTypeVampire)
return new PaletteNextScene();
@@ -160,17 +164,17 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new Hot1FrSceneChange(CursorManager::kMoveLeft);
else
- return new HotMultiframeSceneChange(CursorManager::kHotspot); // Moved from 11 in Nancy 10
+ return new HotMultiframeSceneChange(CursorManager::kHotspot); // Moved from 11
case 23:
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new Hot1FrSceneChange(CursorManager::kMoveRight);
else
- return new HotMultiframeSceneChange(CursorManager::kMoveForward); // Moved from 19 in Nancy 10
+ return new HotMultiframeSceneChange(CursorManager::kMoveForward); // Moved from 19
case 24:
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new HotMultiframeMultiSceneCursorTypeSceneChange();
else
- return new HotMultiframeSceneChange(CursorManager::kMoveUp); // Moved from 20 in Nancy 10
+ return new HotMultiframeSceneChange(CursorManager::kMoveUp); // Moved from 20
case 25: {
if (g_nancy->getGameType() <= kGameTypeNancy9) {
// Weird case; instead of storing the cursor id, they instead chose to store
@@ -184,18 +188,18 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
newRec->_isTerse = true;
return newRec;
} else {
- return new HotMultiframeSceneChange(CursorManager::kMoveDown); // Moved from 21 in Nancy 10
+ return new HotMultiframeSceneChange(CursorManager::kMoveDown); // Moved from 21
}
}
case 26:
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new InteractiveVideo();
else
- return new HotMultiframeMultiSceneChange(); // Moved from 13 in Nancy 10
- case 27:
- return new HotMultiframeMultiSceneCursorTypeSceneChange(); // Moved from 24 to 27 in Nancy10
+ return new HotMultiframeMultiSceneChange(); // Moved from 13
+ case 27: // Nancy10
+ return new HotMultiframeMultiSceneCursorTypeSceneChange(); // Moved from 24
case 28: // Nancy10
- return new InteractiveVideo(); // Moved from 26 to 28 in Nancy10
+ return new InteractiveVideo(); // Moved from 26
case 29: // Nancy10
return new ControlUIItems();
case 30: // Nancy11
@@ -219,15 +223,14 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 43: // Nancy14
case 45: // Nancy11
return new PlaySecondaryMovie(PlaySecondaryMovie::kRandomMovie);
- case 44: // Nancy14 (adds a trailing volume byte)
+ case 44: // Nancy14
return new PlaySecondaryMovie(PlaySecondaryMovie::kMovieWithVolume);
case 46: // Nancy11
return new PlayRandomMovieControl();
case 47: // Nancy14
- // A PlaySecondaryMovie subclass that appends a named {value, flag} list
return new PlaySecondaryMovie(PlaySecondaryMovie::kInteractiveMovie);
case 50:
- return new ConversationVideo(); // PlayPrimaryVideoChan0
+ return new ConversationVideo();
case 51:
case 52:
return new PlaySecondaryVideo();
@@ -238,12 +241,12 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new RolloverOverlay();
case 54:
if (g_nancy->getGameType() <= kGameTypeNancy1)
- return new Overlay(Overlay::kStaticAnimation); // PlayStaticBitmapAnimation
+ return new Overlay(Overlay::kStaticAnimation);
else
return new Overlay(Overlay::kInterruptibleAnimation);
case 55:
if (g_nancy->getGameType() <= kGameTypeNancy1)
- return new Overlay(Overlay::kInterruptibleAnimation); // PlayIntStaticBitmapAnimation
+ return new Overlay(Overlay::kInterruptibleAnimation);
else
return new OverlayStaticTerse();
case 56:
@@ -259,19 +262,19 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ConversationCelT();
case 60:
if (g_nancy->getGameType() <= kGameTypeNancy5)
- return new MapCall(); // Only used in tvd and nancy1
+ return new MapCall(); // Only used in TVD and nancy1
else
return new ConversationSoundT();
case 61:
if (g_nancy->getGameType() <= kGameTypeNancy5)
- return new MapCallHot1Fr(); // Only used in tvd and nancy1
+ return new MapCallHot1Fr(); // Only used in TVD and nancy1
else
return new Autotext();
case 62:
if (g_nancy->getGameType() <= kGameTypeNancy7)
- return new MapCallHotMultiframe(); // TVD/nancy1 only
+ return new MapCallHotMultiframe(); // Only used in TVD and nancy1
else
- return new ConversationCelTerse(); // nancy8 and up
+ return new ConversationCelTerse();
case 63:
return new ConversationSoundTerse();
case 65:
@@ -282,7 +285,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy9)
return new TableIndexSetValueHS();
else
- return new Autotext(); // Moved from 61 in Nancy 10
+ return new Autotext(); // Moved from 61
case 68:
if (g_nancy->getGameType() <= kGameTypeNancy11)
return new TextScroll(TextScroll::kTextScroll);
@@ -298,7 +301,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ModifyListEntry(ModifyListEntry::kDelete);
case 73:
return new ModifyListEntry(ModifyListEntry::kMark);
- case 74: // Nancy10 only: writes the full, taskbar-covering box
+ case 74: // Nancy10 only
return new FrameTextBox(FrameTextBox::kFullBox);
case 75:
if (g_nancy->getGameType() <= kGameTypeNancy9)
@@ -316,9 +319,9 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 81: // Nancy11
return new TextBoxWrite(TextBoxWrite::kAutotextWrite);
case 94: // Nancy12
- return new EventFlagsMultiHS(EventFlagsMultiHS::kMultiHS); // moved from 106
+ return new EventFlagsMultiHS(EventFlagsMultiHS::kMultiHS); // Moved from 106
case 95: // Nancy12
- return new EventFlags(EventFlags::kEventFlags); // moved from 107
+ return new EventFlags(EventFlags::kEventFlags); // Moved from 107
case 96: // Nancy11
return new RandomizeEventFlags();
case 97:
@@ -396,48 +399,49 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new ResourceUse();
case 133: // Nancy14
return new CameraAction();
- case 134: // Nancy15 - PlayCharAR
+ case 134: // Nancy15
// Switches the active player character (Nancy / Frank / Joe), the
// dual-protagonist mechanic new to The Creature of Kapu Cave.
// TODO: not yet implemented (depends on the PCUI/LDSN player-char UI)
- return nullptr;
+ // return new PlayCharAR();
+ return nullptr; // TODO
case 140:
if (g_nancy->getGameType() <= kGameTypeNancy11)
- return new SetVolume(); // Legacy SetVolume slot (used up to Nancy8)
+ return new SetVolume(); // Moved to 149 in Nancy9, empty slot in Nancy9-11
else
- return new SetPlayerClock(); // Moved from 170 in Nancy12
+ return new SetPlayerClock(); // Moved from 170
case 141: // Nancy12
- return new MakeScreenFile(); // Moved from 148 in Nancy12
+ return new MakeScreenFile(); // Moved from 148
case 143: // Nancy14
return new ConcatSound();
case 144: // Nancy14
return new MultiSound();
case 145: // Nancy13
- return new PlaySound(); // Moved from 150 in Nancy13
+ return new PlaySound(); // Moved from 150
case 146: // Nancy13
- return new FadeSoundToSilence(); // Moved from 147 in Nancy13
+ return new FadeSoundToSilence(); // Moved from 147
case 147: // Nancy11
if (g_nancy->getGameType() <= kGameTypeNancy12)
return new FadeSoundToSilence();
else
- return new SetVolume(); // Moved from 148 in Nancy13
+ return new SetVolume(); // Moved from 148
case 148: // Nancy11
if (g_nancy->getGameType() <= kGameTypeNancy11)
- return new MakeScreenFile(); // Moved to 141 in Nancy12
+ return new MakeScreenFile();
else if (g_nancy->getGameType() <= kGameTypeNancy12)
- return new SetVolume(); // Moved from 149 in Nancy12
+ return new SetVolume(); // Moved from 149
else
- return new StopSound(); // Nancy13: StopSound moved here (was 154)
- case 149: // Nancy11
+ return new StopSound(); // Moved from 154
+ case 149: // Nancy9
if (g_nancy->getGameType() <= kGameTypeNancy11)
- return new SetVolume(); // Moved from 140 in Nancy9, then to 148 in Nancy12
+ return new SetVolume(); // Moved from 140
else if (g_nancy->getGameType() <= kGameTypeNancy12)
- return new PlaySoundEventFlagTerse(); // Moved from 161 in Nancy12
+ return new PlaySoundEventFlagTerse(); // Moved from 161
else
- return new StopSound(); // Nancy13: StopAndUnloadSound moved here (was 155)
+ return new StopSound(); // Moved from 155
case 150:
if (g_nancy->getGameType() <= kGameTypeNancy13)
- return new PlaySound(); // Moved to 145 in Nancy13
+ return new PlaySound();
else
return new SetMovieVolume();
case 151:
@@ -462,7 +466,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy13)
return new PlaySoundTerse();
else
- return new GridMapPuzzle(); // Moved from 244 in Nancy14
+ return new GridMapPuzzle(); // Moved from 244
case 160:
if (g_nancy->getGameType() <= kGameTypeNancy11)
return new HintSystem();
@@ -483,7 +487,11 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 165:
return new MindPuzzle();
case 166:
- return new OneBuildPuzzle(); // moved from 234 in Nancy12
+ if (g_nancy->getGameType() <= kGameTypeNancy13)
+ return new OneBuildPuzzle(); // Moved from 234
+ else
+ //return new BuildPuzzle();
+ return nullptr; // TODO
case 167:
return new DrivingPuzzle(DrivingPuzzle::kChase);
case 168:
@@ -493,21 +501,21 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new StepObjectsPuzzle();
case 170:
if (g_nancy->getGameType() <= kGameTypeNancy12)
- return new SetPlayerClock(); // Moved to 140 in Nancy12
+ return new SetPlayerClock(); // Moved to 140 in Nancy12, unused slot in Nancy12
else
return new WordFindPuzzle();
case 171:
- return new TurningPuzzle(); // moved from 209 in Nancy13
+ return new TurningPuzzle(); // Moved from 209
case 172:
return new BlocksPuzzle();
case 173:
return new PegsPuzzle();
case 174:
- return new ScalePuzzle(); // balance scale
+ return new ScalePuzzle();
case 175:
- return new PachinkoPuzzle(); // ball drop / pinball
+ return new PachinkoPuzzle();
case 176:
- return new DropSortPuzzle(); // conveyor-belt candy sorting
+ return new DropSortPuzzle();
// -- Nancy14 new puzzles (types 177-182) --
case 177:
return new HangmanPuzzle();
@@ -515,23 +523,23 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new AdjustPuzzle();
case 179:
return new MeterPuzzle();
- case 180: // BlockingPuzzle
- // TODO: not yet implemented
- return nullptr;
+ case 180:
+ //return new BlockingPuzzle();
+ return nullptr; // TODO
case 181:
return new PaintPuzzle();
case 182:
return new DecoderPuzzle();
// -- Nancy15 new puzzles (types 183-185) --
- case 183: // MagicBoxPuzzle
- // TODO: not yet implemented
- return nullptr;
- case 184: // EscapeGridPuzzle
- // TODO: not yet implemented
- return nullptr;
- case 185: // WeightSortPuzzle
- // TODO: not yet implemented
- return nullptr;
+ case 183:
+ //return new MagicBoxPuzzle();
+ return nullptr; // TODO
+ case 184:
+ //return new EscapeGridPuzzle();
+ return nullptr; // TODO
+ case 185:
+ // return new WeightSortPuzzle();
+ return nullptr; // TODO
case 200:
return new SoundEqualizerPuzzle();
case 201:
@@ -601,7 +609,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 233:
return new SoundMatchPuzzle();
case 234:
- return new OneBuildPuzzle(); // moved to 166 in Nancy12
+ return new OneBuildPuzzle();
case 235:
return new MultiBuildPuzzle();
case 237:
Commit: 36cd91f7a03ded17a5c1a28e28d7029b8dd9fdaf
https://github.com/scummvm/scummvm/commit/36cd91f7a03ded17a5c1a28e28d7029b8dd9fdaf
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-09-03T01:26:54+03:00
Commit Message:
NANCY: NANCY14: Implement BuildPuzzle
This effectively replaces OneBuildPuzzle and MultiBuildPuzzle used in
Nancy9 - Nancy13. It's a rewritten version that resembles
MultiBuildPuzzle's capabilities. It's used in four different kinds of
puzzles in Nancy14 - either matching ingredients for a recipe (tea,
parfait, cookie making puzzles), or items on a target (clothes design
puzzle).
Changed paths:
A engines/nancy/action/puzzle/buildpuzzle.cpp
A engines/nancy/action/puzzle/buildpuzzle.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 830df3649f4..191117924d2 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -42,7 +42,7 @@
//#include "engines/nancy/action/puzzle/blockingpuzzle.h"
#include "engines/nancy/action/puzzle/blockspuzzle.h"
#include "engines/nancy/action/puzzle/boardgamepuzzle.h"
-//#include "engines/nancy/action/puzzle/buildpuzzle.h"
+#include "engines/nancy/action/puzzle/buildpuzzle.h"
#include "engines/nancy/action/puzzle/bulpuzzle.h"
#include "engines/nancy/action/puzzle/bombpuzzle.h"
#include "engines/nancy/action/puzzle/cardgamepuzzle.h"
@@ -490,8 +490,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
if (g_nancy->getGameType() <= kGameTypeNancy13)
return new OneBuildPuzzle(); // Moved from 234
else
- //return new BuildPuzzle();
- return nullptr; // TODO
+ return new BuildPuzzle();
case 167:
return new DrivingPuzzle(DrivingPuzzle::kChase);
case 168:
diff --git a/engines/nancy/action/puzzle/buildpuzzle.cpp b/engines/nancy/action/puzzle/buildpuzzle.cpp
new file mode 100644
index 00000000000..cfa8add0054
--- /dev/null
+++ b/engines/nancy/action/puzzle/buildpuzzle.cpp
@@ -0,0 +1,813 @@
+/* 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/cursor.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/enginedata.h"
+#include "engines/nancy/state/scene.h"
+
+#include "engines/nancy/action/puzzle/buildpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+// Reads one of the puzzle's grouped sound blocks into a plain SoundDescription,
+// keeping only the first of the random alternatives.
+static void readSoundBlock(Common::SeekableReadStream &stream, SoundDescription &out) {
+ RandomSoundBlock block;
+ block.readData(stream);
+
+ out.name = block.names.empty() ? "NO SOUND" : block.names[0];
+ out.channelID = block.channel;
+ out.numLoops = block.numLoops;
+ out.volume = block.volume;
+}
+
+void BuildPuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName);
+ readFilename(stream, _altImageName);
+
+ _trayImageMode = stream.readByte();
+ stream.skip(3); // 0x46: the difficulty gate's flag
+ _requiredPlaced = stream.readUint16LE();
+ _usePlacedGate = stream.readByte();
+ _stateItemID = stream.readUint16LE();
+ stream.skip(32); // 0x4e: two overlay rects
+ SoundDescription unused;
+ readSoundBlock(stream, unused);
+ stream.skip(32); // 0x6e: two more overlay rects
+ readSoundBlock(stream, unused);
+ stream.skip(32); // 0x8e: the "done" overlay gate and its rect
+
+ readFilename(stream, _anim1Name);
+ readRect(stream, _anim1Rect);
+ readFilename(stream, _anim2Name);
+ readRect(stream, _anim2Rect);
+ _pieceCursorType = stream.readSint16LE();
+ _heldPieceCursorType = stream.readSint16LE();
+ stream.skip(3); // 0xe4: a third cursor type and a flag
+
+ uint16 numZones = stream.readUint16LE();
+ _zones.resize(numZones);
+ for (uint i = 0; i < numZones; ++i) {
+ Zone &zone = _zones[i];
+ readRect(stream, zone.hotspot);
+ zone.capacity = stream.readUint16LE();
+ stream.skip(1);
+ zone.fill = (ZoneFill)stream.readByte();
+ zone.marksPlaced = stream.readByte();
+
+ uint16 numIngredients = stream.readUint16LE();
+ zone.ingredients.resize(numIngredients);
+ for (uint j = 0; j < numIngredients; ++j) {
+ Ingredient &ingredient = zone.ingredients[j];
+ ingredient.pieceID = stream.readSint16LE();
+ ingredient.quantity = stream.readByte();
+ ingredient.mode = stream.readByte();
+ }
+ }
+
+ uint16 numHolds = stream.readUint16LE();
+ _holds.resize(numHolds);
+ for (uint i = 0; i < numHolds; ++i) {
+ HoldSlot &hold = _holds[i];
+ readRect(stream, hold.srcRect);
+ readRect(stream, hold.destRect);
+ readRect(stream, hold.fillSrcRect1);
+ readRect(stream, hold.fillSrcRect2);
+ hold.amount = stream.readByte();
+ }
+
+ // Only present when the puzzle has hold slots.
+ if (numHolds > 0)
+ readSoundBlock(stream, _holdSound);
+
+ uint16 numPieces = stream.readUint16LE();
+ _pieces.resize(numPieces);
+ for (uint i = 0; i < numPieces; ++i) {
+ Piece &piece = _pieces[i];
+ readRect(stream, piece.srcRect);
+ readRect(stream, piece.destRect);
+ readRect(stream, piece.dragSrcRect);
+ readRect(stream, piece.placedSrcRect);
+ readRect(stream, piece.closeupSrcRect);
+ readRect(stream, piece.closeupDestRect);
+ readRect(stream, piece.placedDestRect);
+
+ piece.kind = stream.readByte();
+ if (piece.kind == 3)
+ readFilename(stream, piece.imageName);
+ else
+ piece.zoneID = stream.readSint16LE();
+
+ piece.itemID = stream.readSint16LE();
+
+ // The list names the scoops this piece can be taken with, and the last
+ // entry picks which of a scoop's two full images to show while carrying it.
+ int16 numValues = stream.readSint16LE();
+ if (numValues > 0) {
+ piece.holds.resize(numValues - 1);
+ for (int16 j = 0; j < numValues - 1; ++j) {
+ piece.holds[j] = stream.readSint16LE();
+ }
+
+ piece.fillVariant = stream.readSint16LE();
+ }
+ }
+
+ _counterItemID = stream.readByte();
+ if (_counterItemID != 255) {
+ for (uint i = 0; i < kNumDigits; ++i)
+ readRect(stream, _digitSrcRects[i]);
+
+ _counterPos.x = (int16)stream.readSint32LE();
+ _counterPos.y = (int16)stream.readSint32LE();
+ _counterSpacing = stream.readSint32LE();
+ }
+
+ readSoundBlock(stream, _pickupSound);
+ readSoundBlock(stream, _dropSound);
+ readSoundBlock(stream, _notebookSound);
+ readSoundBlock(stream, _resetSound);
+
+ _wrongIngredientFlag = stream.readSint16LE();
+ _solvedFlag = stream.readSint16LE();
+ _solveScene.sceneID = stream.readUint16LE();
+ _solveScene.frameID = stream.readUint16LE();
+ _solveScene.continueSceneSound = kContinueSceneSound;
+ _solveFlag.label = stream.readSint16LE();
+ _solveFlag.flag = stream.readByte();
+
+ readSoundBlock(stream, unused);
+
+ _failScene.sceneID = stream.readUint16LE();
+ _failScene.frameID = stream.readUint16LE();
+ _failScene.continueSceneSound = kContinueSceneSound;
+ _failFlag.label = stream.readSint16LE();
+ _failFlag.flag = stream.readByte();
+
+ readSoundBlock(stream, unused);
+
+ // The count-prefixed 23-byte hotspot records shared by the later puzzles.
+ int16 numExitZones = stream.readSint16LE();
+ for (int16 i = 0; i < numExitZones; ++i) {
+ Common::Rect zone;
+ readRect(stream, zone);
+ uint16 cursorType = stream.readUint16LE();
+ uint16 sceneID = stream.readUint16LE();
+ int16 flagLabel = stream.readSint16LE();
+ byte flagValue = stream.readByte();
+
+ if (i == 0) {
+ _exitHotspot = zone;
+ _exitCursorType = cursorType;
+ _exitScene.sceneID = sceneID;
+ _exitScene.frameID = 0;
+ _exitScene.continueSceneSound = kContinueSceneSound;
+ _exitFlag.label = flagLabel;
+ _exitFlag.flag = flagValue;
+ }
+ }
+}
+
+void BuildPuzzle::setFlagOnChange(int16 label, bool value, int8 &last) {
+ if (label == -1 || last == (int8)value) {
+ return;
+ }
+
+ last = (int8)value;
+ NancySceneState.setEventFlag(label, value ? g_nancy->_true : g_nancy->_false);
+}
+
+void BuildPuzzle::setPieceCursor(bool isHeld) {
+ int16 cursorType = isHeld && _heldPieceCursorType != 0 ? _heldPieceCursorType : _pieceCursorType;
+
+ if (cursorType != 0) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)cursorType, true, true);
+ } else {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ }
+}
+
+void BuildPuzzle::init() {
+ const uint32 transColor = g_nancy->_graphics->getTransColor();
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(transColor);
+
+ // A puzzle without a second image draws everything from the first one.
+ g_nancy->_resource->loadImage(_altImageName.empty() ? _imageName : _altImageName, _altImage);
+ _altImage.setTransparentColor(transColor);
+
+ _numDefined = _pieces.size();
+
+ // Every piece but a kind 1 is copied when it is dropped, so the array needs
+ // room for as many copies as the recipes can ask for. It is grown once, here,
+ // because the pieces are render objects and must not move afterwards.
+ uint numSpare = 0;
+ for (uint i = 0; i < _zones.size(); ++i) {
+ for (uint j = 0; j < _zones[i].ingredients.size(); ++j) {
+ numSpare += _zones[i].ingredients[j].quantity;
+ }
+ }
+
+ _pieces.resize(_numDefined + numSpare);
+
+ for (uint i = 0; i < _zones.size(); ++i) {
+ _zones[i].counts.resize(_zones[i].ingredients.size());
+ }
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ Piece &piece = _pieces[i];
+
+ // A piece's art defaults down the chain when a rect is left empty.
+ if (piece.dragSrcRect.isEmpty()) {
+ piece.dragSrcRect = piece.srcRect;
+ }
+
+ if (piece.placedSrcRect.isEmpty()) {
+ piece.placedSrcRect = piece.dragSrcRect;
+ }
+
+ piece.inUse = (i < _numDefined);
+ piece.sourceID = (int16)i;
+ piece.liveRect = piece.destRect;
+ piece.setZ(_z + (uint16)i + 1);
+ updatePieceRender((int16)i);
+ }
+
+ for (uint i = 0; i < _holds.size(); ++i) {
+ HoldSlot &hold = _holds[i];
+ if (hold.srcRect.isEmpty()) {
+ continue;
+ }
+
+ hold._drawSurface.create(_altImage, hold.srcRect);
+ hold.setTransparent(true);
+ hold.moveTo(hold.destRect);
+ hold.setZ(_z + (uint16)i + 1);
+ hold.setVisible(true);
+ }
+
+ _cursorItem.setTransparent(true);
+ _cursorItem.setVisible(false);
+
+ _isInitialized = true;
+}
+
+void BuildPuzzle::registerGraphics() {
+ if (!_isInitialized) {
+ return;
+ }
+
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ _pieces[i].registerGraphics();
+ }
+
+ for (uint i = 0; i < _holds.size(); ++i) {
+ _holds[i].registerGraphics();
+ }
+
+ _cursorItem.registerGraphics();
+}
+
+// The graphics manager keeps its object list sorted as objects are inserted, so
+// a new z only takes effect once the piece is registered again.
+byte BuildPuzzle::carriedAmount() const {
+ return _activeHold != -1 ? MAX<byte>(_holds[_activeHold].amount, 1) : 1;
+}
+
+void BuildPuzzle::updateCursorItem(const Common::Point &mouseVP) {
+ // A scoop stays on the cursor while an ingredient is picked up with it, so
+ // the scoop's own art wins over the ingredient's.
+ const Common::Rect *src = nullptr;
+
+ if (_activeHold != -1) {
+ const HoldSlot &hold = _holds[_activeHold];
+
+ // A scoop carrying an ingredient shows itself full.
+ if (_heldPiece != -1) {
+ src = _pieces[_heldPiece].fillVariant == 0 ? &hold.fillSrcRect1 : &hold.fillSrcRect2;
+ }
+
+ if (!src || src->isEmpty()) {
+ src = &hold.srcRect;
+ }
+ } else if (_heldPiece != -1) {
+ src = &_pieces[_heldPiece].dragSrcRect;
+ }
+
+ if (!src || src->isEmpty()) {
+ _cursorItem.setVisible(false);
+ return;
+ }
+
+ int width = src->width();
+ int height = src->height();
+ Common::Rect dest((int16)(mouseVP.x - width / 2), (int16)(mouseVP.y - height / 2),
+ (int16)(mouseVP.x - width / 2 + width), (int16)(mouseVP.y - height / 2 + height));
+
+ _cursorItem._drawSurface.create(_altImage, *src);
+ _cursorItem.setTransparent(true);
+ _cursorItem.moveTo(dest);
+ _cursorItem.setVisible(true);
+ _cursorItem.registerGraphics();
+}
+
+void BuildPuzzle::setPieceZ(int16 pieceIdx, uint16 z) {
+ _pieces[pieceIdx].setZ(z);
+ _pieces[pieceIdx].registerGraphics();
+}
+
+void BuildPuzzle::updatePieceRender(int16 pieceIdx) {
+ Piece &piece = _pieces[pieceIdx];
+
+ if (!piece.inUse || piece.liveRect.isEmpty()) {
+ piece.setVisible(false);
+ return;
+ }
+
+ // Each of the three states has its own art, and only a piece resting at home
+ // is drawn from the image the puzzle selects; the other two always come from
+ // the alt one.
+ // A carried piece lives on the cursor instead of on the board.
+ if (pieceIdx == _heldPiece) {
+ piece.setVisible(false);
+ return;
+ }
+
+ bool isPlaced = piece.assignedZone != -1;
+
+ const Common::Rect *src = &piece.srcRect;
+ Graphics::ManagedSurface *surf = _trayImageMode == 1 ? &_image : &_altImage;
+
+ if (isPlaced) {
+ src = &piece.placedSrcRect;
+ surf = &_altImage;
+ }
+
+ if (src->isEmpty()) {
+ piece.setVisible(false);
+ return;
+ }
+
+ piece._drawSurface.create(*surf, *src);
+ piece.setTransparent(true);
+ piece.moveTo(piece.liveRect);
+ piece.setVisible(true);
+}
+
+int16 BuildPuzzle::clonePiece(int16 pieceIdx) {
+ for (uint i = _numDefined; i < _pieces.size(); ++i) {
+ if (_pieces[i].inUse) {
+ continue;
+ }
+
+ Piece &clone = _pieces[i];
+ const Piece &original = _pieces[pieceIdx];
+
+ clone.srcRect = original.srcRect;
+ clone.destRect = original.destRect;
+ clone.dragSrcRect = original.dragSrcRect;
+ clone.placedSrcRect = original.placedSrcRect;
+ clone.closeupSrcRect = original.closeupSrcRect;
+ clone.closeupDestRect = original.closeupDestRect;
+ clone.placedDestRect = original.placedDestRect;
+ clone.kind = original.kind;
+ clone.zoneID = original.zoneID;
+ clone.holds = original.holds;
+ clone.fillVariant = original.fillVariant;
+ clone.liveRect = original.liveRect;
+ clone.sourceID = original.sourceID;
+ clone.assignedZone = -1;
+ clone.inUse = true;
+
+ return (int16)i;
+ }
+
+ return -1;
+}
+
+void BuildPuzzle::adjustZone(int16 zoneIdx, int16 pieceID, int8 delta) {
+ Zone &zone = _zones[zoneIdx];
+
+ for (uint i = 0; i < zone.ingredients.size(); ++i) {
+ if (zone.ingredients[i].pieceID != pieceID) {
+ continue;
+ }
+
+ zone.counts[i] = (byte)(zone.counts[i] + delta);
+ zone.numHeld += delta;
+ return;
+ }
+
+ // Nothing in the recipe wanted this piece.
+ zone.numWrong += delta;
+ zone.numHeld += delta;
+
+ int16 totalWrong = 0;
+ for (uint i = 0; i < _zones.size(); ++i) {
+ totalWrong += _zones[i].numWrong;
+ }
+
+ setFlagOnChange(_wrongIngredientFlag, totalWrong > 0, _lastWrongFlag);
+}
+
+bool BuildPuzzle::checkSolved() const {
+ for (uint i = 0; i < _zones.size(); ++i) {
+ const Zone &zone = _zones[i];
+
+ if (zone.numWrong != 0) {
+ return false;
+ }
+
+ for (uint j = 0; j < zone.ingredients.size(); ++j) {
+ const Ingredient &ingredient = zone.ingredients[j];
+
+ if (ingredient.mode == 2) {
+ if (zone.counts[j] < ingredient.quantity) {
+ return false;
+ }
+ } else if (ingredient.mode == 0) {
+ if (zone.counts[j] != ingredient.quantity) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+void BuildPuzzle::openCloseup(int16 pieceIdx) {
+ _closeupPiece = pieceIdx;
+
+ Piece &piece = _pieces[pieceIdx];
+ Common::Rect dest = piece.closeupDestRect;
+
+ // An empty destination means the close-up is centered in the viewport.
+ if (dest.isEmpty()) {
+ const VIEW *viewData = GetEngineData(VIEW);
+ if (viewData) {
+ int width = piece.closeupSrcRect.width();
+ int height = piece.closeupSrcRect.height();
+ int left = (viewData->screenPosition.width() - width) / 2;
+ int top = (viewData->screenPosition.height() - height) / 2;
+ dest = Common::Rect((int16)left, (int16)top, (int16)(left + width), (int16)(top + height));
+ }
+ }
+
+ // A piece carrying its own filename keeps its close-up art in that image.
+ Graphics::ManagedSurface *surf = _trayImageMode == 1 ? &_image : &_altImage;
+ if (!piece.imageName.empty()) {
+ if (_pieceImageName != Common::Path(piece.imageName)) {
+ _pieceImageName = Common::Path(piece.imageName);
+ g_nancy->_resource->loadImage(_pieceImageName, _pieceImage);
+ _pieceImage.setTransparentColor(g_nancy->_graphics->getTransColor());
+ }
+
+ surf = &_pieceImage;
+ }
+
+ piece._drawSurface.create(*surf, piece.closeupSrcRect);
+ piece.setTransparent(true);
+ piece.moveTo(dest);
+ piece.setVisible(true);
+ setPieceZ(pieceIdx, (uint16)(_z + _pieces.size() + 2));
+}
+
+void BuildPuzzle::closeCloseup() {
+ if (_closeupPiece == -1) {
+ return;
+ }
+
+ int16 pieceIdx = _closeupPiece;
+ _closeupPiece = -1;
+ setPieceZ(pieceIdx, (uint16)(_z + pieceIdx + 1));
+ updatePieceRender(pieceIdx);
+}
+
+void BuildPuzzle::pickUpPiece(int16 pieceIdx) {
+ Piece &piece = _pieces[pieceIdx];
+
+ // Taking a piece back out of a zone undoes its contribution. A copy only
+ // exists while it is in a zone, so it goes away rather than onto the cursor.
+ if (piece.assignedZone != -1) {
+ adjustZone(piece.assignedZone, piece.sourceID, -1);
+ piece.assignedZone = -1;
+
+ if (pieceIdx >= (int16)_numDefined) {
+ piece.inUse = false;
+ piece.setVisible(false);
+ return;
+ }
+ }
+
+ _heldPiece = pieceIdx;
+ setPieceZ(pieceIdx, (uint16)(_z + _pieces.size() + 1));
+
+ g_nancy->_sound->loadSound(_pickupSound);
+ g_nancy->_sound->playSound(_pickupSound);
+ updatePieceRender(pieceIdx);
+}
+
+void BuildPuzzle::returnPiece(int16 pieceIdx) {
+ Piece &piece = _pieces[pieceIdx];
+ piece.liveRect = piece.destRect;
+ piece.assignedZone = -1;
+ _heldPiece = -1;
+ setPieceZ(pieceIdx, (uint16)(_z + pieceIdx + 1));
+ updatePieceRender(pieceIdx);
+}
+
+void BuildPuzzle::placePiece(int16 pieceIdx, int16 zoneIdx, const Common::Point &dropPos) {
+ Zone &zone = _zones[zoneIdx];
+ int16 placedIdx = pieceIdx;
+
+ // A zone with a fill mode shows what went into it, so it needs something to
+ // keep: the piece itself when its kind is consumed, otherwise a copy, which
+ // leaves the original on the shelf to be used again.
+ if (zone.fill != kFillAbsorb) {
+ if (_pieces[pieceIdx].kind != kConsumedKind) {
+ placedIdx = clonePiece(pieceIdx);
+ returnPiece(pieceIdx);
+
+ if (placedIdx == -1) {
+ return;
+ }
+ }
+
+ _pieces[placedIdx].assignedZone = zoneIdx;
+ }
+
+ Piece &piece = _pieces[placedIdx];
+ int width = piece.placedSrcRect.width();
+ int height = piece.placedSrcRect.height();
+
+ if (!piece.placedDestRect.isEmpty()) {
+ // The piece names its own spot, whatever the zone would have done.
+ piece.liveRect = piece.placedDestRect;
+ } else {
+ switch (zone.fill) {
+ case kFillCentered: {
+ int left = dropPos.x - width / 2;
+ int top = dropPos.y - height / 2;
+ piece.liveRect = Common::Rect((int16)left, (int16)top,
+ (int16)(left + width), (int16)(top + height));
+ break;
+ }
+ case kFillTopLeft:
+ // Left aligned to the zone, sitting on its bottom edge.
+ piece.liveRect = Common::Rect(zone.hotspot.left, (int16)(zone.hotspot.bottom - height),
+ (int16)(zone.hotspot.left + width), zone.hotspot.bottom);
+ break;
+ default:
+ // Absorbed: a consumed piece is gone, anything else goes back home.
+ piece.liveRect = piece.kind == kConsumedKind ? Common::Rect() : piece.destRect;
+ break;
+ }
+ }
+
+ adjustZone(zoneIdx, piece.sourceID, (int8)carriedAmount());
+
+ g_nancy->_sound->loadSound(_dropSound);
+ g_nancy->_sound->playSound(_dropSound);
+
+ _heldPiece = -1;
+ setPieceZ(placedIdx, (uint16)(_z + placedIdx + 1));
+ updatePieceRender(placedIdx);
+
+ bool solved = checkSolved();
+ setFlagOnChange(_solvedFlag, solved, _lastSolvedFlag);
+
+ if (solved) {
+ _isSolved = true;
+ _state = kActionTrigger;
+ }
+}
+
+void BuildPuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ registerGraphics();
+ _state = kRun;
+ break;
+ case kRun:
+ break;
+ case kActionTrigger:
+ if (_isSolved) {
+ NancySceneState.setEventFlag(_solveFlag);
+ NancySceneState.changeScene(_solveScene);
+ } else {
+ NancySceneState.setEventFlag(_exitFlag);
+ NancySceneState.changeScene(_exitScene);
+ }
+
+ finishExecution();
+ break;
+ }
+}
+
+void BuildPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _isSolved) {
+ return;
+ }
+
+ const VIEW *viewData = GetEngineData(VIEW);
+ if (!viewData || !viewData->screenPosition.contains(input.mousePos)) {
+ return;
+ }
+
+ Common::Point mouseVP(input.mousePos.x - viewData->screenPosition.left,
+ input.mousePos.y - viewData->screenPosition.top);
+ bool clicked = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
+
+ updateCursorItem(mouseVP);
+
+ // A close-up covers the board; clicking it takes the piece, except for a
+ // piece that is only ever there to be looked at.
+ if (_closeupPiece != -1) {
+ setPieceCursor(false);
+
+ if (clicked) {
+ int16 pieceIdx = _closeupPiece;
+ closeCloseup();
+
+ if (_pieces[pieceIdx].kind != 3) {
+ pickUpPiece(pieceIdx);
+ }
+ }
+
+ return;
+ }
+
+ // Carrying an ingredient: release it over a zone, or anywhere else to send
+ // it home.
+ if (_heldPiece != -1) {
+ setPieceCursor(true);
+
+ if (clicked) {
+ // Stacked zones overlap, so the release only says whether the drop is
+ // over them at all; which one it lands in is the first with room, so a
+ // glass fills from the bottom rather than leaving a gap under a layer.
+ int16 target = -1;
+ int16 over = -1;
+
+ for (uint i = 0; i < _zones.size(); ++i) {
+ const Piece &held = _pieces[_heldPiece];
+
+ // A piece can be restricted to a single zone.
+ if (held.zoneID != -1 && held.zoneID != (int16)i) {
+ continue;
+ }
+
+ if (over == -1 && _zones[i].hotspot.contains(mouseVP)) {
+ over = (int16)i;
+ }
+
+ if (target == -1 && (_zones[i].capacity == 0 || _zones[i].numHeld < _zones[i].capacity)) {
+ target = (int16)i;
+ }
+ }
+
+ if (over == -1) {
+ target = -1;
+ } else if (target == -1) {
+ target = over;
+ }
+
+ if (target != -1) {
+ placePiece(_heldPiece, target, mouseVP);
+
+ // The scoop is emptied by the drop and goes back to its place.
+ if (_activeHold != -1) {
+ _holds[_activeHold].setVisible(true);
+ _holds[_activeHold].registerGraphics();
+ _activeHold = -1;
+ }
+ } else {
+ returnPiece(_heldPiece);
+ }
+
+ updateCursorItem(mouseVP);
+ }
+
+ return;
+ }
+
+ // Topmost piece under the cursor.
+ int16 hovered = -1;
+ for (uint i = 0; i < _pieces.size(); ++i) {
+ const Piece &piece = _pieces[i];
+ if (!piece.inUse || !piece.liveRect.contains(mouseVP)) {
+ continue;
+ }
+
+ if (hovered == -1 || piece.getZOrder() > _pieces[hovered].getZOrder()) {
+ hovered = (int16)i;
+ }
+ }
+
+ if (hovered != -1) {
+ const Piece &piece = _pieces[hovered];
+
+ // An ingredient that has no art of its own is scooped rather than carried,
+ // and a piece that names its scoops can only be taken with one of those.
+ bool needsScoop = piece.dragSrcRect.isEmpty() || !piece.holds.empty();
+ bool scoopFits = _activeHold != -1 &&
+ (piece.holds.empty() ||
+ Common::find(piece.holds.begin(), piece.holds.end(), _activeHold) != piece.holds.end());
+
+ if (!piece.closeupSrcRect.isEmpty() || !needsScoop || scoopFits) {
+ setPieceCursor(false);
+
+ if (clicked) {
+ if (!piece.closeupSrcRect.isEmpty()) {
+ openCloseup(hovered);
+ } else {
+ pickUpPiece(hovered);
+ updateCursorItem(mouseVP);
+ }
+ }
+
+ return;
+ }
+ }
+
+ // The scoops themselves: one click takes it, another puts it back.
+ for (uint i = 0; i < _holds.size(); ++i) {
+ if (!_holds[i].destRect.contains(mouseVP)) {
+ continue;
+ }
+
+ setPieceCursor(false);
+
+ if (clicked) {
+ if (_activeHold == (int16)i) {
+ _holds[i].setVisible(true);
+ _holds[i].registerGraphics();
+ _activeHold = -1;
+ } else {
+ if (_activeHold != -1) {
+ _holds[_activeHold].setVisible(true);
+ _holds[_activeHold].registerGraphics();
+ }
+
+ _activeHold = (int16)i;
+ _holds[i].setVisible(false);
+ }
+
+ g_nancy->_sound->loadSound(_pickupSound);
+ g_nancy->_sound->playSound(_pickupSound);
+ updateCursorItem(mouseVP);
+ }
+
+ return;
+ }
+
+ if (_exitHotspot.isEmpty()) {
+ return;
+ }
+
+ if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
+ if (_exitCursorType != 0) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitCursorType, true, true);
+ } else {
+ g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+ }
+
+ if (clicked) {
+ _state = kActionTrigger;
+ }
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/buildpuzzle.h b/engines/nancy/action/puzzle/buildpuzzle.h
new file mode 100644
index 00000000000..fafabdf034d
--- /dev/null
+++ b/engines/nancy/action/puzzle/buildpuzzle.h
@@ -0,0 +1,230 @@
+/* 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_BUILDPUZZLE_H
+#define NANCY_ACTION_BUILDPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+namespace Action {
+
+// Nancy 14 reuses AR 166 for a rebuilt assembly puzzle (tea, cookies, parfait,
+// clothes design). Pieces are dragged into zones, and a zone is satisfied once
+// it holds the quantities its ingredient list asks for; a piece is placed by
+// being assigned a zone index rather than by matching a rect.
+class BuildPuzzle : public RenderActionRecord {
+public:
+ BuildPuzzle() : RenderActionRecord(7), _cursorItem(99) {}
+ virtual ~BuildPuzzle() {}
+
+ void init() override;
+ void registerGraphics() 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 "BuildPuzzle"; }
+
+ static const uint kNumDigits = 10;
+
+ // One entry of a zone's required contents.
+ struct Ingredient {
+ int16 pieceID = 0;
+ byte quantity = 0;
+ byte mode = 0;
+ };
+
+ // How a zone arranges a piece dropped into it. A zone with no fill mode just
+ // absorbs the ingredient: nothing new is drawn and the piece goes back home.
+ enum ZoneFill {
+ kFillAbsorb = 0,
+ kFillCentered = 1,
+ kFillTopLeft = 2
+ };
+
+ static const byte kConsumedKind = 1; // this kind is taken by the zone, not copied
+
+ // A container pieces are dropped into.
+ struct Zone {
+ Common::Rect hotspot;
+ uint16 capacity = 0; // the zone counts as full at this many pieces
+ ZoneFill fill = kFillAbsorb;
+ byte marksPlaced = 0;
+ Common::Array<Ingredient> ingredients;
+
+ // Runtime
+ Common::Array<byte> counts; // how many of each ingredient are in the zone
+ int16 numWrong = 0; // pieces in here that no ingredient asked for
+ int16 numHeld = 0;
+ };
+
+ // A measuring implement. Scooping with one adds `amount` of an ingredient at
+ // a time instead of a single unit.
+ struct HoldSlot : RenderObject {
+ HoldSlot() : RenderObject(0) {}
+
+ void setZ(uint16 z) { _z = z; _needsRedraw = true; }
+ bool isViewportRelative() const override { return true; }
+
+ Common::Rect srcRect; // empty
+ Common::Rect destRect;
+ Common::Rect fillSrcRect1; // holding an ingredient, by its fillVariant
+ Common::Rect fillSrcRect2;
+ byte amount = 0; // how much one scoop is worth
+ };
+
+ struct Piece : RenderObject {
+ Piece() : RenderObject(0) {}
+
+ void setZ(uint16 z) { _z = z; _needsRedraw = true; }
+ bool isViewportRelative() const override { return true; }
+
+ Common::Rect srcRect; // art while the piece sits at home
+ Common::Rect destRect; // where the piece is drawn
+ Common::Rect dragSrcRect; // art while the piece is on the cursor; empty means srcRect
+ Common::Rect placedSrcRect; // art once the piece is in a zone; empty means dragSrcRect
+ Common::Rect closeupSrcRect;
+ Common::Rect closeupDestRect; // empty means centered on the viewport
+ Common::Rect placedDestRect; // exact spot in a zone; overrides the zone's fill mode
+ byte kind = 0;
+ Common::String imageName; // only when kind == 3
+ int16 zoneID = -1; // the only zone this piece may go in, -1 = any
+ int16 itemID = 0; // index into the shared item state, 255 = none
+ Common::Array<int16> holds; // the scoops this piece can be taken with, empty = by hand
+ int16 fillVariant = 0; // which of a scoop's two full images to show
+
+ // Runtime
+ Common::Rect liveRect; // where the piece currently sits
+ int16 sourceID = -1; // index of the definition this piece was cloned from
+ int16 assignedZone = -1;
+ bool inUse = false; // false for the spare slots kept for clones
+ };
+
+ Common::Path _imageName;
+ Common::Path _altImageName; // empty means the main image is used for both
+
+ // Selects where a piece that is not in a zone, and any close-up, is drawn
+ // from: 1 means the main image, anything else the alt one. A piece sitting
+ // in a zone always comes from the alt image.
+ byte _trayImageMode = 0;
+
+ // Two animations played over the puzzle, each with the rect it plays in.
+ Common::Path _anim1Name;
+ Common::Rect _anim1Rect;
+ Common::Path _anim2Name;
+ Common::Rect _anim2Rect;
+
+ // Shown while hovering a piece, and while carrying one.
+ int16 _pieceCursorType = 0;
+ int16 _heldPieceCursorType = 0;
+
+ Common::Array<Zone> _zones;
+ Common::Array<HoldSlot> _holds;
+ Common::Array<Piece> _pieces;
+
+ // Index into the shared item state whose value is drawn as a running count.
+ // 255 means the puzzle has no counter.
+ byte _counterItemID = 255;
+ Common::Rect _digitSrcRects[kNumDigits];
+ Common::Point _counterPos;
+ int32 _counterSpacing = 0;
+
+ SoundDescription _pickupSound;
+ SoundDescription _dropSound;
+ SoundDescription _notebookSound;
+ SoundDescription _resetSound;
+ SoundDescription _holdSound;
+
+ // Both cleared when the puzzle starts from scratch.
+ int16 _wrongIngredientFlag = -1; // set once something not in a recipe is dropped in
+ int16 _solvedFlag = -1;
+
+ // When _usePlacedGate is set, the scene only changes once this many pieces
+ // have been placed.
+ uint16 _requiredPlaced = 0;
+ byte _usePlacedGate = 0;
+ uint16 _stateItemID = 255; // shared item state tracking the placed count
+
+ SceneChangeDescription _solveScene;
+ FlagDescription _solveFlag;
+
+ // Used instead of _solveScene when the player leaves the zones unfinished.
+ SceneChangeDescription _failScene;
+ FlagDescription _failFlag;
+
+ // --- Runtime ---
+
+ Graphics::ManagedSurface _image;
+ Graphics::ManagedSurface _altImage;
+ Graphics::ManagedSurface _pieceImage; // a kind 3 piece's own close-up art
+ Common::Path _pieceImageName;
+
+ // Whatever is currently on the cursor: a scoop, or an ingredient.
+ RenderObject _cursorItem;
+ int16 _activeHold = -1;
+ int8 _lastWrongFlag = -1;
+ int8 _lastSolvedFlag = -1;
+ int16 _heldPiece = -1;
+ int16 _closeupPiece = -1;
+ uint16 _numDefined = 0; // pieces read from the record; the rest are spare slots
+ bool _isSolved = false;
+ bool _leaveRequested = false;
+ bool _isInitialized = false;
+
+ void setPieceCursor(bool isHeld);
+ void setPieceZ(int16 pieceIdx, uint16 z);
+ // Draw the carried art at the cursor, or hide it when nothing is carried
+ void updateCursorItem(const Common::Point &mouseVP);
+ // The scoop a piece is dropped with, 1 when it is carried by hand
+ byte carriedAmount() const;
+ // Writing an event flag re-triggers whatever reacts to it, so both of the
+ // puzzle's flags are only written when their value actually changes.
+ void setFlagOnChange(int16 label, bool value, int8 &last);
+ // Show a piece's close-up, or dismiss the one that is showing
+ void openCloseup(int16 pieceIdx);
+ void closeCloseup();
+ // Attach a piece to the cursor, and release it over a zone or back home
+ void pickUpPiece(int16 pieceIdx);
+ void placePiece(int16 pieceIdx, int16 zoneIdx, const Common::Point &dropPos);
+ void returnPiece(int16 pieceIdx);
+ // Add or remove a piece from a zone's tallies
+ void adjustZone(int16 zoneIdx, int16 pieceID, int8 delta);
+ // The spare slot a clone goes into, or -1 when the puzzle has run out
+ int16 clonePiece(int16 pieceIdx);
+ void updatePieceRender(int16 pieceIdx);
+ bool checkSolved() const;
+
+ SceneChangeDescription _exitScene;
+ FlagDescription _exitFlag;
+ Common::Rect _exitHotspot;
+ uint16 _exitCursorType = 0;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_BUILDPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 8e3fc2335c0..e0060def98c 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -25,6 +25,7 @@ MODULE_OBJS = \
action/puzzle/beadpuzzle.o \
action/puzzle/blockspuzzle.o \
action/puzzle/boardgamepuzzle.o \
+ action/puzzle/buildpuzzle.o \
action/puzzle/bulpuzzle.o \
action/puzzle/bombpuzzle.o \
action/puzzle/cardgamepuzzle.o \
More information about the Scummvm-git-logs
mailing list