[Scummvm-git-logs] scummvm master -> ced3d127744bc288383f183324527c343e6c3aca
bluegr
noreply at scummvm.org
Mon Mar 16 12:45:42 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ced3d12774 NANCY: Implement Quiz Puzzle for Nancy 8 and Nancy 9+
Commit: ced3d127744bc288383f183324527c343e6c3aca
https://github.com/scummvm/scummvm/commit/ced3d127744bc288383f183324527c343e6c3aca
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-16T14:43:08+02:00
Commit Message:
NANCY: Implement Quiz Puzzle for Nancy 8 and Nancy 9+
Changed paths:
engines/nancy/action/puzzle/quizpuzzle.cpp
engines/nancy/action/puzzle/quizpuzzle.h
diff --git a/engines/nancy/action/puzzle/quizpuzzle.cpp b/engines/nancy/action/puzzle/quizpuzzle.cpp
index 7e5a014c98d..c3f2c12bf27 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.cpp
+++ b/engines/nancy/action/puzzle/quizpuzzle.cpp
@@ -21,75 +21,662 @@
#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/quizpuzzle.h"
+#include "engines/nancy/puzzledata.h"
namespace Nancy {
namespace Action {
+QuizPuzzle::~QuizPuzzle() {
+ g_nancy->_input->setVKEnabled(false);
+ g_nancy->_sound->stopSound(_correctSound);
+ g_nancy->_sound->stopSound(_wrongSound);
+ g_nancy->_sound->stopSound(_doneSound);
+ g_nancy->_sound->stopSound(_activeBoxSound);
+}
+
void QuizPuzzle::init() {
- // TODO
+ Common::Rect screenClip = NancySceneState.getViewport().getBounds();
+ _screenPosition = screenClip;
+ _drawSurface.create(screenClip.width(), screenClip.height(), g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+
+ g_nancy->_input->setVKEnabled(true);
+ RenderObject::init();
}
-void QuizPuzzle::execute() {
- // TODO
-
- if (g_nancy->getGameType() == kGameTypeNancy8) {
- warning("STUB - Nancy 8 Quiz Puzzle");
- } else if (g_nancy->getGameType() == kGameTypeNancy9) {
- const uint16 sceneId = NancySceneState.getSceneInfo().sceneID;
- if (sceneId == 6450) {
- warning("STUB - Nancy 9 Quiz Puzzle - Holt Scotto's quiz, page 1");
- // Set the puzzle event flags to flag it as done
- NancySceneState.setEventFlag(59, g_nancy->_true); // EV_Answered_SQ_Q06
- } else if (sceneId == 6451) {
- warning("STUB - Nancy 9 Quiz Puzzle - Holt Scotto's quiz, page 2");
- // Set the puzzle event flags to flag it as done
- NancySceneState.setEventFlag(61, g_nancy->_true); // EV_Answered_SQ_Q09
- } else if (sceneId == 6342) {
- warning("STUB - Nancy 9 Quiz Puzzle - GPS new waypoint");
- // Set the GPS waypoint as discovered
- NancySceneState.setEventFlag(410, g_nancy->_true); // EV_Solved_GPS_Beach
- } else if (sceneId == 6344) {
- warning("STUB - Nancy 9 Quiz Puzzle - GPS new waypoint - cache A");
- // Set the GPS waypoint as discovered
- NancySceneState.setEventFlag(411, g_nancy->_true); // EV_Solved_GPS_CacheA
- } else if (sceneId == 6345) {
- warning("STUB - Nancy 9 Quiz Puzzle - GPS new waypoint - cache B");
- // Set the GPS waypoint as discovered
- NancySceneState.setEventFlag(412, g_nancy->_true); // EV_Solved_GPS_CacheB
- } else if (sceneId == 6431) {
- warning("STUB - Nancy 9 Quiz Puzzle - Hilda Swenson's letter");
- NancySceneState.setEventFlag(179, g_nancy->_true); // EV_Hilda_Said_Objects
- } else if (sceneId == 6443) {
- warning("STUB - Nancy 9 Quiz Puzzle - Holt Scotto's chess problem");
- NancySceneState.setEventFlag(119, g_nancy->_true); // EV_Finished_Chess_Quiz
- } else if (sceneId == 4184) {
- warning("STUB - Nancy 9 Quiz Puzzle - Lighthouse Morse code");
- SceneChangeDescription scene;
- scene.sceneID = 4190;
- NancySceneState.resetStateToInit();
- NancySceneState.changeScene(scene);
- } else {
- warning("STUB - Nancy 9 Quiz Puzzle");
+// ---- Nancy 8 data format ----
+// Offset Size Field
+// 0x000 2 fontID
+// 0x002 2 blinkInterval
+// 0x004 2 numBoxes
+// 0x006 80 5 box rects (4Ãsint32 each)
+// 0x056 310 5Ã(3Ã20-byte answers + int16 flag)
+// 0x18C 49 correctSound (readNormal)
+// 0x1BD 30 correct subtitle (skip)
+// 0x1DB 49 wrongSound (readNormal)
+// 0x20C 30 wrong subtitle (skip)
+// 0x22A 25 solveScene
+// 0x243 49 doneSound (readNormal)
+// 0x274 30 done subtitle (skip)
+// 0x292 25 cancelScene
+void QuizPuzzle::readDataOld(Common::SeekableReadStream &stream) {
+ _fontID = stream.readUint16LE();
+ _cursorBlinkInterval = stream.readUint16LE();
+ _numBoxes = stream.readUint16LE();
+
+ for (int i = 0; i < 5; ++i) {
+ readRect(stream, _boxRects[i]);
+ }
+
+ char answerBuf[20];
+ for (int i = 0; i < 5; ++i) {
+ for (int j = 0; j < 3; ++j) {
+ stream.read(answerBuf, 20);
+ answerBuf[19] = '\0';
+ _answers[i][j] = answerBuf;
}
+ _answerFlags[i] = stream.readSint16LE();
}
- _isDone = true;
+ _correctSound.readNormal(stream);
+ stream.skip(30); // correct subtitle
+
+ _wrongSound.readNormal(stream);
+ stream.skip(30); // wrong subtitle
+
+ _solveScene.readData(stream);
+ _doneSound.readNormal(stream);
+ stream.skip(30); // done subtitle
+
+ _cancelScene.readData(stream);
+}
+
+// ---- Nancy 9+ data format ----
+// Header (0xB2 bytes):
+// 0x00 2 fontID
+// 0x02 2 blinkInterval
+// 0x04 1 cursor char
+// 0x05 20 allowed chars (null-terminated)
+// 0x19 25 solveScene
+// 0x32 49 doneSound (readNormal)
+// 0x63 30 done subtitle (skip)
+// 0x81 25 cancelScene
+// 0x9A 16 unknown (skip)
+// 0xAA 2 correctSoundChannel
+// 0xAC 2 wrongSoundChannel
+// 0xAE 1 skipEmptyOnEnter flag
+// 0xAF 1 autoCheck flag (0 = ON)
+// 0xB0 2 numBoxes
+// Per-box (0xD0 bytes):
+// +0x00 16 box rect (4Ãsint32)
+// +0x10 60 3Ã20-byte answers
+// +0x4C 2 event flag (int16)
+// +0x4E 33 correct sound name
+// +0x6F 2 correct sound volume
+// +0x71 30 correct subtitle (skip)
+// +0x8F 33 wrong sound name
+// +0xB0 2 wrong sound volume
+// +0xB2 30 wrong subtitle (skip)
+void QuizPuzzle::readDataNew(Common::SeekableReadStream &stream) {
+ _fontID = stream.readUint16LE();
+ _cursorBlinkInterval = stream.readUint16LE();
+ _cursorChar = stream.readByte();
+
+ char allowedBuf[20];
+ stream.read(allowedBuf, 20);
+ allowedBuf[19] = '\0';
+ _allowedChars = allowedBuf;
+
+ _solveScene.readData(stream);
+ _doneSound.readNormal(stream);
+ stream.skip(30); // done subtitle
+
+ _cancelScene.readData(stream);
+ stream.skip(16); // unknown
+
+ _correctSoundChannel = stream.readUint16LE();
+ _wrongSoundChannel = stream.readUint16LE();
+ _skipEmptyOnEnter = (stream.readByte() != 0);
+ _autoCheck = (stream.readByte() == 0); // 0 = auto-check ON
+ _numBoxes = stream.readUint16LE();
+
+ if (_numBoxes > (uint16)kMaxBoxes) {
+ warning("QuizPuzzle: numBoxes %d exceeds maximum %d, clamping", _numBoxes, kMaxBoxes);
+ _numBoxes = kMaxBoxes;
+ }
+
+ char soundNameBuf[33];
+ char answerBuf[20];
+ for (int i = 0; i < _numBoxes; ++i) {
+ readRect(stream, _boxRects[i]);
+
+ for (int j = 0; j < 3; ++j) {
+ stream.read(answerBuf, 20);
+ answerBuf[19] = '\0';
+ _answers[i][j] = answerBuf;
+ }
+
+ _answerFlags[i] = stream.readSint16LE();
+
+ stream.read(soundNameBuf, 33);
+ soundNameBuf[32] = '\0';
+ _boxCorrectSoundName[i] = soundNameBuf;
+ _boxCorrectSoundVolume[i] = stream.readUint16LE();
+ stream.skip(30); // correct subtitle
+
+ stream.read(soundNameBuf, 33);
+ soundNameBuf[32] = '\0';
+ _boxWrongSoundName[i] = soundNameBuf;
+ _boxWrongSoundVolume[i] = stream.readUint16LE();
+ stream.skip(30); // wrong subtitle
+
+ // Precompute max answer length for auto-check mode
+ _boxMaxLen[i] = 0;
+ for (int j = 0; j < 3; ++j) {
+ uint16 len = (uint16)_answers[i][j].size();
+ if (len > _boxMaxLen[i])
+ _boxMaxLen[i] = len;
+ }
+ }
}
void QuizPuzzle::readData(Common::SeekableReadStream &stream) {
- // TODO
- stream.skip(stream.size() - stream.pos());
+ if (g_nancy->getGameType() == kGameTypeNancy8)
+ readDataOld(stream);
+ else
+ readDataNew(stream);
+}
+
+// ---- Nancy 8 state machine ----
+void QuizPuzzle::executeOld() {
+ switch (_internalState) {
+ case kTyping: {
+ if (_hasNewKey) {
+ _hasNewKey = false;
+
+ Common::String &text = _typedText[_currentBox];
+ bool hadCursor = !text.empty() && text.lastChar() == '-';
+ if (hadCursor)
+ text.deleteLastChar();
+
+ if (_pendingBackspace) {
+ if (!text.empty())
+ text.deleteLastChar();
+ drawText();
+ _pendingBackspace = false;
+ } else if (_pendingReturn) {
+ if (text.empty()) {
+ if (hadCursor) text += '-';
+ advanceToNextBox();
+ } else {
+ if (hadCursor) text += '-';
+ _internalState = kCheckAnswer;
+ }
+ _pendingReturn = false;
+ } else if (_pendingChar != 0) {
+ if (text.size() < 16)
+ text += _pendingChar;
+ drawText();
+ _pendingChar = 0;
+ }
+ }
+
+ // Cursor blink: toggle trailing '-'
+ Time now = g_nancy->getTotalPlayTime();
+ if (now >= _nextBlinkTime) {
+ _nextBlinkTime = now + _cursorBlinkInterval;
+ Common::String &text = _typedText[_currentBox];
+ if (!text.empty() && text.lastChar() == '-')
+ text.deleteLastChar();
+ else
+ text += '-';
+ drawText();
+ }
+ break;
+ }
+
+ case kCheckAnswer: {
+ bool correct = checkAnswerForCurrentBox();
+ if (correct) {
+ advanceToNextBox();
+ _internalState = kStartCorrect;
+ } else {
+ _internalState = kStartWrong;
+ }
+ break;
+ }
+
+ case kStartCorrect: {
+ if (_correctSound.name == "NO SOUND") {
+ _solved = checkAllSolved();
+ _internalState = _solved ? kStartDone : kTyping;
+ } else {
+ g_nancy->_sound->playSound(_correctSound);
+ _internalState = kWaitCorrect;
+ }
+ _nextBlinkTime = 0;
+ drawText();
+ break;
+ }
+
+ case kWaitCorrect:
+ if (!g_nancy->_sound->isSoundPlaying(_correctSound)) {
+ g_nancy->_sound->stopSound(_correctSound);
+ _solved = checkAllSolved();
+ _internalState = _solved ? kStartDone : kTyping;
+ _nextBlinkTime = 0;
+ drawText();
+ }
+ break;
+
+ case kStartWrong: {
+ _typedText[_currentBox].clear();
+ drawText();
+
+ if (_wrongSound.name == "NO SOUND") {
+ _internalState = kTyping;
+ } else {
+ g_nancy->_sound->playSound(_wrongSound);
+ _internalState = kWaitWrong;
+ }
+ break;
+ }
+
+ case kWaitWrong:
+ if (!g_nancy->_sound->isSoundPlaying(_wrongSound)) {
+ g_nancy->_sound->stopSound(_wrongSound);
+ _internalState = kTyping;
+ _nextBlinkTime = 0;
+ }
+ break;
+
+ case kStartDone: {
+ if (_doneSound.name == "NO SOUND") {
+ _internalState = kFinish;
+ } else {
+ g_nancy->_sound->playSound(_doneSound);
+ _internalState = kWaitDone;
+ }
+ break;
+ }
+
+ case kWaitDone:
+ if (!g_nancy->_sound->isSoundPlaying(_doneSound)) {
+ g_nancy->_sound->stopSound(_doneSound);
+ _internalState = kFinish;
+ }
+ break;
+
+ case kFinish:
+ _state = kActionTrigger;
+ break;
+ }
+}
+
+// ---- Nancy 9+ state machine ----
+void QuizPuzzle::executeNew() {
+ switch (_internalState) {
+ case kTyping: {
+ if (_hasNewKey) {
+ _hasNewKey = false;
+
+ Common::String &text = _typedText[_currentBox];
+ bool hadCursor = !text.empty() && text.lastChar() == _cursorChar;
+ if (hadCursor)
+ text.deleteLastChar();
+
+ if (_pendingBackspace) {
+ if (!text.empty())
+ text.deleteLastChar();
+ drawText();
+ _pendingBackspace = false;
+ } else if (_pendingReturn) {
+ if (text.empty()) {
+ if (hadCursor) text += _cursorChar;
+ if (!_skipEmptyOnEnter)
+ advanceToNextBox();
+ } else {
+ if (hadCursor) text += _cursorChar;
+ _internalState = kCheckAnswer;
+ }
+ _pendingReturn = false;
+ } else if (_pendingChar != 0) {
+ bool charAllowed = _allowedChars.empty();
+ if (!charAllowed) {
+ for (uint ci = 0; ci < _allowedChars.size(); ++ci) {
+ if (_allowedChars[ci] == _pendingChar) {
+ charAllowed = true;
+ break;
+ }
+ }
+ }
+ if (charAllowed && text.size() < 16) {
+ text += _pendingChar;
+
+ if (_autoCheck) {
+ bool correct = checkAnswerForCurrentBox();
+ if (correct) {
+ _internalState = kStartCorrect;
+ } else if ((uint16)text.size() > _boxMaxLen[_currentBox]) {
+ _internalState = kStartWrong;
+ }
+ }
+ }
+ drawText();
+ _pendingChar = 0;
+ }
+ }
+
+ // Cursor blink (only while still typing)
+ if (_internalState == kTyping && !checkAllSolved()) {
+ Time now = g_nancy->getTotalPlayTime();
+ if (now >= _nextBlinkTime) {
+ _nextBlinkTime = now + _cursorBlinkInterval;
+ Common::String &text = _typedText[_currentBox];
+ if (!text.empty() && text.lastChar() == _cursorChar)
+ text.deleteLastChar();
+ else
+ text += _cursorChar;
+ drawText();
+ }
+ }
+ break;
+ }
+
+ case kCheckAnswer: {
+ bool correct = checkAnswerForCurrentBox();
+ _internalState = correct ? kStartCorrect : kStartWrong;
+ break;
+ }
+
+ case kStartCorrect: {
+ _activeBoxSound.name = _boxCorrectSoundName[_currentBox];
+ _activeBoxSound.channelID = _correctSoundChannel;
+ _activeBoxSound.volume = _boxCorrectSoundVolume[_currentBox];
+ _activeBoxSound.playCommands = 1;
+ _activeBoxSound.numLoops = 1;
+
+ if (_activeBoxSound.name == "NO SOUND") {
+ advanceToNextBox();
+ _solved = checkAllSolved();
+ _internalState = _solved ? kStartDone : kTyping;
+ } else {
+ g_nancy->_sound->loadSound(_activeBoxSound);
+ g_nancy->_sound->playSound(_activeBoxSound);
+ advanceToNextBox();
+ _internalState = kWaitCorrect;
+ }
+ _nextBlinkTime = 0;
+ drawText();
+ break;
+ }
+
+ case kWaitCorrect:
+ if (!g_nancy->_sound->isSoundPlaying(_activeBoxSound)) {
+ g_nancy->_sound->stopSound(_activeBoxSound);
+ _solved = checkAllSolved();
+ _internalState = _solved ? kStartDone : kTyping;
+ _nextBlinkTime = 0;
+ drawText();
+ }
+ break;
+
+ case kStartWrong: {
+ _typedText[_currentBox].clear();
+ drawText();
+
+ _activeBoxSound.name = _boxWrongSoundName[_currentBox];
+ _activeBoxSound.channelID = _wrongSoundChannel;
+ _activeBoxSound.volume = _boxWrongSoundVolume[_currentBox];
+ _activeBoxSound.playCommands = 1;
+ _activeBoxSound.numLoops = 1;
+
+ if (_activeBoxSound.name == "NO SOUND") {
+ _internalState = kTyping;
+ } else {
+ g_nancy->_sound->loadSound(_activeBoxSound);
+ g_nancy->_sound->playSound(_activeBoxSound);
+ _internalState = kWaitWrong;
+ }
+ break;
+ }
+
+ case kWaitWrong:
+ if (!g_nancy->_sound->isSoundPlaying(_activeBoxSound)) {
+ g_nancy->_sound->stopSound(_activeBoxSound);
+ _internalState = kTyping;
+ _nextBlinkTime = 0;
+ }
+ break;
+
+ case kStartDone: {
+ if (_doneSound.name == "NO SOUND") {
+ _internalState = kFinish;
+ } else {
+ g_nancy->_sound->playSound(_doneSound);
+ _internalState = kWaitDone;
+ }
+ break;
+ }
+
+ case kWaitDone:
+ if (!g_nancy->_sound->isSoundPlaying(_doneSound)) {
+ g_nancy->_sound->stopSound(_doneSound);
+ _internalState = kFinish;
+ }
+ break;
+
+ case kFinish:
+ _state = kActionTrigger;
+ break;
+ }
+}
+
+void QuizPuzzle::execute() {
+ switch (_state) {
+ case kBegin: {
+ init();
+ registerGraphics();
+ _nextBlinkTime = g_nancy->getTotalPlayTime() + _cursorBlinkInterval;
+ if (g_nancy->getGameType() == kGameTypeNancy8) {
+ g_nancy->_sound->loadSound(_correctSound);
+ g_nancy->_sound->loadSound(_wrongSound);
+ }
+ g_nancy->_sound->loadSound(_doneSound);
+
+ // Restore previously correct boxes from persistent puzzle data
+ QuizPuzzleData *qpd = (QuizPuzzleData *)NancySceneState.getPuzzleData(QuizPuzzleData::getTag());
+ if (qpd) {
+ uint16 key = _solveScene._sceneChange.sceneID;
+ if (qpd->boxCorrect.contains(key)) {
+ const auto &bc = qpd->boxCorrect[key];
+ const auto &tt = qpd->typedText[key];
+ for (uint i = 0; i < _numBoxes && i < bc.size(); ++i) {
+ if (bc[i]) {
+ _boxCorrect[i] = true;
+ _typedText[i] = tt[i];
+ }
+ }
+ }
+ }
+ // Start cursor on first unsolved box
+ for (uint i = 0; i < _numBoxes; ++i) {
+ if (!_boxCorrect[i]) {
+ _currentBox = i;
+ break;
+ }
+ }
+ drawText();
+
+ _state = kRun;
+ // fall through
+ }
+ case kRun:
+ if (g_nancy->getGameType() == kGameTypeNancy8)
+ executeOld();
+ else
+ executeNew();
+ break;
+
+ case kActionTrigger:
+ if (_cancelled)
+ _cancelScene.execute();
+ else if (_solved)
+ _solveScene.execute();
+
+ g_nancy->_input->setVKEnabled(false);
+ finishExecution();
+ break;
+ }
}
void QuizPuzzle::handleInput(NancyInput &input) {
- // TODO
+ if (_internalState != kTyping)
+ return;
+
+ char cursorChar = (g_nancy->getGameType() == kGameTypeNancy8) ? '-' : _cursorChar;
+
+ // Mouse click: select a different (unsolved) box
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ for (uint i = 0; i < _numBoxes; ++i) {
+ if (_boxCorrect[i])
+ continue;
+ Common::Rect screenRect = NancySceneState.getViewport().convertViewportToScreen(_boxRects[i]);
+ if (screenRect.contains(input.mousePos)) {
+ if (i != _currentBox) {
+ Common::String &oldText = _typedText[_currentBox];
+ if (!oldText.empty() && oldText.lastChar() == cursorChar)
+ oldText.deleteLastChar();
+ _currentBox = i;
+ _nextBlinkTime = 0;
+ drawText();
+ }
+ break;
+ }
+ }
+ }
+
+ for (auto &key : input.otherKbdInput) {
+ if (key.keycode == Common::KEYCODE_BACKSPACE) {
+ _pendingBackspace = true;
+ _hasNewKey = true;
+ } else if (key.keycode == Common::KEYCODE_TAB) {
+ // Tab: advance to next unsolved box without submitting
+ Common::String &oldText = _typedText[_currentBox];
+ if (!oldText.empty() && oldText.lastChar() == cursorChar)
+ oldText.deleteLastChar();
+ advanceToNextBox();
+ _nextBlinkTime = 0;
+ drawText();
+ } else if (key.keycode == Common::KEYCODE_RETURN ||
+ key.keycode == Common::KEYCODE_KP_ENTER) {
+ _pendingReturn = true;
+ _hasNewKey = true;
+ } else if (key.ascii != 0 && key.ascii != (byte)cursorChar) {
+ bool accept = false;
+ if (g_nancy->getGameType() == kGameTypeNancy8) {
+ accept = Common::isAlnum(key.ascii) || key.ascii == '.' || key.ascii == '-' ||
+ key.ascii == '\'' || Common::isSpace(key.ascii);
+ } else {
+ accept = key.ascii >= 0x20 && key.ascii < 0x7f;
+ }
+ if (accept) {
+ _pendingChar = key.ascii;
+ _hasNewKey = true;
+ }
+ }
+ }
+}
+
+void QuizPuzzle::onPause(bool paused) {
+ g_nancy->_input->setVKEnabled(!paused);
+ RenderActionRecord::onPause(paused);
+}
+
+void QuizPuzzle::drawText() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ const Graphics::Font *font = g_nancy->_graphics->getFont(_fontID);
+ if (!font)
+ return;
+
+ for (uint i = 0; i < _numBoxes; ++i) {
+ const Common::String &text = _typedText[i];
+ if (text.empty())
+ continue;
+
+ Common::Rect bounds = _boxRects[i];
+ bounds = NancySceneState.getViewport().convertViewportToScreen(bounds);
+ bounds = convertToLocal(bounds);
+
+ int y = bounds.bottom + 1 - font->getFontHeight();
+ font->drawString(&_drawSurface, text, bounds.left, y, bounds.width(), 0);
+ }
+
+ _needsRedraw = true;
+}
+
+void QuizPuzzle::advanceToNextBox() {
+ for (uint n = 0; n < _numBoxes; ++n) {
+ _currentBox = (_currentBox + 1) % _numBoxes;
+ if (!_boxCorrect[_currentBox])
+ return;
+ }
+}
+
+bool QuizPuzzle::checkAllSolved() const {
+ for (uint i = 0; i < _numBoxes; ++i) {
+ if (!_boxCorrect[i])
+ return false;
+ }
+ return true;
+}
+
+bool QuizPuzzle::checkAnswerForCurrentBox() {
+ Common::String input = _typedText[_currentBox];
+ char cursorChar = (g_nancy->getGameType() == kGameTypeNancy8) ? '-' : _cursorChar;
+ if (!input.empty() && input.lastChar() == cursorChar)
+ input.deleteLastChar();
+
+ bool correct = false;
+ for (int j = 0; j < 3; ++j) {
+ if (!_answers[_currentBox][j].empty() &&
+ input.equalsIgnoreCase(_answers[_currentBox][j])) {
+ correct = true;
+ break;
+ }
+ }
+
+ if (correct) {
+ // Strip cursor from actual buffer (matches original game state-1 behaviour)
+ if (!_typedText[_currentBox].empty() && _typedText[_currentBox].lastChar() == cursorChar)
+ _typedText[_currentBox].deleteLastChar();
+ _boxCorrect[_currentBox] = true;
+ if (_answerFlags[_currentBox] != -1)
+ NancySceneState.setEventFlag(_answerFlags[_currentBox], g_nancy->_true);
+
+ // Persist so the answered box survives scene re-entry
+ QuizPuzzleData *qpd = (QuizPuzzleData *)NancySceneState.getPuzzleData(QuizPuzzleData::getTag());
+ if (qpd) {
+ uint16 key = _solveScene._sceneChange.sceneID;
+ auto &bc = qpd->boxCorrect[key];
+ auto &tt = qpd->typedText[key];
+ if (bc.size() < _numBoxes) {
+ bc.resize(_numBoxes, false);
+ tt.resize(_numBoxes);
+ }
+ bc[_currentBox] = true;
+ tt[_currentBox] = _typedText[_currentBox];
+ }
+ }
+ return correct;
}
} // End of namespace Action
diff --git a/engines/nancy/action/puzzle/quizpuzzle.h b/engines/nancy/action/puzzle/quizpuzzle.h
index c70aaab2b1a..1277fa766ef 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.h
+++ b/engines/nancy/action/puzzle/quizpuzzle.h
@@ -23,25 +23,114 @@
#define NANCY_ACTION_QUIZPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
namespace Nancy {
namespace Action {
-// Stenography tutorial in Nancy 8
-
+// Text-entry quiz with multiple text boxes.
+// Different implementation for Nancy 8 vs Nancy 9+ (which has more
+// features and a different data format)
class QuizPuzzle : public RenderActionRecord {
public:
QuizPuzzle() : RenderActionRecord(7) {}
- virtual ~QuizPuzzle() {}
+ virtual ~QuizPuzzle();
void init() override;
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
void handleInput(NancyInput &input) override;
+ void onPause(bool paused) override;
protected:
Common::String getRecordTypeName() const override { return "QuizPuzzle"; }
+
+private:
+ static const int kMaxBoxes = 8;
+
+ // Format-specific read/execute implementations
+ void readDataOld(Common::SeekableReadStream &stream); // Nancy 8
+ void readDataNew(Common::SeekableReadStream &stream); // Nancy 9+
+ void executeOld(); // Nancy 8 state machine
+ void executeNew(); // Nancy 9+ state machine
+
+ // Helpers (shared by Nancy 8 and Nancy 9)
+ void drawText();
+ void advanceToNextBox();
+ bool checkAllSolved() const;
+ bool checkAnswerForCurrentBox(); // checks, marks correct, sets event flag
+
+ // ---- Data (Nancy 8) ----
+ uint16 _fontID = 0;
+ uint16 _cursorBlinkInterval = 500;
+ uint16 _numBoxes = 0;
+
+ // Text-box screen rects (viewport-relative), up to kMaxBoxes stored
+ Common::Rect _boxRects[kMaxBoxes];
+
+ // Per-box answer data: up to 3 valid answers (case-insensitive match), plus
+ // an optional event flag to set when that box is answered correctly.
+ Common::String _answers[kMaxBoxes][3];
+ int16 _answerFlags[kMaxBoxes] = { -1, -1, -1, -1, -1, -1, -1, -1 };
+
+ SoundDescription _correctSound; // Nancy 8: global correct sound
+ SoundDescription _wrongSound; // Nancy 8: global wrong sound
+ SoundDescription _doneSound; // done sound (both Nancy 8 and Nancy 9)
+
+ SceneChangeWithFlag _solveScene; // scene to go to when all boxes are solved
+ SceneChangeWithFlag _cancelScene; // scene to go to on cancel
+
+ // ---- Data (Nancy 9+) ----
+ char _cursorChar = '-'; // cursor character (configurable in Nancy 9)
+ Common::String _allowedChars; // allowed typing chars (empty = all allowed)
+ bool _autoCheck = false; // check answer after each char typed
+ bool _skipEmptyOnEnter = false; // if true, Enter on empty box does nothing
+
+ uint16 _correctSoundChannel = 0; // global channel for per-box correct sounds
+ uint16 _wrongSoundChannel = 0; // global channel for per-box wrong sounds
+
+ // Per-box sounds for Nancy 9 (name + volume; channel from global above)
+ Common::String _boxCorrectSoundName[kMaxBoxes];
+ uint16 _boxCorrectSoundVolume[kMaxBoxes] = {};
+ Common::String _boxWrongSoundName[kMaxBoxes];
+ uint16 _boxWrongSoundVolume[kMaxBoxes] = {};
+
+ // Per-box max answer length (computed from answer strings, used in auto-check mode)
+ uint16 _boxMaxLen[kMaxBoxes] = {};
+
+ // ---- Runtime state (shared) ----
+ Common::String _typedText[kMaxBoxes]; // current text in each box (may end with cursor char)
+ bool _boxCorrect[kMaxBoxes] = {}; // true when box i has been answered correctly
+ uint16 _currentBox = 0; // which box currently receives keyboard input
+ bool _solved = false; // all boxes answered correctly
+ bool _cancelled = false; // user cancelled
+
+ enum SolveState {
+ kTyping = 0, // waiting for key input; cursor blinks
+ kCheckAnswer = 1, // Enter pressed (or auto-check triggered); evaluate typed text
+ kStartCorrect = 2, // answer correct: play correct sound, advance box
+ kWaitCorrect = 3, // waiting for correct sound to finish
+ kStartWrong = 4, // answer wrong: clear text, play wrong sound
+ kWaitWrong = 5, // waiting for wrong sound to finish
+ kStartDone = 6, // all boxes solved: play done sound
+ kWaitDone = 7, // waiting for done sound to finish
+ kFinish = 8 // trigger scene transition
+ };
+
+ // Internal state machine mirroring the original engine's 0..8 states
+ SolveState _internalState = kTyping;
+
+ // Key input from handleInput, consumed in execute()
+ bool _hasNewKey = false;
+ bool _pendingReturn = false;
+ bool _pendingBackspace = false;
+ char _pendingChar = 0;
+
+ Time _nextBlinkTime = 0;
+
+ // Runtime: assembled sound description for current per-box sound (Nancy 9)
+ SoundDescription _activeBoxSound;
};
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list