/* 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 "ultima/ultima4/controllers/intro_controller.h" #include "ultima/ultima4/controllers/menu_controller.h" #include "ultima/ultima4/controllers/read_string_controller.h" #include "ultima/ultima4/controllers/read_choice_controller.h" #include "ultima/ultima4/game/player.h" #include "ultima/ultima4/views/menu.h" #include "ultima/ultima4/core/config.h" #include "ultima/ultima4/core/utils.h" #include "ultima/ultima4/core/settings.h" #include "ultima/ultima4/events/event_handler.h" #include "ultima/ultima4/filesys/savegame.h" #include "ultima/ultima4/filesys/u4file.h" #include "ultima/ultima4/gfx/imagemgr.h" #include "ultima/ultima4/gfx/screen.h" #include "ultima/ultima4/map/mapmgr.h" #include "ultima/ultima4/map/shrine.h" #include "ultima/ultima4/map/tileset.h" #include "ultima/ultima4/map/tilemap.h" #include "ultima/ultima4/sound/music.h" #include "ultima/ultima4/sound/sound.h" #include "ultima/ultima4/ultima4.h" #include "common/savefile.h" #include "common/system.h" namespace Ultima { namespace Ultima4 { IntroController *g_intro; #define INTRO_MAP_HEIGHT 5 #define INTRO_MAP_WIDTH 19 #define INTRO_TEXT_X 0 #define INTRO_TEXT_Y 19 #define INTRO_TEXT_WIDTH 40 #define INTRO_TEXT_HEIGHT 6 #define GYP_PLACES_FIRST 0 #define GYP_PLACES_TWOMORE 1 #define GYP_PLACES_LAST 2 #define GYP_UPON_TABLE 3 #define GYP_SEGUE1 13 #define GYP_SEGUE2 14 #define INTRO_SCRIPT_TABLE_SIZE 548 #define INTRO_BASETILE_TABLE_SIZE 15 #define BEASTIE1_FRAMES 0x80 #define BEASTIE2_FRAMES 0x40 class IntroObjectState { public: IntroObjectState() : x(0), y(0), tile(0) {} int x, y; MapTile tile; /* base tile + tile frame */ }; void IntroController::AnimElement::shufflePlotData() { for (int idx = 0; idx < ((int)_plotData.size() - 1); ++idx) { // Pick a random element from the remainder of the plot data int srcIndex = idx + g_ultima->getRandomNumber(_plotData.size() - idx - 1); if (srcIndex != idx) SWAP(_plotData[srcIndex], _plotData[idx]); } } IntroBinData::IntroBinData() : _sigData(nullptr), _scriptTable(nullptr), _baseTileTable(nullptr), _beastie1FrameTable(nullptr), _beastie2FrameTable(nullptr) { } IntroBinData::~IntroBinData() { if (_sigData) delete [] _sigData; if (_scriptTable) delete [] _scriptTable; if (_baseTileTable) delete [] _baseTileTable; if (_beastie1FrameTable) delete [] _beastie1FrameTable; if (_beastie2FrameTable) delete [] _beastie2FrameTable; _introQuestions.clear(); _introText.clear(); _introGypsy.clear(); } void IntroBinData::openFile(Common::File &f, const Common::String &name) { if (!f.open(Common::Path(Common::String::format("data/intro/%s.dat", name.c_str())))) error("Could not load '%s'", name.c_str()); } bool IntroBinData::load() { int i; _introQuestions = u4read_stringtable("intro_questions"); _introText = u4read_stringtable("intro_text"); _introGypsy = u4read_stringtable("intro_gypsy"); // Clean up stray newlines at end of strings for (i = 0; i < 15; i++) trim(_introGypsy[i]); if (_sigData) delete _sigData; _sigData = new byte[533]; Common::File f; openFile(f, "intro_sig"); f.read(_sigData, 533); f.close(); openFile(f, "intro_map"); _introMap.clear(); _introMap.resize(INTRO_MAP_WIDTH * INTRO_MAP_HEIGHT); for (i = 0; i < INTRO_MAP_HEIGHT * INTRO_MAP_WIDTH; i++) _introMap[i] = g_tileMaps->get("base")->translate(f.readByte()); f.close(); openFile(f, "intro_script"); _scriptTable = new byte[INTRO_SCRIPT_TABLE_SIZE]; for (i = 0; i < INTRO_SCRIPT_TABLE_SIZE; i++) _scriptTable[i] = f.readByte(); f.close(); openFile(f, "intro_base_tile"); _baseTileTable = new Tile*[INTRO_BASETILE_TABLE_SIZE]; for (i = 0; i < INTRO_BASETILE_TABLE_SIZE; i++) { MapTile tile = g_tileMaps->get("base")->translate(f.readByte()); _baseTileTable[i] = g_tileSets->get("base")->get(tile._id); } f.close(); /* -------------------------- load beastie frame table 1 -------------------------- */ openFile(f, "intro_beastie1"); _beastie1FrameTable = new byte[BEASTIE1_FRAMES]; for (i = 0; i < BEASTIE1_FRAMES; i++) { _beastie1FrameTable[i] = f.readByte(); } f.close(); /* -------------------------- load beastie frame table 2 -------------------------- */ openFile(f, "intro_beastie2"); _beastie2FrameTable = new byte[BEASTIE2_FRAMES]; for (i = 0; i < BEASTIE2_FRAMES; i++) { _beastie2FrameTable[i] = f.readByte(); } f.close(); return true; } IntroController::IntroController() : Controller(1), _backgroundArea(), _menuArea(1 * CHAR_WIDTH, 13 * CHAR_HEIGHT, 38, 11), _extendedMenuArea(2 * CHAR_WIDTH, 10 * CHAR_HEIGHT, 36, 13), _questionArea(INTRO_TEXT_X * CHAR_WIDTH, INTRO_TEXT_Y * CHAR_HEIGHT, INTRO_TEXT_WIDTH, INTRO_TEXT_HEIGHT), _mapArea(BORDER_WIDTH, (TILE_HEIGHT * 6) + BORDER_HEIGHT, INTRO_MAP_WIDTH, INTRO_MAP_HEIGHT, "base"), _binData(nullptr), _mode(INTRO_TITLES), _answerInd(0), _questionRound(0), _beastie1Cycle(0), _beastie2Cycle(0), _beastieOffset(0), _beastiesVisible(false), _sleepCycles(0), _scrPos(0), _objectStateTable(nullptr), _justInitiatedNewGame(false), _titles(), // element list _title(_titles.begin()), // element iterator _transparentIndex(13), // palette index for transparency _transparentColor(), // palette color for transparency _bSkipTitles(false), _useProfile(false) { Common::fill(&_questionTree[0], &_questionTree[15], -1); // Setup a separate image surface for rendering the animated map on _mapScreen = Image::create(g_screen->w, g_screen->h, g_screen->format); _mapArea.setDest(_mapScreen); // initialize menus _confMenu.setTitle("XU4 Configuration:", 0, 0); _confMenu.add(MI_CONF_VIDEO, "\010 Video Options", 2, 2,/*'v'*/ 2); _confMenu.add(MI_CONF_SOUND, "\010 Sound Options", 2, 3,/*'s'*/ 2); _confMenu.add(MI_CONF_INPUT, "\010 Input Options", 2, 4,/*'i'*/ 2); _confMenu.add(MI_CONF_SPEED, "\010 Speed Options", 2, 5,/*'p'*/ 3); _confMenu.add(MI_CONF_01, new BoolMenuItem("Game Enhancements %s", 2, 7,/*'e'*/ 5, &_settingsChanged._enhancements)); _confMenu.add(MI_CONF_GAMEPLAY, "\010 Enhanced Gameplay Options", 2, 9,/*'g'*/ 11); _confMenu.add(MI_CONF_INTERFACE, "\010 Enhanced Interface Options", 2, 10,/*'n'*/ 12); _confMenu.add(CANCEL, "\017 Main Menu", 2, 12,/*'m'*/ 2); _confMenu.addShortcutKey(CANCEL, ' '); _confMenu.setClosesMenu(CANCEL); /* set the default visibility of the two enhancement menus */ _confMenu.getItemById(MI_CONF_GAMEPLAY)->setVisible(settings._enhancements); _confMenu.getItemById(MI_CONF_INTERFACE)->setVisible(settings._enhancements); _videoMenu.setTitle("Video Options:", 0, 0); _videoMenu.add(MI_VIDEO_CONF_GFX, "\010 Game Graphics Options", 2, 2,/*'g'*/ 2); _videoMenu.add(MI_VIDEO_08, new IntMenuItem("Gamma %s", 2, 4,/*'a'*/ 1, &_settingsChanged._gamma, 50, 150, 10, MENU_OUTPUT_GAMMA)); _videoMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _videoMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _videoMenu.addShortcutKey(CANCEL, ' '); _videoMenu.setClosesMenu(USE_SETTINGS); _videoMenu.setClosesMenu(CANCEL); _gfxMenu.setTitle("Game Graphics Options", 0, 0); _gfxMenu.add(MI_GFX_SCHEME, new StringMenuItem("Graphics Scheme %s", 2, 2, /*'G'*/ 0, &_settingsChanged._videoType, imageMgr->getSetNames())); _gfxMenu.add(MI_GFX_TILE_TRANSPARENCY, new BoolMenuItem("Transparency Hack %s", 2, 4, /*'t'*/ 0, &_settingsChanged._enhancementsOptions._u4TileTransparencyHack)); _gfxMenu.add(MI_GFX_TILE_TRANSPARENCY_SHADOW_SIZE, new IntMenuItem(" Shadow Size: %d", 2, 5, /*'s'*/ 9, &_settingsChanged._enhancementsOptions._u4TrileTransparencyHackShadowBreadth, 0, 16, 1)); _gfxMenu.add(MI_GFX_TILE_TRANSPARENCY_SHADOW_OPACITY, new IntMenuItem(" Shadow Opacity: %d", 2, 6, /*'o'*/ 9, &_settingsChanged._enhancementsOptions._u4TileTransparencyHackPixelShadowOpacity, 8, 256, 8)); _gfxMenu.add(MI_VIDEO_02, new StringMenuItem("Gem Layout %s", 2, 8,/*'e'*/ 1, &_settingsChanged._gemLayout, screenGetGemLayoutNames())); _gfxMenu.add(MI_VIDEO_03, new StringMenuItem("Line Of Sight %s", 2, 9,/*'l'*/ 0, &_settingsChanged._lineOfSight, screenGetLineOfSightStyles())); _gfxMenu.add(MI_VIDEO_07, new BoolMenuItem("Screen Shaking %s", 2, 10,/*'k'*/ 8, &_settingsChanged._screenShakes)); _gfxMenu.add(MI_GFX_RETURN, "\010 Return to Video Options", 2, 12,/*'r'*/ 2); _gfxMenu.setClosesMenu(MI_GFX_RETURN); _soundMenu.setTitle("Sound Options:", 0, 0); _soundMenu.add(MI_SOUND_03, new BoolMenuItem("Fading %s", 2, 4,/*'f'*/ 0, &_settingsChanged._volumeFades)); _soundMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _soundMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _soundMenu.addShortcutKey(CANCEL, ' '); _soundMenu.setClosesMenu(USE_SETTINGS); _soundMenu.setClosesMenu(CANCEL); _inputMenu.setTitle("Mouse Options:", 0, 0); _inputMenu.add(MI_INPUT_03, new BoolMenuItem("Mouse %s", 2, 2,/*'m'*/ 0, &_settingsChanged._mouseOptions._enabled)); _inputMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _inputMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _inputMenu.addShortcutKey(CANCEL, ' '); _inputMenu.setClosesMenu(USE_SETTINGS); _inputMenu.setClosesMenu(CANCEL); _speedMenu.setTitle("Speed Options:", 0, 0); _speedMenu.add(MI_SPEED_01, new IntMenuItem("Game Cycles per Second %3d", 2, 2,/*'g'*/ 0, &_settingsChanged._gameCyclesPerSecond, 1, MAX_CYCLES_PER_SECOND, 1)); _speedMenu.add(MI_SPEED_02, new IntMenuItem("Battle Speed %3d", 2, 3,/*'b'*/ 0, &_settingsChanged._battleSpeed, 1, MAX_BATTLE_SPEED, 1)); _speedMenu.add(MI_SPEED_03, new IntMenuItem("Spell Effect Length %s", 2, 4,/*'p'*/ 1, &_settingsChanged._spellEffectSpeed, 1, MAX_SPELL_EFFECT_SPEED, 1, MENU_OUTPUT_SPELL)); _speedMenu.add(MI_SPEED_04, new IntMenuItem("Camping Length %3d sec", 2, 5,/*'m'*/ 2, &_settingsChanged._campTime, 1, MAX_CAMP_TIME, 1)); _speedMenu.add(MI_SPEED_05, new IntMenuItem("Inn Rest Length %3d sec", 2, 6,/*'i'*/ 0, &_settingsChanged._innTime, 1, MAX_INN_TIME, 1)); _speedMenu.add(MI_SPEED_06, new IntMenuItem("Shrine Meditation Length %3d sec", 2, 7,/*'s'*/ 0, &_settingsChanged._shrineTime, 1, MAX_SHRINE_TIME, 1)); _speedMenu.add(MI_SPEED_07, new IntMenuItem("Screen Shake Interval %3d msec", 2, 8,/*'r'*/ 2, &_settingsChanged._shakeInterval, MIN_SHAKE_INTERVAL, MAX_SHAKE_INTERVAL, 10)); _speedMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _speedMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _speedMenu.addShortcutKey(CANCEL, ' '); _speedMenu.setClosesMenu(USE_SETTINGS); _speedMenu.setClosesMenu(CANCEL); /* move the BATTLE DIFFICULTY, DEBUG, and AUTOMATIC ACTIONS settings to "enhancementsOptions" */ _gameplayMenu.setTitle("Enhanced Gameplay Options:", 0, 0); _gameplayMenu.add(MI_GAMEPLAY_01, new StringMenuItem("Battle Difficulty %s", 2, 2,/*'b'*/ 0, &_settingsChanged._battleDiff, settings.getBattleDiffs())); _gameplayMenu.add(MI_GAMEPLAY_02, new BoolMenuItem("Fixed Chest Traps %s", 2, 3,/*'t'*/ 12, &_settingsChanged._enhancementsOptions._c64ChestTraps)); _gameplayMenu.add(MI_GAMEPLAY_03, new BoolMenuItem("Gazer Spawns Insects %s", 2, 4,/*'g'*/ 0, &_settingsChanged._enhancementsOptions._gazerSpawnsInsects)); _gameplayMenu.add(MI_GAMEPLAY_04, new BoolMenuItem("Gem View Shows Objects %s", 2, 5,/*'e'*/ 1, &_settingsChanged._enhancementsOptions._peerShowsObjects)); _gameplayMenu.add(MI_GAMEPLAY_05, new BoolMenuItem("Slime Divides %s", 2, 6,/*'s'*/ 0, &_settingsChanged._enhancementsOptions._slimeDivides)); _gameplayMenu.add(MI_GAMEPLAY_06, new BoolMenuItem("Debug Mode (Cheats) %s", 2, 8,/*'d'*/ 0, &_settingsChanged._debug)); _gameplayMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _gameplayMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _gameplayMenu.addShortcutKey(CANCEL, ' '); _gameplayMenu.setClosesMenu(USE_SETTINGS); _gameplayMenu.setClosesMenu(CANCEL); _interfaceMenu.setTitle("Enhanced Interface Options:", 0, 0); _interfaceMenu.add(MI_INTERFACE_01, new BoolMenuItem("Automatic Actions %s", 2, 2,/*'a'*/ 0, &_settingsChanged._shortcutCommands)); /* "(Open, Jimmy, etc.)" */ _interfaceMenu.add(MI_INTERFACE_02, new BoolMenuItem("Set Active Player %s", 2, 4,/*'p'*/ 11, &_settingsChanged._enhancementsOptions._activePlayer)); _interfaceMenu.add(MI_INTERFACE_03, new BoolMenuItem("Smart 'Enter' Key %s", 2, 5,/*'e'*/ 7, &_settingsChanged._enhancementsOptions._smartEnterKey)); _interfaceMenu.add(MI_INTERFACE_04, new BoolMenuItem("Text Colorization %s", 2, 6,/*'t'*/ 0, &_settingsChanged._enhancementsOptions._textColorization)); _interfaceMenu.add(MI_INTERFACE_05, new BoolMenuItem("Ultima V Shrines %s", 2, 7,/*'s'*/ 9, &_settingsChanged._enhancementsOptions._u5Shrines)); _interfaceMenu.add(MI_INTERFACE_06, new BoolMenuItem("Ultima V Spell Mixing %s", 2, 8,/*'m'*/ 15, &_settingsChanged._enhancementsOptions._u5SpellMixing)); _interfaceMenu.add(USE_SETTINGS, "\010 Use These Settings", 2, 11,/*'u'*/ 2); _interfaceMenu.add(CANCEL, "\010 Cancel", 2, 12,/*'c'*/ 2); _interfaceMenu.addShortcutKey(CANCEL, ' '); _interfaceMenu.setClosesMenu(USE_SETTINGS); _interfaceMenu.setClosesMenu(CANCEL); } IntroController::~IntroController() { delete _mapScreen; } bool IntroController::init() { _justInitiatedNewGame = false; // sigData is referenced during Titles initialization _binData = new IntroBinData(); _binData->load(); if (_bSkipTitles) { // the init() method is called again from within the // game via ALT-Q, so return to the menu // #ifndef IOS_ULTIMA4 _mode = INTRO_MENU; #else mode = INTRO_MAP; #endif _beastiesVisible = true; _beastieOffset = 0; g_music->intro(); } else { // initialize the titles initTitles(); _mode = INTRO_TITLES; _beastiesVisible = false; _beastieOffset = -32; } _beastie1Cycle = 0; _beastie2Cycle = 0; _sleepCycles = 0; _scrPos = 0; _objectStateTable = new IntroObjectState[INTRO_BASETILE_TABLE_SIZE]; _backgroundArea.reinit(); _menuArea.reinit(); _extendedMenuArea.reinit(); _questionArea.reinit(); _mapArea.reinit(); // only update the screen if we are returning from game mode if (_bSkipTitles) updateScreen(); return true; } bool IntroController::hasInitiatedNewGame() { return this->_justInitiatedNewGame; } void IntroController::deleteIntro() { delete _binData; _binData = nullptr; delete [] _objectStateTable; _objectStateTable = nullptr; imageMgr->freeIntroBackgrounds(); } byte *IntroController::getSigData() { assertMsg(_binData->_sigData != nullptr, "intro sig data not loaded"); return _binData->_sigData; } bool IntroController::keyPressed(int key) { bool valid = true; switch (_mode) { case INTRO_TITLES: // the user pressed a key to abort the sequence skipTitles(); break; case INTRO_MAP: case INTRO_ABOUT: _mode = INTRO_MENU; updateScreen(); break; case INTRO_MENU: switch (key) { case 'i': _errorMessage.clear(); initiateNewGame(); break; case 'j': journeyOnward(); break; case 'r': _errorMessage.clear(); _mode = INTRO_MAP; updateScreen(); break; case 'c': { _errorMessage.clear(); // Make a copy of our settings so we can change them _settingsChanged = settings; g_screen->screenDisableCursor(); runMenu(&_confMenu, &_extendedMenuArea, true); g_screen->screenEnableCursor(); updateScreen(); break; } case 'a': _errorMessage.clear(); about(); break; case 'q': EventHandler::end(); g_ultima->quitGame(); break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': g_music->introSwitch(key - '0'); break; default: valid = false; break; } break; default: error("key handler called in wrong mode"); return true; } return valid || KeyHandler::defaultHandler(key, nullptr); } bool IntroController::mousePressed(const Common::Point &mousePos) { switch (_mode) { case INTRO_TITLES: // Finish the title sequence skipTitles(); break; case INTRO_MAP: case INTRO_ABOUT: _mode = INTRO_MENU; updateScreen(); break; case INTRO_MENU: { char key = _menuArea.getOptionAt(mousePos); if (key) keyPressed(key); } default: break; } return true; } void IntroController::drawMap() { if (0 && _sleepCycles > 0) { drawMapStatic(); drawMapAnimated(); _sleepCycles--; } else { byte commandNibble; byte dataNibble; do { commandNibble = _binData->_scriptTable[_scrPos] >> 4; switch (commandNibble) { /* 0-4 = set object position and tile frame */ case 0: case 1: case 2: case 3: case 4: /* ---------------------------------------------------------- Set object position and tile frame Format: yi [t(3); x(5)] i = table index x = x coordinate (5 least significant bits of second byte) y = y coordinate t = tile frame (3 most significant bits of second byte) ---------------------------------------------------------- */ dataNibble = _binData->_scriptTable[_scrPos] & 0xf; _objectStateTable[dataNibble].x = _binData->_scriptTable[_scrPos + 1] & 0x1f; _objectStateTable[dataNibble].y = commandNibble; // See if the tile id needs to be recalculated if ((_binData->_scriptTable[_scrPos + 1] >> 5) >= _binData->_baseTileTable[dataNibble]->getFrames()) { int frame = (_binData->_scriptTable[_scrPos + 1] >> 5) - _binData->_baseTileTable[dataNibble]->getFrames(); _objectStateTable[dataNibble].tile = MapTile(_binData->_baseTileTable[dataNibble]->getId() + 1); _objectStateTable[dataNibble].tile._frame = frame; } else { _objectStateTable[dataNibble].tile = MapTile(_binData->_baseTileTable[dataNibble]->getId()); _objectStateTable[dataNibble].tile._frame = (_binData->_scriptTable[_scrPos + 1] >> 5); } _scrPos += 2; break; case 7: /* --------------- Delete object Format: 7i i = table index --------------- */ dataNibble = _binData->_scriptTable[_scrPos] & 0xf; _objectStateTable[dataNibble].tile = 0; _scrPos++; break; case 8: /* ---------------------------------------------- Redraw intro map and objects, then go to sleep Format: 8c c = cycles to sleep ---------------------------------------------- */ drawMapStatic(); drawMapAnimated(); _mapScreen->drawSubRect( SCALED(8), SCALED(13 * 8), SCALED(8), SCALED(13 * 8), SCALED(38 * 8), SCALED(10 * 8)); /* set sleep cycles */ _sleepCycles = _binData->_scriptTable[_scrPos] & 0xf; _scrPos++; break; case 0xf: /* ------------------------------------- Jump to the start of the script table Format: f? ? = doesn't matter ------------------------------------- */ _scrPos = 0; break; default: /* invalid command */ _scrPos++; break; } } while (commandNibble != 8); } } void IntroController::drawMapStatic() { int x, y; // draw unmodified map for (y = 0; y < INTRO_MAP_HEIGHT; y++) for (x = 0; x < INTRO_MAP_WIDTH; x++) _mapArea.drawTile(_binData->_introMap[x + (y * INTRO_MAP_WIDTH)], false, x, y); } void IntroController::drawMapAnimated() { int i; // draw animated objects for (i = 0; i < INTRO_BASETILE_TABLE_SIZE; i++) if (_objectStateTable[i].tile != 0) { Std::vector tiles; tiles.push_back(_objectStateTable[i].tile); tiles.push_back(_binData->_introMap[_objectStateTable[i].x + (_objectStateTable[i].y * INTRO_MAP_WIDTH)]); _mapArea.drawTile(tiles, false, _objectStateTable[i].x, _objectStateTable[i].y); } } void IntroController::drawBeasties() { drawBeastie(0, _beastieOffset, _binData->_beastie1FrameTable[_beastie1Cycle]); drawBeastie(1, _beastieOffset, _binData->_beastie2FrameTable[_beastie2Cycle]); if (_beastieOffset < 0) _beastieOffset++; } void IntroController::drawBeastie(int beast, int vertoffset, int frame) { char buffer[128]; int destx; assertMsg(beast == 0 || beast == 1, "invalid beast: %d", beast); Common::sprintf_s(buffer, "beast%dframe%02d", beast, frame); destx = beast ? (320 - 48) : 0; _backgroundArea.draw(buffer, destx, vertoffset); } void IntroController::animateTree(const Common::String &frame) { _backgroundArea.draw(frame, 72, 68); } void IntroController::drawCard(int pos, int card) { static const char *cardNames[] = { "honestycard", "compassioncard", "valorcard", "justicecard", "sacrificecard", "honorcard", "spiritualitycard", "humilitycard" }; assertMsg(pos == 0 || pos == 1, "invalid pos: %d", pos); assertMsg(card >= 0 && card < 8, "invalid card: %d", card); _backgroundArea.draw(cardNames[card], pos ? 218 : 12, 12); } void IntroController::drawAbacusBeads(int row, int selectedVirtue, int rejectedVirtue) { assertMsg(row >= 0 && row < 7, "invalid row: %d", row); assertMsg(selectedVirtue < 8 && selectedVirtue >= 0, "invalid virtue: %d", selectedVirtue); assertMsg(rejectedVirtue < 8 && rejectedVirtue >= 0, "invalid virtue: %d", rejectedVirtue); _backgroundArea.draw("whitebead", 128 + (selectedVirtue * 9), 24 + (row * 15)); _backgroundArea.draw("blackbead", 128 + (rejectedVirtue * 9), 24 + (row * 15)); } void IntroController::updateScreen() { g_screen->screenHideCursor(); _menuArea.clearOptions(); switch (_mode) { case INTRO_MAP: _backgroundArea.draw(BKGD_INTRO); drawMap(); drawBeasties(); // display the profile name if a local profile is being used if (_useProfile) g_screen->screenTextAt(40 - _profileName.size(), 24, "%s", _profileName.c_str()); break; case INTRO_MENU: // draw the extended background for all option screens _backgroundArea.draw(BKGD_INTRO); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); // if there is an error message to display, show it if (!_errorMessage.empty()) { _menuArea.textAt(6, 5, "%s", _errorMessage.c_str()); drawBeasties(); g_screen->update(); // wait for a couple seconds EventHandler::wait_msecs(2000); // clear the screen again _errorMessage.clear(); _backgroundArea.draw(BKGD_INTRO); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } _menuArea.textAt(1, 1, "In another world, in a time to come."); _menuArea.textAt(14, 3, "Options:"); _menuArea.optionAt(10, 5, 'r', "%s", _menuArea.colorizeString("Return to the view", FG_YELLOW, 0, 1).c_str()); _menuArea.optionAt(10, 6, 'j', "%s", _menuArea.colorizeString("Journey Onward", FG_YELLOW, 0, 1).c_str()); _menuArea.optionAt(10, 7, 'i', "%s", _menuArea.colorizeString("Initiate New Game", FG_YELLOW, 0, 1).c_str()); _menuArea.optionAt(10, 8, 'c', "%s", _menuArea.colorizeString("Configure", FG_YELLOW, 0, 1).c_str()); _menuArea.optionAt(10, 9, 'a', "%s", _menuArea.colorizeString("About", FG_YELLOW, 0, 1).c_str()); drawBeasties(); // draw the cursor last g_screen->screenSetCursorPos(24, 16); g_screen->screenShowCursor(); break; default: error("bad mode in updateScreen"); } g_screen->screenUpdateCursor(); g_screen->update(); } void IntroController::initiateNewGame() { // disable the screen cursor because a text cursor will now be used g_screen->screenDisableCursor(); _menuArea.clear(); // draw the extended background for all option screens _backgroundArea.draw(BKGD_INTRO); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); // display name prompt and read name from keyboard _menuArea.textAt(3, 3, "By what name shalt thou be known"); _menuArea.textAt(3, 4, "in this world and time?"); // enable the text cursor after setting it's initial position // this will avoid drawing in undesirable areas like 0,0 _menuArea.setCursorPos(11, 7, false); _menuArea.setCursorFollowsText(true); _menuArea.enableCursor(); drawBeasties(); g_screen->update(); Common::String nameBuffer = ReadStringController::get(12, &_menuArea); if (nameBuffer.empty() || shouldQuit()) { // the user didn't enter a name _menuArea.disableCursor(); g_screen->screenEnableCursor(); updateScreen(); return; } // draw the extended background for all option screens _backgroundArea.draw(BKGD_INTRO); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); // display sex prompt and read sex from keyboard _menuArea.textAt(3, 3, "Art thou Male or Female?"); // the cursor is already enabled, just change its position _menuArea.setCursorPos(28, 3, true); drawBeasties(); SexType sex; int sexChoice = ReadChoiceController::get("mf"); if (!shouldQuit()) { if (sexChoice == 'm') sex = SEX_MALE; else sex = SEX_FEMALE; finishInitiateGame(nameBuffer, sex); } } void IntroController::finishInitiateGame(const Common::String &nameBuffer, SexType sex) { #ifdef IOS_ULTIMA4 mode = INTRO_MENU; // ensure we are now in the menu mode, (i.e., stop drawing the map). #endif // no more text entry, so disable the text cursor _menuArea.disableCursor(); // show the lead up story if (!shouldQuit()) showStory(); // ask questions that determine character class if (!shouldQuit()) startQuestions(); if (!shouldQuit()) { // Setup savegame fields. The original wrote out multiple files and // then loaded them up once the game starts. Now we're simply setting // up the savegame fields and letting the game read from them later, // as if a savegame had been loaded SaveGame &saveGame = *g_ultima->_saveGame; SaveGamePlayerRecord avatar; avatar.init(); strncpy(avatar._name, nameBuffer.c_str(), 15); avatar._name[15] = '\0'; avatar._sex = sex; saveGame.init(&avatar); g_screen->screenHideCursor(); initPlayers(&saveGame); saveGame._food = 30000; saveGame._gold = 200; saveGame._reagents[REAG_GINSENG] = 3; saveGame._reagents[REAG_GARLIC] = 4; saveGame._torches = 2; _justInitiatedNewGame = true; // show the text that's segues into the main game showText(_binData->_introGypsy[GYP_SEGUE1]); #ifdef IOS_ULTIMA4 U4IOS::switchU4IntroControllerToContinueButton(); #endif ReadChoiceController pauseController(""); eventHandler->pushController(&pauseController); pauseController.waitFor(); showText(_binData->_introGypsy[GYP_SEGUE2]); eventHandler->pushController(&pauseController); pauseController.waitFor(); } // done: exit intro and let game begin _questionArea.disableCursor(); EventHandler::setControllerDone(); } void IntroController::showStory() { ReadChoiceController pauseController(""); _beastiesVisible = false; _questionArea.setCursorFollowsText(true); for (int storyInd = 0; storyInd < 24 && !g_ultima->shouldQuit(); storyInd++) { if (storyInd == 0) _backgroundArea.draw(BKGD_TREE); else if (storyInd == 3) animateTree("moongate"); else if (storyInd == 5) animateTree("items"); else if (storyInd == 6) _backgroundArea.draw(BKGD_PORTAL); else if (storyInd == 11) _backgroundArea.draw(BKGD_TREE); else if (storyInd == 15) _backgroundArea.draw(BKGD_OUTSIDE); else if (storyInd == 17) _backgroundArea.draw(BKGD_INSIDE); else if (storyInd == 20) _backgroundArea.draw(BKGD_WAGON); else if (storyInd == 21) _backgroundArea.draw(BKGD_GYPSY); else if (storyInd == 23) _backgroundArea.draw(BKGD_ABACUS); showText(_binData->_introText[storyInd]); eventHandler->pushController(&pauseController); // enable the cursor here to avoid drawing in undesirable locations _questionArea.enableCursor(); pauseController.waitFor(); } } void IntroController::startQuestions() { ReadChoiceController pauseController(""); ReadChoiceController questionController("ab"); _questionRound = 0; initQuestionTree(); while (!shouldQuit()) { // draw the abacus background, if necessary if (_questionRound == 0) _backgroundArea.draw(BKGD_ABACUS); // draw the cards and show the lead up text drawCard(0, _questionTree[_questionRound * 2]); drawCard(1, _questionTree[_questionRound * 2 + 1]); _questionArea.clear(); _questionArea.textAt(0, 0, "%s", _binData->_introGypsy[_questionRound == 0 ? GYP_PLACES_FIRST : (_questionRound == 6 ? GYP_PLACES_LAST : GYP_PLACES_TWOMORE)].c_str()); _questionArea.textAt(0, 1, "%s", _binData->_introGypsy[GYP_UPON_TABLE].c_str()); _questionArea.textAt(0, 2, "%s and %s. She says", _binData->_introGypsy[_questionTree[_questionRound * 2] + 4].c_str(), _binData->_introGypsy[_questionTree[_questionRound * 2 + 1] + 4].c_str()); _questionArea.textAt(0, 3, "\"Consider this:\""); #ifdef IOS_ULTIMA4 U4IOS::switchU4IntroControllerToContinueButton(); #endif // wait for a key eventHandler->pushController(&pauseController); pauseController.waitFor(); g_screen->screenEnableCursor(); // show the question to choose between virtues showText(getQuestion(_questionTree[_questionRound * 2], _questionTree[_questionRound * 2 + 1])); #ifdef IOS_ULTIMA4 U4IOS::switchU4IntroControllerToABButtons(); #endif // wait for an answer int choice; do { eventHandler->pushController(&questionController); choice = questionController.waitFor(); } while (!shouldQuit() && choice == -1); // update the question tree if (shouldQuit() || doQuestion(choice == 'a' ? 0 : 1)) { return; } } } Common::String IntroController::getQuestion(int v1, int v2) { int i = 0; int d = 7; assertMsg(v1 < v2, "first virtue must be smaller (v1 = %d, v2 = %d)", v1, v2); while (v1 > 0) { i += d; d--; v1--; v2--; } assertMsg((i + v2 - 1) < 28, "calculation failed"); return _binData->_introQuestions[i + v2 - 1]; } void IntroController::journeyOnward() { bool validSave = false; int lastSave = ConfMan.hasKey("last_save") ? ConfMan.getInt("last_save") : -1; if (lastSave != -1) { // At this point the game context hasn't been created yet, so we only // want to validate that the given savegame file exists without loading it Common::InSaveFile *saveFile = g_system->getSavefileManager()->openForLoading( g_ultima->getSaveStateName(lastSave)); validSave = saveFile != nullptr; delete saveFile; } if (validSave) { EventHandler::setControllerDone(); g_ultima->setToJourneyOnwards(); } else { _errorMessage = "Initiate a new game first!"; updateScreen(); g_screen->update(); } } void IntroController::about() { // draw the extended background for all option screens _backgroundArea.draw(BKGD_INTRO); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); g_screen->screenHideCursor(); _menuArea.textAt(11, 1, "ScummVM Ultima IV"); _menuArea.textAt(1, 3, "Based on the xu4 project"); drawBeasties(); _mode = INTRO_ABOUT; } void IntroController::showText(const Common::String &text) { Common::String current = text; int lineNo = 0; _questionArea.clear(); unsigned long pos = current.find("\n"); while (pos < current.size()) { _questionArea.textAt(0, lineNo++, "%s", current.substr(0, pos).c_str()); current = current.substr(pos + 1); pos = current.find("\n"); } /* write the last line (possibly only line) */ _questionArea.textAt(0, lineNo++, "%s", current.substr(0, pos).c_str()); } void IntroController::runMenu(Menu *menu, TextView *view, bool withBeasties) { menu->addObserver(this); menu->reset(); // if the menu has an extended height, fill the menu background, otherwise reset the display menu->show(view); if (withBeasties) drawBeasties(); MenuController menuController(menu, view); eventHandler->pushController(&menuController); menuController.waitFor(); // enable the cursor here, after the menu has been established view->enableCursor(); menu->deleteObserver(this); view->disableCursor(); } void IntroController::timerFired() { g_screen->screenCycle(); g_screen->screenUpdateCursor(); if (_mode == INTRO_TITLES) if (updateTitle() == false) { // setup the map screen _mode = INTRO_MAP; _beastiesVisible = true; g_music->intro(); updateScreen(); } if (_mode == INTRO_MAP) drawMap(); if (_beastiesVisible) drawBeasties(); if (xu4_random(2) && ++_beastie1Cycle >= BEASTIE1_FRAMES) _beastie1Cycle = 0; if (xu4_random(2) && ++_beastie2Cycle >= BEASTIE2_FRAMES) _beastie2Cycle = 0; } void IntroController::update(Menu *menu, MenuEvent &event) { if (menu == &_confMenu) updateConfMenu(event); else if (menu == &_videoMenu) updateVideoMenu(event); else if (menu == &_gfxMenu) updateGfxMenu(event); else if (menu == &_soundMenu) updateSoundMenu(event); else if (menu == &_inputMenu) updateInputMenu(event); else if (menu == &_speedMenu) updateSpeedMenu(event); else if (menu == &_gameplayMenu) updateGameplayMenu(event); else if (menu == &_interfaceMenu) updateInterfaceMenu(event); // beasties are always visible on the menus drawBeasties(); } void IntroController::updateConfMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { // show or hide game enhancement options if enhancements are enabled/disabled _confMenu.getItemById(MI_CONF_GAMEPLAY)->setVisible(_settingsChanged._enhancements); _confMenu.getItemById(MI_CONF_INTERFACE)->setVisible(_settingsChanged._enhancements); // save settings settings.setData(_settingsChanged); settings.write(); switch (event.getMenuItem()->getId()) { case MI_CONF_VIDEO: runMenu(&_videoMenu, &_extendedMenuArea, true); break; case MI_VIDEO_CONF_GFX: runMenu(&_gfxMenu, &_extendedMenuArea, true); break; case MI_CONF_SOUND: runMenu(&_soundMenu, &_extendedMenuArea, true); break; case MI_CONF_INPUT: runMenu(&_inputMenu, &_extendedMenuArea, true); break; case MI_CONF_SPEED: runMenu(&_speedMenu, &_extendedMenuArea, true); break; case MI_CONF_GAMEPLAY: runMenu(&_gameplayMenu, &_extendedMenuArea, true); break; case MI_CONF_INTERFACE: runMenu(&_interfaceMenu, &_extendedMenuArea, true); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateVideoMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // Save settings (if necessary) if (settings != _settingsChanged) { settings.setData(_settingsChanged); settings.write(); // FIXME: resize images, etc. g_screen->screenReInit(); // Go back to menu mode _mode = INTRO_MENU; } break; case MI_VIDEO_CONF_GFX: runMenu(&_gfxMenu, &_extendedMenuArea, true); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateGfxMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case MI_GFX_RETURN: runMenu(&_videoMenu, &_extendedMenuArea, true); break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateSoundMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // save settings settings.setData(_settingsChanged); settings.write(); g_music->intro(); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateInputMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // save settings settings.setData(_settingsChanged); settings.write(); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateSpeedMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // save settings settings.setData(_settingsChanged); settings.write(); // re-initialize events settings._eventTimerGranularity = (1000 / settings._gameCyclesPerSecond); eventHandler->getTimer()->reset(settings._eventTimerGranularity); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateGameplayMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // save settings settings.setData(_settingsChanged); settings.write(); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); } void IntroController::updateInterfaceMenu(MenuEvent &event) { if (event.getType() == MenuEvent::ACTIVATE || event.getType() == MenuEvent::INCREMENT || event.getType() == MenuEvent::DECREMENT) { switch (event.getMenuItem()->getId()) { case USE_SETTINGS: // save settings settings.setData(_settingsChanged); settings.write(); break; case CANCEL: // discard settings _settingsChanged = settings; break; default: break; } } // draw the extended background for all option screens _backgroundArea.draw(BKGD_OPTIONS_TOP, 0, 0); _backgroundArea.draw(BKGD_OPTIONS_BTM, 0, 120); // after drawing the menu, extra menu text can be added here _extendedMenuArea.textAt(2, 3, " (Open, Jimmy, etc.)"); } void IntroController::initQuestionTree() { int i, r; for (i = 0; i < 8; i++) _questionTree[i] = i; for (i = 0; i < 8; i++) { r = xu4_random(8); SWAP(_questionTree[r], _questionTree[i]); } _answerInd = 8; if (_questionTree[0] > _questionTree[1]) { SWAP(_questionTree[0], _questionTree[1]); } } bool IntroController::doQuestion(int answer) { if (!answer) _questionTree[_answerInd] = _questionTree[_questionRound * 2]; else _questionTree[_answerInd] = _questionTree[_questionRound * 2 + 1]; drawAbacusBeads(_questionRound, _questionTree[_answerInd], _questionTree[_questionRound * 2 + ((answer) ? 0 : 1)]); _answerInd++; _questionRound++; if (_questionRound > 6) return true; if (_questionTree[_questionRound * 2] > _questionTree[_questionRound * 2 + 1]) { SWAP(_questionTree[_questionRound * 2], _questionTree[_questionRound * 2 + 1]); } return false; } void IntroController::initPlayers(SaveGame *saveGame) { int i, p; static const struct { WeaponType weapon; ArmorType armor; int level, xp, x, y; } initValuesForClass[] = { { WEAP_STAFF, ARMR_CLOTH, 2, 125, 231, 136 }, /* CLASS_MAGE */ { WEAP_SLING, ARMR_CLOTH, 3, 240, 83, 105 }, /* CLASS_BARD */ { WEAP_AXE, ARMR_LEATHER, 3, 205, 35, 221 }, /* CLASS_FIGHTER */ { WEAP_DAGGER, ARMR_CLOTH, 2, 175, 59, 44 }, /* CLASS_DRUID */ { WEAP_MACE, ARMR_LEATHER, 2, 110, 158, 21 }, /* CLASS_TINKER */ { WEAP_SWORD, ARMR_CHAIN, 3, 325, 105, 183 }, /* CLASS_PALADIN */ { WEAP_SWORD, ARMR_LEATHER, 2, 150, 23, 129 }, /* CLASS_RANGER */ { WEAP_STAFF, ARMR_CLOTH, 1, 5, 186, 171 } /* CLASS_SHEPHERD */ }; static const struct { const char *name; int str, dex, intel; SexType sex; } initValuesForNpcClass[] = { { "Mariah", 9, 12, 20, SEX_FEMALE }, /* CLASS_MAGE */ { "Iolo", 16, 19, 13, SEX_MALE }, /* CLASS_BARD */ { "Geoffrey", 20, 15, 11, SEX_MALE }, /* CLASS_FIGHTER */ { "Jaana", 17, 16, 13, SEX_FEMALE }, /* CLASS_DRUID */ { "Julia", 15, 16, 12, SEX_FEMALE }, /* CLASS_TINKER */ { "Dupre", 17, 14, 17, SEX_MALE }, /* CLASS_PALADIN */ { "Shamino", 16, 15, 15, SEX_MALE }, /* CLASS_RANGER */ { "Katrina", 11, 12, 10, SEX_FEMALE } /* CLASS_SHEPHERD */ }; saveGame->_players[0]._class = static_cast(_questionTree[14]); assertMsg((int)saveGame->_players[0]._class < 8, "bad class: %d", saveGame->_players[0]._class); saveGame->_positions.resize(1); saveGame->_positions[0] = LocationCoords(MAP_WORLD, initValuesForClass[saveGame->_players[0]._class].x, initValuesForClass[saveGame->_players[0]._class].y, 0); saveGame->_players[0]._weapon = initValuesForClass[saveGame->_players[0]._class].weapon; saveGame->_players[0]._armor = initValuesForClass[saveGame->_players[0]._class].armor; saveGame->_players[0]._xp = initValuesForClass[saveGame->_players[0]._class].xp; saveGame->_players[0]._str = 15; saveGame->_players[0]._dex = 15; saveGame->_players[0]._intel = 15; for (i = 0; i < VIRT_MAX; i++) saveGame->_karma[i] = 50; for (i = 8; i < 15; i++) { saveGame->_karma[_questionTree[i]] += 5; switch (_questionTree[i]) { case VIRT_HONESTY: saveGame->_players[0]._intel += 3; break; case VIRT_COMPASSION: saveGame->_players[0]._dex += 3; break; case VIRT_VALOR: saveGame->_players[0]._str += 3; break; case VIRT_JUSTICE: saveGame->_players[0]._intel++; saveGame->_players[0]._dex++; break; case VIRT_SACRIFICE: saveGame->_players[0]._dex++; saveGame->_players[0]._str++; break; case VIRT_HONOR: saveGame->_players[0]._intel++; saveGame->_players[0]._str++; break; case VIRT_SPIRITUALITY: saveGame->_players[0]._intel++; saveGame->_players[0]._dex++; saveGame->_players[0]._str++; break; case VIRT_HUMILITY: /* no stats for you! */ break; } } PartyMember player(nullptr, &saveGame->_players[0]); saveGame->_players[0]._hp = saveGame->_players[0]._hpMax = player.getMaxLevel() * 100; saveGame->_players[0]._mp = player.getMaxMp(); p = 1; for (i = 0; i < VIRT_MAX; i++) { player = PartyMember(nullptr, &saveGame->_players[i]); /* Initial setup for party members that aren't in your group yet... */ if (i != saveGame->_players[0]._class) { saveGame->_players[p]._class = static_cast(i); saveGame->_players[p]._xp = initValuesForClass[i].xp; saveGame->_players[p]._str = initValuesForNpcClass[i].str; saveGame->_players[p]._dex = initValuesForNpcClass[i].dex; saveGame->_players[p]._intel = initValuesForNpcClass[i].intel; saveGame->_players[p]._weapon = initValuesForClass[i].weapon; saveGame->_players[p]._armor = initValuesForClass[i].armor; strncpy(saveGame->_players[p]._name, initValuesForNpcClass[i].name, 15); saveGame->_players[p]._name[15] = '\0'; saveGame->_players[p]._sex = initValuesForNpcClass[i].sex; saveGame->_players[p]._hp = saveGame->_players[p]._hpMax = initValuesForClass[i].level * 100; saveGame->_players[p]._mp = player.getMaxMp(); p++; } } } void IntroController::preloadMap() { int x, y, i; // draw unmodified map for (y = 0; y < INTRO_MAP_HEIGHT; y++) for (x = 0; x < INTRO_MAP_WIDTH; x++) _mapArea.loadTile(_binData->_introMap[x + (y * INTRO_MAP_WIDTH)]); // draw animated objects for (i = 0; i < INTRO_BASETILE_TABLE_SIZE; i++) { if (_objectStateTable[i].tile != 0) _mapArea.loadTile(_objectStateTable[i].tile); } } void IntroController::initTitles() { // add the intro elements // x, y, w, h, method, delay, duration // addTitle(97, 0, 130, 16, SIGNATURE, 1000, 3000); // "Lord British" addTitle(148, 17, 24, 4, AND, 1000, 100); // "and" addTitle(84, 31, 152, 1, BAR, 1000, 500); // addTitle(86, 21, 148, 9, ORIGIN, 1000, 100); // "Origin Systems, Inc." addTitle(133, 33, 54, 5, PRESENT, 0, 100); // "present" addTitle(59, 33, 202, 46, TITLE, 1000, 5000); // "Ultima IV" addTitle(40, 80, 240, 13, SUBTITLE, 1000, 100); // "Quest of the Avatar" addTitle(0, 96, 320, 96, MAP, 1000, 100); // the map // get the source data for the elements getTitleSourceData(); // reset the iterator _title = _titles.begin(); // speed up the timer while the intro titles are displayed eventHandler->getTimer()->reset(settings._titleSpeedOther); } void IntroController::addTitle(int x, int y, int w, int h, AnimType method, uint32 delay, int duration) { AnimElement data = { x, y, // source x and y w, h, // source width and height method, // render method 0, // animStep 0, // animStepMax 0, // timeBase delay, // delay before rendering begins duration, // total animation time nullptr, // storage for the source image nullptr, // storage for the animation frame Std::vector(), false }; // prescaled _titles.push_back(data); } void IntroController::getTitleSourceData() { uint r, g, b, a; // color values byte *srcData; // plot data const int BLUE[16] = { 255, 250, 226, 226, 210, 194, 161, 161, 129, 97, 97, 64, 64, 32, 32, 0 }; // The BKGD_INTRO image is assumed to have not been // loaded yet. The unscaled version will be loaded // here, and elements of the image will be stored // individually. Afterward, the BKGD_INTRO image // will be scaled appropriately. ImageInfo *info = imageMgr->get(BKGD_INTRO, true); if (!info) error("ERROR 1007: Unable to load the image \"%s\"", BKGD_INTRO); if (info->_width / info->_prescale != 320 || info->_height / info->_prescale != 200) // The image appears to have been scaled already warning("ERROR 1008: The title image (\"%s\") has been scaled too early", BKGD_INTRO); // get the transparent color _transparentColor = info->_image->getPaletteColor(_transparentIndex); // turn alpha off, if necessary bool alpha = info->_image->isAlphaOn(); info->_image->alphaOff(); // for each element, get the source data for (unsigned i = 0; i < _titles.size(); i++) { if ((_titles[i]._method != SIGNATURE) && (_titles[i]._method != BAR)) { // create a place to store the source image _titles[i]._srcImage = Image::create( _titles[i]._rw * info->_prescale, _titles[i]._rh * info->_prescale, _titles[i]._method == MAP ? _mapScreen->format() : info->_image->format()); if (_titles[i]._srcImage->isIndexed()) _titles[i]._srcImage->setPaletteFromImage(info->_image); // get the source image info->_image->drawSubRectOn( _titles[i]._srcImage, 0, 0, _titles[i]._rx * info->_prescale, _titles[i]._ry * info->_prescale, _titles[i]._rw * info->_prescale, _titles[i]._rh * info->_prescale); } // after getting the srcImage switch (_titles[i]._method) { case SIGNATURE: { // PLOT: "Lord British" srcData = getSigData(); RGBA color = info->_image->setColor(0, 255, 255); // cyan for EGA int x = 0, y = 0; while (srcData[_titles[i]._animStepMax] != 0) { x = srcData[_titles[i]._animStepMax] - 0x4C; y = 0xC0 - srcData[_titles[i]._animStepMax + 1]; if (settings._videoType != "EGA") { // yellow gradient color = info->_image->setColor(255, (y == 2 ? 250 : 255), BLUE[y - 1]); } AnimPlot plot = { (uint8)x, (uint8)y, (uint8)color.r, (uint8)color.g, (uint8)color.b, 255 }; _titles[i]._plotData.push_back(plot); _titles[i]._animStepMax += 2; } _titles[i]._animStepMax = _titles[i]._plotData.size(); break; } case BAR: { _titles[i]._animStepMax = _titles[i]._rw; // image width break; } case TITLE: { for (int y = 0; y < _titles[i]._rh; y++) { for (int x = 0; x < _titles[i]._rw ; x++) { _titles[i]._srcImage->getPixel(x * info->_prescale, y * info->_prescale, r, g, b, a); if (r || g || b) { AnimPlot plot = { (uint8)(x + 1), (uint8)(y + 1), (uint8)r, (uint8)g, (uint8)b, (uint8)a }; _titles[i]._plotData.push_back(plot); } } } _titles[i]._animStepMax = _titles[i]._plotData.size(); break; } case MAP: { // fill the map area with the transparent color _titles[i]._srcImage->fillRect( 8, 8, 304, 80, _transparentColor.r, _transparentColor.g, _transparentColor.b); Image *scaled; // the scaled and filtered image scaled = g_screen->screenScale(_titles[i]._srcImage, settings._scale / info->_prescale, 1, 1); if (_transparentIndex >= 0) scaled->setTransparentIndex(_transparentIndex); _titles[i]._prescaled = true; delete _titles[i]._srcImage; _titles[i]._srcImage = scaled; _titles[i]._animStepMax = 20; break; } default: { _titles[i]._animStepMax = _titles[i]._rh ; // image height break; } } // permanently disable alpha if (_titles[i]._srcImage) _titles[i]._srcImage->alphaOff(); // create the initial animation frame _titles[i]._destImage = Image::create( 2 + (_titles[i]._prescaled ? SCALED(_titles[i]._rw) : _titles[i]._rw) * info->_prescale , 2 + (_titles[i]._prescaled ? SCALED(_titles[i]._rh) : _titles[i]._rh) * info->_prescale, _titles[i]._method == MAP ? _mapScreen->format() : info->_image->format()); if (_titles[i]._destImage->isIndexed()) _titles[i]._destImage->setPaletteFromImage(info->_image); } // turn alpha back on if (alpha) { info->_image->alphaOn(); } // scale the original image now Image *scaled = g_screen->screenScale(info->_image, settings._scale / info->_prescale, info->_image->isIndexed(), 1); delete info->_image; info->_image = scaled; } bool IntroController::updateTitle() { #ifdef IOS_ULTIMA4 static bool firstTime = true; if (firstTime) { firstTime = false; startTicks(); } #endif int animStepTarget = 0; uint32 timeCurrent = g_system->getMillis(); float timePercent = 0; if (_title->_animStep == 0 && !_bSkipTitles) { if (_title->_timeBase == 0) { // reset the base time _title->_timeBase = timeCurrent; } if (_title == _titles.begin()) { // clear the screen Image *screen = imageMgr->get("screen")->_image; screen->fillRect(0, 0, screen->width(), screen->height(), 0, 0, 0); } if (_title->_method == TITLE) { // assume this is the first frame of "Ultima IV" and begin sound soundPlay(SOUND_TITLE_FADE); } } // abort after processing all elements if (_title == _titles.end()) { return false; } // delay the drawing of this phase if ((timeCurrent - _title->_timeBase) < _title->_timeDelay) { return true; } // determine how much of the animation should have been drawn up until now timePercent = float(timeCurrent - _title->_timeBase - _title->_timeDelay) / _title->_timeDuration; if (timePercent > 1 || _bSkipTitles) timePercent = 1; animStepTarget = int(_title->_animStepMax * timePercent); // perform the animation switch (_title->_method) { case SIGNATURE: { for (; animStepTarget > _title->_animStep; _title->_animStep++) { // blit the pixel-pair to the src surface _title->_destImage->fillRect( _title->_plotData[_title->_animStep].x, _title->_plotData[_title->_animStep].y, 2, 1, _title->_plotData[_title->_animStep].r, _title->_plotData[_title->_animStep].g, _title->_plotData[_title->_animStep].b); } break; } case BAR: { RGBA color; while (animStepTarget > _title->_animStep) { _title->_animStep++; color = _title->_destImage->setColor(128, 0, 0); // dark red for the underline // blit bar to the canvas _title->_destImage->fillRect( 1, 1, _title->_animStep, 1, color.r, color.g, color.b); } break; } case AND: { // blit the entire src to the canvas _title->_srcImage->drawOn(_title->_destImage, 1, 1); _title->_animStep = _title->_animStepMax; break; } case ORIGIN: { if (_bSkipTitles) _title->_animStep = _title->_animStepMax; else { _title->_animStep++; _title->_timeDelay = g_system->getMillis() - _title->_timeBase + 100; } // blit src to the canvas one row at a time, bottom up _title->_srcImage->drawSubRectOn( _title->_destImage, 1, _title->_destImage->height() - 1 - _title->_animStep, 0, 0, _title->_srcImage->width(), _title->_animStep); break; } case PRESENT: { if (_bSkipTitles) _title->_animStep = _title->_animStepMax; else { _title->_animStep++; _title->_timeDelay = g_system->getMillis() - _title->_timeBase + 100; } // blit src to the canvas one row at a time, top down _title->_srcImage->drawSubRectOn( _title->_destImage, 1, 1, 0, _title->_srcImage->height() - _title->_animStep, _title->_srcImage->width(), _title->_animStep); break; } case TITLE: { // blit src to the canvas in a random pixelized manner _title->_animStep = animStepTarget; _title->shufflePlotData(); _title->_destImage->fillRect(1, 1, _title->_rw, _title->_rh, 0, 0, 0); // @TODO: animStepTarget (for this loop) should not exceed // half of animStepMax. If so, instead draw the entire // image, and then black out the necessary pixels. // this should speed the loop up at the end for (int i = 0; i < animStepTarget; ++i) { _title->_destImage->putPixel( _title->_plotData[i].x, _title->_plotData[i].y, _title->_plotData[i].r, _title->_plotData[i].g, _title->_plotData[i].b, _title->_plotData[i].a); } // cover the "present" area with the transparent color _title->_destImage->fillRect( 75, 1, 54, 5, _transparentColor.r, _transparentColor.g, _transparentColor.b); break; } case SUBTITLE: { if (_bSkipTitles) _title->_animStep = _title->_animStepMax; else { _title->_animStep++; _title->_timeDelay = g_system->getMillis() - _title->_timeBase + 100; } // blit src to the canvas one row at a time, center out int y = int(_title->_rh / 2) - _title->_animStep + 1; _title->_srcImage->drawSubRectOn( _title->_destImage, 1, y + 1, 0, y, _title->_srcImage->width(), 1 + ((_title->_animStep - 1) * 2)); break; } case MAP: { if (_bSkipTitles) _title->_animStep = _title->_animStepMax; else { _title->_animStep++; _title->_timeDelay = g_system->getMillis() - _title->_timeBase + 100; } int step = (_title->_animStep == _title->_animStepMax ? _title->_animStepMax - 1 : _title->_animStep); // blit src to the canvas one row at a time, center out _title->_srcImage->drawSubRectOn( _title->_destImage, SCALED(153 - (step * 8)), SCALED(1), 0, 0, SCALED((step + 1) * 8), SCALED(_title->_srcImage->height())); _title->_srcImage->drawSubRectOn( _title->_destImage, SCALED(161), SCALED(1), SCALED(312 - (step * 8)), 0, SCALED((step + 1) * 8), SCALED(_title->_srcImage->height())); // Create a destimage for the map tiles int newtime = g_system->getMillis(); if (newtime > _title->_timeDuration + 250 / 4) { // Draw the updated map display drawMapStatic(); _mapScreen->drawSubRectOn( _title->_srcImage, SCALED(8), SCALED(8), SCALED(8), SCALED(13 * 8), SCALED(38 * 8), SCALED(10 * 8)); _title->_timeDuration = newtime + 250 / 4; } _title->_srcImage->drawSubRectOn( _title->_destImage, SCALED(161 - (step * 8)), SCALED(9), SCALED(160 - (step * 8)), SCALED(8), SCALED((step * 2) * 8), SCALED((10 * 8))); break; } } // draw the titles drawTitle(); // if the animation for this title has completed, // move on to the next title if (_title->_animStep >= _title->_animStepMax) { // free memory that is no longer needed compactTitle(); _title++; if (_title == _titles.end()) { // reset the timer to the pre-titles granularity eventHandler->getTimer()->reset(settings._eventTimerGranularity); // make sure the titles only appear when the app first loads _bSkipTitles = true; return false; } if (_title->_method == TITLE) { // assume this is "Ultima IV" and pre-load sound // soundLoad(SOUND_TITLE_FADE); eventHandler->getTimer()->reset(settings._titleSpeedRandom); } else if (_title->_method == MAP) { eventHandler->getTimer()->reset(settings._titleSpeedOther); } else { eventHandler->getTimer()->reset(settings._titleSpeedOther); } } return true; } void IntroController::compactTitle() { if (_title->_srcImage) { delete _title->_srcImage; _title->_srcImage = nullptr; } _title->_plotData.clear(); } void IntroController::drawTitle() { Image *scaled; // the scaled and filtered image // blit the scaled and filtered surface to the screen if (_title->_prescaled) scaled = _title->_destImage; else scaled = g_screen->screenScale(_title->_destImage, settings._scale, 1, 1); scaled->setTransparentIndex(_transparentIndex); scaled->drawSubRect( SCALED(_title->_rx), // dest x, y SCALED(_title->_ry), SCALED(1), // src x, y, w, h SCALED(1), SCALED(_title->_rw), SCALED(_title->_rh)); if (!_title->_prescaled) { delete scaled; scaled = nullptr; } } void IntroController::skipTitles() { _bSkipTitles = true; soundStop(); } #ifdef IOS_ULTIMA4 void IntroController::tryTriggerIntroMusic() { if (mode == INTRO_MAP) g_music->intro(); } #endif } // End of namespace Ultima4 } // End of namespace Ultima