/* 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 . * */ #include "common/random.h" #include "common/config-manager.h" #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/hamradiopuzzle.h" namespace Nancy { namespace Action { static const char *morseCodeTable[] = { ".-", "-...", "-.-.", "-..", // a, b, c, d ".", "..-.", "--.", "....", // e, f, g, h "..", ".---", "-.-", ".-..", // i, j, k, l "--", "-.", "---", ".--.", // m, n, o, p "--.-", ".-.", "...", "-", // q, r, s, t "..-", "...-", ".--", "-..-", // u, v, w, x "-.--", "--.." // y, z }; void HamRadioPuzzle::init() { Common::Rect screenBounds = NancySceneState.getViewport().getBounds(); _drawSurface.create(screenBounds.width(), screenBounds.height(), g_nancy->_graphics->getInputPixelFormat()); _drawSurface.clear(g_nancy->_graphics->getTransColor()); setTransparent(true); setVisible(true); moveTo(screenBounds); g_nancy->_resource->loadImage(_imageName, _image); _image.setTransparentColor(_drawSurface.getTransparentColor()); } void HamRadioPuzzle::updateGraphics() { if (_digitsRolling) { uint32 curTime = g_nancy->getTotalPlayTime(); bool allDigitsCorrect = true; for (uint i = 0; i < _numDigits; ++i) { if (curTime > _nextDigitFrameTimes[i]) { uint targetFrame = (_curDigits[i] == 0 ? (10 - 1) * 3 : (_curDigits[i] - 1) * 3); if (_displayedDigitFrames[i] == targetFrame) { continue; } if (++_displayedDigitFrames[i] >= 10 * 3) { _displayedDigitFrames[i] = 0; } // Have we arrived at the correct digit? if (_displayedDigitFrames[i] != targetFrame) { // If not, set the next frame time depending on how far away the next digit is // This way, the animation slows down as we approach the correct digit int frameDifference = targetFrame - _displayedDigitFrames[i]; if (frameDifference < 0) { frameDifference += 10 * 3; } if (_nextDigitFrameTimes[i] == 0) { _nextDigitFrameTimes[i] = curTime; } switch (frameDifference) { case 1: _nextDigitFrameTimes[i] += 300; break; case 2: // fall through case 3: _nextDigitFrameTimes[i] += 200; break; case 4: // fall through case 5: _nextDigitFrameTimes[i] += 100; break; default: _nextDigitFrameTimes[i] += 50; break; } // Mark digits as incorrect allDigitsCorrect = false; } // Play the rolling sound g_nancy->_sound->loadSound(_digitRollSound, nullptr, true); g_nancy->_sound->playSound(_digitRollSound); // Finally, change the digit graphic _drawSurface.blitFrom(_image, _digitSrcs[_displayedDigitFrames[i]], _digitDests[i]); _needsRedraw = true; } else { // Still animating a digit, so we can't be at the correct one yet allDigitsCorrect = false; } } if (allDigitsCorrect) { // We've arrived at the correct digits, end the animation state _digitsRolling = false; Common::fill(_nextDigitFrameTimes.begin(), _nextDigitFrameTimes.end(), 0); } } } void HamRadioPuzzle::setFrequency(const Common::Array &freq) { _isOnCorrectFrequency = false; _curMorseString.clear(); _curCharString.clear(); if (freq == _startFreq.frequency) { // Check start frequency _startFreq.sound.loadAndPlay(); NancySceneState.setEventFlag(_startFreq.flag); } else if (freq == _correctFreq.frequency) { // Check correct transmission frequency _correctFreq.sound.loadAndPlay(); NancySceneState.setEventFlag(_correctFreq.flag); _isOnCorrectFrequency = true; } else { // Check other frequencies for (auto &otherFreq : _otherFrequencies) { if (freq == otherFreq.frequency) { otherFreq.sound.loadAndPlay(); NancySceneState.setEventFlag(otherFreq.flag); return; } } // No frequency found, pick random "bad" sound // This is re-rolled every time a bad frequency is connected to, even if the player // hasn't inputted any new digits _badFrequencySounds[g_nancy->_randomSource->getRandomNumber(_badFrequencySounds.size() - 1)].loadAndPlay(); } } void HamRadioPuzzle::CCSound::readData(Common::SeekableReadStream &stream) { char buf[100]; stream.read(buf, 100); assembleTextLine(buf, text, 100); sound.readNormal(stream); } void HamRadioPuzzle::CCSound::loadAndPlay() { g_nancy->_sound->loadSound(sound); g_nancy->_sound->playSound(sound); if (text.size() && ConfMan.getBool("subtitles")) { NancySceneState.getTextbox().clear(); NancySceneState.getTextbox().addTextLine(text); NancySceneState.getTextbox().drawTextbox(); } } void HamRadioPuzzle::Frequency::readData(Common::SeekableReadStream &stream, uint16 numDigits) { frequency.resize(numDigits); for (uint i = 0; i < numDigits; ++i) { frequency[i] = stream.readUint16LE(); } stream.skip((8 - numDigits) * 2); sound.readData(stream); flag.label = stream.readUint16LE(); flag.flag = stream.readByte(); } void HamRadioPuzzle::readData(Common::SeekableReadStream &stream) { readFilename(stream, _imageName); _numDigits = stream.readUint16LE(); _startFreq.readData(stream, _numDigits); _correctFreq.readData(stream, _numDigits); _passwordMaxSize = stream.readUint16LE(); readFilename(stream, _password); // not a filename _passwordFlag.label = stream.readUint16LE(); _passwordFlag.flag = stream.readByte(); readFilename(stream, _codeWord); // not a filename stream.skip(2); readRectArray(stream, _digitDests, _numDigits, 8); readRectArray(stream, _buttonDests, 10 + 6); readRectArray(stream, _digitSrcs, 10 * 3); // digits 0-9, plus 2 inbetweens per digit readRectArray(stream, _buttonSrcs, 10 + 6); _digitRollSound.readNormal(stream); _frequencyButtonSound.readData(stream); _connectButtonSound.readData(stream); _dotButtonSound.readData(stream); _dashButtonSound.readData(stream); _sendButtonSound.readData(stream); _deleteButtonSound.readData(stream); _resetButtonSound.readData(stream); _badLetterSound.readData(stream); _longMorseOtherSound.readData(stream); _goodPasswordSound.readData(stream); _longMorseSound.readData(stream); _badFrequencySounds.resize(3); for (uint i = 0; i < 3; ++i) { _badFrequencySounds[i].readData(stream); } _solveScene.readData(stream); _solveSoundDelay = stream.readUint16LE(); _solveSound.readData(stream); readRect(stream, _exitButtonDest); readRect(stream, _exitButtonSrc); _exitScene.readData(stream); _exitSoundDelay = stream.readUint16LE(); _exitSound.readNormal(stream); uint16 numOtherFreqs = stream.readUint16LE(); _otherFrequencies.resize(numOtherFreqs); for (uint i = 0; i < numOtherFreqs; ++i) { _otherFrequencies[i].readData(stream, _numDigits); } _curDigits.resize(_numDigits, 0); _displayedDigitFrames.resize(_numDigits, 0); _nextDigitFrameTimes.resize(_numDigits, 0); } void HamRadioPuzzle::execute() { switch (_state) { case kBegin : init(); registerGraphics(); g_nancy->_sound->loadSound(_digitRollSound); setFrequency(_startFreq.frequency); _curDigits = _startFreq.frequency; _state = kRun; // fall through case kRun : if (_pressedButton != kNone && g_nancy->getTotalPlayTime() > _buttonEndTime) { bool isDot = false; // Check for button presses switch (_pressedButton) { case kConnect: setFrequency(_curDigits); break; case kDot: isDot = true; // fall through case kDash: _curMorseString += isDot ? '.' : '-'; // Original engine uses the captions inside the dot and dash sounds if (_curMorseString.size() > 4) { _curMorseString.clear(); _badLetterSound.loadAndPlay(); } else { if (ConfMan.getBool("subtitles")) { NancySceneState.getTextbox().clear(); NancySceneState.getTextbox().addTextLine(_curMorseString); NancySceneState.getTextbox().setOverrideFont(3); // Original engine pushes tag instead NancySceneState.getTextbox().drawTextbox(); } } break; case kSend: { bool foundCorrect = false; if (_curMorseString.size()) { for (uint i = 0; i < ARRAYSIZE(morseCodeTable); ++i) { if (_curMorseString == morseCodeTable[i]) { foundCorrect = true; _curCharString += ('a' + i); break; } } } // Check if above maximum length string if (_curMorseString.size() > 10) { _longMorseSound.loadAndPlay(); _curCharString.clear(); _curMorseString.clear(); break; } // Morse code is incorrect if (!foundCorrect) { _badLetterSound.loadAndPlay(); } } // fall through case kDelete: _curMorseString.clear(); if (_curCharString.size() > 10) { // Password is above max size, clear _curCharString.clear(); NancySceneState.getTextbox().clear(); _longMorseSound.loadAndPlay(); } else if (_solvedPassword && _curCharString.size() > _passwordMaxSize) { // Password is above max size, clear _curCharString.clear(); NancySceneState.getTextbox().clear(); _longMorseOtherSound.loadAndPlay(); } if (ConfMan.getBool("subtitles")) { NancySceneState.getTextbox().clear(); NancySceneState.getTextbox().addTextLine(_curCharString); NancySceneState.getTextbox().drawTextbox(); } if (_isOnCorrectFrequency) { // When transmitting on right frequency, check password/code word if (!_solvedPassword) { // Password not solved, check against it if (_curCharString == _password) { _solvedPassword = true; NancySceneState.setEventFlag(_passwordFlag); _curCharString.clear(); _goodPasswordSound.loadAndPlay(); } } else { // Password solved, check against codeword if (_curCharString == _codeWord) { _solvedCodeword = true; _curCharString.clear(); _solveSound.loadAndPlay(); // Sound delay is ignored } } } break; case kReset: _curCharString.clear(); _curMorseString.clear(); NancySceneState.getTextbox().clear(); break; default: // Number digit for (int i = 0; i < _numDigits - 1; ++i) { _curDigits[i] = _curDigits[i + 1]; } _curDigits.back() = (_pressedButton == 9 ? 0 : _pressedButton + 1); _digitsRolling = true; break; } _drawSurface.fillRect(_buttonDests[_pressedButton], _drawSurface.getTransparentColor()); _needsRedraw = true; _pressedButton = kNone; } break; case kActionTrigger: if (_digitsRolling) { return; } if (_solvedCodeword) { _solveScene.execute(); } else { // Fail sound is ignored _exitScene.execute(); } finishExecution(); break; } } void HamRadioPuzzle::handleInput(NancyInput &input) { if (_digitsRolling || _state != kRun || _pressedButton != kNone) { return; } // Handle exit button if (NancySceneState.getViewport().convertViewportToScreen(_exitButtonDest).contains(input.mousePos)) { g_nancy->_cursor->setCursorType(CursorManager::kHotspot); if (input.input & NancyInput::kLeftMouseButtonUp) { _state = kActionTrigger; for (uint i = 0; i < _curDigits.size(); ++i) { _curDigits[i] = 0; } _digitsRolling = true; _drawSurface.blitFrom(_image, _exitButtonSrc, _exitButtonDest); _needsRedraw = true; } return; } // Handle other buttons for (uint i = 0; i < _buttonDests.size(); ++i) { if (NancySceneState.getViewport().convertViewportToScreen(_buttonDests[i]).contains(input.mousePos)) { if (i >= 10 || _pressedButton == kNone) { g_nancy->_cursor->setCursorType(CursorManager::kHotspot); if (input.input & NancyInput::kLeftMouseButtonUp) { _pressedButton = i; _drawSurface.blitFrom(_image, _buttonSrcs[i], _buttonDests[i]); _needsRedraw = true; CCSound *soundToPlay = nullptr; switch (i) { case kConnect: soundToPlay = &_connectButtonSound; break; case kDot: soundToPlay = &_dotButtonSound; break; case kDash: soundToPlay = &_dashButtonSound; break; case kSend: soundToPlay = &_sendButtonSound; break; case kDelete: soundToPlay = &_deleteButtonSound; break; case kReset: soundToPlay = &_resetButtonSound; break; default: soundToPlay = &_frequencyButtonSound; break; } // Do NOT use the loadAndPlaySound() function, since the dot/dash sounds have ./- captions g_nancy->_sound->loadSound(soundToPlay->sound, nullptr, true); g_nancy->_sound->playSound(soundToPlay->sound); } } break; } } if (_pressedButton != kNone) { _buttonEndTime = g_nancy->getTotalPlayTime() + 250; } } } // End of namespace Action } // End of namespace Nancy