scummvm/engines/crab/game.cpp
2023-12-24 13:19:25 +01:00

871 lines
23 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
/*
* This code is based on the CRAB engine
*
* Copyright (c) Arvind Raja Yadav
*
* Licensed under MIT
*
*/
#include "graphics/managed_surface.h"
#include "graphics/screen.h"
#include "crab/backInserter.h"
#include "crab/game.h"
#include "crab/LoadingScreen.h"
#include "crab/input/cursor.h"
#include "crab/rapidxml/rapidxml_print.hpp"
namespace Crab {
using namespace pyrodactyl::image;
using namespace pyrodactyl::ui;
using namespace pyrodactyl::input;
//------------------------------------------------------------------------
// Purpose: Loading stuff
//------------------------------------------------------------------------
void Game::startNewGame() {
init(g_engine->_filePath->_modCur);
loadLevel(_info.curLocID());
_info.ironMan(g_engine->_tempData->_ironman);
_savefile._ironman = g_engine->_tempData->_filename;
_clock.start();
_hud._pause.updateMode(_info.ironMan());
createSaveGame(SAVEGAME_EVENT);
}
void Game::loadGame() {
init(g_engine->_filePath->_modCur);
}
void Game::init(const Common::Path &filename) {
g_engine->_loadingScreen->dim();
g_engine->_eventStore->clear();
_gameOver.clear(false);
_state = STATE_GAME;
_savefile._autoSlot = false;
_gem.init();
_info.Init();
XMLDoc conf(filename);
if (conf.ready()) {
rapidxml::xml_node<char> *node = conf.doc()->first_node("config");
_info.load(node);
Common::Path path;
if (nodeValid("level", node)) {
loadPath(path, "list", node->first_node("level"));
g_engine->_filePath->loadLevel(path);
}
if (nodeValid("hud", node)) {
loadPath(path, "layout", node->first_node("hud"));
_hud.load(path, _level._talkNotify, _level._destMarker);
}
if (nodeValid("sprite", node)) {
loadPath(path, "animation", node->first_node("sprite"));
_level.loadMoves(path);
loadPath(path, "constant", node->first_node("sprite"));
_level.loadConst(path);
}
if (nodeValid("event", node)) {
_gem.load(node->first_node("event"), _popDefault);
loadPath(path, "store", node->first_node("event"));
g_engine->_eventStore->load(path);
}
if (nodeValid("map", node)) {
loadPath(path, "layout", node->first_node("map"));
_map.load(path, _info);
}
if (nodeValid("save", node))
_savefile.load(node->first_node("save"));
if (nodeValid("debug", node)) {
loadPath(path, "layout", node->first_node("debug"));
_debugConsole.load(path);
}
}
_isInited = true;
}
bool Game::loadLevel(const Common::String &id, int playerX, int playerY) {
if (g_engine->_filePath->_level.contains(id)) {
g_engine->_loadingScreen->draw();
// Load the assets local to this level
// If the filename is same as the previous one, skip loading
if (g_engine->_filePath->_currentR != g_engine->_filePath->_level[id]._asset) {
g_engine->_filePath->_currentR = g_engine->_filePath->_level[id]._asset;
g_engine->_imageManager->loadMap(g_engine->_filePath->_level[id]._asset);
}
// Load the level itself
_level._pop = _popDefault;
_level.load(g_engine->_filePath->_level[id]._layout, _info, _gameOver, playerX, playerY);
// Set the current location
_info.curLocID(id);
_info.curLocName(g_engine->_filePath->_level[id]._name);
_map._playerPos = _level._mapLoc;
// Update and center the world map to the player current position
_map.update(_info);
_map.center(_map._playerPos);
// If this is our first time visiting a level, reveal the associated area on the world map
_map.revealAdd(_level._mapClip._id, _level._mapClip._rect);
// Initialize inventory
_info._inv.init(_level.playerId());
// Initialize journal
_info._journal.init(_level.playerId());
_level.preDraw();
return true;
}
return false;
}
//------------------------------------------------------------------------
// Purpose: Handle events
//------------------------------------------------------------------------
void Game::handleEvents(Common::Event &event, bool &shouldChangeState, GameStateID &newStateId) {
g_engine->_mouse->handleEvents(event);
// if (GameDebug)
// debug_console.handleEvents(Event);
if (!_debugConsole.restrictInput()) {
if (_state == STATE_LOSE_MENU) {
switch (_hud._gom.handleEvents(event)) {
case 0:
_state = STATE_LOSE_LOAD;
break;
case 1:
quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU);
break;
default:
break;
}
} else if (_state == STATE_LOSE_LOAD) {
if (g_engine->_loadMenu->handleEvents(event)) {
shouldChangeState = true;
newStateId = GAMESTATE_LOAD_GAME;
return;
}
if (_hud._pausekey.handleEvents(event) || _hud._back.handleEvents(event) == BUAC_LCLICK)
_state = STATE_LOSE_MENU;
} else {
if (!_gem.eventInProgress() && !_hud._pause.disableHotkeys()) {
switch (_hud.handleEvents(_info, event)) {
case HS_MAP:
toggleState(STATE_MAP);
break;
case HS_PAUSE:
g_engine->_thumbnail->copyFrom(g_engine->_screen->rawSurface());
toggleState(STATE_PAUSE);
break;
case HS_CHAR:
toggleState(STATE_CHARACTER);
_gem._per.Cache(_info, _level.playerId(), _level);
break;
case HS_JOURNAL:
toggleState(STATE_JOURNAL);
break;
case HS_INV:
toggleState(STATE_INVENTORY);
break;
default:
break;
}
}
if (_state == STATE_GAME) {
if (_gem.eventInProgress()) {
_gem.handleEvents(_info, _level.playerId(), event, _hud, _level, _eventRes);
if (applyResult())
quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU);
} else {
// Update the talk key state
_info._talkKeyDown = g_engine->_inputManager->state(IG_TALK) || _level.containsClick(_info.lastPerson(), event);
_level.handleEvents(_info, event);
if (!_gameOver.empty() && _gameOver.evaluate(_info)) {
_state = STATE_LOSE_MENU;
_hud._gom.reset();
return;
}
#if 0
if (g_engine->_inputManager->Equals(IG_QUICKSAVE, Event) == SDL_RELEASED) {
CreateSaveGame(SAVEGAME_QUICK);
return;
} else if (g_engine->_inputManager->Equals(IG_QUICKLOAD, Event) == SDL_RELEASED && !info.IronMan()) {
ShouldChangeState = true;
NewStateID = GAMESTATE_LOAD_GAME;
g_engine->_loadMenu->SelectedPath(FullPath(savefile.quick));
return;
}
#endif
if (_hud._pausekey.handleEvents(event))
toggleState(STATE_PAUSE);
}
} else if (_state == STATE_PAUSE) {
switch (_hud._pause.handleEvents(event, _hud._back)) {
case PS_RESUME:
toggleState(STATE_GAME);
_hud.setTooltip();
break;
case PS_SAVE:
createSaveGame(SAVEGAME_NORMAL);
toggleState(STATE_GAME);
_hud.setTooltip();
break;
case PS_LOAD:
//ShouldChangeState = true;
//NewStateID = GAMESTATE_LOAD_GAME;
return;
case PS_HELP:
toggleState(STATE_HELP);
break;
case PS_QUIT_MENU:
createSaveGame(SAVEGAME_EXIT);
quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU);
break;
case PS_QUIT_GAME:
createSaveGame(SAVEGAME_EXIT);
quit(shouldChangeState, newStateId, GAMESTATE_EXIT);
break;
default:
break;
}
} else {
if (_hud._back.handleEvents(event) == BUAC_LCLICK)
toggleState(STATE_GAME);
switch (_state) {
case STATE_MAP:
if (_map.handleEvents(_info, event)) {
// We need to load the new level
loadLevel(_map._curLoc);
toggleState(STATE_GAME);
}
break;
case STATE_JOURNAL:
if (_info._journal.handleEvents(_level.playerId(), event)) {
// This means we selected the "find on map" button, so we need to:
// switch to the world map, and highlight the appropriate quest marker
_map.selectDest(_info._journal._markerTitle);
toggleState(STATE_MAP);
}
break;
case STATE_CHARACTER:
_gem._per.handleEvents(_info, _level.playerId(), event);
break;
case STATE_INVENTORY:
_info._inv.handleEvents(_level.playerId(), event);
break;
case STATE_HELP:
g_engine->_helpScreen->handleEvents(event);
default:
break;
}
}
}
}
}
#if 0
//------------------------------------------------------------------------
// Purpose: Handle events
//------------------------------------------------------------------------
void Game::handleEvents(SDL_Event &Event, bool &ShouldChangeState, GameStateID &NewStateID) {
g_engine->_mouse->handleEvents(Event);
if (GameDebug)
debug_console.handleEvents(Event);
if (!debug_console.RestrictInput()) {
if (state == STATE_LOSE_MENU) {
switch (hud.gom.handleEvents(Event)) {
case 0:
state = STATE_LOSE_LOAD;
break;
case 1:
Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU);
break;
default:
break;
}
} else if (state == STATE_LOSE_LOAD) {
if (g_engine->_loadMenu->handleEvents(Event)) {
ShouldChangeState = true;
NewStateID = GAMESTATE_LOAD_GAME;
return;
}
if (hud.pausekey.handleEvents(Event) || hud.back.handleEvents(Event) == BUAC_LCLICK)
state = STATE_LOSE_MENU;
} else {
if (!gem.EventInProgress() && !hud.pause.DisableHotkeys()) {
switch (hud.handleEvents(info, Event)) {
case HS_MAP:
ToggleState(STATE_MAP);
break;
case HS_PAUSE:
ToggleState(STATE_PAUSE);
break;
case HS_CHAR:
ToggleState(STATE_CHARACTER);
gem.per.Cache(info, level.PlayerID(), level);
break;
case HS_JOURNAL:
ToggleState(STATE_JOURNAL);
break;
case HS_INV:
ToggleState(STATE_INVENTORY);
break;
default:
break;
}
}
if (state == STATE_GAME) {
if (gem.EventInProgress()) {
gem.handleEvents(info, level.PlayerID(), Event, hud, level, event_res);
if (ApplyResult())
Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU);
} else {
// Update the talk key state
info.TalkKeyDown = g_engine->_inputManager->State(IG_TALK) || level.ContainsClick(info.LastPerson(), Event);
level.handleEvents(info, Event);
if (!game_over.Empty() && game_over.Evaluate(info)) {
state = STATE_LOSE_MENU;
hud.gom.reset();
return;
}
if (g_engine->_inputManager->Equals(IG_QUICKSAVE, Event) == SDL_RELEASED) {
CreateSaveGame(SAVEGAME_QUICK);
return;
} else if (g_engine->_inputManager->Equals(IG_QUICKLOAD, Event) == SDL_RELEASED && !info.IronMan()) {
ShouldChangeState = true;
NewStateID = GAMESTATE_LOAD_GAME;
g_engine->_loadMenu->SelectedPath(FullPath(savefile.quick));
return;
}
if (hud.pausekey.handleEvents(Event))
ToggleState(STATE_PAUSE);
}
} else if (state == STATE_PAUSE) {
switch (hud.pause.handleEvents(Event, hud.back)) {
case PS_RESUME:
ToggleState(STATE_GAME);
hud.SetTooltip();
break;
case PS_SAVE:
CreateSaveGame(SAVEGAME_NORMAL);
ToggleState(STATE_GAME);
hud.SetTooltip();
break;
case PS_LOAD:
ShouldChangeState = true;
NewStateID = GAMESTATE_LOAD_GAME;
return;
case PS_HELP:
ToggleState(STATE_HELP);
break;
case PS_QUIT_MENU:
CreateSaveGame(SAVEGAME_EXIT);
Quit(ShouldChangeState, NewStateID, GAMESTATE_MAIN_MENU);
break;
case PS_QUIT_GAME:
CreateSaveGame(SAVEGAME_EXIT);
Quit(ShouldChangeState, NewStateID, GAMESTATE_EXIT);
break;
default:
break;
}
} else {
if (hud.back.handleEvents(Event) == BUAC_LCLICK)
ToggleState(STATE_GAME);
switch (state) {
case STATE_MAP:
if (map.handleEvents(info, Event)) {
// We need to load the new level
LoadLevel(map.cur_loc);
ToggleState(STATE_GAME);
}
break;
case STATE_JOURNAL:
if (info.journal.handleEvents(level.PlayerID(), Event)) {
// This means we selected the "find on map" button, so we need to:
// switch to the world map, and highlight the appropriate quest marker
map.SelectDest(info.journal.marker_title);
ToggleState(STATE_MAP);
}
break;
case STATE_CHARACTER:
gem.per.handleEvents(info, level.PlayerID(), Event);
break;
case STATE_INVENTORY:
info.inv.handleEvents(level.PlayerID(), Event);
break;
case STATE_HELP:
g_engine->_helpScreen->handleEvents(Event);
default:
break;
}
}
}
}
}
#endif
//------------------------------------------------------------------------
// Purpose: InternalEvents
//------------------------------------------------------------------------
void Game::internalEvents(bool &shouldChangeState, GameStateID &newStateId) {
switch (_state) {
case STATE_GAME:
_hud.internalEvents(_level.showMap());
_eventRes.clear();
{
// HACK: Since sequences can only be ended in GameEventManager, we use this empty array
// to get effects to work for levels
Common::Array<pyrodactyl::event::EventSeqInfo> endSeq;
applyResult(_level.internalEvents(_info, _eventRes, endSeq, _gem.eventInProgress()));
}
_gem.internalEvents(_info, _level, _eventRes);
_info._talkKeyDown = false;
if (applyResult())
quit(shouldChangeState, newStateId, GAMESTATE_MAIN_MENU);
break;
case STATE_MAP:
_map.internalEvents(_info);
break;
case STATE_CHARACTER:
_gem._per.internalEvents();
break;
default:
break;
}
}
//------------------------------------------------------------------------
// Purpose: Draw
//------------------------------------------------------------------------
void Game::draw() {
if (_gem._drawGame)
_level.draw(_info);
else
g_engine->_imageManager->blackScreen();
switch (_state) {
case STATE_GAME:
if (_gem.eventInProgress())
_gem.draw(_info, _hud, _level);
else
_hud.draw(_info, _level.playerId());
break;
case STATE_PAUSE:
g_engine->_imageManager->dimScreen();
_hud._pause.draw(_hud._back);
_hud.draw(_info, _level.playerId());
break;
case STATE_MAP:
g_engine->_imageManager->dimScreen();
_map.draw(_info);
_hud.draw(_info, _level.playerId());
_hud._back.draw();
break;
case STATE_JOURNAL:
g_engine->_imageManager->dimScreen();
_info._journal.draw(_level.playerId());
_hud.draw(_info, _level.playerId());
_hud._back.draw();
break;
case STATE_CHARACTER:
g_engine->_imageManager->dimScreen();
_gem._per.draw(_info, _level.playerId());
_hud.draw(_info, _level.playerId());
_hud._back.draw();
break;
case STATE_INVENTORY:
g_engine->_imageManager->dimScreen();
_info.invDraw(_level.playerId());
_hud.draw(_info, _level.playerId());
_hud._back.draw();
break;
case STATE_HELP:
g_engine->_imageManager->dimScreen();
g_engine->_helpScreen->draw();
_hud._back.draw();
_hud.draw(_info, _level.playerId());
break;
case STATE_LOSE_MENU:
_hud._gom.draw();
break;
case STATE_LOSE_LOAD:
g_engine->_loadMenu->draw();
_hud._back.draw();
break;
default:
break;
}
if (GameDebug)
_debugConsole.draw(_info);
g_engine->_mouse->draw();
}
//------------------------------------------------------------------------
// Purpose: Apply results of events and levels
//------------------------------------------------------------------------
bool Game::applyResult() {
using namespace pyrodactyl::event;
for (const auto &i : _eventRes) {
switch (i._type) {
case ER_MAP:
if (i._val == "img")
_map.setImage(i._y);
else if (i._val == "pos") {
_map._playerPos.x = i._x;
_map._playerPos.y = i._y;
}
break;
case ER_DEST:
if (i._x < 0 || i._y < 0) {
_info._journal.marker(_level.playerId(), i._val, false);
_map.destDel(i._val);
} else {
_map.destAdd(i._val, i._x, i._y);
_info._journal.marker(_level.playerId(), i._val, true);
_info._unread._map = true;
}
break;
case ER_IMG:
playerImg();
break;
case ER_TRAIT:
if (i._x == 42)
_info.traitDel(i._val, i._y);
else
_info.traitAdd(i._val, i._y);
break;
case ER_LEVEL:
if (i._val == "Map")
toggleState(STATE_MAP);
else
loadLevel(i._val, i._x, i._y);
break;
case ER_MOVE:
for (auto &o : _level._objects) {
if (i._val == o.id()) {
o.x(i._x);
o.y(i._y);
break;
}
}
break;
case ER_PLAYER:
// First stop the movement of the current player sprite
_level.playerStop();
// Then swap to the new id
_level.playerId(i._val, i._x, i._y);
// Stop the new player sprite's movement as well
_level.playerStop();
break;
case ER_SAVE:
createSaveGame(SAVEGAME_EVENT);
break;
case ER_SYNC:
_level.calcProperties(_info);
_map.update(_info);
break;
case ER_QUIT:
g_engine->_tempData->_credits = (i._val == "credits");
return true;
default:
break;
}
}
_gem._per.Cache(_info, _level.playerId(), _level);
_eventRes.clear();
return false;
}
void Game::applyResult(LevelResult result) {
switch (result._type) {
case LR_LEVEL:
if (result._val == "Map")
toggleState(STATE_MAP);
else
loadLevel(result._val, result._x, result._y);
return;
case LR_GAMEOVER:
_state = STATE_LOSE_MENU;
_hud._gom.reset();
break;
default:
break;
}
}
//------------------------------------------------------------------------
// Purpose: Save/load game
//------------------------------------------------------------------------
bool Game::loadState(Common::SeekableReadStream *stream) {
Common::String data = stream->readString();
uint64 end = data.findLastOf(">");
if (end == Common::String::npos) // invalid save file, returning false from here will display "Reading data failed" dialogbox
return false;
// +1 to include > as well
end++;
uint8 *dataC = new uint8[end + 1];
dataC[end] = '\0';
memcpy(dataC, data.c_str(), end);
XMLDoc conf(dataC);
if (!_isInited)
loadGame();
if (conf.ready()) {
rapidxml::xml_node<char> *node = conf.doc()->first_node("save");
if (nodeValid(node)) {
_info.loadIronMan(node);
loadStr(_savefile._ironman, "file", node);
_hud._pause.updateMode(_info.ironMan());
if (nodeValid("events", node))
_gem.loadState(node->first_node("events"));
if (nodeValid("info", node))
_info.loadState(node->first_node("info"));
if (nodeValid("map", node))
_map.loadState(node->first_node("map"));
playerImg();
Common::String loc;
loadStr(loc, "loc_id", node);
loadLevel(loc);
if (nodeValid("level", node))
_level.loadState(node->first_node("level"));
_gem._per.Cache(_info, _level.playerId(), _level);
Common::String playtime;
loadStr(playtime, "time", node);
_clock.start(playtime);
}
}
return true;
}
//------------------------------------------------------------------------
// Purpose: Write game state to file
//------------------------------------------------------------------------
void Game::saveState(Common::SeekableWriteStream *stream) {
rapidxml::xml_document<char> doc;
// xml declaration
rapidxml::xml_node<char> *decl = doc.allocate_node(rapidxml::node_declaration);
decl->append_attribute(doc.allocate_attribute("version", "1.0"));
decl->append_attribute(doc.allocate_attribute("encoding", "utf-8"));
doc.append_node(decl);
// root node
rapidxml::xml_node<char> *root = doc.allocate_node(rapidxml::node_element, "save");
doc.append_node(root);
// Save location id
Common::String loc = _info.curLocID();
root->append_attribute(doc.allocate_attribute("loc_id", loc.c_str()));
// Save location name
Common::String locName = _info.curLocName();
root->append_attribute(doc.allocate_attribute("loc_name", locName.c_str()));
// Save player character name
Common::String charName;
if (_info.personValid(_level.playerId()))
charName = _info.personGet(_level.playerId())._name;
root->append_attribute(doc.allocate_attribute("char_name", charName.c_str()));
// Difficulty
Common::String diff = "Normal";
if (_info.ironMan())
diff = "Iron Man";
root->append_attribute(doc.allocate_attribute("diff", diff.c_str()));
// Save file used if iron man
root->append_attribute(doc.allocate_attribute("file", _savefile._ironman.c_str()));
// Preview image used
root->append_attribute(doc.allocate_attribute("preview", _level._previewPath.toString('/').c_str()));
// Time played
Common::String playtime = _clock.getTime();
root->append_attribute(doc.allocate_attribute("time", playtime.c_str()));
rapidxml::xml_node<char> *childGem = doc.allocate_node(rapidxml::node_element, "events");
_gem.saveState(doc, childGem);
root->append_node(childGem);
rapidxml::xml_node<char> *childInfo = doc.allocate_node(rapidxml::node_element, "info");
_info.saveState(doc, childInfo);
root->append_node(childInfo);
rapidxml::xml_node<char> *childMap = doc.allocate_node(rapidxml::node_element, "map");
_map.saveState(doc, childMap);
root->append_node(childMap);
rapidxml::xml_node<char> *childLevel = doc.allocate_node(rapidxml::node_element, "level");
_level.saveState(doc, childLevel);
root->append_node(childLevel);
Common::String xmlAsString;
rapidxml::print(Crab::backInserter(xmlAsString), doc);
stream->writeString(xmlAsString);
}
//------------------------------------------------------------------------
// Purpose: Quit the game
//------------------------------------------------------------------------
void Game::quit(bool &shouldChangeState, GameStateID &newStateId, const GameStateID &newStateVal) {
shouldChangeState = true;
newStateId = newStateVal;
g_engine->_imageManager->loadMap(g_engine->_filePath->_mainmenuR);
}
//------------------------------------------------------------------------
// Purpose: Change our internal state
//------------------------------------------------------------------------
void Game::toggleState(const State &s) {
if (_state != s)
_state = s;
else
_state = STATE_GAME;
// If we are in game state switch to KBM_GAME
if (_state == STATE_GAME && g_engine->_inputManager->getKeyBindingMode() != KBM_GAME) {
g_engine->_inputManager->setKeyBindingMode(KBM_GAME);
}
// This is because game is the first state, the rest are in order
_hud.State(_state - 1);
_hud._pause.reset();
// Only load help screen image if we have to
if (_state == STATE_HELP)
g_engine->_helpScreen->refresh();
else
g_engine->_helpScreen->clear();
}
//------------------------------------------------------------------------
// Purpose: Use this function to actually save your games
//------------------------------------------------------------------------
void Game::createSaveGame(const SaveGameType &savetype) {
#if 0
// Disregard type in iron man mode, we only save to one file
if (info.IronMan())
saveState(savefile.ironman, true);
else {
switch (savetype) {
case SAVEGAME_NORMAL:
saveState(hud.pause.SaveFile(), false);
break;
case SAVEGAME_EVENT:
if (savefile.auto_slot)
saveState(savefile.auto_2, true);
else
saveState(savefile.auto_1, true);
savefile.auto_slot = !savefile.auto_slot;
break;
case SAVEGAME_EXIT:
saveState(savefile.auto_quit, true);
break;
case SAVEGAME_QUICK:
saveState(savefile.quick, true);
break;
default:
break;
}
}
#endif
}
void Game::setUI() {
_map.setUI();
_hud.setUI();
g_engine->_loadMenu->setUI();
g_engine->_optionMenu->setUI();
_gem.setUI();
_info.setUI();
_level.setUI();
}
} // End of namespace Crab