Files
2026-02-02 04:50:13 +01:00

579 lines
16 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
#include "engines/nancy/resource.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/input.h"
#include "engines/nancy/util.h"
#include "engines/nancy/font.h"
#include "engines/nancy/action/puzzle/telephone.h"
#include "engines/nancy/state/scene.h"
namespace Nancy {
namespace Action {
void Telephone::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);
g_nancy->_resource->loadImage(_displayAnimName, _animImage);
if (_isNewPhone) {
_font = g_nancy->_graphics->getFont(_displayFont);
}
// Set the phone tutorial flag to false for Nancy9, so that
// the actual phone interface is available after the tutorial.
// TODO: Is this the right place to set this flag?
if (g_nancy->getGameType() == kGameTypeNancy9) {
NancySceneState.setEventFlag(592, g_nancy->_false);
}
}
void Telephone::readData(Common::SeekableReadStream &stream) {
readFilename(stream, _imageName);
uint16 numButtons = 12;
uint16 maxNumButtons = _isNewPhone ? 20 : 12;
if (_isNewPhone) {
_hasDisplay = stream.readByte();
_displayFont = stream.readUint16LE();
readFilename(stream, _displayAnimName);
_displayAnimFrameTime = stream.readUint32LE();
uint16 numFrames = stream.readUint16LE();
readRectArray(stream, _displaySrcs, numFrames, 10);
readRect(stream, _displayDest);
_dialAutomatically = stream.readByte();
numButtons = stream.readUint16LE();
}
readRectArray(stream, _srcRects, numButtons, maxNumButtons);
readRectArray(stream, _destRects, numButtons, maxNumButtons);
if (_isNewPhone) {
readRect(stream, _dirHighlightSrc);
readRect(stream, _dialHighlightSrc);
_upDirButtonID = stream.readUint16LE();
_downDirButtonID = stream.readUint16LE();
_dialButtonID = stream.readUint16LE();
_dirButtonID = stream.readUint16LE();
readRect(stream, _displayDialingSrc);
}
if (!_isNewPhone) {
_genericDialogueSound.readNormal(stream);
_genericButtonSound.readNormal(stream);
_ringSound.readNormal(stream);
_dialToneSound.readNormal(stream);
_dialAgainSound.readNormal(stream);
_hangUpSound.readNormal(stream);
} else {
_ringSound.readNormal(stream);
_dialToneSound.readNormal(stream);
_preCallSound.readNormal(stream);
_hangUpSound.readNormal(stream);
_genericButtonSound.readNormal(stream);
}
readFilenameArray(stream, _buttonSoundNames, numButtons);
stream.skip(33 * (maxNumButtons - numButtons));
char textBuf[200];
if (!_isNewPhone) {
stream.read(textBuf, 200);
textBuf[199] = '\0';
_addressBookString = textBuf;
} else {
_dialAgainSound.readNormal(stream);
}
stream.read(textBuf, 200);
textBuf[199] = '\0';
_dialAgainString = textBuf;
_reloadScene.readData(stream);
stream.skip(1);
_exitScene.readData(stream);
stream.skip(1);
readRect(stream, _exitHotspot);
uint numCalls = stream.readUint16LE();
_calls.resize(numCalls);
for (uint i = 0; i < numCalls; ++i) {
PhoneCall &call = _calls[i];
if (_isNewPhone) {
call.directoryDisplayCondition = stream.readSint16LE();
}
call.phoneNumber.resize(11);
for (uint j = 0; j < 11; ++j) {
call.phoneNumber[j] = stream.readByte();
}
if (!_isNewPhone) {
readFilename(stream, call.soundName);
stream.read(textBuf, 200);
textBuf[199] = '\0';
call.text = textBuf;
} else {
readRect(stream, call.displaySrc);
}
call.sceneChange.readData(stream);
stream.skip(1);
}
}
void Telephone::execute() {
switch (_state) {
case kBegin:
init();
registerGraphics();
g_nancy->_sound->loadSound(_dialToneSound);
g_nancy->_sound->playSound(_dialToneSound);
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_addressBookString);
_state = kRun;
// fall through
case kRun:
switch (_callState) {
case kWaiting:
if (_isNewPhone && !_animIsStopped) {
if (g_nancy->getTotalPlayTime() > _displayAnimEnd) {
if (_displayAnimEnd == 0) {
_displayAnimEnd = g_nancy->getTotalPlayTime() + _displayAnimFrameTime;
} else {
_displayAnimEnd += _displayAnimFrameTime;
}
_drawSurface.blitFrom(_animImage, _displaySrcs[_displayAnimFrame], _displayDest);
_needsRedraw = true;
++_displayAnimFrame;
if (_displayAnimFrame >= _displaySrcs.size()) {
_displayAnimFrame = 0;
}
}
}
if (_checkNumbers) {
// Pressed a new button, check all numbers for match
// We do this before going to the ringing state to support nancy4's voice mail system,
// where call numbers can be 1 digit long
for (uint i = 0; i < _calls.size(); ++i) {
auto &call = _calls[i];
bool invalid = false;
for (uint j = 0; j < _calledNumber.size(); ++j) {
if (_calledNumber[j] != call.phoneNumber[j]) {
// Invalid number, move onto next
invalid = true;
break;
}
}
// We do not want to check for a terminator if the dialed number is of
// appropriate size (7 digits, or 11 when the number starts with '1')
bool checkNextDigit = true;
if (_calledNumber.size() >= 11 || (_calledNumber.size() >= 7 && (_calledNumber[0] != 1))) {
checkNextDigit = false;
}
if (!invalid && checkNextDigit) {
// Check if the next digit in the phone number is '10' (star). Presumably, that will never
// be contained in a valid phone number
if (_calls[i].phoneNumber[_calledNumber.size()] != 10) {
invalid = true;
}
}
if (!invalid) {
_selected = i;
break;
}
}
bool shouldRing = false;
if (_selected == -1) {
// Did not find a suitable match, check if the dialed number is above allowed size
if (_calledNumber.size() >= 11 || (_calledNumber.size() >= 7 && (_calledNumber[0] != 1))) {
shouldRing = true;
}
} else {
shouldRing = true;
}
if (shouldRing) {
if (_ringSound.name != "NO SOUND") {
if (_hasDisplay) {
_drawSurface.blitFrom(_image, _displayDialingSrc, _displayDest);
_needsRedraw = true;
} else {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(g_nancy->getStaticData().ringingText);
}
g_nancy->_sound->loadSound(_ringSound);
g_nancy->_sound->playSound(_ringSound);
}
_callState = kRinging;
}
_checkNumbers = false;
}
break;
case kButtonPress:
if (!g_nancy->_sound->isSoundPlaying(_genericButtonSound)) {
g_nancy->_sound->stopSound(_genericButtonSound);
_drawSurface.fillRect(_destRects[_buttonLastPushed], g_nancy->_graphics->getTransColor());
_needsRedraw = true;
if (_isShowingDirectory) {
_drawSurface.blitFrom(_image, _dirHighlightSrc, _destRects[_dirButtonID]);
_drawSurface.blitFrom(_image, _calls[_displayedDirectory].displaySrc, _displayDest);
} else if (_dirButtonID != -1) {
_drawSurface.fillRect(_destRects[_dirButtonID], _drawSurface.getTransparentColor());
}
_buttonLastPushed = -1;
_callState = kWaiting;
}
break;
case kRinging:
if (!g_nancy->_sound->isSoundPlaying(_ringSound)) {
g_nancy->_sound->stopSound(_ringSound);
if (_selected != -1) {
// Called a valid number
if (_preCallSound.name == "NO SOUND") {
// Old phone, go directly to call
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_calls[_selected].text);
_genericDialogueSound.name = _calls[_selected].soundName;
g_nancy->_sound->loadSound(_genericDialogueSound);
g_nancy->_sound->playSound(_genericDialogueSound);
_callState = kCall;
} else {
// New phone, play a short sound of phone being picked up
g_nancy->_sound->loadSound(_preCallSound);
g_nancy->_sound->playSound(_preCallSound);
_callState = kPreCall;
}
} else {
// Called an invalid number
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_dialAgainString);
if (_hasDisplay) {
_drawSurface.fillRect(_displayDest, _drawSurface.getTransparentColor());
_needsRedraw = true;
}
if (_dialButtonID != -1) {
_drawSurface.fillRect(_destRects[_dialButtonID], _drawSurface.getTransparentColor());
_needsRedraw = true;
}
_calledNumber.clear();
g_nancy->_sound->loadSound(_dialAgainSound);
g_nancy->_sound->playSound(_dialAgainSound);
_callState = kBadNumber;
}
return;
}
break;
case kPreCall:
if (!g_nancy->_sound->isSoundPlaying(_preCallSound)) {
g_nancy->_sound->stopSound(_preCallSound);
if (!_calls[_selected].text.empty()) {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_calls[_selected].text);
}
_genericDialogueSound.name = _calls[_selected].soundName;
g_nancy->_sound->loadSound(_genericDialogueSound);
g_nancy->_sound->playSound(_genericDialogueSound);
_callState = kCall;
}
break;
case kBadNumber:
if (!g_nancy->_sound->isSoundPlaying(_dialAgainSound)) {
_state = kActionTrigger;
}
break;
case kCall:
if (!g_nancy->_sound->isSoundPlaying(_genericDialogueSound)) {
_state = kActionTrigger;
}
break;
case kHangUp:
if (!g_nancy->_sound->isSoundPlaying(_hangUpSound)) {
_state = kActionTrigger;
}
break;
}
break;
case kActionTrigger:
switch (_callState) {
case kBadNumber:
_reloadScene.execute();
_calledNumber.clear();
_state = kRun;
_callState = kWaiting;
break;
case kCall: {
PhoneCall &call = _calls[_selected];
// Make sure we don't get stuck here. Happens in nancy3 when calling George's number
// Check ignored in nancy1 since the HintSystem AR is in the same scene as the Telephone
if (call.sceneChange._sceneChange.sceneID == kNoScene && g_nancy->getGameType() != kGameTypeNancy1) {
call.sceneChange._sceneChange = NancySceneState.getSceneInfo();
}
call.sceneChange.execute();
break;
}
case kHangUp:
_exitScene.execute();
break;
default:
break;
}
g_nancy->_sound->stopSound(_hangUpSound);
g_nancy->_sound->stopSound(_genericDialogueSound);
g_nancy->_sound->stopSound(_genericButtonSound);
g_nancy->_sound->stopSound(_dialAgainSound);
g_nancy->_sound->stopSound(_ringSound);
g_nancy->_sound->stopSound(_dialToneSound);
finishExecution();
}
}
void Telephone::handleInput(NancyInput &input) {
int buttonNr = -1;
// Cursor gets changed regardless of state
for (int i = 0; i < (int)_destRects.size(); ++i) {
// Dial button is an exception
if (i == _dialButtonID && !_calledNumber.size() && !_isShowingDirectory) {
continue;
}
if (NancySceneState.getViewport().convertViewportToScreen(_destRects[i]).contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
buttonNr = i;
break;
}
}
if (_callState != kWaiting && _callState != kRinging) {
return;
}
if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
if (input.input & NancyInput::kLeftMouseButtonUp) {
g_nancy->_sound->loadSound(_hangUpSound);
g_nancy->_sound->playSound(_hangUpSound);
_callState = kHangUp;
}
return;
}
if (_callState != kWaiting) {
return;
}
if (buttonNr != -1) {
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (g_nancy->_sound->isSoundPlaying(_dialToneSound)) {
g_nancy->_sound->stopSound(_dialToneSound);
}
// Handle non-digit numbers
bool directorySwitch = false;
bool changeDirectoryEntry = false;
int dirEntryDelta = 1;
if (_dialButtonID != -1 && buttonNr == _dialButtonID) {
if (_isShowingDirectory) {
_calledNumber = _calls[_displayedDirectory].phoneNumber;
while (_calledNumber.back() == 10) {
_calledNumber.pop_back();
}
}
_checkNumbers = true;
// Dial button doesn't make sound, and doesn't get pressed down
_drawSurface.blitFrom(_image, _dialHighlightSrc, _destRects[_dialButtonID]);
if (_dirButtonID != -1) {
_drawSurface.fillRect(_destRects[_dirButtonID], _drawSurface.getTransparentColor());
}
_animIsStopped = true;
return;
} else if (_upDirButtonID != -1 && buttonNr == _upDirButtonID) {
if (!_isShowingDirectory) {
directorySwitch = true;
} else {
++_displayedDirectory;
changeDirectoryEntry = true;
}
_animIsStopped = true;
} else if (_downDirButtonID != -1 && buttonNr == _downDirButtonID) {
if (!_isShowingDirectory) {
directorySwitch = true;
} else {
--_displayedDirectory;
dirEntryDelta = -1;
changeDirectoryEntry = true;
}
_animIsStopped = true;
} else if (_dirButtonID != -1 && buttonNr == _dirButtonID) {
if (!_isShowingDirectory) {
directorySwitch = true;
}
_animIsStopped = true;
} else {
if (_isShowingDirectory || !_calledNumber.size()) {
_isShowingDirectory = false;
_displayedDirectory = 0;
_drawSurface.fillRect(_displayDest, _drawSurface.getTransparentColor());
}
_calledNumber.push_back(buttonNr);
_checkNumbers = _dialAutomatically;
_animIsStopped = true;
if (_calledNumber.size() > 11) {
_calledNumber.clear();
if (_hasDisplay) {
_drawSurface.fillRect(_displayDest, _drawSurface.getTransparentColor());
} else if (_isNewPhone) {
NancySceneState.getTextbox().clear();
}
_checkNumbers = false;
}
if (_isNewPhone && _calledNumber.size()) {
Common::String numberString;
for (uint j = 0; j < _calledNumber.size(); ++j) {
numberString += '0' + _calledNumber[j];
}
if (_hasDisplay) {
_font->drawString(&_drawSurface, numberString, _displayDest.left + 19, _displayDest.top + 21 - _font->getFontHeight(),
_displayDest.width() - 20, 0);
} else {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(numberString);
}
}
}
if (directorySwitch) {
// Handle switch to directory mode
_isShowingDirectory = true;
changeDirectoryEntry = true;
_calledNumber.clear();
}
if (changeDirectoryEntry) {
int start = _displayedDirectory;
do {
if (_displayedDirectory >= (int)_calls.size()) {
_displayedDirectory = 0;
} else if (_displayedDirectory < 0) {
_displayedDirectory = _calls.size() - 1;
}
if (_calls[_displayedDirectory].directoryDisplayCondition == kEvNoEvent) {
break;
}
if (NancySceneState.getEventFlag(_calls[_displayedDirectory].directoryDisplayCondition, g_nancy->_true)) {
break;
}
_displayedDirectory += dirEntryDelta;
} while (_displayedDirectory != start);
}
_genericButtonSound.name = _buttonSoundNames[buttonNr];
g_nancy->_sound->loadSound(_genericButtonSound);
g_nancy->_sound->playSound(_genericButtonSound);
_drawSurface.blitFrom(_image, _srcRects[buttonNr], _destRects[buttonNr]);
_needsRedraw = true;
_displayAnimEnd = 0;
_displayAnimFrame = 0;
_buttonLastPushed = buttonNr;
_callState = kButtonPress;
}
}
}
} // End of namespace Action
} // End of namespace Nancy