/* 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 "backends/keymapper/action.h" #include "backends/keymapper/keymap.h" #include "backends/keymapper/standard-actions.h" #include "common/file.h" #include "common/translation.h" #include "freescape/freescape.h" #include "freescape/games/dark/dark.h" #include "freescape/language/8bitDetokeniser.h" #include "freescape/objects/global.h" #include "freescape/objects/connections.h" namespace Freescape { DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) { // These sounds can be overriden by the class of each platform _soundIndexShoot = 1; _soundIndexCollide = -1; _soundIndexStepDown = 3; _soundIndexStepUp = 4; _soundIndexMenu = -1; _soundIndexStart = 9; _soundIndexAreaChange = 5; _soundIndexHit = 2; _soundIndexFall = 14; _soundIndexRestoreECD = 19; _soundIndexDestroyECD = -1; _soundIndexNoShield = 20; _soundIndexNoEnergy = 20; _soundIndexFallen = 20; _soundIndexTimeout = 20; _soundIndexForceEndGame = 20; _soundIndexCrushed = 20; _soundIndexMissionComplete = -1; if (isDOS()) initDOS(); else if (isSpectrum()) initZX(); else if (isC64()) initC64(); else if (isCPC()) initCPC(); else if (isAmiga() || isAtariST()) initAmigaAtari(); _playerHeightNumber = 1; _playerHeightMaxNumber = 1; _playerWidth = 12; _playerDepth = 32; _stepUpDistance = 64; _lastTenSeconds = -1; _lastSecond = -1; _angleRotations.push_back(5); _angleRotations.push_back(10); _angleRotations.push_back(15); _angleRotations.push_back(30); _angleRotations.push_back(45); _angleRotations.push_back(90); _initialEnergy = 11; _initialShield = 15; _jetFuelSeconds = _initialEnergy * 6; } void DarkEngine::addECDs(Area *area) { if (!area->entranceWithID(255)) return; GlobalStructure *rs = (GlobalStructure *)area->entranceWithID(255); debugC(1, kFreescapeDebugParser, "ECD positions:"); for (uint i = 0; i < rs->_structure.size(); i = i + 3) { int x = 32 * rs->_structure[i]; int y = 32 * rs->_structure[i + 1]; int z = 32 * rs->_structure[i + 2]; debugC(1, kFreescapeDebugParser, "%d %d %d", x, y, z); if (x == 0 && y == 0 && z == 0) { debugC(1, kFreescapeDebugParser, "Skiping ECD zero position"); continue; } addECD(area, Math::Vector3d(x, y, z), i / 3); } } void DarkEngine::addWalls(Area *area) { if (!area->entranceWithID(254)) return; AreaConnections *cons = (AreaConnections *)area->entranceWithID(254); debugC(1, kFreescapeDebugParser, "Adding walls for area %d:", area->getAreaID()); int id = 240; for (uint i = 1; i < cons->_connections.size(); i = i + 2) { int target = cons->_connections[i]; debugC(1, kFreescapeDebugParser, "Connection to %d using id: %d", target, id); if (target > 0) { area->addObjectFromArea(id, _areaMap[255]); GeometricObject *gobj = (GeometricObject *)area->objectWithID(id); assert(gobj); assert((*(gobj->_condition[0]._thenInstructions))[0].getType() == Token::Type::GOTO); assert((*(gobj->_condition[0]._thenInstructions))[0]._destination == 0); (*(gobj->_condition[0]._thenInstructions))[0].setSource(target); } else area->addObjectFromArea(id + 1, _areaMap[255]); id = id + 2; } } void DarkEngine::addECD(Area *area, const Math::Vector3d position, int index) { GeometricObject *obj = nullptr; Math::Vector3d origin = position; int16 id = 227 + index * 6; int heightLastObject = 0; for (int i = 0; i < 4; i++) { debugC(1, kFreescapeDebugParser, "Adding object %d to room structure", id); obj = (GeometricObject *)_areaMap[255]->objectWithID(id); assert(obj); // Set position for object origin.setValue(0, origin.x()); origin.setValue(1, origin.y() + heightLastObject); origin.setValue(2, origin.z()); obj = (GeometricObject *)obj->duplicate(); obj->setOrigin(origin); obj->makeVisible(); area->addObject(obj); heightLastObject = obj->getSize().y(); id--; } } void DarkEngine::restoreECD(Area &area, int index) { Object *obj = nullptr; int16 id = 227 + index * 6; for (int i = 0; i < 4; i++) { debugC(1, kFreescapeDebugParser, "Restoring object %d to from ECD %d", id, index); obj = (GeometricObject *)area.objectWithID(id); assert(obj); obj->restore(); obj->makeVisible(); id--; } } bool DarkEngine::checkECD(uint16 areaID, int index) { Area *area = _areaMap[areaID]; assert(area != nullptr); int16 id = 227 + index * 6 - 2; debugC(1, kFreescapeDebugParser, "Checking object %d to from ECD %d", id, index); Object *obj = (GeometricObject *)area->objectWithID(id); assert(obj != nullptr); debugC(1, kFreescapeDebugParser, "Result: %d", !obj->isDestroyed()); return !obj->isDestroyed(); } void DarkEngine::initKeymaps(Common::Keymap *engineKeyMap, Common::Keymap *infoScreenKeyMap, const char *target) { FreescapeEngine::initKeymaps(engineKeyMap, infoScreenKeyMap, target); Common::Action *act; act = new Common::Action("SAVE", _("Save game")); act->setCustomEngineActionEvent(kActionSave); act->addDefaultInputMapping("s"); infoScreenKeyMap->addAction(act); act = new Common::Action("LOAD", _("Load game")); act->setCustomEngineActionEvent(kActionLoad); act->addDefaultInputMapping("l"); infoScreenKeyMap->addAction(act); act = new Common::Action("QUIT", _("Quit game")); act->setCustomEngineActionEvent(kActionEscape); if (isSpectrum()) act->addDefaultInputMapping("1"); else act->addDefaultInputMapping("ESCAPE"); infoScreenKeyMap->addAction(act); if (!(isAmiga() || isAtariST())) { act = new Common::Action("TOGGLESOUND", _("Toggle Sound")); act->setCustomEngineActionEvent(kActionToggleSound); act->addDefaultInputMapping("t"); infoScreenKeyMap->addAction(act); } act = new Common::Action("ROTL", _("Rotate Left")); act->setCustomEngineActionEvent(kActionRotateLeft); act->addDefaultInputMapping("q"); engineKeyMap->addAction(act); act = new Common::Action("ROTR", _("Rotate Right")); act->setCustomEngineActionEvent(kActionRotateRight); act->addDefaultInputMapping("w"); engineKeyMap->addAction(act); act = new Common::Action("ROLL_LEFT", _("Roll left")); act->setCustomEngineActionEvent(kActionRollLeft); act->addDefaultInputMapping("n"); engineKeyMap->addAction(act); act = new Common::Action("ROLL_RIGHT", _("Roll right")); act->setCustomEngineActionEvent(kActionRollRight); act->addDefaultInputMapping("m"); engineKeyMap->addAction(act); // I18N: Illustrates the angle at which you turn left or right. act = new Common::Action("INCANGLE", _("Increase Turn Angle")); act->setCustomEngineActionEvent(kActionIncreaseAngle); act->addDefaultInputMapping("a"); engineKeyMap->addAction(act); act = new Common::Action("DECANGLE", _("Decrease Turn Angle")); act->setCustomEngineActionEvent(kActionDecreaseAngle); act->addDefaultInputMapping("z"); engineKeyMap->addAction(act); // I18N: STEP SIZE: Measures the size of one movement in the direction you are facing (1-250 standard distance units (SDUs)) act = new Common::Action("INCSTEPSIZE", _("Increase Step Size")); act->setCustomEngineActionEvent(kActionIncreaseStepSize); act->addDefaultInputMapping("s"); engineKeyMap->addAction(act); // I18N: STEP SIZE: Measures the size of one movement in the direction you are facing (1-250 standard distance units (SDUs)) act = new Common::Action("DECSTEPSIZE", _("Decrease Step Size")); act->setCustomEngineActionEvent(kActionDecreaseStepSize); act->addDefaultInputMapping("x"); engineKeyMap->addAction(act); act = new Common::Action("RISE", _("Rise/Fly up")); act->setCustomEngineActionEvent(kActionRiseOrFlyUp); act->addDefaultInputMapping("JOY_B"); act->addDefaultInputMapping("r"); engineKeyMap->addAction(act); act = new Common::Action("LOWER", _("Lower/Fly down")); act->setCustomEngineActionEvent(kActionLowerOrFlyDown); act->addDefaultInputMapping("JOY_Y"); act->addDefaultInputMapping("f"); engineKeyMap->addAction(act); act = new Common::Action("JETPACK", _("Enable/Disable Jetpack")); act->setCustomEngineActionEvent(kActionToggleFlyMode); act->addDefaultInputMapping("JOY_LEFT_SHOULDER"); act->addDefaultInputMapping("JOY_RIGHT_SHOULDER"); act->addDefaultInputMapping("j"); engineKeyMap->addAction(act); } void DarkEngine::initGameState() { FreescapeEngine::initGameState(); _gameStateVars[k8bitVariableEnergy] = _initialEnergy; _gameStateVars[k8bitVariableShield] = _initialShield; _gameStateVars[kVariableActiveECDs] = 100; _playerHeightNumber = 1; _exploredAreas[_startArea] = true; _endArea = 1; _endEntrance = 26; int seconds, minutes, hours; getTimeFromCountdown(seconds, minutes, hours); _lastMinute = minutes; _lastTenSeconds = seconds / 10; _angleRotationIndex = 0; _playerStepIndex = 6; // Start playing music, if any, in any supported format playMusic("Dark Side Theme"); } void DarkEngine::loadAssets() { FreescapeEngine::loadAssets(); for (auto &it : _areaMap) { addWalls(it._value); addECDs(it._value); if (it._value->getAreaID() != 255) addSkanner(it._value); } _timeoutMessage = _messagesList[14]; _noShieldMessage = _messagesList[15]; _noEnergyMessage = _messagesList[16]; _fallenMessage = _messagesList[17]; _crushedMessage = _messagesList[10]; _forceEndGameMessage = _messagesList[18]; } bool DarkEngine::tryDestroyECDFullGame(int index) { switch (_currentArea->getAreaID()) { case 1: assert(index == 0); return true; case 4: assert(index == 0); return !(checkECD(1, 0) && checkECD(10, 0)); case 5: assert(index == 0); return !(checkECD(12, 0) && checkECD(12, 1)); // Check both case 8: assert(index <= 1); if (index == 0) return !(checkECD(18, 0) && checkECD(10, 0)); // Check both else if (index == 1) return true; break; case 10: assert(index <= 2); if (index == 0) return !(checkECD(4, 0) && checkECD(18, 1)); else if (index == 1) { int connections = 0; connections += checkECD(16, 0); connections += checkECD(8, 0); connections += checkECD(11, 0); return connections <= 1; } else if (index == 2) return true; break; case 11: assert(index <= 1); if (index == 0) return true; else if (index == 1) return !(checkECD(10, 0) && checkECD(12, 0)); // TODO: verify break; case 12: assert(index <= 2); if (index == 0) return !(checkECD(5, 0) && checkECD(11, 1)); // Check last one else if (index == 1) return !(checkECD(5, 0) && checkECD(13, 0)); // Check both else if (index == 2) return true; else assert(false); break; case 13: assert(index <= 1); if (index == 0) return !(checkECD(13, 1) && checkECD(12, 1)); else if (index == 1) return true; else assert(false); break; case 14: if (index == 0) return true; else if (index == 1) return !(checkECD(14, 0) && checkECD(14, 2)); else if (index == 2) return !(checkECD(14, 1) && checkECD(18, 0)); else assert(false); break; case 16: assert(index <= 1); if (index == 0) return !(checkECD(10, 1) && checkECD(18, 0)); else if (index == 1) return !(checkECD(10, 2) && checkECD(18, 1)); else assert(false); break; case 17: assert(index <= 2); if (index == 0) return !(checkECD(12, 2) && checkECD(18, 1)); else if (index == 1) return true; else if (index == 2) return !(checkECD(17, 1) && checkECD(18, 1)); else assert(0); break; case 18: assert(index <= 1); if (index == 0) { int connections = 0; connections += checkECD(16, 0); connections += checkECD(8, 1); connections += checkECD(14, 2); return connections <= 1; } else if (index == 1) { int connections = 0; connections += checkECD(10, 0); connections += checkECD(16, 1); connections += checkECD(17, 0); connections += checkECD(17, 2); return connections <= 1; } else assert(false); default: break; } error("Not implemented"); } bool DarkEngine::tryDestroyECD(int index) { if (isDemo()) { if (index == 0) { return false; } return true; } else { return tryDestroyECDFullGame(index); } return true; // Unreachable } void DarkEngine::addSkanner(Area *area) { debugC(1, kFreescapeDebugParser, "Adding skanner to room %d", area->getAreaID()); int16 id = 0; if (isAmiga() || isAtariST()) { id = 251; debugC(1, kFreescapeDebugParser, "Adding group %d", id); area->addGroupFromArea(id, _areaMap[255]); } else { Object *obj = nullptr; id = 248; // If first object is already added, do not re-add any if (area->objectWithID(id) != nullptr) return; debugC(1, kFreescapeDebugParser, "Adding object %d to room structure", id); obj = _areaMap[255]->objectWithID(id); assert(obj); obj = obj->duplicate(); obj->makeInvisible(); area->addObject(obj); id = 249; debugC(1, kFreescapeDebugParser, "Adding object %d to room structure", id); obj = _areaMap[255]->objectWithID(id); assert(obj); obj = obj->duplicate(); obj->makeInvisible(); area->addObject(obj); id = 250; debugC(1, kFreescapeDebugParser, "Adding object %d to room structure", id); obj = _areaMap[255]->objectWithID(id); assert(obj); obj = obj->duplicate(); obj->makeInvisible(); area->addObject(obj); } } bool DarkEngine::checkIfGameEnded() { if (_gameStateControl == kFreescapeGameStatePlaying) { FreescapeEngine::checkIfGameEnded(); // If the game state changed to game over, then the player failed if (_gameStateControl == kFreescapeGameStateEnd) { _gameStateVars[kVariableDarkEnding] = kDarkEndingEvathDestroyed; } } if (_gameStateVars[kVariableDarkECD] > 0) { int index = _gameStateVars[kVariableDarkECD] - 1; bool destroyed = tryDestroyECD(index); if (destroyed) { _gameStateVars[kVariableActiveECDs] -= 4; _gameStateVars[k8bitVariableScore] += 52750; insertTemporaryMessage(_messagesList[2], _countdown - 2); } else { restoreECD(*_currentArea, index); insertTemporaryMessage(_messagesList[1], _countdown - 2); stopAllSounds(_movementSoundHandle); playSound(_soundIndexRestoreECD, false, _soundFxHandle); } _gameStateVars[kVariableDarkECD] = 0; if (_gameStateVars[kVariableActiveECDs] == 0) { _gameStateControl = kFreescapeGameStateEnd; _gameStateVars[kVariableDarkEnding] = kDarkEndingECDsDestroyed; } } return false; } void DarkEngine::endGame() { FreescapeEngine::endGame(); if (!_endGamePlayerEndArea) return; if (_gameStateControl == kFreescapeGameStateEnd) { if (_gameStateVars[kVariableDarkEnding] == kDarkEndingECDsDestroyed) { insertTemporaryMessage(_messagesList[19], INT_MIN); executeLocalGlobalConditions(false, true, false); _currentArea->_colorRemaps.clear(); _gfx->setColorRemaps(&_currentArea->_colorRemaps); _gameStateVars[kVariableDarkEnding] = 0; } else if (_gameStateVars[kVariableDarkEnding] == kDarkEndingEvathDestroyed) { if (!_ticksFromEnd) _ticksFromEnd = _ticks; else if ((_ticks - _ticksFromEnd) / 15 >= 15) { if (_gameStateVars[kVariableDarkEnding]) { executeLocalGlobalConditions(false, true, false); if (_gameStateVars[kVariableDarkEnding] == kDarkEndingEvathDestroyed) insertTemporaryMessage(_messagesList[22], INT_MIN); _currentArea->_colorRemaps.clear(); _gfx->setColorRemaps(&_currentArea->_colorRemaps); _gameStateVars[kVariableDarkEnding] = 0; } } } } if (_endGameKeyPressed && _gameStateVars[kVariableDarkEnding] == 0) { _gameStateControl = kFreescapeGameStateRestart; } _endGameKeyPressed = false; } void DarkEngine::gotoArea(uint16 areaID, int entranceID) { debugC(1, kFreescapeDebugMove, "Jumping to area: %d, entrance: %d", areaID, entranceID); if (!_exploredAreas.contains(areaID)) { _gameStateVars[k8bitVariableScore] += 17500; _exploredAreas[areaID] = true; } if (isDemo()) { if (!_areaMap.contains(areaID)) { drawFullscreenMessageAndWait(_messagesList[30]); return; } } assert(_areaMap.contains(areaID)); int16 previousArea = _currentArea ? _currentArea->getAreaID() : -127; bool sameArea = areaID == previousArea; _currentArea = _areaMap[areaID]; _currentArea->show(); _currentAreaMessages.clear(); _currentAreaMessages.push_back(_currentArea->_name); int scale = _currentArea->getScale(); assert(scale > 0); if (sameArea || entranceID == 0) { int newPos = -1; /* This code needed some modificatins to deal with the area transition in the poles. Only the light side is considered, since the dark side pole is only reached at the end of the game using a single path. */ if (_position.z() < 200 || _position.z() >= 3800) { if (_position.z() < 200) newPos = 4000; else newPos = 100; // Correct position and yaw for transtions to and from the light side if (previousArea == 14 && areaID == 18) { _position.setValue(2, _position.x()); _position.setValue(0, 100); _yaw = 0; } else if (previousArea == 18 && areaID == 17) { _yaw = 90; } else if (previousArea == 17 && areaID == 18) { _yaw = 90; } else if (previousArea == 16 && areaID == 18) { _position.setValue(2, 4000 - _position.x()); _position.setValue(0, 4000); _yaw = 180; } else _position.setValue(2, newPos); } else if(_position.x() < 200 || _position.x() >= 3800) { if (_position.x() < 200) newPos = 4000; else newPos = 100; // Correct position and yaw for transtions to and from the light side if (previousArea == 18 && areaID == 14) { _position.setValue(0, _position.z()); _position.setValue(2, 100); _yaw = 90; } else if (previousArea == 18 && areaID == 16) { _position.setValue(0, 4000 - _position.z()); _position.setValue(2, 100); _yaw = 90; } else _position.setValue(0, newPos); } assert(newPos != -1); _sensors = _currentArea->getSensors(); } else if (entranceID > 0 || areaID == 127) traverseEntrance(entranceID); else if (entranceID == -1) debugC(1, kFreescapeDebugMove, "Loading game, no change in position"); else error("Invalid area change!"); _lastPosition = _position; _gameStateVars[0x1f] = 0; if (areaID == _startArea && entranceID == _startEntrance) { playSound(_soundIndexStart, true, _soundFxHandle); } else if (areaID == _endArea && entranceID == _endEntrance) { _pitch = 10; } else { playSound(_soundIndexAreaChange, false, _soundFxHandle); } debugC(1, kFreescapeDebugMove, "starting player position: %f, %f, %f", _position.x(), _position.y(), _position.z()); clearTemporalMessages(); // Ignore sky/ground fields _gfx->_keyColor = 0; // Color remaps are not restored in Dark Side // since they are used to simulate a fade to black effect // that should not persist _currentArea->_colorRemaps.clear(); _gfx->setColorRemaps(&_currentArea->_colorRemaps); swapPalette(areaID); _currentArea->_skyColor = isCPC() ? 1 : 0; _currentArea->_usualBackgroundColor = isCPC() ? 1 : 0; resetInput(); } void DarkEngine::pressedKey(const int keycode) { // This code is duplicated in the DrillerEngine::pressedKey (except for the J case) if (keycode == kActionIncreaseStepSize) { increaseStepSize(); } else if (keycode == kActionDecreaseStepSize) { decreaseStepSize(); } else if (keycode == kActionIncreaseAngle) { changeAngle(1, false); } else if (keycode == kActionDecreaseAngle) { changeAngle(-1, false); } else if (keycode == kActionRollRight) { rotate(0, 0, -_angleRotations[_angleRotationIndex]); } else if (keycode == kActionRollLeft) { rotate(0, 0, _angleRotations[_angleRotationIndex]); } else if (keycode == kActionRiseOrFlyUp) { rise(); } else if (keycode == kActionLowerOrFlyDown) { lower(); } else if (keycode == kActionToggleFlyMode) { _flyMode = !_flyMode; //debugC(1, kFreescapeDebugMedia, "raw %d, hz: %f", freq, hzFreq); if (_flyMode && _gameStateVars[k8bitVariableEnergy] == 0) { _flyMode = false; insertTemporaryMessage(_messagesList[13], _countdown - 2); } else if (_flyMode) { float hzFreq = 1193180.0f / 0xd537; _speaker->play(Audio::PCSpeaker::kWaveFormSquare, hzFreq, -1); _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandleJetpack, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO); insertTemporaryMessage(_messagesList[11], _countdown - 2); } else { _speaker->stop(); resolveCollisions(_position); if (!_hasFallen) insertTemporaryMessage(_messagesList[12], _countdown - 2); } } } void DarkEngine::updateTimeVariables() { if (_gameStateControl != kFreescapeGameStatePlaying) return; // This function only executes "on collision" room/global conditions int seconds, minutes, hours; getTimeFromCountdown(seconds, minutes, hours); if (_flyMode && seconds != _lastSecond) { _jetFuelSeconds--; _lastSecond = seconds; if (seconds % 6 == 0) if (_gameStateVars[k8bitVariableEnergy] > 0) _gameStateVars[k8bitVariableEnergy]--; if (_flyMode && _gameStateVars[k8bitVariableEnergy] == 0) { _mixer->stopHandle(_soundFxHandleJetpack); _flyMode = false; insertTemporaryMessage(_messagesList[13], _countdown - 2); } } if (_lastTenSeconds != seconds / 10) { _gameStateVars[0x1e] += 1; _gameStateVars[0x1f] += 1; _lastTenSeconds = seconds / 10; executeLocalGlobalConditions(false, false, true); } if (_lastMinute != minutes) { _lastMinute = minutes; executeLocalGlobalConditions(false, true, false); } } void DarkEngine::borderScreen() { if (_border) { drawBorder(); // Modify and reload the border _border->fillRect(_viewArea, _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0, 0, 0)); delete _borderTexture; _borderTexture = nullptr; loadBorder(); if (isDemo()) { drawFullscreenMessageAndWait(_messagesList[27]); drawFullscreenMessageAndWait(_messagesList[28]); drawFullscreenMessageAndWait(_messagesList[29]); } else { FreescapeEngine::borderScreen(); } } } void DarkEngine::executePrint(FCLInstruction &instruction) { uint16 index = instruction._source - 1; debugC(1, kFreescapeDebugCode, "Printing message %d", index); if (index == 239 && (isAmiga() || isAtariST())) { // Total Eclipse easter egg in Dark Side (Amiga/Atari) Common::String message; for (int i = 60; i < 66; i++) message += _messagesList[i]; drawFullscreenMessageAndWait(message); return; } if (index > 127) { index = _messagesList.size() - (index - 254) - 2; drawFullscreenMessageAndWait(_messagesList[index]); return; } insertTemporaryMessage(_messagesList[index], _countdown - 2); } void DarkEngine::drawBinaryClock(Graphics::Surface *surface, int xPosition, int yPosition, uint32 front, uint32 back) { int number = _ticks / 2; if (_gameStateControl == kFreescapeGameStatePlaying) number = _ticks / 2; else if (_gameStateControl == kFreescapeGameStateEnd) { if (_endGameDelayTicks > 0) // Wait until the endgame is ready return; if (_gameStateVars[kVariableDarkEnding] == 0) number = (1 << 15) - 1; else { int shift = (_ticks - _ticksFromEnd) / 15; if (shift >= 15) number = (1 << 15) - 1; else number = 1 << shift; } } else return; int maxBits = 14; int bits = 0; while (bits <= maxBits) { int y = 0; if (isAmiga() || isAtariST()) { y = yPosition - (3 * bits); surface->fillRect(Common::Rect(xPosition, y - 2, xPosition + 4, y), number & 1 ? front : back); } else { y = yPosition - (7 * bits); surface->drawLine(xPosition, y, xPosition + 3, y, number & 1 ? front : back); } number = number >> 1; bits++; } } void DarkEngine::drawVerticalCompass(Graphics::Surface *surface, int x, int y, float angle, uint32 color) { int pitch = int(angle / 1.65); Common::Array xpoints; Common::Array ypoints; xpoints.push_back(x); xpoints.push_back(x + 3); xpoints.push_back(x + 3); xpoints.push_back(x); ypoints.push_back(y - pitch); ypoints.push_back(y + 3 - pitch); ypoints.push_back(y - 4 - pitch); ypoints.push_back(y - pitch); surface->drawPolygonScan(xpoints.data(), ypoints.data(), 4, Common::Rect(0, 0, surface->w, surface->h), color); } void DarkEngine::drawHorizontalCompass(int x, int y, float angle, uint32 front, uint32 back, Graphics::Surface *surface) { // TODO implement different compass styles for C64, Amiga and Atari ST uint32 transparent = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00); int delta = (angle - 180) / 5.5; drawStringInSurface("-N-E-S-W-N-E-S", delta + x, y, front, back, surface); surface->fillRect(Common::Rect(x - 20, y - 5, x + 40, y + 10), transparent); surface->fillRect(Common::Rect(x + 80, y - 5, 320, y + 10), transparent); } void DarkEngine::drawIndicator(Graphics::Surface *surface, int xPosition, int yPosition) { if (_indicators.size() == 0) return; if (_hasFallen) surface->copyRectToSurface(*_indicators[0], xPosition, yPosition, Common::Rect(_indicators[0]->w, _indicators[0]->h)); else if (_flyMode) surface->copyRectToSurface(*_indicators[3], xPosition, yPosition, Common::Rect(_indicators[3]->w, _indicators[3]->h)); else if (_playerHeightNumber == 0) surface->copyRectToSurface(*_indicators[1], xPosition, yPosition, Common::Rect(_indicators[1]->w, _indicators[1]->h)); else surface->copyRectToSurface(*_indicators[2], xPosition, yPosition, Common::Rect(_indicators[2]->w, _indicators[2]->h)); } void DarkEngine::drawSensorShoot(Sensor *sensor) { if (_gameStateControl == kFreescapeGameStatePlaying) { // Avoid playing new sounds, so the endgame can progress playSound(_soundIndexHit, true, _soundFxHandle); } Math::Vector3d target; target = _position; target.y() = target.y() - _playerHeight; target.x() = target.x() - 5; _gfx->renderSensorShoot(1, sensor->getOrigin(), target, _viewArea); target = _position; target.y() = target.y() - _playerHeight; _gfx->renderSensorShoot(1, sensor->getOrigin(), target, _viewArea); target = _position; target.y() = target.y() - _playerHeight; target.x() = target.x() + 5; _gfx->renderSensorShoot(1, sensor->getOrigin(), target, _viewArea); } void DarkEngine::drawInfoMenu() { PauseToken pauseToken = pauseEngine(); if (_savedScreen) { _savedScreen->free(); delete _savedScreen; } _savedScreen = _gfx->getScreenshot(); uint32 color = 0; switch (_renderMode) { case Common::kRenderCGA: color = 1; break; case Common::kRenderZX: color = 6; break; default: color = 14; } Texture *menuTexture = nullptr; Graphics::Surface *surface = new Graphics::Surface(); surface->create(_screenW, _screenH, _gfx->_texturePixelFormat); if (isAmiga() || isAtariST()) { uint32 white = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xFF, 0xFF, 0xFF); uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00); drawString(kDarkFontSmall, "L-LOAD S-SAVE ESC-ABORT", 32, 145, white, white, black, surface); drawString(kDarkFontSmall, "OR USE THE ICONS. OTHER", 32, 151, white, white, black, surface); drawString(kDarkFontSmall, "KEYS WILL CONTINUE GAME", 32, 157, white, white, black, surface); } else { uint8 r, g, b; _gfx->readFromPalette(color, r, g, b); uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, r, g, b); uint32 black = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00); surface->fillRect(Common::Rect(88, 48, 231, 103), black); surface->frameRect(Common::Rect(88, 48, 231, 103), front); surface->frameRect(Common::Rect(90, 50, 229, 101), front); drawStringInSurface("L-LOAD S-SAVE", 105, 56, front, black, surface); if (isSpectrum()) drawStringInSurface("1-TERMINATE", 105, 64, front, black, surface); else drawStringInSurface("ESC-TERMINATE", 105, 64, front, black, surface); drawStringInSurface("T-TOGGLE", 128, 81, front, black, surface); drawStringInSurface("SOUND ON/OFF", 113, 88, front, black, surface); } menuTexture = _gfx->createTexture(surface); Common::Event event; bool cont = true; while (!shouldQuit() && cont) { while (_eventManager->pollEvent(event)) { // Events switch (event.type) { case Common::EVENT_CUSTOM_ENGINE_ACTION_START: if (event.customType == kActionLoad) { _gfx->setViewport(_fullscreenViewArea); _eventManager->purgeKeyboardEvents(); loadGameDialog(); _gfx->setViewport(_viewArea); } else if (event.customType == kActionSave) { _gfx->setViewport(_fullscreenViewArea); _eventManager->purgeKeyboardEvents(); saveGameDialog(); _gfx->setViewport(_viewArea); } else if (isDOS() && event.customType == kActionToggleSound) { playSound(6, true, _soundFxHandle); } else if (event.customType == kActionEscape) { _forceEndGame = true; cont = false; } break; case Common::EVENT_KEYDOWN: cont = false; break; case Common::EVENT_SCREEN_CHANGED: _gfx->computeScreenViewport(); break; default: break; } } drawFrame(); _gfx->drawTexturedRect2D(_fullscreenViewArea, _fullscreenViewArea, menuTexture); _gfx->flipBuffer(); g_system->updateScreen(); g_system->delayMillis(15); // try to target ~60 FPS } _savedScreen->free(); delete _savedScreen; _savedScreen = nullptr; surface->free(); delete surface; delete menuTexture; pauseToken.clear(); } void DarkEngine::loadMessagesVariableSize(Common::SeekableReadStream *file, int offset, int number) { file->seek(offset); debugC(1, kFreescapeDebugParser, "String table:"); for (int i = 0; i < number; i++) { Common::String message = ""; while (true) { byte c = file->readByte(); if (c <= 21) break; message = message + c; } _messagesList.push_back(message); debugC(1, kFreescapeDebugParser, "'%s'", _messagesList[i].c_str()); } } Common::Error DarkEngine::saveGameStreamExtended(Common::WriteStream *stream, bool isAutosave) { for (auto &it : _areaMap) { stream->writeUint16LE(it._key); stream->writeUint32LE(_exploredAreas[it._key]); } return Common::kNoError; } Common::Error DarkEngine::loadGameStreamExtended(Common::SeekableReadStream *stream) { for (uint i = 0; i < _areaMap.size(); i++) { uint16 key = stream->readUint16LE(); _exploredAreas[key] = stream->readUint32LE(); } return Common::kNoError; } } // End of namespace Freescape