diff options
Diffstat (limited to 'engines/sherlock')
122 files changed, 40350 insertions, 0 deletions
diff --git a/engines/sherlock/animation.cpp b/engines/sherlock/animation.cpp new file mode 100644 index 0000000000..bbf7c913b7 --- /dev/null +++ b/engines/sherlock/animation.cpp @@ -0,0 +1,324 @@ +/* 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, bool intro, 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, intro); + + // 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 sampleFilename; + + if (!intro) { + // regular animation, append 1-digit number + sampleFilename = Common::String::format("%s%01d", filename.c_str(), soundNumber); + } else { + // intro animation, append 2-digit number + sampleFilename = Common::String::format("%s%02d", filename.c_str(), soundNumber); + } + + if (sound._voices) + sound.playSound(sampleFilename, 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(); +} + +bool Animation::play3DO(const Common::String &filename, bool intro, int minDelay, bool fadeFromGrey, + int speed) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Sound &sound = *_vm->_sound; + int soundNumber = 0; + + bool fadeActive = false; + uint16 fadeLimitColor = 0; + uint16 fadeLimitColorRed = 0; + uint16 fadeLimitColorGreen = 0; + uint16 fadeLimitColorBlue = 0; + + // Check for any any sound frames for the given animation + const int *soundFrames = checkForSoundFrames(filename, intro); + + // Add the VDX extension + Common::String indexName = "prologue/" + filename + ".3dx"; + + // Load the animation + Common::File *indexStream = new Common::File(); + + if (!indexStream->open(indexName)) { + warning("unable to open %s\n", indexName.c_str()); + return false; + } + + // Load initial image + Common::String graphicsName = "prologue/" + filename + ".3da"; + ImageFile3DO images(graphicsName, kImageFile3DOType_Animation); + + events.wait(minDelay); + + if (fadeFromGrey) { + fadeActive = true; + fadeLimitColor = 0xCE59; // RGB565: 25, 50, 25 -> "grey" + } + + int frameNumber = 0; + Common::Point pt; + bool skipped = false; + while (!_vm->shouldQuit()) { + // Get the next sprite to display + int imageFrame = indexStream->readSint16BE(); + + 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 = indexStream->readUint16BE(); + pt.y = indexStream->readUint16BE(); + } 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 + if (!fadeActive) { + screen.transBlitFrom(images[imageFrame]._frame, pt); + } else { + // Fade active, blit to backbuffer1 + screen._backBuffer1.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 (fadeActive) { + // process fading + screen.blitFrom3DOcolorLimit(fadeLimitColor); + + if (!fadeLimitColor) { + // we are at the end, so stop + fadeActive = false; + } else { + // decrease limit color + fadeLimitColorRed = fadeLimitColor & 0xF800; + fadeLimitColorGreen = fadeLimitColor & 0x07E0; + fadeLimitColorBlue = fadeLimitColor & 0x001F; + if (fadeLimitColorRed) + fadeLimitColor -= 0x0800; + if (fadeLimitColorGreen) + fadeLimitColor -= 0x0040; // -2 because we are using RGB565, sherlock uses RGB555 + if (fadeLimitColorBlue) + fadeLimitColor -= 0x0001; + } + } + + // Check if we've reached a frame with sound + if (frameNumber++ == *soundFrames) { + ++soundNumber; + ++soundFrames; + + Common::String sampleFilename; + + // append 1-digit number + sampleFilename = Common::String::format("prologue/sounds/%s%01d", filename.c_str(), soundNumber); + + if (sound._voices) + sound.playSound(sampleFilename, WAIT_RETURN_IMMEDIATELY, 100); // no sound library + } + 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 indexStream; + + 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, bool intro) { + const int *frames = &NO_FRAMES; + + if (!intro) { + // regular animation is playing + for (uint idx = 0; idx < _prologueNames.size(); ++idx) { + if (filename.equalsIgnoreCase(_prologueNames[idx])) { + frames = &_prologueFrames[idx][0]; + break; + } + } + } else { + // intro-animation is playing + 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..f3c95d4027 --- /dev/null +++ b/engines/sherlock/animation.h @@ -0,0 +1,86 @@ +/* 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, bool intro); +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, bool intro, int minDelay, int fade, bool setPalette, int speed); + + bool play3DO(const Common::String &filename, bool intro, int minDelay, bool fadeFromGrey, 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..2813a7eb69 --- /dev/null +++ b/engines/sherlock/debugger.cpp @@ -0,0 +1,138 @@ +/* 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" +#include "sherlock/music.h" +#include "sherlock/scalpel/3do/movie_decoder.h" +#include "sherlock/scalpel/scalpel_debugger.h" +#include "sherlock/tattoo/tattoo_debugger.h" +#include "audio/mixer.h" +#include "audio/decoders/aiff.h" +#include "audio/decoders/wave.h" + +namespace Sherlock { + +Debugger *Debugger::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_RoseTattoo) + return new Tattoo::TattooDebugger(vm); + else + return new Scalpel::ScalpelDebugger(vm); +} + +Debugger::Debugger(SherlockEngine *vm) : GUI::Debugger(), _vm(vm) { + _showAllLocations = LOC_DISABLED; + + registerCmd("continue", WRAP_METHOD(Debugger, cmdExit)); + registerCmd("scene", WRAP_METHOD(Debugger, cmdScene)); + registerCmd("song", WRAP_METHOD(Debugger, cmdSong)); + registerCmd("dumpfile", WRAP_METHOD(Debugger, cmdDumpFile)); + registerCmd("locations", WRAP_METHOD(Debugger, cmdLocations)); +} + +void Debugger::postEnter() { + if (!_3doPlayMovieFile.empty()) { + Scalpel3DOMoviePlay(_3doPlayMovieFile.c_str(), Common::Point(0, 0)); + + _3doPlayMovieFile.clear(); + } + + _vm->pauseEngine(false); +} + +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; + } +} + +bool Debugger::cmdSong(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: song <room>\n"); + return true; + } + + if (!_vm->_music->loadSong(strToInt(argv[1]))) { + debugPrintf("Invalid song number.\n"); + return true; + } + return false; +} + +bool Debugger::cmdDumpFile(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: dumpfile <resource name>\n"); + return true; + } + + Common::SeekableReadStream *s = _vm->_res->load(argv[1]); + if (!s) { + debugPrintf("Invalid resource.\n"); + return true; + } + + byte *buffer = new byte[s->size()]; + s->read(buffer, s->size()); + + Common::DumpFile dumpFile; + dumpFile.open(argv[1]); + + dumpFile.write(buffer, s->size()); + dumpFile.flush(); + dumpFile.close(); + + delete[] buffer; + + debugPrintf("Resource %s has been dumped to disk.\n", argv[1]); + + return true; +} + +bool Debugger::cmdLocations(int argc, const char **argv) { + _showAllLocations = LOC_REFRESH; + + debugPrintf("Now showing all map locations\n"); + return false; +} + + +} // End of namespace Sherlock diff --git a/engines/sherlock/debugger.h b/engines/sherlock/debugger.h new file mode 100644 index 0000000000..abc8ef012d --- /dev/null +++ b/engines/sherlock/debugger.h @@ -0,0 +1,76 @@ +/* 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; + +enum AllLocations { LOC_REFRESH = -1, LOC_DISABLED = 0, LOC_ALL = 1 }; + +class Debugger : public GUI::Debugger { +private: + /** + * 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); + + /** + * Plays a song + */ + bool cmdSong(int argc, const char **argv); + + /** + * Dumps a file to disk + */ + bool cmdDumpFile(int argc, const char **argv); + + /** + * Show all locations on the map + */ + bool cmdLocations(int argc, const char **argv); +protected: + SherlockEngine *_vm; + Common::String _3doPlayMovieFile; +public: + AllLocations _showAllLocations; +public: + Debugger(SherlockEngine *vm); + virtual ~Debugger() {} + static Debugger *init(SherlockEngine *vm); + + void postEnter(); +}; + +} // End of namespace Sherlock + +#endif /* SHERLOCK_DEBUGGER_H */ diff --git a/engines/sherlock/decompress.cpp b/engines/sherlock/decompress.cpp new file mode 100644 index 0000000000..8e02da3212 --- /dev/null +++ b/engines/sherlock/decompress.cpp @@ -0,0 +1,128 @@ +/* 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/decompress.h" + +namespace Sherlock { + +/** + * Decompresses an LZW compressed resource. If no outSize is specified, it will + * decompress the entire resource. If, however, an explicit size is specified, + * then it means we're already within a resource, and only want to decompress + * part of it. + */ +Common::SeekableReadStream *decompressLZ(Common::SeekableReadStream &source, int32 outSize) { + if (outSize == -1) { + source.seek(5); + outSize = source.readSint32LE(); + } + + byte lzWindow[4096]; + uint16 lzWindowPos; + uint16 cmd; + + byte *outBuffer = new byte[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; +} + + +/** + * Decompresses a Rose Tattoo resource + * +Common::SeekableReadStream *decompress32(Common::SeekableReadStream &source, int32 outSize) { + if (outSize == -1) { + outSize = source.readSint32LE(); + } + + byte lzWindow[8192]; + byte *outBuffer = new byte[outSize]; + byte *outBufferEnd = outBuffer + outSize; + Common::MemoryReadStream *outS = new Common::MemoryReadStream(outBuffer, outSize, DisposeAfterUse::YES); + + memset(lzWindow, 0xFF, 8192); + int lzWindowPos = 0xFEE; + int 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; +} +*/ + +} // namespace Sherlock diff --git a/engines/sherlock/detection.cpp b/engines/sherlock/detection.cpp new file mode 100644 index 0000000000..35a810efb1 --- /dev/null +++ b/engines/sherlock/detection.cpp @@ -0,0 +1,245 @@ +/* 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; +} + +Common::Language SherlockEngine::getLanguage() const { + return _gameDescription->desc.language; +} + +} // 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..991fc2055d --- /dev/null +++ b/engines/sherlock/detection_tables.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. + * + */ + +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 - German CD (from multilingual CD) + // Provided by m_kiewitz + { + "scalpel", + 0, { + {"talk.lib", 0, "40a5f9f37c0e0d2ad48d8f44d8e393c9", 284278}, + {"music.lib", 0, "68ae2f7684ecf903bd60a00bb6bae195", 366465}, + AD_LISTEND}, + Common::DE_DEU, + 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 - Spanish CD (from multilingual CD) + // Provided by m_kiewitz + { + "scalpel", + 0, { + {"talk.lib", 0, "27697804b637a7f3b77234bf16f15dce", 171419}, + {"music.lib", 0, "68ae2f7684ecf903bd60a00bb6bae195", 366465}, + AD_LISTEND}, + Common::ES_ESP, + 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 - English 3DO + { + "scalpel", + 0, + AD_ENTRY1s("talk.lib", "20f74a29f2db6475e85b029ac9fc03bc", 240610), + Common::EN_ANY, + Common::kPlatform3DO, + 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, + }, + + { + // Case of the Rose Tattoo - German CD + // Provided by m_kiewitz + { + "rosetattoo", + "CD", + AD_ENTRY1s("talk.lib", "5027aa72f0d263ed3b1c764a6c397911", 873864), + Common::DE_DEU, + 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..a8912f6f1e --- /dev/null +++ b/engines/sherlock/events.cpp @@ -0,0 +1,318 @@ +/* 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; + _firstPress = false; + + if (_vm->_interactiveFl) + loadCursors("rmouse.vgs"); +} + +Events::~Events() { + delete _cursorImages; +} + +void Events::loadCursors(const Common::String &filename) { + hideCursor(); + delete _cursorImages; + + if (!IS_3DO) { + // PC + _cursorImages = new ImageFile(filename); + } else { + // 3DO + _cursorImages = new ImageFile3DO(filename, kImageFile3DOType_RoomFormat); + } + _cursorId = INVALID_CURSOR; +} + +void Events::setCursor(CursorId cursorId) { + if (cursorId == _cursorId) + return; + + int hotspotX, hotspotY; + + if (cursorId == MAGNIFY) { + hotspotX = 8; + hotspotY = 8; + } else { + hotspotX = 0; + hotspotY = 0; + } + + // Set the cursor data + Graphics::Surface &s = (*_cursorImages)[cursorId]._frame; + + setCursor(s, hotspotX, hotspotY); + + _cursorId = cursorId; +} + +void Events::setCursor(const Graphics::Surface &src, int hotspotX, int hotspotY) { + _cursorId = INVALID_CURSOR; + _hotspotPos = Common::Point(hotspotX, hotspotY); + + if (!IS_3DO) { + // PC 8-bit palettized + CursorMan.replaceCursor(src.getPixels(), src.w, src.h, hotspotX, hotspotY, 0xff); + } else { + // 3DO RGB565 + CursorMan.replaceCursor(src.getPixels(), src.w, src.h, hotspotX, hotspotY, 0x0000, false, &src.format); + } + showCursor(); +} + +void Events::setCursor(CursorId cursorId, const Graphics::Surface &surface) { + _cursorId = cursorId; + + int hotspotX, hotspotY; + if (cursorId == MAGNIFY) { + hotspotX = 8; + hotspotY = 8; + } else { + hotspotX = 0; + hotspotY = 0; + } + + // Get the standard cursor frame + Graphics::Surface &surface2 = (*_cursorImages)[cursorId]._frame; + + // Form a single surface containing both frames + int maxWidth = MAX(surface.w, surface2.w); + Graphics::Surface s; + s.create(maxWidth, surface.h + surface2.h, Graphics::PixelFormat::createFormatCLUT8()); + s.fillRect(Common::Rect(0, 0, maxWidth, surface.h + surface2.h), TRANSPARENCY); + + s.copyRectToSurface(surface, (maxWidth - surface.w) / 2, 0, Common::Rect(0, 0, surface.w, surface.h)); + s.copyRectToSurface(surface2, (maxWidth - surface2.w) / 2, surface.h, Common::Rect(0, 0, surface2.w, surface2.h)); + + // Adjust hotspot position + hotspotX += (maxWidth - surface2.w) / 2; + hotspotY += surface.h; + + // Set the cursor + setCursor(s, hotspotX, hotspotY); +} + +void Events::animateCursorIfNeeded() { + if (_cursorId >= WAIT && _cursorId < (WAIT + 3)) { + CursorId newId = (_cursorId == WAIT + 2) ? WAIT : (CursorId)((int)_cursorId + 1); + setCursor(newId); + } +} + + +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); +} + +void Events::warpMouse(const Common::Point &pt) { + g_system->warpMouse(pt.x, pt.y); +} + +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::screenMousePos() const { + return g_system->getEventManager()->getMousePos(); +} + +Common::Point Events::mousePos() const { + return screenMousePos() + _vm->_screen->_currentScroll; +} + +Common::KeyState Events::getKey() { + return _pendingKeys.pop(); +} + +void Events::clearEvents() { + _pendingKeys.clear(); + _mouseButtons = 0; + _pressed = _released = false; + _rightPressed = _rightReleased = false; + _oldButtons = _oldRightButton = false; + _firstPress = 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 || _vm->shouldQuit())); + + 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 !_vm->shouldQuit(); + } +} + +void Events::setButtonState() { + _firstPress = ((_mouseButtons & 1) && !_pressed) || ((_mouseButtons & 2) && !_rightPressed); + + _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..93a5e54f81 --- /dev/null +++ b/engines/sherlock/events.h @@ -0,0 +1,194 @@ +/* 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/image_file.h" + +namespace Sherlock { + +#define GAME_FRAME_RATE 60 +#define GAME_FRAME_TIME (1000 / GAME_FRAME_RATE) + +enum CursorId { ARROW = 0, MAGNIFY = 1, WAIT = 2, EXIT_ZONES_START = 5, 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; + bool _firstPress; + Common::Stack<Common::KeyState> _pendingKeys; + Common::Point _hotspotPos; +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, int hotspotX = 0, int hotspotY = 0); + + /** + * Set both a standard cursor as well as an inventory item above it + */ + void setCursor(CursorId cursorId, const Graphics::Surface &surface); + + /** + * Animates the mouse cursor if the Wait cursor is showing + */ + void animateCursorIfNeeded(); + + /** + * 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(); + + /** + * Move the mouse cursor + */ + void warpMouse(const Common::Point &pt); + + /** + * Get the current mouse position + */ + Common::Point screenMousePos() const; + + /** + * Get the current mouse position within the scene, adjusted by the scroll position + */ + Common::Point mousePos() const; + + /** + * Return the current game frame number + */ + uint32 getFrameCounter() const { return _frameCounter; } + + /** + * Returns true if there's a pending keyboard key + */ + 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/fixed_text.cpp b/engines/sherlock/fixed_text.cpp new file mode 100644 index 0000000000..cbee944120 --- /dev/null +++ b/engines/sherlock/fixed_text.cpp @@ -0,0 +1,38 @@ +/* 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/fixed_text.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" + +namespace Sherlock { + +FixedText *FixedText::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelFixedText(vm); + else + return new Tattoo::TattooFixedText(vm); +} + + +} // End of namespace Sherlock diff --git a/engines/sherlock/fixed_text.h b/engines/sherlock/fixed_text.h new file mode 100644 index 0000000000..40444f4052 --- /dev/null +++ b/engines/sherlock/fixed_text.h @@ -0,0 +1,66 @@ +/* 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_FIXED_TEXT_H +#define SHERLOCK_FIXED_TEXT_H + +#include "common/scummsys.h" +#include "common/language.h" + +namespace Sherlock { + +#define FIXED(MSG) _vm->_fixedText->getText(kFixedText_##MSG) + +enum FixedTextActionId { + kFixedTextAction_Invalid = -1, + kFixedTextAction_Open = 0, + kFixedTextAction_Close, + kFixedTextAction_Move, + kFixedTextAction_Pick, + kFixedTextAction_Use +}; + +class SherlockEngine; + +class FixedText { +protected: + SherlockEngine *_vm; + + FixedText(SherlockEngine *vm) : _vm(vm) {} +public: + static FixedText *init(SherlockEngine *vm); + virtual ~FixedText() {} + + /** + * Gets text + */ + virtual const char *getText(int fixedTextId) = 0; + + /** + * Get action message + */ + virtual const Common::String getActionMessage(FixedTextActionId actionId, int messageIndex) = 0; +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/fonts.cpp b/engines/sherlock/fonts.cpp new file mode 100644 index 0000000000..440e31915c --- /dev/null +++ b/engines/sherlock/fonts.cpp @@ -0,0 +1,204 @@ +/* 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/system.h" +#include "common/platform.h" +#include "sherlock/fonts.h" +#include "sherlock/image_file.h" +#include "sherlock/surface.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +SherlockEngine *Fonts::_vm; +ImageFile *Fonts::_font; +int Fonts::_fontNumber; +int Fonts::_fontHeight; +int Fonts::_widestChar; +uint16 Fonts::_charCount; +byte Fonts::_yOffsets[255]; + +void Fonts::setVm(SherlockEngine *vm) { + _vm = vm; + _font = nullptr; + _charCount = 0; +} + +void Fonts::free() { + delete _font; +} + +void Fonts::setFont(int fontNum) { + _fontNumber = fontNum; + + // Discard previous font + delete _font; + + Common::String fontFilename; + + if (_vm->getPlatform() != Common::kPlatform3DO) { + // PC + // use FONT[number].VGS, which is a regular sherlock graphic file + fontFilename = Common::String::format("FONT%d.VGS", fontNum + 1); + + // load font data + _font = new ImageFile(fontFilename); + } else { + // 3DO + switch (fontNum) { + case 0: + case 1: + fontFilename = "helvetica14.font"; + break; + case 2: + fontFilename = "darts.font"; + break; + default: + error("setFont(): unsupported 3DO font number"); + } + + // load font data + _font = new ImageFile3DO(fontFilename, kImageFile3DOType_Font); + } + + _charCount = _font->size(); + + // Iterate through the frames to find the widest and tallest font characters + _fontHeight = _widestChar = 0; + for (uint idx = 0; idx < _charCount; ++idx) { + _fontHeight = MAX((uint16)_fontHeight, (*_font)[idx]._frame.h); + _widestChar = MAX((uint16)_widestChar, (*_font)[idx]._frame.w); + } + + // Initialize the Y offset table for the extended character set + for (int idx = 0; idx < 255; ++idx) { + _yOffsets[idx] = 0; + + if (IS_ROSE_TATTOO) { + if ((idx >= 129 && idx < 135) || (idx >= 136 && idx < 143) || (idx >= 147 && idx < 155) || + (idx >= 156 && idx < 165)) + _yOffsets[idx] = 1; + else if ((idx >= 143 && idx < 146) || idx == 165) + _yOffsets[idx] = 2; + } + } +} + +inline byte Fonts::translateChar(byte c) { + switch (c) { + case ' ': + return 0; // translate to first actual character + case 225: + // This was done in the German interpreter + // happens when talking to the kid in the 2nd room + return 135; // special handling for 0xE1 + default: + if (c >= 0x80) { // German SH1 version did this + c--; + } + // Spanish SH1 did this (reverse engineered code) + //if ((c >= 0xA0) && (c <= 0xAD) || (c == 0x82)) { + // c--; + //} + assert(c > 32); // anything above space is allowed + return c - 33; + } +} + +void Fonts::writeString(Surface *surface, const Common::String &str, + const Common::Point &pt, int overrideColor) { + Common::Point charPos = pt; + + if (!_font) + return; + + for (const char *curCharPtr = str.c_str(); *curCharPtr; ++curCharPtr) { + byte curChar = *curCharPtr; + + if (curChar == ' ') { + charPos.x += 5; // hardcoded space + continue; + } + curChar = translateChar(curChar); + + assert(curChar < _charCount); + ImageFrame &frame = (*_font)[curChar]; + surface->transBlitFrom(frame, Common::Point(charPos.x, charPos.y + _yOffsets[curChar]), false, overrideColor); + charPos.x += frame._frame.w + 1; + } +} + +int Fonts::stringWidth(const Common::String &str) { + int width = 0; + + if (!_font) + return 0; + + for (const char *c = str.c_str(); *c; ++c) + width += charWidth(*c); + + return width; +} + +int Fonts::stringHeight(const Common::String &str) { + int height = 0; + + if (!_font) + return 0; + + for (const char *c = str.c_str(); *c; ++c) + height = MAX(height, charHeight(*c)); + + return height; +} + +int Fonts::charWidth(unsigned char c) { + byte curChar; + + if (!_font) + return 0; + + if (c == ' ') { + return 5; // hardcoded space + } + curChar = translateChar(c); + + if (curChar < _charCount) + return (*_font)[curChar]._frame.w + 1; + return 0; +} + +int Fonts::charHeight(unsigned char c) { + byte curChar; + + if (!_font) + return 0; + + // Space is supposed to be handled like the first actual character (which is decimal 33) + curChar = translateChar(c); + + assert(curChar < _charCount); + const ImageFrame &img = (*_font)[curChar]; + return img._height + img._offset.y + 1; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/fonts.h b/engines/sherlock/fonts.h new file mode 100644 index 0000000000..a527cc73c0 --- /dev/null +++ b/engines/sherlock/fonts.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. + * + */ + +#ifndef SHERLOCK_FONTS_H +#define SHERLOCK_FONTS_H + +#include "common/rect.h" +#include "common/platform.h" +#include "graphics/surface.h" + +namespace Sherlock { + +class SherlockEngine; +class ImageFile; +class Surface; + +class Fonts { +private: + static ImageFile *_font; + static byte _yOffsets[255]; +protected: + static SherlockEngine *_vm; + static int _fontNumber; + static int _fontHeight; + static int _widestChar; + static uint16 _charCount; + + static void writeString(Surface *surface, const Common::String &str, + const Common::Point &pt, int overrideColor = 0); + + static inline byte translateChar(byte c); +public: + /** + * Initialise the font manager + */ + static void setVm(SherlockEngine *vm); + + /** + * Frees the font manager + */ + static void free(); + + /** + * Set the font to use for writing text on the screen + */ + void setFont(int fontNum); + + /** + * Returns the width of a string in pixels + */ + int stringWidth(const Common::String &str); + + /** + * Returns the height of a string in pixels (i.e. the tallest displayed character) + */ + int stringHeight(const Common::String &str); + + /** + * Returns the width of a character in pixels + */ + int charWidth(unsigned char c); + + /** + * Returns the width of a character in pixels + */ + int charHeight(unsigned char c); + + /** + * Return the font height + */ + int fontHeight() const { return _fontHeight; } + + /** + * Return the width of the widest character in the font + */ + int widestChar() const { return _widestChar; } + + /** + * Return the currently active font number + */ + int fontNumber() const { return _fontNumber; } +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/image_file.cpp b/engines/sherlock/image_file.cpp new file mode 100644 index 0000000000..81087dae8b --- /dev/null +++ b/engines/sherlock/image_file.cpp @@ -0,0 +1,1085 @@ +/* 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/image_file.h" +#include "sherlock/screen.h" +#include "sherlock/sherlock.h" +#include "common/debug.h" +#include "common/memstream.h" + +namespace Sherlock { + +SherlockEngine *ImageFile::_vm; + +void ImageFile::setVm(SherlockEngine *vm) { + _vm = vm; +} + +ImageFile::ImageFile() { +} + +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; + frame._offset.y = stream.readByte(); + } 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 + 4]; + stream.read(data, frame._size); + Common::fill(data + frame._size, data + frame._size + 4, 0); + frame.decompressFrame(data, IS_ROSE_TATTOO); + delete[] data; + + push_back(frame); + } +} + +void ImageFile::loadPalette(Common::SeekableReadStream &stream) { + // Check for palette + uint16 width = stream.readUint16LE() + 1; + uint16 height = stream.readUint16LE() + 1; + byte paletteBase = stream.readByte(); + byte rleEncoded = stream.readByte(); + byte offsetX = stream.readByte(); + byte offsetY = stream.readByte(); + uint32 palSignature = 0; + + if ((width == 390) && (height == 2) && (!paletteBase) && (!rleEncoded) && (!offsetX) && (!offsetY)) { + // We check for these specific values + // We can't do "width * height", because at least the first German+Spanish menu bar is 60 x 13 + // which is 780, which is the size of the palette. We obviously don't want to detect it as palette. + + // As another security measure, we also check for the signature text + palSignature = stream.readUint32BE(); + if (palSignature != MKTAG('V', 'G', 'A', ' ')) { + // signature mismatch, rewind + stream.seek(-12, SEEK_CUR); + return; + } + // Found palette, so read it in + stream.seek(8, SEEK_CUR); // Skip over the rest of the signature text "VGA palette" + 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(-8, SEEK_CUR); + } +} + +void ImageFrame::decompressFrame(const byte *src, bool isRoseTattoo) { + _frame.create(_width, _height, Graphics::PixelFormat::createFormatCLUT8()); + byte *dest = (byte *)_frame.getPixels(); + Common::fill(dest, dest + _width * _height, 0xff); + + if (_paletteBase) { + // Nibble-packed + for (uint idx = 0; idx < _size; ++idx, ++src) { + *dest++ = *src & 0xF; + *dest++ = (*src >> 4); + } + } else if (_rleEncoded && isRoseTattoo) { + // Rose Tattoo run length encoding doesn't use the RLE marker byte + for (int yp = 0; yp < _height; ++yp) { + int xSize = _width; + while (xSize > 0) { + // Skip a given number of pixels + byte skip = *src++; + dest += skip; + xSize -= skip; + if (!xSize) + break; + + // Get a run length, and copy the following number of pixels + int rleCount = *src++; + xSize -= rleCount; + while (rleCount-- > 0) + *dest++ = *src++; + } + assert(xSize == 0); + } + } else if (_rleEncoded) { + // RLE encoded + int frameSize = _width * _height; + while (frameSize > 0) { + if (*src == _rleMarker) { + byte rleColor = src[1]; + byte rleCount = src[2]; + src += 3; + frameSize -= rleCount; + while (rleCount--) + *dest++ = rleColor; + } else { + *dest++ = *src++; + --frameSize; + } + } + assert(frameSize == 0); + } else { + // Uncompressed frame + Common::copy(src, src + _width * _height, dest); + } +} + +/*----------------------------------------------------------------*/ + +int ImageFrame::sDrawXSize(int scaleVal) const { + int width = _width; + int scale = scaleVal == 0 ? 1 : scaleVal; + + if (scaleVal >= SCALE_THRESHOLD) + --width; + + int result = width * SCALE_THRESHOLD / scale; + if (scaleVal >= SCALE_THRESHOLD) + ++result; + + return result; +} + +int ImageFrame::sDrawYSize(int scaleVal) const { + int height = _height; + int scale = scaleVal == 0 ? 1 : scaleVal; + + if (scaleVal >= SCALE_THRESHOLD) + --height; + + int result = height * SCALE_THRESHOLD / scale; + if (scaleVal >= SCALE_THRESHOLD) + ++result; + + return result; +} + +int ImageFrame::sDrawXOffset(int scaleVal) const { + int width = _offset.x; + int scale = scaleVal == 0 ? 1 : scaleVal; + + if (scaleVal >= SCALE_THRESHOLD) + --width; + + int result = width * SCALE_THRESHOLD / scale; + if (scaleVal >= SCALE_THRESHOLD) + ++result; + + return result; +} + +int ImageFrame::sDrawYOffset(int scaleVal) const { + int height = _offset.y; + int scale = scaleVal == 0 ? 1 : scaleVal; + + if (scaleVal >= SCALE_THRESHOLD) + --height; + + int result = height * SCALE_THRESHOLD / scale; + if (scaleVal >= SCALE_THRESHOLD) + ++result; + + return result; +} + +// ******************************************************* + +/*----------------------------------------------------------------*/ + +SherlockEngine *ImageFile3DO::_vm; + +void ImageFile3DO::setVm(SherlockEngine *vm) { + _vm = vm; +} + +ImageFile3DO::ImageFile3DO(const Common::String &name, ImageFile3DOType imageFile3DOType) { +#if 0 + Common::File *dataStream = new Common::File(); + + if (!dataStream->open(name)) { + error("unable to open %s\n", name.c_str()); + } +#endif + Common::SeekableReadStream *dataStream = _vm->_res->load(name); + + switch(imageFile3DOType) { + case kImageFile3DOType_Animation: + loadAnimationFile(*dataStream); + break; + case kImageFile3DOType_Cel: + case kImageFile3DOType_CelAnimation: + load3DOCelFile(*dataStream); + break; + case kImageFile3DOType_RoomFormat: + load3DOCelRoomData(*dataStream); + break; + case kImageFile3DOType_Font: + loadFont(*dataStream); + break; + default: + error("unknown Imagefile-3DO-Type"); + break; + } + + delete dataStream; +} + +ImageFile3DO::ImageFile3DO(Common::SeekableReadStream &stream, bool isRoomData) { + if (!isRoomData) { + load(stream, isRoomData); + } else { + load3DOCelRoomData(stream); + } +} + +ImageFile3DO::~ImageFile3DO() { + // already done in ImageFile destructor + //for (uint idx = 0; idx < size(); ++idx) + // (*this)[idx]._frame.free(); +} + +void ImageFile3DO::load(Common::SeekableReadStream &stream, bool isRoomData) { + uint32 headerId = 0; + + if (isRoomData) { + load3DOCelRoomData(stream); + return; + } + + headerId = stream.readUint32BE(); + assert(!stream.eos()); + + // Seek back to the start + stream.seek(-4, SEEK_CUR); + + // Identify type of file + switch (headerId) { + case MKTAG('C', 'C', 'B', ' '): + case MKTAG('A', 'N', 'I', 'M'): + case MKTAG('O', 'F', 'S', 'T'): // 3DOSplash.cel + // 3DO .cel (title1a.cel, etc.) or animation file (walk.anim) + load3DOCelFile(stream); + break; + + default: + // Sherlock animation file (.3da files) + loadAnimationFile(stream); + break; + } +} + +// 3DO uses RGB555, we use RGB565 internally so that more platforms are able to run us +inline uint16 ImageFile3DO::convertPixel(uint16 pixel3DO) { + byte red = (pixel3DO >> 10) & 0x1F; + byte green = (pixel3DO >> 5) & 0x1F; + byte blue = pixel3DO & 0x1F; + + return ((red << 11) | (green << 6) | (blue)); +} + +void ImageFile3DO::loadAnimationFile(Common::SeekableReadStream &stream) { + uint32 streamLeft = stream.size() - stream.pos(); + uint32 celDataSize = 0; + + while (streamLeft > 0) { + ImageFrame frame; + + // We expect a basic header of 8 bytes + if (streamLeft < 8) + error("load3DOAnimationFile: expected animation header, not enough bytes"); + + celDataSize = stream.readUint16BE(); + + frame._width = stream.readUint16BE() + 1; // 2 bytes BE width + frame._height = stream.readByte() + 1; // 1 byte BE height + frame._paletteBase = 0; + + frame._rleEncoded = true; // always compressed + if (frame._width & 0x8000) { + frame._width &= 0x7FFF; + celDataSize += 0x10000; + } + + frame._offset.x = stream.readUint16BE(); + frame._offset.y = stream.readByte(); + frame._size = 0; + // Got header + streamLeft -= 8; + + // cel data follows + if (streamLeft < celDataSize) + error("load3DOAnimationFile: expected cel data, not enough bytes"); + + // + // Load data for frame and decompress it + byte *data = new byte[celDataSize]; + stream.read(data, celDataSize); + streamLeft -= celDataSize; + + // always 16 bits per pixel (RGB555) + decompress3DOCelFrame(frame, data, celDataSize, 16, NULL); + + delete[] data; + + push_back(frame); + } +} + +static byte imagefile3DO_cel_bitsPerPixelLookupTable[8] = { + 0, 1, 2, 4, 6, 8, 16, 0 +}; + +// Reads a 3DO .cel/.anim file +void ImageFile3DO::load3DOCelFile(Common::SeekableReadStream &stream) { + int32 streamSize = stream.size(); + int32 chunkStartPos = 0; + uint32 chunkTag = 0; + uint32 chunkSize = 0; + byte *chunkDataPtr = NULL; + + // ANIM chunk (animation header for animation files) + bool animFound = false; + uint32 animVersion = 0; + uint32 animType = 0; + uint32 animFrameCount = 1; // we expect 1 frame without an ANIM header + // CCB chunk (cel control block) + bool ccbFound = false; + uint32 ccbVersion = 0; + uint32 ccbFlags = 0; + bool ccbFlags_compressed = false; + uint16 ccbPPMP0 = 0; + uint16 ccbPPMP1 = 0; + uint32 ccbPRE0 = 0; + uint16 ccbPRE0_height = 0; + byte ccbPRE0_bitsPerPixel = 0; + uint32 ccbPRE1 = 0; + uint16 ccbPRE1_width = 0; + uint32 ccbWidth = 0; + uint32 ccbHeight = 0; + // pixel lookup table + bool plutFound = false; + uint32 plutCount = 0; + ImageFile3DOPixelLookupTable plutRGBlookupTable; + + memset(&plutRGBlookupTable, 0, sizeof(plutRGBlookupTable)); + + while (!stream.err() && (stream.pos() < streamSize)) { + chunkStartPos = stream.pos(); + chunkTag = stream.readUint32BE(); + chunkSize = stream.readUint32BE(); + + if (stream.eos() || stream.err()) + break; + + if (chunkSize < 8) + error("load3DOCelFile: Invalid chunk size"); + + uint32 dataSize = chunkSize - 8; + + switch (chunkTag) { + case MKTAG('A', 'N', 'I', 'M'): + // animation header + assert(dataSize >= 24); + + if (animFound) + error("load3DOCelFile: multiple ANIM chunks not supported"); + + animFound = true; + animVersion = stream.readUint32BE(); + animType = stream.readUint32BE(); + animFrameCount = stream.readUint32BE(); + // UINT32 - framerate (0x2000 in walk.anim???) + // UINT32 - starting frame (0 for walk.anim) + // UINT32 - number of loops (0 for walk.anim) + + if (animVersion != 0) + error("load3DOCelFile: Unsupported animation file version"); + if (animType != 1) + error("load3DOCelFile: Only single CCB animation files are supported"); + break; + + case MKTAG('C', 'C', 'B', ' '): + // CEL control block + assert(dataSize >= 72); + + if (ccbFound) + error("load3DOCelFile: multiple CCB chunks not supported"); + + ccbFound = true; + ccbVersion = stream.readUint32BE(); + ccbFlags = stream.readUint32BE(); + stream.skip(3 * 4); // skip over 3 pointer fields, which are used in memory only by 3DO hardware + stream.skip(8 * 4); // skip over 8 offset fields + ccbPPMP0 = stream.readUint16BE(); + ccbPPMP1 = stream.readUint16BE(); + ccbPRE0 = stream.readUint32BE(); + ccbPRE1 = stream.readUint32BE(); + ccbWidth = stream.readUint32BE(); + ccbHeight = stream.readUint32BE(); + + if (ccbVersion != 0) + error("load3DOCelFile: Unsupported CCB version"); + + if (ccbFlags & 0x200) // bit 9 + ccbFlags_compressed = true; + + // bit 5 of ccbFlags defines how RGB-black (0, 0, 0) will get treated + // = false -> RGB-black is treated as transparent + // = true -> RGB-black is treated as actual black + // atm we are always treating it as transparent + // it seems this bit is not set for any data of Sherlock Holmes + + // PRE0 first 3 bits define how many bits per encoded pixel are used + ccbPRE0_bitsPerPixel = imagefile3DO_cel_bitsPerPixelLookupTable[ccbPRE0 & 0x07]; + if (!ccbPRE0_bitsPerPixel) + error("load3DOCelFile: Invalid CCB PRE0 bits per pixel"); + + ccbPRE0_height = ((ccbPRE0 >> 6) & 0x03FF) + 1; + ccbPRE1_width = (ccbPRE1 & 0x03FF) + 1; + assert(ccbPRE0_height == ccbHeight); + assert(ccbPRE1_width == ccbWidth); + break; + + case MKTAG('P', 'L', 'U', 'T'): + // pixel lookup table + // optional, not required for at least 16-bit pixel data + assert(dataSize >= 6); + + if (!ccbFound) + error("load3DOCelFile: PLUT chunk found without CCB chunk"); + if (plutFound) + error("load3DOCelFile: multiple PLUT chunks currently not supported"); + + plutFound = true; + plutCount = stream.readUint32BE(); + // table follows, each entry is 16bit RGB555 + assert(dataSize >= 4 + (plutCount * 2)); // security check + assert(plutCount <= 256); // security check + + assert(plutCount <= 32); // PLUT should never contain more than 32 entries + + for (uint32 plutColorNr = 0; plutColorNr < plutCount; plutColorNr++) { + plutRGBlookupTable.pixelColor[plutColorNr] = stream.readUint16BE(); + } + + if (ccbPRE0_bitsPerPixel == 8) { + // In case we are getting 8-bits per pixel, we calculate the shades accordingly + // I'm not 100% sure if the calculation is correct. It's difficult to find information + // on this topic. + // The map uses this type of cel + assert(plutCount == 32); // And we expect 32 entries inside PLUT chunk + + uint16 plutColorRGB = 0; + for (uint32 plutColorNr = 0; plutColorNr < plutCount; plutColorNr++) { + plutColorRGB = plutRGBlookupTable.pixelColor[plutColorNr]; + + // Extract RGB values + byte plutColorRed = (plutColorRGB >> 10) & 0x1F; + byte plutColorGreen = (plutColorRGB >> 5) & 0x1F; + byte plutColorBlue = plutColorRGB & 0x1F; + + byte shadeMultiplier = 2; + for (uint32 plutShadeNr = 1; plutShadeNr < 8; plutShadeNr++) { + uint16 shadedColorRGB; + byte shadedColorRed = (plutColorRed * shadeMultiplier) >> 3; + byte shadedColorGreen = (plutColorGreen * shadeMultiplier) >> 3; + byte shadedColorBlue = (plutColorBlue * shadeMultiplier) >> 3; + + shadedColorRed = CLIP<byte>(shadedColorRed, 0, 0x1F); + shadedColorGreen = CLIP<byte>(shadedColorGreen, 0, 0x1F); + shadedColorBlue = CLIP<byte>(shadedColorBlue, 0, 0x1F); + shadedColorRGB = (shadedColorRed << 10) | (shadedColorGreen << 5) | shadedColorBlue; + + plutRGBlookupTable.pixelColor[plutColorNr + (plutShadeNr << 5)] = shadedColorRGB; + shadeMultiplier++; + } + } + } + break; + + case MKTAG('X', 'T', 'R', 'A'): + // Unknown contents, occurs right before PDAT + break; + + case MKTAG('P', 'D', 'A', 'T'): { + // pixel data for one frame + // may be compressed or uncompressed pixels + + if (ccbPRE0_bitsPerPixel != 16) { + // We require a pixel lookup table in case bits-per-pixel is lower than 16 + if (!plutFound) + error("load3DOCelFile: bits per pixel < 16, but no pixel lookup table was found"); + } else { + // But we don't like it in case bits-per-pixel is 16 and we find one + if (plutFound) + error("load3DOCelFile: bits per pixel == 16, but pixel lookup table was found as well"); + } + // read data into memory + chunkDataPtr = new byte[dataSize]; + + stream.read(chunkDataPtr, dataSize); + + // Set up frame + ImageFrame imageFrame; + + imageFrame._width = ccbWidth; + imageFrame._height = ccbHeight; + imageFrame._paletteBase = 0; + imageFrame._offset.x = 0; + imageFrame._offset.y = 0; + imageFrame._rleEncoded = ccbFlags_compressed; + imageFrame._size = 0; + + // Decompress/copy this frame + if (!plutFound) { + decompress3DOCelFrame(imageFrame, chunkDataPtr, dataSize, ccbPRE0_bitsPerPixel, NULL); + } else { + decompress3DOCelFrame(imageFrame, chunkDataPtr, dataSize, ccbPRE0_bitsPerPixel, &plutRGBlookupTable); + } + + delete[] chunkDataPtr; + + push_back(imageFrame); + break; + } + + case MKTAG('O', 'F', 'S', 'T'): // 3DOSplash.cel + // unknown contents + break; + + default: + error("Unsupported '%s' chunk in 3DO cel file", tag2str(chunkTag)); + } + + // Seek to end of chunk + stream.seek(chunkStartPos + chunkSize); + } + + // Warning below being used to silence unused variable warnings for now + warning("TODO: Remove %d %d %d", animFrameCount, ccbPPMP0, ccbPPMP1); +} + +// Reads 3DO .cel data (room file format) +void ImageFile3DO::load3DOCelRoomData(Common::SeekableReadStream &stream) { + uint32 streamLeft = stream.size() - stream.pos(); + uint16 roomDataHeader_size = 0; + byte roomDataHeader_offsetX = 0; + byte roomDataHeader_offsetY = 0; + + // CCB chunk (cel control block) + uint32 ccbFlags = 0; + bool ccbFlags_compressed = false; + uint16 ccbPPMP0 = 0; + uint16 ccbPPMP1 = 0; + uint32 ccbPRE0 = 0; + uint16 ccbPRE0_height = 0; + byte ccbPRE0_bitsPerPixel = 0; + uint32 ccbPRE1 = 0; + uint16 ccbPRE1_width = 0; + uint32 ccbWidth = 0; + uint32 ccbHeight = 0; + // cel data + uint32 celDataSize = 0; + + while (streamLeft > 0) { + // We expect at least 8 bytes basic header + if (streamLeft < 8) + error("load3DOCelRoomData: expected room data header, not enough bytes"); + + // 3DO sherlock holmes room data header + stream.skip(4); // Possibly UINT16 width, UINT16 height?!?! + roomDataHeader_size = stream.readUint16BE(); + roomDataHeader_offsetX = stream.readByte(); + roomDataHeader_offsetY = stream.readByte(); + streamLeft -= 8; + + // We expect the header size specified in the basic header to be at least a raw CCB + if (roomDataHeader_size < 68) + error("load3DOCelRoomData: header size is too small"); + // Check, that enough bytes for CCB are available + if (streamLeft < 68) + error("load3DOCelRoomData: expected raw cel control block, not enough bytes"); + + // 3DO raw cel control block + ccbFlags = stream.readUint32BE(); + stream.skip(3 * 4); // skip over 3 pointer fields, which are used in memory only by 3DO hardware + stream.skip(8 * 4); // skip over 8 offset fields + ccbPPMP0 = stream.readUint16BE(); + ccbPPMP1 = stream.readUint16BE(); + ccbPRE0 = stream.readUint32BE(); + ccbPRE1 = stream.readUint32BE(); + ccbWidth = stream.readUint32BE(); + ccbHeight = stream.readUint32BE(); + + if (ccbFlags & 0x200) // bit 9 + ccbFlags_compressed = true; + + // PRE0 first 3 bits define how many bits per encoded pixel are used + ccbPRE0_bitsPerPixel = imagefile3DO_cel_bitsPerPixelLookupTable[ccbPRE0 & 0x07]; + if (!ccbPRE0_bitsPerPixel) + error("load3DOCelRoomData: Invalid CCB PRE0 bits per pixel"); + + ccbPRE0_height = ((ccbPRE0 >> 6) & 0x03FF) + 1; + ccbPRE1_width = (ccbPRE1 & 0x03FF) + 1; + assert(ccbPRE0_height == ccbHeight); + assert(ccbPRE1_width == ccbWidth); + + if (ccbPRE0_bitsPerPixel != 16) { + // We currently support 16-bits per pixel in here + error("load3DOCelRoomData: bits per pixel < 16?!?!?"); + } + // Got the raw CCB + streamLeft -= 68; + + // cel data follows + // size field does not include the 8 byte header + celDataSize = roomDataHeader_size - 68; + + if (streamLeft < celDataSize) + error("load3DOCelRoomData: expected cel data, not enough bytes"); + + // read data into memory + byte *celDataPtr = new byte[celDataSize]; + + stream.read(celDataPtr, celDataSize); + streamLeft -= celDataSize; + + // Set up frame + { + ImageFrame imageFrame; + + imageFrame._width = ccbWidth; + imageFrame._height = ccbHeight; + imageFrame._paletteBase = 0; + imageFrame._offset.x = roomDataHeader_offsetX; + imageFrame._offset.y = roomDataHeader_offsetY; + imageFrame._rleEncoded = ccbFlags_compressed; + imageFrame._size = 0; + + // Decompress/copy this frame + decompress3DOCelFrame(imageFrame, celDataPtr, celDataSize, ccbPRE0_bitsPerPixel, NULL); + + delete[] celDataPtr; + + push_back(imageFrame); + } + } + + // Suppress compiler warning + warning("ccbPPMP0 = %d, ccbPPMP1 = %d", ccbPPMP0, ccbPPMP1); +} + +static uint16 imagefile3DO_cel_bitsMask[17] = { + 0, + 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, + 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF +}; + +// gets [bitCount] bits from dataPtr, going from MSB to LSB +inline uint16 ImageFile3DO::celGetBits(const byte *&dataPtr, byte bitCount, byte &dataBitsLeft) { + byte resultBitsLeft = bitCount; + uint16 result = 0; + byte currentByte = *dataPtr; + + // Get bits of current byte + while (resultBitsLeft) { + if (resultBitsLeft < dataBitsLeft) { + // we need less than we have left + result |= (currentByte >> (dataBitsLeft - resultBitsLeft)) & imagefile3DO_cel_bitsMask[resultBitsLeft]; + dataBitsLeft -= resultBitsLeft; + resultBitsLeft = 0; + + } else { + // we need as much as we have left or more + resultBitsLeft -= dataBitsLeft; + result |= (currentByte & imagefile3DO_cel_bitsMask[dataBitsLeft]) << resultBitsLeft; + + // Go to next byte + dataPtr++; + dataBitsLeft = 8; + if (resultBitsLeft) { + currentByte = *dataPtr; + } + } + } + return result; +} + +// decompress/copy 3DO cel data +void ImageFile3DO::decompress3DOCelFrame(ImageFrame &frame, const byte *dataPtr, uint32 dataSize, byte bitsPerPixel, ImageFile3DOPixelLookupTable *pixelLookupTable) { + frame._frame.create(frame._width, frame._height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); + uint16 *dest = (uint16 *)frame._frame.getPixels(); + Common::fill(dest, dest + frame._width * frame._height, 0); + + int frameHeightLeft = frame._height; + int frameWidthLeft = frame._width; + uint16 pixelCount = 0; + uint16 pixel = 0; + + const byte *srcLineStart = dataPtr; + const byte *srcLineData = dataPtr; + byte srcLineDataBitsLeft = 0; + uint16 lineDWordSize = 0; + uint16 lineByteSize = 0; + + if (bitsPerPixel == 16) { + // Must not use pixel lookup table on 16-bits-per-pixel data + assert(!pixelLookupTable); + } + + if (frame._rleEncoded) { + // compressed + byte compressionType = 0; + byte compressionPixels = 0; + + while (frameHeightLeft > 0) { + frameWidthLeft = frame._width; + + if (bitsPerPixel >= 8) { + lineDWordSize = READ_BE_UINT16(srcLineStart); + srcLineData = srcLineStart + 2; + } else { + lineDWordSize = *srcLineStart; + srcLineData = srcLineStart + 1; + } + srcLineDataBitsLeft = 8; + + lineDWordSize += 2; + lineByteSize = lineDWordSize * 4; // calculate compressed data size in bytes for current line + + // debug + //warning("offset %d: decoding line, size %d, bytesize %d", srcSeeker - src, dwordSize, lineByteSize); + + while (frameWidthLeft > 0) { + // get 2 bits -> compressionType + // get 6 bits -> pixel count (0 = 1 pixel) + compressionType = celGetBits(srcLineData, 2, srcLineDataBitsLeft); + // 6 bits == length (0 = 1 pixel) + compressionPixels = celGetBits(srcLineData, 6, srcLineDataBitsLeft) + 1; + + if (!compressionType) // end of line + break; + + switch(compressionType) { + case 1: // simple copy + for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { + pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); + if (pixelLookupTable) { + pixel = pixelLookupTable->pixelColor[pixel]; + } + *dest++ = convertPixel(pixel); + } + break; + case 2: // transparent + for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { + *dest++ = 0; + } + break; + case 3: // duplicate pixels + pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); + if (pixelLookupTable) { + pixel = pixelLookupTable->pixelColor[pixel]; + } + pixel = convertPixel(pixel); + for (pixelCount = 0; pixelCount < compressionPixels; pixelCount++) { + *dest++ = pixel; + } + break; + default: + break; + } + frameWidthLeft -= compressionPixels; + } + + assert(frameWidthLeft >= 0); + + if (frameWidthLeft > 0) { + // still pixels left? skip them + dest += frameWidthLeft; + } + + frameHeightLeft--; + + // Seek to next line start + srcLineStart += lineByteSize; + } + } else { + // uncompressed + srcLineDataBitsLeft = 8; + lineDWordSize = ((frame._width * bitsPerPixel) + 31) >> 5; + lineByteSize = lineDWordSize * 4; + uint32 totalExpectedSize = lineByteSize * frame._height; + + assert(totalExpectedSize <= dataSize); // security check + + while (frameHeightLeft > 0) { + srcLineData = srcLineStart; + frameWidthLeft = frame._width; + + while (frameWidthLeft > 0) { + pixel = celGetBits(srcLineData, bitsPerPixel, srcLineDataBitsLeft); + if (pixelLookupTable) { + pixel = pixelLookupTable->pixelColor[pixel]; + } + *dest++ = convertPixel(pixel); + + frameWidthLeft--; + } + frameHeightLeft--; + + // Seek to next line start + srcLineStart += lineByteSize; + } + } +} + +// Reads Sherlock Holmes 3DO font file +void ImageFile3DO::loadFont(Common::SeekableReadStream &stream) { + uint32 streamSize = stream.size(); + uint32 header_offsetWidthTable = 0; + uint32 header_offsetBitsTable = 0; + uint32 header_fontHeight = 0; + uint32 header_bytesPerLine = 0; + uint32 header_maxChar = 0; + uint32 header_charCount = 0; + + byte *widthTablePtr = NULL; + uint32 bitsTableSize = 0; + byte *bitsTablePtr = NULL; + + stream.skip(2); // Unknown bytes + stream.skip(2); // Unknown bytes (0x000E) + header_offsetWidthTable = stream.readUint32BE(); + header_offsetBitsTable = stream.readUint32BE(); + stream.skip(4); // Unknown bytes (0x00000004) + header_fontHeight = stream.readUint32BE(); + header_bytesPerLine = stream.readUint32BE(); + header_maxChar = stream.readUint32BE(); + + assert(header_maxChar <= 255); + header_charCount = header_maxChar + 1; + + // Allocate memory for width table + widthTablePtr = new byte[header_charCount]; + + stream.seek(header_offsetWidthTable); + stream.read(widthTablePtr, header_charCount); + + // Allocate memory for the bits + assert(header_offsetBitsTable < streamSize); // Security check + bitsTableSize = streamSize - header_offsetBitsTable; + bitsTablePtr = new byte[bitsTableSize]; + stream.read(bitsTablePtr, bitsTableSize); + + // Now extract all characters + uint16 curChar = 0; + const byte *curBitsLinePtr = bitsTablePtr; + const byte *curBitsPtr = NULL; + byte curBitsLeft = 0; + uint32 curCharHeightLeft = 0; + uint32 curCharWidthLeft = 0; + byte curBits = 0; + byte curBitsReversed = 0; + byte curPosX = 0; + + assert(bitsTableSize >= (header_maxChar * header_fontHeight * header_bytesPerLine)); // Security + + // first frame needs to be "!" (33 decimal) + // our font code is subtracting 33 from the actual character code + curBitsLinePtr += (33 * (header_fontHeight * header_bytesPerLine)); + + for (curChar = 33; curChar < header_charCount; curChar++) { + // create frame + { + ImageFrame imageFrame; + + imageFrame._width = widthTablePtr[curChar]; + imageFrame._height = header_fontHeight; + imageFrame._paletteBase = 0; + imageFrame._offset.x = 0; + imageFrame._offset.y = 0; + imageFrame._rleEncoded = false; + imageFrame._size = 0; + + // Extract pixels + imageFrame._frame.create(imageFrame._width, imageFrame._height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); + uint16 *dest = (uint16 *)imageFrame._frame.getPixels(); + Common::fill(dest, dest + imageFrame._width * imageFrame._height, 0); + + curCharHeightLeft = header_fontHeight; + while (curCharHeightLeft) { + curCharWidthLeft = widthTablePtr[curChar]; + curBitsPtr = curBitsLinePtr; + curBitsLeft = 8; + curPosX = 0; + + while (curCharWidthLeft) { + if (!(curPosX & 1)) { + curBits = *curBitsPtr >> 4; + } else { + curBits = *curBitsPtr & 0x0F; + curBitsPtr++; + } + // doing this properly is complicated + // the 3DO has built-in anti-aliasing + // this here at least results in somewhat readable text + // TODO: make it better + if (curBits) { + curBitsReversed = (curBits >> 3) | ((curBits & 0x04) >> 1) | ((curBits & 0x02) << 1) | ((curBits & 0x01) << 3); + curBits = 20 - curBits; + } + + byte curIntensity = curBits; + *dest = (curIntensity << 11) | (curIntensity << 6) | curIntensity; + dest++; + + curCharWidthLeft--; + curPosX++; + } + + curCharHeightLeft--; + curBitsLinePtr += header_bytesPerLine; + } + + push_back(imageFrame); + } + } + + // Warning below being used to silence unused variable warnings for now + warning("TODO: Remove %d %d", curBitsLeft, curBitsReversed); + + delete[] bitsTablePtr; + delete[] widthTablePtr; +} + +/*----------------------------------------------------------------*/ + +StreamingImageFile::StreamingImageFile() { + _frameNumber = 0; + _stream = nullptr; + _flags = 0; + _scaleVal = 0; + _zPlacement = 0; + _compressed = false; +} + +StreamingImageFile::~StreamingImageFile() { + close(); +} + +void StreamingImageFile::load(Common::SeekableReadStream *stream, bool compressed) { + _stream = stream; + _compressed = compressed; + _frameNumber = -1; +} + +void StreamingImageFile::close() { + delete _stream; + _stream = nullptr; + _frameNumber = -1; +} + +void StreamingImageFile::getNextFrame() { + // Don't proceed if we're already at the end of the stream + if (_stream->pos() >= _stream->size()) + return; + + // Increment frame number + ++_frameNumber; + + // If necessary, decompress the next frame + Common::SeekableReadStream *frameStream = _stream; + if (_compressed) { + uint32 inSize = _stream->readUint32LE(); + Resources::decompressLZ(*_stream, _buffer, STREAMING_BUFFER_SIZE, inSize); + frameStream = new Common::MemoryReadStream(_buffer, 11, DisposeAfterUse::NO); + } + + // Load the data for the frame + _imageFrame._width = frameStream->readUint16LE() + 1; + _imageFrame._height = frameStream->readUint16LE() + 1; + _imageFrame._paletteBase = frameStream->readByte(); + _imageFrame._rleEncoded = frameStream->readByte() == 1; + _imageFrame._offset.x = frameStream->readByte(); + _imageFrame._offset.y = frameStream->readByte(); + _imageFrame._size = frameStream->readUint16LE() - 11; + _imageFrame._rleMarker = frameStream->readByte(); + + // Decode the frame + if (_compressed) { + delete frameStream; + _imageFrame.decompressFrame(_buffer + 11, true); + } else { + byte *data = new byte[_imageFrame._size]; + _stream->read(data, _imageFrame._size); + _imageFrame.decompressFrame(_buffer + 11, true); + delete[] data; + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/image_file.h b/engines/sherlock/image_file.h new file mode 100644 index 0000000000..3ac0cf4b5f --- /dev/null +++ b/engines/sherlock/image_file.h @@ -0,0 +1,207 @@ +/* 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_IMAGE_FILE_H +#define SHERLOCK_IMAGE_FILE_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 { + +class SherlockEngine; + +struct ImageFrame { + uint32 _size; + uint16 _width, _height; + int _paletteBase; + bool _rleEncoded; + Common::Point _offset; + byte _rleMarker; + Graphics::Surface _frame; + + /** + * Decompress a single frame for the sprite + */ + void decompressFrame(const byte *src, bool isRoseTattoo); + + /** + * Return the frame width adjusted by a specified scale amount + */ + int sDrawXSize(int scaleVal) const; + + /** + * Return the frame height adjusted by a specified scale amount + */ + int sDrawYSize(int scaleVal) const; + + /** + * Return the frame offset x adjusted by a specified scale amount + */ + int sDrawXOffset(int scaleVal) const; + + /** + * Return the frame offset y adjusted by a specified scale amount + */ + int sDrawYOffset(int scaleVal) const; +}; + +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); +public: + byte _palette[256 * 3]; +public: + ImageFile(); + ImageFile(const Common::String &name, bool skipPal = false, bool animImages = false); + ImageFile(Common::SeekableReadStream &stream, bool skipPal = false); + ~ImageFile(); + static void setVm(SherlockEngine *vm); +}; + +enum ImageFile3DOType { + kImageFile3DOType_Animation = 0, + kImageFile3DOType_Cel = 1, + kImageFile3DOType_CelAnimation = 2, + kImageFile3DOType_RoomFormat = 3, + kImageFile3DOType_Font = 4 +}; + +struct ImageFile3DOPixelLookupTable { + uint16 pixelColor[256]; +}; + +class ImageFile3DO : public ImageFile { // Common::Array<ImageFrame> { +private: + static SherlockEngine *_vm; + + /** + * Load the data of the sprite + */ + void load(Common::SeekableReadStream &stream, bool isRoomData); + + /** + * convert pixel RGB values from RGB555 to RGB565 + */ + inline uint16 convertPixel(uint16 pixel3DO); + + /** + * Load 3DO cel file + */ + void load3DOCelFile(Common::SeekableReadStream &stream); + + /** + * Load 3DO cel data (room file format) + */ + void load3DOCelRoomData(Common::SeekableReadStream &stream); + + inline uint16 celGetBits(const byte *&dataPtr, byte bitCount, byte &dataBitsLeft); + + /** + * Decompress a single frame of a 3DO cel file + */ + void decompress3DOCelFrame(ImageFrame &frame, const byte *dataPtr, uint32 dataSize, byte bitsPerPixel, ImageFile3DOPixelLookupTable *pixelLookupTable); + + /** + * Load animation graphics file + */ + void loadAnimationFile(Common::SeekableReadStream &stream); + + /** + * Load Sherlock Holmes 3DO font file + */ + void loadFont(Common::SeekableReadStream &stream); + +public: + ImageFile3DO(const Common::String &name, ImageFile3DOType imageFile3DOType); + ImageFile3DO(Common::SeekableReadStream &stream, bool isRoomData = false); + ~ImageFile3DO(); + static void setVm(SherlockEngine *vm); +}; + +#define STREAMING_BUFFER_SIZE 65536 + +class StreamingImageFile { +private: + int _frameNumber; + Common::SeekableReadStream *_stream; + bool _compressed; + byte _buffer[STREAMING_BUFFER_SIZE]; +public: + ImageFrame _imageFrame; + + Common::Point _position; // Animation position + Common::Rect _oldBounds; // Bounds of previous frame + Common::Rect _removeBounds; // Remove area for just drawn frame + + int _flags; // Flags + int _scaleVal; // Specifies the scale amount + int _zPlacement; // Used by doBgAnim for determining Z order +public: + StreamingImageFile(); + ~StreamingImageFile(); + + /** + * Initialize reading of the specified stream + */ + void load(Common::SeekableReadStream *stream, bool compressed); + + /** + * Close the streamining image file + */ + void close(); + + /** + * Get the next frame of the file + */ + void getNextFrame(); + + /** + * Returns whether there are any remaining frames or not + */ + bool active() const { return _stream != nullptr && _stream->pos() < _stream->size(); } + + /** + * Return the current frame number + */ + int frameNumber() const { return _frameNumber; } +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/inventory.cpp b/engines/sherlock/inventory.cpp new file mode 100644 index 0000000000..d0982542f2 --- /dev/null +++ b/engines/sherlock/inventory.cpp @@ -0,0 +1,246 @@ +/* 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" +#include "sherlock/scalpel/scalpel_inventory.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/tattoo/tattoo_inventory.h" + +namespace Sherlock { + +InventoryItem::InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine) : + _requiredFlag(requiredFlag), _requiredFlag1(0), _name(name), _description(description), + _examine(examine), _lookFlag(0) { +} + +InventoryItem::InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine, const Common::String &verbName) : + _requiredFlag(requiredFlag), _requiredFlag1(0), _name(name), _description(description), + _examine(examine), _lookFlag(0) { + _verb._verb = verbName; +} + +void InventoryItem::synchronize(Serializer &s) { + s.syncAsSint16LE(_requiredFlag); + s.syncAsSint16LE(_lookFlag); + s.syncString(_name); + s.syncString(_description); + s.syncString(_examine); + _verb.synchronize(s); +} + +/*----------------------------------------------------------------*/ + +Inventory *Inventory::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelInventory(vm); + else + return new Tattoo::TattooInventory(vm); +} + +Inventory::Inventory(SherlockEngine *vm) : Common::Array<InventoryItem>(), _vm(vm) { + _invGraphicsLoaded = false; + _invIndex = 0; + _holdings = 0; + _invMode = INVMODE_EXIT; +} + +Inventory::~Inventory() { + freeGraphics(); +} + +void Inventory::freeInv() { + freeGraphics(); + + _names.clear(); + _invGraphicsLoaded = false; +} + +void Inventory::freeGraphics() { + int count = _invShapes.size(); + for (int idx = 0; idx < count; ++idx) + delete _invShapes[idx]; + _invShapes.clear(); + _invShapes.resize(count); + + _invGraphicsLoaded = false; +} + +void Inventory::loadGraphics() { + if (_invGraphicsLoaded) + return; + + for (int idx = _invIndex; (idx < _holdings) && (idx - _invIndex) < (int)_invShapes.size(); ++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 filename = Common::String::format("item%02d.vgs", invNum + 1); + + if (!IS_3DO) { + // PC + _invShapes[idx - _invIndex] = new ImageFile(filename); + } else { + _invShapes[idx - _invIndex] = new ImageFile3DO(filename, kImageFile3DOType_RoomFormat); + } + } + + _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()); +} + +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[0]; + + 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(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..057e923e5d --- /dev/null +++ b/engines/sherlock/inventory.h @@ -0,0 +1,150 @@ +/* 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/str-array.h" +#include "sherlock/objects.h" +#include "sherlock/resources.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +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; + + // Rose Tattoo fields + int _requiredFlag1; + UseType _verb; + + InventoryItem() : _requiredFlag(0), _lookFlag(0), _requiredFlag1(0) {} + InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine); + InventoryItem(int requiredFlag, const Common::String &name, + const Common::String &description, const Common::String &examine, const Common::String &verbName); + + /** + * Synchronize the data for an inventory item + */ + void synchronize(Serializer &s); +}; + +class Inventory : public Common::Array<InventoryItem> { +protected: + SherlockEngine *_vm; + Common::StringArray _names; + + /** + * Copy the passed object into the inventory + */ + void copyToInventory(Object &obj); +public: + Common::Array<ImageFile *> _invShapes; + 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: + static Inventory *init(SherlockEngine *vm); + Inventory(SherlockEngine *vm); + virtual ~Inventory(); + + /** + * Free inventory data + */ + void freeInv(); + + /** + * 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); + + /** + * 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(Serializer &s); + + /** + * Load the list of names the inventory items correspond to, if not already loaded, + * and then calls loadGraphics to load the associated graphics + */ + virtual void loadInv() = 0; +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/journal.cpp b/engines/sherlock/journal.cpp new file mode 100644 index 0000000000..9441d6682a --- /dev/null +++ b/engines/sherlock/journal.cpp @@ -0,0 +1,706 @@ +/* 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/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_journal.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_journal.h" + +namespace Sherlock { + +Journal *Journal::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelJournal(vm); + else + return new Tattoo::TattooJournal(vm); +} + +Journal::Journal(SherlockEngine *vm) : _vm(vm) { + _up = _down = false; + _index = 0; + _page = 0; + _maxPage = 0; + _sub = 0; +} + +bool Journal::drawJournal(int direction, int howFar) { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + 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) + drawFrame(); + + screen.gPrint(Common::Point(235, 21), COL_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 { + // 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); + drawFrame(); + } + + Common::String fixedText_Page = IS_SERRATED_SCALPEL ? fixedText.getText(Scalpel::kFixedText_Journal_Page) : "TODO"; + + screen.gPrint(Common::Point(235, 21), COL_PEN_COLOR, fixedText_Page.c_str(), _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), COL_PEN_COLOR, "%s", lineStart.c_str()); + } + + // Print out the found keyword + Common::String lineMatch(matchP, matchP + _find.size()); + byte fgColor = IS_SERRATED_SCALPEL ? (byte)Scalpel::INV_FOREGROUND : (byte)Tattoo::INV_FOREGROUND; + screen.gPrint(Common::Point(53 + width, yp), fgColor, "%s", lineMatch.c_str()); + width += screen.stringWidth(lineMatch.c_str()); + + // Print remainder of line + screen.gPrint(Common::Point(53 + width, yp), COL_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), COL_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), COL_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; +} + +void Journal::loadJournalFile(bool alreadyLoaded) { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + JournalEntry &journalEntry = _journal[_index]; + const byte *opcodes = talk._opcodes; + + 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 already 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._characters[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 = "@"; + if (IS_SERRATED_SCALPEL || newLocation - 1 < 100) + journalString += _locations[newLocation - 1]; + journalString += ":"; + + // 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 "; + + if (talk._talkTo == 1) { + journalString += "me"; + } else if ((talk._talkTo == 2 && IS_SERRATED_SCALPEL) || (talk._talkTo == 18 && IS_ROSE_TATTOO)) { + journalString += "the Inspector"; + } else { + journalString += people._characters[talk._talkTo]._name; + } + 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(); + const int inspectorId = (IS_SERRATED_SCALPEL) ? 2 : 18; + + while (*replyP) { + byte c = *replyP++; + + if (IS_ROSE_TATTOO) { + // Ignore commented out data + if (c == '/' && *(replyP + 1) == '*') { + replyP++; // skip * + while (*replyP++ != '*') {} // empty loop on purpose + replyP++; // skip / + c = *replyP; + } + } + + // Is it a control character? + if (c < opcodes[0]) { + // Nope. Set flag for allowing control codes to insert spaces + ctrlSpace = true; + assert(c >= ' '); + + // Check for embedded comments + if (c == '{' || c == '}') { + + // TODO: Rose Tattoo checks if no text was added for the last + // comment here. In such a case, the last "XXX said" string is + // removed here. + + // 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 == inspectorId) + journalString += "The Inspector"; + else + journalString += people._characters[talk._talkTo]._name; + + const byte *strP = replyP + 1; + byte v; + do { + v = *strP++; + } while (v && (v < opcodes[0]) && (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 < opcodes[0] && *replyP != '{' && *replyP != '}'); + + commentJustPrinted = false; + } + } else if (c == opcodes[OP_SWITCH_SPEAKER]) { + if (!startOfReply) { + if (!commentFlag && !commentJustPrinted) + journalString += "\"\n"; + + journalString += "Then "; + commentFlag = false; + } else if (!replyOnly) { + journalString += "\"\n"; + } + + startOfReply = false; + c = *replyP++ - 1; + if (IS_ROSE_TATTOO) + replyP++; + + if (c == 0) + journalString += "Holmes"; + else if (c == 1) + journalString += "I"; + else if (c == inspectorId) + journalString += "the Inspector"; + else + journalString += people._characters[c]._name; + + const byte *strP = replyP; + byte v; + do { + v = *strP++; + } while (v && v < opcodes[0] && v != '.' && v != '!' && v != '?'); + + if (v == '?') + journalString += " asked, \""; + else + journalString += " said, \""; + } else { + if (IS_SERRATED_SCALPEL) { + // Control code, so move past it and any parameters + if (c == opcodes[OP_RUN_CANIMATION] || + c == opcodes[OP_ASSIGN_PORTRAIT_LOCATION] || + c == opcodes[OP_PAUSE] || + c == opcodes[OP_PAUSE_WITHOUT_CONTROL] || + c == opcodes[OP_WALK_TO_CANIMATION]) { + // These commands have a single parameter + ++replyP; + } else if (c == opcodes[OP_ADJUST_OBJ_SEQUENCE]) { + replyP += (replyP[0] & 127) + replyP[1] + 2; + } else if (c == opcodes[OP_WALK_TO_COORDS] || c == opcodes[OP_MOVE_MOUSE]) { + replyP += 4; + } else if (c == opcodes[OP_SET_FLAG] || c == opcodes[OP_IF_STATEMENT]) { + replyP += 2; + } else if (c == opcodes[OP_SFX_COMMAND] || c == opcodes[OP_PLAY_PROLOGUE] || + c == opcodes[OP_CALL_TALK_FILE]) { + replyP += 8; + break; + } else if ( + c == opcodes[OP_TOGGLE_OBJECT] || + c == opcodes[OP_ADD_ITEM_TO_INVENTORY] || + c == opcodes[OP_SET_OBJECT] || + c == opcodes[OP_DISPLAY_INFO_LINE] || + c == opcodes[OP_REMOVE_ITEM_FROM_INVENTORY]) { + replyP += (*replyP & 127) + 1; + } else if (c == opcodes[OP_GOTO_SCENE]) { + replyP += 5; + } else if (c == opcodes[OP_END_TEXT_WINDOW]) { + journalString += "\n"; + } + } else { + if (c == opcodes[OP_RUN_CANIMATION] || + c == opcodes[OP_PAUSE] || + c == opcodes[OP_MOUSE_OFF_ON] || + c == opcodes[OP_SET_WALK_CONTROL] || + c == opcodes[OP_PAUSE_WITHOUT_CONTROL] || + c == opcodes[OP_WALK_TO_CANIMATION] || + c == opcodes[OP_TURN_NPC_OFF] || + c == opcodes[OP_TURN_NPC_ON] || + c == opcodes[OP_RESTORE_PEOPLE_SEQUENCE]) + ++replyP; + else if ( + c == opcodes[OP_SET_TALK_SEQUENCE] || + c == opcodes[OP_SET_FLAG] || + c == opcodes[OP_WALK_NPC_TO_CANIM] || + c == opcodes[OP_WALK_HOLMES_AND_NPC_TO_CANIM] || + c == opcodes[OP_NPC_PATH_LABEL] || + c == opcodes[OP_PATH_GOTO_LABEL]) + replyP += 2; + else if ( + c == opcodes[OP_SET_NPC_PATH_PAUSE] || + c == opcodes[OP_NPC_PATH_PAUSE_TAKING_NOTES] || + c == opcodes[OP_NPC_PATH_PAUSE_LOOKING_HOLMES] || + c == opcodes[OP_NPC_VERB_CANIM]) + replyP += 3; + else if ( + c == opcodes[OP_SET_SCENE_ENTRY_FLAG] || + c == opcodes[OP_PATH_IF_FLAG_GOTO_LABEL]) + replyP += 4; + else if ( + c == opcodes[OP_WALK_TO_COORDS]) + replyP += 5; + else if ( + c == opcodes[OP_WALK_NPC_TO_COORDS] || + c == opcodes[OP_GOTO_SCENE] || + c == opcodes[OP_SET_NPC_PATH_DEST] || + c == opcodes[OP_SET_NPC_POSITION]) + replyP += 6; + else if ( + c == opcodes[OP_PLAY_SONG] || + c == opcodes[OP_NEXT_SONG]) + replyP += 8; + else if ( + c == opcodes[OP_CALL_TALK_FILE] || + c == opcodes[OP_SET_NPC_TALK_FILE] || + c == opcodes[OP_NPC_WALK_GRAPHICS]) + replyP += 9; + else if ( + c == opcodes[OP_NPC_VERB_SCRIPT]) + replyP += 10; + else if ( + c == opcodes[OP_WALK_HOLMES_AND_NPC_TO_COORDS]) + replyP += 11; + else if ( + c == opcodes[OP_NPC_VERB] || + c == opcodes[OP_NPC_VERB_TARGET]) + replyP += 14; + else if ( + c == opcodes[OP_ADJUST_OBJ_SEQUENCE]) + replyP += (replyP[0] & 127) + replyP[1] + 2; + else if ( + c == opcodes[OP_TOGGLE_OBJECT] || + c == opcodes[OP_ADD_ITEM_TO_INVENTORY] || + c == opcodes[OP_SET_OBJECT] || + c == opcodes[OP_REMOVE_ITEM_FROM_INVENTORY]) + replyP += (*replyP & 127) + 1; + else if ( + c == opcodes[OP_END_TEXT_WINDOW]) { + journalString += '\n'; + } else if ( + c == opcodes[OP_NPC_DESC_ON_OFF]) { + replyP++; + while (replyP[0] && replyP[0] != opcodes[OP_NPC_DESC_ON_OFF]) + replyP++; + replyP++; + } else if ( + c == opcodes[OP_SET_NPC_INFO_LINE]) + replyP += replyP[1] + 2; + } + + // Put a space in the output for a control character, unless it's + // immediately coming after another control character + if (ctrlSpace && c != opcodes[OP_ASSIGN_PORTRAIT_LOCATION] && c != opcodes[OP_END_TEXT_WINDOW] && + !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::synchronize(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..d2baae11a9 --- /dev/null +++ b/engines/sherlock/journal.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. + * + */ + +#ifndef SHERLOCK_JOURNAL_H +#define SHERLOCK_JOURNAL_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "common/stream.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +#define LINES_PER_PAGE (IS_SERRATED_SCALPEL ? 11 : 17) + +class SherlockEngine; + +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 Journal { +protected: + SherlockEngine *_vm; + Common::StringArray _directory; + Common::StringArray _locations; + Common::Array<JournalEntry> _journal; + Common::StringArray _lines; + bool _up, _down; + int _index; + int _page; + int _maxPage; + int _sub; + Common::String _find; + + Journal(SherlockEngine *vm); + + /** + * 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); +public: + static Journal *init(SherlockEngine *vm); + virtual ~Journal() {} + + /** + * Displays a page of the journal at the current index + */ + bool drawJournal(int direction, int howFar); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Serializer &s); +public: + /** + * Draw the journal background, frame, and interface buttons + */ + virtual void drawFrame() = 0; + + /** + * Records statements that are said, in the order which they are said. The player + * can then read the journal to review them + */ + virtual void record(int converseNum, int statementNum, bool replyOnly = false) {} + + /** + * Reset viewing position to the start of the journal + */ + virtual void resetPosition() {} +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/map.cpp b/engines/sherlock/map.cpp new file mode 100644 index 0000000000..fbccaf244f --- /dev/null +++ b/engines/sherlock/map.cpp @@ -0,0 +1,52 @@ +/* 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/system.h" +#include "sherlock/map.h" +#include "sherlock/sherlock.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/tattoo/tattoo_map.h" + +namespace Sherlock { + +Map *Map::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelMap(vm); + else + return new Tattoo::TattooMap(vm); +} + +Map::Map(SherlockEngine *vm) : _vm(vm) { + _charPoint = _oldCharPoint = 0; + _active = _frameChangeFlag = false; +} + +void Map::synchronize(Serializer &s) { + s.syncAsSint32LE(_bigPos.x); + s.syncAsSint32LE(_bigPos.y); + s.syncAsSint32LE(_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..104f5e9c8a --- /dev/null +++ b/engines/sherlock/map.h @@ -0,0 +1,61 @@ +/* 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 "sherlock/objects.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +class SherlockEngine; + +class Map { +protected: + SherlockEngine *_vm; + + Map(SherlockEngine *vm); +public: + Point32 _overPos; + Point32 _bigPos; + int _charPoint, _oldCharPoint; + bool _active; + bool _frameChangeFlag; +public: + static Map *init(SherlockEngine *vm); + virtual ~Map() {} + + /** + * Show the map + */ + virtual int show() = 0; + + /** + * Synchronize the data for a savegame + */ + void synchronize(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..32c5d3acc3 --- /dev/null +++ b/engines/sherlock/module.mk @@ -0,0 +1,71 @@ +MODULE := engines/sherlock + +MODULE_OBJS = \ + scalpel/darts.o \ + scalpel/scalpel.o \ + scalpel/3do/movie_decoder.o \ + scalpel/drivers/adlib.o \ + scalpel/drivers/mt32.o \ + scalpel/tsage/logo.o \ + scalpel/tsage/resources.o \ + scalpel/scalpel_debugger.o \ + scalpel/scalpel_fixed_text.o \ + scalpel/scalpel_inventory.o \ + scalpel/scalpel_journal.o \ + scalpel/scalpel_map.o \ + scalpel/scalpel_people.o \ + scalpel/scalpel_saveload.o \ + scalpel/scalpel_scene.o \ + scalpel/scalpel_screen.o \ + scalpel/scalpel_talk.o \ + scalpel/scalpel_user_interface.o \ + scalpel/settings.o \ + tattoo/tattoo.o \ + tattoo/tattoo_darts.o \ + tattoo/tattoo_debugger.o \ + tattoo/tattoo_fixed_text.o \ + tattoo/tattoo_inventory.o \ + tattoo/tattoo_journal.o \ + tattoo/tattoo_map.o \ + tattoo/tattoo_people.o \ + tattoo/tattoo_resources.o \ + tattoo/tattoo_scene.o \ + tattoo/tattoo_talk.o \ + tattoo/tattoo_user_interface.o \ + tattoo/widget_base.o \ + tattoo/widget_inventory.o \ + tattoo/widget_lab.o \ + tattoo/widget_talk.o \ + tattoo/widget_text.o \ + tattoo/widget_tooltip.o \ + tattoo/widget_verbs.o \ + animation.o \ + debugger.o \ + detection.o \ + events.o \ + fixed_text.o \ + fonts.o \ + image_file.o \ + inventory.o \ + journal.o \ + map.o \ + music.o \ + objects.o \ + people.o \ + resources.o \ + saveload.o \ + scene.o \ + screen.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/music.cpp b/engines/sherlock/music.cpp new file mode 100644 index 0000000000..49c48126f4 --- /dev/null +++ b/engines/sherlock/music.cpp @@ -0,0 +1,598 @@ +/* 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/config-manager.h" +#include "common/mutex.h" +#include "sherlock/sherlock.h" +#include "sherlock/music.h" +#include "sherlock/scalpel/drivers/mididriver.h" +// for Miles Audio (Sherlock Holmes 2) +#include "audio/miles.h" +// for 3DO digital music +#include "audio/decoders/aiff.h" + +namespace Sherlock { + +#define NUM_SONGS 45 + +/* This tells which song to play in each room, 0 = no song played */ +static const char ROOM_SONG[62] = { + 0, 20, 43, 6, 11, 2, 8, 15, 6, 28, + 6, 38, 7, 32, 16, 5, 8, 41, 9, 22, + 10, 23, 4, 39, 19, 24, 13, 27, 0, 30, + 3, 21, 26, 25, 16, 29, 1, 1, 18, 12, + 1, 17, 17, 31, 17, 34, 36, 7, 20, 20, + 33, 8, 44, 40, 42, 35, 0, 0, 0, 12, + 12 +}; + +static const char *const SONG_NAMES[NUM_SONGS] = { + "SINGERF", "CHEMIST", "TOBAC", "EQUEST", "MORTUARY", "DOCKS", "LSTUDY", + "LORD", "BOY", "PERFUM1", "BAKER1", "BAKER2", "OPERA1", "HOLMES", + "FFLAT", "OP1FLAT", "ZOO", "SROOM", "FLOWERS", "YARD", "TAXID", + "PUB1", "VICTIM", "RUGBY", "DORM", "SHERMAN", "LAWYER", "THEATRE", + "DETECT", "OPERA4", "POOL", "SOOTH", "ANNA1", "ANNA2", "PROLOG3", + "PAWNSHOP", "MUSICBOX", "MOZART1", "ROBHUNT", "PANCRAS1", "PANCRAS2", "LORDKILL", + "BLACKWEL", "RESCUE", "MAP" +}; + +MidiParser_SH::MidiParser_SH() { + _ppqn = 1; + setTempo(16667); + _data = nullptr; + _beats = 0; + _lastEvent = 0; + _trackEnd = nullptr; + + _musData = nullptr; + _musDataSize = 0; +} + +MidiParser_SH::~MidiParser_SH() { + Common::StackLock lock(_mutex); + unloadMusic(); + _driver = NULL; +} + +void MidiParser_SH::parseNextEvent(EventInfo &info) { + Common::StackLock lock(_mutex); + +// warning("parseNextEvent"); + + // there is no delta right at the start of the music data + // this order is essential, otherwise notes will get delayed or even go missing + if (_position._playPos != _tracks[0]) { + info.delta = *(_position._playPos++); + } else { + info.delta = 0; + } + + info.start = _position._playPos; + + info.event = *_position._playPos++; + //warning("Event %x", info.event); + _position._runningStatus = info.event; + + switch (info.command()) { + case 0xC: { // program change + int idx = *_position._playPos++; + info.basic.param1 = idx & 0x7f; + info.basic.param2 = 0; + } + break; + case 0xD: + info.basic.param1 = *_position._playPos++; + info.basic.param2 = 0; + break; + + case 0xB: + info.basic.param1 = *_position._playPos++; + info.basic.param2 = *_position._playPos++; + info.length = 0; + break; + + case 0x8: + case 0x9: + case 0xA: + case 0xE: + info.basic.param1 = *(_position._playPos++); + info.basic.param2 = *(_position._playPos++); + if (info.command() == 0x9 && info.basic.param2 == 0) { + // NoteOn with param2==0 is a NoteOff + info.event = info.channel() | 0x80; + } + info.length = 0; + break; + case 0xF: + if (info.event == 0xFF) { + error("SysEx META event 0xFF"); + + byte type = *(_position._playPos++); + switch(type) { + case 0x2F: + // End of Track + allNotesOff(); + stopPlaying(); + unloadMusic(); + return; + case 0x51: + warning("TODO: 0xFF / 0x51"); + return; + default: + warning("TODO: 0xFF / %x Unknown", type); + break; + } + } else if (info.event == 0xFC) { + // Official End-Of-Track signal + debugC(kDebugLevelMusic, "Music: System META event 0xFC"); + + byte type = *(_position._playPos++); + switch (type) { + case 0x80: // end of track, triggers looping + debugC(kDebugLevelMusic, "Music: META event triggered looping"); + jumpToTick(0, true, true, false); + break; + case 0x81: // end of track, stop playing + debugC(kDebugLevelMusic, "Music: META event triggered music stop"); + stopPlaying(); + unloadMusic(); + break; + default: + error("MidiParser_SH::parseNextEvent: Unknown META event 0xFC type %x", type); + break; + } + } else { + warning("TODO: %x / Unknown", info.event); + break; + } + break; + default: + warning("MidiParser_SH::parseNextEvent: Unsupported event code %x", info.event); + break; + }// switch (info.command()) +} + +bool MidiParser_SH::loadMusic(byte *musData, uint32 musDataSize) { + Common::StackLock lock(_mutex); + + debugC(kDebugLevelMusic, "Music: loadMusic()"); + unloadMusic(); + + _musData = musData; + _musDataSize = musDataSize; + + byte *headerPtr = _musData + 12; // skip over the already checked SPACE header + byte *pos = headerPtr; + + uint16 headerSize = READ_LE_UINT16(headerPtr); + assert(headerSize == 0x7F); // Security check + + // Skip over header + pos += headerSize; + + _lastEvent = 0; + _trackEnd = _musData + _musDataSize; + + _numTracks = 1; + _tracks[0] = pos; + + _ppqn = 1; + setTempo(16667); + setTrack(0); + + return true; +} + +void MidiParser_SH::unloadMusic() { + Common::StackLock lock(_mutex); + + if (_musData) { + delete[] _musData; + _musData = NULL; + _musDataSize = 0; + } + + MidiParser::unloadMusic(); +} + +/*----------------------------------------------------------------*/ + +Music::Music(SherlockEngine *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) { + _midiDriver = NULL; + _midiParser = NULL; + _musicType = MT_NULL; + _musicPlaying = false; + _musicOn = false; + _midiOption = false; + _musicVolume = 0; + + if (IS_3DO) { + // 3DO - uses digital samples for music + _musicOn = true; + return; + } + + if (_vm->_interactiveFl) + _vm->_res->addToCache("MUSIC.LIB"); + + MidiDriver::DeviceHandle dev; + + if (IS_SERRATED_SCALPEL) { + // Serrated Scalpel: used an internal Electronic Arts .MUS music engine + _midiParser = new MidiParser_SH(); + dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32); + _musicType = MidiDriver::getMusicType(dev); + + switch (_musicType) { + case MT_ADLIB: + _midiDriver = MidiDriver_SH_AdLib_create(); + break; + case MT_MT32: + _midiDriver = MidiDriver_MT32_create(); + break; + case MT_GM: + if (ConfMan.getBool("native_mt32")) { + _midiDriver = MidiDriver_MT32_create(); + _musicType = MT_MT32; + } + break; + default: + // Create default one + // I guess we shouldn't do this anymore + //_midiDriver = MidiDriver::createMidi(dev); + break; + } + } else { + // Rose Tattooo: seems to use Miles Audio 3 + _midiParser = MidiParser::createParser_XMIDI(); + dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM); + _musicType = MidiDriver::getMusicType(dev); + + switch (_musicType) { + case MT_ADLIB: + // SAMPLE.AD -> regular AdLib instrument data + // SAMPLE.OPL -> OPL-3 instrument data + // although in case of Rose Tattoo both files are exactly the same + _midiDriver = Audio::MidiDriver_Miles_AdLib_create("SAMPLE.AD", "SAMPLE.OPL"); + break; + case MT_MT32: + // Sherlock Holmes 2 does not have a MT32 timbre file + _midiDriver = Audio::MidiDriver_Miles_MT32_create(""); + break; + case MT_GM: + if (ConfMan.getBool("native_mt32")) { + _midiDriver = Audio::MidiDriver_Miles_MT32_create(""); + _musicType = MT_MT32; + } else { + _midiDriver = MidiDriver::createMidi(dev); + _musicType = MT_GM; + } + break; + default: + // Do not create anything + break; + } + } + + if (_midiDriver) { + int ret = _midiDriver->open(); + if (ret == 0) { + // Reset is done inside our MIDI driver + _midiDriver->setTimerCallback(_midiParser, &_midiParser->timerCallback); + } + _midiParser->setMidiDriver(_midiDriver); + _midiParser->setTimerRate(_midiDriver->getBaseTempo()); + + if (IS_SERRATED_SCALPEL) { + if (_musicType == MT_MT32) { + // Upload patches + Common::SeekableReadStream *MT32driverStream = _vm->_res->load("MTHOM.DRV", "MUSIC.LIB"); + + if (!MT32driverStream) + error("Music: could not load MTHOM.DRV, critical"); + + byte *MT32driverData = new byte[MT32driverStream->size()]; + int32 MT32driverDataSize = MT32driverStream->size(); + assert(MT32driverData); + + MT32driverStream->read(MT32driverData, MT32driverDataSize); + delete MT32driverStream; + + assert(MT32driverDataSize > 12); + byte *MT32driverDataPtr = MT32driverData + 12; + MT32driverDataSize -= 12; + + MidiDriver_MT32_uploadPatches(_midiDriver, MT32driverDataPtr, MT32driverDataSize); + delete[] MT32driverData; + } + } + + _musicOn = true; + } +} + +Music::~Music() { + stopMusic(); + if (_midiDriver) { + _midiDriver->setTimerCallback(this, NULL); + } + if (_midiParser) { + _midiParser->stopPlaying(); + delete _midiParser; + _midiParser = nullptr; + } + if (_midiDriver) { + _midiDriver->close(); + delete _midiDriver; + } +} + +bool Music::loadSong(int songNumber) { + debugC(kDebugLevelMusic, "Music: loadSong()"); + + if(songNumber == 100) + songNumber = 55; + else if(songNumber == 70) + songNumber = 54; + + if((songNumber > 60) || (songNumber < 1)) + return false; + + songNumber = ROOM_SONG[songNumber]; + + if(songNumber == 0) + songNumber = 12; + + if((songNumber > NUM_SONGS) || (songNumber < 1)) + return false; + + Common::String songName = Common::String(SONG_NAMES[songNumber - 1]); + + freeSong(); // free any song that is currently loaded + stopMusic(); + + if (!playMusic(songName)) + return false; + + startSong(); + return true; +} + +bool Music::loadSong(const Common::String &songName) { + freeSong(); // free any song that is currently loaded + stopMusic(); + + if (!playMusic(songName)) + return false; + + startSong(); + return true; +} + +void Music::syncMusicSettings() { + _musicOn = !ConfMan.getBool("mute") && !ConfMan.getBool("music_mute"); +} + +bool Music::playMusic(const Common::String &name) { + if (!_musicOn) + return false; + + debugC(kDebugLevelMusic, "Music: playMusic('%s')", name.c_str()); + + if (!IS_3DO) { + // MIDI based + if (!_midiDriver) + return false; + + Common::String midiMusicName = (IS_SERRATED_SCALPEL) ? name + ".MUS" : name + ".XMI"; + Common::SeekableReadStream *stream = _vm->_res->load(midiMusicName, "MUSIC.LIB"); + + byte *midiMusicData = new byte[stream->size()]; + int32 midiMusicDataSize = stream->size(); + + stream->read(midiMusicData, midiMusicDataSize); + delete stream; + + // for dumping the music tracks +#if 0 + Common::DumpFile outFile; + outFile.open(name + ".RAW"); + outFile.write(data, stream->size()); + outFile.flush(); + outFile.close(); +#endif + + if (midiMusicDataSize < 14) { + warning("Music: not enough data in music file"); + delete[] midiMusicData; + return false; + } + + byte *dataPos = midiMusicData; + uint32 dataSize = midiMusicDataSize; + + if (IS_SERRATED_SCALPEL) { + if (memcmp(" ", dataPos, 12)) { + warning("Music: expected header not found in music file"); + delete[] midiMusicData; + return false; + } + dataPos += 12; + dataSize -= 12; + + if (dataSize < 0x7F) { + warning("Music: expected music header not found in music file"); + delete[] midiMusicData; + return false; + } + + uint16 headerSize = READ_LE_UINT16(dataPos); + if (headerSize != 0x7F) { + warning("Music: header is not as expected"); + delete[] midiMusicData; + return false; + } + } else { + if (memcmp("FORM", dataPos, 4)) { + warning("Music: expected header not found in music file"); + delete[] midiMusicData; + return false; + } + } + + if (IS_SERRATED_SCALPEL) { + // Pass the music data to the driver as well + // because channel mapping and a few other things inside the header + switch (_musicType) { + case MT_ADLIB: + MidiDriver_SH_AdLib_newMusicData(_midiDriver, dataPos, dataSize); + break; + + case MT_MT32: + MidiDriver_MT32_newMusicData(_midiDriver, dataPos, dataSize); + break; + + default: + // should never happen + break; + } + } + + _midiParser->loadMusic(midiMusicData, midiMusicDataSize); + + } else { + // 3DO: sample based + Audio::AudioStream *musicStream; + Common::String digitalMusicName = "music/" + name + "_MW22.aifc"; + + if (isPlaying()) { + _mixer->stopHandle(_digitalMusicHandle); + } + + Common::File *digitalMusicFile = new Common::File(); + if (!digitalMusicFile->open(digitalMusicName)) { + warning("playMusic: can not open 3DO music '%s'", digitalMusicName.c_str()); + return false; + } + + // Try to load the given file as AIFF/AIFC + musicStream = Audio::makeAIFFStream(digitalMusicFile, DisposeAfterUse::YES); + if (!musicStream) { + warning("playMusic: can not load 3DO music '%s'", digitalMusicName.c_str()); + return false; + } + _mixer->playStream(Audio::Mixer::kMusicSoundType, &_digitalMusicHandle, musicStream); + } + return true; +} + +void Music::stopMusic() { + freeSong(); +} + +void Music::startSong() { + if (!_musicOn) + return; + + // TODO + warning("TODO: Sound::startSong"); + _musicPlaying = true; +} + +void Music::freeSong() { + if (!IS_3DO) { + if (_midiParser->isPlaying()) + _midiParser->stopPlaying(); + + // Free the MIDI MUS data buffer + _midiParser->unloadMusic(); + } + + _musicPlaying = false; +} + +void Music::waitTimerRoland(uint time) { + // TODO + warning("TODO: Sound::waitTimerRoland"); +} + +bool Music::isPlaying() { + if (!IS_3DO) { + // MIDI based + return _midiParser->isPlaying(); + } else { + // 3DO: sample based + return _mixer->isSoundHandleActive(_digitalMusicHandle); + } +} + +// Returns the current music position in milliseconds +uint32 Music::getCurrentPosition() { + if (!IS_3DO) { + // MIDI based + return (_midiParser->getTick() * 1000) / 60; // translate tick to millisecond + } else { + // 3DO: sample based + return _mixer->getSoundElapsedTime(_digitalMusicHandle); + } +} + +// This is used to wait for the music in certain situations like especially the intro +// Note: the original game didn't do this, instead it just waited for certain amounts of time +// We do this, so that the intro graphics + music work together even on faster/slower hardware. +bool Music::waitUntilMSec(uint32 msecTarget, uint32 msecMax, uint32 additionalDelay, uint32 noMusicDelay) { + uint32 msecCurrent = 0; + + if (!isPlaying()) { + return _vm->_events->delay(noMusicDelay, true); + } + while (1) { + if (!isPlaying()) { // Music is not playing anymore -> we are done + if (additionalDelay > 0) { + if (!_vm->_events->delay(additionalDelay, true)) + return false; + } + return true; + } + + msecCurrent = getCurrentPosition(); + //warning("waitUntilMSec: %lx", msecCurrent); + + if ((!msecMax) || (msecCurrent <= msecMax)) { + if (msecCurrent >= msecTarget) { + if (additionalDelay > 0) { + if (!_vm->_events->delay(additionalDelay, true)) + return false; + } + return true; + } + } + if (!_vm->_events->delay(10, true)) + return false; + } +} + +void Music::setMIDIVolume(int volume) { + warning("TODO: Music::setMIDIVolume"); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/music.h b/engines/sherlock/music.h new file mode 100644 index 0000000000..93c565c162 --- /dev/null +++ b/engines/sherlock/music.h @@ -0,0 +1,131 @@ +/* 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_MUSIC_H +#define SHERLOCK_MUSIC_H + +#include "audio/midiplayer.h" +#include "audio/midiparser.h" +//#include "audio/mididrv.h" +#include "sherlock/scalpel/drivers/mididriver.h" +// for 3DO digital music +#include "audio/audiostream.h" +#include "audio/mixer.h" +#include "common/mutex.h" + +namespace Sherlock { + +class SherlockEngine; + +class MidiParser_SH : public MidiParser { +public: + MidiParser_SH(); + ~MidiParser_SH(); + +protected: + Common::Mutex _mutex; + void parseNextEvent(EventInfo &info); + + uint8 _beats; + uint8 _lastEvent; + byte *_data; + byte *_trackEnd; + +public: + bool loadMusic(byte *musData, uint32 musSize); + virtual void unloadMusic(); + +private: + byte *_musData; + uint32 _musDataSize; +}; + +class Music { +private: + SherlockEngine *_vm; + Audio::Mixer *_mixer; + MidiParser *_midiParser; + MidiDriver *_midiDriver; + Audio::SoundHandle _digitalMusicHandle; + MusicType _musicType; + + /** + * Play the specified music resource + */ + bool playMusic(const Common::String &name); +public: + bool _musicPlaying; + bool _musicOn; + int _musicVolume; + bool _midiOption; + Common::String _currentSongName, _nextSongName; +public: + Music(SherlockEngine *vm, Audio::Mixer *mixer); + ~Music(); + + /** + * Saves sound-related settings + */ + void syncMusicSettings(); + + /** + * Load a specified song + */ + bool loadSong(int songNumber); + + /** + * Load a specified song + */ + bool loadSong(const Common::String &songName); + + /** + * Start playing a song + */ + void startSong(); + + /** + * Free any currently loaded song + */ + void freeSong(); + + /** + * Stop playing the music + */ + void stopMusic(); + + void waitTimerRoland(uint time); + + bool isPlaying(); + uint32 getCurrentPosition(); + + bool waitUntilMSec(uint32 msecTarget, uint32 maxMSec, uint32 additionalDelay, uint32 noMusicDelay); + + /** + * Sets the volume of the MIDI music with a value ranging from 0 to 127 + */ + void setMIDIVolume(int volume); +}; + +} // End of namespace Sherlock + +#endif + diff --git a/engines/sherlock/objects.cpp b/engines/sherlock/objects.cpp new file mode 100644 index 0000000000..6ef08c28cc --- /dev/null +++ b/engines/sherlock/objects.cpp @@ -0,0 +1,1533 @@ +/* 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/util.h" +#include "sherlock/objects.h" +#include "sherlock/people.h" +#include "sherlock/scene.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +#define START_FRAME 0 + +#define NUM_ADJUSTED_WALKS 21 + +// Distance to walk around WALK_AROUND boxes +#define CLEAR_DIST_X 5 +#define CLEAR_DIST_Y 0 + +#define ADJUST_COORD(COORD) \ + if (COORD.x != -1) \ + COORD.x *= FIXED_INT_MULTIPLIER; \ + if (COORD.y != -1) \ + COORD.y *= FIXED_INT_MULTIPLIER + +SherlockEngine *BaseObject::_vm; +bool BaseObject::_countCAnimFrames; + +/*----------------------------------------------------------------*/ + +void BaseObject::setVm(SherlockEngine *vm) { + _vm = vm; + _countCAnimFrames = false; +} + +BaseObject::BaseObject() { + _type = INVALID; + _sequences = nullptr; + _images = nullptr; + _imageFrame = nullptr; + _walkCount = 0; + _allow = 0; + _frameNumber = 0; + _lookFlag = 0; + _requiredFlag[0] = _requiredFlag[1] = 0; + _status = 0; + _misc = 0; + _maxFrames = 0; + _flags = 0; + _aType = OBJECT; + _lookFrames = 0; + _seqCounter = 0; + _lookcAnim = 0; + _seqStack = 0; + _seqTo = 0; + _descOffset = 0; + _seqCounter2 = 0; + _seqSize = 0; + _quickDraw = 0; + _scaleVal = 0; + _gotoSeq = 0; + _talkSeq = 0; + _restoreSlot = 0; +} + +bool BaseObject::hasAborts() const { + int seqNum = _talkSeq; + + // See if the object is in it's regular sequence + bool startChecking = !seqNum || _type == CHARACTER; + + uint idx = 0; + do + { + // Get the Frame value + int v = _sequences[idx++]; + + // See if we found an Allow Talk Interrupt Code + if (startChecking && v == ALLOW_TALK_CODE) + return true; + + // If we've started checking and we've encountered another Talk or Listen Sequence Code, + // then we're done checking this sequence because this is where it would repeat + if (startChecking && (v == TALK_SEQ_CODE || v == TALK_LISTEN_CODE)) + return false; + + // See if we've found the beginning of a Talk Sequence + if ((v == TALK_SEQ_CODE && seqNum < 128) || (v == TALK_LISTEN_CODE && seqNum >= 128)) { + // If checking was already on and we came across one of these codes, then there couldn't + // have been an Allow Talk Interrupt code in the sequence we were checking, so we're done. + if (startChecking) + return false; + + seqNum--; + // See if we're at the correct Talk Sequence Number + if (!(seqNum & 127)) + { + // Correct Sequence, Start Checking Now + startChecking = true; + } + } else { + // Move ahead any extra because of special control codes + switch (v) { + case 0: idx++; break; + case MOVE_CODE: + case TELEPORT_CODE: idx += 4; break; + case CALL_TALK_CODE:idx += 8; break; + case HIDE_CODE: idx += 2; break; + } + } + } while (idx < _seqSize); + + return true; +} + +void BaseObject::checkObject() { + Scene &scene = *_vm->_scene; + Sound &sound = *_vm->_sound; + Talk &talk = *_vm->_talk; + int checkFrame = _allow ? MAX_FRAME : FRAMES_END; + bool codeFound; + + if (_seqTo) { + byte *ptr = &_sequences[_frameNumber]; + if (*ptr == _seqTo) { + // The sequence is completed. Reset to normal + *ptr = _seqTo + (IS_ROSE_TATTOO ? 0 : SEQ_TO_CODE + 128); + _seqTo = 0; + } else { + // Continue doing sequence + if (*ptr > _seqTo) + *ptr -= 1; + else + *ptr += 1; + + return; + } + } + + ++_frameNumber; + + do { + if (!_sequences) { + warning("checkObject: _sequences is not set"); + break; + } + + // Check for end of sequence + codeFound = checkEndOfSequence(); + + if (_sequences[_frameNumber] >= 128 && _frameNumber < checkFrame) { + codeFound = true; + int v = _sequences[_frameNumber]; + + // Check for a Talk or Listen Sequence + if (IS_ROSE_TATTOO && v == ALLOW_TALK_CODE) { + if (_gotoSeq) { + setObjTalkSequence(_gotoSeq); + } else { + ++_frameNumber; + } + } else if (IS_ROSE_TATTOO && (v == TALK_SEQ_CODE || v == TALK_LISTEN_CODE)) { + if (_talkSeq) + setObjTalkSequence(_talkSeq); + else + setObjSequence(0, false); + } else if (v >= GOTO_CODE) { + // Goto code found + v -= GOTO_CODE; + _seqCounter2 = _seqCounter; + _seqStack = _frameNumber + 1; + setObjSequence(v, false); + } else if (v >= SOUND_CODE && (v < (SOUND_CODE + 30))) { + codeFound = true; + ++_frameNumber; + v -= SOUND_CODE + (IS_SERRATED_SCALPEL ? 1 : 0); + + if (sound._soundOn && !_countCAnimFrames) { + if (!scene._sounds[v]._name.empty() && sound._digitized) + sound.playLoadedSound(v, 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 if (IS_ROSE_TATTOO && v == TELEPORT_CODE) { + _position.x = READ_LE_UINT16(&_sequences[_frameNumber + 1]); + _position.y = READ_LE_UINT16(&_sequences[_frameNumber + 3]); + + _frameNumber += 5; + } else if (IS_ROSE_TATTOO && v == CALL_TALK_CODE) { + Common::String filename; + for (int idx = 0; idx < 8; ++idx) { + if (_sequences[_frameNumber + 1 + idx] != 1) + filename += (char)_sequences[_frameNumber + 1 + idx]; + else + break; + } + + _frameNumber += 8; + talk.talkTo(filename); + + } else if (IS_ROSE_TATTOO && v == HIDE_CODE) { + switch (_sequences[_frameNumber + 2]) { + case 1: + // Hide Object + if (scene._bgShapes[_sequences[_frameNumber + 1] - 1]._type != HIDDEN) + scene._bgShapes[_sequences[_frameNumber + 1] - 1].toggleHidden(); + break; + + case 2: + // Activate Object + if (scene._bgShapes[_sequences[_frameNumber + 1] - 1]._type == HIDDEN) + scene._bgShapes[_sequences[_frameNumber + 1] - 1].toggleHidden(); + break; + + case 3: + // Toggle Object + scene._bgShapes[_sequences[_frameNumber + 1] - 1].toggleHidden(); + break; + + default: + break; + } + _frameNumber += 3; + + } else { + v -= 128; + + // 68-99 is a sequence code + if (v > SEQ_TO_CODE) { + if (IS_ROSE_TATTOO) { + ++_frameNumber; + byte *p = &_sequences[_frameNumber]; + _seqTo = *p; + *p = *(p - 2); + + if (*p > _seqTo) + *p -= 1; + else + *p += 1; + + --_frameNumber; + } else { + 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 (IS_ROSE_TATTOO && v == 10) { + // Set delta for objects + _delta = Common::Point(READ_LE_UINT16(&_sequences[_frameNumber + 1]), + READ_LE_UINT16(&_sequences[_frameNumber + 3])); + _noShapeSize = Common::Point(0, 0); + _frameNumber += 4; + + } 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]); + } + + if (_use[v]._useFlag) + _vm->setFlags(_use[v]._useFlag); + } + + ++_frameNumber; + } + } + } while (codeFound); +} + +bool BaseObject::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 if (IS_ROSE_TATTOO && _talkSeq && seq == 0) { + setObjTalkSequence(_talkSeq); + } 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 BaseObject::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 BaseObject::checkNameForCodes(const Common::String &name, FixedTextActionId fixedTextActionId) { + FixedText &fixedText = *_vm->_fixedText; + 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; + } + + case 'V': + // Do nothing for Verb codes. This is only a flag for Inventory syntax + break; + + default: + if (ch >= '0' && ch <= '9') { + scene._goToScene = atoi(name.c_str() + 1); + + if (IS_SERRATED_SCALPEL && scene._goToScene < 97) { + Scalpel::ScalpelMap &map = *(Scalpel::ScalpelMap *)_vm->_map; + if (map[scene._goToScene].x) { + map._overPos.x = (map[scene._goToScene].x - 6) * FIXED_INT_MULTIPLIER; + map._overPos.y = (map[scene._goToScene].y + 9) * FIXED_INT_MULTIPLIER; + } + } + + const char *p; + if ((p = strchr(name.c_str(), ',')) != nullptr) { + ++p; + + Common::String s(p, p + 3); + people._savedPos.x = atoi(s.c_str()); + + s = Common::String(p + 3, p + 6); + people._savedPos.y = atoi(s.c_str()); + + s = Common::String(p + 6, p + 9); + people._savedPos._facing = atoi(s.c_str()); + if (people._savedPos._facing == 0) + people._savedPos._facing = 10; + } else if ((p = strchr(name.c_str(), '/')) != nullptr) { + people._savedPos = PositionFacing(1, 0, 100 + atoi(p + 1)); + } + } else { + scene._goToScene = 100; + } + + people[HOLMES]._position = Point32(0, 0); + break; + } + } else if (name.hasPrefix("!")) { + // Message attached to canimation + int messageNum = atoi(name.c_str() + 1); + ui._infoFlag = true; + ui.clearInfo(); + Common::String errorMessage = fixedText.getActionMessage(fixedTextActionId, messageNum); + screen.print(Common::Point(0, INFO_LINE + 1), COL_INFO_FOREGROUND, "%s", errorMessage.c_str()); + ui._menuCounter = 25; + } else if (name.hasPrefix("@")) { + // Message attached to canimation + ui._infoFlag = true; + ui.clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), COL_INFO_FOREGROUND, "%s", name.c_str() + 1); + printed = true; + ui._menuCounter = 25; + } + + return printed; +} + +/*----------------------------------------------------------------*/ + +void Sprite::clear() { + _name = ""; + _description = ""; + _examine.clear(); + _pickUp = ""; + _walkSequences.clear(); + _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; + _altImages = nullptr; + _altSeq = 0; + Common::fill(&_stopFrames[0], &_stopFrames[8], (ImageFrame *)nullptr); +} + +void Sprite::setImageFrame() { + int frameNum = MAX(_frameNumber, 0); + int imageNumber = _walkSequences[_sequenceNumber][frameNum]; + + if (IS_SERRATED_SCALPEL) + imageNumber = imageNumber + _walkSequences[_sequenceNumber][0] - 2; + else if (imageNumber > _maxFrames) + imageNumber = 1; + + // Get the images to use + ImageFile *images = _altSeq ? _altImages : _images; + assert(images); + + if (IS_3DO) { + // only do this to the image-array with 110 entries + // map uses another image-array and this code + if (images->size() == 110) { + // 3DO has 110 animation frames inside walk.anim + // PC has 55 + // this adjusts the framenumber accordingly + // sort of HACK + imageNumber *= 2; + } + } else if (IS_ROSE_TATTOO) { + --imageNumber; + } + + // Set the frame pointer + _imageFrame = &(*images)[imageNumber]; +} + +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 / FIXED_INT_MULTIPLIER, _position.y / FIXED_INT_MULTIPLIER); + + if (_type != CHARACTER || (IS_SERRATED_SCALPEL && talk._talkCounter)) + return; + + pt = _walkCount ? _position + _delta : _position; + pt.x /= FIXED_INT_MULTIPLIER; + pt.y /= FIXED_INT_MULTIPLIER; + + if (IS_ROSE_TATTOO) { + checkObject(); + + // For Rose Tattoo, we only do the further processing for Sherlock + if (this != &people[HOLMES]) + return; + } + + 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 { + gotoStand(); + } + break; + + case TALK_EVERY: + if (obj._aType == TALK_EVERY) { + obj._type = HIDDEN; + obj.setFlagsAndToggles(); + talk.talkTo(obj._use[0]._target); + } else { + gotoStand(); + } + break; + + case FLAG_SET: + obj.setFlagsAndToggles(); + obj._type = HIDDEN; + break; + + case WALK_AROUND: + if (objBounds.contains(people[HOLMES]._walkTo.front())) { + // Reached zone + gotoStand(); + } 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[HOLMES]._imageFrame->_frame.w / 2; + people[HOLMES]._walkDest = walkPos; + people[HOLMES]._walkTo.push(walkPos); + people[HOLMES].setWalking(); + } + break; + + case DELTA: + _position.x += 200; + break; + + default: + break; + } + } + } + } +} + +const Common::Rect Sprite::getOldBounds() const { + return Common::Rect(_oldPosition.x, _oldPosition.y, _oldPosition.x + _oldSize.x, _oldPosition.y + _oldSize.y); +} + +/*----------------------------------------------------------------*/ + +void WalkSequence::load(Common::SeekableReadStream &s) { + char buffer[9]; + s.read(buffer, 9); + _vgsName = Common::String(buffer); + _horizFlip = s.readByte() != 0; + + _sequences.resize(s.readUint16LE()); + s.skip(4); // Skip over pointer field of structure + + s.read(&_sequences[0], _sequences.size()); +} + +/*----------------------------------------------------------------*/ + +WalkSequences &WalkSequences::operator=(const WalkSequences &src) { + resize(src.size()); + for (uint idx = 0; idx < size(); ++idx) { + const WalkSequence &wSrc = src[idx]; + WalkSequence &wDest = (*this)[idx]; + wDest._horizFlip = wSrc._horizFlip; + + wDest._sequences.resize(wSrc._sequences.size()); + Common::copy(&wSrc._sequences[0], &wSrc._sequences[0] + wSrc._sequences.size(), &wDest._sequences[0]); + } + + return *this; +} + +/*----------------------------------------------------------------*/ + +ActionType::ActionType() { + _cAnimNum = _cAnimSpeed = 0; + _useFlag = 0; +} + +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(): ActionType() { +} + +void UseType::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + char buffer[12]; + + if (isRoseTattoo) { + s.read(buffer, 12); + _verb = Common::String(buffer); + } + + ActionType::load(s); + + _useFlag = s.readSint16LE(); + + if (!isRoseTattoo) + s.skip(6); + + s.read(buffer, 12); + _target = Common::String(buffer); +} + +void UseType::load3DO(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.readSint16BE(); + + s.skip(6); + + s.read(buffer, 12); + _target = Common::String(buffer); +} + +void UseType::synchronize(Serializer &s) { + s.syncString(_verb); + s.syncAsSint16LE(_cAnimNum); + s.syncAsSint16LE(_cAnimSpeed); + s.syncAsSint16LE(_useFlag); + + for (int idx = 0; idx < 4; ++idx) + s.syncString(_names[idx]); + s.syncString(_target); +} + +/*----------------------------------------------------------------*/ + +Object::Object(): BaseObject() { + _sequenceNumber = 0; + _sequenceOffset = 0; + _pickup = 0; + _defaultCommand = 0; + _pickupFlag = 0; +} + +void Object::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + 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(); + if (!isRoseTattoo) { + _goto.x = _goto.x * FIXED_INT_MULTIPLIER / 100; + _goto.y = _goto.y * FIXED_INT_MULTIPLIER / 100; + } + + _pickup = isRoseTattoo ? 0 : s.readByte(); + _defaultCommand = isRoseTattoo ? 0 : s.readByte(); + _lookFlag = s.readSint16LE(); + _pickupFlag = isRoseTattoo ? 0 : s.readSint16LE(); + _requiredFlag[0] = s.readSint16LE(); + _noShapeSize.x = s.readUint16LE(); + _noShapeSize.y = s.readUint16LE(); + _status = s.readUint16LE(); + _misc = s.readByte(); + _maxFrames = s.readUint16LE(); + _flags = s.readByte(); + + if (!isRoseTattoo) + _aOpen.load(s); + + _aType = (AType)s.readByte(); + _lookFrames = s.readByte(); + _seqCounter = s.readByte(); + if (isRoseTattoo) { + _lookPosition.x = s.readUint16LE() * FIXED_INT_MULTIPLIER; + _lookPosition.y = s.readSint16LE() * FIXED_INT_MULTIPLIER; + } else { + _lookPosition.x = s.readUint16LE() * FIXED_INT_MULTIPLIER / 100; + _lookPosition.y = s.readByte() * FIXED_INT_MULTIPLIER; + } + _lookPosition._facing = s.readByte(); + _lookcAnim = s.readByte(); + + if (!isRoseTattoo) + _aClose.load(s); + + _seqStack = s.readByte(); + _seqTo = s.readByte(); + _descOffset = s.readUint16LE(); + _seqCounter2 = s.readByte(); + _seqSize = s.readUint16LE(); + + if (isRoseTattoo) { + for (int idx = 0; idx < 6; ++idx) + _use[idx].load(s, true); + + _quickDraw = s.readByte(); + _scaleVal = s.readUint16LE(); + _requiredFlag[1] = s.readSint16LE(); + _gotoSeq = s.readByte(); + _talkSeq = s.readByte(); + _restoreSlot = s.readByte(); + } else { + s.skip(1); + _aMove.load(s); + s.skip(8); + + for (int idx = 0; idx < 4; ++idx) + _use[idx].load(s, false); + } + //warning("object %s, useAnim %d", _name.c_str(), _use[0]._cAnimNum); +} + +void Object::load3DO(Common::SeekableReadStream &s) { + int32 streamStartPos = s.pos(); + char buffer[41]; + + _examine.clear(); + _sequences = nullptr; + _images = nullptr; + _imageFrame = nullptr; + + // on 3DO all of this data is reordered!!! + // it seems that possibly the 3DO compiler reordered the global struct + // 3DO size for 1 object is 588 bytes + s.skip(4); + _sequenceOffset = s.readUint16LE(); // weird that this seems to be LE + s.seek(10, SEEK_CUR); + + // Offset 16 + _frameNumber = s.readSint16BE(); + _sequenceNumber = s.readSint16BE(); + _position.x = s.readSint16BE(); + _position.y = s.readSint16BE(); + _delta.x = s.readSint16BE(); + _delta.y = s.readSint16BE(); + _type = (SpriteType)s.readUint16BE(); + _oldPosition.x = s.readSint16BE(); + _oldPosition.y = s.readSint16BE(); + _oldSize.x = s.readUint16BE(); + _oldSize.y = s.readUint16BE(); + + _goto.x = s.readSint16BE(); + _goto.y = s.readSint16BE(); + _goto.x = _goto.x * FIXED_INT_MULTIPLIER / 100; + _goto.y = _goto.y * FIXED_INT_MULTIPLIER / 100; + + // Offset 42 + warning("pos %d", s.pos()); + + // Unverified + _lookFlag = s.readSint16BE(); + _pickupFlag = s.readSint16BE(); + _requiredFlag[0] = s.readSint16BE(); + _noShapeSize.x = s.readUint16BE(); + _noShapeSize.y = s.readUint16BE(); + _status = s.readUint16BE(); + // Unverified END + + _maxFrames = s.readUint16BE(); + // offset 56 + _lookPosition.x = s.readUint16BE() * FIXED_INT_MULTIPLIER / 100; + // offset 58 + _descOffset = s.readUint16BE(); + _seqSize = s.readUint16BE(); + + s.skip(2); // boundary filler + + // 288 bytes + for (int idx = 0; idx < 4; ++idx) { + _use[idx].load3DO(s); + s.skip(2); // Filler + } + + // 158 bytes + _aOpen.load(s); // 2 + 12*4 bytes = 50 bytes + s.skip(2); // Boundary filler + _aClose.load(s); + s.skip(2); // Filler + _aMove.load(s); + s.skip(2); // Filler + + // offset 508 + // 3DO: name is at the end + s.read(buffer, 12); + _name = Common::String(buffer); + s.read(buffer, 41); + _description = Common::String(buffer); + + // Unverified + _walkCount = s.readByte(); + _allow = s.readByte(); + _pickup = s.readByte(); + _defaultCommand = s.readByte(); + // Unverified END + + // Probably those here?!?! + _misc = s.readByte(); + _flags = s.readByte(); + + // Unverified + _aType = (AType)s.readByte(); + _lookFrames = s.readByte(); + _seqCounter = s.readByte(); + // Unverified END + + _lookPosition.y = s.readByte() * FIXED_INT_MULTIPLIER; + _lookPosition._facing = s.readByte(); + + // Unverified + _lookcAnim = s.readByte(); + _seqStack = s.readByte(); + _seqTo = s.readByte(); + _seqCounter2 = s.readByte(); + // Unverified END + + s.skip(12); // Unknown + + //warning("object %s, offset %d", _name.c_str(), streamPos); + //warning("object %s, lookPosX %d, lookPosY %d", _name.c_str(), _lookPosition.x, _lookPosition.y); + //warning("object %s, defCmd %d", _name.c_str(), _defaultCommand); + int32 dataSize = s.pos() - streamStartPos; + assert(dataSize == 588); +} + +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::setObjTalkSequence(int seq) { + Talk &talk = *_vm->_talk; + + // See if we're supposed to restore the object's sequence from the talk sequence stack + if (seq == -1) { + TalkSequence &ts = talk._talkSequenceStack[_restoreSlot]; + if (_seqTo != 0) + _sequences[_frameNumber] = _seqTo; + _frameNumber = ts._frameNumber; + _sequenceNumber = ts._sequenceNumber; + _seqStack = ts._seqStack; + _seqTo = ts._seqTo; + _seqCounter = ts._seqCounter; + _seqCounter2 = ts._seqCounter2; + _talkSeq = 0; + + // Flag this slot as free again + ts._obj = nullptr; + + return; + } + + assert(_type != CHARACTER); + + talk.pushTalkSequence(this); + int talkSeqNum = seq; + + // Find where the talk sequence data begins in the object + int idx = 0; + for (;;) { + // Get the Frame value + byte f = _sequences[idx++]; + + // See if we've found the beginning of a Talk Sequence + if ((f == TALK_SEQ_CODE && seq < 128) || (f == TALK_LISTEN_CODE && seq > 128)) { + --seq; + + // See if we're at the correct Talk Sequence Number + if (!(seq & 127)) + { + // Correct Sequence, Start Talking Here + if (_seqTo != 0) + _sequences[_frameNumber] = _seqTo; + _frameNumber = idx; + _seqTo = 0; + _seqStack = 0; + _seqCounter = 0; + _seqCounter2 = 0; + _talkSeq = talkSeqNum; + break; + } + } else { + // Move ahead any extra because of special control codes + switch (f) { + case 0: idx++; break; + case MOVE_CODE: + case TELEPORT_CODE: idx += 4; break; + case CALL_TALK_CODE: idx += 8; break; + case HIDE_CODE: idx += 2; break; + } + } + + // See if we're out of sequence data + if (idx >= (int)_seqSize) + break; + } +} + +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; + + if (IS_ROSE_TATTOO && (_delta.x || _delta.y)) { + // The shape position is in pixels, and the delta is in fixed integer amounts + int t; + _noShapeSize.x += _delta.x; + t = _noShapeSize.x / (FIXED_INT_MULTIPLIER / 10); + _noShapeSize.x -= t * (FIXED_INT_MULTIPLIER / 10); + _position.x += t; + + _noShapeSize.y += _delta.y; + t = _noShapeSize.y / (FIXED_INT_MULTIPLIER / 10); + _noShapeSize.y -= t * (FIXED_INT_MULTIPLIER / 10); + _position.y += t; + } else if (IS_SERRATED_SCALPEL) { + // The delta is in whole pixels, so simply adjust the position with it + _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(FixedTextActionId fixedTextActionId) { + FixedText &fixedText = *_vm->_fixedText; + 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], kFixedTextAction_Invalid)) { + 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(); + Common::String errorMessage = fixedText.getActionMessage(fixedTextActionId, message); + screen.print(Common::Point(0, INFO_LINE + 1), COL_INFO_FOREGROUND, "%s", errorMessage.c_str()); + 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[HOLMES].goAllTheWay(); + ui._menuCounter = 25; + ui._temp1 = 1; + } + + for (int idx = 0; idx < NAMES_COUNT && !talk._talkToAbort; ++idx) { + if (checkNameForCodes(_use[0]._names[idx], kFixedTextAction_Invalid)) { + 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), COL_INFO_FOREGROUND, "Picked up %s", itemName.c_str()); + ui._menuCounter = 25; + } + } + + return numObjects; +} + +const Common::Rect Object::getNewBounds() const { + Point32 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, bool isRoseTattoo, uint32 dataOffset) { + char buffer[12]; + s.read(buffer, 12); + _name = Common::String(buffer); + + if (isRoseTattoo) { + Common::fill(&_sequences[0], &_sequences[30], 0); + _dataSize = s.readUint32LE(); + } else { + s.read(_sequences, 30); + } + + _position.x = s.readSint16LE(); + _position.y = s.readSint16LE(); + + if (isRoseTattoo) { + _flags = s.readByte(); + _scaleVal = s.readSint16LE(); + } else { + _dataSize = s.readUint32LE(); + _type = (SpriteType)s.readUint16LE(); + _flags = s.readByte(); + } + + _goto[0].x = s.readSint16LE(); + _goto[0].y = s.readSint16LE(); + _goto[0]._facing = s.readSint16LE(); + ADJUST_COORD(_goto[0]); + + if (isRoseTattoo) { + // Get Goto position and facing for second NPC + _goto[1].x = s.readSint16LE(); + _goto[1].y = s.readSint16LE(); + _goto[1]._facing = s.readSint16LE(); + ADJUST_COORD(_goto[1]); + } else if (_goto[0].x != -1) { + // For Serrated Scalpel, adjust the loaded co-ordinates + _goto[0].x = _goto[0].x / 100; + _goto[0].y = _goto[0].y / 100; + } + + _teleport[0].x = s.readSint16LE(); + _teleport[0].y = s.readSint16LE(); + _teleport[0]._facing = s.readSint16LE(); + ADJUST_COORD(_teleport[0]); + + if (isRoseTattoo) { + // Get Teleport position and facing for second NPC + _teleport[1].x = s.readSint16LE(); + _teleport[1].y = s.readSint16LE(); + _teleport[1]._facing = s.readSint16LE(); + ADJUST_COORD(_teleport[1]); + } else if (_teleport[0].x != -1) { + // For Serrated Scalpel, adjust the loaded co-ordinates + _teleport[0].x = _teleport[0].x / 100; + _teleport[0].y = _teleport[0].y / 100; + } + + // Save offset of data, which is actually inside another table inside the room data file + // This table is at offset 44 for Serrated Scalpel + // TODO: find it for the other game + _dataOffset = dataOffset; +} + +void CAnim::load3DO(Common::SeekableReadStream &s, uint32 dataOffset) { + // this got reordered on 3DO + // maybe it was the 3DO compiler + + _dataSize = s.readUint32BE(); + // Save offset of data, which is inside another table inside the room data file + _dataOffset = dataOffset; + + _position.x = s.readSint16BE(); + _position.y = s.readSint16BE(); + + _type = (SpriteType)s.readUint16BE(); + + _goto[0].x = s.readSint16BE(); + _goto[0].y = s.readSint16BE(); + _goto[0]._facing = s.readSint16BE(); + + _teleport[0].x = s.readSint16BE(); + _teleport[0].y = s.readSint16BE(); + _teleport[0]._facing = s.readSint16BE(); + + char buffer[12]; + s.read(buffer, 12); + _name = Common::String(buffer); + + s.read(_sequences, 30); + _flags = s.readByte(); + + s.skip(3); // Filler + + _goto[0].x = _goto[0].x * FIXED_INT_MULTIPLIER / 100; + _goto[0].y = _goto[0].y * FIXED_INT_MULTIPLIER / 100; + _teleport[0].x = _teleport[0].x * FIXED_INT_MULTIPLIER / 100; + _teleport[0].y = _teleport[0].y * FIXED_INT_MULTIPLIER / 100; +} + +/*----------------------------------------------------------------*/ + +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..996054be43 --- /dev/null +++ b/engines/sherlock/objects.h @@ -0,0 +1,478 @@ +/* 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/image_file.h" +#include "sherlock/fixed_text.h" +#include "sherlock/saveload.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 + + // Rose Tattoo + HIDDEN_CHARACTER = 128 +}; + +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 +#define FIXED_INT_MULTIPLIER 1000 + +// 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) +#define HIDE_CODE (7+128) // Code for hiding/unhiding an object from a Sequence +#define CALL_TALK_CODE (8+128) // Code for call a Talk File from a Sequence +#define TELEPORT_CODE (9+128) // Code for setting Teleport Data (X,Y) +#define MOVE_CODE (10+128) // Code for setting Movement Delta (X,Y) + +#define GOTO_CODE 228 +#define TALK_SEQ_CODE 252 // Code specifying start of talk sequence frames in a Sequence +#define TALK_LISTEN_CODE 251 // Code specifying start of talk listen frames in a Sequence +#define ALLOW_TALK_CODE 250 + +#define UPPER_LIMIT 0 +#define LOWER_LIMIT (IS_SERRATED_SCALPEL ? CONTROLS_Y : SHERLOCK_SCREEN_HEIGHT) +#define LEFT_LIMIT 0 +#define RIGHT_LIMIT SHERLOCK_SCREEN_WIDTH + +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 PositionFacing : public Point32 { +public: + int _facing; + + PositionFacing() : Point32(), _facing(0) {} + PositionFacing(int xp, int yp, int theFacing) : Point32(xp, yp), _facing(theFacing) {} + PositionFacing &operator=(const Point32 &pt) { + x = pt.x; y = pt.y; + return *this; + } +}; + +struct WalkSequence { + Common::String _vgsName; + bool _horizFlip; + Common::Array<byte> _sequences; + + WalkSequence() : _horizFlip(false) {} + const byte &operator[](int idx) { return _sequences[idx]; } + + /** + * Load data for the sequence from a stream + */ + void load(Common::SeekableReadStream &s); +}; + +class WalkSequences : public Common::Array < WalkSequence > { +public: + WalkSequences &operator=(const WalkSequences &src); +}; + +enum { REVERSE_DIRECTION = 0x80 }; +#define NAMES_COUNT 4 + +struct ActionType { + int _cAnimNum; + int _cAnimSpeed; + Common::String _names[NAMES_COUNT]; + int _useFlag; // Which flag USE will set (if any) + + ActionType(); + + /** + * Load the data for the action + */ + void load(Common::SeekableReadStream &s); +}; + +struct UseType: public ActionType { + Common::String _target; + Common::String _verb; + + UseType(); + + /** + * Load the data for the UseType + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); + void load3DO(Common::SeekableReadStream &s); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Serializer &s); +}; + +class BaseObject { +protected: + static SherlockEngine *_vm; +protected: + /** + * 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; +public: + SpriteType _type; // Type of object/sprite + Common::String _description; // Description lines + byte *_sequences; // Holds animation sequences + ImageFile *_images; // Sprite images + ImageFrame *_imageFrame; // Pointer to shape in the images + int _walkCount; // Walk counter + int _allow; // Allowed UI commands + int _frameNumber; // Frame number in rame sequence to draw + Point32 _position; // Current position + Point32 _delta; // Momvement amount + Common::Point _oldPosition; // Old position + Common::Point _oldSize; // Image's old size + Point32 _goto; // Walk destination + + int _lookFlag; // Which flag LOOK will set (if any) + int _requiredFlag[2]; // 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 + 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 + PositionFacing _lookPosition; // Where to walk when examining object + int _lookcAnim; + 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 + UseType _use[6]; // Serrated Scalpel uses 4, Rose Tattoo 6 + int _quickDraw; // Flag telling whether to use quick draw routine or not + int _scaleVal; // Tells how to scale the sprite + int _gotoSeq; // Used by Talk to tell which sequence to goto when able + int _talkSeq; // Tells which talk sequence currently in use (Talk or Listen) + int _restoreSlot; // Used when talk returns to the previous sequence +public: + BaseObject(); + virtual ~BaseObject() {} + static void setVm(SherlockEngine *vm); + + /** + * Returns true if the the object has an Allow Talk Code in the sequence that it's + * currently running, specified by the _talkSeq field of the object. If it's 0, + * then it's a regular sequence. If it's not 0 but below 128, then it's a Talk Sequence. + * If it's above 128, then it's one of the Listen sequences. + */ + bool hasAborts() const; + + /** + * 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, FixedTextActionId fixedTextActionId = kFixedTextAction_Invalid); + + /** + * Adjusts the frame and sequence variables of a sprite that corresponds to the current speaker + * so that it points to the beginning of the sequence number's talk sequence in the object's + * sequence buffer + * @param seq Which sequence to use (if there's more than 1) + * @remarks 1: First talk seq, 2: second talk seq, etc. + */ + virtual void setObjTalkSequence(int seq) {} +}; + +class Sprite: public BaseObject { +public: + Common::String _name; + Common::String _examine; // Examine in-depth description + Common::String _pickUp; // Message for if you can't pick up object + + WalkSequences _walkSequences; // Holds animation sequences + int _sequenceNumber; // Sequence being used + Common::Point _noShapeSize; // Size of a NO_SHAPE + int _status; // Status: open/closed, moved/not moved + int8 _misc; // Miscellaneous use + + // Rose Tattoo fields + int _startSeq; // Frame sequence starts at + ImageFrame *_stopFrames[8]; // Stop/rest frame for each direction + ImageFile *_altImages; // Images used for alternate NPC sequences + int _altSeq; // Which of the sequences the alt graphics apply to (0: main, 1=NPC seq) + int _centerWalk; // Flag telling the walk code to offset the walk destination + Common::Point _adjust; // Fine tuning adjustment to position when drawn + int _oldWalkSequence; +public: + Sprite(): BaseObject() { clear(); } + virtual ~Sprite() {} + + 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(); + + /** + * Checks the sprite's position to see if it's collided with any special objects + */ + void checkSprite(); + + /** + * Adjusts the frame and sequence variables of a sprite that corresponds to the current speaker + * so that it points to the beginning of the sequence number's talk sequence in the object's + * sequence buffer + * @param seq Which sequence to use (if there's more than 1) + * @remarks 1: First talk seq, 2: second talk seq, etc. + */ + virtual void setObjTalkSequence(int seq) {} + + /** + * Return frame width + */ + int frameWidth() const { return _imageFrame ? _imageFrame->_frame.w : 0; } + + /** + * Return frame height + */ + int frameHeight() const { return _imageFrame ? _imageFrame->_frame.h : 0; } + + /** + * Returns the old bounsd for the sprite from the previous frame + */ + const Common::Rect getOldBounds() const; + + /** + * This adjusts the sprites position, as well as it's animation sequence: + */ + virtual void adjustSprite() = 0; + + /** + * Bring a moving character using the sprite to a standing position + */ + virtual void gotoStand() = 0; + + /** + * Set the variables for moving a character from one poisition to another + * in a straight line + */ + virtual void setWalking() = 0; +}; + +enum { OBJ_BEHIND = 1, OBJ_FLIPPED = 2, OBJ_FORWARD = 4, TURNON_OBJ = 0x20, TURNOFF_OBJ = 0x40 }; +#define USE_COUNT 4 + +class Object: public BaseObject { +public: + Common::String _name; // Name + Common::String _examine; // Examine in-depth description + int _sequenceNumber; + int _sequenceOffset; + int _pickup; + int _defaultCommand; // Default right-click command + + // Serrated Scalpel fields + int _pickupFlag; // Which flag PICKUP will set (if any) + ActionType _aOpen; // Holds data for moving object + ActionType _aClose; + ActionType _aMove; + + Object(); + virtual ~Object() {} + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); + void load3DO(Common::SeekableReadStream &s); + + /** + * Toggle the type of an object between hidden and active + */ + void toggleHidden(); + + /** + * 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(FixedTextActionId fixedTextActionId = kFixedTextAction_Invalid); + + /** + * 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; + + /** + * Adjusts the frame and sequence variables of a sprite that corresponds to the current speaker + * so that it points to the beginning of the sequence number's talk sequence in the object's + * sequence buffer + * @param seq Which sequence to use (if there's more than 1) + * @remarks 1: First talk seq, 2: second talk seq, etc. + */ + virtual void setObjTalkSequence(int seq); +}; + +struct CAnim { + Common::String _name; // Name + Common::Point _position; // Position + int _dataSize; // Size of uncompressed animation data + uint32 _dataOffset; // offset within room file of animation data + int _flags; // Tells if can be walked behind + PositionFacing _goto[2]; // Position Holmes (and NPC in Rose Tattoo) should walk to before anim starts + PositionFacing _teleport[2]; // Location Holmes (and NPC) shoul teleport to after playing canim + + // Scalpel specific + byte _sequences[MAX_FRAME]; // Animation sequences + SpriteType _type; + + // Rose Tattoo specific + int _scaleVal; // How much the canim is scaled + + /** + * Load the data for the animation + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo, uint32 dataOffset); + void load3DO(Common::SeekableReadStream &s, uint32 dataOffset); +}; + +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..1e19dccb32 --- /dev/null +++ b/engines/sherlock/people.cpp @@ -0,0 +1,332 @@ +/* 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" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/tattoo/tattoo_people.h" + +namespace Sherlock { + +// 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 +}; + +// Rose Tattoo walk image libraries +// Walk resources within WALK.LIB +const char *const WALK_LIB_NAMES[NUM_IN_WALK_LIB] = { + "SVGAWALK.VGS", + "COATWALK.VGS", + "WATSON.VGS", + "NOHAT.VGS", + "TUPRIGHT.VGS", + "TRIGHT.VGS", + "TDOWNRG.VGS", + "TWUPRIGH.VGS", + "TWRIGHT.VGS", + "TWDOWNRG.VGS" +}; + +/*----------------------------------------------------------------*/ + +Person::Person() : Sprite() { + _walkLoaded = false; + _oldWalkSequence = -1; + _srcZone = _destZone = 0; +} + +void Person::goAllTheWay() { + Scene &scene = *_vm->_scene; + Common::Point srcPt = getSourcePoint(); + + // 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); + + // Check for any restriction of final destination position + _walkDest = _vm->_people->restrictToZone(_destZone, _walkDest); + } + + // 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]; + + const WalkArray &points = scene._walkPoints[i]; + + // See how many points there are between the src and dest zones + if (!points._pointsCount || points._pointsCount == -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) { + for (int idx = (int)points.size() - 1; idx >= 0; --idx) + _walkTo.push(points[idx]); + } else { + for (int idx = 0; idx < (int)points.size(); ++idx) { + _walkTo.push(points[idx]); + } + } + + // Final position + _walkTo.push(_walkDest); + + // Start walking + _walkDest = _walkTo.pop(); + setWalking(); + } + } +} + +/*----------------------------------------------------------------*/ + +People *People::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelPeople(vm); + else + return new Tattoo::TattooPeople(vm); +} + +People::People(SherlockEngine *vm) : _vm(vm) { + _holmesOn = true; + _allowWalkAbort = true; + _portraitLoaded = false; + _portraitsOn = true; + _clearingThePortrait = false; + _talkPics = nullptr; + _portraitSide = 0; + _speakerFlip = false; + _holmesFlip = false; + _holmesQuotient = 0; + _savedPos = Point32(-1, -1); + _savedPos._facing = -1; + _forceWalkReload = false; + _useWalkLib = false; + _walkControl = 0; + + _portrait._sequences = new byte[32]; +} + +People::~People() { + for (uint idx = 0; idx < _data.size(); ++idx) { + if (_data[idx]->_walkLoaded) + delete _data[idx]->_images; + delete _data[idx]; + } + + delete _talkPics; + delete[] _portrait._sequences; +} + +void People::reset() { + SaveManager &saves = *_vm->_saves; + Talk &talk = *_vm->_talk; + _data[HOLMES]->_description = "Sherlock Holmes!"; + + // Note: Serrated Scalpel only uses a single Person slot for Sherlock.. Watson is handled by scene sprites + int count = IS_SERRATED_SCALPEL ? 1 : MAX_CHARACTERS; + for (int idx = 0; idx < count; ++idx) { + Person &p = *_data[idx]; + + if (IS_SERRATED_SCALPEL) { + p._type = CHARACTER; + p._sequenceNumber = (int)Tattoo::STOP_DOWNRIGHT; + p._position = Point32(100 * FIXED_INT_MULTIPLIER, 110 * FIXED_INT_MULTIPLIER); + } else if (!talk._scriptMoreFlag && !saves._justLoaded) { + p._type = (idx == 0) ? CHARACTER : INVALID; + p._sequenceNumber = (int)Scalpel::STOP_DOWNRIGHT; + p._position = Point32(36 * FIXED_INT_MULTIPLIER, 29 * FIXED_INT_MULTIPLIER); + p._use[0]._verb = ""; + p._use[1]._verb = ""; + } + + p._imageFrame = nullptr; + p._frameNumber = 1; + p._delta = Point32(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; + p._seqTo = 0; + p._seqCounter = p._seqCounter2 = 0; + p._seqStack = 0; + p._gotoSeq = p._talkSeq = 0; + p._restoreSlot = 0; + p._startSeq = 0; + p._altImages = nullptr; + p._altSeq = 0; + p._centerWalk = true; + p._adjust = Common::Point(0, 0); + + // Load the default walk sequences + p._walkTo.clear(); + p._oldWalkSequence = -1; + p._walkSequences.clear(); + if (IS_SERRATED_SCALPEL) { + p._walkSequences.resize(MAX_HOLMES_SEQUENCE); + for (int seqIdx = 0; seqIdx < MAX_HOLMES_SEQUENCE; ++seqIdx) { + p._walkSequences[seqIdx]._sequences.clear(); + + const byte *pSrc = &CHARACTER_SEQUENCES[seqIdx][0]; + do { + p._walkSequences[seqIdx]._sequences.push_back(*pSrc); + } while (*pSrc++); + } + } + } +} + +bool People::freeWalk() { + bool result = false; + + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (_data[idx]->_walkLoaded) { + delete _data[idx]->_images; + _data[idx]->_images = nullptr; + + _data[idx]->_walkLoaded = false; + result = true; + } + } + + return result; +} + +int People::findSpeaker(int speaker) { + Scene &scene = *_vm->_scene; + const char *portrait = _characters[speaker]._portrait; + + 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(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::synchronize(Serializer &s) { + s.syncAsByte(_holmesOn); + + if (IS_SERRATED_SCALPEL) { + s.syncAsSint16LE(_data[HOLMES]->_position.x); + s.syncAsSint16LE(_data[HOLMES]->_position.y); + s.syncAsSint16LE(_data[HOLMES]->_sequenceNumber); + } else { + for (uint idx = 0; idx < _data.size(); ++idx) { + Person &p = *_data[idx]; + s.syncAsSint16LE(p._position.x); + s.syncAsSint16LE(p._position.y); + s.syncAsSint16LE(p._sequenceNumber); + s.syncAsSint16LE(p._type); + s.syncString(p._walkVGSName); + s.syncString(p._description); + s.syncString(p._examine); + } + } + + s.syncAsSint16LE(_holmesQuotient); + + if (s.isLoading()) { + _savedPos = _data[HOLMES]->_position; + _savedPos._facing = _data[HOLMES]->_sequenceNumber; + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/people.h b/engines/sherlock/people.h new file mode 100644 index 0000000000..b59522e4b8 --- /dev/null +++ b/engines/sherlock/people.h @@ -0,0 +1,168 @@ +/* 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/queue.h" +#include "sherlock/objects.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +enum PeopleId { + HOLMES = 0, + WATSON = 1, + MAX_NPC_PATH = 200 +}; + +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 +}; + +#define NUM_IN_WALK_LIB 10 +extern const char *const WALK_LIB_NAMES[10]; + +#define MAX_CHARACTERS (IS_SERRATED_SCALPEL ? 1 : 6) + +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 Person : public Sprite { +protected: + /** + * Get the source position for a character potentially affected by scaling + */ + virtual Common::Point getSourcePoint() const = 0; +public: + Common::Queue<Common::Point> _walkTo; + int _srcZone, _destZone; + bool _walkLoaded; + Common::String _portrait; + Common::Point _walkDest; + Common::String _npcName; + + // Rose Tattoo fields + Common::String _walkVGSName; // Name of walk library person is using +public: + Person(); + virtual ~Person() {} + + /** + * 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(); + + /** + * Walk to the co-ordinates passed, and then face the given direction + */ + virtual void walkToCoords(const Point32 &destPos, int destDir) = 0; +}; + +class SherlockEngine; + +class People { +protected: + SherlockEngine *_vm; + Common::Array<Person *> _data; + + People(SherlockEngine *vm); +public: + Common::Array<PersonData> _characters; + ImageFile *_talkPics; + PositionFacing _savedPos; + bool _holmesOn; + bool _portraitLoaded; + bool _portraitsOn; + Object _portrait; + bool _clearingThePortrait; + bool _allowWalkAbort; + int _portraitSide; + bool _speakerFlip; + bool _holmesFlip; + int _holmesQuotient; + bool _forceWalkReload; + bool _useWalkLib; + + int _walkControl; +public: + static People *init(SherlockEngine *vm); + virtual ~People(); + + Person &operator[](PeopleId id) { return *_data[id]; } + Person &operator[](int idx) { return *_data[idx]; } + + /** + * Reset the player data + */ + void reset(); + + /** + * If the walk data has been loaded, then it will be freed + */ + bool freeWalk(); + + /** + * Turn off any currently active portraits, and removes them from being drawn + */ + void clearTalking(); + + /** + * Finds the scene background object corresponding to a specified speaker + */ + virtual int findSpeaker(int speaker); + + /** + * Synchronize the data for a savegame + */ + virtual void synchronize(Serializer &s) = 0; + + /** + * Change the sequence of the scene background object associated with the current speaker. + */ + virtual void setTalkSequence(int speaker, int sequenceNum = 1) = 0; + + /** + * Load the walking images for Sherlock + */ + virtual bool loadWalk() = 0; + + /** + * Restrict passed point to zone using Sherlock's positioning rules + */ + virtual const Common::Point restrictToZone(int zoneId, const Common::Point &destPos) = 0; +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/resources.cpp b/engines/sherlock/resources.cpp new file mode 100644 index 0000000000..206d7173ac --- /dev/null +++ b/engines/sherlock/resources.cpp @@ -0,0 +1,372 @@ +/* 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->decompress(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) { + if (!IS_3DO) { + addToCache("vgs.lib"); + addToCache("talk.lib"); + addToCache("journal.txt"); + + if (IS_SERRATED_SCALPEL) { + addToCache("sequence.txt"); + addToCache("portrait.lib"); + } else { + addToCache("walk.lib"); + } + } else { + // 3DO + + // ITEM data from VGS.LIB is in ITEM.LIB + addToCache("item.lib"); + + // talk.lib - resources themselves seem to be the same, although a few texts were slightly changed + addToCache("talk.lib"); + + // remaining files are missing + // portraits were replaced with FMV + } + } +} + +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, false); + else if (header == MKTAG('L', 'I', 'C', 26)) + loadLibraryIndex(filename, stream, true); + + 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); + + if (isCompressed) { + int outSize = stream->readUint32LE(); + Common::SeekableReadStream *newStream = decompressLZ(*stream, outSize); + delete stream; + stream = newStream; + } else { + stream->seek(-4, SEEK_CUR); + } +} + +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, false); + + // 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, bool isNewStyle) { + 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 = 0; + + if (!IS_3DO) { + // PC + count = stream->readUint16LE(); + + if (isNewStyle) + stream->seek((count + 1) * 8, SEEK_CUR); + + // 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); + } + + } else { + // 3DO + count = stream->readUint16BE(); + + // 3DO header + // Loop through reading in the entries + + // Read offset of first entry + offset = stream->readUint32BE(); + + for (int idx = 0; idx < count; ++idx) { + + // Read the name of the resource + char resName[13]; + stream->read(resName, 13); + resName[12] = '\0'; + + stream->skip(3); // filler + + if (idx == (count - 1)) { + nextOffset = stream->size(); + } else { + // Read the offset of the next entry + nextOffset = stream->readUint32BE(); + } + + // Add the entry to the index + index[resName] = LibraryEntry(idx, offset, nextOffset - offset); + + // use next offset as current offset + offset = nextOffset; + } + } +} + +int Resources::resourceIndex() const { + return _resourceIndex; +} + +Common::SeekableReadStream *Resources::decompress(Common::SeekableReadStream &source) { + // This variation can't be used by Rose Tattoo, since compressed resources include the input size, + // not the output size. Which means their decompression has to be done via passed buffers + assert(IS_SERRATED_SCALPEL); + + uint32 id = source.readUint32BE(); + assert(id == MKTAG('L', 'Z', 'V', 0x1A)); + + uint32 outputSize = source.readUint32LE(); + return decompressLZ(source, outputSize); +} + +Common::SeekableReadStream *Resources::decompress(Common::SeekableReadStream &source, uint32 outSize) { + int inSize = IS_ROSE_TATTOO ? source.readSint32LE() : -1; + byte *outBuffer = (byte *)malloc(outSize); + Common::MemoryReadStream *outStream = new Common::MemoryReadStream(outBuffer, outSize, DisposeAfterUse::YES); + + decompressLZ(source, outBuffer, outSize, inSize); + + return outStream; +} + +void Resources::decompress(Common::SeekableReadStream &source, byte *buffer, uint32 outSize) { + int inputSize = IS_ROSE_TATTOO ? source.readSint32LE() : -1; + + decompressLZ(source, buffer, outSize, inputSize); +} + +Common::SeekableReadStream *Resources::decompressLZ(Common::SeekableReadStream &source, uint32 outSize) { + byte *dataOut = (byte *)malloc(outSize); + decompressLZ(source, dataOut, outSize, -1); + + return new Common::MemoryReadStream(dataOut, outSize, DisposeAfterUse::YES); +} + +void Resources::decompressLZ(Common::SeekableReadStream &source, byte *outBuffer, int32 outSize, int32 inSize) { + byte lzWindow[4096]; + uint16 lzWindowPos; + uint16 cmd; + + byte *outBufferEnd = outBuffer + outSize; + int endPos = source.pos() + inSize; + + 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 ((outSize == -1 || outBuffer < outBufferEnd) && (inSize == -1 || source.pos() < endPos)); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/resources.h b/engines/sherlock/resources.h new file mode 100644 index 0000000000..8e0216d69d --- /dev/null +++ b/engines/sherlock/resources.h @@ -0,0 +1,170 @@ +/* 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, bool isNewStyle); +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 LZW compressed data + */ + Common::SeekableReadStream *decompress(Common::SeekableReadStream &source); + + /** + * Decompresses LZW compressed data + */ + Common::SeekableReadStream *decompress(Common::SeekableReadStream &source, uint32 outSize); + + /** + * Decompresses LZW compressed data + */ + void decompress(Common::SeekableReadStream &source, byte *buffer, uint32 outSize); + + /** + * Decompresses LZW compressed data + */ + static Common::SeekableReadStream *decompressLZ(Common::SeekableReadStream &source, uint32 outSize); + + /** + * Decompresses LZW compressed data + */ + static void decompressLZ(Common::SeekableReadStream &source, byte *outBuffer, int32 outSize, int32 inSize); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/saveload.cpp b/engines/sherlock/saveload.cpp new file mode 100644 index 0000000000..fae8196dc1 --- /dev/null +++ b/engines/sherlock/saveload.cpp @@ -0,0 +1,278 @@ +/* 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 "sherlock/scalpel/scalpel_saveload.h" +#include "common/system.h" +#include "graphics/scaler.h" +#include "graphics/thumbnail.h" + +namespace Sherlock { + +const char *const EMPTY_SAVEGAME_SLOT = "-EMPTY-"; +static const char *const SAVEGAME_STR = "SHLK"; +#define SAVEGAME_STR_SIZE 4 + +/*----------------------------------------------------------------*/ + +SaveManager *SaveManager::init(SherlockEngine *vm, const Common::String &target) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelSaveManager(vm, target); + else + return new SaveManager(vm, target); +} + +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::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) { + if (!readSavegameHeader(in, header)) + continue; + + 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 < MINIMUM_SAVEGAME_VERSION || header._version > CURRENT_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(CURRENT_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; + } + + _saveThumb = new Graphics::Surface(); + + if (!IS_3DO) { + uint8 thumbPalette[PALETTE_SIZE]; + _vm->_screen->getPalette(thumbPalette); + ::createThumbnail(_saveThumb, (const byte *)_vm->_screen->getPixels(), SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT, thumbPalette); + } else { + ::createThumbnailFromScreen(_saveThumb); + } +} + +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 + Serializer s(saveFile, nullptr); + s.setSaveVersion(header._version); + 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 + Serializer s(nullptr, out); + s.setSaveVersion(CURRENT_SAVEGAME_VERSION); + 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(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::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..0aaa9cf801 --- /dev/null +++ b/engines/sherlock/saveload.h @@ -0,0 +1,151 @@ +/* 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 + +enum { + CURRENT_SAVEGAME_VERSION = 2, + MINIMUM_SAVEGAME_VERSION = 2 +}; + +enum SaveMode { SAVEMODE_NONE = 0, SAVEMODE_LOAD = 1, SAVEMODE_SAVE = 2 }; + +extern const char *const EMPTY_SAVEGAME_SLOT; + +struct SherlockSavegameHeader { + uint8 _version; + Common::String _saveName; + Graphics::Surface *_thumbnail; + int _year, _month, _day; + int _hour, _minute; + int _totalFrames; +}; + +class SherlockEngine; + + +/** + * Derived serializer class with extra synchronization types + */ +class Serializer : public Common::Serializer { +public: + Serializer(Common::SeekableReadStream *in, Common::WriteStream *out) : Common::Serializer(in, out) {} + + /** + * New method to allow setting the version + */ + void setSaveVersion(byte version) { _version = version; } +}; + +class SaveManager { +protected: + 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(Serializer &s); +public: + Common::StringArray _savegames; + int _savegameIndex; + SaveMode _envMode; + bool _justLoaded; +public: + static SaveManager *init(SherlockEngine *vm, const Common::String &target); + SaveManager(SherlockEngine *vm, const Common::String &target); + virtual ~SaveManager(); + + /** + * 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); + + /** + * 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/3do/movie_decoder.cpp b/engines/sherlock/scalpel/3do/movie_decoder.cpp new file mode 100644 index 0000000000..8e8f99bc19 --- /dev/null +++ b/engines/sherlock/scalpel/3do/movie_decoder.cpp @@ -0,0 +1,510 @@ +/* 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/stream.h" +#include "common/textconsole.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" +#include "audio/decoders/3do.h" + +#include "sherlock/scalpel/3do/movie_decoder.h" +#include "image/codecs/cinepak.h" + +// for Test-Code +#include "common/system.h" +#include "common/events.h" +#include "common/keyboard.h" +#include "engines/engine.h" +#include "engines/util.h" +#include "graphics/palette.h" +#include "graphics/pixelformat.h" +#include "graphics/surface.h" + +namespace Sherlock { + +Scalpel3DOMovieDecoder::Scalpel3DOMovieDecoder() + : _stream(0), _videoTrack(0), _audioTrack(0) { + _streamVideoOffset = 0; + _streamAudioOffset = 0; +} + +Scalpel3DOMovieDecoder::~Scalpel3DOMovieDecoder() { + close(); +} + +bool Scalpel3DOMovieDecoder::loadStream(Common::SeekableReadStream *stream) { + uint32 videoSubType = 0; + uint32 videoCodecTag = 0; + uint32 videoHeight = 0; + uint32 videoWidth = 0; + uint32 videoFrameCount = 0; + uint32 audioSubType = 0; + uint32 audioCodecTag = 0; + uint32 audioChannels = 0; + uint32 audioSampleRate = 0; + + close(); + + _stream = stream; + _streamVideoOffset = 0; + _streamAudioOffset = 0; + + // Look for packets that we care about + static const int maxPacketCheckCount = 20; + for (int i = 0; i < maxPacketCheckCount; i++) { + uint32 chunkTag = _stream->readUint32BE(); + uint32 chunkSize = _stream->readUint32BE() - 8; + + // Bail out if done + if (_stream->eos()) + break; + + uint32 dataStartOffset = _stream->pos(); + + switch (chunkTag) { + case MKTAG('F','I','L','M'): { + // See if this is a FILM header + _stream->skip(4); // time stamp (based on 240 per second) + _stream->skip(4); // Unknown 0x00000000 + videoSubType = _stream->readUint32BE(); + + switch (videoSubType) { + case MKTAG('F', 'H', 'D', 'R'): + // FILM header found + if (_videoTrack) { + warning("Sherlock 3DO movie: Multiple FILM headers found"); + close(); + return false; + } + _stream->readUint32BE(); + videoCodecTag = _stream->readUint32BE(); + videoHeight = _stream->readUint32BE(); + videoWidth = _stream->readUint32BE(); + _stream->skip(4); // time scale + videoFrameCount = _stream->readUint32BE(); + + _videoTrack = new StreamVideoTrack(videoWidth, videoHeight, videoCodecTag, videoFrameCount); + addTrack(_videoTrack); + break; + + case MKTAG('F', 'R', 'M', 'E'): + break; + + default: + warning("Sherlock 3DO movie: Unknown subtype inside FILM packet"); + close(); + return false; + } + break; + } + + case MKTAG('S','N','D','S'): { + _stream->skip(8); + audioSubType = _stream->readUint32BE(); + + switch (audioSubType) { + case MKTAG('S', 'H', 'D', 'R'): + // Audio header + + // Bail if we already have a track + if (_audioTrack) { + warning("Sherlock 3DO movie: Multiple SNDS headers found"); + close(); + return false; + } + + // OK, this is the start of a audio stream + _stream->readUint32BE(); // Version, always 0x00000000 + _stream->readUint32BE(); // Unknown 0x00000008 ?! + _stream->readUint32BE(); // Unknown 0x00007500 + _stream->readUint32BE(); // Unknown 0x00004000 + _stream->readUint32BE(); // Unknown 0x00000000 + _stream->readUint32BE(); // Unknown 0x00000010 + audioSampleRate = _stream->readUint32BE(); + audioChannels = _stream->readUint32BE(); + audioCodecTag = _stream->readUint32BE(); + _stream->readUint32BE(); // Unknown 0x00000004 compression ratio? + _stream->readUint32BE(); // Unknown 0x00000A2C + + _audioTrack = new StreamAudioTrack(audioCodecTag, audioSampleRate, audioChannels); + addTrack(_audioTrack); + break; + + case MKTAG('S', 'S', 'M', 'P'): + // Audio data + break; + default: + warning("Sherlock 3DO movie: Unknown subtype inside FILM packet"); + close(); + return false; + } + break; + } + + case MKTAG('C','T','R','L'): + case MKTAG('F','I','L','L'): // filler chunk, fills to certain boundary + case MKTAG('D','A','C','Q'): + case MKTAG('J','O','I','N'): // add cel data (not used in sherlock) + // Ignore these chunks + break; + + case MKTAG('S','H','D','R'): + // Happens for EA logo, seems to be garbage data right at the start of the file + break; + + default: + warning("Unknown chunk-tag '%s' inside Sherlock 3DO movie", tag2str(chunkTag)); + close(); + return false; + } + + if ((_videoTrack) && (_audioTrack)) + break; + + // Seek to next chunk + _stream->seek(dataStartOffset + chunkSize); + } + + // Bail if we didn't find video + audio + if ((!_videoTrack) || (!_audioTrack)) { + close(); + return false; + } + + // Rewind back to the beginning + _stream->seek(0); + + return true; +} + +void Scalpel3DOMovieDecoder::close() { + Video::VideoDecoder::close(); + + delete _stream; _stream = 0; + _videoTrack = 0; +} + +// We try to at least decode 1 frame +// and also try to get at least 0.5 seconds of audio queued up +void Scalpel3DOMovieDecoder::readNextPacket() { + uint32 currentMovieTime = getTime(); + uint32 wantedAudioQueued = currentMovieTime + 500; // always try to be 0.500 seconds in front of movie time + + int32 chunkOffset = 0; + int32 dataStartOffset = 0; + int32 nextChunkOffset = 0; + uint32 chunkTag = 0; + uint32 chunkSize = 0; + + uint32 videoSubType = 0; + uint32 videoTimeStamp = 0; + uint32 videoFrameSize = 0; + uint32 audioSubType = 0; + uint32 audioBytes = 0; + bool videoGotFrame = false; + bool videoDone = false; + bool audioDone = false; + + // Seek to smallest stream offset + if (_streamVideoOffset <= _streamAudioOffset) { + _stream->seek(_streamVideoOffset); + } else { + _stream->seek(_streamAudioOffset); + } + + if (wantedAudioQueued <= _audioTrack->getTotalAudioQueued()) { + // already got enough audio queued up + audioDone = true; + } + + while (1) { + chunkOffset = _stream->pos(); + assert(chunkOffset >= 0); + + // Read chunk header + chunkTag = _stream->readUint32BE(); + chunkSize = _stream->readUint32BE() - 8; + + // Calculate offsets + dataStartOffset = _stream->pos(); + assert(dataStartOffset >= 0); + nextChunkOffset = dataStartOffset + chunkSize; + + //warning("offset %lx - tag %lx", dataStartOffset, tag); + + if (_stream->eos()) + break; + + switch (chunkTag) { + case MKTAG('F','I','L','M'): + videoTimeStamp = _stream->readUint32BE(); + _stream->skip(4); // Unknown + videoSubType = _stream->readUint32BE(); + + switch (videoSubType) { + case MKTAG('F', 'H', 'D', 'R'): + // Ignore video header + break; + + case MKTAG('F', 'R', 'M', 'E'): + // Found frame data + if (_streamVideoOffset <= chunkOffset) { + // We are at an offset that is still relevant to video decoding + if (!videoDone) { + if (!videoGotFrame) { + // We haven't decoded any frame yet, so do so now + _stream->readUint32BE(); + videoFrameSize = _stream->readUint32BE(); + _videoTrack->decodeFrame(_stream->readStream(videoFrameSize), videoTimeStamp); + + _streamVideoOffset = nextChunkOffset; + videoGotFrame = true; + + } else { + // Already decoded a frame, so get timestamp of follow-up frame + // and then we are done with video + + // Calculate next frame time + // 3DO clock time for movies runs at 240Hh, that's why timestamps are based on 240. + uint32 currentFrameStartTime = _videoTrack->getNextFrameStartTime(); + uint32 nextFrameStartTime = videoTimeStamp * 1000 / 240; + assert(currentFrameStartTime <= nextFrameStartTime); + _videoTrack->setNextFrameStartTime(nextFrameStartTime); + + // next time we want to start at the current chunk + _streamVideoOffset = chunkOffset; + videoDone = true; + } + } + } + break; + + default: + error("Sherlock 3DO movie: Unknown subtype inside FILM packet"); + break; + } + break; + + case MKTAG('S','N','D','S'): + _stream->skip(8); + audioSubType = _stream->readUint32BE(); + + switch (audioSubType) { + case MKTAG('S', 'H', 'D', 'R'): + // Ignore the audio header + break; + + case MKTAG('S', 'S', 'M', 'P'): + // Got audio chunk + if (_streamAudioOffset <= chunkOffset) { + // We are at an offset that is still relevant to audio decoding + if (!audioDone) { + audioBytes = _stream->readUint32BE(); + _audioTrack->queueAudio(_stream, audioBytes); + + _streamAudioOffset = nextChunkOffset; + if (wantedAudioQueued <= _audioTrack->getTotalAudioQueued()) { + // Got enough audio + audioDone = true; + } + } + } + break; + + default: + error("Sherlock 3DO movie: Unknown subtype inside SNDS packet"); + break; + } + break; + + case MKTAG('C','T','R','L'): + case MKTAG('F','I','L','L'): // filler chunk, fills to certain boundary + case MKTAG('D','A','C','Q'): + case MKTAG('J','O','I','N'): // add cel data (not used in sherlock) + // Ignore these chunks + break; + + case MKTAG('S','H','D','R'): + // Happens for EA logo, seems to be garbage data right at the start of the file + break; + + default: + error("Unknown chunk-tag '%s' inside Sherlock 3DO movie", tag2str(chunkTag)); + } + + // Always seek to end of chunk + // Sometimes not all of the chunk is filled with audio + _stream->seek(nextChunkOffset); + + if ((videoDone) && (audioDone)) { + return; + } + } +} + +Scalpel3DOMovieDecoder::StreamVideoTrack::StreamVideoTrack(uint32 width, uint32 height, uint32 codecTag, uint32 frameCount) { + _width = width; + _height = height; + _frameCount = frameCount; + _curFrame = -1; + _nextFrameStartTime = 0; + + // Create the Cinepak decoder, if we're using it + if (codecTag == MKTAG('c', 'v', 'i', 'd')) + _codec = new Image::CinepakDecoder(); + else + error("Unsupported Sherlock 3DO movie video codec tag '%s'", tag2str(codecTag)); +} + +Scalpel3DOMovieDecoder::StreamVideoTrack::~StreamVideoTrack() { + delete _codec; +} + +bool Scalpel3DOMovieDecoder::StreamVideoTrack::endOfTrack() const { + return getCurFrame() >= getFrameCount() - 1; +} + +Graphics::PixelFormat Scalpel3DOMovieDecoder::StreamVideoTrack::getPixelFormat() const { + return _codec->getPixelFormat(); +} + +void Scalpel3DOMovieDecoder::StreamVideoTrack::decodeFrame(Common::SeekableReadStream *stream, uint32 videoTimeStamp) { + _surface = _codec->decodeFrame(*stream); + _curFrame++; +} + +Scalpel3DOMovieDecoder::StreamAudioTrack::StreamAudioTrack(uint32 codecTag, uint32 sampleRate, uint32 channels) { + switch (codecTag) { + case MKTAG('A','D','P','4'): + case MKTAG('S','D','X','2'): + // ADP4 + SDX2 are both allowed + break; + + default: + error("Unsupported Sherlock 3DO movie audio codec tag '%s'", tag2str(codecTag)); + } + + _totalAudioQueued = 0; // currently 0 milliseconds queued + + _codecTag = codecTag; + _sampleRate = sampleRate; + switch (channels) { + case 1: + _stereo = false; + break; + case 2: + _stereo = true; + break; + default: + error("Unsupported Sherlock 3DO movie audio channels %d", channels); + } + + _audioStream = Audio::makeQueuingAudioStream(sampleRate, _stereo); + + // reset audio decoder persistent spaces + memset(&_ADP4_PersistentSpace, 0, sizeof(_ADP4_PersistentSpace)); + memset(&_SDX2_PersistentSpace, 0, sizeof(_SDX2_PersistentSpace)); +} + +Scalpel3DOMovieDecoder::StreamAudioTrack::~StreamAudioTrack() { + delete _audioStream; +// free(_ADP4_PersistentSpace); +// free(_SDX2_PersistentSpace); +} + +void Scalpel3DOMovieDecoder::StreamAudioTrack::queueAudio(Common::SeekableReadStream *stream, uint32 size) { + Common::SeekableReadStream *compressedAudioStream = 0; + Audio::RewindableAudioStream *audioStream = 0; + uint32 audioLengthMSecs = 0; + + // Read the specified chunk into memory + compressedAudioStream = stream->readStream(size); + + switch(_codecTag) { + case MKTAG('A','D','P','4'): + audioStream = Audio::make3DO_ADP4AudioStream(compressedAudioStream, _sampleRate, _stereo, &audioLengthMSecs, DisposeAfterUse::YES, &_ADP4_PersistentSpace); + break; + case MKTAG('S','D','X','2'): + audioStream = Audio::make3DO_SDX2AudioStream(compressedAudioStream, _sampleRate, _stereo, &audioLengthMSecs, DisposeAfterUse::YES, &_SDX2_PersistentSpace); + break; + default: + break; + } + if (audioStream) { + _totalAudioQueued += audioLengthMSecs; + _audioStream->queueAudioStream(audioStream, DisposeAfterUse::YES); + } else { + // in case there was an error + delete compressedAudioStream; + } +} + +Audio::AudioStream *Scalpel3DOMovieDecoder::StreamAudioTrack::getAudioStream() const { + return _audioStream; +} + +// Test-code + +// Code for showing a movie. Only meant for testing/debug purposes +bool Scalpel3DOMoviePlay(const char *filename, Common::Point pos) { + Scalpel3DOMovieDecoder *videoDecoder = new Scalpel3DOMovieDecoder(); + + if (!videoDecoder->loadFile(filename)) { + warning("Scalpel3DOMoviePlay: could not open '%s'", filename); + return false; + } + + bool skipVideo = false; + //byte bytesPerPixel = videoDecoder->getPixelFormat().bytesPerPixel; + uint16 width = videoDecoder->getWidth(); + uint16 height = videoDecoder->getHeight(); + //uint16 pitch = videoDecoder->getWidth() * bytesPerPixel; + + videoDecoder->start(); + + while (!g_engine->shouldQuit() && !videoDecoder->endOfVideo() && (!skipVideo)) { + if (videoDecoder->needsUpdate()) { + const Graphics::Surface *frame = videoDecoder->decodeNextFrame(); + + if (frame) { + g_system->copyRectToScreen(frame->getPixels(), frame->pitch, pos.x, pos.y, width, height); + g_system->updateScreen(); + } + } + + Common::Event event; + while (g_system->getEventManager()->pollEvent(event)) { + if ((event.type == Common::EVENT_KEYDOWN && event.kbd.keycode == Common::KEYCODE_ESCAPE)) + skipVideo = true; + } + + g_system->delayMillis(10); + } + videoDecoder->close(); + delete videoDecoder; + + return !skipVideo; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/3do/movie_decoder.h b/engines/sherlock/scalpel/3do/movie_decoder.h new file mode 100644 index 0000000000..9f1670fc6c --- /dev/null +++ b/engines/sherlock/scalpel/3do/movie_decoder.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_SCALPEL_3DO_MOVIE_DECODER_H +#define SHERLOCK_SCALPEL_3DO_MOVIE_DECODER_H + +#include "common/rect.h" +#include "video/video_decoder.h" +#include "audio/decoders/3do.h" + +namespace Audio { +class QueuingAudioStream; +} + +namespace Common { +class SeekableReadStream; +} + +namespace Image { +class Codec; +} + +namespace Sherlock { + +class Scalpel3DOMovieDecoder : public Video::VideoDecoder { +public: + Scalpel3DOMovieDecoder(); + ~Scalpel3DOMovieDecoder(); + + bool loadStream(Common::SeekableReadStream *stream); + void close(); + +protected: + void readNextPacket(); + +private: + int32 _streamVideoOffset; /* current stream offset for video decoding */ + int32 _streamAudioOffset; /* current stream offset for audio decoding */ + +private: + class StreamVideoTrack : public VideoTrack { + public: + StreamVideoTrack(uint32 width, uint32 height, uint32 codecTag, uint32 frameCount); + ~StreamVideoTrack(); + + bool endOfTrack() const; + + uint16 getWidth() const { return _width; } + uint16 getHeight() const { return _height; } + Graphics::PixelFormat getPixelFormat() const; + int getCurFrame() const { return _curFrame; } + int getFrameCount() const { return _frameCount; } + void setNextFrameStartTime(uint32 nextFrameStartTime) { _nextFrameStartTime = nextFrameStartTime; } + uint32 getNextFrameStartTime() const { return _nextFrameStartTime; } + const Graphics::Surface *decodeNextFrame() { return _surface; } + + void decodeFrame(Common::SeekableReadStream *stream, uint32 videoTimeStamp); + + private: + const Graphics::Surface *_surface; + + int _curFrame; + uint32 _frameCount; + uint32 _nextFrameStartTime; + + Image::Codec *_codec; + uint16 _width, _height; + }; + + class StreamAudioTrack : public AudioTrack { + public: + StreamAudioTrack(uint32 codecTag, uint32 sampleRate, uint32 channels); + ~StreamAudioTrack(); + + void queueAudio(Common::SeekableReadStream *stream, uint32 size); + + protected: + Audio::AudioStream *getAudioStream() const; + + private: + Audio::QueuingAudioStream *_audioStream; + uint32 _totalAudioQueued; /* total amount of milliseconds of audio, that we queued up already */ + + public: + uint32 getTotalAudioQueued() const { return _totalAudioQueued; } + + private: + int16 decodeSample(uint8 dataNibble); + + uint32 _codecTag; + uint16 _sampleRate; + bool _stereo; + + Audio::audio_3DO_ADP4_PersistentSpace _ADP4_PersistentSpace; + Audio::audio_3DO_SDX2_PersistentSpace _SDX2_PersistentSpace; + }; + + Common::SeekableReadStream *_stream; + StreamVideoTrack *_videoTrack; + StreamAudioTrack *_audioTrack; +}; + +// Testing +extern bool Scalpel3DOMoviePlay(const char *filename, Common::Point pos); + +} // 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..a24af4e444 --- /dev/null +++ b/engines/sherlock/scalpel/darts.cpp @@ -0,0 +1,553 @@ +/* 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) + _vm->setFlagsDirect(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), BLACK); + screen._backBuffer1.fillRect(Common::Rect(DARTBARVX, DARTHEIGHTY, DARTBARVX + 10, DARTHEIGHTY + DARTBARSIZE), BLACK); + 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; + Music &music = *_vm->_music; + bool done; + int idx = 0; + + events.clearEvents(); + if (music._musicOn) + music.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 (music._musicOn) { + if (!(idx % 3)) + music.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; +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/darts.h b/engines/sherlock/scalpel/darts.h new file mode 100644 index 0000000000..4368954814 --- /dev/null +++ b/engines/sherlock/scalpel/darts.h @@ -0,0 +1,130 @@ +/* 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/image_file.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); +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/drivers/adlib.cpp b/engines/sherlock/scalpel/drivers/adlib.cpp new file mode 100644 index 0000000000..29a39f0c39 --- /dev/null +++ b/engines/sherlock/scalpel/drivers/adlib.cpp @@ -0,0 +1,646 @@ +/* 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/scalpel/drivers/mididriver.h" + +#include "common/file.h" +#include "common/system.h" +#include "common/textconsole.h" + +#include "audio/fmopl.h" +#include "audio/softsynth/emumidi.h" + +namespace Sherlock { + +#define SHERLOCK_ADLIB_VOICES_COUNT 9 +#define SHERLOCK_ADLIB_NOTES_COUNT 96 + +byte operator1Register[SHERLOCK_ADLIB_VOICES_COUNT] = { + 0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12 +}; + +byte operator2Register[SHERLOCK_ADLIB_VOICES_COUNT] = { + 0x03, 0x04, 0x05, 0x0B, 0x0C, 0x0D, 0x13, 0x14, 0x15 +}; + +struct percussionChannelEntry { + byte requiredNote; + byte replacementNote; +}; + +// hardcoded, dumped from ADHOM.DRV +const percussionChannelEntry percussionChannelTable[SHERLOCK_ADLIB_VOICES_COUNT] = { + { 0x00, 0x00 }, + { 0x00, 0x00 }, + { 0x00, 0x00 }, + { 0x00, 0x00 }, + { 0x00, 0x00 }, + { 0x00, 0x00 }, + { 0x24, 0x0C }, + { 0x38, 0x01 }, + { 0x26, 0x1E } +}; + +struct InstrumentEntry { + byte reg20op1; + byte reg40op1; + byte reg60op1; + byte reg80op1; + byte regE0op1; + byte reg20op2; + byte reg40op2; + byte reg60op2; + byte reg80op2; + byte regE0op2; + byte regC0; + byte frequencyAdjust; +}; + +// hardcoded, dumped from ADHOM.DRV +const InstrumentEntry instrumentTable[] = { + { 0x71, 0x89, 0x51, 0x11, 0x00, 0x61, 0x23, 0x42, 0x15, 0x01, 0x02, 0xF4 }, + { 0x22, 0x20, 0x97, 0x89, 0x00, 0xA2, 0x1F, 0x70, 0x07, 0x00, 0x0A, 0xF4 }, + { 0x70, 0x1A, 0x64, 0x13, 0x00, 0x20, 0x1F, 0x53, 0x46, 0x00, 0x0E, 0xF4 }, + { 0xB6, 0x4A, 0xB6, 0x32, 0x00, 0x11, 0x2B, 0xD1, 0x31, 0x00, 0x0E, 0xE8 }, + { 0x71, 0x8B, 0x51, 0x11, 0x00, 0x61, 0x20, 0x32, 0x35, 0x01, 0x02, 0xF4 }, + { 0x71, 0x8A, 0x51, 0x11, 0x00, 0x61, 0x20, 0x32, 0x25, 0x01, 0x02, 0xF4 }, + { 0x23, 0x0F, 0xF4, 0x04, 0x02, 0x2F, 0x25, 0xF0, 0x43, 0x00, 0x06, 0xE8 }, + { 0x71, 0x1C, 0x71, 0x03, 0x00, 0x21, 0x1F, 0x54, 0x17, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x8A, 0x6E, 0x17, 0x00, 0x25, 0x27, 0x6B, 0x0E, 0x00, 0x02, 0xF4 }, + { 0x71, 0x1D, 0x81, 0x03, 0x00, 0x21, 0x1F, 0x64, 0x17, 0x00, 0x0E, 0xF4 }, + { 0x01, 0x4B, 0xF1, 0x50, 0x00, 0x01, 0x23, 0xD2, 0x76, 0x00, 0x06, 0xF4 }, + { 0x2F, 0xCA, 0xF8, 0xE5, 0x00, 0x21, 0x1F, 0xC0, 0xFF, 0x00, 0x00, 0xF4 }, + { 0x29, 0xCD, 0xF0, 0x91, 0x00, 0x21, 0x1F, 0xE0, 0x86, 0x00, 0x02, 0xF4 }, + { 0x24, 0xD0, 0xF0, 0x01, 0x00, 0x21, 0x1F, 0xE0, 0x86, 0x00, 0x02, 0xF4 }, + { 0x23, 0xC8, 0xF0, 0x01, 0x00, 0x21, 0x1F, 0xE0, 0x86, 0x00, 0x02, 0xF4 }, + { 0x64, 0xC9, 0xB0, 0x01, 0x00, 0x61, 0x1F, 0xF0, 0x86, 0x00, 0x02, 0xF4 }, + { 0x33, 0x85, 0xA1, 0x10, 0x00, 0x15, 0x9F, 0x72, 0x23, 0x00, 0x08, 0xF4 }, + { 0x31, 0x85, 0xA1, 0x10, 0x00, 0x15, 0x9F, 0x73, 0x33, 0x00, 0x08, 0xF4 }, + { 0x31, 0x81, 0xA1, 0x30, 0x00, 0x16, 0x9F, 0xC2, 0x74, 0x00, 0x08, 0xF4 }, + { 0x03, 0x8A, 0xF0, 0x7B, 0x00, 0x02, 0x9F, 0xF4, 0x7B, 0x00, 0x08, 0xF4 }, + { 0x03, 0x8A, 0xF0, 0x7B, 0x00, 0x01, 0x9F, 0xF4, 0x7B, 0x00, 0x08, 0xF4 }, + { 0x23, 0x8A, 0xF2, 0x7B, 0x00, 0x01, 0x9F, 0xF4, 0x7B, 0x00, 0x08, 0xF4 }, + { 0x32, 0x80, 0x01, 0x10, 0x00, 0x12, 0x9F, 0x72, 0x33, 0x00, 0x08, 0xF4 }, + { 0x32, 0x80, 0x01, 0x10, 0x00, 0x14, 0x9F, 0x73, 0x33, 0x00, 0x08, 0xF4 }, + { 0x31, 0x16, 0x73, 0x8E, 0x00, 0x21, 0x1F, 0x80, 0x9E, 0x00, 0x0E, 0xF4 }, + { 0x30, 0x16, 0x73, 0x7E, 0x00, 0x21, 0x1F, 0x80, 0x9E, 0x00, 0x0E, 0x00 }, + { 0x31, 0x94, 0x33, 0x73, 0x00, 0x21, 0x1F, 0xA0, 0x97, 0x00, 0x0E, 0xF4 }, + { 0x31, 0x94, 0xD3, 0x73, 0x00, 0x21, 0x20, 0xA0, 0x97, 0x00, 0x0E, 0xF4 }, + { 0x31, 0x45, 0xF1, 0x53, 0x00, 0x32, 0x1F, 0xF2, 0x27, 0x00, 0x06, 0xF4 }, + { 0x13, 0x0C, 0xF2, 0x01, 0x00, 0x15, 0x2F, 0xF2, 0xB6, 0x00, 0x08, 0xF4 }, + { 0x11, 0x0C, 0xF2, 0x01, 0x00, 0x11, 0x1F, 0xF2, 0xB6, 0x00, 0x08, 0xF4 }, + { 0x11, 0x0A, 0xFE, 0x04, 0x00, 0x11, 0x1F, 0xF2, 0xBD, 0x00, 0x08, 0xF4 }, + { 0x16, 0x4D, 0xFA, 0x11, 0x00, 0xE1, 0x20, 0xF1, 0xF1, 0x00, 0x08, 0xF4 }, + { 0x16, 0x40, 0xBA, 0x11, 0x00, 0xF1, 0x20, 0x24, 0x31, 0x00, 0x08, 0xF4 }, + { 0x61, 0xA7, 0x72, 0x8E, 0x00, 0xE1, 0x9F, 0x50, 0x1A, 0x00, 0x02, 0xF4 }, + { 0x18, 0x4D, 0x32, 0x13, 0x00, 0xE1, 0x20, 0x51, 0xE3, 0x00, 0x08, 0xF4 }, + { 0x17, 0xC0, 0x12, 0x41, 0x00, 0x31, 0x9F, 0x13, 0x31, 0x00, 0x06, 0xF4 }, + { 0x03, 0x8F, 0xF5, 0x55, 0x00, 0x21, 0x9F, 0xF3, 0x33, 0x00, 0x00, 0xF4 }, + { 0x13, 0x4D, 0xFA, 0x11, 0x00, 0xE1, 0x20, 0xF1, 0xF1, 0x00, 0x08, 0xF4 }, + { 0x11, 0x43, 0x20, 0x15, 0x00, 0xF1, 0x20, 0x31, 0xF8, 0x00, 0x08, 0xF4 }, + { 0x11, 0x03, 0x82, 0x97, 0x00, 0xE4, 0x60, 0xF0, 0xF2, 0x00, 0x08, 0xF4 }, + { 0x05, 0x40, 0xD1, 0x53, 0x00, 0x14, 0x1F, 0x51, 0x71, 0x00, 0x06, 0xF4 }, + { 0xF1, 0x01, 0x77, 0x17, 0x00, 0x21, 0x1F, 0x81, 0x18, 0x00, 0x02, 0xF4 }, + { 0xF1, 0x18, 0x32, 0x11, 0x00, 0xE1, 0x1F, 0xF1, 0x13, 0x00, 0x00, 0xF4 }, + { 0x73, 0x48, 0xF1, 0x53, 0x00, 0x71, 0x1F, 0xF1, 0x06, 0x00, 0x08, 0xF4 }, + { 0x71, 0x8D, 0x71, 0x11, 0x00, 0x61, 0x5F, 0x72, 0x15, 0x00, 0x06, 0xF4 }, + { 0xD7, 0x4F, 0xF2, 0x61, 0x00, 0xD2, 0x1F, 0xF1, 0xB2, 0x00, 0x08, 0xF4 }, + { 0x01, 0x11, 0xF0, 0xFF, 0x00, 0x01, 0x1F, 0xF0, 0xF8, 0x00, 0x0A, 0xF4 }, + { 0x31, 0x8B, 0x41, 0x11, 0x00, 0x61, 0x1F, 0x22, 0x13, 0x00, 0x06, 0xF4 }, + { 0x71, 0x1C, 0x71, 0x03, 0x00, 0x21, 0x1F, 0x64, 0x07, 0x00, 0x0E, 0xF4 }, + { 0x31, 0x8B, 0x41, 0x11, 0x00, 0x61, 0x1F, 0x32, 0x15, 0x00, 0x02, 0xF4 }, + { 0x71, 0x1C, 0xFD, 0x13, 0x00, 0x21, 0x1F, 0xE7, 0xD6, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x1C, 0x51, 0x03, 0x00, 0x21, 0x1F, 0x54, 0x67, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x1C, 0x51, 0x03, 0x00, 0x21, 0x1F, 0x54, 0x17, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x1C, 0x54, 0x15, 0x00, 0x21, 0x1F, 0x53, 0x49, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x56, 0x51, 0x03, 0x00, 0x61, 0x1F, 0x54, 0x17, 0x00, 0x0E, 0xF4 }, + { 0x71, 0x1C, 0x51, 0x03, 0x00, 0x21, 0x1F, 0x54, 0x17, 0x00, 0x0E, 0xF4 }, + { 0x02, 0x29, 0xF5, 0x75, 0x00, 0x01, 0x9F, 0xF2, 0xF3, 0x00, 0x00, 0xF4 }, + { 0x02, 0x29, 0xF0, 0x75, 0x00, 0x01, 0x9F, 0xF4, 0x33, 0x00, 0x00, 0xF4 }, + { 0x01, 0x49, 0xF1, 0x53, 0x00, 0x11, 0x1F, 0xF1, 0x74, 0x00, 0x06, 0xF4 }, + { 0x01, 0x89, 0xF1, 0x53, 0x00, 0x11, 0x1F, 0xF1, 0x74, 0x00, 0x06, 0xF4 }, + { 0x02, 0x89, 0xF1, 0x53, 0x00, 0x11, 0x1F, 0xF1, 0x74, 0x00, 0x06, 0xF4 }, + { 0x02, 0x80, 0xF1, 0x53, 0x00, 0x11, 0x1F, 0xF1, 0x74, 0x00, 0x06, 0xF4 }, + { 0x01, 0x40, 0xF1, 0x53, 0x00, 0x08, 0x5F, 0xF1, 0x53, 0x00, 0x00, 0xF4 }, + { 0x21, 0x15, 0xD3, 0x2C, 0x00, 0x21, 0x9F, 0xC3, 0x2C, 0x00, 0x0A, 0xF4 }, + { 0x01, 0x18, 0xD4, 0xF2, 0x00, 0x21, 0x9F, 0xC4, 0x8A, 0x00, 0x0A, 0xF4 }, + { 0x01, 0x4E, 0xF0, 0x7B, 0x00, 0x11, 0x1F, 0xF4, 0xC8, 0x00, 0x04, 0xF4 }, + { 0x01, 0x44, 0xF0, 0xAB, 0x00, 0x11, 0x1F, 0xF3, 0xAB, 0x00, 0x04, 0xF4 }, + { 0x53, 0x0E, 0xF4, 0xC8, 0x00, 0x11, 0x1F, 0xF1, 0xBB, 0x00, 0x04, 0xF4 }, + { 0x53, 0x0B, 0xF2, 0xC8, 0x00, 0x11, 0x1F, 0xF2, 0xC5, 0x00, 0x04, 0xF4 }, + { 0x21, 0x15, 0xB4, 0x4C, 0x00, 0x21, 0x1F, 0x94, 0xAC, 0x00, 0x0A, 0xF4 }, + { 0x21, 0x15, 0x94, 0x1C, 0x00, 0x21, 0x1F, 0x64, 0xAC, 0x00, 0x0A, 0xF4 }, + { 0x22, 0x1B, 0x97, 0x89, 0x00, 0xA2, 0x1F, 0x70, 0x07, 0x00, 0x0A, 0xF4 }, + { 0x21, 0x19, 0x77, 0xBF, 0x00, 0xA1, 0x9F, 0x60, 0x2A, 0x00, 0x06, 0xF4 }, + { 0xA1, 0x13, 0xD6, 0xAF, 0x00, 0xE2, 0x9F, 0x60, 0x2A, 0x00, 0x02, 0xF4 }, + { 0xA2, 0x1D, 0x95, 0x24, 0x00, 0xE2, 0x9F, 0x60, 0x2A, 0x00, 0x02, 0xF4 }, + { 0x32, 0x9A, 0x51, 0x19, 0x00, 0x61, 0x9F, 0x60, 0x39, 0x00, 0x0C, 0xF4 }, + { 0xA4, 0x12, 0xF4, 0x30, 0x00, 0xE2, 0x9F, 0x60, 0x2A, 0x00, 0x02, 0xF4 }, + { 0x21, 0x16, 0x63, 0x0E, 0x00, 0x21, 0x1F, 0x63, 0x0E, 0x00, 0x0C, 0xF4 }, + { 0x31, 0x16, 0x63, 0x0A, 0x00, 0x21, 0x1F, 0x63, 0x0B, 0x00, 0x0C, 0xF4 }, + { 0x21, 0x1B, 0x63, 0x0A, 0x00, 0x21, 0x1F, 0x63, 0x0B, 0x00, 0x0C, 0xF4 }, + { 0x20, 0x1B, 0x63, 0x0A, 0x00, 0x21, 0x1F, 0x63, 0x0B, 0x00, 0x0C, 0xF4 }, + { 0x32, 0x1C, 0x82, 0x18, 0x00, 0x61, 0x9F, 0x60, 0x07, 0x00, 0x0C, 0xF4 }, + { 0x32, 0x18, 0x61, 0x14, 0x00, 0xE1, 0x9F, 0x72, 0x16, 0x00, 0x0C, 0xF4 }, + { 0x31, 0xC0, 0x77, 0x17, 0x00, 0x22, 0x1F, 0x6B, 0x09, 0x00, 0x02, 0xF4 }, + { 0x71, 0xC3, 0x8E, 0x17, 0x00, 0x22, 0x24, 0x8B, 0x0E, 0x00, 0x02, 0xF4 }, + { 0x70, 0x8D, 0x6E, 0x17, 0x00, 0x22, 0x1F, 0x6B, 0x0E, 0x00, 0x02, 0xF4 }, + { 0x24, 0x4F, 0xF2, 0x06, 0x00, 0x31, 0x1F, 0x52, 0x06, 0x00, 0x0E, 0xF4 }, + { 0x31, 0x1B, 0x64, 0x07, 0x00, 0x61, 0x1F, 0xD0, 0x67, 0x00, 0x0E, 0xF4 }, + { 0x31, 0x1B, 0x61, 0x06, 0x00, 0x61, 0x1F, 0xD2, 0x36, 0x00, 0x0C, 0xF4 }, + { 0x31, 0x1F, 0x31, 0x06, 0x00, 0x61, 0x1F, 0x50, 0x36, 0x00, 0x0C, 0xF4 }, + { 0x31, 0x1F, 0x41, 0x06, 0x00, 0x61, 0x1F, 0xA0, 0x36, 0x00, 0x0C, 0xF4 }, + { 0x21, 0x9A, 0x53, 0x56, 0x00, 0x21, 0x9F, 0xA0, 0x16, 0x00, 0x0E, 0xF4 }, + { 0x21, 0x9A, 0x53, 0x56, 0x00, 0x21, 0x9F, 0xA0, 0x16, 0x00, 0x0E, 0xF4 }, + { 0x61, 0x19, 0x53, 0x58, 0x00, 0x21, 0x1F, 0xA0, 0x18, 0x00, 0x0C, 0xF4 }, + { 0x61, 0x19, 0x73, 0x57, 0x00, 0x21, 0x1F, 0xA0, 0x17, 0x00, 0x0C, 0xF4 }, + { 0x21, 0x1B, 0x71, 0xA6, 0x00, 0x21, 0x1F, 0xA1, 0x96, 0x00, 0x0E, 0xF4 }, + { 0x85, 0x91, 0xF5, 0x44, 0x00, 0xA1, 0x1F, 0xF0, 0x45, 0x00, 0x06, 0xF4 }, + { 0x07, 0x51, 0xF5, 0x33, 0x00, 0x61, 0x1F, 0xF0, 0x25, 0x00, 0x06, 0xF4 }, + { 0x13, 0x8C, 0xFF, 0x21, 0x00, 0x11, 0x9F, 0xFF, 0x03, 0x00, 0x0E, 0xF4 }, + { 0x38, 0x8C, 0xF3, 0x0D, 0x00, 0xB1, 0x5F, 0xF5, 0x33, 0x00, 0x0E, 0xF4 }, + { 0x87, 0x91, 0xF5, 0x55, 0x00, 0x22, 0x1F, 0xF0, 0x54, 0x00, 0x06, 0xF4 }, + { 0xB6, 0x4A, 0xB6, 0x32, 0x00, 0x11, 0x2B, 0xD1, 0x31, 0x00, 0x0E, 0xF4 }, + { 0x04, 0x00, 0xFE, 0xF0, 0x00, 0xC2, 0x1F, 0xF6, 0xB5, 0x00, 0x0E, 0xF4 }, + { 0x05, 0x4E, 0xDA, 0x15, 0x00, 0x01, 0x9F, 0xF0, 0x13, 0x00, 0x0A, 0xF4 }, + { 0x31, 0x44, 0xF2, 0x9A, 0x00, 0x32, 0x1F, 0xF0, 0x27, 0x00, 0x06, 0xF4 }, + { 0xB0, 0xC4, 0xA4, 0x02, 0x00, 0xD7, 0x9F, 0x40, 0x42, 0x00, 0x00, 0xF4 }, + { 0xCA, 0x84, 0xF0, 0xF0, 0x00, 0xCF, 0x1F, 0x59, 0x62, 0x00, 0x0C, 0xF4 }, + { 0x30, 0x35, 0xF5, 0xF0, 0x00, 0x35, 0x1F, 0xF0, 0x9B, 0x00, 0x02, 0xF4 }, + { 0x63, 0x0F, 0xF4, 0x04, 0x02, 0x6F, 0x1F, 0xF0, 0x43, 0x00, 0x06, 0xF4 }, + { 0x07, 0x40, 0x09, 0x53, 0x00, 0x05, 0x1F, 0xF6, 0x94, 0x00, 0x0E, 0xF4 }, + { 0x09, 0x4E, 0xDA, 0x25, 0x00, 0x01, 0x1F, 0xF1, 0x15, 0x00, 0x0A, 0xF4 }, + { 0x04, 0x00, 0xF3, 0xA0, 0x02, 0x04, 0x1F, 0xF8, 0x46, 0x00, 0x0E, 0xF4 }, + { 0x07, 0x00, 0xF0, 0xF0, 0x00, 0x00, 0x1F, 0x5C, 0xDC, 0x00, 0x0E, 0xF4 }, + { 0x1F, 0x1E, 0xE5, 0x5B, 0x00, 0x0F, 0x1F, 0x5D, 0xFA, 0x00, 0x0E, 0xF4 }, + { 0x11, 0x8A, 0xF1, 0x11, 0x00, 0x01, 0x5F, 0xF1, 0xB3, 0x00, 0x06, 0xF4 }, + { 0x00, 0x40, 0xD1, 0x53, 0x00, 0x00, 0x1F, 0xF2, 0x56, 0x00, 0x0E, 0xF4 }, + { 0x32, 0x44, 0xF8, 0xFF, 0x00, 0x11, 0x1F, 0xF5, 0x7F, 0x00, 0x0E, 0xF4 }, + { 0x00, 0x40, 0x09, 0x53, 0x00, 0x02, 0x1F, 0xF7, 0x94, 0x00, 0x0E, 0xF4 }, + { 0x11, 0x86, 0xF2, 0xA8, 0x00, 0x01, 0x9F, 0xA0, 0xA8, 0x00, 0x08, 0xF4 }, + { 0x00, 0x50, 0xF2, 0x70, 0x00, 0x13, 0x1F, 0xF2, 0x72, 0x00, 0x0E, 0xF4 }, + { 0xF0, 0x00, 0x11, 0x11, 0x00, 0xE0, 0xDF, 0x11, 0x11, 0x00, 0x0E, 0xF4 } +}; + +// hardcoded, dumped from ADHOM.DRV +uint16 frequencyLookUpTable[SHERLOCK_ADLIB_NOTES_COUNT] = { + 0x0158, 0x016C, 0x0182, 0x0199, 0x01B1, 0x01CB, 0x01E6, 0x0203, 0x0222, 0x0242, + 0x0265, 0x0289, 0x0558, 0x056C, 0x0582, 0x0599, 0x05B1, 0x05CB, 0x05E6, 0x0603, + 0x0622, 0x0642, 0x0665, 0x0689, 0x0958, 0x096C, 0x0982, 0x0999, 0x09B1, 0x09CB, + 0x09E6, 0x0A03, 0x0A22, 0x0A42, 0x0A65, 0x0A89, 0x0D58, 0x0D6C, 0x0D82, 0x0D99, + 0x0DB1, 0x0DCB, 0x0DE6, 0x0E03, 0x0E22, 0x0E42, 0x0E65, 0x0E89, 0x1158, 0x116C, + 0x1182, 0x1199, 0x11B1, 0x11CB, 0x11E6, 0x1203, 0x1222, 0x1242, 0x1265, 0x1289, + 0x1558, 0x156C, 0x1582, 0x1599, 0x15B1, 0x15CB, 0x15E6, 0x1603, 0x1622, 0x1642, + 0x1665, 0x1689, 0x1958, 0x196C, 0x1982, 0x1999, 0x19B1, 0x19CB, 0x19E6, 0x1A03, + 0x1A22, 0x1A42, 0x1A65, 0x1A89, 0x1D58, 0x1D6C, 0x1D82, 0x1D99, 0x1DB1, 0x1DCB, + 0x1DE6, 0x1E03, 0x1E22, 0x1E42, 0x1E65, 0x1E89 +}; + +class MidiDriver_SH_AdLib : public MidiDriver { +public: + MidiDriver_SH_AdLib(Audio::Mixer *mixer) + : _masterVolume(15), _opl(0), + _adlibTimerProc(0), _adlibTimerParam(0), _isOpen(false) { + memset(_voiceChannelMapping, 0, sizeof(_voiceChannelMapping)); + } + virtual ~MidiDriver_SH_AdLib() { } + + // MidiDriver + int open(); + void close(); + void send(uint32 b); + MidiChannel *allocateChannel() { return NULL; } + MidiChannel *getPercussionChannel() { return NULL; } + bool isOpen() const { return _isOpen; } + uint32 getBaseTempo() { return 1000000 / OPL::OPL::kDefaultCallbackFrequency; } + + int getPolyphony() const { return SHERLOCK_ADLIB_VOICES_COUNT; } + bool hasRhythmChannel() const { return false; } + + virtual void setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc); + + void setVolume(byte volume); + virtual uint32 property(int prop, uint32 param); + + void newMusicData(byte *musicData, int32 musicDataSize); + +private: + struct adlib_ChannelEntry { + bool inUse; + uint16 inUseTimer; + const InstrumentEntry *currentInstrumentPtr; + byte currentNote; + byte currentA0hReg; + byte currentB0hReg; + + adlib_ChannelEntry() : inUse(false), inUseTimer(0), currentInstrumentPtr(NULL), currentNote(0), + currentA0hReg(0), currentB0hReg(0) { } + }; + + OPL::OPL *_opl; + int _masterVolume; + + Common::TimerManager::TimerProc _adlibTimerProc; + void *_adlibTimerParam; + + bool _isOpen; + + // points to a MIDI channel for each of the new voice channels + byte _voiceChannelMapping[SHERLOCK_ADLIB_VOICES_COUNT]; + + // stores information about all FM voice channels + adlib_ChannelEntry _channels[SHERLOCK_ADLIB_VOICES_COUNT]; + + void onTimer(); + + void resetAdLib(); + void resetAdLibOperatorRegisters(byte baseRegister, byte value); + void resetAdLibFMVoiceChannelRegisters(byte baseRegister, byte value); + + void programChange(byte MIDIchannel, byte parameter); + void setRegister(int reg, int value); + void noteOn(byte MIDIchannel, byte note, byte velocity); + void noteOff(byte MIDIchannel, byte note); + void voiceOnOff(byte FMVoiceChannel, bool KeyOn, byte note, byte velocity); + + void pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2); +}; + +int MidiDriver_SH_AdLib::open() { + debugC(kDebugLevelAdLibDriver, "AdLib: starting driver"); + + _opl = OPL::Config::create(OPL::Config::kOpl2); + + if (!_opl) + return -1; + + _opl->init(); + + _isOpen = true; + + _opl->start(new Common::Functor0Mem<void, MidiDriver_SH_AdLib>(this, &MidiDriver_SH_AdLib::onTimer)); + + return 0; +} + +void MidiDriver_SH_AdLib::close() { + // Stop the OPL timer + _opl->stop(); + + delete _opl; +} + +void MidiDriver_SH_AdLib::setVolume(byte volume) { + _masterVolume = volume; + //renewNotes(-1, true); +} + +// this should/must get called per tick +// original driver did this before MIDI data processing on each tick +// we do it atm after MIDI data processing +void MidiDriver_SH_AdLib::onTimer() { + if (_adlibTimerProc) + (*_adlibTimerProc)(_adlibTimerParam); + + // this should/must get called per tick + // original driver did this before MIDI data processing on each tick + // we do it atm after MIDI data processing + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_channels[FMvoiceChannel].inUse) { + _channels[FMvoiceChannel].inUseTimer++; + } + } +} + +// Called when a music track got loaded into memory +void MidiDriver_SH_AdLib::newMusicData(byte *musicData, int32 musicDataSize) { + assert(musicDataSize >= 0x7F); + // MIDI Channel <-> FM Voice Channel mapping at offset 0x22 of music data + memcpy(&_voiceChannelMapping, musicData + 0x22, 9); + + // reset OPL + resetAdLib(); + + // reset current channel data + memset(&_channels, 0, sizeof(_channels)); +} + +void MidiDriver_SH_AdLib::resetAdLib() { + + setRegister(0x01, 0x20); // enable waveform control on both operators + setRegister(0x04, 0xE0); // Timer control + + setRegister(0x08, 0); // select FM music mode + setRegister(0xBD, 0); // disable Rhythm + + // reset FM voice instrument data + resetAdLibOperatorRegisters(0x20, 0); + resetAdLibOperatorRegisters(0x60, 0); + resetAdLibOperatorRegisters(0x80, 0); + resetAdLibFMVoiceChannelRegisters(0xA0, 0); + resetAdLibFMVoiceChannelRegisters(0xB0, 0); + resetAdLibFMVoiceChannelRegisters(0xC0, 0); + resetAdLibOperatorRegisters(0xE0, 0); + resetAdLibOperatorRegisters(0x40, 0x3F); +} + +void MidiDriver_SH_AdLib::resetAdLibOperatorRegisters(byte baseRegister, byte value) { + byte operatorIndex; + + for (operatorIndex = 0; operatorIndex < 0x16; operatorIndex++) { + switch (operatorIndex) { + case 0x06: + case 0x07: + case 0x0E: + case 0x0F: + break; + default: + setRegister(baseRegister + operatorIndex, value); + } + } +} + +void MidiDriver_SH_AdLib::resetAdLibFMVoiceChannelRegisters(byte baseRegister, byte value) { + byte FMvoiceChannel; + + for (FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + setRegister(baseRegister + FMvoiceChannel, value); + } +} + +// MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php +void MidiDriver_SH_AdLib::send(uint32 b) { + byte command = b & 0xf0; + byte channel = b & 0xf; + byte op1 = (b >> 8) & 0xff; + byte op2 = (b >> 16) & 0xff; + + switch (command) { + case 0x80: + noteOff(channel, op1); + break; + case 0x90: + noteOn(channel, op1, op2); + break; + case 0xb0: // Control change + // Doesn't seem to be implemented in the Sherlock Holmes adlib driver + break; + case 0xc0: // Program Change + programChange(channel, op1); + break; + case 0xa0: // Polyphonic key pressure (aftertouch) + case 0xd0: // Channel pressure (aftertouch) + // Aftertouch doesn't seem to be implemented in the Sherlock Holmes adlib driver + break; + case 0xe0: + debugC(kDebugLevelAdLibDriver, "AdLib: pitch bend change"); + pitchBendChange(channel, op1, op2); + break; + case 0xf0: // SysEx + warning("ADLIB: SysEx: %x", b); + break; + default: + warning("ADLIB: Unknown event %02x", command); + } +} + +void MidiDriver_SH_AdLib::noteOn(byte MIDIchannel, byte note, byte velocity) { + int16 oldestInUseChannel = -1; + uint16 oldestInUseTimer = 0; + + if (velocity == 0) + return noteOff(MIDIchannel, note); + + if (MIDIchannel != 9) { + // Not Percussion + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + if (!_channels[FMvoiceChannel].inUse) { + _channels[FMvoiceChannel].inUse = true; + _channels[FMvoiceChannel].currentNote = note; + + voiceOnOff(FMvoiceChannel, true, note, velocity); + return; + } + } + } + + // Look for oldest in-use channel + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + if (_channels[FMvoiceChannel].inUseTimer > oldestInUseTimer) { + oldestInUseTimer = _channels[FMvoiceChannel].inUseTimer; + oldestInUseChannel = FMvoiceChannel; + } + } + } + if (oldestInUseChannel >= 0) { + // channel found + debugC(kDebugLevelAdLibDriver, "AdLib: used In-Use channel"); + // original driver used note 0, we use the current note + // because using note 0 could create a bad note (out of index) and we check that. Original driver didn't. + voiceOnOff(oldestInUseChannel, false, _channels[oldestInUseChannel].currentNote, 0); + + _channels[oldestInUseChannel].inUse = true; + _channels[oldestInUseChannel].inUseTimer = 0; // safety, original driver also did this + _channels[oldestInUseChannel].currentNote = note; + voiceOnOff(oldestInUseChannel, true, note, velocity); + return; + } + debugC(kDebugLevelAdLibDriver, "AdLib: MIDI channel not mapped/all FM voice channels busy %d", MIDIchannel); + + } else { + // Percussion channel + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + if (note == percussionChannelTable[FMvoiceChannel].requiredNote) { + _channels[FMvoiceChannel].inUse = true; + _channels[FMvoiceChannel].currentNote = note; + + voiceOnOff(FMvoiceChannel, true, percussionChannelTable[FMvoiceChannel].replacementNote, velocity); + return; + } + } + } + debugC(kDebugLevelAdLibDriver, "AdLib: percussion MIDI channel not mapped/all FM voice channels busy"); + } +} + +void MidiDriver_SH_AdLib::noteOff(byte MIDIchannel, byte note) { + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + if (_channels[FMvoiceChannel].currentNote == note) { + _channels[FMvoiceChannel].inUse = false; + _channels[FMvoiceChannel].inUseTimer = 0; + _channels[FMvoiceChannel].currentNote = 0; + + if (MIDIchannel != 9) { + // not-percussion + voiceOnOff(FMvoiceChannel, false, note, 0); + } else { + voiceOnOff(FMvoiceChannel, false, percussionChannelTable[FMvoiceChannel].replacementNote, 0); + } + return; + } + } + } +} + +void MidiDriver_SH_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, byte velocity) { + byte frequencyOffset = 0; + uint16 frequency = 0; + byte op2RegAdjust = 0; + byte regValue40h = 0; + byte regValueA0h = 0; + byte regValueB0h = 0; + + // Look up frequency + if (_channels[FMvoiceChannel].currentInstrumentPtr) { + frequencyOffset = note + _channels[FMvoiceChannel].currentInstrumentPtr->frequencyAdjust; + } else { + frequencyOffset = note; + } + if (frequencyOffset >= SHERLOCK_ADLIB_NOTES_COUNT) { + warning("CRITICAL - AdLib driver: bad note!!!"); + return; + } + frequency = frequencyLookUpTable[frequencyOffset]; + + if (keyOn) { + // adjust register 40h + if (_channels[FMvoiceChannel].currentInstrumentPtr) { + regValue40h = _channels[FMvoiceChannel].currentInstrumentPtr->reg40op2; + } + regValue40h = regValue40h - (velocity >> 3); + op2RegAdjust = operator2Register[FMvoiceChannel]; + setRegister(0x40 + op2RegAdjust, regValue40h); + } + + regValueA0h = frequency & 0xFF; + regValueB0h = frequency >> 8; + if (keyOn) { + regValueB0h |= 0x20; // set Key-On flag + } + + setRegister(0xA0 + FMvoiceChannel, regValueA0h); + setRegister(0xB0 + FMvoiceChannel, regValueB0h); + _channels[FMvoiceChannel].currentA0hReg = regValueA0h; + _channels[FMvoiceChannel].currentB0hReg = regValueB0h; +} + +void MidiDriver_SH_AdLib::pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2) { + uint16 channelFrequency = 0; + byte channelRegB0hWithoutFrequency = 0; + uint16 parameter = 0; + byte regValueA0h = 0; + byte regValueB0h = 0; + + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + if (_channels[FMvoiceChannel].inUse) { + // FM voice channel found and it's currently in use -> apply pitch bend change + + // Remove frequency bits from current channel B0h-register + channelFrequency = ((_channels[FMvoiceChannel].currentB0hReg << 8) | (_channels[FMvoiceChannel].currentA0hReg)) & 0x3FF; + channelRegB0hWithoutFrequency = _channels[FMvoiceChannel].currentB0hReg & 0xFC; + + if (parameter2 < 0x40) { + channelFrequency = channelFrequency / 2; + } else { + parameter2 = parameter2 - 0x40; + } + parameter1 = parameter1 * 2; + parameter = parameter1 | (parameter2 << 8); + parameter = parameter * 4; + + parameter = (parameter >> 8) + 0xFF; + channelFrequency = channelFrequency * parameter; + channelFrequency = (channelFrequency >> 8) | (parameter << 8); + + regValueA0h = channelFrequency & 0xFF; + regValueB0h = (channelFrequency >> 8) | channelRegB0hWithoutFrequency; + + setRegister(0xA0 + FMvoiceChannel, regValueA0h); + setRegister(0xB0 + FMvoiceChannel, regValueB0h); + } + } + } +} + +void MidiDriver_SH_AdLib::programChange(byte MIDIchannel, byte op1) { + const InstrumentEntry *instrumentPtr; + byte op1Reg = 0; + byte op2Reg = 0; + + // setup instrument + instrumentPtr = &instrumentTable[op1]; + //warning("program change for MIDI channel %d, instrument id %d", MIDIchannel, op1); + + for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) { + if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) { + + op1Reg = operator1Register[FMvoiceChannel]; + op2Reg = operator2Register[FMvoiceChannel]; + + setRegister(0x20 + op1Reg, instrumentPtr->reg20op1); + setRegister(0x40 + op1Reg, instrumentPtr->reg40op1); + setRegister(0x60 + op1Reg, instrumentPtr->reg60op1); + setRegister(0x80 + op1Reg, instrumentPtr->reg80op1); + setRegister(0xE0 + op1Reg, instrumentPtr->regE0op1); + + setRegister(0x20 + op2Reg, instrumentPtr->reg20op2); + setRegister(0x40 + op2Reg, instrumentPtr->reg40op2); + setRegister(0x60 + op2Reg, instrumentPtr->reg60op2); + setRegister(0x80 + op2Reg, instrumentPtr->reg80op2); + setRegister(0xE0 + op2Reg, instrumentPtr->regE0op2); + + setRegister(0xC0 + FMvoiceChannel, instrumentPtr->regC0); + + // Remember instrument + _channels[FMvoiceChannel].currentInstrumentPtr = instrumentPtr; + } + } +} +void MidiDriver_SH_AdLib::setRegister(int reg, int value) { + _opl->write(0x220, reg); + _opl->write(0x221, value); +} + +uint32 MidiDriver_SH_AdLib::property(int prop, uint32 param) { + return 0; +} + +void MidiDriver_SH_AdLib::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) { + _adlibTimerProc = timerProc; + _adlibTimerParam = timerParam; +} + +MidiDriver *MidiDriver_SH_AdLib_create() { + return new MidiDriver_SH_AdLib(g_system->getMixer()); +} + +void MidiDriver_SH_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize) { + static_cast<MidiDriver_SH_AdLib *>(driver)->newMusicData(musicData, musicDataSize); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/drivers/mididriver.h b/engines/sherlock/scalpel/drivers/mididriver.h new file mode 100644 index 0000000000..1b8ceeda3d --- /dev/null +++ b/engines/sherlock/scalpel/drivers/mididriver.h @@ -0,0 +1,41 @@ +/* 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_DRIVERS_MIDIDRIVER_H +#define SHERLOCK_SCALPEL_DRIVERS_MIDIDRIVER_H + +#include "sherlock/sherlock.h" +#include "audio/mididrv.h" +#include "common/error.h" + +namespace Sherlock { + +extern MidiDriver *MidiDriver_SH_AdLib_create(); +extern void MidiDriver_SH_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize); + +extern MidiDriver *MidiDriver_MT32_create(); +extern void MidiDriver_MT32_uploadPatches(MidiDriver *driver, byte *driverData, int32 driverSize); +extern void MidiDriver_MT32_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize); + +} // End of namespace Sherlock + +#endif // SHERLOCK_SCALPEL_DRIVERS_MIDIDRIVER_H diff --git a/engines/sherlock/scalpel/drivers/mt32.cpp b/engines/sherlock/scalpel/drivers/mt32.cpp new file mode 100644 index 0000000000..33e7671719 --- /dev/null +++ b/engines/sherlock/scalpel/drivers/mt32.cpp @@ -0,0 +1,282 @@ +/* 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/scalpel/drivers/mididriver.h" + +#include "common/config-manager.h" +#include "common/file.h" +#include "common/system.h" +#include "common/textconsole.h" + +//#include "audio/mididrv.h" + +namespace Sherlock { + +#define SHERLOCK_MT32_CHANNEL_COUNT 16 + +const byte mt32ReverbDataSysEx[] = { + 0x10, 0x00, 0x01, 0x01, 0x05, 0x05, 0xFF +}; + +class MidiDriver_MT32 : public MidiDriver { +public: + MidiDriver_MT32() { + _driver = NULL; + _isOpen = false; + _nativeMT32 = false; + _baseFreq = 250; + + memset(_MIDIchannelActive, 1, sizeof(_MIDIchannelActive)); + } + virtual ~MidiDriver_MT32(); + + // MidiDriver + int open(); + void close(); + bool isOpen() const { return _isOpen; } + + void send(uint32 b); + + void newMusicData(byte *musicData, int32 musicDataSize); + + MidiChannel *allocateChannel() { + if (_driver) + return _driver->allocateChannel(); + return NULL; + } + MidiChannel *getPercussionChannel() { + if (_driver) + return _driver->getPercussionChannel(); + return NULL; + } + + void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) { + if (_driver) + _driver->setTimerCallback(timer_param, timer_proc); + } + + uint32 getBaseTempo() { + if (_driver) { + return _driver->getBaseTempo(); + } + return 1000000 / _baseFreq; + } + +protected: + Common::Mutex _mutex; + MidiDriver *_driver; + bool _nativeMT32; + + bool _isOpen; + int _baseFreq; + +private: + // points to a MIDI channel for each of the new voice channels + byte _MIDIchannelActive[SHERLOCK_MT32_CHANNEL_COUNT]; + +public: + void uploadMT32Patches(byte *driverData, int32 driverSize); + + void mt32SysEx(const byte *&dataPtr, int32 &bytesLeft); +}; + +MidiDriver_MT32::~MidiDriver_MT32() { + Common::StackLock lock(_mutex); + if (_driver) { + _driver->setTimerCallback(0, 0); + _driver->close(); + delete _driver; + } + _driver = NULL; +} + +int MidiDriver_MT32::open() { + assert(!_driver); + + debugC(kDebugLevelMT32Driver, "MT32: starting driver"); + + // Setup midi driver + MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_MT32); + MusicType musicType = MidiDriver::getMusicType(dev); + + switch (musicType) { + case MT_MT32: + _nativeMT32 = true; + break; + case MT_GM: + if (ConfMan.getBool("native_mt32")) { + _nativeMT32 = true; + } + break; + default: + break; + } + + _driver = MidiDriver::createMidi(dev); + if (!_driver) + return 255; + + if (_nativeMT32) + _driver->property(MidiDriver::PROP_CHANNEL_MASK, 0x03FE); + + int ret = _driver->open(); + if (ret) + return ret; + + if (_nativeMT32) + _driver->sendMT32Reset(); + else + _driver->sendGMReset(); + + return 0; +} + +void MidiDriver_MT32::close() { + if (_driver) { + _driver->close(); + } +} + +// Called when a music track got loaded into memory +void MidiDriver_MT32::newMusicData(byte *musicData, int32 musicDataSize) { + assert(musicDataSize >= 0x7F); // Security check + + // MIDI Channel Enable/Disable bytes at offset 0x2 of music data + memcpy(&_MIDIchannelActive, musicData + 0x2, SHERLOCK_MT32_CHANNEL_COUNT); + + // Send 16 bytes from offset 0x12 to MT32 + // All the music tracks of Sherlock seem to contain dummy data + // probably a feature, that was used in the game "Ski or Die" + // that's why we don't implement this + + // Also send these bytes to MT32 (SysEx) - seems to be reverb configuration + if (_nativeMT32) { + const byte *reverbData = mt32ReverbDataSysEx; + int32 reverbDataSize = sizeof(mt32ReverbDataSysEx); + mt32SysEx(reverbData, reverbDataSize); + } +} + +void MidiDriver_MT32::uploadMT32Patches(byte *driverData, int32 driverSize) { + if (!_driver) + return; + + if (!_nativeMT32) + return; + + // patch data starts at offset 0x863 + assert(driverSize == 0x13B9); // Security check + assert(driverData[0x863] == 0x7F); // another security check + + const byte *patchPtr = driverData + 0x863; + int32 bytesLeft = driverSize - 0x863; + + while(1) { + mt32SysEx(patchPtr, bytesLeft); + + assert(bytesLeft); + if (*patchPtr == 0x80) // List terminator + break; + } +} + +void MidiDriver_MT32::mt32SysEx(const byte *&dataPtr, int32 &bytesLeft) { + byte sysExMessage[270]; + uint16 sysExPos = 0; + byte sysExByte = 0; + uint16 sysExChecksum = 0; + + memset(&sysExMessage, 0, sizeof(sysExMessage)); + + sysExMessage[0] = 0x41; // Roland + sysExMessage[1] = 0x10; + sysExMessage[2] = 0x16; // Model MT32 + sysExMessage[3] = 0x12; // Command DT1 + + sysExPos = 4; + sysExChecksum = 0; + while (1) { + assert(bytesLeft); + + sysExByte = *dataPtr++; + bytesLeft--; + if (sysExByte == 0xff) + break; // Message done + + assert(sysExPos < sizeof(sysExMessage)); + sysExMessage[sysExPos++] = sysExByte; + sysExChecksum -= sysExByte; + } + + // Calculate checksum + assert(sysExPos < sizeof(sysExMessage)); + sysExMessage[sysExPos++] = sysExChecksum & 0x7f; + + debugC(kDebugLevelMT32Driver, "MT32: uploading patch data, size %d", sysExPos); + + // Send SysEx + _driver->sysEx(sysExMessage, sysExPos); + + // Wait the time it takes to send the SysEx data + uint32 delay = (sysExPos + 2) * 1000 / 3125; + + // Plus an additional delay for the MT-32 rev00 + if (_nativeMT32) + delay += 40; + + g_system->delayMillis(delay); +} + +// MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php +void MidiDriver_MT32::send(uint32 b) { + byte command = b & 0xf0; + byte channel = b & 0xf; + + if (command == 0xF0) { + if (_driver) { + _driver->send(b); + } + return; + } + + if (_MIDIchannelActive[channel]) { + // Only forward MIDI-data in case the channel is currently enabled via music-data + if (_driver) { + _driver->send(b); + } + } +} + +MidiDriver *MidiDriver_MT32_create() { + return new MidiDriver_MT32(); +} + +void MidiDriver_MT32_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize) { + static_cast<MidiDriver_MT32 *>(driver)->newMusicData(musicData, musicDataSize); +} + +void MidiDriver_MT32_uploadPatches(MidiDriver *driver, byte *driverData, int32 driverSize) { + static_cast<MidiDriver_MT32 *>(driver)->uploadMT32Patches(driverData, driverSize); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel.cpp b/engines/sherlock/scalpel/scalpel.cpp new file mode 100644 index 0000000000..af9d613cce --- /dev/null +++ b/engines/sherlock/scalpel/scalpel.cpp @@ -0,0 +1,1160 @@ +/* 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 "engines/util.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_scene.h" +#include "sherlock/scalpel/tsage/logo.h" +#include "sherlock/sherlock.h" +#include "sherlock/music.h" +#include "sherlock/animation.h" +// for 3DO +#include "sherlock/scalpel/3do/movie_decoder.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 + +struct PeopleData { + const char *portrait; + const char *name; + byte stillSequences[MAX_TALK_SEQUENCES]; + byte talkSequences[MAX_TALK_SEQUENCES]; +}; + +const PeopleData PEOPLE_DATA[MAX_PEOPLE] = { + { "HOLM", "Sherlock Holmes", { 1, 0, 0 }, { 1, 0, 0 } }, + { "WATS", "Dr. Watson", { 6, 0, 0 }, { 5, 5, 6, 7, 8, 7, 8, 6, 0, 0 } }, + { "LEST", "Inspector Lestrade", { 4, 0, 0 }, { 2, 0, 0 } }, + { "CON1", "Constable O'Brien", { 2, 0, 0 }, { 1, 0, 0 } }, + { "CON2", "Constable Lewis", { 2, 0, 0 }, { 1, 0, 0 } }, + { "SHEI", "Sheila Parker", { 2, 0, 0 }, { 2, 3, 0, 0 } }, + { "HENR", "Henry Carruthers", { 3, 0, 0 }, { 3, 0, 0 } }, + { "LESL", "Lesley", { 9, 0, 0 }, { 1, 2, 3, 2, 1, 2, 3, 0, 0 } }, + { "USH1", "An Usher", { 13, 0, 0 }, { 13, 14, 0, 0 } }, + { "USH2", "An Usher", { 2, 0, 0 }, { 2, 0, 0 } }, + { "FRED", "Fredrick Epstein", { 4, 0, 0 }, { 1, 2, 3, 4, 3, 4, 3, 2, 0, 0 } }, + { "WORT", "Mrs. Worthington", { 9, 0, 0 }, { 8, 0, 0 } }, + { "COAC", "The Coach", { 2, 0, 0 }, { 1, 2, 3, 4, 5, 4, 3, 2, 0, 0 } }, + { "PLAY", "A Player", { 8, 0, 0 }, { 7, 8, 0, 0 } }, + { "WBOY", "Tim", { 13, 0, 0 }, { 12, 13, 0, 0 } }, + { "JAME", "James Sanders", { 6, 0, 0 }, { 3, 4, 0, 0 } }, + { "BELL", "Belle", { 1, 0, 0 }, { 4, 5, 0, 0 } }, + { "GIRL", "Cleaning Girl", { 20, 0, 0 }, { 14, 15, 16, 17, 18, 19, 20, 20, 20, 0, 0 } }, + { "EPST", "Fredrick Epstein", { 17, 0, 0 }, { 16, 17, 18, 18, 18, 17, 17, 0, 0 } }, + { "WIGG", "Wiggins", { 3, 0, 0 }, { 2, 3, 0, 0 } }, + { "PAUL", "Paul", { 2, 0, 0 }, { 1, 2, 0, 0 } }, + { "BART", "The Bartender", { 1, 0, 0 }, { 1, 0, 0 } }, + { "DIRT", "A Dirty Drunk", { 1, 0, 0 }, { 1, 0, 0 } }, + { "SHOU", "A Shouting Drunk", { 1, 0, 0 }, { 1, 0, 0 } }, + { "STAG", "A Staggering Drunk", { 1, 0, 0 }, { 1, 0, 0 } }, + { "BOUN", "The Bouncer", { 1, 0, 0 }, { 1, 0, 0 } }, + { "SAND", "James Sanders", { 6, 0, 0 }, { 5, 6, 0, 0 } }, + { "CORO", "The Coroner", { 6, 0, 0 }, { 4, 5, 0, 0 } }, + { "EQUE", "Reginald Snipes", { 1, 0, 0 }, { 1, 0, 0 } }, + { "GEOR", "George Blackwood", { 1, 0, 0 }, { 1, 0, 0 } }, + { "LARS", "Lars", { 7, 0, 0 }, { 5, 6, 0, 0 } }, + { "PARK", "Sheila Parker", { 1, 0, 0 }, { 1, 0, 0 } }, + { "CHEM", "The Chemist", { 8, 0, 0 }, { 8, 9, 0, 0 } }, + { "GREG", "Inspector Gregson", { 6, 0, 0 }, { 5, 6, 0, 0 } }, + { "LAWY", "Jacob Farthington", { 1, 0, 0 }, { 1, 0, 0 } }, + { "MYCR", "Mycroft", { 1, 0, 0 }, { 1, 0, 0 } }, + { "SHER", "Old Sherman", { 7, 0, 0 }, { 7, 8, 0, 0 } }, + { "CHMB", "Richard", { 1, 0, 0 }, { 1, 0, 0 } }, + { "BARM", "The Barman", { 1, 0, 0 }, { 1, 0, 0 } }, + { "DAND", "A Dandy Player", { 1, 0, 0 }, { 1, 0, 0 } }, + { "ROUG", "A Rough-looking Player", { 1, 0, 0 }, { 1, 0, 0 } }, + { "SPEC", "A Spectator", { 1, 0, 0 }, { 1, 0, 0 } }, + { "HUNT", "Robert Hunt", { 1, 0, 0 }, { 1, 0, 0 } }, + { "VIOL", "Violet", { 3, 0, 0 }, { 3, 4, 0, 0 } }, + { "PETT", "Pettigrew", { 1, 0, 0 }, { 1, 0, 0 } }, + { "APPL", "Augie", { 8, 0, 0 }, { 14, 15, 0, 0 } }, + { "ANNA", "Anna Carroway", { 16, 0, 0 }, { 3, 4, 5, 6, 0, 0 } }, + { "GUAR", "A Guard", { 1, 0, 0 }, { 4, 5, 6, 0, 0 } }, + { "ANTO", "Antonio Caruso", { 8, 0, 0 }, { 7, 8, 0, 0 } }, + { "TOBY", "Toby the Dog", { 1, 0, 0 }, { 1, 0, 0 } }, + { "KING", "Simon Kingsley", { 13, 0, 0 }, { 13, 14, 0, 0 } }, + { "ALFR", "Alfred", { 2, 0, 0 }, { 2, 3, 0, 0 } }, + { "LADY", "Lady Brumwell", { 1, 0, 0 }, { 3, 4, 0, 0 } }, + { "ROSA", "Madame Rosa", { 1, 0, 0 }, { 1, 30, 0, 0 } }, + { "LADB", "Lady Brumwell", { 1, 0, 0 }, { 3, 4, 0, 0 } }, + { "MOOR", "Joseph Moorehead", { 1, 0, 0 }, { 1, 0, 0 } }, + { "BEAL", "Mrs. Beale", { 5, 0, 0 }, { 14, 15, 16, 17, 18, 19, 20, 0, 0 } }, + { "LION", "Felix", { 1, 0, 0 }, { 1, 0, 0 } }, + { "HOLL", "Hollingston", { 1, 0, 0 }, { 1, 0, 0 } }, + { "CALL", "Constable Callaghan", { 1, 0, 0 }, { 1, 0, 0 } }, + { "JERE", "Sergeant Duncan", { 2, 0, 0 }, { 1, 1, 2, 2, 0, 0 } }, + { "LORD", "Lord Brumwell", { 1, 0, 0 }, { 9, 10, 0, 0 } }, + { "NIGE", "Nigel Jaimeson", { 1, 0, 0 }, { 1, 2, 0, 138, 3, 4, 0, 138, 0, 0 } }, + { "JONA", "Jonas", { 1, 0, 0 }, { 1, 8, 0, 0 } }, + { "DUGA", "Constable Dugan", { 1, 0, 0 }, { 1, 0, 0 } }, + { "INSP", "Inspector Lestrade", { 4, 0, 0 }, { 2, 0, 0 } } +}; + +/*----------------------------------------------------------------*/ + +ScalpelEngine::ScalpelEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + SherlockEngine(syst, gameDesc) { + _darts = nullptr; + _mapResult = 0; +} + +ScalpelEngine::~ScalpelEngine() { + delete _darts; +} + +void ScalpelEngine::initialize() { + // 3DO actually uses RGB555, but some platforms of ours only support RGB565, so we use that + + if (getPlatform() == Common::kPlatform3DO) { + const Graphics::PixelFormat pixelFormatRGB565 = Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0); + // 320x200 16-bit RGB565 for 3DO support + initGraphics(320, 200, false, &pixelFormatRGB565); + } else { + // 320x200 palettized + initGraphics(320, 200, false); + } + + // Let the base engine intialize + 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 + ScalpelMap &map = *(ScalpelMap *)_map; + 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_DATA[idx].name, PEOPLE_DATA[idx].portrait, + PEOPLE_DATA[idx].stillSequences, PEOPLE_DATA[idx].talkSequences)); + + _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() { + bool finished = true; + + if (isDemo() && _interactiveFl) + return; + + if (getPlatform() == Common::kPlatform3DO) { + show3DOSplash(); + + finished = showCityCutscene3DO(); + if (finished) + finished = showAlleyCutscene3DO(); + if (finished) + finished = showStreetCutscene3DO(); + if (finished) + showOfficeCutscene3DO(); + + _events->clearEvents(); + _music->stopMusic(); + return; + } + + TsAGE::Logo::show(this); + finished = showCityCutscene(); + if (finished) + finished = showAlleyCutscene(); + if (finished) + finished = showStreetCutscene(); + if (finished) + showOfficeCutscene(); + + _events->clearEvents(); + _music->stopMusic(); +} + +bool ScalpelEngine::showCityCutscene() { + byte greyPalette[PALETTE_SIZE]; + byte palette[PALETTE_SIZE]; + + // Demo fades from black into grey and then fades from grey into the scene + Common::fill(&greyPalette[0], &greyPalette[PALETTE_SIZE], 142); + _screen->fadeIn((const byte *)greyPalette, 3); + + _music->loadSong("prolog1"); + _animation->_gfxLibraryFilename = "title.lib"; + _animation->_soundLibraryFilename = "title.snd"; + bool finished = _animation->play("26open1", true, 1, 255, true, 2); + + if (finished) { + ImageFile titleImages_LondonNovember("title2.vgs", true); + _screen->_backBuffer1.blitFrom(*_screen); + _screen->_backBuffer2.blitFrom(*_screen); + + Common::Point londonPosition; + + if ((titleImages_LondonNovember[0]._width == 302) && (titleImages_LondonNovember[0]._height == 39)) { + // Spanish + londonPosition = Common::Point(9, 8); + } else { + // English (German uses the same English graphics), width 272, height 37 + // In the German version this is placed differently, check against German floppy version TODO + londonPosition = Common::Point(30, 50); + } + + // London, England + _screen->_backBuffer1.transBlitFrom(titleImages_LondonNovember[0], londonPosition); + _screen->randomTransition(); + finished = _events->delay(1000, true); + + // November, 1888 + if (finished) { + _screen->_backBuffer1.transBlitFrom(titleImages_LondonNovember[1], Common::Point(100, 100)); + _screen->randomTransition(); + finished = _events->delay(5000, true); + } + + // Transition out the title + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2); + _screen->randomTransition(); + } + + if (finished) + finished = _animation->play("26open2", true, 1, 0, false, 2); + + if (finished) { + ImageFile titleImages_SherlockHolmesTitle("title.vgs", true); + _screen->_backBuffer1.blitFrom(*_screen); + _screen->_backBuffer2.blitFrom(*_screen); + + Common::Point lostFilesPosition; + Common::Point sherlockHolmesPosition; + Common::Point copyrightPosition; + + if ((titleImages_SherlockHolmesTitle[0]._width == 306) && (titleImages_SherlockHolmesTitle[0]._height == 39)) { + // Spanish + lostFilesPosition = Common::Point(5, 5); + sherlockHolmesPosition = Common::Point(24, 40); + copyrightPosition = Common::Point(3, 190); + } else { + // English (German uses the same English graphics), width 208, height 39 + lostFilesPosition = Common::Point(75, 6); + sherlockHolmesPosition = Common::Point(34, 21); + copyrightPosition = Common::Point(4, 190); + } + + // The Lost Files of + _screen->_backBuffer1.transBlitFrom(titleImages_SherlockHolmesTitle[0], lostFilesPosition); + // Sherlock Holmes + _screen->_backBuffer1.transBlitFrom(titleImages_SherlockHolmesTitle[1], sherlockHolmesPosition); + // copyright + _screen->_backBuffer1.transBlitFrom(titleImages_SherlockHolmesTitle[2], copyrightPosition); + + _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... + Common::Point alleyPosition; + + if ((titleImages_SherlockHolmesTitle[3]._width == 105) && (titleImages_SherlockHolmesTitle[3]._height == 16)) { + // German + alleyPosition = Common::Point(72, 50); + } else if ((titleImages_SherlockHolmesTitle[3]._width == 166) && (titleImages_SherlockHolmesTitle[3]._height == 36)) { + // Spanish + alleyPosition = Common::Point(71, 50); + } else { + // English, width 175, height 38 + alleyPosition = Common::Point(72, 51); + } + _screen->transBlitFrom(titleImages_SherlockHolmesTitle[3], alleyPosition); + _screen->fadeIn(palette, 3); + + // Wait until the track got looped and the first few notes were played + finished = _music->waitUntilMSec(4300, 21300, 0, 2500); // ticks 0x104 / ticks 0x500 + } + } + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +bool ScalpelEngine::showAlleyCutscene() { + byte palette[PALETTE_SIZE]; + _music->loadSong("prolog2"); + + _animation->_gfxLibraryFilename = "TITLE.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + // Fade "In The Alley..." text to black + _screen->fadeToBlack(2); + + bool finished = _animation->play("27PRO1", true, 1, 3, true, 2); + if (finished) { + _screen->getPalette(palette); + _screen->fadeToBlack(2); + + // wait until second lower main note + finished = _music->waitUntilMSec(26800, 0xFFFFFFFF, 0, 1000); // ticks 0x64A + } + + if (finished) { + _screen->setPalette(palette); + finished = _animation->play("27PRO2", true, 1, 0, false, 2); + } + + if (finished) { + showLBV("scream.lbv"); + + // wait until first "scream" in music happened + finished = _music->waitUntilMSec(45800, 0xFFFFFFFF, 0, 6000); // ticks 0xABE + } + + if (finished) { + // quick fade out + _screen->fadeToBlack(1); + + // wait until after third "scream" in music happened + finished = _music->waitUntilMSec(49000, 0xFFFFFFFF, 0, 2000); // ticks 0xB80 + } + + if (finished) + finished = _animation->play("27PRO3", true, 1, 0, true, 2); + + if (finished) { + _screen->getPalette(palette); + _screen->fadeToBlack(2); + } + + if (finished) { + ImageFile titleImages_EarlyTheFollowingMorning("title3.vgs", true); + // "Early the following morning on Baker Street..." + Common::Point earlyTheFollowingMorningPosition; + + if ((titleImages_EarlyTheFollowingMorning[0]._width == 164) && (titleImages_EarlyTheFollowingMorning[0]._height == 19)) { + // German + earlyTheFollowingMorningPosition = Common::Point(35, 50); + } else if ((titleImages_EarlyTheFollowingMorning[0]._width == 171) && (titleImages_EarlyTheFollowingMorning[0]._height == 32)) { + // Spanish + earlyTheFollowingMorningPosition = Common::Point(35, 50); + } else { + // English, width 218, height 31 + earlyTheFollowingMorningPosition = Common::Point(35, 52); + } + + _screen->transBlitFrom(titleImages_EarlyTheFollowingMorning[0], earlyTheFollowingMorningPosition); + + // fast fade-in + _screen->fadeIn(palette, 1); + + // wait for music to end and wait an additional 2.5 seconds + finished = _music->waitUntilMSec(0xFFFFFFFF, 0xFFFFFFFF, 2500, 3000); + } + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +bool ScalpelEngine::showStreetCutscene() { + _animation->_gfxLibraryFilename = "TITLE.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + _music->loadSong("prolog3"); + + // wait a bit + bool finished = _events->delay(500); + + if (finished) { + // fade out "Early the following morning..." + _screen->fadeToBlack(2); + + // wait for music a bit + finished = _music->waitUntilMSec(3800, 0xFFFFFFFF, 0, 1000); // ticks 0xE4 + } + + if (finished) + finished = _animation->play("14KICK", true, 1, 3, true, 2); + + // Constable animation plays slower than speed 2 + // If we play it with speed 2, music gets obviously out of sync + if (finished) + finished = _animation->play("14NOTE", true, 1, 0, false, 3); + + // Fade to black + if (finished) + _screen->fadeToBlack(1); + + _animation->_gfxLibraryFilename = ""; + _animation->_soundLibraryFilename = ""; + return finished; +} + +bool ScalpelEngine::showOfficeCutscene() { + _music->loadSong("prolog4"); + _animation->_gfxLibraryFilename = "TITLE2.LIB"; + _animation->_soundLibraryFilename = "TITLE.SND"; + + bool finished = _animation->play("COFF1", true, 1, 3, true, 3); + if (finished) + finished = _animation->play("COFF2", true, 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); + + if (finished) { + _events->clearEvents(); + finished = _events->delay(500); + } + } + + if (finished) + finished = _animation->play("COFF3", true, 1, 0, true, 3); + + if (finished) + finished = _animation->play("COFF4", true, 1, 0, false, 3); + + if (finished) + finished = scrollCredits(); + + if (finished) + _screen->fadeToBlack(3); + + _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); + + // Demo fades slowly from the scene into credits palette + _screen->fadeIn(creditsImages._palette, 3); + + 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, _screen->w(), 10)); + _screen->blitFrom(_screen->_backBuffer1, Common::Point(0, _screen->h() - 10), + Common::Rect(0, _screen->h() - 10, _screen->w(), _screen->h())); + + _events->delay(100); + } + + return true; +} + +// 3DO variant +bool ScalpelEngine::show3DOSplash() { + // 3DO EA Splash screen + ImageFile3DO titleImage_3DOSplash("3DOSplash.cel", kImageFile3DOType_Cel); + + _screen->transBlitFrom(titleImage_3DOSplash[0]._frame, Common::Point(0, -20)); + bool finished = _events->delay(3000, true); + + if (finished) { + _screen->clear(); + finished = _events->delay(500, true); + } + + if (finished) { + // EA logo movie + Scalpel3DOMoviePlay("EAlogo.stream", Common::Point(20, 0)); + } + + // Always clear screen + _screen->clear(); + return finished; +} + +bool ScalpelEngine::showCityCutscene3DO() { + _animation->_soundLibraryFilename = "TITLE.SND"; + + _screen->clear(); + bool finished = _events->delay(2500, true); + + // rain.aiff seems to be playing in an endless loop until + // sherlock logo fades away TODO + + if (finished) { + finished = _events->delay(2500, true); + + // Play intro music + _music->loadSong("prolog"); + + // Fade screen to grey + _screen->_backBuffer1.fill(0xCE59); // RGB565: 25, 50, 25 (grey) + _screen->fadeIntoScreen3DO(2); + } + + if (finished) { + finished = _music->waitUntilMSec(3400, 0, 0, 3400); + } + + if (finished) { + _screen->_backBuffer1.fill(0); // fill backbuffer with black to avoid issues during fade from white + finished = _animation->play3DO("26open1", true, 1, true, 2); + } + + if (finished) { + _screen->_backBuffer1.blitFrom(*_screen); // save into backbuffer 1, used for fade + _screen->_backBuffer2.blitFrom(*_screen); // save into backbuffer 2, for restoring later + + // "London, England" + ImageFile3DO titleImage_London("title2a.cel", kImageFile3DOType_Cel); + _screen->_backBuffer1.transBlitFrom(titleImage_London[0]._frame, Common::Point(30, 50)); + + _screen->fadeIntoScreen3DO(1); + finished = _events->delay(1500, true); + + if (finished) { + // "November, 1888" + ImageFile3DO titleImage_November("title2b.cel", kImageFile3DOType_Cel); + _screen->_backBuffer1.transBlitFrom(titleImage_November[0]._frame, Common::Point(100, 100)); + + _screen->fadeIntoScreen3DO(1); + finished = _music->waitUntilMSec(14700, 0, 0, 5000); + } + + if (finished) { + // Restore screen + _screen->blitFrom(_screen->_backBuffer2); + } + } + + if (finished) + finished = _animation->play3DO("26open2", true, 1, false, 2); + + if (finished) { + _screen->_backBuffer1.blitFrom(*_screen); // save into backbuffer 1, used for fade + + // "Sherlock Holmes" (title) + ImageFile3DO titleImage_SherlockHolmesTitle("title1ab.cel", kImageFile3DOType_Cel); + _screen->_backBuffer1.transBlitFrom(titleImage_SherlockHolmesTitle[0]._frame, Common::Point(34, 5)); + + // Blend in + _screen->fadeIntoScreen3DO(2); + finished = _events->delay(500, true); + + // Title should fade in, Copyright should be displayed a bit after that + if (finished) { + ImageFile3DO titleImage_Copyright("title1c.cel", kImageFile3DOType_Cel); + + _screen->transBlitFrom(titleImage_Copyright[0]._frame, Common::Point(20, 190)); + finished = _events->delay(3500, true); + } + } + + if (finished) + finished = _music->waitUntilMSec(33600, 0, 0, 2000); + + if (finished) { + // Fade to black + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(3); + } + + if (finished) { + // "In the alley behind the Regency Theatre..." + ImageFile3DO titleImage_InTheAlley("title1d.cel", kImageFile3DOType_Cel); + _screen->_backBuffer1.transBlitFrom(titleImage_InTheAlley[0]._frame, Common::Point(72, 51)); + + // Fade in + _screen->fadeIntoScreen3DO(4); + finished = _music->waitUntilMSec(39900, 0, 0, 2500); + + // Fade out + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(4); + } + return finished; +} + +bool ScalpelEngine::showAlleyCutscene3DO() { + bool finished = _music->waitUntilMSec(43500, 0, 0, 1000); + + if (finished) + finished = _animation->play3DO("27PRO1", true, 1, false, 2); + + if (finished) { + // Fade out... + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(3); + + finished = _music->waitUntilMSec(67100, 0, 0, 1000); // 66700 + } + + if (finished) + finished = _animation->play3DO("27PRO2", true, 1, false, 2); + + if (finished) + finished = _music->waitUntilMSec(76000, 0, 0, 1000); + + if (finished) { + // Show screaming victim + ImageFile3DO titleImage_ScreamingVictim("scream.cel", kImageFile3DOType_Cel); + + _screen->clear(); + _screen->transBlitFrom(titleImage_ScreamingVictim[0]._frame, Common::Point(0, 0)); + + // Play "scream.aiff" + if (_sound->_voices) + _sound->playSound("prologue/sounds/scream.aiff", WAIT_RETURN_IMMEDIATELY, 100); + + finished = _music->waitUntilMSec(81600, 0, 0, 6000); + } + + if (finished) { + // Fade out + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(5); + + finished = _music->waitUntilMSec(84400, 0, 0, 2000); + } + + if (finished) + finished = _animation->play3DO("27PRO3", true, 1, false, 2); + + if (finished) { + // Fade out + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(5); + } + + if (finished) { + // "Early the following morning on Baker Street..." + ImageFile3DO titleImage_EarlyTheFollowingMorning("title3.cel", kImageFile3DOType_Cel); + _screen->_backBuffer1.transBlitFrom(titleImage_EarlyTheFollowingMorning[0]._frame, Common::Point(35, 51)); + + // Fade in + _screen->fadeIntoScreen3DO(4); + finished = _music->waitUntilMSec(96700, 0, 0, 3000); + } + + return finished; +} + +bool ScalpelEngine::showStreetCutscene3DO() { + bool finished = true; + + if (finished) { + // fade out "Early the following morning..." + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(4); + + // wait for music a bit + finished = _music->waitUntilMSec(100300, 0, 0, 1000); + } + + if (finished) + finished = _animation->play3DO("14KICK", true, 1, false, 2); + + // note: part of the constable is sticking to the door during the following + // animation, when he walks away. This is a bug of course, but it actually happened on 3DO! + // I'm not sure if it happens because the door is pure black (0, 0, 0) and it's because + // of transparency - or if the animation itself is bad. We will definitely have to adjust + // the animation data to fix it. + if (finished) + finished = _animation->play3DO("14NOTE", true, 1, false, 3); + + if (finished) { + // Fade out + _screen->_backBuffer1.clear(); + _screen->fadeIntoScreen3DO(4); + } + + return finished; +} + +bool ScalpelEngine::showOfficeCutscene3DO() { + bool finished = _music->waitUntilMSec(151000, 0, 0, 1000); + + if (finished) + finished = _animation->play3DO("COFF1", true, 1, false, 3); + + if (finished) + finished = _animation->play3DO("COFF2", true, 1, false, 3); + + if (finished) + finished = _music->waitUntilMSec(182400, 0, 0, 1000); + + if (finished) { + // Show the note + ImageFile3DO titleImage_CoffeeNote("note.cel", kImageFile3DOType_Cel); + + _screen->clear(); + _screen->transBlitFrom(titleImage_CoffeeNote[0]._frame, Common::Point(0, 0)); + + if (_sound->_voices) { + finished = _sound->playSound("prologue/sounds/note.aiff", WAIT_KBD_OR_FINISH); + } else + finished = _events->delay(19000); + + if (finished) + finished = _music->waitUntilMSec(218800, 0, 0, 1000); + + // Fade out + _screen->clear(); + } + + if (finished) + finished = _music->waitUntilMSec(222200, 0, 0, 1000); + + if (finished) + finished = _animation->play3DO("COFF3", true, 1, false, 3); + + if (finished) + finished = _animation->play3DO("COFF4", true, 1, false, 3); + + if (finished) { + finished = _music->waitUntilMSec(244500, 0, 0, 2000); + + // TODO: Brighten the image, possibly by doing a partial fade + // to white. + + _screen->_backBuffer1.blitFrom(*_screen); + + for (int nr = 1; finished && nr <= 4; nr++) { + char filename[15]; + sprintf(filename, "credits%d.cel", nr); + ImageFile3DO *creditsImage = new ImageFile3DO(filename, kImageFile3DOType_Cel); + ImageFrame *creditsFrame = &(*creditsImage)[0]; + for (int i = 0; finished && i < 200 + creditsFrame->_height; i++) { + _screen->blitFrom(_screen->_backBuffer1); + _screen->transBlitFrom(creditsFrame->_frame, Common::Point((320 - creditsFrame->_width) / 2, 200 - i)); + if (!_events->delay(70, true)) + finished = false; + } + delete creditsImage; + } + } + + return finished; +} + +void ScalpelEngine::loadInventory() { + ScalpelFixedText &fixedText = *(ScalpelFixedText *)_fixedText; + Inventory &inv = *_inventory; + + Common::String fixedText_Message = fixedText.getText(kFixedText_InitInventory_Message); + Common::String fixedText_HolmesCard = fixedText.getText(kFixedText_InitInventory_HolmesCard); + Common::String fixedText_Tickets = fixedText.getText(kFixedText_InitInventory_Tickets); + Common::String fixedText_CuffLink = fixedText.getText(kFixedText_InitInventory_CuffLink); + Common::String fixedText_WireHook = fixedText.getText(kFixedText_InitInventory_WireHook); + Common::String fixedText_Note = fixedText.getText(kFixedText_InitInventory_Note); + Common::String fixedText_OpenWatch = fixedText.getText(kFixedText_InitInventory_OpenWatch); + Common::String fixedText_Paper = fixedText.getText(kFixedText_InitInventory_Paper); + Common::String fixedText_Letter = fixedText.getText(kFixedText_InitInventory_Letter); + Common::String fixedText_Tarot = fixedText.getText(kFixedText_InitInventory_Tarot); + Common::String fixedText_OrnateKey = fixedText.getText(kFixedText_InitInventory_OrnateKey); + Common::String fixedText_PawnTicket = fixedText.getText(kFixedText_InitInventory_PawnTicket); + + // Initial inventory + inv._holdings = 2; + inv.push_back(InventoryItem(0, "Message", fixedText_Message, "_ITEM03A")); + inv.push_back(InventoryItem(0, "Holmes Card", fixedText_HolmesCard, "_ITEM07A")); + + // Hidden items + inv.push_back(InventoryItem(95, "Tickets", fixedText_Tickets, "_ITEM10A")); + inv.push_back(InventoryItem(138, "Cuff Link", fixedText_CuffLink, "_ITEM04A")); + inv.push_back(InventoryItem(138, "Wire Hook", fixedText_WireHook, "_ITEM06A")); + inv.push_back(InventoryItem(150, "Note", fixedText_Note, "_ITEM13A")); + inv.push_back(InventoryItem(481, "Open Watch", fixedText_OpenWatch, "_ITEM62A")); + inv.push_back(InventoryItem(481, "Paper", fixedText_Paper, "_ITEM44A")); + inv.push_back(InventoryItem(532, "Letter", fixedText_Letter, "_ITEM68A")); + inv.push_back(InventoryItem(544, "Tarot", fixedText_Tarot, "_ITEM71A")); + inv.push_back(InventoryItem(544, "Ornate Key", fixedText_OrnateKey, "_ITEM70A")); + inv.push_back(InventoryItem(586, "Pawn ticket", fixedText_PawnTicket, "_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 (_music->_musicOn && _music->loadSong(100)) + _music->startSong(); + + _scene->_goToScene = _map->show(); + + _music->freeSong(); + _people->_savedPos = Common::Point(-1, -1); + _people->_savedPos._facing = -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 (_music->_musicOn && _music->loadSong(_scene->_goToScene)) + _music->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", false, 1, 3, true, 4); + _animation->play("final2", false, 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", false, 1, 3, true, 4); + _animation->play("finalr2", false, 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", false, 1, 0, false, 4); + _animation->play("finale2", false, 1, 0, false, 4); + _animation->play("finale3", false, 1, 0, false, 4); + + _useEpilogue2 = true; + _animation->play("finale4", false, 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", false, 1, 3, true, 4); + _animation->play("SUBWAY2", false, 1, 0, false, 4); + _animation->play("SUBWAY3", false, 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", false, 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 + _music->freeSong(); + break; + + case EXIT_GAME: + // Exit game + _screen->fadeToBlack(3); + quitGame(); + return; + + default: + break; + } + + _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)[HOLMES]._position.x / FIXED_INT_MULTIPLIER, (*_people)[HOLMES]._position.y / FIXED_INT_MULTIPLIER); + + // 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; + Person &player = people[HOLMES]; + + Common::Point pt((*_people)[HOLMES]._position.x / FIXED_INT_MULTIPLIER, (*_people)[HOLMES]._position.y / FIXED_INT_MULTIPLIER); + int frameNum = player._walkSequences[player._sequenceNumber][player._frameNumber] + + player._walkSequences[player._sequenceNumber][0] - 2; + + switch ((*_people)[HOLMES]._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[HOLMES]._images)[frameNum]; + + // Draw the mirror image of Holmes + bool flipped = people[HOLMES]._sequenceNumber == WALK_LEFT || people[HOLMES]._sequenceNumber == STOP_LEFT + || people[HOLMES]._sequenceNumber == WALK_UPRIGHT || people[HOLMES]._sequenceNumber == STOP_UPRIGHT + || people[HOLMES]._sequenceNumber == WALK_DOWNLEFT || people[HOLMES]._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)[HOLMES]._position.x / FIXED_INT_MULTIPLIER, (*_people)[HOLMES]._position.y / FIXED_INT_MULTIPLIER); + + // 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 Sherlock diff --git a/engines/sherlock/scalpel/scalpel.h b/engines/sherlock/scalpel/scalpel.h new file mode 100644 index 0000000000..d2524ccbc7 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel.h @@ -0,0 +1,133 @@ +/* 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 { + BUTTON_TOP = 233, + BUTTON_MIDDLE = 244, + BUTTON_BOTTOM = 248, + COMMAND_FOREGROUND = 15, + COMMAND_HIGHLIGHTED = 10, + COMMAND_NULL = 248, + INFO_FOREGROUND = 11, + INFO_BACKGROUND = 1, + INV_FOREGROUND = 14, + INV_BACKGROUND = 1, + PEN_COLOR = 250 +}; + +class ScalpelEngine : public SherlockEngine { +private: + Darts *_darts; + int _mapResult; + + bool show3DOSplash(); + + /** + * Show the starting city cutscene which shows the game title + */ + bool showCityCutscene(); + bool showCityCutscene3DO(); + + /** + * Show the back alley where the initial murder takes place + */ + bool showAlleyCutscene(); + bool showAlleyCutscene3DO(); + + /** + * Show the Baker Street outside cutscene + */ + bool showStreetCutscene(); + bool showStreetCutscene3DO(); + + /** + * Show Holmes and Watson at the breakfast table, lestrade's note, and then the scrolling credits + */ + bool showOfficeCutscene(); + bool showOfficeCutscene3DO(); + + /** + * 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/scalpel/scalpel_debugger.cpp b/engines/sherlock/scalpel/scalpel_debugger.cpp new file mode 100644 index 0000000000..7f5e1efa69 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_debugger.cpp @@ -0,0 +1,91 @@ +/* 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_debugger.h" +#include "sherlock/sherlock.h" +#include "audio/mixer.h" +#include "audio/decoders/3do.h" +#include "audio/decoders/aiff.h" +#include "audio/decoders/wave.h" + +namespace Sherlock { + +namespace Scalpel { + +ScalpelDebugger::ScalpelDebugger(SherlockEngine *vm) : Debugger(vm) { + registerCmd("3do_playmovie", WRAP_METHOD(ScalpelDebugger, cmd3DO_PlayMovie)); + registerCmd("3do_playaudio", WRAP_METHOD(ScalpelDebugger, cmd3DO_PlayAudio)); +} + +bool ScalpelDebugger::cmd3DO_PlayMovie(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: 3do_playmovie <3do-movie-file>\n"); + return true; + } + + // play gets postboned until debugger is closed + Common::String filename = argv[1]; + _3doPlayMovieFile = filename; + + return cmdExit(0, 0); +} + +bool ScalpelDebugger::cmd3DO_PlayAudio(int argc, const char **argv) { + if (argc != 2) { + debugPrintf("Format: 3do_playaudio <3do-audio-file>\n"); + return true; + } + + Common::File *file = new Common::File(); + if (!file->open(argv[1])) { + debugPrintf("can not open specified audio file\n"); + return true; + } + + Audio::AudioStream *testStream; + Audio::SoundHandle testHandle; + + // Try to load the given file as AIFF/AIFC + testStream = Audio::makeAIFFStream(file, DisposeAfterUse::YES); + + if (testStream) { + g_system->getMixer()->playStream(Audio::Mixer::kPlainSoundType, &testHandle, testStream); + _vm->_events->clearEvents(); + + while ((!_vm->shouldQuit()) && g_system->getMixer()->isSoundHandleActive(testHandle)) { + _vm->_events->pollEvents(); + g_system->delayMillis(10); + if (_vm->_events->kbHit()) { + break; + } + } + + debugPrintf("playing completed\n"); + g_system->getMixer()->stopHandle(testHandle); + } + + return true; +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_debugger.h b/engines/sherlock/scalpel/scalpel_debugger.h new file mode 100644 index 0000000000..17a84779f0 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_debugger.h @@ -0,0 +1,54 @@ +/* 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_DEBUGGER_H +#define SHERLOCK_SCALPEL_DEBUGGER_H + +#include "sherlock/debugger.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Scalpel { + +class ScalpelDebugger : public Debugger { +private: + /** + * Plays a 3DO movie + */ + bool cmd3DO_PlayMovie(int argc, const char **argv); + + /** + * Plays a 3DO audio + */ + bool cmd3DO_PlayAudio(int argc, const char **argv); +public: + ScalpelDebugger(SherlockEngine *vm); + virtual ~ScalpelDebugger() {} +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif /* SHERLOCK_DEBUGGER_H */ diff --git a/engines/sherlock/scalpel/scalpel_fixed_text.cpp b/engines/sherlock/scalpel/scalpel_fixed_text.cpp new file mode 100644 index 0000000000..63f84d68c6 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_fixed_text.cpp @@ -0,0 +1,377 @@ +/* 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_fixed_text.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Scalpel { + +static const char *const fixedTextEN[] = { + // SH1: Window buttons + "Exit", + "Up", + "Down", + // SH1: Inventory buttons + "Exit", + "Look", + "Use", + "Give", + // SH1: Journal text + "Watson's Journal", + "Page %d", + // SH1: Journal buttons + "Exit", + "Back 10", + "Up", + "Down", + "Ahead 10", + "Search", + "First Page", + "Last Page", + "Print Text", + // SH1: Journal search + "Exit", + "Backward", + "Forward", + "Text Not Found !", + // SH1: Initial Inventory + "A message requesting help", + "A number of business cards", + "Opera Tickets", + "Cuff Link", + "Wire Hook", + "Note", + "An open pocket watch", + "A piece of paper with numbers on it", + "A letter folded many times", + "Tarot Cards", + "An ornate key", + "A pawn ticket", + // SH2: Verbs + "Open", + "Look", + "Talk", + "Journal" +}; + +// sharp-s : 0xE1 / octal 341 +// small a-umlaut: 0x84 / octal 204 +// small o-umlaut: 0x94 / octal 224 +// small u-umlaut: 0x81 / octal 201 +static const char *const fixedTextDE[] = { + // SH1: Window buttons + "Zur\201ck", + "Hoch", + "Runter", + // SH1: Inventory buttons + "Zur\201ck", + "Schau", + "Benutze", + "Gib", + // SH1: Journal text + "Watsons Tagebuch", + "Seite %d", + // SH1: Journal buttons + "Zur\201ck", + "10 hoch", + "Hoch", + "Runter", + "10 runter", + "Suche", + "Erste Seite", + "Letzte Seite", + "Drucke Text", + // SH1: Journal search + "Zur\201ck", + "R\201ckw\204rts", // original: "Backward" + "Vorw\204rts", // original: "Forward" + "Text nicht gefunden!", + // SH1: Initial Inventory + "Ein Hilferuf von Lestrade", + "Holmes' Visitenkarten", + "Karten f\201rs Opernhaus", + "Manschettenkn\224pfe", + "Zum Haken verbogener Drahtkorb", + "Mitteilung am Epstein", + "Eine offene Taschenuhr", + "Ein Zettel mit Zahlen drauf", + "Ein mehrfach gefalteter Briefbogen", + "Ein Tarock-Kartenspiel", // [sic] + "Ein verzierter Schl\201ssel", + "Ein Pfandschein", + // SH2: Verbs + "\231ffne", + "Schau", + "Rede", + "Tagebuch" +}; + +// up-side down exclamation mark - 0xAD / octal 255 +// up-side down question mark - 0xA8 / octal 250 +// n with a wave on top - 0xA4 / octal 244 +static const char *const fixedTextES[] = { + // SH1: Window buttons + "Exit", + "Subir", + "Bajar", + // SH1: Inventory buttons + "Exit", + "Mirar", + "Usar", + "Dar", + // SH1: Journal text + "Diario de Watson", + "Pagina %d", + // SH1: Journal buttons + "Exit", + "Retroceder", + "Subir", + "baJar", + "Adelante", + "Buscar", + "1a pagina", + "Ult pagina", + "Imprimir", + // SH1: Journal search + "Exit", + "Retroceder", + "Avanzar", + "Texto no encontrado!", + // SH1: Initial Inventory + "Un mensaje solicitando ayuda", + "Unas cuantas tarjetas de visita", + "Entradas para la opera", + "Unos gemelos", + "Un gancho de alambre", + "Una nota", + "Un reloj de bolsillo abierto", + "Un trozo de papel con unos numeros", + "Un carta muy plegada", + "Unas cartas de Tarot", + "Una llave muy vistosa", + "Una papeleta de empe\244o", +}; + +// ========================================= + +// === Sherlock Holmes 1: Serrated Scalpel === +static const char *const fixedTextEN_ActionOpen[] = { + "This cannot be opened", + "It is already open", + "It is locked", + "Wait for Watson", + " ", + "." +}; + +static const char *const fixedTextDE_ActionOpen[] = { + "Das kann man nicht \224ffnen", + "Ist doch schon offen!", + "Leider verschlossen", + "Warte auf Watson", + " ", + "." +}; + +static const char *const fixedTextES_ActionOpen[] = { + "No puede ser abierto", + "Ya esta abierto", + "Esta cerrado", + "Espera a Watson", + " ", + "." +}; + +static const char *const fixedTextEN_ActionClose[] = { + "This cannot be closed", + "It is already closed", + "The safe door is in the way" +}; + +static const char *const fixedTextDE_ActionClose[] = { + "Das kann man nicht schlie\341en", + "Ist doch schon zu!", + "Die safet\201r ist Weg" +}; + +static const char *const fixedTextES_ActionClose[] = { + "No puede ser cerrado", + "Ya esta cerrado", + "La puerta de seguridad esta entre medias" +}; + +static const char *const fixedTextEN_ActionMove[] = { + "This cannot be moved", + "It is bolted to the floor", + "It is too heavy", + "The other crate is in the way" +}; + + +static const char *const fixedTextDE_ActionMove[] = { + "L\204\341t sich nicht bewegen", + "Festged\201belt in der Erde...", + "Oha, VIEL zu schwer", + "Der andere Kiste ist im Weg" // [sic] +}; + +static const char *const fixedTextES_ActionMove[] = { + "No puede moverse", + "Esta sujeto a la pared", + "Es demasiado pesado", + "El otro cajon esta en mitad" +}; + +static const char *const fixedTextEN_ActionPick[] = { + "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" +}; + +static const char *const fixedTextDE_ActionPick[] = { + "Nichts Interessantes da", + "Zu gut befestigt", + "Ist ja wohl ein bi\341chen zu gro\341, oder ?", + "Oha, VIEL zu schwer", + "Ich denke, Du stehst mehr auf M\204dchen ?", + "Diese Blumen geh\224ren Penny", + "Sie ist doch viel zu jung f\201r Dich!", + "Ich denke, Du stehst mehr auf M\204dchen ?", + "Staatseigentum - Nur f\201r den Dienstgebrauch !" +}; + +static const char *const fixedTextES_ActionPick[] = { + "No hay nada interesante", + "Esta anclado al suelo", + "Es muy grande para llevarlo", + "Pesa demasiado", + "Creo que una chica sera mas tu tipo", + "Esas flores pertenecen a Penny", + "\255Es demasiado joven para ti!", + "\255Creo que una chica sera mas tu tipo!", + "Propiedad del gobierno para uso oficial" +}; + +static const char *const fixedTextEN_ActionUse[] = { + "You can't do that", + "It had no effect", + "You can't reach it", + "OK, the door looks bigger! Happy?", + "Doors don't smoke" +}; + +static const char *const fixedTextDE_ActionUse[] = { + "Nein, das geht wirklich nicht", + "Tja keinerlei Wirkung", + "Da kommst du nicht dran", + "Na gut, die T\201r sieht jetzt gr\224\341er aus. Zufrieden?", + "T\201ren sind Nichtraucher!" +}; + +static const char *const fixedTextES_ActionUse[] = { + "No puedes hacerlo", + "No tuvo ningun efecto", + "No puedes alcanzarlo", + "Bien, \255es enorme! \250Feliz?", + "Las puertas no fuman" +}; + +#define FIXEDTEXT_GETCOUNT(_name_) sizeof(_name_) / sizeof(byte *) +#define FIXEDTEXT_ENTRY(_name_) _name_, FIXEDTEXT_GETCOUNT(_name_) + +static const FixedTextActionEntry fixedTextEN_Actions[] = { + { FIXEDTEXT_ENTRY(fixedTextEN_ActionOpen) }, + { FIXEDTEXT_ENTRY(fixedTextEN_ActionClose) }, + { FIXEDTEXT_ENTRY(fixedTextEN_ActionMove) }, + { FIXEDTEXT_ENTRY(fixedTextEN_ActionPick) }, + { FIXEDTEXT_ENTRY(fixedTextEN_ActionUse) } +}; + +static const FixedTextActionEntry fixedTextDE_Actions[] = { + { FIXEDTEXT_ENTRY(fixedTextDE_ActionOpen) }, + { FIXEDTEXT_ENTRY(fixedTextDE_ActionClose) }, + { FIXEDTEXT_ENTRY(fixedTextDE_ActionMove) }, + { FIXEDTEXT_ENTRY(fixedTextDE_ActionPick) }, + { FIXEDTEXT_ENTRY(fixedTextDE_ActionUse) } +}; + +static const FixedTextActionEntry fixedTextES_Actions[] = { + { FIXEDTEXT_ENTRY(fixedTextES_ActionOpen) }, + { FIXEDTEXT_ENTRY(fixedTextES_ActionClose) }, + { FIXEDTEXT_ENTRY(fixedTextES_ActionMove) }, + { FIXEDTEXT_ENTRY(fixedTextES_ActionPick) }, + { FIXEDTEXT_ENTRY(fixedTextES_ActionUse) } +}; + +// ========================================= + +// TODO: +// It seems there was a French version of Sherlock Holmes 2 +static const FixedTextLanguageEntry fixedTextLanguages[] = { + { Common::DE_DEU, fixedTextDE, fixedTextDE_Actions }, + { Common::ES_ESP, fixedTextES, fixedTextES_Actions }, + { Common::EN_ANY, fixedTextEN, fixedTextEN_Actions }, + { Common::UNK_LANG, fixedTextEN, fixedTextEN_Actions } +}; + +// ========================================= + +// ========================================= + +ScalpelFixedText::ScalpelFixedText(SherlockEngine *vm) : FixedText(vm) { + // Figure out which fixed texts to use + Common::Language curLanguage = _vm->getLanguage(); + + const FixedTextLanguageEntry *curLanguageEntry = fixedTextLanguages; + + while (curLanguageEntry->language != Common::UNK_LANG) { + if (curLanguageEntry->language == curLanguage) + break; // found current language + curLanguageEntry++; + } + _curLanguageEntry = curLanguageEntry; +} + +const char *ScalpelFixedText::getText(int fixedTextId) { + return _curLanguageEntry->fixedTextArray[fixedTextId]; +} + +const Common::String ScalpelFixedText::getActionMessage(FixedTextActionId actionId, int messageIndex) { + assert(actionId >= 0); + assert(messageIndex >= 0); + const FixedTextActionEntry *curActionEntry = &_curLanguageEntry->actionArray[actionId]; + + assert(messageIndex < curActionEntry->fixedTextArrayCount); + return Common::String(curActionEntry->fixedTextArray[messageIndex]); +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_fixed_text.h b/engines/sherlock/scalpel/scalpel_fixed_text.h new file mode 100644 index 0000000000..eae86b8f27 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_fixed_text.h @@ -0,0 +1,108 @@ +/* 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_FIXED_TEXT_H +#define SHERLOCK_SCALPEL_FIXED_TEXT_H + +#include "sherlock/fixed_text.h" + +namespace Sherlock { + +namespace Scalpel { + +enum FixedTextId { + // Window buttons + kFixedText_Window_Exit = 0, + kFixedText_Window_Up, + kFixedText_Window_Down, + // Inventory buttons + kFixedText_Inventory_Exit, + kFixedText_Inventory_Look, + kFixedText_Inventory_Use, + kFixedText_Inventory_Give, + // Journal text + kFixedText_Journal_WatsonsJournal, + kFixedText_Journal_Page, + // Journal buttons + kFixedText_Journal_Exit, + kFixedText_Journal_Back10, + kFixedText_Journal_Up, + kFixedText_Journal_Down, + kFixedText_Journal_Ahead10, + kFixedText_Journal_Search, + kFixedText_Journal_FirstPage, + kFixedText_Journal_LastPage, + kFixedText_Journal_PrintText, + // Journal search + kFixedText_JournalSearch_Exit, + kFixedText_JournalSearch_Backward, + kFixedText_JournalSearch_Forward, + kFixedText_JournalSearch_NotFound, + // Initial inventory + kFixedText_InitInventory_Message, + kFixedText_InitInventory_HolmesCard, + kFixedText_InitInventory_Tickets, + kFixedText_InitInventory_CuffLink, + kFixedText_InitInventory_WireHook, + kFixedText_InitInventory_Note, + kFixedText_InitInventory_OpenWatch, + kFixedText_InitInventory_Paper, + kFixedText_InitInventory_Letter, + kFixedText_InitInventory_Tarot, + kFixedText_InitInventory_OrnateKey, + kFixedText_InitInventory_PawnTicket +}; + +struct FixedTextActionEntry { + const char *const *fixedTextArray; + int fixedTextArrayCount; +}; + +struct FixedTextLanguageEntry { + Common::Language language; + const char *const *fixedTextArray; + const FixedTextActionEntry *actionArray; +}; + +class ScalpelFixedText: public FixedText { +private: + const FixedTextLanguageEntry *_curLanguageEntry; +public: + ScalpelFixedText(SherlockEngine *vm); + virtual ~ScalpelFixedText() {} + + /** + * Gets text + */ + virtual const char *getText(int fixedTextId); + + /** + * Get action message + */ + virtual const Common::String getActionMessage(FixedTextActionId actionId, int messageIndex); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_inventory.cpp b/engines/sherlock/scalpel/scalpel_inventory.cpp new file mode 100644 index 0000000000..e19a43238c --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_inventory.cpp @@ -0,0 +1,296 @@ +/* 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_inventory.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +namespace Scalpel { + +ScalpelInventory::ScalpelInventory(SherlockEngine *vm) : Inventory(vm) { + _invShapes.resize(6); +} + +ScalpelInventory::~ScalpelInventory() { +} + +void ScalpelInventory::drawInventory(InvNewMode mode) { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_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 + Common::String fixedText_Exit = fixedText.getText(kFixedText_Inventory_Exit); + Common::String fixedText_Look = fixedText.getText(kFixedText_Inventory_Look); + Common::String fixedText_Use = fixedText.getText(kFixedText_Inventory_Use); + Common::String fixedText_Give = fixedText.getText(kFixedText_Inventory_Give); + + screen.makeButton(Common::Rect(INVENTORY_POINTS[0][0], CONTROLS_Y1, INVENTORY_POINTS[0][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[0][2] - screen.stringWidth(fixedText_Exit) / 2, fixedText_Exit); + screen.makeButton(Common::Rect(INVENTORY_POINTS[1][0], CONTROLS_Y1, INVENTORY_POINTS[1][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[1][2] - screen.stringWidth(fixedText_Look) / 2, fixedText_Look); + screen.makeButton(Common::Rect(INVENTORY_POINTS[2][0], CONTROLS_Y1, INVENTORY_POINTS[2][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[2][2] - screen.stringWidth(fixedText_Use) / 2, fixedText_Use); + screen.makeButton(Common::Rect(INVENTORY_POINTS[3][0], CONTROLS_Y1, INVENTORY_POINTS[3][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[3][2] - screen.stringWidth(fixedText_Give) / 2, fixedText_Give); + screen.makeButton(Common::Rect(INVENTORY_POINTS[4][0], CONTROLS_Y1, INVENTORY_POINTS[4][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[4][2], "^^"); // 2 arrows pointing to the left + screen.makeButton(Common::Rect(INVENTORY_POINTS[5][0], CONTROLS_Y1, INVENTORY_POINTS[5][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[5][2], "^"); // 1 arrow pointing to the left + screen.makeButton(Common::Rect(INVENTORY_POINTS[6][0], CONTROLS_Y1, INVENTORY_POINTS[6][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[6][2], "_"); // 1 arrow pointing to the right + screen.makeButton(Common::Rect(INVENTORY_POINTS[7][0], CONTROLS_Y1, INVENTORY_POINTS[7][1], + CONTROLS_Y1 + 10), INVENTORY_POINTS[7][2], "__"); // 2 arrows pointing to the right + + if (tempMode == INVENTORY_DONT_DISPLAY) + mode = LOOK_INVENTORY_MODE; + _invMode = (InvMode)((int)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; + } + + assert(IS_SERRATED_SCALPEL); + ((ScalpelUserInterface *)_vm->_ui)->_oldUse = -1; +} + +void ScalpelInventory::invCommands(bool slamIt) { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + UserInterface &ui = *_vm->_ui; + + Common::String fixedText_Exit = fixedText.getText(kFixedText_Inventory_Exit); + Common::String fixedText_Look = fixedText.getText(kFixedText_Inventory_Look); + Common::String fixedText_Use = fixedText.getText(kFixedText_Inventory_Use); + Common::String fixedText_Give = fixedText.getText(kFixedText_Inventory_Give); + + if (slamIt) { + screen.buttonPrint(Common::Point(INVENTORY_POINTS[0][2], CONTROLS_Y1), + _invMode == INVMODE_EXIT ? COMMAND_HIGHLIGHTED :COMMAND_FOREGROUND, + true, fixedText_Exit); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), + _invMode == INVMODE_LOOK ? COMMAND_HIGHLIGHTED :COMMAND_FOREGROUND, + true, fixedText_Look); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), + _invMode == INVMODE_USE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + true, fixedText_Use); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), + _invMode == INVMODE_GIVE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + true, fixedText_Give); + screen.print(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1 + 1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^^"); // 2 arrows pointing to the left + screen.print(Common::Point(INVENTORY_POINTS[5][2], CONTROLS_Y1 + 1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^"); // 2 arrows pointing to the left + screen.print(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1 + 1), + (_holdings - _invIndex <= 6) ? COMMAND_NULL : COMMAND_FOREGROUND, + "_"); // 1 arrow pointing to the right + screen.print(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1 + 1), + (_holdings - _invIndex <= 6) ? COMMAND_NULL : COMMAND_FOREGROUND, + "__"); // 2 arrows pointing to the right + 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, fixedText_Exit); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), + _invMode == INVMODE_LOOK ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, fixedText_Look); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), + _invMode == INVMODE_USE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, fixedText_Use); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), + _invMode == INVMODE_GIVE ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND, + false, fixedText_Give); + screen.gPrint(Common::Point(INVENTORY_POINTS[4][2], CONTROLS_Y1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^^"); // 2 arrows pointing to the left + screen.gPrint(Common::Point(INVENTORY_POINTS[5][2], CONTROLS_Y1), + _invIndex == 0 ? COMMAND_NULL : COMMAND_FOREGROUND, + "^"); // 1 arrow pointing to the left + screen.gPrint(Common::Point(INVENTORY_POINTS[6][2], CONTROLS_Y1), + (_holdings - _invIndex < 7) ? COMMAND_NULL : COMMAND_FOREGROUND, + "_"); // 1 arrow pointing to the right + screen.gPrint(Common::Point(INVENTORY_POINTS[7][2], CONTROLS_Y1), + (_holdings - _invIndex < 7) ? COMMAND_NULL : COMMAND_FOREGROUND, + "__"); // 2 arrows pointing to the right + } +} + +void ScalpelInventory::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 ScalpelInventory::refreshInv() { + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + ScalpelUserInterface &ui = *(ScalpelUserInterface *)_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(); + } +} + +void ScalpelInventory::putInv(InvSlamMode slamIt) { + ScalpelScreen &screen = *(ScalpelScreen *)_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 - (int)_invShapes.size())) { + --_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) < (int)_invShapes.size(); ++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, BUTTON_BACKGROUND); + } + 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 ScalpelInventory::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(); +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_inventory.h b/engines/sherlock/scalpel/scalpel_inventory.h new file mode 100644 index 0000000000..afafb0b94a --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_inventory.h @@ -0,0 +1,74 @@ +/* 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_INVENTORY_H +#define SHERLOCK_SCALPEL_INVENTORY_H + +#include "sherlock/inventory.h" + +namespace Sherlock { + +namespace Scalpel { + +class ScalpelInventory : public Inventory { +public: + ScalpelInventory(SherlockEngine *vm); + ~ScalpelInventory(); + + /** + * 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(); + + /** + * Display the character's inventory. The slamIt parameter specifies: + */ + void putInv(InvSlamMode slamIt); + + /** + * Load the list of names the inventory items correspond to, if not already loaded, + * and then calls loadGraphics to load the associated graphics + */ + virtual void loadInv(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_journal.cpp b/engines/sherlock/scalpel/scalpel_journal.cpp new file mode 100644 index 0000000000..8e356c0f65 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_journal.cpp @@ -0,0 +1,667 @@ +/* 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" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_journal.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/tattoo/tattoo_journal.h" + +namespace Sherlock { + +namespace Scalpel { + +#define JOURNAL_BUTTONS_Y 178 +#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 } +}; + +/*----------------------------------------------------------------*/ + +ScalpelJournal::ScalpelJournal(SherlockEngine *vm) : Journal(vm) { + // Initialize fields + _maxPage = 0; + _index = 0; + _sub = 0; + _up = _down = false; + _page = 1; + + if (_vm->_interactiveFl) { + // Load the journal directory and location names + loadLocations(); + } +} + +void ScalpelJournal::record(int converseNum, int statementNum, bool replyOnly) { + int saveIndex = _index; + int saveSub = _sub; + + if (IS_3DO) { + // there seems to be no journal in the 3DO version + return; + } + + // 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 ScalpelJournal::loadLocations() { + Resources &res = *_vm->_res; + + _directory.clear(); + _locations.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; + + if (IS_3DO) { + // 3DO: storage of locations is currently unknown TODO + return; + } + + // Load in the locations stored in journal.txt + Common::SeekableReadStream *loc = res.load("journal.txt"); + + while (loc->pos() < loc->size()) { + Common::String line; + char c; + while ((c = loc->readByte()) != 0) + line += c; + + _locations.push_back(line); + } + + delete loc; +} + +void ScalpelJournal::drawFrame() { + FixedText &fixedText = *_vm->_fixedText; + Resources &res = *_vm->_res; + ScalpelScreen &screen = *(ScalpelScreen *)_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]); + + Common::String fixedText_WatsonsJournal = fixedText.getText(kFixedText_Journal_WatsonsJournal); + Common::String fixedText_Exit = fixedText.getText(kFixedText_Journal_Exit); + Common::String fixedText_Back10 = fixedText.getText(kFixedText_Journal_Back10); + Common::String fixedText_Up = fixedText.getText(kFixedText_Journal_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Journal_Down); + Common::String fixedText_Ahead10 = fixedText.getText(kFixedText_Journal_Ahead10); + Common::String fixedText_Search = fixedText.getText(kFixedText_Journal_Search); + Common::String fixedText_FirstPage = fixedText.getText(kFixedText_Journal_FirstPage); + Common::String fixedText_LastPage = fixedText.getText(kFixedText_Journal_LastPage); + Common::String fixedText_PrintText = fixedText.getText(kFixedText_Journal_PrintText); + + // Set the palette and print the title + screen.setPalette(palette); + screen.gPrint(Common::Point(111, 18), BUTTON_BOTTOM, "%s", fixedText_WatsonsJournal.c_str()); + screen.gPrint(Common::Point(110, 17), INV_FOREGROUND, "%s", fixedText_WatsonsJournal.c_str()); + + // 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(fixedText_Exit) / 2, fixedText_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(fixedText_Back10) / 2, fixedText_Back10); + 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(fixedText_Up) / 2, fixedText_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(fixedText_Down) / 2, fixedText_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(fixedText_Ahead10) / 2, fixedText_Ahead10); + 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(fixedText_Search) / 2, fixedText_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(fixedText_FirstPage) / 2, fixedText_FirstPage); + 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(fixedText_LastPage) / 2, fixedText_LastPage); + + // 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(fixedText_PrintText) / 2, fixedText_PrintText); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), + COMMAND_NULL, false, fixedText_PrintText); +} + +void ScalpelJournal::drawInterface() { + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + + drawFrame(); + + 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 ScalpelJournal::doArrows() { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + byte color; + + Common::String fixedText_Back10 = fixedText.getText(kFixedText_Journal_Back10); + Common::String fixedText_Up = fixedText.getText(kFixedText_Journal_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Journal_Down); + Common::String fixedText_Ahead10 = fixedText.getText(kFixedText_Journal_Ahead10); + Common::String fixedText_Search = fixedText.getText(kFixedText_Journal_Search); + Common::String fixedText_FirstPage = fixedText.getText(kFixedText_Journal_FirstPage); + Common::String fixedText_LastPage = fixedText.getText(kFixedText_Journal_LastPage); + Common::String fixedText_PrintText = fixedText.getText(kFixedText_Journal_PrintText); + + color = (_page > 1) ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), color, false, fixedText_Back10); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), color, false, fixedText_Up); + + color = _down ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), color, false, fixedText_Down); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), color, false, fixedText_Ahead10); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[7][2], JOURNAL_BUTTONS_Y + 11), color, false, fixedText_LastPage); + + color = _journal.size() > 0 ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[5][2], JOURNAL_BUTTONS_Y + 11), color, false, fixedText_Search); + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), COMMAND_NULL, false, fixedText_PrintText); + + color = _page > 1 ? COMMAND_FOREGROUND : COMMAND_NULL; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[6][2], JOURNAL_BUTTONS_Y + 11), color, false, fixedText_FirstPage); +} + +JournalButton ScalpelJournal::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 ScalpelJournal::handleEvents(int key) { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + bool doneFlag = false; + + Common::Point pt = events.mousePos(); + JournalButton btn = getHighlightedButton(pt); + byte color; + + if (events._pressed || events._released) { + Common::String fixedText_Exit = fixedText.getText(kFixedText_Journal_Exit); + Common::String fixedText_Back10 = fixedText.getText(kFixedText_Journal_Back10); + Common::String fixedText_Up = fixedText.getText(kFixedText_Journal_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Journal_Down); + Common::String fixedText_Ahead10 = fixedText.getText(kFixedText_Journal_Ahead10); + Common::String fixedText_Search = fixedText.getText(kFixedText_Journal_Search); + Common::String fixedText_FirstPage = fixedText.getText(kFixedText_Journal_FirstPage); + Common::String fixedText_LastPage = fixedText.getText(kFixedText_Journal_LastPage); + Common::String fixedText_PrintText = fixedText.getText(kFixedText_Journal_PrintText); + + // Exit button + color = (btn == BTN_EXIT) ? COMMAND_HIGHLIGHTED : COMMAND_FOREGROUND; + screen.buttonPrint(Common::Point(JOURNAL_POINTS[0][2], JOURNAL_BUTTONS_Y), color, true, fixedText_Exit); + + // Back 10 button + if (btn == BTN_BACK10) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, fixedText_Back10); + } else if (_page > 1) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[1][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, fixedText_Back10); + } + + // Up button + if (btn == BTN_UP) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, fixedText_Up); + } else if (_up) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[2][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, fixedText_Up); + } + + // Down button + if (btn == BTN_DOWN) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, fixedText_Down); + } else if (_down) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[3][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, fixedText_Down); + } + + // Ahead 10 button + if (btn == BTN_AHEAD110) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), COMMAND_HIGHLIGHTED, true, fixedText_Ahead10); + } else if (_down) { + screen.buttonPrint(Common::Point(JOURNAL_POINTS[4][2], JOURNAL_BUTTONS_Y), COMMAND_FOREGROUND, true, fixedText_Ahead10); + } + + // 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, fixedText_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, fixedText_FirstPage); + + // 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, fixedText_LastPage); + + // Print Text button + screen.buttonPrint(Common::Point(JOURNAL_POINTS[8][2], JOURNAL_BUTTONS_Y + 11), COMMAND_NULL, true, fixedText_PrintText); + } + + 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; + + drawFrame(); + 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; + + drawFrame(); + 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 ScalpelJournal::getSearchString(bool printError) { + enum Button { BTN_NONE, BTN_EXIT, BTN_BACKWARD, BTN_FORWARD }; + + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + Talk &talk = *_vm->_talk; + int xp; + int yp = 174; + bool flag = false; + Common::String name; + int done = 0; + byte color; + + Common::String fixedText_Exit = fixedText.getText(kFixedText_JournalSearch_Exit); + Common::String fixedText_Backward = fixedText.getText(kFixedText_JournalSearch_Backward); + Common::String fixedText_Forward = fixedText.getText(kFixedText_JournalSearch_Forward); + Common::String fixedText_NotFound = fixedText.getText(kFixedText_JournalSearch_NotFound); + + // 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(fixedText_Exit) / 2, fixedText_Exit); + screen.makeButton(Common::Rect(SEARCH_POINTS[1][0], yp, SEARCH_POINTS[1][1], yp + 10), + SEARCH_POINTS[1][2] - screen.stringWidth(fixedText_Backward) / 2, fixedText_Backward); + screen.makeButton(Common::Rect(SEARCH_POINTS[2][0], yp, SEARCH_POINTS[2][1], yp + 10), + SEARCH_POINTS[2][2] - screen.stringWidth(fixedText_Forward) / 2, fixedText_Forward); + + screen.gPrint(Common::Point(SEARCH_POINTS[0][2] - screen.stringWidth(fixedText_Exit) / 2, yp), + COMMAND_HIGHLIGHTED, "%c", fixedText_Exit[0]); + screen.gPrint(Common::Point(SEARCH_POINTS[1][2] - screen.stringWidth(fixedText_Backward) / 2, yp), + COMMAND_HIGHLIGHTED, "%c", fixedText_Backward[0]); + screen.gPrint(Common::Point(SEARCH_POINTS[2][2] - screen.stringWidth(fixedText_Forward) / 2, yp), + COMMAND_HIGHLIGHTED, "%c", fixedText_Forward[0]); + + 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(fixedText_NotFound)) / 2, 185), + INV_FOREGROUND, "%s", fixedText_NotFound.c_str()); + } 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(fixedText_Exit) / 2, 175), color, "%s", fixedText_Exit.c_str()); + + 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(fixedText_Backward) / 2, 175), color, "%s", fixedText_Backward.c_str()); + + 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(fixedText_Forward) / 2, 175), color, "%s", fixedText_Forward.c_str()); + } + + 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 + drawFrame(); + drawJournal(0, 0); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + return done; +} + +void ScalpelJournal::resetPosition() { + _index = _sub = _up = _down = 0; + _page = 1; +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_journal.h b/engines/sherlock/scalpel/scalpel_journal.h new file mode 100644 index 0000000000..6bc0aa012c --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_journal.h @@ -0,0 +1,102 @@ +/* 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_JOURNAL_H +#define SHERLOCK_SCALPEL_JOURNAL_H + +#include "sherlock/journal.h" +#include "sherlock/saveload.h" +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "common/stream.h" + +namespace Sherlock { + +namespace Scalpel { + +#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 +}; + +class ScalpelJournal: public Journal { +private: + /** + * Load the list of journal locations + */ + void loadLocations(); + + /** + * Display the arrows that can be used to scroll up and down pages + */ + void doArrows(); + + /** + * Show the search submenu and allow the player to enter a search string + */ + int getSearchString(bool printError); + + /** + * Returns the button, if any, that is under the specified position + */ + JournalButton getHighlightedButton(const Common::Point &pt); +public: + ScalpelJournal(SherlockEngine *vm); + virtual ~ScalpelJournal() {} + + /** + * Display the journal + */ + void drawInterface(); + + /** + * Handle events whilst the journal is being displayed + */ + bool handleEvents(int key); +public: + /** + * Draw the journal background, frame, and interface buttons + */ + virtual void drawFrame(); + + /** + * Records statements that are said, in the order which they are said. The player + * can then read the journal to review them + */ + virtual void record(int converseNum, int statementNum, bool replyOnly = false); + + /** + * Reset viewing position to the start of the journal + */ + virtual void resetPosition(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_map.cpp b/engines/sherlock/scalpel/scalpel_map.cpp new file mode 100644 index 0000000000..369822ba02 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_map.cpp @@ -0,0 +1,596 @@ +/* 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/system.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/events.h" +#include "sherlock/people.h" +#include "sherlock/screen.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Scalpel { + +MapPaths::MapPaths() { + _numLocations = 0; +} + +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]; +} + +/*----------------------------------------------------------------*/ + +ScalpelMap::ScalpelMap(SherlockEngine *vm): Map(vm), _topLine(g_system->getWidth(), 12) { + _mapCursors = nullptr; + _shapes = nullptr; + _iconShapes = nullptr; + _point = 0; + _placesShown = false; + _cursorIndex = -1; + _drawMap = false; + _overPos = Point32(130 * FIXED_INT_MULTIPLIER, 126 * FIXED_INT_MULTIPLIER); + _frameChangeFlag = false; + + // Initialise the initial walk sequence set + _walkSequences.resize(MAX_HOLMES_SEQUENCE); + for (int idx = 0; idx < MAX_HOLMES_SEQUENCE; ++idx) { + _walkSequences[idx]._sequences.resize(MAX_FRAME); + Common::fill(&_walkSequences[idx]._sequences[0], &_walkSequences[idx]._sequences[0] + MAX_FRAME, 0); + } + + if (!_vm->isDemo()) + loadData(); +} + +void ScalpelMap::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 ScalpelMap::loadSequences(int count, const byte *seq) { + for (int idx = 0; idx < count; ++idx, seq += MAX_FRAME) + Common::copy(seq, seq + MAX_FRAME, &_walkSequences[idx]._sequences[0]); +} + +void ScalpelMap::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 ScalpelMap::show() { + Debugger &debugger = *_vm->_debugger; + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + 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 = NULL; + if (!IS_3DO) { + // PC + bigMap = new ImageFile("bigmap.vgs"); + screen.setPalette(bigMap->_palette); + } else { + // 3DO + bigMap = new ImageFile3DO("overland.cel", kImageFile3DOType_Cel); + } + + // Load need sprites + setupSprites(); + + if (!IS_3DO) { + 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)); + } else { + screen._backBuffer1.blitFrom((*bigMap)[0], Common::Point(-_bigPos.x, -_bigPos.y)); + screen.blitFrom((*bigMap)[0], Common::Point(-_bigPos.x, -_bigPos.y)); + } + + _drawMap = true; + _charPoint = -1; + _point = -1; + people[HOLMES]._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(); + + if (debugger._showAllLocations == LOC_REFRESH) { + showPlaces(); + screen.slamArea(screen._currentScroll.x, screen._currentScroll.y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_WIDTH); + } + + // 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; + + if (!IS_3DO) { + 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)); + } else { + screen._backBuffer1.blitFrom((*bigMap)[0], Common::Point(-_bigPos.x, -_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[HOLMES]._walkCount == 0) { + people[HOLMES]._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[HOLMES]._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[HOLMES]._position; + + // Reset font + screen.setFont(oldFont); + + // Free map graphic + delete bigMap; + + _active = false; + return _charPoint; +} + +void ScalpelMap::setupSprites() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + _savedPos.x = -1; + + if (!IS_3DO) { + // PC + _mapCursors = new ImageFile("omouse.vgs"); + _shapes = new ImageFile("mapicon.vgs"); + _iconShapes = new ImageFile("overicon.vgs"); + } else { + // 3DO + _mapCursors = new ImageFile3DO("omouse.vgs", kImageFile3DOType_RoomFormat); + _shapes = new ImageFile3DO("mapicon.vgs", kImageFile3DOType_RoomFormat); + _iconShapes = new ImageFile3DO("overicon.vgs", kImageFile3DOType_RoomFormat); + } + + _cursorIndex = 0; + events.setCursor((*_mapCursors)[_cursorIndex]._frame); + + _iconSave.create((*_shapes)[4]._width, (*_shapes)[4]._height); + Person &p = people[HOLMES]; + p._description = " "; + p._type = CHARACTER; + p._position = Common::Point(12400, 5000); + p._sequenceNumber = 0; + 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._walkSequences = _walkSequences; + p.setImageFrame(); + scene._bgShapes.clear(); +} + +void ScalpelMap::freeSprites() { + delete _mapCursors; + delete _shapes; + delete _iconShapes; + _iconSave.free(); +} + +void ScalpelMap::showPlaces() { + Debugger &debugger = *_vm->_debugger; + 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 (debugger._showAllLocations != LOC_DISABLED) + _vm->setFlagsDirect(idx); + + 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)); + } + } + } + } + + if (debugger._showAllLocations == LOC_REFRESH) + debugger._showAllLocations = LOC_ALL; +} + +void ScalpelMap::saveTopLine() { + _topLine.blitFrom(_vm->_screen->_backBuffer1, Common::Point(0, 0), Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, 12)); +} + +void ScalpelMap::eraseTopLine() { + Screen &screen = *_vm->_screen; + screen._backBuffer1.blitFrom(_topLine, Common::Point(0, 0)); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, _topLine.h()); +} + +void ScalpelMap::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[HOLMES]._imageFrame, _lDrawnPos); + + bool flipped = people[HOLMES]._sequenceNumber == MAP_DOWNLEFT || people[HOLMES]._sequenceNumber == MAP_LEFT + || people[HOLMES]._sequenceNumber == MAP_UPLEFT; + screen._backBuffer1.transBlitFrom(*people[HOLMES]._imageFrame, _lDrawnPos, flipped); + } + + if (highlighted) { + int xp = (SHERLOCK_SCREEN_WIDTH - screen.stringWidth(name)) / 2; + screen.gPrint(Common::Point(xp + 2, 2), BLACK, "%s", name.c_str()); + screen.gPrint(Common::Point(xp + 1, 1), BLACK, "%s", name.c_str()); + screen.gPrint(Common::Point(xp, 0), 12, "%s", name.c_str()); + + screen.slamArea(xp, 0, width + 2, 15); + } +} + +void ScalpelMap::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[HOLMES].adjustSprite(); + + _lDrawnPos.x = hPos.x = people[HOLMES]._position.x / FIXED_INT_MULTIPLIER - _bigPos.x; + _lDrawnPos.y = hPos.y = people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES].frameHeight() - _bigPos.y; + + // Draw the person icon + saveIcon(people[HOLMES]._imageFrame, hPos); + if (people[HOLMES]._sequenceNumber == MAP_DOWNLEFT || people[HOLMES]._sequenceNumber == MAP_LEFT + || people[HOLMES]._sequenceNumber == MAP_UPLEFT) + screen._backBuffer1.transBlitFrom(*people[HOLMES]._imageFrame, hPos, true); + else + screen._backBuffer1.transBlitFrom(*people[HOLMES]._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[HOLMES]._imageFrame, Common::Point(people[HOLMES]._position.x / FIXED_INT_MULTIPLIER - _bigPos.x, + people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES].frameHeight() - _bigPos.y), + &people[HOLMES]._oldPosition.x, &people[HOLMES]._oldPosition.y, &people[HOLMES]._oldSize.x, &people[HOLMES]._oldSize.y); + + if (osPos.x != -1) + screen.slamArea(osPos.x, osPos.y, osSize.x, osSize.y); + } +} + +void ScalpelMap::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[HOLMES]._walkTo.clear(); + Common::Point destPos = people[HOLMES]._walkDest; + + // Check for any intermediate points between the two locations + if (path[0] || _charPoint > 50 || _oldCharPoint > 50) { + people[HOLMES]._sequenceNumber = -1; + + if (_charPoint == 51 || _oldCharPoint == 51) { + people[HOLMES].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[HOLMES]._walkTo.clear(); + + if (reversePath) { + for (int idx = (int)tempPath.size() - 1; idx >= 0; --idx) + people[HOLMES]._walkTo.push(tempPath[idx]); + } else { + for (int idx = 0; idx < (int)tempPath.size(); ++idx) + people[HOLMES]._walkTo.push(tempPath[idx]); + } + + people[HOLMES]._walkDest = people[HOLMES]._walkTo.pop() + Common::Point(12, 6); + people[HOLMES].setWalking(); + } + } else { + people[HOLMES]._walkCount = 0; + } + + // Store the final destination icon position + people[HOLMES]._walkTo.push(destPos); +} + +void ScalpelMap::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 ScalpelMap::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 ScalpelMap::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(); + } +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_map.h b/engines/sherlock/scalpel/scalpel_map.h new file mode 100644 index 0000000000..b17677725c --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_map.h @@ -0,0 +1,173 @@ +/* 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_MAP_H +#define SHERLOCK_SCALPEL_MAP_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "sherlock/surface.h" +#include "sherlock/map.h" +#include "sherlock/resources.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Scalpel { + + +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: + MapPaths(); + + /** + * 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 ScalpelMap: public Map { +private: + 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; + WalkSequences _walkSequences; + Point32 _lDrawnPos; + int _point; + bool _placesShown; + int _cursorIndex; + bool _drawMap; + Surface _iconSave; +protected: + /** + * 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: + ScalpelMap(SherlockEngine *vm); + virtual ~ScalpelMap() {} + + 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 + */ + virtual int show(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_people.cpp b/engines/sherlock/scalpel/scalpel_people.cpp new file mode 100644 index 0000000000..53876f8f1c --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_people.cpp @@ -0,0 +1,532 @@ +/* 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_people.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Scalpel { + +// Walk speeds +#define MWALK_SPEED 2 +#define XWALK_SPEED 4 +#define YWALK_SPEED 1 + +/*----------------------------------------------------------------*/ + +void ScalpelPerson::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[HOLMES]._walkTo.empty()) { + _walkDest = people[HOLMES]._walkTo.pop(); + setWalking(); + } else { + gotoStand(); + } + } + } + + if (_type == CHARACTER && !map._active) { + if ((_position.y / FIXED_INT_MULTIPLIER) > LOWER_LIMIT) { + _position.y = LOWER_LIMIT * FIXED_INT_MULTIPLIER; + gotoStand(); + } + + if ((_position.y / FIXED_INT_MULTIPLIER) < UPPER_LIMIT) { + _position.y = UPPER_LIMIT * FIXED_INT_MULTIPLIER; + gotoStand(); + } + + if ((_position.x / FIXED_INT_MULTIPLIER) < LEFT_LIMIT) { + _position.x = LEFT_LIMIT * FIXED_INT_MULTIPLIER; + gotoStand(); + } + + if ((_position.x / FIXED_INT_MULTIPLIER) > RIGHT_LIMIT) { + _position.x = RIGHT_LIMIT * FIXED_INT_MULTIPLIER; + gotoStand(); + } + } else if (!map._active) { + _position.y = CLIP((int)_position.y, (int)UPPER_LIMIT, (int)LOWER_LIMIT); + _position.x = CLIP((int)_position.x, (int)LEFT_LIMIT, (int)RIGHT_LIMIT); + } + + if (!map._active || (map._frameChangeFlag = !map._frameChangeFlag)) + ++_frameNumber; + + if (_frameNumber >= (int)_walkSequences[_sequenceNumber]._sequences.size() || + _walkSequences[_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 / FIXED_INT_MULTIPLIER - 5, _position.y / FIXED_INT_MULTIPLIER - 2, + _position.x / FIXED_INT_MULTIPLIER + 5, _position.y / FIXED_INT_MULTIPLIER + 2); + Exit *exit = scene.checkForExit(charRect); + + if (exit) { + scene._goToScene = exit->_scene; + + if (exit->_newPosition.x != 0) { + people._savedPos = exit->_newPosition; + + if (people._savedPos._facing > 100 && people._savedPos.x < 1) + people._savedPos.x = 100; + } + } + } +} + +void ScalpelPerson::gotoStand() { + ScalpelMap &map = *(ScalpelMap *)_vm->_map; + People &people = *_vm->_people; + _walkTo.clear(); + _walkCount = 0; + + switch (_sequenceNumber) { + case Scalpel::WALK_UP: + _sequenceNumber = STOP_UP; + break; + case WALK_DOWN: + _sequenceNumber = STOP_DOWN; + break; + case TALK_LEFT: + case WALK_LEFT: + _sequenceNumber = STOP_LEFT; + break; + case TALK_RIGHT: + case WALK_RIGHT: + _sequenceNumber = STOP_RIGHT; + break; + case WALK_UPRIGHT: + _sequenceNumber = STOP_UPRIGHT; + break; + case WALK_UPLEFT: + _sequenceNumber = STOP_UPLEFT; + break; + case WALK_DOWNRIGHT: + _sequenceNumber = STOP_DOWNRIGHT; + break; + case WALK_DOWNLEFT: + _sequenceNumber = STOP_DOWNLEFT; + break; + default: + break; + } + + // Only restart frame at 0 if the sequence number has changed + if (_oldWalkSequence != -1 || _sequenceNumber == Scalpel::STOP_UP) + _frameNumber = 0; + + if (map._active) { + _sequenceNumber = 0; + people[HOLMES]._position.x = (map[map._charPoint].x - 6) * FIXED_INT_MULTIPLIER; + people[HOLMES]._position.y = (map[map._charPoint].y + 10) * FIXED_INT_MULTIPLIER; + } + + _oldWalkSequence = -1; + people._allowWalkAbort = true; +} + +void ScalpelPerson::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 + _walkCount = 0; + oldDirection = _sequenceNumber; + oldFrame = _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 = _imageFrame->_frame.w / 2)) + _walkDest.x -= temp; + + delta = Common::Point( + ABS(_position.x / FIXED_INT_MULTIPLIER - _walkDest.x), + ABS(_position.y / FIXED_INT_MULTIPLIER - _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 < (_position.x / FIXED_INT_MULTIPLIER)) { + _sequenceNumber = (map._active ? (int)MAP_LEFT : (int)WALK_LEFT); + _delta.x = speed.x * -FIXED_INT_MULTIPLIER; + } else { + _sequenceNumber = (map._active ? (int)MAP_RIGHT : (int)WALK_RIGHT); + _delta.x = speed.x * FIXED_INT_MULTIPLIER; + } + + // 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 + _delta.y = (delta.y * FIXED_INT_MULTIPLIER) / (delta.x / speed.x); + if (_walkDest.y < (_position.y / FIXED_INT_MULTIPLIER)) + _delta.y = -_delta.y; + + // Set how many times we should add the delta to the player's position + _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 + _delta = Point32(0, 0); + _position = Point32(_walkDest.x * FIXED_INT_MULTIPLIER, _walkDest.y * FIXED_INT_MULTIPLIER); + + _walkCount = 1; + } + + // See if the sequence needs to be changed for diagonal walking + if (_delta.y > 150) { + if (!map._active) { + switch (_sequenceNumber) { + case WALK_LEFT: + _sequenceNumber = WALK_DOWNLEFT; + break; + case WALK_RIGHT: + _sequenceNumber = WALK_DOWNRIGHT; + break; + } + } + } else if (_delta.y < -150) { + if (!map._active) { + switch (_sequenceNumber) { + case WALK_LEFT: + _sequenceNumber = WALK_UPLEFT; + break; + case WALK_RIGHT: + _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 < (_position.y / FIXED_INT_MULTIPLIER)) { + _sequenceNumber = WALK_UP; + _delta.y = speed.y * -FIXED_INT_MULTIPLIER; + } else { + _sequenceNumber = WALK_DOWN; + _delta.y = speed.y * FIXED_INT_MULTIPLIER; + } + + // If we're on the overhead map, set the sequence so we keep moving + // in the same direction + if (map._active) + _sequenceNumber = (oldDirection == -1) ? MAP_RIGHT : oldDirection; + + // Set the delta x + _delta.x = (delta.x * FIXED_INT_MULTIPLIER) / (delta.y / speed.y); + if (_walkDest.x < (_position.x / FIXED_INT_MULTIPLIER)) + _delta.x = -_delta.x; + + _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 (_sequenceNumber != _oldWalkSequence) + _frameNumber = 0; + _oldWalkSequence = _sequenceNumber; + + if (!_walkCount) + gotoStand(); + + // 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 (_sequenceNumber == oldDirection) + _frameNumber = oldFrame; +} + +void ScalpelPerson::walkToCoords(const Point32 &destPos, int destDir) { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Talk &talk = *_vm->_talk; + + CursorId oldCursor = events.getCursor(); + events.setCursor(WAIT); + + _walkDest = Common::Point(destPos.x / FIXED_INT_MULTIPLIER + 10, destPos.y / FIXED_INT_MULTIPLIER); + people._allowWalkAbort = true; + goAllTheWay(); + + // Keep calling doBgAnim until the walk is done + do { + events.pollEventsAndWait(); + scene.doBgAnim(); + } while (!_vm->shouldQuit() && _walkCount); + + if (!talk._talkToAbort) { + // Put character exactly on destination position, and set direction + _position = destPos; + _sequenceNumber = destDir; + gotoStand(); + + // Draw Holmes facing the new direction + scene.doBgAnim(); + + if (!talk._talkToAbort) + events.setCursor(oldCursor); + } +} + +Common::Point ScalpelPerson::getSourcePoint() const { + return Common::Point(_position.x / FIXED_INT_MULTIPLIER + frameWidth() / 2, + _position.y / FIXED_INT_MULTIPLIER); +} + +/*----------------------------------------------------------------*/ + +ScalpelPeople::ScalpelPeople(SherlockEngine *vm) : People(vm) { + _data.push_back(new ScalpelPerson()); +} + +void ScalpelPeople::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 ScalpelPeople::synchronize(Serializer &s) { + s.syncAsByte(_holmesOn); + s.syncAsSint32LE(_data[HOLMES]->_position.x); + s.syncAsSint32LE(_data[HOLMES]->_position.y); + s.syncAsSint16LE(_data[HOLMES]->_sequenceNumber); + s.syncAsSint16LE(_holmesQuotient); + + if (s.isLoading()) { + _savedPos = _data[HOLMES]->_position; + _savedPos._facing = _data[HOLMES]->_sequenceNumber; + } +} + +void ScalpelPeople::setTalkSequence(int speaker, int sequenceNum) { + 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; + } + } + } + } +} + +bool ScalpelPeople::loadWalk() { + bool result = false; + + if (_data[HOLMES]->_walkLoaded) { + return false; + } else { + if (!IS_3DO) { + _data[HOLMES]->_images = new ImageFile("walk.vgs"); + } else { + // Load walk.anim on 3DO, which is a cel animation file + _data[HOLMES]->_images = new ImageFile3DO("walk.anim", kImageFile3DOType_CelAnimation); + } + _data[HOLMES]->setImageFrame(); + _data[HOLMES]->_walkLoaded = true; + + result = true; + } + + _forceWalkReload = false; + return result; +} + +const Common::Point ScalpelPeople::restrictToZone(int zoneId, const Common::Point &destPos) { + Scene &scene = *_vm->_scene; + Common::Point walkDest = destPos; + + // 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[zoneId]; + const Common::Point destCenter((destRect.left + destRect.right) / 2, + (destRect.top + destRect.bottom) / 2); + const Common::Point delta = walkDest - destCenter; + Point32 pt(destCenter.x * FIXED_INT_MULTIPLIER, destCenter.y * FIXED_INT_MULTIPLIER); + + // Move along the line until the zone is left + do { + pt += delta; + } while (destRect.contains(pt.x / FIXED_INT_MULTIPLIER, pt.y / FIXED_INT_MULTIPLIER)); + + // Set the new walk destination to the last point that was in the + // zone just before it was left + return Common::Point((pt.x - delta.x * 2) / FIXED_INT_MULTIPLIER, + (pt.y - delta.y * 2) / FIXED_INT_MULTIPLIER); +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_people.h b/engines/sherlock/scalpel/scalpel_people.h new file mode 100644 index 0000000000..b53da2e6d8 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_people.h @@ -0,0 +1,114 @@ +/* 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_PEOPLE_H +#define SHERLOCK_SCALPEL_PEOPLE_H + +#include "common/scummsys.h" +#include "sherlock/people.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Scalpel { + +// Animation sequence identifiers for characters +enum ScalpelSequences { + 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 +}; + +class ScalpelPerson : public Person { +public: + ScalpelPerson() : Person() {} + virtual ~ScalpelPerson() {} + + /** + * This adjusts the sprites position, as well as it's animation sequence: + */ + virtual void adjustSprite(); + + /** + * Bring a moving character to a standing position + */ + virtual void gotoStand(); + + /** + * Set the variables for moving a character from one poisition to another + * in a straight line + */ + virtual void setWalking(); + + /** + * Walk to the co-ordinates passed, and then face the given direction + */ + virtual void walkToCoords(const Point32 &destPos, int destDir); + + /** + * Get the source position for a character potentially affected by scaling + */ + virtual Common::Point getSourcePoint() const; +}; + +class ScalpelPeople : public People { +public: + ScalpelPeople(SherlockEngine *vm); + virtual ~ScalpelPeople() {} + + ScalpelPerson &operator[](PeopleId id) { return *(ScalpelPerson *)_data[id]; } + ScalpelPerson &operator[](int idx) { return *(ScalpelPerson *)_data[idx]; } + + /** + * Setup the data for an animating speaker portrait at the top of the screen + */ + void setTalking(int speaker); + + /** + * Synchronize the data for a savegame + */ + virtual void synchronize(Serializer &s); + + /** + * Change the sequence of the scene background object associated with the specified speaker. + */ + virtual void setTalkSequence(int speaker, int sequenceNum = 1); + + /** + * Restrict passed point to zone using Sherlock's positioning rules + */ + virtual const Common::Point restrictToZone(int zoneId, const Common::Point &destPos); + + /** + * Load the walking images for Sherlock + */ + virtual bool loadWalk(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_saveload.cpp b/engines/sherlock/scalpel/scalpel_saveload.cpp new file mode 100644 index 0000000000..01ba149813 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_saveload.cpp @@ -0,0 +1,261 @@ +/* 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_saveload.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +namespace Scalpel { + +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 +}; + +/*----------------------------------------------------------------*/ + +ScalpelSaveManager::ScalpelSaveManager(SherlockEngine *vm, const Common::String &target) : SaveManager(vm, target) { +} + +void ScalpelSaveManager::drawInterface() { + ScalpelScreen &screen = *(ScalpelScreen *)_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; +} + +int ScalpelSaveManager::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 ScalpelSaveManager::highlightButtons(int btnIndex) { + ScalpelScreen &screen = *(ScalpelScreen *)_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"); +} + +bool ScalpelSaveManager::checkGameOnScreen(int slot) { + ScalpelScreen &screen = *(ScalpelScreen *)_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 ScalpelSaveManager::promptForDescription(int slot) { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + ScalpelScreen &screen = *(ScalpelScreen *)_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; +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_saveload.h b/engines/sherlock/scalpel/scalpel_saveload.h new file mode 100644 index 0000000000..db4fa1a2ab --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_saveload.h @@ -0,0 +1,69 @@ +/* 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_SAVELOAD_H +#define SHERLOCK_SCALPEL_SAVELOAD_H + +#include "sherlock/saveload.h" + +namespace Sherlock { + +namespace Scalpel { + +extern const int ENV_POINTS[6][3]; + +class ScalpelSaveManager: public SaveManager { +public: + ScalpelSaveManager(SherlockEngine *vm, const Common::String &target); + virtual ~ScalpelSaveManager() {} + + /** + * Shows the in-game dialog interface for loading and saving games + */ + void drawInterface(); + + /** + * Return the index of the button the mouse is over, if any + */ + int getHighlightedButton() const; + + /** + * Handle highlighting buttons + */ + void highlightButtons(int btnIndex); + + /** + * 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); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_scene.cpp b/engines/sherlock/scalpel/scalpel_scene.cpp new file mode 100644 index 0000000000..4e6e9e4c7c --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_scene.cpp @@ -0,0 +1,726 @@ +/* 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_scene.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/events.h" +#include "sherlock/people.h" +#include "sherlock/screen.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Scalpel { + +bool ScalpelScene::loadScene(const Common::String &filename) { + ScalpelMap &map = *(ScalpelMap *)_vm->_map; + bool result = Scene::loadScene(filename); + + if (!_vm->isDemo()) { + // Reset the previous map location and position on overhead map + map._oldCharPoint = _currentScene; + + map._overPos.x = (map[_currentScene].x - 6) * FIXED_INT_MULTIPLIER; + map._overPos.y = (map[_currentScene].y + 9) * FIXED_INT_MULTIPLIER; + + } + + return result; +} + +void ScalpelScene::drawAllShapes() { + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + + // Restrict drawing window + screen.setDisplayBounds(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + + // 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 any active characters + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + Person &p = people[idx]; + if (p._type == CHARACTER && p._walkLoaded) { + bool flipped = IS_SERRATED_SCALPEL && ( + p._sequenceNumber == WALK_LEFT || p._sequenceNumber == STOP_LEFT || + p._sequenceNumber == WALK_UPLEFT || p._sequenceNumber == STOP_UPLEFT || + p._sequenceNumber == WALK_DOWNRIGHT || p._sequenceNumber == STOP_DOWNRIGHT); + + screen._backBuffer->transBlitFrom(*p._imageFrame, Common::Point(p._position.x / FIXED_INT_MULTIPLIER, + p._position.y / FIXED_INT_MULTIPLIER - p.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(); +} + +void ScalpelScene::checkBgShapes() { + People &people = *_vm->_people; + Person &holmes = people[HOLMES]; + Common::Point pt(holmes._position.x / FIXED_INT_MULTIPLIER, holmes._position.y / FIXED_INT_MULTIPLIER); + + // Call the base scene method to handle bg shapes + Scene::checkBgShapes(); + + // Iterate through the canim list + 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; + } + } + } +} + +void ScalpelScene::doBgAnimCheckCursor() { + Inventory &inv = *_vm->_inventory; + Events &events = *_vm->_events; + Sound &sound = *_vm->_sound; + UserInterface &ui = *_vm->_ui; + Common::Point mousePos = events.mousePos(); + events.animateCursorIfNeeded(); + + 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(); + } +} + +void ScalpelScene::doBgAnim() { + ScalpelEngine &vm = *((ScalpelEngine *)_vm); + Events &events = *_vm->_events; + People &people = *_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + + doBgAnimCheckCursor(); + + screen.setDisplayBounds(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCENE_HEIGHT)); + talk._talkToAbort = false; + + if (_restoreFlag) { + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) + people[idx].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.eraseMirror12(); + + // Restore the back buffer from the back buffer 2 in the changed area + Common::Rect bounds(people[HOLMES]._oldPosition.x, people[HOLMES]._oldPosition.y, + people[HOLMES]._oldPosition.x + people[HOLMES]._oldSize.x, + people[HOLMES]._oldPosition.y + people[HOLMES]._oldSize.y); + Common::Point pt(bounds.left, bounds.top); + + if (people[HOLMES]._type == CHARACTER) + screen.restoreBackground(bounds); + else if (people[HOLMES]._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[HOLMES]._type == CHARACTER && people._holmesOn) + people[HOLMES].adjustSprite(); + + // Flag the bg shapes which need to be redrawn + checkBgShapes(); + + if (_currentScene == 12) + 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[HOLMES]._type == CHARACTER && people[HOLMES]._walkLoaded) { + // If Holmes is too far to the right, move him back so he's on-screen + int xRight = SHERLOCK_SCREEN_WIDTH - 2 - people[HOLMES]._imageFrame->_frame.w; + int tempX = MIN(people[HOLMES]._position.x / FIXED_INT_MULTIPLIER, xRight); + + bool flipped = people[HOLMES]._sequenceNumber == WALK_LEFT || people[HOLMES]._sequenceNumber == STOP_LEFT || + people[HOLMES]._sequenceNumber == WALK_UPLEFT || people[HOLMES]._sequenceNumber == STOP_UPLEFT || + people[HOLMES]._sequenceNumber == WALK_DOWNRIGHT || people[HOLMES]._sequenceNumber == STOP_DOWNRIGHT; + screen._backBuffer->transBlitFrom(*people[HOLMES]._imageFrame, + Common::Point(tempX, people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES]._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[HOLMES]._type != INVALID && ((_goToScene == -1 || _canimShapes.empty()))) { + if (people[HOLMES]._type == REMOVE) { + screen.slamRect(Common::Rect( + people[HOLMES]._oldPosition.x, people[HOLMES]._oldPosition.y, + people[HOLMES]._oldPosition.x + people[HOLMES]._oldSize.x, + people[HOLMES]._oldPosition.y + people[HOLMES]._oldSize.y + )); + people[HOLMES]._type = INVALID; + } else { + screen.flushImage(people[HOLMES]._imageFrame, + Common::Point(people[HOLMES]._position.x / FIXED_INT_MULTIPLIER, + people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES].frameHeight()), + &people[HOLMES]._oldPosition.x, &people[HOLMES]._oldPosition.y, + &people[HOLMES]._oldSize.x, &people[HOLMES]._oldSize.y); + } + } + + if (_currentScene == 12) + 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 ScalpelScene::startCAnim(int cAnimNum, int playRate) { + Events &events = *_vm->_events; + ScalpelMap &map = *(ScalpelMap *)_vm->_map; + People &people = *_vm->_people; + Resources &res = *_vm->_res; + Talk &talk = *_vm->_talk; + ScalpelUserInterface &ui = *(ScalpelUserInterface *)_vm->_ui; + Point32 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._teleport[0]; + walkDir = cAnim._teleport[0]._facing; + tpPos = cAnim._goto[0]; + tpDir = cAnim._goto[0]._facing; + } else { + // Forward direction + walkPos = cAnim._goto[0]; + walkDir = cAnim._goto[0]._facing; + tpPos = cAnim._teleport[0]; + tpDir = cAnim._teleport[0]._facing; + } + + 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[HOLMES]._position != walkPos) + people[HOLMES].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[HOLMES]._type = REMOVE; + + Common::String fname = cAnim._name + ".vgs"; + if (!res.isInCache(fname)) { + // Set up RRM scene data + Common::SeekableReadStream *roomStream = res.load(_roomFilename); + roomStream->seek(cAnim._dataOffset); + //rrmStream->seek(44 + cAnimNum * 4); + //rrmStream->seek(rrmStream->readUint32LE()); + + // Load the canimation into the cache + Common::SeekableReadStream *imgStream = !_compressed ? roomStream->readStream(cAnim._dataSize) : + Resources::decompressLZ(*roomStream, cAnim._dataSize); + res.addToCache(fname, *imgStream); + + delete imgStream; + delete roomStream; + } + + // Now load the resource as an image + if (!IS_3DO) { + cObj._images = new ImageFile(fname); + } else { + cObj._images = new ImageFile3DO(fname, kImageFile3DOType_RoomFormat); + } + cObj._imageFrame = &(*cObj._images)[0]; + cObj._maxFrames = cObj._images->size(); + + int frames = 0; + if (playRate < 0) { + // Reverse direction + // Count number of frames + while (frames < MAX_FRAME && cObj._sequences[frames]) + ++frames; + } else { + // Forward direction + BaseObject::_countCAnimFrames = true; + + while (cObj._type == ACTIVE_BG_SHAPE) { + cObj.checkObject(); + ++frames; + + if (frames >= 1000) + error("CAnim has infinite loop sequence"); + } + + if (frames > 1) + --frames; + + BaseObject::_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[HOLMES]._type = CHARACTER; + } + + // Teleport to ending coordinates if necessary + if (tpPos.x != -1) { + people[HOLMES]._position = tpPos; // Place the player + people[HOLMES]._sequenceNumber = tpDir; + people[HOLMES].gotoStand(); + } + + 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[HOLMES]._position = tpPos; + people[HOLMES]._sequenceNumber = tpDir; + + people[HOLMES].gotoStand(); + } + + events.setCursor(oldCursor); + + return 1; +} + +int ScalpelScene::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; +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_scene.h b/engines/sherlock/scalpel/scalpel_scene.h new file mode 100644 index 0000000000..fa65ecd95b --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_scene.h @@ -0,0 +1,96 @@ +/* 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_SCENE_H +#define SHERLOCK_SCALPEL_SCENE_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/rect.h" +#include "common/serializer.h" +#include "sherlock/objects.h" +#include "sherlock/scene.h" +#include "sherlock/screen.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 ScalpelScene : public Scene { +private: + void doBgAnimCheckCursor(); +protected: + /** + * Loads the data associated for a given scene. The room resource 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. + */ + virtual bool loadScene(const Common::String &filename); + + /** + * 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 + */ + virtual void checkBgShapes(); + + /** + * Draw all the shapes, people and NPCs in the correct order + */ + virtual void drawAllShapes(); + + /** + * Returns the index of the closest zone to a given point. + */ + virtual int closestZone(const Common::Point &pt); +public: + ScalpelScene(SherlockEngine *vm) : Scene(vm) {} + + /** + * Draw all objects and characters. + */ + virtual void doBgAnim(); + + /** + * 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 + */ + virtual int startCAnim(int cAnimNum, int playRate = 1); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_screen.cpp b/engines/sherlock/scalpel/scalpel_screen.cpp new file mode 100644 index 0000000000..2096dabcdf --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_screen.cpp @@ -0,0 +1,93 @@ +/* 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_screen.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +namespace Scalpel { + +ScalpelScreen::ScalpelScreen(SherlockEngine *vm) : Screen(vm) { +} + +void ScalpelScreen::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 ScalpelScreen::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 ScalpelScreen::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 ScalpelScreen::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); +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_screen.h b/engines/sherlock/scalpel/scalpel_screen.h new file mode 100644 index 0000000000..472fe9e220 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_screen.h @@ -0,0 +1,66 @@ +/* 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_SCREEN_H +#define SHERLOCK_SCALPEL_SCREEN_H + +#include "sherlock/screen.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Scalpel { + +class ScalpelScreen : public Screen { +public: + ScalpelScreen(SherlockEngine *vm); + virtual ~ScalpelScreen() {} + + /** + * 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); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_talk.cpp b/engines/sherlock/scalpel/scalpel_talk.cpp new file mode 100644 index 0000000000..aa0a2f48b4 --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_talk.cpp @@ -0,0 +1,844 @@ +/* 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_talk.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_map.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_scene.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/screen.h" +#include "sherlock/scalpel/3do/movie_decoder.h" + +namespace Sherlock { + +namespace Scalpel { + +const byte SCALPEL_OPCODES[] = { + 128, // OP_SWITCH_SPEAKER + 129, // OP_RUN_CANIMATION + 130, // OP_ASSIGN_PORTRAIT_LOCATION + 131, // OP_PAUSE + 132, // OP_REMOVE_PORTRAIT + 133, // OP_CLEAR_WINDOW + 134, // OP_ADJUST_OBJ_SEQUENCE + 135, // OP_WALK_TO_COORDS + 136, // OP_PAUSE_WITHOUT_CONTROL + 137, // OP_BANISH_WINDOW + 138, // OP_SUMMON_WINDOW + 139, // OP_SET_FLAG + 140, // OP_SFX_COMMAND + 141, // OP_TOGGLE_OBJECT + 142, // OP_STEALTH_MODE_ACTIVE + 143, // OP_IF_STATEMENT + 144, // OP_ELSE_STATEMENT + 145, // OP_END_IF_STATEMENT + 146, // OP_STEALTH_MODE_DEACTIVATE + 147, // OP_TURN_HOLMES_OFF + 148, // OP_TURN_HOLMES_ON + 149, // OP_GOTO_SCENE + 150, // OP_PLAY_PROLOGUE + 151, // OP_ADD_ITEM_TO_INVENTORY + 152, // OP_SET_OBJECT + 153, // OP_CALL_TALK_FILE + 143, // OP_MOVE_MOUSE + 155, // OP_DISPLAY_INFO_LINE + 156, // OP_CLEAR_INFO_LINE + 157, // OP_WALK_TO_CANIMATION + 158, // OP_REMOVE_ITEM_FROM_INVENTORY + 159, // OP_ENABLE_END_KEY + 160, // OP_DISABLE_END_KEY + 161, // OP_END_TEXT_WINDOW + 0, // OP_MOUSE_ON_OFF + 0, // OP_SET_WALK_CONTROL + 0, // OP_SET_TALK_SEQUENCE + 0, // OP_PLAY_SONG + 0, // OP_WALK_HOLMES_AND_NPC_TO_CANIM + 0, // OP_SET_NPC_PATH_DEST + 0, // OP_NEXT_SONG + 0, // OP_SET_NPC_PATH_PAUSE + 0, // OP_PASSWORD + 0, // OP_SET_SCENE_ENTRY_FLAG + 0, // OP_WALK_NPC_TO_CANIM + 0, // OP_WALK_HOLMES_AND_NPC_TO_COORDS + 0, // OP_WALK_HOLMES_AND_NPC_TO_COORDS + 0, // OP_SET_NPC_TALK_FILE + 0, // OP_TURN_NPC_OFF + 0, // OP_TURN_NPC_ON + 0, // OP_NPC_DESC_ON_OFF + 0, // OP_NPC_PATH_PAUSE_TAKING_NOTES + 0, // OP_NPC_PATH_PAUSE_LOOKING_HOLMES + 0, // OP_ENABLE_TALK_INTERRUPTS + 0, // OP_DISABLE_TALK_INTERRUPTS + 0, // OP_SET_NPC_INFO_LINE + 0, // OP_SET_NPC_POSITION + 0, // OP_NPC_PATH_LABEL + 0, // OP_PATH_GOTO_LABEL + 0, // OP_PATH_IF_FLAG_GOTO_LABEL + 0, // OP_NPC_WALK_GRAPHICS + 0, // OP_NPC_VERB + 0, // OP_NPC_VERB_CANIM + 0, // OP_NPC_VERB_SCRIPT + 0, // OP_RESTORE_PEOPLE_SEQUENCE + 0, // OP_NPC_VERB_TARGET + 0, // OP_TURN_SOUNDS_OFF + 0 // OP_NULL +}; + +/*----------------------------------------------------------------*/ + +ScalpelTalk::ScalpelTalk(SherlockEngine *vm) : Talk(vm) { + static OpcodeMethod OPCODE_METHODS[] = { + (OpcodeMethod)&ScalpelTalk::cmdSwitchSpeaker, + (OpcodeMethod)&ScalpelTalk::cmdRunCAnimation, + (OpcodeMethod)&ScalpelTalk::cmdAssignPortraitLocation, + + (OpcodeMethod)&ScalpelTalk::cmdPause, + (OpcodeMethod)&ScalpelTalk::cmdRemovePortrait, + (OpcodeMethod)&ScalpelTalk::cmdClearWindow, + (OpcodeMethod)&ScalpelTalk::cmdAdjustObjectSequence, + (OpcodeMethod)&ScalpelTalk::cmdWalkToCoords, + (OpcodeMethod)&ScalpelTalk::cmdPauseWithoutControl, + (OpcodeMethod)&ScalpelTalk::cmdBanishWindow, + (OpcodeMethod)&ScalpelTalk::cmdSummonWindow, + (OpcodeMethod)&ScalpelTalk::cmdSetFlag, + (OpcodeMethod)&ScalpelTalk::cmdSfxCommand, + + (OpcodeMethod)&ScalpelTalk::cmdToggleObject, + (OpcodeMethod)&ScalpelTalk::cmdStealthModeActivate, + (OpcodeMethod)&ScalpelTalk::cmdIf, + (OpcodeMethod)&ScalpelTalk::cmdElse, + nullptr, + (OpcodeMethod)&ScalpelTalk::cmdStealthModeDeactivate, + (OpcodeMethod)&ScalpelTalk::cmdHolmesOff, + (OpcodeMethod)&ScalpelTalk::cmdHolmesOn, + (OpcodeMethod)&ScalpelTalk::cmdGotoScene, + (OpcodeMethod)&ScalpelTalk::cmdPlayPrologue, + + (OpcodeMethod)&ScalpelTalk::cmdAddItemToInventory, + (OpcodeMethod)&ScalpelTalk::cmdSetObject, + (OpcodeMethod)&ScalpelTalk::cmdCallTalkFile, + (OpcodeMethod)&ScalpelTalk::cmdMoveMouse, + (OpcodeMethod)&ScalpelTalk::cmdDisplayInfoLine, + (OpcodeMethod)&ScalpelTalk::cmdClearInfoLine, + (OpcodeMethod)&ScalpelTalk::cmdWalkToCAnimation, + (OpcodeMethod)&ScalpelTalk::cmdRemoveItemFromInventory, + (OpcodeMethod)&ScalpelTalk::cmdEnableEndKey, + (OpcodeMethod)&ScalpelTalk::cmdDisableEndKey, + + (OpcodeMethod)&ScalpelTalk::cmdEndTextWindow, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr + }; + + _opcodeTable = OPCODE_METHODS; + _opcodes = SCALPEL_OPCODES; + + if (vm->getLanguage() == Common::DE_DEU || vm->getLanguage() == Common::ES_ESP) { + // The German and Spanish versions use a different opcode range + static byte opcodes[sizeof(SCALPEL_OPCODES)]; + for (uint idx = 0; idx < sizeof(SCALPEL_OPCODES); ++idx) + opcodes[idx] = SCALPEL_OPCODES[idx] ? SCALPEL_OPCODES[idx] + 47 : 0; + + _opcodes = opcodes; + } + +} + +void ScalpelTalk::talkInterface(const byte *&str) { + FixedText &fixedText = *_vm->_fixedText; + People &people = *_vm->_people; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + UserInterface &ui = *_vm->_ui; + + // If the window isn't yet open, draw the window before printing starts + if (!ui._windowOpen && _noTextYet) { + _noTextYet = false; + drawInterface(); + + if (_talkTo != -1) { + Common::String fixedText_Exit = fixedText.getText(kFixedText_Window_Exit); + Common::String fixedText_Up = fixedText.getText(kFixedText_Window_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Window_Down); + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, false, fixedText_Exit); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, false, fixedText_Up); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, fixedText_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] != '{' && (!isOpcode(str[idx]))); + + if (str[idx] || width >= 298) { + if ((!isOpcode(str[idx])) && 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 ((!isOpcode(str[0])) && str[0] != '{') + ++str; + + _yp += 9; + ++_line; + + // Certain different conditions require a wait + if ((_line == 4 && str < _scriptEnd && str[0] != _opcodes[OP_SFX_COMMAND] && str[0] != _opcodes[OP_PAUSE] && _speaker != -1) || + (_line == 5 && str < _scriptEnd && str[0] != _opcodes[OP_PAUSE] && _speaker == -1) || + _endStr) { + _wait = 1; + } + + byte v = (str >= _scriptEnd ? 0 : str[0]); + if (v == _opcodes[OP_SWITCH_SPEAKER] || v == _opcodes[OP_ASSIGN_PORTRAIT_LOCATION] || + v == _opcodes[OP_BANISH_WINDOW] || v == _opcodes[OP_IF_STATEMENT] || + v == _opcodes[OP_ELSE_STATEMENT] || v == _opcodes[OP_END_IF_STATEMENT] || + v == _opcodes[OP_GOTO_SCENE] || v == _opcodes[OP_CALL_TALK_FILE]) { + _wait = 1; + } +} + +OpcodeReturn ScalpelTalk::cmdSwitchSpeaker(const byte *&str) { + ScalpelPeople &people = *(ScalpelPeople *)_vm->_people; + UserInterface &ui = *_vm->_ui; + + if (!(_speaker & SPEAKER_REMOVE)) + people.clearTalking(); + if (_talkToAbort) + return RET_EXIT; + + ui.clearWindow(); + _yp = CONTROLS_Y + 12; + _charCount = _line = 0; + + _speaker = *++str - 1; + people.setTalking(_speaker); + pullSequence(); + pushSequence(_speaker); + people.setTalkSequence(_speaker); + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdGotoScene(const byte *&str) { + ScalpelMap &map = *(ScalpelMap *)_vm->_map; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + scene._goToScene = str[1] - 1; + + if (scene._goToScene != OVERHEAD_MAP) { + // Not going to the map overview + map._oldCharPoint = scene._goToScene; + map._overPos.x = (map[scene._goToScene].x - 6) * FIXED_INT_MULTIPLIER; + map._overPos.y = (map[scene._goToScene].y + 9) * FIXED_INT_MULTIPLIER; + + // Run a canimation? + if (str[2] > 100) { + people._savedPos = PositionFacing(160, 100, str[2]); + } else { + int32 posX = (str[3] - 1) * 256 + str[4] - 1; + int32 posY = str[5] - 1; + people._savedPos = PositionFacing(posX, posY, str[2] - 1); + } + } + + str += 6; + + _scriptMoreFlag = (scene._goToScene == 100) ? 2 : 1; + _scriptSaveIndex = str - _scriptStart; + _endStr = true; + _wait = 0; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdAssignPortraitLocation(const byte *&str) { + People &people = *_vm->_people; + + ++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; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdClearInfoLine(const byte *&str) { + UserInterface &ui = *_vm->_ui; + + ui._infoFlag = true; + ui.clearInfo(); + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdClearWindow(const byte *&str) { + UserInterface &ui = *_vm->_ui; + + ui.clearWindow(); + _yp = CONTROLS_Y + 12; + _charCount = _line = 0; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdDisplayInfoLine(const byte *&str) { + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + Common::String tempString; + + ++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; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdElse(const byte *&str) { + // 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] != _opcodes[OP_END_IF_STATEMENT]); + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdIf(const byte *&str) { + ++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] != _opcodes[OP_ELSE_STATEMENT] && str[0] != _opcodes[OP_END_IF_STATEMENT]); + + if (!str[0]) + _endStr = true; + } + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdMoveMouse(const byte *&str) { + Events &events = *_vm->_events; + + ++str; + events.moveMouse(Common::Point((str[0] - 1) * 256 + str[1] - 1, str[2])); + if (_talkToAbort) + return RET_EXIT; + str += 3; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdPlayPrologue(const byte *&str) { + Animation &anim = *_vm->_animation; + Common::String tempString; + + ++str; + for (int idx = 0; idx < 8 && str[idx] != '~'; ++idx) + tempString += str[idx]; + + anim.play(tempString, false, 1, 3, true, 4); + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdRemovePortrait(const byte *&str) { + People &people = *_vm->_people; + + if (_speaker >= 0 && _speaker < SPEAKER_REMOVE) + people.clearTalking(); + pullSequence(); + if (_talkToAbort) + return RET_EXIT; + + _speaker |= SPEAKER_REMOVE; + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdWalkToCoords(const byte *&str) { + People &people = *_vm->_people; + ++str; + + people[HOLMES].walkToCoords(Point32(((str[0] - 1) * 256 + str[1] - 1) * FIXED_INT_MULTIPLIER, + str[2] * FIXED_INT_MULTIPLIER), str[3] - 1); + if (_talkToAbort) + return RET_EXIT; + + str += 3; + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdSfxCommand(const byte *&str) { + Sound &sound = *_vm->_sound; + Common::String tempString; + + ++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; + + return RET_SUCCESS; +} + +OpcodeReturn ScalpelTalk::cmdSummonWindow(const byte *&str) { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + + drawInterface(); + events._pressed = events._released = false; + events.clearKeyboard(); + _noTextYet = false; + + if (_speaker != -1) { + Common::String fixedText_Exit = fixedText.getText(kFixedText_Window_Exit); + Common::String fixedText_Up = fixedText.getText(kFixedText_Window_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Window_Down); + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, false, fixedText_Exit); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, false, fixedText_Up); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, fixedText_Down); + } + + return RET_SUCCESS; +} + +void ScalpelTalk::talkWait(const byte *&str) { + UserInterface &ui = *_vm->_ui; + bool pauseFlag = _pauseFlag; + + Talk::talkWait(str); + + // Clear the window unless the wait was due to a PAUSE command + if (!pauseFlag && _wait != -1 && str < _scriptEnd && str[0] != _opcodes[OP_SFX_COMMAND]) { + if (!_talkStealth) + ui.clearWindow(); + _yp = CONTROLS_Y + 12; + _charCount = _line = 0; + } +} + +void ScalpelTalk::talk3DOMovieTrigger(int subIndex) { + if (!IS_3DO) { + // No 3DO? No movie! + return; + } + + // Find out a few things that we need + int userSelector = _vm->_ui->_selector; + int scriptSelector = _scriptSelect; + int selector = 0; + int roomNr = _vm->_scene->_currentScene; + + if (userSelector >= 0) { + // User-selected dialog + selector = userSelector; + } else { + if (scriptSelector >= 0) { + // Script-selected dialog + selector = scriptSelector; + subIndex--; // for scripts we adjust subIndex, b/c we won't get called from doTalkControl() + } else { + warning("talk3DOMovieTrigger: unable to find selector"); + return; + } + } + + // Make a quick update, so that current text is shown on screen + _vm->_screen->update(); + + // Figure out that movie filename + Common::String movieFilename; + + movieFilename = _scriptName; + movieFilename.deleteChar(1); // remove 2nd character of scriptname + // cut scriptname to 6 characters + while (movieFilename.size() > 6) { + movieFilename.deleteChar(6); + } + + movieFilename.insertChar(selector + 'a', movieFilename.size()); + movieFilename.insertChar(subIndex + 'a', movieFilename.size()); + movieFilename = Common::String::format("movies/%02d/%s.stream", roomNr, movieFilename.c_str()); + + warning("3DO movie player:"); + warning("room: %d", roomNr); + warning("script: %s", _scriptName.c_str()); + warning("selector: %d", selector); + warning("subindex: %d", subIndex); + + Scalpel3DOMoviePlay(movieFilename.c_str(), Common::Point(5, 5)); + + // Restore screen HACK + _vm->_screen->makeAllDirty(); +} + +void ScalpelTalk::drawInterface() { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_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) { + Common::String fixedText_Exit = fixedText.getText(kFixedText_Window_Exit); + Common::String fixedText_Up = fixedText.getText(kFixedText_Window_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Window_Down); + + screen.makeButton(Common::Rect(99, CONTROLS_Y, 139, CONTROLS_Y + 10), + 119 - screen.stringWidth(fixedText_Exit) / 2, fixedText_Exit); + screen.makeButton(Common::Rect(140, CONTROLS_Y, 180, CONTROLS_Y + 10), + 159 - screen.stringWidth(fixedText_Up) / 2, fixedText_Up); + screen.makeButton(Common::Rect(181, CONTROLS_Y, 221, CONTROLS_Y + 10), + 200 - screen.stringWidth(fixedText_Down) / 2, fixedText_Down); + } else { + int strWidth = screen.stringWidth(Scalpel::PRESS_KEY_TO_CONTINUE); + screen.makeButton(Common::Rect(46, CONTROLS_Y, 273, CONTROLS_Y + 10), + 160 - strWidth / 2, Scalpel::PRESS_KEY_TO_CONTINUE); + screen.gPrint(Common::Point(160 - strWidth / 2, CONTROLS_Y), COMMAND_FOREGROUND, "P"); + } +} + +bool ScalpelTalk::displayTalk(bool slamIt) { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_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 + Common::String fixedText_Up = fixedText.getText(kFixedText_Window_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Window_Down); + if (_moreTalkUp) { + if (slamIt) { + screen.print(Common::Point(5, CONTROLS_Y + 13), INV_FOREGROUND, "~"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, true, fixedText_Up); + } else { + screen.gPrint(Common::Point(5, CONTROLS_Y + 12), INV_FOREGROUND, "~"); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, false, fixedText_Up); + } + } else { + if (slamIt) { + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, true, fixedText_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, fixedText_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 ? (byte)TALK_NULL : (byte)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, fixedText_Down); + } else { + screen.gPrint(Common::Point(5, 189), INV_FOREGROUND, "|"); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_FOREGROUND, false, fixedText_Down); + } + } else { + if (slamIt) { + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, true, fixedText_Down); + screen.vgaBar(Common::Rect(5, 189, 16, 199), INV_BACKGROUND); + } else { + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, false, fixedText_Down); + screen._backBuffer1.fillRect(Common::Rect(5, 189, 16, 199), INV_BACKGROUND); + } + } + + return done; +} + +int ScalpelTalk::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 ScalpelTalk::showTalk() { + FixedText &fixedText = *_vm->_fixedText; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + ScalpelUserInterface &ui = *(ScalpelUserInterface *)_vm->_ui; + Common::String fixedText_Exit = fixedText.getText(kFixedText_Window_Exit); + byte color = ui._endKeyActive ? COMMAND_FOREGROUND : COMMAND_NULL; + + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + + ui._selector = ui._oldSelector = -1; + + if (!ui._windowOpen) { + // Draw the talk interface on the back buffer + drawInterface(); + displayTalk(false); + } else { + displayTalk(true); + } + + // 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, fixedText_Exit); + } else { + screen.buttonPrint(Common::Point(119, CONTROLS_Y), color, false, fixedText_Exit); + + if (!ui._slideWindows) { + screen.slamRect(Common::Rect(0, CONTROLS_Y, + SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + } else { + ui.summonWindow(); + } + + ui._windowOpen = true; + } +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_talk.h b/engines/sherlock/scalpel/scalpel_talk.h new file mode 100644 index 0000000000..24188d8fcd --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_talk.h @@ -0,0 +1,99 @@ +/* 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_TALK_H +#define SHERLOCK_SCALPEL_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" +#include "sherlock/talk.h" + +namespace Sherlock { + +namespace Scalpel { + +class ScalpelTalk : public Talk { +private: + OpcodeReturn cmdSwitchSpeaker(const byte *&str); + OpcodeReturn cmdAssignPortraitLocation(const byte *&str); + OpcodeReturn cmdGotoScene(const byte *&str); + OpcodeReturn cmdClearInfoLine(const byte *&str); + OpcodeReturn cmdClearWindow(const byte *&str); + OpcodeReturn cmdDisplayInfoLine(const byte *&str); + OpcodeReturn cmdElse(const byte *&str); + OpcodeReturn cmdIf(const byte *&str); + OpcodeReturn cmdMoveMouse(const byte *&str); + OpcodeReturn cmdPlayPrologue(const byte *&str); + OpcodeReturn cmdRemovePortrait(const byte *&str); + OpcodeReturn cmdSfxCommand(const byte *&str); + OpcodeReturn cmdSummonWindow(const byte *&str); + OpcodeReturn cmdWalkToCoords(const byte *&str); +protected: + /** + * Display the talk interface window + */ + virtual void talkInterface(const byte *&str); + + /** + * Pause when displaying a talk dialog on-screen + */ + virtual void talkWait(const byte *&str); + + /** + * Trigger to play a 3DO talk dialog movie + */ + virtual void talk3DOMovieTrigger(int subIndex); + + /** + * Show the talk display + */ + virtual void showTalk(); +public: + ScalpelTalk(SherlockEngine *vm); + virtual ~ScalpelTalk() {} + + /** + * Draws the interface for conversation display + */ + void drawInterface(); + + /** + * 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); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/scalpel_user_interface.cpp b/engines/sherlock/scalpel/scalpel_user_interface.cpp new file mode 100644 index 0000000000..8fcc92b2ac --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_user_interface.cpp @@ -0,0 +1,2189 @@ +/* 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_user_interface.h" +#include "sherlock/scalpel/scalpel_fixed_text.h" +#include "sherlock/scalpel/scalpel_inventory.h" +#include "sherlock/scalpel/scalpel_journal.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_saveload.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/settings.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Scalpel { + +// 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 int UI_OFFSET_3DO = 16; // (320 - 288) / 2 + +/*----------------------------------------------------------------*/ + + +ScalpelUserInterface::ScalpelUserInterface(SherlockEngine *vm): UserInterface(vm) { + if (_vm->_interactiveFl) { + if (!IS_3DO) { + // PC + _controls = new ImageFile("menu.all"); + _controlPanel = new ImageFile("controls.vgs"); + } else { + // 3DO + _controls = new ImageFile3DO("menu.all", kImageFile3DOType_RoomFormat); + _controlPanel = new ImageFile3DO("controls.vgs", kImageFile3DOType_RoomFormat); + } + } else { + _controls = nullptr; + _controlPanel = nullptr; + } + + _keyPress = '\0'; + _lookHelp = 0; + _help = _oldHelp = 0; + _key = _oldKey = '\0'; + _temp = _oldTemp = 0; + _oldLook = 0; + _keyboardInput = false; + _pause = false; + _cNum = 0; + _find = 0; + _oldUse = 0; +} + +ScalpelUserInterface::~ScalpelUserInterface() { + delete _controls; + delete _controlPanel; +} + +void ScalpelUserInterface::reset() { + UserInterface::reset(); + _help = _oldHelp = -1; +} + +void ScalpelUserInterface::drawInterface(int bufferNum) { + Screen &screen = *_vm->_screen; + + const ImageFrame &src = (*_controlPanel)[0]; + int16 x = (!IS_3DO) ? 0 : UI_OFFSET_3DO; + + if (bufferNum & 1) + screen._backBuffer1.transBlitFrom(src, Common::Point(x, CONTROLS_Y)); + if (bufferNum & 2) + screen._backBuffer2.transBlitFrom(src, Common::Point(x, CONTROLS_Y)); + if (bufferNum == 3) + screen._backBuffer2.fillRect(0, INFO_LINE, SHERLOCK_SCREEN_WIDTH, INFO_LINE + 10, INFO_BLACK); +} + +void ScalpelUserInterface::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(pt); + _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._allowWalkAbort = false; + people[HOLMES]._walkDest = pt; + people[HOLMES].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 ScalpelUserInterface::depressButton(int num) { + Screen &screen = *_vm->_screen; + Common::Point pt(MENU_POINTS[num][0], MENU_POINTS[num][1]); + offsetButton3DO(pt, num); + + ImageFrame &frame = (*_controls)[num]; + screen._backBuffer1.transBlitFrom(frame, pt); + screen.slamArea(pt.x, pt.y, pt.x + frame._width, pt.y + frame._height); +} + +void ScalpelUserInterface::restoreButton(int num) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point pt(MENU_POINTS[num][0], MENU_POINTS[num][1]); + offsetButton3DO(pt, num); + + Graphics::Surface &frame = (*_controls)[num]._frame; + + // Reset the cursor + events.setCursor(ARROW); + + // Restore the UI on the back buffer + 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 ScalpelUserInterface::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 ScalpelUserInterface::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]); + offsetButton3DO(pt, num); + 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 ScalpelUserInterface::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 ScalpelUserInterface::clearWindow() { + if (_windowOpen) { + _vm->_screen->vgaBar(Common::Rect(3, CONTROLS_Y + 11, SHERLOCK_SCREEN_WIDTH - 2, + SHERLOCK_SCREEN_HEIGHT - 2), INV_BACKGROUND); + } +} + +void ScalpelUserInterface::whileMenuCounter() { + if (!(--_menuCounter) || _vm->_events->checkInput()) { + _menuCounter = 0; + _infoFlag = true; + clearInfo(); + } +} + +void ScalpelUserInterface::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[HOLMES].walkToCoords(obj._lookPosition, obj._lookPosition._facing); + } + + 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 ScalpelUserInterface::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._rightReleased) && !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 ScalpelUserInterface::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 ScalpelUserInterface::doEnvControl() { + Events &events = *_vm->_events; + ScalpelSaveManager &saves = *(ScalpelSaveManager *)_vm->_saves; + Scene &scene = *_vm->_scene; + ScalpelScreen &screen = *(ScalpelScreen *)_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 ScalpelUserInterface::doInvControl() { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + ScalpelInventory &inv = *(ScalpelInventory *)_vm->_inventory; + Scene &scene = *_vm->_scene; + ScalpelScreen &screen = *(ScalpelScreen *)_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(); + + Common::String fixedText_Exit = fixedText.getText(kFixedText_Inventory_Exit); + Common::String fixedText_Look = fixedText.getText(kFixedText_Inventory_Look); + Common::String fixedText_Use = fixedText.getText(kFixedText_Inventory_Use); + Common::String fixedText_Give = fixedText.getText(kFixedText_Inventory_Give); + + 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, fixedText_Exit); + + if (found >= 0 && found <= 3) { + screen.buttonPrint(Common::Point(INVENTORY_POINTS[1][2], CONTROLS_Y1), colors[1], true, fixedText_Look); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[2][2], CONTROLS_Y1), colors[2], true, fixedText_Use); + screen.buttonPrint(Common::Point(INVENTORY_POINTS[3][2], CONTROLS_Y1), colors[3], true, fixedText_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, BUTTON_BACKGROUND); + + _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, kFixedTextAction_Use, _find, giveFl); + else + // Now inv object has been highlighted + checkUseAction(&scene._bgShapes[_find]._use[0], "*SELF*", kFixedTextAction_Use, _find, giveFl); + + _selector = _oldSelector = -1; + } + } + } + } +} + +void ScalpelUserInterface::doLookControl() { + Events &events = *_vm->_events; + ScalpelInventory &inv = *(ScalpelInventory *)_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]); + offsetButton3DO(pt, 0); + 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 ScalpelUserInterface::doMainControl() { + Events &events = *_vm->_events; + ScalpelInventory &inv = *(ScalpelInventory *)_vm->_inventory; + ScalpelSaveManager &saves = *(ScalpelSaveManager *)_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 (IS_3DO && _temp >= 0 && _temp <= 2) { + r.left += UI_OFFSET_3DO - 1; + r.right += UI_OFFSET_3DO - 1; + } + 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 ScalpelUserInterface::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, _temp, kFixedTextAction_Open); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(OPEN_MODE - 1); + _key = _oldKey = -1; + } + break; + + case ALLOW_CLOSE: + checkAction(obj._aClose, _temp, kFixedTextAction_Close); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(CLOSE_MODE - 1); + _key = _oldKey = -1; + } + break; + + case ALLOW_MOVE: + checkAction(obj._aMove, _temp, kFixedTextAction_Move); + if (_menuMode != TALK_MODE && !talk._talkToAbort) { + _menuMode = STD_MODE; + restoreButton(MOVE_MODE - 1); + _key = _oldKey = -1; + } + break; + + default: + break; + } + } + } + } +} + +void ScalpelUserInterface::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(kFixedTextAction_Pick); + + if (!talk._talkToAbort && _menuMode != TALK_MODE) { + _key = _oldKey = -1; + _menuMode = STD_MODE; + restoreButton(PICKUP_MODE - 1); + } + } + } + } +} + +void ScalpelUserInterface::doTalkControl() { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + ScalpelJournal &journal = *(ScalpelJournal *)_vm->_journal; + ScalpelPeople &people = *(ScalpelPeople *)_vm->_people; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + Sound &sound = *_vm->_sound; + Talk &talk = *_vm->_talk; + Common::Point mousePos = events.mousePos(); + + _key = _oldKey = -1; + _keyboardInput = false; + + Common::String fixedText_Exit = fixedText.getText(kFixedText_Window_Exit); + Common::String fixedText_Up = fixedText.getText(kFixedText_Window_Up); + Common::String fixedText_Down = fixedText.getText(kFixedText_Window_Down); + + 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, fixedText_Exit); + else if (_endKeyActive) + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_FOREGROUND, true, fixedText_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, fixedText_Up); + else if (talk._moreTalkUp) + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_FOREGROUND, true, fixedText_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, fixedText_Down); + else if (talk._moreTalkDown) + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_FOREGROUND, true, fixedText_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(); + + drawInterface(2); + 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, fixedText_Exit); + screen.buttonPrint(Common::Point(159, CONTROLS_Y), COMMAND_NULL, true, fixedText_Up); + screen.buttonPrint(Common::Point(200, CONTROLS_Y), COMMAND_NULL, true, fixedText_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; + } + + // Trigger to play 3DO movie + talk.talk3DOMovieTrigger(0); + + 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, fixedText_Exit); + else + screen.buttonPrint(Common::Point(119, CONTROLS_Y), COMMAND_NULL, true, fixedText_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 ScalpelUserInterface::journalControl() { + Events &events = *_vm->_events; + ScalpelJournal &journal = *(ScalpelJournal *)_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 ScalpelUserInterface::printObjectDesc(const Common::String &str, bool firstTime) { + Events &events = *_vm->_events; + ScalpelInventory &inv = *(ScalpelInventory *)_vm->_inventory; + ScalpelScreen &screen = *(ScalpelScreen *)_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]); + offsetButton3DO(pt, 0); + + 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 ScalpelUserInterface::printObjectDesc() { + printObjectDesc(_cAnimStr, true); +} + +void ScalpelUserInterface::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 ScalpelUserInterface::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 ScalpelUserInterface::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 ScalpelUserInterface::checkUseAction(const UseType *use, const Common::String &invName, + FixedTextActionId fixedTextActionId, int objNum, bool giveMode) { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + Inventory &inv = *_vm->_inventory; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + bool printed = fixedTextActionId == kFixedTextAction_Invalid; + + 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], fixedTextActionId)) { + 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 (fixedTextActionId == kFixedTextAction_Invalid) { + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "You can't do that."); + } else { + Common::String errorMessage = fixedText.getActionMessage(fixedTextActionId, 0); + screen.print(Common::Point(0, INFO_LINE + 1), INFO_FOREGROUND, "%s", errorMessage.c_str()); + } + + _infoFlag = true; + _menuCounter = 30; + } + + events.setCursor(ARROW); +} + +void ScalpelUserInterface::offsetButton3DO(Common::Point &pt, int num) { + if (IS_3DO) { + if (num >= 0 && num <= 2) + pt.x += 15; + else if (num >= 6 && num <= 8) + pt.x -= 4; + else if (num >= 9 && num <= 11) + pt.x -= 8; + } +} + +} // End of namespace Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/scalpel_user_interface.h b/engines/sherlock/scalpel/scalpel_user_interface.h new file mode 100644 index 0000000000..7829ffca9f --- /dev/null +++ b/engines/sherlock/scalpel/scalpel_user_interface.h @@ -0,0 +1,223 @@ +/* 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_UI_H +#define SHERLOCK_SCALPEL_UI_H + +#include "common/scummsys.h" +#include "sherlock/user_interface.h" + +namespace Sherlock { + +class Inventory; +class Talk; + +namespace Scalpel { + +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 Settings; + +class ScalpelUserInterface: public UserInterface { + friend class Settings; + friend class Talk; +private: + char _keyPress; + int _lookHelp; + int _help, _oldHelp; + int _key, _oldKey; + int _temp, _oldTemp; + int _oldLook; + bool _keyboardInput; + bool _pause; + int _cNum; + Common::String _cAnimStr; + Common::String _descStr; + int _find; +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); + + /** + * 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, FixedTextActionId fixedTextActionId, + int objNum, bool giveMode); + + /** + * Print the previously selected object's decription + */ + void printObjectDesc(const Common::String &str, bool firstTime); +public: + ImageFile *_controlPanel; + ImageFile *_controls; + int _oldUse; +public: + ScalpelUserInterface(SherlockEngine *vm); + virtual ~ScalpelUserInterface(); + + /** + * Handles counting down whilst checking for input, then clears the info line. + */ + void whileMenuCounter(); + + /** + * Draws the image for the given user interface button in the up + * (not selected) position + */ + void restoreButton(int num); + + /** + * Creates a text window and uses it to display the in-depth description + * of the highlighted object + */ + void examine(); + + void offsetButton3DO(Common::Point &pt, int num); +public: + /** + * Resets the user interface + */ + virtual void reset(); + + /** + * Main input handler for the user interface + */ + virtual void handleInput(); + + /** + * Draw the user interface onto the screen's back buffers + */ + virtual void drawInterface(int bufferNum = 3); + + /** + * Displays a passed window by gradually scrolling it vertically on-screen + */ + virtual void summonWindow(const Surface &bgSurface, bool slideUp = true); + + /** + * Slide the window stored in the back buffer onto the screen + */ + virtual 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 + */ + virtual void banishWindow(bool slideUp = true); + + /** + * Clears the info line of the screen + */ + virtual void clearInfo(); + + /** + * Clear any active text window + */ + virtual void clearWindow(); + + /** + * Print the previously selected object's decription + */ + virtual void printObjectDesc(); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/settings.cpp b/engines/sherlock/scalpel/settings.cpp new file mode 100644 index 0000000000..f6769a4b99 --- /dev/null +++ b/engines/sherlock/scalpel/settings.cpp @@ -0,0 +1,343 @@ +/* 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/scalpel/settings.h" +#include "sherlock/scalpel/scalpel_screen.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { + +namespace Scalpel { + +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_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; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + Sound &sound = *_vm->_sound; + Music &music = *_vm->_music; + 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[music._musicOn]); + 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; + ScalpelScreen &screen = *(ScalpelScreen *)_vm->_screen; + Music &music = *_vm->_music; + 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._pressed || 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[music._musicOn]); + 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; + Music &music = *vm->_music; + Talk &talk = *vm->_talk; + ScalpelUserInterface &ui = *(ScalpelUserInterface *)vm->_ui; + bool updateConfig = false; + + assert(vm->getGameID() == GType_SerratedScalpel); + 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 + music._musicOn = !music._musicOn; + if (!music._musicOn) + music.stopMusic(); + else + music.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 Scalpel + +} // End of namespace Sherlock diff --git a/engines/sherlock/scalpel/settings.h b/engines/sherlock/scalpel/settings.h new file mode 100644 index 0000000000..ff2e647a62 --- /dev/null +++ b/engines/sherlock/scalpel/settings.h @@ -0,0 +1,63 @@ +/* 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; + +namespace Scalpel { + +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 Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/tsage/logo.cpp b/engines/sherlock/scalpel/tsage/logo.cpp new file mode 100644 index 0000000000..4eab01947a --- /dev/null +++ b/engines/sherlock/scalpel/tsage/logo.cpp @@ -0,0 +1,684 @@ +/* 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 "sherlock/scalpel/tsage/logo.h" +#include "sherlock/scalpel/scalpel.h" + +namespace Sherlock { +namespace Scalpel { +namespace TsAGE { + +TLib *Visage::_tLib; + +Visage::Visage() { + _resNum = -1; + _rlbNum = -1; + _stream = nullptr; +} + +void Visage::setVisage(int resNum, int rlbNum) { + if ((_resNum != resNum) || (_rlbNum != rlbNum)) { + _resNum = resNum; + _rlbNum = rlbNum; + delete _stream; + + // Games after Ringworld have an extra indirection via the visage index file + Common::SeekableReadStream *stream = _tLib->getResource(RES_VISAGE, resNum, 9999); + if (rlbNum == 0) + rlbNum = 1; + + // Check how many slots there are + uint16 count = stream->readUint16LE(); + if (rlbNum > count) + rlbNum = count; + + // Get the flags/rlbNum to use + stream->seek((rlbNum - 1) * 4 + 2); + uint32 v = stream->readUint32LE(); + int flags = v >> 30; + + if (flags & 3) { + rlbNum = (int)(v & 0xff); + } + assert((flags & 3) == 0); + delete stream; + + _stream = _tLib->getResource(RES_VISAGE, resNum, rlbNum); + } +} + +void Visage::clear() { + delete _stream; + _stream = nullptr; +} + +Visage::~Visage() { + delete _stream; +} + +void Visage::getFrame(ObjectSurface &s, int frameNum) { + _stream->seek(0); + int numFrames = _stream->readUint16LE(); + if (frameNum > numFrames) + frameNum = numFrames; + if (frameNum > 0) + --frameNum; + + _stream->seek(frameNum * 4 + 2); + int offset = _stream->readUint32LE(); + _stream->seek(offset); + + surfaceFromRes(s); +} + +int Visage::getFrameCount() const { + _stream->seek(0); + return _stream->readUint16LE(); +} + +bool Visage::isLoaded() const { + return _stream != nullptr; +} + +void Visage::surfaceFromRes(ObjectSurface &s) { + int frameWidth = _stream->readUint16LE(); + int frameHeight = _stream->readUint16LE(); + Common::Rect r(0, 0, frameWidth, frameHeight); + s.create(r.width(), r.height()); + + s._centroid.x = _stream->readSint16LE(); + s._centroid.y = _stream->readSint16LE(); + + _stream->skip(1); + byte flags = _stream->readByte(); + bool rleEncoded = (flags & 2) != 0; + + byte *destP = (byte *)s.getPixels(); + + if (!rleEncoded) { + _stream->read(destP, r.width() * r.height()); + } else { + Common::fill(destP, destP + (r.width() * r.height()), 0xff); + + for (int yp = 0; yp < r.height(); ++yp) { + int width = r.width(); + destP = (byte *)s.getBasePtr(0, yp); + + while (width > 0) { + uint8 controlVal = _stream->readByte(); + if ((controlVal & 0x80) == 0) { + // Copy specified number of bytes + _stream->read(destP, controlVal); + width -= controlVal; + destP += controlVal; + } else if ((controlVal & 0x40) == 0) { + // Skip a specified number of output pixels + destP += controlVal & 0x3f; + width -= controlVal & 0x3f; + } else { + // Copy a specified pixel a given number of times + controlVal &= 0x3f; + int pixel = _stream->readByte(); + + Common::fill(destP, destP + controlVal, pixel); + destP += controlVal; + width -= controlVal; + } + } + assert(width == 0); + } + } +} + +/*--------------------------------------------------------------------------*/ + +ScalpelEngine *Object::_vm; + +Object::Object() { + _vm = nullptr; + _isAnimating = _finished = false; + _frame = 0; + _numFrames = 0; + _frameChange = 0; + _angle = _changeCtr = 0; + _walkStartFrame = 0; + _majorDiff = _minorDiff = 0; +} + +void Object::setVisage(int visage, int strip) { + _visage.setVisage(visage, strip); +} + +void Object::setAnimMode(bool isAnimating) { + _isAnimating = isAnimating; + _finished = false; + + _updateStartFrame = _vm->_events->getFrameCounter(); + if (_numFrames) + _updateStartFrame += 60 / _numFrames; + _frameChange = 1; +} + +void Object::setDestination(const Common::Point &pt) { + _destination = pt; + + int moveRate = 10; + _walkStartFrame = _vm->_events->getFrameCounter(); + _walkStartFrame += 60 / moveRate; + + calculateMoveAngle(); + + // Get the difference + int diffX = _destination.x - _position.x; + int diffY = _destination.y - _position.y; + int xSign = (diffX < 0) ? -1 : (diffX > 0 ? 1 : 0); + int ySign = (diffY < 0) ? -1 : (diffY > 0 ? 1 : 0); + diffX = ABS(diffX); + diffY = ABS(diffY); + + if (diffX < diffY) { + _minorDiff = diffX / 2; + _majorDiff = diffY; + } else { + _minorDiff = diffY / 2; + _majorDiff = diffX; + } + + // Set the destination position + _moveDelta = Common::Point(diffX, diffY); + _moveSign = Common::Point(xSign, ySign); + _changeCtr = 0; + + assert(diffX || diffY); +} + +void Object::erase() { + Screen &screen = *_vm->_screen; + + if (_visage.isLoaded() && !_oldBounds.isEmpty()) + screen.blitFrom(screen._backBuffer1, Common::Point(_oldBounds.left, _oldBounds.top), _oldBounds); +} + +void Object::update() { + Screen &screen = *_vm->_screen; + + if (_visage.isLoaded()) { + if (isMoving()) { + uint32 currTime = _vm->_events->getFrameCounter(); + if (_walkStartFrame <= currTime) { + int moveRate = 10; + int frameInc = 60 / moveRate; + _walkStartFrame = currTime + frameInc; + move(); + } + } + + if (_isAnimating) { + if (_frame < _visage.getFrameCount()) + _frame = changeFrame(); + else + _finished = true; + } + + // Get the new frame + ObjectSurface s; + _visage.getFrame(s, _frame); + + // Display the frame + _oldBounds = Common::Rect(_position.x, _position.y, _position.x + s.w(), _position.y + s.h()); + _oldBounds.translate(-s._centroid.x, -s._centroid.y); + screen.transBlitFrom(s, Common::Point(_oldBounds.left, _oldBounds.top)); + } +} + +int Object::changeFrame() { + int frameNum = _frame; + uint32 currentFrame = _vm->_events->getFrameCounter(); + + if (_updateStartFrame <= currentFrame) { + if (_numFrames > 0) { + int v = 60 / _numFrames; + _updateStartFrame = currentFrame + v; + + frameNum = getNewFrame(); + } + } + + return frameNum; +} + +int Object::getNewFrame() { + int frameNum = _frame + _frameChange; + + if (_frameChange > 0) { + if (frameNum > _visage.getFrameCount()) { + frameNum = 1; + } + } else if (frameNum < 1) { + frameNum = _visage.getFrameCount(); + } + + return frameNum; +} + +void Object::calculateMoveAngle() { + int xDiff = _destination.x - _position.x, yDiff = _position.y - _destination.y; + + if (!xDiff && !yDiff) + _angle = 0; + else if (!xDiff) + _angle = (_destination.y >= _position.y) ? 180 : 0; + else if (!yDiff) + _angle = (_destination.x >= _position.x) ? 90 : 270; + else { + int result = (((xDiff * 100) / ((abs(xDiff) + abs(yDiff))) * 90) / 100); + + if (yDiff < 0) + result = 180 - result; + else if (xDiff < 0) + result += 360; + + _angle = result; + } +} + +bool Object::isAnimEnded() const { + return _finished; +} + +bool Object::isMoving() const { + return (_destination.x != 0) && (_destination != _position); +} + +void Object::move() { + Common::Point currPos = _position; + Common::Point moveDiff(5, 3); + int percent = 100; + + if (dontMove()) + return; + + if (_moveDelta.x >= _moveDelta.y) { + int xAmount = _moveSign.x * moveDiff.x * percent / 100; + if (!xAmount) + xAmount = _moveSign.x; + currPos.x += xAmount; + + int yAmount = ABS(_destination.y - currPos.y); + int yChange = _majorDiff / ABS(xAmount); + int ySign; + + if (!yChange) + ySign = _moveSign.y; + else { + int v = yAmount / yChange; + _changeCtr += yAmount % yChange; + if (_changeCtr >= yChange) { + ++v; + _changeCtr -= yChange; + } + + ySign = _moveSign.y * v; + } + + currPos.y += ySign; + _majorDiff -= ABS(xAmount); + } else { + int yAmount = _moveSign.y * moveDiff.y * percent / 100; + if (!yAmount) + yAmount = _moveSign.y; + currPos.y += yAmount; + + int xAmount = ABS(_destination.x - currPos.x); + int xChange = _majorDiff / ABS(yAmount); + int xSign; + + if (!xChange) + xSign = _moveSign.x; + else { + int v = xAmount / xChange; + _changeCtr += xAmount % xChange; + if (_changeCtr >= xChange) { + ++v; + _changeCtr -= xChange; + } + + xSign = _moveSign.x * v; + } + + currPos.x += xSign; + _majorDiff -= ABS(yAmount); + } + + _position = currPos; + if (dontMove()) + _position = _destination; +} + +bool Object::dontMove() const { + return (_majorDiff <= 0); +} + +void Object::endMove() { + _position = _destination; +} + +/*----------------------------------------------------------------*/ + +bool Logo::show(ScalpelEngine *vm) { + Events &events = *vm->_events; + Logo *logo = new Logo(vm); + bool interrupted = false; + + while (!logo->finished()) { + logo->nextFrame(); + + // Erase areas from previous frame, and update and re-draw objects + for (int idx = 0; idx < 4; ++idx) + logo->_objects[idx].erase(); + for (int idx = 0; idx < 4; ++idx) + logo->_objects[idx].update(); + + events.wait(2); + events.setButtonState(); + + interrupted = vm->shouldQuit() || events.kbHit() || events._pressed; + if (interrupted) { + // Keyboard or mouse button pressed, so break out of logo display + events.clearEvents(); + break; + } + } + + delete logo; + return !interrupted; +} + +Logo::Logo(ScalpelEngine *vm) : _vm(vm), _lib("sf3.rlb") { + Object::_vm = vm; + Visage::_tLib = &_lib; + + _finished = false; + + // Initialize counter + _counter = 0; + + // Initialize wait frame counters + _waitFrames = 0; + _waitStartFrame = 0; + + // Initialize animation counters + _animateObject = 0; + _animateStartFrame = 0; + _animateFrameDelay = 0; + _animateFrames = NULL; + _animateFrame = 0; + + // Save a copy of the original palette + _vm->_screen->getPalette(_originalPalette); + + // Set up the palettes + Common::fill(&_palette1[0], &_palette1[PALETTE_SIZE], 0); + Common::fill(&_palette1[0], &_palette2[PALETTE_SIZE], 0); + Common::fill(&_palette1[0], &_palette3[PALETTE_SIZE], 0); + + _lib.getPalette(_palette1, 1111); + _lib.getPalette(_palette1, 10); + _lib.getPalette(_palette2, 1111); + _lib.getPalette(_palette2, 1); + _lib.getPalette(_palette3, 1111); + _lib.getPalette(_palette3, 14); +} + +Logo::~Logo() { + // Restore the original palette + _vm->_screen->setPalette(_originalPalette); +} + +bool Logo::finished() const { + return _finished; +} + +const AnimationFrame handFrames[] = { + { 1, 33, 91 }, { 2, 44, 124 }, { 3, 64, 153 }, { 4, 87, 174 }, + { 5, 114, 191 }, { 6, 125, 184 }, { 7, 154, 187 }, { 8, 181, 182 }, + { 9, 191, 167 }, { 10, 190, 150 }, { 11, 182, 139 }, { 11, 170, 130 }, + { 11, 158, 121 }, { 0, 0, 0 } +}; + +const AnimationFrame companyFrames[] = { + { 1, 155, 94 }, { 2, 155, 94 }, { 3, 155, 94 }, { 4, 155, 94 }, + { 5, 155, 94 }, { 6, 155, 94 }, { 7, 155, 94 }, { 8, 155, 94 }, + { 0, 0, 0 } +}; + +void Logo::nextFrame() { + Screen &screen = *_vm->_screen; + + if (_waitFrames) { + uint32 currFrame = _vm->_events->getFrameCounter(); + if (currFrame - _waitStartFrame < _waitFrames) { + return; + } + _waitStartFrame = 0; + _waitFrames = 0; + } + + if (_animateFrames) { + uint32 currFrame = _vm->_events->getFrameCounter(); + if (currFrame > _animateStartFrame + _animateFrameDelay) { + AnimationFrame animationFrame = _animateFrames[_animateFrame]; + if (animationFrame.frame) { + _objects[_animateObject]._frame = animationFrame.frame; + _objects[_animateObject]._position = Common::Point(animationFrame.x, animationFrame.y); + _animateStartFrame += _animateFrameDelay; + _animateFrame++; + } else { + _animateObject = 0; + _animateFrameDelay = 0; + _animateFrames = NULL; + _animateStartFrame = 0; + _animateFrame = 0; + } + } + if (_animateFrames) + return; + } + + switch (_counter++) { + case 0: + // Load the background and fade it in + loadBackground(); + fade(_palette1); + break; + + case 1: + // First half of square, circle, and triangle arranging themselves + _objects[0].setVisage(16, 1); + _objects[0]._frame = 1; + _objects[0]._position = Common::Point(169, 107); + _objects[0]._numFrames = 7; + _objects[0].setAnimMode(true); + break; + + case 2: + // Keep waiting until first animation ends + if (!_objects[0].isAnimEnded()) { + --_counter; + } else { + // Start second half of the shapes animation + _objects[0].setVisage(16, 2); + _objects[0]._frame = 1; + _objects[0]._numFrames = 11; + _objects[0].setAnimMode(true); + } + break; + + case 3: + // Keep waiting until second animation of shapes ordering themselves ends + if (!_objects[0].isAnimEnded()) { + --_counter; + } else { + // Fade out the background but keep the shapes visible + fade(_palette2); + screen._backBuffer1.clear(); + } + waitFrames(10); + break; + + case 4: + // Load the new palette + byte palette[PALETTE_SIZE]; + Common::copy(&_palette2[0], &_palette2[PALETTE_SIZE], &palette[0]); + _lib.getPalette(palette, 12); + screen.clear(); + screen.setPalette(palette); + + // Morph into the EA logo + _objects[0].setVisage(12, 1); + _objects[0]._frame = 1; + _objects[0]._numFrames = 7; + _objects[0].setAnimMode(true); + _objects[0]._position = Common::Point(170, 142); + _objects[0].setDestination(Common::Point(158, 71)); + break; + + case 5: + // Wait until the logo has expanded upwards to form EA logo + if (_objects[0].isMoving()) + --_counter; + break; + + case 6: + fade(_palette3, 40); + break; + + case 7: + // Show the 'Electronic Arts' company name + _objects[1].setVisage(14, 1); + _objects[1]._frame = 1; + _objects[1]._position = Common::Point(152, 98); + waitFrames(120); + break; + + case 8: + // Start sequence of positioning and size hand cursor in an arc + _objects[2].setVisage(18, 1); + startAnimation(2, 5, &handFrames[0]); + break; + + case 9: + // Show a highlighting of the company name + _objects[1].remove(); + _objects[2].erase(); + _objects[2].remove(); + _objects[3].setVisage(19, 1); + startAnimation(3, 8, &companyFrames[0]); + break; + + case 10: + waitFrames(180); + break; + + case 11: + _finished = true; + break; + + default: + break; + } +} + +void Logo::waitFrames(uint frames) { + _waitFrames = frames; + _waitStartFrame = _vm->_events->getFrameCounter(); +} + +void Logo::startAnimation(uint object, uint frameDelay, const AnimationFrame *frames) { + _animateObject = object; + _animateFrameDelay = frameDelay; + _animateFrames = frames; + _animateStartFrame = _vm->_events->getFrameCounter(); + _animateFrame = 1; + + _objects[object]._frame = frames[0].frame; + _objects[object]._position = Common::Point(frames[0].x, frames[0].y); +} + +void Logo::loadBackground() { + Screen &screen = *_vm->_screen; + + for (int idx = 0; idx < 4; ++idx) { + // Get the portion of the screen + Common::SeekableReadStream *stream = _lib.getResource(RES_BITMAP, 10, idx); + + // Load it onto the surface + Common::Point pt((idx / 2) * (SHERLOCK_SCREEN_WIDTH / 2), (idx % 2) * (SHERLOCK_SCREEN_HEIGHT / 2)); + for (int y = 0; y < (SHERLOCK_SCREEN_HEIGHT / 2); ++y, ++pt.y) { + byte *pDest = (byte *)screen._backBuffer1.getBasePtr(pt.x, pt.y); + stream->read(pDest, SHERLOCK_SCREEN_WIDTH / 2); + } + + // _backgroundBounds = Rect(0, 0, READ_LE_UINT16(data), READ_LE_UINT16(data + 2)); + delete stream; + } + + // Default to a blank palette + byte palette[PALETTE_SIZE]; + Common::fill(&palette[0], &palette[PALETTE_SIZE], 0); + screen.setPalette(palette); + + // Copy the surface to the screen + screen.blitFrom(screen._backBuffer1); +} + +void Logo::fade(const byte palette[PALETTE_SIZE], int step) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + byte startPalette[PALETTE_SIZE]; + byte tempPalette[PALETTE_SIZE]; + + screen.getPalette(startPalette); + + for (int percent = 0; percent < 100; percent += step) { + for (int palIndex = 0; palIndex < 256; ++palIndex) { + const byte *pal1P = (const byte *)&startPalette[palIndex * 3]; + const byte *pal2P = (const byte *)&palette[palIndex * 3]; + byte *destP = &tempPalette[palIndex * 3]; + + for (int rgbIndex = 0; rgbIndex < 3; ++rgbIndex, ++pal1P, ++pal2P, ++destP) { + *destP = (int)*pal1P + ((int)*pal2P - (int)*pal1P) * percent / 100; + } + } + + screen.setPalette(tempPalette); + events.wait(1); + } + + // Set final palette + screen.setPalette(palette); +} + +} // end of namespace TsAGE +} // end of namespace Scalpel +} // end of namespace Sherlock diff --git a/engines/sherlock/scalpel/tsage/logo.h b/engines/sherlock/scalpel/tsage/logo.h new file mode 100644 index 0000000000..c9fac00d9c --- /dev/null +++ b/engines/sherlock/scalpel/tsage/logo.h @@ -0,0 +1,250 @@ +/* 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_TSAGE_LOGO_H +#define SHERLOCK_SCALPEL_TSAGE_LOGO_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/file.h" +#include "common/list.h" +#include "common/str.h" +#include "common/str-array.h" +#include "common/util.h" +#include "graphics/surface.h" +#include "sherlock/scalpel/tsage/resources.h" +#include "sherlock/screen.h" + +namespace Sherlock { +namespace Scalpel { + +class ScalpelEngine; + +namespace TsAGE { + +class ObjectSurface : public Surface { +public: + Common::Point _centroid; +public: + ObjectSurface() : Surface() {} + virtual ~ObjectSurface() {} +}; + +class Visage { +private: + Common::SeekableReadStream *_stream; + + /** + * Translates a raw image resource into a graphics surface + */ + void surfaceFromRes(ObjectSurface &s); +public: + static TLib *_tLib; + int _resNum; + int _rlbNum; +public: + Visage(); + ~Visage(); + + /** + * Set the visage number + */ + void setVisage(int resNum, int rlbNum = 9999); + + /** + * Clear the visage + */ + void clear(); + + /** + * Get a frame from the visage + */ + void getFrame(ObjectSurface &s, int frameNum); + + /** + * Return the number of frames + */ + int getFrameCount() const; + + /** + * Returns whether the visage is loaded + */ + bool isLoaded() const; +}; + +class Object { +private: + Visage _visage; + uint32 _updateStartFrame; + bool _isAnimating; + bool _finished; + uint32 _walkStartFrame; + int _angle; + int _changeCtr; + int _majorDiff, _minorDiff; + Common::Point _moveDelta; + Common::Point _moveSign; + + /** + * Return the next frame when the object is animating + */ + int changeFrame(); + + /** + * Gets the next frame in the sequence + */ + int getNewFrame(); + + /** + * Calculate the angle between the current position and a designated destination + */ + void calculateMoveAngle(); + + /** + * Handle any object movement + */ + void move(); + + /** + * Returns whether not to make any movement + */ + bool dontMove() const; + + /** + * Ends any current movement + */ + void endMove(); +public: + static ScalpelEngine *_vm; + Common::Point _position; + Common::Point _destination; + Common::Rect _oldBounds; + int _frame; + int _numFrames; + int _frameChange; +public: + Object(); + + /** + * Load the data for the object + */ + void setVisage(int visage, int strip); + + /** + * Sets whether the object is animating + */ + void setAnimMode(bool isAnimating); + + /** + * Starts an object moving to a given destination + */ + void setDestination(const Common::Point &pt); + + /** + * Returns true if an animation is ended + */ + bool isAnimEnded() const; + + /** + * Return true if object is moving + */ + bool isMoving() const; + + /** + * Erase the area the object was previously drawn at, by restoring the background + */ + void erase(); + + /** + * Update the frame + */ + void update(); + + /** + * Remove an object from being displayed + */ + void remove() { _visage.clear(); } +}; + +struct AnimationFrame { + int frame; + int x; + int y; +}; + +class Logo { +private: + ScalpelEngine *_vm; + TLib _lib; + int _counter; + bool _finished; + byte _originalPalette[PALETTE_SIZE]; + byte _palette1[PALETTE_SIZE]; + byte _palette2[PALETTE_SIZE]; + byte _palette3[PALETTE_SIZE]; + Object _objects[4]; + uint _waitFrames; + uint32 _waitStartFrame; + int _animateObject; + uint32 _animateStartFrame; + uint _animateFrameDelay; + const AnimationFrame *_animateFrames; + uint _animateFrame; + + Logo(ScalpelEngine *vm); + ~Logo(); + + void nextFrame(); + + bool finished() const; + + /** + * Wait for a number of frames. Note that the frame count in _events is + * not the same as the number of calls to nextFrame(). + */ + void waitFrames(uint frames); + + /** + * Start an animation sequence. Used for sequences that are described + * one frame at a time because they do unusual things, or run at + * unusual rates. + */ + void startAnimation(uint object, uint frameDelay, const AnimationFrame *frames); + + /** + * Load the background for the scene + */ + void loadBackground(); + + /** + * Fade from the current palette to a new one + */ + void fade(const byte palette[PALETTE_SIZE], int step = 6); +public: + static bool show(ScalpelEngine *vm); +}; + +} // end of namespace TsAGE +} // end of namespace Scalpel +} // end of namespace Sherlock + +#endif diff --git a/engines/sherlock/scalpel/tsage/resources.cpp b/engines/sherlock/scalpel/tsage/resources.cpp new file mode 100644 index 0000000000..56b7021563 --- /dev/null +++ b/engines/sherlock/scalpel/tsage/resources.cpp @@ -0,0 +1,373 @@ +/* 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/memstream.h" +#include "common/stack.h" +#include "sherlock/scalpel/tsage/resources.h" + +namespace Sherlock { +namespace Scalpel { +namespace TsAGE { + +static uint16 bitMasks[4] = {0x1ff, 0x3ff, 0x7ff, 0xfff}; + +uint16 BitReader::readToken() { + assert((numBits >= 9) && (numBits <= 12)); + uint16 result = _remainder; + int bitsLeft = numBits - _bitsLeft; + int bitOffset = _bitsLeft; + _bitsLeft = 0; + + while (bitsLeft >= 0) { + _remainder = readByte(); + result |= _remainder << bitOffset; + bitsLeft -= 8; + bitOffset += 8; + } + + _bitsLeft = -bitsLeft; + _remainder >>= 8 - _bitsLeft; + return result & bitMasks[numBits - 9]; +} + +/*-------------------------------------------------------------------------*/ + +TLib::TLib(const Common::String &filename) : _filename(filename) { + + // If the resource strings list isn't yet loaded, load them + if (_resStrings.size() == 0) { + Common::File f; + if (f.open("tsage.cfg")) { + while (!f.eos()) { + _resStrings.push_back(f.readLine()); + } + f.close(); + } + } + + if (!_file.open(filename)) + error("Missing file %s", filename.c_str()); + + loadIndex(); +} + +TLib::~TLib() { + _resStrings.clear(); +} + +/** + * Load a section index from the given position in the file + */ +void TLib::loadSection(uint32 fileOffset) { + _resources.clear(); + _file.seek(fileOffset); + _sections.fileOffset = fileOffset; + + loadSection(_file, _resources); +} + +struct DecodeReference { + uint16 vWord; + uint8 vByte; +}; + +/** + * Gets a resource from the currently loaded section + */ +Common::SeekableReadStream *TLib::getResource(uint16 id, bool suppressErrors) { + // Scan for an entry for the given Id + ResourceEntry *re = nullptr; + ResourceList::iterator iter; + for (iter = _resources.begin(); iter != _resources.end(); ++iter) { + if ((*iter).id == id) { + re = &(*iter); + break; + } + } + if (!re) { + if (suppressErrors) + return nullptr; + error("Could not find resource Id #%d", id); + } + + if (!re->isCompressed) { + // Read in the resource data and return it + byte *dataP = (byte *)malloc(re->size); + _file.seek(_sections.fileOffset + re->fileOffset); + _file.read(dataP, re->size); + + return new Common::MemoryReadStream(dataP, re->size, DisposeAfterUse::YES); + } + + /* + * Decompress the data block + */ + + _file.seek(_sections.fileOffset + re->fileOffset); + Common::ReadStream *compStream = _file.readStream(re->size); + BitReader bitReader(*compStream); + + byte *dataOut = (byte *)malloc(re->uncompressedSize); + byte *destP = dataOut; + uint bytesWritten = 0; + + uint16 ctrCurrent = 0x102, ctrMax = 0x200; + uint16 word_48050 = 0, currentToken = 0, word_48054 =0; + byte byte_49068 = 0, byte_49069 = 0; + + const uint tableSize = 0x1000; + DecodeReference *table = (DecodeReference *)malloc(tableSize * sizeof(DecodeReference)); + if (!table) + error("[TLib::getResource] Cannot allocate table buffer"); + + for (uint i = 0; i < tableSize; ++i) { + table[i].vByte = table[i].vWord = 0; + } + Common::Stack<uint16> tokenList; + + for (;;) { + // Get the next decode token + uint16 token = bitReader.readToken(); + + // Handle the token + if (token == 0x101) { + // End of compressed stream + break; + } else if (token == 0x100) { + // Reset bit-rate + bitReader.numBits = 9; + ctrMax = 0x200; + ctrCurrent = 0x102; + + // Set variables with next token + currentToken = word_48050 = bitReader.readToken(); + byte_49069 = byte_49068 = (byte)currentToken; + + ++bytesWritten; + assert(bytesWritten <= re->uncompressedSize); + *destP++ = byte_49069; + } else { + word_48054 = word_48050 = token; + + if (token >= ctrCurrent) { + word_48050 = currentToken; + tokenList.push(byte_49068); + } + + while (word_48050 >= 0x100) { + assert(word_48050 < 0x1000); + tokenList.push(table[word_48050].vByte); + word_48050 = table[word_48050].vWord; + } + + byte_49069 = byte_49068 = (byte)word_48050; + tokenList.push(word_48050); + + // Write out any cached tokens + while (!tokenList.empty()) { + ++bytesWritten; + assert(bytesWritten <= re->uncompressedSize); + *destP++ = tokenList.pop(); + } + + assert(ctrCurrent < 0x1000); + table[ctrCurrent].vByte = byte_49069; + table[ctrCurrent].vWord = currentToken; + ++ctrCurrent; + + currentToken = word_48054; + if ((ctrCurrent >= ctrMax) && (bitReader.numBits != 12)) { + // Move to the next higher bit-rate + ++bitReader.numBits; + ctrMax <<= 1; + } + } + } + + free(table); + + assert(bytesWritten == re->uncompressedSize); + delete compStream; + return new Common::MemoryReadStream(dataOut, re->uncompressedSize, DisposeAfterUse::YES); +} + +/** + * Finds the correct section and loads the specified resource within it + */ +Common::SeekableReadStream *TLib::getResource(ResourceType resType, uint16 resNum, uint16 rlbNum, bool suppressErrors) { + SectionList::iterator i = _sections.begin(); + while ((i != _sections.end()) && ((*i).resType != resType || (*i).resNum != resNum)) + ++i; + if (i == _sections.end()) { + if (suppressErrors) + return nullptr; + error("Unknown resource type %d num %d", resType, resNum); + } + + loadSection((*i).fileOffset); + + return getResource(rlbNum, suppressErrors); +} + +/** + * Gets the offset of the start of a resource in the resource file + */ +uint32 TLib::getResourceStart(ResourceType resType, uint16 resNum, uint16 rlbNum, ResourceEntry &entry) { + // Find the correct section + SectionList::iterator i = _sections.begin(); + while ((i != _sections.end()) && ((*i).resType != resType || (*i).resNum != resNum)) + ++i; + if (i == _sections.end()) { + error("Unknown resource type %d num %d", resType, resNum); + } + + // Load in the section index + loadSection((*i).fileOffset); + + // Scan for an entry for the given Id + ResourceEntry *re = nullptr; + ResourceList::iterator iter; + for (iter = _resources.begin(); iter != _resources.end(); ++iter) { + if ((*iter).id == rlbNum) { + re = &(*iter); + break; + } + } + + // Throw an error if no resource was found, or the resource is compressed + if (!re || re->isCompressed) + error("Invalid resource Id #%d", rlbNum); + + // Return the resource entry as well as the file offset + entry = *re; + return _sections.fileOffset + entry.fileOffset; +} + +void TLib::loadIndex() { + uint16 resNum, configId, fileOffset; + + // Load the root resources section + loadSection(0); + + // Get the single resource from it + Common::SeekableReadStream *stream = getResource(0); + + _sections.clear(); + + // Loop through reading the entries + while ((resNum = stream->readUint16LE()) != 0xffff) { + configId = stream->readUint16LE(); + fileOffset = stream->readUint16LE(); + + SectionEntry se; + se.resNum = resNum; + se.resType = (ResourceType)(configId & 0x1f); + se.fileOffset = (((configId >> 5) & 0x7ff) << 16) | fileOffset; + + _sections.push_back(se); + } + + delete stream; +} + +/** + * Retrieves the specified palette resource and returns it's data + * + * @paletteNum Specefies the palette number + */ +void TLib::getPalette(byte palette[PALETTE_SIZE], int paletteNum) { + // Get the specified palette + Common::SeekableReadStream *stream = getResource(RES_PALETTE, paletteNum, 0, true); + if (!stream) + return; + + int startNum = stream->readUint16LE(); + int numEntries = stream->readUint16LE(); + assert((startNum < 256) && ((startNum + numEntries) <= 256)); + stream->skip(2); + + // Copy over the data + stream->read(&palette[startNum * 3], numEntries * 3); + + delete stream; +} + +/** + * Open up the given resource file using a passed file object. If the desired entry is found + * in the index, return the index entry for it, and move the file to the start of the resource + */ +bool TLib::scanIndex(Common::File &f, ResourceType resType, int rlbNum, int resNum, + ResourceEntry &resEntry) { + // Load the root section index + ResourceList resList; + loadSection(f, resList); + + // Loop through the index for the desired entry + ResourceList::iterator iter; + for (iter = resList.begin(); iter != resList.end(); ++iter) { + ResourceEntry &re = *iter; + if (re.id == resNum) { + // Found it, so exit + resEntry = re; + f.seek(re.fileOffset); + return true; + } + } + + // No matching entry found + return false; +} + +/** + * Inner logic for decoding a section index into a passed resource list object + */ +void TLib::loadSection(Common::File &f, ResourceList &resources) { + if (f.readUint32BE() != 0x544D492D) + error("Data block is not valid Rlb data"); + + /*uint8 unknown1 = */f.readByte(); + uint16 numEntries = f.readByte(); + + for (uint i = 0; i < numEntries; ++i) { + uint16 id = f.readUint16LE(); + uint16 size = f.readUint16LE(); + uint16 uncSize = f.readUint16LE(); + uint8 sizeHi = f.readByte(); + uint8 type = f.readByte() >> 5; + assert(type <= 1); + uint32 offset = f.readUint32LE(); + + ResourceEntry re; + re.id = id; + re.fileOffset = offset; + re.isCompressed = type != 0; + re.size = ((sizeHi & 0xF) << 16) | size; + re.uncompressedSize = ((sizeHi & 0xF0) << 12) | uncSize; + + resources.push_back(re); + } +} + +} // end of namespace TsAGE +} // end of namespace Scalpel +} // end of namespace Sherlock diff --git a/engines/sherlock/scalpel/tsage/resources.h b/engines/sherlock/scalpel/tsage/resources.h new file mode 100644 index 0000000000..3e09b6b0b1 --- /dev/null +++ b/engines/sherlock/scalpel/tsage/resources.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_SCALPEL_TSAGE_RESOURCES_H +#define SHERLOCK_SCALPEL_TSAGE_RESOURCES_H + +#include "common/scummsys.h" +#include "common/array.h" +#include "common/file.h" +#include "common/list.h" +#include "common/str.h" +#include "common/str-array.h" +#include "common/util.h" +#include "graphics/surface.h" +#include "sherlock/screen.h" + +namespace Sherlock { +namespace Scalpel { +namespace TsAGE { + +// Magic number used by original game to identify valid memory blocks +const uint32 MEMORY_ENTRY_ID = 0xE11DA722; + +const int MEMORY_POOL_SIZE = 1000; + +enum ResourceType { RES_LIBRARY, RES_STRIP, RES_IMAGE, RES_PALETTE, RES_VISAGE, RES_SOUND, RES_MESSAGE, + RES_FONT, RES_POINTER, RES_BANK, RES_SND_DRIVER, RES_PRIORITY, RES_CONTROL, RES_WALKRGNS, + RES_BITMAP, RES_SAVE, RES_SEQUENCE, + // Return to Ringworld specific resource types + RT17, RT18, RT19, RT20, RT21, RT22, RT23, RT24, RT25, RT26, RT27, RT28, RT29, RT30, RT31 +}; + +class SectionEntry { +public: + ResourceType resType; + uint16 resNum; + uint32 fileOffset; + + SectionEntry() { + resType = RES_LIBRARY; + resNum = 0; + fileOffset = 0; + } +}; + +class ResourceEntry { +public: + uint16 id; + bool isCompressed; + uint32 fileOffset; + uint32 size; + uint32 uncompressedSize; + + ResourceEntry() { + id = 0; + isCompressed = false; + fileOffset = 0; + size = 0; + uncompressedSize = 0; + } +}; + +typedef Common::List<ResourceEntry> ResourceList; + +class SectionList : public Common::List<SectionEntry> { +public: + uint32 fileOffset; + + SectionList() { + fileOffset = 0; + } +}; + +class BitReader { +private: + Common::ReadStream &_stream; + uint8 _remainder, _bitsLeft; + byte readByte() { return _stream.eos() ? 0 : _stream.readByte(); } +public: + BitReader(Common::ReadStream &s) : _stream(s) { + numBits = 9; + _remainder = 0; + _bitsLeft = 0; + } + uint16 readToken(); + + int numBits; +}; + +class TLib { +private: + Common::StringArray _resStrings; +private: + Common::File _file; + Common::String _filename; + ResourceList _resources; + SectionList _sections; + + void loadSection(uint32 fileOffset); + void loadIndex(); + + static bool scanIndex(Common::File &f, ResourceType resType, int rlbNum, int resNum, ResourceEntry &resEntry); + static void loadSection(Common::File &f, ResourceList &resources); +public: + TLib(const Common::String &filename); + ~TLib(); + + const Common::String &getFilename() { return _filename; } + const SectionList &getSections() { return _sections; } + Common::SeekableReadStream *getResource(uint16 id, bool suppressErrors = false); + Common::SeekableReadStream *getResource(ResourceType resType, uint16 resNum, uint16 rlbNum, bool suppressErrors = false); + uint32 getResourceStart(ResourceType resType, uint16 resNum, uint16 rlbNum, ResourceEntry &entry); + void getPalette(byte palette[PALETTE_SIZE], int paletteNum); +}; + +} // end of namespace TsAGE +} // 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..328bf647d4 --- /dev/null +++ b/engines/sherlock/scene.cpp @@ -0,0 +1,1437 @@ +/* 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/screen.h" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_scene.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" + +namespace Sherlock { + +static const int FS_TRANS[8] = { + Scalpel::STOP_UP, Scalpel::STOP_UPRIGHT, Scalpel::STOP_RIGHT, Scalpel::STOP_DOWNRIGHT, + Scalpel::STOP_DOWN, Scalpel::STOP_DOWNLEFT, Scalpel::STOP_LEFT, Scalpel::STOP_UPLEFT +}; + +/*----------------------------------------------------------------*/ + +BgFileHeader::BgFileHeader() { + _numStructs = -1; + _numImages = -1; + _numcAnimations = -1; + _descSize = -1; + _seqSize = -1; + + // Serrated Scalpel + _fill = -1; + + // Rose Tattoo + _scrollSize = -1; + _bytesWritten = -1; + _fadeStyle = -1; + Common::fill(&_palette[0], &_palette[PALETTE_SIZE], 0); +} + +void BgFileHeader::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + _numStructs = s.readUint16LE(); + _numImages = s.readUint16LE(); + _numcAnimations = s.readUint16LE(); + _descSize = s.readUint16LE(); + _seqSize = s.readUint16LE(); + + if (isRoseTattoo) { + _scrollSize = s.readUint16LE(); + _bytesWritten = s.readUint32LE(); + _fadeStyle = s.readByte(); + } else { + _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 BgFileHeaderInfo::load3DO(Common::SeekableReadStream &s) { + _filesize = s.readUint32BE(); + _maxFrames = s.readByte(); + + char buffer[9]; + s.read(buffer, 9); + _filename = Common::String(buffer); + s.skip(2); // only on 3DO! +} + +/*----------------------------------------------------------------*/ + +void Exit::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + if (isRoseTattoo) { + char buffer[41]; + s.read(buffer, 41); + _dest = Common::String(buffer); + } + + left = s.readSint16LE(); + top = s.readSint16LE(); + setWidth(s.readUint16LE()); + setHeight(s.readUint16LE()); + + _image = isRoseTattoo ? s.readByte() : 0; + _scene = s.readSint16LE(); + + if (!isRoseTattoo) + _allow = s.readSint16LE(); + + _newPosition.x = s.readSint16LE(); + _newPosition.y = s.readSint16LE(); + _newPosition._facing = s.readUint16LE(); + + if (isRoseTattoo) + _allow = s.readSint16LE(); +} + +void Exit::load3DO(Common::SeekableReadStream &s) { + left = s.readSint16BE(); + top = s.readSint16BE(); + setWidth(s.readUint16BE()); + setHeight(s.readUint16BE()); + + _image = 0; + _scene = s.readSint16BE(); + + _allow = s.readSint16BE(); + + _newPosition.x = s.readSint16BE(); + _newPosition.y = s.readSint16BE(); + _newPosition._facing = s.readUint16BE(); + s.skip(2); // Filler +} + +/*----------------------------------------------------------------*/ + +void SceneEntry::load(Common::SeekableReadStream &s) { + _startPosition.x = s.readSint16LE(); + _startPosition.y = s.readSint16LE(); + _startDir = s.readByte(); + _allow = s.readByte(); +} + +void SceneEntry::load3DO(Common::SeekableReadStream &s) { + _startPosition.x = s.readSint16BE(); + _startPosition.y = s.readSint16BE(); + _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(); +} + +void SceneSound::load3DO(Common::SeekableReadStream &s) { + load(s); +} + +/*----------------------------------------------------------------*/ + +int ObjectArray::indexOf(const Object &obj) const { + for (uint idx = 0; idx < size(); ++idx) { + if (&(*this)[idx] == &obj) + return idx; + } + + return -1; +} + +/*----------------------------------------------------------------*/ + +void ScaleZone::load(Common::SeekableReadStream &s) { + left = s.readSint16LE(); + top = s.readSint16LE(); + setWidth(s.readUint16LE()); + setHeight(s.readUint16LE()); + + _topNumber = s.readByte(); + _bottomNumber = s.readByte(); +} + +/*----------------------------------------------------------------*/ + +void WalkArray::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + _pointsCount = (int8)s.readByte(); + + for (int idx = 0; idx < _pointsCount; ++idx) { + int x = s.readSint16LE(); + int y = isRoseTattoo ? s.readSint16LE() : s.readByte(); + push_back(Common::Point(x, y)); + } +} + +/*----------------------------------------------------------------*/ + +Scene *Scene::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelScene(vm); + else + return new Tattoo::TattooScene(vm); +} + +Scene::Scene(SherlockEngine *vm): _vm(vm) { + _sceneStats = new bool *[SCENES_COUNT]; + _sceneStats[0] = new bool[SCENES_COUNT * 65]; + Common::fill(&_sceneStats[0][0], &_sceneStats[0][SCENES_COUNT * 65], false); + for (int idx = 1; idx < SCENES_COUNT; ++idx) { + _sceneStats[idx] = _sceneStats[idx - 1] + 65; + } + _currentScene = -1; + _goToScene = -1; + _loadingSavedGame = false; + _walkedInScene = false; + _version = 0; + _compressed = false; + _invGraphicItems = 0; + _cAnimFramePause = 0; + _restoreFlag = false; + _animating = 0; + _doBgAnimDone = true; + _tempFadeStyle = 0; + _doBgAnimDone = false; +} + +Scene::~Scene() { + freeScene(); + delete[] _sceneStats[0]; + delete[] _sceneStats; +} + +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; + + // Free any previous scene + freeScene(); + + // Load the scene + Common::String sceneFile = Common::String::format("res%02d", _goToScene); + // _rrmName gets set during loadScene() + // _rrmName is for ScalpelScene::startCAnim + _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[HOLMES]._walkDest = Common::Point(people[HOLMES]._position.x / FIXED_INT_MULTIPLIER, + people[HOLMES]._position.y / FIXED_INT_MULTIPLIER); + + _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->_ui->clearWindow(); + _vm->_talk->freeTalkVars(); + _vm->_inventory->freeInv(); + _vm->_music->freeSong(); + _vm->_sound->freeLoadedSounds(); + + if (!_loadingSavedGame) + saveSceneStatus(); + else + _loadingSavedGame = false; + + _sequenceBuffer.clear(); + _descText.clear(); + _walkPoints.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; + Music &music = *_vm->_music; + People &people = *_vm->_people; + Resources &res = *_vm->_res; + SaveManager &saves = *_vm->_saves; + Screen &screen = *_vm->_screen; + 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 the room resource file for the scene + // + + if (!IS_3DO) { + // PC version + Common::String roomFilename = filename + ".rrm"; + _roomFilename = roomFilename; + + flag = _vm->_res->exists(roomFilename); + if (flag) { + Common::SeekableReadStream *rrmStream = _vm->_res->load(roomFilename); + + rrmStream->seek(39); + if (IS_SERRATED_SCALPEL) { + _version = rrmStream->readByte(); + _compressed = _version == 10; + } else { + _compressed = rrmStream->readByte() > 0; + } + + // Go to header and read it in + rrmStream->seek(rrmStream->readUint32LE()); + + BgFileHeader bgHeader; + bgHeader.load(*rrmStream, IS_ROSE_TATTOO); + _invGraphicItems = bgHeader._numImages + 1; + + if (IS_ROSE_TATTOO) { + // Resize the screen if necessary + int fullWidth = SHERLOCK_SCREEN_WIDTH + bgHeader._scrollSize; + if (screen._backBuffer1.w() != fullWidth) { + screen._backBuffer1.create(fullWidth, SHERLOCK_SCREEN_HEIGHT); + screen._backBuffer2.create(fullWidth, SHERLOCK_SCREEN_HEIGHT); + } + + // Handle initializing the palette + screen.initPaletteFade(bgHeader._bytesWritten); + rrmStream->read(screen._cMap, PALETTE_SIZE); + screen.translatePalette(screen._cMap); + + paletteLoaded(); + + // Read in background + if (_compressed) { + res.decompress(*rrmStream, (byte *)screen._backBuffer1.getPixels(), fullWidth * SHERLOCK_SCREEN_HEIGHT); + } else { + rrmStream->read(screen._backBuffer1.getPixels(), fullWidth * SHERLOCK_SCREEN_HEIGHT); + } + } + + // 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 (IS_ROSE_TATTOO) { + // Load shapes + Common::SeekableReadStream *infoStream = !_compressed ? rrmStream : res.decompress(*rrmStream, bgHeader._numStructs * 625); + + _bgShapes.resize(bgHeader._numStructs); + for (int idx = 0; idx < bgHeader._numStructs; ++idx) + _bgShapes[idx].load(*infoStream, true); + + if (_compressed) + delete infoStream; + + // Load description text + _descText.resize(bgHeader._descSize); + if (_compressed) + res.decompress(*rrmStream, (byte *)&_descText[0], bgHeader._descSize); + else + rrmStream->read(&_descText[0], bgHeader._descSize); + + // Load sequences + _sequenceBuffer.resize(bgHeader._seqSize); + if (_compressed) + res.decompress(*rrmStream, &_sequenceBuffer[0], bgHeader._seqSize); + else + rrmStream->read(&_sequenceBuffer[0], bgHeader._seqSize); + } else if (!_compressed) { + // Serrated Scalpel uncompressed info + _bgShapes.resize(bgHeader._numStructs); + for (int idx = 0; idx < bgHeader._numStructs; ++idx) + _bgShapes[idx].load(*rrmStream, false); + + 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 { + // Serrated Scalpel compressed info + 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, false); + + 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 = _compressed ? + res.decompress(*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) { + int animSize = IS_SERRATED_SCALPEL ? 65 : 47; + Common::SeekableReadStream *cAnimStream = _compressed ? + res.decompress(*rrmStream, animSize * bgHeader._numcAnimations) : + rrmStream->readStream(animSize * bgHeader._numcAnimations); + + // Load cAnim offset table as well + uint32 *cAnimOffsetTablePtr = new uint32[bgHeader._numcAnimations]; + uint32 *cAnimOffsetPtr = cAnimOffsetTablePtr; + memset(cAnimOffsetTablePtr, 0, bgHeader._numcAnimations * sizeof(uint32)); + if (IS_SERRATED_SCALPEL) { + // Save current stream offset + int32 curOffset = rrmStream->pos(); + rrmStream->seek(44); // Seek to cAnim-Offset-Table + for (uint16 curCAnim = 0; curCAnim < bgHeader._numcAnimations; curCAnim++) { + *cAnimOffsetPtr = rrmStream->readUint32LE(); + cAnimOffsetPtr++; + } + // Seek back to original stream offset + rrmStream->seek(curOffset); + } + // TODO: load offset table for Rose Tattoo as well + + // Go to the start of the cAnimOffsetTable + cAnimOffsetPtr = cAnimOffsetTablePtr; + + _cAnim.resize(bgHeader._numcAnimations); + for (uint idx = 0; idx < _cAnim.size(); ++idx) { + _cAnim[idx].load(*cAnimStream, IS_ROSE_TATTOO, *cAnimOffsetPtr); + cAnimOffsetPtr++; + } + + delete cAnimStream; + delete[] cAnimOffsetTablePtr; + } + + + + // Read in the room bounding areas + int size = rrmStream->readUint16LE(); + Common::SeekableReadStream *boundsStream = !_compressed ? rrmStream : + res.decompress(*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 (_compressed) + delete boundsStream; + + // Ensure we've reached the path version byte + if (rrmStream->readByte() != (IS_SERRATED_SCALPEL ? 254 : 251)) + error("Invalid scene path data"); + + // Load the walk directory and walk data + assert(_zones.size() < MAX_ZONES); + + + for (uint idx1 = 0; idx1 < _zones.size(); ++idx1) { + Common::fill(&_walkDirectory[idx1][0], &_walkDirectory[idx1][MAX_ZONES], 0); + for (uint idx2 = 0; idx2 < _zones.size(); ++idx2) + _walkDirectory[idx1][idx2] = rrmStream->readSint16LE(); + } + + // Read in the walk data + size = rrmStream->readUint16LE(); + Common::SeekableReadStream *walkStream = !_compressed ? rrmStream : + res.decompress(*rrmStream, size); + + int startPos = walkStream->pos(); + while ((walkStream->pos() - startPos) < size) { + _walkPoints.push_back(WalkArray()); + _walkPoints[_walkPoints.size() - 1]._fileOffset = walkStream->pos() - startPos; + _walkPoints[_walkPoints.size() - 1].load(*walkStream, IS_ROSE_TATTOO); + } + + if (_compressed) + delete walkStream; + + // Translate the file offsets of the walk directory to indexes in the loaded walk data + for (uint idx1 = 0; idx1 < _zones.size(); ++idx1) { + for (uint idx2 = 0; idx2 < _zones.size(); ++idx2) { + int fileOffset = _walkDirectory[idx1][idx2]; + if (fileOffset == -1) + continue; + + uint dataIndex = 0; + while (dataIndex < _walkPoints.size() && _walkPoints[dataIndex]._fileOffset != fileOffset) + ++dataIndex; + assert(dataIndex < _walkPoints.size()); + _walkDirectory[idx1][idx2] = dataIndex; + } + } + + if (IS_ROSE_TATTOO) { + // Read in the entrance + _entrance.load(*rrmStream); + + // Load scale zones + _scaleZones.resize(rrmStream->readByte()); + for (uint idx = 0; idx < _scaleZones.size(); ++idx) + _scaleZones[idx].load(*rrmStream); + } + + // Read in the exits + int numExits = rrmStream->readByte(); + _exits.resize(numExits); + + for (int idx = 0; idx < numExits; ++idx) + _exits[idx].load(*rrmStream, IS_ROSE_TATTOO); + + if (IS_SERRATED_SCALPEL) + // 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); + + loadSceneSounds(); + + if (IS_ROSE_TATTOO) { + // Load the object sound list + char buffer[27]; + + _objSoundList.resize(rrmStream->readUint16LE()); + for (uint idx = 0; idx < _objSoundList.size(); ++idx) { + rrmStream->read(buffer, 27); + _objSoundList[idx] = Common::String(buffer); + } + } else { + // Read in palette + rrmStream->read(screen._cMap, PALETTE_SIZE); + screen.translatePalette(screen._cMap); + Common::copy(screen._cMap, screen._cMap + PALETTE_SIZE, screen._sMap); + + // Read in the background + Common::SeekableReadStream *bgStream = !_compressed ? rrmStream : + res.decompress(*rrmStream, SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCENE_HEIGHT); + + bgStream->read(screen._backBuffer1.getPixels(), SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCENE_HEIGHT); + + if (_compressed) + delete bgStream; + } + + // Backup the image and set the palette + screen._backBuffer2.blitFrom(screen._backBuffer1); + screen.setPalette(screen._cMap); + + delete rrmStream; + } + + } else { + // === 3DO version === + _roomFilename = "rooms/" + filename + ".rrm"; + flag = _vm->_res->exists(_roomFilename); + if (!flag) + error("loadScene: 3DO room data file not found"); + + Common::SeekableReadStream *roomStream = _vm->_res->load(_roomFilename); + uint32 roomStreamSize = roomStream->size(); + + // there should be at least all bytes of the header data + if (roomStreamSize < 128) + error("loadScene: 3DO room data file is too small"); + + // Read 3DO header + roomStream->skip(4); // UINT32: offset graphic data? + uint16 header3DO_numStructs = roomStream->readUint16BE(); + uint16 header3DO_numImages = roomStream->readUint16BE(); + uint16 header3DO_numAnimations = roomStream->readUint16BE(); + roomStream->skip(6); + + uint32 header3DO_bgInfo_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_bgInfo_size = roomStream->readUint32BE(); + uint32 header3DO_bgShapes_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_bgShapes_size = roomStream->readUint32BE(); + uint32 header3DO_descriptions_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_descriptions_size = roomStream->readUint32BE(); + uint32 header3DO_sequence_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_sequence_size = roomStream->readUint32BE(); + uint32 header3DO_cAnim_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_cAnim_size = roomStream->readUint32BE(); + uint32 header3DO_roomBounding_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_roomBounding_size = roomStream->readUint32BE(); + uint32 header3DO_walkDirectory_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_walkDirectory_size = roomStream->readUint32BE(); + uint32 header3DO_walkData_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_walkData_size = roomStream->readUint32BE(); + uint32 header3DO_exits_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_exits_size = roomStream->readUint32BE(); + uint32 header3DO_entranceData_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_entranceData_size = roomStream->readUint32BE(); + uint32 header3DO_soundList_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_soundList_size = roomStream->readUint32BE(); + //uint32 header3DO_unknown_offset = roomStream->readUint32BE() + 0x80; + //uint32 header3DO_unknown_size = roomStream->readUint32BE(); + roomStream->skip(8); // Skip over unknown offset+size + uint32 header3DO_bgGraphicData_offset = roomStream->readUint32BE() + 0x80; + uint32 header3DO_bgGraphicData_size = roomStream->readUint32BE(); + + // Calculate amount of entries + int32 header3DO_soundList_count = header3DO_soundList_size / 9; + + _invGraphicItems = header3DO_numImages + 1; + + // Verify all offsets + if (header3DO_bgInfo_offset >= roomStreamSize) + error("loadScene: 3DO bgInfo offset points outside of room file"); + if (header3DO_bgInfo_size > (roomStreamSize - header3DO_bgInfo_offset)) + error("loadScene: 3DO bgInfo size goes beyond room file"); + if (header3DO_bgShapes_offset >= roomStreamSize) + error("loadScene: 3DO bgShapes offset points outside of room file"); + if (header3DO_bgShapes_size > (roomStreamSize - header3DO_bgShapes_offset)) + error("loadScene: 3DO bgShapes size goes beyond room file"); + if (header3DO_descriptions_offset >= roomStreamSize) + error("loadScene: 3DO descriptions offset points outside of room file"); + if (header3DO_descriptions_size > (roomStreamSize - header3DO_descriptions_offset)) + error("loadScene: 3DO descriptions size goes beyond room file"); + if (header3DO_sequence_offset >= roomStreamSize) + error("loadScene: 3DO sequence offset points outside of room file"); + if (header3DO_sequence_size > (roomStreamSize - header3DO_sequence_offset)) + error("loadScene: 3DO sequence size goes beyond room file"); + if (header3DO_cAnim_offset >= roomStreamSize) + error("loadScene: 3DO cAnim offset points outside of room file"); + if (header3DO_cAnim_size > (roomStreamSize - header3DO_cAnim_offset)) + error("loadScene: 3DO cAnim size goes beyond room file"); + if (header3DO_roomBounding_offset >= roomStreamSize) + error("loadScene: 3DO roomBounding offset points outside of room file"); + if (header3DO_roomBounding_size > (roomStreamSize - header3DO_roomBounding_offset)) + error("loadScene: 3DO roomBounding size goes beyond room file"); + if (header3DO_walkDirectory_offset >= roomStreamSize) + error("loadScene: 3DO walkDirectory offset points outside of room file"); + if (header3DO_walkDirectory_size > (roomStreamSize - header3DO_walkDirectory_offset)) + error("loadScene: 3DO walkDirectory size goes beyond room file"); + if (header3DO_walkData_offset >= roomStreamSize) + error("loadScene: 3DO walkData offset points outside of room file"); + if (header3DO_walkData_size > (roomStreamSize - header3DO_walkData_offset)) + error("loadScene: 3DO walkData size goes beyond room file"); + if (header3DO_exits_offset >= roomStreamSize) + error("loadScene: 3DO exits offset points outside of room file"); + if (header3DO_exits_size > (roomStreamSize - header3DO_exits_offset)) + error("loadScene: 3DO exits size goes beyond room file"); + if (header3DO_entranceData_offset >= roomStreamSize) + error("loadScene: 3DO entranceData offset points outside of room file"); + if (header3DO_entranceData_size > (roomStreamSize - header3DO_entranceData_offset)) + error("loadScene: 3DO entranceData size goes beyond room file"); + if (header3DO_soundList_offset >= roomStreamSize) + error("loadScene: 3DO soundList offset points outside of room file"); + if (header3DO_soundList_size > (roomStreamSize - header3DO_soundList_offset)) + error("loadScene: 3DO soundList size goes beyond room file"); + if (header3DO_bgGraphicData_offset >= roomStreamSize) + error("loadScene: 3DO bgGraphicData offset points outside of room file"); + if (header3DO_bgGraphicData_size > (roomStreamSize - header3DO_bgGraphicData_offset)) + error("loadScene: 3DO bgGraphicData size goes beyond room file"); + + // === BGINFO === read in the shapes header info + Common::Array<BgFileHeaderInfo> bgInfo; + + uint32 expected3DO_bgInfo_size = header3DO_numStructs * 16; + if (expected3DO_bgInfo_size != header3DO_bgInfo_size) // Security check + error("loadScene: 3DO bgInfo size mismatch"); + + roomStream->seek(header3DO_bgInfo_offset); + bgInfo.resize(header3DO_numStructs); + for (uint idx = 0; idx < bgInfo.size(); ++idx) + bgInfo[idx].load3DO(*roomStream); + + // === BGSHAPES === read in the shapes info + uint32 expected3DO_bgShapes_size = header3DO_numStructs * 588; + if (expected3DO_bgShapes_size != header3DO_bgShapes_size) // Security check + error("loadScene: 3DO bgShapes size mismatch"); + + roomStream->seek(header3DO_bgShapes_offset); + _bgShapes.resize(header3DO_numStructs); + for (int idx = 0; idx < header3DO_numStructs; ++idx) + _bgShapes[idx].load3DO(*roomStream); + + // === DESCRIPTION === read description text + if (header3DO_descriptions_size) { + roomStream->seek(header3DO_descriptions_offset); + _descText.resize(header3DO_descriptions_size); + roomStream->read(&_descText[0], header3DO_descriptions_size); + } + + // === SEQUENCE === read sequence buffer + if (header3DO_sequence_size) { + roomStream->seek(header3DO_sequence_offset); + _sequenceBuffer.resize(header3DO_sequence_size); + roomStream->read(&_sequenceBuffer[0], header3DO_sequence_size); + } + + // === IMAGES === set up the list of images used by the scene + roomStream->seek(header3DO_bgGraphicData_offset); + _images.resize(header3DO_numImages + 1); + for (int idx = 0; idx < header3DO_numImages; ++idx) { + _images[idx + 1]._filesize = bgInfo[idx]._filesize; + _images[idx + 1]._maxFrames = bgInfo[idx]._maxFrames; + + // Read image data into memory + Common::SeekableReadStream *imageStream = roomStream->readStream(bgInfo[idx]._filesize); + + // Load image data into an ImageFile array as room file data + // which is basically a fixed header, followed by a raw cel header, followed by regular cel data + _images[idx + 1]._images = new ImageFile3DO(*imageStream, true); + + delete imageStream; + } + + // === BGSHAPES === Set up the bgShapes + for (int idx = 0; idx < header3DO_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); + } + + // === CANIM === read cAnim list + _cAnim.clear(); + if (header3DO_numAnimations) { + roomStream->seek(header3DO_cAnim_offset); + Common::SeekableReadStream *cAnimStream = roomStream->readStream(header3DO_cAnim_size); + + uint32 *cAnimOffsetTablePtr = new uint32[header3DO_numAnimations]; + uint32 *cAnimOffsetPtr = cAnimOffsetTablePtr; + uint32 cAnimOffset = 0; + memset(cAnimOffsetTablePtr, 0, header3DO_numAnimations * sizeof(uint32)); + + // Seek to end of graphics data and load cAnim offset table from there + roomStream->seek(header3DO_bgGraphicData_offset + header3DO_bgGraphicData_size); + for (uint16 curCAnim = 0; curCAnim < header3DO_numAnimations; curCAnim++) { + cAnimOffset = roomStream->readUint32BE(); + if (cAnimOffset >= roomStreamSize) + error("loadScene: 3DO cAnim entry offset points outside of room file"); + + *cAnimOffsetPtr = cAnimOffset; + cAnimOffsetPtr++; + } + + // Go to the start of the cAnimOffsetTable + cAnimOffsetPtr = cAnimOffsetTablePtr; + + _cAnim.resize(header3DO_numAnimations); + for (uint idx = 0; idx < _cAnim.size(); ++idx) { + _cAnim[idx].load3DO(*cAnimStream, *cAnimOffsetPtr); + cAnimOffsetPtr++; + } + + delete cAnimStream; + delete[] cAnimOffsetTablePtr; + } + + // === BOUNDING AREAS === Read in the room bounding areas + int roomBoundingCount = header3DO_roomBounding_size / 12; + uint32 expected3DO_roomBounding_size = roomBoundingCount * 12; + if (expected3DO_roomBounding_size != header3DO_roomBounding_size) + error("loadScene: 3DO roomBounding size mismatch"); + + roomStream->seek(header3DO_roomBounding_offset); + _zones.resize(roomBoundingCount); + for (uint idx = 0; idx < _zones.size(); ++idx) { + _zones[idx].left = roomStream->readSint16BE(); + _zones[idx].top = roomStream->readSint16BE(); + _zones[idx].setWidth(roomStream->readSint16BE() + 1); + _zones[idx].setHeight(roomStream->readSint16BE() + 1); + roomStream->skip(4); // skip UINT32 + } + + // === WALK DIRECTORY === Load the walk directory + uint32 expected3DO_walkDirectory_size = _zones.size() * _zones.size() * 2; + if (expected3DO_walkDirectory_size != header3DO_walkDirectory_size) + error("loadScene: 3DO walkDirectory size mismatch"); + + roomStream->seek(header3DO_walkDirectory_offset); + assert(_zones.size() < MAX_ZONES); + for (uint idx1 = 0; idx1 < _zones.size(); ++idx1) { + for (uint idx2 = 0; idx2 < _zones.size(); ++idx2) + _walkDirectory[idx1][idx2] = roomStream->readSint16BE(); + } + + // === WALK DATA === Read in the walk data + roomStream->seek(header3DO_walkData_offset); + + int startPos = roomStream->pos(); + while ((roomStream->pos() - startPos) < (int)header3DO_walkData_size) { + _walkPoints.push_back(WalkArray()); + _walkPoints[_walkPoints.size() - 1]._fileOffset = roomStream->pos() - startPos; + _walkPoints[_walkPoints.size() - 1].load(*roomStream, false); + } + + // Translate the file offsets of the walk directory to indexes in the loaded walk data + for (uint idx1 = 0; idx1 < _zones.size(); ++idx1) { + for (uint idx2 = 0; idx2 < _zones.size(); ++idx2) { + int fileOffset = _walkDirectory[idx1][idx2]; + if (fileOffset == -1) + continue; + + uint dataIndex = 0; + while (dataIndex < _walkPoints.size() && _walkPoints[dataIndex]._fileOffset != fileOffset) + ++dataIndex; + assert(dataIndex < _walkPoints.size()); + _walkDirectory[idx1][idx2] = dataIndex; + } + } + + // === EXITS === Read in the exits + roomStream->seek(header3DO_exits_offset); + + int exitsCount = header3DO_exits_size / 20; + + _exits.resize(exitsCount); + for (int idx = 0; idx < exitsCount; ++idx) + _exits[idx].load3DO(*roomStream); + + // === ENTRANCE === Read in the entrance + if (header3DO_entranceData_size != 8) + error("loadScene: 3DO entranceData size mismatch"); + + roomStream->seek(header3DO_entranceData_offset); + _entrance.load3DO(*roomStream); + + // === SOUND LIST === Initialize sound list + roomStream->seek(header3DO_soundList_offset); + _sounds.resize(header3DO_soundList_count); + for (int idx = 0; idx < header3DO_soundList_count; ++idx) + _sounds[idx].load3DO(*roomStream); + + delete roomStream; + + // === BACKGROUND PICTURE === + // load from file rooms\[filename].bg + // it's uncompressed 15-bit RGB555 data + + Common::String roomBackgroundFilename = "rooms/" + filename + ".bg"; + flag = _vm->_res->exists(roomBackgroundFilename); + if (!flag) + error("loadScene: 3DO room background file not found (%s)", roomBackgroundFilename.c_str()); + + Common::File roomBackgroundStream; + if (!roomBackgroundStream.open(roomBackgroundFilename)) + error("Could not open file - %s", roomBackgroundFilename.c_str()); + + int totalPixelCount = SHERLOCK_SCREEN_WIDTH * SHERLOCK_SCENE_HEIGHT; + uint16 *roomBackgroundDataPtr = NULL; + uint16 *pixelSourcePtr = NULL; + uint16 *pixelDestPtr = (uint16 *)screen._backBuffer1.getPixels(); + uint16 curPixel = 0; + uint32 roomBackgroundStreamSize = roomBackgroundStream.size(); + uint32 expectedBackgroundSize = totalPixelCount * 2; + + // Verify file size of background file + if (expectedBackgroundSize != roomBackgroundStreamSize) + error("loadScene: 3DO room background file not expected size"); + + roomBackgroundDataPtr = new uint16[totalPixelCount]; + roomBackgroundStream.read(roomBackgroundDataPtr, roomBackgroundStreamSize); + roomBackgroundStream.close(); + + // Convert data from RGB555 to RGB565 + pixelSourcePtr = roomBackgroundDataPtr; + for (int pixels = 0; pixels < totalPixelCount; pixels++) { + curPixel = READ_BE_UINT16(pixelSourcePtr++); + + byte curPixelRed = (curPixel >> 10) & 0x1F; + byte curPixelGreen = (curPixel >> 5) & 0x1F; + byte curPixelBlue = curPixel & 0x1F; + *pixelDestPtr = ((curPixelRed << 11) | (curPixelGreen << 6) | (curPixelBlue)); + pixelDestPtr++; + } + + delete[] roomBackgroundDataPtr; + +#if 0 + // code to show the background + screen.blitFrom(screen._backBuffer1); + _vm->_events->wait(10000); +#endif + + // Backup the image + screen._backBuffer2.blitFrom(screen._backBuffer1); + } + + // Handle drawing any on-screen interface + ui.drawInterface(); + + 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 (IS_SERRATED_SCALPEL && music._musicOn && music.loadSong(_currentScene)) + music.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; + + events.clearEvents(); + return flag; +} + +void Scene::loadSceneSounds() { + Sound &sound = *_vm->_sound; + + for (uint idx = 0; idx < _sounds.size(); ++idx) + sound.loadSound(_sounds[idx]._name, _sounds[idx]._priority); +} + +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]; + bool objectFlag = true; + + if (o._requiredFlag[0] || o._requiredFlag[1]) { + if (o._requiredFlag[0] != 0) + objectFlag = _vm->readFlags(o._requiredFlag[0]); + if (o._requiredFlag[1] != 0) + objectFlag &= _vm->readFlags(o._requiredFlag[1]); + + if (!objectFlag) { + // 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 (IS_ROSE_TATTOO || o._requiredFlag[0] > 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; + Point32 &hSavedPos = people._savedPos; + int &hSavedFacing = people._savedPos._facing; + + 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 + if (IS_SERRATED_SCALPEL) { + hSavedPos = Point32(160 * FIXED_INT_MULTIPLIER, 100 * FIXED_INT_MULTIPLIER); + hSavedFacing = 4; + } else { + hSavedPos = people[HOLMES]._position; + hSavedFacing = people[HOLMES]._sequenceNumber; + } + } else { + // setup entrance info + hSavedPos.x = _entrance._startPosition.x * FIXED_INT_MULTIPLIER; + hSavedPos.y = _entrance._startPosition.y * FIXED_INT_MULTIPLIER; + if (IS_SERRATED_SCALPEL) { + hSavedPos.x /= 100; + hSavedPos.y /= 100; + } + + 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 *= FIXED_INT_MULTIPLIER; + hSavedPos.y *= FIXED_INT_MULTIPLIER; + } + } + + int cAnimNum = -1; + + if (hSavedFacing < 101) { + // Standard info, so set it + people[HOLMES]._position = hSavedPos; + people[HOLMES]._sequenceNumber = hSavedFacing; + + if (saves._justLoaded && IS_ROSE_TATTOO) { + Tattoo::TattooUserInterface &ui = *(Tattoo::TattooUserInterface *)_vm->_ui; + + // For scrolling scenes, make sure the player is on-screen + ui._targetScroll.x = CLIP(people[HOLMES]._position.x / FIXED_INT_MULTIPLIER - + SHERLOCK_SCREEN_WIDTH / 8 - 250, 0, screen._backBuffer1.w() - SHERLOCK_SCREEN_WIDTH); + screen._currentScroll = ui._targetScroll; + } + } 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[HOLMES]._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[HOLMES]._position.x / FIXED_INT_MULTIPLIER, + people[HOLMES]._position.y / FIXED_INT_MULTIPLIER))) { + // 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(); + + // Actually do the transition + if (screen._fadeStyle) { + if (!IS_3DO) { + // do pixel-transition for PC + screen.randomTransition(); + } else { + // fade in for 3DO + screen.clear(); + screen.fadeIntoScreen3DO(3); + } + } else { + screen.slamArea(screen._currentScroll.x, screen._currentScroll.y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + screen.update(); + + // Start any initial animation for the scene + if (cAnimNum != -1) { + CAnim &c = _cAnim[cAnimNum]; + PositionFacing pt = c._goto[0]; + + c._goto[0].x = c._goto[0].y = -1; + people[HOLMES]._position = Common::Point(0, 0); + + startCAnim(cAnimNum, 1); + c._goto[0] = 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; + + // Update Holmes if he's turned on + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) + people[idx].adjustSprite(); + } + + // Flag the bg shapes which need to be redrawn + checkBgShapes(); + + // Draw the shapes for the scene + drawAllShapes(); +} + +Exit *Scene::checkForExit(const Common::Rect &r) { + for (uint idx = 0; idx < _exits.size(); ++idx) { + if (_exits[idx].intersects(r)) + return &_exits[idx]; + } + + return nullptr; +} + +int Scene::findBgShape(const Common::Point &pt) { + 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 (o.getNewBounds().contains(pt)) + return idx; + } else if (o._type == NO_SHAPE) { + if (o.getNoShapeBounds().contains(pt)) + 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; +} + +void Scene::synchronize(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]); + } + } +} + +void Scene::checkBgShapes() { + People &people = *_vm->_people; + Person &holmes = people[HOLMES]; + Common::Point pt(holmes._position.x / FIXED_INT_MULTIPLIER, holmes._position.y / FIXED_INT_MULTIPLIER); + + // Iterate through the shapes + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + if (obj._type == ACTIVE_BG_SHAPE || (IS_SERRATED_SCALPEL && obj._type == STATIC_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; + } + } + } +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/scene.h b/engines/sherlock/scene.h new file mode 100644 index 0000000000..0fbda38fe8 --- /dev/null +++ b/engines/sherlock/scene.h @@ -0,0 +1,323 @@ +/* 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" +#include "sherlock/screen.h" + +namespace Sherlock { + +#define MAX_ZONES 40 +#define INFO_LINE 140 + +class SherlockEngine; + +struct BgFileHeader { + int _numStructs; + int _numImages; + int _numcAnimations; + int _descSize; + int _seqSize; + + // Serrated Scalpel + int _fill; + + // Rose Tattoo + int _scrollSize; + int _bytesWritten; // Size of the main body of the RRM + int _fadeStyle; // Fade style + byte _palette[PALETTE_SIZE]; // Palette + + + BgFileHeader(); + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); +}; + +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); + void load3DO(Common::SeekableReadStream &s); +}; + +class Exit: public Common::Rect { +public: + int _scene; + int _allow; + PositionFacing _newPosition; + + Common::String _dest; + int _image; // Arrow image to use + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); + void load3DO(Common::SeekableReadStream &s); +}; + +struct SceneEntry { + Common::Point _startPosition; + int _startDir; + int _allow; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); + void load3DO(Common::SeekableReadStream &s); +}; + +struct SceneSound { + Common::String _name; + int _priority; + + /** + * Load the data for the object + */ + void load(Common::SeekableReadStream &s); + void load3DO(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 ScaleZone: public Common::Rect { +public: + int _topNumber; // Numerator of scale size at the top of the zone + int _bottomNumber; // Numerator of scale size at the bottom of the zone + + void load(Common::SeekableReadStream &s); +}; + +class WalkArray : public Common::Array < Common::Point > { +public: + int _pointsCount; + int _fileOffset; + + WalkArray() : _pointsCount(0), _fileOffset(-1) {} + + /** + * Load data for the walk array entry + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); +}; + +class Scene { +private: + bool _loadingSavedGame; + + /** + * Loads sounds for the scene + */ + void loadSceneSounds(); + + /** + * 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(); + + /** + * 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(); +protected: + SherlockEngine *_vm; + Common::String _roomFilename; + + /** + * Loads the data associated for a given scene. The room resource 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. + */ + virtual bool loadScene(const Common::String &filename); + + /** + * 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 + */ + virtual void checkBgShapes(); + + /** + * Draw all the shapes, people and NPCs in the correct order + */ + virtual void drawAllShapes() = 0; + + /** + * Called by loadScene when the palette is loaded for Rose Tattoo + */ + virtual void paletteLoaded() {} + + Scene(SherlockEngine *vm); +public: + int _currentScene; + int _goToScene; + bool **_sceneStats; + bool _walkedInScene; + int _version; + bool _compressed; + 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<WalkArray> _walkPoints; + Common::Array<Exit> _exits; + SceneEntry _entrance; + Common::Array<SceneSound> _sounds; + ObjectArray _canimShapes; + Common::Array<ScaleZone> _scaleZones; + Common::StringArray _objSoundList; + bool _restoreFlag; + int _animating; + bool _doBgAnimDone; + int _tempFadeStyle; + int _cAnimFramePause; +public: + static Scene *init(SherlockEngine *vm); + virtual ~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); + + /** + * 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); + + /** + * 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. + */ + virtual int closestZone(const Common::Point &pt) = 0; + + /** + * Attempts to find a background shape within the passed bounds. If found, + * it will return the shape number, or -1 on failure. + */ + virtual int findBgShape(const Common::Point &pt); + + /** + * Synchronize the data for a savegame + */ + virtual void synchronize(Serializer &s); +public: + /** + * Draw all objects and characters. + */ + virtual void doBgAnim() = 0; + + /** + * Update the screen back buffer with all of the scene objects which need + * to be drawn + */ + virtual void updateBackground(); + + /** + * 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 + */ + virtual int startCAnim(int cAnimNum, int playRate = 1) = 0; +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/screen.cpp b/engines/sherlock/screen.cpp new file mode 100644 index 0000000000..782869d77e --- /dev/null +++ b/engines/sherlock/screen.cpp @@ -0,0 +1,553 @@ +/* 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 "sherlock/scalpel/scalpel_screen.h" +#include "common/system.h" +#include "common/util.h" +#include "graphics/palette.h" + +namespace Sherlock { + +Screen *Screen::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelScreen(vm); + else + return new Screen(vm); +} + +Screen::Screen(SherlockEngine *vm) : Surface(g_system->getWidth(), g_system->getHeight()), _vm(vm), + _backBuffer1(g_system->getWidth(), g_system->getHeight()), + _backBuffer2(g_system->getWidth(), g_system->getHeight()), + _backBuffer(&_backBuffer1) { + _transitionSeed = 1; + _fadeStyle = false; + Common::fill(&_cMap[0], &_cMap[PALETTE_SIZE], 0); + Common::fill(&_sMap[0], &_sMap[PALETTE_SIZE], 0); + Common::fill(&_tMap[0], &_tMap[PALETTE_SIZE], 0); + + // Set up the initial font + setFont(IS_SERRATED_SCALPEL ? 1 : 4); + + // Rose Tattoo specific fields + _fadeBytesRead = _fadeBytesToRead = 0; + _oldFadePercent = 0; + _flushScreen = false; +} + +Screen::~Screen() { + Fonts::free(); +} + +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::makeAllDirty() { + addDirtyRect(Common::Rect(0, 0, this->w(), this->h())); +} + +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(); + assert(IS_SERRATED_SCALPEL); + + for (int idx = 0; idx <= 65535 && !_vm->shouldQuit(); ++idx) { + _transitionSeed = _transitionSeed * TRANSITION_MULTIPLIER + 1; + int offset = _transitionSeed & 0xFFFF; + + if (offset < (this->w() * this->h())) + *((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[640]; + Common::fill(&table[0], &table[640], 0); + + for (int yp = 0; yp < this->h(); ++yp) { + for (int xp = 0; xp < this->w(); ++xp) { + int temp = (table[xp] >= (this->h() - 3)) ? this->h() - 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::fadeIntoScreen3DO(int speed) { + Events &events = *_vm->_events; + uint16 *currentScreenBasePtr = (uint16 *)getPixels(); + uint16 *targetScreenBasePtr = (uint16 *)_backBuffer->getPixels(); + uint16 currentScreenPixel = 0; + uint16 targetScreenPixel = 0; + + uint16 currentScreenPixelRed = 0; + uint16 currentScreenPixelGreen = 0; + uint16 currentScreenPixelBlue = 0; + + uint16 targetScreenPixelRed = 0; + uint16 targetScreenPixelGreen = 0; + uint16 targetScreenPixelBlue = 0; + + uint16 screenWidth = this->w(); + uint16 screenHeight = this->h(); + uint16 screenX = 0; + uint16 screenY = 0; + uint16 pixelsChanged = 0; + + _dirtyRects.clear(); + + do { + pixelsChanged = 0; + uint16 *currentScreenPtr = currentScreenBasePtr; + uint16 *targetScreenPtr = targetScreenBasePtr; + + for (screenY = 0; screenY < screenHeight; screenY++) { + for (screenX = 0; screenX < screenWidth; screenX++) { + currentScreenPixel = *currentScreenPtr; + targetScreenPixel = *targetScreenPtr; + + if (currentScreenPixel != targetScreenPixel) { + // pixel doesn't match, adjust accordingly + currentScreenPixelRed = currentScreenPixel & 0xF800; + currentScreenPixelGreen = currentScreenPixel & 0x07E0; + currentScreenPixelBlue = currentScreenPixel & 0x001F; + targetScreenPixelRed = targetScreenPixel & 0xF800; + targetScreenPixelGreen = targetScreenPixel & 0x07E0; + targetScreenPixelBlue = targetScreenPixel & 0x001F; + + if (currentScreenPixelRed != targetScreenPixelRed) { + if (currentScreenPixelRed < targetScreenPixelRed) { + currentScreenPixelRed += 0x0800; + } else { + currentScreenPixelRed -= 0x0800; + } + } + if (currentScreenPixelGreen != targetScreenPixelGreen) { + // Adjust +2/-2 because we are running RGB555 at RGB565 + if (currentScreenPixelGreen < targetScreenPixelGreen) { + currentScreenPixelGreen += 0x0040; + } else { + currentScreenPixelGreen -= 0x0040; + } + } + if (currentScreenPixelBlue != targetScreenPixelBlue) { + if (currentScreenPixelBlue < targetScreenPixelBlue) { + currentScreenPixelBlue += 0x0001; + } else { + currentScreenPixelBlue -= 0x0001; + } + } + *currentScreenPtr = currentScreenPixelRed | currentScreenPixelGreen | currentScreenPixelBlue; + pixelsChanged++; + } + + currentScreenPtr++; + targetScreenPtr++; + } + } + + // Too much considered dirty at the moment + addDirtyRect(Common::Rect(0, 0, screenWidth, screenHeight)); + + events.pollEvents(); + events.delay(10 * speed); + } while ((pixelsChanged) && (!_vm->shouldQuit())); +} + +void Screen::blitFrom3DOcolorLimit(uint16 limitColor) { + uint16 *currentScreenPtr = (uint16 *)getPixels(); + uint16 *targetScreenPtr = (uint16 *)_backBuffer->getPixels(); + uint16 currentScreenPixel = 0; + + uint16 screenWidth = this->w(); + uint16 screenHeight = this->h(); + uint16 screenX = 0; + uint16 screenY = 0; + + uint16 currentScreenPixelRed = 0; + uint16 currentScreenPixelGreen = 0; + uint16 currentScreenPixelBlue = 0; + + uint16 limitPixelRed = limitColor & 0xF800; + uint16 limitPixelGreen = limitColor & 0x07E0; + uint16 limitPixelBlue = limitColor & 0x001F; + + for (screenY = 0; screenY < screenHeight; screenY++) { + for (screenX = 0; screenX < screenWidth; screenX++) { + currentScreenPixel = *targetScreenPtr; + + currentScreenPixelRed = currentScreenPixel & 0xF800; + currentScreenPixelGreen = currentScreenPixel & 0x07E0; + currentScreenPixelBlue = currentScreenPixel & 0x001F; + + if (currentScreenPixelRed < limitPixelRed) + currentScreenPixelRed = limitPixelRed; + if (currentScreenPixelGreen < limitPixelGreen) + currentScreenPixelGreen = limitPixelGreen; + if (currentScreenPixelBlue < limitPixelBlue) + currentScreenPixelBlue = limitPixelBlue; + + *currentScreenPtr = currentScreenPixelRed | currentScreenPixelGreen | currentScreenPixelBlue; + currentScreenPtr++; + targetScreenPtr++; + } + } + + // Too much considered dirty at the moment + addDirtyRect(Common::Rect(0, 0, screenWidth, screenHeight)); +} + +void Screen::restoreBackground(const Common::Rect &r) { + if (r.width() > 0 && r.height() > 0) + _backBuffer1.blitFrom(_backBuffer2, Common::Point(r.left, r.top), r); +} + +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 srcRect = r, destRect = r; + + destRect.translate(-_currentScroll.x, -_currentScroll.y); + + if (destRect.left < 0) { + srcRect.left += -destRect.left; + destRect.left = 0; + } + if (destRect.top < 0) { + srcRect.top += -destRect.top; + destRect.top = 0; + } + if (destRect.right > SHERLOCK_SCREEN_WIDTH) { + srcRect.right -= (destRect.left - SHERLOCK_SCREEN_WIDTH); + destRect.right = SHERLOCK_SCREEN_WIDTH; + } + if (destRect.bottom > SHERLOCK_SCREEN_HEIGHT) { + srcRect.bottom -= (destRect.bottom - SHERLOCK_SCREEN_HEIGHT); + destRect.bottom = SHERLOCK_SCREEN_HEIGHT; + } + + if (srcRect.isValidRect()) + blitFrom(*_backBuffer, Common::Point(destRect.left, destRect.top), srcRect); + } +} + + +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); + + if (!_flushScreen) { + // 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::flushScaleImage(ImageFrame *frame, const Common::Point &pt, int16 *xp, int16 *yp, + int16 *width, int16 *height, int scaleVal) { + Common::Point imgPos = pt + frame->_offset; + Common::Rect newBounds(imgPos.x, imgPos.y, imgPos.x + frame->sDrawXSize(scaleVal), + imgPos.y + frame->sDrawYSize(scaleVal)); + Common::Rect oldBounds(*xp, *yp, *xp + *width, *yp + *height); + + if (!_flushScreen) { + // 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::flushImage(ImageFrame *frame, const Common::Point &pt, Common::Rect &newBounds, int scaleVal) { + Common::Point newPos, newSize; + + if (scaleVal == 256) + flushImage(frame, pt, &newPos.x, &newPos.y, &newSize.x, &newSize.y); + else + flushScaleImage(frame, pt, &newPos.x, &newPos.y, &newSize.x, &newSize.y, scaleVal); + + // Transfer the pos and size amounts into a single bounds rect + newBounds = Common::Rect(newPos.x, newPos.y, newPos.x + newSize.x, newPos.y + newSize.y); +} + +void Screen::blockMove(const Common::Rect &r) { + Common::Rect bounds = r; + slamRect(bounds); +} + +void Screen::blockMove() { + blockMove(Common::Rect(0, 0, w(), h())); +} + +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 = (this->w() - width) / 2; + + Common::Rect textBounds(pos.x, pos.y, pos.x + width, pos.y + _fontHeight); + if (textBounds.right > this->w()) + textBounds.moveTo(this->w() - width, textBounds.top); + if (textBounds.bottom > this->h()) + textBounds.moveTo(textBounds.left, this->h() - _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); +} + +void Screen::writeString(const Common::String &str, const Common::Point &pt, byte overrideColor) { + Fonts::writeString(_backBuffer, str, pt, overrideColor); +} + +void Screen::vgaBar(const Common::Rect &r, int color) { + _backBuffer->fillRect(r, color); + slamRect(r); +} + +void Screen::setDisplayBounds(const Common::Rect &r) { + _sceneSurface.setPixels(_backBuffer1.getBasePtr(r.left, r.top), r.width(), r.height(), _backBuffer1.getPixelFormat()); + + _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, this->w(), this->h()); +} + +void Screen::synchronize(Serializer &s) { + int fontNumb = _fontNumber; + s.syncAsByte(fontNumb); + if (s.isLoading()) + setFont(fontNumb); +} + +void Screen::initPaletteFade(int bytesToRead) { + Common::copy(&_cMap[0], &_cMap[PALETTE_SIZE], &_sMap[0]); + Common::copy(&_cMap[0], &_cMap[PALETTE_SIZE], &_tMap[0]); + + // Set how many bytes need to be read / have been read + _fadeBytesRead = 0; + _fadeBytesToRead = bytesToRead; + _oldFadePercent = 0; +} + +int Screen::fadeRead(Common::SeekableReadStream &stream, byte *buf, int totalSize) { + warning("TODO: fadeRead"); + stream.read(buf, totalSize); + return totalSize; +} + +void Screen::translatePalette(byte palette[PALETTE_SIZE]) { + for (int idx = 0; idx < PALETTE_SIZE; ++idx) + palette[idx] = VGA_COLOR_TRANS(palette[idx]); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/screen.h b/engines/sherlock/screen.h new file mode 100644 index 0000000000..2e0cef72ca --- /dev/null +++ b/engines/sherlock/screen.h @@ -0,0 +1,243 @@ +/* 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 "sherlock/surface.h" +#include "sherlock/resources.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +#define PALETTE_SIZE 768 +#define PALETTE_COUNT 256 +#define VGA_COLOR_TRANS(x) ((x) * 255 / 63) +#define BG_GREYSCALE_RANGE_END 229 + +enum { + BLACK = 0, + INFO_BLACK = 1, + BORDER_COLOR = 237, + COMMAND_BACKGROUND = 4, + BUTTON_BACKGROUND = 235, + TALK_FOREGROUND = 12, + TALK_NULL = 16 +}; + +class SherlockEngine; + +class Screen : public Surface { +private: + SherlockEngine *_vm; + Common::List<Common::Rect> _dirtyRects; + uint32 _transitionSeed; + Surface _sceneSurface; + + // Rose Tattoo fields + int _fadeBytesRead, _fadeBytesToRead; + int _oldFadePercent; +private: + /** + * 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); +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]; + byte _tMap[PALETTE_SIZE]; + bool _flushScreen; + Common::Point _currentScroll; +public: + static Screen *init(SherlockEngine *vm); + Screen(SherlockEngine *vm); + virtual ~Screen(); + + /** + * Handles updating any dirty areas of the screen Surface object to the physical screen + */ + void update(); + + /** + * Makes the whole screen dirty, Hack for 3DO movie playing + */ + void makeAllDirty(); + + /** + * 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(); + + /** + * Fade backbuffer 1 into screen (3DO RGB!) + */ + void fadeIntoScreen3DO(int speed); + + void blitFrom3DOcolorLimit(uint16 color); + + /** + * 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); + + /** + * Similar to flushImage, this method takes in an extra parameter for the scale proporation, + * which affects the calculated bounds accordingly + */ + void flushScaleImage(ImageFrame *frame, const Common::Point &pt, int16 *xp, int16 *yp, + int16 *width, int16 *height, int scaleVal); + + /** + * Variation of flushImage/flushScaleImage that takes in and updates a rect + */ + void flushImage(ImageFrame *frame, const Common::Point &pt, Common::Rect &newBounds, int scaleVal); + + /** + * Copies data from the back buffer to the screen + */ + void blockMove(const Common::Rect &r); + + /** + * Copies the entire screen from the back buffer + */ + void blockMove(); + + /** + * Fills an area on the back buffer, and then copies it to the screen + */ + void vgaBar(const Common::Rect &r, int color); + + /** + * 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(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Serializer &s); + + /** + * Draws the given string into the back buffer using the images stored in _font + */ + virtual void writeString(const Common::String &str, const Common::Point &pt, byte overrideColor); + + + // Rose Tattoo specific methods + void initPaletteFade(int bytesToRead); + + int fadeRead(Common::SeekableReadStream &stream, byte *buf, int totalSize); + + /** + * Translate a palette from 6-bit RGB values to full 8-bit values suitable for passing + * to the underlying palette manager + */ + static void translatePalette(byte palette[PALETTE_SIZE]); +}; + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/sherlock.cpp b/engines/sherlock/sherlock.cpp new file mode 100644 index 0000000000..18b9332867 --- /dev/null +++ b/engines/sherlock/sherlock.cpp @@ -0,0 +1,282 @@ +/* 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" + +namespace Sherlock { + +SherlockEngine::SherlockEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + Engine(syst), _gameDescription(gameDesc), _randomSource("Sherlock") { + _animation = nullptr; + _debugger = nullptr; + _events = nullptr; + _fixedText = nullptr; + _inventory = nullptr; + _journal = nullptr; + _map = nullptr; + _music = 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 _fixedText; + delete _journal; + delete _map; + delete _people; + delete _saves; + delete _scene; + delete _screen; + delete _music; + delete _sound; + delete _talk; + delete _ui; + delete _inventory; + delete _res; +} + +void SherlockEngine::initialize() { + DebugMan.addDebugChannel(kDebugLevelScript, "scripts", "Script debug level"); + DebugMan.addDebugChannel(kDebugLevelAdLibDriver, "AdLib", "AdLib driver debugging"); + DebugMan.addDebugChannel(kDebugLevelMT32Driver, "MT32", "MT32 driver debugging"); + DebugMan.addDebugChannel(kDebugLevelMusic, "Music", "Music debugging"); + + Fonts::setVm(this); + ImageFile::setVm(this); + ImageFile3DO::setVm(this); + BaseObject::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 = Debugger::init(this); + _events = new Events(this); + _fixedText = FixedText::init(this); + _inventory = Inventory::init(this); + _map = Map::init(this); + _music = new Music(this, _mixer); + _journal = Journal::init(this); + _people = People::init(this); + _saves = SaveManager::init(this, _targetName); + _scene = Scene::init(this); + _screen = Screen::init(this); + _sound = new Sound(this, _mixer); + _talk = Talk::init(this); + _ui = UserInterface::init(this); + + // Load game settings + loadConfig(); + + if (getPlatform() == Common::kPlatform3DO) { + // Disable portraits on 3DO + // 3DO does not include portrait data + _people->_portraitsOn = false; + } +} + +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 >= 0 && 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->_savedPos.x == -1) { + _canLoadSave = true; + _scene->doBgAnim(); + _canLoadSave = false; + } + } + + _scene->freeScene(); + _people->freeWalk(); +} + +void SherlockEngine::handleInput() { + _canLoadSave = _ui->_menuMode == STD_MODE; + _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::setFlagsDirect(int flagNum) { + _flags[ABS(flagNum)] = flagNum >= 0; +} + +void SherlockEngine::loadConfig() { + // Load sound settings + syncSoundSettings(); + + ConfMan.registerDefault("font", getGameID() == GType_SerratedScalpel ? 1 : 4); + + _screen->setFont(ConfMan.getInt("font")); + if (getGameID() == GType_SerratedScalpel) + _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", !_music->_musicOn); + 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(); + _music->syncMusicSettings(); +} + +void SherlockEngine::synchronize(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..69000e1fbc --- /dev/null +++ b/engines/sherlock/sherlock.h @@ -0,0 +1,229 @@ +/* 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/fixed_text.h" +#include "sherlock/inventory.h" +#include "sherlock/journal.h" +#include "sherlock/map.h" +#include "sherlock/music.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 { + kDebugLevelScript = 1 << 0, + kDebugLevelAdLibDriver = 2 << 0, + kDebugLevelMT32Driver = 3 << 0, + kDebugLevelMusic = 4 << 0 +}; + +enum GameType { + GType_SerratedScalpel = 0, + GType_RoseTattoo = 1 +}; + +#define SHERLOCK_SCREEN_WIDTH _vm->_screen->w() +#define SHERLOCK_SCREEN_HEIGHT _vm->_screen->h() +#define SHERLOCK_SCENE_WIDTH _vm->_screen->_backBuffer1.w() +#define SHERLOCK_SCENE_HEIGHT (IS_SERRATED_SCALPEL ? 138 : 480) +#define SCENES_COUNT (IS_SERRATED_SCALPEL ? 63 : 101) + +#define COL_INFO_FOREGROUND (IS_SERRATED_SCALPEL ? (byte)Scalpel::INFO_FOREGROUND : (byte)Tattoo::INFO_FOREGROUND) +#define COL_PEN_COLOR (IS_SERRATED_SCALPEL ? (byte)Scalpel::PEN_COLOR : (byte)Tattoo::PEN_COLOR) + +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; + FixedText *_fixedText; + Inventory *_inventory; + Journal *_journal; + Map *_map; + Music *_music; + 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 the game's language + */ + Common::Language getLanguage() 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); + + /** + * 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 setFlagsDirect(int flagNum); + + /** + * Saves game configuration information + */ + void saveConfig(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Serializer &s); +}; + +#define IS_ROSE_TATTOO (_vm->getGameID() == GType_RoseTattoo) +#define IS_SERRATED_SCALPEL (_vm->getGameID() == GType_SerratedScalpel) +#define IS_3DO (_vm->getPlatform() == Common::kPlatform3DO) + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/sound.cpp b/engines/sherlock/sound.cpp new file mode 100644 index 0000000000..b46eb67b50 --- /dev/null +++ b/engines/sherlock/sound.cpp @@ -0,0 +1,269 @@ +/* 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" +#include "audio/decoders/aiff.h" +#include "audio/decoders/wave.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; + _voices = 0; + _diskSoundPlaying = false; + _soundPlaying = false; + _soundIsOn = &_soundPlaying; + _curPriority = 0; + _digiBuf = nullptr; + + _soundOn = true; + _speechOn = true; + + if (IS_3DO) { + // 3DO: we don't need to prepare anything for sound + return; + } + + _vm->_res->addToCache("MUSIC.LIB"); + if (!_vm->_interactiveFl) + _vm->_res->addToCache("TITLE.SND"); + else { + _vm->_res->addToCache("MUSIC.LIB"); + + if (IS_ROSE_TATTOO) { + _vm->_res->addToCache("SOUND.LIB"); + } else { + _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"); + _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 + samp = 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('.')) { + if (!IS_3DO) { + if (IS_SERRATED_SCALPEL) { + filename += ".SND"; + } else { + filename += ".WAV"; + } + } else { + // 3DO uses .aiff extension + filename += ".AIFF"; + if (!filename.contains('/')) { + // if no directory was given, use the room sounds directory + filename = "rooms/sounds/" + filename; + } + } + } + + Common::String libFilename(libraryFilename); + Common::SeekableReadStream *stream = libFilename.empty() ? res.load(filename) : res.load(filename, libFilename); + + Audio::AudioStream *audioStream; + + if (!IS_3DO) { + if (IS_SERRATED_SCALPEL) { + stream->skip(2); + int size = stream->readUint32BE(); + int rate = stream->readUint16BE(); + byte *data = (byte *)malloc(size); + byte *ptr = data; + stream->read(ptr, size); + delete stream; + + 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 + + audioStream = Audio::makeRawStream(decoded, (size - 2) * 2, rate, Audio::FLAG_UNSIGNED, DisposeAfterUse::YES); + } else { + audioStream = Audio::makeWAVStream(stream, DisposeAfterUse::YES); + } + } else { + // 3DO: AIFF file + audioStream = Audio::makeAIFFStream(stream, DisposeAfterUse::YES); + } + + Audio::SoundHandle effectsHandle = (IS_SERRATED_SCALPEL) ? _scalpelEffectsHandle : getFreeSoundHandle(); + + _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 (IS_SERRATED_SCALPEL) { + if (_mixer->isSoundHandleActive(_scalpelEffectsHandle) && (_curPriority > _vm->_scene->_sounds[bufNum]._priority)) + return; + + stopSound(); + } + + playSound(_vm->_scene->_sounds[bufNum]._name, waitType, _vm->_scene->_sounds[bufNum]._priority); +} + +void Sound::freeLoadedSounds() { + // As sounds are played with DisposeAfterUse::YES, stopping the sounds also + // frees them + stopSound(); +} + +void Sound::stopSound() { + if (IS_SERRATED_SCALPEL) { + _mixer->stopHandle(_scalpelEffectsHandle); + } else { + for (int i = 0; i < MAX_MIXER_CHANNELS; i++) + _mixer->stopHandle(_tattooEffectsHandle[i]); + } +} + +void Sound::stopSndFuncPtr(int v1, int v2) { + // TODO + warning("TODO: Sound::stopSndFuncPtr"); +} + +void Sound::freeDigiSound() { + delete[] _digiBuf; + _digiBuf = nullptr; + _diskSoundPlaying = false; + _soundPlaying = false; +} + +Audio::SoundHandle Sound::getFreeSoundHandle() { + for (int i = 0; i < MAX_MIXER_CHANNELS; i++) { + if (!_mixer->isSoundHandleActive(_tattooEffectsHandle[i])) + return _tattooEffectsHandle[i]; + } + + error("getFreeSoundHandle: No sound handle found"); +} + +} // End of namespace Sherlock + diff --git a/engines/sherlock/sound.h b/engines/sherlock/sound.h new file mode 100644 index 0000000000..414b1ef49b --- /dev/null +++ b/engines/sherlock/sound.h @@ -0,0 +1,106 @@ +/* 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 +}; + +#define MAX_MIXER_CHANNELS 10 + +class Sound { +private: + SherlockEngine *_vm; + Audio::Mixer *_mixer; + Audio::SoundHandle _scalpelEffectsHandle; + Audio::SoundHandle _tattooEffectsHandle[MAX_MIXER_CHANNELS]; + int _curPriority; + + byte decodeSample(byte sample, byte& reference, int16& scale); +public: + bool _digitized; + int _voices; + bool _soundOn; + bool _speechOn; + bool _diskSoundPlaying; + bool _soundPlaying; + bool *_soundIsOn; + byte *_digiBuf; + + Common::String _talkSoundFile; +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(); + + void stopSndFuncPtr(int v1, int v2); + void freeDigiSound(); + + Audio::SoundHandle getFreeSoundHandle(); +}; + +} // End of namespace Sherlock + +#endif + diff --git a/engines/sherlock/surface.cpp b/engines/sherlock/surface.cpp new file mode 100644 index 0000000000..42194de7a3 --- /dev/null +++ b/engines/sherlock/surface.cpp @@ -0,0 +1,307 @@ +/* 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) : Fonts(), _freePixels(true) { + create(width, height); +} + +Surface::Surface() : Fonts(), _freePixels(false) { +} + +Surface::~Surface() { + if (_freePixels) + _surface.free(); +} + +void Surface::create(uint16 width, uint16 height) { + if (_freePixels) + _surface.free(); + + if (_vm->getPlatform() == Common::kPlatform3DO) { + _surface.create(width, height, Graphics::PixelFormat(2, 5, 6, 5, 0, 11, 5, 0, 0)); + } else { + _surface.create(width, height, Graphics::PixelFormat::createFormatCLUT8()); + } + _freePixels = true; +} + +Graphics::PixelFormat Surface::getPixelFormat() { + return _surface.format; +} + +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, int scaleVal) { + transBlitFrom(src._frame, pt + src._offset, flipped, overrideColor, scaleVal); +} + +void Surface::transBlitFrom(const Surface &src, const Common::Point &pt, + bool flipped, int overrideColor, int scaleVal) { + const Graphics::Surface &s = src._surface; + transBlitFrom(s, pt, flipped, overrideColor, scaleVal); +} + +void Surface::transBlitFrom(const Graphics::Surface &src, const Common::Point &pt, + bool flipped, int overrideColor, int scaleVal) { + if (scaleVal == SCALE_THRESHOLD) { + transBlitFromUnscaled(src, pt, flipped, overrideColor); + return; + } + + int scaleX = SCALE_THRESHOLD * SCALE_THRESHOLD / scaleVal; + int scaleY = scaleX; + int scaleXCtr = 0, scaleYCtr = 0; + int destX, destY; + int xCtr, yCtr; + int maxX = pt.x; + + for (yCtr = 0, destY = pt.y; yCtr < src.h && destY < this->h(); ++yCtr) { + // Handle skipping lines if Y scaling + scaleYCtr += scaleY; + + while (scaleYCtr >= SCALE_THRESHOLD && destY < this->h()) { + scaleYCtr -= SCALE_THRESHOLD; + + if (destY >= 0) { + // Handle drawing the line + const byte *pSrc = (const byte *)src.getBasePtr(flipped ? src.w - 1 : 0, yCtr); + byte *pDest = (byte *)getBasePtr(pt.x, destY); + scaleXCtr = 0; + + for (xCtr = 0, destX = pt.x; xCtr < src.w && destX < this->w(); ++xCtr) { + // Handle horizontal scaling + scaleXCtr += scaleX; + + while (scaleXCtr >= SCALE_THRESHOLD && destX < this->w()) { + scaleXCtr -= SCALE_THRESHOLD; + + // Only handle on-screen pixels + if (destX >= 0 && *pSrc != TRANSPARENCY) + *pDest = *pSrc; + + ++pDest; + ++destX; + } + + maxX = MAX(maxX, destX); + pSrc = pSrc + (flipped ? -1 : 1); + } + } + + ++destY; + } + } + + // Mark the affected area + addDirtyRect(Common::Rect(pt.x, pt.y, maxX, destY)); +} + +void Surface::transBlitFromUnscaled(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())); + + switch (src.format.bytesPerPixel) { + case 1: + // 8-bit palettized: Draw loop + assert(_surface.format.bytesPerPixel == 1); // Security check + 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; + } + } + break; + case 2: + // 3DO 15-bit RGB565: Draw loop + assert(_surface.format.bytesPerPixel == 2); // Security check + for (int yp = 0; yp < drawRect.height(); ++yp) { + const uint16 *srcP = (const uint16 *)src.getBasePtr( + flipped ? drawRect.right - 1 : drawRect.left, drawRect.top + yp); + uint16 *destP = (uint16 *)getBasePtr(destPt.x, destPt.y + yp); + + for (int xp = 0; xp < drawRect.width(); ++xp, ++destP) { + if (*srcP) // RGB 0, 0, 0 -> transparent on 3DO + *destP = *srcP; // overrideColor ? overrideColor : *srcP; + + srcP = flipped ? srcP - 1 : srcP + 1; + } + } + break; + default: + error("Surface: unsupported bytesperpixel"); + break; + } +} + +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); +} + +void Surface::fill(uint16 color) { + _surface.fillRect(Common::Rect(_surface.w, _surface.h), color); +} + +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, Graphics::PixelFormat pixelFormat) { + _surface.format = pixelFormat; + _surface.w = width; + _surface.h = height; + _surface.pitch = width * pixelFormat.bytesPerPixel; + _surface.setPixels(pixels); +} + +void Surface::writeString(const Common::String &str, const Common::Point &pt, byte overrideColor) { + Fonts::writeString(this, str, pt, overrideColor); +} + +void Surface::writeFancyString(const Common::String &str, const Common::Point &pt, byte overrideColor1, byte overrideColor2) { + writeString(str, Common::Point(pt.x, pt.y), overrideColor1); + writeString(str, Common::Point(pt.x + 1, pt.y), overrideColor1); + writeString(str, Common::Point(pt.x + 2, pt.y), overrideColor1); + writeString(str, Common::Point(pt.x, pt.y + 1), overrideColor1); + writeString(str, Common::Point(pt.x + 2, pt.y + 1), overrideColor1); + writeString(str, Common::Point(pt.x, pt.y + 2), overrideColor1); + writeString(str, Common::Point(pt.x + 1, pt.y + 2), overrideColor1); + writeString(str, Common::Point(pt.x + 2, pt.y + 2), overrideColor1); + writeString(str, Common::Point(pt.x + 1, pt.y + 1), overrideColor2); +} + +void Surface::maskArea(const ImageFrame &src, const Common::Point &pt) { + // TODO + error("TODO: maskArea"); +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/surface.h b/engines/sherlock/surface.h new file mode 100644 index 0000000000..38e3339617 --- /dev/null +++ b/engines/sherlock/surface.h @@ -0,0 +1,185 @@ +/* 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 "common/platform.h" +#include "graphics/surface.h" +#include "sherlock/fonts.h" + +namespace Sherlock { + +#define SCALE_THRESHOLD 0x100 +#define TRANSPARENCY 255 + +struct ImageFrame; + +class Surface: public Fonts { +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); + + /** + * Draws a surface at a given position within this surface with transparency + */ + void transBlitFromUnscaled(const Graphics::Surface &src, const Common::Point &pt, bool flipped, + int overrideColor); + +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); + + Graphics::PixelFormat getPixelFormat(); + + /** + * 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, int scaleVal = 256); + + /** + * 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, int scaleVal = 256); + + /** + * 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, int scaleVal = 256); + + /** + * 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); + + void fill(uint16 color); + + void maskArea(const ImageFrame &src, const Common::Point &pt); + + /** + * Clear the surface + */ + void clear(); + + /** + * Free the underlying surface + */ + void free(); + + /** + * Returns true if the surface is empty + */ + bool empty() const { return _surface.getPixels() == nullptr; } + + /** + * Set the pixels for the surface to an existing data block + */ + void setPixels(byte *pixels, int width, int height, Graphics::PixelFormat format); + + /** + * Draws the given string into the back buffer using the images stored in _font + */ + virtual void writeString(const Common::String &str, const Common::Point &pt, byte overrideColor); + void writeFancyString(const Common::String &str, const Common::Point &pt, byte overrideColor1, byte overrideColor2); + + 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..a1a003d751 --- /dev/null +++ b/engines/sherlock/talk.cpp @@ -0,0 +1,1297 @@ +/* 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" +#include "sherlock/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_people.h" +#include "sherlock/scalpel/scalpel_talk.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_talk.h" + +namespace Sherlock { + +SequenceEntry::SequenceEntry() { + _objNum = 0; + _frameNumber = 0; + _seqTo = 0; +} + +/*----------------------------------------------------------------*/ + +void Statement::load(Common::SeekableReadStream &s, bool isRoseTattoo) { + 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(); + _journal = isRoseTattoo ? s.readByte() : 0; +} + +/*----------------------------------------------------------------*/ + +TalkHistoryEntry::TalkHistoryEntry() { + Common::fill(&_data[0], &_data[16], false); +} + +/*----------------------------------------------------------------*/ + +TalkSequence::TalkSequence() { + _obj = nullptr; + _frameNumber = 0; + _sequenceNumber = 0; + _seqStack = 0; + _seqTo = 0; + _seqCounter = _seqCounter2 = 0; +} + +/*----------------------------------------------------------------*/ + +Talk *Talk::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelTalk(vm); + else + return new Tattoo::TattooTalk(vm); +} + +Talk::Talk(SherlockEngine *vm) : _vm(vm) { + _talkCounter = 0; + _talkToAbort = false; + _openTalkWindow = false; + _speaker = 0; + _talkIndex = 0; + _talkTo = 0; + _scriptSelect = 0; + _converseNum = -1; + _talkStealth = 0; + _talkToFlag = -1; + _moreTalkDown = _moreTalkUp = false; + _scriptMoreFlag = 0; + _scriptSaveIndex = -1; + _opcodes = nullptr; + _opcodeTable = nullptr; + + _charCount = 0; + _line = 0; + _yp = 0; + _wait = 0; + _pauseFlag = false; + _seqCount = 0; + _scriptStart = _scriptEnd = nullptr; + _endStr = _noTextYet = false; + + _talkHistory.resize(IS_ROSE_TATTOO ? 1500 : 500); +} + +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 + bool ongoingAnim = scene._canimShapes.size() > 0; + if (IS_ROSE_TATTOO) { + ongoingAnim = static_cast<Tattoo::TattooScene *>(_vm->_scene)->_activeCAnim.active(); + } + if (ongoingAnim || 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[HOLMES]._walkCount || (people[HOLMES]._walkTo.size() > 0 && + (IS_SERRATED_SCALPEL || people._allowWalkAbort))) { + // Only interrupt if trying to do an action, and not just if player is walking around the scene + if (people._allowWalkAbort) + abortFlag = true; + + people[HOLMES].gotoStand(); + } + + 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(); + + if (IS_SERRATED_SCALPEL) { + // Restore any pressed button + if (!ui._windowOpen && savedMode != STD_MODE) + static_cast<Scalpel::ScalpelUserInterface *>(_vm->_ui)->restoreButton((int)(savedMode - 1)); + } else { + static_cast<Tattoo::TattooPeople *>(_vm->_people)->pullNPCPaths(); + } + + // 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("^")) { + _talkIndex = select; + showTalk(); + + // 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 = Scalpel::COMMANDS[TALK_MODE - 1]; + ui._temp = ui._oldTemp = 0; + ui._menuMode = TALK_MODE; + _talkToFlag = 2; + } else { + freeTalkVars(); + + if (!ui._lookScriptFlag) { + ui.drawInterface(2); + ui._menuMode = STD_MODE; + ui._windowBounds.top = CONTROLS_Y1; + + ui.banishWindow(); + } + + 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 (IS_SERRATED_SCALPEL && 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; + UserInterface &ui = *_vm->_ui; + + ui._windowBounds.top = CONTROLS_Y; + ui._infoFlag = true; + _speaker = SPEAKER_REMOVE; + + Common::String talkFilename = (objNum >= 1000) ? people[objNum - 1000]._npcName : scene._bgShapes[objNum]._name; + loadTalkFile(talkFilename); + + // 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(talkFilename); + } else if (statement._statement.hasPrefix("*")) { + // Character being spoken to will speak first + if (objNum > 1000) { + (*static_cast<Tattoo::TattooPeople *>(_vm->_people))[objNum - 1000].walkHolmesToNPC(); + } else { + Object &obj = scene._bgShapes[objNum]; + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + + events.setCursor(WAIT); + if (obj._lookPosition.y != 0) + // Need to walk to character first + people[HOLMES].walkToCoords(obj._lookPosition, obj._lookPosition._facing); + events.setCursor(ARROW); + } + + if (!_talkToAbort) + talkTo(talkFilename); + } else { + // Holmes will be speaking first + _talkToFlag = false; + + if (objNum > 1000) { + (*static_cast<Tattoo::TattooPeople *>(_vm->_people))[objNum - 1000].walkHolmesToNPC(); + } else { + Object &obj = scene._bgShapes[objNum]; + clearSequences(); + pushSequence(_talkTo); + setStillSeq(_talkTo); + + events.setCursor(WAIT); + if (obj._lookPosition.y != 0) + // Walk over to person to talk to + people[HOLMES].walkToCoords(obj._lookPosition, obj._lookPosition._facing); + 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 { + _talkIndex = select; + showTalk(); + + // Break out of loop now that we're waiting for player input + events.setCursor(ARROW); + } + + _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].load(*talkStream, IS_ROSE_TATTOO); + + 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)_opcodes[OP_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::clearSequences() { + _sequenceStack.clear(); +} + +void Talk::pullSequence() { + Scene &scene = *_vm->_scene; + + if (_sequenceStack.empty() || IS_ROSE_TATTOO) + 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 || IS_ROSE_TATTOO) + 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::pushTalkSequence(Object *obj) { + // Check if the shape is already on the stack + for (uint idx = 0; idx < TALK_SEQUENCE_STACK_SIZE; ++idx) { + if (_talkSequenceStack[idx]._obj == obj) + return; + } + + // Find a free slot and save the details in it + for (uint idx = 0; idx < TALK_SEQUENCE_STACK_SIZE; ++idx) { + TalkSequence &ts = _talkSequenceStack[idx]; + if (ts._obj == nullptr) { + ts._obj = obj; + ts._frameNumber = obj->_frameNumber; + ts._sequenceNumber = obj->_sequenceNumber; + ts._seqStack = obj->_seqStack; + ts._seqTo = obj->_seqTo; + ts._seqCounter = obj->_seqCounter; + ts._seqCounter2 = obj->_seqCounter2; + return; + } + } + + error("Ran out of talk sequence stack space"); +} + +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) { + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + + _savedSequences.clear(); + + _scriptStart = (const byte *)script.c_str(); + _scriptEnd = _scriptStart + script.size(); + const byte *str = _scriptStart; + _charCount = 0; + _line = 0; + _wait = 0; + _pauseFlag = false; + _seqCount = 0; + _noTextYet = true; + _endStr = false; + _openTalkWindow = false; + + if (IS_SERRATED_SCALPEL) + _yp = CONTROLS_Y + 12; + else + _yp = (_talkTo == -1) ? 5 : screen.fontHeight() + 11; + + if (IS_ROSE_TATTOO) { + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + Tattoo::TattooPerson &p = (*(Tattoo::TattooPeople *)_vm->_people)[idx]; + p._savedNpcSequence = p._sequenceNumber; + p._savedNpcFrame = p._frameNumber; + } + } + + if (_scriptMoreFlag) { + _scriptMoreFlag = 0; + str = _scriptStart + _scriptSaveIndex; + } + + // Check if the script begins with a Stealh Mode Active command + if (str[0] == _opcodes[OP_STEALTH_MODE_ACTIVE] || _talkStealth) { + _talkStealth = 2; + _speaker |= SPEAKER_REMOVE; + } else { + pushSequence(_speaker); + if (IS_SERRATED_SCALPEL || ui._windowOpen) + ui.clearWindow(); + + // Need to switch speakers? + if (str[0] == _opcodes[OP_SWITCH_SPEAKER]) { + _speaker = str[1] - 1; + str += IS_SERRATED_SCALPEL ? 2 : 3; + + pullSequence(); + pushSequence(_speaker); + people.setTalkSequence(_speaker); + } else { + people.setTalkSequence(_speaker); + } + + if (IS_SERRATED_SCALPEL) { + // Assign portrait location? + if (str[0] == _opcodes[OP_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; + } + + if (IS_SERRATED_SCALPEL) { + // Remove portrait? + if ( str[0] == _opcodes[OP_REMOVE_PORTRAIT]) { + _speaker = -1; + } else { + // Nope, so set the first speaker + ((Scalpel::ScalpelPeople *)_vm->_people)->setTalking(_speaker); + } + } + } + } + + bool trigger3DOMovie = true; + uint16 subIndex = 1; + + 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 (isOpcode(c)) { + // Handle control code + switch ((this->*_opcodeTable[c - _opcodes[0]])(str)) { + case RET_EXIT: + return; + case RET_CONTINUE: + continue; + case OP_SWITCH_SPEAKER: + trigger3DOMovie = true; + break; + default: + break; + } + + ++str; + } else { + // Handle drawing the talk interface with the text + talkInterface(str); + } + + // Open window if it wasn't already open, and text has already been printed + if ((_openTalkWindow && _wait) || (_openTalkWindow && str[0] >= _opcodes[0] && str[0] != _opcodes[OP_END_TEXT_WINDOW])) { + 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) && (trigger3DOMovie)) { + // Trigger to play 3DO movie + talk3DOMovieTrigger(subIndex); + + trigger3DOMovie = false; // wait for next switch speaker opcode + subIndex++; + } + + if (_wait) + // Handling pausing + talkWait(str); + } 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 (IS_SERRATED_SCALPEL) { + if (_speaker >= 0 && _speaker < SPEAKER_REMOVE) + people.clearTalking(); + } else { + static_cast<Tattoo::TattooPeople *>(_vm->_people)->pullNPCPaths(); + } + } +} + +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 (keyState.keycode == Common::KEYCODE_ESCAPE) { + if (IS_ROSE_TATTOO && static_cast<Tattoo::TattooEngine *>(_vm)->_runningProlog) { + // Skip out of the introduction + _vm->setFlags(-76); + _vm->setFlags(396); + scene._goToScene = 1; + } + break; + + } else 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; +} + +bool Talk::isOpcode(byte checkCharacter) { + if ((checkCharacter < _opcodes[0]) || (checkCharacter >= (_opcodes[0] + 99))) + return false; // outside of range + if (_opcodeTable[checkCharacter - _opcodes[0]]) + return true; + return false; +} + +void Talk::popStack() { + if (!_scriptStack.empty()) { + ScriptStackEntry scriptEntry = _scriptStack.pop(); + _scriptName = scriptEntry._name; + _scriptSaveIndex = scriptEntry._currentIndex; + _scriptSelect = scriptEntry._select; + _scriptMoreFlag = 1; + } +} + +void Talk::synchronize(Serializer &s) { + for (uint idx = 0; idx < _talkHistory.size(); ++idx) { + TalkHistoryEntry &he = _talkHistory[idx]; + + for (int flag = 0; flag < 16; ++flag) + s.syncAsByte(he._data[flag]); + } +} + +OpcodeReturn Talk::cmdAddItemToInventory(const byte *&str) { + Inventory &inv = *_vm->_inventory; + Common::String tempString; + + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + str += str[0]; + + inv.putNameInInventory(tempString); + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdAdjustObjectSequence(const byte *&str) { + Scene &scene = *_vm->_scene; + Common::String tempString; + + // 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; + + return RET_CONTINUE; +} + +OpcodeReturn Talk::cmdBanishWindow(const byte *&str) { + People &people = *_vm->_people; + UserInterface &ui = *_vm->_ui; + + if (!(_speaker & SPEAKER_REMOVE)) + people.clearTalking(); + pullSequence(); + + if (_talkToAbort) + return RET_EXIT; + + _speaker |= SPEAKER_REMOVE; + ui.banishWindow(); + ui._menuMode = TALK_MODE; + _noTextYet = true; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdCallTalkFile(const byte *&str) { + Common::String tempString; + + ++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; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdDisableEndKey(const byte *&str) { + _vm->_ui->_endKeyActive = false; + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdEnableEndKey(const byte *&str) { + _vm->_ui->_endKeyActive = true; + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdEndTextWindow(const byte *&str) { + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdHolmesOff(const byte *&str) { + People &people = *_vm->_people; + people[HOLMES]._type = REMOVE; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdHolmesOn(const byte *&str) { + People &people = *_vm->_people; + people[HOLMES]._type = CHARACTER; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdPause(const byte *&str) { + _charCount = *++str; + _wait = _pauseFlag = true; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdPauseWithoutControl(const byte *&str) { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + ++str; + + for (int idx = 0; idx < (str[0] - 1); ++idx) { + scene.doBgAnim(); + if (_talkToAbort) + return RET_EXIT; + + // Check for button press + events.pollEvents(); + events.setButtonState(); + } + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdRemoveItemFromInventory(const byte *&str) { + Inventory &inv = *_vm->_inventory; + Common::String tempString; + + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + str += str[0]; + + inv.deleteItemFromInventory(tempString); + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdRunCAnimation(const byte *&str) { + Scene &scene = *_vm->_scene; + + ++str; + scene.startCAnim((str[0] - 1) & 127, (str[0] & 0x80) ? -1 : 1); + if (_talkToAbort) + return RET_EXIT; + + // Check if next character is changing side or changing portrait + if (_charCount && (str[1] == _opcodes[OP_SWITCH_SPEAKER] || str[1] == _opcodes[OP_ASSIGN_PORTRAIT_LOCATION])) + _wait = 1; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdSetFlag(const byte *&str) { + ++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; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdSetObject(const byte *&str) { + Scene &scene = *_vm->_scene; + Common::String tempString; + + ++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(); + } + } + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdStealthModeActivate(const byte *&str) { + _talkStealth = 2; + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdStealthModeDeactivate(const byte *&str) { + Events &events = *_vm->_events; + + _talkStealth = 0; + events.clearKeyboard(); + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdToggleObject(const byte *&str) { + Scene &scene = *_vm->_scene; + Common::String tempString; + + ++str; + for (int idx = 0; idx < str[0]; ++idx) + tempString += str[idx + 1]; + + scene.toggleObject(tempString); + str += str[0]; + + return RET_SUCCESS; +} + +OpcodeReturn Talk::cmdWalkToCAnimation(const byte *&str) { + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + + ++str; + CAnim &animation = scene._cAnim[str[0] - 1]; + people[HOLMES].walkToCoords(animation._goto[0], animation._goto[0]._facing); + + return _talkToAbort ? RET_EXIT : RET_SUCCESS; +} + +void Talk::talkWait(const byte *&str) { + 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] == _opcodes[OP_SFX_COMMAND]) + str += 9; + } + + _pauseFlag = false; +} + +} // End of namespace Sherlock diff --git a/engines/sherlock/talk.h b/engines/sherlock/talk.h new file mode 100644 index 0000000000..ddb81f1b04 --- /dev/null +++ b/engines/sherlock/talk.h @@ -0,0 +1,381 @@ +/* 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/stream.h" +#include "common/stack.h" +#include "sherlock/objects.h" +#include "sherlock/saveload.h" + +namespace Sherlock { + +#define SPEAKER_REMOVE 0x80 +#define MAX_TALK_SEQUENCES 11 +#define TALK_SEQUENCE_STACK_SIZE 20 + +enum { + OP_SWITCH_SPEAKER = 0, + OP_RUN_CANIMATION = 1, + OP_ASSIGN_PORTRAIT_LOCATION = 2, + OP_PAUSE = 3, + OP_REMOVE_PORTRAIT = 4, + OP_CLEAR_WINDOW = 5, + OP_ADJUST_OBJ_SEQUENCE = 6, + OP_WALK_TO_COORDS = 7, + OP_PAUSE_WITHOUT_CONTROL = 8, + OP_BANISH_WINDOW = 9, + OP_SUMMON_WINDOW = 10, + OP_SET_FLAG = 11, + OP_SFX_COMMAND = 12, + OP_TOGGLE_OBJECT = 13, + OP_STEALTH_MODE_ACTIVE = 14, + OP_IF_STATEMENT = 15, + OP_ELSE_STATEMENT = 16, + OP_END_IF_STATEMENT = 17, + OP_STEALTH_MODE_DEACTIVATE = 18, + OP_TURN_HOLMES_OFF = 19, + OP_TURN_HOLMES_ON = 20, + OP_GOTO_SCENE = 21, + OP_PLAY_PROLOGUE = 22, + OP_ADD_ITEM_TO_INVENTORY = 23, + OP_SET_OBJECT = 24, + OP_CALL_TALK_FILE = 25, + OP_MOVE_MOUSE = 26, + OP_DISPLAY_INFO_LINE = 27, + OP_CLEAR_INFO_LINE = 28, + OP_WALK_TO_CANIMATION = 29, + OP_REMOVE_ITEM_FROM_INVENTORY = 30, + OP_ENABLE_END_KEY = 31, + OP_DISABLE_END_KEY = 32, + OP_END_TEXT_WINDOW = 33, + + OP_MOUSE_OFF_ON = 34, + OP_SET_WALK_CONTROL = 35, + OP_SET_TALK_SEQUENCE = 36, + OP_PLAY_SONG = 37, + OP_WALK_HOLMES_AND_NPC_TO_CANIM = 38, + OP_SET_NPC_PATH_DEST = 39, + OP_NEXT_SONG = 40, + OP_SET_NPC_PATH_PAUSE = 41, + OP_NEED_PASSWORD = 42, + OP_SET_SCENE_ENTRY_FLAG = 43, + OP_WALK_NPC_TO_CANIM = 44, + OP_WALK_NPC_TO_COORDS = 45, + OP_WALK_HOLMES_AND_NPC_TO_COORDS = 46, + OP_SET_NPC_TALK_FILE = 47, + OP_TURN_NPC_OFF = 48, + OP_TURN_NPC_ON = 49, + OP_NPC_DESC_ON_OFF = 50, + OP_NPC_PATH_PAUSE_TAKING_NOTES = 51, + OP_NPC_PATH_PAUSE_LOOKING_HOLMES = 52, + OP_ENABLE_TALK_INTERRUPTS = 53, + OP_DISABLE_TALK_INTERRUPTS = 54, + OP_SET_NPC_INFO_LINE = 55, + OP_SET_NPC_POSITION = 56, + OP_NPC_PATH_LABEL = 57, + OP_PATH_GOTO_LABEL = 58, + OP_PATH_IF_FLAG_GOTO_LABEL = 59, + OP_NPC_WALK_GRAPHICS = 60, + OP_NPC_VERB = 61, + OP_NPC_VERB_CANIM = 62, + OP_NPC_VERB_SCRIPT = 63, + OP_RESTORE_PEOPLE_SEQUENCE = 64, + OP_NPC_VERB_TARGET = 65, + OP_TURN_SOUNDS_OFF = 66, + OP_NULL = 67 +}; + +enum OpcodeReturn { RET_EXIT = -1, RET_SUCCESS = 0, RET_CONTINUE = 1 }; + +class SherlockEngine; +class Talk; +namespace Scalpel { class ScalpelUserInterface; } + +typedef OpcodeReturn(Talk::*OpcodeMethod)(const byte *&str); + +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; + int _journal; + + /** + * Load the data for a single statement within a talk file + */ + void load(Common::SeekableReadStream &s, bool isRoseTattoo); +}; + +struct TalkHistoryEntry { + bool _data[16]; + + TalkHistoryEntry(); + bool &operator[](int index) { return _data[index]; } +}; + +struct TalkSequence { + Object *_obj; // Pointer to the bgshape that these values go to + short _frameNumber; // Frame number in frame sequence to draw + short _sequenceNumber; // Start frame of sequences that are repeated + int _seqStack; // Allows gosubs to return to calling frame + int _seqTo; // Allows 1-5, 8-3 type sequences encoded + int _seqCounter; // How many times this sequence has been executed + int _seqCounter2; + + TalkSequence(); +}; + + +class Talk { + friend class Scalpel::ScalpelUserInterface; +private: + /** + * Remove any voice commands from a loaded statement list + */ + void stripVoiceCommands(); +protected: + SherlockEngine *_vm; + OpcodeMethod *_opcodeTable; + Common::Stack<SequenceEntry> _savedSequences; + Common::Stack<SequenceEntry> _sequenceStack; + Common::Stack<ScriptStackEntry> _scriptStack; + Common::Array<TalkHistoryEntry> _talkHistory; + int _speaker; + int _talkIndex; + int _scriptSelect; + int _talkStealth; + int _talkToFlag; + int _scriptSaveIndex; + + // These fields are used solely by doScript, but are fields because all the script opcodes are + // separate methods now, and need access to these fields + int _yp; + int _charCount; + int _line; + int _wait; + bool _pauseFlag; + bool _endStr, _noTextYet; + int _seqCount; + const byte *_scriptStart, *_scriptEnd; +protected: + Talk(SherlockEngine *vm); + + OpcodeReturn cmdAddItemToInventory(const byte *&str); + OpcodeReturn cmdAdjustObjectSequence(const byte *&str); + OpcodeReturn cmdBanishWindow(const byte *&str); + OpcodeReturn cmdCallTalkFile(const byte *&str); + OpcodeReturn cmdDisableEndKey(const byte *&str); + OpcodeReturn cmdEnableEndKey(const byte *&str); + OpcodeReturn cmdEndTextWindow(const byte *&str); + OpcodeReturn cmdHolmesOff(const byte *&str); + OpcodeReturn cmdHolmesOn(const byte *&str); + OpcodeReturn cmdPause(const byte *&str); + OpcodeReturn cmdPauseWithoutControl(const byte *&str); + OpcodeReturn cmdRemoveItemFromInventory(const byte *&str); + OpcodeReturn cmdRunCAnimation(const byte *&str); + OpcodeReturn cmdSetFlag(const byte *&str); + OpcodeReturn cmdSetObject(const byte *&str); + OpcodeReturn cmdStealthModeActivate(const byte *&str); + OpcodeReturn cmdStealthModeDeactivate(const byte *&str); + OpcodeReturn cmdToggleObject(const byte *&str); + OpcodeReturn cmdWalkToCAnimation(const byte *&str); +protected: + /** + * Checks, if a character is an opcode + */ + bool isOpcode(byte checkCharacter); + + /** + * Form a table of the display indexes for statements + */ + void setTalkMap(); + + /** + * 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); + + /** + * Display the talk interface window + */ + virtual void talkInterface(const byte *&str) = 0; + + /** + * Pause when displaying a talk dialog on-screen + */ + virtual void talkWait(const byte *&str); + + /** + * Trigger to play a 3DO talk dialog movie + */ + virtual void talk3DOMovieTrigger(int subIndex) {}; + + /** + * Show the talk display + */ + virtual void showTalk() = 0; +public: + TalkSequence _talkSequenceStack[TALK_SEQUENCE_STACK_SIZE]; + Common::Array<Statement> _statements; + bool _talkToAbort; + int _talkCounter; + int _talkTo; + int _scriptMoreFlag; + bool _openTalkWindow; + Common::String _scriptName; + bool _moreTalkUp, _moreTalkDown; + int _converseNum; + const byte *_opcodes; +public: + static Talk *init(SherlockEngine *vm); + virtual ~Talk() {} + + /** + * 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); + + /** + * 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); + + /** + * 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(); + + /** + * 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); + + /** + * Push a given shape's sequence data onto the Rose Tattoo talk sequence stack + */ + void pushTalkSequence(Object *obj); + + /** + * 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(Serializer &s); + + /** + * Draws the interface for conversation display + */ + virtual void drawInterface() {} + + /** + * Display a list of statements in a window at the bottom of the screen that the + * player can select from. + */ + virtual bool displayTalk(bool slamIt) { return false; } + + /** + * Prints a single conversation option in the interface window + */ + virtual int talkLine(int lineNum, int stateNum, byte color, int lineY, bool slamIt) { return 0; } +}; + +} // 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..90d2e5d958 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo.cpp @@ -0,0 +1,576 @@ +/* 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 "engines/util.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_resources.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/widget_base.h" +#include "sherlock/people.h" + +namespace Sherlock { + +namespace Tattoo { + +TattooEngine::TattooEngine(OSystem *syst, const SherlockGameDescription *gameDesc) : + SherlockEngine(syst, gameDesc), _darts(this) { + _creditsActive = false; + _runningProlog = false; + _fastMode = false; + _allowFastMode = true; + _transparentMenus = true; + _creditSpeed = 4; +} + +TattooEngine::~TattooEngine() { +} + +void TattooEngine::showOpening() { + // No implementation - opening is done using in-game scenes +} + +void TattooEngine::initialize() { + initGraphics(640, 480, true); + + // Initialize the base engine + SherlockEngine::initialize(); + + // Initialise the global flags + _flags.resize(3200); + _flags[1] = _flags[4] = _flags[76] = true; + _runningProlog = true; + + // Add some more files to the cache + _res->addToCache("walk.lib"); + + // Set up list of people + for (int idx = 0; idx < TATTOO_MAX_PEOPLE; ++idx) { + _people->_characters.push_back(PersonData( + getLanguage() == Common::FR_FRA ? FRENCH_NAMES[idx] : ENGLISH_NAMES[idx], + PORTRAITS[idx], nullptr, nullptr)); + } + + // Load the inventory + loadInventory(); + + // Starting scene + _scene->_goToScene = STARTING_INTRO_SCENE; + + // Load an initial palette + loadInitialPalette(); +} + +void TattooEngine::startScene() { + TattooUserInterface &ui = *(TattooUserInterface *)_ui; + + switch (_scene->_goToScene) { + case 7: + case 8: + case 18: + case 53: + case 68: + // Load overlay mask(s) for the scene + ui._mask = new ImageFile(Common::String::format("res%02d.msk", _scene->_goToScene)); + if (_scene->_goToScene == 8) + ui._mask1 = new ImageFile("res08a.msk"); + else if (_scene->_goToScene == 18 || _scene->_goToScene == 68) + ui._mask1 = new ImageFile("res08a.msk"); + break; + + case OVERHEAD_MAP: + case OVERHEAD_MAP2: + // Show the map + _scene->_currentScene = OVERHEAD_MAP; + _scene->_goToScene = _map->show(); + + _people->_savedPos = Common::Point(-1, -1); + _people->_savedPos._facing = -1; + break; + + case 101: + // Darts Board minigame + _darts.playDarts(GAME_CRICKET); + break; + + case 102: + // Darts Board minigame + _darts.playDarts(GAME_301); + break; + + case 103: + // Darts Board minigame + _darts.playDarts(GAME_501); + break; + + default: + break; + } + + _events->setCursor(ARROW); +} + +void TattooEngine::loadInitialPalette() { + byte palette[768]; + Common::SeekableReadStream *stream = _res->load("room.pal"); + stream->read(palette, PALETTE_SIZE); + _screen->translatePalette(palette); + _screen->setPalette(palette); + + delete stream; +} + +void TattooEngine::loadInventory() { + Inventory &inv = *_inventory; + + Common::String inv1 = _fixedText->getText(kFixedText_Inv1); + Common::String inv2 = _fixedText->getText(kFixedText_Inv2); + Common::String inv3 = _fixedText->getText(kFixedText_Inv3); + Common::String inv4 = _fixedText->getText(kFixedText_Inv4); + Common::String inv5 = _fixedText->getText(kFixedText_Inv5); + Common::String inv6 = _fixedText->getText(kFixedText_Inv6); + Common::String inv7 = _fixedText->getText(kFixedText_Inv7); + Common::String inv8 = _fixedText->getText(kFixedText_Inv8); + Common::String invDesc1 = _fixedText->getText(kFixedText_InvDesc1); + Common::String invDesc2 = _fixedText->getText(kFixedText_InvDesc2); + Common::String invDesc3 = _fixedText->getText(kFixedText_InvDesc3); + Common::String invDesc4 = _fixedText->getText(kFixedText_InvDesc4); + Common::String invDesc5 = _fixedText->getText(kFixedText_InvDesc5); + Common::String invDesc6 = _fixedText->getText(kFixedText_InvDesc6); + Common::String invDesc7 = _fixedText->getText(kFixedText_InvDesc7); + Common::String invDesc8 = _fixedText->getText(kFixedText_InvDesc8); + Common::String solve = _fixedText->getText(kFixedText_Solve); + + // Initial inventory + inv._holdings = 5; + inv.push_back(InventoryItem(0, inv1, invDesc1, "_ITEM01A")); + inv.push_back(InventoryItem(0, inv2, invDesc2, "_ITEM02A")); + inv.push_back(InventoryItem(0, inv3, invDesc3, "_ITEM03A")); + inv.push_back(InventoryItem(0, inv4, invDesc4, "_ITEM04A")); + inv.push_back(InventoryItem(0, inv5, invDesc5, "_ITEM05A")); + + // Hidden items + inv.push_back(InventoryItem(0, inv6, invDesc6, "_PAP212D", solve)); + inv.push_back(InventoryItem(0, inv7, invDesc7, "_PAP212I")); + inv.push_back(InventoryItem(0, inv8, invDesc8, "_LANT02I")); +} + +void TattooEngine::initCredits() { + Common::SeekableReadStream *stream = _res->load("credits.txt"); + int spacing = _screen->fontHeight() * 2; + int yp = _screen->h(); + + _creditsActive = true; + _creditLines.clear(); + + while (stream->pos() < stream->size()) { + Common::String line = stream->readLine(); + + if (line.hasPrefix("Scroll Speed")) { + const char *p = line.c_str() + 12; + while ((*p < '0') || (*p > '9')) + p++; + + _creditSpeed = atoi(p); + } else if (line.hasPrefix("Y Spacing")) { + const char *p = line.c_str() + 12; + while ((*p < '0') || (*p > '9')) + p++; + + spacing = atoi(p) + _screen->fontHeight() + 1; + } else { + int width = _screen->stringWidth(line) + 2; + + _creditLines.push_back(CreditLine(line, Common::Point((_screen->w() - width) / 2 + 1, yp), width)); + yp += spacing; + } + } + + // Post-processing for finding split lines + for (int l = 0; l < (int)_creditLines.size(); ++l) { + CreditLine &cl = _creditLines[l]; + const char *p = strchr(cl._line.c_str(), '-'); + + if (p != nullptr && p[1] == '>') { + cl._line2 = Common::String(p + 3); + cl._line = Common::String(cl._line.c_str(), p); + + int width = cl._width; + int width1 = _screen->stringWidth(cl._line); + int width2 = _screen->stringWidth(cl._line2); + + int c = 1; + for (int l1 = l + 1; l1 < (int)_creditLines.size(); ++l1) { + if ((p = strchr(_creditLines[l1]._line.c_str(), '-')) != nullptr) { + if (p[1] == '>') { + Common::String line1 = Common::String(_creditLines[l1]._line.c_str(), p); + Common::String line2 = Common::String(p + 3); + + width1 = MAX(width1, _screen->stringWidth(line1)); + + if (_screen->stringWidth(line2) > width2) + width2 = _screen->stringWidth(line2); + ++c; + } else { + break; + } + } else { + break; + } + } + + width = width1 + width2 + _screen->widestChar(); + width1 += _screen->widestChar(); + + for (int l1 = l; l1 < l + c; ++l1) { + _creditLines[l1]._width = width; + _creditLines[l1]._xOffset = width1; + } + + l += c - 1; + } + } + + delete stream; +} + +void TattooEngine::drawCredits() { + Common::Rect screenRect(0, 0, _screen->w(), _screen->h()); + Surface &bb1 = _screen->_backBuffer1; + + for (uint idx = 0; idx < _creditLines.size() && _creditLines[idx]._position.y < _screen->h(); ++idx) { + if (screenRect.contains(_creditLines[idx]._position)) { + if (!_creditLines[idx]._line2.empty()) { + int x1 = _creditLines[idx]._position.x; + int x2 = x1 + _creditLines[idx]._xOffset; + const Common::String &line1 = _creditLines[idx]._line; + const Common::String &line2 = _creditLines[idx]._line2; + + bb1.writeString(line1, Common::Point(x1 - 1, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(line1, Common::Point(x1, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(line1, Common::Point(x1 + 1, _creditLines[idx]._position.y - 1), 0); + + bb1.writeString(line1, Common::Point(x1 - 1, _creditLines[idx]._position.y), 0); + bb1.writeString(line1, Common::Point(x1 + 1, _creditLines[idx]._position.y), 0); + + bb1.writeString(line1, Common::Point(x1 - 1, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(line1, Common::Point(x1, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(line1, Common::Point(x1 + 1, _creditLines[idx]._position.y + 1), 0); + + bb1.writeString(line1, Common::Point(x1, _creditLines[idx]._position.y), INFO_TOP); + + bb1.writeString(line2, Common::Point(x2 - 1, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(line2, Common::Point(x2, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(line2, Common::Point(x2 + 1, _creditLines[idx]._position.y - 1), 0); + + bb1.writeString(line2, Common::Point(x2 - 1, _creditLines[idx]._position.y), 0); + bb1.writeString(line2, Common::Point(x2 + 1, _creditLines[idx]._position.y), 0); + + bb1.writeString(line2, Common::Point(x2 - 1, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(line2, Common::Point(x2, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(line2, Common::Point(x2 + 1, _creditLines[idx]._position.y + 1), 0); + + bb1.writeString(line2, Common::Point(x2, _creditLines[idx]._position.y), INFO_TOP); + } else { + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x - 1, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x, _creditLines[idx]._position.y - 1), 0); + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x + 1, _creditLines[idx]._position.y - 1), 0); + + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x - 1, _creditLines[idx]._position.y), 0); + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x + 1, _creditLines[idx]._position.y), 0); + + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x - 1, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x, _creditLines[idx]._position.y + 1), 0); + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x + 1, _creditLines[idx]._position.y + 1), 0); + + bb1.writeString(_creditLines[idx]._line, Common::Point(_creditLines[idx]._position.x, _creditLines[idx]._position.y), INFO_TOP); + } + } + } +} + +void TattooEngine::blitCredits() { + Common::Rect screenRect(0, -_creditSpeed, _screen->w(), _screen->h() + _creditSpeed); + + for (uint idx = 0; idx < _creditLines.size(); ++idx) { + if (screenRect.contains(_creditLines[idx]._position)) { + Common::Rect r(_creditLines[idx]._width, _screen->fontHeight() + 2); + r.moveTo(_creditLines[idx]._position.x, _creditLines[idx]._position.y - 1); + + _screen->slamRect(r); + } + + _creditLines[idx]._position.y -= _creditSpeed; + } +} + +void TattooEngine::eraseCredits() { + Common::Rect screenRect(0, -_creditSpeed, _screen->w(), _screen->h() + _creditSpeed); + + for (uint idx = 0; idx < _creditLines.size(); ++idx) { + if (screenRect.contains(_creditLines[idx]._position)) { + Common::Rect r(_creditLines[idx]._width, _screen->fontHeight() + 3); + r.moveTo(_creditLines[idx]._position.x, _creditLines[idx]._position.y - 1 + _creditSpeed); + + _screen->restoreBackground(r); + } + } + + if (_creditLines[_creditLines.size() - 1]._position.y < -_creditSpeed) { + _creditLines.clear(); + _creditsActive = false; + setFlags(!3000); + } +} + +void TattooEngine::doHangManPuzzle() { + char answers[3][10]; + Common::Point lines[3]; + const char *solutions[3]; + int numWide, spacing; + ImageFile *paper; + Common::Point cursorPos; + byte cursorColor = 254; + bool solved = false; + bool done = false; + bool flag = false; + size_t i = 0; + + switch (getLanguage()) { + case Common::FR_FRA: + lines[0] = Common::Point(34, 210); + lines[1] = Common::Point(72, 242); + lines[2] = Common::Point(34, 276); + numWide = 8; + spacing = 19; + paper = new ImageFile("paperf.vgs"); + break; + + case Common::DE_DEU: + lines[0] = Common::Point(44, 73); + lines[1] = Common::Point(56, 169); + lines[2] = Common::Point(47, 256); + numWide = 7; + spacing = 19; + paper = new ImageFile("paperg.vgs"); + break; + + default: + // English + lines[0] = Common::Point(65, 84); + lines[1] = Common::Point(65, 159); + lines[2] = Common::Point(75, 234); + numWide = 5; + spacing = 20; + paper = new ImageFile("paper.vgs"); + break; + } + + ImageFrame &paperFrame = (*paper)[0]; + Common::Rect paperBounds(paperFrame._width, paperFrame._height); + paperBounds.moveTo((_screen->w() - paperFrame._width) / 2, (_screen->h() - paperFrame._height) / 2); + + for (int line = 0; line<3; ++line) { + lines[line].x += paperBounds.left; + lines[line].y += paperBounds.top; + + for (i = 0; i <= (size_t)numWide; ++i) + answers[line][i] = 0; + } + + _screen->_backBuffer1.blitFrom(paperFrame, Common::Point(paperBounds.left + _screen->_currentScroll.x, 0)); + + // If they have already solved the puzzle, put the answer on the graphic + if (readFlags(299)) { + for (int line = 0; line < 3; ++line) { + cursorPos.y = lines[line].y - _screen->fontHeight() - 2; + + for (i = 0; i < strlen(solutions[line]); ++i) { + cursorPos.x = lines[line].x + 8 - _screen->widestChar() / 2 + i * spacing; + _screen->gPrint(Common::Point(cursorPos.x + _screen->widestChar() / 2 - + _screen->charWidth(solutions[line][i]) / 2, cursorPos.y), 0, "%c", solutions[line][i]); + } + } + } + + _screen->slamRect(paperBounds); + cursorPos = Common::Point(lines[0].x + 8 - _screen->widestChar() / 2, lines[0].y - _screen->fontHeight() - 2); + int line = 0; + + // If they have not solved the puzzle, let them solve it here + if (!readFlags(299)) { + do { + while (!_events->kbHit()) { + // See if a key or a mouse button is pressed + _events->pollEventsAndWait(); + _events->setButtonState(); + + flag = !flag; + if (flag) { + _screen->_backBuffer1.fillRect(Common::Rect(cursorPos.x + _screen->_currentScroll.x, cursorPos.y, + cursorPos.x + _screen->widestChar() + _screen->_currentScroll.x - 1, cursorPos.y + _screen->fontHeight() - 1), cursorColor); + if (answers[line][i]) + _screen->gPrint(Common::Point(cursorPos.x + _screen->widestChar() / 2 - _screen->charWidth(answers[line][i]) / 2, + cursorPos.y), 0, "%c", answers[line][i]); + _screen->slamArea(cursorPos.x, cursorPos.y, _screen->widestChar(), _screen->fontHeight()); + } else { + _screen->setDisplayBounds(Common::Rect(cursorPos.x + _screen->_currentScroll.x, cursorPos.y, + cursorPos.x + _screen->widestChar() + _screen->_currentScroll.x, cursorPos.y + _screen->fontHeight())); + _screen->_backBuffer->blitFrom(paperFrame, Common::Point(paperBounds.left + _screen->_currentScroll.x, paperBounds.top)); + _screen->resetDisplayBounds(); + + if (answers[line][i]) + _screen->gPrint(Common::Point(cursorPos.x + _screen->widestChar() / 2 - _screen->charWidth(answers[line][i]) / 2, + cursorPos.y), 0, "%c", answers[line][i]); + _screen->slamArea(cursorPos.x, cursorPos.y, _screen->widestChar(), _screen->fontHeight()); + } + + if (!_events->kbHit()) + _events->wait(2); + } + + if (_events->kbHit()) { + Common::KeyState keyState = _events->getKey(); + + if (((toupper(keyState.ascii) >= 'A') && (toupper(keyState.ascii) <= 'Z')) || + ((keyState.ascii >= 128) && ((keyState.ascii <= 168) || (keyState.ascii == 225)))) { + answers[line][i] = keyState.ascii; + keyState.keycode = Common::KEYCODE_RIGHT; + } + + _screen->setDisplayBounds(Common::Rect(cursorPos.x + _screen->_currentScroll.x, cursorPos.y, + cursorPos.x + _screen->widestChar() + _screen->_currentScroll.x, cursorPos.y + _screen->fontHeight())); + _screen->_backBuffer->blitFrom(paperFrame, Common::Point(paperBounds.left + _screen->_currentScroll.x, paperBounds.top)); + _screen->resetDisplayBounds(); + + if (answers[line][i]) + _screen->gPrint(Common::Point(cursorPos.x + _screen->widestChar() / 2 - _screen->charWidth(answers[line][i]) / 2, + cursorPos.y), 0, "%c", answers[line][i]); + _screen->slamArea(cursorPos.x, cursorPos.y, _screen->widestChar(), _screen->fontHeight()); + + switch (keyState.keycode) { + case Common::KEYCODE_ESCAPE: + done = true; + break; + + case Common::KEYCODE_UP: + case Common::KEYCODE_KP8: + if (line) { + line--; + if (i >= strlen(solutions[line])) + i = strlen(solutions[line]) - 1; + } + break; + + case Common::KEYCODE_DOWN: + case Common::KEYCODE_KP2: + if (line < 2) { + ++line; + if (i >= strlen(solutions[line])) + i = strlen(solutions[line]) - 1; + } + break; + + case Common::KEYCODE_BACKSPACE: + case Common::KEYCODE_LEFT: + case Common::KEYCODE_KP4: + if (i) + --i; + else if (line) { + --line; + + i = strlen(solutions[line]) - 1; + } + + if (keyState.keycode == Common::KEYCODE_BACKSPACE) + answers[line][i] = ' '; + break; + + case Common::KEYCODE_RIGHT: + case Common::KEYCODE_KP6: + if (i < strlen(solutions[line]) - 1) + i++; + else if (line < 2) { + ++line; + i = 0; + } + break; + + case Common::KEYCODE_DELETE: + answers[line][i] = ' '; + break; + + default: + break; + } + } + + cursorPos.x = lines[line].x + 8 - _screen->widestChar() / 2 + i * spacing; + cursorPos.y = lines[line].y - _screen->fontHeight() - 2; + + // See if all of their anwers are correct + if (!scumm_stricmp(answers[0], solutions[0]) && !scumm_stricmp(answers[1], solutions[1]) && + !scumm_stricmp(answers[2], solutions[2])) { + done = true; + solved = true; + } + } while (!done && !shouldQuit()); + } else { + // They have already solved the puzzle, so just display the solution and wait for a mouse or key click + do { + _events->pollEventsAndWait(); + _events->setButtonState(); + + if ((_events->kbHit()) || (_events->_released) || (_events->_rightReleased)) { + done = true; + _events->clearEvents(); + } + } while (!done && !shouldQuit()); + } + + delete paper; + _screen->_backBuffer1.blitFrom(_screen->_backBuffer2, Common::Point(paperBounds.left + _screen->_currentScroll.x, paperBounds.top), + Common::Rect(paperBounds.left + _screen->_currentScroll.x, paperBounds.top, + paperBounds.right + _screen->_currentScroll.x, paperBounds.bottom)); + _scene->doBgAnim(); + + _screen->slamArea(paperBounds.left + _screen->_currentScroll.x, paperBounds.top, + paperBounds.width(), paperBounds.height()); + + // Don't call the talk files if the puzzle has already been solved + if (readFlags(299)) + return; + + // If they solved the puzzle correctly, set the solved flag and run the appropriate talk scripts + if (solved) { + _talk->talkTo("SLVE12S.TLK"); + _talk->talkTo("WATS12X.TLK"); + setFlags(299); + } else { + _talk->talkTo("HOLM12X.TLK"); + } +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo.h b/engines/sherlock/tattoo/tattoo.h new file mode 100644 index 0000000000..a9798dce41 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo.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_TATTOO_H +#define SHERLOCK_TATTOO_H + +#include "sherlock/sherlock.h" +#include "sherlock/tattoo/tattoo_darts.h" + +namespace Sherlock { + +namespace Tattoo { + +enum { + INV_FOREGROUND = 167, + INV_BACKGROUND = 1, + INFO_FOREGROUND = 233, + INFO_BACKGROUND = 239, + INFO_TOP = 185, + INFO_MIDDLE = 186, + INFO_BOTTOM = 188, + MENU_BACKGROUND = 225, + COMMAND_FOREGROUND = 15, + COMMAND_HIGHLIGHTED = 254, + COMMAND_NULL = 193, + PEN_COLOR = 248, + PEN_HIGHLIGHT_COLOR = 129 +}; + +enum { + FLAG_PLAYER_IS_HOLMES = 76, + FLAG_ALT_MAP_MUSIC = 525 +}; + +struct CreditLine { + Common::Point _position; + int _xOffset; + int _width; + Common::String _line, _line2; + + CreditLine(const Common::String &line, const Common::Point &pt, int width) : + _line(line), _position(pt), _width(width), _xOffset(0) {} +}; + +class TattooEngine : public SherlockEngine { +private: + Darts _darts; + Common::Array<CreditLine> _creditLines; + int _creditSpeed; + + /** + * Loads the initial palette for the game + */ + void loadInitialPalette(); + + /** + * Load the initial inventory + */ + void loadInventory(); +protected: + /** + * Initialize the engine + */ + virtual void initialize(); + + virtual void showOpening(); + + /** + * Starting a scene within the game + */ + virtual void startScene(); +public: + bool _creditsActive; + bool _runningProlog; + bool _fastMode, _allowFastMode; + bool _transparentMenus; +public: + TattooEngine(OSystem *syst, const SherlockGameDescription *gameDesc); + virtual ~TattooEngine(); + + /** + * Initialize and load credit data for display + */ + void initCredits(); + + /** + * Draw credits on the screen + */ + void drawCredits(); + + /** + * Blit the drawn credits to the screen + */ + void blitCredits(); + + /** + * Erase any area of the screen covered by credits + */ + void eraseCredits(); + + void doHangManPuzzle(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_darts.cpp b/engines/sherlock/tattoo/tattoo_darts.cpp new file mode 100644 index 0000000000..842320e270 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_darts.cpp @@ -0,0 +1,967 @@ +/* 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_darts.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +enum { + DART_COLOR_FORE = 5, + PLAYER_COLOR = 11 +}; + +static const int STATUS_INFO_X = 430; +static const int STATUS_INFO_Y = 50; +static const int STATUS_INFO_WIDTH = 205; +static const int STATUS_INFO_HEIGHT = 330; +static const int STATUS2_INFO_X = 510; +static const int STATUS2_X_ADD = STATUS2_INFO_X - STATUS_INFO_X; +static const int DART_BAR_VX = 10; +static const int DART_HEIGHT_Y = 121; +static const int DART_BAR_SIZE = 150; +static const int DARTBOARD_LEFT = 73; +static const int DARTBOARD_TOP = 68; +static const int DARTBOARD_WIDTH = 257; +static const int DARTBOARD_HEIGHT = 256; +static const int DARTBOARD_TOTALX = DARTBOARD_WIDTH * 120 / 100; +static const int DARTBOARD_TOTALY = DARTBOARD_HEIGHT * 120 / 100; +static const int DARTBOARD_TOTALTOP = DARTBOARD_TOP - DARTBOARD_WIDTH / 10; +static const int DARTBOARD_TOTALLEFT = DARTBOARD_LEFT - DARTBOARD_HEIGHT / 10; +static const int CRICKET_VALUE[7] = { 20, 19, 18, 17, 16, 15, 25 }; + +Darts::Darts(SherlockEngine *vm) : _vm(vm) { + _gameType = GAME_301; + _hand1 = _hand2 = nullptr; + _dartGraphics = nullptr; + _dartsLeft = nullptr; + _dartMap = nullptr; + _dartBoard = nullptr; + Common::fill(&_cricketScore[0][0], &_cricketScore[0][7], 0); + Common::fill(&_cricketScore[1][0], &_cricketScore[1][7], 0); + _score1 = _score2 = 0; + _roundNum = 0; + _roundScore = 0; + _level = 0; + _oldDartButtons = false; + _handX = 0; + _compPlay = 1; +} + +void Darts::playDarts(GameType gameType) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + int oldFontType = screen.fontNumber(); + int playerNum = 0; + int roundStart, score; + int lastDart; + int numHits = 0; + bool gameOver = false; + bool done = false; + const char *const NUM_HITS_STR[3] = { "a", FIXED(Double), FIXED(Triple) }; + + screen.setFont(7); + _spacing = screen.fontHeight() + 2; + + while (!_vm->shouldQuit()) { + roundStart = score = (playerNum == 0) ? _score1 : _score2; + + showNames(playerNum); + showStatus(playerNum); + _roundScore = 0; + + for (int idx = 0; idx < 3; ++idx) { + if (_compPlay == 1) + lastDart = throwDart(idx + 1, playerNum * 2); /* Throw one dart */ + else + if (_compPlay == 2) + lastDart = throwDart(idx + 1, playerNum + 1); /* Throw one dart */ + else + lastDart = throwDart(idx + 1, 0); /* Throw one dart */ + + if (_gameType == GAME_301) { + score -= lastDart; + _roundScore += lastDart; + } else { + numHits = lastDart >> 16; + if (numHits == 0) + numHits = 1; + if (numHits > 3) + numHits = 3; + + lastDart = lastDart & 0xffff; + updateCricketScore(playerNum, lastDart, numHits); + score = (playerNum == 0) ? _score1 : _score2; + } + + if (_gameType == GAME_301) { + if (playerNum == 0) + _score1 = score; + else + _score2 = score; + + if (score == 0) + // Someone won + gameOver = true; + } else { + // check for cricket game over + bool allClosed = true; + int nOtherScore; + + for (int y = 0; y < 7; y++) { + if (_cricketScore[playerNum][y] < 3) + allClosed = false; + } + + if (allClosed) { + nOtherScore = (playerNum == 0) ? _score2 : _score1; + if (score >= nOtherScore) + gameOver = true; + } + } + + // Show scores + showStatus(playerNum); + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(_dartInfo.left, _dartInfo.top - 1), + Common::Rect(_dartInfo.left, _dartInfo.top - 1, _dartInfo.right, _dartInfo.bottom - 1)); + screen.print(Common::Point(_dartInfo.left, _dartInfo.top), 0, "%s # %d", FIXED(Dart), idx + 1); + + if (_gameType == GAME_301) { + if (_vm->getLanguage() == Common::FR_FRA) + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing), 0, + "%s %s: %d", FIXED(Scored), FIXED(Points), lastDart); + else + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing), 0, + "%s %d %s", FIXED(Scored), lastDart, FIXED(Points)); + } else { + if (lastDart != 25) + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing), 0, + "%s %s %d", FIXED(Hit), NUM_HITS_STR[numHits - 1], lastDart); + else + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing), 0, + "%s %s %s", FIXED(Hit), NUM_HITS_STR[numHits - 1], FIXED(Bullseye)); + } + + if (score != 0 && playerNum == 0 && !gameOver) + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 3), 0, + "%s", FIXED(PressAKey)); + + if (gameOver) { + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 3), + 0, "%s", FIXED(GameOver)); + if (playerNum == 0) { + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 4), 0, + "%s %s", FIXED(Holmes), FIXED(Wins)); + _vm->setFlagsDirect(531); + } else { + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 4), 0, + "%s %s!", _opponent.c_str(), FIXED(Wins)); + _vm->setFlagsDirect(530); + } + + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 5), 0, + "%s", FIXED(PressAKey)); + + done = true; + idx = 10; + } else if (_gameType == GAME_301 && score < 0) { + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 2), 0, + "%s!", FIXED(Busted)); + + // End turn + idx = 10; + score = roundStart; + if (playerNum == 0) + _score1 = score; + else + _score2 = score; + } + + // Clear keyboard events + events.clearEvents(); + + if ((playerNum == 0 && _compPlay == 1) || _compPlay == 0 || done) { + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + if (keyState.keycode == Common::KEYCODE_ESCAPE) { + done = true; + idx = 10; + } + } + } else { + events.wait(20); + } + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(_dartInfo.left, _dartInfo.top - 1), + Common::Rect(_dartInfo.left, _dartInfo.top - 1, _dartInfo.right, _dartInfo.bottom - 1)); + screen.blitFrom(screen._backBuffer1); + } + + playerNum ^= 1; + if (!playerNum) + ++_roundNum; + + if (!done) { + screen._backBuffer2.blitFrom((*_dartBoard)[0], Common::Point(0, 0)); + screen._backBuffer1.blitFrom(screen._backBuffer2); + screen.blitFrom(screen._backBuffer2); + } + } + + closeDarts(); + screen.fadeToBlack(); + screen.setFont(oldFontType); +} + +void Darts::initDarts() { + _dartInfo = Common::Rect(430, 50, 430 + 205, 50 + 330); + + if (_gameType == GAME_CRICKET) { + _dartInfo = Common::Rect(430, 245, 430 + 205, 245 + 150); + } + + Common::fill(&_cricketScore[0][0], &_cricketScore[0][7], 0); + Common::fill(&_cricketScore[1][0], &_cricketScore[1][7], 0); + + switch (_gameType) { + case GAME_501: + _score1 = _score2 = 501; + _gameType = GAME_301; + break; + + case GAME_301: + _score1 = _score2 = 301; + break; + + default: + // Cricket + _score1 = _score2 = 0; + break; + } + + _roundNum = 1; + + if (_level == 9) { + // No computer players + _compPlay = 0; + _level = 0; + } else if (_level == 8) { + _level = _vm->getRandomNumber(3); + _compPlay = 2; + } else { + // Check for opponent flags + for (int idx = 0; idx < 4; ++idx) { + if (_vm->readFlags(314 + idx)) + _level = idx; + } + } + + _opponent = FIXED(Jock); +} + +void Darts::loadDarts() { + Resources &res = *_vm->_res; + Screen &screen = *_vm->_screen; + byte palette[PALETTE_SIZE]; + + // Load images + _hand1 = new ImageFile("hand1.vgs"); + _hand2 = new ImageFile("hand2.vgs"); + _dartGraphics = new ImageFile("darts.vgs"); + _dartsLeft = new ImageFile("DartsLft.vgs"); + _dartMap = new ImageFile("DartMap.vgs"); + _dartBoard = new ImageFile("DartBd.vgs"); + + // Load and set the palette + Common::SeekableReadStream *stream = res.load("DartBoard.pal"); + stream->read(palette, PALETTE_SIZE); + screen.translatePalette(palette); + screen.setPalette(palette); + delete stream; + + // Load the initial background + screen._backBuffer1.blitFrom((*_dartBoard)[0], Common::Point(0, 0)); + screen._backBuffer2.blitFrom(screen._backBuffer1); + screen.blitFrom(screen._backBuffer1); +} + +void Darts::closeDarts() { + delete _dartBoard; + delete _dartsLeft; + delete _dartGraphics; + delete _dartMap; + delete _hand1; + delete _hand2; +} + +void Darts::showNames(int playerNum) { + Screen &screen = *_vm->_screen; + byte color; + + color = playerNum == 0 ? PLAYER_COLOR : DART_COLOR_FORE; + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y), 0, "%s", FIXED(Holmes)); + screen._backBuffer1.fillRect(Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + _spacing + 1, + STATUS_INFO_X + 50, STATUS_INFO_Y + _spacing + 3), color); + screen.fillRect(Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + _spacing + 1, + STATUS_INFO_X + 50, STATUS_INFO_Y + _spacing + 3), color); + + color = playerNum == 1 ? PLAYER_COLOR : DART_COLOR_FORE; + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y), 0, "%s", _opponent.c_str()); + screen._backBuffer1.fillRect(Common::Rect(STATUS2_INFO_X, STATUS_INFO_Y + _spacing + 1, + STATUS2_INFO_X + 50, STATUS_INFO_Y + _spacing + 3), color); + screen.fillRect(Common::Rect(STATUS2_INFO_X, STATUS_INFO_Y + _spacing + 1, + STATUS2_INFO_X + 50, STATUS_INFO_Y + _spacing + 3), color); + + screen._backBuffer2.blitFrom(screen._backBuffer1); +} + +void Darts::showStatus(int playerNum) { + Screen &screen = *_vm->_screen; + const char *const CRICKET_SCORE_NAME[7] = { "20", "19", "18", "17", "16", "15", FIXED(Bull) }; + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 10), + Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + 10, STATUS_INFO_X + STATUS_INFO_WIDTH, + STATUS_INFO_Y + STATUS_INFO_HEIGHT - 10)); + screen.print(Common::Point(STATUS_INFO_X + 30, STATUS_INFO_Y + _spacing + 4), 0, "%d", _score1); + + screen.print(Common::Point(STATUS2_INFO_X + 30, STATUS_INFO_Y + _spacing + 4), 0, "%d", _score2); + + int temp = (_gameType == GAME_CRICKET) ? STATUS_INFO_Y + 10 * _spacing + 5 : STATUS_INFO_Y + 55; + screen.print(Common::Point(STATUS_INFO_X, temp), 0, "%s: %d", FIXED(Round), _roundNum); + + if (_gameType == GAME_301) { + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 75), 0, "%s: %d", FIXED(TurnTotal), _roundScore); + } else { + // Show cricket scores + for (int x = 0; x < 7; ++x) { + screen.print(Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 40 + x * _spacing), 0, "%s:", CRICKET_SCORE_NAME[x]); + + for (int y = 0; y < 2; ++y) { + switch (CRICKET_SCORE_NAME[y][x]) { + case 1: + screen.print(Common::Point(STATUS_INFO_X + 38 + y*STATUS2_X_ADD, STATUS_INFO_Y + 40 + x * _spacing), 0, "/"); + break; + case 2: + screen.print(Common::Point(STATUS_INFO_X + 38 + y*STATUS2_X_ADD, STATUS_INFO_Y + 40 + x * _spacing), 0, "X"); + break; + case 3: + screen.print(Common::Point(STATUS_INFO_X + 38 + y * STATUS2_X_ADD - 1, STATUS_INFO_Y + 40 + x * _spacing), 0, "X"); + screen.print(Common::Point(STATUS_INFO_X + 37 + y * STATUS2_X_ADD, STATUS_INFO_Y + 40 + x * _spacing), 0, "O"); + break; + default: + break; + } + } + } + } + + screen.blitFrom(screen._backBuffer1, Common::Point(STATUS_INFO_X, STATUS_INFO_Y + 10), + Common::Rect(STATUS_INFO_X, STATUS_INFO_Y + 10, STATUS_INFO_X + STATUS_INFO_WIDTH, + STATUS_INFO_Y + STATUS_INFO_HEIGHT - 10)); +} + +void Darts::erasePowerBars() { + Screen &screen = *_vm->_screen; + + // Erase the old power bars and replace them with empty ones + screen.fillRect(Common::Rect(DART_BAR_VX, DART_HEIGHT_Y, DART_BAR_VX + 9, DART_HEIGHT_Y + DART_BAR_SIZE), 0); + screen._backBuffer1.transBlitFrom((*_dartGraphics)[0], Common::Point(DART_BAR_VX - 1, DART_HEIGHT_Y - 1)); + screen.slamArea(DART_BAR_VX - 1, DART_HEIGHT_Y - 1, 10, DART_BAR_SIZE + 2); +} + +bool Darts::dartHit() { + Events &events = *_vm->_events; + events.pollEventsAndWait(); + + // Keyboard check + if (events.kbHit()) { + events.clearEvents(); + return true; + } + + bool result = events._pressed && !_oldDartButtons; + _oldDartButtons = events._pressed; + return result; +} + +int Darts::doPowerBar(const Common::Point &pt, byte color, int goToPower, int orientation) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + int x = 0; + + events.clearEvents(); + events.delay(100); + + while (!_vm->shouldQuit()) { + if (x >= DART_BAR_SIZE) + break; + + if ((goToPower - 1) == x) + break; + else if (goToPower == 0) { + if (dartHit()) + break; + } + + screen._backBuffer1.fillRect(Common::Rect(pt.x, pt.y + DART_BAR_SIZE - 1 - x, + pt.x + 8, pt.y + DART_BAR_SIZE - 2 - x), color); + screen._backBuffer1.transBlitFrom((*_dartGraphics)[0], Common::Point(pt.x - 1, pt.y - 1)); + screen.slamArea(pt.x, pt.y + DART_BAR_SIZE - 1 - x, 8, 2); + + if (!(x % 8)) + events.wait(1); + + x += 1; + } + + return MIN(x * 100 / DART_BAR_SIZE, 100); +} + +int Darts::drawHand(int goToPower, int computer) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + const int HAND_OFFSET[2] = { 72, 44 }; + ImageFile *hands; + int hand; + + goToPower = (goToPower * DARTBOARD_WIDTH) / 150; + + if (!computer) { + hand = 0; + hands = _hand1; + } else { + hand = 1; + hands = _hand2; + } + + _handSize.x = (*hands)[0]._offset.x + (*hands)[0]._width; + _handSize.y = (*hands)[0]._offset.y + (*hands)[0]._height; + + // Clear keyboard buffer + events.clearEvents(); + events.delay(100); + + Common::Point pt(DARTBOARD_LEFT - HAND_OFFSET[hand], SHERLOCK_SCREEN_HEIGHT - _handSize.y); + int x = 0; + + while (!_vm->shouldQuit()) { + if (x >= DARTBOARD_WIDTH) + break; + + if ((goToPower - 1) == x) + break; + else if (goToPower == 0) { + if (dartHit()) + break; + } + + screen._backBuffer1.transBlitFrom((*hands)[0], pt); + screen.slamArea(pt.x - 1, pt.y, _handSize.x + 1, _handSize.y); + screen.restoreBackground(Common::Rect(pt.x, pt.y, pt.x + _handSize.x, pt.y + _handSize.y)); + + if (!(x % 8)) + events.wait(1); + + ++x; + ++pt.x; + } + + _handX = pt.x - 1; + + return MIN(x * 100 / DARTBOARD_WIDTH, 100); +} + +Common::Point Darts::convertFromScreenToScoreCoords(const Common::Point &pt) const { + return Common::Point(CLIP((int)pt.x, 0, DARTBOARD_WIDTH), CLIP((int)pt.y, 0, DARTBOARD_HEIGHT)); +} + +int Darts::dartScore(const Common::Point &pt) { + Common::Point pos(pt.x - DARTBOARD_LEFT, pt.y - DARTBOARD_TOP); + if (pos.x < 0 || pos.y < 0) + return 0; + int score; + + if (pos.x < DARTBOARD_WIDTH && pos.y < DARTBOARD_HEIGHT) { + pos = convertFromScreenToScoreCoords(pos); + score = *(const byte *)(*_dartMap)[0]._frame.getBasePtr(pos.x, pos.y); + + if (_gameType == GAME_301) { + if (score >= 100) { + if (score <= 120) + // Hit a double + score = (score - 100) * 2; + else + // Hit a triple + score = (score - 120) * 3; + } + } else if (score >= 100) { + if (score >= 120) + // Hit a double + score = (2 << 16) + (score - 100); + else + // Hit a triple + score = (3 << 16) + (score - 120); + } + } else { + score = 0; + } + + return score; +} + +void Darts::drawDartThrow(const Common::Point &dartPos, int computer) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + int cx, cy; + int handCy; + int drawX = 0, drawY = 0, oldDrawX = 0, oldDrawY = 0; + int xSize = 0, ySize = 0, oldxSize = 0, oldySize = 0; + int handOCx, handOCy; + int ocx, ocy; + int handOldxSize, handOldySize; + int delta = 9; + int dartNum; + int hddy; + + // Draw the animation of the hand throwing the dart first + // See which hand animation to use + ImageFile &hands = !computer ? *_hand1 : *_hand2; + int numFrames = !computer ? 14 : 13; + + ocx = ocy = handOCx = handOCy = 0; + oldxSize = oldySize = handOldxSize = handOldySize = 1; + cx = dartPos.x; + cy = SHERLOCK_SCREEN_HEIGHT - _handSize.y - 20; + + hddy = (cy - dartPos.y) / (numFrames - 7); + hddy += 2; + hddy = hddy * 10 / 8; + if (dartPos.y > 275) + hddy += 3; + + for (int idx = 0; idx < numFrames; ++idx) { + _handSize.x = hands[idx]._offset.x + hands[idx]._width; + _handSize.y = hands[idx]._offset.y + hands[idx]._height; + handCy = SHERLOCK_SCREEN_HEIGHT - _handSize.y; + + screen._backBuffer1.transBlitFrom(hands[idx], Common::Point(_handX, handCy)); + screen.slamArea(_handX, handCy, _handSize.x + 1, _handSize.y); + screen.slamArea(handOCx, handOCy, handOldxSize, handOldySize); + screen.restoreBackground(Common::Rect(_handX, handCy, _handX + _handSize.x, handCy + _handSize.y)); + + handOCx = _handX; + handOCy = handCy; + handOldxSize = _handSize.x; + handOldySize = _handSize.y; + + if (idx > 6) { + dartNum = idx - 6; + if (computer) + dartNum += 19; + + xSize = (*_dartGraphics)[dartNum]._width; + ySize = (*_dartGraphics)[dartNum]._height; + + ocx = drawX = cx - (*_dartGraphics)[dartNum]._width / 2; + ocy = drawY = cy - (*_dartGraphics)[dartNum]._height; + + // Draw dart + screen._backBuffer1.transBlitFrom((*_dartGraphics)[dartNum], dartPos); + + if (drawX < 0) { + xSize += drawX; + if (xSize < 0) + xSize = 1; + drawX = 0; + } + + if (drawY < 0) { + ySize += drawY; + if (ySize < 0) + ySize = 1; + drawY = 0; + } + + // Flush the drawn dart to the screen + screen.slamArea(drawX, drawY, xSize, ySize); + if (oldDrawX != -1) + // Flush the erased dart area + screen.slamArea(oldDrawX, oldDrawY, oldxSize, oldySize); + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(drawX, drawY), + Common::Rect(drawX, drawY, drawX + xSize, drawY + ySize)); + + oldDrawX = drawX; + oldDrawY = drawY; + oldxSize = xSize; + oldySize = ySize; + + cy -= hddy; + } + + events.wait(1); + } + + // Clear the last little bit of the hand from the screen + screen.slamArea(handOCx, handOCy, handOldxSize, handOldySize); + + // Erase the old dart + if (oldDrawX != -1) + screen.slamArea(oldDrawX, oldDrawY, oldxSize, oldySize); + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(drawX, drawY), + Common::Rect(drawX, drawY, drawX + xSize, drawY + ySize)); + + cx = dartPos.x; + cy = dartPos.y + 2; + oldDrawX = oldDrawY = -1; + + for (int idx = 5; idx <= 23; ++idx) { + dartNum = idx - 4; + if (computer) + dartNum += 19; + + if (idx < 14) + cy -= delta--; + else + if (idx == 14) + delta = 1; + if (idx > 14) + cy += delta++; + + xSize = (*_dartGraphics)[dartNum]._width; + ySize = (*_dartGraphics)[dartNum]._height; + + ocx = drawX = cx - (*_dartGraphics)[dartNum]._width / 2; + ocy = drawY = cy - (*_dartGraphics)[dartNum]._height; + + screen._backBuffer1.transBlitFrom((*_dartGraphics)[dartNum], Common::Point(drawX, drawY)); + + if (drawX < 0) { + xSize += drawX; + if (xSize < 0) + xSize = 1; + drawX = 0; + } + + if (drawY < 0) { + ySize += drawY; + if (ySize < 0) + ySize = 1; + drawY = 0; + } + + // flush the dart + screen.slamArea(drawX, drawY, xSize, ySize); + if (oldDrawX != -1) + screen.slamArea(oldDrawX, oldDrawY, oldxSize, oldySize); + + if (idx != 23) + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(drawX, drawY), + Common::Rect(drawX, drawY, drawX + xSize, drawY + ySize)); // erase dart + + events.wait(1); + + oldDrawX = drawX; + oldDrawY = drawY; + oldxSize = xSize; + oldySize = ySize; + } + + dartNum = 19; + if (computer) + dartNum += 19; + xSize = (*_dartGraphics)[dartNum]._width; + ySize = (*_dartGraphics)[dartNum]._height; + + // Draw final dart on the board + screen._backBuffer1.transBlitFrom((*_dartGraphics)[dartNum], Common::Point(ocx, ocy)); + screen._backBuffer2.transBlitFrom((*_dartGraphics)[dartNum], Common::Point(ocx, ocy)); + screen.slamArea(ocx, ocy, xSize, ySize); +} + +int Darts::findNumberOnBoard(int aim, Common::Point &pt) { + ImageFrame &img = (*_dartMap)[0]; + + if ((aim > 20) && ((aim != 25) && (aim != 50))) { + if ((aim <= 40) && ((aim & 1) == 0)) { + aim /= 2; + aim += 100; + } else { + aim /= 3; + aim += 120; + } + } + + bool done = false; + for (int y = 0; y < img._width && !done; ++y) { + for (int x = 0; x < img._height && !done; ++x) { + byte score = *(const byte *)img._frame.getBasePtr(x, y); + + if (score == aim) { + // Found a match. Aim at non-double/triple numbers whenever possible. + // ie. Aim at 18 instead of triple 6 or double 9 + done = true; + + if (aim < 21) { + pt.x = x + 10; + pt.y = y + 10; + + score = *(const byte *)img._frame.getBasePtr(x, y); + if (score != aim) + done = false; + } else { + // Aiming at double or triple + pt.x = x + 3; + pt.y = y + 3; + } + } + } + } + + pt = convertFromScreenToScoreCoords(pt); + + if (aim == 3) + pt.y += 30; + if (aim == 17) + pt.y += 10; + + if (aim == 15) { + pt.y += 5; + pt.x += 5; + } + + pt.y = DARTBOARD_HEIGHT - pt.y; + return done; +} + +void Darts::getComputerNumber(int playerNum, Common::Point &targetPos) { + int score; + int aim = 0; + Common::Point pt; + bool done = false; + int cricketaimset = false; + bool shootBull = false; + + score = (playerNum == 0) ? _score1 : _score2; + + if (_gameType == GAME_301) { + // Try to hit number + aim = score; + if(score > 60) + shootBull = true; + } else { + if (_cricketScore[playerNum][6] < 3) { + // shoot at bull first + aim = CRICKET_VALUE[6]; + cricketaimset = true; + } else { + // Now check and shoot in this order: 20,19,18,17,16,15 + for (int idx = 0; idx < 7; ++idx) { + if (_cricketScore[playerNum][idx] < 3) { + aim = CRICKET_VALUE[idx]; + cricketaimset = true; + break; + } + } + } + + if (!cricketaimset) { + // Everything is closed + // just in case we don't get set in loop below, which should never happen + aim = 14; + for (int idx = 0; idx < 7; ++idx) { + if (_cricketScore[playerNum^1][idx] < 3) { + // Opponent has this open + aim = CRICKET_VALUE[idx]; + + if (idx == 6) + shootBull = true; + } + } + } + } + + if (shootBull) { + // Aim at bulls eye + targetPos.x = targetPos.y = 75; + + if (_level <= 1) { + if (_vm->getRandomNumber(1) == 1) { + targetPos.x += (_vm->getRandomNumber(20)-10); + targetPos.y += (_vm->getRandomNumber(20)-10); + } + } + } else { + // Loop in case number does not exist on board + do { + done = findNumberOnBoard(aim, pt); + --aim; + } while (!done); + + pt.x += DARTBOARD_TOTALLEFT * 70 / 100; + pt.y += DARTBOARD_TOTALTOP * 70 / 100; + + // old * 3/2 + targetPos.x = pt.x * 100 / DARTBOARD_TOTALX * 3 / 2; + targetPos.y = pt.y * 100 / DARTBOARD_TOTALY * 3 / 2; + } + + // the higher the level, the more accurate the throw + int v = _vm->getRandomNumber(9); + v += _level * 2; + + if (v <= 2) { + targetPos.x += _vm->getRandomNumber(70) - 35; + targetPos.y += _vm->getRandomNumber(70) - 35; + } else if (v <= 4) { + targetPos.x += _vm->getRandomNumber(50) - 25; + targetPos.y += _vm->getRandomNumber(50) - 25; + } else if (v <= 6) { + targetPos.x += _vm->getRandomNumber(30) - 15; + targetPos.y += _vm->getRandomNumber(30) - 15; + } else if (v <= 8) { + targetPos.x += _vm->getRandomNumber(20) -10; + targetPos.y += _vm->getRandomNumber(20) -10; + } else if (v <= 10) { + targetPos.x += _vm->getRandomNumber(11) - 5; + targetPos.y += _vm->getRandomNumber(11) - 5; + } + + if (targetPos.x < 1) + targetPos.x = 1; + if (targetPos.y < 1) + targetPos.y = 1; +} + +int Darts::throwDart(int dartNum, int computer) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + int height; + int horiz; + Common::Point targetPos; + Common::String temp; + + /* clear keyboard buffer */ + events.clearEvents(); + + erasePowerBars(); + screen.print(Common::Point(_dartInfo.left, _dartInfo.top), 0, "%s # %d", FIXED(Dart), dartNum); + + drawDartsLeft(dartNum, computer); + + if (!computer) { + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing), 0, "%s", FIXED(HitAKey)); + screen.print(Common::Point(_dartInfo.left, _dartInfo.top + _spacing * 2), 0, "%s", FIXED(ToStart)); + } + + if (!computer) { + // Wait for a hit + while (!dartHit()) + ; + } else { + events.wait(1); + } + + drawDartsLeft(dartNum + 1, computer); + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(_dartInfo.left, _dartInfo.top - 1), + Common::Rect(_dartInfo.left, _dartInfo.top - 1, _dartInfo.right, _dartInfo.bottom - 1)); + screen.blitFrom(screen._backBuffer1, Common::Point(_dartInfo.left, _dartInfo.top - 1), + Common::Rect(_dartInfo.left, _dartInfo.top - 1, _dartInfo.right, _dartInfo.bottom - 1)); + + if (computer) { + getComputerNumber(computer - 1, targetPos); + } else { + // Keyboard control + targetPos = Common::Point(0, 0); + } + + horiz = drawHand(targetPos.x, computer); + height = doPowerBar(Common::Point(DART_BAR_VX, DART_HEIGHT_Y), DART_COLOR_FORE, targetPos.y, 1); + + // Invert height + height = 101 - height; + + // Copy power bars to the secondary back buffer + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(DART_BAR_VX - 1, DART_HEIGHT_Y - 1), + Common::Rect(DART_BAR_VX - 1, DART_HEIGHT_Y - 1, DART_BAR_VX - 1 + 10, + DART_HEIGHT_Y - 1 + DART_BAR_SIZE + 2)); + + Common::Point dartPos(DARTBOARD_TOTALLEFT + horiz*DARTBOARD_TOTALX / 100, + DARTBOARD_TOTALTOP + height * DARTBOARD_TOTALY / 100); + + dartPos.x += 2 - _vm->getRandomNumber(4); + dartPos.y += 2 - _vm->getRandomNumber(4); + + drawDartThrow(dartPos, computer); + return dartScore(dartPos); +} + +void Darts::doCricketScoreHits(int player, int scoreIndex, int numHits) { + while (numHits--) { + if (_cricketScore[player][scoreIndex] < 3) + _cricketScore[player][scoreIndex]++; + else if (_cricketScore[player ^ 1][scoreIndex] < 3) { + if (player == 0) + _score1 += CRICKET_VALUE[scoreIndex]; + else + _score2 += CRICKET_VALUE[scoreIndex]; + } + } +} + +void Darts::updateCricketScore(int player, int dartVal, int multiplier) { + if (dartVal < 15) + return; + + if (dartVal <= 20) + doCricketScoreHits(player, 20 - dartVal, multiplier); + else if (dartVal == 25) + doCricketScoreHits(player, 6, multiplier); +} + +void Darts::drawDartsLeft(int dartNum, int computer) { + Screen &screen = *_vm->_screen; + const int DART_X1[3] = { 391, 451, 507 }; + const int DART_Y1[3] = { 373, 373, 373 }; + const int DART_X2[3] = { 393, 441, 502 }; + const int DART_Y2[3] = { 373, 373, 373 }; + + screen._backBuffer2.blitFrom(screen._backBuffer1, Common::Point(DART_X1[0], DART_Y1[0]), + Common::Rect(DART_X1[0], DART_Y1[0], SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + for (int idx = 2; idx >= dartNum - 1; --idx) { + if (computer) + screen._backBuffer1.transBlitFrom((*_dartsLeft)[idx + 3], Common::Point(DART_X2[idx], DART_Y2[idx])); + else + screen._backBuffer1.transBlitFrom((*_dartsLeft)[idx], Common::Point(DART_X1[idx], DART_Y1[idx])); + } + + screen.slamArea(DART_X1[0], DART_Y1[0], SHERLOCK_SCREEN_WIDTH - DART_X1[0], SHERLOCK_SCREEN_HEIGHT - DART_Y1[0]); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_darts.h b/engines/sherlock/tattoo/tattoo_darts.h new file mode 100644 index 0000000000..f65ec19d10 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_darts.h @@ -0,0 +1,172 @@ +/* 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_DARTS_H +#define SHERLOCK_TATTOO_DARTS_H + +#include "common/scummsys.h" +#include "sherlock/image_file.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +enum GameType { GAME_301, GAME_CRICKET, GAME_501 }; + +class Darts { +private: + SherlockEngine *_vm; + GameType _gameType; + ImageFile *_hand1, *_hand2; + ImageFile *_dartGraphics; + ImageFile *_dartsLeft; + ImageFile *_dartMap; + ImageFile *_dartBoard; + Common::Rect _dartInfo; + int _cricketScore[2][7]; + int _score1, _score2; + int _roundNum; + int _roundScore; + int _level; + int _compPlay; + Common::String _opponent; + int _spacing; + bool _oldDartButtons; + int _handX; + Common::Point _handSize; + + /** + * Initialize game variables + */ + void initDarts(); + + /** + * Load dartboard graphics + */ + void loadDarts(); + + /** + * Free loaded dart images + */ + void closeDarts(); + + /** + * Show the player names + */ + void showNames(int playerNum); + + /** + * Show the current scores + */ + void showStatus(int playerNum); + + /** + * Erases the power bars + */ + void erasePowerBars(); + + /** + * Returns true if a mouse button or key is pressed + */ + bool dartHit(); + + /** + * Shows a power bar and increments it until a key or mouse button is pressed. If the bar + * reaches the end, it will also end. The reached power bar number is returned. + * @param pt Bar position + * @param color draw color + * @param goToPower If provided, input is ignored, and the bar is increased up to the specified level + * @param orientation 0=Horizontal, 1=Vertical + */ + int doPowerBar(const Common::Point &pt, byte color, int goToPower, int orientation); + + /** + * This is similar to doPowerBar, except it draws the player's hand moving across the + * bottom of the screen to indicate the positioning of the darts + */ + int drawHand(int goToPower, int computer); + + /** + * Converts a passed co-ordinates from screen co-ordinates to an offset within the dartboard + */ + Common::Point convertFromScreenToScoreCoords(const Common::Point &pt) const; + + /** + * Return the score a dart at the given position will get + */ + int dartScore(const Common::Point &pt); + + /** + * Draw a dart travelling to the board + */ + void drawDartThrow(const Common::Point &dartPos, int computer); + + /** + * Looks for the passed number on the dartboard. If it finds it, it will return + * the co-ordinates of the center of the number + */ + int findNumberOnBoard(int aim, Common::Point &pt); + + /** + * Calculates a position for the comptuer wants to throw, and then calculates where they + * actually did throw. The computer will not always hit what it's aiming it. + */ + void getComputerNumber(int playerNum, Common::Point &targetPos); + + /** + * Throw one dart. If computer is 1 or 2, the computer will throw the dart, and user input + * will be ignored. + * @param computer 1=1st computer player, 2=2nd computer player + */ + int throwDart(int dartNum, int computer); + + /** + * This will update the number of hits for the target score, as well as updating the + * score if it's closed + */ + void doCricketScoreHits(int player, int scoreIndex, int numHits); + + /** + * Updates the score based upon what the dart hit + */ + void updateCricketScore(int player, int dartVal, int multiplier); + + /** + * Draw the darts left + */ + void drawDartsLeft(int dartNum, int computer); +public: + Darts(SherlockEngine *vm); + + /** + * Play the darts game + */ + void playDarts(GameType gameType); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_debugger.cpp b/engines/sherlock/tattoo/tattoo_debugger.cpp new file mode 100644 index 0000000000..8d59b4c592 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_debugger.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_debugger.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Tattoo { + +TattooDebugger::TattooDebugger(SherlockEngine *vm) : Debugger(vm) { +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_debugger.h b/engines/sherlock/tattoo/tattoo_debugger.h new file mode 100644 index 0000000000..e729262b11 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_debugger.h @@ -0,0 +1,44 @@ +/* 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_DEBUGGER_H +#define SHERLOCK_TATTOO_DEBUGGER_H + +#include "sherlock/debugger.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +class TattooDebugger : public Debugger { +public: + TattooDebugger(SherlockEngine *vm); + virtual ~TattooDebugger() {} +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif /* SHERLOCK_TATTOO_DEBUGGER_H */ diff --git a/engines/sherlock/tattoo/tattoo_fixed_text.cpp b/engines/sherlock/tattoo/tattoo_fixed_text.cpp new file mode 100644 index 0000000000..72c67570de --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_fixed_text.cpp @@ -0,0 +1,107 @@ +/* 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_fixed_text.h" +#include "sherlock/sherlock.h" + +namespace Sherlock { + +namespace Tattoo { + +static const char *const FIXED_TEXT_ENGLISH[] = { + "Money", + "Card", + "Tobacco", + "Timetable", + "Summons", + "Foolscap", + "Damp Paper", + "Bull's Eye", + + "Money", + "Card", + "Tobacco", + "Timetable", + "Summons", + "Foolscap", + "Foolscap", + "Bull's Eye Lantern", + + "Open", + "Look", + "Talk", + "Use", + "Journal", + "Inventory", + "Options", + "Solve", + "with", + "No effect...", + "This person has nothing to say at the moment", + + "Close Journal", + "Search Journal", + "Save Journal", + "Abort Search", + "Search Backwards", + "Search Forwards", + "Text Not Found !", + + "Holmes", + "Jock", + "Bull", + "Round", + "Turn Total", + "Dart", + "to start", + "Hit a key", + "Press a key", + "bullseye", + "GAME OVER", + "BUSTED", + "Wins", + "Scored", + "points", + "Hit", + "Double", + "Triple", + + "Apply", + "Water", + "Heat" +}; + +TattooFixedText::TattooFixedText(SherlockEngine *vm) : FixedText(vm) { +} + +const char *TattooFixedText::getText(int fixedTextId) { + return FIXED_TEXT_ENGLISH[fixedTextId]; +} + +const Common::String TattooFixedText::getActionMessage(FixedTextActionId actionId, int messageIndex) { + return Common::String(); +} + + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_fixed_text.h b/engines/sherlock/tattoo/tattoo_fixed_text.h new file mode 100644 index 0000000000..c26d787095 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_fixed_text.h @@ -0,0 +1,114 @@ +/* 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_FIXED_TEXT_H +#define SHERLOCK_TATTOO_FIXED_TEXT_H + +#include "sherlock/fixed_text.h" + +namespace Sherlock { + +namespace Tattoo { + +enum FixedTextId { + kFixedText_Inv1, + kFixedText_Inv2, + kFixedText_Inv3, + kFixedText_Inv4, + kFixedText_Inv5, + kFixedText_Inv6, + kFixedText_Inv7, + kFixedText_Inv8, + kFixedText_InvDesc1, + kFixedText_InvDesc2, + kFixedText_InvDesc3, + kFixedText_InvDesc4, + kFixedText_InvDesc5, + kFixedText_InvDesc6, + kFixedText_InvDesc7, + kFixedText_InvDesc8, + kFixedText_Open, + kFixedText_Look, + kFixedText_Talk, + kFixedText_Use, + kFixedText_Journal, + kFixedText_Inventory, + kFixedText_Options, + kFixedText_Solve, + kFixedText_With, + kFixedText_NoEffect, + kFixedText_NothingToSay, + + kFixedText_CloseJournal, + kFixedText_SearchJournal, + kFixedText_SaveJournal, + kFixedText_AbortSearch, + kFixedText_SearchBackwards, + kFixedText_SearchForwards, + kFixedText_TextNotFound, + + kFixedText_Holmes, + kFixedText_Jock, + kFixedText_Bull, + kFixedText_Round, + kFixedText_TurnTotal, + kFixedText_Dart, + kFixedText_ToStart, + kFixedText_HitAKey, + kFixedText_PressAKey, + kFixedText_Bullseye, + kFixedText_GameOver, + kFixedText_Busted, + kFixedText_Wins, + kFixedText_Scored, + kFixedText_Points, + kFixedText_Hit, + kFixedText_Double, + kFixedText_Triple, + + kFixedText_Apply, + kFixedText_Water, + kFixedText_Heat +}; + +class TattooFixedText: public FixedText { +private: +public: + TattooFixedText(SherlockEngine *vm); + virtual ~TattooFixedText() {} + + /** + * Gets text + */ + virtual const char *getText(int fixedTextId); + + /** + * Get action message + */ + virtual const Common::String getActionMessage(FixedTextActionId actionId, int messageIndex); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_inventory.cpp b/engines/sherlock/tattoo/tattoo_inventory.cpp new file mode 100644 index 0000000000..6bd1822c10 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_inventory.cpp @@ -0,0 +1,63 @@ +/* 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_inventory.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +TattooInventory::TattooInventory(SherlockEngine *vm) : Inventory(vm) { + _invShapes.resize(8); +} + +TattooInventory::~TattooInventory() { +} + +void TattooInventory::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 count = stream->readByte(); + char c; + + for (int idx = 0; idx < count; ++idx) { + Common::String name; + while ((c = stream->readByte()) != 0) + name += c; + + _names.push_back(name); + } + + delete stream; + + loadGraphics(); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_inventory.h b/engines/sherlock/tattoo/tattoo_inventory.h new file mode 100644 index 0000000000..a18324b785 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_inventory.h @@ -0,0 +1,48 @@ +/* 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_INVENTORY_H +#define SHERLOCK_TATTOO_INVENTORY_H + +#include "sherlock/inventory.h" + +namespace Sherlock { + +namespace Tattoo { + +class TattooInventory : public Inventory { +public: + TattooInventory(SherlockEngine *vm); + ~TattooInventory(); + + /** + * Load the list of names the inventory items correspond to, if not already loaded, + * and then calls loadGraphics to load the associated graphics + */ + virtual void loadInv(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_journal.cpp b/engines/sherlock/tattoo/tattoo_journal.cpp new file mode 100644 index 0000000000..6df5ee7458 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_journal.cpp @@ -0,0 +1,900 @@ +/* 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_journal.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define JOURNAL_BAR_WIDTH 450 + +TattooJournal::TattooJournal(SherlockEngine *vm) : Journal(vm) { + _journalImages = nullptr; + _selector = _oldSelector = 0; + _wait = false; + _exitJournal = false; + _scrollingTimer = 0; + _savedIndex = _savedSub = _savedPage = 0; + + loadLocations(); +} + +void TattooJournal::show() { + Events &events = *_vm->_events; + Resources &res = *_vm->_res; + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + byte palette[PALETTE_SIZE]; + + // Load journal images + _journalImages = new ImageFile("journal.vgs"); + + // Load palette + Common::SeekableReadStream *stream = res.load("journal.pal"); + stream->read(palette, PALETTE_SIZE); + screen.translatePalette(palette); + ui.setupBGArea(palette); + + // Set screen to black, and set background + screen._backBuffer1.blitFrom((*_journalImages)[0], Common::Point(0, 0)); + screen.empty(); + screen.setPalette(palette); + + if (_journal.empty()) { + _up = _down = false; + } else { + drawJournal(0, 0); + } + drawControls(0); + screen.slamRect(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + _exitJournal = false; + _scrollingTimer = 0; + + do { + events.pollEventsAndWait(); + events.setButtonState(); + _wait = true; + + handleKeyboardEvents(); + highlightJournalControls(true); + + handleButtons(); + + if (_wait) + events.wait(2); + + } while (!_vm->shouldQuit() && !_exitJournal); + + // Clear events + events.clearEvents(); + + // Free the images + delete _journalImages; +} + +void TattooJournal::handleKeyboardEvents() { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + + if (!events.kbHit()) + return; + + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_TAB && (keyState.flags & Common::KBD_SHIFT)) { + // Shift tab + Common::Rect r(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, SHERLOCK_SCREEN_HEIGHT - r.height()); + + // See if mouse is over any of the journal controls + _selector = -1; + if (Common::Rect(r.left + 3, r.top + 3, r.right - 3, r.top + screen.fontHeight() + 4).contains(mousePos)) + _selector = (mousePos.x - r.left) / (r.width() / 3); + + // If the mouse is not over an option, move the mouse to that it points to the first option + if (_selector == -1) { + events.warpMouse(Common::Point(r.left + r.width() / 3 - 10, r.top + screen.fontHeight() + 2)); + } else { + if (_selector == 0) + _selector = 2; + else + --_selector; + + events.warpMouse(Common::Point(r.left + (r.width() / 3) * (_selector + 1) - 10, mousePos.y)); + } + + } else if (keyState.keycode == Common::KEYCODE_PAGEUP || keyState.keycode == Common::KEYCODE_KP9) { + // See if they have Shift held down to go forward 10 pages + if (keyState.flags & Common::KBD_SHIFT) { + if (_page > 1) { + // Scroll Up 10 pages if possible + if (_page < 11) + drawJournal(1, (_page - 1) * LINES_PER_PAGE); + else + drawJournal(1, 10 * LINES_PER_PAGE); + + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + } else { + if (_page > 1) { + // Scroll Up 1 page + drawJournal(1, LINES_PER_PAGE); + drawScrollBar(); + show(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + } + + } else if (keyState.keycode == Common::KEYCODE_PAGEDOWN || keyState.keycode == Common::KEYCODE_KP3) { + if (keyState.flags & Common::KBD_SHIFT) { + if (_down) { + // Scroll down 10 Pages + if (_page + 10 > _maxPage) + drawJournal(2, (_maxPage - _page) * LINES_PER_PAGE); + else + drawJournal(2, 10 * LINES_PER_PAGE); + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + _wait = false; + } + } else { + if (_down) { + // Scroll down 1 page + drawJournal(2, LINES_PER_PAGE); + drawScrollBar(); + show(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + _wait = false; + } + } + + } else if (keyState.keycode == Common::KEYCODE_HOME || keyState.keycode == Common::KEYCODE_KP7) { + // Scroll to start of journal + if (_page > 1) { + // Go to the beginning of the journal + _index = _sub = _up = _down = 0; + _page = 1; + + drawFrame(); + drawJournal(0, 0); + + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + _wait = false; + } + + } else if (keyState.keycode == Common::KEYCODE_END || keyState.keycode == Common::KEYCODE_KP1) { + // Scroll to end of journal + if (_down) { + // Go to the end of the journal + drawJournal(2, 100000); + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + _wait = false; + } + } else if (keyState.keycode == Common::KEYCODE_RETURN || keyState.keycode == Common::KEYCODE_KP_ENTER) { + events._pressed = false; + events._released = true; + events._oldButtons = 0; + } else if (keyState.keycode == Common::KEYCODE_ESCAPE) { + _exitJournal = true; + } else if (keyState.keycode == Common::KEYCODE_TAB) { + Common::Rect r(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, SHERLOCK_SCENE_HEIGHT - r.height()); + + // See if the mouse is over any of the journal controls + _selector = -1; + if (Common::Rect(r.left + 3, r.top + 3, r.right - 3, r.top + screen.fontHeight() + 4).contains(mousePos)) + _selector = (mousePos.x - r.left) / (r.width() / 3); + + // If the mouse is not over any of the options, move the mouse so that it points to the first option + if (_selector == -1) { + events.warpMouse(Common::Point(r.left + r.width() / 3 - 10, r.top + screen.fontHeight() + 2)); + } else { + if (_selector == 2) + _selector = 0; + else + ++_selector; + + events.warpMouse(Common::Point(r.left + (r.width() / 3) * (_selector + 1) - 10, mousePos.y)); + } + } +} + +void TattooJournal::handleButtons() { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + uint32 frameCounter = events.getFrameCounter(); + + if (_selector != -1 && events._pressed) { + if (frameCounter >= _scrollingTimer) { + // Set next scrolling time + _scrollingTimer = frameCounter + 6; + + // Handle different scrolling actions + switch (_selector) { + case 3: + // Scroll left (1 page back) + if (_page > 1) { + // Scroll Up + drawJournal(1, LINES_PER_PAGE); + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + break; + + case 4: + // Page left (10 pages back) + if (_page > 1) { + // Scroll Up 10 Pages if possible + if (_page < 11) + drawJournal(1, (_page - 1) * LINES_PER_PAGE); + else + drawJournal(1, 10 * LINES_PER_PAGE); + drawScrollBar(); + show(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + break; + + case 5: + // Page right (10 pages ahead) + if (_down) { + // Scroll Down 10 Pages + if (_page + 10 > _maxPage) + drawJournal(2, (_maxPage - _page) * LINES_PER_PAGE); + else + drawJournal(2, 10 * LINES_PER_PAGE); + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + break; + + case 6: + // Scroll right (1 Page Ahead) + if (_down) { + // Scroll Down + drawJournal(2, LINES_PER_PAGE); + drawScrollBar(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + _wait = false; + } + break; + + default: + break; + } + } + } + + if (events._released || events._rightReleased) { + _scrollingTimer = 0; + + switch (_selector) { + case 0: + _exitJournal = true; + break; + + case 1: { + // Search Journal + disableControls(); + + bool notFound = false; + int dir; + + do { + if ((dir = getFindName(notFound)) != 0) { + _savedIndex = _index; + _savedSub = _sub; + _savedPage = _page; + + if (drawJournal(dir + 2, 1000 * LINES_PER_PAGE) == 0) + { + _index = _savedIndex; + _sub = _savedSub; + _page = _savedPage; + + drawFrame(); + drawJournal(0, 0); + notFound = true; + } else { + break; + } + + highlightJournalControls(false); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } else { + break; + } + } while (!_vm->shouldQuit()); + break; + } + + case 2: + // Print Journal - not implemented in ScummVM + break; + + default: + break; + } + } +} + +void TattooJournal::loadLocations() { + Resources &res = *_vm->_res; + + _directory.clear(); + _locations.clear(); + + Common::SeekableReadStream *dir = res.load("talk.lib"); + dir->skip(4); // Skip header + + // Get the numer of entries + _directory.resize(dir->readUint16LE()); + dir->seek((_directory.size() + 1) * 8, SEEK_CUR); + + // 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"); + + // Initialize locations + _locations.resize(100); + for (int idx = 0; idx < 100; ++idx) + _locations[idx] = "No Description"; + + while (loc->pos() < loc->size()) { + // In Rose Tattoo, each location line starts with the location + // number, followed by a dot, some spaces and its description + // in quotes + Common::String line = loc->readLine(); + Common::String locNumStr; + int locNum = 0; + int i = 0; + Common::String locDesc; + + // Get the location + while (Common::isDigit(line[i])) { + locNumStr += line[i]; + i++; + } + locNum = atoi(locNumStr.c_str()); + + // Skip the dot, spaces and initial quotation mark + while (line[i] == ' ' || line[i] == '.' || line[i] == '\"') + i++; + + do { + locDesc += line[i]; + i++; + } while (line[i] != '\"'); + + _locations[locNum] = locDesc; + } + + delete loc; +} + +void TattooJournal::drawFrame() { + Screen &screen = *_vm->_screen; + + screen._backBuffer1.blitFrom((*_journalImages)[0], Common::Point(0, 0)); + drawControls(0); + +} + +void TattooJournal::drawControls(int mode) { + TattooEngine &vm = *(TattooEngine *)_vm; + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + ImageFile &images = *ui._interfaceImages; + + Common::Rect r(JOURNAL_BAR_WIDTH, !mode ? (BUTTON_SIZE + screen.fontHeight() + 13) : + (screen.fontHeight() + 4) * 2 + 9); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, !mode ? (SHERLOCK_SCREEN_HEIGHT - r.height()) : + (SHERLOCK_SCREEN_HEIGHT - r.height()) / 2); + + Common::Rect inner = r; + inner.grow(-3); + + if (vm._transparentMenus) + ui.makeBGArea(inner); + else + screen._backBuffer1.fillRect(inner, MENU_BACKGROUND); + + // Draw the four corners of the info box + screen._backBuffer1.transBlitFrom(images[0], Common::Point(r.left, r.top)); + screen._backBuffer1.transBlitFrom(images[1], Common::Point(r.right - images[1]._width, r.top)); + screen._backBuffer1.transBlitFrom(images[1], Common::Point(r.left, r.bottom - images[1]._height)); + screen._backBuffer1.transBlitFrom(images[1], Common::Point(r.right - images[1]._width, r.bottom - images[1]._height)); + + // Draw the top of the info box + screen._backBuffer1.hLine(r.left + images[0]._width, r.top, r.right - images[0]._height, INFO_TOP); + screen._backBuffer1.hLine(r.left + images[0]._width, r.top + 1, r.right - images[0]._height, INFO_MIDDLE); + screen._backBuffer1.hLine(r.left + images[0]._width, r.top + 2, r.right - images[0]._height, INFO_BOTTOM); + + // Draw the bottom of the info box + screen._backBuffer1.hLine(r.left + images[0]._width, r.bottom - 3, r.right - images[0]._height, INFO_TOP); + screen._backBuffer1.hLine(r.left + images[0]._width, r.bottom - 2, r.right - images[0]._height, INFO_MIDDLE); + screen._backBuffer1.hLine(r.left + images[0]._width, r.bottom - 1, r.right - images[0]._height, INFO_BOTTOM); + + // Draw the left side of the info box + screen._backBuffer1.vLine(r.left, r.top + images[0]._height, r.bottom - images[2]._height, INFO_TOP); + screen._backBuffer1.vLine(r.left + 1, r.top + images[0]._height, r.bottom - images[2]._height, INFO_MIDDLE); + screen._backBuffer1.vLine(r.left + 2, r.top + images[0]._height, r.bottom - images[2]._height, INFO_BOTTOM); + + // Draw the right side of the info box + screen._backBuffer1.vLine(r.right - 3, r.top + images[0]._height, r.bottom - images[2]._height, INFO_TOP); + screen._backBuffer1.vLine(r.right - 2, r.top + images[0]._height, r.bottom - images[2]._height, INFO_MIDDLE); + screen._backBuffer1.vLine(r.right - 1, r.top + images[0]._height, r.bottom - images[2]._height, INFO_BOTTOM); + + // Draw the sides of the separator bar above the scroll bar + int yp = r.top + screen.fontHeight() + 7; + screen._backBuffer1.transBlitFrom(images[4], Common::Point(r.left, yp - 1)); + screen._backBuffer1.transBlitFrom(images[5], Common::Point(r.right - images[5]._width, yp - 1)); + + // Draw the bar above the scroll bar + screen._backBuffer1.hLine(r.left + images[4]._width, yp, r.right - images[5]._width, INFO_TOP); + screen._backBuffer1.hLine(r.left + images[4]._width, yp + 1, r.right - images[5]._width, INFO_MIDDLE); + screen._backBuffer1.hLine(r.left + images[4]._width, yp + 2, r.right - images[5]._width, INFO_BOTTOM); + + if (mode != 2) { + // Draw the Bars separating the Journal Commands + int xp = r.right / 3; + for (int idx = 0; idx < 2; ++idx) { + screen._backBuffer1.transBlitFrom(images[6], Common::Point(xp - 2, r.top + 1)); + screen._backBuffer1.transBlitFrom(images[7], Common::Point(xp - 2, yp - 1)); + + screen._backBuffer1.hLine(xp - 1, r.top + 4, yp - 2, INFO_TOP); + screen._backBuffer1.hLine(xp, r.top + 4, yp - 2, INFO_MIDDLE); + screen._backBuffer1.hLine(xp + 1, r.top + 4, yp - 2, INFO_BOTTOM); + xp = r.right / 3 * 2; + } + } + + int savedSelector = _oldSelector; + _oldSelector = 100; + + switch (mode) { + case 0: + highlightJournalControls(false); + break; + case 1: + highlightSearchControls(false); + break; + default: + break; + } + + _oldSelector = savedSelector; +} + +void TattooJournal::highlightJournalControls(bool slamIt) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + Common::Rect r(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, SHERLOCK_SCREEN_HEIGHT - r.height()); + + // Calculate the Scroll Position Bar + int numPages = (_maxPage + LINES_PER_PAGE) / LINES_PER_PAGE; + int barWidth = (r.width() - BUTTON_SIZE * 2 - 6) / numPages; + barWidth = CLIP(barWidth, BUTTON_SIZE, r.width() - BUTTON_SIZE * 2 - 6); + + int barX = (numPages <= 1) ? r.left + 3 + BUTTON_SIZE : (r.width() - BUTTON_SIZE * 2 - 6 - barWidth) + * FIXED_INT_MULTIPLIER / (numPages - 1) * (_page - 1) / FIXED_INT_MULTIPLIER + r.left + 3 + BUTTON_SIZE; + + // See if the mouse is over any of the Journal Controls + Common::Rect bounds(r.left, r.top, r.right - 3, r.top + screen.fontHeight() + 7); + _selector = -1; + if (bounds.contains(mousePos)) + _selector = (mousePos.x - r.left) / (r.width() / 3); + + else if (events._pressed) { + if (Common::Rect(r.left, r.top + screen.fontHeight() + 10, r.left + BUTTON_SIZE, r.top + + screen.fontHeight() + 10 + BUTTON_SIZE).contains(mousePos)) + // Press on the Scroll Left button + _selector = 3; + else if (Common::Rect(r.left + BUTTON_SIZE + 3, r.top + screen.fontHeight() + 10, + r.left + BUTTON_SIZE + 3 + (barX - r.left - BUTTON_SIZE - 3), r.top + screen.fontHeight() + + 10 + BUTTON_SIZE).contains(mousePos)) + // Press on the Page Left button + _selector = 4; + else if (Common::Rect(barX + barWidth, r.top + screen.fontHeight() + 10, + barX + barWidth + (r.right - BUTTON_SIZE - 3 - barX - barWidth), + r.top + screen.fontHeight() + 10 + BUTTON_SIZE).contains(mousePos)) + // Press on the Page Right button + _selector = 5; + else if (Common::Rect(r.right - BUTTON_SIZE - 3, r.top + screen.fontHeight() + 10, r.right - 3, + r.top + screen.fontHeight() + 10 + BUTTON_SIZE).contains(mousePos)) + // Press of the Scroll Right button + _selector = 6; + } + + // See if the Search was selected, but is not available + if (_journal.empty() && (_selector == 1 || _selector == 2)) + _selector = -1; + + if (_selector == 4 && _oldSelector == 5) + _selector = 5; + else if (_selector == 5 && _oldSelector == 4) + _selector = 4; + + // See if they're pointing at a different control + if (_selector != _oldSelector) { + // Print the Journal commands + int xp = r.left + r.width() / 6; + byte color = (_selector == 0) ? COMMAND_HIGHLIGHTED : INFO_TOP; + + screen.gPrint(Common::Point(xp - screen.stringWidth(FIXED(CloseJournal)) / 2, r.top + 5), + color, "%s", FIXED(CloseJournal)); + xp += r.width() / 3; + + if (!_journal.empty()) + color = (_selector == 1) ? COMMAND_HIGHLIGHTED : INFO_TOP; + else + color = INFO_BOTTOM; + screen.gPrint(Common::Point(xp - screen.stringWidth(FIXED(SearchJournal)) / 2, r.top + 5), + color, "%s", FIXED(SearchJournal)); + xp += r.width() / 3; + + color = INFO_BOTTOM; + screen.gPrint(Common::Point(xp - screen.stringWidth(FIXED(SaveJournal)) / 2, r.top + 5), + color, "%s", FIXED(SaveJournal)); + + // Draw the horizontal scrollbar + drawScrollBar(); + + if (slamIt) + screen.slamRect(r); + + _oldSelector = _selector; + } +} + +void TattooJournal::highlightSearchControls(bool slamIt) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + Common::Rect r(JOURNAL_BAR_WIDTH, (screen.fontHeight() + 4) * 2 + 9); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, (SHERLOCK_SCREEN_HEIGHT - r.height()) / 2); + const char *SEARCH_COMMANDS[3] = { FIXED(AbortSearch), FIXED(SearchBackwards), FIXED(SearchForwards) }; + + // See if the mouse is over any of the Journal Controls + _selector = -1; + if (Common::Rect(r.left + 3, r.top + 3, r.right - 3, r.top + 7 + screen.fontHeight()).contains(mousePos)) + _selector = (mousePos.x - r.left) / (r.width() / 3); + + // See if they're pointing at a different control + if (_selector != _oldSelector) { + // Print the search commands + int xp = r.left + r.width() / 6; + + for (int idx = 0; idx < 3; ++idx) { + byte color = (_selector == idx) ? COMMAND_HIGHLIGHTED : INFO_TOP; + screen.gPrint(Common::Point(xp - screen.stringWidth(SEARCH_COMMANDS[idx]) / 2, + r.top + 5), color, "%s", SEARCH_COMMANDS[idx]); + xp += r.width() / 3; + } + + if (slamIt) + screen.slamRect(r); + + _oldSelector = _selector; + } +} + +void TattooJournal::drawScrollBar() { + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + bool raised; + byte color; + + Common::Rect r(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, SHERLOCK_SCREEN_HEIGHT - r.height()); + + // Calculate the Scroll Position Bar + int numPages = (_maxPage + LINES_PER_PAGE) / LINES_PER_PAGE; + int barWidth = (r.width() - BUTTON_SIZE * 2 - 6) / numPages; + barWidth = CLIP(barWidth, BUTTON_SIZE, r.width() - BUTTON_SIZE * 2 - 6); + int barX; + if (numPages <= 1) { + barX = r.left + 3 + BUTTON_SIZE; + } else { + barX = (r.width() - BUTTON_SIZE * 2 - 6 - barWidth) * FIXED_INT_MULTIPLIER / (numPages - 1) * + (_page - 1) / FIXED_INT_MULTIPLIER + r.left + 3 + BUTTON_SIZE; + if (barX + BUTTON_SIZE > r.left + r.width() - BUTTON_SIZE - 3) + barX = r.right - BUTTON_SIZE * 2 - 3; + } + + // Draw the scroll bar here + // Draw the Scroll Left button + raised = _selector != 3; + screen._backBuffer1.fillRect(Common::Rect(r.left, r.top + screen.fontHeight() + 12, r.left + BUTTON_SIZE, + r.top + screen.fontHeight() + BUTTON_SIZE + 9), INFO_MIDDLE); + ui.drawDialogRect(screen._backBuffer1, Common::Rect(r.left + 3, r.top + screen.fontHeight() + 10, r.left + 3 + BUTTON_SIZE, + r.top + screen.fontHeight() + 10 + BUTTON_SIZE), raised); + + color = (_page > 1) ? INFO_BOTTOM + 2 : INFO_BOTTOM; + screen._backBuffer1.vLine(r.left + 1 + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 10 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 10 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.left + 2 + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 9 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 11 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.left + 3 + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 8 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 12 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.left + 4 + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 7 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 13 + BUTTON_SIZE / 2, color); + + // Draw the Scroll Right button + raised = _selector != 6; + screen._backBuffer1.fillRect(Common::Rect(r.right - BUTTON_SIZE - 1, r.top + screen.fontHeight() + 12, + r.right - 5, r.top + screen.fontHeight() + BUTTON_SIZE + 9), INFO_MIDDLE); + ui.drawDialogRect(screen._backBuffer1, Common::Rect(r.right - BUTTON_SIZE - 3, r.top + screen.fontHeight() + 10, r.right - 3, + r.top + screen.fontHeight() + BUTTON_SIZE + 9), raised); + + color = _down ? INFO_BOTTOM + 2 : INFO_BOTTOM; + screen._backBuffer1.vLine(r.right - 1 - BUTTON_SIZE + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 10 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 10 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.right - 2 - BUTTON_SIZE + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 9 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 11 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.right - 3 - BUTTON_SIZE + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 8 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 12 + BUTTON_SIZE / 2, color); + screen._backBuffer1.vLine(r.right - 4 - BUTTON_SIZE + BUTTON_SIZE / 2, r.top + screen.fontHeight() + 7 + BUTTON_SIZE / 2, + r.top + screen.fontHeight() + 13 + BUTTON_SIZE / 2, color); + + // Draw the scroll bar + screen._backBuffer1.fillRect(Common::Rect(barX + 2, r.top + screen.fontHeight() + 12, barX + barWidth - 3, + r.top + screen.fontHeight() + BUTTON_SIZE + 9), INFO_MIDDLE); + ui.drawDialogRect(screen._backBuffer1, Common::Rect(barX, r.top + screen.fontHeight() + 10, barX + barWidth, + r.top + screen.fontHeight() + 10 + BUTTON_SIZE), true); +} + +void TattooJournal::disableControls() { + Screen &screen = *_vm->_screen; + Common::Rect r(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_HEIGHT - r.width()) / 2, SHERLOCK_SCREEN_HEIGHT - r.height()); + const char *JOURNAL_COMMANDS[3] = { FIXED(CloseJournal), FIXED(SearchJournal), FIXED(SaveJournal) }; + + // Print the Journal commands + int xp = r.left + r.width() / 6; + for (int idx = 0; idx < 2; ++idx) { + screen.gPrint(Common::Point(xp - screen.stringWidth(JOURNAL_COMMANDS[idx]) / 2, r.top + 5), + INFO_BOTTOM, "%s", JOURNAL_COMMANDS[idx]); + + xp += r.width() / 3; + } + + screen.slamRect(r); +} + +int TattooJournal::getFindName(bool printError) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + int result = 0; + int done = 0; + Common::String name; + int cursorX, cursorY; + bool flag = false; + + Common::Rect r(JOURNAL_BAR_WIDTH, (screen.fontHeight() + 4) * 2 + 9); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, (SHERLOCK_SCREEN_HEIGHT - r.height()) / 2); + + // Set the cursors Y position + cursorY = r.top + screen.fontHeight() + 12; + + drawControls(1); + + // Backup the area under the text entry + Surface bgSurface(r.width() - 6, screen.fontHeight()); + bgSurface.blitFrom(screen._backBuffer1, Common::Point(0, 0), Common::Rect(r.left + 3, cursorY, + r.right - 3, cursorY + screen.fontHeight())); + + if (printError) { + screen.gPrint(Common::Point(0, cursorY), INFO_TOP, "%s", FIXED(TextNotFound)); + } else { + // If there was a name already entered, copy it to name and display it + if (!_find.empty()) { + screen.gPrint(Common::Point(r.left + screen.widestChar() + 3, cursorY), COMMAND_HIGHLIGHTED, "%s", _find.c_str()); + name = _find; + } + } + + screen.slamRect(r); + + if (printError) { + // Pause to allow error to be shown + int timer = 0; + + do { + events.pollEvents(); + events.setButtonState(); + + ++timer; + events.wait(2); + } while (!_vm->shouldQuit() && !events.kbHit() && !events._released && !events._rightReleased && timer < 40); + + events.clearEvents(); + + // Restore the text background + screen._backBuffer1.blitFrom(bgSurface, Common::Point(r.left, cursorY)); + + // If there was a name already entered, copy it to name and display it + if (!_find.empty()) { + screen.gPrint(Common::Point(r.left + screen.widestChar() + 3, cursorY), COMMAND_HIGHLIGHTED, "%s", _find.c_str()); + name = _find; + } + + screen.slamArea(r.left + 3, cursorY, r.width() - 6, screen.fontHeight()); + } + + // Set the cursors X position + cursorX = r.left + screen.widestChar() + 3 + screen.stringWidth(name); + + do { + events._released = events._rightReleased = false; + + while (!events.kbHit() && !events._released && !events._rightReleased) { + if (talk._talkToAbort) + return 0; + + // See if a key or a mouse button is pressed + events.pollEventsAndWait(); + events.setButtonState(); + + // Handle blinking cursor + flag = !flag; + if (flag) { + // Draw cursor + screen._backBuffer1.fillRect(Common::Rect(cursorX, cursorY, cursorX + 7, cursorY + 8), COMMAND_HIGHLIGHTED); + screen.slamArea(cursorX, cursorY, 8, 9); + } else { + // Erase cursor by restoring background and writing current text + screen._backBuffer1.blitFrom(bgSurface, Common::Point(r.left + 3, cursorY)); + screen.gPrint(Common::Point(r.left + screen.widestChar() + 3, cursorY), COMMAND_HIGHLIGHTED, "%s", name.c_str()); + screen.slamArea(r.left + 3, r.top, r.width() - 3, screen.fontHeight()); + } + + highlightSearchControls(true); + + events.wait(2); + } + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + Common::Point mousePos = events.mousePos(); + + if (keyState.keycode == Common::KEYCODE_BACKSPACE && !name.empty()) { + cursorX -= screen.charWidth(name.lastChar()); + name.deleteLastChar(); + } + + if (keyState.keycode == Common::KEYCODE_RETURN) + done = 1; + + else if (keyState.keycode == Common::KEYCODE_ESCAPE) + done = -1; + + if (keyState.keycode == Common::KEYCODE_TAB) { + r = Common::Rect(JOURNAL_BAR_WIDTH, BUTTON_SIZE + screen.fontHeight() + 13); + r.moveTo((SHERLOCK_SCREEN_WIDTH - r.width()) / 2, (SHERLOCK_SCREEN_HEIGHT - r.height()) / 2); + + // See if the mouse is over any of the journal controls + _selector = -1; + if (Common::Rect(r.left + 3, r.top + 3, r.right - 3, r.top + screen.fontHeight() + 4).contains(mousePos)) + _selector = (mousePos.x - r.left) / (r.width() / 3); + + // If the mouse is not over any of the options, move the mouse so that it points to the first option + if (_selector == -1) { + events.warpMouse(Common::Point(r.left + r.width() / 3, r.top + screen.fontHeight() + 2)); + } else { + if (keyState.keycode & Common::KBD_SHIFT) { + if (_selector == 0) + _selector = 2; + else + --_selector; + } else { + if (_selector == 2) + _selector = 0; + else + ++_selector; + } + + events.warpMouse(Common::Point(r.left + (r.width() / 3) * (_selector + 1) - 10, mousePos.y)); + } + } + + if (keyState.ascii && keyState.ascii != '@' && name.size() < 50) { + if ((cursorX + screen.charWidth(keyState.ascii)) < (r.right - screen.widestChar() * 3)) { + cursorX += screen.charWidth(keyState.ascii); + name += toupper(keyState.ascii); + } + } + + // Redraw the text + screen._backBuffer1.blitFrom(bgSurface, Common::Point(r.left + 3, cursorY)); + screen.gPrint(Common::Point(r.left + screen.widestChar() + 3, cursorY), COMMAND_HIGHLIGHTED, + "%s", name.c_str()); + screen.slamArea(r.left + 3, cursorY, r.right - 3, screen.fontHeight()); + } + + if (events._released || events._rightReleased) { + switch (_selector) + { + case 0: + done = -1; + break; + case 1: + done = 2; + break; + case 2: + done = 1; + break; + default: + break; + } + } + } while (!done); + + if (done != -1) { + _find = name; + result = done; + } else { + result = 0; + } + + drawFrame(); + drawJournal(0, 0); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + return result; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_journal.h b/engines/sherlock/tattoo/tattoo_journal.h new file mode 100644 index 0000000000..5e5cfda8c2 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_journal.h @@ -0,0 +1,103 @@ +/* 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_JOURNAL_H +#define SHERLOCK_TATTOO_JOURNAL_H + +#include "sherlock/journal.h" +#include "sherlock/image_file.h" + +namespace Sherlock { + +namespace Tattoo { + +class TattooJournal : public Journal { +private: + ImageFile *_journalImages; + int _selector, _oldSelector; + bool _wait; + bool _exitJournal; + uint32 _scrollingTimer; + int _savedIndex, _savedSub, _savedPage; + + /** + * Load the list of journal locations + */ + void loadLocations(); + + /** + * Displays the controls used by the journal + * @param mode 0: Normal journal buttons, 1: Search interface + */ + void drawControls(int mode); + + /** + * Draw the journal controls used by the journal + */ + void highlightJournalControls(bool slamIt); + + /** + * Draw the journal controls used in search mode + */ + void highlightSearchControls(bool slamIt); + + void drawScrollBar(); + + /** + * Check for and handle any pending keyboard events + */ + void handleKeyboardEvents(); + + /** + * Handle mouse presses on interface buttons + */ + void handleButtons(); + + /** + * Disable the journal controls + */ + void disableControls(); + + /** + * Get in a name to search through the journal for + */ + int getFindName(bool printError); +public: + TattooJournal(SherlockEngine *vm); + virtual ~TattooJournal() {} + + /** + * Show the journal + */ + void show(); +public: + /** + * Draw the journal background, frame, and interface buttons + */ + virtual void drawFrame(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_map.cpp b/engines/sherlock/tattoo/tattoo_map.cpp new file mode 100644 index 0000000000..21abf7d3c0 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_map.cpp @@ -0,0 +1,439 @@ +/* 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_map.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define MAP_NAME_COLOR 131 +#define CLOSEUP_STEPS 30 +#define SCROLL_SPEED 16 + +/*-------------------------------------------------------------------------*/ + +void MapEntry::clear() { + _iconNum = -1; + _description = ""; +} + +/*-------------------------------------------------------------------------*/ + +TattooMap::TattooMap(SherlockEngine *vm) : Map(vm), _mapTooltip(vm) { + _iconImages = nullptr; + _bgFound = _oldBgFound = 0; + + loadData(); +} + +int TattooMap::show() { + Debugger &debugger = *_vm->_debugger; + Events &events = *_vm->_events; + Music &music = *_vm->_music; + Resources &res = *_vm->_res; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Screen &screen = *_vm->_screen; + int result = 0; + + // Check if we need to keep track of how many times player has been to the map + for (uint idx = 0; idx < scene._sceneTripCounters.size(); ++idx) { + SceneTripEntry &entry = scene._sceneTripCounters[idx]; + + if (entry._sceneNumber == OVERHEAD_MAP || entry._sceneNumber == OVERHEAD_MAP2) { + if (--entry._numTimes == 0) { + _vm->setFlagsDirect(entry._flag); + scene._sceneTripCounters.remove_at(idx); + } + } + } + + if (music._midiOption) { + // See if Holmes or Watson is the active character + Common::String song; + if (_vm->readFlags(FLAG_PLAYER_IS_HOLMES)) + // Player is Holmes + song = "Cue9"; + else if (_vm->readFlags(FLAG_ALT_MAP_MUSIC)) + song = "Cue8"; + else + song = "Cue7"; + + if (music.loadSong(song)) { + music.setMIDIVolume(music._musicVolume); + if (music._musicOn) + music.startSong(); + } + } + + screen.initPaletteFade(1364485); + + // Load the custom mouse cursors for the map + ImageFile cursors("omouse.vgs"); + events.setCursor(cursors[0]._frame); + events.warpMouse(Common::Point(SHERLOCK_SCREEN_WIDTH / 2, SHERLOCK_SCREEN_HEIGHT / 2)); + + // Load the data for the map + _iconImages = new ImageFile("mapicons.vgs"); + loadData(); + + // Load the palette + Common::SeekableReadStream *stream = res.load("map.pal"); + stream->read(screen._cMap, PALETTE_SIZE); + screen.translatePalette(screen._cMap); + delete stream; + + // Load the map image and draw it to the back buffer + ImageFile *map = new ImageFile("map.vgs"); + screen._backBuffer1.create(SHERLOCK_SCREEN_WIDTH * 2, SHERLOCK_SCREEN_HEIGHT * 2); + screen._backBuffer1.blitFrom((*map)[0], Common::Point(0, 0)); + delete map; + + screen.clear(); + screen.setPalette(screen._cMap); + drawMapIcons(); + + // Copy the map drawn in the back buffer to the secondary back buffer + screen._backBuffer2.create(SHERLOCK_SCREEN_WIDTH * 2, SHERLOCK_SCREEN_HEIGHT * 2); + screen._backBuffer2.blitFrom(screen._backBuffer1); + + // Display the built map to the screen + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + // Set initial scroll position + _targetScroll = _bigPos; + screen._currentScroll = Common::Point(-1, -1); + + do { + // Allow for event processing and get the current mouse position + events.pollEventsAndWait(); + events.setButtonState(); + Common::Point mousePos = events.screenMousePos(); + + if (debugger._showAllLocations == LOC_REFRESH) { + drawMapIcons(); + screen.slamArea(screen._currentScroll.x, screen._currentScroll.y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_WIDTH); + } + + checkMapNames(true); + + if (mousePos.x < (SHERLOCK_SCREEN_WIDTH / 6)) + _targetScroll.x -= 2 * SCROLL_SPEED * (SHERLOCK_SCREEN_WIDTH / 6 - mousePos.x) / (SHERLOCK_SCREEN_WIDTH / 6); + if (mousePos.x > (SHERLOCK_SCREEN_WIDTH * 5 / 6)) + _targetScroll.x += 2 * SCROLL_SPEED * (mousePos.x - (SHERLOCK_SCREEN_WIDTH * 5 / 6)) / (SHERLOCK_SCREEN_WIDTH / 6); + if (mousePos.y < (SHERLOCK_SCREEN_HEIGHT / 6)) + _targetScroll.y -= 2 * SCROLL_SPEED * (SHERLOCK_SCREEN_HEIGHT / 6 - mousePos.y) / (SHERLOCK_SCREEN_HEIGHT / 6); + if (mousePos.y > (SHERLOCK_SCREEN_HEIGHT * 5 / 6)) + _targetScroll.y += 2 * SCROLL_SPEED * (mousePos.y - SHERLOCK_SCREEN_HEIGHT * 5 / 6) / (SHERLOCK_SCREEN_HEIGHT / 6); + + if (_targetScroll.x < 0) + _targetScroll.x = 0; + if ((_targetScroll.x + SHERLOCK_SCREEN_WIDTH) > screen._backBuffer1.w()) + _targetScroll.x = screen._backBuffer1.w() - SHERLOCK_SCREEN_WIDTH; + if (_targetScroll.y < 0) + _targetScroll.y = 0; + if ((_targetScroll.y + SHERLOCK_SCREEN_HEIGHT) > screen._backBuffer1.h()) + _targetScroll.y = screen._backBuffer1.h() - SHERLOCK_SCREEN_HEIGHT; + + // Check the keyboard + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + switch (keyState.keycode) { + case Common::KEYCODE_HOME: + case Common::KEYCODE_KP7: + _targetScroll.x = 0; + _targetScroll.y = 0; + break; + + case Common::KEYCODE_END: + case Common::KEYCODE_KP1: + _targetScroll.x = screen._backBuffer1.w() - SHERLOCK_SCREEN_WIDTH; + _targetScroll.y = screen._backBuffer1.h() - SHERLOCK_SCREEN_HEIGHT; + break; + + case Common::KEYCODE_PAGEUP: + case Common::KEYCODE_KP9: + _targetScroll.y -= SHERLOCK_SCREEN_HEIGHT; + if (_targetScroll.y < 0) + _targetScroll.y = 0; + break; + + case Common::KEYCODE_PAGEDOWN: + case Common::KEYCODE_KP3: + _targetScroll.y += SHERLOCK_SCREEN_HEIGHT; + if (_targetScroll.y > (screen._backBuffer1.h() - SHERLOCK_SCREEN_HEIGHT)) + _targetScroll.y = screen._backBuffer1.h() - SHERLOCK_SCREEN_HEIGHT; + break; + + case Common::KEYCODE_SPACE: + events._pressed = false; + events._oldButtons = 0; + events._released = true; + break; + + default: + break; + } + } + + // Handle any scrolling of the map + if (screen._currentScroll != _targetScroll) { + // If there is a Text description being displayed, restore the area under it + _mapTooltip.erase(); + + screen._currentScroll = _targetScroll; + + checkMapNames(false); + screen.slamArea(_targetScroll.x, _targetScroll.y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + } + + // Handling if a location has been clicked on + if (events._released && _bgFound != -1) { + // If there is a Text description being displayed, restore the area under it + _mapTooltip.erase(); + + // Save the current scroll position on the map + _bigPos = screen._currentScroll; + + showCloseUp(_bgFound); + result = _bgFound + 1; + } + } while (!result && !_vm->shouldQuit()); + + music.stopMusic(); + events.clearEvents(); + _mapTooltip.banishWindow(); + + // Reset the back buffers back to standard size + screen._backBuffer1.create(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + screen._backBuffer2.create(SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + return result; +} + +void TattooMap::loadData() { + Resources &res = *_vm->_res; + char c; + + Common::SeekableReadStream *stream = res.load("map.txt"); + + _data.resize(100); + for (uint idx = 0; idx < _data.size(); ++idx) + _data[idx].clear(); + + do + { + // Find the start of the number + do { + c = stream->readByte(); + if (stream->pos() >= stream->size()) + return; + } while (c < '0' || c > '9'); + + // Get the scene number + Common::String locStr; + locStr += c; + while ((c = stream->readByte()) != '.') + locStr += c; + MapEntry &mapEntry = _data[atoi(locStr.c_str()) - 1]; + + // Get the location name + while (stream->readByte() != '"') + ; + + while ((c = stream->readByte()) != '"') + mapEntry._description += c; + + // Find the ( specifying the (X,Y) position of the Icon + while (stream->readByte() != '(') + ; + + // Get the X Position of the icon + Common::String numStr; + while ((c = stream->readByte()) != ',') + numStr += c; + mapEntry.x = atoi(numStr.c_str()); + + // Get the Y position of the icon + numStr = ""; + while ((c = stream->readByte()) != ')') + numStr += c; + mapEntry.y = atoi(numStr.c_str()); + + // Find and get the location's icon number + while (stream->readByte() != '#') + ; + + Common::String iconStr; + while (stream->pos() < stream->size() && (c = stream->readByte()) != '\r') + iconStr += c; + + mapEntry._iconNum = atoi(iconStr.c_str()) - 1; + } while (stream->pos() < stream->size()); + + delete stream; +} + +void TattooMap::drawMapIcons() { + Debugger &debugger = *_vm->_debugger; + Screen &screen = *_vm->_screen; + + for (uint idx = 0; idx < _data.size(); ++idx) { + if (debugger._showAllLocations != LOC_DISABLED) + _vm->setFlagsDirect(idx + 1); + + if (_data[idx]._iconNum != -1 && _vm->readFlags(idx + 1)) { + MapEntry &mapEntry = _data[idx]; + ImageFrame &img = (*_iconImages)[mapEntry._iconNum]; + screen._backBuffer1.transBlitFrom(img._frame, Common::Point(mapEntry.x - img._width / 2, + mapEntry.y - img._height / 2)); + } + } + + if (debugger._showAllLocations == LOC_REFRESH) + debugger._showAllLocations = LOC_ALL; +} + +void TattooMap::checkMapNames(bool slamIt) { + Events &events = *_vm->_events; + Common::Point mapPos = events.mousePos(); + + // See if the mouse is pointing at any of the map locations + _bgFound = -1; + + for (uint idx = 0; idx < _data.size(); ++idx) { + if (_data[idx]._iconNum != -1 && _vm->readFlags(idx + 1)) { + MapEntry &mapEntry = _data[idx]; + ImageFrame &img = (*_iconImages)[mapEntry._iconNum]; + Common::Rect r(mapEntry.x - img._width / 2, mapEntry.y - img._height / 2, + mapEntry.x + img._width / 2, mapEntry.y + img._height / 2); + + if (r.contains(mapPos)) { + _bgFound = idx; + break; + } + } + } + + // Handle updating the tooltip + if (_bgFound != _oldBgFound) { + if (_bgFound == -1) { + _mapTooltip.setText(""); + } else { + const Common::String &desc = _data[_bgFound]._description; + _mapTooltip.setText(desc); + } + + _oldBgFound = _bgFound; + } + + _mapTooltip.handleEvents(); + if (slamIt) + _mapTooltip.draw(); +} + +void TattooMap::restoreArea(const Common::Rect &bounds) { + Screen &screen = *_vm->_screen; + + Common::Rect r = bounds; + r.clip(Common::Rect(0, 0, screen._backBuffer1.w(), screen._backBuffer1.h())); + + if (!r.isEmpty()) + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(r.left, r.top), r); +} + +void TattooMap::showCloseUp(int closeUpNum) { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + + // Reset scroll position + screen._currentScroll = Common::Point(0, 0); + + // Get the closeup images + Common::String fname = Common::String::format("res%02d.vgs", closeUpNum + 1); + ImageFile pic(fname); + + Point32 closeUp(_data[closeUpNum].x * 100, _data[closeUpNum].y * 100); + Point32 delta((SHERLOCK_SCREEN_WIDTH / 2 - closeUp.x / 100) * 100 / CLOSEUP_STEPS, + (SHERLOCK_SCREEN_HEIGHT / 2 - closeUp.y / 100) * 100 / CLOSEUP_STEPS); + Common::Rect oldBounds(closeUp.x / 100, closeUp.y / 100, closeUp.x / 100 + 1, closeUp.y / 100 + 1); + int size = 64; + int n = 256; + int deltaVal = 512; + bool minimize = false; + int scaleVal, newSize; + + do { + scaleVal = n; + newSize = pic[0].sDrawXSize(n); + + if (newSize > size) { + if (minimize) + deltaVal /= 2; + n += deltaVal; + } else { + minimize = true; + deltaVal /= 2; + n -= deltaVal; + if (n < 1) + n = 1; + } + } while (deltaVal && size != newSize); + + int deltaScale = (SCALE_THRESHOLD - scaleVal) / CLOSEUP_STEPS; + + for (int step = 0; step < CLOSEUP_STEPS; ++step) { + Common::Point picSize(pic[0].sDrawXSize(scaleVal), pic[0].sDrawYSize(scaleVal)); + Common::Point pt(closeUp.x / 100 - picSize.x / 2, closeUp.y / 100 - picSize.y / 2); + + restoreArea(oldBounds); + screen._backBuffer1.transBlitFrom(pic[0], pt, false, 0, scaleVal); + + screen.slamRect(oldBounds); + screen.slamArea(pt.x, pt.y, picSize.x, picSize.y); + + oldBounds = Common::Rect(pt.x, pt.y, pt.x + picSize.x + 1, pt.y + picSize.y + 1); + closeUp += delta; + scaleVal += deltaScale; + + events.wait(1); + } + + // Handle final drawing of closeup + // TODO: Handle scrolling + Common::Rect r(SHERLOCK_SCREEN_WIDTH / 2 - pic[0]._width / 2, SHERLOCK_SCREEN_HEIGHT / 2 - pic[0]._height / 2, + SHERLOCK_SCREEN_WIDTH / 2 - pic[0]._width / 2 + pic[0]._width, + SHERLOCK_SCREEN_HEIGHT / 2 - pic[0]._height / 2 + pic[0]._height); + + restoreArea(oldBounds); + screen._backBuffer1.transBlitFrom(pic[0], Common::Point(r.left, r.top)); + screen.slamRect(oldBounds); + screen.slamRect(r); + events.wait(2); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_map.h b/engines/sherlock/tattoo/tattoo_map.h new file mode 100644 index 0000000000..1c7173bdb0 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_map.h @@ -0,0 +1,93 @@ +/* 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_MAP_H +#define SHERLOCK_TATTOO_MAP_H + +#include "common/scummsys.h" +#include "sherlock/map.h" +#include "sherlock/resources.h" +#include "sherlock/surface.h" +#include "sherlock/tattoo/widget_tooltip.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +struct MapEntry : Common::Point { + int _iconNum; + Common::String _description; + + MapEntry() : Common::Point(), _iconNum(-1) {} + MapEntry(int posX, int posY, int iconNum) : Common::Point(posX, posY), _iconNum(iconNum) {} + void clear(); +}; + +class TattooMap : public Map { +private: + Common::Array<MapEntry> _data; + ImageFile *_iconImages; + int _bgFound, _oldBgFound; + WidgetMapTooltip _mapTooltip; + Common::Point _targetScroll; + + /** + * Load data needed for the map + */ + void loadData(); + + /** + * Draws all available location icons onto the back buffer + */ + void drawMapIcons(); + + /** + * Draws the location names of whatever the mouse moves over on the map + */ + void checkMapNames(bool slamIt); + + /** + * Restores an area of the map background + */ + void restoreArea(const Common::Rect &bounds); + + /** + * This will load a specified close up and zoom it up to the middle of the screen + */ + void showCloseUp(int closeUpNum); +public: + TattooMap(SherlockEngine *vm); + virtual ~TattooMap() {} + + /** + * Show the map + */ + virtual int show(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_people.cpp b/engines/sherlock/tattoo/tattoo_people.cpp new file mode 100644 index 0000000000..7aaa0a082c --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_people.cpp @@ -0,0 +1,1432 @@ +/* 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_people.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_talk.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define FACING_PLAYER 16 +#define NUM_ADJUSTED_WALKS 21 + +struct AdjustWalk { + char _vgsName[9]; + int _xAdjust; + int _flipXAdjust; + int _yAdjust; +} ; + +static const AdjustWalk ADJUST_WALKS[NUM_ADJUSTED_WALKS] = { + { "TUPRIGHT", -7, -19, 6 }, + { "TRIGHT", 8, -14, 0 }, + { "TDOWNRG", 14, -12, 0 }, + { "TWUPRIGH", 12, 4, 2 }, + { "TWRIGHT", 31, -14, 0 }, + { "TWDOWNRG", 6, -24, 0 }, + { "HTUPRIGH", 2, -20, 0 }, + { "HTRIGHT", 28, -20, 0 }, + { "HTDOWNRG", 8, -2, 0 }, + { "GTUPRIGH", 4, -12, 0 }, + { "GTRIGHT", 12, -16, 0 }, + { "GTDOWNRG", 10, -18, 0 }, + { "JTUPRIGH", 8, -10, 0 }, + { "JTRIGHT", 22, -6, 0 }, + { "JTDOWNRG", 4, -20, 0 }, + { "CTUPRIGH", 10, 0, 0 }, + { "CTRIGHT", 26, -22, 0 }, + { "CTDOWNRI", 16, 4, 0 }, + { "ITUPRIGH", 0, 0, 0 }, + { "ITRIGHT", 20, 0, 0 }, + { "ITDOWNRG", 8, 0, 0 } +}; + +static const int WALK_SPEED_X[99] = { + 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 98, 90, 90, 90, 90, 90, 91, 90, 90, + 90, 90,100, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90,100, 90, + 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90,103, 90, 90, 90, 90, 90, 90, 90, + 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, + 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 90 +}; + +static const int WALK_SPEED_Y[99] = { + 28, 28, 28, 28, 28, 28, 28, 28, 28, 32, 32, 32, 28, 28, 28, 28, 28, 26, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 32, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 31, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28 +}; + +static const int WALK_SPEED_DIAG_X[99] = { + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 90, 50, 50, 50, 50, 50, 50, 50, 50, 50, + 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50 +}; + +/*----------------------------------------------------------------*/ + +SavedNPCPath::SavedNPCPath() { + Common::fill(&_path[0], &_path[MAX_NPC_PATH], 0); + _npcIndex = 0; + _npcPause = 0; + _npcFacing = 0; + _lookHolmes = false; +} + +SavedNPCPath::SavedNPCPath(byte path[MAX_NPC_PATH], int npcIndex, int npcPause, const Common::Point &walkDest, + int npcFacing, bool lookHolmes) : _npcIndex(npcIndex), _npcPause(npcPause), _walkDest(walkDest), + _npcFacing(npcFacing), _lookHolmes(lookHolmes) { + Common::copy(&path[0], &path[MAX_NPC_PATH], &_path[0]); +} + +/*----------------------------------------------------------------*/ + +TattooPerson::TattooPerson() : Person() { + Common::fill(&_npcPath[0], &_npcPath[MAX_NPC_PATH], 0); + _tempX = _tempScaleVal = 0; + _npcIndex = 0; + _npcMoved = false; + _npcFacing = -1; + _resetNPCPath = true; + _savedNpcSequence = 0; + _savedNpcFrame = 0; + _updateNPCPath = true; + _npcPause = 0; + _lookHolmes = false; +} + +void TattooPerson::freeAltGraphics() { + if (_altImages != nullptr) { + delete _altImages; + _altImages = nullptr; + } + + _altSeq = 0; +} + +void TattooPerson::adjustSprite() { + People &people = *_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + if (_type == INVALID) + return; + + if (_type == CHARACTER && _status) { + // Sprite waiting to move, so restart walk + _walkCount = _status; + _status = 0; + + _walkDest = _walkTo.front(); + setWalking(); + } else if (_type == CHARACTER && _walkCount) { + if (_walkCount > 10) { + _walkDest = _nextDest; + setWalking(); + } + + _position += _delta; + if (_walkCount) + --_walkCount; + + if (!_walkCount) { + // If there are remaining points to walk, move to the next one + if (!_walkTo.empty()) { + _walkDest = _walkTo.pop(); + setWalking(); + } else { + gotoStand(); + } + } + } + + if (_type != CHARACTER) { + if (_position.y > SHERLOCK_SCREEN_HEIGHT) + _position.y = SHERLOCK_SCREEN_HEIGHT; + + if (_position.y < UPPER_LIMIT) + _position.y = UPPER_LIMIT; + + if (_position.x < LEFT_LIMIT) + _position.x = LEFT_LIMIT; + + if (_position.x > RIGHT_LIMIT) + _position.x = RIGHT_LIMIT; + } + + int frameNum = _frameNumber; + if (frameNum == -1) + frameNum = 0; + int idx = _walkSequences[_sequenceNumber][frameNum]; + if (idx > _maxFrames) + idx = 1; + + // Set the image frame + if (_altSeq) + _imageFrame = &(*_altImages)[idx - 1]; + else + _imageFrame = &(*_images)[idx - 1]; + + // See if the player has come to a stop after clicking on an Arrow zone to leave the scene. + // If so, this will set up the exit information for the scene transition + if (!_walkCount && ui._exitZone != -1 && scene._walkedInScene && scene._goToScene == -1 && + !_description.compareToIgnoreCase(people[HOLMES]._description)) { + Exit &exit = scene._exits[ui._exitZone]; + scene._goToScene = exit._scene; + + if (exit._newPosition.x != 0) { + people._savedPos = exit._newPosition; + + if (people._savedPos._facing > 100 && people._savedPos.x < 1) + people._savedPos.x = 100; + } + } +} + +void TattooPerson::gotoStand() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + + // If the misc field is set, then we're running a special talk sequence, so don't interrupt it. + if (_misc) + return; + + _walkTo.clear(); + _walkCount = 0; + int oldFacing = _sequenceNumber; + + // If the person was talking or listening, just return it to the standing sequence + // in the direction they were pointing + if (_sequenceNumber >= TALK_UPRIGHT && _sequenceNumber <= LISTEN_UPLEFT) { + switch (_sequenceNumber) { + case TALK_UPRIGHT: + case LISTEN_UPRIGHT: + _sequenceNumber = STOP_UPRIGHT; + break; + case TALK_RIGHT: + case LISTEN_RIGHT: + _sequenceNumber = STOP_RIGHT; + break; + case TALK_DOWNRIGHT: + case LISTEN_DOWNRIGHT: + _sequenceNumber = STOP_DOWNRIGHT; + break; + case TALK_DOWNLEFT: + case LISTEN_DOWNLEFT: + _sequenceNumber = STOP_DOWNLEFT; + break; + case TALK_LEFT: + case LISTEN_LEFT: + _sequenceNumber = STOP_LEFT; + break; + case TALK_UPLEFT: + case LISTEN_UPLEFT: + _sequenceNumber = STOP_UPLEFT; + break; + default: + break; + } + + if (_seqTo) { + // Reset to previous value + _walkSequences[oldFacing]._sequences[_frameNumber] = _seqTo; + _seqTo = 0; + } + + // Set the Frame number to the last frame so we don't move + _frameNumber = 0; + + checkWalkGraphics(); + + _oldWalkSequence = -1; + people._allowWalkAbort = true; + return; + } + + // If the sprite that is stopping is an NPC and he is supposed to face a certain direction + // when he stops, set that direction here + int npc = -1; + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + if (_imageFrame == people[idx]._imageFrame) + npc = idx; + } + + if (npc != -1 && people[npc]._npcFacing != -1) { + if (people[npc]._npcFacing == FACING_PLAYER) { + // See where Holmes is with respect to the NPC (x coords) + if (people[HOLMES]._position.x < people[npc]._position.x) + people[npc]._npcFacing = STOP_LEFT; + else + people[npc]._npcFacing = STOP_RIGHT; + + // See where Holmes is with respect to the NPC (y coords) + if (people[HOLMES]._position.y < people[npc]._position.y - (10 * FIXED_INT_MULTIPLIER)) { + // Holmes is above the NPC so reset the facing to the diagonal ups + if (people[npc]._npcFacing == STOP_RIGHT) + people[npc]._npcFacing = STOP_UPRIGHT; + else + people[npc]._npcFacing = STOP_UPLEFT; + } else { + if (people[HOLMES]._position.y > people[npc]._position.y + (10 * FIXED_INT_MULTIPLIER)) { + // Holmes is below the NPC so reset the facing to the diagonal downs + if (people[npc]._npcFacing == STOP_RIGHT) + people[npc]._npcFacing = STOP_DOWNRIGHT; + else + people[npc]._npcFacing = STOP_DOWNLEFT; + } + } + } + + _sequenceNumber = people[npc]._npcFacing; + } else { + switch (_sequenceNumber) { + case WALK_UP: _sequenceNumber = STOP_UP; break; + case WALK_UPRIGHT: _sequenceNumber = STOP_UPRIGHT; break; + case WALK_RIGHT: _sequenceNumber = STOP_RIGHT; break; + case WALK_DOWNRIGHT: _sequenceNumber = STOP_DOWNRIGHT; break; + case WALK_DOWN: _sequenceNumber = STOP_DOWN; break; + case WALK_DOWNLEFT: _sequenceNumber = STOP_DOWNLEFT;break; + case WALK_LEFT: _sequenceNumber = STOP_LEFT; break; + case WALK_UPLEFT: _sequenceNumber = STOP_UPLEFT; break; + } + } + + // Only restart the frame number at 0 if the new sequence is different from the last sequence + // so we don't let Holmes repeat standing. + if (_oldWalkSequence != -1) { + if (_seqTo) { + // Reset to previous value + _walkSequences[oldFacing]._sequences[_frameNumber] = _seqTo; + _seqTo = 0; + } + + _frameNumber = 0; + } + + checkWalkGraphics(); + + _oldWalkSequence = -1; + people._allowWalkAbort = true; +} + +void TattooPerson::setWalking() { + TattooScene &scene = *(TattooScene *)_vm->_scene; + int oldDirection, oldFrame; + Common::Point delta; + _nextDest = _walkDest; + + // Flag that player has now walked in the scene + scene._walkedInScene = true; + + // Stop any previous walking, since a new dest is being set + _walkCount = 0; + oldDirection = _sequenceNumber; + oldFrame = _frameNumber; + + // Set speed to use horizontal and vertical movement + int scaleVal = scene.getScaleVal(_position); + Common::Point speed(MAX(WALK_SPEED_X[scene._currentScene - 1] * SCALE_THRESHOLD / scaleVal, 2), + MAX(WALK_SPEED_Y[scene._currentScene - 1] * SCALE_THRESHOLD / scaleVal, 2)); + Common::Point diagSpeed(MAX(WALK_SPEED_DIAG_X[scene._currentScene - 1] * SCALE_THRESHOLD / scaleVal, 2), + MAX((WALK_SPEED_Y[scene._currentScene - 1] - 2) * SCALE_THRESHOLD / scaleVal, 2)); + + // 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 (;;) { + if (_centerWalk || !_walkTo.empty()) { + // Since we want the player to be centered on the ultimate destination, and the player + // is drawn from the left side, move the cursor half the width of the player to center it + delta = Common::Point(_position.x / FIXED_INT_MULTIPLIER - _walkDest.x, + _position.y / FIXED_INT_MULTIPLIER - _walkDest.y); + + int dir; + if (ABS(delta.x) > ABS(delta.y)) + dir = (delta.x < 0) ? WALK_LEFT : WALK_RIGHT; + else + dir = (delta.y < 0) ? WALK_UP : WALK_DOWN; + + scaleVal = scene.getScaleVal(Point32(_walkDest.x * FIXED_INT_MULTIPLIER, + _walkDest.y * FIXED_INT_MULTIPLIER)); + _walkDest.x -= _stopFrames[dir]->sDrawXSize(scaleVal) / 2; + } + + delta = Common::Point( + ABS(_position.x / FIXED_INT_MULTIPLIER - _walkDest.x), + ABS(_position.y / FIXED_INT_MULTIPLIER - _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 < (_position.x / FIXED_INT_MULTIPLIER)) { + _sequenceNumber = WALK_LEFT; + _delta.x = speed.x * -(FIXED_INT_MULTIPLIER / 10); + } else { + _sequenceNumber = WALK_RIGHT; + _delta.x = speed.x * (FIXED_INT_MULTIPLIER / 10); + } + + // 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 * 10) >= speed.x) { + // Det the delta y + _delta.y = (delta.y * FIXED_INT_MULTIPLIER) / ((delta.x * 10) / speed.x); + if (_walkDest.y < (_position.y / FIXED_INT_MULTIPLIER)) + _delta.y = -_delta.y; + + // Set how many times we should add the delta to the player's position + _walkCount = (delta.x * 10) / 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 + _delta = Point32(0, 0); + _position = Point32(_walkDest.x * FIXED_INT_MULTIPLIER, _walkDest.y * FIXED_INT_MULTIPLIER); + + _walkCount = 1; + } + + // See if the sequence needs to be changed for diagonal walking + if (_delta.y > 1500) { + if (_sequenceNumber == WALK_LEFT || _sequenceNumber == WALK_RIGHT) { + _delta.x = _delta.x / speed.x * diagSpeed.x; + _delta.y = (delta.y * FIXED_INT_MULTIPLIER) / (delta.x * 10 / diagSpeed.x); + + _walkCount = delta.x * 10 / diagSpeed.x; + } + + switch (_sequenceNumber) { + case WALK_LEFT: + _sequenceNumber = WALK_DOWNLEFT; + break; + case WALK_RIGHT: + _sequenceNumber = WALK_DOWNRIGHT; + break; + } + } else if (_delta.y < -1500) { + if (_sequenceNumber == WALK_LEFT || _sequenceNumber == WALK_RIGHT) { + _delta.x = _delta.x / speed.x * diagSpeed.x; + _delta.y = -1 * (delta.y * FIXED_INT_MULTIPLIER) / (delta.x * 10 / diagSpeed.x); + + _walkCount = (delta.x * 10) / diagSpeed.x; + } + + switch (_sequenceNumber) { + case WALK_LEFT: + _sequenceNumber = WALK_UPLEFT; + break; + case WALK_RIGHT: + _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 < (_position.y / FIXED_INT_MULTIPLIER)) { + _sequenceNumber = WALK_UP; + _delta.y = speed.y * -(FIXED_INT_MULTIPLIER / 10); + } else { + speed.y = diagSpeed.y; + _sequenceNumber = WALK_DOWN; + _delta.y = speed.y * (FIXED_INT_MULTIPLIER / 10); + } + + // Set the delta x + if (delta.y * 10 / speed.y) + _delta.x = (delta.x * FIXED_INT_MULTIPLIER) / (delta.y * 10 / speed.y); + else + _delta.x = (delta.x * FIXED_INT_MULTIPLIER) / delta.y; + + if (_walkDest.x < _position.y / FIXED_INT_MULTIPLIER) + _delta.x = -_delta.x; + + // Set how many times we should add the delta's to the players position + if (delta.y * 10 / speed.y) + _walkCount = delta.y * 10 / speed.y; + else + _walkCount = delta.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 (_sequenceNumber != _oldWalkSequence) { + if (_seqTo) { + // Reset to previous value + _walkSequences[oldDirection]._sequences[_frameNumber] = _seqTo; + _seqTo = 0; + } + _frameNumber = 0; + } + + checkWalkGraphics(); + _oldWalkSequence = _sequenceNumber; + + if (!_walkCount && _walkTo.empty()) + gotoStand(); + + // 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 (_sequenceNumber == oldDirection) + _frameNumber = oldFrame; +} + +void TattooPerson::walkToCoords(const Point32 &destPos, int destDir) { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Talk &talk = *_vm->_talk; + + CursorId oldCursor = events.getCursor(); + events.setCursor(WAIT); + + _walkDest = Common::Point(destPos.x / FIXED_INT_MULTIPLIER, destPos.y / FIXED_INT_MULTIPLIER); + + bool isHolmes = this == &people[HOLMES]; + if (isHolmes) { + people._allowWalkAbort = true; + } else { + // Clear the path Variables + _npcIndex = _npcPause; + Common::fill(_npcPath, _npcPath + 100, 0); + _npcFacing = destDir; + } + + _centerWalk = false; + + // Only move the person if they're going an appreciable distance + if (ABS(_walkDest.x - (_position.x / FIXED_INT_MULTIPLIER)) > 8 || + ABS(_walkDest.y - (_position.y / FIXED_INT_MULTIPLIER)) > 4) { + goAllTheWay(); + + do { + // Keep doing animations whilst walk is in progress + events.wait(1); + scene.doBgAnim(); + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_ESCAPE && vm._runningProlog) { + vm.setFlags(-76); + vm.setFlags(396); + scene._goToScene = 1; + talk._talkToAbort = true; + } + } + } while (!_vm->shouldQuit() && _walkCount && !talk._talkToAbort); + } + + _centerWalk = true; + if (!isHolmes) + _updateNPCPath = true; + + if (!talk._talkToAbort) { + // put character exactly on right spot + _position = destPos; + + if (_sequenceNumber != destDir) { + // Facing character to correct ending direction + _sequenceNumber = destDir; + gotoStand(); + } + + if (!isHolmes) + _updateNPCPath = false; + + // Secondary walking wait loop + bool done = false; + while (!done && !_vm->shouldQuit()) { + events.wait(1); + scene.doBgAnim(); + + // See if we're past the initial goto stand sequence + for (int idx = 0; idx < _frameNumber; ++idx) { + if (_walkSequences[_sequenceNumber][idx] == 0) { + done = true; + break; + } + } + + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_ESCAPE && vm._runningProlog) { + vm.setFlags(-76); + vm.setFlags(396); + scene._goToScene = 1; + talk._talkToAbort = true; + } + } + } + + if (!isHolmes) + _updateNPCPath = true; + + if (!talk._talkToAbort) + events.setCursor(oldCursor); + } +} + +void TattooPerson::clearNPC() { + Common::fill(&_npcPath[0], &_npcPath[MAX_NPC_PATH], 0); + _npcIndex = 0; + _pathStack.clear(); + _npcName = ""; +} + +void TattooPerson::updateNPC() { + People &people = *_vm->_people; + Talk &talk = *_vm->_talk; + + // If the NPC isn't on, or it's in Talk or Listen Mode, then return without doing anything + if (_type != CHARACTER || _sequenceNumber >= TALK_UPRIGHT) + return; + + // If the NPC is paused, just decrement his pause counter and exit + if (_npcPause) { + // Decrement counter + --_npcPause; + + // Now see if we need to update the NPC's frame sequence so that he faces Holmes + if (_lookHolmes) { + // See where Holmes is with respect to the NPC (x coordinate) + _npcFacing = (people[HOLMES]._position.x < _position.x) ? STOP_LEFT : STOP_RIGHT; + + // See where Holmes is with respect to the NPC (y coordinate) + if (people[HOLMES]._position.y < (_position.y - 10 * FIXED_INT_MULTIPLIER)) { + // Holmes is above the NPC so reset the facing to a diagonal up + _npcFacing = (_npcFacing == STOP_RIGHT) ? STOP_UPRIGHT : STOP_UPLEFT; + } else if (people[HOLMES]._position.y > (_position.y + 10 * FIXED_INT_MULTIPLIER)) { + // Holmes is below the NPC so reset the facing to a diagonal down + _npcFacing = (_npcFacing == STOP_RIGHT) ? STOP_DOWNRIGHT : STOP_DOWNLEFT; + } + + // See if we need to set the old_walk_sequence so the NPC will put his arms + // up if he turns another way + if (_sequenceNumber != _npcFacing) + _oldWalkSequence = _sequenceNumber; + + gotoStand(); + } + } else { + // Reset the look flag so the NPC won't face Holmes anymore + _lookHolmes = false; + + // See if the NPC is stopped or not. Don't do anything if he's moving + if (!_walkCount) { + // If there is no new command, reset the path back to the beginning + if (!_npcPath[_npcIndex]) + _npcIndex = 0; + + // The NPC is stopped and any pause he was doing is done. We can now see what + // the next command in the NPC path is. + + // Scan Past any NPC Path Labels since they do nothing except mark places for If's and Goto's + while (_npcPath[_npcIndex] == NPCPATH_PATH_LABEL) + _npcIndex += 2; + + if (_npcPath[_npcIndex]) { + _npcFacing = -1; + + switch (_npcPath[_npcIndex]) { + case NPCPATH_SET_DEST: { + // Set the NPC's new destination + int xp = (_npcPath[_npcIndex + 1] - 1) * 256 + _npcPath[_npcIndex + 2] - 1; + if (xp > 16384) + xp = -1 * (xp - 16384); + _walkDest.x = xp; + _walkDest.y = (_npcPath[_npcIndex + 3] - 1) * 256 + _npcPath[_npcIndex + 4] - 1; + _npcFacing = _npcPath[_npcIndex + 5] - 1; + + goAllTheWay(); + _npcIndex += 6; + break; + } + + case NPCPATH_PAUSE: + // Set the NPC to pause where he is + _npcPause = (_npcPath[_npcIndex + 1] - 1) * 256 + _npcPath[_npcIndex + 2] - 1; + _npcIndex += 3; + break; + + case NPCPATH_SET_TALK_FILE: { + // Set the NPC's Talk File to use if Holmes talks to them + ++_npcIndex; + + _npcName = ""; + for (int idx = 0; idx < 8; ++idx) { + if (_npcPath[_npcIndex + idx] != '~') + _npcName += _npcPath[_npcIndex + idx]; + else + break; + } + + _npcIndex += 8; + break; + } + + case NPCPATH_CALL_TALK_FILE: { + // Call a Talk File + ++_npcIndex; + + Common::String name; + for (int idx = 0; idx < 8; ++idx) { + if (_npcPath[_npcIndex + idx] != '~') + name += _npcPath[_npcIndex + idx]; + else + break; + } + + _npcIndex += 8; + talk.talkTo(name); + break; + } + + case NPCPATH_TAKE_NOTES: + // Set the NPC to Pause where he is and set his frame sequences + // so he takes notes while he's paused + _npcPause = (_npcPath[_npcIndex + 1] - 1) * 256 + _npcPath[_npcIndex + 2] - 1; + _npcIndex += 3; + break; + + case NPCPATH_FACE_HOLMES: + // Set the NPC to Pause where he is and set his look flag so he will always face Holmes + // while he is paused + _npcPause = (_npcPath[_npcIndex + 1] - 1) * 256 + _npcPath[_npcIndex + 2] - 1; + _lookHolmes = true; + _npcIndex += 3; + break; + + //case NPCPATH_PATH_LABEL: // No implementation needed here + + case NPCPATH_GOTO_LABEL: { + // Goto NPC Path Label + int label = _npcPath[_npcIndex + 1]; + _npcIndex = 0; + + // Scan through NPC path data to find the label + bool found = false; + while (!found) { + switch (_npcPath[_npcIndex]) { + case NPCPATH_SET_DEST: + _npcIndex += 6; + break; + case NPCPATH_PAUSE: + case NPCPATH_TAKE_NOTES: + case NPCPATH_FACE_HOLMES: + _npcIndex += 3; + break; + case NPCPATH_SET_TALK_FILE: + case NPCPATH_CALL_TALK_FILE: + _npcIndex += 8; + break; + case NPCPATH_PATH_LABEL: + if (_npcPath[_npcIndex + 1] == label) + found = true; + _npcIndex += 2; + break; + case NPCPATH_GOTO_LABEL: + _npcIndex += 2; + break; + case NPCPATH_IFFLAG_GOTO_LABEL: + _npcIndex += 4; + break; + } + } + break; + } + + case NPCPATH_IFFLAG_GOTO_LABEL: { + // If FLAG then Goto Label + int flag = (_npcPath[_npcIndex + 1] - 1) * 256 + _npcPath[_npcIndex + 2] - 1 - (_npcPath[_npcIndex + 2] == 1 ? 1 : 0); + + // Set the value the flag should be for the if statement to succeed + bool flagVal = flag < 16384; + + int label = _npcPath[_npcIndex + 3]; + _npcIndex += 4; + + // If the flag is set Correctly, move the NPC Index to the given label + if (_vm->readFlags(flag & 16383) == flagVal) { + _npcIndex = 0; + bool found = false; + while (!found) + { + switch (_npcPath[_npcIndex]) + { + case NPCPATH_SET_DEST: + _npcIndex += 6; + break; + case NPCPATH_PAUSE: + case NPCPATH_TAKE_NOTES: + case NPCPATH_FACE_HOLMES: + _npcIndex += 3; + break; + case NPCPATH_SET_TALK_FILE: + case NPCPATH_CALL_TALK_FILE: + _npcIndex += 8; + break; + case NPCPATH_PATH_LABEL: + if (_npcPath[_npcIndex + 1] == label) + found = true; + _npcIndex += 2; + break; + case NPCPATH_GOTO_LABEL: + _npcIndex += 2; + break; + case NPCPATH_IFFLAG_GOTO_LABEL: + _npcIndex += 4; + break; + } + } + } + + break; + } + + default: + break; + } + } + } + } +} + +void TattooPerson::pushNPCPath() { + assert(_pathStack.size() < 2); + SavedNPCPath savedPath(_npcPath, _npcIndex, _npcPause, _position, _sequenceNumber, _lookHolmes); + _pathStack.push(savedPath); +} + +void TattooPerson::pullNPCPath() { + // Pop the stack entry and restore the fields + SavedNPCPath path = _pathStack.pop(); + Common::copy(&path._path[0], &path._path[MAX_NPC_PATH], &_npcPath[0]); + _npcIndex = path._npcIndex; + _npcPause = path._npcPause; + + // Handle the first case if the NPC was paused + if (_npcPause) { + _walkDest = Common::Point(path._walkDest.x / FIXED_INT_MULTIPLIER, path._walkDest.y / FIXED_INT_MULTIPLIER); + _npcFacing = path._npcFacing; + _lookHolmes = path._lookHolmes; + + // See if the NPC was moved + if (_walkDest.x != (_position.x / FIXED_INT_MULTIPLIER) || + _walkDest.y != (_position.y / FIXED_INT_MULTIPLIER)) { + goAllTheWay(); + _npcPause = 0; + _npcIndex -= 3; + } else { + // See if we need to set the old walk sequence so the NPC will put his arms up if he turns another way + if (_npcFacing != _sequenceNumber) + _oldWalkSequence = _sequenceNumber; + + gotoStand(); + } + } else { + // Handle the second case if the NPC was in motion + _npcIndex -= 6; + } +} + +Common::Point TattooPerson::getSourcePoint() const { + TattooScene &scene = *(TattooScene *)_vm->_scene; + int scaleVal = scene.getScaleVal(_position); + + return Common::Point(_position.x / FIXED_INT_MULTIPLIER + _imageFrame->sDrawXSize(scaleVal) / 2, + _position.y / FIXED_INT_MULTIPLIER); +} + +void TattooPerson::setObjTalkSequence(int seq) { + assert(seq != -1 && _type == CHARACTER); + + if (_seqTo) { + // reset to previous value + _walkSequences[_sequenceNumber]._sequences[_frameNumber] = _seqTo; + _seqTo = 0; + } + + _sequenceNumber = _gotoSeq; + _frameNumber = 0; + checkWalkGraphics(); +} + +void TattooPerson::checkWalkGraphics() { + People &people = *_vm->_people; + + if (_images == nullptr) { + freeAltGraphics(); + return; + } + + Common::String filename = Common::String::format("%s.vgs", _walkSequences[_sequenceNumber]._vgsName.c_str()); + + // Set the adjust depending on if we have to fine tune the x position of this particular graphic + _adjust.x = _adjust.y = 0; + + for (int idx = 0; idx < NUM_ADJUSTED_WALKS; ++idx) { + if (!scumm_strnicmp(_walkSequences[_sequenceNumber]._vgsName.c_str(), ADJUST_WALKS[idx]._vgsName, + strlen(ADJUST_WALKS[idx]._vgsName))) { + if (_walkSequences[_sequenceNumber]._horizFlip) + _adjust.x = ADJUST_WALKS[idx]._flipXAdjust; + else + _adjust.x = ADJUST_WALKS[idx]._xAdjust; + + _adjust.y = ADJUST_WALKS[idx]._yAdjust; + break; + } + } + + // See if we're already using Alternate Graphics + if (_altSeq) { + // See if the VGS file called for is different than the alternate graphics already loaded + if (!_walkSequences[_sequenceNumber]._vgsName.compareToIgnoreCase(_walkSequences[_altSeq - 1]._vgsName)) { + // Different AltGraphics, Free the old ones + freeAltGraphics(); + } + } + + // If there is no Alternate Sequence set, see if we need to load a new one + if (!_altSeq) { + int npcNum = -1; + // Find which NPC this is so we can check the name of the graphics loaded + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (this == &people[idx]) { + npcNum = idx; + break; + } + } + + if (npcNum != -1) { + // See if the VGS file called for is different than the main graphics which are already loaded + if (filename.compareToIgnoreCase(people[npcNum]._walkVGSName) != 0) { + // See if this is one of the more used Walk Graphics stored in WALK.LIB + for (int idx = 0; idx < NUM_IN_WALK_LIB; ++idx) { + if (!scumm_stricmp(filename.c_str(), WALK_LIB_NAMES[idx])) { + people._useWalkLib = true; + break; + } + } + + _altImages = new ImageFile(filename); + people._useWalkLib = false; + + _altSeq = _sequenceNumber + 1; + } + } + } + + // If this is a different seqeunce from the current sequence, reset the appropriate variables + if (_sequences != &_walkSequences[_sequenceNumber]._sequences[0]) { + _seqTo = _seqCounter = _seqCounter2 = _seqStack = _startSeq = 0; + _sequences = &_walkSequences[_sequenceNumber]._sequences[0]; + _seqSize = _walkSequences[_sequenceNumber]._sequences.size(); + } + + setImageFrame(); +} + +void TattooPerson::synchronize(Serializer &s) { + s.syncAsSint32LE(_position.x); + s.syncAsSint32LE(_position.y); + s.syncAsSint16LE(_sequenceNumber); + s.syncAsSint16LE(_type); + s.syncString(_walkVGSName); + s.syncString(_description); + s.syncString(_examine); + + // NPC specific properties + s.syncBytes(&_npcPath[0], MAX_NPC_PATH); + s.syncString(_npcName); + s.syncAsSint32LE(_npcPause); + s.syncAsByte(_lookHolmes); + s.syncAsByte(_updateNPCPath); + + // Walk to list + uint count = _walkTo.size(); + s.syncAsUint16LE(count); + if (s.isLoading()) { + // Load path + for (uint idx = 0; idx < count; ++idx) { + int xp = 0, yp = 0; + s.syncAsSint16LE(xp); + s.syncAsSint16LE(yp); + _walkTo.push(Common::Point(xp, yp)); + } + } else { + // Save path + Common::Array<Common::Point> path; + + // Save the points of the path + for (uint idx = 0; idx < count; ++idx) { + Common::Point pt = _walkTo.pop(); + s.syncAsSint16LE(pt.x); + s.syncAsSint16LE(pt.y); + path.push_back(pt); + } + + // Re-add the pending points back to the _walkTo queue + for (uint idx = 0; idx < count; ++idx) + _walkTo.push(path[idx]); + } + + // Verbs + for (int idx = 0; idx < 2; ++idx) + _use[idx].synchronize(s); +} + +void TattooPerson::walkHolmesToNPC() { + Events &events = *_vm->_events; + TattooScene &scene = *(TattooScene *)_vm->_scene; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + TattooPerson &holmes = people[HOLMES]; + int facing; + + // If the NPC is moving, stop him at his current position + if (_walkCount) { + // Reset the facing so the NPC will stop facing the direction he was going, + // rather than the direction he was supposed to when he finished wlaking + _npcFacing = -1; + gotoStand(); + } + + int scaleVal = scene.getScaleVal(_position); + ImageFrame &imgFrame = (*holmes._images)[0]; + + // Clear the path variables + memset(_npcPath, 0, 100); + + // Set the NPC path so he pauses for 250 while looking at Holmes + _npcPath[0] = 6; + _npcPath[1] = 1; + _npcPath[2] = 251; + _npcIndex = 0; + _npcPause = 250; + _lookHolmes = true; + + // See where Holmes is with respect to the NPC (x coords) + if (holmes._position.x < _position.x) { + holmes._walkDest.x = MAX(_position.x / FIXED_INT_MULTIPLIER - imgFrame.sDrawXSize(scaleVal), 0); + } else { + holmes._walkDest.x = MIN(_position.x / FIXED_INT_MULTIPLIER + imgFrame.sDrawXSize(scaleVal) * 2, + screen._backBuffer1.w() - 1); + } + + // See where Holmes is with respect to the NPC (y coords) + if (holmes._position.y < (_position.y - imgFrame.sDrawXSize(scaleVal) * 500)) { + holmes._walkDest.y = MAX(_position.y / FIXED_INT_MULTIPLIER - imgFrame.sDrawXSize(scaleVal) / 2, 0); + } else { + if (holmes._position.y > (_position.y + imgFrame.sDrawXSize(scaleVal) * 500)) { + // Holmes is below the NPC + holmes._walkDest.y = MIN(_position.y / FIXED_INT_MULTIPLIER + imgFrame.sDrawXSize(scaleVal) / 2, + SHERLOCK_SCREEN_HEIGHT - 1); + } else { + // Holmes is roughly on the same Y as the NPC + holmes._walkDest.y = _position.y / FIXED_INT_MULTIPLIER; + } + } + + events.setCursor(WAIT); + + _walkDest.x += 10; + people._allowWalkAbort = true; + holmes.goAllTheWay(); + + // Do doBgAnim should be called over and over until walk is done + do { + events.wait(1); + scene.doBgAnim(); + } while (holmes._walkCount); + + if (!talk._talkToAbort) { + // Setup correct direction for Holmes to face + + // See where Holmes is with respect to the NPC (x coords) + facing = (holmes._position.x < _position.x) ? STOP_RIGHT : STOP_LEFT; + + // See where Holmes is with respect to the NPC (y coords) + if (holmes._position.y < (_position.y - (10 * FIXED_INT_MULTIPLIER))) { + // Holmes is above the NPC. Reset the facing to the diagonal downs + facing = (facing == STOP_RIGHT) ? STOP_DOWNRIGHT : STOP_DOWNLEFT; + } else { + if (holmes._position.y > (_position.y + 10 * FIXED_INT_MULTIPLIER)) { + // Holmes is below the NPC. Reset the facing to the diagonal ups + facing = (facing == STOP_RIGHT) ? STOP_UPRIGHT : STOP_UPLEFT; + } + } + + holmes._sequenceNumber = facing; + holmes.gotoStand(); + + events.setCursor(ARROW); + } +} + +/*----------------------------------------------------------------*/ + +TattooPeople::TattooPeople(SherlockEngine *vm) : People(vm) { + for (int idx = 0; idx < 6; ++idx) + _data.push_back(new TattooPerson()); +} + +void TattooPeople::setListenSequence(int speaker, int sequenceNum) { + Scene &scene = *_vm->_scene; + + // If no speaker is specified, then nothing needs to be done + if (speaker == -1) + return; + + int objNum = findSpeaker(speaker); + if (objNum < 256 && objNum != -1) { + // See if the Object has to wait for an Abort Talk Code + Object &obj = scene._bgShapes[objNum]; + if (obj.hasAborts()) + obj._gotoSeq = sequenceNum; + else + obj.setObjTalkSequence(sequenceNum); + } else if (objNum != -1) { + objNum -= 256; + TattooPerson &person = (*this)[objNum]; + + int newDir = person._sequenceNumber; + switch (person._sequenceNumber) { + case WALK_UP: + case STOP_UP: + case WALK_UPRIGHT: + case STOP_UPRIGHT: + case TALK_UPRIGHT: + case LISTEN_UPRIGHT: + newDir = LISTEN_UPRIGHT; + break; + case WALK_RIGHT: + case STOP_RIGHT: + case TALK_RIGHT: + case LISTEN_RIGHT: + newDir = LISTEN_RIGHT; + break; + case WALK_DOWNRIGHT: + case STOP_DOWNRIGHT: + case TALK_DOWNRIGHT: + case LISTEN_DOWNRIGHT: + newDir = LISTEN_DOWNRIGHT; + break; + case WALK_DOWN: + case STOP_DOWN: + case WALK_DOWNLEFT: + case STOP_DOWNLEFT: + case TALK_DOWNLEFT: + case LISTEN_DOWNLEFT: + newDir = LISTEN_DOWNLEFT; + break; + case WALK_LEFT: + case STOP_LEFT: + case TALK_LEFT: + case LISTEN_LEFT: + newDir = LISTEN_LEFT; + break; + case WALK_UPLEFT: + case STOP_UPLEFT: + case TALK_UPLEFT: + case LISTEN_UPLEFT: + newDir = LISTEN_UPLEFT; + break; + + default: + break; + } + + // See if the NPC's Seq has to wait for an Abort Talk Code + if (person.hasAborts()) { + person._gotoSeq = newDir; + } else { + if (person._seqTo) { + // Reset to previous value + person._walkSequences[person._sequenceNumber]._sequences[person._frameNumber] = person._seqTo; + person._seqTo = 0; + } + + person._sequenceNumber = newDir; + person._frameNumber = 0; + person.checkWalkGraphics(); + } + } +} + +void TattooPeople::setTalkSequence(int speaker, int sequenceNum) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + Scene &scene = *_vm->_scene; + TattooTalk &talk = *(TattooTalk *)_vm->_talk; + + // If no speaker is specified, then nothing needs to be done + if (speaker == -1) + return; + + int objNum = people.findSpeaker(speaker); + if (objNum != -1 && objNum < 256) { + Object &obj = scene._bgShapes[objNum]; + + // See if the Object has to wait for an Abort Talk Code + if (obj.hasAborts()) { + talk.pushTalkSequence(&obj); + obj._gotoSeq = sequenceNum; + } + else { + obj.setObjTalkSequence(sequenceNum); + } + } + else if (objNum != -1) { + objNum -= 256; + TattooPerson &person = people[objNum]; + int newDir = person._sequenceNumber; + + switch (newDir) { + case WALK_UP: + case STOP_UP: + case WALK_UPRIGHT: + case STOP_UPRIGHT: + case TALK_UPRIGHT: + case LISTEN_UPRIGHT: + newDir = TALK_UPRIGHT; + break; + case WALK_RIGHT: + case STOP_RIGHT: + case TALK_RIGHT: + case LISTEN_RIGHT: + newDir = TALK_RIGHT; + break; + case WALK_DOWNRIGHT: + case STOP_DOWNRIGHT: + case TALK_DOWNRIGHT: + case LISTEN_DOWNRIGHT: + newDir = TALK_DOWNRIGHT; + break; + case WALK_DOWN: + case STOP_DOWN: + case WALK_DOWNLEFT: + case STOP_DOWNLEFT: + case TALK_DOWNLEFT: + case LISTEN_DOWNLEFT: + newDir = TALK_DOWNLEFT; + break; + case WALK_LEFT: + case STOP_LEFT: + case TALK_LEFT: + case LISTEN_LEFT: + newDir = TALK_LEFT; + break; + case WALK_UPLEFT: + case STOP_UPLEFT: + case TALK_UPLEFT: + case LISTEN_UPLEFT: + newDir = TALK_UPLEFT; + break; + default: + break; + } + + // See if the NPC's sequence has to wait for an Abort Talk Code + if (person.hasAborts()) { + person._gotoSeq = newDir; + } else { + if (person._seqTo) { + // Reset to previous value + person._walkSequences[person._sequenceNumber]._sequences[person._frameNumber] = person._seqTo; + person._seqTo = 0; + } + + person._sequenceNumber = newDir; + person._frameNumber = 0; + person.checkWalkGraphics(); + } + } +} + + +int TattooPeople::findSpeaker(int speaker) { + int result = People::findSpeaker(speaker); + const char *portrait = _characters[speaker]._portrait; + + // Fallback that Rose Tattoo uses if no speaker was found + if (result == -1) { + bool flag = _vm->readFlags(FLAG_PLAYER_IS_HOLMES); + + if (_data[HOLMES]->_type == CHARACTER && ((speaker == HOLMES && flag) || (speaker == WATSON && !flag))) + return -1; + + for (uint idx = 1; idx < _data.size(); ++idx) { + TattooPerson &p = (*this)[idx]; + + if (p._type == CHARACTER) { + Common::String name(p._name.c_str(), p._name.c_str() + 4); + + if (name.equalsIgnoreCase(portrait) && p._npcName[4] >= '0' && p._npcName[4] <= '9') + return idx + 256; + } + } + } + + return -1; +} + +void TattooPeople::synchronize(Serializer &s) { + s.syncAsByte(_holmesOn); + + for (uint idx = 0; idx < _data.size(); ++idx) + (*this)[idx].synchronize(s); + + s.syncAsSint16LE(_holmesQuotient); + + if (s.isLoading()) { + _savedPos.x = _data[HOLMES]->_position.x; + _savedPos.y = _data[HOLMES]->_position.y; + _savedPos._facing = _data[HOLMES]->_sequenceNumber; + } +} + +bool TattooPeople::loadWalk() { + Resources &res = *_vm->_res; + bool result = false; + + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + Person &person = *_data[idx]; + + if (!person._walkLoaded && (person._type == CHARACTER || person._type == HIDDEN_CHARACTER)) { + if (person._type == HIDDEN_CHARACTER) + person._type = INVALID; + + // See if this is one of the more used Walk Graphics stored in WALK.LIB + for (int libNum = 0; libNum < NUM_IN_WALK_LIB; ++libNum) { + if (!person._walkVGSName.compareToIgnoreCase(WALK_LIB_NAMES[libNum])) { + _useWalkLib = true; + break; + } + } + + // Load the images for the character + person._images = new ImageFile(person._walkVGSName, false); + person._maxFrames = person._images->size(); + + // Load walk sequence data + Common::String fname = Common::String(person._walkVGSName.c_str(), strchr(person._walkVGSName.c_str(), '.')); + fname += ".SEQ"; + + // Load the walk sequence data + Common::SeekableReadStream *stream = res.load(fname, _useWalkLib ? "walk.lib" : "vgs.lib"); + + person._walkSequences.resize(stream->readByte()); + + for (uint seqNum = 0; seqNum < person._walkSequences.size(); ++seqNum) + person._walkSequences[seqNum].load(*stream); + + // Close the sequences resource + delete stream; + _useWalkLib = false; + + person._sequences = &person._walkSequences[person._sequenceNumber]._sequences[0]; + person._seqSize = person._walkSequences[person._sequenceNumber]._sequences.size(); + person._frameNumber = 0; + person.setImageFrame(); + + // Set the stop Frames pointers + for (int dirNum = 0; dirNum < 8; ++dirNum) { + int count = 0; + while (person._walkSequences[dirNum + 8][count] != 0) + ++count; + count += 2; + count = person._walkSequences[dirNum + 8][count] - 1; + person._stopFrames[dirNum] = &(*person._images)[count]; + } + + result = true; + person._walkLoaded = true; + } else if (person._type != CHARACTER) { + person._walkLoaded = false; + } + } + + _forceWalkReload = false; + return result; +} + + +void TattooPeople::pullNPCPaths() { + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + TattooPerson &p = (*this)[idx]; + if (p._npcMoved) { + while (!p._pathStack.empty()) + p.pullNPCPath(); + } + } +} + +const Common::Point TattooPeople::restrictToZone(int zoneId, const Common::Point &destPos) { + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Common::Rect &r = scene._zones[zoneId]; + + if (destPos.x < 0 || destPos.x > screen._backBuffer1.w()) + return destPos; + else if (destPos.y < r.top && r.left < destPos.x && destPos.x < r.right) + return Common::Point(destPos.x, r.top); + else if (destPos.y > r.bottom && r.left < destPos.x && destPos.x < r.right) + return Common::Point(destPos.x, r.bottom); + else if (destPos.x < r.left && r.top < destPos.y && destPos.y < r.bottom) + return Common::Point(r.left, destPos.y); + else if (destPos.x > r.right && r.top < destPos.y && destPos.y < r.bottom) + return Common::Point(r.bottom, destPos.y); + + // Find which corner of the zone the point is closet to + if (destPos.x <= r.left) { + return Common::Point(r.left, (destPos.y <= r.top) ? r.top : r.bottom); + } else { + return Common::Point(r.right, (destPos.y <= r.top) ? r.top : r.bottom); + } +} + + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_people.h b/engines/sherlock/tattoo/tattoo_people.h new file mode 100644 index 0000000000..fb3f6e7628 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_people.h @@ -0,0 +1,270 @@ +/* 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_PEOPLE_H +#define SHERLOCK_TATTOO_PEOPLE_H + +#include "common/scummsys.h" +#include "common/stack.h" +#include "sherlock/people.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +// Animation sequence identifiers for characters +enum TattooSequences { + // Walk Sequences Numbers for NPCs + WALK_UP = 0, + WALK_UPRIGHT = 1, + WALK_RIGHT = 2, + WALK_DOWNRIGHT = 3, + WALK_DOWN = 4, + WALK_DOWNLEFT = 5, + WALK_LEFT = 6, + WALK_UPLEFT = 7, + + // Stop Sequences Numbers for NPCs + STOP_UP = 8, + STOP_UPRIGHT = 9, + STOP_RIGHT = 10, + STOP_DOWNRIGHT = 11, + STOP_DOWN = 12, + STOP_DOWNLEFT = 13, + STOP_LEFT = 14, + STOP_UPLEFT = 15, + + // NPC Talk Sequence Numbers + TALK_UPRIGHT = 16, + TALK_RIGHT = 17, + TALK_DOWNRIGHT = 18, + TALK_DOWNLEFT = 19, + TALK_LEFT = 20, + TALK_UPLEFT = 21, + + // NPC Listen Sequence Numbers + LISTEN_UPRIGHT = 22, + LISTEN_RIGHT = 23, + LISTEN_DOWNRIGHT = 24, + LISTEN_DOWNLEFT = 25, + LISTEN_LEFT = 26, + LISTEN_UPLEFT = 27 +}; + +enum NpcPath { + NPCPATH_SET_DEST = 1, + NPCPATH_PAUSE = 2, + NPCPATH_SET_TALK_FILE = 3, + NPCPATH_CALL_TALK_FILE = 4, + NPCPATH_TAKE_NOTES = 5, + NPCPATH_FACE_HOLMES = 6, + NPCPATH_PATH_LABEL = 7, + NPCPATH_GOTO_LABEL = 8, + NPCPATH_IFFLAG_GOTO_LABEL = 9 +}; + +struct SavedNPCPath { + byte _path[MAX_NPC_PATH]; + int _npcIndex; + int _npcPause; + Common::Point _walkDest; + int _npcFacing; + bool _lookHolmes; + + SavedNPCPath(); + SavedNPCPath(byte path[MAX_NPC_PATH], int npcIndex, int npcPause, const Common::Point &walkDest, + int npcFacing, bool lookHolmes); +}; + +class TattooPerson: public Person { +private: + Point32 _nextDest; +private: + bool checkCollision() const; + + /** + * Free the alternate graphics used by NPCs + */ + void freeAltGraphics(); +protected: + /** + * Get the source position for a character potentially affected by scaling + */ + virtual Common::Point getSourcePoint() const; +public: + Common::Stack<SavedNPCPath> _pathStack; + int _npcIndex; + int _npcPause; + byte _npcPath[MAX_NPC_PATH]; + bool _npcMoved; + int _npcFacing; + bool _resetNPCPath; + int _savedNpcSequence; + int _savedNpcFrame; + int _tempX; + int _tempScaleVal; + bool _updateNPCPath; + bool _lookHolmes; +public: + TattooPerson(); + virtual ~TattooPerson() {} + + /** + * Clear the NPC related data + */ + void clearNPC(); + + /** + * Called from doBgAnim to move NPCs along any set paths. If an NPC is paused in his path, + * he will remain paused until his pause timer runs out. If he is walking somewhere, + * he will continue walking there until he reaches the dest position. When an NPC stops moving, + * the next element of his path is processed. + * + * The path is an array of bytes with control codes followed by their parameters as needed. + */ + void updateNPC(); + + /** + * Push the NPC's path data onto the path stack for when a talk file moves the NPC that + * has some control codes. + */ + void pushNPCPath(); + + /** + * Pull an NPC's path data that has been previously saved on the path stack for that character. + * There are two possibilities for when the NPC was interrupted, and both are handled differently: + * 1) The NPC was paused at a position + * If the NPC didn't move, we can just restore his pause counter and exit. But if he did move, + * he must return to that position, and the path index must be reset to the pause he was executing. + * This means that the index must be decremented by 3 + * 2) The NPC was in route to a position + * He must be set to walk to that position again. This is done by moving the path index + * so that it points to the code that set the NPC walking there in the first place. + * The regular calls to updateNPC will handle the rest + */ + void pullNPCPath(); + + /** + * Checks a sprite associated with an NPC to see if the frame sequence specified + * in the sequence number uses alternate graphics, and if so if they need to be loaded + */ + void checkWalkGraphics(); + + /** + * Synchronize the data for a savegame + */ + void synchronize(Serializer &s); + + /** + * This adjusts the sprites position, as well as it's animation sequence: + */ + virtual void adjustSprite(); + + /** + * Bring a moving character to a standing position + */ + virtual void gotoStand(); + + /** + * Set the variables for moving a character from one poisition to another + * in a straight line + */ + virtual void setWalking(); + + /** + * Walk to the co-ordinates passed, and then face the given direction + */ + virtual void walkToCoords(const Point32 &destPos, int destDir); + + /** + * Adjusts the frame and sequence variables of a sprite that corresponds to the current speaker + * so that it points to the beginning of the sequence number's talk sequence in the object's + * sequence buffer + * @param seq Which sequence to use (if there's more than 1) + * @remarks 1: First talk seq, 2: second talk seq, etc. + */ + virtual void setObjTalkSequence(int seq); + + /** + * Walk Holmes to the NPC + */ + void walkHolmesToNPC(); +}; + +class TattooPeople : public People { +public: + TattooPeople(SherlockEngine *vm); + virtual ~TattooPeople() {} + + TattooPerson &operator[](PeopleId id) { return *(TattooPerson *)_data[id]; } + TattooPerson &operator[](int idx) { return *(TattooPerson *)_data[idx]; } + + /** + * If the specified speaker is a background object, it will set it so that it uses + * the Listen Sequence (specified by the sequence number). If the current sequence + * has an Allow Talk Code in it, the _gotoSeq field will be set so that the object + * begins listening as soon as it hits the Allow Talk Code. If there is no Abort Code, + * the Listen Sequence will begin immediately. + * @param speaker Who is speaking + * @param sequenceNum Which listen sequence to use + */ + void setListenSequence(int speaker, int sequenceNum = 1); + + /** + * Restore any saved NPC walk path data from any of the NPCs + */ + void pullNPCPaths(); + + /** + * Finds the scene background object corresponding to a specified speaker + */ + virtual int findSpeaker(int speaker); + + /** + * Synchronize the data for a savegame + */ + virtual void synchronize(Serializer &s); + + /** + * Change the sequence of the scene background object associated with the specified speaker. + */ + virtual void setTalkSequence(int speaker, int sequenceNum = 1); + + /** + * Load the walking images for Sherlock + */ + virtual bool loadWalk(); + + /** + * Restrict passed point to zone using Sherlock's positioning rules + */ + virtual const Common::Point restrictToZone(int zoneId, const Common::Point &destPos); +}; + +} // End of namespace Scalpel + +} // End of namespace Sherlock + + +#endif diff --git a/engines/sherlock/tattoo/tattoo_resources.cpp b/engines/sherlock/tattoo/tattoo_resources.cpp new file mode 100644 index 0000000000..3be41e2650 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_resources.cpp @@ -0,0 +1,329 @@ +/* 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_resources.h" + +namespace Sherlock { + +namespace Tattoo { + +const char PORTRAITS[TATTOO_MAX_PEOPLE][5] = { + { "HOLM" }, // Sherlock Holmes + { "WATS" }, // Dr. Watson + { "HUDS" }, // Mrs. Hudson + { "FORB" }, // Stanley Forbes + { "MYCR" }, // Mycroft Holmes + { "WIGG" }, // Wiggins + { "BURN" }, // Police Constable Burns + { "TRIM" }, // Augustus Trimble + { "DALE" }, // Police Constable Daley + { "MATR" }, // Matron + { "GRAC" }, // Sister Grace + { "MCCA" }, // Preston McCabe + { "COLL" }, // Bob Colleran + { "JONA" }, // Jonas Rigby + { "ROAC" }, // Police Constable Roach + { "DEWA" }, // James Dewar + { "JERE" }, // Sergeant Jeremy Duncan + { "GREG" }, // Inspector Gregson + { "LEST" }, // Inspector Lestrade + { "NEED" }, // Jesse Needhem + { "FLEM" }, // Arthur Fleming + { "PRAT" }, // Mr. Thomas Pratt + { "TILL" }, // Mathilda (Tillie) Mason + { "RUSS" }, // Adrian Russell + { "WHIT" }, // Eldridge Whitney + { "HEPP" }, // Hepplethwaite + { "HORA" }, // Horace Silverbridge + { "SHER" }, // Old Sherman + { "VERN" }, // Maxwell Verner + { "REDD" }, // Millicent Redding + { "VIRG" }, // Virgil Silverbridge + { "GEOR" }, // George O'Keeffe + { "LAWT" }, // Lord Denys Lawton + { "JENK" }, // Jenkins + { "JOCK" }, // Jock Mahoney + { "BART" }, // Bartender + { "LADY" }, // Lady Cordelia Lockridge + { "PETT" }, // Pettigrew + { "FANS" }, // Sir Avery Fanshawe + { "HODG" }, // Hodgkins + { "WILB" }, // Wilbur "Birdy" Heywood + { "JACO" }, // Jacob Farthington + { "BLED" }, // Philip Bledsoe + { "FOWL" }, // Sidney Fowler + { "PROF" }, // Professor Theodore Totman + { "ROSE" }, // Rose Hinchem + { "TALL" }, // Tallboy + { "STIT" }, // Ethlebert "Stitch" Rumsey + { "FREE" }, // Charles Freedman + { "HEMM" }, // Nigel Hemmings + { "CART" }, // Fairfax Carter + { "WILH" }, // Wilhelm II + { "WACH" }, // Wachthund + { "WILS" }, // Jonathan Wilson + { "DAVE" }, // David Lloyd-Jones + { "HARG" }, // Edward Hargrove + { "MORI" }, // Professor James Moriarty + { "LASC" }, // The Lascar + { "PARR" }, // Parrot + { "SCAR" }, // Vincent Scarrett + { "ALEX" }, // Alexandra + { "QUEE" }, // Queen Victoria + { "JOHN" }, // John Brown + { "PAT1" }, // Patient #1 + { "PAT2" }, // Patient #2 + { "PATR" }, // Patron + { "QUEN" }, // Queen Victoria + { "WITE" }, // Patient in White + { "LUSH" }, // Lush + { "DRNK" }, // Drunk + { "PROS" }, // Prostitute + { "MUDL" }, // Mudlark + { "GRIN" }, // Grinder + { "BOUN" }, // Bouncer + { "RATC" }, // Agnes Ratchet + { "ALOY" }, // Aloysius Ratchet + { "REAL" }, // Real Estate Agent + { "CAND" }, // Candy Clerk + { "BEAD" }, // Beadle + { "PRUS" }, // Prussian + { "ROWB" }, // Mrs. Rowbottom + { "MSLJ" }, // Miss Lloyd-Jones + { "TPAT" }, // Tavern patron + { "USER" }, // User + { "TOBY" }, // Toby + { "STAT" }, // Stationer + { "CLRK" }, // Law Clerk + { "CLER" }, // Ministry Clerk + { "BATH" }, // Bather + { "MAID" }, // Maid + { "LADF" }, // Lady Fanshawe + { "SIDN" }, // Sidney Ratchet + { "BOYO" }, // Boy + { "PTR2" }, // Second Patron + { "BRIT" }, // Constable Brit + { "DROV" } // Wagon Driver +}; + +const char *const FRENCH_NAMES[TATTOO_MAX_PEOPLE] = { + "Sherlock Holmes", + "Dr. Watson", + "Mme. Hudson", + "Stanley Forbes", + "Mycroft Holmes", + "Wiggins", + "Sergent Burns", + "Augustus Trimble", + "Sergent Daley", + "Infirmi?re chef", + "Mme. Grace", + "Preston McCabe", + "Bob Colleran", + "Jonas Rigby", + "Sergent Roach", + "James Dewar", + "Sergent Jeremy Duncan", + "Inspecteur Gregson", + "Inspecteur Lestrade", + "Jesse Needhem", + "Arthur Fleming", + "M. Thomas Pratt", + "Mathilda (Tillie) Mason", + "Adrian Russell", + "Eldridge Whitney", + "Hepplethwaite", + "Horace Silverbridge", + "Sherman", + "Maxwell Verner", + "Millicent Redding", + "Virgil Silverbridge", + "George O'Keeffe", + "Lord Denys Lawton", + "Jenkins", + "Jock Mahoney", + "Serveur", + "Lady Cordelia Lockridge", + "Pettigrew", + "Sir Avery Fanshawe", + "Hodgkins", + "Wilbur \"Birdy\" Heywood", + "Jacob Farthington", + "Philip Bledsoe", + "Sidney Fowler", + "Professeur Theodore Totman", + "Rose Hinchem", + "Tallboy", + "Ethlebert \"Stitch\" Rumsey", + "Charles Freedman", + "Nigel Hemmings", + "Fairfax Carter", + "Wilhelm II", + "Wachthund", + "Jonathan Wilson", + "David Lloyd-Jones", + "Edward Hargrove", + "Misteray", + "Le Lascar", + "Oiseau", + "Vincent Scarrett", + "Alexandra", + "Queen Victoria", + "John Brown", + "Patient", + "Patient", + "Client", + "Queen Victoria", + "Patient en blanc", + "Ivrogne", + "Ivrogne", + "Belle femme", + "Mudlark", + "Broyeur", + "Videur", + "Agnes Ratchet", + "Aloysius Ratchet", + "Immobilier", + "Gar?on", + "Beadle", + "Prussian", + "Mme. Rowbottom", + "Mme Lloyd-Jones", + "Tavern Client", + "User", + "Toby", + "Papeterie", + "Law Clerc", + "Ministry Employ?", + "Clint du thermes", + "Bonne", + "Lady Fanshawe", + "Sidney Ratchet", + "Gar?on", + "Client", + "Sergent Brit", + "Wagon Driver" +}; + +const char *const ENGLISH_NAMES[TATTOO_MAX_PEOPLE] = { + "Sherlock Holmes", + "Dr. Watson", + "Mrs. Hudson", + "Stanley Forbes", + "Mycroft Holmes", + "Wiggins", + "Police Constable Burns", + "Augustus Trimble", + "Police Constable Daley", + "Matron", + "Sister Grace", + "Preston McCabe", + "Bob Colleran", + "Jonas Rigby", + "Police Constable Roach", + "James Dewar", + "Sergeant Jeremy Duncan", + "Inspector Gregson", + "Inspector Lestrade", + "Jesse Needhem", + "Arthur Fleming", + "Mr. Thomas Pratt", + "Mathilda (Tillie) Mason", + "Adrian Russell", + "Eldridge Whitney", + "Hepplethwaite", + "Horace Silverbridge", + "Old Sherman", + "Maxwell Verner", + "Millicent Redding", + "Virgil Silverbridge", + "George O'Keeffe", + "Lord Denys Lawton", + "Jenkins", + "Jock Mahoney", + "Bartender", + "Lady Cordelia Lockridge", + "Pettigrew", + "Sir Avery Fanshawe", + "Hodgkins", + "Wilbur \"Birdy\" Heywood", + "Jacob Farthington", + "Philip Bledsoe", + "Sidney Fowler", + "Professor Theodore Totman", + "Rose Hinchem", + "Tallboy", + "Ethlebert \"Stitch\" Rumsey", + "Charles Freedman", + "Nigel Hemmings", + "Fairfax Carter", + "Wilhelm II", + "Wachthund", + "Jonathan Wilson", + "David Lloyd-Jones", + "Edward Hargrove", + "Misteray", + "The Lascar", + "Parrot", + "Vincent Scarrett", + "Alexandra", + "Queen Victoria", + "John Brown", + "A Patient", + "A Patient", + "Patron", + "Queen Victoria", + "Patient in white", + "Lush", + "Drunk", + "Prostitute", + "Mudlark", + "Grinder", + "Bouncer", + "Agnes Ratchet", + "Aloysius Ratchet", + "Real Estate Agent", + "Candy Clerk", + "Beadle", + "Prussian", + "Mrs. Rowbottom", + "Miss Lloyd-Jones", + "Tavern patron", + "User", + "Toby", + "Stationer", + "Law Clerk", + "Ministry Clerk", + "Bather", + "Maid", + "Lady Fanshawe", + "Sidney Ratchet", + "Boy", + "Patron", + "Constable Brit", + "Wagon Driver" +}; + + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_resources.h b/engines/sherlock/tattoo/tattoo_resources.h new file mode 100644 index 0000000000..b706d90f2d --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_resources.h @@ -0,0 +1,42 @@ +/* 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_RESOURCES_H +#define SHERLOCK_TATTOO_RESOURCES_H + +#include "common/scummsys.h" + +namespace Sherlock { + +namespace Tattoo { + +#define TATTOO_MAX_PEOPLE 96 + +extern const char PORTRAITS[TATTOO_MAX_PEOPLE][5]; +extern const char *const FRENCH_NAMES[TATTOO_MAX_PEOPLE]; +extern const char *const ENGLISH_NAMES[TATTOO_MAX_PEOPLE]; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_scene.cpp b/engines/sherlock/tattoo/tattoo_scene.cpp new file mode 100644 index 0000000000..ba462ca255 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_scene.cpp @@ -0,0 +1,821 @@ +/* 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_scene.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/events.h" +#include "sherlock/people.h" + +namespace Sherlock { + +namespace Tattoo { + +struct ShapeEntry { + Object *_shape; + TattooPerson *_person; + bool _isAnimation; + int _yp; + + ShapeEntry(TattooPerson *person, int yp) : _shape(nullptr), _person(person), _yp(yp), _isAnimation(false) {} + ShapeEntry(Object *shape, int yp) : _shape(shape), _person(nullptr), _yp(yp), _isAnimation(false) {} + ShapeEntry(int yp) : _shape(nullptr), _person(nullptr), _yp(yp), _isAnimation(true) {} +}; +typedef Common::List<ShapeEntry> ShapeList; + +static bool sortImagesY(const ShapeEntry &s1, const ShapeEntry &s2) { + return s1._yp <= s2._yp; +} + +/*----------------------------------------------------------------*/ + +TattooScene::TattooScene(SherlockEngine *vm) : Scene(vm) { + _labTableScene = false; +} + +bool TattooScene::loadScene(const Common::String &filename) { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + Music &music = *_vm->_music; + Talk &talk = *_vm->_talk; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + // If we're going to the first game scene after the intro sequence, flag it as finished + if (vm._runningProlog && _currentScene == STARTING_GAME_SCENE) { + vm._runningProlog = false; + events.showCursor(); + talk._talkToAbort = false; + } + + // Check if it's a scene we need to keep trakc track of how many times we've visited + for (int idx = (int)_sceneTripCounters.size() - 1; idx >= 0; --idx) { + if (_sceneTripCounters[idx]._sceneNumber == _currentScene) { + if (--_sceneTripCounters[idx]._numTimes == 0) { + _vm->setFlags(_sceneTripCounters[idx]._flag); + _sceneTripCounters.remove_at(idx); + } + } + } + + // Set the NPC paths for the scene + setNPCPath(0); + + // Handle loading music for the scene + if (music._musicOn) { + if (talk._scriptMoreFlag != 1 && talk._scriptMoreFlag != 3) + music._nextSongName = Common::String::format("res%02d", _currentScene); + + // If it's a new song, then start it up + if (music._currentSongName.compareToIgnoreCase(music._nextSongName)) { + if (music.loadSong(music._nextSongName)) { + music.setMIDIVolume(music._musicVolume); + if (music._musicOn) + music.startSong(); + } + } + } + + bool result = Scene::loadScene(filename); + + if (_currentScene != STARTING_INTRO_SCENE) { + // Set the menu/ui mode and whether we're in a lab table close-up scene + _labTableScene = _currentScene > 91 && _currentScene < 100; + ui._menuMode = _labTableScene ? LAB_MODE : STD_MODE; + + if (_labTableScene) + ui._labWidget.summonWindow(); + } + + return result; +} + +void TattooScene::drawAllShapes() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + ShapeList shapeList; + + // Draw all objects and animations that are set to behind + screen.setDisplayBounds(Common::Rect(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT)); + + // Draw all active shapes which are behind the person + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE && obj._misc == BEHIND) { + if (obj._quickDraw && obj._scaleVal == SCALE_THRESHOLD) + screen._backBuffer1.blitFrom(*obj._imageFrame, obj._position); + else + screen._backBuffer1.transBlitFrom(*obj._imageFrame, obj._position, obj._flags & OBJ_FLIPPED, 0, obj._scaleVal); + } + } + + // Draw the animation if it is behind the person + if (_activeCAnim.active() && _activeCAnim._zPlacement == BEHIND) + screen._backBuffer1.transBlitFrom(_activeCAnim._imageFrame, _activeCAnim._position, + (_activeCAnim._flags & 4) >> 1, 0, _activeCAnim._scaleVal); + + screen.resetDisplayBounds(); + + // Queue drawing of all objects that are set to NORMAL. + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE && (obj._misc == NORMAL_BEHIND || obj._misc == NORMAL_FORWARD)) { + if (obj._scaleVal == SCALE_THRESHOLD) + shapeList.push_back(ShapeEntry(&obj, obj._position.y + obj._imageFrame->_offset.y + + obj._imageFrame->_height)); + else + shapeList.push_back(ShapeEntry(&obj, obj._position.y + obj._imageFrame->sDrawYOffset(obj._scaleVal) + + obj._imageFrame->sDrawYSize(obj._scaleVal))); + } + } + + // Queue drawing the animation if it is NORMAL and can fall in front of, or behind the people + if (_activeCAnim.active() && (_activeCAnim._zPlacement == NORMAL_BEHIND || _activeCAnim._zPlacement == NORMAL_FORWARD)) { + if (_activeCAnim._scaleVal == SCALE_THRESHOLD) + shapeList.push_back(ShapeEntry(_activeCAnim._position.y + _activeCAnim._imageFrame._offset.y + + _activeCAnim._imageFrame._height)); + else + shapeList.push_back(ShapeEntry(_activeCAnim._position.y + _activeCAnim._imageFrame.sDrawYOffset(_activeCAnim._scaleVal) + + _activeCAnim._imageFrame.sDrawYSize(_activeCAnim._scaleVal))); + } + + // Queue all active characters for drawing + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER && people[idx]._walkLoaded) + shapeList.push_back(ShapeEntry(&people[idx], people[idx]._position.y / FIXED_INT_MULTIPLIER)); + } + + // Sort the list + Common::sort(shapeList.begin(), shapeList.end(), sortImagesY); + + // Draw the list of shapes in order + for (ShapeList::iterator i = shapeList.begin(); i != shapeList.end(); ++i) { + ShapeEntry &se = *i; + + if (se._shape) { + // it's a bg shape + if (se._shape->_quickDraw && se._shape->_scaleVal == SCALE_THRESHOLD) + screen._backBuffer1.blitFrom(*se._shape->_imageFrame, se._shape->_position); + else + screen._backBuffer1.transBlitFrom(*se._shape->_imageFrame, se._shape->_position, + se._shape->_flags & OBJ_FLIPPED, 0, se._shape->_scaleVal); + } else if (se._isAnimation) { + // It's an active animation + screen._backBuffer1.transBlitFrom(_activeCAnim._imageFrame, _activeCAnim._position, + (_activeCAnim._flags & 4) >> 1, 0, _activeCAnim._scaleVal); + } else { + // Drawing person + TattooPerson &p = *se._person; + + p._tempX = p._position.x / FIXED_INT_MULTIPLIER; + p._tempScaleVal = getScaleVal(p._position); + Common::Point adjust = p._adjust; + + if (p._tempScaleVal == SCALE_THRESHOLD) { + p._tempX += adjust.x; + screen._backBuffer1.transBlitFrom(*p._imageFrame, Common::Point(p._tempX, p._position.y / FIXED_INT_MULTIPLIER + - p.frameHeight() - adjust.y), p._walkSequences[p._sequenceNumber]._horizFlip, 0, p._tempScaleVal); + } else { + if (adjust.x) { + if (!p._tempScaleVal) + ++p._tempScaleVal; + + if (p._tempScaleVal >= SCALE_THRESHOLD && adjust.x) + --adjust.x; + + adjust.x = adjust.x * SCALE_THRESHOLD / p._tempScaleVal; + + if (p._tempScaleVal >= SCALE_THRESHOLD) + ++adjust.x; + p._tempX += adjust.x; + } + + if (adjust.y) { + if (!p._tempScaleVal) + p._tempScaleVal++; + + if (p._tempScaleVal >= SCALE_THRESHOLD && adjust.y) + --adjust.y; + + adjust.y = adjust.y * SCALE_THRESHOLD / p._tempScaleVal; + + if (p._tempScaleVal >= SCALE_THRESHOLD) + ++adjust.y; + } + + screen._backBuffer1.transBlitFrom(*p._imageFrame, Common::Point(p._tempX, p._position.y / FIXED_INT_MULTIPLIER + - p._imageFrame->sDrawYSize(p._tempScaleVal) - adjust.y), p._walkSequences[p._sequenceNumber]._horizFlip, 0, p._tempScaleVal); + } + } + } + + // Draw all objects & canimations that are set to FORWARD. + // Draw all static and active shapes that are FORWARD + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE && obj._misc == FORWARD) { + if (obj._quickDraw && obj._scaleVal == SCALE_THRESHOLD) + screen._backBuffer1.blitFrom(*obj._imageFrame, obj._position); + else + screen._backBuffer1.transBlitFrom(*obj._imageFrame, obj._position, obj._flags & OBJ_FLIPPED, 0, obj._scaleVal); + } + } + + // Draw the canimation if it is set as FORWARD + if (_activeCAnim.active() && _activeCAnim._zPlacement == FORWARD) + screen._backBuffer1.transBlitFrom(_activeCAnim._imageFrame, _activeCAnim._position, (_activeCAnim._flags & 4) >> 1, 0, _activeCAnim._scaleVal); + + // Draw all NO_SHAPE shapes which have their flag bits clear + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + if (obj._type == NO_SHAPE && (obj._flags & 1) == 0) + screen._backBuffer1.fillRect(obj.getNoShapeBounds(), 15); + } +} + +void TattooScene::paletteLoaded() { + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + ui.setupBGArea(screen._cMap); + ui.initScrollVars(); +} + +void TattooScene::checkBgShapes() { + // Call the base scene method to handle bg shapes + Scene::checkBgShapes(); + + // Check for any active playing animation + if (_activeCAnim.active() && _activeCAnim._zPlacement != REMOVE) { + switch (_activeCAnim._flags & 3) { + case 0: + _activeCAnim._zPlacement = BEHIND; + break; + case 1: + _activeCAnim._zPlacement = ((_activeCAnim._position.y + _activeCAnim._imageFrame._frame.h - 1)) ? + NORMAL_FORWARD : NORMAL_BEHIND; + break; + case 2: + _activeCAnim._zPlacement = FORWARD; + break; + default: + break; + } + } +} + +void TattooScene::doBgAnimCheckCursor() { + Events &events = *_vm->_events; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + + // If we're in Look Mode, make sure the cursor is the magnifying glass + if (ui._menuMode == LOOK_MODE && events.getCursor() != MAGNIFY) + events.setCursor(MAGNIFY); + + // See if the mouse is over any of the arrow zones, and if so, change the cursor to the correct + // arrow cursor indicating the direcetion of the exit + if (events.getCursor() == ARROW || events.getCursor() >= EXIT_ZONES_START) { + CursorId cursorId = ARROW; + + if (ui._menuMode == STD_MODE && ui._arrowZone != -1 && _currentScene != 90) { + for (uint idx = 0; idx < _exits.size(); ++idx) { + Exit &exit = _exits[idx]; + if (exit.contains(mousePos)) + cursorId = (CursorId)(exit._image + EXIT_ZONES_START); + } + } + + events.setCursor(cursorId); + } else { + events.animateCursorIfNeeded(); + } +} + +void TattooScene::doBgAnim() { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + TattooUserInterface &ui = *((TattooUserInterface *)_vm->_ui); + + doBgAnimCheckCursor(); + + talk._talkToAbort = false; + + // Check the characters and sprites for updates + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) + people[idx].checkSprite(); + } + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + if (_bgShapes[idx]._type == ACTIVE_BG_SHAPE) + _bgShapes[idx].checkObject(); + } + + // If one of the objects has signalled a call to a talk file, to go to another scene, exit immediately + if (_goToScene != -1) + return; + + // Erase any affected background areas + ui.doBgAnimEraseBackground(); + + doBgAnimUpdateBgObjectsAndAnim(); + + doBgAnimDrawSprites(); + + ui.drawInterface(); + + if (vm._creditsActive) + vm.blitCredits(); + + if (!vm._fastMode) + events.wait(3); + + if (screen._flushScreen) { + screen.slamArea(screen._currentScroll.x, screen._currentScroll.y, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + screen._flushScreen = false; + } + + screen._flushScreen = false; + _doBgAnimDone = true; + ui._drawMenu = false; + + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._updateNPCPath) + people[idx].updateNPC(); + } +} + +void TattooScene::doBgAnimUpdateBgObjectsAndAnim() { + People &people = *_vm->_people; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + if (obj._type == ACTIVE_BG_SHAPE || obj._type == NO_SHAPE) + obj.adjustObject(); + } + + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) + people[idx].adjustSprite(); + } + + // Flag the bg shapes which need to be redrawn + checkBgShapes(); + drawAllShapes(); + + ui.drawMaskArea(true); +} + +void TattooScene::updateBackground() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + Scene::updateBackground(); + + ui.drawMaskArea(false); + + screen._flushScreen = true; + + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + TattooPerson &p = people[idx]; + + if (p._type != INVALID) { + if (_goToScene == -1 || _cAnim.size() == 0) { + if (p._type == REMOVE) { + screen.slamArea(p._oldPosition.x, p._oldPosition.y, p._oldSize.x, p._oldSize.y); + p._type = INVALID; + } else { + if (p._tempScaleVal == SCALE_THRESHOLD) { + screen.flushImage(p._imageFrame, Common::Point(p._tempX, p._position.y / FIXED_INT_MULTIPLIER + - p._imageFrame->_width), &p._oldPosition.x, &p._oldPosition.y, &p._oldSize.x, &p._oldSize.y); + } else { + int ts = p._imageFrame->sDrawYSize(p._tempScaleVal); + int ty = p._position.y / FIXED_INT_MULTIPLIER - ts; + screen.flushScaleImage(p._imageFrame, Common::Point(p._tempX, ty), + &p._oldPosition.x, &p._oldPosition.y, &p._oldSize.x, &p._oldSize.y, p._tempScaleVal); + } + } + } + } + } + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE || obj._type == REMOVE) { + if (_goToScene == -1) { + if (obj._scaleVal == SCALE_THRESHOLD) + screen.flushImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y); + else + screen.flushScaleImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y, obj._scaleVal); + + if (obj._type == REMOVE) + obj._type = INVALID; + } + } + } + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (_goToScene == -1) { + if (obj._type == NO_SHAPE && (obj._flags & 1) == 0) { + screen.slamRect(obj.getNoShapeBounds()); + screen.slamRect(obj.getOldBounds()); + } else if (obj._type == HIDE_SHAPE) { + if (obj._scaleVal == SCALE_THRESHOLD) + screen.flushImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y); + else + screen.flushScaleImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y, obj._scaleVal); + obj._type = HIDDEN; + } + } + } + + screen._flushScreen = false; +} + +void TattooScene::doBgAnimDrawSprites() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) { + TattooPerson &person = people[idx]; + + if (person._type != INVALID) { + if (_goToScene == -1 || _cAnim.size() == 0) { + if (person._type == REMOVE) { + screen.slamRect(person.getOldBounds()); + person._type = INVALID; + } else { + if (person._tempScaleVal == SCALE_THRESHOLD) { + screen.flushImage(person._imageFrame, Common::Point(person._tempX, person._position.y / FIXED_INT_MULTIPLIER + - person.frameHeight()), &person._oldPosition.x, &person._oldPosition.y, &person._oldSize.x, &person._oldSize.y); + } else { + int ts = person._imageFrame->sDrawYSize(person._tempScaleVal); + int ty = person._position.y / FIXED_INT_MULTIPLIER - ts; + screen.flushScaleImage(person._imageFrame, Common::Point(person._tempX, ty), + &person._oldPosition.x, &person._oldPosition.y, &person._oldSize.x, &person._oldSize.y, person._tempScaleVal); + } + } + } + } + } + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE || obj._type == REMOVE) { + if (_goToScene == -1) { + if (obj._scaleVal == SCALE_THRESHOLD) + screen.flushImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y); + else + screen.flushScaleImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y, obj._scaleVal); + + if (obj._type == REMOVE) + obj._type = INVALID; + } + } + } + + for (uint idx = 0; idx < _bgShapes.size(); ++idx) { + Object &obj = _bgShapes[idx]; + + if (_goToScene == -1) { + if (obj._type == NO_SHAPE && (obj._flags & 1) == 0) { + screen.slamRect(obj.getNoShapeBounds()); + screen.slamRect(obj.getOldBounds()); + } else if (obj._type == HIDE_SHAPE) { + if (obj._scaleVal == SCALE_THRESHOLD) + screen.flushImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y); + else + screen.flushScaleImage(obj._imageFrame, obj._position, &obj._oldPosition.x, &obj._oldPosition.y, + &obj._oldSize.x, &obj._oldSize.y, obj._scaleVal); + obj._type = HIDDEN; + } + } + } + + if (_activeCAnim.active() || _activeCAnim._zPlacement == REMOVE) { + if (_activeCAnim._zPlacement != REMOVE) { + screen.flushImage(&_activeCAnim._imageFrame, _activeCAnim._position, _activeCAnim._oldBounds, _activeCAnim._scaleVal); + } else { + screen.slamRect(_activeCAnim._removeBounds); + _activeCAnim._removeBounds = Common::Rect(0, 0, 0, 0); + _activeCAnim._zPlacement = -1; // Reset _zPlacement so we don't REMOVE again + } + } +} + +int TattooScene::getScaleVal(const Point32 &pt) { + bool found = false; + int result = SCALE_THRESHOLD; + Common::Point pos(pt.x / FIXED_INT_MULTIPLIER, pt.y / FIXED_INT_MULTIPLIER); + + for (uint idx = 0; idx < _scaleZones.size() && !found; ++idx) { + ScaleZone &sz = _scaleZones[idx]; + if (sz.contains(pos)) { + int n = (sz._bottomNumber - sz._topNumber) * 100 / sz.height() * (pos.y - sz.top) / 100 + sz._topNumber; + result = 25600L / n; + // CHECKME: Shouldn't we set 'found' at this place? + } + } + + // If it wasn't found, we may be off screen to the left or right, so find the scale zone + // that would apply to the y val passed in disregarding the x + if (!found) { + for (uint idx = 0; idx < _scaleZones.size() && !found; ++idx) { + ScaleZone &sz = _scaleZones[idx]; + if (pos.y >= sz.top && pos.y < sz.bottom) { + int n = (sz._bottomNumber - sz._topNumber) * 100 / sz.height() * (pos.y - sz.top) / 100 + sz._topNumber; + result = 25600L / n; + } + } + } + + return result; +} + +int TattooScene::startCAnim(int cAnimNum, int playRate) { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Resources &res = *_vm->_res; + Talk &talk = *_vm->_talk; + UserInterface &ui = *_vm->_ui; + + // Exit immediately if the anim number is out of range, or the anim doesn't have a position specified + if (cAnimNum < 0 || cAnimNum >= (int)_cAnim.size() || _cAnim[cAnimNum]._position.x == -1) + // Return out of range error + return -1; + + // Get the co-ordinates that the Player & NPC #1 must walk to and end on + CAnim &cAnim = _cAnim[cAnimNum]; + PositionFacing goto1 = cAnim._goto[0]; + PositionFacing goto2 = cAnim._goto[1]; + PositionFacing teleport1 = cAnim._teleport[0]; + PositionFacing teleport2 = cAnim._teleport[1]; + + // See if the Player must walk to a position before the animation starts + SpriteType savedPlayerType = people[HOLMES]._type; + if (goto1.x != -1 && people[HOLMES]._type == CHARACTER) { + if (people[HOLMES]._position != goto1) + people[HOLMES].walkToCoords(goto1, goto1._facing); + } + + if (talk._talkToAbort) + return 1; + + // See if NPC #1 must walk to a position before the animation starts + SpriteType savedNPCType = people[WATSON]._type; + if (goto2.x != -1 && people[WATSON]._type == CHARACTER) { + if (people[WATSON]._position != goto2) + people[WATSON].walkToCoords(goto2, goto2._facing); + } + + if (talk._talkToAbort) + return 1; + + // Turn the player (and NPC #1 if neccessary) off before running the canimation + if (teleport1.x != -1 && savedPlayerType == CHARACTER) + people[HOLMES]._type = REMOVE; + + if (teleport2.x != -1 && savedNPCType == CHARACTER) + people[WATSON]._type = REMOVE; + + if (ui._windowOpen) + ui.banishWindow(); + + //_activeCAnim._filesize = cAnim._size; + + // Open up the room resource file and get the data for the animation + Common::SeekableReadStream *stream = res.load(_roomFilename); + stream->seek(44 + cAnimNum * 4); + stream->seek(stream->readUint32LE()); + Common::SeekableReadStream *animStream = stream->readStream(cAnim._dataSize); + delete stream; + + // Set up the active animation + _activeCAnim._position = cAnim._position; + _activeCAnim._oldBounds = Common::Rect(0, 0, 0, 0); + _activeCAnim._flags = cAnim._flags; + _activeCAnim._scaleVal = cAnim._scaleVal; + _activeCAnim._zPlacement = 0; + + _activeCAnim.load(animStream, _compressed); + + while (_activeCAnim.active() && !_vm->shouldQuit()) { + // Get the next frame + _activeCAnim.getNextFrame(); + + // Draw the frame + doBgAnim(); + + // Check for Escape key being pressed to abort animation + events.pollEvents(); + if (events.kbHit()) { + Common::KeyState keyState = events.getKey(); + + if (keyState.keycode == Common::KEYCODE_ESCAPE && vm._runningProlog) { + _vm->setFlags(-76); + _vm->setFlags(396); + _goToScene = STARTING_GAME_SCENE; + talk._talkToAbort = true; + _activeCAnim.close(); + } + } + } + + // Turn the people back on + people[HOLMES]._type = savedPlayerType; + if (teleport2.x != -1) + people[WATSON]._type = savedNPCType; + + // Teleport the Player to the ending coordinates if necessary + if (teleport1.x != -1 && savedPlayerType == CHARACTER) { + people[HOLMES]._position = teleport1; + people[HOLMES]._sequenceNumber = teleport1._facing; + people[HOLMES].gotoStand(); + } + + // Teleport Watson to the ending coordinates if necessary + if (teleport2.x != -1 && savedNPCType == CHARACTER) { + people[WATSON]._position = teleport2; + people[WATSON]._sequenceNumber = teleport2._facing; + people[WATSON].gotoStand(); + } + + // Flag the Canimation to be cleared + _activeCAnim._zPlacement = REMOVE; + _activeCAnim._removeBounds = _activeCAnim._oldBounds; + + // Free up the animation + _activeCAnim.close(); + + return 1; +} + +void TattooScene::setNPCPath(int npc) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + SaveManager &saves = *_vm->_saves; + Talk &talk = *_vm->_talk; + + // Don't do initial scene setup if a savegame has just been loaded + if (saves._justLoaded) + return; + + people[npc].clearNPC(); + people[npc]._name = Common::String::format("WATS%.2dA", _currentScene); + + // If we're in the middle of a script that will continue once the scene is loaded, + // return without calling the path script + if (talk._scriptMoreFlag == 1 || talk._scriptMoreFlag == 3) + return; + + // Turn off all the NPCs, since the talk script will turn them back on as needed + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) + people[idx]._type = INVALID; + + // Call the path script for the scene + Common::String pathFile = Common::String::format("PATH%.2dA", _currentScene); + talk.talkTo(pathFile); +} + +int TattooScene::findBgShape(const Common::Point &pt) { + People &people = *_vm->_people; + + if (!_doBgAnimDone) + // New frame hasn't been drawn yet + return -1; + + int result = Scene::findBgShape(pt); + if (result == -1) { + if (_labTableScene) { + // Check for SOLID objects in the lab scene + 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 == SOLID) { + if (o.getNewBounds().contains(pt)) + return idx; + } + } + } + + // No shape found, so check whether a character is highlighted + for (int idx = 1; idx < MAX_CHARACTERS && result == -1; ++idx) { + Person &person = people[idx]; + + if (person._type == CHARACTER) { + int scaleVal = getScaleVal(person._position); + Common::Rect charRect; + + if (scaleVal == SCALE_THRESHOLD) + charRect = Common::Rect(person.frameWidth(), person.frameHeight()); + else + charRect = Common::Rect(person._imageFrame->sDrawXSize(scaleVal), person._imageFrame->sDrawYSize(scaleVal)); + charRect.moveTo(person._position.x / FIXED_INT_MULTIPLIER, person._position.y / FIXED_INT_MULTIPLIER + - charRect.height()); + + if (charRect.contains(pt)) + result = 1000 + idx; + } + } + } + + return result; +} + +void TattooScene::synchronize(Serializer &s) { + TattooEngine &vm = *(TattooEngine *)_vm; + Scene::synchronize(s); + + if (s.isLoading()) + vm._runningProlog = false; +} + +int TattooScene::closestZone(const Common::Point &pt) { + int zone = -1; + int dist = 9999; + int d; + + for (uint idx = 0; idx < _zones.size(); ++idx) { + Common::Rect &r = _zones[idx]; + + // Check the distance from the point to the center of the zone + d = ABS(r.left + (r.width() / 2) - pt.x) + ABS(r.top + (r.height() / 2) - pt.y); + if (d < dist) { + dist = d; + zone = idx; + } + + // Check the distance from the point to the upper left of the zone + d = ABS((int)(r.left - pt.x)) + ABS((int)(r.top - pt.y)); + if (d < dist) + { + dist = d; + zone = idx; + } + + // Check the distance from the point to the upper right of the zone + d = ABS(r.left + r.width() - pt.x) + ABS(r.top - pt.y); + if (d < dist) { + dist = d; + zone = idx; + } + + // Check the distance from the point to the lower left of the zone + d = ABS(r.left - pt.x) + ABS(r.top + r.height() - pt.y); + if (d < dist) { + dist = d; + zone = idx; + } + + // Check the distance from the point to the lower right of the zone + d = ABS(r.left + r.width() - pt.x) + ABS(r.top + r.height() - pt.y); + if (d < dist) { + dist = d; + zone = idx; + } + } + + return zone; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_scene.h b/engines/sherlock/tattoo/tattoo_scene.h new file mode 100644 index 0000000000..c432849bed --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_scene.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_TATTOO_SCENE_H +#define SHERLOCK_TATTOO_SCENE_H + +#include "common/scummsys.h" +#include "sherlock/scene.h" + +namespace Sherlock { + +namespace Tattoo { + +enum { + STARTING_GAME_SCENE = 1, STARTING_INTRO_SCENE = 91, OVERHEAD_MAP2 = 90, OVERHEAD_MAP = 100 +}; + +struct SceneTripEntry { + int _flag; + int _sceneNumber; + int _numTimes; + + SceneTripEntry() : _flag(0), _sceneNumber(0), _numTimes(0) {} + SceneTripEntry(int flag, int sceneNumber, int numTimes) : _flag(flag), + _sceneNumber(sceneNumber), _numTimes(numTimes) {} +}; + +class TattooScene : public Scene { +private: + void doBgAnimCheckCursor(); + + /** + * Update the background objects and canimations as part of doBgAnim + */ + void doBgAnimUpdateBgObjectsAndAnim(); + + void doBgAnimDrawSprites(); + + /** + * Resets the NPC path information when entering a new scene. + * @remarks The default talk file for the given NPC is set to WATS##A, where ## is + * the scene number being entered + */ + void setNPCPath(int npc); +protected: + /** + * Loads the data associated for a given scene. The room resource 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. + */ + virtual bool loadScene(const Common::String &filename); + + /** + * 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 + */ + virtual void checkBgShapes(); + + /** + * Draw all the shapes, people and NPCs in the correct order + */ + virtual void drawAllShapes(); + + /** + * Called by loadScene when the palette is loaded for Rose Tattoo + */ + virtual void paletteLoaded(); + + /** + * Synchronize the data for a savegame + */ + virtual void synchronize(Serializer &s); + + /** + * Returns the index of the closest zone to a given point. + */ + virtual int closestZone(const Common::Point &pt); +public: + StreamingImageFile _activeCAnim; + Common::Array<SceneTripEntry> _sceneTripCounters; + bool _labTableScene; +public: + TattooScene(SherlockEngine *vm); + + /** + * Returns the scale value for the passed co-ordinates. This is taken from the scene's + * scale zones, interpolating inbetween the top and bottom values of the zones as needed + */ + int getScaleVal(const Point32 &pt); + + /** + * Draw all objects and characters. + */ + virtual void doBgAnim(); + + /** + * Update the screen back buffer with all of the scene objects which need + * to be drawn + */ + virtual void updateBackground(); + + /** + * 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 + */ + virtual int startCAnim(int cAnimNum, int playRate = 1); + + /** + * Attempts to find a background shape within the passed bounds. If found, + * it will return the shape number, or -1 on failure. + */ + virtual int findBgShape(const Common::Point &pt); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_talk.cpp b/engines/sherlock/tattoo/tattoo_talk.cpp new file mode 100644 index 0000000000..dbeeaf8918 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_talk.cpp @@ -0,0 +1,882 @@ +/* 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_talk.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/sherlock.h" +#include "sherlock/screen.h" + +namespace Sherlock { + +namespace Tattoo { + +static const uint8 DIRECTION_CONVERSION[] = { + WALK_RIGHT, WALK_DOWN, WALK_LEFT, WALK_UP, STOP_RIGHT, STOP_DOWN, STOP_LEFT, STOP_UP, + WALK_UPRIGHT, WALK_DOWNRIGHT, WALK_UPLEFT, WALK_DOWNLEFT, STOP_UPRIGHT, STOP_UPLEFT, + STOP_DOWNRIGHT, STOP_DOWNLEFT +}; + +const byte TATTOO_OPCODES[] = { + 170, // OP_SWITCH_SPEAKER + 171, // OP_RUN_CANIMATION + 0, // OP_ASSIGN_PORTRAIT_LOCATION + 173, // OP_PAUSE + 0, // OP_REMOVE_PORTRAIT + 0, // OP_CLEAR_WINDOW + 176, // OP_ADJUST_OBJ_SEQUENCE + 177, // OP_WALK_HOlMES_TO_COORDS + 178, // OP_PAUSE_WITHOUT_CONTROL + 179, // OP_BANISH_WINDOW + 0, // OP_SUMMON_WINDOW + 181, // OP_SET_FLAG + 0, // OP_SFX_COMMAND + 183, // OP_TOGGLE_OBJECT + 184, // OP_STEALTH_MODE_ACTIVE + 0, // OP_IF_STATEMENT + 0, // OP_ELSE_STATEMENT + 0, // OP_END_IF_STATEMENT + 188, // OP_STEALTH_MODE_DEACTIVATE + 189, // OP_TURN_HOLMES_OFF + 190, // OP_TURN_HOLMES_ON + 191, // OP_GOTO_SCENE + 0, // OP_PLAY_PROLOGUE + 193, // OP_ADD_ITEM_TO_INVENTORY + 194, // OP_SET_OBJECT + 172, // OP_CALL_TALK_FILE + 0, // OP_MOVE_MOUSE + 0, // OP_DISPLAY_INFO_LINE + 0, // OP_CLEAR_INFO_LINE + 199, // OP_WALK_TO_CANIMATION + 200, // OP_REMOVE_ITEM_FROM_INVENTORY + 201, // OP_ENABLE_END_KEY + 202, // OP_DISABLE_END_KEY + 203, // OP_END_TEXT_WINDOW + 174, // OP_MOUSE_ON_OFF + 175, // OP_SET_WALK_CONTROL + 180, // OP_SET_TALK_SEQUENCE + 182, // OP_PLAY_SONG + 187, // OP_WALK_HOLMES_AND_NPC_TO_CANIM + 192, // OP_SET_NPC_PATH_DEST + 195, // OP_NEXT_SONG + 196, // OP_SET_NPC_PATH_PAUSE + 197, // OP_PASSWORD + 198, // OP_SET_SCENE_ENTRY_FLAG + 185, // OP_WALK_NPC_TO_CANIM + 186, // OP_WALK_NPC_TO_COORDS + 204, // OP_WALK_HOLMES_AND_NPC_TO_COORDS + 205, // OP_SET_NPC_TALK_FILE + 206, // OP_TURN_NPC_OFF + 207, // OP_TURN_NPC_ON + 208, // OP_NPC_DESC_ON_OFF + 209, // OP_NPC_PATH_PAUSE_TAKING_NOTES + 210, // OP_NPC_PATH_PAUSE_LOOKING_HOLMES + 211, // OP_ENABLE_TALK_INTERRUPTS + 212, // OP_DISABLE_TALK_INTERRUPTS + 213, // OP_SET_NPC_INFO_LINE + 214, // OP_SET_NPC_POSITION + 215, // OP_NPC_PATH_LABEL + 216, // OP_PATH_GOTO_LABEL + 217, // OP_PATH_IF_FLAG_GOTO_LABEL + 218, // OP_NPC_WALK_GRAPHICS + 220, // OP_NPC_VERB + 221, // OP_NPC_VERB_CANIM + 222, // OP_NPC_VERB_SCRIPT + 224, // OP_RESTORE_PEOPLE_SEQUENCE + 226, // OP_NPC_VERB_TARGET + 227, // OP_TURN_SOUNDS_OFF + 225 // OP_NULL +}; + +/*----------------------------------------------------------------*/ + +TattooTalk::TattooTalk(SherlockEngine *vm) : Talk(vm), _talkWidget(vm) { + static OpcodeMethod OPCODE_METHODS[] = { + (OpcodeMethod)&TattooTalk::cmdSwitchSpeaker, + + (OpcodeMethod)&TattooTalk::cmdRunCAnimation, + (OpcodeMethod)&TattooTalk::cmdCallTalkFile, + (OpcodeMethod)&TattooTalk::cmdPause, + (OpcodeMethod)&TattooTalk::cmdMouseOnOff, + (OpcodeMethod)&TattooTalk::cmdSetWalkControl, + (OpcodeMethod)&TattooTalk::cmdAdjustObjectSequence, + (OpcodeMethod)&TattooTalk::cmdWalkHolmesToCoords, + (OpcodeMethod)&TattooTalk::cmdPauseWithoutControl, + (OpcodeMethod)&TattooTalk::cmdBanishWindow, + (OpcodeMethod)&TattooTalk::cmdSetTalkSequence, + + (OpcodeMethod)&TattooTalk::cmdSetFlag, + (OpcodeMethod)&TattooTalk::cmdPlaySong, + (OpcodeMethod)&TattooTalk::cmdToggleObject, + (OpcodeMethod)&TattooTalk::cmdStealthModeActivate, + (OpcodeMethod)&TattooTalk::cmdWalkNPCToCAnimation, + (OpcodeMethod)&TattooTalk::cmdWalkNPCToCoords, + (OpcodeMethod)&TattooTalk::cmdWalkHomesAndNPCToCoords, + (OpcodeMethod)&TattooTalk::cmdStealthModeDeactivate, + (OpcodeMethod)&TattooTalk::cmdHolmesOff, + (OpcodeMethod)&TattooTalk::cmdHolmesOn, + + (OpcodeMethod)&TattooTalk::cmdGotoScene, + (OpcodeMethod)&TattooTalk::cmdSetNPCPathDest, + (OpcodeMethod)&TattooTalk::cmdAddItemToInventory, + (OpcodeMethod)&TattooTalk::cmdSetObject, + (OpcodeMethod)&TattooTalk::cmdNextSong, + (OpcodeMethod)&TattooTalk::cmdSetNPCPathPause, + (OpcodeMethod)&TattooTalk::cmdPassword, + (OpcodeMethod)&TattooTalk::cmdSetSceneEntryFlag, + (OpcodeMethod)&TattooTalk::cmdWalkToCAnimation, + (OpcodeMethod)&TattooTalk::cmdRemoveItemFromInventory, + + (OpcodeMethod)&TattooTalk::cmdEnableEndKey, + (OpcodeMethod)&TattooTalk::cmdDisableEndKey, + (OpcodeMethod)&TattooTalk::cmdEndTextWindow, + (OpcodeMethod)&TattooTalk::cmdWalkHomesAndNPCToCoords, + (OpcodeMethod)&TattooTalk::cmdSetNPCTalkFile, + (OpcodeMethod)&TattooTalk::cmdSetNPCOff, + (OpcodeMethod)&TattooTalk::cmdSetNPCOn, + (OpcodeMethod)&TattooTalk::cmdSetNPCDescOnOff, + (OpcodeMethod)&TattooTalk::cmdSetNPCPathPauseTakingNotes, + (OpcodeMethod)&TattooTalk::cmdSetNPCPathPauseLookingHolmes, + + (OpcodeMethod)&TattooTalk::cmdTalkInterruptsEnable, + (OpcodeMethod)&TattooTalk::cmdTalkInterruptsDisable, + (OpcodeMethod)&TattooTalk::cmdSetNPCInfoLine, + (OpcodeMethod)&TattooTalk::cmdSetNPCPosition, + (OpcodeMethod)&TattooTalk::cmdNPCLabelSet, + (OpcodeMethod)&TattooTalk::cmdNPCLabelGoto, + (OpcodeMethod)&TattooTalk::cmdNPCLabelIfFlagGoto, + (OpcodeMethod)&TattooTalk::cmdSetNPCWalkGraphics, + nullptr, + (OpcodeMethod)&TattooTalk::cmdSetNPCVerb, + + (OpcodeMethod)&TattooTalk::cmdSetNPCVerbCAnimation, + (OpcodeMethod)&TattooTalk::cmdSetNPCVerbScript, + nullptr, + (OpcodeMethod)&TattooTalk::cmdRestorePeopleSequence, + (OpcodeMethod)&TattooTalk::cmdSetNPCVerbTarget, + (OpcodeMethod)&TattooTalk::cmdTurnSoundsOff + }; + + _opcodes = TATTOO_OPCODES; + _opcodeTable = OPCODE_METHODS; +} + +void TattooTalk::talkInterface(const byte *&str) { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + const byte *s = str; + + // Move to past the end of the text string + _charCount = 0; + while ((*str < TATTOO_OPCODES[0] || *str == TATTOO_OPCODES[OP_NULL]) && *str) { + ++_charCount; + ++str; + } + + // Display the text window +// ui.banishWindow(); + ui._textWidget.load(Common::String((const char *)s, (const char *)str), _speaker); + ui._textWidget.summonWindow(); + _wait = true; +} + +void TattooTalk::showTalk() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + _sequenceStack.clear(); + people.setListenSequence(_talkTo, 129); + + _talkWidget.load(); + _talkWidget.summonWindow(); + _talkWidget.refresh(); + + if (ui._menuMode != MESSAGE_MODE) + ui._menuMode = TALK_MODE; +} + +OpcodeReturn TattooTalk::cmdSwitchSpeaker(const byte *&str) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + Screen &screen = *_vm->_screen; + UserInterface &ui = *_vm->_ui; + + if (_talkToAbort) + return RET_EXIT; + + ui.clearWindow(); + + _yp = screen.fontHeight() + 11; + _charCount = _line = 0; + + people.setListenSequence(_speaker, 129); + _speaker = *++str - 1; + ++str; + + people.setTalkSequence(_speaker, 1); + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdMouseOnOff(const byte *&str) { + Events &events = *_vm->_events; + bool mouseOn = *++str == 2; + if (mouseOn) + events.showCursor(); + else + events.hideCursor(); + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdWalkHolmesToCoords(const byte *&str) { + People &people = *_vm->_people; + ++str; + + int xp = (str[0] - 1) * 256 + str[1] - 1; + if (xp > 16384) + // Negative X + xp = -1 * (xp - 16384); + int yp = (str[2] - 1) * 256 + str[3] - 1; + + people[HOLMES].walkToCoords(Point32(xp * FIXED_INT_MULTIPLIER, yp * FIXED_INT_MULTIPLIER), + DIRECTION_CONVERSION[str[4] - 1]); + + if (_talkToAbort) + return RET_EXIT; + + str += 4; + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdGotoScene(const byte *&str) { + Map &map = *_vm->_map; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Scene &scene = *_vm->_scene; + scene._goToScene = str[1] - 1; + + if (scene._goToScene != OVERHEAD_MAP) { + // Not going to the map overview + map._oldCharPoint = scene._goToScene; + + // Run a canimation? + if (str[2] > 100) { + people._savedPos = PositionFacing(160, 100, str[2]); + } else { + int32 posX = (str[3] - 1) * 256 + str[4] - 1; + if (posX > 16384) + posX = -1 * (posX - 16384); + int32 posY = (str[5] - 1) * 256 + str[6] - 1; + people._savedPos = PositionFacing(posX, posY, str[2] - 1); + } + + _scriptMoreFlag = 1; + } + + str += 7; + if (scene._goToScene != OVERHEAD_MAP) + _scriptSaveIndex = str - _scriptStart; + + _endStr = true; + _wait = 0; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdNextSong(const byte *&str) { + Music &music = *_vm->_music; + + // Get the name of the next song to play + ++str; + music._nextSongName = ""; + for (int idx = 0; idx < 8; ++idx) { + if (str[idx] != '~') + music._nextSongName += str[idx]; + else + break; + } + str += 7; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdNPCLabelGoto(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 8; + person._npcPath[person._npcIndex + 1] = str[1]; + person._npcIndex += 2; + str++; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdNPCLabelIfFlagGoto(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 9; + for (int i = 1; i <= 3; i++) + person._npcPath[person._npcIndex + i] = str[i]; + + person._npcIndex += 4; + str += 3; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdNPCLabelSet(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 7; + person._npcPath[person._npcIndex + 1] = str[1]; + person._npcIndex += 2; + str++; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdPassword(const byte *&str) { error("TODO: script opcode (cmdPassword)"); } + +OpcodeReturn TattooTalk::cmdPlaySong(const byte *&str) { + Music &music = *_vm->_music; + Common::String currentSong = music._currentSongName; + + // Get the name of the song to play + music._currentSongName = ""; + str++; + for (int idx = 0; idx < 8; ++idx) { + if (str[idx] != '~') + music._currentSongName += str[idx]; + else + break; + } + str += 7; + + // Play the song + music.loadSong(music._currentSongName); + + // Copy the old song name to _nextSongName so that when the new song is finished, the old song will restart + music._nextSongName = currentSong; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdRestorePeopleSequence(const byte *&str) { + int npcNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + person._misc = 0; + + if (person._seqTo) { + person._walkSequences[person._sequenceNumber]._sequences[person._frameNumber] = person._seqTo; + person._seqTo = 0; + } + person._sequenceNumber = person._savedNpcSequence; + person._frameNumber = person._savedNpcFrame; + person.checkWalkGraphics(); + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCDescOnOff(const byte *&str) { + int npcNum = *++str; + ++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Person &person = people[npcNum]; + + // Copy over the NPC examine text until we reach a stop marker, which is + // the same as a start marker, or we reach the end of the file + while (*str && *str != _opcodes[OP_NPC_DESC_ON_OFF]) + person._examine += *str++; + + // Move past any leftover text till we reach a stop marker + while (*str && *str != _opcodes[OP_NPC_DESC_ON_OFF]) + str++; + + if (!*str) + // Reached end of file, so decrement pointer so outer loop will terminate on NULL + --str; + else + // Move past the ending OP_NPC_DEST_ON_OFF opcode + ++str; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCInfoLine(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + person._description = ""; + int len = *++str; + for (int idx = 0; idx < len; ++idx) + person._description += str[idx + 1]; + + str += len; + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCOff(const byte *&str) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + int npcNum = *++str; + people[npcNum]._type = REMOVE; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCOn(const byte *&str) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + int npcNum = *++str; + people[npcNum]._type = CHARACTER; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCPathDest(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 1; + for (int i = 1; i <= 4; i++) + person._npcPath[person._npcIndex + i] = str[i]; + person._npcPath[person._npcIndex + 5] = DIRECTION_CONVERSION[str[5] - 1] + 1; + + person._npcIndex += 6; + str += 5; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCPathPause(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 2; + for (int i = 1; i <= 2; i++) + person._npcPath[person._npcIndex + i] = str[i]; + + person._npcIndex += 3; + str += 2; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCPathPauseTakingNotes(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 5; + for (int i = 1; i <= 2; i++) + person._npcPath[person._npcIndex + i] = str[i]; + + person._npcIndex += 3; + str += 2; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCPathPauseLookingHolmes(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 6; + for (int i = 1; i <= 2; i++) + person._npcPath[person._npcIndex + i] = str[i]; + + person._npcIndex += 3; + str += 2; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCPosition(const byte *&str) { + int npcNum = *++str - 1; + ++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + int32 posX = (str[0] - 1) * 256 + str[1] - 1; + if (posX > 16384) + posX = -1 * (posX - 16384); + int32 posY = (str[2] - 1) * 256 + str[3] - 1; + + people[npcNum]._position = Point32(posX * FIXED_INT_MULTIPLIER, posY * FIXED_INT_MULTIPLIER); + if (person._seqTo && person._walkLoaded) { + person._walkSequences[person._sequenceNumber]._sequences[person._frameNumber] = person._seqTo; + person._seqTo = 0; + } + + assert(str[4] - 1 < 16); + person._sequenceNumber = DIRECTION_CONVERSION[str[4] - 1]; + person._frameNumber = 0; + + if (person._walkLoaded) + person.checkWalkGraphics(); + + if (person._walkLoaded && person._type == CHARACTER && + person._sequenceNumber >= STOP_UP && person._sequenceNumber <= STOP_UPLEFT) { + bool done = false; + do { + person.checkSprite(); + for (int x = 0; x < person._frameNumber; x++) { + if (person._walkSequences[person._sequenceNumber]._sequences[x] == 0) { + done = true; + break; + } + } + } while(!done); + } + + str += 4; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCTalkFile(const byte *&str) { + int npcNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._resetNPCPath) { + person._npcIndex = person._npcPause = 0; + person._resetNPCPath = false; + memset(person._npcPath, 0, 100); + } + + person._npcPath[person._npcIndex] = 3; + for (int i = 1; i <= 8; i++) + person._npcPath[person._npcIndex + i] = str[i]; + + person._npcIndex += 9; + str += 8; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCVerb(const byte *&str) { + int npcNum = *++str; + int verbNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Common::String &verb = people[npcNum]._use[verbNum]._verb; + + for (int x = 0; x < 12; x++) { + if (str[x + 1] != '~') + verb.setChar(str[x + 1], x); + else + verb.setChar(0, x); + } + + verb.setChar(0, 11); + + uint len = verb.size() - 1; + while (verb[len] == ' ' && len) + len--; + verb.setChar(0, len + 1); + if (verb != " ") + verb.clear(); + str += 12; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCVerbCAnimation(const byte *&str) { + int npcNum = *++str; + int verbNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + UseType &useType = people[npcNum]._use[verbNum]; + + useType._cAnimNum = (str[1] - 1) & 127; + useType._cAnimSpeed = 1 + 128 * (str[1] >= 128); + str++; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCVerbScript(const byte *&str) { + int npcNum = *++str; + int verbNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + UseType &useType = people[npcNum]._use[verbNum]; + Common::String &name = useType._names[0]; + name.setChar('*', 0); + name.setChar('C', 1); + + for (int x = 0; x < 8; x++) { + if (str[x + 1] != '~') + name.setChar(str[x + 1], x + 2); + else + name.setChar(0, x + 2); + } + + name.setChar(0, 11); + useType._cAnimNum = 99; + useType._cAnimSpeed = 1; + str += 8; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCVerbTarget(const byte *&str) { + int npcNum = *++str; + int verbNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Common::String &target = people[npcNum]._use[verbNum]._target; + + for (int x = 0; x < 12; x++) { + if (str[x + 1] != '~') + target.setChar(str[x + 1], x); + else + target.setChar(0, x); + } + + target.setChar(0, 11); + + uint len = target.size() - 1; + while (target[len] == ' ' && len) + len--; + target.setChar(0, len + 1); + str += 12; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetNPCWalkGraphics(const byte *&str) { + int npcNum = *++str - 1; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Person &person = people[npcNum]; + + // Build up walk library name for the given NPC + person._walkVGSName = ""; + for (int idx = 0; idx < 8; ++idx) { + if (str[idx + 1] != '~') + person._walkVGSName += str[idx + 1]; + else + break; + } + person._walkVGSName += ".VGS"; + + people._forceWalkReload = true; + str += 8; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetSceneEntryFlag(const byte *&str) { + TattooScene &scene = *(TattooScene *)_vm->_scene; + ++str; + int flag = (str[0] - 1) * 256 + str[1] - 1 - (str[1] == 1); + + int flag1 = flag & 16383; + if (flag > 16383) + flag1 *= -1; + + str += 2; + + // Make sure that this instance is not already being tracked + bool found = false; + for (uint idx = 0; idx < scene._sceneTripCounters.size() && !found; ++idx) { + SceneTripEntry &entry = scene._sceneTripCounters[idx]; + if (entry._flag == flag1 && entry._sceneNumber == str[0] - 1) + found = true; + } + + // Only add it if it's not being tracked already + if (!found) + scene._sceneTripCounters.push_back(SceneTripEntry(flag1, str[0] - 1, str[1] - 1)); + + ++str; + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetTalkSequence(const byte *&str) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + int speaker = str[1] - 1; + int sequenceNumber = str[2]; + + if (sequenceNumber < 128) + people.setTalkSequence(speaker, sequenceNumber); + else + people.setListenSequence(speaker, sequenceNumber); + + str += 2; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdSetWalkControl(const byte *&str) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + ++str; + people._walkControl = str[0] - 1; + + return RET_SUCCESS; +} + +// Dummy opcode +OpcodeReturn TattooTalk::cmdTalkInterruptsDisable(const byte *&str) { error("Dummy opcode cmdTalkInterruptsDisable called"); } + +// Dummy opcode +OpcodeReturn TattooTalk::cmdTalkInterruptsEnable(const byte *&str) { error("Dummy opcode cmdTalkInterruptsEnable called"); } + +OpcodeReturn TattooTalk::cmdTurnSoundsOff(const byte *&str) { error("TODO: script opcode (cmdTurnSoundsOff)"); } + +OpcodeReturn TattooTalk::cmdWalkHolmesAndNPCToCAnimation(const byte *&str) { + int npcNum = *++str; + int cAnimNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + Scene &scene = *_vm->_scene; + CAnim &anim = scene._cAnim[cAnimNum]; + + if (person._pathStack.empty()) + person.pushNPCPath(); + person._npcMoved = true; + + person.walkToCoords(anim._goto[1], anim._goto[1]._facing); + + if (_talkToAbort) + return RET_EXIT; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdWalkNPCToCAnimation(const byte *&str) { + int npcNum = *++str; + int cAnimNum = *++str; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + Scene &scene = *_vm->_scene; + CAnim &anim = scene._cAnim[cAnimNum]; + + if (person._pathStack.empty()) + person.pushNPCPath(); + person._npcMoved = true; + + person.walkToCoords(anim._goto[1], anim._goto[1]._facing); + + if (_talkToAbort) + return RET_EXIT; + + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdWalkNPCToCoords(const byte *&str) { + int npcNum = *++str; + str++; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._pathStack.empty()) + person.pushNPCPath(); + person._npcMoved = true; + + int xp = (str[0] - 1) * 256 + str[1] - 1; + if (xp > 16384) + xp = -1 * (xp - 16384); + int yp = (str[2] - 1) * 256 + str[3] - 1; + + person.walkToCoords(Point32(xp * FIXED_INT_MULTIPLIER, yp * FIXED_INT_MULTIPLIER), + DIRECTION_CONVERSION[str[4] - 1]); + if (_talkToAbort) + return RET_EXIT; + + str += 4; + return RET_SUCCESS; +} + +OpcodeReturn TattooTalk::cmdWalkHomesAndNPCToCoords(const byte *&str) { + int npcNum = *++str; + str++; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooPerson &person = people[npcNum]; + + if (person._pathStack.empty()) + person.pushNPCPath(); + person._npcMoved = true; + + int xp = (str[0] - 1) * 256 + str[1] - 1; + if (xp > 16384) + xp = -1 * (xp - 16384); + int yp = (str[2] - 1) * 256 + str[3] - 1; + + person.walkToCoords(Point32(xp * FIXED_INT_MULTIPLIER, yp * FIXED_INT_MULTIPLIER), + DIRECTION_CONVERSION[str[4] - 1]); + + if (_talkToAbort) + return RET_EXIT; + + str += 9; + return RET_SUCCESS; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_talk.h b/engines/sherlock/tattoo/tattoo_talk.h new file mode 100644 index 0000000000..8abbb1b8be --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_talk.h @@ -0,0 +1,101 @@ +/* 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_TALK_H +#define SHERLOCK_TATTOO_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" +#include "sherlock/talk.h" +#include "sherlock/tattoo/widget_talk.h" + +namespace Sherlock { + +namespace Tattoo { + +class WidgetTalk; + +class TattooTalk : public Talk { + friend class WidgetTalk; +private: + WidgetTalk _talkWidget; + + OpcodeReturn cmdSwitchSpeaker(const byte *&str); + OpcodeReturn cmdMouseOnOff(const byte *&str); + OpcodeReturn cmdGotoScene(const byte *&str); + OpcodeReturn cmdWalkHolmesToCoords(const byte *&str); + OpcodeReturn cmdNextSong(const byte *&str); + OpcodeReturn cmdPassword(const byte *&str); + OpcodeReturn cmdPlaySong(const byte *&str); + OpcodeReturn cmdRestorePeopleSequence(const byte *&str); + OpcodeReturn cmdSetNPCDescOnOff(const byte *&str); + OpcodeReturn cmdSetNPCInfoLine(const byte *&str); + OpcodeReturn cmdNPCLabelGoto(const byte *&str); + OpcodeReturn cmdNPCLabelIfFlagGoto(const byte *&str); + OpcodeReturn cmdNPCLabelSet(const byte *&str); + OpcodeReturn cmdSetNPCOff(const byte *&str); + OpcodeReturn cmdSetNPCOn(const byte *&str); + OpcodeReturn cmdSetNPCPathDest(const byte *&str); + OpcodeReturn cmdSetNPCPathPause(const byte *&str); + OpcodeReturn cmdSetNPCPathPauseTakingNotes(const byte *&str); + OpcodeReturn cmdSetNPCPathPauseLookingHolmes(const byte *&str); + OpcodeReturn cmdSetNPCPosition(const byte *&str); + OpcodeReturn cmdSetNPCTalkFile(const byte *&str); + OpcodeReturn cmdSetNPCVerb(const byte *&str); + OpcodeReturn cmdSetNPCVerbCAnimation(const byte *&str); + OpcodeReturn cmdSetNPCVerbScript(const byte *&str); + OpcodeReturn cmdSetNPCVerbTarget(const byte *&str); + OpcodeReturn cmdSetNPCWalkGraphics(const byte *&str); + OpcodeReturn cmdSetSceneEntryFlag(const byte *&str); + OpcodeReturn cmdSetTalkSequence(const byte *&str); + OpcodeReturn cmdSetWalkControl(const byte *&str); + OpcodeReturn cmdTalkInterruptsDisable(const byte *&str); + OpcodeReturn cmdTalkInterruptsEnable(const byte *&str); + OpcodeReturn cmdTurnSoundsOff(const byte *&str); + OpcodeReturn cmdWalkHolmesAndNPCToCAnimation(const byte *&str); + OpcodeReturn cmdWalkNPCToCAnimation(const byte *&str); + OpcodeReturn cmdWalkNPCToCoords(const byte *&str); + OpcodeReturn cmdWalkHomesAndNPCToCoords(const byte *&str); +protected: + /** + * Display the talk interface window + */ + virtual void talkInterface(const byte *&str); + + /** + * Show the talk display + */ + virtual void showTalk(); +public: + TattooTalk(SherlockEngine *vm); + virtual ~TattooTalk() {} +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/tattoo_user_interface.cpp b/engines/sherlock/tattoo/tattoo_user_interface.cpp new file mode 100644 index 0000000000..ae09ba5fc7 --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_user_interface.cpp @@ -0,0 +1,862 @@ +/* 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_user_interface.h" +#include "sherlock/tattoo/tattoo_journal.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +TattooUserInterface::TattooUserInterface(SherlockEngine *vm): UserInterface(vm), + _inventoryWidget(vm), _messageWidget(vm), _textWidget(vm), _tooltipWidget(vm), _verbsWidget(vm), + _labWidget(vm) { + Common::fill(&_lookupTable[0], &_lookupTable[PALETTE_COUNT], 0); + Common::fill(&_lookupTable1[0], &_lookupTable1[PALETTE_COUNT], 0); + _scrollSize = 0; + _scrollSpeed = 16; + _drawMenu = false; + _bgShape = nullptr; + _personFound = false; + _lockoutTimer = 0; + _fileMode = SAVEMODE_NONE; + _exitZone = -1; + _scriptZone = -1; + _arrowZone = _oldArrowZone = -1; + _activeObj = -1; + _cAnimFramePause = 0; + _scrollHighlight = SH_NONE; + _mask = _mask1 = nullptr; + _maskCounter = 0; + + _interfaceImages = new ImageFile("intrface.vgs"); +} + +TattooUserInterface::~TattooUserInterface() { + delete _interfaceImages; +} + +void TattooUserInterface::initScrollVars() { + Screen &screen = *_vm->_screen; + _scrollSize = screen._backBuffer1.w() - SHERLOCK_SCREEN_WIDTH; + _targetScroll = Common::Point(0, 0); + screen._currentScroll = Common::Point(0, 0); +} + +void TattooUserInterface::lookAtObject() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Sound &sound = *_vm->_sound; + Talk &talk = *_vm->_talk; + Common::Point mousePos = events.mousePos(); + Common::String desc; + int cAnimSpeed = 0; + + _lookPos = mousePos; + _menuMode = LOOK_MODE; + + if (_personFound) { + desc = people[_bgFound - 1000]._examine; + } else { + // Check if there is a Look animation + if (_bgShape->_lookcAnim != 0) { + cAnimSpeed = _bgShape->_lookcAnim & 0xe0; + cAnimSpeed >>= 5; + ++cAnimSpeed; + + _cAnimFramePause = _bgShape->_lookFrames; + desc = _bgShape->_examine; + + int cNum = (_bgShape->_lookcAnim & 0x1f) - 1; + scene.startCAnim(cNum); + } else if (_bgShape->_lookPosition.y != 0) { + // Need to walk to object before looking at it + people[HOLMES].walkToCoords(_bgShape->_lookPosition, _bgShape->_lookPosition._facing); + } + + if (!talk._talkToAbort) { + desc = _bgShape->_examine; + + if (_bgShape->_lookFlag) + _vm->setFlags(_bgShape->_lookFlag); + + // Find the Sound File to Play if there is one + if (!desc.hasPrefix("_")) { + for (uint idx = 0; idx < scene._objSoundList.size(); ++idx) { + // Get the object name up to the equals + const char *p = strchr(scene._objSoundList[idx].c_str(), '='); + + // Form the name and remove any trailing spaces + Common::String name(scene._objSoundList[idx].c_str(), p); + while (name.hasSuffix(" ")) + name.deleteLastChar(); + + // See if this Object Sound List entry matches the object's name + if (!_bgShape->_name.compareToIgnoreCase(name)) { + // Move forward to get the sound filename + while ((*p == ' ') || (*p == '=')) + ++p; + + // If it's not "NONE", play the Sound File + Common::String soundName(p); + if (soundName.compareToIgnoreCase("NONE")) { + soundName.toLowercase(); + if (!soundName.contains('.')) + soundName += ".wav"; + + sound.playSound(soundName, WAIT_RETURN_IMMEDIATELY); + } + + break; + } + } + } + } + } + + // Only show the desciption if the object has one, and if no talk file interrupted while walking to it + if (!talk._talkToAbort && !desc.empty()) { + if (_cAnimFramePause == 0) + printObjectDesc(desc, true); + else + // The description was already printed by an animation + _cAnimFramePause = 0; + } else if (desc.empty()) { + // There was no description to display, so reset back to STD_MODE + _menuMode = STD_MODE; + } +} + +void TattooUserInterface::printObjectDesc(const Common::String &str, bool firstTime) { + Events &events = *_vm->_events; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Talk &talk = *_vm->_talk; + + if (str.hasPrefix("_")) { + // The passed string specifies a talk file + _lookScriptFlag = true; + events.setCursor(MAGNIFY); + int savedSelector = _selector; + + if (!_invLookFlag) + _windowOpen = false; + + talk.talkTo(str.c_str() + 1); + _lookScriptFlag = false; + + if (talk._talkToAbort) { + events.setCursor(ARROW); + return; + } + + // See if we're looking at an inventory item + if (_invLookFlag) { + _selector = _oldSelector = savedSelector; + doInventory(0); + _invLookFlag = false; + + } else { + // Nope + events.setCursor(ARROW); + _key = -1; + _menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + events._pressed = events._released = events._rightReleased = false; + events._oldButtons = 0; + } + } else { + events._pressed = events._released = events._rightReleased = false; + + // Show text dialog + _textWidget.load(str); + _textWidget.summonWindow(); + + if (firstTime) + _selector = _oldSelector = -1; + + _drawMenu = _windowOpen = true; + } +} + +void TattooUserInterface::doJournal() { + TattooJournal &journal = *(TattooJournal *)_vm->_journal; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Screen &screen = *_vm->_screen; + + _menuMode = JOURNAL_MODE; + journal.show(); + + _menuMode = STD_MODE; + _windowOpen = false; + _key = -1; + + setupBGArea(screen._cMap); + screen.clear(); + screen.setPalette(screen._cMap); + + screen._backBuffer1.blitFrom(screen._backBuffer2); + scene.updateBackground(); + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); +} + +void TattooUserInterface::reset() { + UserInterface::reset(); + _lookPos = Common::Point(SHERLOCK_SCREEN_WIDTH / 2, SHERLOCK_SCREEN_HEIGHT / 2); + _tooltipWidget.setText(""); + _widgets.clear(); +} + +void TattooUserInterface::handleInput() { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Common::Point mousePos = events.mousePos(); + + _vm->_canLoadSave = _menuMode == STD_MODE; + events.pollEventsAndWait(); + _vm->_canLoadSave = false; + _keyState.keycode = Common::KEYCODE_INVALID; + + // Check for credits starting + if (_vm->readFlags(3000) && !vm._creditsActive) + vm.initCredits(); + + // Check the mouse positioning + if (events.isCursorVisible()) + _bgFound = scene.findBgShape(mousePos); + _personFound = _bgFound >= 1000; + _bgShape = (_bgFound != -1 && _bgFound < 1000) ? &scene._bgShapes[_bgFound] : nullptr; + + if (_lockoutTimer) + --_lockoutTimer; + + // Key handling + if (events.kbHit()) { + _keyState = events.getKey(); + + if (_keyState.keycode == Common::KEYCODE_s && vm._allowFastMode) + vm._fastMode = !vm._fastMode; + + else if (_keyState.keycode == Common::KEYCODE_l && _bgFound != -1) { + // Beging used for testing that Look dialogs work + lookAtObject(); + + } else if (_keyState.keycode == Common::KEYCODE_ESCAPE && vm._runningProlog && !_lockoutTimer) { + vm.setFlags(-76); + vm.setFlags(396); + scene._goToScene = STARTING_GAME_SCENE; + } + } + + if (!events.isCursorVisible()) + _keyState.keycode = Common::KEYCODE_INVALID; + + // If there's any active widgets/windows, let the most recently open one do event processing + if (!_widgets.empty()) + _widgets.back()->handleEvents(); + + // Handle input depending on what mode we're in + switch (_menuMode) { + case STD_MODE: + doStandardControl(); + break; + case LOOK_MODE: + doLookControl(); + break; + case FILES_MODE: + doFileControl(); + break; + default: + break; + } +} + +void TattooUserInterface::drawInterface(int bufferNum) { + Screen &screen = *_vm->_screen; + TattooEngine &vm = *(TattooEngine *)_vm; + + // Draw any active on-screen widgets + for (Common::List<WidgetBase *>::iterator i = _widgets.begin(); i != _widgets.end(); ++i) + (*i)->draw(); + + // Handle drawing credits + if (vm._creditsActive) + vm.drawCredits(); + + // Bring the widgets to the screen + if (_mask != nullptr) + screen._flushScreen = true; + + if (screen._flushScreen) + screen.blockMove(); + + // Handle drawing the text tooltip if necessary + if (_menuMode == STD_MODE || _menuMode == LAB_MODE) + _tooltipWidget.draw(); +} + +void TattooUserInterface::doBgAnimRestoreUI() { + TattooScene &scene = *((TattooScene *)_vm->_scene); + Screen &screen = *_vm->_screen; + + // If there are any on-screen widgets, then erase them + for (Common::List<WidgetBase *>::iterator i = _widgets.begin(); i != _widgets.end(); ++i) + (*i)->erase(); + + // If there is a Text Tag being display, restore the area underneath it + _tooltipWidget.erase(); + + // If a canimation is active, restore the graphics underneath it + if (scene._activeCAnim.active()) + screen.restoreBackground(scene._activeCAnim._oldBounds); + + // If a canimation just ended, remove it's graphics from the backbuffer + if (scene._activeCAnim._removeBounds.width() > 0) + screen.restoreBackground(scene._activeCAnim._removeBounds); +} + +void TattooUserInterface::doScroll() { + Screen &screen = *_vm->_screen; + + // If we're already at the target scroll position, nothing needs to be done + if (_targetScroll.x == screen._currentScroll.x) + return; + + screen._flushScreen = true; + if (_targetScroll.x > screen._currentScroll.x) { + screen._currentScroll.x += _scrollSpeed; + if (screen._currentScroll.x > _targetScroll.x) + screen._currentScroll.x = _targetScroll.x; + } else if (_targetScroll.x < screen._currentScroll.x) { + screen._currentScroll.x -= _scrollSpeed; + if (screen._currentScroll.x < _targetScroll.x) + screen._currentScroll.x = _targetScroll.x; + } +} + +void TattooUserInterface::doStandardControl() { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + People &people = *_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Talk &talk = *_vm->_talk; + Common::Point mousePos = events.mousePos(); + bool noDesc = false; + + // Don't do any input processing whilst the prolog is running + if (vm._runningProlog) + return; + + // Display the names of any Objects the cursor is pointing at + displayObjectNames(); + + switch (_keyState.keycode) { + case Common::KEYCODE_F5: + // Save game + freeMenu(); + _fileMode = SAVEMODE_SAVE; + initFileMenu(); + return; + + case Common::KEYCODE_F7: + // Load game + freeMenu(); + _fileMode = SAVEMODE_LOAD; + initFileMenu(); + return; + + case Common::KEYCODE_F1: + // Display journal + if (vm.readFlags(FLAG_PLAYER_IS_HOLMES)) { + freeMenu(); + doJournal(); + + // See if we're in a Lab Table Room + _menuMode = (scene._labTableScene) ? LAB_MODE : STD_MODE; + return; + } + break; + + case Common::KEYCODE_TAB: + case Common::KEYCODE_F3: + // Display inventory + freeMenu(); + doInventory(3); + return; + + case Common::KEYCODE_F4: + // Display options + freeMenu(); + doControls(); + return; + + case Common::KEYCODE_F10: + // Quit menu + freeMenu(); + doQuitMenu(); + return; + + default: + break; + } + + // See if a mouse button was released + if (events._released || events._rightReleased) { + // See if the mouse was released in an exit (Arrow) zone. Unless it's also pointing at an object + // within the zone, in which case the object gets precedence + _exitZone = -1; + if (_arrowZone != -1 && events._released) + _exitZone = _arrowZone; + + // Turn any Text display off + if (_arrowZone == -1 || events._rightReleased) + freeMenu(); + + if (_personFound) { + if (people[_bgFound - 1000]._description.empty() || people[_bgFound - 1000]._description.hasPrefix(" ")) + noDesc = true; + } else if (_bgFound != -1) { + if (_bgShape->_description.empty() || _bgShape->_description.hasPrefix(" ")) + noDesc = true; + } else { + noDesc = true; + } + + if (events._rightReleased) { + // Show the verbs menu for the highlighted object + _tooltipWidget.banishWindow(); + _verbsWidget.load(!noDesc); + _verbsWidget.summonWindow(); + + _selector = _oldSelector = -1; + _activeObj = _bgFound; + _menuMode = VERB_MODE; + } else if (_personFound || (_bgFound != -1 && _bgFound < 1000 && _bgShape->_aType == PERSON)) { + // The object found is a person (the default for people is TALK) + talk.talk(_bgFound); + _activeObj = -1; + } else if (!noDesc) { + // Either call the code to Look at it's Examine Field or call the Exit animation + // if the object is an exit, specified by the first four characters of the name being "EXIT" + Common::String name = _personFound ? people[_bgFound - 1000]._name : _bgShape->_name; + if (!name.hasPrefix("EXIT")) { + lookAtObject(); + } else { + // Run the Exit animation and set which scene to go to next + for (int idx = 0; idx < 6; ++idx) { + if (!_bgShape->_use[idx]._verb.compareToIgnoreCase("Open")) { + checkAction(_bgShape->_use[idx], _bgFound); + _activeObj = -1; + } + } + } + } else { + // See if there are any Script Zones where they clicked + if (scene.checkForZones(mousePos, _scriptZone) != 0) { + // Mouse click in a script zone + events._pressed = events._released = false; + } else if (scene.checkForZones(mousePos, NOWALK_ZONE) != 0) { + events._pressed = events._released = false; + } else { + // Walk to where the mouse was clicked + people[HOLMES]._walkDest = mousePos; + people[HOLMES].goAllTheWay(); + } + } + } +} + +void TattooUserInterface::doLookControl() { + Events &events = *_vm->_events; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Sound &sound = *_vm->_sound; + + // See if a mouse button was released or a key pressed, and we want to initiate an action + // TODO: Not sure about _soundOn.. should be check for speaking voice for text being complete + if (events._released || events._rightReleased || _keyState.keycode || (sound._voices && !sound._soundOn)) { + // See if we were looking at an inventory object + if (!_invLookFlag) { + // See if there is any more text to display + if (!_textWidget._remainingText.empty()) { + printObjectDesc(_textWidget._remainingText, false); + } else { + // Otherwise restore the background and go back into STD_MODE + freeMenu(); + _key = -1; + _menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + + events.setCursor(ARROW); + events._pressed = events._released = events._rightReleased = false; + events._oldButtons = 0; + } + } else { + // We were looking at a Inventory object + // Erase the text window, and then redraw the inventory window + _textWidget.banishWindow(); + doInventory(0); + + _invLookFlag = false; + _key = -1; + + events.setCursor(ARROW); + events._pressed = events._released = events._rightReleased = false; + events._oldButtons = 0; + } + } +} + +void TattooUserInterface::doFileControl() { + warning("TODO: ui control (file)"); +} + +void TattooUserInterface::displayObjectNames() { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + Common::Point mousePos = events.mousePos(); + _arrowZone = -1; + + if (_bgFound == -1 || scene._currentScene == 90) { + for (uint idx = 0; idx < scene._exits.size() && _arrowZone == -1; ++idx) { + Exit &exit = scene._exits[idx]; + if (exit.contains(mousePos)) + _arrowZone = idx; + } + } + + _tooltipWidget.handleEvents(); + _oldArrowZone = _arrowZone; +} + +void TattooUserInterface::initFileMenu() { + // TODO +} + +void TattooUserInterface::doInventory(int mode) { + People &people = *_vm->_people; + people[HOLMES].gotoStand(); + + _inventoryWidget.load(mode); + _inventoryWidget.summonWindow(); + + _menuMode = INV_MODE; +} + +void TattooUserInterface::doControls() { + // TODO +} + +void TattooUserInterface::pickUpObject(int objNum) { + // TOOD +} + +void TattooUserInterface::doQuitMenu() { + // TODO +} + +void TattooUserInterface::putMessage(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); + + // Open the message widget + _menuMode = MESSAGE_MODE; + _messageWidget.load(str, 25); + _messageWidget.summonWindow(); +} + +void TattooUserInterface::setupBGArea(const byte cMap[PALETTE_SIZE]) { + Scene &scene = *_vm->_scene; + + // This requires that there is a 16 grayscale palette sequence in the palette that goes from lighter + // to darker as the palette numbers go up. The last palette entry in that run is specified by _bgColor + byte *p = &_lookupTable[0]; + for (int idx = 0; idx < PALETTE_COUNT; ++idx) + *p++ = BG_GREYSCALE_RANGE_END - ((cMap[idx * 3] / 4) * 30 + (cMap[idx * 3 + 1] / 4) * 59 + + (cMap[idx * 3 + 2] / 4) * 11) / 480; + + // If we're going to a scene with a haze special effect, initialize the translate table to lighten the colors + if (_mask != nullptr) { + p = &_lookupTable1[0]; + + for (int idx = 0; idx < PALETTE_COUNT; ++idx) { + int r, g, b; + switch (scene._currentScene) { + case 8: + r = cMap[idx * 3] * 4 / 5; + g = cMap[idx * 3 + 1] * 3 / 4; + b = cMap[idx * 3 + 2] * 3 / 4; + break; + + case 18: + case 68: + r = cMap[idx * 3] * 4 / 3; + g = cMap[idx * 3 + 1] * 4 / 3; + b = cMap[idx * 3 + 2] * 4 / 3; + break; + + case 7: + case 53: + r = cMap[idx * 3] * 4 / 3; + g = cMap[idx * 3 + 1] * 4 / 3; + b = cMap[idx * 3 + 2] * 4 / 3; + break; + + default: + r = g = b = 0; + break; + } + + byte c = 0; + int cd = (r - cMap[0]) * (r - cMap[0]) + (g - cMap[1]) * (g - cMap[1]) + (b - cMap[2]) * (b - cMap[2]); + + for (int pal = 0; pal < PALETTE_COUNT; ++pal) { + int d = (r - cMap[pal * 3]) * (r - cMap[pal * 3]) + (g - cMap[pal * 3 + 1]) * (g - cMap[pal * 3 + 1]) + + (b - cMap[pal * 3 + 2])*(b - cMap[pal * 3 + 2]); + + if (d < cd) { + c = pal; + cd = d; + if (!d) + break; + } + } + *p++ = c; + } + } +} + +void TattooUserInterface::doBgAnimEraseBackground() { + TattooEngine &vm = *((TattooEngine *)_vm); + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + + static const int16 OFFSETS[16] = { -1, -2, -3, -3, -2, -1, -1, 0, 1, 2, 3, 3, 2, 1, 0, 0 }; + + if (_mask != nullptr) { + screen.slamArea(0, 0, SHERLOCK_SCREEN_WIDTH, SHERLOCK_SCREEN_HEIGHT); + + switch (scene._currentScene) { + case 7: + if (++_maskCounter == 2) { + _maskCounter = 0; + if (--_maskOffset.x < 0) + _maskOffset.x = SHERLOCK_SCREEN_WIDTH - 1; + } + break; + + case 8: + _maskOffset.x += 2; + if (_maskOffset.x >= SHERLOCK_SCREEN_WIDTH) + _maskOffset.x = 0; + break; + + case 18: + case 68: + ++_maskCounter; + if (_maskCounter / 4 >= 16) + _maskCounter = 0; + + _maskOffset.x = OFFSETS[_maskCounter / 4]; + break; + + case 53: + if (++_maskCounter == 2) { + _maskCounter = 0; + if (++_maskOffset.x == screen._backBuffer1.w()) + _maskOffset.x = 0; + } + break; + + default: + break; + } + } else { + // Standard scene without mask, so call user interface to erase any UI elements as necessary + doBgAnimRestoreUI(); + + // Restore background for any areas covered by characters and shapes + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) + screen.restoreBackground(Common::Rect(people[idx]._oldPosition.x, people[idx]._oldPosition.y, + people[idx]._oldPosition.x + people[idx]._oldSize.x, people[idx]._oldPosition.y + people[idx]._oldSize.y)); + + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &obj = scene._bgShapes[idx]; + + if ((obj._type == ACTIVE_BG_SHAPE && (obj._maxFrames > 1 || obj._delta.x != 0 || obj._delta.y != 0)) || + obj._type == HIDE_SHAPE || obj._type == REMOVE) + screen._backBuffer1.blitFrom(screen._backBuffer2, obj._oldPosition, + Common::Rect(obj._oldPosition.x, obj._oldPosition.y, obj._oldPosition.x + obj._oldSize.x, + obj._oldPosition.y + obj._oldSize.y)); + } + + // If credits are active, erase the area they cover + if (vm._creditsActive) + vm.eraseCredits(); + } + + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &obj = scene._bgShapes[idx]; + + if (obj._type == NO_SHAPE && (obj._flags & 1) == 0) { + screen._backBuffer1.blitFrom(screen._backBuffer2, obj._position, obj.getNoShapeBounds()); + + obj._oldPosition = obj._position; + obj._oldSize = obj._noShapeSize; + } + } + + // Adjust the Target Scroll if needed + if ((people[people._walkControl]._position.x / FIXED_INT_MULTIPLIER - screen._currentScroll.x) < + (SHERLOCK_SCREEN_WIDTH / 8) && people[people._walkControl]._delta.x < 0) { + + _targetScroll.x = (short)(people[people._walkControl]._position.x / FIXED_INT_MULTIPLIER - + SHERLOCK_SCREEN_WIDTH / 8 - 250); + if (_targetScroll.x < 0) + _targetScroll.x = 0; + } + + if ((people[people._walkControl]._position.x / FIXED_INT_MULTIPLIER - screen._currentScroll.x) > + (SHERLOCK_SCREEN_WIDTH / 4 * 3) && people[people._walkControl]._delta.x > 0) + _targetScroll.x = (short)(people[people._walkControl]._position.x / FIXED_INT_MULTIPLIER - + SHERLOCK_SCREEN_WIDTH / 4 * 3 + 250); + + if (_targetScroll.x > _scrollSize) + _targetScroll.x = _scrollSize; + + doScroll(); +} + +void TattooUserInterface::drawMaskArea(bool mode) { + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + int xp = mode ? _maskOffset.x : 0; + + if (_mask != nullptr) { + switch (scene._currentScene) { + case 7: + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x - SHERLOCK_SCREEN_WIDTH, 110)); + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x, 110)); + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x + SHERLOCK_SCREEN_WIDTH, 110)); + break; + + case 8: + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x - SHERLOCK_SCREEN_WIDTH, 180)); + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x, 180)); + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x + SHERLOCK_SCREEN_WIDTH, 180)); + if (!_vm->readFlags(880)) + screen._backBuffer1.maskArea((*_mask1)[0], Common::Point(940, 300)); + break; + + case 18: + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(xp, 203)); + if (!_vm->readFlags(189)) + screen._backBuffer1.maskArea((*_mask1)[0], Common::Point(124 + xp, 239)); + break; + + case 53: + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x, 110)); + if (mode) + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(_maskOffset.x - SHERLOCK_SCREEN_WIDTH, 110)); + break; + + case 68: + screen._backBuffer1.maskArea((*_mask)[0], Common::Point(xp, 203)); + screen._backBuffer1.maskArea((*_mask1)[0], Common::Point(124 + xp, 239)); + break; + } + } +} + +void TattooUserInterface::makeBGArea(const Common::Rect &r) { + Screen &screen = *_vm->_screen; + + for (int yp = r.top; yp < r.bottom; ++yp) { + byte *ptr = screen._backBuffer1.getBasePtr(r.left, yp); + + for (int xp = r.left; xp < r.right; ++xp, ++ptr) + *ptr = _lookupTable[*ptr]; + } + + screen.slamRect(r); +} + +void TattooUserInterface::drawDialogRect(Surface &s, const Common::Rect &r, bool raised) { + if (raised) { + // Draw Left + s.vLine(r.left, r.top, r.bottom - 1, INFO_TOP); + s.vLine(r.left + 1, r.top, r.bottom - 2, INFO_TOP); + // Draw Top + s.hLine(r.left + 2, r.top, r.right - 1, INFO_TOP); + s.hLine(r.left + 2, r.top + 1, r.right - 2, INFO_TOP); + // Draw Right + s.vLine(r.right - 1, r.top + 1, r.bottom - 1, INFO_BOTTOM); + s.vLine(r.right - 2, r.top + 2, r.bottom - 1, INFO_BOTTOM); + // Draw Bottom + s.hLine(r.left + 1, r.bottom - 1, r.right - 3, INFO_BOTTOM); + s.hLine(r.left + 2, r.bottom - 2, r.right - 3, INFO_BOTTOM); + + } else { + // Draw Left + s.vLine(r.left, r.top, r.bottom - 1, INFO_BOTTOM); + s.vLine(r.left + 1, r.top, r.bottom - 2, INFO_BOTTOM); + // Draw Top + s.hLine(r.left + 2, r.top, r.right - 1, INFO_BOTTOM); + s.hLine(r.left + 2, r.top + 1, r.right - 2, INFO_BOTTOM); + // Draw Right + s.vLine(r.right - 1, r.top + 1, r.bottom - 1, INFO_TOP); + s.vLine(r.right - 2, r.top + 2, r.bottom - 1, INFO_TOP); + // Draw Bottom + s.hLine(r.left + 1, r.bottom - 1, r.right - 3, INFO_TOP); + s.hLine(r.left + 2, r.bottom - 2, r.right - 3, INFO_TOP); + } +} + +void TattooUserInterface::banishWindow(bool slideUp) { + TattooScene &scene = *(TattooScene *)_vm->_scene; + if (!_widgets.empty()) + _widgets.back()->banishWindow(); + + if (scene._labTableScene && !_labWidget.active()) { + // In the lab table scene, so ensure + _labWidget.summonWindow(); + _menuMode = LAB_MODE; + } +} + +void TattooUserInterface::freeMenu() { + for (Common::List<WidgetBase *>::iterator i = _widgets.begin(); i != _widgets.end(); ++i) + (*i)->erase(); + _widgets.clear(); +} + +void TattooUserInterface::clearWindow() { + banishWindow(); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/tattoo_user_interface.h b/engines/sherlock/tattoo/tattoo_user_interface.h new file mode 100644 index 0000000000..1cefec688d --- /dev/null +++ b/engines/sherlock/tattoo/tattoo_user_interface.h @@ -0,0 +1,229 @@ +/* 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_UI_H +#define SHERLOCK_TATTOO_UI_H + +#include "common/scummsys.h" +#include "common/list.h" +#include "sherlock/saveload.h" +#include "sherlock/screen.h" +#include "sherlock/user_interface.h" +#include "sherlock/tattoo/widget_inventory.h" +#include "sherlock/tattoo/widget_lab.h" +#include "sherlock/tattoo/widget_text.h" +#include "sherlock/tattoo/widget_tooltip.h" +#include "sherlock/tattoo/widget_verbs.h" + +namespace Sherlock { + +namespace Tattoo { + +#define BUTTON_SIZE 15 // Button width/height + +class WidgetBase; + +enum ScrollHighlight { SH_NONE = 0, SH_SCROLL_UP = 1, SH_PAGE_UP = 2, SH_THUMBNAIL = 3, SH_PAGE_DOWN = 4, SH_SCROLL_DOWN = 5 }; + +class TattooUserInterface : public UserInterface { + friend class WidgetBase; +private: + int _lockoutTimer; + SaveMode _fileMode; + int _scriptZone; + int _cAnimFramePause; + WidgetInventory _inventoryWidget; + WidgetMessage _messageWidget; + Common::List<WidgetBase *> _widgets; + byte _lookupTable[PALETTE_COUNT]; + byte _lookupTable1[PALETTE_COUNT]; +private: + /** + * Handle any input when we're in standard mode (with no windows open) + */ + void doStandardControl(); + + /** + * Handle input when in look mode + */ + void doLookControl(); + + /** + * Handle input when the File window is open + */ + void doFileControl(); + + /** + * Handle input while the verb menu is open + */ + void doVerbControl(); + + /** + * Set up to display the Files menu + */ + void initFileMenu(); + + /** + * Handle displaying the quit menu + */ + void doQuitMenu(); + + /** + * Free any active menu + */ + void freeMenu(); +public: + Common::Point _targetScroll; + int _scrollSize, _scrollSpeed; + bool _drawMenu; + int _arrowZone, _oldArrowZone; + Object *_bgShape; + bool _personFound; + int _activeObj; + Common::KeyState _keyState; + Common::Point _lookPos; + ScrollHighlight _scrollHighlight; + ImageFile *_mask, *_mask1; + Common::Point _maskOffset; + int _maskCounter; + ImageFile *_interfaceImages; + WidgetText _textWidget; + WidgetLab _labWidget; + WidgetVerbs _verbsWidget; + WidgetSceneTooltip _tooltipWidget; +public: + TattooUserInterface(SherlockEngine *vm); + virtual ~TattooUserInterface(); + + /** + * Handles restoring any areas of the back buffer that were/are covered by UI elements + */ + void doBgAnimRestoreUI(); + + /** + * Checks to see if the screen needs to be scrolled. If so, scrolls it towards the target position + */ + void doScroll(); + + /** + * Initializes scroll variables + */ + void initScrollVars(); + + /** + * Display the long description for an object in a window + */ + void lookAtObject(); + + /** + * Display the passed long description for an object. If the flag firstTime is set, + * the window will be opened to accomodate the text. Otherwise, the remaining text + * will be printed in an already open window + */ + void printObjectDesc(const Common::String &str, bool firstTime); + + /** + * Handles displaying the journal + */ + void doJournal(); + + /** + * Put the game in inventory mode by opening the inventory dialog + */ + void doInventory(int mode); + + /** + * Handle the display of the options/setup menu + */ + void doControls(); + + /** + * Pick up the selected object + */ + void pickUpObject(int objNum); + + /** + * This will display a text message in a dialog at the bottom of the screen + */ + void putMessage(const char *formatStr, ...) GCC_PRINTF(2, 3); + + /** + * Makes a greyscale translation table for each palette entry in the table + */ + void setupBGArea(const byte cMap[PALETTE_SIZE]); + + /** + * Erase any background as needed before drawing frame + */ + void doBgAnimEraseBackground(); + + void drawMaskArea(bool mode); + + /** + * Translate a given area of the back buffer to greyscale shading + */ + void makeBGArea(const Common::Rect &r); + + /** + * Draws all the dialog rectangles for any items that need them + */ + void drawDialogRect(Surface &s, const Common::Rect &r, bool raised); + + /** + * If the mouse cursor is point at the cursor, then display the name of the object on the screen. + * If there is no object being pointed it, clear any previously displayed name + */ + void displayObjectNames(); +public: + /** + * Resets the user interface + */ + virtual void reset(); + + /** + * Main input handler for the user interface + */ + virtual void handleInput(); + + /** + * Draw the user interface onto the screen's back buffers + */ + virtual void drawInterface(int bufferNum = 3); + + /** + * Clear any active text window + */ + virtual void clearWindow(); + + /** + * Banish any active window + * @remarks Tattoo doesn't use sliding windows, but the parameter is in the base + * UserInterface class as a convenience for Scalpel UI code + */ + virtual void banishWindow(bool slideUp = true); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_base.cpp b/engines/sherlock/tattoo/widget_base.cpp new file mode 100644 index 0000000000..9d95fed9bf --- /dev/null +++ b/engines/sherlock/tattoo/widget_base.cpp @@ -0,0 +1,299 @@ +/* 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/widget_base.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_talk.h" +#include "sherlock/tattoo/tattoo_user_interface.h" + +namespace Sherlock { + +namespace Tattoo { + +WidgetBase::WidgetBase(SherlockEngine *vm) : _vm(vm) { + _scroll = false; +} + +void WidgetBase::summonWindow() { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + // Double-check that the same widget isn't added twice + for (Common::List<WidgetBase *>::iterator i = ui._widgets.begin(); i != ui._widgets.end(); ++i) { + if ((*i) == this) + error("Tried to add a widget twice"); + } + + // Add widget to the screen + ui._widgets.push_back(this); + _outsideMenu = false; + + draw(); +} + +void WidgetBase::banishWindow() { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + erase(); + _surface.free(); + ui._widgets.remove(this); +} + +bool WidgetBase::active() const { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + for (Common::List<WidgetBase *>::iterator i = ui._widgets.begin(); i != ui._widgets.end(); ++i) { + if ((*i) == this) + return true; + } + + return false; +} + + +void WidgetBase::erase() { + Screen &screen = *_vm->_screen; + + if (_oldBounds.width() > 0) { + // Restore the affected area from the secondary back buffer into the first one, and then copy to screen + screen._backBuffer1.blitFrom(screen._backBuffer2, Common::Point(_oldBounds.left, _oldBounds.top), _oldBounds); + screen.slamRect(_oldBounds); + + // Reset the old bounds so it won't be erased again + _oldBounds = Common::Rect(0, 0, 0, 0); + } +} + +void WidgetBase::draw() { + Screen &screen = *_vm->_screen; + + // If there was a previously drawn frame in a different position that hasn't yet been erased, then erase it + if (_oldBounds.width() > 0 && _oldBounds != _bounds) + erase(); + + if (_bounds.width() > 0 && !_surface.empty()) { + // Get the area to draw, adjusted for scroll position + restrictToScreen(); + + // Draw the background for the widget + drawBackground(); + + // Draw the widget onto the back buffer and then slam it to the screen + screen._backBuffer1.transBlitFrom(_surface, Common::Point(_bounds.left, _bounds.top)); + screen.slamRect(_bounds); + + // Store a copy of the drawn area for later erasing + _oldBounds = _bounds; + } +} + +void WidgetBase::drawBackground() { + TattooEngine &vm = *(TattooEngine *)_vm; + Screen &screen = *_vm->_screen; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + Common::Rect bounds = _bounds; + + if (vm._transparentMenus) { + ui.makeBGArea(bounds); + } else { + bounds.grow(-3); + screen._backBuffer1.fillRect(bounds, MENU_BACKGROUND); + } +} + +Common::String WidgetBase::splitLines(const Common::String &str, Common::StringArray &lines, int maxWidth, uint maxLines) { + Talk &talk = *_vm->_talk; + const char *strP = str.c_str(); + + // Loop counting up lines + lines.clear(); + do { + int width = 0; + const char *spaceP = nullptr; + const char *lineStartP = strP; + + // Find how many characters will fit on the next line + while (width < maxWidth && *strP && ((byte)*strP < talk._opcodes[OP_SWITCH_SPEAKER] || + (byte)*strP == talk._opcodes[OP_NULL])) { + width += _surface.charWidth(*strP); + + // Keep track of the last space + if (*strP == ' ') + spaceP = strP; + ++strP; + } + + // If the line was too wide to fit on a single line, go back to the last space + // if there was one, or otherwise simply break the line at this point + if (width >= maxWidth && spaceP != nullptr) + strP = spaceP; + + // Add the line to the output array + lines.push_back(Common::String(lineStartP, strP)); + + // Move the string ahead to the next line + if (*strP == ' ' || *strP == 13) + ++strP; + } while (*strP && (lines.size() < maxLines) && ((byte)*strP < talk._opcodes[OP_SWITCH_SPEAKER] + || (byte)*strP == talk._opcodes[OP_NULL])); + + // Return any remaining text left over + return *strP ? Common::String(strP) : Common::String(); +} + +void WidgetBase::restrictToScreen() { + Screen &screen = *_vm->_screen; + + if (_bounds.left < screen._currentScroll.x) + _bounds.moveTo(screen._currentScroll.x, _bounds.top); + if (_bounds.top < 0) + _bounds.moveTo(_bounds.left, 0); + if (_bounds.right > screen._backBuffer1.w()) + _bounds.moveTo(screen._backBuffer1.w() - _bounds.width(), _bounds.top); + if (_bounds.bottom > screen._backBuffer1.h()) + _bounds.moveTo(_bounds.left, screen._backBuffer1.h() - _bounds.height()); +} + +void WidgetBase::makeInfoArea(Surface &s) { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + ImageFile &images = *ui._interfaceImages; + + // Draw the four corners of the Info Box + s.transBlitFrom(images[0], Common::Point(0, 0)); + s.transBlitFrom(images[1], Common::Point(s.w() - images[1]._width, 0)); + s.transBlitFrom(images[2], Common::Point(0, s.h() - images[2]._height)); + s.transBlitFrom(images[3], Common::Point(s.w() - images[3]._width, s.h())); + + // Draw the top of the Info Box + s.hLine(images[0]._width, 0, s.w() - images[1]._width, INFO_TOP); + s.hLine(images[0]._width, 1, s.w() - images[1]._width, INFO_MIDDLE); + s.hLine(images[0]._width, 2, s.w() - images[1]._width, INFO_BOTTOM); + + // Draw the bottom of the Info Box + s.hLine(images[0]._width, s.h()- 3, s.w() - images[1]._width, INFO_TOP); + s.hLine(images[0]._width, s.h()- 2, s.w() - images[1]._width, INFO_MIDDLE); + s.hLine(images[0]._width, s.h()- 1, s.w() - images[1]._width, INFO_BOTTOM); + + // Draw the left Side of the Info Box + s.vLine(0, images[0]._height, s.h()- images[2]._height, INFO_TOP); + s.vLine(1, images[0]._height, s.h()- images[2]._height, INFO_MIDDLE); + s.vLine(2, images[0]._height, s.h()- images[2]._height, INFO_BOTTOM); + + // Draw the right Side of the Info Box + s.vLine(s.w() - 3, images[0]._height, s.h()- images[2]._height, INFO_TOP); + s.vLine(s.w() - 2, images[0]._height, s.h()- images[2]._height, INFO_MIDDLE); + s.vLine(s.w() - 1, images[0]._height, s.h()- images[2]._height, INFO_BOTTOM); +} + +void WidgetBase::makeInfoArea() { + makeInfoArea(_surface); +} + +void WidgetBase::checkTabbingKeys(int numOptions) { +} + +void WidgetBase::drawScrollBar(int index, int pageSize, int count) { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + bool raised; + + // Fill the area with transparency + Common::Rect r(BUTTON_SIZE, _bounds.height() - 6); + r.moveTo(_bounds.width() - BUTTON_SIZE - 3, 3); + _surface.fillRect(r, TRANSPARENCY); + + raised = ui._scrollHighlight != 1; + _surface.fillRect(Common::Rect(r.left + 2, r.top + 2, r.right - 2, r.top + BUTTON_SIZE - 2), INFO_MIDDLE); + ui.drawDialogRect(_surface, Common::Rect(r.left, r.top, r.left + BUTTON_SIZE, r.top + BUTTON_SIZE), raised); + + raised = ui._scrollHighlight != 5; + _surface.fillRect(Common::Rect(r.left + 2, r.bottom - BUTTON_SIZE + 2, r.right - 2, r.bottom - 2), INFO_MIDDLE); + ui.drawDialogRect(_surface, Common::Rect(r.left, r.bottom - BUTTON_SIZE, r.right, r.bottom), raised); + + // Draw the arrows on the scroll buttons + byte color = index ? INFO_BOTTOM + 2 : INFO_BOTTOM; + _surface.hLine(r.right / 2, r.top - 2 + BUTTON_SIZE / 2, r.right / 2, color); + _surface.fillRect(Common::Rect(r.right / 2 - 1, r.top - 1 + BUTTON_SIZE / 2, + r.right / 2 + 1, r.top - 1 + BUTTON_SIZE / 2), color); + _surface.fillRect(Common::Rect(r.right / 2 - 2, r.top + BUTTON_SIZE / 2, + r.right / 2 + 2, r.top + BUTTON_SIZE / 2), color); + _surface.fillRect(Common::Rect(r.right / 2 - 3, r.top + 1 + BUTTON_SIZE / 2, + r.right / 2 + 3, r.top + 1 + BUTTON_SIZE / 2), color); + + color = (index + pageSize) < count ? INFO_BOTTOM + 2 : INFO_BOTTOM; + _surface.fillRect(Common::Rect(r.right / 2 - 3, r.bottom - 1 - BUTTON_SIZE + BUTTON_SIZE / 2, + r.right / 2 + 3, r.bottom - 1 - BUTTON_SIZE + BUTTON_SIZE / 2), color); + _surface.fillRect(Common::Rect(r.right / 2 - 2, r.bottom - 1 - BUTTON_SIZE + 1 + BUTTON_SIZE / 2, + r.right / 2 + 2, r.bottom - 1 - BUTTON_SIZE + 1 + BUTTON_SIZE / 2), color); + _surface.fillRect(Common::Rect(r.right / 2 - 1, r.bottom - 1 - BUTTON_SIZE + 2 + BUTTON_SIZE / 2, + r.right / 2 + 1, r.bottom - 1 - BUTTON_SIZE + 2 + BUTTON_SIZE / 2), color); + _surface.fillRect(Common::Rect(r.right / 2, r.bottom - 1 - BUTTON_SIZE + 3 + BUTTON_SIZE / 2, + r.right / 2, r.bottom - 1 - BUTTON_SIZE + 3 + BUTTON_SIZE / 2), color); + + // Draw the scroll position bar + int barHeight = (_bounds.height() - BUTTON_SIZE * 2) * pageSize / count; + barHeight = CLIP(barHeight, BUTTON_SIZE, _bounds.height() - BUTTON_SIZE * 2); + int barY = (r.height() - BUTTON_SIZE * 2 - barHeight) * index / pageSize + r.top + BUTTON_SIZE; + + _surface.fillRect(Common::Rect(r.left + 2, barY + 2, r.right - 2, barY + barHeight - 3), INFO_MIDDLE); + ui.drawDialogRect(_surface, Common::Rect(r.left, barY, r.right, barY + barHeight), true); +} + +void WidgetBase::handleScrollbarEvents(int index, int pageSize, int count) { + Events &events = *_vm->_events; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + + // If they have selected the sollbar, return with the Scroll Bar Still selected + if (ui._scrollHighlight == 3) + return; + + ui._scrollHighlight = SH_NONE; + + if ((!events._pressed && !events._rightReleased) || !_scroll) + return; + + Common::Rect r(_bounds.right - BUTTON_SIZE - 3, _bounds.top + 3, _bounds.right - 3, _bounds.bottom - 3); + + // Calculate the Scroll Position bar + int barHeight = (_bounds.height() - BUTTON_SIZE * 2) * pageSize / count; + barHeight = CLIP(barHeight, BUTTON_SIZE, _bounds.height() - BUTTON_SIZE * 2); + int barY = (r.height() - BUTTON_SIZE * 2 - barHeight) * index / pageSize + r.top + BUTTON_SIZE; + + if (Common::Rect(r.left, r.top, r.right, r.top + BUTTON_SIZE).contains(mousePos)) + // Mouse on scroll up button + ui._scrollHighlight = SH_SCROLL_UP; + else if (Common::Rect(r.left, r.top + BUTTON_SIZE, r.right, barY).contains(mousePos)) + // Mouse on paging up area (the area of the vertical bar above the thumbnail) + ui._scrollHighlight = SH_PAGE_UP; + else if (Common::Rect(r.left, barY, r.right, barY + barHeight).contains(mousePos)) + // Mouse on scrollbar thumb + ui._scrollHighlight = SH_THUMBNAIL; + else if (Common::Rect(r.left, barY + barHeight, r.right, r.bottom - BUTTON_SIZE).contains(mousePos)) + // Mouse on paging down area (the area of the vertical bar below the thumbnail) + ui._scrollHighlight = SH_PAGE_DOWN; + else if (Common::Rect(r.left, r.bottom - BUTTON_SIZE, r.right, r.bottom).contains(mousePos)) + // Mouse on scroll down button + ui._scrollHighlight = SH_SCROLL_DOWN; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_base.h b/engines/sherlock/tattoo/widget_base.h new file mode 100644 index 0000000000..fcf22cee8e --- /dev/null +++ b/engines/sherlock/tattoo/widget_base.h @@ -0,0 +1,125 @@ +/* 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_WIDGET_BASE_H +#define SHERLOCK_TATTOO_WIDGET_BASE_H + +#include "common/scummsys.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "sherlock/surface.h" + +namespace Sherlock { + +class SherlockEngine; +class ImageFile; + +namespace Tattoo { + +class WidgetBase { +protected: + SherlockEngine *_vm; + Common::Rect _bounds; + Common::Rect _oldBounds; + Surface _surface; + bool _outsideMenu; + bool _scroll; + + /** + * Used by descendent classes to split up long text for display across multiple lines + */ + Common::String splitLines(const Common::String &str, Common::StringArray &lines, int maxWidth, uint maxLines); + + /** + * Ensure that menu is drawn entirely on-screen + */ + void restrictToScreen(); + + /** + * Draw a window frame around the dges of the passed surface + */ + void makeInfoArea(Surface &s); + + /** + * Draw a window frame around the widget's surface + */ + void makeInfoArea(); + + /** + * Draw the scrollbar for the dialog + */ + void drawScrollBar(int index, int pageSize, int count); + + /** + * Handles any events when the mouse is on the scrollbar + */ + void handleScrollbarEvents(int index, int pageSize, int count); + + /** + * Handle drawing the background on the area the widget is going to cover + */ + virtual void drawBackground(); +public: + WidgetBase(SherlockEngine *vm); + virtual ~WidgetBase() {} + + /** + * Returns true if the given widget is active in the user interface's widget list + */ + bool active() const; + + /** + * Erase any previous display of the widget on the screen + */ + virtual void erase(); + + /** + * Update the display of the widget on the screen + */ + virtual void draw(); + + /** + * Used by some descendents to check for keys to mouse the mouse within the dialog + */ + void checkTabbingKeys(int numOptions); + + /** + * Summon the window + */ + virtual void summonWindow(); + + /** + * Close a currently active menu + */ + virtual void banishWindow(); + + /** + * Handle event processing + */ + virtual void handleEvents() {} +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_inventory.cpp b/engines/sherlock/tattoo/widget_inventory.cpp new file mode 100644 index 0000000000..170fb02481 --- /dev/null +++ b/engines/sherlock/tattoo/widget_inventory.cpp @@ -0,0 +1,762 @@ +/* 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/widget_inventory.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define INVENTORY_XSIZE 70 // Width of the box that surrounds inventory items +#define INVENTORY_YSIZE 70 // Height of the box that surrounds inventory items +#define MAX_INV_COMMANDS 10 // Maximum elements in dialog + +WidgetInventoryTooltip::WidgetInventoryTooltip(SherlockEngine *vm, WidgetInventory *owner) : + WidgetTooltipBase(vm), _owner(owner) { +} + +void WidgetInventoryTooltip::setText(const Common::String &str) { + // If no text specified, erase any previously displayed tooltip and free it's surface + if (str.empty()) { + erase(); + _surface.free(); + return; + } + + int width = _surface.stringWidth(str) + 2; + int height = 0; + Common::String line1 = str, line2; + + // See if we need to split it into two lines + if (width > 150) { + // Yes, we do + const char *s = str.c_str(); + const char *space = nullptr; + int dif = 10000; + + while (*s) { + s = strchr(s, ' '); + + if (!s) { + if (!space) { + height = _surface.stringHeight(str) + 2; + } else { + line1 = Common::String(str.c_str(), space); + line2 = Common::String(space + 1); + height = _surface.stringHeight(line1) + _surface.stringHeight(line2) + 4; + } + break; + } else { + line1 = Common::String(str.c_str(), s); + line2 = Common::String(s + 1); + int width1 = _surface.stringWidth(line1); + int width2 = _surface.stringWidth(line2); + + if (ABS(width1 - width2) < dif) { + // Found a split point that results in less overall width + space = s; + dif = ABS(width1 - width2); + width = MAX(width1, width2); + } + + s++; + } + } + } else { + height = _surface.stringHeight(str) + 2; + } + + // Allocate a fresh surface for the new string + _bounds = Common::Rect(width, height); + _surface.create(width, height); + _surface.fill(TRANSPARENCY); + + if (line2.empty()) { + _surface.writeFancyString(str, Common::Point(0, 0), BLACK, INFO_TOP); + } else { + int xp, yp; + + xp = (_bounds.width() - _surface.stringWidth(line1) - 2) / 2; + _surface.writeFancyString(line1, Common::Point(xp, 0), BLACK, INFO_TOP); + + xp = (_bounds.width() - _surface.stringWidth(line2) - 2) / 2; + yp = _surface.stringHeight(line2) + 2; + _surface.writeFancyString(line2, Common::Point(xp, yp), BLACK, INFO_TOP); + } +} + +void WidgetInventoryTooltip::handleEvents() { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + Inventory &inv = *_vm->_inventory; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Scene &scene = *_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + Common::String str; + int select = -1, oldSelect = 999; + Common::String strWith = fixedText.getText(kFixedText_With); + Common::String strUse = fixedText.getText(kFixedText_Use); + + // If we are using an inventory item on an object in the room, display the appropriate text above the mouse cursor + if (_owner->_invVerbMode == 3) { + select = ui._bgFound; + oldSelect = ui._oldBgFound; + + if (select != -1 && (select != oldSelect || (select != -1 && _surface.empty()))) { + // See if we're pointing at a shape or a sprite + if (select < 1000) { + Object &obj = scene._bgShapes[select]; + + if (!obj._description.empty() && !obj._description.hasPrefix(" ")) { + if (_vm->getLanguage() == Common::GR_GRE) { + + if (!_owner->_swapItems) + str = Common::String::format("%s %s %s %s", _owner->_action.c_str(), obj._description.c_str(), + inv[_owner->_invSelect]._name.c_str(), _owner->_verb.c_str()); + else + str = Common::String::format("%s %s %s %s", _owner->_action.c_str(), inv[_owner->_invSelect]._name.c_str(), + obj._description.c_str(), _owner->_verb.c_str()); + } else { + if (_owner->_swapItems) + str = Common::String::format("%s %s %s %s", _owner->_verb.c_str(), obj._description.c_str(), _owner->_action.c_str(), + inv[_owner->_invSelect]._name.c_str()); + else + str = Common::String::format("%s %s %s %s", _owner->_verb.c_str(), inv[_owner->_invSelect]._name.c_str(), + _owner->_action.c_str(), obj._description.c_str()); + } + } + } else { + Person &person = people[ui._bgFound - 1000]; + + if (!person._description.empty() && !person._description.hasPrefix(" ")) { + if (_vm->getLanguage() == Common::GR_GRE) { + if (!_owner->_swapItems) + str = Common::String::format("%s %s %s %s", _owner->_action.c_str(), person._description.c_str(), + inv[_owner->_invSelect]._name.c_str(), _owner->_verb.c_str()); + else + str = Common::String::format("%s %s %s %s", _owner->_action.c_str(), inv[_owner->_invSelect]._name.c_str(), + person._description.c_str(), _owner->_verb.c_str()); + } else { + + if (_owner->_swapItems) + str = Common::String::format("%s %s %s %s", _owner->_verb.c_str(), person._description.c_str(), + _owner->_action.c_str(), inv[_owner->_invSelect]._name.c_str()); + else + str = Common::String::format("%s %s %s %s", _owner->_verb.c_str(), + inv[_owner->_invSelect]._name.c_str(), _owner->_action.c_str(), person._description.c_str()); + } + } + } + } + } else { + Common::Rect r = _owner->_bounds; + r.grow(-3); + + if (r.contains(mousePos)) { + select = (mousePos.x - r.left) / (INVENTORY_XSIZE + 3) + NUM_INVENTORY_SHOWN / 2 * + ((mousePos.y - r.top) / (INVENTORY_YSIZE + 3)) + inv._invIndex; + + if (select >= inv._holdings) { + select = -1; + } else { + oldSelect = _owner->_invSelect; + + if (select != _owner->_invSelect || _surface.empty()) { + + if (_owner->_invMode == 1) { + // See if we were pointing at a shapre or sprite + if (ui._activeObj < 1000) { + Object &obj = scene._bgShapes[ui._activeObj]; + + if (!obj._description.empty() && !obj._description.hasPrefix(" ")) + str = Common::String::format("%s %s %s %s", strUse.c_str(), inv[select]._name.c_str(), + strWith.c_str(), obj._description.c_str()); + } else { + Person &person = people[ui._activeObj - 1000]; + + if (!person._description.empty() && !person._description.hasPrefix(" ")) + str = Common::String::format("%s %s %s %s", strUse.c_str(), inv[select]._name.c_str(), + strWith.c_str(), person._description.c_str()); + } + } else { + if (_owner->_invVerbMode == 2) + str = Common::String::format("%s %s %s %s", strUse.c_str(), inv[_owner->_invSelect]._name.c_str(), + strWith.c_str(), inv[select]._name.c_str()); + else + str = inv[select]._description.c_str(); + } + } + } + } + } + + // See if they are pointing at a different inventory object and we need to + // change the graphics of the Text Tag + if (select != oldSelect || (select != -1 && _surface.empty())) { + // Set the text + setText(str); + + if (_owner->_invVerbMode != 3) + _owner->_invSelect = select; + else + ui._oldBgFound = select; + } else if (select == -1 && oldSelect != -1) { + setText(Common::String()); + return; + } + + if (_owner->_invVerbMode == 3) + // Adjust tooltip to be above the inventory item being shown above the standard cursor + mousePos.y -= events._hotspotPos.y; + + // Update the position of the tooltip + int xs = CLIP(mousePos.x - _bounds.width() / 2, 0, SHERLOCK_SCENE_WIDTH - _bounds.width()); + int ys = CLIP(mousePos.y - _bounds.height(), 0, SHERLOCK_SCREEN_HEIGHT - _bounds.height()); + _bounds.moveTo(xs, ys); +} + +/*----------------------------------------------------------------*/ + +WidgetInventoryVerbs::WidgetInventoryVerbs(SherlockEngine *vm, WidgetInventory *owner) : + WidgetBase(vm), _owner(owner) { + _invVerbSelect = _oldInvVerbSelect = -1; +} + +void WidgetInventoryVerbs::load() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + + // Make the Verb List for this Inventory Item + _inventCommands.clear(); + _inventCommands.push_back(FIXED(Look)); + + // Default the Action word to "with" + _owner->_action = _vm->getLanguage() == Common::GR_GRE ? "" : FIXED(With); + + // Search all the bgshapes for any matching Target Fields + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &obj = scene._bgShapes[idx]; + + if (obj._type != INVALID && obj._type != HIDDEN) { + for (int useNum = 0; useNum < 6; ++useNum) { + if (!obj._use[useNum]._verb.hasPrefix("*") && + !obj._use[useNum]._target.compareToIgnoreCase(inv[_owner->_invSelect]._name)) { + // Make sure the Verb is not already in the list + bool found1 = false; + for (uint cmdNum = 0; cmdNum < _inventCommands.size() && !found1; ++cmdNum) { + if (!_inventCommands[cmdNum].compareToIgnoreCase(obj._use[useNum]._verb)) + found1 = true; + } + + if (!found1) { + _inventCommands.push_back(obj._use[useNum]._verb); + + // Check for any Special Action commands + for (int nameNum = 0; nameNum < 4; ++nameNum) { + if (!scumm_strnicmp(obj._use[useNum]._names[nameNum].c_str(), "*V", 2)) { + if (!scumm_strnicmp(obj._use[useNum]._names[nameNum].c_str(), "*VSWAP", 6)) + _owner->_swapItems = true; + else + _owner->_action = Common::String(obj._use[useNum]._names[nameNum].c_str() + 2); + } + } + } + } + } + } + } + + // Search the NPCs for matches as well + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + for (int useNum = 0; useNum < 2; ++useNum) { + if (!people[idx]._use[useNum]._target.compareToIgnoreCase(inv[_owner->_invSelect]._name) && + !people[idx]._use[useNum]._verb.empty() && !people[idx]._use[useNum]._verb.hasPrefix(" ")) { + bool found1 = false; + for (uint cmdNum = 0; cmdNum < _inventCommands.size() && !found1; ++cmdNum) { + if (!_inventCommands[cmdNum].compareToIgnoreCase(people[idx]._use[cmdNum]._verb)) + found1 = true; + } + + if (!found1) + _inventCommands.push_back(people[idx]._use[useNum]._verb); + } + } + } + + // Finally see if the item itself has a verb + if (!inv[_owner->_invSelect]._verb._verb.empty()) { + // Don't add "Solve" to the Foolscap if it's already been "Solved" + if (inv[_owner->_invSelect]._verb._verb.compareToIgnoreCase(FIXED(Solve)) || !_vm->readFlags(299)) + _inventCommands.push_back(inv[_owner->_invSelect]._verb._verb); + } + + // Now find the widest command in the _inventCommands array + int width = 0; + for (uint idx = 0; idx < _inventCommands.size(); ++idx) + width = MAX(width, _surface.stringWidth(_inventCommands[idx])); + + // Set up bounds for the menu + _bounds = Common::Rect(width + _surface.widestChar() * 2 + 6, + (_surface.fontHeight() + 7) * _inventCommands.size() + 3); + _bounds.moveTo(mousePos.x - _bounds.width() / 2, mousePos.y - _bounds.height() / 2); + + // Create the surface + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + makeInfoArea(); + + // Draw the Verb commands and the lines separating them + ImageFile &images = *ui._interfaceImages; + for (int idx = 0; idx < (int)_inventCommands.size(); ++idx) { + _surface.writeString(_inventCommands[idx], Common::Point((_bounds.width() - + _surface.stringWidth(_inventCommands[idx])) / 2, (_surface.fontHeight() + 7) * idx + 5), INFO_TOP); + + if (idx < (int)_inventCommands.size() - 1) { + _surface.vLine(3, (_surface.fontHeight() + 7) * (idx + 1), _bounds.right - 4, INFO_TOP); + _surface.vLine(3, (_surface.fontHeight() + 7) * (idx + 1) + 1, _bounds.right - 4, INFO_MIDDLE); + _surface.vLine(3, (_surface.fontHeight() + 7) * (idx + 1) + 2, _bounds.right - 4, INFO_BOTTOM); + + _surface.transBlitFrom(images[4], Common::Point(0, (_surface.fontHeight() + 7) * (idx + 1))); + _surface.transBlitFrom(images[5], Common::Point(_bounds.width() - images[5]._width, + (_surface.fontHeight() + 7) * (idx + 1) - 1)); + } + } + + summonWindow(); +} + +void WidgetInventoryVerbs::handleEvents() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Common::Point mousePos = events.mousePos(); + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + TattooEngine &vm = *(TattooEngine *)_vm; + + // Handle changing highlighted verb entry + highlightControls(); + + // See if they want to close the menu (by clicking outside the menu) + Common::Rect innerBounds = _bounds; + innerBounds.grow(-3); + + // Flag is they started pressing outside of the menu + if (events._firstPress && !_bounds.contains(mousePos)) + _outsideMenu = true; + + if (events._released || events._rightReleased || ui._keyState.keycode == Common::KEYCODE_ESCAPE) { + ui._scrollHighlight = SH_NONE; + banishWindow(); + + if ((_outsideMenu && !innerBounds.contains(mousePos)) || ui._keyState.keycode == Common::KEYCODE_ESCAPE) { + _owner->_invVerbMode = 0; + } else if (innerBounds.contains(mousePos)) { + _outsideMenu = false; + + // Check if they are trying to solve the Foolscap puzzle, or looking at the completed puzzle + bool doHangman = !inv[_owner->_invSelect]._name.compareToIgnoreCase(FIXED(Inv6)) && + !_inventCommands[_invVerbSelect].compareToIgnoreCase(FIXED(Solve)); + doHangman |= (!inv[_owner->_invSelect]._name.compareToIgnoreCase(FIXED(Inv6)) || !inv[_owner->_invSelect]._name.compareToIgnoreCase(FIXED(Inv7))) + && _inventCommands[_invVerbSelect].compareToIgnoreCase(FIXED(Look)) && vm.readFlags(299); + + if (doHangman) { + // Close the entire Inventory and return to Standard Mode + _owner->_invVerbMode = 0; + + _owner->_tooltipWidget.banishWindow(); + inv.freeInv(); + + events.clearEvents(); + events.setCursor(ARROW); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + + scene.doBgAnim(); + vm.doHangManPuzzle(); + } else if (_invVerbSelect == 0) { + // They have released the mouse on the Look Verb command, so Look at the inventory item + ui._invLookFlag = true; + inv.freeInv(); + ui._windowOpen = false; + ui._lookPos = mousePos; + ui.printObjectDesc(inv[_owner->_invSelect]._examine, true); + } else { + _owner->_invVerbMode = 3; + ui._oldBgFound = -1; + + // See if the selected Verb with the selected Iventory Item, is to be used by itself + if (!_inventCommands[_invVerbSelect].compareToIgnoreCase(inv[_owner->_invSelect]._verb._verb) || + !inv[_owner->_invSelect]._verb._target.compareToIgnoreCase("*SELF")) { + inv.freeInv(); + + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + events.clearEvents(); + ui.checkAction(inv[_owner->_invSelect]._verb, 2000); + } else { + _owner->_verb = _inventCommands[_invVerbSelect]; + } + + // If we are still in Inventory Mode, setup the graphic to float in front of the mouse cursor + if (ui._menuMode == INV_MODE) { + // Add the inventory item to the cursor + ImageFrame &imgFrame = (*inv._invShapes[_owner->_invSelect - inv._invIndex])[0]; + events.setCursor(ARROW, imgFrame._frame); + + // Close the inventory dialog without banishing it, so it can keep getting events + // to handle tooltips and actually making the selection of what object to use them item on + inv.freeInv(); + _owner->_surface.free(); + } + } + } + } +} + +void WidgetInventoryVerbs::highlightControls() { + Events &events = *_vm->_events; + Common::Point mousePos = events.mousePos(); + + Common::Rect innerBounds = _bounds; + innerBounds.grow(-3); + + // Set the highlighted verb + _invVerbSelect = -1; + if (innerBounds.contains(mousePos)) + _invVerbSelect = (mousePos.y - _bounds.top - 3) / (_surface.fontHeight() + 7); + + // See if the highlighted verb has changed + if (_invVerbSelect != _oldInvVerbSelect) { + // Draw the list again, with the new highlighting + for (int idx = 0; idx < (int)_inventCommands.size(); ++idx) { + byte color = (idx == _invVerbSelect) ? COMMAND_HIGHLIGHTED : INFO_TOP; + _surface.writeString(_inventCommands[idx], Common::Point( + (_bounds.width() - _surface.stringWidth(_inventCommands[idx])) / 2, + (_surface.fontHeight() + 7) * idx + 5), color); + } + + _oldInvVerbSelect = _invVerbSelect; + } +} + +/*----------------------------------------------------------------*/ + +WidgetInventory::WidgetInventory(SherlockEngine *vm) : WidgetBase(vm), + _tooltipWidget(vm, this), _verbList(vm, this) { + _invMode = 0; + _invVerbMode = 0; + _invSelect = _oldInvSelect = -1; + _selector = _oldSelector = -1; + _dialogTimer = -1; + _swapItems = false; +} + +void WidgetInventory::load(int mode) { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + + if (mode == 3) { + mode = 2; + mousePos = Common::Point(screen._currentScroll.x + SHERLOCK_SCREEN_WIDTH / 2, SHERLOCK_SCREEN_HEIGHT / 2); + } + + if (mode != 0) + _invMode = mode; + _invVerbMode = 0; + _invSelect = _oldInvSelect = -1; + _selector = _oldSelector = -1; + _dialogTimer = -1; + + if (mode == 0) { + banishWindow(); + } else { + _bounds = Common::Rect((INVENTORY_XSIZE + 3) * NUM_INVENTORY_SHOWN / 2 + BUTTON_SIZE + 6, + (INVENTORY_YSIZE + 3) * 2 + 3); + _bounds.moveTo(mousePos.x - _bounds.width() / 2, mousePos.y - _bounds.height() / 2); + } + + // Ensure menu will be on-screen + restrictToScreen(); + + // Load the inventory data + inv.loadInv(); + + // Redraw the inventory menu on the widget surface + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + + // Draw the window background and then the inventory on top of it + makeInfoArea(_surface); + drawBars(); + drawInventory(); +} + +void WidgetInventory::drawBars() { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + ImageFile &images = *ui._interfaceImages; + int x; + + _surface.hLine(3, INVENTORY_YSIZE + 3, _bounds.width() - 4, INFO_TOP); + _surface.hLine(3, INVENTORY_YSIZE + 4, _bounds.width() - 4, INFO_MIDDLE); + _surface.hLine(3, INVENTORY_YSIZE + 5, _bounds.width() - 4, INFO_BOTTOM); + _surface.transBlitFrom(images[4], Common::Point(0, INVENTORY_YSIZE + 2)); + + for (int idx = 1; idx <= NUM_INVENTORY_SHOWN / 2; ++idx) { + x = idx * (INVENTORY_XSIZE + 3); + + _surface.vLine(x, 3, _bounds.height() - 4, INFO_TOP); + _surface.vLine(x + 1, 3, _bounds.height() - 4, INFO_MIDDLE); + _surface.vLine(x + 2, 3, _bounds.height() - 4, INFO_BOTTOM); + + _surface.transBlitFrom(images[6], Common::Point(x - 1, 1)); + _surface.transBlitFrom(images[7], Common::Point(x - 1, _bounds.height() - 4)); + _surface.transBlitFrom(images[6], Common::Point(x - 1, INVENTORY_YSIZE + 5)); + _surface.transBlitFrom(images[7], Common::Point(x - 1, INVENTORY_YSIZE + 2)); + } + + _surface.hLine(x + 2, INVENTORY_YSIZE + 2, INVENTORY_YSIZE + 8, INFO_BOTTOM); +} + +void WidgetInventory::drawInventory() { + Inventory &inv = *_vm->_inventory; + + // TODO: Refactor _invIndex into this widget class + for (int idx = 0, itemId = inv._invIndex; idx < NUM_INVENTORY_SHOWN; ++idx, ++itemId) { + // Figure out the drawing position + Common::Point pt(3 + (INVENTORY_XSIZE + 3) * (idx % (NUM_INVENTORY_SHOWN / 2)), + 3 + (INVENTORY_YSIZE + 3) * (idx / (NUM_INVENTORY_SHOWN / 2))); + + // Draw the box to serve as the background for the item + _surface.hLine(pt.x + 1, pt.y, pt.x + INVENTORY_XSIZE - 2, TRANSPARENCY); + _surface.fillRect(Common::Rect(pt.x, pt.y + 1, pt.x + INVENTORY_XSIZE, pt.y + INVENTORY_YSIZE - 1), TRANSPARENCY); + _surface.hLine(pt.x + 1, pt.y + INVENTORY_YSIZE - 1, pt.x + INVENTORY_XSIZE - 2, TRANSPARENCY); + + // Draw the item + if (itemId < inv._holdings) { + ImageFrame &img = (*inv._invShapes[idx])[0]; + _surface.transBlitFrom(img, Common::Point(pt.x + (INVENTORY_XSIZE - img._width) / 2, + pt.y + (INVENTORY_YSIZE - img._height) / 2)); + } + } + + drawScrollBar(inv._invIndex, NUM_INVENTORY_SHOWN, inv._holdings); +} + +void WidgetInventory::handleEvents() { + TattooEngine &vm = *(TattooEngine *)_vm; + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + People &people = *_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + + if (_invVerbMode == 1) + checkTabbingKeys(MAX_INV_COMMANDS); + else if (_invVerbMode == 0) + checkInvTabbingKeys(); + + if (_invVerbMode != 1) + _tooltipWidget.handleEvents(); + + // Flag is they started pressing outside of the menu + if (events._firstPress && !_bounds.contains(mousePos)) + _outsideMenu = true; + + if (_invVerbMode != 3) + highlightControls(); + + // See if they released a mouse button button + if (events._released || events._rightReleased || ui._keyState.keycode == Common::KEYCODE_ESCAPE) { + _dialogTimer = -1; + ui._scrollHighlight = SH_NONE; + + // See if they have a Verb List open for an Inventry Item + if (_invVerbMode == 1) + return; + + if (_invVerbMode == 3) { + // Selecting object after inventory verb has been selected + _tooltipWidget.banishWindow(); + close(); + + if (ui._keyState.keycode != Common::KEYCODE_ESCAPE) { + // If user pointed at an item, use the selected inventory item with this item + bool found = false; + if (ui._bgFound != -1) { + if (ui._personFound) { + for (int idx = 0; idx < 2; ++idx) { + if (!people[ui._bgFound - 1000]._use[idx]._verb.compareToIgnoreCase(_verb) && + !people[ui._bgFound - 1000]._use[idx]._target.compareToIgnoreCase(_invTarget)) { + ui.checkAction(people[ui._bgFound - 1000]._use[idx], ui._bgFound); + found = true; + } + } + } else { + for (int idx = 0; idx < 6; ++idx) { + if (!ui._bgShape->_use[idx]._verb.compareToIgnoreCase(_verb) && + !ui._bgShape->_use[idx]._target.compareToIgnoreCase(_invTarget)) { + ui.checkAction(ui._bgShape->_use[idx], ui._bgFound); + found = true; + } + } + } + } + + if (!found) + ui.putMessage("%s", FIXED(NoEffect)); + } + } else if ((_outsideMenu && !_bounds.contains(mousePos)) || ui._keyState.keycode == Common::KEYCODE_ESCAPE) { + // Want to close the window (clicked outside of it). So close the window and return to Standard + close(); + + } else if (_bounds.contains(mousePos)) { + // Mouse button was released inside the inventory window + _outsideMenu = false; + + // See if they are pointing at one of the inventory items + if (_invSelect != -1) { + // See if they are in Use Obj with Inv. Mode (they right clicked on an item + // in the room and selected "Use with Inv.") + if (_invMode == 1) { + _tooltipWidget.banishWindow(); + banishWindow(); + + // See if the item in the room that they started with was a person + bool found = false; + if (ui._activeObj >= 1000) { + // Object was a person, activate anything in his two verb fields + for (int idx = 0; idx < 2; ++idx) { + if (!people[ui._activeObj - 1000]._use[idx]._target.compareToIgnoreCase(inv[_invSelect]._name)) { + ui.checkAction(people[ui._activeObj - 1000]._use[idx], ui._activeObj); + found = true; + } + } + } else { + // Object was a regular object, activate anything in its verb fields + for (int idx = 0; idx < 6; ++idx) { + if (!scene._bgShapes[ui._activeObj]._use[idx]._target.compareToIgnoreCase(inv[_invSelect]._name)) { + ui.checkAction(scene._bgShapes[ui._activeObj]._use[idx], ui._activeObj); + found = true; + } + } + } + if (!found) + ui.putMessage("%s", FIXED(NoEffect)); + + } else { + // See if they right clicked on an item + if (events._rightReleased) { + _invVerbMode = 1; + _verbList._oldInvVerbSelect = -1; + _tooltipWidget.banishWindow(); + + // Keep track of the name of the inventory object so we can check it against the target fields + // of verbs when we activate it + _invTarget = inv[_invSelect]._name; + _swapItems = false; + + _verbList.load(); + } else { + // They left clicked on an inventory item, so Look at it + + // Check if they are looking at the solved Foolscap + if ((!inv[_invSelect]._name.compareToIgnoreCase(FIXED(Inv6)) || !inv[_invSelect]._name.compareToIgnoreCase(FIXED(Inv7))) + && vm.readFlags(299)) { + banishWindow(); + _tooltipWidget.erase(); + + _invVerbMode = 0; + inv.freeInv(); + + events.clearEvents(); + events.setCursor(ARROW); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + + scene.doBgAnim(); + vm.doHangManPuzzle(); + } else { + ui._invLookFlag = true; + inv.freeInv(); + + _tooltipWidget.banishWindow(); + ui._windowOpen = false; + ui._lookPos = mousePos; + ui.printObjectDesc(inv[_invSelect]._examine, true); + } + } + } + } + } + } +} + +void WidgetInventory::checkInvTabbingKeys() { +} + +void WidgetInventory::highlightControls() { + // TODO +} + +void WidgetInventory::banishWindow() { + WidgetBase::banishWindow(); + + _verbList.banishWindow(); +} + +void WidgetInventory::draw() { + WidgetBase::draw(); + _tooltipWidget.draw(); +} + +void WidgetInventory::erase() { + WidgetBase::erase(); + _tooltipWidget.erase(); +} + +void WidgetInventory::close() { + Events &events = *_vm->_events; + Inventory &inv = *_vm->_inventory; + TattooScene &scene = *(TattooScene *)_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + banishWindow(); + inv.freeInv(); + events.clearEvents(); + + events.setCursor(ARROW); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_inventory.h b/engines/sherlock/tattoo/widget_inventory.h new file mode 100644 index 0000000000..53bc2038e0 --- /dev/null +++ b/engines/sherlock/tattoo/widget_inventory.h @@ -0,0 +1,159 @@ +/* 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_WIDGET_INVENTORY_H +#define SHERLOCK_TATTOO_WIDGET_INVENTORY_H + +#include "common/scummsys.h" +#include "sherlock/tattoo/widget_base.h" +#include "sherlock/tattoo/widget_tooltip.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +#define NUM_INVENTORY_SHOWN 8 // Number of Inventory Items Shown + +class WidgetInventory; + +class WidgetInventoryTooltip: public WidgetTooltipBase { +private: + WidgetInventory *_owner; +protected: + /** + * Overriden from base class, since tooltips have a completely transparent background + */ + virtual void drawBackground() {} +public: + WidgetInventoryTooltip(SherlockEngine *vm, WidgetInventory *owner); + virtual ~WidgetInventoryTooltip() {} + + /** + * Set the text for the tooltip + */ + void setText(const Common::String &str); + + /** + * Handle updating the tooltip state + */ + virtual void handleEvents(); +}; + +class WidgetInventoryVerbs : public WidgetBase { +private: + WidgetInventory *_owner; + Common::StringArray _inventCommands; + + void highlightControls(); +public: + int _invVerbSelect, _oldInvVerbSelect; +public: + WidgetInventoryVerbs(SherlockEngine *vm, WidgetInventory *owner); + virtual ~WidgetInventoryVerbs() {} + + void load(); + + /** + * Handle updating the tooltip state + */ + virtual void handleEvents(); +}; + +class WidgetInventory: public WidgetBase { + friend class WidgetInventoryTooltip; + friend class WidgetInventoryVerbs; +private: + int _invVerbMode; + int _selector, _oldSelector; + int _invSelect, _oldInvSelect; + int _dialogTimer; + WidgetInventoryTooltip _tooltipWidget; + WidgetInventoryVerbs _verbList; + bool _swapItems; + Surface _menuSurface; + Common::String _invTarget; + + /** + * Draw the bars within the dialog + */ + void drawBars(); + + /** + * Check for keys to mouse the mouse within the inventory dialog + */ + void checkInvTabbingKeys(); + + /** + * Highlights the controls + */ + void highlightControls(); +public: + int _invMode; + Common::String _action; + Common::String _verb; +public: + WidgetInventory(SherlockEngine *vm); + virtual ~WidgetInventory() {} + + /** + * Load the inventory window + */ + void load(int mode); + + /** + * Draw the inventory on the surface + */ + void drawInventory(); + + /** + * Close the window + */ + void close(); + + /** + * Handle events whilst the widget is on-screen + */ + virtual void handleEvents(); + + /** + * Close a currently active menu + */ + virtual void banishWindow(); + + /** + * Erase any previous display of the widget on the screen + */ + virtual void erase(); + + /** + * Update the display of the widget on the screen + */ + virtual void draw(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_lab.cpp b/engines/sherlock/tattoo/widget_lab.cpp new file mode 100644 index 0000000000..cc0bae0e90 --- /dev/null +++ b/engines/sherlock/tattoo/widget_lab.cpp @@ -0,0 +1,193 @@ +/* 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/widget_lab.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +WidgetLab::WidgetLab(SherlockEngine *vm) : WidgetBase(vm) { + _labObject = nullptr; +} + +void WidgetLab::summonWindow() { + WidgetBase::summonWindow(); + _labObject = nullptr; +} + +void WidgetLab::handleEvents() { + Events &events = *_vm->_events; + Scene &scene = *_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + WidgetBase::handleEvents(); + bool noDesc = false; + + // Handle drawing tooltips. If the user is dragging a lab item, display a tooltip for using the item + // on another. Otherwise, fall back on showing standard tooltips + if (events.getCursor() == INVALID_CURSOR) + displayLabNames(); + else + ui.displayObjectNames(); + + // See if they've released a mouse button to do an action + if (events._released || events._rightReleased) { + // See if the mouse was released in an exit/arrow zone (ie. the "Exit" button) + ui._exitZone = -1; + if (ui._arrowZone != -1 && events._released) + ui._exitZone = ui._arrowZone; + + // Turn any current tooltip off + if (ui._arrowZone == -1 || events._rightReleased) + ui._tooltipWidget.setText(""); + + if (ui._bgFound != -1) { + if (ui._bgShape->_description.hasPrefix(" ") || ui._bgShape->_description.empty()) + noDesc = true; + } else { + noDesc = true; + } + + if (events._rightReleased) { + // If the player is dragging an object around, restore it to its previous location and reset the cursor + if (_labObject) { + _labObject->toggleHidden(); + + // Toggle any other objects (like shadows) tied to this object + for (int idx = 0; idx < 6; ++idx) { + if (!_labObject->_use[idx]._target.compareToIgnoreCase("Toggle")) { + for (int nameNum = 0; nameNum < 4; ++nameNum) + scene.toggleObject(_labObject->_use[idx]._names[nameNum]); + } + } + + events.setCursor(ARROW); + } + + // Show the command list for this object + ui._verbsWidget.load(!noDesc); + } else if (!noDesc) { + // The player has released on an object, see if they had an object selected + // that will be used with this new object + if (_labObject) { + // See if the dragged object can be used with the new object + for (int idx = 0; idx < 6; ++idx) { + // See if the name of the dragged object is in any of the Target + // fields of the verbs for the new object + if (!_labObject->_name.compareToIgnoreCase(ui._bgShape->_use[idx]._target.c_str())) { + // This object can be used, so use it + ui.checkAction(ui._bgShape->_use[idx], ui._bgFound); + ui._activeObj = -1; + } + } + + // Restore the dragged object to its previous location + _labObject->toggleHidden(); + + // Toggle any other objects (like shadows) tied to this object + for (int idx = 0; idx < 6; ++idx) { + if (!_labObject->_use[idx]._target.compareToIgnoreCase("Toggle")) { + for (int nameNum = 0; nameNum < 4; ++nameNum) + scene.toggleObject(_labObject->_use[idx]._names[nameNum]); + } + } + } else if (!ui._bgShape->_name.compareToIgnoreCase("Exit")) { + // Eexecut the Exit button's script, which will leave the scene + ui.lookAtObject(); + } + } else { + // The player has released the mouse while NOT over an object. If theu were dragging an object + // around with the mouse, restore it to its previous location and reset the cursor + if (_labObject) { + _labObject->toggleHidden(); + + // Toggle any other objects (like shadows) tied to this object + for (int idx = 0; idx < 6; ++idx) { + if (!_labObject->_use[idx]._target.compareToIgnoreCase("Toggle")) { + for (int nameNum = 0; nameNum < 4; ++nameNum) + scene.toggleObject(_labObject->_use[idx]._names[nameNum]); + } + } + + events.setCursor(ARROW); + } + } + } else if (events._pressed) { + if (!_labObject) { + // If the mouse is over an object and the object is not SOLID, then we need to pick this object + // up so the player can move it around + if (ui._bgFound != -1) { + // Check if the object is set as SOLID, you can't pick up Solid items + if (ui._bgShape->_aType != SOLID && ui._bgShape->_type != NO_SHAPE) { + // Save a reference to the object about to be dragged + _labObject = ui._bgShape; + + // Set the mouse cursor to the object + Graphics::Surface &img = _labObject->_imageFrame->_frame; + events.setCursor(img, img.w / 2, img.h / 2); + + // Hide this object until they are done with it (releasing it) + _labObject->toggleHidden(); + + // Toggle any other objects (like shadows) tied to this object + for (int idx = 0; idx < 6; ++idx) { + if (!_labObject->_use[idx]._target.compareToIgnoreCase("Toggle")) { + for (int nameNum = 0; nameNum < 4; ++nameNum) + scene.toggleObject(_labObject->_use[idx]._names[nameNum]); + } + } + } + } + } + } +} + +void WidgetLab::displayLabNames() { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + + // See if thay are pointing at a different object and we need to change the tooltip + if (ui._bgFound != ui._oldBgFound) { + // See if there is a new object to be displayed + if (ui._bgFound == -1) { + ui._tooltipWidget.setText(""); + } else { + Common::String str = Common::String::format("%s %s %s %s", FIXED(Use), _labObject->_description.c_str(), + FIXED(With), ui._bgShape->_description.c_str()); + + // Make sure that the Object has a name + if (!ui._bgShape->_description.empty() && !ui._bgShape->_description.hasPrefix(" ")) { + ui._tooltipWidget.setText(str); + } else { + ui._tooltipWidget.setText(""); + } + } + } + + ui._oldArrowZone = ui._arrowZone; +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_lab.h b/engines/sherlock/tattoo/widget_lab.h new file mode 100644 index 0000000000..2f19200b81 --- /dev/null +++ b/engines/sherlock/tattoo/widget_lab.h @@ -0,0 +1,66 @@ +/* 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_WIDGET_LAB_H +#define SHERLOCK_TATTOO_WIDGET_LAB_H + +#include "common/scummsys.h" +#include "sherlock/tattoo/widget_base.h" +#include "sherlock/objects.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +class WidgetLab: public WidgetBase { +private: + Object *_labObject; + + /** + * Display tooltips of an object being dragged along with any object the dragged + * object is currently over + */ + void displayLabNames(); +public: + Common::String _remainingText; +public: + WidgetLab(SherlockEngine *vm); + virtual ~WidgetLab() {} + + /** + * Summon the window + */ + virtual void summonWindow(); + + /** + * Handle event processing + */ + virtual void handleEvents(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_talk.cpp b/engines/sherlock/tattoo/widget_talk.cpp new file mode 100644 index 0000000000..5190773dd1 --- /dev/null +++ b/engines/sherlock/tattoo/widget_talk.cpp @@ -0,0 +1,529 @@ +/* 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/widget_talk.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_journal.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_talk.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define STATEMENT_NUM_X 6 +#define NUM_VISIBLE_TALK_LINES 6 + +WidgetTalk::WidgetTalk(SherlockEngine *vm) : WidgetBase(vm) { + _talkScrollIndex = 0; + _selector = _oldSelector = -1; + _talkTextX = 0; + _dialogTimer = 0; +} + +void WidgetTalk::getTalkWindowSize() { + TattooTalk &talk = *(TattooTalk *)_vm->_talk; + int width, height; + + // See how many statements are going to be available + int numStatements = 0; + for (uint idx = 0; idx < talk._statements.size(); ++idx) { + if (talk._statements[idx]._talkMap != -1) + ++numStatements; + } + + width = SHERLOCK_SCREEN_WIDTH * 2 / 3; + + // Split up the questions into separate strings for each line + _bounds = Common::Rect(width, 1); + setStatementLines(); + + // Make sure that the window does not get too big + if (_statementLines.size() < 7) { + height = (_surface.fontHeight() + 1) * _statementLines.size() + 9; + _scroll = false; + } else { + // Set up the height to a constrained amount, and add extra width for the scrollbar + width += BUTTON_SIZE + 3; + height = (_surface.fontHeight() + 1) * 6 + 9; + _scroll = true; + } + + _bounds = Common::Rect(width, height); +} + +void WidgetTalk::load() { + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + + // Figure out the window size + getTalkWindowSize(); + + // Place the window centered above the player + Common::Point pt; + int scaleVal = scene.getScaleVal(people[HOLMES]._position); + pt.x = people[HOLMES]._position.x / FIXED_INT_MULTIPLIER - _bounds.width() / 2; + + if (scaleVal == SCALE_THRESHOLD) { + pt.x += people[0].frameWidth() / 2; + pt.y = people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES].frameHeight() + - _bounds.height() - _surface.fontHeight(); + } else { + pt.x += people[HOLMES]._imageFrame->sDrawXSize(scaleVal) / 2; + pt.y = people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES]._imageFrame->sDrawYSize(scaleVal) + - _bounds.height() - _surface.fontHeight(); + } + + _bounds.moveTo(pt); + + // Set up the surface + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + + // Form the background for the new window + makeInfoArea(); +} + +void WidgetTalk::handleEvents() { + Events &events = *_vm->_events; + TattooJournal &journal = *(TattooJournal *)_vm->_journal; + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Sound &sound = *_vm->_sound; + TattooTalk &talk = *(TattooTalk *)_vm->_talk; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + Common::KeyCode keycode = ui._keyState.keycode; + bool hotkey = false; + bool callParrotFile = false; + + // Handle scrollbar events + ScrollHighlight oldHighlight = ui._scrollHighlight; + handleScrollbarEvents(_talkScrollIndex, NUM_VISIBLE_TALK_LINES, _statementLines.size()); + + // If the highlight has changed, redraw the scrollbar + if (ui._scrollHighlight != oldHighlight) + render(HL_SCROLLBAR_ONLY); + + if (ui._scrollHighlight != SH_NONE || keycode == Common::KEYCODE_HOME || keycode == Common::KEYCODE_END + || keycode == Common::KEYCODE_PAGEUP || keycode == Common::KEYCODE_PAGEDOWN) { + int scrollIndex = _talkScrollIndex; + + // Check for the scrollbar + if (ui._scrollHighlight == SH_THUMBNAIL) { + int yp = mousePos.y; + yp = CLIP(yp, _bounds.top + BUTTON_SIZE + 3, _bounds.bottom - BUTTON_SIZE - 3); + + // Calculate the line number that corresponds to the position that the mouse is on the scrollbar + int lineNum = (yp - _bounds.top - BUTTON_SIZE - 3) * 100 / (_bounds.height() - BUTTON_SIZE * 2 - 6) + * _statementLines.size() / 100 - 3; + + // If the new position would place part of the text outsidethe text window, adjust it so it doesn't + if (lineNum < 0) + lineNum = 0; + else if (lineNum + NUM_VISIBLE_TALK_LINES > (int)_statementLines.size()) { + lineNum = (int)_statementLines.size() - NUM_VISIBLE_TALK_LINES; + + // Make sure it's not below zero now + if (lineNum < 0) + lineNum = 0; + } + + _talkScrollIndex = lineNum; + } + + // Get the current frame so we can check the scroll timer against it + uint32 frameNum = events.getFrameCounter(); + + if (frameNum > _dialogTimer) { + // Set the timeout for the next scroll if the mouse button remains held down + _dialogTimer = (_dialogTimer == 0) ? frameNum + NUM_VISIBLE_TALK_LINES : frameNum + 1; + + // Check for Scroll Up + if (ui._scrollHighlight == SH_SCROLL_UP && _talkScrollIndex) + --_talkScrollIndex; + + // Check for Page Up + if ((ui._scrollHighlight == SH_PAGE_UP || keycode == Common::KEYCODE_PAGEUP) && _talkScrollIndex) + _talkScrollIndex -= NUM_VISIBLE_TALK_LINES; + + // Check for Page Down + if ((ui._scrollHighlight == SH_PAGE_DOWN || keycode == Common::KEYCODE_PAGEDOWN) + && (_talkScrollIndex + NUM_VISIBLE_TALK_LINES < (int)_statementLines.size())) { + _talkScrollIndex += 6; + if (_talkScrollIndex + NUM_VISIBLE_TALK_LINES >(int)_statementLines.size()) + _talkScrollIndex = _statementLines.size() - NUM_VISIBLE_TALK_LINES; + } + + // Check for Scroll Down + if (ui._scrollHighlight == SH_SCROLL_DOWN && (_talkScrollIndex + NUM_VISIBLE_TALK_LINES < (int)_statementLines.size())) + _talkScrollIndex++; + } + + if (keycode == Common::KEYCODE_END) + _talkScrollIndex = _statementLines.size() - NUM_VISIBLE_TALK_LINES; + + if (_talkScrollIndex < 0 || keycode == Common::KEYCODE_HOME) + _talkScrollIndex = 0; + + // Only redraw the window if the the scrollbar position has changed + if (scrollIndex != _talkScrollIndex) { + _surface.fillRect(Common::Rect(4, 5, _surface.w() - BUTTON_SIZE - 8, _surface.h() - 4), TRANSPARENCY); + render(HL_NO_HIGHLIGHTING); + } + } + + // Flag if they started pressing outside of the window + if (events._firstPress && !_bounds.contains(mousePos)) + _outsideMenu = true; + + // Check for which statement they are pointing at + _selector = -1; + if (ui._scrollHighlight == SH_NONE) { + if (Common::Rect(_bounds.left, _bounds.top + 5, _bounds.right - 3, _bounds.bottom - 5).contains(mousePos)) { + if (_scroll) { + // Disregard the scrollbar when setting the statement number + if (!Common::Rect(_bounds.right - BUTTON_SIZE, _bounds.top, _bounds.right, _bounds.bottom).contains(mousePos)) + _selector = (mousePos.y - _bounds.top - 5) / (_surface.fontHeight() + 1) + _talkScrollIndex; + } else { + _selector = (mousePos.y - _bounds.top - 5) / (_surface.fontHeight() + 1); + } + + // Now translate the line number of the displayed line into the appropriate + // Statement number or set it to 255 to indicate no Statement selected + if (_selector >= 0 && _selector < (int)_statementLines.size()) + _selector = _statementLines[_selector]._num; + else + _selector = -1; + } + } + + // Check for the tab keys + if (keycode == Common::KEYCODE_TAB && ui._scrollHighlight == SH_NONE) { + if (_selector == -1) { + _selector = _statementLines[_scroll ? _talkScrollIndex : 0]._num; + + events.warpMouse(Common::Point(_bounds.right - BUTTON_SIZE - 10, _bounds.top + _surface.fontHeight() + 2)); + } else { + if (ui._keyState.flags & Common::KBD_SHIFT) { + _selector = (mousePos.y - _bounds.top - 5) / (_surface.fontHeight() + 1) + _talkScrollIndex; + if (_statementLines[_selector]._num == _statementLines[_talkScrollIndex]._num) { + _selector = (_bounds.height() - 10) / (_surface.fontHeight() + 1) + _talkScrollIndex; + } else { + int idx = _selector; + do { + --_selector; + } while (_selector > 0 && _statementLines[idx]._num == _statementLines[_selector]._num); + } + + int idx = _selector; + while ((_statementLines[idx]._num == _statementLines[_selector - 1]._num) && (_selector > _talkScrollIndex)) + --_selector; + } else { + _selector = (mousePos.y - _bounds.top - 5) / (_surface.fontHeight() + 1) + _talkScrollIndex; + if (_statementLines[_selector]._num == _statementLines[(_bounds.height() - 10) / (_surface.fontHeight() + 1) + _talkScrollIndex]._num) { + _selector = _talkScrollIndex; + } else { + int idx = _selector; + do { + ++_selector; + } while (_selector < (int)_statementLines.size() && _statementLines[idx]._num == _statementLines[_selector]._num); + } + } + + events.warpMouse(Common::Point(mousePos.x, _bounds.top + _surface.fontHeight() + 2 + (_surface.fontHeight() + 1) + * (_selector - _talkScrollIndex))); + _selector = _statementLines[_selector]._num; + } + } + + // Handle selecting a talk entry if a numeric key has been pressed + if (keycode >= Common::KEYCODE_1 && keycode <= Common::KEYCODE_9) { + int x = 0, t = 0, y = 0; + + do { + if (y == (keycode - Common::KEYCODE_1)) { + _selector = _statementLines[t]._num; + _outsideMenu = false; + hotkey = true; + break; + } + + ++t; + if (_statementLines[x]._num != _statementLines[t]._num) { + x = t; + ++y; + } + } while (t < (int)_statementLines.size()); + } + + // Display the selected statement highlighted and reset the last statement. + if (_selector != _oldSelector) { + render(HL_CHANGED_HIGHLIGHTS); + _oldSelector = _selector; + } + + if (events._released || events._rightReleased || keycode == Common::KEYCODE_ESCAPE || hotkey) { + events.clearEvents(); + _dialogTimer = 0; + ui._scrollHighlight = SH_NONE; + + // See if they want to close the menu (click outside the window or Escape pressed) + if ((_outsideMenu && !_bounds.contains(mousePos)) || keycode == Common::KEYCODE_ESCAPE) { + if (keycode == Common::KEYCODE_ESCAPE) + _selector = -1; + + talk.freeTalkVars(); + talk.pullSequence(); + + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) { + while (!people[idx]._pathStack.empty()) + people[idx].pullNPCPath(); + } + } + + banishWindow(); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + + if (scene._currentScene == 52) + callParrotFile = true; + } + + _outsideMenu = false; + + // See if they have selected a statement to say + if (_selector != -1) { + if (!talk._talkHistory[talk._converseNum][_selector] && talk._statements[_selector]._journal) + journal.record(talk._converseNum, _selector); + talk._talkHistory[talk._converseNum][_selector] = true; + + banishWindow(); + talk._speaker = _vm->readFlags(FLAG_PLAYER_IS_HOLMES) ? HOLMES : WATSON; + _scroll = false; + const byte *msg = (const byte *)talk._statements[_selector]._statement.c_str(); + talk.talkInterface(msg); + + if (sound._speechOn) + sound._talkSoundFile += Common::String::format("%02dA", _selector + 1); + + int msgLen = MAX((int)talk._statements[_selector]._statement.size(), 160); + people.setTalkSequence(talk._speaker); + + talk.waitForMore(msgLen); + if (talk._talkToAbort) + return; + + people.setListenSequence(talk._speaker); + + do { + talk._scriptSelect = _selector; + talk._speaker = talk._talkTo; + + // Make a copy of the reply (since talkTo can reload the statements list), and call talkTo + Common::String reply = talk._statements[_selector]._reply; + talk.doScript(reply); + + // Reset the misc field in case any people changed their sequences + for (int idx = 0; idx < MAX_CHARACTERS; ++idx) + people[idx]._misc = 0; + + if (!talk._talkToAbort) { + 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(); + } + + // See if there is another talk file linked to this. + if (!talk._statements[_selector]._linkFile.empty() && !talk._scriptMoreFlag) { + Common::String linkFile = talk._statements[_selector]._linkFile; + talk.freeTalkVars(); + talk.loadTalkFile(linkFile); + + _talkScrollIndex = 0; + int select = -1; + _selector = _oldSelector = -1; + + // Find the first statement that has all it's flags set correctly + for (uint idx = 0; idx < talk._statements.size() && select == -1; ++select) { + if (!talk._statements[idx]._talkMap) + select = idx; + } + + if (select == -1) { + talk.freeTalkVars(); + ui.putMessage("%s", FIXED(NothingToSay)); + return; + } + + // See is the new statement is in stealth mode + talk._talkStealth = (talk._statements[select]._statement.hasPrefix("^")) ? 2 : 0; + + // See if the new file is a standard file, a reply first file, or a Stealth Mode file + if (!talk._statements[select]._statement.hasPrefix("*") && !talk._statements[select]._statement.hasPrefix("^")) { + load(); + summonWindow(); + + setStatementLines(); + render(HL_NO_HIGHLIGHTING); + break; + } else { + _selector = select; + + if (!talk._talkHistory[talk._converseNum][_selector] && talk._statements[_selector]._journal) + journal.record(talk._converseNum, _selector); + + talk._talkHistory[talk._converseNum][_selector] = true; + } + } else { + talk.freeTalkVars(); + talk.pullSequence(); + + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) + while (!people[idx]._pathStack.empty()) + people[idx].pullNPCPath(); + } + + ui.banishWindow(); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + break; + } + } else { + break; + } + } while (!_vm->shouldQuit()); + + events.clearEvents(); + + // Now, if a script was pushed onto the script stack, restore them to allow the previous script to continue. + talk.popStack(); + } + } + + if (callParrotFile) + talk.talkTo("POUT52A"); +} + +void WidgetTalk::render(Highlight highlightMode) { + TattooTalk &talk = *(TattooTalk *)_vm->_talk; + int yp = 5; + int statementNum = 1; + byte color; + + if (highlightMode != HL_SCROLLBAR_ONLY) { + // Draw all the statements + // Check whether scrolling has occurred, and if so, figure out what the starting + // number for the first visible statement will be + if (_talkScrollIndex) { + for (int idx = 1; idx <= _talkScrollIndex; ++idx) { + if (_statementLines[idx - 1]._num != _statementLines[idx]._num) + ++statementNum; + } + } + + // Main drawing loop + for (uint idx = _talkScrollIndex; idx < _statementLines.size() && yp < (_bounds.height() - _surface.fontHeight()); ++idx) { + if (highlightMode == HL_NO_HIGHLIGHTING || _statementLines[idx]._num == _selector || + _statementLines[idx]._num == _oldSelector) { + // Different coloring based on whether the option has been previously chosen or not + color = (!talk._talkHistory[talk._converseNum][_statementLines[idx]._num]) ? + INFO_TOP : INFO_BOTTOM; + + if (_statementLines[idx]._num == _selector && highlightMode == HL_CHANGED_HIGHLIGHTS) + color = COMMAND_HIGHLIGHTED; + + // See if it's the start of a new statement, so needs the statement number to be displayed + if (!idx || _statementLines[idx]._num != _statementLines[idx - 1]._num) { + Common::String numStr = Common::String::format("%d.", statementNum); + _surface.writeString(numStr, Common::Point(STATEMENT_NUM_X, yp), color); + } + + // Display the statement line + _surface.writeString(_statementLines[idx]._line, Common::Point(_talkTextX, yp), color); + } + yp += _surface.fontHeight() + 1; + + // If the next line starts a new statement, then increment the statement number + if (idx == (_statementLines.size() - 1) || _statementLines[idx]._num != _statementLines[idx + 1]._num) + ++statementNum; + } + } + + // See if the scroll bar needs to be drawn + if (_scroll && highlightMode != HL_CHANGED_HIGHLIGHTS) + drawScrollBar(_talkScrollIndex, NUM_VISIBLE_TALK_LINES, _statementLines.size()); +} + +void WidgetTalk::setStatementLines() { + TattooTalk &talk = *(TattooTalk *)_vm->_talk; + const char *numStr = "19."; + + // See how many statements are going to be available + int numStatements = 0; + for (uint idx = 0; idx < talk._statements.size(); ++idx) { + if (talk._statements[idx]._talkMap != -1) + ++numStatements; + } + + // If there are more lines than can be displayed in the interface window at one time, adjust the allowed + // width to take into account needing a scrollbar + int xSize = _scroll ? _bounds.width() - BUTTON_SIZE - 3 : _bounds.width(); + + // Also adjust the width to allow room for the statement numbers at the left edge of the display + int n = (numStatements < 10) ? 1 : 0; + xSize -= _surface.stringWidth(numStr + n) + _surface.widestChar() / 2 + 9; + _talkTextX = _surface.stringWidth(numStr + n) + _surface.widestChar() / 4 + 6; + _statementLines.clear(); + + for (uint statementNum = 0; statementNum < talk._statements.size(); ++statementNum) { + // See if this statment meets all of it's flag requirements + if (talk._statements[statementNum]._talkMap != -1) { + // Get the next statement text to process + Common::String str = talk._statements[statementNum]._statement; + + Common::StringArray statementLines; + splitLines(str, statementLines, xSize, 999); + + // Add the lines in + for (uint idx = 0; idx < statementLines.size(); ++idx) + _statementLines.push_back(StatementLine(statementLines[idx], statementNum)); + } + } +} + +void WidgetTalk::refresh() { + _talkScrollIndex = 0; + _selector = _oldSelector = -1; + + setStatementLines(); + render(HL_NO_HIGHLIGHTING); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_talk.h b/engines/sherlock/tattoo/widget_talk.h new file mode 100644 index 0000000000..1dbbc22a69 --- /dev/null +++ b/engines/sherlock/tattoo/widget_talk.h @@ -0,0 +1,92 @@ +/* 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_WIDGET_TALK_H +#define SHERLOCK_TATTOO_WIDGET_TALK_H + +#include "common/scummsys.h" +#include "sherlock/tattoo/widget_base.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +enum Highlight { HL_NO_HIGHLIGHTING, HL_CHANGED_HIGHLIGHTS, HL_SCROLLBAR_ONLY }; + +/** + * Handles displaying a dialog with conversation options the player can select from + */ +class WidgetTalk: public WidgetBase { + struct StatementLine { + Common::String _line; + int _num; + + StatementLine() : _num(0) {} + StatementLine(const Common::String &line, int num) : _line(line), _num(num) {} + }; +private: + int _talkScrollIndex; + Common::Array<StatementLine> _statementLines; + int _selector, _oldSelector; + int _talkTextX; + uint32 _dialogTimer; + + void getTalkWindowSize(); + + /** + * Re-renders the contenst of the window to the widget's surface + */ + void render(Highlight highlightMode); + + /** + * This initializes the _statementLines array, which contains the talk options split up line + * by line, as well as which statement a particular line is part of. + */ + void setStatementLines(); +public: + WidgetTalk(SherlockEngine *vm); + virtual ~WidgetTalk() {} + + /** + * Figures out how many lines the available talk lines will take up, and opens a text window + * of appropriate size + */ + void load(); + + /** + * Refresh the talk display + */ + void refresh(); + + /** + * Handle event processing + */ + virtual void handleEvents(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_text.cpp b/engines/sherlock/tattoo/widget_text.cpp new file mode 100644 index 0000000000..33330df2e2 --- /dev/null +++ b/engines/sherlock/tattoo/widget_text.cpp @@ -0,0 +1,223 @@ +/* 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/widget_text.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +WidgetText::WidgetText(SherlockEngine *vm) : WidgetBase(vm) { +} + +void WidgetText::load(const Common::String &str, int speaker) { + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::StringArray lines; + + int width = SHERLOCK_SCREEN_WIDTH / 3; + int height; + + for (;;) { + splitLines(str, lines, width - _surface.widestChar() * 2, 100); + height = (screen.fontHeight() + 1) * lines.size() + 9; + + if ((width - _surface.widestChar() * 2 > height * 3 / 2) || (width - _surface.widestChar() * 2 + > SHERLOCK_SCREEN_WIDTH * 3 / 4)) + break; + + width += (width / 4); + } + + // See if it's only a single line long + if (height == _surface.fontHeight() + 10) { + width = _surface.widestChar() * 2 + 6; + + const char *strP = str.c_str(); + while (*strP && (*strP < talk._opcodes[OP_SWITCH_SPEAKER] || *strP == talk._opcodes[OP_NULL])) + width += _surface.charWidth(*strP++); + } + + _bounds = Common::Rect(width, height); + + if (speaker == -1) { + // No speaker specified, so center window on look position + _bounds.translate(ui._lookPos.x - width / 2, ui._lookPos.y - height / 2); + } else { + // Speaker specified, so center the window above them + centerWindowOnSpeaker(speaker); + } + + render(str); +} + +void WidgetText::centerWindowOnSpeaker(int speaker) { + TattooPeople &people = *(TattooPeople *)_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Common::Point pt; + + bool flag = _vm->readFlags(FLAG_PLAYER_IS_HOLMES); + if (people[HOLMES]._type == CHARACTER && ((speaker == HOLMES && flag) || (speaker == WATSON && !flag))) { + // Place the window centered above the player + pt.x = people[HOLMES]._position.x / FIXED_INT_MULTIPLIER - _bounds.width() / 2; + + int scaleVal = scene.getScaleVal(people[HOLMES]._position); + if (scaleVal == SCALE_THRESHOLD) { + pt.x += people[HOLMES].frameWidth() / 2; + pt.y = people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES].frameHeight() + - _bounds.height() - _surface.fontHeight(); + } else { + pt.x += people[HOLMES]._imageFrame->sDrawXSize(scaleVal) / 2; + pt.y = people[HOLMES]._position.y / FIXED_INT_MULTIPLIER - people[HOLMES]._imageFrame->sDrawYSize(scaleVal) + - _bounds.height() - _surface.fontHeight(); + } + } else { + pt.y = -1; + + // Check each NPC to see if they are the one that is talking + for (int idx = 1; idx < MAX_CHARACTERS; ++idx) { + if (people[idx]._type == CHARACTER) { + if (!scumm_strnicmp(people[idx]._npcName.c_str(), people._characters[speaker]._portrait, 4)) { + // Place the window above the player + pt.x = people[idx]._position.x / FIXED_INT_MULTIPLIER - _bounds.width() / 2; + + int scaleVal = scene.getScaleVal(people[idx]._position); + if (scaleVal == SCALE_THRESHOLD) { + pt.x += people[idx].frameWidth() / 2; + pt.y = people[idx]._position.y / FIXED_INT_MULTIPLIER - people[idx].frameHeight() + - _bounds.height() - _surface.fontHeight(); + } + else { + pt.x += people[idx]._imageFrame->sDrawXSize(scaleVal) / 2; + pt.y = people[idx]._position.y / FIXED_INT_MULTIPLIER - people[idx]._imageFrame->sDrawYSize(scaleVal) + - _bounds.height() - _surface.fontHeight(); + } + + if (pt.y < 0) + pt.y = 0; + break; + } + } + } + + if (pt.y == -1) { + for (uint idx = 0; idx < scene._bgShapes.size(); ++idx) { + Object &obj = scene._bgShapes[idx]; + + if (obj._type == ACTIVE_BG_SHAPE && !scumm_strnicmp(obj._name.c_str(), people._characters[speaker]._portrait, 4)) { + // Place the window centered above the character + pt.x = obj._position.x - _bounds.width() / 2; + pt.y = obj._position.y - _bounds.height() - _surface.fontHeight(); + if (pt.y < 0) + pt.y = 0; + if (obj._scaleVal == SCALE_THRESHOLD) + pt.x += obj.frameWidth() / 2; + else + pt.x += obj._imageFrame->sDrawXSize(obj._scaleVal) / 2; + + break; + } + } + } + + if (pt.y == -1) { + pt.x = SHERLOCK_SCREEN_WIDTH / 2 - _bounds.width() / 2; + pt.y = SHERLOCK_SCREEN_HEIGHT / 2 - _bounds.height() / 2; + } + } + + _bounds.moveTo(pt); +} + +void WidgetText::render(const Common::String &str) { + Common::StringArray lines; + _remainingText = splitLines(str, lines, _bounds.width() - _surface.widestChar() * 2, + _bounds.height() / (_surface.fontHeight() + 1)); + + // Allocate a surface for the window + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + + // Form the background for the new window + makeInfoArea(); + + int yp = 5; + for (int lineNum = 0; yp < (_bounds.height() - _surface.fontHeight() / 2); ++lineNum) { + _surface.writeString(lines[lineNum], Common::Point(_surface.widestChar(), yp), INFO_TOP); + yp += _surface.fontHeight() + 1; + } +} + +/*----------------------------------------------------------------*/ + +WidgetMessage::WidgetMessage(SherlockEngine *vm) : WidgetBase(vm) { + _menuCounter = 0; +} + +void WidgetMessage::load(const Common::String &str, int time) { + Events &events = *_vm->_events; + Common::Point mousePos = events.mousePos(); + _menuCounter = time; + + // Set up the bounds for the dialog to be a single line + _bounds = Common::Rect(_surface.stringWidth(str) + _surface.widestChar() * 2 + 6, _surface.fontHeight() + 10); + _bounds.moveTo(mousePos.x - _bounds.width() / 2, mousePos.y - _bounds.height() / 2); + + // Allocate a surface for the window + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + + // Form the background for the new window and write the line of text + makeInfoArea(); + _surface.writeString(str, Common::Point(_surface.widestChar() + 3, 5), INFO_TOP); +} + +void WidgetMessage::handleEvents() { + Events &events = *_vm->_events; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + WidgetBase::handleEvents(); + + --_menuCounter; + + // Check if a mouse or keypress has occurred, or the display counter has expired + if (events._pressed || events._released || events._rightPressed || events._rightReleased || + ui._keyState.keycode || !_menuCounter) { + // Close the window + banishWindow(); + + // Reset cursor and switch back to standard mode + events.setCursor(ARROW); + events.clearEvents(); + ui._key = -1; + ui._oldBgFound = -1; + ui._menuMode = STD_MODE; + } +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_text.h b/engines/sherlock/tattoo/widget_text.h new file mode 100644 index 0000000000..f027bdae9f --- /dev/null +++ b/engines/sherlock/tattoo/widget_text.h @@ -0,0 +1,80 @@ +/* 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_WIDGET_TEXT_H +#define SHERLOCK_TATTOO_WIDGET_TEXT_H + +#include "common/scummsys.h" +#include "sherlock/tattoo/widget_base.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +class WidgetText: public WidgetBase { +private: + /** + * Center the area the dialog will be drawn on above a given speaker + */ + void centerWindowOnSpeaker(int speaker); + + /** + * Build up the text dialog based on the previously set bounds + */ + void render(const Common::String &str); +public: + Common::String _remainingText; +public: + WidgetText(SherlockEngine *vm); + virtual ~WidgetText() {} + + /** + * Load the data for the text window + */ + void load(const Common::String &str, int speaker = -1); +}; + +class WidgetMessage : public WidgetBase { +private: + int _menuCounter; +public: + WidgetMessage(SherlockEngine *vm); + virtual ~WidgetMessage() {} + + /** + * Load the data for the text window + */ + void load(const Common::String &str, int time); + + /** + * Handle event processing + */ + virtual void handleEvents(); +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_tooltip.cpp b/engines/sherlock/tattoo/widget_tooltip.cpp new file mode 100644 index 0000000000..b1c13c7a29 --- /dev/null +++ b/engines/sherlock/tattoo/widget_tooltip.cpp @@ -0,0 +1,220 @@ +/* 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/widget_tooltip.h" +#include "sherlock/tattoo/tattoo_map.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +#define MAX_TOOLTIP_WIDTH 150 + +void WidgetTooltipBase::draw() { + Screen &screen = *_vm->_screen; + + // If there was a previously drawn frame in a different position that hasn't yet been erased, then erase it + if (_oldBounds.width() > 0 && _oldBounds != _bounds) + erase(); + + if (_bounds.width() > 0 && !_surface.empty()) { + restrictToScreen(); + + // Blit the affected area to the screen + screen.slamRect(_bounds); + + // Draw the widget directly onto the screen. Unlike other widgets, we don't draw to the back buffer, + // since nothing should be drawing on top of tooltips, so there's no need to store in the back buffer + screen.transBlitFrom(_surface, Common::Point(_bounds.left - screen._currentScroll.x, + _bounds.top - screen._currentScroll.y)); + + // Store a copy of the drawn area for later erasing + _oldBounds = _bounds; + } +} + +void WidgetTooltipBase::erase() { + Screen &screen = *_vm->_screen; + + if (_oldBounds.width() > 0) { + // Restore the affected area from the back buffer to the screen + screen.slamRect(_oldBounds); + + // Reset the old bounds so it won't be erased again + _oldBounds = Common::Rect(0, 0, 0, 0); + } +} + +/*----------------------------------------------------------------*/ + +WidgetTooltip::WidgetTooltip(SherlockEngine *vm) : WidgetTooltipBase (vm) { +} + +void WidgetTooltip::setText(const Common::String &str) { + Events &events = *_vm->_events; + Common::Point mousePos = events.mousePos(); + bool reset = false; + + // Make sure that the description is present + if (!str.empty()) { + int width = _surface.stringWidth(str) + 2; + int height = _surface.stringHeight(str) + 2; + Common::String line1 = str, line2 = ""; + + // See if we need to split it into two lines + if (width > MAX_TOOLTIP_WIDTH) { + // Go forward word by word to find out where to split the line + const char *s = str.c_str(); + const char *space = nullptr; + int dif = 10000; + + for (;;) { + // Find end of next word + s = strchr(s + 1, ' '); + + if (s == nullptr) { + // Reached end of string + if (space != nullptr) { + line1 = Common::String(str.c_str(), space); + line2 = Common::String(space + 1); + height = _surface.stringHeight(line1) + _surface.stringHeight(line2) + 4; + } + break; + } + + // Found space separating words, so see what width the string up to now is + Common::String tempLine1 = Common::String(str.c_str(), s); + Common::String tempLine2 = Common::String(s + 1); + int width1 = _surface.stringWidth(tempLine1); + int width2 = _surface.stringWidth(tempLine2); + + // See if we've found a split point that results in a less overall width + if (ABS(width1 - width2) < dif) { + // Found a better split point + dif = ABS(width1 - width2); + space = s; + line1 = tempLine1; + line2 = tempLine2; + } + } + } else { + // No line split needed + height = _surface.stringHeight(str) + 2; + } + + // Reallocate the text surface with the new size + _surface.create(width, height); + _surface.fill(TRANSPARENCY); + + if (line2.empty()) { + // Only a single line + _surface.writeFancyString(str, Common::Point(0, 0), BLACK, INFO_TOP); + } else { + // Two lines to display + int xp, yp; + xp = (width - _surface.stringWidth(line1) - 2) / 2; + _surface.writeFancyString(line1, Common::Point(xp, 0), BLACK, INFO_TOP); + + xp = (width - _surface.stringWidth(line2) - 2) / 2; + yp = _surface.stringHeight(line1) + 2; + _surface.writeFancyString(line2, Common::Point(xp, yp), BLACK, INFO_TOP); + } + + // Set the initial display position for the tooltip text + int tagX = mousePos.x - width / 2; + int tagY = mousePos.y - height; + + _bounds = Common::Rect(tagX, tagY, tagX + width, tagY + height); + } else { + reset = true; + } + + if (reset && !_surface.empty()) { + _surface.free(); + } +} + +void WidgetTooltip::handleEvents() { + Events &events = *_vm->_events; + Common::Point mousePos = events.mousePos(); + + // Set the new position for the tooltip + int xp = mousePos.x - _bounds.width() / 2; + int yp = mousePos.y - _bounds.height(); + + _bounds.moveTo(xp, yp); +} + +/*----------------------------------------------------------------*/ + +void WidgetSceneTooltip::handleEvents() { + Events &events = *_vm->_events; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + + // See if thay are pointing at a different object and we need to regenerate the tooltip text + if (ui._bgFound != ui._oldBgFound || (ui._bgFound != -1 && _surface.empty()) || + ui._arrowZone != ui._oldArrowZone || (ui._arrowZone != -1 && _surface.empty())) { + // See if there is a new object to display text for + if ((ui._bgFound != -1 && (ui._bgFound != ui._oldBgFound || (ui._bgFound != -1 && _surface.empty()))) || + (ui._arrowZone != -1 && (ui._arrowZone != ui._oldArrowZone || (ui._arrowZone != -1 && _surface.empty())))) { + Common::String str; + if (ui._bgFound != -1) { + // Clear the Arrow Zone fields so it won't think we're displaying an Arrow Zone cursor + if (scene._currentScene != 90) // RRR Take out the cludge for room 90 + ui._arrowZone = ui._oldArrowZone = -1; + + // Get the description string + str = (ui._bgFound < 1000) ? scene._bgShapes[ui._bgFound]._description : + people[ui._bgFound - 1000]._description; + } else { + // Get the exit zone description + str = scene._exits[ui._arrowZone]._dest; + } + + setText(str.hasPrefix(" ") ? Common::String() : str); + } else if ((ui._bgFound == -1 && ui._oldBgFound != -1) || (ui._arrowZone == -1 && ui._oldArrowZone != -1)) { + setText(""); + } + + ui._oldBgFound = ui._bgFound; + } else { + + // Set the new position for the tooltip + int tagX = CLIP(mousePos.x - _bounds.width() / 2, 0, SHERLOCK_SCREEN_WIDTH - _bounds.width()); + int tagY = MAX(mousePos.y - _bounds.height(), 0); + + _bounds.moveTo(tagX, tagY); + } + + ui._oldArrowZone = ui._arrowZone; + + WidgetTooltip::handleEvents(); +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_tooltip.h b/engines/sherlock/tattoo/widget_tooltip.h new file mode 100644 index 0000000000..a7758d4863 --- /dev/null +++ b/engines/sherlock/tattoo/widget_tooltip.h @@ -0,0 +1,87 @@ +/* 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_WIDGET_TOOLTIP_H +#define SHERLOCK_TATTOO_WIDGET_TOOLTIP_H + +#include "common/scummsys.h" +#include "common/rect.h" +#include "sherlock/tattoo/widget_base.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +class WidgetTooltipBase : public WidgetBase { +public: + WidgetTooltipBase(SherlockEngine *vm) : WidgetBase(vm) {} + virtual ~WidgetTooltipBase() {} + + /** + * Erase any previous display of the widget on the screen + */ + virtual void erase(); + + /** + * Update the display of the widget on the screen + */ + virtual void draw(); +}; + +class WidgetTooltip: public WidgetTooltipBase { +public: + WidgetTooltip(SherlockEngine *vm); + virtual ~WidgetTooltip() {} + + /** + * Set the text for the tooltip + */ + void setText(const Common::String &str); + + /** + * Handle updating the tooltip state + */ + virtual void handleEvents(); +}; + +class WidgetSceneTooltip : public WidgetTooltip { +public: + WidgetSceneTooltip(SherlockEngine *vm) : WidgetTooltip(vm) {} + + /** + * Handle updating the tooltip state + */ + virtual void handleEvents(); +}; + +class WidgetMapTooltip : public WidgetTooltip { +public: + WidgetMapTooltip(SherlockEngine *vm) : WidgetTooltip(vm) {} +}; + +} // End of namespace Tattoo + +} // End of namespace Sherlock + +#endif diff --git a/engines/sherlock/tattoo/widget_verbs.cpp b/engines/sherlock/tattoo/widget_verbs.cpp new file mode 100644 index 0000000000..3cdd1247c1 --- /dev/null +++ b/engines/sherlock/tattoo/widget_verbs.cpp @@ -0,0 +1,311 @@ +/* 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/widget_verbs.h" +#include "sherlock/tattoo/tattoo_fixed_text.h" +#include "sherlock/tattoo/tattoo_scene.h" +#include "sherlock/tattoo/tattoo_user_interface.h" +#include "sherlock/tattoo/tattoo_people.h" +#include "sherlock/tattoo/tattoo.h" + +namespace Sherlock { + +namespace Tattoo { + +WidgetVerbs::WidgetVerbs(SherlockEngine *vm) : WidgetBase(vm) { + _selector = _oldSelector = -1; + _outsideMenu = false; +} + +void WidgetVerbs::load(bool objectsOn) { + Events &events = *_vm->_events; + TattooPeople &people = *(TattooPeople *)_vm->_people; + Talk &talk = *_vm->_talk; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + bool isWatson = false; + + if (talk._talkToAbort) + return; + + ui._activeObj = ui._bgFound; + _outsideMenu = false; + _verbCommands.clear(); + _selector = _oldSelector = -1; + + // Check if we need to show options for the highlighted object + if (objectsOn) { + // Set the verb list accordingly, depending on the target being a + // person or an object + if (ui._personFound) { + TattooPerson &person = people[ui._activeObj - 1000]; + TattooPerson &npc = people[ui._activeObj - 1001]; + + if (!scumm_strnicmp(npc._npcName.c_str(), "WATS", 4)) + isWatson = true; + + + if (scumm_strnicmp(person._examine.c_str(), "_EXIT", 5)) + _verbCommands.push_back(FIXED(Look)); + + _verbCommands.push_back(FIXED(Talk)); + + // Add any extra active verbs from the NPC's verb list + for (int idx = 0; idx < 2; ++idx) { + if (!person._use[idx]._verb.empty() && !person._use[idx]._verb.hasPrefix(" ") && + (person._use[idx]._target.empty() || person._use[idx]._target.hasPrefix(" "))) { + _verbCommands.push_back(person._use[idx]._verb); + } + } + } else { + if (!scumm_strnicmp(ui._bgShape->_name.c_str(), "WATS", 4)) + // Looking at Watson + isWatson = true; + + if (scumm_strnicmp(ui._bgShape->_examine.c_str(), "_EXIT", 5)) + // It's not an exit, so include Look as an option + _verbCommands.push_back(FIXED(Look)); + + if (ui._bgShape->_aType == PERSON) + _verbCommands.push_back(FIXED(Talk)); + + // Add any extra active verbs from the object's verb list + for (int idx = 0; idx < 6; ++idx) { + if (!ui._bgShape->_use[idx]._verb.empty() && !ui._bgShape->_use[idx]._verb.hasPrefix(" ") && + (ui._bgShape->_use[idx]._target.empty() || ui._bgShape->_use[idx]._target.hasPrefix(" "))) { + _verbCommands.push_back(ui._bgShape->_use[idx]._verb); + } + } + } + } + + // If clicked on Watson, have Journal as an option + if (isWatson) + _verbCommands.push_back(FIXED(Journal)); + + // Add the system commands + _verbCommands.push_back(FIXED(Inventory)); + _verbCommands.push_back(FIXED(Options)); + + // Figure out the needed width to show the commands + int width = 0; + for (uint idx = 0; idx < _verbCommands.size(); ++idx) + width = MAX(width, _surface.stringWidth(_verbCommands[idx])); + width += _surface.widestChar() * 2 + 6; + int height = (_surface.fontHeight() + 7) * _verbCommands.size() + 3; + + // Set the bounds + _bounds = Common::Rect(width, height); + _bounds.moveTo(mousePos.x - _bounds.width() / 2, mousePos.y - _bounds.height() / 2); + + // Render the window on the internal surface + render(); +} + +void WidgetVerbs::render() { + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + ImageFile &images = *ui._interfaceImages; + + // Create the drawing surface + _surface.create(_bounds.width(), _bounds.height()); + _surface.fill(TRANSPARENCY); + + // Draw basic background + makeInfoArea(); + + // Draw the verb commands and the lines separating them + for (uint idx = 0; idx < _verbCommands.size(); ++idx) { + _surface.writeString(_verbCommands[idx], Common::Point((_bounds.width() - _surface.stringWidth(_verbCommands[idx])) / 2, + (_surface.fontHeight() + 7) * idx + 5), INFO_TOP); + + if (idx < (_verbCommands.size() - 1)) { + _surface.hLine(3, (_surface.fontHeight() + 7) * (idx + 1), _bounds.width() - 4, INFO_TOP); + _surface.hLine(3, (_surface.fontHeight() + 7) * (idx + 1) + 1, _bounds.width() - 4, INFO_MIDDLE); + _surface.hLine(3, (_surface.fontHeight() + 7) * (idx + 1) + 2, _bounds.width() - 4, INFO_BOTTOM); + + _surface.transBlitFrom(images[4], Common::Point(0, (_surface.fontHeight() + 7) * (idx + 1) - 1)); + _surface.transBlitFrom(images[5], Common::Point(_bounds.width() - images[5]._width, + (_surface.fontHeight() + 7) * (idx + 1) - 1)); + } + } +} + +void WidgetVerbs::handleEvents() { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + People &people = *_vm->_people; + TattooScene &scene = *(TattooScene *)_vm->_scene; + Talk &talk = *_vm->_talk; + TattooUserInterface &ui = *(TattooUserInterface *)_vm->_ui; + Common::Point mousePos = events.mousePos(); + bool noDesc = false; + + Common::String strLook = fixedText.getText(kFixedText_Look); + Common::String strTalk = fixedText.getText(kFixedText_Talk); + Common::String strJournal = fixedText.getText(kFixedText_Journal); + + checkTabbingKeys(_verbCommands.size()); + + // Highlight verb display as necessary + highlightVerbControls(); + + // Flag if the user has started pressing the button with the cursor outsie the menu + if (events._firstPress && !_bounds.contains(mousePos)) + _outsideMenu = true; + + // See if they released the mouse button + if (events._released || events._rightReleased) { + // See if they want to close the menu (they clicked outside of the menu) + if (!_bounds.contains(mousePos)) { + if (_outsideMenu) { + if (events._rightReleased) { + // Change to the item (if any) that was right-clicked on, and re-draw the verb menu + ui._bgFound = scene.findBgShape(mousePos); + ui._personFound = ui._bgFound >= 1000; + ui._bgShape = ui._personFound || ui._bgFound == -1 ? nullptr : &scene._bgShapes[ui._bgFound]; + + if (ui._personFound) { + if (people[ui._bgFound - 1000]._description.empty() || people[ui._bgFound - 1000]._description.hasPrefix(" ")) + noDesc = true; + } else if (ui._bgFound != -1) { + if (ui._bgShape->_description.empty() || ui._bgShape->_description.hasPrefix(" ")) + noDesc = true; + } else { + noDesc = true; + } + + // Call the Routine to turn on the Commands for this Object + load(!noDesc); + } else { + // Free the current menu graphics & erase the menu + banishWindow(); + + // See if we're in a Lab Table Room + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + } + } + } else if (_bounds.contains(mousePos) && _selector != -1) { + // Mouse is within the menu + // Erase the menu + banishWindow(); + + // See if they are activating the Look Command + if (!_verbCommands[_selector].compareToIgnoreCase(strLook)) { + ui._bgFound = ui._activeObj; + if (ui._activeObj >= 1000) { + ui._personFound = true; + } else { + ui._personFound = false; + ui._bgShape = &scene._bgShapes[ui._activeObj]; + } + + ui.lookAtObject(); + + } else if (!_verbCommands[_selector].compareToIgnoreCase(strTalk)) { + // Talk command is being activated + talk.talk(ui._activeObj); + ui._activeObj = -1; + + } else if (!_verbCommands[_selector].compareToIgnoreCase(strJournal)) { + ui.doJournal(); + + // See if we're in a Lab Table scene + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + } else if (_selector >= ((int)_verbCommands.size() - 2)) { + switch (_selector - (int)_verbCommands.size() + 2) { + case 0: + // Inventory + ui.doInventory(2); + break; + + case 1: + // Options + ui.doControls(); + break; + + default: + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + break; + } + } else { + // If they have selected anything else, process it + people[HOLMES].gotoStand(); + + if (ui._activeObj < 1000) { + for (int idx = 0; idx < 6; ++idx) { + if (!_verbCommands[_selector].compareToIgnoreCase(scene._bgShapes[ui._activeObj]._use[idx]._verb)) { + // See if they are Picking this object up + if (!scene._bgShapes[ui._activeObj]._use[idx]._target.compareToIgnoreCase("*PICKUP")) + ui.pickUpObject(ui._activeObj); + else + ui.checkAction(scene._bgShapes[ui._activeObj]._use[idx], ui._activeObj); + } + } + } else { + for (int idx = 0; idx < 2; ++idx) { + if (!_verbCommands[_selector].compareToIgnoreCase(people[ui._activeObj - 1000]._use[idx]._verb)) + ui.checkAction(people[ui._activeObj - 1000]._use[idx], ui._activeObj); + } + } + + ui._activeObj = -1; + if (ui._menuMode != MESSAGE_MODE) { + // See if we're in a Lab Table Room + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + } + } + } + } else if (ui._keyState.keycode == Common::KEYCODE_ESCAPE) { + // User closing the menu with the ESC key + banishWindow(); + ui._menuMode = scene._labTableScene ? LAB_MODE : STD_MODE; + } +} + +void WidgetVerbs::highlightVerbControls() { + Events &events = *_vm->_events; + Screen &screen = *_vm->_screen; + Common::Point mousePos = events.mousePos(); + + // Get highlighted verb + _selector = -1; + Common::Rect bounds = _bounds; + bounds.grow(-3); + if (bounds.contains(mousePos)) + _selector = (mousePos.y - bounds.top) / (screen.fontHeight() + 7); + + // See if a new verb is being pointed at + if (_selector != _oldSelector) { + // Redraw the verb list + for (int idx = 0; idx < (int)_verbCommands.size(); ++idx) { + byte color = (idx == _selector) ? (byte)COMMAND_HIGHLIGHTED : (byte)INFO_TOP; + _surface.writeString(_verbCommands[idx], Common::Point((_bounds.width() - screen.stringWidth(_verbCommands[idx])) / 2, + (screen.fontHeight() + 7) * idx + 5), color); + } + + _oldSelector = _selector; + } +} + +} // End of namespace Tattoo + +} // End of namespace Sherlock diff --git a/engines/sherlock/tattoo/widget_verbs.h b/engines/sherlock/tattoo/widget_verbs.h new file mode 100644 index 0000000000..ce67842409 --- /dev/null +++ b/engines/sherlock/tattoo/widget_verbs.h @@ -0,0 +1,72 @@ +/* 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_WIDGET_VERBS_H +#define SHERLOCK_TATTOO_WIDGET_VERBS_H + +#include "common/scummsys.h" +#include "common/rect.h" +#include "common/str-array.h" +#include "sherlock/tattoo/widget_base.h" + +namespace Sherlock { + +class SherlockEngine; + +namespace Tattoo { + +class WidgetVerbs: public WidgetBase { +private: + int _selector, _oldSelector; + bool _outsideMenu; + + /** + * Highlights the controls for the verb list + */ + void highlightVerbControls(); + + /** + * Renders the window on an internal surface for later drawing on-screen + */ + void render(); +public: + Common::StringArray _verbCommands; +public: + WidgetVerbs(SherlockEngine *vm); + virtual ~WidgetVerbs() {} + + /** + * Turns on the menu with all the verbs that are available for the given object + */ + void load(bool objectsOn); + + /** + * Process input for the dialog + */ + virtual void handleEvents(); +}; + +} // 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..9df3f1dc24 --- /dev/null +++ b/engines/sherlock/user_interface.cpp @@ -0,0 +1,205 @@ +/* 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/scalpel/scalpel.h" +#include "sherlock/scalpel/scalpel_user_interface.h" +#include "sherlock/tattoo/tattoo.h" +#include "sherlock/tattoo/tattoo_user_interface.h" + +namespace Sherlock { + +UserInterface *UserInterface::init(SherlockEngine *vm) { + if (vm->getGameID() == GType_SerratedScalpel) + return new Scalpel::ScalpelUserInterface(vm); + else + return new Tattoo::TattooUserInterface(vm); +} + +UserInterface::UserInterface(SherlockEngine *vm) : _vm(vm) { + _menuMode = STD_MODE; + _menuCounter = 0; + _infoFlag = false; + _windowOpen = false; + _endKeyActive = true; + _invLookFlag = 0; + _slideWindows = true; + _helpStyle = false; + _windowBounds = Common::Rect(0, CONTROLS_Y1, SHERLOCK_SCREEN_WIDTH - 1, SHERLOCK_SCREEN_HEIGHT - 1); + _lookScriptFlag = false; + _exitZone = -1; + + _bgFound = _oldBgFound = -1; + _key = _oldKey = '\0'; + _selector = _oldSelector = -1; + _temp = _oldTemp = 0; + _temp1 = 0; + _lookHelp = 0; +} + +void UserInterface::checkAction(ActionType &action, int objNum, FixedTextActionId fixedTextActionId) { + Events &events = *_vm->_events; + FixedText &fixedText = *_vm->_fixedText; + People &people = *_vm->_people; + Scene &scene = *_vm->_scene; + Screen &screen = *_vm->_screen; + Talk &talk = *_vm->_talk; + Point32 pt(-1, -1); + + if (action._useFlag) + // Automatically set the given flag + _vm->setFlags(action._useFlag); + + if (objNum >= 1000) + // Ignore actions done on characters + return; + + if (!action._cAnimSpeed) { + // Invalid action, to print error message + _infoFlag = true; + clearInfo(); + Common::String errorMessage = fixedText.getActionMessage(fixedTextActionId, action._cAnimNum); + screen.print(Common::Point(0, INFO_LINE + 1), COL_INFO_FOREGROUND, "%s", errorMessage.c_str()); + _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._teleport[0]; + dir = anim._teleport[0]._facing; + } else { + pt = anim._goto[0]; + dir = anim._goto[0]._facing; + } + } + } else { + pt = Point32(-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), fixedTextActionId)) { + 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[HOLMES].walkToCoords(pt, dir); + + if (!talk._talkToAbort) { + // Ensure Holmes is on the exact intended location + people[HOLMES]._position = pt; + people[HOLMES]._sequenceNumber = dir; + people[HOLMES].gotoStand(); + + 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[HOLMES].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, fixedTextActionId)) { + 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], fixedTextActionId)) { + if (!talk._talkToAbort) + printed = true; + } + } + + // Unless we're leaving the scene, print a "Done" message unless the printed flag has been set + if (IS_SERRATED_SCALPEL && scene._goToScene != 1 && !printed && !talk._talkToAbort) { + _infoFlag = true; + clearInfo(); + screen.print(Common::Point(0, INFO_LINE + 1), COL_INFO_FOREGROUND, "Done..."); + + // Set how long to show the message + _menuCounter = 30; + } + } + } + + // Reset cursor back to arrow + events.setCursor(ARROW); +} + +void UserInterface::reset() { + _bgFound = _oldBgFound = -1; + _oldKey = -1; + _oldTemp = _temp = -1; + _exitZone = -1; +} + + +} // End of namespace Sherlock diff --git a/engines/sherlock/user_interface.h b/engines/sherlock/user_interface.h new file mode 100644 index 0000000000..9af5ad5ae6 --- /dev/null +++ b/engines/sherlock/user_interface.h @@ -0,0 +1,138 @@ +/* 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" +#include "sherlock/fixed_text.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, + + // Rose Tattoo specific + LAB_MODE = 20, + MESSAGE_MODE = 21, + VERB_MODE = 22 +}; + +class UserInterface { +protected: + SherlockEngine *_vm; + + UserInterface(SherlockEngine *vm); +public: + MenuMode _menuMode; + int _menuCounter; + bool _infoFlag; + bool _windowOpen; + bool _endKeyActive; + int _invLookFlag; + bool _slideWindows; + bool _helpStyle; + Common::Rect _windowBounds; + bool _lookScriptFlag; + int _bgFound, _oldBgFound; + int _exitZone; + + // TODO: Not so sure these should be in the base class. May want to refactor them to SherlockEngine, or refactor + // various Scalpel dialogs to keep their own private state of key/selections + signed char _key, _oldKey; + int _selector, _oldSelector; + int _temp, _oldTemp; + int _temp1; + int _lookHelp; +public: + static UserInterface *init(SherlockEngine *vm); + virtual ~UserInterface() {} + + /** + * Called for OPEN, CLOSE, and MOVE actions are being done + */ + void checkAction(ActionType &action, int objNum, FixedTextActionId fixedTextActionId = kFixedTextAction_Invalid); +public: + /** + * Resets the user interface + */ + virtual void reset(); + + /** + * Draw the user interface onto the screen's back buffers + */ + virtual void drawInterface(int bufferNum = 3) {} + + /** + * Main input handler for the user interface + */ + virtual void handleInput() {} + + /** + * Displays a passed window by gradually scrolling it vertically on-screen + */ + virtual void summonWindow(const Surface &bgSurface, bool slideUp = true) {} + + /** + * Slide the window stored in the back buffer onto the screen + */ + virtual 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 + */ + virtual void banishWindow(bool slideUp = true) {} + + /** + * Clears the info line of the screen + */ + virtual void clearInfo() {} + + /** + * Clear any active text window + */ + virtual void clearWindow() {} +}; + +} // End of namespace Sherlock + +#endif |