diff options
author | Paul Gilbert | 2015-05-22 08:04:10 -0400 |
---|---|---|
committer | Paul Gilbert | 2015-05-22 08:04:10 -0400 |
commit | fbc4b98f32b5a4b115ff58f8ffc7a93113565a93 (patch) | |
tree | d404f44fddbb998dd3ab6928dd71b7b59799dc6f | |
parent | ca4793dabdaea4a34ef41fb30a44b9ffd9ffc3b6 (diff) | |
parent | 50f985217cabb4f5d8912db7071f0822f1899e38 (diff) | |
download | scummvm-rg350-fbc4b98f32b5a4b115ff58f8ffc7a93113565a93.tar.gz scummvm-rg350-fbc4b98f32b5a4b115ff58f8ffc7a93113565a93.tar.bz2 scummvm-rg350-fbc4b98f32b5a4b115ff58f8ffc7a93113565a93.zip |
Merge pull request #595 from dreammaster/sherlock
SHERLOCK: Support for The Lost Files of Sherlock Holmes - The Case of the Serrated Scalpel
46 files changed, 17564 insertions, 0 deletions
diff --git a/engines/sherlock/animation.cpp b/engines/sherlock/animation.cpp new file mode 100644 index 0000000000..21d63633d3 --- /dev/null +++ b/engines/sherlock/animation.cpp @@ -0,0 +1,188 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/animation.h" +#include "sherlock/sherlock.h" +#include "common/algorithm.h" + +namespace Sherlock { + +static const int NO_FRAMES = FRAMES_END; + +Animation::Animation(SherlockEngine *vm) : _vm(vm) { +} + +bool Animation::play(const Common::String &filename, int minDelay, int fade, + bool setPalette, int speed) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + int soundNumber = 0; + + // Check for any any sound frames for the given animation + const int *soundFrames = checkForSoundFrames(filename); + + // Add on the VDX extension + Common::String vdxName = filename + ".vdx"; + + // Load the animation + Common::SeekableReadStream *stream; + if (!_gfxLibraryFilename.empty()) + stream = _vm->_res->load(vdxName, _gfxLibraryFilename); + else if (_vm->_useEpilogue2) + stream = _vm->_res->load(vdxName, "epilog2.lib"); + else + stream = _vm->_res->load(vdxName, "epilogue.lib"); + + // Load initial image + Common::String vdaName = filename + ".vda"; + ImageFile images(vdaName, true, true); + + events.wait(minDelay); + if (fade != 0 && fade != 255) + screen.fadeToBlack(); + + if (setPalette) { + if (fade != 255) + screen.setPalette(images._palette); + } + + int frameNumber = 0; + Common::Point pt; + bool skipped = false; + while (!_vm->shouldQuit()) { + // Get the next sprite to display + int imageFrame = stream->readSint16LE(); + + if (imageFrame == -2) { + // End of animation reached + break; + } else if (imageFrame != -1) { + // Read position from either animation stream or the sprite frame itself + if (imageFrame < 0) { + imageFrame += 32768; + pt.x = stream->readUint16LE(); + pt.y = stream->readUint16LE(); + } else { + pt = images[imageFrame]._offset; + } + + // Draw the sprite. Note that we explicitly use the raw frame below, rather than the ImageFrame, + // since we don't want the offsets in the image file to be used, just the explicit position we specify + screen.transBlitFrom(images[imageFrame]._frame, pt); + } else { + // At this point, either the sprites for the frame has been complete, or there weren't any sprites + // at all to draw for the frame + if (fade == 255) { + // Gradual fade in + if (screen.equalizePalette(images._palette) == 0) + fade = 0; + } + + // Check if we've reached a frame with sound + if (frameNumber++ == *soundFrames) { + ++soundNumber; + ++soundFrames; + Common::String fname = _soundLibraryFilename.empty() ? + Common::String::format("%s%01d", filename.c_str(), soundNumber) : + Common::String::format("%s%02d", filename.c_str(), soundNumber); + + if (sound._voices) + sound.playSound(fname, WAIT_RETURN_IMMEDIATELY, 100, _soundLibraryFilename.c_str()); + } + + events.wait(speed * 3); + } + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + if (keyState.keycode == Common::KEYCODE_ESCAPE || + keyState.keycode == Common::KEYCODE_SPACE) { + skipped = true; + break; + } + } else if (events._pressed) { + skipped = true; + break; + } + } + + events.clearEvents(); + sound.stopSound(); + delete stream; + + return !skipped && !_vm->shouldQuit(); +} + +void Animation::setPrologueNames(const char *const *names, int count) { + for (int idx = 0; idx < count; ++idx, ++names) { + _prologueNames.push_back(*names); + } +} + +void Animation::setPrologueFrames(const int *frames, int count, int maxFrames) { + _prologueFrames.resize(count); + + for (int idx = 0; idx < count; ++idx, frames += maxFrames) { + _prologueFrames[idx].resize(maxFrames); + Common::copy(frames, frames + maxFrames, &_prologueFrames[idx][0]); + } +} + +void Animation::setTitleNames(const char *const *names, int count) { + for (int idx = 0; idx < count; ++idx, ++names) { + _titleNames.push_back(*names); + } +} + +void Animation::setTitleFrames(const int *frames, int count, int maxFrames) { + _titleFrames.resize(count); + + for (int idx = 0; idx < count; ++idx, frames += maxFrames) { + _titleFrames[idx].resize(maxFrames); + Common::copy(frames, frames + maxFrames, &_titleFrames[idx][0]); + } +} + +const int *Animation::checkForSoundFrames(const Common::String &filename) { + const int *frames = &NO_FRAMES; + + if (_soundLibraryFilename.empty()) { + for (uint idx = 0; idx < _prologueNames.size(); ++idx) { + if (filename.equalsIgnoreCase(_prologueNames[idx])) { + frames = &_prologueFrames[idx][0]; + break; + } + } + } else { + for (uint idx = 0; idx < _titleNames.size(); ++idx) { + if (filename.equalsIgnoreCase(_titleNames[idx])) { + frames = &_titleFrames[idx][0]; + break; + } + } + } + + return frames; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/animation.h b/engines/sherlock/animation.h new file mode 100644 index 0000000000..b7811d3fa8 --- /dev/null +++ b/engines/sherlock/animation.h @@ -0,0 +1,83 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_ANIMATION_H +#define SHERLOCK_ANIMATION_H + +#include "common/scummsys.h" +#include "common/str.h" +#include "common/array.h" + +namespace Sherlock { + +#define FRAMES_END 32000 + +class SherlockEngine; + +class Animation { +private: + SherlockEngine *_vm; + + Common::Array<const char *> _prologueNames; + Common::Array<Common::Array<int> > _prologueFrames; + Common::Array<const char *> _titleNames; + Common::Array<Common::Array<int> > _titleFrames; + + /** + * Checks for whether an animation is being played that has associated sound + */ + const int *checkForSoundFrames(const Common::String &filename); +public: + Common::String _soundLibraryFilename; + Common::String _gfxLibraryFilename; +public: + Animation(SherlockEngine *vm); + + /** + * Load the prologue name array + */ + void setPrologueNames(const char *const *names, int count); + + /** + * Load the prologue frame array + */ + void setPrologueFrames(const int *frames, int count, int maxFrames); + + /** + * Load the title name array + */ + void setTitleNames(const char *const *names, int count); + + /** + * Load the title frame array + */ + void setTitleFrames(const int *frames, int count, int maxFrames); + + /** + * Play a full-screen animation + */ + bool play(const Common::String &filename, int minDelay, int fade, bool setPalette, int speed); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/configure.engine b/engines/sherlock/configure.engine new file mode 100644 index 0000000000..a56129a8f0 --- /dev/null +++ b/engines/sherlock/configure.engine @@ -0,0 +1,3 @@ +# This file is included from the main "configure" script +# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] +add_engine sherlock "The Lost Files of Sherlock Holmes" no diff --git a/engines/sherlock/debugger.cpp b/engines/sherlock/debugger.cpp new file mode 100644 index 0000000000..cfbea2bc24 --- /dev/null +++ b/engines/sherlock/debugger.cpp @@ -0,0 +1,59 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/debugger.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +Debugger::Debugger(SherlockEngine *vm) : GUI::Debugger(), _vm(vm) { + registerCmd("continue", WRAP_METHOD(Debugger, cmdExit)); + registerCmd("scene", WRAP_METHOD(Debugger, cmdScene)); +} + +int Debugger::strToInt(const char *s) { + if (!*s) + // No string at all + return 0; + else if (toupper(s[strlen(s) - 1]) != 'H') + // Standard decimal string + return atoi(s); + + // Hexadecimal string + uint tmp = 0; + int read = sscanf(s, "%xh", &tmp); + if (read < 1) + error("strToInt failed on string \"%s\"", s); + return (int)tmp; +} + +bool Debugger::cmdScene(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: scene <room>\n"); + return true; + } else { + _vm->_scene->_goToScene = strToInt(argv[1]); + return false; + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/debugger.h b/engines/sherlock/debugger.h new file mode 100644 index 0000000000..e6a3aba828 --- /dev/null +++ b/engines/sherlock/debugger.h @@ -0,0 +1,53 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_DEBUGGER_H +#define SHERLOCK_DEBUGGER_H + +#include "common/scummsys.h" +#include "gui/debugger.h" + +namespace Sherlock { + +class SherlockEngine; + +class Debugger : public GUI::Debugger { +private: + SherlockEngine *_vm; + + /** + * Converts a decimal or hexadecimal string into a number + */ + int strToInt(const char *s); + + /** + * Switch to another scene + */ + bool cmdScene(int argc, const char **argv); +public: + Debugger(SherlockEngine *vm); + virtual ~Debugger() {} +}; + +} // End of namespace Sherlock + +#endif /* SHERLOCK_DEBUGGER_H */ diff --git a/engines/sherlock/detection.cpp b/engines/sherlock/detection.cpp new file mode 100644 index 0000000000..ea68d79a1b --- /dev/null +++ b/engines/sherlock/detection.cpp @@ -0,0 +1,241 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/sherlock.h" +#include "sherlock/saveload.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/tattoo/tattoo.h" +#include "common/system.h" +#include "common/translation.h" +#include "engines/advancedDetector.h" + +namespace Sherlock { + +struct SherlockGameDescription { + ADGameDescription desc; + + GameType gameID; +}; + +GameType SherlockEngine::getGameID() const { + return _gameDescription->gameID; +} + +Common::Platform SherlockEngine::getPlatform() const { + return _gameDescription->desc.platform; +} + +} // End of namespace Sherlock + +static const PlainGameDescriptor sherlockGames[] = { + { "scalpel", "The Case of the Serrated Scalpel" }, + { "rosetattoo", "The Case of the Rose Tattoo" }, + {0, 0} +}; + + +#define GAMEOPTION_ORIGINAL_SAVES GUIO_GAMEOPTIONS1 +#define GAMEOPTION_FADE_STYLE GUIO_GAMEOPTIONS2 +#define GAMEOPTION_HELP_STYLE GUIO_GAMEOPTIONS3 +#define GAMEOPTION_PORTRAITS_ON GUIO_GAMEOPTIONS4 +#define GAMEOPTION_WINDOW_STYLE GUIO_GAMEOPTIONS5 + +static const ADExtraGuiOptionsMap optionsList[] = { + { + GAMEOPTION_ORIGINAL_SAVES, + { + _s("Use original savegame dialog"), + _s("Files button in-game shows original savegame dialog rather than the ScummVM menu"), + "originalsaveload", + false + } + }, + + { + GAMEOPTION_FADE_STYLE, + { + _s("Pixellated scene transitions"), + _s("When changing scenes, a randomized pixel transition is done"), + "fade_style", + true + } + }, + + { + GAMEOPTION_HELP_STYLE, + { + _s("Don't show hotspots when moving mouse"), + _s("Only show hotspot names after you actually click on a hotspot or action button"), + "help_style", + false + } + }, + + { + GAMEOPTION_PORTRAITS_ON, + { + _s("Show character portraits"), + _s("Show portraits for the characters when conversing"), + "portraits_on", + true + } + }, + + { + GAMEOPTION_WINDOW_STYLE, + { + _s("Slide dialogs into view"), + _s("Slide UI dialogs into view, rather than simply showing them immediately"), + "window_style", + true + } + }, + + AD_EXTRA_GUI_OPTIONS_TERMINATOR +}; + + +#include "sherlock/detection_tables.h" + +class SherlockMetaEngine : public AdvancedMetaEngine { +public: + SherlockMetaEngine() : AdvancedMetaEngine(Sherlock::gameDescriptions, sizeof(Sherlock::SherlockGameDescription), + sherlockGames, optionsList) {} + + virtual const char *getName() const { + return "Sherlock Engine"; + } + + virtual const char *getOriginalCopyright() const { + return "Sherlock Engine (C) 1992-1996 Mythos Software, 1992-1996 (C) Electronic Arts"; + } + + /** + * Creates an instance of the game engine + */ + virtual bool createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const; + + /** + * Returns a list of features the game's MetaEngine support + */ + virtual bool hasFeature(MetaEngineFeature f) const; + + /** + * Return a list of savegames + */ + virtual SaveStateList listSaves(const char *target) const; + + /** + * Returns the maximum number of allowed save slots + */ + virtual int getMaximumSaveSlot() const; + + /** + * Deletes a savegame in the specified slot + */ + virtual void removeSaveState(const char *target, int slot) const; + + /** + * Given a specified savegame slot, returns extended information for the save + */ + SaveStateDescriptor querySaveMetaInfos(const char *target, int slot) const; +}; + +bool SherlockMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const { + const Sherlock::SherlockGameDescription *gd = (const Sherlock::SherlockGameDescription *)desc; + if (gd) { + switch (gd->gameID) { + case Sherlock::GType_SerratedScalpel: + *engine = new Sherlock::Scalpel::ScalpelEngine(syst, gd); + break; + case Sherlock::GType_RoseTattoo: + *engine = new Sherlock::Tattoo::TattooEngine(syst, gd); + break; + default: + error("Unknown game"); + break; + } + } + return gd != 0; +} + +bool SherlockMetaEngine::hasFeature(MetaEngineFeature f) const { + return + (f == kSupportsListSaves) || + (f == kSupportsLoadingDuringStartup) || + (f == kSupportsDeleteSave) || + (f == kSavesSupportMetaInfo) || + (f == kSavesSupportThumbnail); +} + +bool Sherlock::SherlockEngine::hasFeature(EngineFeature f) const { + return + (f == kSupportsRTL) || + (f == kSupportsLoadingDuringRuntime) || + (f == kSupportsSavingDuringRuntime); +} + +bool Sherlock::SherlockEngine::isDemo() const { + return _gameDescription->desc.flags & ADGF_DEMO; +} + +SaveStateList SherlockMetaEngine::listSaves(const char *target) const { + return Sherlock::SaveManager::getSavegameList(target); +} + +int SherlockMetaEngine::getMaximumSaveSlot() const { + return MAX_SAVEGAME_SLOTS; +} + +void SherlockMetaEngine::removeSaveState(const char *target, int slot) const { + Common::String filename = Sherlock::SaveManager(nullptr, target).generateSaveName(slot); + g_system->getSavefileManager()->removeSavefile(filename); +} + +SaveStateDescriptor SherlockMetaEngine::querySaveMetaInfos(const char *target, int slot) const { + Common::String filename = Sherlock::SaveManager(nullptr, target).generateSaveName(slot); + Common::InSaveFile *f = g_system->getSavefileManager()->openForLoading(filename); + + if (f) { + Sherlock::SherlockSavegameHeader header; + Sherlock::SaveManager::readSavegameHeader(f, header); + delete f; + + // Create the return descriptor + SaveStateDescriptor desc(slot, header._saveName); + desc.setThumbnail(header._thumbnail); + desc.setSaveDate(header._year, header._month, header._day); + desc.setSaveTime(header._hour, header._minute); + desc.setPlayTime(header._totalFrames * GAME_FRAME_TIME); + + return desc; + } + + return SaveStateDescriptor(); +} + + +#if PLUGIN_ENABLED_DYNAMIC(SHERLOCK) + REGISTER_PLUGIN_DYNAMIC(SHERLOCK, PLUGIN_TYPE_ENGINE, SherlockMetaEngine); +#else + REGISTER_PLUGIN_STATIC(SHERLOCK, PLUGIN_TYPE_ENGINE, SherlockMetaEngine); +#endif diff --git a/engines/sherlock/detection_tables.h b/engines/sherlock/detection_tables.h new file mode 100644 index 0000000000..9315fa1bdd --- /dev/null +++ b/engines/sherlock/detection_tables.h @@ -0,0 +1,105 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +namespace Sherlock { + +static const SherlockGameDescription gameDescriptions[] = { + { + // Case of the Serrated Scalpel - English 3.5" Floppy + // The HitSquad CD version has the same MD5 + { + "scalpel", + 0, + AD_ENTRY1s("talk.lib", "ad0c4d6865edf15da4e9204c08815875", 238928), + Common::EN_ANY, + Common::kPlatformDOS, + ADGF_UNSTABLE, + GUIO6(GUIO_NOSPEECH, GAMEOPTION_ORIGINAL_SAVES, GAMEOPTION_FADE_STYLE, GAMEOPTION_HELP_STYLE, + GAMEOPTION_PORTRAITS_ON, GAMEOPTION_WINDOW_STYLE) + }, + GType_SerratedScalpel, + }, + + { + // Case of the Serrated Scalpel - Interactive English Demo + // Provided by Strangerke + { + "scalpel", + "Interactive Demo", + AD_ENTRY1s("talk.lib", "dbdc8a20c96900aa7e4d02f3fe8a274c", 121102), + Common::EN_ANY, + Common::kPlatformDOS, + ADGF_UNSTABLE | ADGF_DEMO, + GUIO1(GUIO_NOSPEECH) + }, + GType_SerratedScalpel, + }, + + { + // Case of the Serrated Scalpel - Non-Interactive English Demo + // Provided by Strangerke + { + "scalpel", + "Non Interactive Demo", + AD_ENTRY1s("music.lib", "ec19a09b7fef6fd90b1ab812ce6e9739", 38563), + Common::EN_ANY, + Common::kPlatformDOS, + ADGF_UNSTABLE | ADGF_DEMO, + GUIO1(GUIO_NOSPEECH) + }, + GType_SerratedScalpel, + }, + + { + // Case of the Rose Tattoo - French CD + // Provided by Strangerke + { + "rosetattoo", + "CD", + AD_ENTRY1s("talk.lib", "22e8e6406dd2fbbb238c9898928df42e", 770756), + Common::EN_ANY, + Common::kPlatformDOS, + ADGF_UNSTABLE, + GUIO0() + }, + GType_RoseTattoo + }, + + { + // Case of the Rose Tattoo - English CD + // Provided by dreammaster + { + "rosetattoo", + "CD", + AD_ENTRY1s("talk.lib", "9639a756b0993ebd71cb5f4d8b78b2dc", 765134), + Common::EN_ANY, + Common::kPlatformDOS, + ADGF_UNSTABLE, + GUIO0() + }, + GType_RoseTattoo, + }, + + { AD_TABLE_END_MARKER, (GameType)0 } +}; + +} // End of namespace Sherlock diff --git a/engines/sherlock/events.cpp b/engines/sherlock/events.cpp new file mode 100644 index 0000000000..94ddc9a792 --- /dev/null +++ b/engines/sherlock/events.cpp @@ -0,0 +1,239 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "common/scummsys.h" +#include "common/events.h" +#include "common/system.h" +#include "engines/util.h" +#include "graphics/cursorman.h" +#include "sherlock/sherlock.h" +#include "sherlock/events.h" + +namespace Sherlock { + +enum ButtonFlag { LEFT_BUTTON = 1, RIGHT_BUTTON = 2 }; + +Events::Events(SherlockEngine *vm) { + _vm = vm; + _cursorImages = nullptr; + _cursorId = INVALID_CURSOR; + _frameCounter = 1; + _priorFrameTime = 0; + _mouseButtons = 0; + _pressed = _released = false; + _rightPressed = _rightReleased = false; + _oldButtons = _oldRightButton = false; +} + +Events::~Events() { + delete _cursorImages; +} + +void Events::loadCursors(const Common::String &filename) { + hideCursor(); + delete _cursorImages; + + _cursorImages = new ImageFile(filename); + _cursorId = INVALID_CURSOR; +} + +void Events::setCursor(CursorId cursorId) { + if (cursorId == _cursorId) + return; + + _cursorId = cursorId; + + // Set the cursor data + Graphics::Surface &s = (*_cursorImages)[cursorId]._frame; + + setCursor(s); +} + +void Events::setCursor(const Graphics::Surface &src) { + CursorMan.replaceCursor(src.getPixels(), src.w, src.h, 0, 0, 0xff); + showCursor(); +} + +void Events::showCursor() { + CursorMan.showMouse(true); +} + +void Events::hideCursor() { + CursorMan.showMouse(false); +} + +CursorId Events::getCursor() const { + return _cursorId; +} + +bool Events::isCursorVisible() const { + return CursorMan.isVisible(); +} + +void Events::moveMouse(const Common::Point &pt) { + g_system->warpMouse(pt.x, pt.y); +} + +void Events::pollEvents() { + checkForNextFrameCounter(); + + Common::Event event; + while (g_system->getEventManager()->pollEvent(event)) { + // Handle keypress + switch (event.type) { + case Common::EVENT_QUIT: + case Common::EVENT_RTL: + return; + + case Common::EVENT_KEYDOWN: + // Check for debugger + if (event.kbd.keycode == Common::KEYCODE_d && (event.kbd.flags & Common::KBD_CTRL)) { + // Attach to the debugger + _vm->_debugger->attach(); + _vm->_debugger->onFrame(); + } else { + _pendingKeys.push(event.kbd); + } + return; + case Common::EVENT_KEYUP: + return; + case Common::EVENT_LBUTTONDOWN: + _mouseButtons |= LEFT_BUTTON; + return; + case Common::EVENT_RBUTTONDOWN: + _mouseButtons |= RIGHT_BUTTON; + return; + case Common::EVENT_LBUTTONUP: + _mouseButtons &= ~LEFT_BUTTON; + return; + case Common::EVENT_RBUTTONUP: + _mouseButtons &= ~RIGHT_BUTTON; + return; + default: + break; + } + } +} + +void Events::pollEventsAndWait() { + pollEvents(); + g_system->delayMillis(10); +} + +bool Events::checkForNextFrameCounter() { + // Check for next game frame + uint32 milli = g_system->getMillis(); + if ((milli - _priorFrameTime) >= GAME_FRAME_TIME) { + ++_frameCounter; + _priorFrameTime = milli; + + // Give time to the debugger + _vm->_debugger->onFrame(); + + // Display the frame + _vm->_screen->update(); + + return true; + } + + return false; +} + +Common::Point Events::mousePos() const { + return g_system->getEventManager()->getMousePos(); +} + +Common::KeyState Events::getKey() { + return _pendingKeys.pop(); +} + +void Events::clearEvents() { + _pendingKeys.clear(); + _mouseButtons = 0; + _pressed = _released = false; + _rightPressed = _rightReleased = false; + _oldButtons = _oldRightButton = false; +} + +void Events::clearKeyboard() { + _pendingKeys.clear(); +} + +void Events::wait(int numFrames) { + uint32 totalMilli = numFrames * 1000 / GAME_FRAME_RATE; + delay(totalMilli); +} + +bool Events::delay(uint32 time, bool interruptable) { + // Different handling for really short versus extended times + if (time < 10) { + // For really short periods, simply delay by the desired amount + pollEvents(); + g_system->delayMillis(time); + bool result = !(interruptable && (kbHit() || _pressed)); + + clearEvents(); + return result; + } else { + // For long periods go into a loop where we delay by 10ms at a time and then + // check for events. This ensures for longer delays that responsiveness is + // maintained + uint32 delayEnd = g_system->getMillis() + time; + + while (!_vm->shouldQuit() && g_system->getMillis() < delayEnd) { + pollEventsAndWait(); + + if (interruptable && (kbHit() || _pressed)) { + clearEvents(); + return false; + } + } + + return true; + } +} + +void Events::setButtonState() { + _released = _rightReleased = false; + if (_mouseButtons & LEFT_BUTTON) + _pressed = _oldButtons = true; + + if ((_mouseButtons & LEFT_BUTTON) == 0 && _oldButtons) { + _pressed = _oldButtons = false; + _released = true; + } + + if (_mouseButtons & RIGHT_BUTTON) + _rightPressed = _oldRightButton = true; + + if ((_mouseButtons & RIGHT_BUTTON) == 0 && _oldRightButton) { + _rightPressed = _oldRightButton = false; + _rightReleased = true; + } +} + +bool Events::checkInput() { + setButtonState(); + return kbHit() || _pressed || _released || _rightPressed || _rightReleased; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/events.h b/engines/sherlock/events.h new file mode 100644 index 0000000000..c19a92de8c --- /dev/null +++ b/engines/sherlock/events.h @@ -0,0 +1,166 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_EVENTS_H +#define SHERLOCK_EVENTS_H + +#include "common/scummsys.h" +#include "common/events.h" +#include "common/stack.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +#define GAME_FRAME_RATE 60 +#define GAME_FRAME_TIME (1000 / GAME_FRAME_RATE) + +enum CursorId { ARROW = 0, MAGNIFY = 1, WAIT = 2, INVALID_CURSOR = -1 }; + +class SherlockEngine; + +class Events { +private: + SherlockEngine *_vm; + uint32 _frameCounter; + uint32 _priorFrameTime; + ImageFile *_cursorImages; + int _mouseButtons; + + /** + * Check whether it's time to display the next screen frame + */ + bool checkForNextFrameCounter(); +public: + CursorId _cursorId; + bool _pressed; + bool _released; + bool _rightPressed; + bool _rightReleased; + bool _oldButtons; + bool _oldRightButton; + Common::Stack<Common::KeyState> _pendingKeys; +public: + Events(SherlockEngine *vm); + ~Events(); + + /** + * Load a set of cursors from the specified file + */ + void loadCursors(const Common::String &filename); + + /** + * Set the cursor to show + */ + void setCursor(CursorId cursorId); + + /** + * Set the cursor to show from a passed frame + */ + void setCursor(const Graphics::Surface &src); + + /** + * Show the mouse cursor + */ + void showCursor(); + + /** + * Hide the mouse cursor + */ + void hideCursor(); + + /** + * Returns the cursor + */ + CursorId getCursor() const; + + /** + * Returns true if the mouse cursor is visible + */ + bool isCursorVisible() const; + + /** + * Move the mouse + */ + void moveMouse(const Common::Point &pt); + + /** + * Check for any pending events + */ + void pollEvents(); + + /** + * Poll for events and introduce a small delay, to allow the system to + * yield to other running programs + */ + void pollEventsAndWait(); + + /** + * Get the current mouse position + */ + Common::Point mousePos() const; + + uint32 getFrameCounter() const { return _frameCounter; } + + bool kbHit() const { return !_pendingKeys.empty(); } + + /** + * Get a pending keypress + */ + Common::KeyState getKey(); + + /** + * Clear any current keypress or mouse click + */ + void clearEvents(); + + /** + * Clear any pending keyboard inputs + */ + void clearKeyboard(); + + /** + * Delay for a given number of game frames, where each frame is 1/60th of a second + */ + void wait(int numFrames); + + /** + * Does a delay of the specified number of milliseconds + */ + bool delay(uint32 time, bool interruptable = false); + + /** + * Sets the pressed and released button flags on the raw button state previously set in pollEvents calls. + * @remarks The events manager has separate variables for the raw immediate and old button state + * versus the current buttons states for the frame. This method is expected to be called only once + * per game frame + */ + void setButtonState(); + + /** + * Checks to see to see if a key or a mouse button is pressed. + */ + bool checkInput(); +}; + +} // End of namespace Sherlock + +#endif /* SHERLOCK_EVENTS_H */ diff --git a/engines/sherlock/inventory.cpp b/engines/sherlock/inventory.cpp new file mode 100644 index 0000000000..265b12ce76 --- /dev/null +++ b/engines/sherlock/inventory.cpp @@ -0,0 +1,464 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/inventory.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +InventoryItem::InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine) : + _requiredFlag(requiredFlag), _name(name), _description(description), + _examine(examine), _lookFlag(0) { +} + +void InventoryItem::synchronize(Common::Serializer &s) { + s.syncAsSint16LE(_requiredFlag); + s.syncAsSint16LE(_lookFlag); + s.syncString(_name); + s.syncString(_description); + s.syncString(_examine); +} + +/*----------------------------------------------------------------*/ + +Inventory::Inventory(SherlockEngine *vm) : Common::Array<InventoryItem>(), _vm(vm) { + Common::fill(&_invShapes[0], &_invShapes[MAX_VISIBLE_INVENTORY], (ImageFile *)nullptr); + _invGraphicsLoaded = false; + _invIndex = 0; + _holdings = 0; + _invMode = INVMODE_EXIT; +} + +Inventory::~Inventory() { + freeGraphics(); +} + +void Inventory::freeInv() { + freeGraphics(); + + _names.clear(); + _invGraphicsLoaded = false; +} + +void Inventory::freeGraphics() { + for (uint idx = 0; idx < MAX_VISIBLE_INVENTORY; ++idx) + delete _invShapes[idx]; + + Common::fill(&_invShapes[0], &_invShapes[MAX_VISIBLE_INVENTORY], (ImageFile *)nullptr); + _invGraphicsLoaded = false; +} + +void Inventory::loadInv() { + // Exit if the inventory names are already loaded + if (_names.size() > 0) + return; + + // Load the inventory names + Common::SeekableReadStream *stream = _vm->_res->load("invent.txt"); + + int streamSize = stream->size(); + while (stream->pos() < streamSize) { + Common::String name; + char c; + while ((c = stream->readByte()) != 0) + name += c; + + _names.push_back(name); + } + + delete stream; + + loadGraphics(); +} + +void Inventory::loadGraphics() { + if (_invGraphicsLoaded) + return; + + // Default all inventory slots to empty + Common::fill(&_invShapes[0], &_invShapes[MAX_VISIBLE_INVENTORY], (ImageFile *)nullptr); + + for (int idx = _invIndex; (idx < _holdings) && (idx - _invIndex) < MAX_VISIBLE_INVENTORY; ++idx) { + // Get the name of the item to be displayed, figure out its accompanying + // .VGS file with its picture, and then load it + int invNum = findInv((*this)[idx]._name); + Common::String fName = Common::String::format("item%02d.vgs", invNum + 1); + + _invShapes[idx - _invIndex] = new ImageFile(fName); + } + + _invGraphicsLoaded = true; +} + +int Inventory::findInv(const Common::String &name) { + for (int idx = 0; idx < (int)_names.size(); ++idx) { + if (name.equalsIgnoreCase(_names[idx])) + return idx; + } + + // Couldn't find the desired item + error("Couldn't find inventory item - %s", name.c_str()); +} + +void Inventory::putInv(InvSlamMode slamIt) { + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + + // If an inventory item has disappeared (due to using it or giving it), + // a blank space slot may have appeared. If so, adjust the inventory + if (_invIndex > 0 && _invIndex > (_holdings - 6)) { + --_invIndex; + freeGraphics(); + loadGraphics(); + } + + if (slamIt != SLAM_SECONDARY_BUFFER) { + screen.makePanel(Common::Rect(6, 163, 54, 197)); + screen.makePanel(Common::Rect(58, 163, 106, 197)); + screen.makePanel(Common::Rect(110, 163, 158, 197)); + screen.makePanel(Common::Rect(162, 163, 210, 197)); + screen.makePanel(Common::Rect(214, 163, 262, 197)); + screen.makePanel(Common::Rect(266, 163, 314, 197)); + } + + // Iterate through displaying up to 6 objects at a time + for (int idx = _invIndex; idx < _holdings && (idx - _invIndex) < MAX_VISIBLE_INVENTORY; ++idx) { + int itemNum = idx - _invIndex; + Surface &bb = slamIt == SLAM_SECONDARY_BUFFER ? screen._backBuffer2 : screen._backBuffer1; + Common::Rect r(8 + itemNum * 52, 165, 51 + itemNum * 52, 194); + + // Draw the background + if (idx == ui._selector) { + bb.fillRect(r, 235); + } else if (slamIt == SLAM_SECONDARY_BUFFER) { + bb.fillRect(r, BUTTON_MIDDLE); + } + + // Draw the item image + ImageFrame &frame = (*_invShapes[itemNum])[0]; + bb.transBlitFrom(frame, Common::Point(6 + itemNum * 52 + ((47 - frame._width) / 2), + 163 + ((33 - frame._height) / 2))); + } + + if (slamIt == SLAM_DISPLAY) + screen.slamArea(6, 163, 308, 34); + + if (slamIt != SLAM_SECONDARY_BUFFER) + ui.clearInfo(); + + if (slamIt == 0) { + invCommands(0); + } else if (slamIt == SLAM_SECONDARY_BUFFER) { + screen._backBuffer = &screen._backBuffer2; + invCommands(0); + screen._backBuffer = &screen._backBuffer1; + } +} + +void Inventory::drawInventory(InvNewMode mode) { + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + InvNewMode tempMode = mode; + + loadInv(); + + if (mode == INVENTORY_DONT_DISPLAY) { + screen._backBuffer = &screen._backBuffer2; + } + + // Draw the window background + Surface &bb = *screen._backBuffer; + bb.fillRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y1 + 10), BORDER_COLOR); + bb.fillRect(Common::Rect(0, CONTROLS_Y1 + 10, 2, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(SHERLOCK_SCREEN_WIDTH - 2, CONTROLS_Y1 + 10, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - 2, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(2, CONTROLS_Y1 + 10, SHERLOCK_SCREEN_WIDTH - 2, SHERLOCK_SCREEN_HEIGHT - 2), + INV_BACKGROUND); + + // Draw the buttons + screen.makeButton(Common::Rect(INVENTORY_POINTS[0][0], CONTROLS_Y1, INVENTORY_POINTS[0][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[0][2] - screen.stringWidth("Exit") / 2, "Exit"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[1][0], CONTROLS_Y1, INVENTORY_POINTS[1][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[1][2] - screen.stringWidth("Look") / 2, "Look"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[2][0], CONTROLS_Y1, INVENTORY_POINTS[2][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[2][2] - screen.stringWidth("Use") / 2, "Use"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[3][0], CONTROLS_Y1, INVENTORY_POINTS[3][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[3][2] - screen.stringWidth("Give") / 2, "Give"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[4][0], CONTROLS_Y1, INVENTORY_POINTS[4][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[4][2], "^^"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[5][0], CONTROLS_Y1, INVENTORY_POINTS[5][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[5][2], "^"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[6][0], CONTROLS_Y1, INVENTORY_POINTS[6][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[6][2], "_"); + screen.makeButton(Common::Rect(INVENTORY_POINTS[7][0], CONTROLS_Y1, INVENTORY_POINTS[7][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[7][2], "__"); + + if (tempMode == INVENTORY_DONT_DISPLAY) + mode = LOOK_INVENTORY_MODE; + _invMode = (InvMode)mode; + + if (mode != PLAIN_INVENTORY) { + ui._oldKey = INVENTORY_COMMANDS[(int)mode]; + } else { + ui._oldKey = -1; + } + + invCommands(0); + putInv(SLAM_DONT_DISPLAY); + + if (tempMode != INVENTORY_DONT_DISPLAY) { + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(false, CONTROLS_Y1); + } + + ui._windowOpen = true; + } else { + // Reset the screen back buffer to the first buffer now that drawing is done + screen._backBuffer = &screen._backBuffer1; + } + + ui._oldUse = -1; +} + +void Inventory::invCommands(bool slamIt) { + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + + if (slamIt) { + screen.buttonPrint(Common::Point(INVENTORY_POINTS[0][2], CONTROLS_Y1), + _invMode == INVMODE_EXIT ? COMMAND_HIGHLIGHTED :COMMAND_FOREGROUND, + true, "Exit"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), + _invMode == INVMODE_LOOK ? COMMAND_HIGHLIGHTED :COMMAND_FOREGROUND, + true, "Look"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), + _invMode == INVMODE_USE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + true, "Use"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), + _invMode == INVMODE_GIVE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + true, "Give"); + screen.print(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1 + 1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^^"); + screen.print(Common::Point(INVENTORY_POINTS[5][2], CONTROLS_Y1 + 1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^"); + screen.print(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1 + 1), + (_holdings - _invIndex <= 6) ? COMMAND_NULL : COMMAND_FOREGROUND, + "_"); + screen.print(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1 + 1), + (_holdings - _invIndex <= 6) ? COMMAND_NULL : COMMAND_FOREGROUND, + "__"); + if (_invMode != INVMODE_LOOK) + ui.clearInfo(); + } else { + screen.buttonPrint(Common::Point(INVENTORY_POINTS[0][2], CONTROLS_Y1), + _invMode == INVMODE_EXIT ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, "Exit"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), + _invMode == INVMODE_LOOK ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, "Look"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), + _invMode == INVMODE_USE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, "Use"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), + _invMode == INVMODE_GIVE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, "Give"); + screen.gPrint(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^^"); + screen.gPrint(Common::Point(INVENTORY_POINTS[5][2], CONTROLS_Y1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^"); + screen.gPrint(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1), + (_holdings - _invIndex < 7) ? COMMAND_NULL : COMMAND_FOREGROUND, + "_"); + screen.gPrint(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1), + (_holdings - _invIndex < 7) ? COMMAND_NULL : COMMAND_FOREGROUND, + "__"); + } +} + +void Inventory::highlight(int index, byte color) { + Screen &screen = *_vm->_screen; + Surface &bb = *screen._backBuffer; + int slot = index - _invIndex; + ImageFrame &frame = (*_invShapes[slot])[0]; + + bb.fillRect(Common::Rect(8 + slot * 52, 165, (slot + 1) * 52, 194), color); + bb.transBlitFrom(frame, Common::Point(6 + slot * 52 + ((47 - frame._width) / 2), + 163 + ((33 - frame._height) / 2))); + screen.slamArea(8 + slot * 52, 165, 44, 30); +} + +void Inventory::refreshInv() { + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + + ui._invLookFlag = true; + freeInv(); + + ui._infoFlag = true; + ui.clearInfo(); + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(0, CONTROLS_Y), + Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + ui.examine(); + + if (!talk._talkToAbort) { + screen._backBuffer2.blitFrom((*ui._controlPanel)[0], Common::Point(0, CONTROLS_Y)); + loadInv(); + } +} + +int Inventory::putNameInInventory(const Common::String &name) { + Scene &scene = *_vm->_scene; + int matches = 0; + + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &o = scene._bgShapes[idx]; + if (name.equalsIgnoreCase(o._name) && o._type != INVALID) { + putItemInInventory(o); + ++matches; + } + } + + return matches; +} + +int Inventory::putItemInInventory(Object &obj) { + Scene &scene = *_vm->_scene; + int matches = 0; + bool pickupFound = false; + + if (obj._pickupFlag) + _vm->setFlags(obj._pickupFlag); + + for (int useNum = 0; useNum < USE_COUNT; ++useNum) { + if (obj._use[useNum]._target.equalsIgnoreCase("*PICKUP*")) { + pickupFound = true; + + for (int namesNum = 0; namesNum < NAMES_COUNT; ++namesNum) { + for (uint bgNum = 0; bgNum < scene._bgShapes.size(); ++bgNum) { + Object &bgObj = scene._bgShapes[bgNum]; + if (obj._use[useNum]._names[namesNum].equalsIgnoreCase(bgObj._name)) { + copyToInventory(bgObj); + if (bgObj._pickupFlag) + _vm->setFlags(bgObj._pickupFlag); + + if (bgObj._type == ACTIVE_BG_SHAPE || bgObj._type == NO_SHAPE || bgObj._type == HIDE_SHAPE) { + if (bgObj._imageFrame == nullptr || bgObj._frameNumber < 0) + // No shape to erase, so flag as hidden + bgObj._type = INVALID; + else + bgObj._type = REMOVE; + } else if (bgObj._type == HIDDEN) { + bgObj._type = INVALID; + } + + ++matches; + } + } + } + } + } + + if (!pickupFound) { + // No pickup item found, so add the passed item + copyToInventory(obj); + matches = 0; + } + + if (matches == 0) { + if (!pickupFound) + matches = 1; + + if (obj._type == ACTIVE_BG_SHAPE || obj._type == NO_SHAPE || obj._type == HIDE_SHAPE) { + if (obj._imageFrame == nullptr || obj._frameNumber < 0) + // No shape to erase, so flag as hidden + obj._type = INVALID; + else + obj._type = REMOVE; + } else if (obj._type == HIDDEN) { + obj._type = INVALID; + } + } + + return matches; +} + +void Inventory::copyToInventory(Object &obj) { + InventoryItem invItem; + invItem._name = obj._name; + invItem._description = obj._description; + invItem._examine = obj._examine; + invItem._lookFlag = obj._lookFlag; + invItem._requiredFlag = obj._requiredFlag; + + insert_at(_holdings, invItem); + ++_holdings; +} + +int Inventory::deleteItemFromInventory(const Common::String &name) { + int invNum = -1; + + for (int idx = 0; idx < (int)size() && invNum == -1; ++idx) { + if (name.equalsIgnoreCase((*this)[idx]._name)) + invNum = idx; + } + + if (invNum == -1) + // Item not present + return 0; + + // Item found, so delete it + remove_at(invNum); + --_holdings; + + return 1; +} + +void Inventory::synchronize(Common::Serializer &s) { + s.syncAsSint16LE(_holdings); + + uint count = size(); + s.syncAsUint16LE(count); + if (s.isLoading()) { + resize(count); + + // Reset inventory back to start + _invIndex = 0; + } + + for (uint idx = 0; idx < size(); ++idx) { + (*this)[idx].synchronize(s); + + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/inventory.h b/engines/sherlock/inventory.h new file mode 100644 index 0000000000..02f570f5da --- /dev/null +++ b/engines/sherlock/inventory.h @@ -0,0 +1,171 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_INVENTORY_H +#define SHERLOCK_INVENTORY_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/serializer.h" +#include "common/str-array.h" +#include "sherlock/objects.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +#define MAX_VISIBLE_INVENTORY 6 + +enum InvMode { + INVMODE_EXIT = 0, + INVMODE_LOOK = 1, + INVMODE_USE = 2, + INVMODE_GIVE = 3, + INVMODE_FIRST = 4, + INVMODE_PREVIOUS = 5, + INVMODE_NEXT = 6, + INVMODE_LAST = 7, + INVMODE_INVALID = 8, + INVMODE_USE55 = 255 +}; + +enum InvNewMode { + PLAIN_INVENTORY = 0, LOOK_INVENTORY_MODE = 1, USE_INVENTORY_MODE = 2, + GIVE_INVENTORY_MODE = 3, INVENTORY_DONT_DISPLAY = 128 +}; + +enum InvSlamMode { SLAM_DONT_DISPLAY, SLAM_DISPLAY = 1, SLAM_SECONDARY_BUFFER }; + + +struct InventoryItem { + int _requiredFlag; + Common::String _name; + Common::String _description; + Common::String _examine; + int _lookFlag; + + InventoryItem() : _requiredFlag(0), _lookFlag(0) {} + InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine); + + /** + * Synchronize the data for an inventory item + */ + void synchronize(Common::Serializer &s); +}; + +class Inventory : public Common::Array<InventoryItem> { +private: + SherlockEngine *_vm; + Common::StringArray _names; + + /** + * Copy the passed object into the inventory + */ + void copyToInventory(Object &obj); +public: + ImageFile *_invShapes[MAX_VISIBLE_INVENTORY]; + bool _invGraphicsLoaded; + InvMode _invMode; + int _invIndex; + int _holdings; // Used to hold number of visible items in active inventory. + // Since Inventory array also contains some special hidden items + /** + * Free any loaded inventory graphics + */ + void freeGraphics(); +public: + Inventory(SherlockEngine *vm); + ~Inventory(); + + /** + * Free inventory data + */ + void freeInv(); + + /** + * Load the list of names the inventory items correspond to, if not already loaded, + * and then calls loadGraphics to load the associated graphics + */ + void loadInv(); + + /** + * Load the list of names of graphics for the inventory + */ + void loadGraphics(); + + /** + * Searches through the list of names that correspond to the inventory items + * and returns the number that matches the passed name + */ + int findInv(const Common::String &name); + + /** + * Display the character's inventory. The slamIt parameter specifies: + */ + void putInv(InvSlamMode slamIt); + + /** + * Put the game into inventory mode and open the interface window. + */ + void drawInventory(InvNewMode flag); + + /** + * Prints the line of inventory commands at the top of an inventory window with + * the correct highlighting + */ + void invCommands(bool slamIt); + + /** + * Set the highlighting color of a given inventory item + */ + void highlight(int index, byte color); + + /** + * Support method for refreshing the display of the inventory + */ + void refreshInv(); + + /** + * Adds a shape from the scene to the player's inventory + */ + int putNameInInventory(const Common::String &name); + + /** + * Moves a specified item into the player's inventory If the item has a *PICKUP* use action, + * then the item in the use action are added to the inventory. + */ + int putItemInInventory(Object &obj); + + /** + * Deletes a specified item from the player's inventory + */ + int deleteItemFromInventory(const Common::String &name); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/journal.cpp b/engines/sherlock/journal.cpp new file mode 100644 index 0000000000..564db59042 --- /dev/null +++ b/engines/sherlock/journal.cpp @@ -0,0 +1,1190 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/journal.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +#define JOURNAL_BUTTONS_Y 178 +#define LINES_PER_PAGE 11 +#define JOURNAL_SEARCH_LEFT 15 +#define JOURNAL_SEARCH_TOP 186 +#define JOURNAL_SEARCH_RIGHT 296 +#define JOURNAL_SEACRH_MAX_CHARS 50 + +// Positioning of buttons in the journal view +static const int JOURNAL_POINTS[9][3] = { + { 6, 68, 37 }, + { 69, 131, 100 }, + { 132, 192, 162 }, + { 193, 250, 221 }, + { 251, 313, 281 }, + { 6, 82, 44 }, + { 83, 159, 121 }, + { 160, 236, 198 }, + { 237, 313, 275 } +}; + +static const int SEARCH_POINTS[3][3] = { + { 51, 123, 86 }, + { 124, 196, 159 }, + { 197, 269, 232 } +}; + +/*----------------------------------------------------------------*/ + +Journal::Journal(SherlockEngine *vm) : _vm(vm) { + // Initialize fields + _maxPage = 0; + _index = 0; + _sub = 0; + _up = _down = false; + _page = 1; + + if (_vm->_interactiveFl) { + // Load the journal directory and location names + loadJournalLocations(); + } +} + +void Journal::record(int converseNum, int statementNum, bool replyOnly) { + int saveIndex = _index; + int saveSub = _sub; + + // Record the entry into the list + _journal.push_back(JournalEntry(converseNum, statementNum, replyOnly)); + _index = _journal.size() - 1; + + // Load the text for the new entry to get the number of lines it will have + loadJournalFile(true); + + // Restore old state + _index = saveIndex; + _sub = saveSub; + + // If new lines were added to the ournal, update the total number of lines + // the journal continues + if (!_lines.empty()) { + _maxPage += _lines.size(); + } else { + // No lines in entry, so remove the new entry from the journal + _journal.remove_at(_journal.size() - 1); + } +} + +void Journal::loadJournalLocations() { + Resources &res = *_vm->_res; + + _directory.clear(); + + Common::SeekableReadStream *dir = res.load("talk.lib"); + dir->skip(4); // Skip header + + // Get the numer of entries + _directory.resize(dir->readUint16LE()); + + // Read in each entry + char buffer[17]; + for (uint idx = 0; idx < _directory.size(); ++idx) { + dir->read(buffer, 17); + buffer[16] = '\0'; + + _directory[idx] = Common::String(buffer); + } + + delete dir; + + // Load in the locations stored in journal.txt + Common::SeekableReadStream *loc = res.load("journal.txt"); + + _locations.clear(); + while (loc->pos() < loc->size()) { + Common::String line; + char c; + while ((c = loc->readByte()) != 0) + line += c; + + _locations.push_back(line); + } + + delete loc; +} + +void Journal::loadJournalFile(bool alreadyLoaded) { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + JournalEntry &journalEntry = _journal[_index]; + + Common::String dirFilename = _directory[journalEntry._converseNum]; + bool replyOnly = journalEntry._replyOnly; + + // Get the location number from within the filename + Common::String locStr(dirFilename.c_str() + 4, dirFilename.c_str() + 6); + int newLocation = atoi(locStr.c_str()); + + // If not flagged as alrady loaded, load the conversation into script variables + if (!alreadyLoaded) { + // See if the file to be used is already loaded + if (journalEntry._converseNum != talk._converseNum) { + // Nope. Free any previously loaded talk + talk.freeTalkVars(); + + // Find the person being referred to + talk._talkTo = -1; + for (int idx = 0; idx < (int)people._characters.size(); ++idx) { + Common::String portrait = people[idx]._portrait; + Common::String numStr(portrait.c_str(), portrait.c_str() + 4); + + if (locStr == numStr) { + talk._talkTo = idx; + break; + } + } + + // Load their talk file + talk.loadTalkFile(dirFilename); + } + } + + if (talk[0]._statement.hasPrefix("*") || talk[0]._statement.hasPrefix("^")) + replyOnly = true; + + // If this isn't the first journal entry, see if the previous journal entry + // was in the same scene to see if we need to include the scene header + int oldLocation = -1; + if (_index != 0) { + // Get the scene number of the prior journal entry + Common::String priorEntry = _directory[_journal[_index - 1]._converseNum]; + oldLocation = atoi(Common::String(priorEntry.c_str() + 4, priorEntry.c_str() + 6).c_str()); + } + + // Start building journal string + Statement &statement = talk[journalEntry._statementNum]; + Common::String journalString; + + if (newLocation != oldLocation) { + // Add in scene title + journalString = "@" + _locations[newLocation - 1] + ":"; + + // See if title can fit into a single line, or requires splitting on 2 lines + int width = screen.stringWidth(journalString.c_str() + 1); + if (width > JOURNAL_MAX_WIDTH) { + // Scan backwards from end of title to find a space between a word + // where the width is less than the maximum allowed for the line + const char *lineP = journalString.c_str() + journalString.size() - 1; + while (width > JOURNAL_MAX_WIDTH || *lineP != ' ') + width -= screen.charWidth(*lineP--); + + // Split the header into two lines, and add a '@' prefix + // to the second line as well + journalString = Common::String(journalString.c_str(), lineP) + "\n@" + + Common::String(lineP + 1); + } + + // Add a newline at the end of the title + journalString += '\n'; + } + + // If Holmes has something to say first, then take care of it + if (!replyOnly) { + // Handle the grammar + journalString += "Holmes "; + if (talk[journalEntry._statementNum]._statement.hasSuffix("?")) + journalString += "asked "; + else + journalString += "said to "; + + switch (talk._talkTo) { + case 1: + journalString += "me"; + break; + case 2: + journalString += "the Inspector"; + break; + default: + journalString += people._characters[talk._talkTo]._name; + break; + } + journalString += ", \""; + + // Add the statement + journalString += statement._statement; + } + + // Handle including the reply + bool startOfReply = true; + bool ctrlSpace = false; + bool commentFlag = false; + bool commentJustPrinted = false; + const byte *replyP = (const byte *)statement._reply.c_str(); + + while (*replyP) { + byte c = *replyP++; + + // Is it a control character? + if (c < SWITCH_SPEAKER) { + // Nope. Set flag for allowing control codes to insert spaces + ctrlSpace = true; + assert(c >= ' '); + + // Check for embedded comments + if (c == '{' || c == '}') { + // Comment characters. If we're starting a comment and there's + // already text displayed, add a closing quote + if (c == '{' && !startOfReply && !commentJustPrinted) + journalString += '"'; + + // If a reply isn't just being started, and we didn't just end + // a comment (which would have added a line), add a carriage return + if (!startOfReply && ((!commentJustPrinted && c == '{') || c == '}')) + journalString += '\n'; + startOfReply = false; + + // Handle setting or clearing comment state + if (c == '{') { + commentFlag = true; + commentJustPrinted = false; + } else { + commentFlag = false; + commentJustPrinted = true; + } + } else { + if (startOfReply) { + if (!replyOnly) { + journalString += "\"\n"; + + if (talk._talkTo == 1) + journalString += "I replied, \""; + else + journalString += "The reply was, \""; + } else { + if (talk._talkTo == 1) + journalString += "I"; + else if (talk._talkTo == 2) + journalString += "The Inspector"; + else + journalString += people._characters[talk._talkTo]._name; + + const byte *strP = replyP + 1; + byte v; + do { + v = *strP++; + } while (v && (v < SWITCH_SPEAKER) && (v != '.') && (v != '!') && (v != '?')); + + if (v == '?') + journalString += " asked, \""; + else + journalString += " said, \""; + } + + startOfReply = false; + } + + // Copy text from the place until either the reply ends, a comment + // {} block is started, or a control character is encountered + journalString += c; + do { + journalString += *replyP++; + } while (*replyP && *replyP < SWITCH_SPEAKER && *replyP != '{' && *replyP != '}'); + + commentJustPrinted = false; + } + } else if (c == SWITCH_SPEAKER) { + if (!startOfReply) { + if (!commentFlag && !commentJustPrinted) + journalString += "\"\n"; + + journalString += "Then "; + commentFlag = false; + } else if (!replyOnly) { + journalString += "\"\n"; + } + + startOfReply = false; + c = *replyP++ - 1; + + if (c == 0) + journalString += "Holmes"; + else if (c == 1) + journalString += "I"; + else if (c == 2) + journalString += "the Inspector"; + else + journalString += people._characters[c]._name; + + const byte *strP = replyP; + byte v; + do { + v = *strP++; + } while (v && v < SWITCH_SPEAKER && v != '.' && v != '!' && v != '?'); + + if (v == '?') + journalString += " asked, \""; + else + journalString += " said, \""; + } else { + // Control code, so move past it and any parameters + switch (c) { + case RUN_CANIMATION: + case ASSIGN_PORTRAIT_LOCATION: + case PAUSE: + case PAUSE_WITHOUT_CONTROL: + case WALK_TO_CANIMATION: + // These commands have a single parameter + ++replyP; + break; + + case ADJUST_OBJ_SEQUENCE: + replyP += (replyP[0] & 127) + replyP[1] + 2; + break; + + case WALK_TO_COORDS: + case MOVE_MOUSE: + replyP += 4; + break; + + case SET_FLAG: + case IF_STATEMENT: + replyP += 2; + break; + + case SFX_COMMAND: + case PLAY_PROLOGUE: + case CALL_TALK_FILE: + replyP += 8; + break; + + case TOGGLE_OBJECT: + case ADD_ITEM_TO_INVENTORY: + case SET_OBJECT: + case DISPLAY_INFO_LINE: + case REMOVE_ITEM_FROM_INVENTORY: + replyP += (*replyP & 127) + 1; + break; + + case GOTO_SCENE: + replyP += 5; + break; + + case CARRIAGE_RETURN: + journalString += "\n"; + break; + + default: + break; + } + + // Put a space in the output for a control character, unless it's + // immediately coming after another control character + if (ctrlSpace && c != ASSIGN_PORTRAIT_LOCATION && c != CARRIAGE_RETURN && !commentJustPrinted) { + journalString += " "; + ctrlSpace = false; + } + } + } + + if (!startOfReply && !commentJustPrinted) + journalString += '"'; + + // Finally finished building the journal text. Need to process the text to + // word wrap it to fit on-screen. The resulting lines are stored in the + // _lines array + _lines.clear(); + + while (!journalString.empty()) { + const char *startP = journalString.c_str(); + + // If the first character is a '@' flagging a title line, then move + // past it, so the @ won't be included in the line width calculation + if (*startP == '@') + ++startP; + + // Build up chacters until a full line is found + int width = 0; + const char *endP = startP; + while (width < JOURNAL_MAX_WIDTH && *endP && *endP != '\n' && (endP - startP) < (JOURNAL_MAX_CHARS - 1)) + width += screen.charWidth(*endP++); + + // If word wrapping, move back to end of prior word + if (width >= JOURNAL_MAX_WIDTH || (endP - startP) >= (JOURNAL_MAX_CHARS - 1)) { + while (*--endP != ' ') + ; + } + + // Add in the line + _lines.push_back(Common::String(journalString.c_str(), endP)); + + // Strip line off from string being processed + journalString = *endP ? Common::String(endP + 1) : ""; + } + + // Add a blank line at the end of the text as long as text was present + if (!startOfReply) { + _lines.push_back(""); + } else { + _lines.clear(); + } +} + +void Journal::drawJournalFrame() { + Resources &res = *_vm->_res; + Screen &screen = *_vm->_screen; + byte palette[PALETTE_SIZE]; + + // Load in the journal background + Common::SeekableReadStream *bg = res.load("journal.lbv"); + bg->read(screen._backBuffer1.getPixels(), SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCREEN_HEIGHT); + bg->read(palette, PALETTE_SIZE); + delete bg; + + // Translate the palette for display + for (int idx = 0; idx < PALETTE_SIZE; ++idx) + palette[idx] = VGA_COLOR_TRANS(palette[idx]); + + // Set the palette and print the title + screen.setPalette(palette); + screen.gPrint(Common::Point(111, 18), BUTTON_BOTTOM, "Watson's Journal"); + screen.gPrint(Common::Point(110, 17), INV_FOREGROUND, "Watson's Journal"); + + // Draw the buttons + screen.makeButton(Common::Rect(JOURNAL_POINTS[0][0], JOURNAL_BUTTONS_Y, + JOURNAL_POINTS[0][1], JOURNAL_BUTTONS_Y + 10), + JOURNAL_POINTS[0][2] - screen.stringWidth("Exit") / 2, "Exit"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[1][0], JOURNAL_BUTTONS_Y, + JOURNAL_POINTS[1][1], JOURNAL_BUTTONS_Y + 10), + JOURNAL_POINTS[1][2] - screen.stringWidth("Back 10") / 2, "Back 10"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[2][0], JOURNAL_BUTTONS_Y, + JOURNAL_POINTS[2][1], JOURNAL_BUTTONS_Y + 10), + JOURNAL_POINTS[2][2] - screen.stringWidth("Up") / 2, "Up"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[3][0], JOURNAL_BUTTONS_Y, + JOURNAL_POINTS[3][1], JOURNAL_BUTTONS_Y + 10), + JOURNAL_POINTS[3][2] - screen.stringWidth("Down") / 2, "Down"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[4][0], JOURNAL_BUTTONS_Y, + JOURNAL_POINTS[4][1], JOURNAL_BUTTONS_Y + 10), + JOURNAL_POINTS[4][2] - screen.stringWidth("Ahead 10") / 2, "Ahead 10"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[5][0], JOURNAL_BUTTONS_Y + 11, + JOURNAL_POINTS[5][1], JOURNAL_BUTTONS_Y + 21), + JOURNAL_POINTS[5][2] - screen.stringWidth("Search") / 2, "Search"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[6][0], JOURNAL_BUTTONS_Y + 11, + JOURNAL_POINTS[6][1], JOURNAL_BUTTONS_Y + 21), + JOURNAL_POINTS[6][2] - screen.stringWidth("First Page") / 2, "First Page"); + screen.makeButton(Common::Rect(JOURNAL_POINTS[7][0], JOURNAL_BUTTONS_Y + 11, + JOURNAL_POINTS[7][1], JOURNAL_BUTTONS_Y + 21), + JOURNAL_POINTS[7][2] - screen.stringWidth("Last Page") / 2, "Last Page"); + + // WORKAROUND: Draw Print Text button as disabled, since we don't support it in ScummVM + screen.makeButton(Common::Rect(JOURNAL_POINTS[8][0], JOURNAL_BUTTONS_Y + 11, + JOURNAL_POINTS[8][1], JOURNAL_BUTTONS_Y + 21), + JOURNAL_POINTS[8][2] - screen.stringWidth("Print Text") / 2, "Print Text"); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), + COMMAND_NULL, false, "Print Text"); +} + +void Journal::drawInterface() { + Screen &screen = *_vm->_screen; + + drawJournalFrame(); + + if (_journal.empty()) { + _up = _down = 0; + } else { + drawJournal(0, 0); + } + + doArrows(); + + // Show the entire screen + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); +} + +void Journal::doArrows() { + Screen &screen = *_vm->_screen; + byte color; + + color = (_page > 1) ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), color, false, "Back 10"); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), color, false, "Up"); + + color = _down ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), color, false, "Down"); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), color, false, "Ahead 10"); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[7][2], JOURNAL_BUTTONS_Y + 11), color, false, "Last Page"); + + color = _journal.size() > 0 ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[5][2], JOURNAL_BUTTONS_Y + 11), color, false, "Search"); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), COMMAND_NULL, false, "Print Text"); + + color = _page > 1 ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[6][2], JOURNAL_BUTTONS_Y + 11), color, false, "First Page"); +} + +bool Journal::drawJournal(int direction, int howFar) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + int yp = 37; + int startPage = _page; + bool endJournal = false; + bool firstOccurance = true; + bool searchSuccessful = false; + bool endFlag = false; + int lineNum = 0; + int savedIndex; + int temp; + const char *matchP; + int width; + + talk._converseNum = -1; + _down = true; + + do { + // Get the number of lines for the current journal entry + loadJournalFile(false); + if (_lines.empty()) { + // Entry has no text, so it must be a stealth eny. Move onto further journal entries + // until an entry with text is found + if (++_index == (int)_journal.size()) { + endJournal = true; + } else { + _sub = 0; + loadJournalFile(false); + } + } + } while (!endJournal && _lines.empty()); + + // Check if there no further pages with text until the end of the journal + if (endJournal) { + // If moving forward or backwards, clear the page before printing + if (direction) + drawJournalFrame(); + + screen.gPrint(Common::Point(235, 21), PEN_COLOR, "Page %d", _page); + return false; + } + + // If the journal page is being changed, set the wait cursor + if (direction) + events.setCursor(WAIT); + + switch (direction) { + case 1: + case 4: + // Move backwards howFar number of lines unless either the start of the journal is reached, + // or a searched for keyword is found + do { + // Animate the glass mouse cursor + int cursorNum = (int)events.getCursor() + 1; + if (cursorNum > (WAIT + 2)) + cursorNum = WAIT; + events.setCursor((CursorId)cursorNum); + + // Move backwards through the journal file a line at a time + if (--_sub < 0) { + do { + if (--_index < 0) { + _index = 0; + _sub = 0; + endJournal = true; + } + else { + loadJournalFile(false); + _sub = _lines.size() - 1; + } + } while (!endJournal && _lines.empty()); + } + + // If it's search mode, check each line for the given keyword + if (direction >= 3 && !_lines.empty() && !endJournal && !searchSuccessful) { + Common::String line = _lines[_sub]; + line.toUppercase(); + if (strstr(line.c_str(), _find.c_str()) != nullptr) { + // Found a match. Reset howFar so that the start of page that the match + // was found on will be displayed + searchSuccessful = true; + howFar = ((lineNum / LINES_PER_PAGE) + 1) * LINES_PER_PAGE; + } + } + + ++lineNum; + } while (lineNum < howFar && !endJournal); + + if (!_index && !_sub) + _page = 1; + else + _page -= howFar / LINES_PER_PAGE; + break; + + case 2: + case 3: + // Move howFar lines ahead unless the end of the journal is reached, + // or a searched for keyword is found + for (temp = 0; (temp < (howFar / LINES_PER_PAGE)) && !endJournal && !searchSuccessful; ++temp) { + // Handle animating mouse cursor + int cursorNum = (int)events.getCursor() + 1; + if (cursorNum >(WAIT + 2)) + cursorNum = WAIT; + events.setCursor((CursorId)cursorNum); + + lineNum = 0; + savedIndex = _index; + int savedSub = _sub; + + // Move a single page ahead + do { + // If in search mode, check for keyword + if (direction >= 3 && _page != startPage) { + Common::String line = _lines[_sub]; + line.toUppercase(); + if (strstr(line.c_str(), _find.c_str()) != nullptr) + searchSuccessful = true; + } + + // Move forwards a line at a time, unless search word was found + if (!searchSuccessful) { + if (++_sub == (int)_lines.size()) { + // Reached end of page + do { + if (++_index == (int)_journal.size()) { + _index = savedIndex; + _sub = savedSub; + loadJournalFile(false); + endJournal = true; + } else { + _sub = 0; + loadJournalFile(false); + } + } while (!endJournal && _lines.empty()); + } + + ++lineNum; + } + } while ((lineNum < LINES_PER_PAGE) && !endJournal && !searchSuccessful); + + if (!endJournal && !searchSuccessful) + // Move to next page + ++_page; + + if (searchSuccessful) { + // Search found, so show top of the page it was found on + _index = savedIndex; + _sub = savedSub; + loadJournalFile(false); + } + } + break; + + default: + break; + } + + if (direction) { + events.setCursor(ARROW); + drawJournalFrame(); + } + + screen.gPrint(Common::Point(235, 21), PEN_COLOR, "Page %d", _page); + + temp = _sub; + savedIndex = _index; + lineNum = 0; + + do { + bool inc = true; + + // If there wasn't any line to print at the top of the page, we won't need to + // increment the y position + if (_lines[temp].empty() && yp == 37) + inc = false; + + // If there's a searched for keyword in the line, it will need to be highlighted + if (searchSuccessful && firstOccurance) { + // Check if line has the keyword + Common::String line = _lines[temp]; + line.toUppercase(); + if ((matchP = strstr(line.c_str(), _find.c_str())) != nullptr) { + matchP = _lines[temp].c_str() + (matchP - line.c_str()); + firstOccurance = false; + + // Print out the start of the line before the matching keyword + Common::String lineStart(_lines[temp].c_str(), matchP); + if (lineStart.hasPrefix("@")) { + width = screen.stringWidth(lineStart.c_str() + 1); + screen.gPrint(Common::Point(53, yp), 15, "%s", lineStart.c_str() + 1); + } else { + width = screen.stringWidth(lineStart.c_str()); + screen.gPrint(Common::Point(53, yp), PEN_COLOR, "%s", lineStart.c_str()); + } + + // Print out the found keyword + Common::String lineMatch(matchP, matchP + _find.size()); + screen.gPrint(Common::Point(53 + width, yp), INV_FOREGROUND, "%s", lineMatch.c_str()); + width += screen.stringWidth(lineMatch.c_str()); + + // Print remainder of line + screen.gPrint(Common::Point(53 + width, yp), PEN_COLOR, "%s", matchP + _find.size()); + } else if (_lines[temp].hasPrefix("@")) { + screen.gPrint(Common::Point(53, yp), 15, "%s", _lines[temp].c_str() + 1); + } else { + screen.gPrint(Common::Point(53, yp), PEN_COLOR, "%s", _lines[temp].c_str()); + } + } else { + if (_lines[temp].hasPrefix("@")) { + screen.gPrint(Common::Point(53, yp), 15, "%s", _lines[temp].c_str() + 1); + } else { + screen.gPrint(Common::Point(53, yp), PEN_COLOR, "%s", _lines[temp].c_str()); + } + } + + if (++temp == (int)_lines.size()) { + // Move to next page + do { + if (_index < ((int)_journal.size() - 1) && lineNum < (LINES_PER_PAGE - 1)) { + ++_index; + loadJournalFile(false); + temp = 0; + } else { + if (_index == ((int)_journal.size() - 1)) + _down = false; + endFlag = true; + } + } while (!endFlag && _lines.empty()); + } + + if (inc) { + // Move to next line + ++lineNum; + yp += 13; + } + } while (lineNum < LINES_PER_PAGE && !endFlag); + + _index = savedIndex; + _up = _index || _sub; + + return direction >= 3 && searchSuccessful; +} + +JournalButton Journal::getHighlightedButton(const Common::Point &pt) { + if (pt.x > JOURNAL_POINTS[0][0] && pt.x < JOURNAL_POINTS[0][1] && pt.y >= JOURNAL_BUTTONS_Y && + pt.y < (JOURNAL_BUTTONS_Y + 10)) + return BTN_EXIT; + + if (pt.x > JOURNAL_POINTS[1][0] && pt.x < JOURNAL_POINTS[1][1] && pt.y >= JOURNAL_BUTTONS_Y && + pt.y < (JOURNAL_BUTTONS_Y + 10) && _page > 1) + return BTN_BACK10; + + if (pt.x > JOURNAL_POINTS[2][0] && pt.x < JOURNAL_POINTS[2][1] && pt.y >= JOURNAL_BUTTONS_Y && + pt.y < (JOURNAL_BUTTONS_Y + 10) && _up) + return BTN_UP; + + if (pt.x > JOURNAL_POINTS[3][0] && pt.x < JOURNAL_POINTS[3][1] && pt.y >= JOURNAL_BUTTONS_Y && + pt.y < (JOURNAL_BUTTONS_Y + 10) && _down) + return BTN_DOWN; + + if (pt.x > JOURNAL_POINTS[4][0] && pt.x < JOURNAL_POINTS[4][1] && pt.y >= JOURNAL_BUTTONS_Y && + pt.y < (JOURNAL_BUTTONS_Y + 10) && _down) + return BTN_AHEAD110; + + if (pt.x > JOURNAL_POINTS[5][0] && pt.x < JOURNAL_POINTS[5][1] && pt.y >= (JOURNAL_BUTTONS_Y + 11) && + pt.y < (JOURNAL_BUTTONS_Y + 20) && !_journal.empty()) + return BTN_SEARCH; + + if (pt.x > JOURNAL_POINTS[6][0] && pt.x < JOURNAL_POINTS[6][1] && pt.y >= (JOURNAL_BUTTONS_Y + 11) && + pt.y < (JOURNAL_BUTTONS_Y + 20) && _up) + return BTN_FIRST_PAGE; + + if (pt.x > JOURNAL_POINTS[7][0] && pt.x < JOURNAL_POINTS[7][1] && pt.y >= (JOURNAL_BUTTONS_Y + 11) && + pt.y < (JOURNAL_BUTTONS_Y + 20) && _down) + return BTN_LAST_PAGE; + + if (pt.x > JOURNAL_POINTS[8][0] && pt.x < JOURNAL_POINTS[8][1] && pt.y >= (JOURNAL_BUTTONS_Y + 11) && + pt.y < (JOURNAL_BUTTONS_Y + 20) && !_journal.empty()) + return BTN_PRINT_TEXT; + + return BTN_NONE; +} + +bool Journal::handleEvents(int key) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + bool doneFlag = false; + + Common::Point pt = events.mousePos(); + JournalButton btn = getHighlightedButton(pt); + byte color; + + if (events._pressed || events._released) { + // Exit button + color = (btn == BTN_EXIT) ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[0][2], JOURNAL_BUTTONS_Y), color, true, "Exit"); + + // Back 10 button + if (btn == BTN_BACK10) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, "Back 10"); + } else if (_page > 1) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, "Back 10"); + } + + // Up button + if (btn == BTN_UP) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, "Up"); + } else if (_up) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, "Up"); + } + + // Down button + if (btn == BTN_DOWN) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, "Down"); + } else if (_down) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, "Down"); + } + + // Ahead 10 button + if (btn == BTN_AHEAD110) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, "Ahead 10"); + } else if (_down) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, "Ahead 10"); + } + + // Search button + if (btn == BTN_SEARCH) { + color = COMMAND_HIGHLIGHTED; + } else if (_journal.empty()) { + color = COMMAND_NULL; + } else { + color = COMMAND_FOREGROUND; + } + screen.buttonPrint(Common::Point(JOURNAL_POINTS[5][2], JOURNAL_BUTTONS_Y + 11), color, true, "Search"); + + // First Page button + if (btn == BTN_FIRST_PAGE) { + color = COMMAND_HIGHLIGHTED; + } else if (_up) { + color = COMMAND_FOREGROUND; + } else { + color = COMMAND_NULL; + } + screen.buttonPrint(Common::Point(JOURNAL_POINTS[6][2], JOURNAL_BUTTONS_Y + 11), color, true, "First Page"); + + // Last Page button + if (btn == BTN_LAST_PAGE) { + color = COMMAND_HIGHLIGHTED; + } else if (_down) { + color = COMMAND_FOREGROUND; + } else { + color = COMMAND_NULL; + } + screen.buttonPrint(Common::Point(JOURNAL_POINTS[7][2], JOURNAL_BUTTONS_Y + 11), color, true, "Last Page"); + + // Print Text button + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), COMMAND_NULL, true, "Print Text"); + } + + if (btn == BTN_EXIT && events._released) { + // Exit button pressed + doneFlag = true; + + } else if (((btn == BTN_BACK10 && events._released) || key == 'B') && (_page > 1)) { + // Scrolll up 10 pages + if (_page < 11) + drawJournal(1, (_page - 1) * LINES_PER_PAGE); + else + drawJournal(1, 10 * LINES_PER_PAGE); + + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + } else if (((btn == BTN_UP && events._released) || key == 'U') && _up) { + // Scroll up + drawJournal(1, LINES_PER_PAGE); + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + } else if (((btn == BTN_DOWN && events._released) || key == 'D') && _down) { + // Scroll down + drawJournal(2, LINES_PER_PAGE); + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + } else if (((btn == BTN_AHEAD110 && events._released) || key == 'A') && _down) { + // Scroll down 10 pages + if ((_page + 10) > _maxPage) + drawJournal(2, (_maxPage - _page) * LINES_PER_PAGE); + else + drawJournal(2, 10 * LINES_PER_PAGE); + + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + } else if (((btn == BTN_SEARCH && events._released) || key == 'S') && !_journal.empty()) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[5][2], JOURNAL_BUTTONS_Y + 11), COMMAND_FOREGROUND, true, "Search"); + bool notFound = false; + + do { + int dir; + if ((dir = getSearchString(notFound)) != 0) { + int savedIndex = _index; + int savedSub = _sub; + int savedPage = _page; + + if (drawJournal(dir + 2, 1000 * LINES_PER_PAGE) == 0) { + _index = savedIndex; + _sub = savedSub; + _page = savedPage; + + drawJournalFrame(); + drawJournal(0, 0); + notFound = true; + } else { + doneFlag = true; + } + + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } else { + doneFlag = true; + } + } while (!doneFlag); + doneFlag = false; + + } else if (((btn == BTN_FIRST_PAGE && events._released) || key == 'F') && _up) { + // First page + _index = _sub = 0; + _up = _down = false; + _page = 1; + + drawJournalFrame(); + drawJournal(0, 0); + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + } else if (((btn == BTN_LAST_PAGE && events._released) || key == 'L') && _down) { + // Last page + if ((_page + 10) > _maxPage) + drawJournal(2, (_maxPage - _page) * LINES_PER_PAGE); + else + drawJournal(2, 1000 * LINES_PER_PAGE); + + doArrows(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + + events.wait(2); + + return doneFlag; +} + +int Journal::getSearchString(bool printError) { + enum Button { BTN_NONE, BTN_EXIT, BTN_BACKWARD, BTN_FORWARD }; + + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + int xp; + int yp = 174; + bool flag = false; + Common::String name; + int done = 0; + byte color; + + // Draw search panel + screen.makePanel(Common::Rect(6, 171, 313, 199)); + screen.makeButton(Common::Rect(SEARCH_POINTS[0][0], yp, SEARCH_POINTS[0][1], yp + 10), + SEARCH_POINTS[0][2] - screen.stringWidth("Exit") / 2, "Exit"); + screen.makeButton(Common::Rect(SEARCH_POINTS[1][0], yp, SEARCH_POINTS[1][1], yp + 10), + SEARCH_POINTS[1][2] - screen.stringWidth("Backward") / 2, "Backward"); + screen.makeButton(Common::Rect(SEARCH_POINTS[2][0], yp, SEARCH_POINTS[2][1], yp + 10), + SEARCH_POINTS[2][2] - screen.stringWidth("Forward") / 2, "Forward"); + screen.gPrint(Common::Point(SEARCH_POINTS[0][2] - screen.stringWidth("Exit") / 2, yp), + COMMAND_FOREGROUND, "E"); + screen.gPrint(Common::Point(SEARCH_POINTS[1][2] - screen.stringWidth("Backward") / 2, yp), + COMMAND_FOREGROUND, "B"); + screen.gPrint(Common::Point(SEARCH_POINTS[2][2] - screen.stringWidth("Forward") / 2, yp), + COMMAND_FOREGROUND, "F"); + + screen.makeField(Common::Rect(12, 185, 307, 196)); + + screen.fillRect(Common::Rect(12, 185, 307, 186), BUTTON_BOTTOM); + screen.vLine(12, 185, 195, BUTTON_BOTTOM); + screen.hLine(13, 195, 306, BUTTON_TOP); + screen.hLine(306, 186, 195, BUTTON_TOP); + + if (printError) { + screen.gPrint(Common::Point((SHERLOCK_SCREEN_WIDTH - screen.stringWidth("Text Not Found !")) / 2, 185), + INV_FOREGROUND, "Text Not Found !"); + } else if (!_find.empty()) { + // There's already a search term, display it already + screen.gPrint(Common::Point(15, 185), TALK_FOREGROUND, "%s", _find.c_str()); + name = _find; + } + + screen.slamArea(6, 171, 307, 28); + + if (printError) { + // Give time for user to see the message + events.setButtonState(); + for (int idx = 0; idx < 40 && !_vm->shouldQuit() && !events.kbHit() && !events._released; ++idx) { + events.pollEvents(); + events.setButtonState(); + events.wait(2); + } + + events.clearKeyboard(); + screen._backBuffer1.fillRect(Common::Rect(13, 186, 306, 195), BUTTON_MIDDLE); + + if (!_find.empty()) { + screen.gPrint(Common::Point(15, 185), TALK_FOREGROUND, "%s", _find.c_str()); + name = _find; + } + + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + + xp = JOURNAL_SEARCH_LEFT + screen.stringWidth(name); + yp = JOURNAL_SEARCH_TOP; + + do { + events._released = false; + Button found = BTN_NONE; + + while (!_vm->shouldQuit() && !events.kbHit() && !events._released) { + found = BTN_NONE; + if (talk._talkToAbort) + return 0; + + // Check if key or mouse button press has occurred + events.setButtonState(); + Common::Point pt = events.mousePos(); + + flag = !flag; + screen.vgaBar(Common::Rect(xp, yp, xp + 8, yp + 9), flag ? INV_FOREGROUND : BUTTON_MIDDLE); + + if (events._pressed || events._released) { + if (pt.x > SEARCH_POINTS[0][0] && pt.x < SEARCH_POINTS[0][1] && pt.y > 174 && pt.y < 183) { + found = BTN_EXIT; + color = COMMAND_HIGHLIGHTED; + } else { + color = COMMAND_FOREGROUND; + } + screen.print(Common::Point(SEARCH_POINTS[0][2] - screen.stringWidth("Exit") / 2, 175), color, "Exit"); + + if (pt.x > SEARCH_POINTS[1][0] && pt.x < SEARCH_POINTS[1][1] && pt.y > 174 && pt.y < 183) { + found = BTN_BACKWARD; + color = COMMAND_HIGHLIGHTED; + } else { + color = COMMAND_FOREGROUND; + } + screen.print(Common::Point(SEARCH_POINTS[1][2] - screen.stringWidth("Backward") / 2, 175), color, "Backward"); + + if (pt.x > SEARCH_POINTS[2][0] && pt.x < SEARCH_POINTS[2][1] && pt.y > 174 && pt.y < 183) { + found = BTN_FORWARD; + color = COMMAND_HIGHLIGHTED; + } else { + color = COMMAND_FOREGROUND; + } + screen.print(Common::Point(SEARCH_POINTS[2][2] - screen.stringWidth("Forward") / 2, 175), color, "Forward"); + } + + events.wait(2); + } + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + if ((keyState.keycode == Common::KEYCODE_BACKSPACE) && (name.size() > 0)) { + screen.vgaBar(Common::Rect(xp - screen.charWidth(name.lastChar()), yp, xp + 8, yp + 9), BUTTON_MIDDLE); + xp -= screen.charWidth(name.lastChar()); + screen.vgaBar(Common::Rect(xp, yp, xp + 8, yp + 9), INV_FOREGROUND); + name.deleteLastChar(); + + } else if (keyState.keycode == Common::KEYCODE_RETURN) { + done = 1; + + } else if (keyState.keycode == Common::KEYCODE_ESCAPE) { + screen.vgaBar(Common::Rect(xp, yp, xp + 8, yp + 9), BUTTON_MIDDLE); + done = -1; + + } else if (keyState.ascii >= ' ' && keyState.ascii <= 'z' && keyState.keycode != Common::KEYCODE_AT && + name.size() < JOURNAL_SEACRH_MAX_CHARS && (xp + screen.charWidth(keyState.ascii)) < JOURNAL_SEARCH_RIGHT) { + char ch = toupper(keyState.ascii); + screen.vgaBar(Common::Rect(xp, yp, xp + 8, yp + 9), BUTTON_MIDDLE); + screen.print(Common::Point(xp, yp), TALK_FOREGROUND, "%c", ch); + xp += screen.charWidth(ch); + name += ch; + } + } + + if (events._released) { + switch (found) { + case BTN_EXIT: + done = -1; break; + case BTN_BACKWARD: + done = 2; break; + case BTN_FORWARD: + done = 1; break; + default: + break; + } + } + } while (!done && !_vm->shouldQuit()); + + if (done != -1) { + _find = name; + } else { + done = 0; + } + + // Redisplay the journal screen + drawJournalFrame(); + drawJournal(0, 0); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + return done; +} + +void Journal::resetPosition() { + _index = _sub = _up = _down = 0; + _page = 1; +} + +void Journal::synchronize(Common::Serializer &s) { + s.syncAsSint16LE(_index); + s.syncAsSint16LE(_sub); + s.syncAsSint16LE(_page); + s.syncAsSint16LE(_maxPage); + + int journalCount = _journal.size(); + s.syncAsUint16LE(journalCount); + if (s.isLoading()) + _journal.resize(journalCount); + + for (uint idx = 0; idx < _journal.size(); ++idx) { + JournalEntry &je = _journal[idx]; + + s.syncAsSint16LE(je._converseNum); + s.syncAsByte(je._replyOnly); + s.syncAsSint16LE(je._statementNum); + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/journal.h b/engines/sherlock/journal.h new file mode 100644 index 0000000000..d62b8338c0 --- /dev/null +++ b/engines/sherlock/journal.h @@ -0,0 +1,139 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_JOURNAL_H +#define SHERLOCK_JOURNAL_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "common/str-array.h" +#include "common/stream.h" + +namespace Sherlock { + +#define JOURNAL_MAX_WIDTH 230 +#define JOURNAL_MAX_CHARS 80 + +enum JournalButton { + BTN_NONE, BTN_EXIT, BTN_BACK10, BTN_UP, BTN_DOWN, BTN_AHEAD110, BTN_SEARCH, + BTN_FIRST_PAGE, BTN_LAST_PAGE, BTN_PRINT_TEXT +}; + + +struct JournalEntry { + int _converseNum; + bool _replyOnly; + int _statementNum; + + JournalEntry() : _converseNum(0), _replyOnly(false), _statementNum(0) {} + JournalEntry(int converseNum, int statementNum, bool replyOnly = false) : + _converseNum(converseNum), _statementNum(statementNum), _replyOnly(replyOnly) {} +}; + +class SherlockEngine; + +class Journal { +private: + SherlockEngine *_vm; + Common::Array<JournalEntry> _journal; + Common::StringArray _directory; + Common::StringArray _locations; + Common::StringArray _lines; + int _maxPage; + int _index; + int _sub; + bool _up, _down; + int _page; + Common::String _find; + + /** + * Load the list of location names that the journal will make reference to + */ + void loadJournalLocations(); + + /** + * Loads the description for the current display index in the journal, and then + * word wraps the result to prepare it for being displayed + * @param alreadyLoaded Indicates whether the journal file is being loaded for the + * first time, or being reloaded + */ + void loadJournalFile(bool alreadyLoaded); + + /** + * Display the arrows that can be used to scroll up and down pages + */ + void doArrows(); + + /** + * Displays a page of the journal at the current index + */ + bool drawJournal(int direction, int howFar); + + /** + * Show the search submenu and allow the player to enter a search string + */ + int getSearchString(bool printError); + + /** + * Draw the journal background, frame, and interface buttons + */ + void drawJournalFrame(); + + /** + * Returns the button, if any, that is under the specified position + */ + JournalButton getHighlightedButton(const Common::Point &pt); +public: + Journal(SherlockEngine *vm); + + /** + * Records statements that are said, in the order which they are said. The player + * can then read the journal to review them + */ + void record(int converseNum, int statementNum, bool replyOnly = false); + + /** + * Display the journal + */ + void drawInterface(); + + /** + * Handle events whilst the journal is being displayed + */ + bool handleEvents(int key); + + /** + * Reset viewing position to the start of the journal + */ + void resetPosition(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/map.cpp b/engines/sherlock/map.cpp new file mode 100644 index 0000000000..96442893ef --- /dev/null +++ b/engines/sherlock/map.cpp @@ -0,0 +1,552 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/map.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +void MapPaths::load(int numLocations, Common::SeekableReadStream &s) { + _numLocations = numLocations; + _paths.resize(_numLocations * _numLocations); + + for (int idx = 0; idx < (numLocations * numLocations); ++idx) { + Common::Array<byte> &path = _paths[idx]; + int v; + + do { + v = s.readByte(); + path.push_back(v); + } while (v && v < 254); + } +} + +const byte *MapPaths::getPath(int srcLocation, int destLocation) { + return &_paths[srcLocation * _numLocations + destLocation][0]; +} + +/*----------------------------------------------------------------*/ + +Map::Map(SherlockEngine *vm) : _vm(vm), _topLine(SHERLOCK_SCREEN_WIDTH, 12) { + _active = false; + _mapCursors = nullptr; + _shapes = nullptr; + _iconShapes = nullptr; + _point = 0; + _placesShown = false; + _cursorIndex = -1; + _drawMap = false; + _overPos = Common::Point(13000, 12600); + _charPoint = 0; + _oldCharPoint = 0; + _frameChangeFlag = false; + + for (int idx = 0; idx < MAX_HOLMES_SEQUENCE; ++idx) + Common::fill(&_sequences[idx][0], &_sequences[idx][MAX_FRAME], 0); + + if (!_vm->isDemo()) + loadData(); +} + +void Map::loadPoints(int count, const int *xList, const int *yList, const int *transList) { + for (int idx = 0; idx < count; ++idx, ++xList, ++yList, ++transList) { + _points.push_back(MapEntry(*xList, *yList, *transList)); + } +} + +void Map::loadSequences(int count, const byte *seq) { + for (int idx = 0; idx < count; ++idx, seq += MAX_FRAME) + Common::copy(seq, seq + MAX_FRAME, &_sequences[idx][0]); +} + +void Map::loadData() { + // Load the list of location names + Common::SeekableReadStream *txtStream = _vm->_res->load("chess.txt"); + + int streamSize = txtStream->size(); + while (txtStream->pos() < streamSize) { + Common::String line; + char c; + while ((c = txtStream->readByte()) != '\0') + line += c; + + _locationNames.push_back(line); + } + + delete txtStream; + + // Load the path data + Common::SeekableReadStream *pathStream = _vm->_res->load("chess.pth"); + + // Get routes between different locations on the map + _paths.load(31, *pathStream); + + // Load in the co-ordinates that the paths refer to + _pathPoints.resize(208); + for (uint idx = 0; idx < _pathPoints.size(); ++idx) { + _pathPoints[idx].x = pathStream->readSint16LE(); + _pathPoints[idx].y = pathStream->readSint16LE(); + } + + delete pathStream; +} + +int Map::show() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Common::Point lDrawn(-1, -1); + bool changed = false, exitFlag = false; + _active = true; + + // Set font and custom cursor for the map + int oldFont = screen.fontNumber(); + screen.setFont(0); + + // Initial screen clear + screen._backBuffer1.clear(); + screen.clear(); + + // Load the entire map + ImageFile bigMap("bigmap.vgs"); + screen.setPalette(bigMap._palette); + + // Load need sprites + setupSprites(); + + screen._backBuffer1.blitFrom(bigMap[0], Common::Point(-_bigPos.x, -_bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[1], Common::Point(-_bigPos.x, SHERLOCK_SCREEN_HEIGHT - _bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[2], Common::Point(SHERLOCK_SCREEN_WIDTH - _bigPos.x, -_bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[3], Common::Point(SHERLOCK_SCREEN_WIDTH - _bigPos.x, SHERLOCK_SCREEN_HEIGHT - _bigPos.y)); + + _drawMap = true; + _charPoint = -1; + _point = -1; + people[AL]._position = _lDrawnPos = _overPos; + + // Show place icons + showPlaces(); + saveTopLine(); + _placesShown = true; + + // Keep looping until either a location is picked, or the game is ended + while (!_vm->shouldQuit() && !exitFlag) { + events.pollEventsAndWait(); + events.setButtonState(); + + // Keyboard handling + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_RETURN || keyState.keycode == Common::KEYCODE_SPACE) { + // Both space and enter simulate a mouse release + events._pressed = false; + events._released = true; + events._oldButtons = 0; + } + } + + // Ignore scrolling attempts until the screen is drawn + if (!_drawMap) { + Common::Point pt = events.mousePos(); + + // Check for vertical map scrolling + if ((pt.y > (SHERLOCK_SCREEN_HEIGHT - 10) && _bigPos.y < 200) || (pt.y < 10 && _bigPos.y > 0)) { + if (pt.y > (SHERLOCK_SCREEN_HEIGHT - 10)) + _bigPos.y += 10; + else + _bigPos.y -= 10; + + changed = true; + } + + // Check for horizontal map scrolling + if ((pt.x > (SHERLOCK_SCREEN_WIDTH - 10) && _bigPos.x < 315) || (pt.x < 10 && _bigPos.x > 0)) { + if (pt.x > (SHERLOCK_SCREEN_WIDTH - 10)) + _bigPos.x += 15; + else + _bigPos.x -= 15; + + changed = true; + } + } + + if (changed) { + // Map has scrolled, so redraw new map view + changed = false; + + screen._backBuffer1.blitFrom(bigMap[0], Common::Point(-_bigPos.x, -_bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[1], Common::Point(-_bigPos.x, SHERLOCK_SCREEN_HEIGHT - _bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[2], Common::Point(SHERLOCK_SCREEN_WIDTH - _bigPos.x, -_bigPos.y)); + screen._backBuffer1.blitFrom(bigMap[3], Common::Point(SHERLOCK_SCREEN_WIDTH - _bigPos.x, SHERLOCK_SCREEN_HEIGHT - _bigPos.y)); + + showPlaces(); + _placesShown = false; + + saveTopLine(); + _savedPos.x = -1; + updateMap(true); + } else if (!_drawMap) { + if (!_placesShown) { + showPlaces(); + _placesShown = true; + } + + if (_cursorIndex == 0) { + Common::Point pt = events.mousePos(); + highlightIcon(Common::Point(pt.x - 4 + _bigPos.x, pt.y + _bigPos.y)); + } + updateMap(false); + } + + if ((events._released || events._rightReleased) && _point != -1) { + if (people[AL]._walkCount == 0) { + people._walkDest = _points[_point] + Common::Point(4, 9); + _charPoint = _point; + + // Start walking to selected location + walkTheStreets(); + + // Show wait cursor + _cursorIndex = 1; + events.setCursor((*_mapCursors)[_cursorIndex]._frame); + } + } + + // Check if a scene has beeen selected and we've finished "moving" to it + if (people[AL]._walkCount == 0) { + if (_charPoint >= 1 && _charPoint < (int)_points.size()) + exitFlag = true; + } + + if (_drawMap) { + _drawMap = false; + + if (screen._fadeStyle) + screen.randomTransition(); + else + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + + // Wait for a frame + events.wait(1); + } + + freeSprites(); + _overPos = people[AL]._position; + + // Reset font + screen.setFont(oldFont); + + _active = false; + return _charPoint; +} + +void Map::setupSprites() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + _savedPos.x = -1; + + _mapCursors = new ImageFile("omouse.vgs"); + _cursorIndex = 0; + events.setCursor((*_mapCursors)[_cursorIndex]._frame); + + _shapes = new ImageFile("mapicon.vgs"); + _iconShapes = new ImageFile("overicon.vgs"); + _iconSave.create((*_shapes)[4]._width, (*_shapes)[4]._height); + Person &p = people[AL]; + p._description = " "; + p._type = CHARACTER; + p._position = Common::Point(12400, 5000); + p._sequenceNumber = 0; + p._sequences = &_sequences; + p._images = _shapes; + p._imageFrame = nullptr; + p._frameNumber = 0; + p._delta = Common::Point(0, 0); + p._oldSize = Common::Point(0, 0); + p._oldSize = Common::Point(0, 0); + p._misc = 0; + p._walkCount = 0; + p._allow = 0; + p._noShapeSize = Common::Point(0, 0); + p._goto = Common::Point(28000, 15000); + p._status = 0; + p.setImageFrame(); + + scene._bgShapes.clear(); +} + +void Map::freeSprites() { + delete _mapCursors; + delete _shapes; + delete _iconShapes; + _iconSave.free(); +} + +void Map::showPlaces() { + Screen &screen = *_vm->_screen; + + for (uint idx = 0; idx < _points.size(); ++idx) { + const MapEntry &pt = _points[idx]; + + if (pt.x != 0 && pt.y != 0) { + if (pt.x >= _bigPos.x && (pt.x - _bigPos.x) < SHERLOCK_SCREEN_WIDTH + && pt.y >= _bigPos.y && (pt.y - _bigPos.y) < SHERLOCK_SCREEN_HEIGHT) { + if (_vm->readFlags(idx)) { + screen._backBuffer1.transBlitFrom((*_iconShapes)[pt._translate], + Common::Point(pt.x - _bigPos.x - 6, pt.y - _bigPos.y - 12)); + } + } + } + } +} + +void Map::saveTopLine() { + _topLine.blitFrom(_vm->_screen->_backBuffer1, Common::Point(0, 0), Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, 12)); +} + +void Map::eraseTopLine() { + Screen &screen = *_vm->_screen; + screen._backBuffer1.blitFrom(_topLine, Common::Point(0, 0)); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, _topLine.h()); +} + +void Map::showPlaceName(int idx, bool highlighted) { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + + Common::String name = _locationNames[idx]; + int width = screen.stringWidth(name); + + if (!_cursorIndex) { + saveIcon(people[AL]._imageFrame, _lDrawnPos); + + bool flipped = people[AL]._sequenceNumber == MAP_DOWNLEFT || people[AL]._sequenceNumber == MAP_LEFT + || people[AL]._sequenceNumber == MAP_UPLEFT; + screen._backBuffer1.transBlitFrom(*people[AL]._imageFrame, _lDrawnPos, flipped); + } + + if (highlighted) { + int xp = (SHERLOCK_SCREEN_WIDTH - screen.stringWidth(name)) / 2; + screen.gPrint(Common::Point(xp + 2, 2), 0, "%s", name.c_str()); + screen.gPrint(Common::Point(xp + 1, 1), 0, "%s", name.c_str()); + screen.gPrint(Common::Point(xp, 0), 12, "%s", name.c_str()); + + screen.slamArea(xp, 0, width + 2, 15); + } +} + +void Map::updateMap(bool flushScreen) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Common::Point osPos = _savedPos; + Common::Point osSize = _savedSize; + Common::Point hPos; + + if (_cursorIndex >= 1) { + if (++_cursorIndex > (1 + 8)) + _cursorIndex = 1; + + events.setCursor((*_mapCursors)[(_cursorIndex + 1) / 2]._frame); + } + + if (!_drawMap && !flushScreen) + restoreIcon(); + else + _savedPos.x = -1; + + people[AL].adjustSprite(); + + _lDrawnPos.x = hPos.x = people[AL]._position.x / 100 - _bigPos.x; + _lDrawnPos.y = hPos.y = people[AL]._position.y / 100 - people[AL].frameHeight() - _bigPos.y; + + // Draw the person icon + saveIcon(people[AL]._imageFrame, hPos); + if (people[AL]._sequenceNumber == MAP_DOWNLEFT || people[AL]._sequenceNumber == MAP_LEFT + || people[AL]._sequenceNumber == MAP_UPLEFT) + screen._backBuffer1.transBlitFrom(*people[AL]._imageFrame, hPos, true); + else + screen._backBuffer1.transBlitFrom(*people[AL]._imageFrame, hPos, false); + + if (flushScreen) { + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } else if (!_drawMap) { + if (hPos.x > 0 && hPos.y >= 0 && hPos.x < SHERLOCK_SCREEN_WIDTH && hPos.y < SHERLOCK_SCREEN_HEIGHT) + screen.flushImage(people[AL]._imageFrame, Common::Point(people[AL]._position.x / 100 - _bigPos.x, + people[AL]._position.y / 100 - people[AL].frameHeight() - _bigPos.y), + &people[AL]._oldPosition.x, &people[AL]._oldPosition.y, &people[AL]._oldSize.x, &people[AL]._oldSize.y); + + if (osPos.x != -1) + screen.slamArea(osPos.x, osPos.y, osSize.x, osSize.y); + } +} + +void Map::walkTheStreets() { + People &people = *_vm->_people; + Common::Array<Common::Point> tempPath; + + // Get indexes into the path lists for the start and destination scenes + int start = _points[_oldCharPoint]._translate; + int dest = _points[_charPoint]._translate; + + // Get pointer to start of path + const byte *path = _paths.getPath(start, dest); + + // Add in destination position + people._walkTo.clear(); + Common::Point destPos = people._walkDest; + + // Check for any intermediate points between the two locations + if (path[0] || _charPoint > 50 || _oldCharPoint > 50) { + people[AL]._sequenceNumber = -1; + + if (_charPoint == 51 || _oldCharPoint == 51) { + people.setWalking(); + } else { + bool reversePath = false; + + // Check for moving the path backwards or forwards + if (path[0] == 255) { + reversePath = true; + SWAP(start, dest); + path = _paths.getPath(start, dest); + } + + do { + int idx = *path++; + tempPath.push_back(_pathPoints[idx - 1] + Common::Point(4, 4)); + } while (*path != 254); + + // Load up the path to use + people._walkTo.clear(); + + if (reversePath) { + for (int idx = (int)tempPath.size() - 1; idx >= 0; --idx) + people._walkTo.push(tempPath[idx]); + } else { + for (int idx = 0; idx < (int)tempPath.size(); ++idx) + people._walkTo.push(tempPath[idx]); + } + + people._walkDest = people._walkTo.pop() + Common::Point(12, 6); + people.setWalking(); + } + } else { + people[AL]._walkCount = 0; + } + + // Store the final destination icon position + people._walkTo.push(destPos); +} + +void Map::saveIcon(ImageFrame *src, const Common::Point &pt) { + Screen &screen = *_vm->_screen; + Common::Point size(src->_width, src->_height); + Common::Point pos = pt; + + if (pos.x < 0) { + size.x += pos.x; + pos.x = 0; + } + + if (pos.y < 0) { + size.y += pos.y; + pos.y = 0; + } + + if ((pos.x + size.x) > SHERLOCK_SCREEN_WIDTH) + size.x -= (pos.x + size.x) - SHERLOCK_SCREEN_WIDTH; + + if ((pos.y + size.y) > SHERLOCK_SCREEN_HEIGHT) + size.y -= (pos.y + size.y) - SHERLOCK_SCREEN_HEIGHT; + + if (size.x < 1 || size.y < 1 || pos.x >= SHERLOCK_SCREEN_WIDTH || pos.y >= SHERLOCK_SCREEN_HEIGHT || _drawMap) { + // Flag as the area not needing to be saved + _savedPos.x = -1; + return; + } + + assert(size.x <= _iconSave.w() && size.y <= _iconSave.h()); + _iconSave.blitFrom(screen._backBuffer1, Common::Point(0, 0), + Common::Rect(pos.x, pos.y, pos.x + size.x, pos.y + size.y)); + _savedPos = pos; + _savedSize = size; +} + +void Map::restoreIcon() { + Screen &screen = *_vm->_screen; + + if (_savedPos.x >= 0 && _savedPos.y >= 0 && _savedPos.x <= SHERLOCK_SCREEN_WIDTH + && _savedPos.y < SHERLOCK_SCREEN_HEIGHT) + screen._backBuffer1.blitFrom(_iconSave, _savedPos, Common::Rect(0, 0, _savedSize.x, _savedSize.y)); +} + +void Map::highlightIcon(const Common::Point &pt) { + int oldPoint = _point; + + // Iterate through the icon list + bool done = false; + for (int idx = 0; idx < (int)_points.size(); ++idx) { + const MapEntry &entry = _points[idx]; + + // Check whether the mouse is over a given icon + if (entry.x != 0 && entry.y != 0) { + if (Common::Rect(entry.x - 8, entry.y - 8, entry.x + 9, entry.y + 9).contains(pt)) { + done = true; + + if (_point != idx && _vm->readFlags(idx)) { + // Changed to a new valid (visible) location + eraseTopLine(); + showPlaceName(idx, true); + _point = idx; + } + } + } + } + + if (!done) { + // No icon was highlighted + if (_point != -1) { + // No longer highlighting previously highlighted icon, so erase it + showPlaceName(_point, false); + eraseTopLine(); + } + + _point = -1; + } else if (oldPoint != -1 && oldPoint != _point) { + showPlaceName(oldPoint, false); + eraseTopLine(); + } +} + +void Map::synchronize(Common::Serializer &s) { + s.syncAsSint16LE(_bigPos.x); + s.syncAsSint16LE(_bigPos.y); + s.syncAsSint16LE(_overPos.x); + s.syncAsSint16LE(_overPos.y); + s.syncAsSint16LE(_oldCharPoint); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/map.h b/engines/sherlock/map.h new file mode 100644 index 0000000000..2c8c02325b --- /dev/null +++ b/engines/sherlock/map.h @@ -0,0 +1,177 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_MAP_H +#define SHERLOCK_MAP_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "common/str.h" +#include "common/str-array.h" +#include "sherlock/surface.h" +#include "sherlock/objects.h" + +namespace Sherlock { + +class SherlockEngine; + +struct MapEntry : Common::Point { + int _translate; + + MapEntry() : Common::Point(), _translate(-1) {} + + MapEntry(int posX, int posY, int translate) : Common::Point(posX, posY), _translate(translate) {} +}; + +class MapPaths { +private: + int _numLocations; + Common::Array< Common::Array<byte> > _paths; +public: + /** + * Load the data for the paths between locations on the map + */ + void load(int numLocations, Common::SeekableReadStream &s); + + /** + * Get the path between two locations on the map + */ + const byte *getPath(int srcLocation, int destLocation); +}; + +class Map { +private: + SherlockEngine *_vm; + Common::Array<MapEntry> _points; // Map locations for each scene + Common::StringArray _locationNames; + MapPaths _paths; + Common::Array<Common::Point> _pathPoints; + Common::Point _savedPos; + Common::Point _savedSize; + Surface _topLine; + ImageFile *_mapCursors; + ImageFile *_shapes; + ImageFile *_iconShapes; + byte _sequences[MAX_HOLMES_SEQUENCE][MAX_FRAME]; + Point32 _lDrawnPos; + int _point; + bool _placesShown; + int _cursorIndex; + bool _drawMap; + Surface _iconSave; +private: + /** + * Load data needed for the map + */ + void loadData(); + + /** + * Load and initialize all the sprites that are needed for the map display + */ + void setupSprites(); + + /** + * Free the sprites and data used by the map + */ + void freeSprites(); + + /** + * Draws an icon for every place that's currently known + */ + void showPlaces(); + + /** + * Makes a copy of the top rows of the screen that are used to display location names + */ + void saveTopLine(); + + /** + * Erases anything shown in the top line by restoring the previously saved original map background + */ + void eraseTopLine(); + + /** + * Prints the name of the specified icon + */ + void showPlaceName(int idx, bool highlighted); + + /** + * Update all on-screen sprites to account for any scrolling of the map + */ + void updateMap(bool flushScreen); + + /** + * Handle moving icon for player from their previous location on the map to a destination location + */ + void walkTheStreets(); + + /** + * Save the area under the player's icon + */ + void saveIcon(ImageFrame *src, const Common::Point &pt); + + /** + * Restore the area under the player's icon + */ + void restoreIcon(); + + /** + * Handles highlighting map icons, showing their names + */ + void highlightIcon(const Common::Point &pt); +public: + bool _active; + Point32 _overPos; + Point32 _bigPos; + int _charPoint, _oldCharPoint; + bool _frameChangeFlag; +public: + Map(SherlockEngine *vm); + + const MapEntry &operator[](int idx) { return _points[idx]; } + + /** + * Loads the list of points for locations on the map for each scene + */ + void loadPoints(int count, const int *xList, const int *yList, const int *transList); + + /** + * Load the sequence data for player icon animations + */ + void loadSequences(int count, const byte *seq); + + /** + * Show the map + */ + int show(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/module.mk b/engines/sherlock/module.mk new file mode 100644 index 0000000000..2ded999ed1 --- /dev/null +++ b/engines/sherlock/module.mk @@ -0,0 +1,33 @@ +MODULE := engines/sherlock + +MODULE_OBJS = \ + scalpel/darts.o \ + scalpel/scalpel.o \ + tattoo/tattoo.o \ + animation.o \ + debugger.o \ + detection.o \ + events.o \ + inventory.o \ + journal.o \ + map.o \ + objects.o \ + people.o \ + resources.o \ + saveload.o \ + scene.o \ + screen.o \ + settings.o \ + sherlock.o \ + sound.o \ + surface.o \ + talk.o \ + user_interface.o + +# This module can be built as a plugin +ifeq ($(ENABLE_SHERLOCK), DYNAMIC_PLUGIN) +PLUGIN := 1 +endif + +# Include common rules +include $(srcdir)/rules.mk diff --git a/engines/sherlock/objects.cpp b/engines/sherlock/objects.cpp new file mode 100644 index 0000000000..02f2526ae9 --- /dev/null +++ b/engines/sherlock/objects.cpp @@ -0,0 +1,1062 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/objects.h" +#include "sherlock/sherlock.h" +#include "sherlock/people.h" +#include "sherlock/scene.h" +#include "common/util.h" + +namespace Sherlock { + +#define START_FRAME 0 + +#define UPPER_LIMIT 0 +#define LOWER_LIMIT CONTROLS_Y +#define LEFT_LIMIT 0 +#define RIGHT_LIMIT SHERLOCK_SCREEN_WIDTH + +// Distance to walk around WALK_AROUND boxes +#define CLEAR_DIST_X 5 +#define CLEAR_DIST_Y 0 + +SherlockEngine *Sprite::_vm; + +void Sprite::clear() { + _name = ""; + _description = ""; + _examine.clear(); + _pickUp = ""; + _sequences = nullptr; + _images = nullptr; + _imageFrame = nullptr; + _walkCount = 0; + _allow = 0; + _frameNumber = _sequenceNumber = 0; + _position.x = _position.y = 0; + _delta.x = _delta.y = 0; + _oldPosition.x = _oldPosition.y = 0; + _oldSize.x = _oldSize.y = 0; + _goto.x = _goto.y = 0; + _type = INVALID; + _pickUp.clear(); + _noShapeSize.x = _noShapeSize.y = 0; + _status = 0; + _misc = 0; + _numFrames = 0; +} + +void Sprite::setImageFrame() { + int imageNumber = (*_sequences)[_sequenceNumber][_frameNumber] + + (*_sequences)[_sequenceNumber][0] - 2; + _imageFrame = &(*_images)[imageNumber]; +} + +void Sprite::adjustSprite() { + Map &map = *_vm->_map; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + if (_type == INVALID || (_type == CHARACTER && scene._animating)) + return; + + if (!talk._talkCounter && _type == CHARACTER && _walkCount) { + // Handle active movement for the sprite + _position += _delta; + --_walkCount; + + if (!_walkCount) { + // If there any points left for the character to walk to along the + // route to a destination, then move to the next point + if (!people._walkTo.empty()) { + people._walkDest = people._walkTo.pop(); + people.setWalking(); + } else { + people.gotoStand(*this); + } + } + } + + if (_type == CHARACTER && !map._active) { + if ((_position.y / 100) > LOWER_LIMIT) { + _position.y = LOWER_LIMIT * 100; + people.gotoStand(*this); + } + + if ((_position.y / 100) < UPPER_LIMIT) { + _position.y = UPPER_LIMIT * 100; + people.gotoStand(*this); + } + + if ((_position.x / 100) < LEFT_LIMIT) { + _position.x = LEFT_LIMIT * 100; + people.gotoStand(*this); + } + } else if (!map._active) { + _position.y = CLIP((int)_position.y, UPPER_LIMIT, LOWER_LIMIT); + _position.x = CLIP((int)_position.x, LEFT_LIMIT, RIGHT_LIMIT); + } + + if (!map._active || (map._frameChangeFlag = !map._frameChangeFlag)) + ++_frameNumber; + + if ((*_sequences)[_sequenceNumber][_frameNumber] == 0) { + switch (_sequenceNumber) { + case STOP_UP: + case STOP_DOWN: + case STOP_LEFT: + case STOP_RIGHT: + case STOP_UPRIGHT: + case STOP_UPLEFT: + case STOP_DOWNRIGHT: + case STOP_DOWNLEFT: + // We're in a stop sequence, so reset back to the last frame, so + // the character is shown as standing still + --_frameNumber; + break; + + default: + // Move 1 past the first frame - we need to compensate, since we + // already passed the frame increment + _frameNumber = 1; + break; + } + } + + // Update the _imageFrame to point to the new frame's image + setImageFrame(); + + // Check to see if character has entered an exit zone + if (!_walkCount && scene._walkedInScene && scene._goToScene == -1) { + Common::Rect charRect(_position.x / 100 - 5, _position.y / 100 - 2, + _position.x / 100 + 5, _position.y / 100 + 2); + Exit *exit = scene.checkForExit(charRect); + + if (exit) { + scene._goToScene = exit->_scene; + + if (exit->_people.x != 0) { + people._hSavedPos = exit->_people; + people._hSavedFacing = exit->_peopleDir; + + if (people._hSavedFacing > 100 && people._hSavedPos.x < 1) + people._hSavedPos.x = 100; + } + } + } +} + +void Sprite::checkSprite() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + Point32 pt; + Common::Rect objBounds; + Common::Point spritePt(_position.x / 100, _position.y / 100); + + if (!talk._talkCounter && _type == CHARACTER) { + pt = _walkCount ? _position + _delta : _position; + pt.x /= 100; + pt.y /= 100; + + for (uint idx = 0; idx < scene._bgShapes.size() && !talk._talkToAbort; ++idx) { + Object &obj = scene._bgShapes[idx]; + if (obj._aType <= PERSON || obj._type == INVALID || obj._type == HIDDEN) + continue; + + if (obj._type == NO_SHAPE) { + objBounds = Common::Rect(obj._position.x, obj._position.y, + obj._position.x + obj._noShapeSize.x + 1, obj._position.y + obj._noShapeSize.y + 1); + } else { + int xp = obj._position.x + obj._imageFrame->_offset.x; + int yp = obj._position.y + obj._imageFrame->_offset.y; + objBounds = Common::Rect(xp, yp, + xp + obj._imageFrame->_frame.w + 1, yp + obj._imageFrame->_frame.h + 1); + } + + if (objBounds.contains(pt)) { + if (objBounds.contains(spritePt)) { + // Current point is already inside the the bounds, so impact occurred + // on a previous call. So simply do nothing until we're clear of the box + switch (obj._aType) { + case TALK_MOVE: + if (_walkCount) { + // Holmes is moving + obj._type = HIDDEN; + obj.setFlagsAndToggles(); + talk.talkTo(obj._use[0]._target); + } + break; + + case PAL_CHANGE: + case PAL_CHANGE2: + if (_walkCount) { + int palStart = atoi(obj._use[0]._names[0].c_str()) * 3; + int palLength = atoi(obj._use[0]._names[1].c_str()) * 3; + int templ = atoi(obj._use[0]._names[2].c_str()) * 3; + if (templ == 0) + templ = 100; + + // Ensure only valid palette change data found + if (palLength > 0) { + // Figure out how far into the shape Holmes is so that we + // can figure out what percentage of the original palette + // to set the current palette to + int palPercent = (pt.x - objBounds.left) * 100 / objBounds.width(); + palPercent = palPercent * templ / 100; + if (obj._aType == PAL_CHANGE) + // Invert percentage + palPercent = 100 - palPercent; + + for (int i = palStart; i < (palStart + palLength); ++i) + screen._sMap[i] = screen._cMap[i] * palPercent / 100; + + events.pollEvents(); + screen.setPalette(screen._sMap); + } + } + break; + + case TALK: + case TALK_EVERY: + obj._type = HIDDEN; + obj.setFlagsAndToggles(); + talk.talkTo(obj._use[0]._target); + break; + + default: + break; + } + } else { + // New impact just occurred + switch (obj._aType) { + case BLANK_ZONE: + // A blank zone masks out all other remaining zones underneath it. + // If this zone is hit, exit the outer loop so we do not check anymore + return; + + case SOLID: + case TALK: + // Stop walking + if (obj._aType == TALK) { + obj.setFlagsAndToggles(); + talk.talkTo(obj._use[0]._target); + } else { + people.gotoStand(*this); + } + break; + + case TALK_EVERY: + if (obj._aType == TALK_EVERY) { + obj._type = HIDDEN; + obj.setFlagsAndToggles(); + talk.talkTo(obj._use[0]._target); + } else { + people.gotoStand(*this); + } + break; + + case FLAG_SET: + obj.setFlagsAndToggles(); + obj._type = HIDDEN; + break; + + case WALK_AROUND: + if (objBounds.contains(people._walkTo.front())) { + // Reached zone + people.gotoStand(*this); + } else { + // Destination not within box, walk to best corner + Common::Point walkPos; + + if (spritePt.x >= objBounds.left && spritePt.x < objBounds.right) { + // Impact occurred due to vertical movement. Determine whether to + // travel to the left or right side + if (_delta.x > 0) + // Go to right side + walkPos.x = objBounds.right + CLEAR_DIST_X; + else if (_delta.x < 0) { + // Go to left side + walkPos.x = objBounds.left - CLEAR_DIST_X; + } else { + // Going straight up or down. So choose best side + if (spritePt.x >= (objBounds.left + objBounds.width() / 2)) + walkPos.x = objBounds.right + CLEAR_DIST_X; + else + walkPos.x = objBounds.left - CLEAR_DIST_X; + } + + walkPos.y = (_delta.y >= 0) ? objBounds.top - CLEAR_DIST_Y : + objBounds.bottom + CLEAR_DIST_Y; + } else { + // Impact occurred due to horizontal movement + if (_delta.y > 0) + // Go to bottom of box + walkPos.y = objBounds.bottom + CLEAR_DIST_Y; + else if (_delta.y < 0) + // Go to top of box + walkPos.y = objBounds.top - CLEAR_DIST_Y; + else { + // Going straight horizontal, so choose best side + if (spritePt.y >= (objBounds.top + objBounds.height() / 2)) + walkPos.y = objBounds.bottom + CLEAR_DIST_Y; + else + walkPos.y = objBounds.top - CLEAR_DIST_Y; + } + + walkPos.x = (_delta.x >= 0) ? objBounds.left - CLEAR_DIST_X : + objBounds.right + CLEAR_DIST_X; + } + + walkPos.x += people[AL]._imageFrame->_frame.w / 2; + people._walkDest = walkPos; + people._walkTo.push(walkPos); + people.setWalking(); + } + break; + + case DELTA: + _position.x += 200; + break; + + default: + break; + } + } + } + } + } +} + +/*----------------------------------------------------------------*/ + +void ActionType::load(Common::SeekableReadStream &s) { + char buffer[12]; + + _cAnimNum = s.readByte(); + _cAnimSpeed = s.readByte(); + if (_cAnimSpeed & 0x80) + _cAnimSpeed = -(_cAnimSpeed & 0x7f); + + for (int idx = 0; idx < NAMES_COUNT; ++idx) { + s.read(buffer, 12); + _names[idx] = Common::String(buffer); + } +} + +/*----------------------------------------------------------------*/ + +UseType::UseType() { + _cAnimNum = _cAnimSpeed = 0; + _useFlag = 0; +} + +void UseType::load(Common::SeekableReadStream &s) { + char buffer[12]; + + _cAnimNum = s.readByte(); + _cAnimSpeed = s.readByte(); + if (_cAnimSpeed & 0x80) + _cAnimSpeed = -(_cAnimSpeed & 0x7f); + + for (int idx = 0; idx < NAMES_COUNT; ++idx) { + s.read(buffer, 12); + _names[idx] = Common::String(buffer); + } + + _useFlag = s.readSint16LE(); + s.skip(6); + + s.read(buffer, 12); + _target = Common::String(buffer); +} + +/*----------------------------------------------------------------*/ + +SherlockEngine *Object::_vm; +bool Object::_countCAnimFrames; + +void Object::setVm(SherlockEngine *vm) { + _vm = vm; + _countCAnimFrames = false; +} + +Object::Object() { + _sequenceOffset = 0; + _sequences = nullptr; + _images = nullptr; + _imageFrame = nullptr; + _walkCount = 0; + _allow = 0; + _frameNumber = 0; + _sequenceNumber = 0; + _type = INVALID; + _pickup = 0; + _defaultCommand = 0; + _lookFlag = 0; + _pickupFlag = 0; + _requiredFlag = 0; + _status = 0; + _misc = 0; + _maxFrames = 0; + _flags = 0; + _aOpen._cAnimNum = 0; + _aOpen._cAnimSpeed = 0; + _aType = OBJECT; + _lookFrames = 0; + _seqCounter = 0; + _lookFacing = 0; + _lookcAnim = 0; + _seqStack = 0; + _seqTo = 0; + _descOffset = 0; + _seqCounter2 = 0; + _seqSize = 0; +} + +void Object::load(Common::SeekableReadStream &s) { + char buffer[41]; + s.read(buffer, 12); + _name = Common::String(buffer); + s.read(buffer, 41); + _description = Common::String(buffer); + + _examine.clear(); + _sequences = nullptr; + _images = nullptr; + _imageFrame = nullptr; + + s.skip(4); + _sequenceOffset = s.readUint16LE(); + s.seek(10, SEEK_CUR); + + _walkCount = s.readByte(); + _allow = s.readByte(); + _frameNumber = s.readSint16LE(); + _sequenceNumber = s.readSint16LE(); + _position.x = s.readSint16LE(); + _position.y = s.readSint16LE(); + _delta.x = s.readSint16LE(); + _delta.y = s.readSint16LE(); + _type = (SpriteType)s.readUint16LE(); + _oldPosition.x = s.readSint16LE(); + _oldPosition.y = s.readSint16LE(); + _oldSize.x = s.readUint16LE(); + _oldSize.y = s.readUint16LE(); + _goto.x = s.readSint16LE(); + _goto.y = s.readSint16LE(); + + _pickup = s.readByte(); + _defaultCommand = s.readByte(); + _lookFlag = s.readUint16LE(); + _pickupFlag = s.readUint16LE(); + _requiredFlag = s.readSint16LE(); + _noShapeSize.x = s.readUint16LE(); + _noShapeSize.y = s.readUint16LE(); + _status = s.readUint16LE(); + _misc = s.readByte(); + _maxFrames = s.readUint16LE(); + _flags = s.readByte(); + _aOpen.load(s); + _aType = (AType)s.readByte(); + _lookFrames = s.readByte(); + _seqCounter = s.readByte(); + _lookPosition.x = s.readUint16LE(); + _lookPosition.y = s.readByte(); + _lookFacing = s.readByte(); + _lookcAnim = s.readByte(); + _aClose.load(s); + _seqStack = s.readByte(); + _seqTo = s.readByte(); + _descOffset = s.readUint16LE(); + _seqCounter2 = s.readByte(); + _seqSize = s.readUint16LE(); + s.skip(1); + _aMove.load(s); + s.skip(8); + + for (int idx = 0; idx < USE_COUNT; ++idx) + _use[idx].load(s); +} + +void Object::toggleHidden() { + if (_type != HIDDEN && _type != HIDE_SHAPE && _type != INVALID) { + if (_seqTo != 0) + _sequences[_frameNumber] = _seqTo + SEQ_TO_CODE + 128; + _seqTo = 0; + + if (_images == nullptr || _images->size() == 0) + // No shape to erase, so flag as hidden + _type = HIDDEN; + else + // Otherwise, flag it to be hidden after it gets erased + _type = HIDE_SHAPE; + } else if (_type != INVALID) { + if (_seqTo != 0) + _sequences[_frameNumber] = _seqTo + SEQ_TO_CODE + 128; + _seqTo = 0; + + _seqCounter = _seqCounter2 = 0; + _seqStack = 0; + _frameNumber = -1; + + if (_images == nullptr || _images->size() == 0) { + _type = NO_SHAPE; + } else { + _type = ACTIVE_BG_SHAPE; + int idx = _sequences[0]; + if (idx >= _maxFrames) + // Turn on: set up first frame + idx = 0; + + _imageFrame = &(*_images)[idx]; + } + } +} + +void Object::checkObject() { + Scene &scene = *_vm->_scene; + Sound &sound = *_vm->_sound; + int checkFrame = _allow ? MAX_FRAME : FRAMES_END; + bool codeFound; + + if (_seqTo) { + byte *ptr = &_sequences[_frameNumber]; + if (*ptr == _seqTo) { + // The sequence is completed + *ptr = _seqTo + SEQ_TO_CODE + 128; // Reset to normal + _seqTo = 0; + } else { + // Continue doing sequence + if (*ptr > _seqTo) + *ptr -= 1; + else + *ptr += 1; + + return; + } + } + + ++_frameNumber; + + do { + // Check for end of sequence + codeFound = checkEndOfSequence(); + + if (_sequences[_frameNumber] >= 128 && _frameNumber < checkFrame) { + codeFound = true; + int v = _sequences[_frameNumber]; + + if (v >= 228) { + // Goto code found + v -= 228; + _seqCounter2 = _seqCounter; + _seqStack = _frameNumber + 1; + setObjSequence(v, false); + } else if (v >= SOUND_CODE && (v < (SOUND_CODE + 30))) { + codeFound = true; + ++_frameNumber; + v -= SOUND_CODE; + + if (sound._soundOn && !_countCAnimFrames) { + if (!scene._sounds[v - 1]._name.empty() && sound._digitized) + sound.playLoadedSound(v - 1, WAIT_RETURN_IMMEDIATELY); + } + } else if (v >= FLIP_CODE && v < (FLIP_CODE + 3)) { + // Flip code + codeFound = true; + ++_frameNumber; + v -= FLIP_CODE; + + // Alter the flipped status + switch (v) { + case 0: + // Clear the flag + _flags &= ~OBJ_FLIPPED; + break; + case 1: + // Set the flag + _flags |= OBJ_FLIPPED; + break; + case 2: + // Toggle the flag + _flags ^= OBJ_FLIPPED; + break; + default: + break; + } + } else { + v -= 128; + + // 68-99 is a squence code + if (v > SEQ_TO_CODE) { + byte *p = &_sequences[_frameNumber]; + v -= SEQ_TO_CODE; // # from 1-32 + _seqTo = v; + *p = *(p - 1); + + if (*p > 128) + // If the high bit is set, convert to a real frame + *p -= (byte)(SEQ_TO_CODE - 128); + + if (*p > _seqTo) + *p -= 1; + else + *p += 1; + + // Will be incremented below to return back to original value + --_frameNumber; + v = 0; + } else if (v == 10) { + // Set delta for objects + Common::Point pt(_sequences[_frameNumber + 1], _sequences[_frameNumber + 2]); + if (pt.x > 128) + pt.x = (pt.x - 128) * -1; + else + pt.x--; + + if (pt.y > 128) + pt.y = (pt.y - 128) * -1; + else + pt.y--; + + _delta = pt; + _frameNumber += 2; + } else if (v < USE_COUNT) { + for (int idx = 0; idx < NAMES_COUNT; ++idx) { + checkNameForCodes(_use[v]._names[idx], nullptr); + } + + if (_use[v]._useFlag) + _vm->setFlags(_use[v]._useFlag); + } + + ++_frameNumber; + } + } + } while (codeFound); +} + +bool Object::checkEndOfSequence() { + Screen &screen = *_vm->_screen; + int checkFrame = _allow ? MAX_FRAME : FRAMES_END; + bool result = false; + + if (_type == REMOVE || _type == INVALID) + return false; + + if (_sequences[_frameNumber] == 0 || _frameNumber >= checkFrame) { + result = true; + + if (_frameNumber >= (checkFrame - 1)) { + _frameNumber = START_FRAME; + } else { + // Determine next sequence to use + int seq = _sequences[_frameNumber + 1]; + + if (seq == 99) { + --_frameNumber; + screen._backBuffer1.transBlitFrom(*_imageFrame, _position); + screen._backBuffer2.transBlitFrom(*_imageFrame, _position); + _type = INVALID; + } else { + setObjSequence(seq, false); + } + } + + if (_allow && _frameNumber == 0) { + // canimation just ended + if (_type != NO_SHAPE && _type != REMOVE) { + _type = REMOVE; + + if (!_countCAnimFrames) { + // Save details before shape is removed + _delta.x = _imageFrame->_frame.w; + _delta.y = _imageFrame->_frame.h; + _position += _imageFrame->_offset; + + // Free the images + delete _images; + _images = nullptr; + _imageFrame = nullptr; + } + } else { + _type = INVALID; + } + } + } + + return result; +} + +void Object::setObjSequence(int seq, bool wait) { + Scene &scene = *_vm->_scene; + int checkFrame = _allow ? MAX_FRAME : FRAMES_END; + + if (seq >= 128) { + // Loop the sequence until the count exceeded + seq -= 128; + + ++_seqCounter; + if (_seqCounter >= seq) { + // Go to next sequence + if (_seqStack) { + _frameNumber = _seqStack; + _seqStack = 0; + _seqCounter = _seqCounter2; + _seqCounter2 = 0; + if (_frameNumber >= checkFrame) + _frameNumber = START_FRAME; + + return; + } + + _frameNumber += 2; + if (_frameNumber >= checkFrame) + _frameNumber = 0; + + _seqCounter = 0; + if (_sequences[_frameNumber] == 0) + seq = _sequences[_frameNumber + 1]; + else + return; + } else { + // Find beginning of sequence + do { + --_frameNumber; + } while (_frameNumber > 0 && _sequences[_frameNumber] != 0); + + if (_frameNumber != 0) + _frameNumber += 2; + + return; + } + } else { + // Reset sequence counter + _seqCounter = 0; + } + + int idx = 0; + int seqCc = 0; + + while (seqCc < seq && idx < checkFrame) { + ++idx; + if (_sequences[idx] == 0) { + ++seqCc; + idx += 2; + } + } + + if (idx >= checkFrame) + idx = 0; + _frameNumber = idx; + + if (wait) { + seqCc = idx; + while (_sequences[idx] != 0) + ++idx; + + idx = idx - seqCc + 2; + for (; idx > 0; --idx) + scene.doBgAnim(); + } +} + +int Object::checkNameForCodes(const Common::String &name, const char *const messages[]) { + Map &map = *_vm->_map; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + bool printed = false; + + scene.toggleObject(name); + + if (name.hasPrefix("*")) { + // A code was found + printed = true; + char ch = (name == "*") ? 0 : toupper(name[1]); + + switch (ch) { + case 'C': + talk.talkTo(name.c_str() + 2); + break; + + case 'T': + case 'B': + case 'F': + case 'W': + // Nothing: action was already done before canimation + break; + + case 'G': + case 'A': { + // G: Have object go somewhere + // A: Add onto existing co-ordinates + Common::String sx(name.c_str() + 2, name.c_str() + 5); + Common::String sy(name.c_str() + 6, name.c_str() + 9); + + if (ch == 'G') + _position = Common::Point(atoi(sx.c_str()), atoi(sy.c_str())); + else + _position += Common::Point(atoi(sx.c_str()), atoi(sy.c_str())); + break; + } + + default: + if (ch >= '0' && ch <= '9') { + scene._goToScene = atoi(name.c_str() + 1); + + if (scene._goToScene < 97 && map[scene._goToScene].x) { + map._overPos.x = map[scene._goToScene].x * 100 - 600; + map._overPos.y = map[scene._goToScene].y * 100 + 900; + } + + const char *p; + if ((p = strchr(name.c_str(), ',')) != nullptr) { + ++p; + + Common::String s(p, p + 3); + people._hSavedPos.x = atoi(s.c_str()); + + s = Common::String(p + 3, p + 6); + people._hSavedPos.y = atoi(s.c_str()); + + s = Common::String(p + 6, p + 9); + people._hSavedFacing = atoi(s.c_str()); + if (people._hSavedFacing == 0) + people._hSavedFacing = 10; + } else if ((p = strchr(name.c_str(), '/')) != nullptr) { + people._hSavedPos = Common::Point(1, 0); + people._hSavedFacing = 100 + atoi(p + 1); + } + } else { + scene._goToScene = 100; + } + + people[AL]._position = Common::Point(0, 0); + break; + } + } else if (name.hasPrefix("!")) { + // Message attached to canimation + int messageNum = atoi(name.c_str() + 1); + ui._infoFlag = true; + ui.clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", messages[messageNum]); + ui._menuCounter = 25; + } else if (name.hasPrefix("@")) { + // Message attached to canimation + ui._infoFlag = true; + ui.clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", name.c_str() + 1); + printed = true; + ui._menuCounter = 25; + } + + return printed; +} + +void Object::setFlagsAndToggles() { + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + for (int useIdx = 0; useIdx < USE_COUNT; ++useIdx) { + if (_use[useIdx]._useFlag) { + if (!_vm->readFlags(_use[useIdx]._useFlag)) + _vm->setFlags(_use[useIdx]._useFlag); + } + + if (_use[useIdx]._cAnimSpeed) { + if (_use[useIdx]._cAnimNum == 0) + // 0 is really a 10 + scene.startCAnim(9, _use[useIdx]._cAnimSpeed); + else + scene.startCAnim(_use[useIdx]._cAnimNum - 1, _use[useIdx]._cAnimSpeed); + } + + if (!talk._talkToAbort) { + for (int idx = 0; idx < NAMES_COUNT; ++idx) + scene.toggleObject(_use[useIdx]._names[idx]); + } + } +} + +void Object::adjustObject() { + if (_type == REMOVE) + return; + + _position += _delta; + + if (_position.y > LOWER_LIMIT) + _position.y = LOWER_LIMIT; + + if (_type != NO_SHAPE) { + int frame = _frameNumber; + if (frame == -1) + frame = 0; + + int imgNum = _sequences[frame]; + if (imgNum > _maxFrames) + imgNum = 1; + + _imageFrame = &(*_images)[imgNum - 1]; + } +} + +int Object::pickUpObject(const char *const messages[]) { + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + int pickup = _pickup & 0x7f; + bool printed = false; + int numObjects = 0; + + if (pickup == 99) { + for (int idx = 0; idx < NAMES_COUNT && !talk._talkToAbort; ++idx) { + if (checkNameForCodes(_use[0]._names[idx], nullptr)) { + if (!talk._talkToAbort) + printed = true; + } + } + + return 0; + } + + if (!pickup || (pickup > 50 && pickup <= 80)) { + int message = _pickup; + if (message > 50) + message -= 50; + + ui._infoFlag = true; + ui.clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", messages[message]); + ui._menuCounter = 30; + } else { + // Pick it up + bool takeFlag = true; + if ((_pickup & 0x80) == 0) { + // Play an animation + if (pickup > 80) { + takeFlag = false; // Don't pick it up + scene.startCAnim(pickup - 81, 1); + if (_pickupFlag) + _vm->setFlags(_pickupFlag); + } else { + scene.startCAnim(pickup - 1, 1); + if (!talk._talkToAbort) { + // Erase the shape + _type = _type == NO_SHAPE ? INVALID : REMOVE; + } + } + + if (talk._talkToAbort) + return 0; + } else { + // Play generic pickup sequence + // Original moved cursor position here + people.goAllTheWay(); + ui._menuCounter = 25; + ui._temp1 = 1; + } + + for (int idx = 0; idx < NAMES_COUNT && !talk._talkToAbort; ++idx) { + if (checkNameForCodes(_use[0]._names[idx], nullptr)) { + if (!talk._talkToAbort) + printed = true; + } + } + if (talk._talkToAbort) + return 0; + + // Add the item to the player's inventory + if (takeFlag) + numObjects = inv.putItemInInventory(*this); + + if (!printed) { + ui._infoFlag = true; + ui.clearInfo(); + + Common::String itemName = _description; + itemName.setChar(tolower(itemName[0]), 0); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "Picked up %s", itemName.c_str()); + ui._menuCounter = 25; + } + } + + return numObjects; +} + +const Common::Rect Object::getNewBounds() const { + Common::Point pt = _position; + if (_imageFrame) + pt += _imageFrame->_offset; + + return Common::Rect(pt.x, pt.y, pt.x + frameWidth(), pt.y + frameHeight()); +} + +const Common::Rect Object::getNoShapeBounds() const { + return Common::Rect(_position.x, _position.y, + _position.x + _noShapeSize.x, _position.y + _noShapeSize.y); +} + +const Common::Rect Object::getOldBounds() const { + return Common::Rect(_oldPosition.x, _oldPosition.y, + _oldPosition.x + _oldSize.x, _oldPosition.y + _oldSize.y); +} + +/*----------------------------------------------------------------*/ + +void CAnim::load(Common::SeekableReadStream &s) { + char buffer[12]; + s.read(buffer, 12); + _name = Common::String(buffer); + + s.read(_sequences, 30); + _position.x = s.readSint16LE(); + _position.y = s.readSint16LE(); + _size = s.readUint32LE(); + _type = (SpriteType)s.readUint16LE(); + _flags = s.readByte(); + _goto.x = s.readSint16LE(); + _goto.y = s.readSint16LE(); + _gotoDir = s.readSint16LE(); + _teleportPos.x = s.readSint16LE(); + _teleportPos.y = s.readSint16LE(); + _teleportDir = s.readSint16LE(); +} + +/*----------------------------------------------------------------*/ + +SceneImage::SceneImage() { + _images = nullptr; + _maxFrames = 0; + _filesize = 0; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/objects.h b/engines/sherlock/objects.h new file mode 100644 index 0000000000..6dbe645d4b --- /dev/null +++ b/engines/sherlock/objects.h @@ -0,0 +1,356 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_OBJECTS_H +#define SHERLOCK_OBJECTS_H + +#include "common/scummsys.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "common/str.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +class SherlockEngine; + +enum ObjectAllow { + ALLOW_MOVE = 1, ALLOW_OPEN = 2, ALLOW_CLOSE = 4 +}; + +enum SpriteType { + INVALID = 0, + CHARACTER = 1, + CURSOR = 2, + STATIC_BG_SHAPE = 3, // Background shape that doesn't animate + ACTIVE_BG_SHAPE = 4, // Background shape that animates + REMOVE = 5, // Object should be removed next frame + NO_SHAPE = 6, // Background object with no shape + HIDDEN = 7, // Hidden backgruond object + HIDE_SHAPE = 8 // Object needs to be hidden +}; + +enum AType { + OBJECT = 0, + PERSON = 1, + SOLID = 2, + TALK = 3, // Standard talk zone + FLAG_SET = 4, + DELTA = 5, + WALK_AROUND = 6, + TALK_EVERY = 7, // Talk zone that turns on every room visit + TALK_MOVE = 8, // Talk zone that only activates when Holmes moves + PAL_CHANGE = 9, // Changes the palette down so that it gets darker + PAL_CHANGE2 = 10, // Same as PAL_CHANGE, except that it goes up + SCRIPT_ZONE = 11, // If this is clicked in, it is activated + BLANK_ZONE = 12, // This masks out other objects when entered + NOWALK_ZONE = 13 // Player cannot walk here +}; + +// Different levels for sprites to be at +enum { + BEHIND = 0, NORMAL_BEHIND = 1, NORMAL_FORWARD = 2, FORWARD = 3 +}; + +#define MAX_HOLMES_SEQUENCE 16 +#define MAX_FRAME 30 + +// code put into sequences to defines 1-10 type seqs +#define SEQ_TO_CODE 67 +#define FLIP_CODE (64 + 128) +#define SOUND_CODE (34 + 128) + +class Point32 { +public: + int x; + int y; + + Point32() : x(0), y(0) {} + Point32(int x1, int y1) : x(x1), y(y1) {} + Point32(const Common::Point &pt) : x(pt.x), y(pt.y) {} + + bool operator==(const Point32 &p) const { return x == p.x && y == p.y; } + bool operator!=(const Point32 &p) const { return x != p.x || y != p.y; } + Point32 operator+(const Point32 &delta) const { return Point32(x + delta.x, y + delta.y); } + Point32 operator-(const Point32 &delta) const { return Point32(x - delta.x, y - delta.y); } + operator Common::Point() { return Common::Point(x, y); } + + void operator+=(const Point32 &delta) { x += delta.x; y += delta.y; } + void operator-=(const Point32 &delta) { x -= delta.x; y -= delta.y; } +}; + +class Sprite { +private: + static SherlockEngine *_vm; +public: + Common::String _name; + Common::String _description; + Common::StringArray _examine; // Examine in-depth description + Common::String _pickUp; // Message for if you can't pick up object + + const uint8 (*_sequences)[MAX_HOLMES_SEQUENCE][MAX_FRAME]; // Holds animation sequences + ImageFile *_images; // Sprite images + ImageFrame *_imageFrame; // Pointer to shape in the images + int _walkCount; // Character walk counter + int _allow; // Allowed menu commands - ObjectAllow + int _frameNumber; // Frame number in rame sequence to draw + int _sequenceNumber; // Sequence being used + Point32 _position; // Current position + Point32 _delta; // Momvement delta + Common::Point _oldPosition; // Old position + Common::Point _oldSize; // Image's old size + Common::Point _goto; // Walk destination + SpriteType _type; // Type of object + int _pickup; + Common::Point _noShapeSize; // Size of a NO_SHAPE + int _status; // Status: open/closed, moved/not moved + int8 _misc; // Miscellaneous use + int _numFrames; // How many frames the object has +public: + Sprite() { clear(); } + static void setVm(SherlockEngine *vm) { _vm = vm; } + + /** + * Reset the data for the sprite + */ + void clear(); + + /** + * Updates the image frame poiner for the sprite + */ + void setImageFrame(); + + /** + * This adjusts the sprites position, as well as it's animation sequence: + */ + void adjustSprite(); + + /** + * Checks the sprite's position to see if it's collided with any special objects + */ + void checkSprite(); + + /** + * Return frame width + */ + int frameWidth() const { return _imageFrame ? _imageFrame->_frame.w : 0; } + + /** + * Return frame height + */ + int frameHeight() const { return _imageFrame ? _imageFrame->_frame.h : 0; } +}; + +enum { REVERSE_DIRECTION = 0x80 }; +#define NAMES_COUNT 4 + +struct ActionType { + int _cAnimNum; + int _cAnimSpeed; + Common::String _names[NAMES_COUNT]; + + /** + * Load the data for the action + */ + void load(Common::SeekableReadStream &s); +}; + +struct UseType { + int _cAnimNum; + int _cAnimSpeed; + Common::String _names[NAMES_COUNT]; + int _useFlag; // Which flag USE will set (if any) + Common::String _target; + + UseType(); + + /** + * Load the data for the UseType + */ + void load(Common::SeekableReadStream &s); +}; + +enum { OBJ_BEHIND = 1, OBJ_FLIPPED = 2, OBJ_FORWARD = 4, TURNON_OBJ = 0x20, TURNOFF_OBJ = 0x40 }; +#define USE_COUNT 4 + +class Object { +private: + static SherlockEngine *_vm; + + /** + * This will check to see if the object has reached the end of a sequence. + * If it has, it switch to whichever next sequence should be started. + * @returns true if the end of a sequence was reached + */ + bool checkEndOfSequence(); + + /** + * Scans through the sequences array and finds the designated sequence. + * It then sets the frame number of the start of that sequence + */ + void setObjSequence(int seq, bool wait); +public: + static bool _countCAnimFrames; + + static void setVm(SherlockEngine *vm); +public: + Common::String _name; // Name + Common::String _description; // Description lines + Common::String _examine; // Examine in-depth description + int _sequenceOffset; + uint8 *_sequences; // Holds animation sequences + ImageFile *_images; // Sprite images + ImageFrame *_imageFrame; // Pointer to shape in the images + int _walkCount; // Character walk counter + int _allow; // Allowed menu commands - ObjectAllow + int _frameNumber; // Frame number in rame sequence to draw + int _sequenceNumber; // Sequence being used + SpriteType _type; // Object type + Common::Point _position; // Current position + Common::Point _delta; // Momvement amount + Common::Point _oldPosition; // Old position + Common::Point _oldSize; // Image's old size + Common::Point _goto; // Walk destination + + int _pickup; + int _defaultCommand; // Default right-click command + int _lookFlag; // Which flag LOOK will set (if any) + int _pickupFlag; // Which flag PICKUP will set (if any) + int _requiredFlag; // Object will be hidden if not set + Common::Point _noShapeSize; // Size of a NO_SHAPE + int _status; // Status (open/closed, moved/not) + int8 _misc; // Misc field -- use varies with type + int _maxFrames; // Number of frames + int _flags; // Tells if object can be walked behind + ActionType _aOpen; // Holds data for moving object + AType _aType; // Tells if this is an object, person, talk, etc. + int _lookFrames; // How many frames to play of the look anim before pausing + int _seqCounter; // How many times this sequence has been executed + Common::Point _lookPosition; // Where to walk when examining object + int _lookFacing; // Direction to face when examining object + int _lookcAnim; + ActionType _aClose; + int _seqStack; // Allows gosubs to return to calling frame + int _seqTo; // Allows 1-5, 8-3 type sequences encoded in 2 bytes + uint _descOffset; // Tells where description starts in DescText + int _seqCounter2; // Counter of calling frame sequence + uint _seqSize; // Tells where description starts + ActionType _aMove; + UseType _use[USE_COUNT]; + + Object(); + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); + + /** + * Toggle the type of an object between hidden and active + */ + void toggleHidden(); + + /** + * Check the state of the object + */ + void checkObject(); + + /** + * Checks for codes + * @param name The name to check for codes + * @param messages Provides a lookup list of messages that can be printed + * @returns 0 if no codes are found, 1 if codes were found + */ + int checkNameForCodes(const Common::String &name, const char *const messages[]); + + /** + * Handle setting any flags associated with the object + */ + void setFlagsAndToggles(); + + /** + * Adjusts the sprite's position and animation sequence, advancing by 1 frame. + * If the end of the sequence is reached, the appropriate action is taken. + */ + void adjustObject(); + + /** + * Handles trying to pick up an object. If allowed, plays an y necessary animation for picking + * up the item, and then adds it to the player's inventory + */ + int pickUpObject(const char *const messages[]); + + /** + * Return the frame width + */ + int frameWidth() const { return _imageFrame ? _imageFrame->_frame.w : 0; } + + /** + * Return the frame height + */ + int frameHeight() const { return _imageFrame ? _imageFrame->_frame.h : 0; } + + /** + * Returns the current bounds for the sprite + */ + const Common::Rect getNewBounds() const; + + /** + * Returns the bounds for a sprite without a shape + */ + const Common::Rect getNoShapeBounds() const; + + /** + * Returns the old bounsd for the sprite from the previous frame + */ + const Common::Rect getOldBounds() const; +}; + +struct CAnim { + Common::String _name; // Name + byte _sequences[MAX_FRAME]; // Animation sequences + Common::Point _position; // Position + int _size; // Size of uncompressed animation + SpriteType _type; + int _flags; // Tells if can be walked behind + Common::Point _goto; // coords holmes should walk to before starting canim + int _gotoDir; + Common::Point _teleportPos; // Location Holmes shoul teleport to after + int _teleportDir; // playing canim + + /** + * Load the data for the animation + */ + void load(Common::SeekableReadStream &s); +}; + +struct SceneImage { + ImageFile *_images; // Object images + int _maxFrames; // How many frames in object + int _filesize; // File size + + SceneImage(); +} ; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/people.cpp b/engines/sherlock/people.cpp new file mode 100644 index 0000000000..3a630fdd99 --- /dev/null +++ b/engines/sherlock/people.cpp @@ -0,0 +1,567 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/people.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +// Walk speeds +#define MWALK_SPEED 2 +#define XWALK_SPEED 4 +#define YWALK_SPEED 1 + +// Characer animation sequences +static const uint8 CHARACTER_SEQUENCES[MAX_HOLMES_SEQUENCE][MAX_FRAME] = { + { 29, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Right + { 22, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Down + { 29, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Left + { 15, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Up + { 42, 1, 2, 3, 4, 5, 0 }, // Goto Stand Right + { 47, 1, 2, 3, 4, 5, 0 }, // Goto Stand Down + { 42, 1, 2, 3, 4, 5, 0 }, // Goto Stand Left + { 36, 1, 0 }, // Goto Stand Up + { 8, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Up Right + { 1, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Down Right + { 8, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Up Left + { 1, 1, 2, 3, 4, 5, 6, 7, 0 }, // Walk Down Left + { 37, 1, 2, 3, 4, 5, 0 }, // Goto Stand Up Right + { 37, 1, 2, 3, 4, 5, 0 }, // Goto Stand Up Left + { 52, 1, 2, 3, 4, 0 }, // Goto Stand Down Right + { 52, 1, 2, 3, 4, 0 } // Goto Stand Down Left +}; + +/*----------------------------------------------------------------*/ + +People::People(SherlockEngine *vm) : _vm(vm), _player(_data[0]) { + _walkLoaded = false; + _holmesOn = true; + _oldWalkSequence = -1; + _allowWalkAbort = false; + _portraitLoaded = false; + _portraitsOn = true; + _clearingThePortrait = false; + _srcZone = _destZone = 0; + _talkPics = nullptr; + _portraitSide = 0; + _speakerFlip = false; + _holmesFlip = false; + _holmesQuotient = 0; + _hSavedPos = Common::Point(-1, -1); + _hSavedFacing = -1; + + _portrait._sequences = new byte[32]; +} + +People::~People() { + if (_walkLoaded) + delete _data[PLAYER]._images; + delete _talkPics; + delete[] _portrait._sequences; +} + +void People::reset() { + // Note: The engine has theoretical support for two player characters but only the first one is used. + // Watson is, instead, handled by a different sprite in each scene, with a very simple initial movement, if any + Sprite &p = _data[PLAYER]; + + p._description = "Sherlock Holmes!"; + p._type = CHARACTER; + p._position = Common::Point(10000, 11000); + p._sequenceNumber = STOP_DOWNRIGHT; + p._sequences = &CHARACTER_SEQUENCES; + p._imageFrame = nullptr; + p._frameNumber = 1; + p._delta = Common::Point(0, 0); + p._oldPosition = Common::Point(0, 0); + p._oldSize = Common::Point(0, 0); + p._misc = 0; + p._walkCount = 0; + p._pickUp = ""; + p._allow = 0; + p._noShapeSize = Common::Point(0, 0); + p._goto = Common::Point(0, 0); + p._status = 0; + + // Reset any walk path in progress when Sherlock leaves scenes + _walkTo.clear(); +} + +bool People::loadWalk() { + if (_walkLoaded) { + return false; + } else { + _data[PLAYER]._images = new ImageFile("walk.vgs"); + _data[PLAYER].setImageFrame(); + _walkLoaded = true; + + return true; + } +} + +bool People::freeWalk() { + if (_walkLoaded) { + delete _player._images; + _player._images = nullptr; + + _walkLoaded = false; + return true; + } else { + return false; + } +} + +void People::setWalking() { + Map &map = *_vm->_map; + Scene &scene = *_vm->_scene; + int oldDirection, oldFrame; + Common::Point speed, delta; + + // Flag that player has now walked in the scene + scene._walkedInScene = true; + + // Stop any previous walking, since a new dest is being set + _player._walkCount = 0; + oldDirection = _player._sequenceNumber; + oldFrame = _player._frameNumber; + + // Set speed to use horizontal and vertical movement + if (map._active) { + speed = Common::Point(MWALK_SPEED, MWALK_SPEED); + } else { + speed = Common::Point(XWALK_SPEED, YWALK_SPEED); + } + + // If the player is already close to the given destination that no + // walking is needed, move to the next straight line segment in the + // overall walking route, if there is one + for (;;) { + // Since we want the player to be centered on the destination they + // clicked, but characters draw positions start at their left, move + // the destination half the character width to draw him centered + int temp; + if (_walkDest.x >= (temp = _player._imageFrame->_frame.w / 2)) + _walkDest.x -= temp; + + delta = Common::Point( + ABS(_player._position.x / 100 - _walkDest.x), + ABS(_player._position.y / 100 - _walkDest.y) + ); + + // If we're ready to move a sufficient distance, that's it. Otherwise, + // move onto the next portion of the walk path, if there is one + if ((delta.x > 3 || delta.y > 0) || _walkTo.empty()) + break; + + // Pop next walk segment off the walk route stack + _walkDest = _walkTo.pop(); + } + + // If a sufficient move is being done, then start the move + if (delta.x > 3 || delta.y) { + // See whether the major movement is horizontal or vertical + if (delta.x >= delta.y) { + // Set the initial frame sequence for the left and right, as well + // as setting the delta x depending on direction + if (_walkDest.x < (_player._position.x / 100)) { + _player._sequenceNumber = (map._active ? (int)MAP_LEFT : (int)WALK_LEFT); + _player._delta.x = speed.x * -100; + } else { + _player._sequenceNumber = (map._active ? (int)MAP_RIGHT : (int)WALK_RIGHT); + _player._delta.x = speed.x * 100; + } + + // See if the x delta is too small to be divided by the speed, since + // this would cause a divide by zero error + if (delta.x >= speed.x) { + // Det the delta y + _player._delta.y = (delta.y * 100) / (delta.x / speed.x); + if (_walkDest.y < (_player._position.y / 100)) + _player._delta.y = -_player._delta.y; + + // Set how many times we should add the delta to the player's position + _player._walkCount = delta.x / speed.x; + } else { + // The delta x was less than the speed (ie. we're really close to + // the destination). So set delta to 0 so the player won't move + _player._delta = Common::Point(0, 0); + _player._position = Common::Point(_walkDest.x * 100, _walkDest.y * 100); + _player._walkCount = 1; + } + + // See if the sequence needs to be changed for diagonal walking + if (_player._delta.y > 150) { + if (!map._active) { + switch (_player._sequenceNumber) { + case WALK_LEFT: + _player._sequenceNumber = WALK_DOWNLEFT; + break; + case WALK_RIGHT: + _player._sequenceNumber = WALK_DOWNRIGHT; + break; + } + } + } else if (_player._delta.y < -150) { + if (!map._active) { + switch (_player._sequenceNumber) { + case WALK_LEFT: + _player._sequenceNumber = WALK_UPLEFT; + break; + case WALK_RIGHT: + _player._sequenceNumber = WALK_UPRIGHT; + break; + } + } + } + } else { + // Major movement is vertical, so set the sequence for up and down, + // and set the delta Y depending on the direction + if (_walkDest.y < (_player._position.y / 100)) { + _player._sequenceNumber = WALK_UP; + _player._delta.y = speed.y * -100; + } else { + _player._sequenceNumber = WALK_DOWN; + _player._delta.y = speed.y * 100; + } + + // If we're on the overhead map, set the sequence so we keep moving + // in the same direction + if (map._active) + _player._sequenceNumber = (oldDirection == -1) ? MAP_RIGHT : oldDirection; + + // Set the delta x + _player._delta.x = (delta.x * 100) / (delta.y / speed.y); + if (_walkDest.x < (_player._position.x / 100)) + _player._delta.x = -_player._delta.x; + + _player._walkCount = delta.y / speed.y; + } + } + + // See if the new walk sequence is the same as the old. If it's a new one, + // we need to reset the frame number to zero so it's animation starts at + // it's beginning. Otherwise, if it's the same sequence, we can leave it + // as is, so it keeps the animation going at wherever it was up to + if (_player._sequenceNumber != _oldWalkSequence) + _player._frameNumber = 0; + _oldWalkSequence = _player._sequenceNumber; + + if (!_player._walkCount) + gotoStand(_player); + + // If the sequence is the same as when we started, then Holmes was + // standing still and we're trying to re-stand him, so reset Holmes' + // rame to the old frame number from before it was reset to 0 + if (_player._sequenceNumber == oldDirection) + _player._frameNumber = oldFrame; +} + +void People::gotoStand(Sprite &sprite) { + Map &map = *_vm->_map; + _walkTo.clear(); + sprite._walkCount = 0; + + switch (sprite._sequenceNumber) { + case WALK_UP: + sprite._sequenceNumber = STOP_UP; + break; + case WALK_DOWN: + sprite._sequenceNumber = STOP_DOWN; + break; + case TALK_LEFT: + case WALK_LEFT: + sprite._sequenceNumber = STOP_LEFT; + break; + case TALK_RIGHT: + case WALK_RIGHT: + sprite._sequenceNumber = STOP_RIGHT; + break; + case WALK_UPRIGHT: + sprite._sequenceNumber = STOP_UPRIGHT; + break; + case WALK_UPLEFT: + sprite._sequenceNumber = STOP_UPLEFT; + break; + case WALK_DOWNRIGHT: + sprite._sequenceNumber = STOP_DOWNRIGHT; + break; + case WALK_DOWNLEFT: + sprite._sequenceNumber = STOP_DOWNLEFT; + break; + default: + break; + } + + // Only restart frame at 0 if the sequence number has changed + if (_oldWalkSequence != -1 || sprite._sequenceNumber == STOP_UP) + sprite._frameNumber = 0; + + if (map._active) { + sprite._sequenceNumber = 0; + _player._position.x = (map[map._charPoint].x - 6) * 100; + _player._position.y = (map[map._charPoint].x + 10) * 100; + } + + _oldWalkSequence = -1; + _allowWalkAbort = true; +} + +void People::walkToCoords(const Common::Point &destPos, int destDir) { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + CursorId oldCursor = events.getCursor(); + events.setCursor(WAIT); + + _walkDest = Common::Point(destPos.x / 100 + 10, destPos.y / 100); + _allowWalkAbort = true; + goAllTheWay(); + + // Keep calling doBgAnim until the walk is done + do { + events.pollEventsAndWait(); + scene.doBgAnim(); + } while (!_vm->shouldQuit() && _player._walkCount); + + if (!talk._talkToAbort) { + // Put player exactly on destination position, and set direction + _player._position = destPos; + _player._sequenceNumber = destDir; + gotoStand(_player); + + // Draw Holmes facing the new direction + scene.doBgAnim(); + + if (!talk._talkToAbort) + events.setCursor(oldCursor); + } +} + +void People::goAllTheWay() { + Scene &scene = *_vm->_scene; + Common::Point srcPt(_player._position.x / 100 + _player.frameWidth() / 2, + _player._position.y / 100); + + // Get the zone the player is currently in + _srcZone = scene.whichZone(srcPt); + if (_srcZone == -1) + _srcZone = scene.closestZone(srcPt); + + // Get the zone of the destination + _destZone = scene.whichZone(_walkDest); + if (_destZone == -1) { + _destZone = scene.closestZone(_walkDest); + + // The destination isn't in a zone + if (_walkDest.x >= (SHERLOCK_SCREEN_WIDTH - 1)) + _walkDest.x = SHERLOCK_SCREEN_WIDTH - 2; + + // Trace a line between the centroid of the found closest zone to + // the destination, to find the point at which the zone will be left + const Common::Rect &destRect = scene._zones[_destZone]; + const Common::Point destCenter((destRect.left + destRect.right) / 2, + (destRect.top + destRect.bottom) / 2); + const Common::Point delta = _walkDest - destCenter; + Common::Point pt(destCenter.x * 100, destCenter.y * 100); + + // Move along the line until the zone is left + do { + pt += delta; + } while (destRect.contains(pt.x / 100, pt.y / 100)); + + // Set the new walk destination to the last point that was in the + // zone just before it was left + _walkDest = Common::Point((pt.x - delta.x * 2) / 100, + (pt.y - delta.y * 2) / 100); + } + + // Only do a walk if both zones are acceptable + if (_srcZone == -2 || _destZone == -2) + return; + + // If the start and dest zones are the same, walk directly to the dest point + if (_srcZone == _destZone) { + setWalking(); + } else { + // Otherwise a path needs to be formed from the path information + int i = scene._walkDirectory[_srcZone][_destZone]; + + // See if we need to use a reverse path + if (i == -1) + i = scene._walkDirectory[_destZone][_srcZone]; + + int count = scene._walkData[i]; + ++i; + + // See how many points there are between the src and dest zones + if (!count || count == -1) { + // There are none, so just walk to the new zone + setWalking(); + } else { + // There are points, so set up a multi-step path between points + // to reach the given destination + _walkTo.clear(); + + if (scene._walkDirectory[_srcZone][_destZone] != -1) { + i += 3 * (count - 1); + for (int idx = 0; idx < count; ++idx, i -= 3) { + _walkTo.push(Common::Point(READ_LE_UINT16(&scene._walkData[i]), + scene._walkData[i + 2])); + } + } else { + for (int idx = 0; idx < count; ++idx, i += 3) { + _walkTo.push(Common::Point(READ_LE_UINT16(&scene._walkData[i]), scene._walkData[i + 2])); + } + } + + // Final position + _walkTo.push(_walkDest); + + // Start walking + _walkDest = _walkTo.pop(); + setWalking(); + } + } +} + +int People::findSpeaker(int speaker) { + Scene &scene = *_vm->_scene; + + for (int idx = 0; idx < (int)scene._bgShapes.size(); ++idx) { + Object &obj = scene._bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE) { + Common::String name(obj._name.c_str(), obj._name.c_str() + 4); + + if (name.equalsIgnoreCase(_characters[speaker]._portrait) + && obj._name[4] >= '0' && obj._name[4] <= '9') + return idx; + } + } + + return -1; +} + +void People::clearTalking() { + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + + if (_portraitsOn) { + Common::Point pt = _portrait._position; + int width, height; + _portrait._imageFrame = _talkPics ? &(*_talkPics)[0] : (ImageFrame *)nullptr; + + // Flag portrait for removal, and save the size of the frame to use erasing it + _portrait._type = REMOVE; + _portrait._delta.x = width = _portrait.frameWidth(); + _portrait._delta.y = height = _portrait.frameHeight(); + + delete _talkPics; + _talkPics = nullptr; + + // Flag to let the talk code know not to interrupt on the next doBgAnim + _clearingThePortrait = true; + scene.doBgAnim(); + _clearingThePortrait = false; + + screen.slamArea(pt.x, pt.y, width, height); + + if (!talk._talkToAbort) + _portraitLoaded = false; + } +} + +void People::setTalking(int speaker) { + Resources &res = *_vm->_res; + + // If no speaker is specified, then we can exit immediately + if (speaker == -1) + return; + + if (_portraitsOn) { + delete _talkPics; + Common::String filename = Common::String::format("%s.vgs", _characters[speaker]._portrait); + _talkPics = new ImageFile(filename); + + // Load portrait sequences + Common::SeekableReadStream *stream = res.load("sequence.txt"); + stream->seek(speaker * MAX_FRAME); + + int idx = 0; + do { + _portrait._sequences[idx] = stream->readByte(); + ++idx; + } while (idx < 2 || _portrait._sequences[idx - 2] || _portrait._sequences[idx - 1]); + + delete stream; + + _portrait._maxFrames = idx; + _portrait._frameNumber = 0; + _portrait._sequenceNumber = 0; + _portrait._images = _talkPics; + _portrait._imageFrame = &(*_talkPics)[0]; + _portrait._position = Common::Point(_portraitSide, 10); + _portrait._delta = Common::Point(0, 0); + _portrait._oldPosition = Common::Point(0, 0); + _portrait._goto = Common::Point(0, 0); + _portrait._flags = 5; + _portrait._status = 0; + _portrait._misc = 0; + _portrait._allow = 0; + _portrait._type = ACTIVE_BG_SHAPE; + _portrait._name = " "; + _portrait._description = " "; + _portrait._examine = " "; + _portrait._walkCount = 0; + + if (_holmesFlip || _speakerFlip) { + _portrait._flags |= 2; + + _holmesFlip = false; + _speakerFlip = false; + } + + if (_portraitSide == 20) + _portraitSide = 220; + else + _portraitSide = 20; + + _portraitLoaded = true; + } +} + +void People::synchronize(Common::Serializer &s) { + s.syncAsByte(_holmesOn); + s.syncAsSint16LE(_player._position.x); + s.syncAsSint16LE(_player._position.y); + s.syncAsSint16LE(_player._sequenceNumber); + s.syncAsSint16LE(_holmesQuotient); + + if (s.isLoading()) { + _hSavedPos = _player._position; + _hSavedFacing = _player._sequenceNumber; + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/people.h b/engines/sherlock/people.h new file mode 100644 index 0000000000..8244fd9b59 --- /dev/null +++ b/engines/sherlock/people.h @@ -0,0 +1,181 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_PEOPLE_H +#define SHERLOCK_PEOPLE_H + +#include "common/scummsys.h" +#include "common/serializer.h" +#include "common/queue.h" +#include "sherlock/objects.h" + +namespace Sherlock { + +// Player definitions. The game has theoretical support for two player characters but only the first one is used. +// Watson is, instead, handled by a different sprite in each scene, with a very simple initial movement, if any +enum PeopleId { + PLAYER = 0, + AL = 0, + PEG = 1, + MAX_PLAYERS = 2 +}; + +// Animation sequence identifiers for characters +enum { + WALK_RIGHT = 0, WALK_DOWN = 1, WALK_LEFT = 2, WALK_UP = 3, STOP_LEFT = 4, + STOP_DOWN = 5, STOP_RIGHT = 6, STOP_UP = 7, WALK_UPRIGHT = 8, + WALK_DOWNRIGHT = 9, WALK_UPLEFT = 10, WALK_DOWNLEFT = 11, + STOP_UPRIGHT = 12, STOP_UPLEFT = 13, STOP_DOWNRIGHT = 14, + STOP_DOWNLEFT = 15, TALK_RIGHT = 6, TALK_LEFT = 4 +}; + +enum { + MAP_UP = 1, MAP_UPRIGHT = 2, MAP_RIGHT = 1, MAP_DOWNRIGHT = 4, + MAP_DOWN = 5, MAP_DOWNLEFT = 6, MAP_LEFT = 2, MAP_UPLEFT = 8 +}; + +struct PersonData { + const char *_name; + const char *_portrait; + const byte *_stillSequences; + const byte *_talkSequences; + + PersonData(const char *name, const char *portrait, const byte *stillSequences, const byte *talkSequences) : + _name(name), _portrait(portrait), _stillSequences(stillSequences), _talkSequences(talkSequences) {} +}; + +class SherlockEngine; + +class Person : public Sprite { +public: + Person() : Sprite() {} + + Common::String _portrait; +}; + +class People { +private: + SherlockEngine *_vm; + Person _data[MAX_PLAYERS]; + bool _walkLoaded; + int _oldWalkSequence; + int _srcZone, _destZone; +public: + Common::Array<PersonData> _characters; + ImageFile *_talkPics; + Common::Point _walkDest; + Common::Point _hSavedPos; + int _hSavedFacing; + Common::Queue<Common::Point> _walkTo; + Person &_player; + bool _holmesOn; + bool _portraitLoaded; + bool _portraitsOn; + Object _portrait; + bool _clearingThePortrait; + bool _allowWalkAbort; + int _portraitSide; + bool _speakerFlip; + bool _holmesFlip; + int _holmesQuotient; +public: + People(SherlockEngine *vm); + ~People(); + + Person &operator[](PeopleId id) { + assert(id < MAX_PLAYERS); + return _data[id]; + } + Person &operator[](int idx) { + assert(idx < MAX_PLAYERS); + return _data[idx]; + } + + /** + * Returns true if Sherlock is visible on the screen and enabled + */ + bool isHolmesActive() const { return _walkLoaded && _holmesOn; } + + /** + * Reset the player data + */ + void reset(); + + /** + * Load the walking images for Sherlock + */ + bool loadWalk(); + + /** + * If the walk data has been loaded, then it will be freed + */ + bool freeWalk(); + + /** + * Set the variables for moving a character from one poisition to another + * in a straight line - goAllTheWay must have been previously called to + * check for any obstacles in the path. + */ + void setWalking(); + + /** + * Bring a moving character to a standing position. If the Scalpel chessboard + * is being displayed, then the chraracter will always face down. + */ + void gotoStand(Sprite &sprite); + + /** + * Walk to the co-ordinates passed, and then face the given direction + */ + void walkToCoords(const Common::Point &destPos, int destDir); + + /** + * Called to set the character walking to the current cursor location. + * It uses the zones and the inter-zone points to determine a series + * of steps to walk to get to that position. + */ + void goAllTheWay(); + + /** + * Finds the scene background object corresponding to a specified speaker + */ + int findSpeaker(int speaker); + + /** + * Turn off any currently active portraits, and removes them from being drawn + */ + void clearTalking(); + + /** + * Setup the data for an animating speaker portrait at the top of the screen + */ + void setTalking(int speaker); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/resources.cpp b/engines/sherlock/resources.cpp new file mode 100644 index 0000000000..091ef3ea12 --- /dev/null +++ b/engines/sherlock/resources.cpp @@ -0,0 +1,414 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/resources.h" +#include "sherlock/screen.h" +#include "sherlock/sherlock.h" +#include "common/debug.h" +#include "common/memstream.h" + +namespace Sherlock { + +Cache::Cache(SherlockEngine *vm) : _vm(vm) { +} + +bool Cache::isCached(const Common::String &filename) const { + return _resources.contains(filename); +} + +void Cache::load(const Common::String &name) { + // First check if the entry already exists + if (_resources.contains(name)) + return; + + // Open the file for reading + Common::File f; + if (!f.open(name)) + error("Could not read file - %s", name.c_str()); + + load(name, f); + + f.close(); +} + +void Cache::load(const Common::String &name, Common::SeekableReadStream &stream) { + // First check if the entry already exists + if (_resources.contains(name)) + return; + + int32 signature = stream.readUint32BE(); + stream.seek(0); + + // Allocate a new cache entry + _resources[name] = CacheEntry(); + CacheEntry &cacheEntry = _resources[name]; + + // Check whether the file is compressed + if (signature == MKTAG('L', 'Z', 'V', 26)) { + // It's compressed, so decompress the file and store it's data in the cache entry + Common::SeekableReadStream *decompressed = _vm->_res->decompressLZ(stream); + cacheEntry.resize(decompressed->size()); + decompressed->read(&cacheEntry[0], decompressed->size()); + + delete decompressed; + } else { + // It's not, so read the raw data of the file into the cache entry + cacheEntry.resize(stream.size()); + stream.read(&cacheEntry[0], stream.size()); + } +} + +Common::SeekableReadStream *Cache::get(const Common::String &filename) const { + // Return a memory stream that encapsulates the data + const CacheEntry &cacheEntry = _resources[filename]; + return new Common::MemoryReadStream(&cacheEntry[0], cacheEntry.size()); +} + +/*----------------------------------------------------------------*/ + +Resources::Resources(SherlockEngine *vm) : _vm(vm), _cache(vm) { + _resourceIndex = -1; + + if (_vm->_interactiveFl) { + addToCache("vgs.lib"); + addToCache("talk.lib"); + addToCache("sequence.txt"); + addToCache("journal.txt"); + addToCache("portrait.lib"); + } +} + +void Resources::addToCache(const Common::String &filename) { + _cache.load(filename); + + // Check to see if the file is a library + Common::SeekableReadStream *stream = load(filename); + uint32 header = stream->readUint32BE(); + if (header == MKTAG('L', 'I', 'B', 26)) + loadLibraryIndex(filename, stream); + + delete stream; +} + +void Resources::addToCache(const Common::String &filename, const Common::String &libFilename) { + // Get the resource + Common::SeekableReadStream *stream = load(filename, libFilename); + + _cache.load(filename, *stream); + + delete stream; +} + +void Resources::addToCache(const Common::String &filename, Common::SeekableReadStream &stream) { + _cache.load(filename, stream); +} + +Common::SeekableReadStream *Resources::load(const Common::String &filename) { + // First check if the file is directly in the cache + if (_cache.isCached(filename)) + return _cache.get(filename); + + // Secondly, iterate through any loaded library file looking for a resource + // that has the same name + for (LibraryIndexes::iterator i = _indexes.begin(); i != _indexes.end(); ++i) { + if (i->_value.contains(filename)) { + // Get a stream reference to the given library file + Common::SeekableReadStream *stream = load(i->_key); + LibraryEntry &entry = i->_value[filename]; + _resourceIndex = entry._index; + + stream->seek(entry._offset); + Common::SeekableReadStream *resStream = stream->readStream(entry._size); + decompressIfNecessary(resStream); + + delete stream; + return resStream; + } + } + + // At this point, fall back on a physical file with the given name + Common::File f; + if (!f.open(filename)) + error("Could not load file - %s", filename.c_str()); + + Common::SeekableReadStream *stream = f.readStream(f.size()); + f.close(); + decompressIfNecessary(stream); + + return stream; +} + +void Resources::decompressIfNecessary(Common::SeekableReadStream *&stream) { + bool isCompressed = stream->readUint32BE() == MKTAG('L', 'Z', 'V', 26); + stream->seek(-4, SEEK_CUR); + + if (isCompressed) { + Common::SeekableReadStream *newStream = decompressLZ(*stream); + delete stream; + stream = newStream; + } +} + +Common::SeekableReadStream *Resources::load(const Common::String &filename, const Common::String &libraryFile) { + // Open up the library for access + Common::SeekableReadStream *libStream = load(libraryFile); + + // Check if the library has already had it's index read, and if not, load it + if (!_indexes.contains(libraryFile)) + loadLibraryIndex(libraryFile, libStream); + + // Extract the data for the specified resource and return it + LibraryEntry &entry = _indexes[libraryFile][filename]; + libStream->seek(entry._offset); + Common::SeekableReadStream *stream = libStream->readStream(entry._size); + decompressIfNecessary(stream); + + delete libStream; + return stream; +} + +bool Resources::exists(const Common::String &filename) const { + Common::File f; + return f.exists(filename) || _cache.isCached(filename); +} + +void Resources::loadLibraryIndex(const Common::String &libFilename, + Common::SeekableReadStream *stream) { + uint32 offset, nextOffset; + + // Create an index entry + _indexes[libFilename] = LibraryIndex(); + LibraryIndex &index = _indexes[libFilename]; + + // Read in the number of resources + stream->seek(4); + int count = stream->readUint16LE(); + + // Loop through reading in the entries + for (int idx = 0; idx < count; ++idx) { + // Read the name of the resource + char resName[13]; + stream->read(resName, 13); + resName[12] = '\0'; + + // Read the offset + offset = stream->readUint32LE(); + + if (idx == (count - 1)) { + nextOffset = stream->size(); + } else { + // Read the size by jumping forward to read the next entry's offset + stream->seek(13, SEEK_CUR); + nextOffset = stream->readUint32LE(); + stream->seek(-17, SEEK_CUR); + } + + // Add the entry to the index + index[resName] = LibraryEntry(idx, offset, nextOffset - offset); + } +} + +int Resources::resourceIndex() const { + return _resourceIndex; +} + +Common::SeekableReadStream *Resources::decompressLZ(Common::SeekableReadStream &source) { + if (_vm->getGameID() == GType_SerratedScalpel) { + uint32 id = source.readUint32BE(); + assert(id == MKTAG('L', 'Z', 'V', 0x1A)); + } + + uint32 size = source.readUint32LE(); + return decompressLZ(source, size); +} + +Common::SeekableReadStream *Resources::decompressLZ(Common::SeekableReadStream &source, uint32 outSize) { + byte lzWindow[4096]; + uint16 lzWindowPos; + uint16 cmd; + + byte *outBuffer = (byte *)malloc(outSize); + byte *outBufferEnd = outBuffer + outSize; + Common::MemoryReadStream *outS = new Common::MemoryReadStream(outBuffer, outSize, DisposeAfterUse::YES); + + memset(lzWindow, 0xFF, 0xFEE); + lzWindowPos = 0xFEE; + cmd = 0; + + do { + cmd >>= 1; + if (!(cmd & 0x100)) + cmd = source.readByte() | 0xFF00; + + if (cmd & 1) { + byte literal = source.readByte(); + *outBuffer++ = literal; + lzWindow[lzWindowPos] = literal; + lzWindowPos = (lzWindowPos + 1) & 0x0FFF; + } + else { + int copyPos, copyLen; + copyPos = source.readByte(); + copyLen = source.readByte(); + copyPos = copyPos | ((copyLen & 0xF0) << 4); + copyLen = (copyLen & 0x0F) + 3; + while (copyLen--) { + byte literal = lzWindow[copyPos]; + copyPos = (copyPos + 1) & 0x0FFF; + *outBuffer++ = literal; + lzWindow[lzWindowPos] = literal; + lzWindowPos = (lzWindowPos + 1) & 0x0FFF; + } + } + } while (outBuffer < outBufferEnd); + + return outS; +} + +/*----------------------------------------------------------------*/ + +SherlockEngine *ImageFile::_vm; + +void ImageFile::setVm(SherlockEngine *vm) { + _vm = vm; +} + +ImageFile::ImageFile(const Common::String &name, bool skipPal, bool animImages) { + Common::SeekableReadStream *stream = _vm->_res->load(name); + + Common::fill(&_palette[0], &_palette[PALETTE_SIZE], 0); + load(*stream, skipPal, animImages); + + delete stream; +} + +ImageFile::ImageFile(Common::SeekableReadStream &stream, bool skipPal) { + Common::fill(&_palette[0], &_palette[PALETTE_SIZE], 0); + load(stream, skipPal, false); +} + +ImageFile::~ImageFile() { + for (uint idx = 0; idx < size(); ++idx) + (*this)[idx]._frame.free(); +} + +void ImageFile::load(Common::SeekableReadStream &stream, bool skipPalette, bool animImages) { + loadPalette(stream); + + int streamSize = stream.size(); + while (stream.pos() < streamSize) { + ImageFrame frame; + frame._width = stream.readUint16LE() + 1; + frame._height = stream.readUint16LE() + 1; + frame._paletteBase = stream.readByte(); + + if (animImages) { + // Animation cutscene image files use a 16-bit x offset + frame._offset.x = stream.readUint16LE(); + frame._rleEncoded = (frame._offset.x & 0xff) == 1; + } else { + // Standard image files have a separate byte for the RLE flag, and an 8-bit X offset + frame._rleEncoded = stream.readByte() == 1; + frame._offset.x = stream.readByte(); + } + + frame._offset.y = stream.readByte(); + frame._rleEncoded = !skipPalette && frame._rleEncoded; + + if (frame._paletteBase) { + // Nibble packed frame data + frame._size = (frame._width * frame._height) / 2; + } else if (frame._rleEncoded) { + // This size includes the header size, which we subtract + frame._size = stream.readUint16LE() - 11; + frame._rleMarker = stream.readByte(); + } else { + // Uncompressed data + frame._size = frame._width * frame._height; + } + + // Load data for frame and decompress it + byte *data = new byte[frame._size]; + stream.read(data, frame._size); + decompressFrame(frame, data); + delete[] data; + + push_back(frame); + } +} + +void ImageFile::loadPalette(Common::SeekableReadStream &stream) { + // Check for palette + int v1 = stream.readUint16LE() + 1; + int v2 = stream.readUint16LE() + 1; + stream.skip(1); // Skip paletteBase byte + bool rleEncoded = stream.readByte() == 1; + int palSize = v1 * v2; + + if ((palSize - 12) == PALETTE_SIZE && !rleEncoded) { + // Found palette, so read it in + stream.seek(2 + 12, SEEK_CUR); + for (int idx = 0; idx < PALETTE_SIZE; ++idx) + _palette[idx] = VGA_COLOR_TRANS(stream.readByte()); + } else { + // Not a palette, so rewind to start of frame data for normal frame processing + stream.seek(-6, SEEK_CUR); + } +} + +void ImageFile::decompressFrame(ImageFrame &frame, const byte *src) { + frame._frame.create(frame._width, frame._height, Graphics::PixelFormat::createFormatCLUT8()); + + if (frame._paletteBase) { + // Nibble-packed + byte *pDest = (byte *)frame._frame.getPixels(); + for (uint idx = 0; idx < frame._size; ++idx, ++src) { + *pDest++ = *src & 0xF; + *pDest++ = (*src >> 4); + } + } else if (frame._rleEncoded) { + // RLE encoded + byte *dst = (byte *)frame._frame.getPixels(); + + int frameSize = frame._width * frame._height; + while (frameSize > 0) { + if (*src == frame._rleMarker) { + byte rleColor = src[1]; + byte rleCount = src[2]; + src += 3; + frameSize -= rleCount; + while (rleCount--) + *dst++ = rleColor; + } else { + *dst++ = *src++; + --frameSize; + } + } + assert(frameSize == 0); + } else { + // Uncompressed frame + Common::copy(src, src + frame._width * frame._height, + (byte *)frame._frame.getPixels()); + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/resources.h b/engines/sherlock/resources.h new file mode 100644 index 0000000000..fb91b30f94 --- /dev/null +++ b/engines/sherlock/resources.h @@ -0,0 +1,192 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_RESOURCES_H +#define SHERLOCK_RESOURCES_H + +#include "common/array.h" +#include "common/file.h" +#include "common/hashmap.h" +#include "common/hash-str.h" +#include "common/rect.h" +#include "common/str.h" +#include "common/stream.h" +#include "graphics/surface.h" + +namespace Sherlock { + +typedef Common::Array<byte> CacheEntry; +typedef Common::HashMap<Common::String, CacheEntry, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> CacheHash; + +struct LibraryEntry { + uint32 _offset, _size; + int _index; + + LibraryEntry() : _index(0), _offset(0), _size(0) {} + LibraryEntry(int index, uint32 offset, uint32 size) : + _index(index), _offset(offset), _size(size) {} +}; +typedef Common::HashMap<Common::String, LibraryEntry, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> LibraryIndex; +typedef Common::HashMap<Common::String, LibraryIndex, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> LibraryIndexes; + +class SherlockEngine; + +class Cache { +private: + SherlockEngine *_vm; + CacheHash _resources; +public: + Cache(SherlockEngine *_vm); + + /** + * Returns true if a given file is currently being cached + */ + bool isCached(const Common::String &filename) const; + + /** + * Loads a file into the cache if it's not already present, and returns it. + * If the file is LZW compressed, automatically decompresses it and loads + * the uncompressed version into memory + */ + void load(const Common::String &name); + + /** + * Load a cache entry based on a passed stream + */ + void load(const Common::String &name, Common::SeekableReadStream &stream); + + /** + * Get a file from the cache + */ + Common::SeekableReadStream *get(const Common::String &filename) const; +}; + +class Resources { +private: + SherlockEngine *_vm; + Cache _cache; + LibraryIndexes _indexes; + int _resourceIndex; + + /** + * Reads in the index from a library file, and caches it's index for later use + */ + void loadLibraryIndex(const Common::String &libFilename, Common::SeekableReadStream *stream); +public: + Resources(SherlockEngine *vm); + + /** + * Adds the specified file to the cache. If it's a library file, takes care of + * loading it's index for future use + */ + void addToCache(const Common::String &filename); + + /** + * Adds a resource from a library file to the cache + */ + void addToCache(const Common::String &filename, const Common::String &libFilename); + + /** + * Adds a given stream to the cache under the given name + */ + void addToCache(const Common::String &filename, Common::SeekableReadStream &stream); + + bool isInCache(const Common::String &filename) const { return _cache.isCached(filename); } + + /** + * Checks the passed stream, and if is compressed, deletes it and replaces it with it's uncompressed data + */ + void decompressIfNecessary(Common::SeekableReadStream *&stream); + + /** + * Returns a stream for a given file + */ + Common::SeekableReadStream *load(const Common::String &filename); + + /** + * Loads a specific resource from a given library file + */ + Common::SeekableReadStream *load(const Common::String &filename, const Common::String &libraryFile); + + /** + * Returns true if the given file exists on disk or in the cache + */ + bool exists(const Common::String &filename) const; + + /** + * Returns the index of the last loaded resource in it's given library file. + * This will be used primarily when loading talk files, so the engine can + * update the given conversation number in the journal + */ + int resourceIndex() const; + + /** + * Decompresses an LZW block of data with a specified output size + */ + static Common::SeekableReadStream *decompressLZ(Common::SeekableReadStream &source, uint32 outSize); + + /** + * Decompress an LZW compressed resource + */ + Common::SeekableReadStream *decompressLZ(Common::SeekableReadStream &source); +}; + +struct ImageFrame { + uint32 _size; + uint16 _width, _height; + int _paletteBase; + bool _rleEncoded; + Common::Point _offset; + byte _rleMarker; + Graphics::Surface _frame; +}; + +class ImageFile : public Common::Array<ImageFrame> { +private: + static SherlockEngine *_vm; + + /** + * Load the data of the sprite + */ + void load(Common::SeekableReadStream &stream, bool skipPalette, bool animImages); + + /** + * Gets the palette at the start of the sprite file + */ + void loadPalette(Common::SeekableReadStream &stream); + + /** + * Decompress a single frame for the sprite + */ + void decompressFrame(ImageFrame &frame, const byte *src); +public: + byte _palette[256 * 3]; +public: + ImageFile(const Common::String &name, bool skipPal = false, bool animImages = false); + ImageFile(Common::SeekableReadStream &stream, bool skipPal = false); + ~ImageFile(); + static void setVm(SherlockEngine *vm); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/saveload.cpp b/engines/sherlock/saveload.cpp new file mode 100644 index 0000000000..ddf4917a34 --- /dev/null +++ b/engines/sherlock/saveload.cpp @@ -0,0 +1,484 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/saveload.h" +#include "sherlock/surface.h" +#include "sherlock/sherlock.h" +#include "common/system.h" +#include "graphics/scaler.h" +#include "graphics/thumbnail.h" + +namespace Sherlock { + +const int ENV_POINTS[6][3] = { + { 41, 80, 61 }, // Exit + { 81, 120, 101 }, // Load + { 121, 160, 141 }, // Save + { 161, 200, 181 }, // Up + { 201, 240, 221 }, // Down + { 241, 280, 261 } // Quit +}; + +static const char *const EMPTY_SAVEGAME_SLOT = "-EMPTY-"; +static const char *const SAVEGAME_STR = "SHLK"; +#define SAVEGAME_STR_SIZE 4 + +/*----------------------------------------------------------------*/ + +SaveManager::SaveManager(SherlockEngine *vm, const Common::String &target) : + _vm(vm), _target(target) { + _saveThumb = nullptr; + _envMode = SAVEMODE_NONE; + _justLoaded = false; + _savegameIndex = 0; +} + +SaveManager::~SaveManager() { + if (_saveThumb) { + _saveThumb->free(); + delete _saveThumb; + } +} + +void SaveManager::drawInterface() { + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + + // Create a list of savegame slots + createSavegameList(); + + screen._backBuffer1.fillRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y + 10), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(0, CONTROLS_Y + 10, 2, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(318, CONTROLS_Y + 10, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(0, 199, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(2, CONTROLS_Y + 10, SHERLOCK_SCREEN_WIDTH - 2, SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + + screen.makeButton(Common::Rect(ENV_POINTS[0][0], CONTROLS_Y, ENV_POINTS[0][1], CONTROLS_Y + 10), + ENV_POINTS[0][2] - screen.stringWidth("Exit") / 2, "Exit"); + screen.makeButton(Common::Rect(ENV_POINTS[1][0], CONTROLS_Y, ENV_POINTS[1][1], CONTROLS_Y + 10), + ENV_POINTS[1][2] - screen.stringWidth("Load") / 2, "Load"); + screen.makeButton(Common::Rect(ENV_POINTS[2][0], CONTROLS_Y, ENV_POINTS[2][1], CONTROLS_Y + 10), + ENV_POINTS[2][2] - screen.stringWidth("Save") / 2, "Save"); + screen.makeButton(Common::Rect(ENV_POINTS[3][0], CONTROLS_Y, ENV_POINTS[3][1], CONTROLS_Y + 10), + ENV_POINTS[3][2] - screen.stringWidth("Up") / 2, "Up"); + screen.makeButton(Common::Rect(ENV_POINTS[4][0], CONTROLS_Y, ENV_POINTS[4][1], CONTROLS_Y + 10), + ENV_POINTS[4][2] - screen.stringWidth("Down") / 2, "Down"); + screen.makeButton(Common::Rect(ENV_POINTS[5][0], CONTROLS_Y, ENV_POINTS[5][1], CONTROLS_Y + 10), + ENV_POINTS[5][2] - screen.stringWidth("Quit") / 2, "Quit"); + + if (!_savegameIndex) + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), COMMAND_NULL, 0, "Up"); + + if (_savegameIndex == MAX_SAVEGAME_SLOTS - ONSCREEN_FILES_COUNT) + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), COMMAND_NULL, 0, "Down"); + + for (int idx = _savegameIndex; idx < _savegameIndex + ONSCREEN_FILES_COUNT; ++idx) { + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (idx - _savegameIndex) * 10), + INV_FOREGROUND, "%d.", idx + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (idx - _savegameIndex) * 10), + INV_FOREGROUND, "%s", _savegames[idx].c_str()); + } + + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(); + } + + _envMode = SAVEMODE_NONE; +} + +void SaveManager::createSavegameList() { + Screen &screen = *_vm->_screen; + + _savegames.clear(); + for (int idx = 0; idx < MAX_SAVEGAME_SLOTS; ++idx) + _savegames.push_back(EMPTY_SAVEGAME_SLOT); + + SaveStateList saveList = getSavegameList(_target); + for (uint idx = 0; idx < saveList.size(); ++idx) { + int slot = saveList[idx].getSaveSlot() - 1; + if (slot >= 0 && slot < MAX_SAVEGAME_SLOTS) + _savegames[slot] = saveList[idx].getDescription(); + } + + // Ensure the names will fit on the screen + for (uint idx = 0; idx < _savegames.size(); ++idx) { + int width = screen.stringWidth(_savegames[idx]) + 24; + if (width > 308) { + // It won't fit in, so remove characters until it does + do { + width -= screen.charWidth(_savegames[idx].lastChar()); + _savegames[idx].deleteLastChar(); + } while (width > 300); + } + } +} + +SaveStateList SaveManager::getSavegameList(const Common::String &target) { + Common::SaveFileManager *saveFileMan = g_system->getSavefileManager(); + Common::StringArray filenames; + Common::String saveDesc; + Common::String pattern = Common::String::format("%s.0??", target.c_str()); + SherlockSavegameHeader header; + + filenames = saveFileMan->listSavefiles(pattern); + sort(filenames.begin(), filenames.end()); // Sort to get the files in numerical order + + SaveStateList saveList; + for (Common::StringArray::const_iterator file = filenames.begin(); file != filenames.end(); ++file) { + const char *ext = strrchr(file->c_str(), '.'); + int slot = ext ? atoi(ext + 1) : -1; + + if (slot >= 0 && slot < MAX_SAVEGAME_SLOTS) { + Common::InSaveFile *in = g_system->getSavefileManager()->openForLoading(*file); + + if (in) { + readSavegameHeader(in, header); + saveList.push_back(SaveStateDescriptor(slot, header._saveName)); + + header._thumbnail->free(); + delete header._thumbnail; + delete in; + } + } + } + + return saveList; +} + +bool SaveManager::readSavegameHeader(Common::InSaveFile *in, SherlockSavegameHeader &header) { + char saveIdentBuffer[SAVEGAME_STR_SIZE + 1]; + header._thumbnail = nullptr; + + // Validate the header Id + in->read(saveIdentBuffer, SAVEGAME_STR_SIZE + 1); + if (strncmp(saveIdentBuffer, SAVEGAME_STR, SAVEGAME_STR_SIZE)) + return false; + + header._version = in->readByte(); + if (header._version > SHERLOCK_SAVEGAME_VERSION) + return false; + + // Read in the string + header._saveName.clear(); + char ch; + while ((ch = (char)in->readByte()) != '\0') header._saveName += ch; + + // Get the thumbnail + header._thumbnail = Graphics::loadThumbnail(*in); + if (!header._thumbnail) + return false; + + // Read in save date/time + header._year = in->readSint16LE(); + header._month = in->readSint16LE(); + header._day = in->readSint16LE(); + header._hour = in->readSint16LE(); + header._minute = in->readSint16LE(); + header._totalFrames = in->readUint32LE(); + + return true; +} + +void SaveManager::writeSavegameHeader(Common::OutSaveFile *out, SherlockSavegameHeader &header) { + // Write out a savegame header + out->write(SAVEGAME_STR, SAVEGAME_STR_SIZE + 1); + + out->writeByte(SHERLOCK_SAVEGAME_VERSION); + + // Write savegame name + out->write(header._saveName.c_str(), header._saveName.size()); + out->writeByte('\0'); + + // Handle the thumbnail. If there's already one set by the game, create one + if (!_saveThumb) + createThumbnail(); + Graphics::saveThumbnail(*out, *_saveThumb); + + _saveThumb->free(); + delete _saveThumb; + _saveThumb = nullptr; + + // Write out the save date/time + TimeDate td; + g_system->getTimeAndDate(td); + out->writeSint16LE(td.tm_year + 1900); + out->writeSint16LE(td.tm_mon + 1); + out->writeSint16LE(td.tm_mday); + out->writeSint16LE(td.tm_hour); + out->writeSint16LE(td.tm_min); + out->writeUint32LE(_vm->_events->getFrameCounter()); +} + +void SaveManager::createThumbnail() { + if (_saveThumb) { + _saveThumb->free(); + delete _saveThumb; + } + + uint8 thumbPalette[PALETTE_SIZE]; + _vm->_screen->getPalette(thumbPalette); + _saveThumb = new Graphics::Surface(); + ::createThumbnail(_saveThumb, (const byte *)_vm->_screen->getPixels(), SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT, thumbPalette); +} + +int SaveManager::getHighlightedButton() const { + Common::Point pt = _vm->_events->mousePos(); + + for (int idx = 0; idx < 6; ++idx) { + if (pt.x > ENV_POINTS[idx][0] && pt.x < ENV_POINTS[idx][1] && pt.y > CONTROLS_Y + && pt.y < (CONTROLS_Y + 10)) + return idx; + } + + return -1; +} + +void SaveManager::highlightButtons(int btnIndex) { + Screen &screen = *_vm->_screen; + byte color = (btnIndex == 0) ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND; + + screen.buttonPrint(Common::Point(ENV_POINTS[0][2], CONTROLS_Y), color, 1, "Exit"); + + if ((btnIndex == 1) || ((_envMode == SAVEMODE_LOAD) && (btnIndex != 2))) + screen.buttonPrint(Common::Point(ENV_POINTS[1][2], CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Load"); + else + screen.buttonPrint(Common::Point(ENV_POINTS[1][2], CONTROLS_Y), COMMAND_FOREGROUND, true, "Load"); + + if ((btnIndex == 2) || ((_envMode == SAVEMODE_SAVE) && (btnIndex != 1))) + screen.buttonPrint(Common::Point(ENV_POINTS[2][2], CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Save"); + else + screen.buttonPrint(Common::Point(ENV_POINTS[2][2], CONTROLS_Y), COMMAND_FOREGROUND, true, "Save"); + + if (btnIndex == 3 && _savegameIndex) + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Up"); + else + if (_savegameIndex) + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), COMMAND_FOREGROUND, true, "Up"); + + if ((btnIndex == 4) && (_savegameIndex < MAX_SAVEGAME_SLOTS - 5)) + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Down"); + else if (_savegameIndex < (MAX_SAVEGAME_SLOTS - 5)) + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), COMMAND_FOREGROUND, true, "Down"); + + color = (btnIndex == 5) ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[5][2], CONTROLS_Y), color, 1, "Quit"); +} + +void SaveManager::loadGame(int slot) { + Common::InSaveFile *saveFile = g_system->getSavefileManager()->openForLoading( + generateSaveName(slot)); + if (!saveFile) + return; + + // Load the savaegame header + SherlockSavegameHeader header; + if (!readSavegameHeader(saveFile, header)) + error("Invalid savegame"); + + if (header._thumbnail) { + header._thumbnail->free(); + delete header._thumbnail; + } + + // Synchronize the savegame data + Common::Serializer s(saveFile, nullptr); + synchronize(s); + + delete saveFile; +} + +void SaveManager::saveGame(int slot, const Common::String &name) { + Common::OutSaveFile *out = g_system->getSavefileManager()->openForSaving( + generateSaveName(slot)); + + SherlockSavegameHeader header; + header._saveName = name; + writeSavegameHeader(out, header); + + // Synchronize the savegame data + Common::Serializer s(nullptr, out); + synchronize(s); + + out->finalize(); + delete out; +} + +Common::String SaveManager::generateSaveName(int slot) { + return Common::String::format("%s.%03d", _target.c_str(), slot); +} + +void SaveManager::synchronize(Common::Serializer &s) { + Inventory &inv = *_vm->_inventory; + Journal &journal = *_vm->_journal; + Map &map = *_vm->_map; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + + int oldFont = screen.fontNumber(); + + inv.synchronize(s); + journal.synchronize(s); + people.synchronize(s); + map.synchronize(s); + scene.synchronize(s); + screen.synchronize(s); + talk.synchronize(s); + _vm->synchronize(s); + + if (screen.fontNumber() != oldFont) + journal.resetPosition(); + + _justLoaded = true; +} + +bool SaveManager::checkGameOnScreen(int slot) { + Screen &screen = *_vm->_screen; + + // Check if it's already on-screen + if (slot != -1 && (slot < _savegameIndex || slot >= (_savegameIndex + ONSCREEN_FILES_COUNT))) { + _savegameIndex = slot; + + screen._backBuffer1.fillRect(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 1), INV_BACKGROUND); + + for (int idx = _savegameIndex; idx < (_savegameIndex + 5); ++idx) { + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (idx - _savegameIndex) * 10), + INV_FOREGROUND, "%d.", idx + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (idx - _savegameIndex) * 10), + INV_FOREGROUND, "%s", _savegames[idx].c_str()); + } + + screen.slamRect(Common::Rect(3, CONTROLS_Y + 11, 318, SHERLOCK_SCREEN_HEIGHT)); + + byte color = !_savegameIndex ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), color, 1, "Up"); + + color = (_savegameIndex == (MAX_SAVEGAME_SLOTS - 5)) ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), color, 1, "Down"); + + return true; + } + + return false; +} + +bool SaveManager::promptForDescription(int slot) { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + int xp, yp; + bool flag = false; + + screen.buttonPrint(Common::Point(ENV_POINTS[0][2], CONTROLS_Y), COMMAND_NULL, true, "Exit"); + screen.buttonPrint(Common::Point(ENV_POINTS[1][2], CONTROLS_Y), COMMAND_NULL, true, "Load"); + screen.buttonPrint(Common::Point(ENV_POINTS[2][2], CONTROLS_Y), COMMAND_NULL, true, "Save"); + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), COMMAND_NULL, true, "Up"); + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), COMMAND_NULL, true, "Down"); + screen.buttonPrint(Common::Point(ENV_POINTS[5][2], CONTROLS_Y), COMMAND_NULL, true, "Quit"); + + Common::String saveName = _savegames[slot]; + if (isSlotEmpty(slot)) { + // It's an empty slot, so start off with an empty save name + saveName = ""; + + yp = CONTROLS_Y + 12 + (slot - _savegameIndex) * 10; + screen.vgaBar(Common::Rect(24, yp, 85, yp + 9), INV_BACKGROUND); + } + + screen.print(Common::Point(6, CONTROLS_Y + 12 + (slot - _savegameIndex) * 10), TALK_FOREGROUND, "%d.", slot + 1); + screen.print(Common::Point(24, CONTROLS_Y + 12 + (slot - _savegameIndex) * 10), TALK_FOREGROUND, "%s", saveName.c_str()); + xp = 24 + screen.stringWidth(saveName); + yp = CONTROLS_Y + 12 + (slot - _savegameIndex) * 10; + + int done = 0; + do { + while (!_vm->shouldQuit() && !events.kbHit()) { + scene.doBgAnim(); + + if (talk._talkToAbort) + return false; + + // Allow event processing + events.pollEventsAndWait(); + events.setButtonState(); + + flag = !flag; + if (flag) + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_FOREGROUND); + else + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_BACKGROUND); + } + if (_vm->shouldQuit()) + return false; + + // Get the next keypress + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_BACKSPACE && saveName.size() > 0) { + // Delete character of save name + screen.vgaBar(Common::Rect(xp - screen.charWidth(saveName.lastChar()), yp - 1, + xp + 8, yp + 9), INV_BACKGROUND); + xp -= screen.charWidth(saveName.lastChar()); + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_FOREGROUND); + saveName.deleteLastChar(); + + } else if (keyState.keycode == Common::KEYCODE_RETURN && saveName.compareToIgnoreCase(EMPTY_SAVEGAME_SLOT)) { + done = 1; + + } else if (keyState.keycode == Common::KEYCODE_ESCAPE) { + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_BACKGROUND); + done = -1; + + } else if (keyState.ascii >= ' ' && keyState.ascii <= 'z' && saveName.size() < 50 + && (xp + screen.charWidth(keyState.ascii)) < 308) { + char c = (char)keyState.ascii; + + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_BACKGROUND); + screen.print(Common::Point(xp, yp), TALK_FOREGROUND, "%c", c); + xp += screen.charWidth(c); + screen.vgaBar(Common::Rect(xp, yp - 1, xp + 8, yp + 9), INV_FOREGROUND); + saveName += c; + } + } while (!done); + + if (done == 1) { + // Enter key perssed + _savegames[slot] = saveName; + } else { + done = 0; + _envMode = SAVEMODE_NONE; + highlightButtons(-1); + } + + return done == 1; +} + +bool SaveManager::isSlotEmpty(int slot) const { + return _savegames[slot].equalsIgnoreCase(EMPTY_SAVEGAME_SLOT); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/saveload.h b/engines/sherlock/saveload.h new file mode 100644 index 0000000000..a7ed852a5f --- /dev/null +++ b/engines/sherlock/saveload.h @@ -0,0 +1,147 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SAVELOAD_H +#define SHERLOCK_SAVELOAD_H + +#include "common/scummsys.h" +#include "common/savefile.h" +#include "common/serializer.h" +#include "common/str-array.h" +#include "engines/savestate.h" +#include "graphics/surface.h" + +namespace Sherlock { + +#define MAX_SAVEGAME_SLOTS 99 +#define ONSCREEN_FILES_COUNT 5 +#define SHERLOCK_SAVEGAME_VERSION 1 + +enum SaveMode { SAVEMODE_NONE = 0, SAVEMODE_LOAD = 1, SAVEMODE_SAVE = 2 }; + +extern const int ENV_POINTS[6][3]; + +struct SherlockSavegameHeader { + uint8 _version; + Common::String _saveName; + Graphics::Surface *_thumbnail; + int _year, _month, _day; + int _hour, _minute; + int _totalFrames; +}; + +class SherlockEngine; + +class SaveManager { +private: + SherlockEngine *_vm; + Common::String _target; + Graphics::Surface *_saveThumb; + + /** + * Build up a savegame list, with empty slots given an explicit Empty message + */ + void createSavegameList(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +public: + Common::StringArray _savegames; + int _savegameIndex; + SaveMode _envMode; + bool _justLoaded; +public: + SaveManager(SherlockEngine *vm, const Common::String &target); + ~SaveManager(); + + /** + * Shows the in-game dialog interface for loading and saving games + */ + void drawInterface(); + + /** + * Creates a thumbnail for the current on-screen contents + */ + void createThumbnail(); + + /** + * Load a list of savegames + */ + static SaveStateList getSavegameList(const Common::String &target); + + /** + * Support method that generates a savegame name + * @param slot Slot number + */ + Common::String generateSaveName(int slot); + + /** + * Write out the header information for a savegame + */ + void writeSavegameHeader(Common::OutSaveFile *out, SherlockSavegameHeader &header); + + /** + * Read in the header information for a savegame + */ + static bool readSavegameHeader(Common::InSaveFile *in, SherlockSavegameHeader &header); + + /** + * Return the index of the button the mouse is over, if any + */ + int getHighlightedButton() const; + + /** + * Handle highlighting buttons + */ + void highlightButtons(int btnIndex); + + /** + * Load the game in the specified slot + */ + void loadGame(int slot); + + /** + * Save the game in the specified slot with the given name + */ + void saveGame(int slot, const Common::String &name); + + /** + * Make sure that the selected savegame is on-screen + */ + bool checkGameOnScreen(int slot); + + /** + * Prompts the user to enter a description in a given slot + */ + bool promptForDescription(int slot); + + /** + * Returns true if the given save slot is empty + */ + bool isSlotEmpty(int slot) const; +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/darts.cpp b/engines/sherlock/scalpel/darts.cpp new file mode 100644 index 0000000000..b567d58ab4 --- /dev/null +++ b/engines/sherlock/scalpel/darts.cpp @@ -0,0 +1,557 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/scalpel/darts.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +namespace Scalpel { + +enum { + STATUS_INFO_X = 218, + STATUS_INFO_Y = 53, + DART_INFO_X = 218, + DART_INFO_Y = 103, + DARTBARHX = 35, + DARTHORIZY = 190, + DARTBARVX = 1, + DARTHEIGHTY = 25, + DARTBARSIZE = 150, + DART_BAR_FORE = 8 +}; + +enum { + DART_COL_FORE = 5, + PLAYER_COLOR = 11 +}; +#define OPPONENTS_COUNT 4 + +const char *const OPPONENT_NAMES[OPPONENTS_COUNT] = { + "Skipper", "Willy", "Micky", "Tom" +}; + +/*----------------------------------------------------------------*/ + +Darts::Darts(ScalpelEngine *vm) : _vm(vm) { + _dartImages = nullptr; + _level = 0; + _computerPlayer = 1; + _playerDartMode = false; + _dartScore1 = _dartScore2 = 0; + _roundNumber = 0; + _playerDartMode = false; + _roundScore = 0; + _oldDartButtons = false; +} + +void Darts::playDarts() { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + int playerNumber = 0; + int lastDart; + + // Change the font + int oldFont = screen.fontNumber(); + screen.setFont(2); + + loadDarts(); + initDarts(); + + bool done = false; + do { + int score, roundStartScore; + roundStartScore = score = playerNumber == 0 ? _dartScore1 : _dartScore2; + + // Show player details + showNames(playerNumber); + showStatus(playerNumber); + _roundScore = 0; + + if (_vm->shouldQuit()) + return; + + for (int idx = 0; idx < 3; ++idx) { + // Throw a single dart + if (_computerPlayer == 1) + lastDart = throwDart(idx + 1, playerNumber * 2); + else if (_computerPlayer == 2) + lastDart = throwDart(idx + 1, playerNumber + 1); + else + lastDart = throwDart(idx + 1, 0); + + score -= lastDart; + _roundScore += lastDart; + + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(DART_INFO_X, DART_INFO_Y - 1), + Common::Rect(DART_INFO_X, DART_INFO_Y - 1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y), DART_COL_FORE, "Dart # %d", idx + 1); + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 10), DART_COL_FORE, "Scored %d points", lastDart); + + if (score != 0 && playerNumber == 0) + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 30), DART_COL_FORE, "Press a key"); + + if (score == 0) { + // Some-one has won + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 20), PLAYER_COLOR, "GAME OVER!"); + + if (playerNumber == 0) { + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 30), PLAYER_COLOR, "Holmes Wins!"); + if (_level < OPPONENTS_COUNT) + setFlagsForDarts(318 + _level); + } else { + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 30), PLAYER_COLOR, "%s Wins!", _opponent.c_str()); + } + + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 4), DART_COL_FORE, "Press a key"); + + idx = 10; + done = true; + } else if (score < 0) { + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 20), PLAYER_COLOR, "BUSTED!"); + + idx = 10; + score = roundStartScore; + } + + if (playerNumber == 0) + _dartScore1 = score; + else + _dartScore2 = score; + + showStatus(playerNumber); + events.clearKeyboard(); + + if ((playerNumber == 0 && _computerPlayer == 1) || _computerPlayer == 0 || done) { + int dartKey; + while (!(dartKey = dartHit()) && !_vm->shouldQuit()) + events.delay(10); + + if (dartKey == Common::KEYCODE_ESCAPE) { + idx = 10; + done = true; + } + } else { + events.wait(20); + } + + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(DART_INFO_X, DART_INFO_Y - 1), + Common::Rect(DART_INFO_X, DART_INFO_Y - 1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + + playerNumber ^= 1; + if (!playerNumber) + ++_roundNumber; + + done |= _vm->shouldQuit(); + + if (!done) { + screen._backBuffer2.blitFrom((*_dartImages)[0], Common::Point(0, 0)); + screen._backBuffer1.blitFrom(screen._backBuffer2); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + } while (!done); + + closeDarts(); + screen.fadeToBlack(); + + // Restore font + screen.setFont(oldFont); +} + +void Darts::loadDarts() { + Screen &screen = *_vm->_screen; + + _dartImages = new ImageFile("darts.vgs"); + screen.setPalette(_dartImages->_palette); + + screen._backBuffer1.blitFrom((*_dartImages)[0], Common::Point(0, 0)); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); +} + +void Darts::initDarts() { + _dartScore1 = _dartScore2 = 301; + _roundNumber = 1; + + if (_level == 9) { + // No computer players + _computerPlayer = 0; + _level = 0; + } else if (_level == 8) { + _level = _vm->getRandomNumber(3); + _computerPlayer = 2; + } else { + // Check flags for opponents + for (int idx = 0; idx < OPPONENTS_COUNT; ++idx) { + if (_vm->readFlags(314 + idx)) + _level = idx; + } + } + + _opponent = OPPONENT_NAMES[_level]; +} + +void Darts::closeDarts() { + delete _dartImages; + _dartImages = nullptr; +} + +void Darts::showNames(int playerNum) { + Screen &screen = *_vm->_screen; + byte color = playerNum == 0 ? PLAYER_COLOR : DART_COL_FORE; + + // Print Holmes first + if (playerNum == 0) + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y), PLAYER_COLOR + 3, "Holmes"); + else + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y), color, "Holmes"); + + screen._backBuffer1.fillRect(Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + 10, + STATUS_INFO_X + 31, STATUS_INFO_Y + 12), color); + screen.slamArea(STATUS_INFO_X, STATUS_INFO_Y + 10, 31, 12); + + // Second player + color = playerNum == 1 ? PLAYER_COLOR : DART_COL_FORE; + + if (playerNum != 0) + screen.print(Common::Point(STATUS_INFO_X + 50, STATUS_INFO_Y), PLAYER_COLOR + 3, + "%s", _opponent.c_str()); + else + screen.print(Common::Point(STATUS_INFO_X + 50, STATUS_INFO_Y), color, + "%s", _opponent.c_str()); + + screen._backBuffer1.fillRect(Common::Rect(STATUS_INFO_X + 50, STATUS_INFO_Y + 10, + STATUS_INFO_X + 81, STATUS_INFO_Y + 12), color); + screen.slamArea(STATUS_INFO_X + 50, STATUS_INFO_Y + 10, 81, 12); + + // Make a copy of the back buffer to the secondary one + screen._backBuffer2.blitFrom(screen._backBuffer1); +} + +void Darts::showStatus(int playerNum) { + Screen &screen = *_vm->_screen; + byte color; + + // Copy scoring screen from secondary back buffer. This will erase any previously displayed status/score info + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 10), + Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + 10, SHERLOCK_SCREEN_WIDTH, STATUS_INFO_Y + 48)); + + color = (playerNum == 0) ? PLAYER_COLOR : DART_COL_FORE; + screen.print(Common::Point(STATUS_INFO_X + 6, STATUS_INFO_Y + 13), color, "%d", _dartScore1); + + color = (playerNum == 1) ? PLAYER_COLOR : DART_COL_FORE; + screen.print(Common::Point(STATUS_INFO_X + 56, STATUS_INFO_Y + 13), color, "%d", _dartScore2); + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 25), PLAYER_COLOR, "Round: %d", _roundNumber); + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 35), PLAYER_COLOR, "Turn Total: %d", _roundScore); + screen.slamRect(Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + 10, SHERLOCK_SCREEN_WIDTH, STATUS_INFO_Y + 48)); +} + +int Darts::throwDart(int dartNum, int computer) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point targetNum; + int width, height; + + events.clearKeyboard(); + + erasePowerBars(); + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y), DART_COL_FORE, "Dart # %d", dartNum); + + if (!computer) { + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 10), DART_COL_FORE, "Hit a key"); + screen.print(Common::Point(DART_INFO_X, DART_INFO_Y + 18), DART_COL_FORE, "to start"); + } + + if (!computer) { + while (!_vm->shouldQuit() && !dartHit()) + ; + } else { + events.delay(10); + } + + if (_vm->shouldQuit()) + return 0; + + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(DART_INFO_X, DART_INFO_Y - 1), + Common::Rect(DART_INFO_X, DART_INFO_Y - 1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.slamRect(Common::Rect(DART_INFO_X, DART_INFO_Y - 1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + // If it's a computer player, choose a dart destination + if (computer) + targetNum = getComputerDartDest(computer - 1); + + width = doPowerBar(Common::Point(DARTBARHX, DARTHORIZY), DART_BAR_FORE, targetNum.x, false); + height = 101 - doPowerBar(Common::Point(DARTBARVX, DARTHEIGHTY), DART_BAR_FORE, targetNum.y, true); + + // For human players, slight y adjustment + if (computer == 0) + height += 2; + + // Copy the bars to the secondary back buffer so that they remain fixed at their selected values + // whilst the dart is being animated at being thrown at the board + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(DARTBARHX - 1, DARTHORIZY - 1), + Common::Rect(DARTBARHX - 1, DARTHORIZY - 1, DARTBARHX + DARTBARSIZE + 3, DARTHORIZY + 10)); + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(DARTBARVX - 1, DARTHEIGHTY - 1), + Common::Rect(DARTBARVX - 1, DARTHEIGHTY - 1, DARTBARVX + 11, DARTHEIGHTY + DARTBARSIZE + 3)); + + // Convert height and width to relative range of -50 to 50, where 0,0 is the exact centre of the board + height -= 50; + width -= 50; + + Common::Point dartPos(111 + width * 2, 99 + height * 2); + drawDartThrow(dartPos); + + return dartScore(dartPos); +} + +void Darts::drawDartThrow(const Common::Point &pt) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point pos(pt.x, pt.y + 2); + Common::Rect oldDrawBounds; + int delta = 9; + + for (int idx = 4; idx < 23; ++idx) { + ImageFrame &frame = (*_dartImages)[idx]; + + // Adjust draw position for animating dart + if (idx < 13) + pos.y -= delta--; + else if (idx == 13) + delta = 1; + else + pos.y += delta++; + + // Draw the dart + Common::Point drawPos(pos.x - frame._width / 2, pos.y - frame._height); + screen._backBuffer1.transBlitFrom(frame, drawPos); + screen.slamArea(drawPos.x, drawPos.y, frame._width, frame._height); + + // Handle erasing old dart frame area + if (!oldDrawBounds.isEmpty()) + screen.slamRect(oldDrawBounds); + + oldDrawBounds = Common::Rect(drawPos.x, drawPos.y, drawPos.x + frame._width, drawPos.y + frame._height); + screen._backBuffer1.blitFrom(screen._backBuffer2, drawPos, oldDrawBounds); + + events.wait(2); + } + + // Draw dart in final "stuck to board" form + screen._backBuffer1.transBlitFrom((*_dartImages)[22], Common::Point(oldDrawBounds.left, oldDrawBounds.top)); + screen._backBuffer2.transBlitFrom((*_dartImages)[22], Common::Point(oldDrawBounds.left, oldDrawBounds.top)); + screen.slamRect(oldDrawBounds); +} + +void Darts::erasePowerBars() { + Screen &screen = *_vm->_screen; + + screen._backBuffer1.fillRect(Common::Rect(DARTBARHX, DARTHORIZY, DARTBARHX + DARTBARSIZE, DARTHORIZY + 10), 0); + screen._backBuffer1.fillRect(Common::Rect(DARTBARVX, DARTHEIGHTY, DARTBARVX + 10, DARTHEIGHTY + DARTBARSIZE), 0); + screen._backBuffer1.transBlitFrom((*_dartImages)[2], Common::Point(DARTBARHX - 1, DARTHORIZY - 1)); + screen._backBuffer1.transBlitFrom((*_dartImages)[3], Common::Point(DARTBARVX - 1, DARTHEIGHTY - 1)); + screen.slamArea(DARTBARHX - 1, DARTHORIZY - 1, DARTBARSIZE + 3, 11); + screen.slamArea(DARTBARVX - 1, DARTHEIGHTY - 1, 11, DARTBARSIZE + 3); +} + +int Darts::doPowerBar(const Common::Point &pt, byte color, int goToPower, bool isVertical) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + bool done; + int idx = 0; + + events.clearEvents(); + if (sound._musicOn) + sound.waitTimerRoland(10); + else + events.delay(100); + + // Display loop + do { + done = _vm->shouldQuit() || idx >= DARTBARSIZE; + + if (idx == (goToPower - 1)) + // Reached target power for a computer player + done = true; + else if (goToPower == 0) { + // Check for pres + if (dartHit()) + done = true; + } + + if (isVertical) { + screen._backBuffer1.hLine(pt.x, pt.y + DARTBARSIZE - 1 - idx, pt.x + 8, color); + screen._backBuffer1.transBlitFrom((*_dartImages)[3], Common::Point(pt.x - 1, pt.y - 1)); + screen.slamArea(pt.x, pt.y + DARTBARSIZE - 1 - idx, 8, 2); + } else { + screen._backBuffer1.vLine(pt.x + idx, pt.y, pt.y + 8, color); + screen._backBuffer1.transBlitFrom((*_dartImages)[2], Common::Point(pt.x - 1, pt.y - 1)); + screen.slamArea(pt.x + idx, pt.y, 1, 8); + } + + if (sound._musicOn) { + if (!(idx % 3)) + sound.waitTimerRoland(1); + } else if (!(idx % 8)) + events.wait(1); + + ++idx; + } while (!done); + + return MIN(idx * 100 / DARTBARSIZE, 100); +} + +bool Darts::dartHit() { + Events &events = *_vm->_events; + + // Process pending events + events.pollEventsAndWait(); + + if (events.kbHit()) { + // Key was pressed, so discard it and return true + events.clearKeyboard(); + return true; + } + + _oldDartButtons = events._pressed; + events.setButtonState(); + + // Only return true if the mouse button is newly pressed + return (events._pressed && !_oldDartButtons) ? 1 : 0; +} + +int Darts::dartScore(const Common::Point &pt) { + Common::Point pos(pt.x - 37, pt.y - 33); + Graphics::Surface &scoreImg = (*_dartImages)[1]._frame; + + if (pos.x < 0 || pos.y < 0 || pos.x >= scoreImg.w || pos.y >= scoreImg.h) + // Not on the board + return 0; + + // On board, so get the score from the pixel at that position + int score = *(const byte *)scoreImg.getBasePtr(pos.x, pos.y); + return score; +} + +Common::Point Darts::getComputerDartDest(int playerNum) { + Common::Point target; + int score = playerNum == 0 ? _dartScore1 : _dartScore2; + + if (score > 50) { + // Aim for the bullseye + target.x = target.y = 76; + + if (_level <= 1 && _vm->getRandomNumber(1) == 1) { + // Introduce margin of error + target.x += _vm->getRandomNumber(21) - 10; + target.y += _vm->getRandomNumber(21) - 10; + } + } else { + int aim = score; + + bool done; + Common::Point pt; + do { + done = findNumberOnBoard(aim, pt); + --aim; + } while (!done); + + target.x = 75 + ((target.x - 75) * 20 / 27); + target.y = 75 + ((target.y - 75) * 2 / 3); + } + + // Pick a level of accuracy. The higher the level, the more accurate their throw will be + int accuracy = _vm->getRandomNumber(10) + _level * 2; + + if (accuracy <= 2) { + target.x += _vm->getRandomNumber(71) - 35; + target.y += _vm->getRandomNumber(71) - 35; + } else if (accuracy <= 4) { + target.x += _vm->getRandomNumber(51) - 25; + target.y += _vm->getRandomNumber(51) - 25; + } else if (accuracy <= 6) { + target.x += _vm->getRandomNumber(31) - 15; + target.y += _vm->getRandomNumber(31) - 15; + } else if (accuracy <= 8) { + target.x += _vm->getRandomNumber(21) - 10; + target.y += _vm->getRandomNumber(21) - 10; + } else if (accuracy <= 10) { + target.x += _vm->getRandomNumber(11) - 5; + target.y += _vm->getRandomNumber(11) - 5; + } + + if (target.x < 1) + target.x = 1; + if (target.y < 1) + target.y = 1; + + return target; +} + +bool Darts::findNumberOnBoard(int aim, Common::Point &pt) { + ImageFrame &board = (*_dartImages)[1]; + + // Scan board image for the special "center" pixels + bool done = false; + for (int yp = 0; yp < 132 && !done; ++yp) { + const byte *srcP = (const byte *)board._frame.getBasePtr(0, yp); + for (int xp = 0; xp < 147 && !done; ++xp, ++srcP) { + int score = *srcP; + + // Check for match + if (score == aim) { + done = true; + + // Aim at non-double/triple numbers where possible + if (aim < 21) { + pt.x = xp + 5; + pt.y = yp + 5; + + score = *(const byte *)board._frame.getBasePtr(xp + 10, yp + 10); + if (score != aim) + // Not aiming at non-double/triple number yet + done = false; + } else { + // Aiming at a double or triple + pt.x = xp + 3; + pt.y = yp + 3; + } + } + } + } + + if (aim == 3) + pt.x += 15; + pt.y = 132 - pt.y; + + return done; +} + +void Darts::setFlagsForDarts(int flagNum) { + _vm->_flags[ABS(flagNum)] = flagNum >= 0; +} + +} // End of namespace Scalpel + +} // End of namespace Scalpel diff --git a/engines/sherlock/scalpel/darts.h b/engines/sherlock/scalpel/darts.h new file mode 100644 index 0000000000..42990f8056 --- /dev/null +++ b/engines/sherlock/scalpel/darts.h @@ -0,0 +1,136 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_DARTS_H +#define SHERLOCK_DARTS_H + +#include "sherlock/resources.h" + +namespace Sherlock { + +namespace Scalpel { + +class ScalpelEngine; + +class Darts { +private: + ScalpelEngine *_vm; + ImageFile *_dartImages; + int _dartScore1, _dartScore2; + int _roundNumber; + int _level; + int _computerPlayer; + Common::String _opponent; + bool _playerDartMode; + int _roundScore; + bool _oldDartButtons; + + /** + * Load the graphics needed for the dart game + */ + void loadDarts(); + + /** + * Initializes the variables needed for the dart game + */ + void initDarts(); + + /** + * Frees the images used by the dart game + */ + void closeDarts(); + + /** + * Show the names of the people playing, Holmes and his opponent + */ + void showNames(int playerNum); + + /** + * Show the player score and game status + */ + void showStatus(int playerNum); + + /** + * Throws a single dart. + * @param dartNum Dart number + * @param computer 0 = Player, 1 = 1st player computer, 2 = 2nd player computer + * @returns Score for what dart hit + */ + int throwDart(int dartNum, int computer); + + /** + * Draw a dart moving towards the board + */ + void drawDartThrow(const Common::Point &pt); + + /** + * Erases the power bars + */ + void erasePowerBars(); + + /** + * Show a gradually incrementing incrementing power that bar. If goToPower is provided, it will + * increment to that power level ignoring all keyboard input (ie. for computer throws). + * Otherwise, it will increment until either a key/mouse button is pressed, or it reaches the end + */ + int doPowerBar(const Common::Point &pt, byte color, int goToPower, bool isVertical); + + /** + * Returns true if a mouse button or key is pressed. + */ + bool dartHit(); + + /** + * Return the score of the given location on the dart-board + */ + int dartScore(const Common::Point &pt); + + /** + * Calculates where a computer player is trying to throw their dart, and choose the actual + * point that was hit with some margin of error + */ + Common::Point getComputerDartDest(int playerNum); + + /** + * Returns the center position for the area of the dartboard with a given number + */ + bool findNumberOnBoard(int aim, Common::Point &pt); + + /** + * Set a global flag to 0 or 1 depending on whether the passed flag is negative or positive. + * @remarks We don't use the global setFlags method because we don't want to check scene flags + */ + void setFlagsForDarts(int flagNum); +public: + Darts(ScalpelEngine *vm); + + /** + * Main method for playing darts game + */ + void playDarts(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel.cpp b/engines/sherlock/scalpel/scalpel.cpp new file mode 100644 index 0000000000..d55517cdc3 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel.cpp @@ -0,0 +1,915 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/sherlock.h" +#include "sherlock/animation.h" + +namespace Sherlock { + +namespace Scalpel { + +#define PROLOGUE_NAMES_COUNT 6 + +// The following are a list of filenames played in the prologue that have +// special effects associated with them at specific frames +static const char *const PROLOGUE_NAMES[PROLOGUE_NAMES_COUNT] = { + "subway1", "subway2", "finale2", "suicid", "coff3", "coff4" +}; + +static const int PROLOGUE_FRAMES[6][9] = { + { 4, 26, 54, 72, 92, 134, FRAMES_END }, + { 2, 80, 95, 117, 166, FRAMES_END }, + { 1, FRAMES_END }, + { 42, FRAMES_END }, + { FRAMES_END }, + { FRAMES_END } +}; + +#define TITLE_NAMES_COUNT 7 + +// Title animations file list +static const char *const TITLE_NAMES[TITLE_NAMES_COUNT] = { + "27pro1", "14note", "coff1", "coff2", "coff3", "coff4", "14kick" +}; + +static const int TITLE_FRAMES[7][9] = { + { 29, 131, FRAMES_END }, + { 55, 80, 95, 117, 166, FRAMES_END }, + { 15, FRAMES_END }, + { 4, 37, 92, FRAMES_END }, + { 2, 43, FRAMES_END }, + { 2, FRAMES_END }, + { 10, 50, FRAMES_END } +}; + +#define NUM_PLACES 100 + +static const int MAP_X[NUM_PLACES] = { + 0, 368, 0, 219, 0, 282, 0, 43, 0, 0, 396, 408, 0, 0, 0, 568, 37, 325, + 28, 0, 263, 36, 148, 469, 342, 143, 443, 229, 298, 0, 157, 260, 432, + 174, 0, 351, 0, 528, 0, 136, 0, 0, 0, 555, 165, 0, 506, 0, 0, 344, 0, 0 +}; +static const int MAP_Y[NUM_PLACES] = { + 0, 147, 0, 166, 0, 109, 0, 61, 0, 0, 264, 70, 0, 0, 0, 266, 341, 30, 275, + 0, 294, 146, 311, 230, 184, 268, 133, 94, 207, 0, 142, 142, 330, 255, 0, + 37, 0, 70, 0, 116, 0, 0, 0, 50, 21, 0, 303, 0, 0, 229, 0, 0 +}; + +static const int MAP_TRANSLATE[NUM_PLACES] = { + 0, 0, 0, 1, 0, 2, 0, 3, 4, 0, 4, 6, 0, 0, 0, 8, 9, 10, 11, 0, 12, 13, 14, 7, + 15, 16, 17, 18, 19, 0, 20, 21, 22, 23, 0, 24, 0, 25, 0, 26, 0, 0, 0, 27, + 28, 0, 29, 0, 0, 30, 0 +}; + +static const byte MAP_SEQUENCES[3][MAX_FRAME] = { + { 1, 1, 2, 3, 4, 0 }, // Overview Still + { 5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 0 }, + { 5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 0 } +}; + +#define MAX_PEOPLE 66 + +const char PEOPLE_PORTRAITS[MAX_PEOPLE][5] = { + { "HOLM" }, // Sherlock Holmes + { "WATS" }, // Dr. Watson + { "LEST" }, // Inspector Lestrade + { "CON1" }, // Constable O'Brien + { "CON2" }, // Constable Lewis + { "SHEI" }, // Sheila Parker + { "HENR" }, // Henry Carruthers + { "LESL" }, // Lesley (flower girl) + { "USH1" }, // Usher #1 + { "USH2" }, // Usher #2 + { "FRED" }, // Fredrick Epstein + { "WORT" }, // Mrs. Worthington + { "COAC" }, // Coach + { "PLAY" }, // Player + { "WBOY" }, // Tim (Waterboy) + { "JAME" }, // James Sanders + { "BELL" }, // Belle (perfumerie) + { "GIRL" }, // Cleaning Girl (perfumerie) + { "EPST" }, // Epstien in the Opera Balcony + { "WIGG" }, // Wiggins + { "PAUL" }, // Paul (Brumwell / Carroway) + { "BART" }, // Bartender + { "DIRT" }, // Dirty Drunk + { "SHOU" }, // Shouting Drunk + { "STAG" }, // Staggering Drunk + { "BOUN" }, // Bouncer + { "SAND" }, // James Sanders - At Home + { "CORO" }, // The Coroner + { "EQUE" }, // The Equestrian Shop Keeper + { "GEOR" }, // George Blackwood + { "LARS" }, // Lars + { "PARK" }, // Sheila Parker (happy) + { "CHEM" }, // Chemist + { "GREG" }, // Inspector Gregson + { "LAWY" }, // Jacob Farthington Lawyer + { "MYCR" }, // Mycroft + { "SHER" }, // Old Sherman + { "CHMB" }, // Richard Chemist Stock boy + { "BARM" }, // Barman + { "DAND" }, // Dandy Player + { "ROUG" }, // Rough-looking Player + { "SPEC" }, // Spectator + { "HUNT" }, // Robert Hunt + { "VIOL" }, // Violet Secretary + { "PETT" }, // Pettigrew + { "APPL" }, // Augie (apple seller) + { "ANNA" }, // Anna Carroway + { "GUAR" }, // Guard + { "ANTO" }, // Antonio Caruso + { "TOBY" }, // Toby the Dog + { "KING" }, // Simon Kingsley + { "ALFR" }, // Alfred Tobacco Clerk + { "LADY" }, // Lady Brumwell + { "ROSA" }, // Madame Rosa + { "LADB" }, // Lady Brumwell + { "MOOR" }, // Joseph Moorehead + { "BEAL" }, // Mrs. Beale + { "LION" }, // Felix the Lion + { "HOLL" }, // Hollingston + { "CALL" }, // Constable Callaghan + { "JERE" }, // Sergeant Jeremy Duncan + { "LORD" }, // Lord Brumwell + { "NIGE" }, // Nigel Jameson + { "JONA" }, // Jonas (newspaper seller) + { "DUGA" }, // Constable Dugan + { "INSP" } // Inspector Lestrade (Scotland Yard) +}; + +const char *const PEOPLE_NAMES[MAX_PEOPLE] = { + "Sherlock Holmes", + "Dr. Watson", + "Inspector Lestrade", + "Constable O'Brien", + "Constable Lewis", + "Sheila Parker", + "Henry Carruthers", + "Lesley", + "An Usher", + "An Usher", + "Fredrick Epstein", + "Mrs. Worthington", + "The Coach", + "A Player", + "Tim", + "James Sanders", + "Belle", + "Cleaning Girl", + "Fredrick Epstein", + "Wiggins", + "Paul", + "The Bartender", + "A Dirty Drunk", + "A Shouting Drunk", + "A Staggering Drunk", + "The Bouncer", + "James Sanders", + "The Coroner", + "Reginald Snipes", + "George Blackwood", + "Lars", + "Sheila Parker", + "The Chemist", + "Inspector Gregson", + "Jacob Farthington", + "Mycroft", + "Old Sherman", + "Richard", + "The Barman", + "A Dandy Player", + "A Rough-looking Player", + "A Spectator", + "Robert Hunt", + "Violet", + "Pettigrew", + "Augie", + "Anna Carroway", + "A Guard", + "Antonio Caruso", + "Toby the Dog", + "Simon Kingsley", + "Alfred", + "Lady Brumwell", + "Madame Rosa", + "Lady Brumwell", + "Joseph Moorehead", + "Mrs. Beale", + "Felix", + "Hollingston", + "Constable Callaghan", + "Sergeant Duncan", + "Lord Brumwell", + "Nigel Jaimeson", + "Jonas", + "Constable Dugan", + "Inspector Lestrade" +}; + +static const byte PEOPLE_STILL_SEQUENCES[MAX_PEOPLE][MAX_TALK_SEQUENCES] = { + { 1, 0, 0 }, // Sherlock Holmes + { 6, 0, 0 }, // Dr. Watson + { 4, 0, 0 }, // Inspector Lestrade + { 2, 0, 0 }, // Constable #1 + { 2, 0, 0 }, // Constable #2 + { 2, 0, 0 }, // Sheila Parker + { 3, 0, 0 }, // Henry Carruthers + { 9, 0, 0 }, // Lesly (flower girl) + { 13, 0, 0 }, // Usher #1 + { 2, 0, 0 }, // Usher #2 + { 4, 0, 0 }, // Fredrick Epstein + { 9, 0, 0 }, // Mrs.Worthington + { 2, 0, 0 }, // Coach + { 8, 0, 0 }, // Player + { 13, 0, 0 }, // Waterboy + { 6, 0, 0 }, // James Sanders + { 1, 0, 0 }, // Belle (perfumerie) + { 20, 0, 0 }, // Cleaning Girl (perfumerie) + { 17, 0, 0 }, // Epstien in the Opera Balcony + { 3, 0, 0 }, // Wiggins + { 2, 0, 0 }, // Paul (Brumwell/Carroway) + { 1, 0, 0 }, // Bartender + { 1, 0, 0 }, // Dirty Drunk + { 1, 0, 0 }, // Shouting Drunk + { 1, 0, 0 }, // Staggering Drunk + { 1, 0, 0 }, // Bouncer + { 6, 0, 0 }, // James Sanders - At Home + { 6, 0, 0 }, // The Coroner + { 1, 0, 0 }, // The Equestrian Shop Keeper + { 1, 0, 0 }, // George Blackwood + { 7, 0, 0 }, // Lars + { 1, 0, 0 }, // Sheila Parker + { 8, 0, 0 }, // Chemist + { 6, 0, 0 }, // Inspector Gregson + { 1, 0, 0 }, // Lawyer + { 1, 0, 0 }, // Mycroft + { 7, 0, 0 }, // Old Sherman + { 1, 0, 0 }, // Stock Boy in Chemist Shop + { 1, 0, 0 }, // Barman + { 1, 0, 0 }, // Dandy Player + { 1, 0, 0 }, // Rough-looking Player + { 1, 0, 0 }, // Spectator + { 1, 0, 0 }, // Robert Hunt + { 3, 0, 0 }, // Violet Secretary + { 1, 0, 0 }, // Pettigrew + { 8, 0, 0 }, // Augie (apple seller) + { 16, 0, 0 }, // Anna Carroway + { 1, 0, 0 }, // Guard + { 8, 0, 0 }, // Antonio Caruso + { 1, 0, 0 }, // Toby the Dog + { 13, 0, 0 }, // Simon Kingsley + { 2, 0, 0 }, // Alfred Tobacco Clerk + { 1, 0, 0 }, // Lady Brumwell + { 1, 0, 0 }, // Madame Rosa + { 1, 0, 0 }, // Lady Brumwell + { 1, 0, 0 }, // Joseph Moorehead + { 5, 0, 0 }, // Mrs. Beale + { 1, 0, 0 }, // Felix the Lion + { 1, 0, 0 }, // Hollingston + { 1, 0, 0 }, // Constable Callaghan + { 2, 0, 0 }, // Sergeant Jeremy Duncan + { 1, 0, 0 }, // Lord Brumwell + { 1, 0, 0 }, // Nigel Jameson + { 1, 0, 0 }, // Jonas (newspaper seller) + { 1, 0, 0 }, // Constable Dugan + { 4, 0, 0 } // Inspector Lestrade (Yard) +}; + +static const byte PEOPLE_TALK_SEQUENCES[MAX_PEOPLE][MAX_TALK_SEQUENCES] = { + { 1, 0, 0 }, // Sherlock Holmes + { 5, 5, 6, 7, 8, 7, 8, 6, 0, 0 }, // Dr. Watson + { 2, 0, 0 }, // Inspector Lestrade + { 1, 0, 0 }, // Constable #1 + { 1, 0, 0 }, // Constable #2 + { 2, 3, 0, 0 }, // Sheila Parker + { 3, 0, 0 }, // Henry Carruthers + { 1, 2, 3, 2, 1, 2, 3, 0, 0 }, // Lesly (flower girl) + { 13, 14, 0, 0 }, // Usher #1 + { 2, 0, 0 }, // Usher #2 + { 1, 2, 3, 4, 3, 4, 3, 2, 0, 0 }, // Fredrick Epstein + { 8, 0, 0 }, // Mrs.Worthington + { 1, 2, 3, 4, 5, 4, 3, 2, 0, 0 }, // Coach + { 7, 8, 0, 0 }, // Player + { 12, 13, 0, 0 }, // Waterboy + { 3, 4, 0, 0 }, // James Sanders + { 4, 5, 0, 0 }, // Belle (perfumerie) + { 14, 15, 16, 17, 18, 19, 20, 20, 20, 0, 0 }, // Cleaning Girl (perfumerie) + { 16, 17, 18, 18, 18, 17, 17, 0, 0 }, // Epstien in the Opera Balcony + { 2, 3, 0, 0 }, // Wiggins + { 1, 2, 0, 0 }, // Paul (Brumwell/Carroway) + { 1, 0, 0 }, // Bartender + { 1, 0, 0 }, // Dirty Drunk + { 1, 0, 0 }, // Shouting Drunk + { 1, 0, 0 }, // Staggering Drunk + { 1, 0, 0 }, // Bouncer + { 5, 6, 0, 0 }, // James Sanders - At Home + { 4, 5, 0, 0 }, // The Coroner + { 1, 0, 0 }, // The Equestrian Shop Keeper + { 1, 0, 0 }, // George Blackwood + { 5, 6, 0, 0 }, // Lars + { 1, 0, 0 }, // Sheila Parker + { 8, 9, 0, 0 }, // Chemist + { 5, 6, 0, 0 }, // Inspector Gregson + { 1, 0, 0 }, // Lawyer + { 1, 0, 0 }, // Mycroft + { 7, 8, 0, 0 }, // Old Sherman + { 1, 0, 0 }, // Stock Boy in Chemist Shop + { 1, 0, 0 }, // Barman + { 1, 0, 0 }, // Dandy Player + { 1, 0, 0 }, // Rough-looking Player + { 1, 0, 0 }, // Spectator + { 1, 0, 0 }, // Robert Hunt + { 3, 4, 0, 0 }, // Violet Secretary + { 1, 0, 0 }, // Pettigrew + { 14, 15, 0, 0 }, // Augie (apple seller) + { 3, 4, 5, 6, 0, 0 }, // Anna Carroway + { 4, 5, 6, 0, 0 }, // Guard + { 7, 8, 0, 0 }, // Antonio Caruso + { 1, 0, 0 }, // Toby the Dog + { 13, 14, 0, 0 }, // Simon Kingsley + { 2, 3, 0, 0 }, // Alfred Tobacco Clerk + { 3, 4, 0, 0 }, // Lady Brumwell + { 1, 30, 0, 0 }, // Madame Rosa + { 3, 4, 0, 0 }, // Lady Brumwell + { 1, 0, 0 }, // Joseph Moorehead + { 14, 15, 16, 17, 18, 19, 20, 0, 0 }, // Mrs. Beale + { 1, 0, 0 }, // Felix the Lion + { 1, 0, 0 }, // Hollingston + { 1, 0, 0 }, // Constable Callaghan + { 1, 1, 2, 2, 0, 0 }, // Sergeant Jeremy Duncan + { 9, 10, 0, 0 }, // Lord Brumwell + { 1, 2, 0, 138, 3, 4, 0, 138, 0, 0 }, // Nigel Jameson + { 1, 8, 0, 0 }, // Jonas (newspaper seller) + { 1, 0, 0 }, // Constable Dugan + { 2, 0, 0 } // Inspector Lestrade (Yard) +}; + +/*----------------------------------------------------------------*/ + +ScalpelEngine::ScalpelEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + SherlockEngine(syst, gameDesc) { + _darts = nullptr; + _mapResult = 0; +} + +ScalpelEngine::~ScalpelEngine() { + delete _darts; +} + +void ScalpelEngine::initialize() { + SherlockEngine::initialize(); + + _darts = new Darts(this); + + _flags.resize(100 * 8); + _flags[3] = true; // Turn on Alley + _flags[39] = true; // Turn on Baker Street + + if (!isDemo()) { + // Load the map co-ordinates for each scene and sequence data + _map->loadPoints(NUM_PLACES, &MAP_X[0], &MAP_Y[0], &MAP_TRANSLATE[0]); + _map->loadSequences(3, &MAP_SEQUENCES[0][0]); + _map->_oldCharPoint = BAKER_ST_EXTERIOR; + } + + // Load the inventory + loadInventory(); + + // Set up list of people + for (int idx = 0; idx < MAX_PEOPLE; ++idx) + _people->_characters.push_back(PersonData(PEOPLE_NAMES[idx], PEOPLE_PORTRAITS[idx], + PEOPLE_STILL_SEQUENCES[idx], PEOPLE_TALK_SEQUENCES[idx])); + + _animation->setPrologueNames(&PROLOGUE_NAMES[0], PROLOGUE_NAMES_COUNT); + _animation->setPrologueFrames(&PROLOGUE_FRAMES[0][0], 6, 9); + + _animation->setTitleNames(&TITLE_NAMES[0], TITLE_NAMES_COUNT); + _animation->setTitleFrames(&TITLE_FRAMES[0][0], 7, 9); + + // Starting scene + if (isDemo() && _interactiveFl) + _scene->_goToScene = 3; + else + _scene->_goToScene = 4; +} + +void ScalpelEngine::showOpening() { + if (isDemo() && _interactiveFl) + return; + + if (!showCityCutscene()) + return; + if (!showAlleyCutscene()) + return; + if (!showStreetCutscene()) + return; + if (!showOfficeCutscene()) + return; + + _events->clearEvents(); + _sound->stopMusic(); +} + +bool ScalpelEngine::showCityCutscene() { + byte palette[PALETTE_SIZE]; + + _sound->playMusic("prolog1.mus"); + _animation->_gfxLibraryFilename = "title.lib"; + _animation->_soundLibraryFilename = "title.snd"; + bool finished = _animation->play("26open1", 1, 255, true, 2); + + if (finished) { + ImageFile titleImages("title2.vgs", true); + _screen->_backBuffer1.blitFrom(*_screen); + _screen->_backBuffer2.blitFrom(*_screen); + + // London, England + _screen->_backBuffer1.transBlitFrom(titleImages[0], Common::Point(10, 11)); + _screen->randomTransition(); + finished = _events->delay(1000, true); + + // November, 1888 + if (finished) { + _screen->_backBuffer1.transBlitFrom(titleImages[1], Common::Point(101, 102)); + _screen->randomTransition(); + finished = _events->delay(5000, true); + } + + // Transition out the title + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2); + _screen->randomTransition(); + } + + if (finished) + finished = _animation->play("26open2", 1, 0, false, 2); + + if (finished) { + ImageFile titleImages("title.vgs", true); + _screen->_backBuffer1.blitFrom(*_screen); + _screen->_backBuffer2.blitFrom(*_screen); + + // The Lost Files of + _screen->_backBuffer1.transBlitFrom(titleImages[0], Common::Point(75, 6)); + // Sherlock Holmes + _screen->_backBuffer1.transBlitFrom(titleImages[1], Common::Point(34, 21)); + // copyright + _screen->_backBuffer1.transBlitFrom(titleImages[2], Common::Point(4, 190)); + + _screen->verticalTransition(); + finished = _events->delay(4000, true); + + if (finished) { + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2); + _screen->randomTransition(); + finished = _events->delay(2000); + } + + if (finished) { + _screen->getPalette(palette); + _screen->fadeToBlack(2); + } + + if (finished) { + // In the alley... + _screen->transBlitFrom(titleImages[3], Common::Point(72, 51)); + _screen->fadeIn(palette, 3); + finished = _events->delay(3000, true); + } + } + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +bool ScalpelEngine::showAlleyCutscene() { + byte palette[PALETTE_SIZE]; + _sound->playMusic("prolog2.mus"); + + _animation->_gfxLibraryFilename = "TITLE.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + bool finished = _animation->play("27PRO1", 1, 3, true, 2); + if (finished) + finished = _animation->play("27PRO2", 1, 0, false, 2); + + if (finished) { + showLBV("scream.lbv"); + finished = _events->delay(6000); + } + + if (finished) + finished = _animation->play("27PRO3", 1, 0, true, 2); + + if (finished) { + _screen->getPalette(palette); + _screen->fadeToBlack(2); + } + + if (finished) { + ImageFile titleImages("title3.vgs", true); + // "Early the following morning on Baker Street..." + _screen->_backBuffer1.transBlitFrom(titleImages[0], Common::Point(35, 51), false, 0); + _screen->fadeIn(palette, 3); + finished = _events->delay(1000); + } + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +bool ScalpelEngine::showStreetCutscene() { + _animation->_gfxLibraryFilename = "TITLE.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + _sound->playMusic("PROLOG3.MUS"); + + bool finished = _animation->play("14KICK", 1, 3, true, 2); + + if (finished) + finished = _animation->play("14NOTE", 1, 0, false, 2); + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + + +bool ScalpelEngine::scrollCredits() { + // Load the images for displaying credit text + Common::SeekableReadStream *stream = _res->load("credits.vgs", "title.lib"); + ImageFile creditsImages(*stream); + _screen->setPalette(creditsImages._palette); + delete stream; + + // Save a copy of the screen background for use in drawing each credit frame + _screen->_backBuffer1.blitFrom(*_screen); + + // Loop for showing the credits + for(int idx = 0; idx < 600 && !_events->kbHit() && !shouldQuit(); ++idx) { + // Copy the entire screen background before writing text + _screen->blitFrom(_screen->_backBuffer1); + + // Write the text appropriate for the next frame + if (idx < 400) + _screen->transBlitFrom(creditsImages[0], Common::Point(10, 200 - idx), false, 0); + if (idx > 200) + _screen->transBlitFrom(creditsImages[1], Common::Point(10, 400 - idx), false, 0); + + // Don't show credit text on the top and bottom ten rows of the screen + _screen->blitFrom(_screen->_backBuffer1, Common::Point(0, 0), Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, 10)); + _screen->blitFrom(_screen->_backBuffer1, Common::Point(0, SHERLOCK_SCREEN_HEIGHT - 10), + Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - 10, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + _events->delay(100); + } + + return true; +} + +bool ScalpelEngine::showOfficeCutscene() { + _sound->playMusic("PROLOG4.MUS"); + _animation->_gfxLibraryFilename = "TITLE2.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + bool finished = _animation->play("COFF1", 1, 3, true, 3); + if (finished) + finished = _animation->play("COFF2", 1, 0, false, 3); + if (finished) { + showLBV("note.lbv"); + + if (_sound->_voices) { + finished = _sound->playSound("NOTE1", WAIT_KBD_OR_FINISH); + if (finished) + finished = _sound->playSound("NOTE2", WAIT_KBD_OR_FINISH); + if (finished) + finished = _sound->playSound("NOTE3", WAIT_KBD_OR_FINISH); + if (finished) + finished = _sound->playSound("NOTE4", WAIT_KBD_OR_FINISH); + } else + finished = _events->delay(19000); + + _events->clearEvents(); + finished = _events->delay(500); + } + + if (finished) + finished = _animation->play("COFF3", 1, 0, true, 3); + + if (finished) + finished = _animation->play("COFF4", 1, 0, false, 3); + + if (finished) + finished = scrollCredits(); + + if (finished) + _screen->fadeToBlack(3); + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +void ScalpelEngine::loadInventory() { + Inventory &inv = *_inventory; + + // Initial inventory + inv._holdings = 2; + inv.push_back(InventoryItem(0, "Message", "A message requesting help", "_ITEM03A")); + inv.push_back(InventoryItem(0, "Holmes Card", "A number of business cards", "_ITEM07A")); + + // Hidden items + inv.push_back(InventoryItem(95, "Tickets", "Opera Tickets", "_ITEM10A")); + inv.push_back(InventoryItem(138, "Cuff Link", "Cuff Link", "_ITEM04A")); + inv.push_back(InventoryItem(138, "Wire Hook", "Wire Hook", "_ITEM06A")); + inv.push_back(InventoryItem(150, "Note", "Note", "_ITEM13A")); + inv.push_back(InventoryItem(481, "Open Watch", "An open pocket watch", "_ITEM62A")); + inv.push_back(InventoryItem(481, "Paper", "A piece of paper with numbers on it", "_ITEM44A")); + inv.push_back(InventoryItem(532, "Letter", "A letter folded many times", "_ITEM68A")); + inv.push_back(InventoryItem(544, "Tarot", "Tarot Cards", "_ITEM71A")); + inv.push_back(InventoryItem(544, "Ornate Key", "An ornate key", "_ITEM70A")); + inv.push_back(InventoryItem(586, "Pawn ticket", "A pawn ticket", "_ITEM16A")); +} + +void ScalpelEngine::showLBV(const Common::String &filename) { + Common::SeekableReadStream *stream = _res->load(filename, "title.lib"); + ImageFile images(*stream); + delete stream; + + _screen->setPalette(images._palette); + _screen->_backBuffer1.blitFrom(images[0]); + _screen->verticalTransition(); +} + +void ScalpelEngine::startScene() { + if (_scene->_goToScene == OVERHEAD_MAP || _scene->_goToScene == OVERHEAD_MAP2) { + // Show the map + if (_sound->_musicOn) { + if (_sound->loadSong(100)) { + if (_sound->_music) + _sound->startSong(); + } + } + + _scene->_goToScene = _map->show(); + + _sound->freeSong(); + _people->_hSavedPos = Common::Point(-1, -1); + _people->_hSavedFacing = -1; + } + + // Some rooms are prologue cutscenes, rather than normal game scenes. These are: + // 2: Blackwood's capture + // 52: Rescuing Anna + // 53: Moorehead's death / subway train + // 55: Fade out and exit + // 70: Brumwell suicide + switch (_scene->_goToScene) { + case BLACKWOOD_CAPTURE: + case RESCUE_ANNA: + case MOOREHEAD_DEATH: + case BRUMWELL_SUICIDE: + if (_sound->_musicOn && _sound->loadSong(_scene->_goToScene)) { + if (_sound->_music) + _sound->startSong(); + } + + switch (_scene->_goToScene) { + case BLACKWOOD_CAPTURE: + // Blackwood's capture + _res->addToCache("final2.vda", "epilogue.lib"); + _res->addToCache("final2.vdx", "epilogue.lib"); + _animation->play("final1", 1, 3, true, 4); + _animation->play("final2", 1, 0, false, 4); + break; + + case RESCUE_ANNA: + // Rescuing Anna + _res->addToCache("finalr2.vda", "epilogue.lib"); + _res->addToCache("finalr2.vdx", "epilogue.lib"); + _res->addToCache("finale1.vda", "epilogue.lib"); + _res->addToCache("finale1.vdx", "epilogue.lib"); + _res->addToCache("finale2.vda", "epilogue.lib"); + _res->addToCache("finale2.vdx", "epilogue.lib"); + _res->addToCache("finale3.vda", "epilogue.lib"); + _res->addToCache("finale3.vdx", "epilogue.lib"); + _res->addToCache("finale4.vda", "EPILOG2.lib"); + _res->addToCache("finale4.vdx", "EPILOG2.lib"); + + _animation->play("finalr1", 1, 3, true, 4); + _animation->play("finalr2", 1, 0, false, 4); + + if (!_res->isInCache("finale2.vda")) { + // Finale file isn't cached + _res->addToCache("finale2.vda", "epilogue.lib"); + _res->addToCache("finale2.vdx", "epilogue.lib"); + _res->addToCache("finale3.vda", "epilogue.lib"); + _res->addToCache("finale3.vdx", "epilogue.lib"); + _res->addToCache("finale4.vda", "EPILOG2.lib"); + _res->addToCache("finale4.vdx", "EPILOG2.lib"); + } + + _animation->play("finale1", 1, 0, false, 4); + _animation->play("finale2", 1, 0, false, 4); + _animation->play("finale3", 1, 0, false, 4); + + _useEpilogue2 = true; + _animation->play("finale4", 1, 0, false, 4); + _useEpilogue2 = false; + break; + + case MOOREHEAD_DEATH: + // Moorehead's death / subway train + _res->addToCache("SUBWAY2.vda", "epilogue.lib"); + _res->addToCache("SUBWAY2.vdx", "epilogue.lib"); + _res->addToCache("SUBWAY3.vda", "epilogue.lib"); + _res->addToCache("SUBWAY3.vdx", "epilogue.lib"); + + _animation->play("SUBWAY1", 1, 3, true, 4); + _animation->play("SUBWAY2", 1, 0, false, 4); + _animation->play("SUBWAY3", 1, 0, false, 4); + + // Set fading to direct fade temporary so the transition goes quickly. + _scene->_tempFadeStyle = _screen->_fadeStyle ? 257 : 256; + _screen->_fadeStyle = false; + break; + + case BRUMWELL_SUICIDE: + // Brumwell suicide + _animation->play("suicid", 1, 3, true, 4); + break; + default: + break; + } + + // Except for the Moorehead Murder scene, fade to black first + if (_scene->_goToScene != MOOREHEAD_DEATH) { + _events->wait(40); + _screen->fadeToBlack(3); + } + + switch (_scene->_goToScene) { + case 52: + _scene->_goToScene = LAWYER_OFFICE; // Go to the Lawyer's Office + _map->_bigPos = Common::Point(0, 0); // Overland scroll position + _map->_overPos = Common::Point(22900 - 600, 9400 + 900); // Overland position + _map->_oldCharPoint = LAWYER_OFFICE; + break; + + case 53: + _scene->_goToScene = STATION; // Go to St. Pancras Station + _map->_bigPos = Common::Point(0, 0); // Overland scroll position + _map->_overPos = Common::Point(32500 - 600, 3000 + 900); // Overland position + _map->_oldCharPoint = STATION; + break; + + default: + _scene->_goToScene = BAKER_STREET; // Back to Baker st. + _map->_bigPos = Common::Point(0, 0); // Overland scroll position + _map->_overPos = Common::Point(14500 - 600, 8400 + 900); // Overland position + _map->_oldCharPoint = BAKER_STREET; + break; + } + + // Free any song from the previous scene + _sound->freeSong(); + break; + + case EXIT_GAME: + // Exit game + _screen->fadeToBlack(3); + quitGame(); + return; + + default: + break; + } + + _events->loadCursors("rmouse.vgs"); + _events->setCursor(ARROW); + + if (_scene->_goToScene == 99) { + // Darts Board minigame + _darts->playDarts(); + _mapResult = _scene->_goToScene = PUB_INTERIOR; + } + + _mapResult = _scene->_goToScene; +} + +void ScalpelEngine::eraseMirror12() { + Common::Point pt((*_people)[AL]._position.x / 100, (*_people)[AL]._position.y / 100); + + // If player is in range of the mirror, then restore background from the secondary back buffer + if (Common::Rect(70, 100, 200, 200).contains(pt)) { + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(137, 18), + Common::Rect(137, 18, 184, 74)); + } +} + +void ScalpelEngine::doMirror12() { + People &people = *_people; + Common::Point pt((*_people)[AL]._position.x / 100, (*_people)[AL]._position.y / 100); + int frameNum = (*people[AL]._sequences)[people[AL]._sequenceNumber][people[AL]._frameNumber] + + (*people[AL]._sequences)[people[AL]._sequenceNumber][0] - 2; + + switch ((*_people)[AL]._sequenceNumber) { + case WALK_DOWN: + frameNum -= 7; + break; + case WALK_UP: + frameNum += 7; + break; + case WALK_DOWNRIGHT: + frameNum += 7; + break; + case WALK_UPRIGHT: + frameNum -= 7; + break; + case WALK_DOWNLEFT: + frameNum += 7; + break; + case WALK_UPLEFT: + frameNum -= 7; + break; + case STOP_DOWN: + frameNum -= 10; + break; + case STOP_UP: + frameNum += 11; + break; + case STOP_DOWNRIGHT: + frameNum -= 15; + break; + case STOP_DOWNLEFT: + frameNum -= 15; + break; + case STOP_UPRIGHT: + case STOP_UPLEFT: + frameNum += 15; + if (frameNum == 55) + frameNum = 54; + break; + default: + break; + } + + if (Common::Rect(80, 100, 145, 138).contains(pt)) { + // Get the frame of Sherlock to draw + ImageFrame &imageFrame = (*people[AL]._images)[frameNum]; + + // Draw the mirror image of Holmes + bool flipped = people[AL]._sequenceNumber == WALK_LEFT || people[AL]._sequenceNumber == STOP_LEFT + || people[AL]._sequenceNumber == WALK_UPRIGHT || people[AL]._sequenceNumber == STOP_UPRIGHT + || people[AL]._sequenceNumber == WALK_DOWNLEFT || people[AL]._sequenceNumber == STOP_DOWNLEFT; + _screen->_backBuffer1.transBlitFrom(imageFrame, pt + Common::Point(38, -imageFrame._frame.h - 25), flipped); + + // Redraw the mirror borders to prevent the drawn image of Holmes from appearing outside of the mirror + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(114, 18), + Common::Rect(114, 18, 137, 114)); + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(137, 70), + Common::Rect(137, 70, 142, 114)); + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(142, 71), + Common::Rect(142, 71, 159, 114)); + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(159, 72), + Common::Rect(159, 72, 170, 116)); + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(170, 73), + Common::Rect(170, 73, 184, 114)); + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(184, 18), + Common::Rect(184, 18, 212, 114)); + } +} + +void ScalpelEngine::flushMirror12() { + Common::Point pt((*_people)[AL]._position.x / 100, (*_people)[AL]._position.y / 100); + + // If player is in range of the mirror, then draw the entire mirror area to the screen + if (Common::Rect(70, 100, 200, 200).contains(pt)) + _screen->slamArea(137, 18, 47, 56); +} + +} // End of namespace Scalpel + +} // End of namespace Scalpel diff --git a/engines/sherlock/scalpel/scalpel.h b/engines/sherlock/scalpel/scalpel.h new file mode 100644 index 0000000000..8743bfb7a9 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel.h @@ -0,0 +1,117 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SCALPEL_H +#define SHERLOCK_SCALPEL_H + +#include "sherlock/sherlock.h" +#include "sherlock/scalpel/darts.h" + +namespace Sherlock { + +namespace Scalpel { + +enum { BLACKWOOD_CAPTURE = 2, BAKER_STREET = 4, DRAWING_ROOM = 12, STATION = 17, PUB_INTERIOR = 19, + LAWYER_OFFICE = 27, BAKER_ST_EXTERIOR = 39, RESCUE_ANNA = 52, MOOREHEAD_DEATH = 53, EXIT_GAME = 55, + BRUMWELL_SUICIDE = 70, OVERHEAD_MAP2 = 98, DARTS_GAME = 99, OVERHEAD_MAP = 100 }; + +class ScalpelEngine : public SherlockEngine { +private: + Darts *_darts; + int _mapResult; + + /** + * Show the starting city cutscene which shows the game title + */ + bool showCityCutscene(); + + /** + * Show the back alley where the initial murder takes place + */ + bool showAlleyCutscene(); + + /** + * Show the Baker Street outside cutscene + */ + bool showStreetCutscene(); + + /** + * Show Holmes and Watson at the breakfast table, lestrade's note, and then the scrolling credits + */ + bool showOfficeCutscene(); + + /** + * Show the game credits + */ + bool scrollCredits(); + + /** + * Load the default inventory for the game, which includes both the initial active inventory, + * as well as special pending inventory items which can appear automatically in the player's + * inventory once given required flags are set + */ + void loadInventory(); + + /** + * Transition to show an image + */ + void showLBV(const Common::String &filename); +protected: + /** + * Game initialization + */ + virtual void initialize(); + + /** + * Show the opening sequence + */ + virtual void showOpening(); + + /** + * Starting a scene within the game + */ + virtual void startScene(); +public: + ScalpelEngine(OSystem *syst, const SherlockGameDescription *gameDesc); + virtual ~ScalpelEngine(); + + /** + * Takes care of clearing the mirror in scene 12 (mansion drawing room), in case anything drew over it + */ + void eraseMirror12(); + + /** + * Takes care of drawing Holme's reflection onto the mirror in scene 12 (mansion drawing room) + */ + void doMirror12(); + + /** + * This clears the mirror in scene 12 (mansion drawing room) in case anything messed draw over it + */ + void flushMirror12(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scene.cpp b/engines/sherlock/scene.cpp new file mode 100644 index 0000000000..f97b791724 --- /dev/null +++ b/engines/sherlock/scene.cpp @@ -0,0 +1,1469 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/scene.h" +#include "sherlock/sherlock.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +static const int FS_TRANS[8] = { + STOP_UP, STOP_UPRIGHT, STOP_RIGHT, STOP_DOWNRIGHT, STOP_DOWN, + STOP_DOWNLEFT, STOP_LEFT, STOP_UPLEFT +}; + +/*----------------------------------------------------------------*/ + +void BgFileHeader::load(Common::SeekableReadStream &s) { + _numStructs = s.readUint16LE(); + _numImages = s.readUint16LE(); + _numcAnimations = s.readUint16LE(); + _descSize = s.readUint16LE(); + _seqSize = s.readUint16LE(); + _fill = s.readUint16LE(); +} + +/*----------------------------------------------------------------*/ + +void BgFileHeaderInfo::load(Common::SeekableReadStream &s) { + _filesize = s.readUint32LE(); + _maxFrames = s.readByte(); + + char buffer[9]; + s.read(buffer, 9); + _filename = Common::String(buffer); +} + +/*----------------------------------------------------------------*/ + +void Exit::load(Common::SeekableReadStream &s) { + int xp = s.readSint16LE(); + int yp = s.readSint16LE(); + int xSize = s.readSint16LE(); + int ySize = s.readSint16LE(); + _bounds = Common::Rect(xp, yp, xp + xSize, yp + ySize); + + _scene = s.readSint16LE(); + _allow = s.readSint16LE(); + _people.x = s.readSint16LE(); + _people.y = s.readSint16LE(); + _peopleDir = s.readUint16LE(); +} + +/*----------------------------------------------------------------*/ + +void SceneEntry::load(Common::SeekableReadStream &s) { + _startPosition.x = s.readSint16LE(); + _startPosition.y = s.readSint16LE(); + _startDir = s.readByte(); + _allow = s.readByte(); +} + +void SceneSound::load(Common::SeekableReadStream &s) { + char buffer[9]; + s.read(buffer, 8); + buffer[8] = '\0'; + + _name = Common::String(buffer); + _priority = s.readByte(); +} + +/*----------------------------------------------------------------*/ + +int ObjectArray::indexOf(const Object &obj) const { + for (uint idx = 0; idx < size(); ++idx) { + if (&(*this)[idx] == &obj) + return idx; + } + + return -1; +} + +/*----------------------------------------------------------------*/ + +Scene::Scene(SherlockEngine *vm) : _vm(vm) { + for (int idx = 0; idx < SCENES_COUNT; ++idx) + Common::fill(&_sceneStats[idx][0], &_sceneStats[idx][65], false); + _currentScene = -1; + _goToScene = -1; + _loadingSavedGame = false; + _changes = false; + _keyboardInput = 0; + _walkedInScene = false; + _version = 0; + _lzwMode = false; + _invGraphicItems = 0; + _cAnimFramePause = 0; + _restoreFlag = false; + _invLookFlag = false; + _lookHelp = false; + _animating = 0; + _doBgAnimDone = true; + _tempFadeStyle = 0; +} + +Scene::~Scene() { + freeScene(); +} + +void Scene::selectScene() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + + // Reset fields + ui._windowOpen = ui._infoFlag = false; + ui._menuMode = STD_MODE; + _keyboardInput = 0; + _oldKey = _help = _oldHelp = 0; + _oldTemp = _temp = 0; + + // Free any previous scene + freeScene(); + + // Load the scene + Common::String sceneFile = Common::String::format("res%02d", _goToScene); + _rrmName = Common::String::format("res%02d.rrm", _goToScene); + _currentScene = _goToScene; + _goToScene = -1; + + loadScene(sceneFile); + + // If the fade style was changed from running a movie, then reset it + if (_tempFadeStyle) { + screen._fadeStyle = _tempFadeStyle; + _tempFadeStyle = 0; + } + + people._walkDest = Common::Point(people[AL]._position.x / 100, + people[AL]._position.y / 100); + + _restoreFlag = true; + events.clearEvents(); + + // If there were any scripts waiting to be run, but were interrupt by a running + // canimation (probably the last scene's exit canim), clear the _scriptMoreFlag + if (talk._scriptMoreFlag == 3) + talk._scriptMoreFlag = 0; +} + +void Scene::freeScene() { + if (_currentScene == -1) + return; + + _vm->_talk->freeTalkVars(); + _vm->_inventory->freeInv(); + _vm->_sound->freeSong(); + _vm->_sound->freeLoadedSounds(); + + if (!_loadingSavedGame) + saveSceneStatus(); + else + _loadingSavedGame = false; + + _sequenceBuffer.clear(); + _descText.clear(); + _walkData.clear(); + _cAnim.clear(); + _bgShapes.clear(); + _zones.clear(); + _canimShapes.clear(); + + for (uint idx = 0; idx < _images.size(); ++idx) + delete _images[idx]._images; + _images.clear(); + + _currentScene = -1; +} + +bool Scene::loadScene(const Common::String &filename) { + Events &events = *_vm->_events; + Map &map = *_vm->_map; + People &people = *_vm->_people; + SaveManager &saves = *_vm->_saves; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + bool flag; + + _walkedInScene = false; + + // Reset the list of walkable areas + _zones.clear(); + _zones.push_back(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + _descText.clear(); + _comments = ""; + _bgShapes.clear(); + _cAnim.clear(); + _sequenceBuffer.clear(); + + // + // Load background shapes from <filename>.rrm + // + + Common::String rrmFile = filename + ".rrm"; + flag = _vm->_res->exists(rrmFile); + if (flag) { + Common::SeekableReadStream *rrmStream = _vm->_res->load(rrmFile); + + rrmStream->seek(39); + _version = rrmStream->readByte(); + _lzwMode = _version == 10; + + // Go to header and read it in + rrmStream->seek(rrmStream->readUint32LE()); + BgFileHeader bgHeader; + bgHeader.load(*rrmStream); + _invGraphicItems = bgHeader._numImages + 1; + + // Read in the shapes header info + Common::Array<BgFileHeaderInfo> bgInfo; + bgInfo.resize(bgHeader._numStructs); + + for (uint idx = 0; idx < bgInfo.size(); ++idx) + bgInfo[idx].load(*rrmStream); + + // Read information + if (!_lzwMode) { + _bgShapes.resize(bgHeader._numStructs); + for (int idx = 0; idx < bgHeader._numStructs; ++idx) + _bgShapes[idx].load(*rrmStream); + + if (bgHeader._descSize) { + _descText.resize(bgHeader._descSize); + rrmStream->read(&_descText[0], bgHeader._descSize); + } + + if (bgHeader._seqSize) { + _sequenceBuffer.resize(bgHeader._seqSize); + rrmStream->read(&_sequenceBuffer[0], bgHeader._seqSize); + } + } else { + Common::SeekableReadStream *infoStream; + + // Read shapes + infoStream = Resources::decompressLZ(*rrmStream, bgHeader._numStructs * 569); + + _bgShapes.resize(bgHeader._numStructs); + for (int idx = 0; idx < bgHeader._numStructs; ++idx) + _bgShapes[idx].load(*infoStream); + + delete infoStream; + + // Read description texts + if (bgHeader._descSize) { + infoStream = Resources::decompressLZ(*rrmStream, bgHeader._descSize); + + _descText.resize(bgHeader._descSize); + infoStream->read(&_descText[0], bgHeader._descSize); + + delete infoStream; + } + + // Read sequences + if (bgHeader._seqSize) { + infoStream = Resources::decompressLZ(*rrmStream, bgHeader._seqSize); + + _sequenceBuffer.resize(bgHeader._seqSize); + infoStream->read(&_sequenceBuffer[0], bgHeader._seqSize); + + delete infoStream; + } + } + + // Set up the list of images used by the scene + _images.resize(bgHeader._numImages + 1); + for (int idx = 0; idx < bgHeader._numImages; ++idx) { + _images[idx + 1]._filesize = bgInfo[idx]._filesize; + _images[idx + 1]._maxFrames = bgInfo[idx]._maxFrames; + + // Read in the image data + Common::SeekableReadStream *imageStream = _lzwMode ? + Resources::decompressLZ(*rrmStream, bgInfo[idx]._filesize) : + rrmStream->readStream(bgInfo[idx]._filesize); + + _images[idx + 1]._images = new ImageFile(*imageStream); + + delete imageStream; + } + + // Set up the bgShapes + for (int idx = 0; idx < bgHeader._numStructs; ++idx) { + _bgShapes[idx]._images = _images[_bgShapes[idx]._misc]._images; + _bgShapes[idx]._imageFrame = !_bgShapes[idx]._images ? (ImageFrame *)nullptr : + &(*_bgShapes[idx]._images)[0]; + + _bgShapes[idx]._examine = Common::String(&_descText[_bgShapes[idx]._descOffset]); + _bgShapes[idx]._sequences = &_sequenceBuffer[_bgShapes[idx]._sequenceOffset]; + _bgShapes[idx]._misc = 0; + _bgShapes[idx]._seqCounter = 0; + _bgShapes[idx]._seqCounter2 = 0; + _bgShapes[idx]._seqStack = 0; + _bgShapes[idx]._frameNumber = -1; + _bgShapes[idx]._oldPosition = Common::Point(0, 0); + _bgShapes[idx]._oldSize = Common::Point(1, 1); + } + + // Load in cAnim list + _cAnim.clear(); + if (bgHeader._numcAnimations) { + Common::SeekableReadStream *canimStream = _lzwMode ? + Resources::decompressLZ(*rrmStream, 65 * bgHeader._numcAnimations) : + rrmStream->readStream(65 * bgHeader._numcAnimations); + + _cAnim.resize(bgHeader._numcAnimations); + for (uint idx = 0; idx < _cAnim.size(); ++idx) + _cAnim[idx].load(*canimStream); + + delete canimStream; + } + + // Read in the room bounding areas + int size = rrmStream->readUint16LE(); + Common::SeekableReadStream *boundsStream = !_lzwMode ? rrmStream : + Resources::decompressLZ(*rrmStream, size); + + _zones.resize(size / 10); + for (uint idx = 0; idx < _zones.size(); ++idx) { + _zones[idx].left = boundsStream->readSint16LE(); + _zones[idx].top = boundsStream->readSint16LE(); + _zones[idx].setWidth(boundsStream->readSint16LE() + 1); + _zones[idx].setHeight(boundsStream->readSint16LE() + 1); + boundsStream->skip(2); // Skip unused scene number field + } + + if (_lzwMode) + delete boundsStream; + + // Ensure we've reached the path version byte + if (rrmStream->readByte() != 254) + error("Invalid scene path data"); + + // Load the walk directory + for (uint idx1 = 0; idx1 < _zones.size(); ++idx1) { + for (uint idx2 = 0; idx2 < _zones.size(); ++idx2) + _walkDirectory[idx1][idx2] = rrmStream->readSint16LE(); + } + + // Read in the walk data + size = rrmStream->readUint16LE(); + Common::SeekableReadStream *walkStream = !_lzwMode ? rrmStream : + Resources::decompressLZ(*rrmStream, size); + + _walkData.resize(size); + walkStream->read(&_walkData[0], size); + + if (_lzwMode) + delete walkStream; + + // Read in the exits + int numExits = rrmStream->readByte(); + _exits.resize(numExits); + + for (int idx = 0; idx < numExits; ++idx) + _exits[idx].load(*rrmStream); + + // Read in the entrance + _entrance.load(*rrmStream); + + // Initialize sound list + int numSounds = rrmStream->readByte(); + _sounds.resize(numSounds); + + for (int idx = 0; idx < numSounds; ++idx) + _sounds[idx].load(*rrmStream); + + for (int idx = 0; idx < numSounds; ++idx) + sound.loadSound(_sounds[idx]._name, _sounds[idx]._priority); + + // Read in palette + rrmStream->read(screen._cMap, PALETTE_SIZE); + for (int idx = 0; idx < PALETTE_SIZE; ++idx) + screen._cMap[idx] = VGA_COLOR_TRANS(screen._cMap[idx]); + + Common::copy(screen._cMap, screen._cMap + PALETTE_SIZE, screen._sMap); + + // Read in the background + Common::SeekableReadStream *bgStream = !_lzwMode ? rrmStream : + Resources::decompressLZ(*rrmStream, SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCENE_HEIGHT); + + bgStream->read(screen._backBuffer1.getPixels(), SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCENE_HEIGHT); + + if (_lzwMode) + delete bgStream; + + // Backup the image and set the palette + screen._backBuffer2.blitFrom(screen._backBuffer1); + screen.setPalette(screen._cMap); + + delete rrmStream; + } + + // Clear user interface area and draw controls + ui.drawInterface(); + + _changes = false; + checkSceneStatus(); + + if (!saves._justLoaded) { + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == HIDDEN && _bgShapes[idx]._aType == TALK_EVERY) + _bgShapes[idx].toggleHidden(); + } + + // Check for TURNON objects + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == HIDDEN && (_bgShapes[idx]._flags & TURNON_OBJ)) + _bgShapes[idx].toggleHidden(); + } + + // Check for TURNOFF objects + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type != HIDDEN && (_bgShapes[idx]._flags & TURNOFF_OBJ) && + _bgShapes[idx]._type != INVALID) + _bgShapes[idx].toggleHidden(); + if (_bgShapes[idx]._type == HIDE_SHAPE) + // Hiding isn't needed, since objects aren't drawn yet + _bgShapes[idx]._type = HIDDEN; + } + } + + checkSceneFlags(false); + checkInventory(); + + // Handle starting any music for the scene + if (sound._musicOn && sound.loadSong(_currentScene)) { + if (sound._music) + sound.startSong(); + } + + // Load walking images if not already loaded + people.loadWalk(); + + // Transition to the scene and setup entrance co-ordinates and animations + transitionToScene(); + + // Player has not yet walked in this scene + _walkedInScene = false; + saves._justLoaded = false; + + if (!_vm->isDemo()) { + // Reset the previous map location and position on overhead map + map._oldCharPoint = _currentScene; + map._overPos.x = map[_currentScene].x * 100 - 600; + map._overPos.y = map[_currentScene].y * 100 + 900; + } + + events.clearEvents(); + return flag; +} + +void Scene::checkSceneStatus() { + if (_sceneStats[_currentScene][64]) { + for (uint idx = 0; idx < 64; ++idx) { + bool flag = _sceneStats[_currentScene][idx]; + + if (idx < _bgShapes.size()) { + Object &obj = _bgShapes[idx]; + + if (flag) { + // No shape to erase, so flag as hidden + obj._type = HIDDEN; + } else if (obj._images == nullptr || obj._images->size() == 0) { + // No shape + obj._type = NO_SHAPE; + } else { + obj._type = ACTIVE_BG_SHAPE; + } + } else { + // Finished checks + return; + } + } + } +} + +void Scene::saveSceneStatus() { + // Flag any objects for the scene that have been altered + int count = MIN((int)_bgShapes.size(), 64); + for (int idx = 0; idx < count; ++idx) { + Object &obj = _bgShapes[idx]; + _sceneStats[_currentScene][idx] = obj._type == HIDDEN || obj._type == REMOVE + || obj._type == HIDE_SHAPE || obj._type == INVALID; + } + + // Flag scene as having been visited + _sceneStats[_currentScene][64] = true; +} + +void Scene::checkSceneFlags(bool flag) { + SpriteType mode = flag ? HIDE_SHAPE : HIDDEN; + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + + if (o._requiredFlag) { + if (!_vm->readFlags(_bgShapes[idx]._requiredFlag)) { + // Kill object + if (o._type != HIDDEN && o._type != INVALID) { + if (o._images == nullptr || o._images->size() == 0) + // No shape to erase, so flag as hidden + o._type = HIDDEN; + else + // Flag it as needing to be hidden after first erasing it + o._type = mode; + } + } else if (_bgShapes[idx]._requiredFlag > 0) { + // Restore object + if (o._images == nullptr || o._images->size() == 0) + o._type = NO_SHAPE; + else + o._type = ACTIVE_BG_SHAPE; + } + } + } + + // Check inventory for items to remove based on flag changes + for (int idx = 0; idx < _vm->_inventory->_holdings; ++idx) { + InventoryItem &ii = (*_vm->_inventory)[idx]; + if (ii._requiredFlag && !_vm->readFlags(ii._requiredFlag)) { + // Kill object: move it after the active holdings + InventoryItem tempItem = (*_vm->_inventory)[idx]; + _vm->_inventory->insert_at(_vm->_inventory->_holdings, tempItem); + _vm->_inventory->remove_at(idx); + _vm->_inventory->_holdings--; + } + } + + // Check inactive inventory items for ones to reactivate based on flag changes + for (uint idx = _vm->_inventory->_holdings; idx < _vm->_inventory->size(); ++idx) { + InventoryItem &ii = (*_vm->_inventory)[idx]; + if (ii._requiredFlag && _vm->readFlags(ii._requiredFlag)) { + // Restore object: move it after the active holdings + InventoryItem tempItem = (*_vm->_inventory)[idx]; + _vm->_inventory->remove_at(idx); + _vm->_inventory->insert_at(_vm->_inventory->_holdings, tempItem); + _vm->_inventory->_holdings++; + } + } +} + +void Scene::checkInventory() { + for (uint shapeIdx = 0; shapeIdx < _bgShapes.size(); ++shapeIdx) { + for (int invIdx = 0; invIdx < _vm->_inventory->_holdings; ++invIdx) { + if (_bgShapes[shapeIdx]._name.equalsIgnoreCase((*_vm->_inventory)[invIdx]._name)) { + _bgShapes[shapeIdx]._type = INVALID; + break; + } + } + } +} + +void Scene::transitionToScene() { + People &people = *_vm->_people; + SaveManager &saves = *_vm->_saves; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + Common::Point &hSavedPos = people._hSavedPos; + int &hSavedFacing = people._hSavedFacing; + + if (hSavedPos.x < 1) { + // No exit information from last scene-check entrance info + if (_entrance._startPosition.x < 1) { + // No entrance info either, so use defaults + hSavedPos = Common::Point(16000, 10000); + hSavedFacing = 4; + } else { + // setup entrance info + hSavedPos = _entrance._startPosition; + hSavedFacing = _entrance._startDir; + } + } else { + // Exit information exists, translate it to real sequence info + // Note: If a savegame was just loaded, then the data is already correct. + // Otherwise, this is a linked scene or entrance info, and must be translated + if (hSavedFacing < 8 && !saves._justLoaded) { + hSavedFacing = FS_TRANS[hSavedFacing]; + hSavedPos.x *= 100; + hSavedPos.y *= 100; + } + } + + int cAnimNum = -1; + + if (hSavedFacing < 101) { + // Standard info, so set it + people[PLAYER]._position = hSavedPos; + people[PLAYER]._sequenceNumber = hSavedFacing; + } else { + // It's canimation information + cAnimNum = hSavedFacing - 101; + } + + // Reset positioning for next load + hSavedPos = Common::Point(-1, -1); + hSavedFacing = -1; + + if (cAnimNum != -1) { + // Prevent Holmes from being drawn + people[PLAYER]._position = Common::Point(0, 0); + } + + for (uint objIdx = 0; objIdx < _bgShapes.size(); ++objIdx) { + Object &obj = _bgShapes[objIdx]; + + if (obj._aType > 1 && obj._type != INVALID && obj._type != HIDDEN) { + Common::Point topLeft = obj._position; + Common::Point bottomRight; + + if (obj._type != NO_SHAPE) { + topLeft += obj._imageFrame->_offset; + bottomRight.x = topLeft.x + obj._imageFrame->_frame.w; + bottomRight.y = topLeft.y + obj._imageFrame->_frame.h; + } else { + bottomRight = topLeft + obj._noShapeSize; + } + + if (Common::Rect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y).contains( + Common::Point(people[PLAYER]._position.x / 100, people[PLAYER]._position.y / 100))) { + // Current point is already inside box - impact occurred on + // a previous call. So simply do nothing except talk until the + // player is clear of the box + switch (obj._aType) { + case FLAG_SET: + for (int useNum = 0; useNum < USE_COUNT; ++useNum) { + if (obj._use[useNum]._useFlag) { + if (!_vm->readFlags(obj._use[useNum]._useFlag)) + _vm->setFlags(obj._use[useNum]._useFlag); + } + + if (!talk._talkToAbort) { + for (int nameIdx = 0; nameIdx < NAMES_COUNT; ++nameIdx) { + toggleObject(obj._use[useNum]._names[nameIdx]); + } + } + } + + obj._type = HIDDEN; + break; + + default: + break; + } + } + } + } + + updateBackground(); + + if (screen._fadeStyle) + screen.randomTransition(); + else + screen.blitFrom(screen._backBuffer1); + + if (cAnimNum != -1) { + CAnim &c = _cAnim[cAnimNum]; + Common::Point pt = c._goto; + + c._goto = Common::Point(-1, -1); + people[AL]._position = Common::Point(0, 0); + + startCAnim(cAnimNum, 1); + c._goto = pt; + } +} + +int Scene::toggleObject(const Common::String &name) { + int count = 0; + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (name.equalsIgnoreCase(_bgShapes[idx]._name)) { + ++count; + _bgShapes[idx].toggleHidden(); + } + } + + return count; +} + +void Scene::updateBackground() { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Sprite &player = people[AL]; + + // Restrict drawing window + screen.setDisplayBounds(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + + // Update Holmes if he's turned on + if (people._holmesOn) + player.adjustSprite(); + + // Flag the bg shapes which need to be redrawn + checkBgShapes(player._imageFrame, Common::Point(player._position.x / 100, + player._position.y / 100)); + + // Draw all active shapes which are behind the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == ACTIVE_BG_SHAPE && _bgShapes[idx]._misc == BEHIND) + screen._backBuffer->transBlitFrom(*_bgShapes[idx]._imageFrame, _bgShapes[idx]._position, _bgShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all canimations which are behind the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if (_canimShapes[idx]._type == ACTIVE_BG_SHAPE && _canimShapes[idx]._misc == BEHIND) + screen._backBuffer->transBlitFrom(*_canimShapes[idx]._imageFrame, + _canimShapes[idx]._position, _canimShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all active shapes which are normal and behind the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == ACTIVE_BG_SHAPE && _bgShapes[idx]._misc == NORMAL_BEHIND) + screen._backBuffer->transBlitFrom(*_bgShapes[idx]._imageFrame, _bgShapes[idx]._position, _bgShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all canimations which are normal and behind the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if (_canimShapes[idx]._type == ACTIVE_BG_SHAPE && _canimShapes[idx]._misc == NORMAL_BEHIND) + screen._backBuffer->transBlitFrom(*_canimShapes[idx]._imageFrame, _canimShapes[idx]._position, + _canimShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw the player if he's active + if (player._type == CHARACTER && people.isHolmesActive()) { + bool flipped = player._sequenceNumber == WALK_LEFT || player._sequenceNumber == STOP_LEFT || + player._sequenceNumber == WALK_UPLEFT || player._sequenceNumber == STOP_UPLEFT || + player._sequenceNumber == WALK_DOWNRIGHT || player._sequenceNumber == STOP_DOWNRIGHT; + + screen._backBuffer->transBlitFrom(*player._imageFrame, Common::Point(player._position.x / 100, + player._position.y / 100 - player.frameHeight()), flipped); + } + + // Draw all static and active shapes that are NORMAL and are in front of the player + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if ((_bgShapes[idx]._type == ACTIVE_BG_SHAPE || _bgShapes[idx]._type == STATIC_BG_SHAPE) && + _bgShapes[idx]._misc == NORMAL_FORWARD) + screen._backBuffer->transBlitFrom(*_bgShapes[idx]._imageFrame, _bgShapes[idx]._position, + _bgShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all static and active canimations that are NORMAL and are in front of the player + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if ((_canimShapes[idx]._type == ACTIVE_BG_SHAPE || _canimShapes[idx]._type == STATIC_BG_SHAPE) && + _canimShapes[idx]._misc == NORMAL_FORWARD) + screen._backBuffer->transBlitFrom(*_canimShapes[idx]._imageFrame, _canimShapes[idx]._position, + _canimShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all static and active shapes that are FORWARD + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + _bgShapes[idx]._oldPosition = _bgShapes[idx]._position; + _bgShapes[idx]._oldSize = Common::Point(_bgShapes[idx].frameWidth(), + _bgShapes[idx].frameHeight()); + + if ((_bgShapes[idx]._type == ACTIVE_BG_SHAPE || _bgShapes[idx]._type == STATIC_BG_SHAPE) && + _bgShapes[idx]._misc == FORWARD) + screen._backBuffer->transBlitFrom(*_bgShapes[idx]._imageFrame, _bgShapes[idx]._position, + _bgShapes[idx]._flags & OBJ_FLIPPED); + } + + // Draw all static and active canimations that are forward + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if ((_canimShapes[idx]._type == ACTIVE_BG_SHAPE || _canimShapes[idx]._type == STATIC_BG_SHAPE) && + _canimShapes[idx]._misc == FORWARD) + screen._backBuffer->transBlitFrom(*_canimShapes[idx]._imageFrame, _canimShapes[idx]._position, + _canimShapes[idx]._flags & OBJ_FLIPPED); + } + + screen.resetDisplayBounds(); +} + +Exit *Scene::checkForExit(const Common::Rect &r) { + for (uint idx = 0; idx < _exits.size(); ++idx) { + if (_exits[idx]._bounds.intersects(r)) + return &_exits[idx]; + } + + return nullptr; +} + +void Scene::checkBgShapes(ImageFrame *frame, const Common::Point &pt) { + // Iterate through the shapes + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + if (obj._type == STATIC_BG_SHAPE || obj._type == ACTIVE_BG_SHAPE) { + if ((obj._flags & 5) == 1) { + obj._misc = (pt.y < (obj._position.y + obj.frameHeight() - 1)) ? + NORMAL_FORWARD : NORMAL_BEHIND; + } else if (!(obj._flags & OBJ_BEHIND)) { + obj._misc = BEHIND; + } else if (obj._flags & OBJ_FORWARD) { + obj._misc = FORWARD; + } + } + } + + // Iterate through the canimshapes + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &obj = _canimShapes[idx]; + if (obj._type == STATIC_BG_SHAPE || obj._type == ACTIVE_BG_SHAPE) { + if ((obj._flags & 5) == 1) { + obj._misc = (pt.y < (obj._position.y + obj._imageFrame->_frame.h - 1)) ? + NORMAL_FORWARD : NORMAL_BEHIND; + } + else if (!(obj._flags & 1)) { + obj._misc = BEHIND; + } + else if (obj._flags & 4) { + obj._misc = FORWARD; + } + } + } +} + +int Scene::startCAnim(int cAnimNum, int playRate) { + Events &events = *_vm->_events; + Map &map = *_vm->_map; + People &people = *_vm->_people; + Resources &res = *_vm->_res; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + Common::Point tpPos, walkPos; + int tpDir, walkDir; + int tFrames = 0; + int gotoCode = -1; + + // Validation + if (cAnimNum >= (int)_cAnim.size()) + // number out of bounds + return -1; + if (_canimShapes.size() >= 3 || playRate == 0) + // Too many active animations, or invalid play rate + return 0; + + CAnim &cAnim = _cAnim[cAnimNum]; + if (playRate < 0) { + // Reverse direction + walkPos = cAnim._teleportPos; + walkDir = cAnim._teleportDir; + tpPos = cAnim._goto; + tpDir = cAnim._gotoDir; + } else { + // Forward direction + walkPos = cAnim._goto; + walkDir = cAnim._gotoDir; + tpPos = cAnim._teleportPos; + tpDir = cAnim._teleportDir; + } + + CursorId oldCursor = events.getCursor(); + events.setCursor(WAIT); + + if (walkPos.x != -1) { + // Holmes must walk to the walk point before the cAnimation is started + if (people[AL]._position != walkPos) + people.walkToCoords(walkPos, walkDir); + } + + if (talk._talkToAbort) + return 1; + + // Add new anim shape entry for displaying the animation + _canimShapes.push_back(Object()); + Object &cObj = _canimShapes[_canimShapes.size() - 1]; + + // Copy the canimation into the bgShapes type canimation structure so it can be played + cObj._allow = cAnimNum + 1; // Keep track of the parent structure + cObj._name = _cAnim[cAnimNum]._name; // Copy name + + // Remove any attempt to draw object frame + if (cAnim._type == NO_SHAPE && cAnim._sequences[0] < 100) + cAnim._sequences[0] = 0; + + cObj._sequences = cAnim._sequences; + cObj._images = nullptr; + cObj._position = cAnim._position; + cObj._delta = Common::Point(0, 0); + cObj._type = cAnim._type; + cObj._flags = cAnim._flags; + + cObj._maxFrames = 0; + cObj._frameNumber = -1; + cObj._sequenceNumber = cAnimNum; + cObj._oldPosition = Common::Point(0, 0); + cObj._oldSize = Common::Point(0, 0); + cObj._goto = Common::Point(0, 0); + cObj._status = 0; + cObj._misc = 0; + cObj._imageFrame = nullptr; + + if (cAnim._name.size() > 0 && cAnim._type != NO_SHAPE) { + if (tpPos.x != -1) + people[AL]._type = REMOVE; + + Common::String fname = cAnim._name + ".vgs"; + if (!res.isInCache(fname)) { + // Set up RRM scene data + Common::SeekableReadStream *rrmStream = res.load(_rrmName); + rrmStream->seek(44 + cAnimNum * 4); + rrmStream->seek(rrmStream->readUint32LE()); + + // Load the canimation into the cache + Common::SeekableReadStream *imgStream = !_lzwMode ? rrmStream->readStream(cAnim._size) : + Resources::decompressLZ(*rrmStream, cAnim._size); + res.addToCache(fname, *imgStream); + + delete imgStream; + delete rrmStream; + } + + // Now load the resource as an image + cObj._images = new ImageFile(fname); + cObj._imageFrame = &(*cObj._images)[0]; + cObj._maxFrames = cObj._images->size(); + + int frames = 0; + if (playRate < 0) { + // Reverse direction + // Count number of frames + while (cObj._sequences[frames] && frames < MAX_FRAME) + ++frames; + } else { + // Forward direction + Object::_countCAnimFrames = true; + + while (cObj._type == ACTIVE_BG_SHAPE) { + cObj.checkObject(); + ++frames; + + if (frames >= 1000) + error("CAnim has infinite loop sequence"); + } + + if (frames > 1) + --frames; + + Object::_countCAnimFrames = false; + + cObj._type = cAnim._type; + cObj._frameNumber = -1; + cObj._position = cAnim._position; + cObj._delta = Common::Point(0, 0); + } + + // Return if animation has no frames in it + if (frames == 0) + return -2; + + ++frames; + int repeat = ABS(playRate); + int dir; + + if (playRate < 0) { + // Play in reverse + dir = -2; + cObj._frameNumber = frames - 3; + } else { + dir = 0; + } + + tFrames = frames - 1; + int pauseFrame = (_cAnimFramePause) ? frames - _cAnimFramePause : -1; + + while (--frames) { + if (frames == pauseFrame) + ui.printObjectDesc(); + + doBgAnim(); + + // Repeat same frame + int temp = repeat; + while (--temp > 0) { + cObj._frameNumber--; + doBgAnim(); + + if (_vm->shouldQuit()) + return 0; + } + + cObj._frameNumber += dir; + } + + people[AL]._type = CHARACTER; + } + + // Teleport to ending coordinates if necessary + if (tpPos.x != -1) { + people[AL]._position = tpPos; // Place the player + people[AL]._sequenceNumber = tpDir; + people.gotoStand(people[AL]); + } + + if (playRate < 0) + // Reverse direction - set to end sequence + cObj._frameNumber = tFrames - 1; + + if (cObj._frameNumber <= 26) + gotoCode = cObj._sequences[cObj._frameNumber + 3]; + + // Unless anim shape has already been freed, set it to REMOVE so doBgAnim can free it + if (_canimShapes.indexOf(cObj) != -1) + cObj.checkObject(); + + if (gotoCode > 0 && !talk._talkToAbort) { + _goToScene = gotoCode; + + if (_goToScene < 97 && map[_goToScene].x) { + map._overPos = map[_goToScene]; + } + } + + people.loadWalk(); + + if (tpPos.x != -1 && !talk._talkToAbort) { + // Teleport to ending coordinates + people[AL]._position = tpPos; + people[AL]._sequenceNumber = tpDir; + + people.gotoStand(people[AL]); + } + + events.setCursor(oldCursor); + + return 1; +} + +void Scene::doBgAnim() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + + screen.setDisplayBounds(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + + int cursorId = events.getCursor(); + Common::Point mousePos = events.mousePos(); + + talk._talkToAbort = false; + + // Animate the mouse cursor + if (cursorId >= WAIT) { + if (++cursorId > (WAIT + 2)) + cursorId = WAIT; + + events.setCursor((CursorId)cursorId); + } + + if (ui._menuMode == LOOK_MODE) { + if (mousePos.y > CONTROLS_Y1) + events.setCursor(ARROW); + else if (mousePos.y < CONTROLS_Y) + events.setCursor(MAGNIFY); + } + + // Check for setting magnifying glass cursor + if (ui._menuMode == INV_MODE || ui._menuMode == USE_MODE || ui._menuMode == GIVE_MODE) { + if (inv._invMode == INVMODE_LOOK) { + // Only show Magnifying glass cursor if it's not on the inventory command line + if (mousePos.y < CONTROLS_Y || mousePos.y >(CONTROLS_Y1 + 13)) + events.setCursor(MAGNIFY); + else + events.setCursor(ARROW); + } else { + events.setCursor(ARROW); + } + } + + if (sound._diskSoundPlaying && !*sound._soundIsOn) { + // Loaded sound just finished playing + sound.freeDigiSound(); + } + + if (_restoreFlag) { + if (people[AL]._type == CHARACTER) + people[AL].checkSprite(); + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == ACTIVE_BG_SHAPE) + _bgShapes[idx].checkObject(); + } + + if (people._portraitLoaded && people._portrait._type == ACTIVE_BG_SHAPE) + people._portrait.checkObject(); + + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if (_canimShapes[idx]._type != INVALID && _canimShapes[idx]._type != REMOVE) + _canimShapes[idx].checkObject(); + } + + if (_currentScene == 12 && _vm->getGameID() == GType_SerratedScalpel) + ((Scalpel::ScalpelEngine *)_vm)->eraseMirror12(); + + // Restore the back buffer from the back buffer 2 in the changed area + Common::Rect bounds(people[AL]._oldPosition.x, people[AL]._oldPosition.y, + people[AL]._oldPosition.x + people[AL]._oldSize.x, + people[AL]._oldPosition.y + people[AL]._oldSize.y); + Common::Point pt(bounds.left, bounds.top); + + if (people[AL]._type == CHARACTER) + screen.restoreBackground(bounds); + else if (people[AL]._type == REMOVE) + screen._backBuffer->blitFrom(screen._backBuffer2, pt, bounds); + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE || o._type == HIDE_SHAPE || o._type == REMOVE) + screen.restoreBackground(o.getOldBounds()); + } + + if (people._portraitLoaded) + screen.restoreBackground(Common::Rect( + people._portrait._oldPosition.x, people._portrait._oldPosition.y, + people._portrait._oldPosition.x + people._portrait._oldSize.x, + people._portrait._oldPosition.y + people._portrait._oldSize.y + )); + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == NO_SHAPE && ((o._flags & OBJ_BEHIND) == 0)) { + // Restore screen area + screen._backBuffer->blitFrom(screen._backBuffer2, o._position, + Common::Rect(o._position.x, o._position.y, + o._position.x + o._noShapeSize.x, o._position.y + o._noShapeSize.y)); + + o._oldPosition = o._position; + o._oldSize = o._noShapeSize; + } + } + + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &o = _canimShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE || o._type == HIDE_SHAPE || o._type == REMOVE) + screen.restoreBackground(Common::Rect(o._oldPosition.x, o._oldPosition.y, + o._oldPosition.x + o._oldSize.x, o._oldPosition.y + o._oldSize.y)); + } + } + + // + // Update the background objects and canimations + // + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE || o._type == NO_SHAPE) + o.adjustObject(); + } + + if (people._portraitLoaded && people._portrait._type == ACTIVE_BG_SHAPE) + people._portrait.adjustObject(); + + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + if (_canimShapes[idx]._type != INVALID) + _canimShapes[idx].adjustObject(); + } + + if (people[AL]._type == CHARACTER && people._holmesOn) + people[AL].adjustSprite(); + + // Flag the bg shapes which need to be redrawn + checkBgShapes(people[AL]._imageFrame, + Common::Point(people[AL]._position.x / 100, people[AL]._position.y / 100)); + + if (_currentScene == 12 && _vm->getGameID() == GType_SerratedScalpel) + ((Scalpel::ScalpelEngine *)_vm)->doMirror12(); + + // Draw all active shapes which are behind the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE && o._misc == BEHIND) + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + + // Draw all canimations which are behind the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &o = _canimShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE && o._misc == BEHIND) { + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + } + + // Draw all active shapes which are HAPPEN and behind the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE && o._misc == NORMAL_BEHIND) + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + + // Draw all canimations which are NORMAL and behind the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &o = _canimShapes[idx]; + if (o._type == ACTIVE_BG_SHAPE && o._misc == NORMAL_BEHIND) { + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + } + + // Draw the person if not animating + if (people[AL]._type == CHARACTER && people.isHolmesActive()) { + // If Holmes is too far to the right, move him back so he's on-screen + int xRight = SHERLOCK_SCREEN_WIDTH - 2 - people[AL]._imageFrame->_frame.w; + int tempX = MIN(people[AL]._position.x / 100, xRight); + + bool flipped = people[AL]._sequenceNumber == WALK_LEFT || people[AL]._sequenceNumber == STOP_LEFT || + people[AL]._sequenceNumber == WALK_UPLEFT || people[AL]._sequenceNumber == STOP_UPLEFT || + people[AL]._sequenceNumber == WALK_DOWNRIGHT || people[AL]._sequenceNumber == STOP_DOWNRIGHT; + screen._backBuffer->transBlitFrom(*people[AL]._imageFrame, + Common::Point(tempX, people[AL]._position.y / 100 - people[AL]._imageFrame->_frame.h), flipped); + } + + // Draw all static and active shapes are NORMAL and are in front of the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if ((o._type == ACTIVE_BG_SHAPE || o._type == STATIC_BG_SHAPE) && o._misc == NORMAL_FORWARD) + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + + // Draw all static and active canimations that are NORMAL and are in front of the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &o = _canimShapes[idx]; + if ((o._type == ACTIVE_BG_SHAPE || o._type == STATIC_BG_SHAPE) && o._misc == NORMAL_FORWARD) { + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + } + + // Draw all static and active shapes that are in front of the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if ((o._type == ACTIVE_BG_SHAPE || o._type == STATIC_BG_SHAPE) && o._misc == FORWARD) + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + + // Draw any active portrait + if (people._portraitLoaded && people._portrait._type == ACTIVE_BG_SHAPE) + screen._backBuffer->transBlitFrom(*people._portrait._imageFrame, + people._portrait._position, people._portrait._flags & OBJ_FLIPPED); + + // Draw all static and active canimations that are in front of the person + for (uint idx = 0; idx < _canimShapes.size(); ++idx) { + Object &o = _canimShapes[idx]; + if ((o._type == ACTIVE_BG_SHAPE || o._type == STATIC_BG_SHAPE) && o._misc == FORWARD) { + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + } + + // Draw all NO_SHAPE shapes which have flag bit 0 clear + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == NO_SHAPE && (o._flags & OBJ_BEHIND) == 0) + screen._backBuffer->transBlitFrom(*o._imageFrame, o._position, o._flags & OBJ_FLIPPED); + } + + // Bring the newly built picture to the screen + if (_animating == 2) { + _animating = 0; + screen.slamRect(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + } else { + if (people[AL]._type != INVALID && ((_goToScene == -1 || _canimShapes.empty()))) { + if (people[AL]._type == REMOVE) { + screen.slamRect(Common::Rect( + people[AL]._oldPosition.x, people[AL]._oldPosition.y, + people[AL]._oldPosition.x + people[AL]._oldSize.x, + people[AL]._oldPosition.y + people[AL]._oldSize.y + )); + people[AL]._type = INVALID; + } else { + screen.flushImage(people[AL]._imageFrame, + Common::Point(people[AL]._position.x / 100, + people[AL]._position.y / 100 - people[AL].frameHeight()), + &people[AL]._oldPosition.x, &people[AL]._oldPosition.y, + &people[AL]._oldSize.x, &people[AL]._oldSize.y); + } + } + + if (_currentScene == 12 && _vm->getGameID() == GType_SerratedScalpel) + ((Scalpel::ScalpelEngine *)_vm)->flushMirror12(); + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if ((o._type == ACTIVE_BG_SHAPE || o._type == REMOVE) && _goToScene == -1) { + screen.flushImage(o._imageFrame, o._position, + &o._oldPosition.x, &o._oldPosition.y, &o._oldSize.x, &o._oldSize.y); + } + } + + if (people._portraitLoaded) { + if (people._portrait._type == REMOVE) + screen.slamRect(Common::Rect( + people._portrait._position.x, people._portrait._position.y, + people._portrait._position.x + people._portrait._delta.x, + people._portrait._position.y + people._portrait._delta.y + )); + else + screen.flushImage(people._portrait._imageFrame, people._portrait._position, + &people._portrait._oldPosition.x, &people._portrait._oldPosition.y, + &people._portrait._oldSize.x, &people._portrait._oldSize.y); + + if (people._portrait._type == REMOVE) + people._portrait._type = INVALID; + } + + if (_goToScene == -1) { + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if (o._type == NO_SHAPE && (o._flags & OBJ_BEHIND) == 0) { + screen.slamArea(o._position.x, o._position.y, o._oldSize.x, o._oldSize.y); + screen.slamArea(o._oldPosition.x, o._oldPosition.y, o._oldSize.x, o._oldSize.y); + } else if (o._type == HIDE_SHAPE) { + // Hiding shape, so flush it out and mark it as hidden + screen.flushImage(o._imageFrame, o._position, + &o._oldPosition.x, &o._oldPosition.y, &o._oldSize.x, &o._oldSize.y); + o._type = HIDDEN; + } + } + } + + for (int idx = _canimShapes.size() - 1; idx >= 0; --idx) { + Object &o = _canimShapes[idx]; + + if (o._type == INVALID) { + // Anim shape was invalidated by checkEndOfSequence, so at this point we can remove it + _canimShapes.remove_at(idx); + } else if (o._type == REMOVE) { + if (_goToScene == -1) + screen.slamArea(o._position.x, o._position.y, o._delta.x, o._delta.y); + + // Shape for an animation is no longer needed, so remove it completely + _canimShapes.remove_at(idx); + } else if (o._type == ACTIVE_BG_SHAPE) { + screen.flushImage(o._imageFrame, o._position, + &o._oldPosition.x, &o._oldPosition.y, &o._oldSize.x, &o._oldSize.y); + } + } + } + + _restoreFlag = true; + _doBgAnimDone = true; + + events.wait(3); + screen.resetDisplayBounds(); + + // Check if the method was called for calling a portrait, and a talk was + // interrupting it. This talk file would not have been executed at the time, + // since we needed to finish the 'doBgAnim' to finish clearing the portrait + if (people._clearingThePortrait && talk._scriptMoreFlag == 3) { + // Reset the flags and call to talk + people._clearingThePortrait = false; + talk._scriptMoreFlag = 0; + talk.talkTo(talk._scriptName); + } +} + +int Scene::findBgShape(const Common::Rect &r) { + if (!_doBgAnimDone) + // New frame hasn't been drawn yet + return -1; + + for (int idx = (int)_bgShapes.size() - 1; idx >= 0; --idx) { + Object &o = _bgShapes[idx]; + if (o._type != INVALID && o._type != NO_SHAPE && o._type != HIDDEN + && o._aType <= PERSON) { + if (r.intersects(o.getNewBounds())) + return idx; + } else if (o._type == NO_SHAPE) { + if (r.intersects(o.getNoShapeBounds())) + return idx; + } + } + + return -1; +} + +int Scene::checkForZones(const Common::Point &pt, int zoneType) { + int matches = 0; + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &o = _bgShapes[idx]; + if ((o._aType == zoneType && o._type != INVALID) && o._type != HIDDEN) { + Common::Rect r = o._type == NO_SHAPE ? o.getNoShapeBounds() : o.getNewBounds(); + + if (r.contains(pt)) { + ++matches; + o.setFlagsAndToggles(); + _vm->_talk->talkTo(o._use[0]._target); + } + } + } + + return matches; +} + +int Scene::whichZone(const Common::Point &pt) { + for (uint idx = 0; idx < _zones.size(); ++idx) { + if (_zones[idx].contains(pt)) + return idx; + } + + return -1; +} + +int Scene::closestZone(const Common::Point &pt) { + int dist = 1000; + int zone = -1; + + for (uint idx = 0; idx < _zones.size(); ++idx) { + Common::Point zc((_zones[idx].left + _zones[idx].right) / 2, + (_zones[idx].top + _zones[idx].bottom) / 2); + int d = ABS(zc.x - pt.x) + ABS(zc.y - pt.y); + + if (d < dist) { + // Found a closer zone + dist = d; + zone = idx; + } + } + + return zone; +} + +void Scene::synchronize(Common::Serializer &s) { + if (s.isSaving()) + saveSceneStatus(); + + if (s.isSaving()) { + s.syncAsSint16LE(_currentScene); + } else { + s.syncAsSint16LE(_goToScene); + _loadingSavedGame = true; + } + + for (int sceneNum = 0; sceneNum < SCENES_COUNT; ++sceneNum) { + for (int flag = 0; flag < 65; ++flag) { + s.syncAsByte(_sceneStats[sceneNum][flag]); + } + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/scene.h b/engines/sherlock/scene.h new file mode 100644 index 0000000000..243aba20f8 --- /dev/null +++ b/engines/sherlock/scene.h @@ -0,0 +1,274 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SCENE_H +#define SHERLOCK_SCENE_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "sherlock/objects.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +#define SCENES_COUNT 63 +#define MAX_ZONES 40 +#define INFO_LINE 140 + +class SherlockEngine; + +struct BgFileHeader { + int _numStructs; + int _numImages; + int _numcAnimations; + int _descSize; + int _seqSize; + int _fill; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); +}; + +struct BgFileHeaderInfo { + int _filesize; // How long images are + int _maxFrames; // How many unique frames in object + Common::String _filename; // Filename of object + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); +}; + +struct Exit { + Common::Rect _bounds; + + int _scene; + int _allow; + Common::Point _people; + int _peopleDir; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); +}; + +struct SceneEntry { + Common::Point _startPosition; + int _startDir; + int _allow; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); +}; + +struct SceneSound { + Common::String _name; + int _priority; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); +}; + +class ObjectArray : public Common::Array<Object> { +public: + /** + * Retuurn the index of the passed object in the array + */ + int indexOf(const Object &obj) const; +}; + +class Scene { +private: + SherlockEngine *_vm; + Common::String _rrmName; + int _selector; + bool _lookHelp; + bool _loadingSavedGame; + + /** + * Loads the data associated for a given scene. The .BGD file's format is: + * BGHEADER: Holds an index for the rest of the file + * STRUCTS: The objects for the scene + * IMAGES: The graphic information for the structures + * + * The _misc field of the structures contains the number of the graphic image + * that it should point to after loading; _misc is then set to 0. + */ + bool loadScene(const Common::String &filename); + + /** + * Set objects to their current persistent state. This includes things such as + * opening or moving them + */ + void checkSceneStatus(); + + /** + * Checks scene objects against the player's inventory items. If there are any + * matching names, it means the given item has already been picked up, and should + * be hidden in the scene. + */ + void checkInventory(); + + /** + * Set up any entrance co-ordinates or entrance canimations, and then transition + * in the scene + */ + void transitionToScene(); + + /** + * Checks all the background shapes. If a background shape is animating, + * it will flag it as needing to be drawn. If a non-animating shape is + * colliding with another shape, it will also flag it as needing drawing + */ + void checkBgShapes(ImageFrame *frame, const Common::Point &pt); + + /** + * Restores objects to the correct status. This ensures that things like being opened or moved + * will remain the same on future visits to the scene + */ + void saveSceneStatus(); +public: + int _currentScene; + int _goToScene; + bool _changes; + bool _sceneStats[SCENES_COUNT][65]; + bool _savedStats[SCENES_COUNT][9]; + int _keyboardInput; + int _oldKey, _help, _oldHelp; + int _oldTemp, _temp; + bool _walkedInScene; + int _version; + bool _lzwMode; + int _invGraphicItems; + Common::String _comments; + Common::Array<char> _descText; + Common::Array<Common::Rect> _zones; + Common::Array<Object> _bgShapes; + Common::Array<CAnim> _cAnim; + Common::Array<byte> _sequenceBuffer; + Common::Array<SceneImage> _images; + int _walkDirectory[MAX_ZONES][MAX_ZONES]; + Common::Array<byte> _walkData; + Common::Array<Exit> _exits; + SceneEntry _entrance; + Common::Array<SceneSound> _sounds; + ObjectArray _canimShapes; + bool _restoreFlag; + int _animating; + bool _doBgAnimDone; + int _tempFadeStyle; + int _cAnimFramePause; + bool _invLookFlag; +public: + Scene(SherlockEngine *vm); + ~Scene(); + + /** + * Handles loading the scene specified by _goToScene + */ + void selectScene(); + + /** + * Fres all the graphics and other dynamically allocated data for the scene + */ + void freeScene(); + + /** + * Check the scene's objects against the game flags. If false is passed, + * it means the scene has just been loaded. A value of true means that the scene + * is in use (ie. not just loaded) + */ + void checkSceneFlags(bool mode); + + /** + * Check whether the passed area intersects with one of the scene's exits + */ + Exit *checkForExit(const Common::Rect &r); + + /** + * Attempt to start a canimation sequence. It will load the requisite graphics, and + * then copy the canim object into the _canimShapes array to start the animation. + * + * @param cAnimNum The canim object within the current scene + * @param playRate Play rate. 0 is invalid; 1=normal speed, 2=1/2 speed, etc. + * A negative playRate can also be specified to play the animation in reverse + */ + int startCAnim(int cAnimNum, int playRate); + + /** + * Scans through the object list to find one with a matching name, and will + * call toggleHidden with all matches found. Returns the numer of matches found + */ + int toggleObject(const Common::String &name); + + /** + * Animate all objects and people. + */ + void doBgAnim(); + + /** + * Attempts to find a background shape within the passed bounds. If found, + * it will return the shape number, or -1 on failure. + */ + int findBgShape(const Common::Rect &r); + + /** + * Checks to see if the given position in the scene belongs to a given zone type. + * If it is, the zone is activated and used just like a TAKL zone or aFLAG_SET zone. + */ + int checkForZones(const Common::Point &pt, int zoneType); + + /** + * Check which zone the the given position is located in. + */ + int whichZone(const Common::Point &pt); + + /** + * Returns the index of the closest zone to a given point. + */ + int closestZone(const Common::Point &pt); + + /** + * Update the screen back buffer with all of the scene objects which need + * to be drawn + */ + void updateBackground(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/screen.cpp b/engines/sherlock/screen.cpp new file mode 100644 index 0000000000..e70d0614d1 --- /dev/null +++ b/engines/sherlock/screen.cpp @@ -0,0 +1,429 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/screen.h" +#include "sherlock/sherlock.h" +#include "common/system.h" +#include "common/util.h" +#include "graphics/palette.h" + +namespace Sherlock { + +Screen::Screen(SherlockEngine *vm) : Surface(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), _vm(vm), + _backBuffer1(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), + _backBuffer2(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), + _backBuffer(&_backBuffer1) { + _transitionSeed = 1; + _fadeStyle = false; + _font = nullptr; + _fontHeight = 0; + Common::fill(&_cMap[0], &_cMap[PALETTE_SIZE], 0); + Common::fill(&_sMap[0], &_sMap[PALETTE_SIZE], 0); + setFont(1); +} + +Screen::~Screen() { + delete _font; +} + +void Screen::setFont(int fontNumb) { + // Interactive demo doesn't use fonts + if (!_vm->_interactiveFl) + return; + + _fontNumber = fontNumb; + Common::String fname = Common::String::format("FONT%d.VGS", fontNumb + 1); + + // Discard any previous font and read in new one + delete _font; + _font = new ImageFile(fname); + + // Iterate through the frames to find the tallest font character + _fontHeight = 0; + for (uint idx = 0; idx < _font->size(); ++idx) + _fontHeight = MAX((uint16)_fontHeight, (*_font)[idx]._frame.h); +} + +void Screen::update() { + // Merge the dirty rects + mergeDirtyRects(); + + // Loop through copying dirty areas to the physical screen + Common::List<Common::Rect>::iterator i; + for (i = _dirtyRects.begin(); i != _dirtyRects.end(); ++i) { + const Common::Rect &r = *i; + const byte *srcP = (const byte *)getBasePtr(r.left, r.top); + g_system->copyRectToScreen(srcP, _surface.pitch, r.left, r.top, + r.width(), r.height()); + } + + // Signal the physical screen to update + g_system->updateScreen(); + _dirtyRects.clear(); +} + +void Screen::getPalette(byte palette[PALETTE_SIZE]) { + g_system->getPaletteManager()->grabPalette(palette, 0, PALETTE_COUNT); +} + +void Screen::setPalette(const byte palette[PALETTE_SIZE]) { + g_system->getPaletteManager()->setPalette(palette, 0, PALETTE_COUNT); +} + +int Screen::equalizePalette(const byte palette[PALETTE_SIZE]) { + int total = 0; + byte tempPalette[PALETTE_SIZE]; + getPalette(tempPalette); + + // For any palette component that doesn't already match the given destination + // palette, change by 1 towards the reference palette component + for (int idx = 0; idx < PALETTE_SIZE; ++idx) { + if (tempPalette[idx] > palette[idx]) { + tempPalette[idx] = MAX((int)palette[idx], (int)tempPalette[idx] - 4); + ++total; + } else if (tempPalette[idx] < palette[idx]) { + tempPalette[idx] = MIN((int)palette[idx], (int)tempPalette[idx] + 4); + ++total; + } + } + + if (total > 0) + // Palette changed, so reload it + setPalette(tempPalette); + + return total; +} + +void Screen::fadeToBlack(int speed) { + byte tempPalette[PALETTE_SIZE]; + Common::fill(&tempPalette[0], &tempPalette[PALETTE_SIZE], 0); + + while (equalizePalette(tempPalette)) { + _vm->_events->delay(15 * speed); + } + + setPalette(tempPalette); + fillRect(Common::Rect(0, 0, _surface.w, _surface.h), 0); +} + +void Screen::fadeIn(const byte palette[PALETTE_SIZE], int speed) { + int count = 50; + while (equalizePalette(palette) && --count) { + _vm->_events->delay(15 * speed); + } + + setPalette(palette); +} + +void Screen::addDirtyRect(const Common::Rect &r) { + _dirtyRects.push_back(r); + assert(r.width() > 0 && r.height() > 0); +} + +void Screen::mergeDirtyRects() { + Common::List<Common::Rect>::iterator rOuter, rInner; + + // Process the dirty rect list to find any rects to merge + for (rOuter = _dirtyRects.begin(); rOuter != _dirtyRects.end(); ++rOuter) { + rInner = rOuter; + while (++rInner != _dirtyRects.end()) { + + if ((*rOuter).intersects(*rInner)) { + // these two rectangles overlap or + // are next to each other - merge them + + unionRectangle(*rOuter, *rOuter, *rInner); + + // remove the inner rect from the list + _dirtyRects.erase(rInner); + + // move back to beginning of list + rInner = rOuter; + } + } + } +} + +bool Screen::unionRectangle(Common::Rect &destRect, const Common::Rect &src1, const Common::Rect &src2) { + destRect = src1; + destRect.extend(src2); + + return !destRect.isEmpty(); +} + +void Screen::randomTransition() { + Events &events = *_vm->_events; + const int TRANSITION_MULTIPLIER = 0x15a4e35; + _dirtyRects.clear(); + + for (int idx = 0; idx <= 65535 && !_vm->shouldQuit(); ++idx) { + _transitionSeed = _transitionSeed * TRANSITION_MULTIPLIER + 1; + int offset = _transitionSeed & 0xFFFF; + + if (offset < (SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCREEN_HEIGHT)) + *((byte *)getPixels() + offset) = *((const byte *)_backBuffer->getPixels() + offset); + + if (idx != 0 && (idx % 300) == 0) { + // Ensure there's a full screen dirty rect for the next frame update + if (_dirtyRects.empty()) + addDirtyRect(Common::Rect(0, 0, _surface.w, _surface.h)); + + events.pollEvents(); + events.delay(1); + } + } + + // Make sure everything has been transferred + blitFrom(*_backBuffer); +} + +void Screen::verticalTransition() { + Events &events = *_vm->_events; + + byte table[SHERLOCK_SCREEN_WIDTH]; + Common::fill(&table[0], &table[SHERLOCK_SCREEN_WIDTH], 0); + + for (int yp = 0; yp < SHERLOCK_SCREEN_HEIGHT; ++yp) { + for (int xp = 0; xp < SHERLOCK_SCREEN_WIDTH; ++xp) { + int temp = (table[xp] >= 197) ? SHERLOCK_SCREEN_HEIGHT - table[xp] : + _vm->getRandomNumber(3) + 1; + + if (temp) { + blitFrom(_backBuffer1, Common::Point(xp, table[xp]), + Common::Rect(xp, table[xp], xp + 1, table[xp] + temp)); + table[xp] += temp; + } + } + + events.delay(10); + } +} + +void Screen::restoreBackground(const Common::Rect &r) { + if (r.width() > 0 && r.height() > 0) { + Common::Rect tempRect = r; + tempRect.clip(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + + if (tempRect.isValidRect()) + _backBuffer1.blitFrom(_backBuffer2, Common::Point(tempRect.left, tempRect.top), tempRect); + } +} + +void Screen::slamArea(int16 xp, int16 yp, int16 width, int16 height) { + slamRect(Common::Rect(xp, yp, xp + width, yp + height)); +} + +void Screen::slamRect(const Common::Rect &r) { + if (r.width() && r.height() > 0) { + Common::Rect tempRect = r; + tempRect.clip(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + if (tempRect.isValidRect()) + blitFrom(*_backBuffer, Common::Point(tempRect.left, tempRect.top), tempRect); + } +} + +void Screen::flushImage(ImageFrame *frame, const Common::Point &pt, + int16 *xp, int16 *yp, int16 *width, int16 *height) { + Common::Point imgPos = pt + frame->_offset; + Common::Rect newBounds(imgPos.x, imgPos.y, imgPos.x + frame->_frame.w, imgPos.y + frame->_frame.h); + Common::Rect oldBounds(*xp, *yp, *xp + *width, *yp + *height); + + // See if the areas of the old and new overlap, and if so combine the areas + if (newBounds.intersects(oldBounds)) { + Common::Rect mergedBounds = newBounds; + mergedBounds.extend(oldBounds); + mergedBounds.right += 1; + mergedBounds.bottom += 1; + + slamRect(mergedBounds); + } else { + // The two areas are independent, so copy them both + slamRect(newBounds); + slamRect(oldBounds); + } + + *xp = newBounds.left; + *yp = newBounds.top; + *width = newBounds.width(); + *height = newBounds.height(); +} + +void Screen::print(const Common::Point &pt, byte color, const char *formatStr, ...) { + // Create the string to display + va_list args; + va_start(args, formatStr); + Common::String str = Common::String::vformat(formatStr, args); + va_end(args); + + // Figure out area to draw text in + Common::Point pos = pt; + int width = stringWidth(str); + pos.y--; // Font is always drawing one line higher + if (!pos.x) + // Center text horizontally + pos.x = (SHERLOCK_SCREEN_WIDTH - width) / 2; + + Common::Rect textBounds(pos.x, pos.y, pos.x + width, pos.y + _fontHeight); + if (textBounds.right > SHERLOCK_SCREEN_WIDTH) + textBounds.moveTo(SHERLOCK_SCREEN_WIDTH - width, textBounds.top); + if (textBounds.bottom > SHERLOCK_SCREEN_HEIGHT) + textBounds.moveTo(textBounds.left, SHERLOCK_SCREEN_HEIGHT - _fontHeight); + + // Write out the string at the given position + writeString(str, Common::Point(textBounds.left, textBounds.top), color); + + // Copy the affected area to the screen + slamRect(textBounds); +} + +void Screen::gPrint(const Common::Point &pt, byte color, const char *formatStr, ...) { + // Create the string to display + va_list args; + va_start(args, formatStr); + Common::String str = Common::String::vformat(formatStr, args); + va_end(args); + + // Print the text + writeString(str, pt, color); +} + +int Screen::stringWidth(const Common::String &str) { + int width = 0; + + for (const char *c = str.c_str(); *c; ++c) + width += charWidth(*c); + + return width; +} + +int Screen::charWidth(char c) { + if (c == ' ') + return 5; + else if (Common::isPrint(c)) + return (*_font)[c - 33]._frame.w + 1; + else + return 0; +} + +void Screen::writeString(const Common::String &str, const Common::Point &pt, byte color) { + Common::Point charPos = pt; + + for (const char *c = str.c_str(); *c; ++c) { + if (*c == ' ') + charPos.x += 5; + else { + assert(Common::isPrint(*c)); + ImageFrame &frame = (*_font)[*c - 33]; + _backBuffer->transBlitFrom(frame, charPos, false, color); + charPos.x += frame._frame.w + 1; + } + } +} + +void Screen::vgaBar(const Common::Rect &r, int color) { + _backBuffer->fillRect(r, color); + slamRect(r); +} + +void Screen::makeButton(const Common::Rect &bounds, int textX, + const Common::String &str) { + + Surface &bb = *_backBuffer; + bb.fillRect(Common::Rect(bounds.left, bounds.top, bounds.right, bounds.top + 1), BUTTON_TOP); + bb.fillRect(Common::Rect(bounds.left, bounds.top, bounds.left + 1, bounds.bottom), BUTTON_TOP); + bb.fillRect(Common::Rect(bounds.right - 1, bounds.top, bounds.right, bounds.bottom), BUTTON_BOTTOM); + bb.fillRect(Common::Rect(bounds.left + 1, bounds.bottom - 1, bounds.right, bounds.bottom), BUTTON_BOTTOM); + bb.fillRect(Common::Rect(bounds.left + 1, bounds.top + 1, bounds.right - 1, bounds.bottom - 1), BUTTON_MIDDLE); + + gPrint(Common::Point(textX, bounds.top), COMMAND_HIGHLIGHTED, "%c", str[0]); + gPrint(Common::Point(textX + charWidth(str[0]), bounds.top), + COMMAND_FOREGROUND, "%s", str.c_str() + 1); +} + +void Screen::buttonPrint(const Common::Point &pt, byte color, bool slamIt, + const Common::String &str) { + int xStart = pt.x - stringWidth(str) / 2; + + if (color == COMMAND_FOREGROUND) { + // First character needs to be highlighted + if (slamIt) { + print(Common::Point(xStart, pt.y + 1), COMMAND_HIGHLIGHTED, "%c", str[0]); + print(Common::Point(xStart + charWidth(str[0]), pt.y + 1), + COMMAND_FOREGROUND, "%s", str.c_str() + 1); + } else { + gPrint(Common::Point(xStart, pt.y), COMMAND_HIGHLIGHTED, "%c", str[0]); + gPrint(Common::Point(xStart + charWidth(str[0]), pt.y), + COMMAND_FOREGROUND, "%s", str.c_str() + 1); + } + } else if (slamIt) { + print(Common::Point(xStart, pt.y + 1), color, "%s", str.c_str()); + } else { + gPrint(Common::Point(xStart, pt.y), color, "%s", str.c_str()); + } +} + +void Screen::makePanel(const Common::Rect &r) { + _backBuffer->fillRect(r, BUTTON_MIDDLE); + _backBuffer->hLine(r.left, r.top, r.right - 2, BUTTON_TOP); + _backBuffer->hLine(r.left + 1, r.top + 1, r.right - 3, BUTTON_TOP); + _backBuffer->vLine(r.left, r.top, r.bottom - 1, BUTTON_TOP); + _backBuffer->vLine(r.left + 1, r.top + 1, r.bottom - 2, BUTTON_TOP); + + _backBuffer->vLine(r.right - 1, r.top, r.bottom - 1, BUTTON_BOTTOM); + _backBuffer->vLine(r.right - 2, r.top + 1, r.bottom - 2, BUTTON_BOTTOM); + _backBuffer->hLine(r.left, r.bottom - 1, r.right - 1, BUTTON_BOTTOM); + _backBuffer->hLine(r.left + 1, r.bottom - 2, r.right - 1, BUTTON_BOTTOM); +} + +void Screen::makeField(const Common::Rect &r) { + _backBuffer->fillRect(r, BUTTON_MIDDLE); + _backBuffer->hLine(r.left, r.top, r.right - 1, BUTTON_BOTTOM); + _backBuffer->hLine(r.left + 1, r.bottom - 1, r.right - 1, BUTTON_TOP); + _backBuffer->vLine(r.left, r.top + 1, r.bottom - 1, BUTTON_BOTTOM); + _backBuffer->vLine(r.right - 1, r.top + 1, r.bottom - 2, BUTTON_TOP); +} + +void Screen::setDisplayBounds(const Common::Rect &r) { + assert(r.left == 0 && r.top == 0); + _sceneSurface.setPixels(_backBuffer1.getPixels(), r.width(), r.height()); + + _backBuffer = &_sceneSurface; +} + +void Screen::resetDisplayBounds() { + _backBuffer = &_backBuffer1; +} + +Common::Rect Screen::getDisplayBounds() { + return (_backBuffer == &_sceneSurface) ? Common::Rect(0, 0, _sceneSurface.w(), _sceneSurface.h()) : + Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); +} + +void Screen::synchronize(Common::Serializer &s) { + int fontNumb = _fontNumber; + s.syncAsByte(fontNumb); + if (s.isLoading()) + setFont(fontNumb); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/screen.h b/engines/sherlock/screen.h new file mode 100644 index 0000000000..a2c0aa3c84 --- /dev/null +++ b/engines/sherlock/screen.h @@ -0,0 +1,239 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SCREEN_H +#define SHERLOCK_SCREEN_H + +#include "common/list.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "sherlock/surface.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +#define PALETTE_SIZE 768 +#define PALETTE_COUNT 256 +#define VGA_COLOR_TRANS(x) ((x) * 255 / 63) + +enum { + INFO_BLACK = 1, + INFO_FOREGROUND = 11, + INFO_BACKGROUND = 1, + BORDER_COLOR = 237, + INV_FOREGROUND = 14, + INV_BACKGROUND = 1, + COMMAND_HIGHLIGHTED = 10, + COMMAND_FOREGROUND = 15, + COMMAND_BACKGROUND = 4, + COMMAND_NULL = 248, + BUTTON_TOP = 233, + BUTTON_MIDDLE = 244, + BUTTON_BOTTOM = 248, + TALK_FOREGROUND = 12, + TALK_NULL = 16, + PEN_COLOR = 250 +}; + +class SherlockEngine; + +class Screen : public Surface { +private: + SherlockEngine *_vm; + int _fontNumber; + Common::List<Common::Rect> _dirtyRects; + uint32 _transitionSeed; + ImageFile *_font; + int _fontHeight; + Surface _sceneSurface; + + /** + * Merges together overlapping dirty areas of the screen + */ + void mergeDirtyRects(); + + /** + * Returns the union of two dirty area rectangles + */ + bool unionRectangle(Common::Rect &destRect, const Common::Rect &src1, const Common::Rect &src2); + + /** + * Draws the given string into the back buffer using the images stored in _font + */ + void writeString(const Common::String &str, const Common::Point &pt, byte color); +protected: + /** + * Adds a rectangle to the list of modified areas of the screen during the + * current frame + */ + virtual void addDirtyRect(const Common::Rect &r); +public: + Surface _backBuffer1, _backBuffer2; + Surface *_backBuffer; + bool _fadeStyle; + byte _cMap[PALETTE_SIZE]; + byte _sMap[PALETTE_SIZE]; +public: + Screen(SherlockEngine *vm); + virtual ~Screen(); + + /** + * Set the font to use for writing text on the screen + */ + void setFont(int fontNumber); + + /** + * Handles updating any dirty areas of the screen Surface object to the physical screen + */ + void update(); + + /** + * Return the currently active palette + */ + void getPalette(byte palette[PALETTE_SIZE]); + + /** + * Set the palette + */ + void setPalette(const byte palette[PALETTE_SIZE]); + + /** + * Fades from the currently active palette to the passed palette + */ + int equalizePalette(const byte palette[PALETTE_SIZE]); + + /** + * Fade out the palette to black + */ + void fadeToBlack(int speed = 2); + + /** + * Fade in a given palette + */ + void fadeIn(const byte palette[PALETTE_SIZE], int speed = 2); + + /** + * Do a random pixel transition in from _backBuffer surface to the screen + */ + void randomTransition(); + + /** + * Transition to the surface from _backBuffer using a vertical transition + */ + void verticalTransition(); + + /** + * Prints the text passed onto the back buffer at the given position and color. + * The string is then blitted to the screen + */ + void print(const Common::Point &pt, byte color, const char *formatStr, ...) GCC_PRINTF(4, 5); + + /** + * Print a strings onto the back buffer without blitting it to the screen + */ + void gPrint(const Common::Point &pt, byte color, const char *formatStr, ...) GCC_PRINTF(4, 5); + + /** + * Copies a section of the second back buffer into the main back buffer + */ + void restoreBackground(const Common::Rect &r); + + /** + * Copies a given area to the screen + */ + void slamArea(int16 xp, int16 yp, int16 width, int16 height); + + /** + * Copies a given area to the screen + */ + void slamRect(const Common::Rect &r); + + /** + * Copy an image from the back buffer to the screen, taking care of both the + * new area covered by the shape as well as the old area, which must be restored + */ + void flushImage(ImageFrame *frame, const Common::Point &pt, + int16 *xp, int16 *yp, int16 *width, int16 *height); + + /** + * Returns the width of a string in pixels + */ + int stringWidth(const Common::String &str); + + /** + * Returns the width of a character in pixels + */ + int charWidth(char c); + + /** + * Fills an area on the back buffer, and then copies it to the screen + */ + void vgaBar(const Common::Rect &r, int color); + + /** + * Draws a button for use in the inventory, talk, and examine dialogs. + */ + void makeButton(const Common::Rect &bounds, int textX, const Common::String &str); + + /** + * Prints an interface command with the first letter highlighted to indicate + * what keyboard shortcut is associated with it + */ + void buttonPrint(const Common::Point &pt, byte color, bool slamIt, const Common::String &str); + + /** + * Draw a panel in the back buffer with a raised area effect around the edges + */ + void makePanel(const Common::Rect &r); + + /** + * Draw a field in the back buffer with a raised area effect around the edges, + * suitable for text input. + */ + void makeField(const Common::Rect &r); + + /** + * Sets the active back buffer pointer to a restricted sub-area of the first back buffer + */ + void setDisplayBounds(const Common::Rect &r); + + /** + * Resets the active buffer pointer to point back to the full first back buffer + */ + void resetDisplayBounds(); + + /** + * Return the size of the current display window + */ + Common::Rect getDisplayBounds(); + + int fontNumber() const { return _fontNumber; } + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/settings.cpp b/engines/sherlock/settings.cpp new file mode 100644 index 0000000000..b26acde615 --- /dev/null +++ b/engines/sherlock/settings.cpp @@ -0,0 +1,336 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/sherlock.h" +#include "sherlock/settings.h" + +namespace Sherlock { + +static const int SETUP_POINTS[12][4] = { + { 4, 154, 101, 53 }, // Exit + { 4, 165, 101, 53 }, // Music Toggle + { 219, 165, 316, 268 }, // Voice Toggle + { 103, 165, 217, 160 }, // Sound Effects Toggle + { 219, 154, 316, 268 }, // Help Button Left/Right + { 103, 154, 217, 160 }, // New Font Style + { 4, 187, 101, 53 }, // Joystick Toggle + { 103, 187, 217, 160 }, // Calibrate Joystick + { 219, 176, 316, 268 }, // Fade Style + { 103, 176, 217, 160 }, // Window Open Style + { 4, 176, 101, 53 }, // Portraits Toggle + { 219, 187, 316, 268 } // _key Pad Accel. Toggle +}; + +static const char *const SETUP_STRS0[2] = { "off", "on" }; +static const char *const SETUP_STRS1[2] = { "Directly", "by Pixel" }; +static const char *const SETUP_STRS2[2] = { "Left", "Right" }; +static const char *const SETUP_STRS3[2] = { "Appear", "Slide" }; +static const char *const SETUP_STRS4[2] = { "Slow", "Fast" }; +static const char *const SETUP_STRS5[2] = { "Left", "Right" }; +static const char *const SETUP_NAMES[12] = { + "Exit", "M", "V", "S", "B", "New Font Style", "J", "Calibrate Joystick", "F", "W", "P", "K" +}; + +/*----------------------------------------------------------------*/ + +void Settings::drawInteface(bool flag) { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + Common::String tempStr; + + if (!flag) { + screen._backBuffer1.fillRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y1 + 1), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(0, CONTROLS_Y1 + 1, 2, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(SHERLOCK_SCREEN_WIDTH - 2, CONTROLS_Y1 + 1, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + screen._backBuffer1.hLine(0, SHERLOCK_SCREEN_HEIGHT - 1, SHERLOCK_SCREEN_WIDTH - 1, BORDER_COLOR); + screen._backBuffer1.fillRect(Common::Rect(2, CONTROLS_Y1 + 1, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + } + + screen.makeButton(Common::Rect(SETUP_POINTS[0][0], SETUP_POINTS[0][1], SETUP_POINTS[0][2], SETUP_POINTS[0][1] + 10), + SETUP_POINTS[0][3] - screen.stringWidth("Exit") / 2, "Exit"); + + tempStr = Common::String::format("Music %s", SETUP_STRS0[sound._music]); + screen.makeButton(Common::Rect(SETUP_POINTS[1][0], SETUP_POINTS[1][1], SETUP_POINTS[1][2], SETUP_POINTS[1][1] + 10), + SETUP_POINTS[1][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = Common::String::format("Voices %s", SETUP_STRS0[sound._voices]); + screen.makeButton(Common::Rect(SETUP_POINTS[2][0], SETUP_POINTS[2][1], SETUP_POINTS[2][2], SETUP_POINTS[2][1] + 10), + SETUP_POINTS[2][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = Common::String::format("Sound Effects %s", SETUP_STRS0[sound._digitized]); + screen.makeButton(Common::Rect(SETUP_POINTS[3][0], SETUP_POINTS[3][1], SETUP_POINTS[3][2], SETUP_POINTS[3][1] + 10), + SETUP_POINTS[3][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = Common::String::format("Auto Help %s", SETUP_STRS5[ui._helpStyle]); + screen.makeButton(Common::Rect(SETUP_POINTS[4][0], SETUP_POINTS[4][1], SETUP_POINTS[4][2], SETUP_POINTS[4][1] + 10), + SETUP_POINTS[4][3] - screen.stringWidth(tempStr) / 2, tempStr); + screen.makeButton(Common::Rect(SETUP_POINTS[5][0], SETUP_POINTS[5][1], SETUP_POINTS[5][2], SETUP_POINTS[5][1] + 10), + SETUP_POINTS[5][3] - screen.stringWidth("New Font Style") / 2, "New Font Style"); + + // WORKAROUND: We don't support the joystick in ScummVM, so draw the next two buttons as disabled + tempStr = "Joystick Off"; + screen.makeButton(Common::Rect(SETUP_POINTS[6][0], SETUP_POINTS[6][1], SETUP_POINTS[6][2], SETUP_POINTS[6][1] + 10), + SETUP_POINTS[6][3] - screen.stringWidth(tempStr) / 2, tempStr); + screen.buttonPrint(Common::Point(SETUP_POINTS[6][3], SETUP_POINTS[6][1]), COMMAND_NULL, false, tempStr); + + tempStr = "Calibrate Joystick"; + screen.makeButton(Common::Rect(SETUP_POINTS[7][0], SETUP_POINTS[7][1], SETUP_POINTS[7][2], SETUP_POINTS[7][1] + 10), + SETUP_POINTS[7][3] - screen.stringWidth(tempStr) / 2, tempStr); + screen.buttonPrint(Common::Point(SETUP_POINTS[7][3], SETUP_POINTS[7][1]), COMMAND_NULL, false, tempStr); + + tempStr = Common::String::format("Fade %s", screen._fadeStyle ? "by Pixel" : "Directly"); + screen.makeButton(Common::Rect(SETUP_POINTS[8][0], SETUP_POINTS[8][1], SETUP_POINTS[8][2], SETUP_POINTS[8][1] + 10), + SETUP_POINTS[8][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = Common::String::format("Windows %s", ui._slideWindows ? "Slide" : "Appear"); + screen.makeButton(Common::Rect(SETUP_POINTS[9][0], SETUP_POINTS[9][1], SETUP_POINTS[9][2], SETUP_POINTS[9][1] + 10), + SETUP_POINTS[9][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = Common::String::format("Portraits %s", SETUP_STRS0[people._portraitsOn]); + screen.makeButton(Common::Rect(SETUP_POINTS[10][0], SETUP_POINTS[10][1], SETUP_POINTS[10][2], SETUP_POINTS[10][1] + 10), + SETUP_POINTS[10][3] - screen.stringWidth(tempStr) / 2, tempStr); + + tempStr = "Key Pad Slow"; + screen.makeButton(Common::Rect(SETUP_POINTS[11][0], SETUP_POINTS[11][1], SETUP_POINTS[11][2], SETUP_POINTS[11][1] + 10), + SETUP_POINTS[11][3] - screen.stringWidth(tempStr) / 2, tempStr); + screen.buttonPrint(Common::Point(SETUP_POINTS[11][3], SETUP_POINTS[11][1]), COMMAND_NULL, false, tempStr); + + // Show the window immediately, or slide it on-screen + if (!flag) { + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(true, CONTROLS_Y1); + } + + ui._windowOpen = true; + } else { + screen.slamRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } +} + +int Settings::drawButtons(const Common::Point &pt, int _key) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + int found = -1; + byte color; + Common::String tempStr; + + for (int idx = 0; idx < 12; ++idx) { + if ((pt.x > SETUP_POINTS[idx][0] && pt.x < SETUP_POINTS[idx][2] && pt.y > SETUP_POINTS[idx][1] + && pt.y < (SETUP_POINTS[idx][1] + 10) && (events._released || events._released)) + || (_key == SETUP_NAMES[idx][0])) { + found = idx; + color = COMMAND_HIGHLIGHTED; + } else { + color = COMMAND_FOREGROUND; + } + + // Print the button text + switch (idx) { + case 1: + tempStr = Common::String::format("Music %s", SETUP_STRS0[sound._music]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 2: + tempStr = Common::String::format("Voices %s", SETUP_STRS0[sound._voices]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 3: + tempStr = Common::String::format("Sound Effects %s", SETUP_STRS0[sound._digitized]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 4: + tempStr = Common::String::format("Auto Help %s", SETUP_STRS2[ui._helpStyle]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 6: + tempStr = "Joystick Off"; + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), COMMAND_NULL, true, tempStr); + break; + case 7: + tempStr = "Calibrate Joystick"; + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), COMMAND_NULL, true, tempStr); + break; + case 8: + tempStr = Common::String::format("Fade %s", SETUP_STRS1[screen._fadeStyle]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 9: + tempStr = Common::String::format("Windows %s", SETUP_STRS3[ui._slideWindows]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 10: + tempStr = Common::String::format("Portraits %s", SETUP_STRS0[people._portraitsOn]); + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, tempStr); + break; + case 11: + tempStr = "Key Pad Slow"; + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), COMMAND_NULL, true, tempStr); + break; + default: + screen.buttonPrint(Common::Point(SETUP_POINTS[idx][3], SETUP_POINTS[idx][1]), color, true, SETUP_NAMES[idx]); + break; + } + } + + return found; +} + +void Settings::show(SherlockEngine *vm) { + Events &events = *vm->_events; + People &people = *vm->_people; + Scene &scene = *vm->_scene; + Screen &screen = *vm->_screen; + Sound &sound = *vm->_sound; + Talk &talk = *vm->_talk; + UserInterface &ui = *vm->_ui; + bool updateConfig = false; + + Settings settings(vm); + settings.drawInteface(false); + + do { + if (ui._menuCounter) + ui.whileMenuCounter(); + + int found = -1; + ui._key = -1; + + scene.doBgAnim(); + if (talk._talkToAbort) + return; + + events.setButtonState(); + Common::Point pt = events.mousePos(); + + if (events._pressed || events._released || events.kbHit()) { + ui.clearInfo(); + ui._key = -1; + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + ui._key = toupper(keyState.keycode); + + if (ui._key == Common::KEYCODE_RETURN || ui._key == Common::KEYCODE_SPACE) { + events._pressed = false; + events._oldButtons = 0; + ui._keyPress = '\0'; + events._released = true; + } + } + + // Handle highlighting button under mouse + found = settings.drawButtons(pt, ui._key); + } + + if ((found == 0 && events._released) || (ui._key == 'E' || ui._key == Common::KEYCODE_ESCAPE)) + // Exit + break; + + if ((found == 1 && events._released) || ui._key == 'M') { + // Toggle music + if (sound._music) { + sound.stopSound(); + sound._music = false; + } + else { + sound._music = true; + sound.startSong(); + } + + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 2 && events._released) || ui._key == 'V') { + sound._voices = !sound._voices; + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 3 && events._released) || ui._key == 'S') { + // Toggle sound effects + sound._digitized = !sound._digitized; + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 4 && events._released) || ui._key == 'A') { + // Help button style + ui._helpStyle = !ui._helpStyle; + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 5 && events._released) || ui._key == 'N') { + // New font style + int fontNum = screen.fontNumber() + 1; + if (fontNum == 3) + fontNum = 0; + + screen.setFont(fontNum); + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 8 && events._released) || ui._key == 'F') { + // Toggle fade style + screen._fadeStyle = !screen._fadeStyle; + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 9 && events._released) || ui._key == 'W') { + // Window style + ui._slideWindows = !ui._slideWindows; + updateConfig = true; + settings.drawInteface(true); + } + + if ((found == 10 && events._released) || ui._key == 'P') { + // Toggle portraits being shown + people._portraitsOn = !people._portraitsOn; + updateConfig = true; + settings.drawInteface(true); + } + } while (!vm->shouldQuit()); + + ui.banishWindow(); + + if (updateConfig) + vm->saveConfig(); + + ui._keyPress = '\0'; + ui._keyboardInput = false; + ui._windowBounds.top = CONTROLS_Y1; + ui._key = -1; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/settings.h b/engines/sherlock/settings.h new file mode 100644 index 0000000000..fc5fb2959f --- /dev/null +++ b/engines/sherlock/settings.h @@ -0,0 +1,60 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SETTINGS_H +#define SHERLOCK_SETTINGS_H + +#include "common/scummsys.h" + +namespace Sherlock { + +class SherlockEngine; +class UserInterface; + +class Settings { +private: + SherlockEngine *_vm; + + Settings(SherlockEngine *vm) : _vm(vm) {} + + /** + * Draws the interface for the settings window + */ + void drawInteface(bool flag); + + /** + * Draws the buttons for the settings dialog + */ + int drawButtons(const Common::Point &pt, int key); +public: + /** + * Handles input when the settings window is being shown + * @remarks Whilst this would in theory be better in the Journal class, since it displays in + * the user interface, it uses so many internal UI fields, that it sort of made some sense + * to put it in the UserInterface class. + */ + static void show(SherlockEngine *vm); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/sherlock.cpp b/engines/sherlock/sherlock.cpp new file mode 100644 index 0000000000..d4644d4d9d --- /dev/null +++ b/engines/sherlock/sherlock.cpp @@ -0,0 +1,263 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/sherlock.h" +#include "sherlock/surface.h" +#include "common/scummsys.h" +#include "common/config-manager.h" +#include "common/debug-channels.h" +#include "engines/util.h" + +namespace Sherlock { + +SherlockEngine::SherlockEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + Engine(syst), _gameDescription(gameDesc), _randomSource("Sherlock") { + _animation = nullptr; + _debugger = nullptr; + _events = nullptr; + _inventory = nullptr; + _journal = nullptr; + _map = nullptr; + _people = nullptr; + _res = nullptr; + _saves = nullptr; + _scene = nullptr; + _screen = nullptr; + _sound = nullptr; + _talk = nullptr; + _ui = nullptr; + _useEpilogue2 = false; + _loadGameSlot = -1; + _canLoadSave = false; + _showOriginalSavesDialog = false; + _interactiveFl = true; +} + +SherlockEngine::~SherlockEngine() { + delete _animation; + delete _debugger; + delete _events; + delete _journal; + delete _map; + delete _people; + delete _saves; + delete _scene; + delete _screen; + delete _sound; + delete _talk; + delete _ui; + delete _inventory; + delete _res; +} + +void SherlockEngine::initialize() { + initGraphics(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT, false); + + DebugMan.addDebugChannel(kDebugScript, "scripts", "Script debug level"); + + ImageFile::setVm(this); + Object::setVm(this); + Sprite::setVm(this); + + if (isDemo()) { + Common::File f; + // The interactive demo doesn't have an intro thus doesn't include TITLE.SND + // At the opposite, the non-interactive demo is only the intro. + if (f.exists("TITLE.SND")) + _interactiveFl = false; + } + + _res = new Resources(this); + _animation = new Animation(this); + _debugger = new Debugger(this); + _events = new Events(this); + _inventory = new Inventory(this); + _map = new Map(this); + _journal = new Journal(this); + _people = new People(this); + _saves = new SaveManager(this, _targetName); + _scene = new Scene(this); + _screen = new Screen(this); + _sound = new Sound(this, _mixer); + _talk = new Talk(this); + _ui = new UserInterface(this); + + // Load game settings + loadConfig(); +} + +Common::Error SherlockEngine::run() { + // Initialize the engine + initialize(); + + // Flag for whether to show original saves dialog rather than the ScummVM GMM + _showOriginalSavesDialog = ConfMan.getBool("originalsaveload"); + + // If requested, load a savegame instead of showing the intro + if (ConfMan.hasKey("save_slot")) { + int saveSlot = ConfMan.getInt("save_slot"); + if (saveSlot >= 1 && saveSlot <= MAX_SAVEGAME_SLOTS) + _loadGameSlot = saveSlot; + } + + if (_loadGameSlot != -1) { + _saves->loadGame(_loadGameSlot); + _loadGameSlot = -1; + } else { + do + showOpening(); + while (!shouldQuit() && !_interactiveFl); + } + + while (!shouldQuit()) { + // Prepare for scene, and handle any game-specific scenes. This allows + // for game specific cutscenes or mini-games that aren't standard scenes + startScene(); + if (shouldQuit()) + break; + + // Clear the screen + _screen->clear(); + + // Reset UI flags + _ui->reset(); + + // Reset the data for the player character (Sherlock) + _people->reset(); + + // Initialize and load the scene. + _scene->selectScene(); + + // Scene handling loop + sceneLoop(); + } + + return Common::kNoError; +} + +void SherlockEngine::sceneLoop() { + while (!shouldQuit() && _scene->_goToScene == -1) { + // See if a script needs to be completed from either a goto room code, + // or a script that was interrupted by another script + if (_talk->_scriptMoreFlag == 1 || _talk->_scriptMoreFlag == 3) + _talk->talkTo(_talk->_scriptName); + else + _talk->_scriptMoreFlag = 0; + + // Handle any input from the keyboard or mouse + handleInput(); + + if (_people->_hSavedPos.x == -1) { + _canLoadSave = true; + _scene->doBgAnim(); + _canLoadSave = false; + } + } + + _scene->freeScene(); + _people->freeWalk(); + +} + +void SherlockEngine::handleInput() { + _canLoadSave = true; + _events->pollEventsAndWait(); + _canLoadSave = false; + + // See if a key or mouse button is pressed + _events->setButtonState(); + + _ui->handleInput(); +} + +bool SherlockEngine::readFlags(int flagNum) { + bool value = _flags[ABS(flagNum)]; + if (flagNum < 0) + value = !value; + + return value; +} + +void SherlockEngine::setFlags(int flagNum) { + _flags[ABS(flagNum)] = flagNum >= 0; + + _scene->checkSceneFlags(true); +} + +void SherlockEngine::loadConfig() { + // Load sound settings + syncSoundSettings(); + + ConfMan.registerDefault("font", 1); + + _screen->setFont(ConfMan.getInt("font")); + _screen->_fadeStyle = ConfMan.getBool("fade_style"); + _ui->_helpStyle = ConfMan.getBool("help_style"); + _ui->_slideWindows = ConfMan.getBool("window_style"); + _people->_portraitsOn = ConfMan.getBool("portraits_on"); +} + +void SherlockEngine::saveConfig() { + ConfMan.setBool("mute", !_sound->_digitized); + ConfMan.setBool("music_mute", !_sound->_music); + ConfMan.setBool("speech_mute", !_sound->_voices); + + ConfMan.setInt("font", _screen->fontNumber()); + ConfMan.setBool("fade_style", _screen->_fadeStyle); + ConfMan.setBool("help_style", _ui->_helpStyle); + ConfMan.setBool("window_style", _ui->_slideWindows); + ConfMan.setBool("portraits_on", _people->_portraitsOn); + + ConfMan.flushToDisk(); +} + +void SherlockEngine::syncSoundSettings() { + Engine::syncSoundSettings(); + + // Load sound-related settings + _sound->syncSoundSettings(); +} + +void SherlockEngine::synchronize(Common::Serializer &s) { + for (uint idx = 0; idx < _flags.size(); ++idx) + s.syncAsByte(_flags[idx]); +} + +bool SherlockEngine::canLoadGameStateCurrently() { + return _canLoadSave; +} + +bool SherlockEngine::canSaveGameStateCurrently() { + return _canLoadSave; +} + +Common::Error SherlockEngine::loadGameState(int slot) { + _saves->loadGame(slot); + return Common::kNoError; +} + +Common::Error SherlockEngine::saveGameState(int slot, const Common::String &desc) { + _saves->saveGame(slot, desc); + return Common::kNoError; +} + +} // End of namespace Comet diff --git a/engines/sherlock/sherlock.h b/engines/sherlock/sherlock.h new file mode 100644 index 0000000000..24a72076ef --- /dev/null +++ b/engines/sherlock/sherlock.h @@ -0,0 +1,202 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_HOLMES_H +#define SHERLOCK_HOLMES_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/endian.h" +#include "common/hash-str.h" +#include "common/serializer.h" +#include "common/random.h" +#include "common/savefile.h" +#include "common/util.h" +#include "engines/engine.h" +#include "sherlock/animation.h" +#include "sherlock/debugger.h" +#include "sherlock/events.h" +#include "sherlock/inventory.h" +#include "sherlock/journal.h" +#include "sherlock/map.h" +#include "sherlock/people.h" +#include "sherlock/resources.h" +#include "sherlock/saveload.h" +#include "sherlock/scene.h" +#include "sherlock/screen.h" +#include "sherlock/sound.h" +#include "sherlock/talk.h" +#include "sherlock/user_interface.h" + +namespace Sherlock { + +enum { + kDebugScript = 1 << 0 +}; + +enum GameType { + GType_SerratedScalpel = 0, + GType_RoseTattoo = 1 +}; + +#define SHERLOCK_SCREEN_WIDTH 320 +#define SHERLOCK_SCREEN_HEIGHT 200 +#define SHERLOCK_SCENE_HEIGHT 138 + +struct SherlockGameDescription; + +class Resource; + +class SherlockEngine : public Engine { +private: + /** + * Main loop for displaying a scene and handling all that occurs within it + */ + void sceneLoop(); + + /** + * Handle all player input + */ + void handleInput(); + + /** + * Load game configuration esttings + */ + void loadConfig(); +protected: + /** + * Does basic initialization of the game engine + */ + virtual void initialize(); + + virtual void showOpening() = 0; + + virtual void startScene() {} + + /** + * Returns a list of features the game itself supports + */ + virtual bool hasFeature(EngineFeature f) const; +public: + const SherlockGameDescription *_gameDescription; + Animation *_animation; + Debugger *_debugger; + Events *_events; + Inventory *_inventory; + Journal *_journal; + Map *_map; + People *_people; + Resources *_res; + SaveManager *_saves; + Scene *_scene; + Screen *_screen; + Sound *_sound; + Talk *_talk; + UserInterface *_ui; + Common::RandomSource _randomSource; + Common::Array<bool> _flags; + bool _useEpilogue2; + int _loadGameSlot; + bool _canLoadSave; + bool _showOriginalSavesDialog; + bool _interactiveFl; +public: + SherlockEngine(OSystem *syst, const SherlockGameDescription *gameDesc); + virtual ~SherlockEngine(); + + /** + * Main method for running the game + */ + virtual Common::Error run(); + + /** + * Returns true if a savegame can be loaded + */ + virtual bool canLoadGameStateCurrently(); + + /** + * Returns true if the game can be saved + */ + virtual bool canSaveGameStateCurrently(); + + /** + * Called by the GMM to load a savegame + */ + virtual Common::Error loadGameState(int slot); + + /** + * Called by the GMM to save the game + */ + virtual Common::Error saveGameState(int slot, const Common::String &desc); + + /** + * Called by the engine when sound settings are updated + */ + virtual void syncSoundSettings(); + + /** + * Returns whether the version is a demo + */ + virtual bool isDemo() const; + + /** + * Returns the Id of the game + */ + GameType getGameID() const; + + /** + * Returns the platform the game's datafiles are for + */ + Common::Platform getPlatform() const; + + /** + * Return a random number + */ + int getRandomNumber(int limit) { return _randomSource.getRandomNumber(limit - 1); } + + /** + * Read the state of a global flag + * @remarks If a negative value is specified, it will return the inverse value + * of the positive flag number + */ + bool readFlags(int flagNum); + + /** + * Sets a global flag to either true or false depending on whether the specified + * flag is positive or negative + */ + void setFlags(int flagNum); + + /** + * Saves game configuration information + */ + void saveConfig(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/sound.cpp b/engines/sherlock/sound.cpp new file mode 100644 index 0000000000..2ed6a1bbe8 --- /dev/null +++ b/engines/sherlock/sound.cpp @@ -0,0 +1,262 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/sherlock.h" +#include "sherlock/sound.h" +#include "common/config-manager.h" +#include "audio/audiostream.h" +#include "common/algorithm.h" +#include "audio/mixer.h" +#include "audio/decoders/raw.h" + +namespace Sherlock { + +static const int8 creativeADPCM_ScaleMap[64] = { + 0, 1, 2, 3, 4, 5, 6, 7, 0, -1, -2, -3, -4, -5, -6, -7, + 1, 3, 5, 7, 9, 11, 13, 15, -1, -3, -5, -7, -9, -11, -13, -15, + 2, 6, 10, 14, 18, 22, 26, 30, -2, -6, -10, -14, -18, -22, -26, -30, + 4, 12, 20, 28, 36, 44, 52, 60, -4, -12, -20, -28, -36, -44, -52, -60 +}; + +static const uint8 creativeADPCM_AdjustMap[64] = { + 0, 0, 0, 0, 0, 16, 16, 16, + 0, 0, 0, 0, 0, 16, 16, 16, + 240, 0, 0, 0, 0, 16, 16, 16, + 240, 0, 0, 0, 0, 16, 16, 16, + 240, 0, 0, 0, 0, 16, 16, 16, + 240, 0, 0, 0, 0, 16, 16, 16, + 240, 0, 0, 0, 0, 0, 0, 0, + 240, 0, 0, 0, 0, 0, 0, 0 +}; + +/*----------------------------------------------------------------*/ + +Sound::Sound(SherlockEngine *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) { + _digitized = false; + _music = false; + _voices = 0; + _diskSoundPlaying = false; + _soundPlaying = false; + _soundIsOn = &_soundPlaying; + _curPriority = 0; + + _soundOn = true; + _musicOn = true; + _speechOn = true; + + if (!_vm->_interactiveFl) + _vm->_res->addToCache("TITLE.SND"); + else { + _vm->_res->addToCache("MUSIC.LIB"); + _vm->_res->addToCache("SND.SND"); + + if (!_vm->isDemo()) { + _vm->_res->addToCache("TITLE.SND"); + _vm->_res->addToCache("EPILOGUE.SND"); + } + } +} + +void Sound::syncSoundSettings() { + _digitized = !ConfMan.getBool("mute"); + _music = !ConfMan.getBool("mute") && !ConfMan.getBool("music_mute"); + _voices = !ConfMan.getBool("mute") && !ConfMan.getBool("speech_mute") ? 1 : 0; +} + +void Sound::loadSound(const Common::String &name, int priority) { + // No implementation required in ScummVM + warning("loadSound"); +} + +byte Sound::decodeSample(byte sample, byte &reference, int16 &scale) { + int16 samp = sample + scale; + int16 ref = 0; + + // clip bad ADPCM-4 sample + CLIP<int16>(samp, 0, 63); + + ref = reference + creativeADPCM_ScaleMap[samp]; + if (ref > 0xff) { + reference = 0xff; + } else { + if (ref < 0x00) { + reference = 0; + } else { + reference = (uint8)(ref & 0xff); + } + } + + scale = (scale + creativeADPCM_AdjustMap[samp]) & 0xff; + return reference; +} + +bool Sound::playSound(const Common::String &name, WaitType waitType, int priority, const char *libraryFilename) { + Resources &res = *_vm->_res; + stopSound(); + + Common::String filename = name; + if (!filename.contains('.')) + filename += ".SND"; + + Common::String libFilename(libraryFilename); + Common::SeekableReadStream *stream = libFilename.empty() ? res.load(filename) : res.load(filename, libFilename); + + if (!stream) + error("Unable to find sound file '%s'", filename.c_str()); + + stream->skip(2); + int size = stream->readUint32BE(); + int rate = stream->readUint16BE(); + byte *data = (byte *)malloc(size); + byte *ptr = data; + stream->read(ptr, size); + + assert(size > 2); + + byte *decoded = (byte *)malloc((size - 1) * 2); + + // Holmes uses Creative ADPCM 4-bit data + int counter = 0; + byte reference = ptr[0]; + int16 scale = 0; + + for(int i = 1; i < size; i++) { + decoded[counter++] = decodeSample((ptr[i]>>4)&0x0f, reference, scale); + decoded[counter++] = decodeSample((ptr[i]>>0)&0x0f, reference, scale); + } + + free(data); + +#if 0 + // Debug : used to dump files + Common::DumpFile outFile; + outFile.open(filename); + outFile.write(decoded, (size - 2) * 2); + outFile.flush(); + outFile.close(); +#endif + + Audio::AudioStream *audioStream = Audio::makeRawStream(decoded, (size - 2) * 2, rate, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES); + _mixer->playStream(Audio::Mixer::kPlainSoundType, &_effectsHandle, audioStream, -1, Audio::Mixer::kMaxChannelVolume); + _soundPlaying = true; + _curPriority = priority; + + if (waitType == WAIT_RETURN_IMMEDIATELY) { + _diskSoundPlaying = true; + return true; + } + + bool retval = true; + do { + _vm->_events->pollEvents(); + g_system->delayMillis(10); + if ((waitType == WAIT_KBD_OR_FINISH) && _vm->_events->kbHit()) { + retval = false; + break; + } + } while (!_vm->shouldQuit() && _mixer->isSoundHandleActive(_effectsHandle)); + + _soundPlaying = false; + _mixer->stopHandle(_effectsHandle); + + return retval; +} + +void Sound::playLoadedSound(int bufNum, WaitType waitType) { + if (_mixer->isSoundHandleActive(_effectsHandle) && (_curPriority > _vm->_scene->_sounds[bufNum]._priority)) + return; + + stopSound(); + playSound(_vm->_scene->_sounds[bufNum]._name, waitType, _vm->_scene->_sounds[bufNum]._priority); + + return; +} + +void Sound::freeLoadedSounds() { + // As sounds are played with DisposeAfterUse::YES, stopping the sounds also + // frees them + stopSound(); +} + +void Sound::stopSound() { + _mixer->stopHandle(_effectsHandle); +} + +void Sound::playMusic(const Common::String &name) { + // TODO + warning("Sound::playMusic %s", name.c_str()); + Common::SeekableReadStream *stream = _vm->_res->load(name, "MUSIC.LIB"); + + byte *data = new byte[stream->size()]; + byte *ptr = data; + stream->read(ptr, stream->size()); + Common::DumpFile outFile; + outFile.open(name + ".RAW"); + outFile.write(data, stream->size()); + outFile.flush(); + outFile.close(); + delete[] data; + + stopMusic(); + startSong(); +} + +void Sound::stopMusic() { + // TODO + warning("TODO: Sound::stopMusic"); +} + +int Sound::loadSong(int songNumber) { + // TODO + warning("TODO: Sound::loadSong"); + return 0; +} + +void Sound::startSong() { + // TODO + warning("TODO: Sound::startSong"); +} + +void Sound::freeSong() { + // TODO + warning("TODO: Sound::freeSong"); +} + +void Sound::stopSndFuncPtr(int v1, int v2) { + // TODO + warning("TODO: Sound::stopSndFuncPtr"); +} + +void Sound::waitTimerRoland(uint time) { + // TODO + warning("TODO: Sound::waitTimerRoland"); +} + +void Sound::freeDigiSound() { + delete[] _digiBuf; + _digiBuf = nullptr; + _diskSoundPlaying = false; + _soundPlaying = false; +} + +} // End of namespace Sherlock + diff --git a/engines/sherlock/sound.h b/engines/sherlock/sound.h new file mode 100644 index 0000000000..689e615a36 --- /dev/null +++ b/engines/sherlock/sound.h @@ -0,0 +1,127 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_SOUND_H +#define SHERLOCK_SOUND_H + +#include "common/scummsys.h" +#include "common/str.h" +#include "audio/audiostream.h" +#include "audio/mixer.h" +#include "access/files.h" +#include "audio/midiplayer.h" +#include "audio/midiparser.h" + +namespace Sherlock { + +class SherlockEngine; + +enum WaitType { + WAIT_RETURN_IMMEDIATELY = 0, WAIT_FINISH = 1, WAIT_KBD_OR_FINISH = 2 +}; + +class Sound { +private: + SherlockEngine *_vm; + Audio::Mixer *_mixer; + Audio::SoundHandle _effectsHandle; + int _curPriority; + + byte decodeSample(byte sample, byte& reference, int16& scale); +public: + bool _digitized; + bool _music; + int _voices; + bool _soundOn; + bool _musicOn; + bool _speechOn; + bool _diskSoundPlaying; + bool _soundPlaying; + bool *_soundIsOn; + byte *_digiBuf; +public: + Sound(SherlockEngine *vm, Audio::Mixer *mixer); + + /** + * Saves sound-related settings + */ + void syncSoundSettings(); + + /** + * Load a sound + */ + void loadSound(const Common::String &name, int priority); + + /** + * Play the sound in the specified resource + */ + bool playSound(const Common::String &name, WaitType waitType, int priority = 100, const char *libraryFilename = nullptr); + + /** + * Play a previously loaded sound + */ + void playLoadedSound(int bufNum, WaitType waitType); + + /** + * Free any previously loaded sounds + */ + void freeLoadedSounds(); + + /** + * Stop playing any active sound + */ + void stopSound(); + + /** + * Load a specified song + */ + int loadSong(int songNumber); + + /** + * Start playing a song + */ + void startSong(); + + /** + * Free any currently loaded song + */ + void freeSong(); + + /** + * Play the specified music resource + */ + void playMusic(const Common::String &name); + + /** + * Stop playing the music + */ + void stopMusic(); + + void stopSndFuncPtr(int v1, int v2); + void waitTimerRoland(uint time); + void freeDigiSound(); +}; + +} // End of namespace Sherlock + +#endif + diff --git a/engines/sherlock/surface.cpp b/engines/sherlock/surface.cpp new file mode 100644 index 0000000000..80495a398c --- /dev/null +++ b/engines/sherlock/surface.cpp @@ -0,0 +1,189 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/surface.h" +#include "sherlock/sherlock.h" +#include "sherlock/resources.h" +#include "common/system.h" +#include "graphics/palette.h" + +namespace Sherlock { + +Surface::Surface(uint16 width, uint16 height) : _freePixels(true) { + create(width, height); +} + +Surface::Surface() : _freePixels(false) { +} + +Surface::~Surface() { + if (_freePixels) + _surface.free(); +} + +void Surface::create(uint16 width, uint16 height) { + if (_freePixels) + _surface.free(); + + _surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8()); + _freePixels = true; +} + +void Surface::blitFrom(const Surface &src) { + blitFrom(src, Common::Point(0, 0)); +} + +void Surface::blitFrom(const ImageFrame &src) { + blitFrom(src._frame, Common::Point(0, 0)); +} + +void Surface::blitFrom(const Graphics::Surface &src) { + blitFrom(src, Common::Point(0, 0)); +} + +void Surface::blitFrom(const Surface &src, const Common::Point &pt) { + blitFrom(src, pt, Common::Rect(0, 0, src._surface.w, src._surface.h)); +} + +void Surface::blitFrom(const ImageFrame &src, const Common::Point &pt) { + blitFrom(src._frame, pt, Common::Rect(0, 0, src._frame.w, src._frame.h)); +} + +void Surface::blitFrom(const Graphics::Surface &src, const Common::Point &pt) { + blitFrom(src, pt, Common::Rect(0, 0, src.w, src.h)); +} + +void Surface::blitFrom(const Graphics::Surface &src, const Common::Point &pt, const Common::Rect &srcBounds) { + Common::Rect srcRect = srcBounds; + Common::Rect destRect(pt.x, pt.y, pt.x + srcRect.width(), pt.y + srcRect.height()); + + if (srcRect.isValidRect() && clip(srcRect, destRect)) { + // Surface is at least partially or completely on-screen + addDirtyRect(destRect); + _surface.copyRectToSurface(src, destRect.left, destRect.top, srcRect); + } +} + +void Surface::blitFrom(const ImageFrame &src, const Common::Point &pt, const Common::Rect &srcBounds) { + blitFrom(src._frame, pt, srcBounds); +} + +void Surface::blitFrom(const Surface &src, const Common::Point &pt, const Common::Rect &srcBounds) { + blitFrom(src._surface, pt, srcBounds); +} + +void Surface::transBlitFrom(const ImageFrame &src, const Common::Point &pt, + bool flipped, int overrideColor) { + transBlitFrom(src._frame, pt + src._offset, flipped, overrideColor); +} + +void Surface::transBlitFrom(const Graphics::Surface &src, const Common::Point &pt, + bool flipped, int overrideColor) { + Common::Rect drawRect(0, 0, src.w, src.h); + Common::Rect destRect(pt.x, pt.y, pt.x + src.w, pt.y + src.h); + + // Clip the display area to on-screen + if (!clip(drawRect, destRect)) + // It's completely off-screen + return; + + if (flipped) + drawRect = Common::Rect(src.w - drawRect.right, src.h - drawRect.bottom, + src.w - drawRect.left, src.h - drawRect.top); + + Common::Point destPt(destRect.left, destRect.top); + addDirtyRect(Common::Rect(destPt.x, destPt.y, destPt.x + drawRect.width(), + destPt.y + drawRect.height())); + + // Draw loop + const int TRANSPARENCY = 0xFF; + for (int yp = 0; yp < drawRect.height(); ++yp) { + const byte *srcP = (const byte *)src.getBasePtr( + flipped ? drawRect.right - 1 : drawRect.left, drawRect.top + yp); + byte *destP = (byte *)getBasePtr(destPt.x, destPt.y + yp); + + for (int xp = 0; xp < drawRect.width(); ++xp, ++destP) { + if (*srcP != TRANSPARENCY) + *destP = overrideColor ? overrideColor : *srcP; + + srcP = flipped ? srcP - 1 : srcP + 1; + } + } +} + +void Surface::fillRect(int x1, int y1, int x2, int y2, byte color) { + fillRect(Common::Rect(x1, y1, x2, y2), color); +} + +void Surface::fillRect(const Common::Rect &r, byte color) { + _surface.fillRect(r, color); + addDirtyRect(r); +} + +bool Surface::clip(Common::Rect &srcBounds, Common::Rect &destBounds) { + if (destBounds.left >= _surface.w || destBounds.top >= _surface.h || + destBounds.right <= 0 || destBounds.bottom <= 0) + return false; + + // Clip the bounds if necessary to fit on-screen + if (destBounds.right > _surface.w) { + srcBounds.right -= destBounds.right - _surface.w; + destBounds.right = _surface.w; + } + + if (destBounds.bottom > _surface.h) { + srcBounds.bottom -= destBounds.bottom - _surface.h; + destBounds.bottom = _surface.h; + } + + if (destBounds.top < 0) { + srcBounds.top += -destBounds.top; + destBounds.top = 0; + } + + if (destBounds.left < 0) { + srcBounds.left += -destBounds.left; + destBounds.left = 0; + } + + return true; +} + +void Surface::clear() { + fillRect(Common::Rect(0, 0, _surface.w, _surface.h), 0); +} + +void Surface::free() { + if (_freePixels) { + _surface.free(); + _freePixels = false; + } +} + +void Surface::setPixels(byte *pixels, int width, int height) { + _surface.format = Graphics::PixelFormat::createFormatCLUT8(); + _surface.w = _surface.pitch = width; + _surface.h = height; + _surface.setPixels(pixels); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/surface.h b/engines/sherlock/surface.h new file mode 100644 index 0000000000..ccabf02a23 --- /dev/null +++ b/engines/sherlock/surface.h @@ -0,0 +1,156 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_GRAPHICS_H +#define SHERLOCK_GRAPHICS_H + +#include "common/rect.h" +#include "graphics/surface.h" + +namespace Sherlock { + +struct ImageFrame; + +class Surface { +private: + bool _freePixels; + + /** + * Clips the given source bounds so the passed destBounds will be entirely on-screen + */ + bool clip(Common::Rect &srcBounds, Common::Rect &destBounds); + + /** + * Copy a surface into this one + */ + void blitFrom(const Graphics::Surface &src); + + /** + * Draws a surface at a given position within this surface + */ + void blitFrom(const Graphics::Surface &src, const Common::Point &pt); + + /** + * Draws a sub-section of a surface at a given position within this surface + */ + void blitFrom(const Graphics::Surface &src, const Common::Point &pt, const Common::Rect &srcBounds); +protected: + Graphics::Surface _surface; + + virtual void addDirtyRect(const Common::Rect &r) {} +public: + Surface(uint16 width, uint16 height); + Surface(); + virtual ~Surface(); + + /** + * Sets up an internal surface with the specified dimensions that will be automatically freed + * when the surface object is destroyed + */ + void create(uint16 width, uint16 height); + + /** + * Copy a surface into this one + */ + void blitFrom(const Surface &src); + + /** + * Copy an image frame into this surface + */ + void blitFrom(const ImageFrame &src); + + /** + * Draws a surface at a given position within this surface + */ + void blitFrom(const Surface &src, const Common::Point &pt); + + /** + * Copy an image frame onto this surface at a given position + */ + void blitFrom(const ImageFrame &src, const Common::Point &pt); + + /** + * Draws a sub-section of a surface at a given position within this surface + */ + void blitFrom(const Surface &src, const Common::Point &pt, const Common::Rect &srcBounds); + + /** + * Copy a sub-area of a source image frame into this surface at a given position + */ + void blitFrom(const ImageFrame &src, const Common::Point &pt, const Common::Rect &srcBounds); + + /** + * Draws an image frame at a given position within this surface with transparency + */ + void transBlitFrom(const ImageFrame &src, const Common::Point &pt, + bool flipped = false, int overrideColor = 0); + + /** + * Draws a surface at a given position within this surface with transparency + */ + void transBlitFrom(const Surface &src, const Common::Point &pt, + bool flipped = false, int overrideColor = 0); + + /** + * Draws a surface at a given position within this surface with transparency + */ + void transBlitFrom(const Graphics::Surface &src, const Common::Point &pt, + bool flipped = false, int overrideColor = 0); + + /** + * Fill a given area of the surface with a given color + */ + void fillRect(int x1, int y1, int x2, int y2, byte color); + + /** + * Fill a given area of the surface with a given color + */ + void fillRect(const Common::Rect &r, byte color); + + /** + * Clear the screen + */ + void clear(); + + /** + * Free the underlying surface + */ + void free(); + + /** + * Set the pixels for the surface to an existing data block + */ + void setPixels(byte *pixels, int width, int height); + + inline uint16 w() const { return _surface.w; } + inline uint16 h() const { return _surface.h; } + inline const byte *getPixels() const { return (const byte *)_surface.getPixels(); } + inline byte *getPixels() { return (byte *)_surface.getPixels(); } + inline byte *getBasePtr(int x, int y) { return (byte *)_surface.getBasePtr(x, y); } + inline const byte *getBasePtr(int x, int y) const { return (const byte *)_surface.getBasePtr(x, y); } + inline void hLine(int x, int y, int x2, uint32 color) { _surface.hLine(x, y, x2, color); } + inline void vLine(int x, int y, int y2, uint32 color) { _surface.vLine(x, y, y2, color); } +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/talk.cpp b/engines/sherlock/talk.cpp new file mode 100644 index 0000000000..319d1a383f --- /dev/null +++ b/engines/sherlock/talk.cpp @@ -0,0 +1,1665 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/talk.h" +#include "sherlock/sherlock.h" +#include "sherlock/screen.h" + +namespace Sherlock { + +#define SPEAKER_REMOVE 0x80 + +SequenceEntry::SequenceEntry() { + _objNum = 0; + _frameNumber = 0; + _seqTo = 0; +} + +/*----------------------------------------------------------------*/ + +void Statement::synchronize(Common::SeekableReadStream &s) { + int length; + + length = s.readUint16LE(); + for (int idx = 0; idx < length - 1; ++idx) + _statement += (char)s.readByte(); + s.readByte(); // Null ending + + length = s.readUint16LE(); + for (int idx = 0; idx < length - 1; ++idx) + _reply += (char)s.readByte(); + s.readByte(); // Null ending + + length = s.readUint16LE(); + for (int idx = 0; idx < length - 1; ++idx) + _linkFile += (char)s.readByte(); + s.readByte(); // Null ending + + length = s.readUint16LE(); + for (int idx = 0; idx < length - 1; ++idx) + _voiceFile += (char)s.readByte(); + s.readByte(); // Null ending + + _required.resize(s.readByte()); + _modified.resize(s.readByte()); + + // Read in flag required/modified data + for (uint idx = 0; idx < _required.size(); ++idx) + _required[idx] = s.readSint16LE(); + for (uint idx = 0; idx < _modified.size(); ++idx) + _modified[idx] = s.readSint16LE(); + + _portraitSide = s.readByte(); + _quotient = s.readUint16LE(); +} + +/*----------------------------------------------------------------*/ + +TalkHistoryEntry::TalkHistoryEntry() { + Common::fill(&_data[0], &_data[16], false); +} + +/*----------------------------------------------------------------*/ + +TalkSequences::TalkSequences(const byte *data) { + Common::copy(data, data + MAX_TALK_SEQUENCES, _data); +} + +void TalkSequences::clear() { + Common::fill(&_data[0], &_data[MAX_TALK_SEQUENCES], 0); +} + +/*----------------------------------------------------------------*/ + +Talk::Talk(SherlockEngine *vm) : _vm(vm) { + _talkCounter = 0; + _talkToAbort = false; + _speaker = 0; + _talkIndex = 0; + _talkTo = 0; + _scriptSelect = 0; + _converseNum = -1; + _talkStealth = 0; + _talkToFlag = -1; + _moreTalkDown = _moreTalkUp = false; + _scriptMoreFlag = 0; + _scriptSaveIndex = -1; +} + +void Talk::talkTo(const Common::String &filename) { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Journal &journal = *_vm->_journal; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + Common::Rect savedBounds = screen.getDisplayBounds(); + bool abortFlag = false; + + if (filename.empty()) + // No filename passed, so exit + return; + + // If there any canimations currently running, or a portrait is being cleared, + // save the filename for later executing when the canimation is done + if (scene._canimShapes.size() > 0 || people._clearingThePortrait) { + // Make sure we're not in the middle of a script + if (!_scriptMoreFlag) { + _scriptName = filename; + _scriptSaveIndex = 0; + + // Flag the selection, since we don't yet know which statement yet + _scriptSelect = 100; + _scriptMoreFlag = 3; + } + + return; + } + + // Save the ui mode temporarily and switch to talk mode + MenuMode savedMode = ui._menuMode; + ui._menuMode = TALK_MODE; + + // Turn on the Exit option + ui._endKeyActive = true; + + if (people[AL]._walkCount || people._walkTo.size() > 0) { + // Only interrupt if an action if trying to do an action, and not just + // if the player is walking around the scene + if (people._allowWalkAbort) + abortFlag = true; + + people.gotoStand(people._player); + } + + if (_talkToAbort) + return; + + freeTalkVars(); + + // If any sequences have changed in the prior talk file, restore them + if (_savedSequences.size() > 0) { + for (uint idx = 0; idx < _savedSequences.size(); ++idx) { + SequenceEntry &ss = _savedSequences[idx]; + for (uint idx2 = 0; idx2 < ss._sequences.size(); ++idx2) + scene._bgShapes[ss._objNum]._sequences[idx2] = ss._sequences[idx2]; + + // Reset the object's frame to the beginning of the sequence + scene._bgShapes[ss._objNum]._frameNumber = 0; + } + } + + while (!_sequenceStack.empty()) + pullSequence(); + + // Restore any pressed button + if (!ui._windowOpen && savedMode != STD_MODE) + ui.restoreButton((int)(savedMode - 1)); + + // Clear the ui counter so that anything displayed on the info line + // before the window was opened isn't cleared + ui._menuCounter = 0; + + // Close any previous window before starting the talk + if (ui._windowOpen) { + switch (savedMode) { + case LOOK_MODE: + events.setCursor(ARROW); + + if (ui._invLookFlag) { + screen.resetDisplayBounds(); + ui.drawInterface(2); + } + + ui.banishWindow(); + ui._windowBounds.top = CONTROLS_Y1; + ui._temp = ui._oldTemp = ui._lookHelp = 0; + ui._menuMode = STD_MODE; + events._pressed = events._released = events._oldButtons = 0; + ui._invLookFlag = false; + break; + + case TALK_MODE: + if (_speaker < SPEAKER_REMOVE) + people.clearTalking(); + if (_talkCounter) + return; + + // If we were in inventory mode looking at an object, restore the + // back buffers before closing the window, so we get the ui restored + // rather than the inventory again + if (ui._invLookFlag) { + screen.resetDisplayBounds(); + ui.drawInterface(2); + ui._invLookFlag = ui._lookScriptFlag = false; + } + + ui.banishWindow(); + ui._windowBounds.top = CONTROLS_Y1; + abortFlag = true; + break; + + case INV_MODE: + case USE_MODE: + case GIVE_MODE: + inv.freeInv(); + if (ui._invLookFlag) { + screen.resetDisplayBounds(); + ui.drawInterface(2); + ui._invLookFlag = ui._lookScriptFlag = false; + } + + ui._infoFlag = true; + ui.clearInfo(); + ui.banishWindow(false); + ui._key = -1; + break; + + case FILES_MODE: + ui.banishWindow(true); + ui._windowBounds.top = CONTROLS_Y1; + abortFlag = true; + break; + + case SETUP_MODE: + ui.banishWindow(true); + ui._windowBounds.top = CONTROLS_Y1; + ui._temp = ui._oldTemp = ui._lookHelp = ui._invLookFlag = false; + ui._menuMode = STD_MODE; + events._pressed = events._released = events._oldButtons = 0; + abortFlag = true; + break; + + default: + break; + } + } + + screen.resetDisplayBounds(); + events._pressed = events._released = false; + loadTalkFile(filename); + ui._selector = ui._oldSelector = ui._key = ui._oldKey = -1; + + // Find the first statement that has the correct flags + int select = -1; + for (uint idx = 0; idx < _statements.size() && select == -1; ++idx) { + if (_statements[idx]._talkMap == 0) + select = _talkIndex = idx; + } + + // If there's a pending automatic selection to be made, then use it + if (_scriptMoreFlag && _scriptSelect != 100) + select = _scriptSelect; + + if (select == -1) + error("Couldn't find statement to display"); + + // Add the statement into the journal and talk history + if (_talkTo != -1 && !_talkHistory[_converseNum][select]) + journal.record(_converseNum, select, true); + _talkHistory[_converseNum][select] = true; + + // Check if the talk file is meant to be a non-seen comment + if (filename.size() < 8 || filename[7] != '*') { + // Should we start in stealth mode? + if (_statements[select]._statement.hasPrefix("^")) { + _talkStealth = 2; + } else { + // Not in stealth mode, so bring up the ui window + _talkStealth = 0; + ++_talkToFlag; + events.setCursor(WAIT); + + ui._windowBounds.top = CONTROLS_Y; + ui._infoFlag = true; + ui.clearInfo(); + } + + // Handle replies until there's no further linked file, + // or the link file isn't a reply first cnversation + while (!_vm->shouldQuit()) { + clearSequences(); + _scriptSelect = select; + _speaker = _talkTo; + + Statement &statement = _statements[select]; + doScript(_statements[select]._reply); + + if (_talkToAbort) + return; + + if (!_talkStealth) + ui.clearWindow(); + + if (statement._modified.size() > 0) { + for (uint idx = 0; idx < statement._modified.size(); ++idx) + _vm->setFlags(statement._modified[idx]); + + setTalkMap(); + } + + // Check for a linked file + if (!statement._linkFile.empty() && !_scriptMoreFlag) { + Common::String linkFilename = statement._linkFile; + freeTalkVars(); + loadTalkFile(linkFilename); + + // Scan for the first valid statement in the newly loaded file + select = -1; + for (uint idx = 0; idx < _statements.size(); ++idx) { + if (_statements[idx]._talkMap == 0) { + select = idx; + break; + } + } + + if (_talkToFlag == 1) + pullSequence(); + + // Set the stealth mode for the new talk file + Statement &newStatement = _statements[select]; + _talkStealth = newStatement._statement.hasPrefix("^") ? 2 : 0; + + // If the new conversion is a reply first, then we don't need + // to display any choices, since the reply needs to be shown + if (!newStatement._statement.hasPrefix("*") && + !newStatement._statement.hasPrefix("^")) { + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + _talkIndex = select; + ui._selector = ui._oldSelector = -1; + + if (!ui._windowOpen) { + // Draw the talk interface on the back buffer + drawInterface(); + displayTalk(false); + } else { + displayTalk(true); + } + + byte color = ui._endKeyActive ? COMMAND_FOREGROUND : COMMAND_NULL; + + // If the window is already open, simply draw. Otherwise, do it + // to the back buffer and then summon the window + if (ui._windowOpen) { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), color, true, "Exit"); + } else { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), color, false, "Exit"); + + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(); + } + + ui._windowOpen = true; + } + + // Break out of loop now that we're waiting for player input + events.setCursor(ARROW); + break; + } else { + // Add the statement into the journal and talk history + if (_talkTo != -1 && !_talkHistory[_converseNum][select]) + journal.record(_converseNum, select, true); + _talkHistory[_converseNum][select] = true; + + } + + ui._key = ui._oldKey = COMMANDS[TALK_MODE - 1]; + ui._temp = ui._oldTemp = 0; + ui._menuMode = TALK_MODE; + _talkToFlag = 2; + } else { + freeTalkVars(); + + if (!ui._lookScriptFlag) { + ui.banishWindow(); + ui._windowBounds.top = CONTROLS_Y1; + ui._menuMode = STD_MODE; + } + + break; + } + } + } + + _talkStealth = 0; + events._pressed = events._released = events._oldButtons = 0; + events.clearKeyboard(); + + if (savedBounds.bottom == SHERLOCK_SCREEN_HEIGHT) + screen.resetDisplayBounds(); + else + screen.setDisplayBounds(savedBounds); + + _talkToAbort = abortFlag; + + // If a script was added to the script stack, restore state so that the + // previous script can continue + popStack(); + + if (_vm->getGameID() == GType_SerratedScalpel && filename == "Tube59c") { + // WORKAROUND: Original game bug causes the results of testing the powdery substance + // to disappear too quickly. Introduce a delay to allow it to be properly displayed + ui._menuCounter = 30; + } + + events.setCursor(ARROW); +} + +void Talk::talk(int objNum) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + Object &obj = scene._bgShapes[objNum]; + + ui._windowBounds.top = CONTROLS_Y; + ui._infoFlag = true; + _speaker = SPEAKER_REMOVE; + loadTalkFile(scene._bgShapes[objNum]._name); + + // Find the first statement with the correct flags + int select = -1; + for (uint idx = 0; idx < _statements.size(); ++idx) { + if (_statements[idx]._talkMap == 0) { + select = idx; + break; + } + } + if (select == -1) + error("No entry matched all required flags"); + + // See if the statement is a stealth mode reply + Statement &statement = _statements[select]; + if (statement._statement.hasPrefix("^")) { + clearSequences(); + + // Start talk in stealth mode + _talkStealth = 2; + + talkTo(obj._name); + } else if (statement._statement.hasPrefix("*")) { + // Character being spoken to will speak first + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + + events.setCursor(WAIT); + if (obj._lookPosition.y != 0) + // Need to walk to character first + people.walkToCoords(Common::Point(obj._lookPosition.x, obj._lookPosition.y * 100), + obj._lookFacing); + events.setCursor(ARROW); + + if (!_talkToAbort) + talkTo(obj._name); + } else { + // Holmes will be speaking first + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + + _talkToFlag = false; + events.setCursor(WAIT); + if (obj._lookPosition.y != 0) + // Walk over to person to talk to + people.walkToCoords(Common::Point(obj._lookPosition.x, obj._lookPosition.y * 100), + obj._lookFacing); + events.setCursor(ARROW); + + if (!_talkToAbort) { + // See if walking over triggered a conversation + if (_talkToFlag) { + if (_talkToFlag == 1) { + events.setCursor(ARROW); + // _sequenceStack._count = 1; + pullSequence(); + } + } else { + drawInterface(); + + events._pressed = events._released = false; + _talkIndex = select; + displayTalk(false); + ui._selector = ui._oldSelector = -1; + + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(); + } + + ui._windowOpen = true; + } + + _talkToFlag = -1; + } + } +} + +void Talk::freeTalkVars() { + _statements.clear(); +} + +void Talk::loadTalkFile(const Common::String &filename) { + People &people = *_vm->_people; + Resources &res = *_vm->_res; + Sound &sound = *_vm->_sound; + + // Save a copy of the talk filename + _scriptName = filename; + + // Check for an existing person being talked to + _talkTo = -1; + for (int idx = 0; idx < (int)people._characters.size(); ++idx) { + if (!scumm_strnicmp(filename.c_str(), people._characters[idx]._portrait, 4)) { + _talkTo = idx; + break; + } + } + + const char *chP = strchr(filename.c_str(), '.'); + Common::String talkFile = chP ? Common::String(filename.c_str(), chP) + ".tlk" : + Common::String(filename.c_str(), filename.c_str() + 7) + ".tlk"; + + // Open the talk file for reading + Common::SeekableReadStream *talkStream = res.load(talkFile); + _converseNum = res.resourceIndex(); + talkStream->skip(2); // Skip talk file version num + + _statements.resize(talkStream->readByte()); + for (uint idx = 0; idx < _statements.size(); ++idx) + _statements[idx].synchronize(*talkStream); + + delete talkStream; + + if (!sound._voices) + stripVoiceCommands(); + setTalkMap(); +} + +void Talk::stripVoiceCommands() { + for (uint sIdx = 0; sIdx < _statements.size(); ++sIdx) { + Statement &statement = _statements[sIdx]; + + // Scan for an sound effect byte, which indicates to play a sound + for (uint idx = 0; idx < statement._reply.size(); ++idx) { + if (statement._reply[idx] == (char)SFX_COMMAND) { + // Replace instruction character with a space, and delete the + // rest of the name following it + statement._reply = Common::String(statement._reply.c_str(), + statement._reply.c_str() + idx) + " " + + Common::String(statement._reply.c_str() + 9); + } + } + + // Ensure the last character of the reply is not a space from the prior + // conversion loop, to avoid any issues with the space ever causing a page + // wrap, and ending up displaying another empty page + while (statement._reply.lastChar() == ' ') + statement._reply.deleteLastChar(); + } +} + +void Talk::setTalkMap() { + int statementNum = 0; + + for (uint sIdx = 0; sIdx < _statements.size(); ++sIdx) { + Statement &statement = _statements[sIdx]; + + // Set up talk map entry for the statement + bool valid = true; + for (uint idx = 0; idx < statement._required.size(); ++idx) { + if (!_vm->readFlags(statement._required[idx])) + valid = false; + } + + statement._talkMap = valid ? statementNum++ : -1; + } +} + +void Talk::drawInterface() { + Screen &screen = *_vm->_screen; + Surface &bb = *screen._backBuffer; + + bb.fillRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y1 + 10), BORDER_COLOR); + bb.fillRect(Common::Rect(0, CONTROLS_Y + 10, 2, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(SHERLOCK_SCREEN_WIDTH - 2, CONTROLS_Y + 10, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - 1, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(2, CONTROLS_Y + 10, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + + if (_talkTo != -1) { + screen.makeButton(Common::Rect(99, CONTROLS_Y, 139, CONTROLS_Y + 10), + 119 - screen.stringWidth("Exit") / 2, "Exit"); + screen.makeButton(Common::Rect(140, CONTROLS_Y, 180, CONTROLS_Y + 10), + 159 - screen.stringWidth("Up") / 2, "Up"); + screen.makeButton(Common::Rect(181, CONTROLS_Y, 221, CONTROLS_Y + 10), + 200 - screen.stringWidth("Down") / 2, "Down"); + } else { + int strWidth = screen.stringWidth(PRESS_KEY_TO_CONTINUE); + screen.makeButton(Common::Rect(46, CONTROLS_Y, 273, CONTROLS_Y + 10), + 160 - strWidth / 2, PRESS_KEY_TO_CONTINUE); + screen.gPrint(Common::Point(160 - strWidth / 2, CONTROLS_Y), COMMAND_FOREGROUND, "P"); + } +} + +bool Talk::displayTalk(bool slamIt) { + Screen &screen = *_vm->_screen; + int yp = CONTROLS_Y + 14; + int lineY = -1; + _moreTalkDown = _moreTalkUp = false; + + for (uint idx = 0; idx < _statements.size(); ++idx) { + _statements[idx]._talkPos.top = _statements[idx]._talkPos.bottom = -1; + } + + if (_talkIndex) { + for (int idx = 0; idx < _talkIndex && !_moreTalkUp; ++idx) { + if (_statements[idx]._talkMap != -1) + _moreTalkUp = true; + } + } + + // Display the up arrow and enable Up button if the first option is scrolled off-screen + if (_moreTalkUp) { + if (slamIt) { + screen.print(Common::Point(5, CONTROLS_Y + 13), INV_FOREGROUND, "~"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, true, "Up"); + } else { + screen.gPrint(Common::Point(5, CONTROLS_Y + 12), INV_FOREGROUND, "~"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, false, "Up"); + } + } else { + if (slamIt) { + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, true, "Up"); + screen.vgaBar(Common::Rect(5, CONTROLS_Y + 11, 15, CONTROLS_Y + 22), INV_BACKGROUND); + } else { + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, false, "Up"); + screen._backBuffer1.fillRect(Common::Rect(5, CONTROLS_Y + 11, + 15, CONTROLS_Y + 22), INV_BACKGROUND); + } + } + + // Loop through the statements + bool done = false; + for (uint idx = _talkIndex; idx < _statements.size() && !done; ++idx) { + Statement &statement = _statements[idx]; + + if (statement._talkMap != -1) { + bool flag = _talkHistory[_converseNum][idx]; + lineY = talkLine(idx, statement._talkMap, flag ? TALK_NULL : INV_FOREGROUND, + yp, slamIt); + + if (lineY != -1) { + statement._talkPos.top = yp; + yp = lineY; + statement._talkPos.bottom = yp; + + if (yp == SHERLOCK_SCREEN_HEIGHT) + done = true; + } else { + done = true; + } + } + } + + // Display the down arrow and enable down button if there are more statements available down off-screen + if (lineY == -1 || lineY == SHERLOCK_SCREEN_HEIGHT) { + _moreTalkDown = true; + + if (slamIt) { + screen.print(Common::Point(5, 190), INV_FOREGROUND, "|"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_FOREGROUND, true, "Down"); + } else { + screen.gPrint(Common::Point(5, 189), INV_FOREGROUND, "|"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_FOREGROUND, false, "Down"); + } + } else { + if (slamIt) { + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, true, "Down"); + screen.vgaBar(Common::Rect(5, 189, 16, 199), INV_BACKGROUND); + } else { + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, "Down"); + screen._backBuffer1.fillRect(Common::Rect(5, 189, 16, 199), INV_BACKGROUND); + } + } + + return done; +} + +int Talk::talkLine(int lineNum, int stateNum, byte color, int lineY, bool slamIt) { + Screen &screen = *_vm->_screen; + int idx = lineNum; + Common::String msg, number; + bool numberFlag = false; + + // Get the statement to display as well as optional number prefix + if (idx < SPEAKER_REMOVE) { + number = Common::String::format("%d.", stateNum + 1); + numberFlag = true; + } else { + idx -= SPEAKER_REMOVE; + } + msg = _statements[idx]._statement; + + // Handle potentially multiple lines needed to display entire statement + const char *lineStartP = msg.c_str(); + int maxWidth = 298 - (numberFlag ? 18 : 0); + for (;;) { + // Get as much of the statement as possible will fit on the + Common::String sLine; + const char *lineEndP = lineStartP; + int width = 0; + do { + width += screen.charWidth(*lineEndP); + } while (*++lineEndP && width < maxWidth); + + // Check if we need to wrap the line + if (width >= maxWidth) { + // Work backwards to the prior word's end + while (*--lineEndP != ' ') + ; + + sLine = Common::String(lineStartP, lineEndP++); + } else { + // Can display remainder of the statement on the current line + sLine = Common::String(lineStartP); + } + + + if (lineY <= (SHERLOCK_SCREEN_HEIGHT - 10)) { + // Need to directly display on-screen? + if (slamIt) { + // See if a numer prefix is needed or not + if (numberFlag) { + // Are we drawing the first line? + if (lineStartP == msg.c_str()) { + // We are, so print the number and then the text + screen.print(Common::Point(16, lineY), color, "%s", number.c_str()); + } + + // Draw the line with an indent + screen.print(Common::Point(30, lineY), color, "%s", sLine.c_str()); + } else { + screen.print(Common::Point(16, lineY), color, "%s", sLine.c_str()); + } + } else { + if (numberFlag) { + if (lineStartP == msg.c_str()) { + screen.gPrint(Common::Point(16, lineY - 1), color, "%s", number.c_str()); + } + + screen.gPrint(Common::Point(30, lineY - 1), color, "%s", sLine.c_str()); + } else { + screen.gPrint(Common::Point(16, lineY - 1), color, "%s", sLine.c_str()); + } + } + + // Move to next line, if any + lineY += 9; + lineStartP = lineEndP; + + if (!*lineEndP) + break; + } else { + // We're close to the bottom of the screen, so stop display + lineY = -1; + break; + } + } + + if (lineY == -1 && lineStartP != msg.c_str()) + lineY = SHERLOCK_SCREEN_HEIGHT; + + // Return the Y position of the next line to follow this one + return lineY; +} + +void Talk::clearSequences() { + _sequenceStack.clear(); +} + +void Talk::pullSequence() { + Scene &scene = *_vm->_scene; + + if (_sequenceStack.empty()) + return; + + SequenceEntry seq = _sequenceStack.pop(); + if (seq._objNum != -1) { + Object &obj = scene._bgShapes[seq._objNum]; + + if (obj._seqSize < MAX_TALK_SEQUENCES) { + warning("Tried to restore too few frames"); + } else { + for (int idx = 0; idx < MAX_TALK_SEQUENCES; ++idx) + obj._sequences[idx] = seq._sequences[idx]; + + obj._frameNumber = seq._frameNumber; + obj._seqTo = seq._seqTo; + } + } +} + +void Talk::pushSequence(int speaker) { + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + + // Only proceed if a speaker is specified + if (speaker == -1) + return; + + SequenceEntry seqEntry; + if (!speaker) { + seqEntry._objNum = -1; + } else { + seqEntry._objNum = people.findSpeaker(speaker); + + if (seqEntry._objNum != -1) { + Object &obj = scene._bgShapes[seqEntry._objNum]; + for (uint idx = 0; idx < MAX_TALK_SEQUENCES; ++idx) + seqEntry._sequences.push_back(obj._sequences[idx]); + + seqEntry._frameNumber = obj._frameNumber; + seqEntry._seqTo = obj._seqTo; + } + } + + _sequenceStack.push(seqEntry); + if (_scriptStack.size() >= 5) + error("script stack overflow"); +} + +void Talk::setSequence(int speaker) { + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + + // If no speaker is specified, then nothing needs to be done + if (speaker == -1) + return; + + if (speaker) { + int objNum = people.findSpeaker(speaker); + if (objNum != -1) { + Object &obj = scene._bgShapes[objNum]; + + if (obj._seqSize < MAX_TALK_SEQUENCES) { + warning("Tried to copy too many talk frames"); + } else { + for (int idx = 0; idx < MAX_TALK_SEQUENCES; ++idx) { + obj._sequences[idx] = people._characters[speaker]._talkSequences[idx]; + if (idx > 0 && !obj._sequences[idx] && !obj._sequences[idx - 1]) + return; + + obj._frameNumber = 0; + obj._sequenceNumber = 0; + } + } + } + } +} + +void Talk::setStillSeq(int speaker) { + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + + // Don't bother doing anything if no specific speaker is specified + if (speaker == -1) + return; + + if (speaker) { + int objNum = people.findSpeaker(speaker); + if (objNum != -1) { + Object &obj = scene._bgShapes[objNum]; + + if (obj._seqSize < MAX_TALK_SEQUENCES) { + warning("Tried to copy too few still frames"); + } else { + for (uint idx = 0; idx < MAX_TALK_SEQUENCES; ++idx) { + obj._sequences[idx] = people._characters[speaker]._stillSequences[idx]; + if (idx > 0 && !people._characters[speaker]._talkSequences[idx] && + !people._characters[speaker]._talkSequences[idx - 1]) + break; + } + + obj._frameNumber = 0; + obj._seqTo = 0; + } + } + } +} + +void Talk::doScript(const Common::String &script) { + Animation &anim = *_vm->_animation; + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Map &map = *_vm->_map; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + int wait = 0; + bool pauseFlag = false; + bool endStr = false; + int yp = CONTROLS_Y + 12; + int charCount = 0; + int line = 0; + bool noTextYet = true; + bool openTalkWindow = false; + int seqCount; + + _savedSequences.clear(); + + const byte *scriptStart = (const byte *)script.c_str(); + const byte *scriptEnd = scriptStart + script.size(); + const byte *str = scriptStart; + + if (_scriptMoreFlag) { + _scriptMoreFlag = 0; + str = scriptStart + _scriptSaveIndex; + } + + // Check if the script begins with a Stealh Mode Active command + if (str[0] == STEALTH_MODE_ACTIVE || _talkStealth) { + _talkStealth = 2; + _speaker |= SPEAKER_REMOVE; + } else { + pushSequence(_speaker); + ui.clearWindow(); + + // Need to switch speakers? + if (str[0] == SWITCH_SPEAKER) { + _speaker = str[1] - 1; + str += 2; + pullSequence(); + pushSequence(_speaker); + setSequence(_speaker); + } else { + setSequence(_speaker); + } + + // Assign portrait location? + if (str[0] == ASSIGN_PORTRAIT_LOCATION) { + switch (str[1] & 15) { + case 1: + people._portraitSide = 20; + break; + case 2: + people._portraitSide = 220; + break; + case 3: + people._portraitSide = 120; + break; + default: + break; + + } + + if (str[1] > 15) + people._speakerFlip = true; + str += 2; + } + + // Remove portrait? + if (str[0] == REMOVE_PORTRAIT) { + _speaker = 255; + } else { + // Nope, so set the first speaker + people.setTalking(_speaker); + } + } + + do { + Common::String tempString; + wait = 0; + + byte c = str[0]; + if (!c) { + endStr = true; + } else if (c == '{') { + // Start of comment, so skip over it + while (*str++ != '}') + ; + } else if (c >= SWITCH_SPEAKER) { + // Handle control code + switch (c) { + case SWITCH_SPEAKER: + if (!(_speaker & SPEAKER_REMOVE)) + people.clearTalking(); + if (_talkToAbort) + return; + + ui.clearWindow(); + yp = CONTROLS_Y + 12; + charCount = line = 0; + + _speaker = *++str - 1; + people.setTalking(_speaker); + pullSequence(); + pushSequence(_speaker); + setSequence(_speaker); + break; + + case RUN_CANIMATION: + ++str; + scene.startCAnim((str[0] - 1) & 127, (str[0] & 0x80) ? -1 : 1); + if (_talkToAbort) + return; + + // Check if next character is changing side or changing portrait + if (charCount && (str[1] == SWITCH_SPEAKER || str[1] == ASSIGN_PORTRAIT_LOCATION)) + wait = 1; + break; + + case ASSIGN_PORTRAIT_LOCATION: + ++str; + switch (str[0] & 15) { + case 1: + people._portraitSide = 20; + break; + case 2: + people._portraitSide = 220; + break; + case 3: + people._portraitSide = 120; + break; + default: + break; + } + + if (str[0] > 15) + people._speakerFlip = true; + break; + + case PAUSE: + // Pause + charCount = *++str; + wait = pauseFlag = true; + break; + + case REMOVE_PORTRAIT: + if (_speaker >= 0 && _speaker < SPEAKER_REMOVE) + people.clearTalking(); + pullSequence(); + if (_talkToAbort) + return; + + _speaker |= SPEAKER_REMOVE; + break; + + case CLEAR_WINDOW: + ui.clearWindow(); + yp = CONTROLS_Y + 12; + charCount = line = 0; + break; + + case ADJUST_OBJ_SEQUENCE: + { + // Get the name of the object to adjust + ++str; + for (int idx = 0; idx < (str[0] & 127); ++idx) + tempString += str[idx + 2]; + + // Scan for object + int objId = -1; + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + if (tempString.equalsIgnoreCase(scene._bgShapes[idx]._name)) + objId = idx; + } + if (objId == -1) + error("Could not find object %s to change", tempString.c_str()); + + // Should the script be overwritten? + if (str[0] > 0x80) { + // Save the current sequence + _savedSequences.push(SequenceEntry()); + SequenceEntry &seqEntry = _savedSequences.top(); + seqEntry._objNum = objId; + seqEntry._seqTo = scene._bgShapes[objId]._seqTo; + for (uint idx = 0; idx < scene._bgShapes[objId]._seqSize; ++idx) + seqEntry._sequences.push_back(scene._bgShapes[objId]._sequences[idx]); + } + + // Get number of bytes to change + seqCount = str[1]; + str += (str[0] & 127) + 2; + + // Copy in the new sequence + for (int idx = 0; idx < seqCount; ++idx, ++str) + scene._bgShapes[objId]._sequences[idx] = str[0] - 1; + + // Reset object back to beginning of new sequence + scene._bgShapes[objId]._frameNumber = 0; + continue; + } + + case WALK_TO_COORDS: + ++str; + + people.walkToCoords(Common::Point(((str[0] - 1) * 256 + str[1] - 1) * 100, + str[2] * 100), str[3] - 1); + if (_talkToAbort) + return; + + str += 3; + break; + + case PAUSE_WITHOUT_CONTROL: + ++str; + + for (int idx = 0; idx < (str[0] - 1); ++idx) { + scene.doBgAnim(); + if (_talkToAbort) + return; + + // Check for button press + events.pollEvents(); + events.setButtonState(); + } + break; + + case BANISH_WINDOW: + if (!(_speaker & SPEAKER_REMOVE)) + people.clearTalking(); + pullSequence(); + + if (_talkToAbort) + return; + + _speaker |= SPEAKER_REMOVE; + ui.banishWindow(); + ui._menuMode = TALK_MODE; + noTextYet = true; + break; + + case SUMMON_WINDOW: + drawInterface(); + events._pressed = events._released = false; + events.clearKeyboard(); + noTextYet = false; + + if (_speaker != -1) { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, false, "Exit"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, false, "Up"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, "Down"); + } + break; + + case SET_FLAG: { + ++str; + int flag1 = (str[0] - 1) * 256 + str[1] - 1 - (str[1] == 1 ? 1 : 0); + int flag = (flag1 & 0x3fff) * (flag1 >= 0x4000 ? -1 : 1); + _vm->setFlags(flag); + ++str; + break; + } + + case SFX_COMMAND: + ++str; + if (sound._voices) { + for (int idx = 0; idx < 8 && str[idx] != '~'; ++idx) + tempString += str[idx]; + sound.playSound(tempString, WAIT_RETURN_IMMEDIATELY); + + // Set voices to wait for more + sound._voices = 2; + sound._speechOn = (*sound._soundIsOn); + } + + wait = 1; + str += 7; + break; + + case TOGGLE_OBJECT: + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + + scene.toggleObject(tempString); + str += str[0]; + break; + + case STEALTH_MODE_ACTIVE: + _talkStealth = 2; + break; + + case IF_STATEMENT: { + ++str; + int flag = (str[0] - 1) * 256 + str[1] - 1 - (str[1] == 1 ? 1 : 0); + ++str; + wait = 0; + + bool result = flag < 0x8000; + if (_vm->readFlags(flag & 0x7fff) != result) { + do { + ++str; + } while (str[0] && str[0] != ELSE_STATEMENT && str[0] != END_IF_STATEMENT); + + if (!str[0]) + endStr = true; + } + break; + } + + case ELSE_STATEMENT: + // If this is encountered here, it means that a preceeding IF statement was found, + // and evaluated to true. Now all the statements for the true block are finished, + // so skip over the block of code that would have executed if the result was false + wait = 0; + do { + ++str; + } while (str[0] && str[0] != END_IF_STATEMENT); + break; + + case STEALTH_MODE_DEACTIVATE: + _talkStealth = 0; + events.clearKeyboard(); + break; + + case TURN_HOLMES_OFF: + people._holmesOn = false; + break; + + case TURN_HOLMES_ON: + people._holmesOn = true; + break; + + case GOTO_SCENE: + scene._goToScene = str[1] - 1; + + if (scene._goToScene != 100) { + // Not going to the map overview + map._oldCharPoint = scene._goToScene; + map._overPos.x = map[scene._goToScene].x * 100 - 600; + map._overPos.y = map[scene._goToScene].y * 100 + 900; + + // Run a canimation? + if (str[2] > 100) { + people._hSavedFacing = str[2]; + people._hSavedPos = Common::Point(160, 100); + } + } + str += 6; + + _scriptMoreFlag = (scene._goToScene == 100) ? 2 : 1; + _scriptSaveIndex = str - scriptStart; + endStr = true; + wait = 0; + break; + + case PLAY_PROLOGUE: + ++str; + for (int idx = 0; idx < 8 && str[idx] != '~'; ++idx) + tempString += str[idx]; + + anim.play(tempString, 1, 3, true, 4); + break; + + case ADD_ITEM_TO_INVENTORY: + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + str += str[0]; + + inv.putNameInInventory(tempString); + break; + + case SET_OBJECT: { + ++str; + for (int idx = 0; idx < (str[0] & 127); ++idx) + tempString += str[idx + 1]; + + // Set comparison state according to if we want to hide or unhide + bool state = (str[0] >= SPEAKER_REMOVE); + str += str[0] & 127; + + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &object = scene._bgShapes[idx]; + if (tempString.equalsIgnoreCase(object._name)) { + // Only toggle the object if it's not in the desired state already + if ((object._type == HIDDEN && state) || (object._type != HIDDEN && !state)) + object.toggleHidden(); + } + } + break; + } + + case CALL_TALK_FILE: { + ++str; + for (int idx = 0; idx < 8 && str[idx] != '~'; ++idx) + tempString += str[idx]; + str += 8; + + int scriptCurrentIndex = str - scriptStart; + + // Save the current script position and new talk file + if (_scriptStack.size() < 9) { + ScriptStackEntry rec1; + rec1._name = _scriptName; + rec1._currentIndex = scriptCurrentIndex; + rec1._select = _scriptSelect; + _scriptStack.push(rec1); + + // Push the new talk file onto the stack + ScriptStackEntry rec2; + rec2._name = tempString; + rec2._currentIndex = 0; + rec2._select = 100; + _scriptStack.push(rec2); + } else { + error("Script stack overflow"); + } + + _scriptMoreFlag = 1; + endStr = true; + wait = 0; + break; + } + + case MOVE_MOUSE: + ++str; + events.moveMouse(Common::Point((str[0] - 1) * 256 + str[1] - 1, str[2])); + if (_talkToAbort) + return; + str += 3; + break; + + case DISPLAY_INFO_LINE: + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + str += str[0]; + + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", tempString.c_str()); + ui._menuCounter = 30; + break; + + case CLEAR_INFO_LINE: + ui._infoFlag = true; + ui.clearInfo(); + break; + + case WALK_TO_CANIMATION: { + ++str; + CAnim &animation = scene._cAnim[str[0] - 1]; + + people.walkToCoords(animation._goto, animation._gotoDir); + if (_talkToAbort) + return; + break; + } + + case REMOVE_ITEM_FROM_INVENTORY: + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + str += str[0]; + + inv.deleteItemFromInventory(tempString); + break; + + case ENABLE_END_KEY: + ui._endKeyActive = true; + break; + + case DISABLE_END_KEY: + ui._endKeyActive = false; + break; + + default: + break; + } + + ++str; + } else { + // If the window isn't yet open, draw the window before printing starts + if (!ui._windowOpen && noTextYet) { + noTextYet = false; + drawInterface(); + + if (_talkTo != -1) { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, false, "Exit"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, false, "Up"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, "Down"); + } + } + + // If it's the first line, display the speaker + if (!line && _speaker >= 0 && _speaker < (int)people._characters.size()) { + // If the window is open, display the name directly on-screen. + // Otherwise, simply draw it on the back buffer + if (ui._windowOpen) { + screen.print(Common::Point(16, yp), TALK_FOREGROUND, "%s", + people._characters[_speaker & 127]._name); + } else { + screen.gPrint(Common::Point(16, yp - 1), TALK_FOREGROUND, "%s", + people._characters[_speaker & 127]._name); + openTalkWindow = true; + } + + yp += 9; + } + + // Find amount of text that will fit on the line + int width = 0, idx = 0; + do { + width += screen.charWidth(str[idx]); + ++idx; + ++charCount; + } while (width < 298 && str[idx] && str[idx] != '{' && str[idx] < SWITCH_SPEAKER); + + if (str[idx] || width >= 298) { + if (str[idx] < SWITCH_SPEAKER && str[idx] != '{') { + --idx; + --charCount; + } + } else { + endStr = true; + } + + // If word wrap is needed, find the start of the current word + if (width >= 298) { + while (str[idx] != ' ') { + --idx; + --charCount; + } + } + + // Print the line + Common::String lineStr((const char *)str, (const char *)str + idx); + + // If the speaker indicates a description file, print it in yellow + if (_speaker != -1) { + if (ui._windowOpen) { + screen.print(Common::Point(16, yp), COMMAND_FOREGROUND, "%s", lineStr.c_str()); + } else { + screen.gPrint(Common::Point(16, yp - 1), COMMAND_FOREGROUND, "%s", lineStr.c_str()); + openTalkWindow = true; + } + } else { + if (ui._windowOpen) { + screen.print(Common::Point(16, yp), COMMAND_FOREGROUND, "%s", lineStr.c_str()); + } else { + screen.gPrint(Common::Point(16, yp - 1), COMMAND_FOREGROUND, "%s", lineStr.c_str()); + openTalkWindow = true; + } + } + + // Move to end of displayed line + str += idx; + + // If line wrap occurred, then move to after the separating space between the words + if (str[0] < SWITCH_SPEAKER && str[0] != '{') + ++str; + + yp += 9; + ++line; + + // Certain different conditions require a wait + if ((line == 4 && str < scriptEnd && str[0] != SFX_COMMAND && str[0] != PAUSE && _speaker != -1) || + (line == 5 && str < scriptEnd && str[0] != PAUSE && _speaker == -1) || + endStr) { + wait = 1; + } + + switch (str >= scriptEnd ? 0 : str[0]) { + case SWITCH_SPEAKER: + case ASSIGN_PORTRAIT_LOCATION: + case BANISH_WINDOW: + case IF_STATEMENT: + case ELSE_STATEMENT: + case END_IF_STATEMENT: + case GOTO_SCENE: + case CALL_TALK_FILE: + wait = 1; + break; + default: + break; + } + } + + // Open window if it wasn't already open, and text has already been printed + if ((openTalkWindow && wait) || (openTalkWindow && str[0] >= SWITCH_SPEAKER && str[0] != CARRIAGE_RETURN)) { + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(); + } + + ui._windowOpen = true; + openTalkWindow = false; + } + + if (wait) { + // Handling pausing + if (!pauseFlag && charCount < 160) + charCount = 160; + + wait = waitForMore(charCount); + if (wait == -1) + endStr = true; + + // If a key was pressed to finish the window, see if further voice files should be skipped + if (wait >= 0 && wait < 254) { + if (str[0] == SFX_COMMAND) + str += 9; + } + + // Clear the window unless the wait was due to a PAUSE command + if (!pauseFlag && wait != -1 && str < scriptEnd && str[0] != SFX_COMMAND) { + if (!_talkStealth) + ui.clearWindow(); + yp = CONTROLS_Y + 12; + charCount = line = 0; + } + + pauseFlag = false; + } + } while (!_vm->shouldQuit() && !endStr); + + if (wait != -1) { + for (int ssIndex = 0; ssIndex < (int)_savedSequences.size(); ++ssIndex) { + SequenceEntry &seq = _savedSequences[ssIndex]; + Object &object = scene._bgShapes[seq._objNum]; + + for (uint idx = 0; idx < seq._sequences.size(); ++idx) + object._sequences[idx] = seq._sequences[idx]; + object._frameNumber = seq._frameNumber; + object._seqTo = seq._seqTo; + } + + pullSequence(); + if (_speaker >= 0 && _speaker < SPEAKER_REMOVE) + people.clearTalking(); + } +} + +int Talk::waitForMore(int delay) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + CursorId oldCursor = events.getCursor(); + int key2 = 254; + + // Unless we're in stealth mode, show the appropriate cursor + if (!_talkStealth) { + events.setCursor(ui._lookScriptFlag ? MAGNIFY : ARROW); + } + + do { + if (sound._speechOn && !*sound._soundIsOn) + people._portrait._frameNumber = -1; + + scene.doBgAnim(); + + // If talkTo call was done via doBgAnim, abort out of talk quietly + if (_talkToAbort) { + key2 = -1; + events._released = true; + } else { + // See if there's been a button press + events.setButtonState(); + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + if (Common::isPrint(keyState.ascii)) + key2 = keyState.keycode; + } + + if (_talkStealth) { + key2 = 254; + events._released = false; + } + } + + // Count down the delay + if ((delay > 0 && !ui._invLookFlag && !ui._lookScriptFlag) || _talkStealth) + --delay; + + // If there are voices playing, reset delay so that they keep playing + if (sound._voices == 2 && *sound._soundIsOn) + delay = 0; + } while (!_vm->shouldQuit() && key2 == 254 && (delay || (sound._voices == 2 && *sound._soundIsOn)) + && !events._released && !events._rightReleased); + + // If voices was set 2 to indicate a voice file was place, then reset it back to 1 + if (sound._voices == 2) + sound._voices = 1; + + if (delay > 0 && sound._diskSoundPlaying) + sound.stopSndFuncPtr(0, 0); + + // Adjust _talkStealth mode: + // mode 1 - It was by a pause without stealth being on before the pause, so reset back to 0 + // mode 3 - It was set by a pause with stealth being on before the pause, to set it to active + // mode 0/2 (Inactive/active) No change + switch (_talkStealth) { + case 1: + _talkStealth = 0; + break; + case 2: + _talkStealth = 2; + break; + default: + break; + } + + sound._speechOn = false; + events.setCursor(_talkToAbort ? ARROW : oldCursor); + events._pressed = events._released = false; + + return key2; +} + +void Talk::popStack() { + if (!_scriptStack.empty()) { + ScriptStackEntry scriptEntry = _scriptStack.pop(); + _scriptName = scriptEntry._name; + _scriptSaveIndex = scriptEntry._currentIndex; + _scriptSelect = scriptEntry._select; + _scriptMoreFlag = 1; + } +} + +void Talk::synchronize(Common::Serializer &s) { + for (int idx = 0; idx < MAX_TALK_FILES; ++idx) { + TalkHistoryEntry &he = _talkHistory[idx]; + + for (int flag = 0; flag < 16; ++flag) + s.syncAsByte(he._data[flag]); + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/talk.h b/engines/sherlock/talk.h new file mode 100644 index 0000000000..d26259dbfd --- /dev/null +++ b/engines/sherlock/talk.h @@ -0,0 +1,272 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_TALK_H +#define SHERLOCK_TALK_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "common/stream.h" +#include "common/stack.h" + +namespace Sherlock { + +#define MAX_TALK_SEQUENCES 11 +#define MAX_TALK_FILES 500 + +enum { + SWITCH_SPEAKER = 128, + RUN_CANIMATION = 129, + ASSIGN_PORTRAIT_LOCATION = 130, + PAUSE = 131, + REMOVE_PORTRAIT = 132, + CLEAR_WINDOW = 133, + ADJUST_OBJ_SEQUENCE = 134, + WALK_TO_COORDS = 135, + PAUSE_WITHOUT_CONTROL = 136, + BANISH_WINDOW = 137, + SUMMON_WINDOW = 138, + SET_FLAG = 139, + SFX_COMMAND = 140, + TOGGLE_OBJECT = 141, + STEALTH_MODE_ACTIVE = 142, + IF_STATEMENT = 143, + ELSE_STATEMENT = 144, + END_IF_STATEMENT = 145, + STEALTH_MODE_DEACTIVATE = 146, + TURN_HOLMES_OFF = 147, + TURN_HOLMES_ON = 148, + GOTO_SCENE = 149, + PLAY_PROLOGUE = 150, + ADD_ITEM_TO_INVENTORY = 151, + SET_OBJECT = 152, + CALL_TALK_FILE = 153, + MOVE_MOUSE = 154, + DISPLAY_INFO_LINE = 155, + CLEAR_INFO_LINE = 156, + WALK_TO_CANIMATION = 157, + REMOVE_ITEM_FROM_INVENTORY = 158, + ENABLE_END_KEY = 159, + DISABLE_END_KEY = 160, + CARRIAGE_RETURN = 161 +}; + +struct SequenceEntry { + int _objNum; + Common::Array<byte> _sequences; + int _frameNumber; + int _seqTo; + + SequenceEntry(); +}; + +struct ScriptStackEntry { + Common::String _name; + int _currentIndex; + int _select; +}; + +struct Statement { + Common::String _statement; + Common::String _reply; + Common::String _linkFile; + Common::String _voiceFile; + Common::Array<int> _required; + Common::Array<int> _modified; + int _portraitSide; + int _quotient; + int _talkMap; + Common::Rect _talkPos; + + /** + * Load the data for a single statement within a talk file + */ + void synchronize(Common::SeekableReadStream &s); +}; + +struct TalkHistoryEntry { + bool _data[16]; + + TalkHistoryEntry(); + bool &operator[](int index) { return _data[index]; } +}; + +struct TalkSequences { + byte _data[MAX_TALK_SEQUENCES]; + + TalkSequences() { clear(); } + TalkSequences(const byte *data); + + byte &operator[](int idx) { return _data[idx]; } + void clear(); +}; + +class SherlockEngine; +class UserInterface; + +class Talk { + friend class UserInterface; +private: + SherlockEngine *_vm; + Common::Stack<SequenceEntry> _savedSequences; + Common::Stack<SequenceEntry> _sequenceStack; + Common::Stack<ScriptStackEntry> _scriptStack; + Common::Array<Statement> _statements; + TalkHistoryEntry _talkHistory[MAX_TALK_FILES]; + int _speaker; + int _talkIndex; + int _scriptSelect; + int _talkStealth; + int _talkToFlag; + int _scriptSaveIndex; +private: + /** + * Remove any voice commands from a loaded statement list + */ + void stripVoiceCommands(); + + /** + * Form a table of the display indexes for statements + */ + void setTalkMap(); + + /** + * Display a list of statements in a window at the bottom of the screen that the + * player can select from. + */ + bool displayTalk(bool slamIt); + + /** + * Prints a single conversation option in the interface window + */ + int talkLine(int lineNum, int stateNum, byte color, int lineY, bool slamIt); + + /** + * Parses a reply for control codes and display text. The found text is printed within + * the text window, handles delays, animations, and animating portraits. + */ + void doScript(const Common::String &script); + + /** + * When the talk window has been displayed, waits a period of time proportional to + * the amount of text that's been displayed + */ + int waitForMore(int delay); +public: + bool _talkToAbort; + int _talkCounter; + int _talkTo; + int _scriptMoreFlag; + Common::String _scriptName; + bool _moreTalkUp, _moreTalkDown; + int _converseNum; +public: + Talk(SherlockEngine *vm); + + /** + * Return a given talk statement + */ + Statement &operator[](int idx) { return _statements[idx]; } + + /** + * Called whenever a conversation or item script needs to be run. For standard conversations, + * it opens up a description window similar to how 'talk' does, but shows a 'reply' directly + * instead of waiting for a statement option. + * @remarks It seems that at some point, all item scripts were set up to use this as well. + * In their case, the conversation display is simply suppressed, and control is passed on to + * doScript to implement whatever action is required. + */ + void talkTo(const Common::String &filename); + + /** + * Main method for handling conversations when a character to talk to has been + * selected. It will make Holmes walk to the person to talk to, draws the + * interface window for the conversation and passes on control to give the + * player a list of options to make a selection from + */ + void talk(int objNum); + + /** + * Clear loaded talk data + */ + void freeTalkVars(); + + /** + * Draws the interface for conversation display + */ + void drawInterface(); + + /** + * Opens the talk file 'talk.tlk' and searches the index for the specified + * conversation. If found, the data for that conversation is loaded + */ + void loadTalkFile(const Common::String &filename); + + /** + * Change the sequence of a background object corresponding to a given speaker. + * The new sequence will display the character as "listening" + */ + void setStillSeq(int speaker); + + /** + * Clears the stack of pending object sequences associated with speakers in the scene + */ + void clearSequences(); + + /** + * Pulls a background object sequence from the sequence stack and restore's the + * object's sequence + */ + void pullSequence(); + + /** + * Push the sequence of a background object that's an NPC that needs to be + * saved onto the sequence stack. + */ + void pushSequence(int speaker); + + /** + * Change the sequence of the scene background object associated with the current speaker. + */ + void setSequence(int speaker); + + /** + * Returns true if the script stack is empty + */ + bool isSequencesEmpty() const { return _scriptStack.empty(); } + + /** + * Pops an entry off of the script stack + */ + void popStack(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Common::Serializer &s); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo.cpp b/engines/sherlock/tattoo/tattoo.cpp new file mode 100644 index 0000000000..d4059ac413 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo.cpp @@ -0,0 +1,35 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +void TattooEngine::showOpening() { + // TODO +} + +} // End of namespace Tattoo + +} // End of namespace Scalpel diff --git a/engines/sherlock/tattoo/tattoo.h b/engines/sherlock/tattoo/tattoo.h new file mode 100644 index 0000000000..b98395597c --- /dev/null +++ b/engines/sherlock/tattoo/tattoo.h @@ -0,0 +1,45 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_TATTOO_H +#define SHERLOCK_TATTOO_H + +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Tattoo { + +class TattooEngine : public SherlockEngine { +protected: + virtual void showOpening(); +public: + TattooEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + SherlockEngine(syst, gameDesc) {} + virtual ~TattooEngine() {} +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/user_interface.cpp b/engines/sherlock/user_interface.cpp new file mode 100644 index 0000000000..524ecf3d2f --- /dev/null +++ b/engines/sherlock/user_interface.cpp @@ -0,0 +1,2290 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sherlock/user_interface.h" +#include "sherlock/sherlock.h" +#include "sherlock/settings.h" + +namespace Sherlock { + +// Main user interface menu control locations +const int MENU_POINTS[12][4] = { + { 13, 153, 72, 165 }, + { 13, 169, 72, 181 }, + { 13, 185, 72, 197 }, + { 88, 153, 152, 165 }, + { 88, 169, 152, 181 }, + { 88, 185, 152, 197 }, + { 165, 153, 232, 165 }, + { 165, 169, 232, 181 }, + { 165, 185, 233, 197 }, + { 249, 153, 305, 165 }, + { 249, 169, 305, 181 }, + { 249, 185, 305, 197 } +}; + +// Inventory control locations */ +const int INVENTORY_POINTS[8][3] = { + { 4, 50, 29 }, + { 52, 99, 77 }, + { 101, 140, 123 }, + { 142, 187, 166 }, + { 189, 219, 198 }, + { 221, 251, 234 }, + { 253, 283, 266 }, + { 285, 315, 294 } +}; + +const char COMMANDS[13] = "LMTPOCIUGJFS"; +const char INVENTORY_COMMANDS[9] = { "ELUG-+,." }; +const char *const PRESS_KEY_FOR_MORE = "Press any Key for More."; +const char *const PRESS_KEY_TO_CONTINUE = "Press any Key to Continue."; + +const char *const MOPEN[] = { + "This cannot be opened", "It is already open", "It is locked", "Wait for Watson", " ", "." +}; +const char *const MCLOSE[] = { + "This cannot be closed", "It is already closed", "The safe door is in the way" +}; +const char *const MMOVE[] = { + "This cannot be moved", "It is bolted to the floor", "It is too heavy", "The other crate is in the way" +}; +const char *const MPICK[] = { + "Nothing of interest here", "It is bolted down", "It is too big to carry", "It is too heavy", + "I think a girl would be more your type", "Those flowers belong to Penny", "She's far too young for you!", + "I think a girl would be more your type!", "Government property for official use only" +}; +const char *const MUSE[] = { + "You can't do that", "It had no effect", "You can't reach it", "OK, the door looks bigger! Happy?", + "Doors don't smoke" +}; + +/*----------------------------------------------------------------*/ + +UserInterface::UserInterface(SherlockEngine *vm) : _vm(vm) { + if (_vm->_interactiveFl) { + _controls = new ImageFile("menu.all"); + _controlPanel = new ImageFile("controls.vgs"); + } else { + _controls = nullptr; + _controlPanel = nullptr; + } + + _bgFound = 0; + _oldBgFound = -1; + _keyPress = '\0'; + _helpStyle = false; + _menuCounter = 0; + _menuMode = STD_MODE; + _help = _oldHelp = 0; + _lookHelp = 0; + _key = _oldKey = '\0'; + _temp = _oldTemp = 0; + _temp1 = 0; + _invLookFlag = 0; + _windowOpen = false; + _oldLook = false; + _keyboardInput = false; + _pause = false; + _cNum = 0; + _selector = _oldSelector = -1; + _windowBounds = Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH - 1, + SHERLOCK_SCREEN_HEIGHT - 1); + _slideWindows = true; + _find = 0; + _oldUse = 0; + _endKeyActive = true; + _lookScriptFlag = false; + _infoFlag = false; +} + +UserInterface::~UserInterface() { + delete _controls; + delete _controlPanel; +} + +void UserInterface::reset() { + _oldKey = -1; + _help = _oldHelp = -1; + _oldTemp = _temp = -1; +} + +void UserInterface::drawInterface(int bufferNum) { + Screen &screen = *_vm->_screen; + + if (bufferNum & 1) + screen._backBuffer1.transBlitFrom((*_controlPanel)[0], Common::Point(0, CONTROLS_Y)); + if (bufferNum & 2) + screen._backBuffer2.transBlitFrom((*_controlPanel)[0], Common::Point(0, CONTROLS_Y)); + if (bufferNum == 3) + screen._backBuffer2.fillRect(0, INFO_LINE, SHERLOCK_SCREEN_WIDTH, INFO_LINE + 10, INFO_BLACK); +} + +void UserInterface::handleInput() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + + if (_menuCounter) + whileMenuCounter(); + + Common::Point pt = events.mousePos(); + _bgFound = scene.findBgShape(Common::Rect(pt.x, pt.y, pt.x + 1, pt.y + 1)); + _keyPress = '\0'; + + // Check kbd and set the mouse released flag if Enter or space is pressed. + // Otherwise, the pressed _key is stored for later use + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + _keyPress = keyState.ascii; + + if (keyState.keycode == Common::KEYCODE_x && keyState.flags & Common::KBD_ALT) { + _vm->quitGame(); + events.pollEvents(); + return; + } + } + + // Do button highlighting check + if (!talk._scriptMoreFlag) { // Don't if scripts are running + if (((events._rightPressed || events._rightReleased) && _helpStyle) || + (!_helpStyle && !_menuCounter)) { + // Handle any default commands if we're in STD_MODE + if (_menuMode == STD_MODE) { + if (pt.y < CONTROLS_Y && + (events._rightPressed || (!_helpStyle && !events._released)) && + (_bgFound != -1) && (_bgFound < 1000) && + (scene._bgShapes[_bgFound]._defaultCommand || + !scene._bgShapes[_bgFound]._description.empty())) { + // If there is no default command, so set it to Look + if (scene._bgShapes[_bgFound]._defaultCommand) + _help = scene._bgShapes[_bgFound]._defaultCommand - 1; + else + _help = 0; + + // Reset 'help' if it is an invalid command + if (_help > 5) + _help = -1; + } else if (pt.y < CONTROLS_Y && + ((events._rightReleased && _helpStyle) || (events._released && !_helpStyle)) && + (_bgFound != -1 && _bgFound < 1000) && + (scene._bgShapes[_bgFound]._defaultCommand || + !scene._bgShapes[_bgFound]._description.empty())) { + // If there is no default command, set it to Look + if (scene._bgShapes[_bgFound]._defaultCommand) + _menuMode = (MenuMode)scene._bgShapes[_bgFound]._defaultCommand; + else + _menuMode = LOOK_MODE; + events._released = true; + events._pressed = events._oldButtons = false; + _help = _oldHelp = -1; + + if (_menuMode == LOOK_MODE) { + // Set the flag to tell the game that this was a right-click + // call to look and should exit without the look button being pressed + _lookHelp = true; + } + } else { + _help = -1; + } + + // Check if highlighting a different button than last time + if (_help != _oldHelp) { + // If another button was highlighted previously, restore it + if (_oldHelp != -1) + restoreButton(_oldHelp); + + // If we're highlighting a new button, then draw it pressed + if (_help != -1) + depressButton(_help); + + _oldHelp = _help; + } + + if (_bgFound != _oldBgFound || _oldBgFound == -1) { + _infoFlag = true; + clearInfo(); + + if (_help != -1 && !scene._bgShapes[_bgFound]._description.empty() + && scene._bgShapes[_bgFound]._description[0] != ' ') + screen.print(Common::Point(0, INFO_LINE + 1), + INFO_FOREGROUND, "%s", scene._bgShapes[_bgFound]._description.c_str()); + + _oldBgFound = _bgFound; + } + } else { + // We're not in STD_MODE + // If there isn't a window open, then revert back to STD_MODE + if (!_windowOpen && events._rightReleased) { + // Restore all buttons + for (int idx = 0; idx < 12; ++idx) + restoreButton(idx); + + _menuMode = STD_MODE; + _key = _oldKey = -1; + _temp = _oldTemp = _lookHelp = _invLookFlag = 0; + events.clearEvents(); + } + } + } + } + + // Reset the old bgshape number if the mouse button is released, so that + // it can e re-highlighted when we come back here + if ((events._rightReleased && _helpStyle) || (events._released && !_helpStyle)) + _oldBgFound = -1; + + // Do routines that should be done before input processing + switch (_menuMode) { + case LOOK_MODE: + if (!_windowOpen) { + if (events._released && _bgFound >= 0 && _bgFound < 1000) { + if (!scene._bgShapes[_bgFound]._examine.empty()) + examine(); + } else { + lookScreen(pt); + } + } + break; + + case MOVE_MODE: + case OPEN_MODE: + case CLOSE_MODE: + case PICKUP_MODE: + lookScreen(pt); + break; + + case TALK_MODE: + if (!_windowOpen) { + bool personFound; + + if (_bgFound >= 1000) { + personFound = false; + if (!events._released) + lookScreen(pt); + } else { + personFound = _bgFound != -1 && scene._bgShapes[_bgFound]._aType == PERSON; + } + + if (events._released && personFound) + talk.talk(_bgFound); + else if (personFound) + lookScreen(pt); + else if (_bgFound < 1000) + clearInfo(); + } + break; + + case USE_MODE: + case GIVE_MODE: + case INV_MODE: + if (inv._invMode == INVMODE_LOOK || inv._invMode == INVMODE_USE || inv._invMode == INVMODE_GIVE) { + if (pt.y > CONTROLS_Y) + lookInv(); + else + lookScreen(pt); + } + break; + + default: + break; + } + + // + // Do input processing + // + if (events._pressed || events._released || events._rightPressed || _keyPress || _pause) { + if (((events._released && (_helpStyle || _help == -1)) || (events._rightReleased && !_helpStyle)) && + (pt.y <= CONTROLS_Y) && (_menuMode == STD_MODE)) { + // The mouse was clicked in the playing area with no action buttons down. + // Check if the mouse was clicked in a script zone. If it was, + // then execute the script. Otherwise, walk to the given position + if (scene.checkForZones(pt, SCRIPT_ZONE) != 0 || + scene.checkForZones(pt, NOWALK_ZONE) != 0) { + // Mouse clicked in script zone + events._pressed = events._released = false; + } else { + people._walkDest = pt; + people._allowWalkAbort = false; + people.goAllTheWay(); + } + + if (_oldKey != -1) { + restoreButton(_oldTemp); + _oldKey = -1; + } + } + + // Handle action depending on selected mode + switch (_menuMode) { + case LOOK_MODE: + if (_windowOpen) + doLookControl(); + break; + + case MOVE_MODE: + doMiscControl(ALLOW_MOVE); + break; + + case TALK_MODE: + if (_windowOpen) + doTalkControl(); + break; + + case OPEN_MODE: + doMiscControl(ALLOW_OPEN); + break; + + case CLOSE_MODE: + doMiscControl(ALLOW_CLOSE); + break; + + case PICKUP_MODE: + doPickControl(); + break; + + case USE_MODE: + case GIVE_MODE: + case INV_MODE: + doInvControl(); + break; + + case FILES_MODE: + doEnvControl(); + break; + + default: + break; + } + + // As long as there isn't an open window, do main input processing. + // Windows are opened when in TALK, USE, INV, and GIVE modes + if ((!_windowOpen && !_menuCounter && pt.y > CONTROLS_Y) || + _keyPress) { + if (events._pressed || events._released || _pause || _keyPress) + doMainControl(); + } + + if (pt.y < CONTROLS_Y && events._pressed && _oldTemp != (int)(_menuMode - 1) && _oldKey != -1) + restoreButton(_oldTemp); + } +} + +void UserInterface::depressButton(int num) { + Screen &screen = *_vm->_screen; + Common::Point pt(MENU_POINTS[num][0], MENU_POINTS[num][1]); + + ImageFrame &frame = (*_controls)[num]; + screen._backBuffer1.transBlitFrom(frame, pt); + screen.slamArea(pt.x, pt.y, pt.x + frame._width, pt.y + frame._height); +} + +void UserInterface::restoreButton(int num) { + Screen &screen = *_vm->_screen; + Common::Point pt(MENU_POINTS[num][0], MENU_POINTS[num][1]); + Graphics::Surface &frame = (*_controls)[num]._frame; + + screen._backBuffer1.blitFrom(screen._backBuffer2, pt, + Common::Rect(pt.x, pt.y, pt.x + 90, pt.y + 19)); + screen.slamArea(pt.x, pt.y, pt.x + frame.w, pt.y + frame.h); + + if (!_menuCounter) { + _infoFlag = true; + clearInfo(); + } +} + +void UserInterface::pushButton(int num) { + Events &events = *_vm->_events; + _oldKey = -1; + + if (!events._released) { + if (_oldHelp != -1) + restoreButton(_oldHelp); + if (_help != -1) + restoreButton(_help); + + depressButton(num); + events.wait(6); + } + + restoreButton(num); +} + +void UserInterface::toggleButton(int num) { + Screen &screen = *_vm->_screen; + + if (_menuMode != (MenuMode)(num + 1)) { + _menuMode = (MenuMode)(num + 1); + _oldKey = COMMANDS[num]; + _oldTemp = num; + + if (_keyboardInput) { + if (_oldHelp != -1 && _oldHelp != num) + restoreButton(_oldHelp); + if (_help != -1 && _help != num) + restoreButton(_help); + + _keyboardInput = false; + + ImageFrame &frame = (*_controls)[num]; + Common::Point pt(MENU_POINTS[num][0], MENU_POINTS[num][1]); + screen._backBuffer1.transBlitFrom(frame, pt); + screen.slamArea(pt.x, pt.y, pt.x + frame._width, pt.y + frame._height); + } + } else { + _menuMode = STD_MODE; + _oldKey = -1; + restoreButton(num); + } +} + +void UserInterface::clearInfo() { + if (_infoFlag) { + _vm->_screen->vgaBar(Common::Rect(16, INFO_LINE, SHERLOCK_SCREEN_WIDTH - 19, + INFO_LINE + 10), INFO_BLACK); + _infoFlag = false; + _oldLook = -1; + } +} + +void UserInterface::clearWindow() { + if (_windowOpen) { + _vm->_screen->vgaBar(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + } +} + +void UserInterface::whileMenuCounter() { + if (!(--_menuCounter) || _vm->_events->checkInput()) { + _menuCounter = 0; + _infoFlag = true; + clearInfo(); + } +} + +void UserInterface::examine() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + Common::Point pt = events.mousePos(); + + if (pt.y < (CONTROLS_Y + 9)) { + Object &obj = scene._bgShapes[_bgFound]; + + if (obj._lookcAnim != 0) { + int canimSpeed = ((obj._lookcAnim & 0xe0) >> 5) + 1; + scene._cAnimFramePause = obj._lookFrames; + _cAnimStr = obj._examine; + _cNum = (obj._lookcAnim & 0x1f) - 1; + + scene.startCAnim(_cNum, canimSpeed); + } else if (obj._lookPosition.y != 0) { + // Need to walk to the object to be examined + people.walkToCoords(Common::Point(obj._lookPosition.x, obj._lookPosition.y * 100), obj._lookFacing); + } + + if (!talk._talkToAbort) { + _cAnimStr = obj._examine; + if (obj._lookFlag) + _vm->setFlags(obj._lookFlag); + } + } else { + // Looking at an inventory item + _cAnimStr = inv[_selector]._examine; + if (inv[_selector]._lookFlag) + _vm->setFlags(inv[_selector]._lookFlag); + } + + if (_invLookFlag) { + // Don't close the inventory window when starting an examine display, since its + // window will slide up to replace the inventory display + _windowOpen = false; + _menuMode = LOOK_MODE; + } + + if (!talk._talkToAbort) { + if (!scene._cAnimFramePause) + printObjectDesc(_cAnimStr, true); + else + // description was already printed in startCAnimation + scene._cAnimFramePause = 0; + } +} + +void UserInterface::lookScreen(const Common::Point &pt) { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + int temp; + Common::String tempStr; + + // Don't display anything for right button command + if ((events._rightPressed || events._rightPressed) && !events._pressed) + return; + + if (mousePos.y < CONTROLS_Y && (temp = _bgFound) != -1) { + if (temp != _oldLook) { + _infoFlag = true; + clearInfo(); + + if (temp < 1000) + tempStr = scene._bgShapes[temp]._description; + else + tempStr = scene._bgShapes[temp - 1000]._description; + + _infoFlag = true; + clearInfo(); + + // Only print description if there is one + if (!tempStr.empty() && tempStr[0] != ' ') { + // If inventory is active and an item is selected for a Use or Give action + if ((_menuMode == INV_MODE || _menuMode == USE_MODE || _menuMode == GIVE_MODE) && + (inv._invMode == INVMODE_USE || inv._invMode == INVMODE_GIVE)) { + int width1 = 0, width2 = 0; + int x, width; + if (inv._invMode == INVMODE_USE) { + // Using an object + x = width = screen.stringWidth("Use "); + + if (temp < 1000 && scene._bgShapes[temp]._aType != PERSON) + // It's not a person, so make it lowercase + tempStr.setChar(tolower(tempStr[0]), 0); + + x += screen.stringWidth(tempStr); + + // If we're using an inventory object, add in the width + // of the object name and the " on " + if (_selector != -1) { + width1 = screen.stringWidth(inv[_selector]._name); + x += width1; + width2 = screen.stringWidth(" on "); + x += width2; + } + + // If the line will be too long, keep cutting off characters + // until the string will fit + while (x > 280) { + x -= screen.charWidth(tempStr.lastChar()); + tempStr.deleteLastChar(); + } + + int xStart = (SHERLOCK_SCREEN_WIDTH - x) / 2; + screen.print(Common::Point(xStart, INFO_LINE + 1), + INFO_FOREGROUND, "Use "); + + if (_selector != -1) { + screen.print(Common::Point(xStart + width, INFO_LINE + 1), + TALK_FOREGROUND, "%s", inv[_selector]._name.c_str()); + screen.print(Common::Point(xStart + width + width1, INFO_LINE + 1), + INFO_FOREGROUND, " on "); + screen.print(Common::Point(xStart + width + width1 + width2, INFO_LINE + 1), + INFO_FOREGROUND, "%s", tempStr.c_str()); + } else { + screen.print(Common::Point(xStart + width, INFO_LINE + 1), + INFO_FOREGROUND, "%s", tempStr.c_str()); + } + } else if (temp >= 0 && temp < 1000 && _selector != -1 && + scene._bgShapes[temp]._aType == PERSON) { + // Giving an object to a person + width1 = screen.stringWidth(inv[_selector]._name); + x = width = screen.stringWidth("Give "); + x += width1; + width2 = screen.stringWidth(" to "); + x += width2; + x += screen.stringWidth(tempStr); + + // Ensure string will fit on-screen + while (x > 280) { + x -= screen.charWidth(tempStr.lastChar()); + tempStr.deleteLastChar(); + } + + int xStart = (SHERLOCK_SCREEN_WIDTH - x) / 2; + screen.print(Common::Point(xStart, INFO_LINE + 1), + INFO_FOREGROUND, "Give "); + screen.print(Common::Point(xStart + width, INFO_LINE + 1), + TALK_FOREGROUND, "%s", inv[_selector]._name.c_str()); + screen.print(Common::Point(xStart + width + width1, INFO_LINE + 1), + INFO_FOREGROUND, " to "); + screen.print(Common::Point(xStart + width + width1 + width2, INFO_LINE + 1), + INFO_FOREGROUND, "%s", tempStr.c_str()); + } + } else { + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", tempStr.c_str()); + } + + _infoFlag = true; + _oldLook = temp; + } + } + } else { + clearInfo(); + } +} + +void UserInterface::lookInv() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + + if (mousePos.x > 15 && mousePos.x < 314 && mousePos.y > (CONTROLS_Y1 + 11) + && mousePos.y < (SHERLOCK_SCREEN_HEIGHT - 2)) { + int temp = (mousePos.x - 6) / 52 + inv._invIndex; + if (temp < inv._holdings) { + if (temp < inv._holdings) { + clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, + "%s", inv[temp]._description.c_str()); + _infoFlag = true; + _oldLook = temp; + } + } else { + clearInfo(); + } + } else { + clearInfo(); + } +} + +void UserInterface::doEnvControl() { + Events &events = *_vm->_events; + SaveManager &saves = *_vm->_saves; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + Common::Point mousePos = events.mousePos(); + static const char ENV_COMMANDS[7] = "ELSUDQ"; + + byte color; + + _key = _oldKey = -1; + _keyboardInput = false; + int found = saves.getHighlightedButton(); + + if (events._pressed || events._released) { + events.clearKeyboard(); + + // Check for a filename entry being highlighted + if ((events._pressed || events._released) && mousePos.y > (CONTROLS_Y + 10)) { + int found1 = 0; + for (_selector = 0; (_selector < ONSCREEN_FILES_COUNT) && !found1; ++_selector) + if (mousePos.y > (CONTROLS_Y + 11 + _selector * 10) && mousePos.y < (CONTROLS_Y + 21 + _selector * 10)) + found1 = 1; + + if (_selector + saves._savegameIndex - 1 < MAX_SAVEGAME_SLOTS + (saves._envMode != SAVEMODE_LOAD)) + _selector = _selector + saves._savegameIndex - 1; + else + _selector = -1; + + if (!found1) + _selector = -1; + } + + // Handle selecting buttons, if any + saves.highlightButtons(found); + + if (found == 0 || found == 5) + saves._envMode = SAVEMODE_NONE; + } + + if (_keyPress) { + _key = toupper(_keyPress); + + // Escape _key will close the dialog + if (_key == Common::KEYCODE_ESCAPE) + _key = 'E'; + + if (_key == 'E' || _key == 'L' || _key == 'S' || _key == 'U' || _key == 'D' || _key == 'Q') { + const char *chP = strchr(ENV_COMMANDS, _key); + int btnIndex = !chP ? -1 : chP - ENV_COMMANDS; + saves.highlightButtons(btnIndex); + _keyboardInput = true; + + if (_key == 'E' || _key == 'Q') { + saves._envMode = SAVEMODE_NONE; + } else if (_key >= '1' && _key <= '9') { + _keyboardInput = true; + _selector = _key - '1'; + if (_selector >= MAX_SAVEGAME_SLOTS + (saves._envMode == SAVEMODE_LOAD ? 0 : 1)) + _selector = -1; + + if (saves.checkGameOnScreen(_selector)) + _oldSelector = _selector; + } else { + _selector = -1; + } + } + } + + if (_selector != _oldSelector) { + if (_oldSelector != -1 && _oldSelector >= saves._savegameIndex && _oldSelector < (saves._savegameIndex + ONSCREEN_FILES_COUNT)) { + screen.print(Common::Point(6, CONTROLS_Y + 12 + (_oldSelector - saves._savegameIndex) * 10), + INV_FOREGROUND, "%d.", _oldSelector + 1); + screen.print(Common::Point(24, CONTROLS_Y + 12 + (_oldSelector - saves._savegameIndex) * 10), + INV_FOREGROUND, "%s", saves._savegames[_oldSelector].c_str()); + } + + if (_selector != -1) { + screen.print(Common::Point(6, CONTROLS_Y + 12 + (_selector - saves._savegameIndex) * 10), + TALK_FOREGROUND, "%d.", _selector + 1); + screen.print(Common::Point(24, CONTROLS_Y + 12 + (_selector - saves._savegameIndex) * 10), + TALK_FOREGROUND, "%s", saves._savegames[_selector].c_str()); + } + + _oldSelector = _selector; + } + + if (events._released || _keyboardInput) { + if ((found == 0 && events._released) || _key == 'E') { + banishWindow(); + _windowBounds.top = CONTROLS_Y1; + + events._pressed = events._released = _keyboardInput = false; + _keyPress = '\0'; + } else if ((found == 1 && events._released) || _key == 'L') { + saves._envMode = SAVEMODE_LOAD; + if (_selector != -1) { + saves.loadGame(_selector + 1); + } + } else if ((found == 2 && events._released) || _key == 'S') { + saves._envMode = SAVEMODE_SAVE; + if (_selector != -1) { + if (saves.checkGameOnScreen(_selector)) + _oldSelector = _selector; + + if (saves.promptForDescription(_selector)) { + saves.saveGame(_selector + 1, saves._savegames[_selector]); + + banishWindow(1); + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = -1; + _keyPress = '\0'; + _keyboardInput = false; + } else { + if (!talk._talkToAbort) { + screen._backBuffer1.fillRect(Common::Rect(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10, + SHERLOCK_SCREEN_WIDTH - 2, CONTROLS_Y + 20 + (_selector - saves._savegameIndex) * 10), INV_BACKGROUND); + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10), INV_FOREGROUND, + "%d.", _selector + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10), INV_FOREGROUND, + "%s", saves._savegames[_selector].c_str()); + + screen.slamArea(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10, 311, 10); + _selector = _oldSelector = -1; + } + } + } + } else if (((found == 3 && events._released) || _key == 'U') && saves._savegameIndex) { + bool moreKeys; + do { + saves._savegameIndex--; + screen._backBuffer1.fillRect(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 1), INV_BACKGROUND); + + for (int idx = saves._savegameIndex; idx < (saves._savegameIndex + ONSCREEN_FILES_COUNT); ++idx) { + color = INV_FOREGROUND; + if (idx == _selector && idx >= saves._savegameIndex && idx < (saves._savegameIndex + ONSCREEN_FILES_COUNT)) + color = TALK_FOREGROUND; + + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (idx - saves._savegameIndex) * 10), color, "%d.", idx + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (idx - saves._savegameIndex) * 10), color, "%s", saves._savegames[idx].c_str()); + } + + screen.slamRect(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, SHERLOCK_SCREEN_HEIGHT)); + + color = !saves._savegameIndex ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), color, true, "Up"); + color = (saves._savegameIndex == MAX_SAVEGAME_SLOTS - ONSCREEN_FILES_COUNT) ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), color, true, "Down"); + + // Check whether there are more pending U keys pressed + moreKeys = false; + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + _key = toupper(keyState.keycode); + moreKeys = _key == 'U'; + } + } while ((saves._savegameIndex) && moreKeys); + } else if (((found == 4 && events._released) || _key == 'D') && saves._savegameIndex < (MAX_SAVEGAME_SLOTS - ONSCREEN_FILES_COUNT)) { + bool moreKeys; + do { + saves._savegameIndex++; + screen._backBuffer1.fillRect(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 1), INV_BACKGROUND); + + for (int idx = saves._savegameIndex; idx < (saves._savegameIndex + ONSCREEN_FILES_COUNT); ++idx) { + if (idx == _selector && idx >= saves._savegameIndex && idx < (saves._savegameIndex + ONSCREEN_FILES_COUNT)) + color = TALK_FOREGROUND; + else + color = INV_FOREGROUND; + + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (idx - saves._savegameIndex) * 10), color, + "%d.", idx + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (idx - saves._savegameIndex) * 10), color, + "%s", saves._savegames[idx].c_str()); + } + + screen.slamRect(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, SHERLOCK_SCREEN_HEIGHT)); + + color = (!saves._savegameIndex) ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[3][2], CONTROLS_Y), color, true, "Up"); + + color = (saves._savegameIndex == MAX_SAVEGAME_SLOTS - ONSCREEN_FILES_COUNT) ? COMMAND_NULL : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(ENV_POINTS[4][2], CONTROLS_Y), color, true, "Down"); + + // Check whether there are more pending D keys pressed + moreKeys = false; + if (events.kbHit()) { + Common::KeyState keyState; + _key = toupper(keyState.keycode); + + moreKeys = _key == 'D'; + } + } while (saves._savegameIndex < (MAX_SAVEGAME_SLOTS - ONSCREEN_FILES_COUNT) && moreKeys); + } else if ((found == 5 && events._released) || _key == 'Q') { + clearWindow(); + screen.print(Common::Point(0, CONTROLS_Y + 20), INV_FOREGROUND, "Are you sure you wish to Quit ?"); + screen.vgaBar(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y + 10), BORDER_COLOR); + + screen.makeButton(Common::Rect(112, CONTROLS_Y, 160, CONTROLS_Y + 10), 136 - screen.stringWidth("Yes") / 2, "Yes"); + screen.makeButton(Common::Rect(161, CONTROLS_Y, 209, CONTROLS_Y + 10), 184 - screen.stringWidth("No") / 2, "No"); + screen.slamArea(112, CONTROLS_Y, 97, 10); + + do { + scene.doBgAnim(); + + if (talk._talkToAbort) + return; + + events.pollEventsAndWait(); + events.setButtonState(); + mousePos = events.mousePos(); + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + _key = toupper(keyState.keycode); + + if (_key == 'X' && (keyState.flags & Common::KBD_ALT) != 0) { + _vm->quitGame(); + events.pollEvents(); + return; + } + + if (_key == Common::KEYCODE_ESCAPE) + _key = 'N'; + + if (_key == Common::KEYCODE_RETURN || _key == ' ') { + events._pressed = false; + events._released = true; + events._oldButtons = 0; + _keyPress = '\0'; + } + } + + if (events._pressed || events._released) { + if (mousePos.x > 112 && mousePos.x < 159 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 9)) + color = COMMAND_HIGHLIGHTED; + else + color = COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(136, CONTROLS_Y), color, true, "Yes"); + + if (mousePos.x > 161 && mousePos.x < 208 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 9)) + color = COMMAND_HIGHLIGHTED; + else + color = COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(184, CONTROLS_Y), color, true, "No"); + } + + if (mousePos.x > 112 && mousePos.x < 159 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 9) && events._released) + _key = 'Y'; + + if (mousePos.x > 161 && mousePos.x < 208 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 9) && events._released) + _key = 'N'; + } while (!_vm->shouldQuit() && _key != 'Y' && _key != 'N'); + + if (_key == 'Y') { + _vm->quitGame(); + events.pollEvents(); + return; + } else { + screen.buttonPrint(Common::Point(184, CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "No"); + banishWindow(1); + _windowBounds.top = CONTROLS_Y1; + _key = -1; + } + } else { + if (_selector != -1) { + // Are we already in Load mode? + if (saves._envMode == SAVEMODE_LOAD) { + saves.loadGame(_selector + 1); + } else if (saves._envMode == SAVEMODE_SAVE || saves.isSlotEmpty(_selector)) { + // We're already in save mode, or pointing to an empty save slot + if (saves.checkGameOnScreen(_selector)) + _oldSelector = _selector; + + if (saves.promptForDescription(_selector)) { + saves.saveGame(_selector + 1, saves._savegames[_selector]); + banishWindow(); + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = -1; + _keyPress = '\0'; + _keyboardInput = false; + } else { + if (!talk._talkToAbort) { + screen._backBuffer1.fillRect(Common::Rect(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10, + 317, CONTROLS_Y + 20 + (_selector - saves._savegameIndex) * 10), INV_BACKGROUND); + screen.gPrint(Common::Point(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10), + INV_FOREGROUND, "%d.", _selector + 1); + screen.gPrint(Common::Point(24, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10), + INV_FOREGROUND, "%s", saves._savegames[_selector].c_str()); + screen.slamArea(6, CONTROLS_Y + 11 + (_selector - saves._savegameIndex) * 10, 311, 10); + _selector = _oldSelector = -1; + } + } + } + } + } + } +} + +void UserInterface::doInvControl() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + int colors[8]; + Common::Point mousePos = events.mousePos(); + + _key = _oldKey = -1; + _keyboardInput = false; + + // Check whether any inventory slot is highlighted + int found = -1; + Common::fill(&colors[0], &colors[8], (int)COMMAND_FOREGROUND); + for (int idx = 0; idx < 8; ++idx) { + Common::Rect r(INVENTORY_POINTS[idx][0], CONTROLS_Y1, + INVENTORY_POINTS[idx][1], CONTROLS_Y1 + 10); + if (r.contains(mousePos)) { + found = idx; + break; + } + } + + if (events._pressed || events._released) { + events.clearKeyboard(); + + if (found != -1) + // If a slot highlighted, set its color + colors[found] = COMMAND_HIGHLIGHTED; + screen.buttonPrint(Common::Point(INVENTORY_POINTS[0][2], CONTROLS_Y1), colors[0], true, "Exit"); + + if (found >= 0 && found <= 3) { + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), colors[1], true, "Look"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), colors[2], true, "Use"); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), colors[3], true, "Give"); + inv._invMode = (InvMode)found; + _selector = -1; + } + + if (inv._invIndex) { + screen.print(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1 + 1), colors[4], "^^"); + screen.print(Common::Point(INVENTORY_POINTS[5][2], CONTROLS_Y1 + 1), colors[5], "^"); + } + + if ((inv._holdings - inv._invIndex) > 6) { + screen.print(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1 + 1), colors[6], "_"); + screen.print(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1 + 1), colors[7], "__"); + } + + bool flag = false; + if (inv._invMode == INVMODE_LOOK || inv._invMode == INVMODE_USE || inv._invMode == INVMODE_GIVE) { + Common::Rect r(15, CONTROLS_Y1 + 11, 314, SHERLOCK_SCREEN_HEIGHT - 2); + if (r.contains(mousePos)) { + _selector = (mousePos.x - 6) / 52 + inv._invIndex; + if (_selector < inv._holdings) + flag = true; + } + } + + if (!flag && mousePos.y >(CONTROLS_Y1 + 11)) + _selector = -1; + } + + if (_keyPress) { + _key = toupper(_keyPress); + + if (_key == Common::KEYCODE_ESCAPE) + // Escape will also 'E'xit out of inventory display + _key = 'E'; + + if (_key == 'E' || _key == 'L' || _key == 'U' || _key == 'G' + || _key == '-' || _key == '+') { + InvMode temp = inv._invMode; + + const char *chP = strchr(INVENTORY_COMMANDS, _key); + inv._invMode = !chP ? INVMODE_INVALID : (InvMode)(chP - INVENTORY_COMMANDS); + inv.invCommands(true); + + inv._invMode = temp; + _keyboardInput = true; + if (_key == 'E') + inv._invMode = INVMODE_EXIT; + _selector = -1; + } else { + _selector = -1; + } + } + + if (_selector != _oldSelector) { + if (_oldSelector != -1) { + // Un-highlight + if (_oldSelector >= inv._invIndex && _oldSelector < (inv._invIndex + 6)) + inv.highlight(_oldSelector, BUTTON_MIDDLE); + } + + if (_selector != -1) + inv.highlight(_selector, 235); + + _oldSelector = _selector; + } + + if (events._released || _keyboardInput) { + if ((found == 0 && events._released) || _key == 'E') { + inv.freeInv(); + _infoFlag = true; + clearInfo(); + banishWindow(false); + _key = -1; + events.clearEvents(); + events.setCursor(ARROW); + } else if ((found == 1 && events._released) || (_key == 'L')) { + inv._invMode = INVMODE_LOOK; + } else if ((found == 2 && events._released) || (_key == 'U')) { + inv._invMode = INVMODE_USE; + } else if ((found == 3 && events._released) || (_key == 'G')) { + inv._invMode = INVMODE_GIVE; + } else if (((found == 4 && events._released) || _key == ',') && inv._invIndex) { + if (inv._invIndex >= 6) + inv._invIndex -= 6; + else + inv._invIndex = 0; + + screen.print(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1 + 1), + COMMAND_HIGHLIGHTED, "^^"); + inv.freeGraphics(); + inv.loadGraphics(); + inv.putInv(SLAM_DISPLAY); + inv.invCommands(true); + } else if (((found == 5 && events._released) || _key == '-') && inv._invIndex > 0) { + --inv._invIndex; + screen.print(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1 + 1), COMMAND_HIGHLIGHTED, "^"); + inv.freeGraphics(); + inv.loadGraphics(); + inv.putInv(SLAM_DISPLAY); + inv.invCommands(true); + } else if (((found == 6 && events._released) || _key == '+') && (inv._holdings - inv._invIndex) > 6) { + ++inv._invIndex; + screen.print(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1 + 1), COMMAND_HIGHLIGHTED, "_"); + inv.freeGraphics(); + inv.loadGraphics(); + inv.putInv(SLAM_DISPLAY); + inv.invCommands(true); + } else if (((found == 7 && events._released) || _key == '.') && (inv._holdings - inv._invIndex) > 6) { + inv._invIndex += 6; + if ((inv._holdings - 6) < inv._invIndex) + inv._invIndex = inv._holdings - 6; + + screen.print(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1 + 1), COMMAND_HIGHLIGHTED, "_"); + inv.freeGraphics(); + inv.loadGraphics(); + inv.putInv(SLAM_DISPLAY); + inv.invCommands(true); + } else { + // If something is being given, make sure it's being given to a person + if (inv._invMode == INVMODE_GIVE) { + if (_bgFound != -1 && scene._bgShapes[_bgFound]._aType == PERSON) + _find = _bgFound; + else + _find = -1; + } else { + _find = _bgFound; + } + + if ((mousePos.y < CONTROLS_Y1) && (inv._invMode == INVMODE_LOOK) && (_find >= 0) && (_find < 1000)) { + if (!scene._bgShapes[_find]._examine.empty() && + scene._bgShapes[_find]._examine[0] >= ' ') + inv.refreshInv(); + } else if (_selector != -1 || _find >= 0) { + // Selector is the inventory object that was clicked on, or selected. + // If it's -1, then no inventory item is highlighted yet. Otherwise, + // an object in the scene has been clicked. + + if (_selector != -1 && inv._invMode == INVMODE_LOOK + && mousePos.y >(CONTROLS_Y1 + 11)) + inv.refreshInv(); + + if (talk._talkToAbort) + return; + + // Now check for the Use and Give actions. If inv_mode is INVMODE_GIVE, + // that means GIVE is in effect, _selector is the object being + // given, and _find is the target. + // The same applies to USE, except if _selector is -1, then USE + // is being tried on an object in the scene without an inventory + // object being highlighted first. + + if ((inv._invMode == INVMODE_USE || (_selector != -1 && inv._invMode == INVMODE_GIVE)) && _find >= 0) { + events._pressed = events._released = false; + _infoFlag = true; + clearInfo(); + + int tempSel = _selector; // Save the selector + _selector = -1; + + inv.putInv(SLAM_DISPLAY); + _selector = tempSel; // Restore it + InvMode tempMode = inv._invMode; + inv._invMode = INVMODE_USE55; + inv.invCommands(true); + + _infoFlag = true; + clearInfo(); + banishWindow(false); + _key = -1; + + inv.freeInv(); + + bool giveFl = (tempMode >= INVMODE_GIVE); + if (_selector >= 0) + // Use/Give inv object with scene object + checkUseAction(&scene._bgShapes[_find]._use[0], inv[_selector]._name, MUSE, _find, giveFl); + else + // Now inv object has been highlighted + checkUseAction(&scene._bgShapes[_find]._use[0], "*SELF*", MUSE, _find, giveFl); + + _selector = _oldSelector = -1; + } + } + } + } +} + +void UserInterface::doLookControl() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Screen &screen = *_vm->_screen; + + _key = _oldKey = -1; + _keyboardInput = (_keyPress != '\0'); + + if (events._released || events._rightReleased || _keyboardInput) { + // Is an inventory object being looked at? + if (!_invLookFlag) { + // Is there any remaining text to display? + if (!_descStr.empty()) { + printObjectDesc(_descStr, false); + } else if (!_lookHelp) { + // Need to close the window and depress the Look button + Common::Point pt(MENU_POINTS[0][0], MENU_POINTS[0][1]); + screen._backBuffer2.blitFrom((*_controls)[0], pt); + banishWindow(true); + + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = COMMANDS[LOOK_MODE - 1]; + _temp = _oldTemp = 0; + _menuMode = LOOK_MODE; + events.clearEvents(); + + // Restore UI + drawInterface(); + } else { + events.setCursor(ARROW); + banishWindow(true); + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = -1; + _temp = _oldTemp = 0; + _menuMode = STD_MODE; + events.clearEvents(); + } + } else { + // Looking at an inventory object + // Backup the user interface + Surface tempSurface(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT - CONTROLS_Y1); + tempSurface.blitFrom(screen._backBuffer2, Common::Point(0, 0), + Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + inv.drawInventory(INVENTORY_DONT_DISPLAY); + banishWindow(true); + + // Restore the ui + screen._backBuffer2.blitFrom(tempSurface, Common::Point(0, CONTROLS_Y1)); + + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = COMMANDS[LOOK_MODE - 1]; + _temp = _oldTemp = 0; + events.clearEvents(); + _invLookFlag = false; + _menuMode = INV_MODE; + _windowOpen = true; + } + } +} + +void UserInterface::doMainControl() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + SaveManager &saves = *_vm->_saves; + Common::Point pt = events.mousePos(); + + if ((events._pressed || events._released) && pt.y > CONTROLS_Y) { + events.clearKeyboard(); + _key = -1; + + // Check whether the mouse is in any of the command areas + for (_temp = 0; (_temp < 12) && (_key == -1); ++_temp) { + Common::Rect r(MENU_POINTS[_temp][0], MENU_POINTS[_temp][1], + MENU_POINTS[_temp][2], MENU_POINTS[_temp][3]); + if (r.contains(pt)) + _key = COMMANDS[_temp]; + } + --_temp; + } else if (_keyPress) { + // Keyboard control + _keyboardInput = true; + + if (_keyPress >= 'A' && _keyPress <= 'Z') { + const char *c = strchr(COMMANDS, _keyPress); + _temp = !c ? 12 : c - COMMANDS; + } else { + _temp = 12; + } + + if (_temp == 12) + _key = -1; + + if (events._rightPressed) { + _temp = 12; + _key = -1; + } + } else if (!events._released) { + _key = -1; + } + + // Check if the button being pointed to has changed + if (_oldKey != _key && !_windowOpen) { + // Clear the info line + _infoFlag = true; + clearInfo(); + + // If there was an old button selected, restore it + if (_oldKey != -1) { + _menuMode = STD_MODE; + restoreButton(_oldTemp); + } + + // If a new button is being pointed to, highlight it + if (_key != -1 && _temp < 12 && !_keyboardInput) + depressButton(_temp); + + // Save the new button selection + _oldKey = _key; + _oldTemp = _temp; + } + + if (!events._pressed && !_windowOpen) { + switch (_key) { + case 'L': + toggleButton(0); + break; + case 'M': + toggleButton(1); + break; + case 'T': + toggleButton(2); + break; + case 'P': + toggleButton(3); + break; + case 'O': + toggleButton(4); + break; + case 'C': + toggleButton(5); + break; + case 'I': + pushButton(6); + _selector = _oldSelector = -1; + _menuMode = INV_MODE; + inv.drawInventory(PLAIN_INVENTORY); + break; + case 'U': + pushButton(7); + _selector = _oldSelector = -1; + _menuMode = USE_MODE; + inv.drawInventory(USE_INVENTORY_MODE); + break; + case 'G': + pushButton(8); + _selector = _oldSelector = -1; + _menuMode = GIVE_MODE; + inv.drawInventory(GIVE_INVENTORY_MODE); + break; + case 'J': + pushButton(9); + _menuMode = JOURNAL_MODE; + journalControl(); + break; + case 'F': + pushButton(10); + + // Create a thumbnail of the current screen before the files dialog is shown, in case + // the user saves the game + saves.createThumbnail(); + + _selector = _oldSelector = -1; + + if (_vm->_showOriginalSavesDialog) { + // Show the original dialog + _menuMode = FILES_MODE; + saves.drawInterface(); + _windowOpen = true; + } else { + // Show the ScummVM GMM instead + _vm->_canLoadSave = true; + _vm->openMainMenuDialog(); + _vm->_canLoadSave = false; + } + break; + case 'S': + pushButton(11); + _menuMode = SETUP_MODE; + Settings::show(_vm); + break; + default: + break; + } + + _help = _oldHelp = _oldBgFound = -1; + } +} + +void UserInterface::doMiscControl(int allowed) { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + if (events._released) { + _temp = _bgFound; + if (_bgFound != -1) { + // Only allow pointing to objects, not people + if (_bgFound < 1000) { + events.clearEvents(); + Object &obj = scene._bgShapes[_bgFound]; + + switch (allowed) { + case ALLOW_OPEN: + checkAction(obj._aOpen, MOPEN, _temp); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(OPEN_MODE - 1); + _key = _oldKey = -1; + } + break; + + case ALLOW_CLOSE: + checkAction(obj._aClose, MCLOSE, _temp); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(CLOSE_MODE - 1); + _key = _oldKey = -1; + } + break; + + case ALLOW_MOVE: + checkAction(obj._aMove, MMOVE, _temp); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(MOVE_MODE - 1); + _key = _oldKey = -1; + } + break; + + default: + break; + } + } + } + } +} + +void UserInterface::doPickControl() { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + if (events._released) { + if ((_temp = _bgFound) != -1) { + events.clearEvents(); + + // Don't allow characters to be picked up + if (_bgFound < 1000) { + scene._bgShapes[_bgFound].pickUpObject(MPICK); + + if (!talk._talkToAbort && _menuMode != TALK_MODE) { + _key = _oldKey = -1; + _menuMode = STD_MODE; + restoreButton(PICKUP_MODE - 1); + } + } + } + } +} + +void UserInterface::doTalkControl() { + Events &events = *_vm->_events; + Journal &journal = *_vm->_journal; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + Talk &talk = *_vm->_talk; + Common::Point mousePos = events.mousePos(); + + _key = _oldKey = -1; + _keyboardInput = false; + + if (events._pressed || events._released) { + events.clearKeyboard(); + + // Handle button printing + if (mousePos.x > 99 && mousePos.x < 138 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 10) && !_endKeyActive) + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Exit"); + else if (_endKeyActive) + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_FOREGROUND, true, "Exit"); + + if (mousePos.x > 140 && mousePos.x < 170 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 10) && talk._moreTalkUp) + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Up"); + else if (talk._moreTalkUp) + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, true, "Up"); + + if (mousePos.x > 181&& mousePos.x < 220 && mousePos.y > CONTROLS_Y && mousePos.y < (CONTROLS_Y + 10) && talk._moreTalkDown) + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_HIGHLIGHTED, true, "Down"); + else if (talk._moreTalkDown) + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_FOREGROUND, true, "Down"); + + bool found = false; + for (_selector = talk._talkIndex; _selector < (int)talk._statements.size() && !found; ++_selector) { + if (mousePos.y > talk._statements[_selector]._talkPos.top && + mousePos.y < talk._statements[_selector]._talkPos.bottom) + found = true; + } + --_selector; + if (!found) + _selector = -1; + } + + if (_keyPress) { + _key = toupper(_keyPress); + if (_key == Common::KEYCODE_ESCAPE) + _key = 'E'; + + // Check for number press indicating reply line + if (_key >= '1' && _key <= ('1' + (int)talk._statements.size() - 1)) { + for (uint idx = 0; idx < talk._statements.size(); ++idx) { + if (talk._statements[idx]._talkMap == (_key - '1')) { + // Found the given statement + _selector = idx; + _key = -1; + _keyboardInput = true; + break; + } + } + } else if (_key == 'E' || _key == 'U' || _key == 'D') { + _keyboardInput = true; + } else { + _selector = -1; + } + } + + if (_selector != _oldSelector) { + // Remove highlighting from previous line, if any + if (_oldSelector != -1) { + if (!((talk._talkHistory[talk._converseNum][_oldSelector] >> (_oldSelector & 7)) & 1)) + talk.talkLine(_oldSelector, talk._statements[_oldSelector]._talkMap, INV_FOREGROUND, + talk._statements[_oldSelector]._talkPos.top, true); + else + talk.talkLine(_oldSelector, talk._statements[_oldSelector]._talkMap, TALK_NULL, + talk._statements[_oldSelector]._talkPos.top, true); + } + + // Add highlighting to new line, if any + if (_selector != -1) + talk.talkLine(_selector, talk._statements[_selector]._talkMap, TALK_FOREGROUND, + talk._statements[_selector]._talkPos.top, true); + + _oldSelector = _selector; + } + + if (events._released || _keyboardInput) { + if (((Common::Rect(99, CONTROLS_Y, 138, CONTROLS_Y + 10).contains(mousePos) && events._released) + || _key == 'E') && _endKeyActive) { + talk.freeTalkVars(); + talk.pullSequence(); + banishWindow(); + _windowBounds.top = CONTROLS_Y1; + } else if (((Common::Rect(140, CONTROLS_Y, 179, CONTROLS_Y + 10).contains(mousePos) && events._released) + || _key == 'U') && talk._moreTalkUp) { + while (talk._statements[--talk._talkIndex]._talkMap == -1) + ; + screen._backBuffer1.fillRect(Common::Rect(5, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 1), INV_BACKGROUND); + talk.displayTalk(false); + + screen.slamRect(Common::Rect(5, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH - 5, SHERLOCK_SCREEN_HEIGHT - 2)); + } else if (((Common::Rect(181, CONTROLS_Y, 220, CONTROLS_Y + 10).contains(mousePos) && events._released) + || _key == 'D') && talk._moreTalkDown) { + do { + ++talk._talkIndex; + } while (talk._talkIndex < (int)talk._statements.size() && talk._statements[talk._talkIndex]._talkMap == -1); + + screen._backBuffer1.fillRect(Common::Rect(5, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 1), INV_BACKGROUND); + talk.displayTalk(false); + + screen.slamRect(Common::Rect(5, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH - 5, SHERLOCK_SCREEN_HEIGHT - 2)); + } else if (_selector != -1) { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, true, "Exit"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, true, "Up"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, true, "Down"); + + // If the reply is new, add it to the journal + if (!talk._talkHistory[talk._converseNum][_selector]) { + journal.record(talk._converseNum, _selector); + + // Add any Holmes point to Holmes' total, if any + if (talk._statements[_selector]._quotient) + people._holmesQuotient += talk._statements[_selector]._quotient; + } + + // Flag the response as having been used + talk._talkHistory[talk._converseNum][_selector] = true; + + clearWindow(); + screen.print(Common::Point(16, CONTROLS_Y + 12), TALK_FOREGROUND, "Sherlock Holmes"); + talk.talkLine(_selector + 128, talk._statements[_selector]._talkMap, COMMAND_FOREGROUND, CONTROLS_Y + 21, true); + + switch (talk._statements[_selector]._portraitSide & 3) { + case 0: + case 1: + people._portraitSide = 20; + break; + case 2: + people._portraitSide = 220; + break; + case 3: + people._portraitSide = 120; + break; + } + + // Check for flipping Holmes + if (talk._statements[_selector]._portraitSide & REVERSE_DIRECTION) + people._holmesFlip = true; + + talk._speaker = 0; + people.setTalking(0); + + if (!talk._statements[_selector]._voiceFile.empty() && sound._voices) { + sound.playSound(talk._statements[_selector]._voiceFile, WAIT_RETURN_IMMEDIATELY); + + // Set voices as an indicator for waiting + sound._voices = 2; + sound._speechOn = *sound._soundIsOn; + } else { + sound._speechOn = false; + } + + talk.waitForMore(talk._statements[_selector]._statement.size()); + if (talk._talkToAbort) + return; + + people.clearTalking(); + if (talk._talkToAbort) + return; + + while (!_vm->shouldQuit()) { + talk._scriptSelect = _selector; + talk._speaker = talk._talkTo; + talk.doScript(talk._statements[_selector]._reply); + + if (!talk._talkToAbort) { + if (!talk._talkStealth) + clearWindow(); + + if (!talk._statements[_selector]._modified.empty()) { + for (uint idx = 0; idx < talk._statements[_selector]._modified.size(); ++idx) { + _vm->setFlags(talk._statements[_selector]._modified[idx]); + } + + talk.setTalkMap(); + } + + // Check for another linked talk file + Common::String linkFilename = talk._statements[_selector]._linkFile; + if (!linkFilename.empty() && !talk._scriptMoreFlag) { + talk.freeTalkVars(); + talk.loadTalkFile(linkFilename); + + // Find the first new statement + int select = _selector = _oldSelector = -1; + for (uint idx = 0; idx < talk._statements.size() && select == -1; ++idx) { + if (!talk._statements[idx]._talkMap) + select = talk._talkIndex = idx; + } + + // See if the new statement is a stealth reply + talk._talkStealth = talk._statements[select]._statement.hasPrefix("^") ? 2 : 0; + + // Is the new talk file a standard file, reply first file, or a stealth file + if (!talk._statements[select]._statement.hasPrefix("*") && + !talk._statements[select]._statement.hasPrefix("^")) { + // Not a reply first file, so display the new selections + if (_endKeyActive) + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_FOREGROUND, true, "Exit"); + else + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, true, "Exit"); + + talk.displayTalk(true); + events.setCursor(ARROW); + break; + } else { + _selector = select; + + if (!talk._talkHistory[talk._converseNum][_selector]) + journal.record(talk._converseNum, _selector); + + talk._talkHistory[talk._converseNum][_selector] = true; + } + } else { + talk.freeTalkVars(); + talk.pullSequence(); + banishWindow(); + _windowBounds.top = CONTROLS_Y1; + break; + } + } else { + break; + } + } + + events._pressed = events._released = false; + events._oldButtons = 0; + talk._talkStealth = 0; + + // If a script was pushed onto the script stack, restore it + if (!talk._scriptStack.empty()) { + ScriptStackEntry stackEntry = talk._scriptStack.pop(); + talk._scriptName = stackEntry._name; + talk._scriptSaveIndex = stackEntry._currentIndex; + talk._scriptSelect = stackEntry._select; + } + } + } +} + +void UserInterface::journalControl() { + Events &events = *_vm->_events; + Journal &journal = *_vm->_journal; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + bool doneFlag = false; + + // Draw the journal screen + journal.drawInterface(); + + // Handle journal events + do { + _key = -1; + events.setButtonState(); + + // Handle keypresses + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + if (keyState.keycode == Common::KEYCODE_x && (keyState.flags & Common::KBD_ALT)) { + _vm->quitGame(); + return; + } else if (keyState.keycode == Common::KEYCODE_e || keyState.keycode == Common::KEYCODE_ESCAPE) { + doneFlag = true; + } else { + _key = toupper(keyState.keycode); + } + } + + if (!doneFlag) + doneFlag = journal.handleEvents(_key); + } while (!_vm->shouldQuit() && !doneFlag); + + // Finish up + _infoFlag = _keyboardInput = false; + _keyPress = '\0'; + _windowOpen = false; + _windowBounds.top = CONTROLS_Y1; + _key = -1; + _menuMode = STD_MODE; + + // Reset the palette + screen.setPalette(screen._cMap); + + screen._backBuffer1.blitFrom(screen._backBuffer2); + scene.updateBackground(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); +} + +void UserInterface::printObjectDesc(const Common::String &str, bool firstTime) { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + + if (str.hasPrefix("_")) { + _lookScriptFlag = true; + events.setCursor(MAGNIFY); + int savedSelector = _selector; + talk.talkTo(str.c_str() + 1); + _lookScriptFlag = false; + + if (talk._talkToAbort) { + events.setCursor(ARROW); + return; + } + + // Check if looking at an inventory object + if (!_invLookFlag) { + // See if this look was called by a right button click or not + if (!_lookHelp) { + // If it wasn't a right button click, then we need depress + // the look button before we close the window. So save a copy of the + // menu area, and draw the controls onto it + Surface tempSurface((*_controls)[0]._frame.w, (*_controls)[0]._frame.h); + Common::Point pt(MENU_POINTS[0][0], MENU_POINTS[0][1]); + + tempSurface.blitFrom(screen._backBuffer2, Common::Point(0, 0), + Common::Rect(pt.x, pt.y, pt.x + tempSurface.w(), pt.y + tempSurface.h())); + screen._backBuffer2.transBlitFrom((*_controls)[0], pt); + + banishWindow(1); + events.setCursor(MAGNIFY); + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = COMMANDS[LOOK_MODE - 1]; + _temp = _oldTemp = 0; + _menuMode = LOOK_MODE; + events.clearEvents(); + + screen._backBuffer2.blitFrom(tempSurface, pt); + } else { + events.setCursor(ARROW); + banishWindow(true); + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = -1; + _temp = _oldTemp = 0; + _menuMode = STD_MODE; + _lookHelp = 0; + events.clearEvents(); + } + } else { + // Looking at an inventory object + _selector = _oldSelector = savedSelector; + + // Reload the inventory graphics and draw the inventory + inv.loadInv(); + inv.putInv(SLAM_SECONDARY_BUFFER); + inv.freeInv(); + banishWindow(1); + + _windowBounds.top = CONTROLS_Y1; + _key = _oldKey = COMMANDS[INV_MODE - 1]; + _temp = _oldTemp = 0; + events.clearEvents(); + + _invLookFlag = 0; + _menuMode = INV_MODE; + _windowOpen = true; + } + + return; + } + + Surface &bb = *screen._backBuffer; + if (firstTime) { + // Only draw the border on the first call + _infoFlag = true; + clearInfo(); + + bb.fillRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, + CONTROLS_Y1 + 10), BORDER_COLOR); + bb.fillRect(Common::Rect(0, CONTROLS_Y + 10, 1, SHERLOCK_SCREEN_HEIGHT - 1), + BORDER_COLOR); + bb.fillRect(Common::Rect(SHERLOCK_SCREEN_WIDTH - 2, CONTROLS_Y + 10, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + bb.fillRect(Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - 1, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT), BORDER_COLOR); + } + + // Clear background + bb.fillRect(Common::Rect(2, CONTROLS_Y + 10, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + + _windowBounds.top = CONTROLS_Y; + events.clearEvents(); + + // Loop through displaying up to five lines + bool endOfStr = false; + const char *msgP = str.c_str(); + for (int lineNum = 0; lineNum < ONSCREEN_FILES_COUNT && !endOfStr; ++lineNum) { + int width = 0; + const char *lineStartP = msgP; + + // Determine how much can be displayed on the line + do { + width += screen.charWidth(*msgP++); + } while (width < 300 && *msgP); + + if (*msgP) + --msgP; + else + endOfStr = true; + + // If the line needs to be wrapped, scan backwards to find + // the end of the previous word as a splitting point + if (width >= 300) { + while (*msgP != ' ') + --msgP; + endOfStr = false; + } + + // Print out the line + Common::String line(lineStartP, msgP); + screen.gPrint(Common::Point(16, CONTROLS_Y + 12 + lineNum * 9), + INV_FOREGROUND, "%s", line.c_str()); + + if (!endOfStr) + // Start next line at start of the nxet word after space + ++msgP; + } + + // Handle display depending on whether all the message was shown + if (!endOfStr) { + screen.makeButton(Common::Rect(46, CONTROLS_Y, 272, CONTROLS_Y + 10), + (SHERLOCK_SCREEN_WIDTH - screen.stringWidth(PRESS_KEY_FOR_MORE)) / 2, + PRESS_KEY_FOR_MORE); + screen.gPrint(Common::Point((SHERLOCK_SCREEN_WIDTH - + screen.stringWidth(PRESS_KEY_FOR_MORE)) / 2, CONTROLS_Y), + COMMAND_FOREGROUND, "P"); + _descStr = msgP; + } else { + screen.makeButton(Common::Rect(46, CONTROLS_Y, 272, CONTROLS_Y + 10), + (SHERLOCK_SCREEN_WIDTH - screen.stringWidth(PRESS_KEY_TO_CONTINUE)) / 2, + PRESS_KEY_TO_CONTINUE); + screen.gPrint(Common::Point((SHERLOCK_SCREEN_WIDTH - + screen.stringWidth(PRESS_KEY_TO_CONTINUE)) / 2, CONTROLS_Y), + COMMAND_FOREGROUND, "P"); + _descStr = ""; + } + + if (firstTime) { + if (!_slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + // Display the window + summonWindow(); + } + + _selector = _oldSelector = -1; + _windowOpen = true; + } else { + screen.slamRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT)); + } +} + +void UserInterface::printObjectDesc() { + printObjectDesc(_cAnimStr, true); +} + +void UserInterface::summonWindow(const Surface &bgSurface, bool slideUp) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + + if (_windowOpen) + // A window is already open, so can't open another one + return; + + if (slideUp) { + // Gradually slide up the display of the window + for (int idx = 1; idx <= bgSurface.h(); idx += 2) { + screen._backBuffer->blitFrom(bgSurface, Common::Point(0, SHERLOCK_SCREEN_HEIGHT - idx), + Common::Rect(0, 0, bgSurface.w(), idx)); + screen.slamRect(Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - idx, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + events.delay(10); + } + } else { + // Gradually slide down the display of the window + for (int idx = 1; idx <= bgSurface.h(); idx += 2) { + screen._backBuffer->blitFrom(bgSurface, + Common::Point(0, SHERLOCK_SCREEN_HEIGHT - bgSurface.h()), + Common::Rect(0, bgSurface.h() - idx, bgSurface.w(), bgSurface.h())); + screen.slamRect(Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - bgSurface.h(), + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT - bgSurface.h() + idx)); + + events.delay(10); + } + } + + // Final display of the entire window + screen._backBuffer->blitFrom(bgSurface, Common::Point(0, SHERLOCK_SCREEN_HEIGHT - bgSurface.h()), + Common::Rect(0, 0, bgSurface.w(), bgSurface.h())); + screen.slamArea(0, SHERLOCK_SCREEN_HEIGHT - bgSurface.h(), bgSurface.w(), bgSurface.h()); + + _windowOpen = true; +} + +void UserInterface::summonWindow(bool slideUp, int height) { + Screen &screen = *_vm->_screen; + + // Extract the window that's been drawn on the back buffer + Surface tempSurface(SHERLOCK_SCREEN_WIDTH, + (SHERLOCK_SCREEN_HEIGHT - height)); + Common::Rect r(0, height, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + tempSurface.blitFrom(screen._backBuffer1, Common::Point(0, 0), r); + + // Remove drawn window with original user interface + screen._backBuffer1.blitFrom(screen._backBuffer2, + Common::Point(0, height), r); + + // Display the window gradually on-screen + summonWindow(tempSurface, slideUp); +} + +void UserInterface::banishWindow(bool slideUp) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + + if (_windowOpen) { + if (slideUp || !_slideWindows) { + // Slide window down + // Only slide the window if the window style allows it + if (_slideWindows) { + for (int idx = 2; idx < (SHERLOCK_SCREEN_HEIGHT - CONTROLS_Y); idx += 2) { + // Shift the window down by 2 lines + byte *pSrc = (byte *)screen._backBuffer1.getBasePtr(0, CONTROLS_Y + idx - 2); + byte *pSrcEnd = (byte *)screen._backBuffer1.getBasePtr(0, SHERLOCK_SCREEN_HEIGHT - 2); + byte *pDest = (byte *)screen._backBuffer1.getBasePtr(0, SHERLOCK_SCREEN_HEIGHT); + Common::copy_backward(pSrc, pSrcEnd, pDest); + + // Restore lines from the ui in the secondary back buffer + screen._backBuffer1.blitFrom(screen._backBuffer2, + Common::Point(0, CONTROLS_Y), + Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, CONTROLS_Y + idx)); + + screen.slamArea(0, CONTROLS_Y + idx - 2, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT - CONTROLS_Y - idx + 2); + events.delay(10); + } + + // Restore final two old lines + screen._backBuffer1.blitFrom(screen._backBuffer2, + Common::Point(0, SHERLOCK_SCREEN_HEIGHT - 2), + Common::Rect(0, SHERLOCK_SCREEN_HEIGHT - 2, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.slamArea(0, SHERLOCK_SCREEN_HEIGHT - 2, SHERLOCK_SCREEN_WIDTH, 2); + } else { + // Restore old area to completely erase window + screen._backBuffer1.blitFrom(screen._backBuffer2, + Common::Point(0, CONTROLS_Y), + Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.slamRect(Common::Rect(0, CONTROLS_Y, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT)); + } + } else { + // Slide the original user interface up to cover the dialog + for (int idx = 1; idx < (SHERLOCK_SCREEN_HEIGHT - CONTROLS_Y1); idx += 2) { + byte *pSrc = (byte *)screen._backBuffer2.getBasePtr(0, CONTROLS_Y1); + byte *pSrcEnd = (byte *)screen._backBuffer2.getBasePtr(0, CONTROLS_Y1 + idx); + byte *pDest = (byte *)screen._backBuffer1.getBasePtr(0, SHERLOCK_SCREEN_HEIGHT - idx); + Common::copy(pSrc, pSrcEnd, pDest); + + screen.slamArea(0, SHERLOCK_SCREEN_HEIGHT - idx, SHERLOCK_SCREEN_WIDTH, + SHERLOCK_SCREEN_HEIGHT); + events.delay(10); + } + + // Show entire final area + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(0, CONTROLS_Y1), + Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + screen.slamRect(Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } + + _infoFlag = false; + _windowOpen = false; + } + + _menuMode = STD_MODE; +} + +void UserInterface::checkUseAction(const UseType *use, const Common::String &invName, + const char *const messages[], int objNum, bool giveMode) { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + bool printed = messages == nullptr; + + if (objNum >= 1000) { + // Holmes was specified, so do nothing + _infoFlag = true; + clearInfo(); + _infoFlag = true; + + // Display error message + _menuCounter = 30; + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "You can't do that to yourself."); + return; + } + + // Scan for target item + int targetNum = -1; + if (giveMode) { + for (int idx = 0; idx < USE_COUNT && targetNum == -1; ++idx) { + if ((use[idx]._target.equalsIgnoreCase("*GIVE*") || use[idx]._target.equalsIgnoreCase("*GIVEP*")) + && use[idx]._names[0].equalsIgnoreCase(invName)) { + // Found a match + targetNum = idx; + if (use[idx]._target.equalsIgnoreCase("*GIVE*")) + inv.deleteItemFromInventory(invName); + } + } + } else { + for (int idx = 0; idx < USE_COUNT && targetNum == -1; ++idx) { + if (use[idx]._target.equalsIgnoreCase(invName)) + targetNum = idx; + } + } + + if (targetNum != -1) { + // Found a target, so do the action + const UseType &action = use[targetNum]; + + events.setCursor(WAIT); + + if (action._useFlag) + _vm->setFlags(action._useFlag); + + if (action._cAnimNum != 99) { + if (action._cAnimNum == 0) + scene.startCAnim(9, action._cAnimSpeed); + else + scene.startCAnim(action._cAnimNum - 1, action._cAnimSpeed); + } + + if (!talk._talkToAbort) { + Object &obj = scene._bgShapes[objNum]; + for (int idx = 0; idx < NAMES_COUNT && !talk._talkToAbort; ++idx) { + if (obj.checkNameForCodes(action._names[idx], messages)) { + if (!talk._talkToAbort) + printed = true; + } + } + + // Print "Done..." as an ending, unless flagged for leaving scene or otherwise flagged + if (scene._goToScene != 1 && !printed && !talk._talkToAbort) { + _infoFlag = true; + clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "Done..."); + _menuCounter = 25; + } + } + } else { + // Couldn't find target, so print error + _infoFlag = true; + clearInfo(); + + if (giveMode) { + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "No, thank you."); + } else if (messages == nullptr) { + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "You can't do that."); + } else { + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", messages[0]); + } + + _infoFlag = true; + _menuCounter = 30; + } + + events.setCursor(ARROW); +} + +void UserInterface::checkAction(ActionType &action, const char *const messages[], int objNum) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + Common::Point pt(-1, -1); + + if (objNum >= 1000) + // Ignore actions done on characters + return; + + if (!action._cAnimSpeed) { + // Invalid action, to print error message + _infoFlag = true; + clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", messages[action._cAnimNum]); + _infoFlag = true; + + // Set how long to show the message + _menuCounter = 30; + } else { + Object &obj = scene._bgShapes[objNum]; + + int cAnimNum; + if (action._cAnimNum == 0) + // Really a 10 + cAnimNum = 9; + else + cAnimNum = action._cAnimNum - 1; + + int dir = -1; + if (action._cAnimNum != 99) { + CAnim &anim = scene._cAnim[cAnimNum]; + + if (action._cAnimNum != 99) { + if (action._cAnimSpeed & REVERSE_DIRECTION) { + pt = anim._teleportPos; + dir = anim._teleportDir; + } else { + pt = anim._goto; + dir = anim._gotoDir; + } + } + } else { + pt = Common::Point(-1, -1); + dir = -1; + } + + // Has a value, so do action + // Show wait cursor whilst walking to object and doing action + events.setCursor(WAIT); + bool printed = false; + + for (int nameIdx = 0; nameIdx < NAMES_COUNT; ++nameIdx) { + if (action._names[nameIdx].hasPrefix("*") && action._names[nameIdx].size() >= 2 + && toupper(action._names[nameIdx][1]) == 'W') { + if (obj.checkNameForCodes(Common::String(action._names[nameIdx].c_str() + 2), messages)) { + if (!talk._talkToAbort) + printed = true; + } + } + } + + bool doCAnim = true; + for (int nameIdx = 0; nameIdx < NAMES_COUNT; ++nameIdx) { + if (action._names[nameIdx].hasPrefix("*") && action._names[nameIdx].size() >= 2) { + char ch = toupper(action._names[nameIdx][1]); + + if (ch == 'T' || ch == 'B') { + printed = true; + if (pt.x != -1) + // Holmes needs to walk to object before the action is done + people.walkToCoords(pt, dir); + + if (!talk._talkToAbort) { + // Ensure Holmes is on the exact intended location + people[AL]._position = pt; + people[AL]._sequenceNumber = dir; + people.gotoStand(people[AL]); + + talk.talkTo(action._names[nameIdx].c_str() + 2); + if (ch == 'T') + doCAnim = false; + } + } + } + } + + if (doCAnim && !talk._talkToAbort) { + if (pt.x != -1) + // Holmes needs to walk to object before the action is done + people.walkToCoords(pt, dir); + } + + for (int nameIdx = 0; nameIdx < NAMES_COUNT; ++nameIdx) { + if (action._names[nameIdx].hasPrefix("*") && action._names[nameIdx].size() >= 2 + && toupper(action._names[nameIdx][1]) == 'F') { + if (obj.checkNameForCodes(action._names[nameIdx].c_str() + 2, messages)) { + if (!talk._talkToAbort) + printed = true; + } + } + } + + if (doCAnim && !talk._talkToAbort && action._cAnimNum != 99) + scene.startCAnim(cAnimNum, action._cAnimSpeed); + + if (!talk._talkToAbort) { + for (int nameIdx = 0; nameIdx < NAMES_COUNT && !talk._talkToAbort; ++nameIdx) { + if (obj.checkNameForCodes(action._names[nameIdx], messages)) { + if (!talk._talkToAbort) + printed = true; + } + } + + // Unless we're leaving the scene, print a "Done" message unless the printed flag has been set + if (scene._goToScene != 1 && !printed && !talk._talkToAbort) { + _infoFlag = true; + clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "Done..."); + + // Set how long to show the message + _menuCounter = 30; + } + } + } + + // Reset cursor back to arrow + events.setCursor(ARROW); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/user_interface.h b/engines/sherlock/user_interface.h new file mode 100644 index 0000000000..1f7b5feaab --- /dev/null +++ b/engines/sherlock/user_interface.h @@ -0,0 +1,260 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SHERLOCK_UI_H +#define SHERLOCK_UI_H + +#include "common/scummsys.h" +#include "common/events.h" +#include "sherlock/surface.h" +#include "sherlock/objects.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +#define CONTROLS_Y 138 +#define CONTROLS_Y1 151 + +enum MenuMode { + STD_MODE = 0, + LOOK_MODE = 1, + MOVE_MODE = 2, + TALK_MODE = 3, + PICKUP_MODE = 4, + OPEN_MODE = 5, + CLOSE_MODE = 6, + INV_MODE = 7, + USE_MODE = 8, + GIVE_MODE = 9, + JOURNAL_MODE = 10, + FILES_MODE = 11, + SETUP_MODE = 12 +}; + +extern const char COMMANDS[13]; +extern const int MENU_POINTS[12][4]; + +extern const int INVENTORY_POINTS[8][3]; +extern const char INVENTORY_COMMANDS[9]; +extern const char *const PRESS_KEY_FOR_MORE; +extern const char *const PRESS_KEY_TO_CONTINUE; + +class SherlockEngine; +class Inventory; +class Talk; +class UserInterface; + +class UserInterface { + friend class Inventory; + friend class Settings; + friend class Talk; +private: + SherlockEngine *_vm; + ImageFile *_controlPanel; + ImageFile *_controls; + int _bgFound; + int _oldBgFound; + char _keyPress; + int _lookHelp; + int _help, _oldHelp; + char _key, _oldKey; + int _temp, _oldTemp; + int _oldLook; + bool _keyboardInput; + bool _pause; + int _cNum; + int _selector, _oldSelector; + Common::String _cAnimStr; + bool _lookScriptFlag; + Common::Rect _windowBounds; + Common::String _descStr; + int _find; + int _oldUse; +private: + /** + * Draws the image for a user interface button in the down/pressed state. + */ + void depressButton(int num); + + /** + * If he mouse button is pressed, then calls depressButton to draw the button + * as pressed; if not, it will show it as released with a call to "restoreButton". + */ + void pushButton(int num); + + /** + * By the time this method has been called, the graphics for the button change + * have already been drawn. This simply takes care of switching the mode around + * accordingly + */ + void toggleButton(int num); + + /** + * Creates a text window and uses it to display the in-depth description + * of the highlighted object + */ + void examine(); + + /** + * Print the name of an object in the scene + */ + void lookScreen(const Common::Point &pt); + + /** + * Gets the item in the inventory the mouse is on and display's it's description + */ + void lookInv(); + + /** + * Handles input when the file list window is being displayed + */ + void doEnvControl(); + + /** + * Handle input whilst the inventory is active + */ + void doInvControl(); + + /** + * Handles waiting whilst an object's description window is open. + */ + void doLookControl(); + + /** + * Handles input until one of the user interface buttons/commands is selected + */ + void doMainControl(); + + /** + * Handles the input for the MOVE, OPEN, and CLOSE commands + */ + void doMiscControl(int allowed); + + /** + * Handles input for picking up items + */ + void doPickControl(); + + /** + * Handles input when in talk mode. It highlights the buttons and available statements, + * and handles allowing the user to click on them + */ + void doTalkControl(); + + /** + * Handles events when the Journal is active. + * @remarks Whilst this would in theory be better in the Journal class, since it displays in + * the user interface, it uses so many internal UI fields, that it sort of made some sense + * to put it in the UserInterface class. + */ + void journalControl(); + + /** + * Checks to see whether a USE action is valid on the given object + */ + void checkUseAction(const UseType *use, const Common::String &invName, const char *const messages[], + int objNum, bool giveMode); + + /** + * Called for OPEN, CLOSE, and MOVE actions are being done + */ + void checkAction(ActionType &action, const char *const messages[], int objNum); +public: + MenuMode _menuMode; + int _menuCounter; + bool _infoFlag; + bool _windowOpen; + bool _endKeyActive; + int _invLookFlag; + int _temp1; + bool _slideWindows; + bool _helpStyle; +public: + UserInterface(SherlockEngine *vm); + ~UserInterface(); + + /** + * Resets the user interface + */ + void reset(); + + /** + * Draw the user interface onto the screen's back buffers + */ + void drawInterface(int bufferNum = 3); + + /** + * Main input handler for the user interface + */ + void handleInput(); + + /** + * Clears the info line of the screen + */ + void clearInfo(); + + /** + * Clear any active text window + */ + void clearWindow(); + + /** + * Handles counting down whilst checking for input, then clears the info line. + */ + void whileMenuCounter(); + + /** + * Print the description of an object + */ + void printObjectDesc(const Common::String &str, bool firstTime); + + /** + * Print the previously selected object's decription + */ + void printObjectDesc(); + + /** + * Displays a passed window by gradually scrolling it vertically on-screen + */ + void summonWindow(const Surface &bgSurface, bool slideUp = true); + + /** + * Slide the window stored in the back buffer onto the screen + */ + void summonWindow(bool slideUp = true, int height = CONTROLS_Y); + + /** + * Close a currently open window + * @param flag 0 = slide old window down, 1 = slide prior UI back up + */ + void banishWindow(bool slideUp = true); + + /** + * Draws the image for the given user interface button in the up + * (not selected) position + */ + void restoreButton(int num); +}; + +} // End of namespace Sherlock + +#endif |