/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/system.h" #include "common/config-manager.h" #include "common/macresman.h" #include "engines/engine.h" #include "graphics/macgui/macfontmanager.h" #include "graphics/macgui/macwindowmanager.h" #include "graphics/surface.h" #include "scumm/scumm.h" #include "scumm/detection.h" #include "scumm/macgui/macgui_impl.h" #include "scumm/macgui/macgui_colors.h" #include "scumm/macgui/macgui_loom.h" #include "scumm/music.h" #include "scumm/sound.h" #include "scumm/verbs.h" namespace Scumm { // =========================================================================== // The Mac Loom GUI. This one is pretty simple. // =========================================================================== MacLoomGui::MacLoomGui(ScummEngine *vm, const Common::Path &resourceFile) : MacGuiImpl(vm, resourceFile) { // The practice box can be moved, but this is its default position on // a large screen, and it's not saved. _practiceBoxPos = Common::Point(215, 376 + 2 * _vm->_macScreenDrawOffset); } MacLoomGui::~MacLoomGui() { if (_practiceBox) { _practiceBox->free(); delete _practiceBox; } } const Graphics::Font *MacLoomGui::getFontByScummId(int32 id) { switch (id) { case 0: return getFont(kLoomFontLarge); default: error("MacLoomGui::getFontByScummId: Invalid font id %d", id); } } bool MacLoomGui::getFontParams(FontId fontId, int &id, int &size, int &slant) const { if (MacGuiImpl::getFontParams(fontId, id, size, slant)) return true; // Loom uses only font size 13 for in-game text, but size 12 is used // for system messages, e.g. the original pause dialog. // // Special characters: // // 16-23 are the note names c through c'. // 60 is an upside-down note, i.e. the one used for c'. // 95 is a used for the rest of the notes. switch (fontId) { case kLoomFontSmall: id = _gameFontId; size = 9; slant = Graphics::kMacFontRegular; return true; case kLoomFontMedium: id = _gameFontId; size = 12; slant = Graphics::kMacFontRegular; return true; case kLoomFontLarge: id = _gameFontId; size = 13; slant = Graphics::kMacFontRegular; return true; default: error("MacLoomGui: getFontParams: Unknown font id %d", (int)fontId); } return false; } void MacLoomGui::setupCursor(int &width, int &height, int &hotspotX, int &hotspotY, int &animate) { setupResourceCursor(1000, width, height, hotspotX, hotspotY, animate); } void MacLoomGui::updateMenus() { bool saveCondition, loadCondition; // TODO: Complete LOOM with the rest of the proper code from disasm, // for now we only have the copy protection code and a best guess in place... // // Details: // VAR(221) & 0x4000: Copy protection bit (the only thing I could confirm from the disasm) // VAR(VAR_VERB_SCRIPT) == 5: Best guess... it prevents saving/loading from e.g. difficulty selection screen // _userPut > 0: Best guess... it prevents saving/loading during cutscenes saveCondition = loadCondition = !(_vm->VAR(221) & 0x4000) && (_vm->VAR(_vm->VAR_VERB_SCRIPT) == 5) && (_vm->_userPut > 0); Graphics::MacMenu *menu = _windowManager->getMenu(); Graphics::MacMenuItem *gameMenu = menu->getMenuItem(1); Graphics::MacMenuItem *loadMenu = menu->getSubMenuItem(gameMenu, 0); Graphics::MacMenuItem *saveMenu = menu->getSubMenuItem(gameMenu, 1); loadMenu->enabled = _vm->canLoadGameStateCurrently() && loadCondition; saveMenu->enabled = _vm->canSaveGameStateCurrently() && saveCondition; } bool MacLoomGui::handleMenu(int id, Common::String &name) { if (MacGuiImpl::handleMenu(id, name)) return true; switch (id) { case 101: // Drafts inventory runDraftsInventory(); return true; case 204: // Options runOptionsDialog(); return true; case 205: // Quit if (runQuitDialog()) _vm->quitGame(); return true; default: warning("Unknown menu command: %d", id); break; } return false; } void MacLoomGui::runAboutDialog() { // The About window is not a dialog resource. Its size appears to be // hard-coded (416x166), and it's drawn centered. The graphics are in // PICT 5000 and 5001. int width = 416; int height = 166; int x = (640 - width) / 2; int y = (400 - height) / 2; Common::Rect bounds(x, y, x + width, y + height); MacDialogWindow *window = createWindow(bounds, kWindowStyleNormal, kMenuStyleApple); Graphics::Surface *lucasFilm = loadPict(5000); Graphics::Surface *loom = loadPict(5001); const char *subVers = (const char *)_vm->getStringAddress(5); Common::String version = Common::String::format(_strsStrings[kMSIAboutString2].c_str(), subVers, '5', '1', '6'); const TextLine page1[] = { { 0, 23, kStyleExtraBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString1].c_str() }, // "PRESENTS" TEXT_END_MARKER }; const TextLine page2[] = { { 1, 59, kStyleRegular, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString3].c_str() }, // "TM & \xA9 1990 LucasArts Entertainment Company. All rights reserved." { 0, 70, kStyleRegular, Graphics::kTextAlignCenter, version.c_str() }, // "Release Version 1.2 25-JAN-91 Interpreter version 5.1.6" TEXT_END_MARKER }; const TextLine page3[] = { { 1, 11, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString4].c_str() }, // "Macintosh version by" { 0, 25, kStyleHeader1, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString6].c_str() }, // "Eric Johnston" { 0, 49, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString5].c_str() }, // "Macintosh scripting by" { 1, 63, kStyleHeader1, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString7].c_str() }, // "Ron Baldwin" TEXT_END_MARKER }; const TextLine page4[] = { { 0, 26, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString8].c_str() }, // "Original game created by" { 1, 40, kStyleHeader1, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString9].c_str() }, // "Brian Moriarty" TEXT_END_MARKER }; const TextLine page5[] = { { 1, 11, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString10].c_str() }, // "Produced by" { 0, 25, kStyleHeader1, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString12].c_str() }, // "Gregory D. Hammond" { 0, 49, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString11].c_str() }, // "Macintosh Version Produced by" { 1, 63, kStyleHeader1, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString13].c_str() }, // "David Fox" TEXT_END_MARKER }; const TextLine page6[] = { { 1, 6, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString14].c_str() }, // "SCUMM Story System" {1, 16, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString15].c_str()}, // "created by" { 97, 35, kStyleHeader1, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString17].c_str() }, // "Ron Gilbert" { 1, 51, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString16].c_str() }, // "and" { 122, 65, kStyleHeader1, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString18].c_str() }, // "Aric Wilmunder" TEXT_END_MARKER }; const TextLine page7[] = { { 1, 16, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString19].c_str() }, // "Stumped? Loom hint books are available!" { 76, 33, kStyleRegular, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString22].c_str() }, // "In the U.S. call" { 150, 34, kStyleBold, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString20].c_str() }, // "1 (800) STAR-WARS" { 150, 43, kStyleRegular, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString24].c_str() }, // "that\xD5s 1 (800) 782-7927" { 80, 63, kStyleRegular, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString23].c_str() }, // "In Canada call" { 150, 64, kStyleBold, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString21].c_str() }, // "1 (800) 828-7927" TEXT_END_MARKER }; const TextLine page8[] = { { 1, 11, kStyleBold, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString25].c_str() }, // "Need a hint NOW? Having problems?" { 81, 25, kStyleRegular, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString28].c_str() }, // "For technical support call" { 205, 26, kStyleBold, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString26].c_str() }, // "1 (415) 721-3333" { 137, 35, kStyleRegular, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString29].c_str() }, // "For hints call" { 205, 36, kStyleBold, Graphics::kTextAlignLeft, _strsStrings[kMSIAboutString27].c_str() }, // "1 (900) 740-JEDI" { 1, 50, kStyleRegular, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString30].c_str() }, // "The charge for the hint line is 75\xA2 per minute." { 1, 60, kStyleRegular, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString31].c_str() }, // "(You must have your parents\xD5 permission to" { 1, 70, kStyleRegular, Graphics::kTextAlignCenter, _strsStrings[kMSIAboutString32].c_str() }, // "call this number if you are under 18.)" TEXT_END_MARKER }; struct AboutPage { const TextLine *text; int waitFrames; }; AboutPage aboutPages[] = { { nullptr, 60 }, // ~3 seconds { page1, 40 }, // ~2 seconds { page2, 130 }, // ~6.5 seconds { page3, 80 }, // ~4 seconds { page4, 80 }, { page5, 80 }, { page6, 80 }, { page7, 260 }, // ~13 seconds { page8, 0 } }; int page = 0; // I've based the animation speed on what it looks like when Mini vMac // emulates an old black-and-white Mac at normal speed. It looks a bit // different in Basilisk II, but that's probably because it emulates a // much faster Mac. // // The animation is either either growing or shrinking, depending on // if growth is positive or negative. During each scene, the animation // may reach its smallest point, at which time it bounces back. When // it reaches its outer limit, the scene ends. window->show(); int scene = 0; DelayStatus status = kDelayDone; Common::Rect r(0, 0, 404, 154); int growth = -2; int pattern; bool darkenOnly = false; int waitFrames = 0; int innerBounce = 72; int targetTop = 48; int targetGrowth = 2; bool changeScene = false; bool fastForward = false; while (!_vm->shouldQuit()) { if ((scene % 2) == 0) { // This appears to be pixel perfect or at least nearly // so for the outer layers, but breaks down slightly // near the middle. // // Also, the original does an inexplicable skip in the // first animation that I haven't bothered to // implement. I don't know if it was intentional or // not, but I think it looks awkward. And I wasn't able // to get it quite right anyway. pattern = (r.top / 2) % 8; if (pattern > 4) darkenOnly = false; window->drawPatternRoundRect(r, 7, pattern, true, darkenOnly); if (!fastForward) window->markRectAsDirty(r); if (r.top == targetTop && growth == targetGrowth) { changeScene = true; } else { r.grow(growth); if (growth < 0 && r.top >= innerBounce) growth = -growth; } } else { if (--waitFrames <= 0) changeScene = true; } if (!fastForward) { window->update(); status = delay(50); } if (status == kDelayInterrupted) fastForward = true; if (status == kDelayAborted) break; if (changeScene) { changeScene = false; scene++; // Animations happen on even-numbered scenes. All // animations start in an inwards direction. // // Odd-numbered scenes are the text pages where it // waits for a bit before continuing. This is where // fast-forwarding (by clicking) stops. Unlike Last // Crusade, we can't just skip the animation because // everything has to be drawn. (Well, some could // probably be skipped, but I doubt it's worth the // trouble to do so.) if ((scene % 2) == 0) growth = -2; else { fastForward = false; darkenOnly = true; if (aboutPages[page].text) window->drawTexts(r, aboutPages[page].text); waitFrames = aboutPages[page].waitFrames; page++; } switch (scene) { case 1: window->drawSprite(lucasFilm, 134, 61); break; case 4: // All subsequent text pages are larger, which // we compensate by making the inner bounce // happen earlier. innerBounce -= 8; targetTop -= 16; break; case 5: window->drawSprite(loom, 95, 38); break; } window->update(true); if (scene >= 17) break; } } if (status != kDelayAborted) delay(); _windowManager->popCursor(); lucasFilm->free(); loom->free(); delete lucasFilm; delete loom; delete window; } void MacLoomGui::runDraftsInventory() { char notesBuf[6]; const char *names[18] = { "Drafts", "Opening:", "Straw Into Gold:", "Dyeing:", "Night Vision:", "Twisting:", "Sleep:", "Emptying:", "Invisibility:", "Terror:", "Sharpening:", "Reflection:", "Healing:", "Silence:", "Shaping:", "Unmaking:", "Transcendence:", "Unknown:" }; const char *notes = "cdefgabC"; // ACT 1: Draw the Mac dialog window MacGuiImpl::MacDialogWindow *window = createWindow(Common::Rect(110, 20, 540, 252), kWindowStyleNormal, kMenuStyleApple); const Graphics::Font *font = getFont(kSystemFont); Graphics::Surface *s = window->innerSurface(); // ACT 2: Draw the drafts text // // Drafts are stored in SCUMM global variables; we choose the appropriate // first entry in the variables at which these drafts start. int base = 55; // TODO: Can these be drawn in different styles? (e.g. Checkerboard) byte unlockedColor = kBlack; byte inactiveColor = kBlack; byte newDraftColor = kBlack; for (int i = 0; i < 16; i++) { int draft = _vm->_scummVars[base + i * 2]; // In which row are we rendering our text? int heightMultiplier = i < 8 ? i : (i % 8); int textHeight = 24; // Has the draft been unlocked by the player? //int titleColor = (draft & 0x2000) ? unlockedColor : inactiveColor; // Has the new draft been used at least once? int notesColor = (draft & 0x4000) ? unlockedColor : newDraftColor; // Has the draft been unlocked? Great: put it in our text buffer // otherwise just prepare to render the "????" string. if (draft & 0x2000) { Common::sprintf_s(notesBuf, sizeof(notesBuf), "%c%c%c%c", notes[draft & 0x0007], notes[(draft & 0x0038) >> 3], notes[(draft & 0x01c0) >> 6], notes[(draft & 0x0e00) >> 9]); } else { notesColor = inactiveColor; Common::sprintf_s(notesBuf, sizeof(notesBuf), "????"); } // Where are we positioning the text? // Left column or right column? int xPos = i < 8 ? 40 : 260; // Draw the titles of the drafts... if (draft & 0x2000) { font->drawString(s, names[i + 1], xPos - 20, 24 + textHeight * heightMultiplier, s->w, notesColor); // FIXME: titleColor, not notesColor? } else { // Draw "Unknown:" as the title of the draft font->drawString(s, names[17], xPos - 20, 24 + textHeight * heightMultiplier, s->w, notesColor); // FIXME: titleColor, not notesColor? } // Draw the notes of the draft... font->drawString(s, notesBuf, xPos + 100, 24 + textHeight * heightMultiplier, s->w, notesColor); } // Draw "Drafts" on top of the dialog font->drawString(s, names[0], 0, 4, s->w, kBlack, Graphics::kTextAlignCenter); // Draw a vertical line to separate the two columns s->vLine(210, 44, 184, kBlack); // Update the screen with all the new stuff! window->show(); delay(); delete window; } bool MacLoomGui::runOptionsDialog() { // Widgets: // // 0 - Okay button // 1 - Cancel button // 2 - Sound checkbox // 3 - Music checkbox // 4 - Picture (text speed background) // 5 - Picture (text speed handle) // 6 - Scrolling checkbox // 7 - Full Animation checkbox // 8 - Picture (music quality background) // 9 - Picture (music quality handle) // 10 - "Machine Speed: ^0" text // 11 - Text speed slider (manually created) // 12 - Music quality slider (manually created) int sound = (!ConfMan.hasKey("mute") || !ConfMan.getBool("mute")) ? 1 : 0; int music = (!ConfMan.hasKey("music_mute") || !ConfMan.getBool("music_mute")) ? 1 : 0; int scrolling = _vm->_snapScroll == 0; int fullAnimation = _vm->VAR(_vm->VAR_MACHINE_SPEED) == 1 ? 0 : 1; int textSpeed = _vm->_defaultTextSpeed; int musicQuality = ConfMan.hasKey("mac_snd_quality") ? ConfMan.getInt("mac_snd_quality") : 0; int musicQualityOption = (musicQuality == 0) ? 1 : (musicQuality - 1) % 3; musicQuality = (musicQuality == 0) ? (_vm->VAR(_vm->VAR_SOUNDCARD) == 10 ? 0 : 2) : (musicQuality - 1) / 3; MacDialogWindow *window = createDialog(1000); MacButton *buttonOk = (MacButton *)window->getWidget(kWidgetButton, 0); MacButton *buttonCancel = (MacButton *)window->getWidget(kWidgetButton, 1); MacCheckbox *checkboxSound = (MacCheckbox *)window->getWidget(kWidgetCheckbox, 0); MacCheckbox *checkboxMusic = (MacCheckbox *)window->getWidget(kWidgetCheckbox, 1); MacCheckbox *checkboxScrolling = (MacCheckbox *)window->getWidget(kWidgetCheckbox, 2); MacCheckbox *checkboxFullAnimation = (MacCheckbox *)window->getWidget(kWidgetCheckbox, 3); checkboxSound->setValue(sound); checkboxMusic->setValue(music); checkboxScrolling->setValue(scrolling); checkboxFullAnimation->setValue(fullAnimation); if (!sound) checkboxMusic->setEnabled(false); MacImageSlider *sliderTextSpeed = window->addImageSlider(4, 5, true, 5, 105, 0, 9); sliderTextSpeed->setValue(textSpeed); MacImageSlider *sliderMusicQuality = window->addImageSlider(8, 9, true, 5, 69, 0, 2, 6, 4); sliderMusicQuality->setValue(musicQualityOption); // Machine rating window->addSubstitution(Common::String::format("%d", _vm->VAR(53))); while (!_vm->shouldQuit()) { MacDialogEvent event; while (window->runDialog(event)) { switch (event.type) { case kDialogClick: if (event.widget == buttonOk) { // TEXT SPEED _vm->_defaultTextSpeed = CLIP(sliderTextSpeed->getValue(), 0, 9); ConfMan.setInt("original_gui_text_speed", _vm->_defaultTextSpeed); _vm->setTalkSpeed(_vm->_defaultTextSpeed); // SOUND&MUSIC ACTIVATION // 0 - Sound&Music on // 1 - Sound on, music off // 2 - Sound&Music off int musicVariableValue = 0; if (checkboxSound->getValue() == 0) musicVariableValue = 2; else if (checkboxSound->getValue() == 1 && checkboxMusic->getValue() == 0) musicVariableValue = 1; _vm->_musicEngine->toggleMusic(musicVariableValue == 0); _vm->_musicEngine->toggleSoundEffects(musicVariableValue < 2); ConfMan.setBool("music_mute", musicVariableValue > 0); ConfMan.setBool("mute", musicVariableValue == 2); // SCROLLING ACTIVATION _vm->_snapScroll = checkboxScrolling->getValue() == 0; if (_vm->VAR_CAMERA_FAST_X != 0xFF) _vm->VAR(_vm->VAR_CAMERA_FAST_X) = _vm->_snapScroll; // FULL ANIMATION ACTIVATION _vm->VAR(_vm->VAR_MACHINE_SPEED) = checkboxFullAnimation->getValue() == 1 ? 0 : 1; // MUSIC QUALITY SELECTOR musicQuality = musicQuality * 3 + 1 + sliderMusicQuality->getValue(); _vm->_musicEngine->setQuality(musicQuality); ConfMan.setInt("mac_snd_quality", musicQuality); _vm->syncSoundSettings(); ConfMan.flushToDisk(); delete window; return true; } else if (event.widget == buttonCancel) { delete window; return false; } break; case kDialogValueChange: if (event.widget == checkboxSound) { checkboxMusic->setEnabled(checkboxSound->getValue() != 0); } break; default: break; } } window->delayAndUpdate(); } delete window; return false; } void MacLoomGui::resetAfterLoad() { reset(); // We used to use verb 53 for the Loom practice box, and while it's // still the verb we pretend to use when clicking on it we no longer // use the actual verb slot. // // Apparently the practice box isn't restored on saving, so it seems // that savegame compatibility isn't broken. And if it is, it happened // shortly after the savegame version was increased for other reasons, // so the damage would be very limited. for (int i = 0; i < _vm->_numVerbs; i++) { if (_vm->_verbs[i].verbid == 53) _vm->killVerb(i); } } void MacLoomGui::update(int delta) { // Unlike the PC version, the Macintosh version of Loom appears to // hard-code the drawing of the practice mode box. This is handled by // script 27 in both versions, but whereas the PC version draws the // notes, the Mac version just sets variables 50 and 54. // // In this script, the variables are set to the same value but it // appears that only variable 50 is cleared when the box is supposed to // disappear. I don't know what the purpose of variable 54 is. // // Variable 128 is the game difficulty: // // 0 - Practice // 1 - Standard // 2 - Expert // // Note that the practice mode box is never inscribed on the "Mac // screen" surface. It's drawn last on every update, so it floats // above everything else. int notes = _vm->VAR(50); if (_vm->VAR(128) == 0) { if (notes) { int w = 64; int h = 24; bool bw = (_vm->_renderMode == Common::kRenderMacintoshBW); if (!_practiceBox) { debug(1, "MacLoomGui: Creating practice mode box"); _practiceBox = new Graphics::Surface(); _practiceBox->create(w, h, Graphics::PixelFormat::createFormatCLUT8()); _practiceBox->fillRect(Common::Rect(w, h), kBlack); byte color = bw ? kWhite : kLightGray; _practiceBox->hLine(2, 1, w - 3, color); _practiceBox->hLine(2, h - 2, w - 3, color); _practiceBox->vLine(1, 2, h - 3, color); _practiceBox->vLine(w - 2, 2, h - 3, color); _practiceBoxNotes = 0; } if (notes != _practiceBoxNotes) { debug(1, "MacLoomGui: Drawing practice mode notes"); _practiceBoxNotes = notes; _practiceBox->fillRect(Common::Rect(2, 2, w - 2, h - 2), kBlack); const Graphics::Font *font = getFont(kLoomFontLarge); byte colors[] = { kRed, kBrightRed, kBrightYellow, kBrightGreen, kBrightCyan, kCyan, kBrightBlue, kWhite }; for (int i = 0; i < 4; i++) { int note = (notes >> (4 * i)) & 0x0F; if (note >= 2 && note <= 9) { font->drawChar(_practiceBox, 14 + note, 9 + i * 13, 5, bw ? kWhite : colors[note - 2]); } } } _system->copyRectToScreen(_practiceBox->getBasePtr(0, 0), _practiceBox->pitch, _practiceBoxPos.x, _practiceBoxPos.y, w, h); } else { if (_practiceBox) { debug(1, "MacLoomGui: Deleting practice mode box"); _system->copyRectToScreen(_surface->getBasePtr(_practiceBoxPos.x, _practiceBoxPos.y), _surface->pitch, _practiceBoxPos.x, _practiceBoxPos.y, _practiceBox->w, _practiceBox->h); _practiceBox->free(); delete _practiceBox; _practiceBox = nullptr; } } } } bool MacLoomGui::handleEvent(Common::Event event) { if (MacGuiImpl::handleEvent(event)) return true; if (_vm->isPaused()) return false; if (!_practiceBox || _vm->_userPut <= 0) return false; // Perhaps the silliest feature in Mac Loom, that literally only one // person has ever asked for: You can drag the Loom practice box. // // The game will freeze while the button is held down, but that's how // the original acted as well. Should sounds keep playing? I don't know // if that situation can even occur. I think it's nicer to let them // play if it does. if (event.type != Common::EVENT_LBUTTONDOWN) return false; Common::Rect bounds; bounds.left = _practiceBoxPos.x; bounds.top = _practiceBoxPos.y; bounds.right = _practiceBoxPos.x + _practiceBox->w; bounds.bottom = _practiceBoxPos.y + _practiceBox->h; if (!bounds.contains(event.mouse)) return false; int clickX = event.mouse.x; int clickY = event.mouse.y; bool dragMode = false; while (!_vm->shouldQuit()) { bool dragging = false; int dragX = 0; int dragY = 0; while (_system->getEventManager()->pollEvent(event)) { switch (event.type) { case Common::EVENT_LBUTTONUP: if (!dragMode) _vm->runInputScript(kVerbClickArea, 53, 1); return true; case Common::EVENT_MOUSEMOVE: if (ABS(event.mouse.x - clickX) >= 3 || ABS(event.mouse.y - clickY) >= 3) dragMode = true; if (dragMode) { dragging = true; dragX = event.mouse.x; dragY = event.mouse.y; } break; default: break; } } if (dragging) { // How much has the mouse moved since the initial // click? Calculate new position from that. int newX = bounds.left + (dragX - clickX); int newY = bounds.top + (dragY - clickY); // The box has to stay completely inside the screen. // Also, things get weird if you move the box into the // menu hotzone, so don't allow that. int maxY = _surface->h - _practiceBox->h - 2 * _vm->_macScreenDrawOffset; int minY = 2 * _vm->_macScreenDrawOffset; if (_vm->isUsingOriginalGUI() && minY < 23) minY = 23; newX = CLIP(newX, 0, _surface->w - _practiceBox->w); newY = CLIP(newY, minY, maxY); // For some reason, X coordinates can only change in // increments of 16 pixels. As an enhancement, we allow // any X coordinate. if (!_vm->enhancementEnabled(kEnhUIUX)) newX &= ~0xF; if (newX != _practiceBoxPos.x || newY != _practiceBoxPos.y) { int w = _practiceBox->w; int h = _practiceBox->h; // The old and new rect will almost certainly // overlap, so it's possible to optimize this. // But increasing the delay in the event loop // was a better optimization than removing one // of the copyRectToScreen() calls completely, // so I doubt it's worth it. _system->copyRectToScreen(_surface->getBasePtr(_practiceBoxPos.x, _practiceBoxPos.y), _surface->pitch, _practiceBoxPos.x, _practiceBoxPos.y, w, h); _system->copyRectToScreen(_practiceBox->getBasePtr(0, 0), _practiceBox->pitch, newX, newY, w, h); _practiceBoxPos = Common::Point(newX, newY); } _system->delayMillis(20); _system->updateScreen(); } } return false; } } // End of namespace Scumm